feat(quality): source mappers + populate real quality on streaming results
Add core/quality/source_map.py centralising each source's tier->AudioQuality
mapping (Tidal/HiFi tiers, Qobuz real kHz/bit-depth, Deezer codes, Amazon
codec/tier). Add TrackResult.set_quality() to merge a mapped AudioQuality
onto a result. Wire HiFi, Qobuz, Deezer, Tidal, Amazon search results to
stamp real sample_rate/bit_depth so the global ranker no longer relies on
crude kbps heuristics for streaming sources. Fixes Qobuz/Amazon display
labels ('FLAC 24-bit/192kHz', 'Lossless') breaking format derivation.
Tested: 22 passing (mappers + set_quality merge semantics).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
97242cbd8d
commit
12341f006b
9 changed files with 319 additions and 4 deletions
|
|
@ -32,6 +32,7 @@ from config.settings import config_manager
|
||||||
from core.amazon_client import AmazonClient, AmazonClientError
|
from core.amazon_client import AmazonClient, AmazonClientError
|
||||||
from core.download_plugins.base import DownloadSourcePlugin
|
from core.download_plugins.base import DownloadSourcePlugin
|
||||||
from core.download_plugins.types import AlbumResult, DownloadStatus, TrackResult
|
from core.download_plugins.types import AlbumResult, DownloadStatus, TrackResult
|
||||||
|
from core.quality.model import AudioQuality
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger("amazon_download_client")
|
logger = get_logger("amazon_download_client")
|
||||||
|
|
@ -133,11 +134,17 @@ class AmazonDownloadClient(DownloadSourcePlugin):
|
||||||
album_map: Dict[str, AlbumResult] = {}
|
album_map: Dict[str, AlbumResult] = {}
|
||||||
album_order: List[str] = []
|
album_order: List[str] = []
|
||||||
preferred = self._client.preferred_codec
|
preferred = self._client.preferred_codec
|
||||||
|
# Search results only carry the codec (real sample_rate arrives at
|
||||||
|
# stream time). Claim the format honestly — FLAC for the lossless
|
||||||
|
# codec, lossy otherwise — so audio_quality derives a real format
|
||||||
|
# instead of the display label ("Lossless"), and the post-download
|
||||||
|
# probe pins the actual sample_rate/bit_depth.
|
||||||
|
amazon_q = AudioQuality(format='flac' if _codec_key(preferred) == 'flac' else 'aac')
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
quality = _quality_label(preferred)
|
quality = _quality_label(preferred)
|
||||||
if item.is_track:
|
if item.is_track:
|
||||||
track_results.append(TrackResult(
|
tr = TrackResult(
|
||||||
username="amazon",
|
username="amazon",
|
||||||
filename=f"{item.asin}||{item.artist_name} - {item.title}",
|
filename=f"{item.asin}||{item.artist_name} - {item.title}",
|
||||||
size=0,
|
size=0,
|
||||||
|
|
@ -155,7 +162,9 @@ class AmazonDownloadClient(DownloadSourcePlugin):
|
||||||
"album_asin": item.album_asin,
|
"album_asin": item.album_asin,
|
||||||
"isrc": item.isrc,
|
"isrc": item.isrc,
|
||||||
},
|
},
|
||||||
))
|
)
|
||||||
|
tr.set_quality(amazon_q)
|
||||||
|
track_results.append(tr)
|
||||||
elif item.is_album:
|
elif item.is_album:
|
||||||
album_asin = item.album_asin or item.asin
|
album_asin = item.album_asin or item.asin
|
||||||
if album_asin not in album_map:
|
if album_asin not in album_map:
|
||||||
|
|
@ -173,6 +182,7 @@ class AmazonDownloadClient(DownloadSourcePlugin):
|
||||||
title=item.title,
|
title=item.title,
|
||||||
album=item.album_name,
|
album=item.album_name,
|
||||||
)
|
)
|
||||||
|
placeholder.set_quality(amazon_q)
|
||||||
album_map[album_asin] = AlbumResult(
|
album_map[album_asin] = AlbumResult(
|
||||||
username="amazon",
|
username="amazon",
|
||||||
album_path=album_asin,
|
album_path=album_asin,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from core.download_plugins.types import AlbumResult, DownloadStatus, TrackResult
|
from core.download_plugins.types import AlbumResult, DownloadStatus, TrackResult
|
||||||
|
from core.quality.source_map import quality_from_deezer
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger("deezer_download")
|
logger = get_logger("deezer_download")
|
||||||
|
|
@ -581,7 +582,7 @@ class DeezerDownloadClient(DownloadSourcePlugin):
|
||||||
bitrate = 128
|
bitrate = 128
|
||||||
quality = 'mp3'
|
quality = 'mp3'
|
||||||
|
|
||||||
results.append(TrackResult(
|
tr = TrackResult(
|
||||||
username='deezer_dl',
|
username='deezer_dl',
|
||||||
filename=f"{track_id}||{artist} - {title}",
|
filename=f"{track_id}||{artist} - {title}",
|
||||||
size=est_size,
|
size=est_size,
|
||||||
|
|
@ -595,7 +596,10 @@ class DeezerDownloadClient(DownloadSourcePlugin):
|
||||||
title=title,
|
title=title,
|
||||||
album=album,
|
album=album,
|
||||||
track_number=item.get('track_position'),
|
track_number=item.get('track_position'),
|
||||||
))
|
)
|
||||||
|
# Stamp CD-quality FLAC (16/44.1) so lossless ranks correctly.
|
||||||
|
tr.set_quality(quality_from_deezer(self._quality))
|
||||||
|
results.append(tr)
|
||||||
|
|
||||||
logger.info(f"Deezer search for '{query}' returned {len(results)} results")
|
logger.info(f"Deezer search for '{query}' returned {len(results)} results")
|
||||||
return results, []
|
return results, []
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,22 @@ class SearchResult:
|
||||||
bit_depth=self.bit_depth,
|
bit_depth=self.bit_depth,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_quality(self, aq: AudioQuality) -> None:
|
||||||
|
"""Merge a mapped :class:`AudioQuality` onto this result's fields.
|
||||||
|
|
||||||
|
Used by streaming sources to stamp their claimed tier (Tidal/HiFi
|
||||||
|
tier strings, Qobuz API values, …) so ``audio_quality`` ranks
|
||||||
|
correctly. Mapper-provided fields win; a ``None`` from the mapper
|
||||||
|
leaves any already-reported value (e.g. a probed bitrate) intact.
|
||||||
|
"""
|
||||||
|
self.quality = aq.format
|
||||||
|
if aq.bitrate is not None:
|
||||||
|
self.bitrate = aq.bitrate
|
||||||
|
if aq.sample_rate is not None:
|
||||||
|
self.sample_rate = aq.sample_rate
|
||||||
|
if aq.bit_depth is not None:
|
||||||
|
self.bit_depth = aq.bit_depth
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quality_score(self) -> float:
|
def quality_score(self) -> float:
|
||||||
quality_weights = {
|
quality_weights = {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import requests as http_requests
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
from config.settings import config_manager
|
from config.settings import config_manager
|
||||||
from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus
|
from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus
|
||||||
|
from core.quality.source_map import quality_from_tidal_tier
|
||||||
|
|
||||||
logger = get_logger("hifi_client")
|
logger = get_logger("hifi_client")
|
||||||
|
|
||||||
|
|
@ -754,10 +755,15 @@ class HiFiClient(DownloadSourcePlugin):
|
||||||
quality_key = config_manager.get('hifi_download.quality', 'lossless')
|
quality_key = config_manager.get('hifi_download.quality', 'lossless')
|
||||||
q_info = HLS_QUALITY_MAP.get(quality_key, HLS_QUALITY_MAP['lossless'])
|
q_info = HLS_QUALITY_MAP.get(quality_key, HLS_QUALITY_MAP['lossless'])
|
||||||
|
|
||||||
|
# HiFi is Tidal-backed; stamp the configured tier so the global
|
||||||
|
# ranker sees real sample_rate/bit_depth, not just 'flac'.
|
||||||
|
tier_quality = quality_from_tidal_tier(quality_key)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for t in tracks:
|
for t in tracks:
|
||||||
try:
|
try:
|
||||||
tr = self._to_track_result(t, q_info)
|
tr = self._to_track_result(t, q_info)
|
||||||
|
tr.set_quality(tier_quality)
|
||||||
results.append(tr)
|
results.append(tr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Skipping track result conversion: {e}")
|
logger.debug(f"Skipping track result conversion: {e}")
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ from config.settings import config_manager
|
||||||
|
|
||||||
# Import Soulseek data structures for drop-in replacement compatibility
|
# Import Soulseek data structures for drop-in replacement compatibility
|
||||||
from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus
|
from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus
|
||||||
|
from core.quality.source_map import quality_from_qobuz
|
||||||
|
|
||||||
logger = get_logger("qobuz_client")
|
logger = get_logger("qobuz_client")
|
||||||
|
|
||||||
|
|
@ -1072,6 +1073,14 @@ class QobuzClient(DownloadSourcePlugin):
|
||||||
track_number=track.get('track_number'),
|
track_number=track.get('track_number'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Stamp real API quality so the global ranker sees actual
|
||||||
|
# sample_rate/bit_depth. Hi-res only when the track itself is
|
||||||
|
# hires-streamable; otherwise it streams as CD-quality FLAC.
|
||||||
|
if hires_streamable and max_bit_depth >= 24:
|
||||||
|
track_result.set_quality(quality_from_qobuz(max_sample_rate, max_bit_depth))
|
||||||
|
else:
|
||||||
|
track_result.set_quality(quality_from_qobuz(44.1, 16))
|
||||||
|
|
||||||
return track_result
|
return track_result
|
||||||
|
|
||||||
# ===================== Download =====================
|
# ===================== Download =====================
|
||||||
|
|
|
||||||
104
core/quality/source_map.py
Normal file
104
core/quality/source_map.py
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
"""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,
|
||||||
|
)
|
||||||
|
|
@ -33,6 +33,7 @@ from config.settings import config_manager
|
||||||
|
|
||||||
# Import Soulseek data structures for drop-in replacement compatibility
|
# Import Soulseek data structures for drop-in replacement compatibility
|
||||||
from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus
|
from core.download_plugins.types import TrackResult, AlbumResult, DownloadStatus
|
||||||
|
from core.quality.source_map import quality_from_tidal_tier
|
||||||
|
|
||||||
logger = get_logger("tidal_download_client")
|
logger = get_logger("tidal_download_client")
|
||||||
|
|
||||||
|
|
@ -457,11 +458,15 @@ class TidalDownloadClient(DownloadSourcePlugin):
|
||||||
|
|
||||||
quality_key = config_manager.get('tidal_download.quality', 'lossless')
|
quality_key = config_manager.get('tidal_download.quality', 'lossless')
|
||||||
quality_info = QUALITY_MAP.get(quality_key, QUALITY_MAP['lossless'])
|
quality_info = QUALITY_MAP.get(quality_key, QUALITY_MAP['lossless'])
|
||||||
|
# Stamp the configured tier (what will actually be downloaded) so
|
||||||
|
# the global ranker sees real sample_rate/bit_depth.
|
||||||
|
tier_quality = quality_from_tidal_tier(quality_key)
|
||||||
|
|
||||||
track_results = []
|
track_results = []
|
||||||
for track in tidal_tracks:
|
for track in tidal_tracks:
|
||||||
try:
|
try:
|
||||||
track_result = self._tidal_to_track_result(track, quality_info)
|
track_result = self._tidal_to_track_result(track, quality_info)
|
||||||
|
track_result.set_quality(tier_quality)
|
||||||
track_results.append(track_result)
|
track_results.append(track_result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Skipping track conversion error: {e}")
|
logger.debug(f"Skipping track conversion error: {e}")
|
||||||
|
|
|
||||||
122
tests/quality/test_source_map.py
Normal file
122
tests/quality/test_source_map.py
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
"""Tests for core.quality.source_map — each download source's tier/string
|
||||||
|
mapped into a unified AudioQuality.
|
||||||
|
|
||||||
|
These mappers express each source's *claimed* capability tier. The values
|
||||||
|
are later verified post-download by the quality guard, so over-claiming is
|
||||||
|
the failure mode to avoid: an unknown tier must NOT pretend to be lossless.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.quality.source_map import (
|
||||||
|
quality_from_tidal_tier,
|
||||||
|
quality_from_qobuz,
|
||||||
|
quality_from_deezer,
|
||||||
|
quality_from_amazon,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tidal / HiFi (Tidal-backed) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("tier", ["HI_RES_LOSSLESS", "HI_RES", "hi_res", "hires", "HIRES"])
|
||||||
|
def test_tidal_hires_is_flac_24_96(tier):
|
||||||
|
aq = quality_from_tidal_tier(tier)
|
||||||
|
assert aq.format == "flac"
|
||||||
|
assert aq.bit_depth == 24
|
||||||
|
assert aq.sample_rate == 96000
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("tier", ["LOSSLESS", "lossless"])
|
||||||
|
def test_tidal_lossless_is_flac_16_44(tier):
|
||||||
|
aq = quality_from_tidal_tier(tier)
|
||||||
|
assert aq.format == "flac"
|
||||||
|
assert aq.bit_depth == 16
|
||||||
|
assert aq.sample_rate == 44100
|
||||||
|
|
||||||
|
|
||||||
|
def test_tidal_high_is_lossy_aac_320():
|
||||||
|
aq = quality_from_tidal_tier("HIGH")
|
||||||
|
assert aq.format == "aac"
|
||||||
|
assert aq.bitrate == 320
|
||||||
|
assert aq.bit_depth is None # lossy: no bit depth
|
||||||
|
|
||||||
|
|
||||||
|
def test_tidal_low_is_lossy_aac_96():
|
||||||
|
aq = quality_from_tidal_tier("LOW")
|
||||||
|
assert aq.format == "aac"
|
||||||
|
assert aq.bitrate == 96
|
||||||
|
|
||||||
|
|
||||||
|
def test_tidal_unknown_tier_does_not_overclaim():
|
||||||
|
# An unrecognised tier must not masquerade as lossless.
|
||||||
|
aq = quality_from_tidal_tier("SOMETHING_NEW")
|
||||||
|
assert aq.format == "unknown"
|
||||||
|
assert aq.bit_depth is None
|
||||||
|
assert aq.sample_rate is None
|
||||||
|
|
||||||
|
|
||||||
|
# ── Qobuz (real API values) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_qobuz_hires_khz_to_hz():
|
||||||
|
aq = quality_from_qobuz(96.0, 24)
|
||||||
|
assert aq.format == "flac"
|
||||||
|
assert aq.sample_rate == 96000
|
||||||
|
assert aq.bit_depth == 24
|
||||||
|
|
||||||
|
|
||||||
|
def test_qobuz_cd_quality_fractional_khz():
|
||||||
|
aq = quality_from_qobuz(44.1, 16)
|
||||||
|
assert aq.format == "flac"
|
||||||
|
assert aq.sample_rate == 44100
|
||||||
|
assert aq.bit_depth == 16
|
||||||
|
|
||||||
|
|
||||||
|
def test_qobuz_192k():
|
||||||
|
aq = quality_from_qobuz(192.0, 24)
|
||||||
|
assert aq.sample_rate == 192000
|
||||||
|
assert aq.bit_depth == 24
|
||||||
|
|
||||||
|
|
||||||
|
# ── Deezer (config code) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_deezer_flac_is_16_44():
|
||||||
|
aq = quality_from_deezer("flac")
|
||||||
|
assert aq.format == "flac"
|
||||||
|
assert aq.bit_depth == 16
|
||||||
|
assert aq.sample_rate == 44100
|
||||||
|
|
||||||
|
|
||||||
|
def test_deezer_mp3_320():
|
||||||
|
aq = quality_from_deezer("mp3_320")
|
||||||
|
assert aq.format == "mp3"
|
||||||
|
assert aq.bitrate == 320
|
||||||
|
assert aq.bit_depth is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_deezer_mp3_128():
|
||||||
|
aq = quality_from_deezer("mp3_128")
|
||||||
|
assert aq.format == "mp3"
|
||||||
|
assert aq.bitrate == 128
|
||||||
|
|
||||||
|
|
||||||
|
# ── Amazon (real sampleRate preferred, tier fallback) ──────────────────────
|
||||||
|
|
||||||
|
def test_amazon_prefers_real_sample_rate():
|
||||||
|
aq = quality_from_amazon("HD", sample_rate=88200, bit_depth=24)
|
||||||
|
assert aq.format == "flac"
|
||||||
|
assert aq.sample_rate == 88200
|
||||||
|
assert aq.bit_depth == 24
|
||||||
|
|
||||||
|
|
||||||
|
def test_amazon_hd_tier_fallback():
|
||||||
|
aq = quality_from_amazon("HD")
|
||||||
|
assert aq.format == "flac"
|
||||||
|
assert aq.sample_rate == 44100
|
||||||
|
assert aq.bit_depth == 16
|
||||||
|
|
||||||
|
|
||||||
|
def test_amazon_uhd_tier_fallback():
|
||||||
|
aq = quality_from_amazon("UHD")
|
||||||
|
assert aq.format == "flac"
|
||||||
|
assert aq.bit_depth == 24
|
||||||
|
assert aq.sample_rate == 96000
|
||||||
39
tests/quality/test_track_result_quality.py
Normal file
39
tests/quality/test_track_result_quality.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""TrackResult.set_quality() — merge a mapped AudioQuality onto a result so
|
||||||
|
its derived ``audio_quality`` reflects the source's real/claimed tier.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from core.download_plugins.types import TrackResult
|
||||||
|
from core.quality.model import AudioQuality
|
||||||
|
|
||||||
|
|
||||||
|
def _tr(**kw):
|
||||||
|
base = dict(
|
||||||
|
username='x', filename='f', size=0, bitrate=None, duration=None,
|
||||||
|
quality='', free_upload_slots=0, upload_speed=0, queue_length=0,
|
||||||
|
)
|
||||||
|
base.update(kw)
|
||||||
|
return TrackResult(**base)
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_quality_populates_lossless_fields():
|
||||||
|
tr = _tr(quality='flac', bitrate=1411)
|
||||||
|
tr.set_quality(AudioQuality('flac', sample_rate=96000, bit_depth=24))
|
||||||
|
assert tr.quality == 'flac'
|
||||||
|
assert tr.sample_rate == 96000
|
||||||
|
assert tr.bit_depth == 24
|
||||||
|
# derived descriptor must reflect the merge
|
||||||
|
assert tr.audio_quality.sample_rate == 96000
|
||||||
|
assert tr.audio_quality.bit_depth == 24
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_quality_keeps_existing_bitrate_when_mapper_has_none():
|
||||||
|
tr = _tr(quality='flac', bitrate=950)
|
||||||
|
tr.set_quality(AudioQuality('flac', sample_rate=44100, bit_depth=16))
|
||||||
|
assert tr.bitrate == 950 # mapper bitrate is None → preserve probed/reported
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_quality_overwrites_bitrate_for_lossy():
|
||||||
|
tr = _tr(quality='mp3', bitrate=128)
|
||||||
|
tr.set_quality(AudioQuality('mp3', bitrate=320))
|
||||||
|
assert tr.bitrate == 320
|
||||||
|
assert tr.bit_depth is None
|
||||||
Loading…
Reference in a new issue