Design for source-binding + quality-aware fall-through ranking (per-source population, source-priority-king), ranked-targets UI, quarantine-reason surfacing, and tests. Locks the constraints that quality quarantine reuses the trigger='quality' retry path and never sets force_imported (reserved for AcoustID mismatches). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.6 KiB
Global Quality System — Source Binding, Ranking & UI
Branch: feature/global-quality-system
Date: 2026-06-14
Status: Approved design — ready for implementation plan
Problem
The quality model (core/quality/model.py) is complete and Soulseek already
uses it, but the system has no cross-cutting quality behaviour:
- Streaming sources don't populate real quality.
SearchResult.audio_qualityis a derived property overformat/bitrate/sample_rate/bit_depth, but Tidal, HiFi, Deezer, Qobuz, Amazon, YouTube never fillsample_rate/bit_depth(and sometimes not even the rightformat). Theiraudio_qualitytherefore falls back to crude kbps heuristics. - Ranking is inconsistent. Only the Soulseek path runs
filter_results_by_quality_preference. Streaming results are ordered by match-confidence only — quality is ignored. - No quality-aware source fall-through.
search_with_fallbackis "first non-empty source wins": it never escalates to the next source when the current source can't deliver the wanted quality. - No UI for the v3 ranked-target list. The profile model is v3 (ordered target list) but there is no editor for it.
Decisions (locked)
- Per-Source Population, not cross-source pooling. Source priority (the
hybrid chain order) stays king. Each source populates an accurate
audio_quality; the chain fall-through becomes quality-aware so a source that cannot meet any target is skipped in favour of the next source. - Full scope: source mappers (A) + ranking wiring + streaming-path unify, UI ranked-target editor (B), quarantine-reason surfacing (C), tests (D).
- Bitrate is a settable minimum threshold (a range "≥ X"), never an exact
match. Lossless (FLAC/WAV) is matched on
bit_depth/sample_rate; bitrate is only a fallback heuristic when those are absent. This is already howAudioQuality.matches_targetbehaves — the work is to expose it correctly in the UI and add a small VBR tolerance for lossy presets.
Hard constraints (must not regress)
These are already correct in the codebase and the new work must preserve them:
- Retry harmony. A quality reject already flows through the same retry path as
AcoustID:
check_quality_target→move_to_quarantine(trigger='quality')→requeue_quarantined_task_for_retry(..., 'quality')(pipeline.py:584-620). The worker walks the next-best candidate using the per-source retry budget. New code must keepcheck_quality_targetreturning a reason string that feeds this path. - force_import isolation.
force_importedstatus is set ONLY by the AcoustID version-mismatch fallback (core/imports/version_mismatch_fallback.py). The quality guard sets NO verification status — it quarantines withtrigger='quality'and the bypass flag_skip_quarantine_check='quality'. A quality mismatch must NEVER becomeforce_imported. force_import stays reserved for AcoustID mismatches.
A — Source mappers
New module core/quality/source_map.py centralises each source's tier
knowledge (kept out of model.py to avoid bloat). Each download client populates
the four quality fields (quality, bitrate, sample_rate, bit_depth) of its
TrackResult via these helpers; audio_quality then derives automatically.
Each tier value is a claim, verified post-download by check_quality_target
reading the real file. Ranking uses the claim to pick; the guard catches lies.
| Source | Source of values | Mapping |
|---|---|---|
| Soulseek | slskd attrs type 4 (sample_rate) / 5 (bit_depth) — real | already done (AudioQuality.from_slskd_file) |
| Qobuz | API maximum_sampling_rate (kHz) + maximum_bit_depth — real |
sample_rate = rate*1000, bit_depth, format='flac' |
| Deezer | config code flac/mp3_320/mp3_128 |
flac→16-bit/44.1kHz · mp3_320→320kbps · mp3_128→128kbps |
| Tidal | track audioQuality tier |
HI_RES_LOSSLESS/HI_RES→flac 24/96 · LOSSLESS→flac 16/44.1 · HIGH→aac 320 · LOW→aac 96 |
| HiFi / Monochrome | Tidal-backed; config quality key | same map as Tidal |
| Amazon | real sampleRate when present, else tier |
UHD→24/96 · HD→16/44.1; prefer real sampleRate |
| YouTube / SoundCloud | yt-dlp / stream format + abr |
lossy: format + bitrate, no bit_depth |
Helper shape:
# core/quality/source_map.py
TIDAL_TIER_MAP: dict[str, AudioQuality]
AMAZON_TIER_MAP: dict[str, AudioQuality]
def quality_from_tidal_tier(tier: str) -> AudioQuality: ...
def quality_from_qobuz(sampling_rate_khz: float, bit_depth: int) -> AudioQuality: ...
def quality_from_deezer(code: str) -> AudioQuality: ...
def quality_from_amazon(stream_info: dict) -> AudioQuality: ...
Clients copy the result onto the TrackResult fields (small helper
TrackResult.set_quality(aq) to avoid four-line repetition at each call site).
Wiring — quality-aware fall-through
New helper core/quality/selection.py:
def rank_for_profile(candidates) -> tuple[list, bool]:
"""Return (ranked_candidates, satisfied_a_target).
satisfied = filter_and_rank(fallback_enabled=False) produced anything.
"""
search_with_fallback (core/download_engine/engine.py) changes from
"first non-empty wins" to:
best_fallback = []
for source in chain:
tracks, albums = source.search(query)
if not tracks: continue
ranked, satisfied = rank_for_profile(tracks)
if satisfied:
return ranked, albums # this source meets a target → done
best_fallback = best_fallback or (ranked, albums)
# chain exhausted, nothing satisfied a target
return best_fallback if fallback_enabled else ([], [])
Key behavioural note: with source-priority-king plus a long target list (down to MP3 192), fall-through only triggers when a source meets no target at all (below the floor) — e.g. user wants only FLAC and a source has only MP3. Otherwise the first source that returns acceptable results wins, exactly as today but now quality-ranked within.
Streaming-path unification
In download_orchestrator.search_and_download_best, both paths converge on:
match-filter first (right track), then quality-rank (best version). The
streaming branch keeps its confidence filter (≥0.55) and then applies
rank_for_profile to the survivors; the Soulseek branch keeps its quality ranking
and is unchanged in spirit. No path is left quality-blind.
B — Ranked-targets UI
A draggable, ordered list in the quality settings panel:
- Each row: drag handle, label, format, the relevant constraint (bit_depth + min_sample_rate for lossless; min_bitrate as a settable "≥ X kbps" field for lossy), delete button.
- "Add target" control with format/bit_depth/sample_rate/bitrate inputs.
fallback_enabledtoggle.- Persists via the existing
GET/POST /api/quality-profile(already v3-shaped).
Bitrate field is explicitly a minimum threshold, defaulting to small VBR
headroom for the common presets (e.g. a "320" preset stores min_bitrate≈315 so
VBR/mono files near 320 still match). Lossless rows hide the bitrate field since
they match on bit_depth/sample_rate.
C — Quarantine reason
Mostly wiring — the UI already carries quarantineReason dataset and an approve
flow (webui/static/downloads.js), and check_quality_target already returns a
"file is X, wanted Y" string.
- Ensure the quality rejection reason is threaded into the quarantine record's reason field and rendered in the track-detail modal (what was wanted vs what the file actually is).
- "Approve anyway" sets
_skip_quarantine_check='quality'(the bypass already honoured at pipeline.py:584-585).
D — Tests
tests/quality/test_source_map.py— each mapper produces the expectedAudioQuality(Tidal tiers, Qobuz kHz→Hz, Deezer codes, Amazon real-vs-tier, lossy no bit_depth).tests/quality/test_selection.py—rank_for_profilesatisfied/unsatisfied; fall-through: source A below floor → engine escalates to source B; nothing matches withfallback_enabledon (best returned) vs off (empty).- Extend
tests/quality/test_model.py—matches_targetbitrate-as-minimum, FLAC matched on bit_depth/sample_rate not bitrate, v2→v3 migration. tests/imports/test_quality_guard.py—check_quality_targetquarantine reason scenarios plus a regression test asserting a quality mismatch usestrigger='quality'and never setsforce_imported.
Out of scope
- Cross-source candidate pooling (rejected in favour of per-source population).
- The Monochrome/HiFi 30-second-silence bug (tracked separately in
PLAN.md; needs ffmpegsilencedetect, not tier mapping).