Toggle to search all sources and download best→worst by actual audio quality, vs today's priority-first (first satisfying source wins). Reuses exhausted_download_sources for per-source budget removal; query generator and budget counters untouched. Independent of retry_exhaustive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
Best-Quality Search Mode — Design
Date: 2026-06-14
Branch: (new, off current fix/import-folder-artist-override-optin)
Status: Approved approach (Option A), pending spec review
Goal
Add a user-toggleable download strategy. Today hybrid search is priority-first:
engine.search_with_fallback walks the source chain in priority order and accepts
the first source that meets a quality target — so a passable Soulseek 16-bit FLAC
"wins" even when HiFi/Qobuz/Tidal could deliver a 24-bit version of the same track.
The new best-quality mode instead searches all configured sources, pools their candidates, and works them best→worst by actual audio quality. Source priority becomes only a tiebreaker between equally-good candidates.
Hard constraints (from the user)
- Two independent toggles. The new
search_modeand the existingpost_processing.retry_exhaustiveare orthogonal. The feature must behave correctly in all four combinations (priority/best × default/exhaustive budget). - Budget semantics preserved 1:1. No change to how retries are counted:
- Default mode: single global cap
MAX_QUARANTINE_RETRIES = 5across all sources. - Exhaustive mode: per-source budget
query_count × retries_per_query. When a source completely spends its budget, all of that source's candidates are removed from the entire pooled list (not just skipped once).
- Default mode: single global cap
- Do not touch the query generator.
matching_engine.generate_download_queriesand the legacy query-building block indownload_track_workerstay exactly as-is. force_importnever fires on quality (unchanged existing invariant — only AcoustID mismatches can force-import).
Key realization: the budget-removal mechanism already exists
When a source spends its budget, monitor.requeue_quarantined_task_for_retry
already adds it to the task's exhausted_download_sources set, and the worker
already passes that set as exclude_sources to its searches. Constraint #2's
"remove the source from the whole list" is therefore not new logic — best-quality
mode simply consults the same set when assembling/filtering its pooled candidate
list. No new budget bookkeeping is introduced.
Architecture
The one genuinely new piece: quality-dominant candidate ordering
attempt_download_with_candidates (core/downloads/candidates.py:71) sorts candidates
confidence-first, then quality_score. That is correct for priority mode (never
download a high-quality wrong file). For best-quality mode we keep the
"correct-first" guarantee — get_valid_candidates already drops candidates below the
match threshold, so every candidate in the list is correctly matched — but among
those valid candidates we want profile quality rank to dominate, with confidence as
the tiebreaker.
Add an opt-in parameter:
attempt_download_with_candidates(task_id, candidates, track, batch_id=None,
deps=None, *, quality_first=False)
quality_first=False(default): today's sort, byte-for-byte unchanged.quality_first=True: sort key becomes(profile_quality_rank, confidence, upload_speed, -queue_length, free_slots, size), i.e. profile quality dominates, all existing signals become tiebreakers.
profile_quality_rank is derived from the candidate's stamped AudioQuality against
the user's ranked_targets (reusing core.quality.model / filter_and_rank ordering).
Candidates with no usable quality info sort last (rank = worst), never dropped.
New engine method: search_all_sources
Mirror of search_with_fallback, but it does not stop at the first satisfying
source. It iterates every configured, non-excluded source in the chain, runs each
source's search, and returns the combined raw track list (each track already
quality-stamped by its client's set_quality, as today). Per-source exceptions are
swallowed exactly like the existing method. Excluded (exhausted) sources are skipped.
async def search_all_sources(self, query, source_chain, timeout=None,
progress_callback=None, exclude_sources=None)
-> Tuple[list, list] # (combined_tracks, combined_albums)
Quality ranking is not done here — the orchestrator/worker owns final ranking, same division of labour as today (engine returns raw, orchestrator filters/ranks).
Orchestrator wiring
download_orchestrator.search(...) gains awareness of search_mode:
priority(default) → unchanged:search_with_fallback.best_qualityandmode == 'hybrid'→search_all_sources.- Single-source mode → unchanged regardless of
search_mode(only one source exists; its candidates are still quality-ranked downstream, so the toggle is a no-op there).
A thin helper load_search_mode() -> str reads the quality profile (see Config).
Worker integration (the per-query loop stays intact)
In download_track_worker's existing query loop (core/downloads/task_worker.py:398),
when best-quality mode is active:
- The loop and the query generator are untouched.
orchestrator.search(query, exclude_sources=_exclude_sources)now returns pooled results from all non-exhausted sources (viasearch_all_sources).candidates = get_valid_candidates(pooled, track, query)— unchanged.ranked, _ = rank_for_profile(candidates)— orders best→worst by profile quality.attempt_download_with_candidates(task_id, ranked, track, batch_id, quality_first=True).
Because the pool already came from every source, the hybrid-fallback block
(task_worker.py:529–593, which re-tries hybrid_order[1:] individually) is
redundant in best-quality mode and is skipped — it would just re-search sources the
pool already covered. In priority mode it runs exactly as today.
The exhausted_download_sources set is already folded into _exclude_sources
(task_worker.py:446), so a budget-spent source is excluded from search_all_sources's
pool automatically — satisfying constraint #2 with no new code. A belt-and-suspenders
filter drops any straggler exhausted-source candidates before step 5.
Config / persistence
search_mode belongs to the quality profile (it is a quality-driven policy). Stored
in the v3 quality-profile JSON (database.music_database.get_quality_profile):
{ "version": 3, "search_mode": "priority", ... }
- Default:
"priority"(preserves today's behavior for every existing install). - Accessor: extend
core/quality/selection.pywithload_search_mode() -> strreturning'priority'unless the profile says'best_quality'. - No migration needed: a profile lacking the key reads as
'priority'.
UI
A single toggle in the existing Quality Profile tile (webui/index.html + webui/static/settings.js), e.g. a labelled switch:
Search strategy: ( ) Source priority — fastest, stops at first good source (•) Best quality — searches all sources, picks the highest quality
With a short note that best-quality searches all sources every track (slower / more
API calls). collectQualityProfileFromUI emits search_mode; populateQualityProfileUI
reads it. No change to the ranked-targets editor or the fallback toggle.
Behaviour matrix (all four combinations must hold)
| search_mode | retry_exhaustive | Behaviour |
|---|---|---|
| priority | off | Unchanged today. First satisfying source wins; 5 global retries. |
| priority | on | Unchanged today. First source wins; per-source budgets. |
| best_quality | off | Pool all sources, best→worst; 5 global retries total. A source can't be "removed from the pool" because there's no per-source budget — the global cap fires first. (Documented: combine best-quality with exhaustive for per-source removal.) |
| best_quality | on | Pool all sources, best→worst; each source has query_count × retries_per_query; on spend → source removed from the whole pool via exhausted_download_sources. This is the user's target configuration. |
Error handling
search_all_sources: fail-open per source (swallow exceptions, keep going), identical tosearch_with_fallback. If the whole pool is empty, the worker falls through to its existing not-found handling.- Quality-rank computation: any candidate that can't be ranked sorts last, never crashes the walk (mirrors the engine's existing fail-open ranking).
Testing (TDD)
New tests under tests/quality/ and tests/downloads/:
load_search_mode— defaults to'priority'; returns'best_quality'when set; unknown value falls back to'priority'.search_all_sources— returns combined candidates from multiple fake sources; skips excluded/exhausted sources; swallows a raising source; empty when all empty.- quality-first ordering —
attempt_download_with_candidates(quality_first=True)tries a 24-bit candidate before a higher-confidence 16-bit one;quality_first=Falsereproduces today's confidence-first order exactly (regression lock). - budget-removal in pool — given an exhausted source, its candidates never appear in the walked list, while other sources' candidates remain (exhaustive mode).
- toggle independence — orchestrator chooses
search_all_sourcesonly whensearch_mode == best_quality and mode == hybrid; single-source and priority paths callsearch_with_fallback(or single-client search) unchanged.
Each test written first, watched fail, then minimal code to green. No production code before a failing test.
Files touched
core/quality/selection.py— addload_search_mode(); small quality-rank helper for the sort key (or reuse model).core/download_engine/engine.py— addsearch_all_sources.core/download_orchestrator.py— branchsearch()onsearch_mode.core/downloads/candidates.py— addquality_firstparam + alternate sort key.core/downloads/task_worker.py— pass pooled results +quality_first=True; skip the redundant hybrid-fallback block in best-quality mode. (Query generator block untouched.)database/music_database.py— includesearch_modedefault in the v3 profile.webui/index.html,webui/static/settings.js— the toggle.tests/quality/,tests/downloads/— the tests above.
Explicitly out of scope
- No change to the query generator.
- No change to the budget counters /
MAX_*constants /requeue_quarantined_task_for_retry. - No change to AcoustID / force_import behavior.
- No cross-query pooling: best-quality pools across sources within each query of the existing loop. The primary query already covers all sources, so this captures the best candidate without restructuring the loop. Later queries remain per-source fallbacks for the empty-result case.