feat(quality): migrate old per-source Hi-Res preference into the profile (#896 #5)

PR #896 removed the per-source quality dropdowns; streaming sources now derive
their request tier from the global profile via quality_tier_for_source. Without
a migration, a user who had tidal_download/qobuz/hifi_download.quality on
'hires'/'hires_max' silently dropped to lossless (their migrated v2 'flac (any)'
top target resolves to the lossless tier).

_migrate_v2_to_v3 now seeds the 24-bit FLAC ladder at the top of ranked_targets
when such a Hi-Res source preference is detected — but only if the profile
doesn't already express 24-bit, so it never duplicates the ladder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
dev 2026-06-24 21:39:53 +02:00
parent 13bfdaeda2
commit d310b090e2
2 changed files with 87 additions and 2 deletions

View file

@ -8760,15 +8760,52 @@ class MusicDatabase:
return self._get_default_quality_profile()
# 24-bit FLAC ladder seeded on migration for users who had a streaming
# source on Hi-Res under the old (now removed) per-source quality dropdowns.
_HIRES_24BIT_TARGETS = [
{"label": "FLAC 24-bit/192kHz", "format": "flac", "bit_depth": 24, "min_sample_rate": 192000},
{"label": "FLAC 24-bit/96kHz", "format": "flac", "bit_depth": 24, "min_sample_rate": 96000},
{"label": "FLAC 24-bit/48kHz", "format": "flac", "bit_depth": 24, "min_sample_rate": 48000},
{"label": "FLAC 24-bit/44.1kHz","format": "flac", "bit_depth": 24, "min_sample_rate": 44100},
]
def _had_hires_source_preference(self) -> bool:
"""True if the user had any streaming source set to a Hi-Res tier under
the old per-source quality dropdowns (tidal_download/qobuz/hifi_download
.quality = 'hires'|'hires_max'), which #896 removed in favour of the
global profile. Used to preserve their intent on migration."""
try:
from config.settings import config_manager
except Exception:
return False
hires = {'hires', 'hires_max'}
for key in ('tidal_download.quality', 'qobuz.quality', 'hifi_download.quality'):
try:
if str(config_manager.get(key) or '').strip().lower() in hires:
return True
except Exception:
continue
return False
def _migrate_v2_to_v3(self, profile: dict) -> dict:
"""Add ranked_targets to a v2 profile without losing its qualities dict."""
from core.quality.model import v2_qualities_to_ranked_targets
profile = dict(profile)
profile['version'] = 3
if 'ranked_targets' not in profile:
profile['ranked_targets'] = v2_qualities_to_ranked_targets(
profile.get('qualities', {})
ranked = v2_qualities_to_ranked_targets(profile.get('qualities', {}))
# #896 review #5: the per-source quality dropdowns are gone — sources
# now derive their tier from this profile. If the user had a source on
# Hi-Res, seed 24-bit FLAC targets at the top so they keep Hi-Res
# instead of silently dropping to lossless. Skip when the profile
# already expresses 24-bit (don't duplicate the ladder).
already_24bit = any(
t.get('format') == 'flac' and (t.get('bit_depth') or 0) >= 24
for t in ranked
)
if not already_24bit and self._had_hires_source_preference():
ranked = [dict(t) for t in self._HIRES_24BIT_TARGETS] + ranked
profile['ranked_targets'] = ranked
return profile
def _get_default_quality_profile(self) -> dict:

View file

@ -31,3 +31,51 @@ def test_balanced_still_includes_16bit_and_mp3():
assert 'FLAC 16-bit' in labels
assert any('MP3' in l for l in labels)
assert p['fallback_enabled'] is True
# ── v2 → v3 migration seeds Hi-Res from the old per-source dropdowns (#896 #5) ──
class _FakeCfg:
def __init__(self, values):
self._v = values
def get(self, key, default=None):
return self._v.get(key, default)
_V2_LOSSLESS = {
'version': 2, 'preset': 'balanced',
'qualities': {
'flac': {'enabled': True, 'priority': 1, 'bit_depth': 'any'},
'mp3_320': {'enabled': True, 'priority': 2},
},
}
def _migrate(profile, cfg_values, monkeypatch):
monkeypatch.setattr('config.settings.config_manager', _FakeCfg(cfg_values), raising=False)
db = MusicDatabase.__new__(MusicDatabase)
return db._migrate_v2_to_v3(profile)
def test_v2_to_v3_seeds_hires_when_a_source_was_hires(monkeypatch):
"""A user who had Tidal on Hi-Res keeps it: the migrated profile gains 24-bit
targets at the top so quality_tier_for_source resolves to 'hires', not a
silent drop to lossless."""
out = _migrate(dict(_V2_LOSSLESS), {'tidal_download.quality': 'hires'}, monkeypatch)
top = out['ranked_targets'][0]
assert top['format'] == 'flac' and top['bit_depth'] == 24
# the user's existing lossy fallback is preserved below the seeded ladder
assert any(t.get('format') == 'mp3' for t in out['ranked_targets'])
def test_v2_to_v3_no_seed_without_hires_preference(monkeypatch):
out = _migrate(dict(_V2_LOSSLESS), {'tidal_download.quality': 'lossless'}, monkeypatch)
assert not any(t.get('bit_depth') == 24 for t in out['ranked_targets'])
def test_v2_to_v3_no_duplicate_when_profile_already_24bit(monkeypatch):
v2 = dict(_V2_LOSSLESS)
v2['qualities'] = {'flac': {'enabled': True, 'priority': 1, 'bit_depth': '24'}}
out = _migrate(v2, {'qobuz.quality': 'hires_max'}, monkeypatch)
assert sum(1 for t in out['ranked_targets'] if t.get('bit_depth') == 24) == 1