"""Per-source quality mappers — turn each download source's tier string or API values into a unified :class:`~core.quality.model.AudioQuality`. Every streaming source describes quality differently (Tidal/HiFi use tier strings, Qobuz reports real kHz + bit depth, Deezer uses config codes, Amazon mixes real values with HD/UHD tiers). Centralising the knowledge here keeps the per-client code to a single call and keeps the tier tables in one auditable place. Each value is a *claim*: the download client populates its ``TrackResult`` from it so the global ranker can choose a source, and the post-download quality guard later verifies the real file. Over-claiming is the danger — an unknown tier maps to ``format='unknown'`` rather than pretending to be lossless. """ from __future__ import annotations from typing import Optional from core.quality.model import AudioQuality # ── Tidal / HiFi (Monochrome is Tidal-backed) ────────────────────────────── # # Tidal exposes UPPER_SNAKE tier strings (``HI_RES_LOSSLESS``); HiFi's config # uses lowercase keys (``hires``/``lossless``). We normalise both into the # same lookup so one mapper serves both sources. _TIDAL_HIRES = AudioQuality(format='flac', sample_rate=96000, bit_depth=24) _TIDAL_LOSSLESS = AudioQuality(format='flac', sample_rate=44100, bit_depth=16) _TIDAL_HIGH = AudioQuality(format='aac', bitrate=320) _TIDAL_LOW = AudioQuality(format='aac', bitrate=96) TIDAL_TIER_MAP = { 'HI_RES_LOSSLESS': _TIDAL_HIRES, 'HI_RES': _TIDAL_HIRES, 'HIRES': _TIDAL_HIRES, 'LOSSLESS': _TIDAL_LOSSLESS, 'HIGH': _TIDAL_HIGH, 'LOW': _TIDAL_LOW, } def quality_from_tidal_tier(tier: str) -> AudioQuality: """Map a Tidal/HiFi quality tier string to an AudioQuality. Case-insensitive; accepts both ``HI_RES`` and ``hires`` spellings. Unrecognised tiers map to ``format='unknown'`` so they never over-claim lossless quality. """ key = (tier or '').strip().upper() return TIDAL_TIER_MAP.get(key, AudioQuality(format='unknown')) # ── Qobuz (real API values) ──────────────────────────────────────────────── def quality_from_qobuz(sampling_rate_khz: float, bit_depth: int) -> AudioQuality: """Qobuz reports ``maximum_sampling_rate`` in kHz (e.g. 44.1, 96, 192) and ``maximum_bit_depth``. These are real values from the API. """ sample_rate = int(round(sampling_rate_khz * 1000)) if sampling_rate_khz else None return AudioQuality(format='flac', sample_rate=sample_rate, bit_depth=bit_depth) # ── Deezer (config code) ─────────────────────────────────────────────────── DEEZER_CODE_MAP = { 'flac': AudioQuality(format='flac', sample_rate=44100, bit_depth=16), 'mp3_320': AudioQuality(format='mp3', bitrate=320), 'mp3_128': AudioQuality(format='mp3', bitrate=128), } def quality_from_deezer(code: str) -> AudioQuality: """Map a Deezer download quality code to AudioQuality. Deezer FLAC is always CD-quality (16-bit/44.1 kHz). """ return DEEZER_CODE_MAP.get((code or '').lower(), AudioQuality(format='unknown')) # ── Amazon Music (real sampleRate preferred, HD/UHD tier fallback) ───────── _AMAZON_TIER_MAP = { 'UHD': AudioQuality(format='flac', sample_rate=96000, bit_depth=24), 'HD': AudioQuality(format='flac', sample_rate=44100, bit_depth=16), } def quality_from_amazon( tier: str, sample_rate: Optional[int] = None, bit_depth: Optional[int] = None, ) -> AudioQuality: """Amazon Music is FLAC; prefer the real ``sampleRate``/``bitDepth`` from the stream info when present, otherwise fall back to the HD/UHD tier. """ base = _AMAZON_TIER_MAP.get((tier or '').strip().upper(), AudioQuality(format='flac')) return AudioQuality( format='flac', sample_rate=sample_rate if sample_rate is not None else base.sample_rate, bit_depth=bit_depth if bit_depth is not None else base.bit_depth, )