feat(quality): rank-based candidate ordering toggle for priority mode
Adds an opt-in `rank_candidates_by_quality` profile flag. When on, the priority-mode download walk orders candidates by the ranked-target quality (confidence/speed only break ties) instead of confidence-first. Default off keeps the byte-for-byte old behaviour, so existing installs are unaffected. Best-quality search mode is always quality-first regardless of the flag; the toggle only affects priority mode. Search-time source selection is unchanged — nothing is skipped, so a track can never go missing, only the order in which copies are tried changes. The version-mismatch force-import follows automatically: it accepts the first-tried (= best-ordered) quarantined candidate, which is the highest-quality one once the walk is quality-first. No change to its selection logic needed. - core/quality/selection.py: load_rank_candidates_by_quality() (fail-closed). - core/downloads/task_worker.py: _best_quality_ordering -> _candidate_ordering; quality-first when best_quality mode OR the toggle is on. - database/music_database.py: default profile carries the flag (False). - web_server.py: flag is preserved globally across preset apply/reset, like search_mode. - core/imports/version_mismatch_fallback.py: comment clarified (no behaviour change). Tests (TDD): load_rank_candidates_by_quality default/enabled/disabled/error; _candidate_ordering across all mode+toggle combinations + fail-closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
05b36704c3
commit
ab05508acc
7 changed files with 134 additions and 16 deletions
|
|
@ -54,21 +54,34 @@ def _cand_user_file(candidate):
|
|||
return getattr(candidate, 'username', None), getattr(candidate, 'filename', None)
|
||||
|
||||
|
||||
def _best_quality_ordering():
|
||||
"""Return ``(quality_first, targets)`` for the active search mode.
|
||||
def _candidate_ordering():
|
||||
"""Return ``(quality_first, targets)`` for the active search mode + toggle.
|
||||
|
||||
In best-quality mode the candidate walk is ordered by the user's profile
|
||||
quality rank (best→worst) instead of confidence-first. Fails closed to
|
||||
priority-mode ordering on any error so a profile/DB hiccup never blocks a
|
||||
download. See docs/superpowers/specs/2026-06-14-best-quality-search-mode-design.md.
|
||||
The candidate walk is ordered by the user's profile quality rank
|
||||
(best→worst) instead of confidence-first when EITHER:
|
||||
- best-quality search mode is active (always quality-first), OR
|
||||
- priority mode and the ``rank_candidates_by_quality`` toggle is on
|
||||
(opt-in; default off keeps the byte-for-byte confidence-first walk).
|
||||
|
||||
Quality-first ordering also makes the version-mismatch force-import pick
|
||||
the highest-quality candidate, because that fallback accepts the
|
||||
first-tried (= best-ordered) quarantined entry.
|
||||
|
||||
Fails closed to confidence-first ordering on any error so a profile/DB
|
||||
hiccup never blocks a download. See
|
||||
docs/superpowers/specs/2026-06-14-best-quality-search-mode-design.md.
|
||||
"""
|
||||
try:
|
||||
from core.quality.selection import load_search_mode, load_profile_targets
|
||||
if load_search_mode() == 'best_quality':
|
||||
from core.quality.selection import (
|
||||
load_search_mode,
|
||||
load_profile_targets,
|
||||
load_rank_candidates_by_quality,
|
||||
)
|
||||
if load_search_mode() == 'best_quality' or load_rank_candidates_by_quality():
|
||||
targets, _ = load_profile_targets()
|
||||
return True, targets
|
||||
except Exception as exc:
|
||||
logger.debug("[Modal Worker] best-quality ordering unavailable: %s", exc)
|
||||
logger.debug("[Modal Worker] quality ordering unavailable: %s", exc)
|
||||
return False, None
|
||||
|
||||
|
||||
|
|
@ -109,7 +122,7 @@ def _try_cached_candidates(task_id, batch_id, track, deps):
|
|||
f"[Modal Worker] Quarantine retry: trying {len(remaining)} cached "
|
||||
f"candidate(s) before re-searching (task {task_id})"
|
||||
)
|
||||
_qf, _qt = _best_quality_ordering()
|
||||
_qf, _qt = _candidate_ordering()
|
||||
return deps.attempt_download_with_candidates(
|
||||
task_id, remaining, track, batch_id, quality_first=_qf, quality_targets=_qt,
|
||||
)
|
||||
|
|
@ -393,7 +406,7 @@ def download_track_worker(task_id: str, batch_id: Optional[str], deps: TaskWorke
|
|||
# Best-quality search mode: the orchestrator already pooled candidates
|
||||
# across every source for each query, so order the candidate walk by the
|
||||
# user's profile quality rank (best→worst). Computed once per task.
|
||||
_best_quality, _quality_targets = _best_quality_ordering()
|
||||
_best_quality, _quality_targets = _candidate_ordering()
|
||||
|
||||
# 2. Sequential Query Search (matches GUI's start_search_worker_parallel logic)
|
||||
search_diagnostics = [] # Track what happened per query for detailed error messages
|
||||
|
|
|
|||
|
|
@ -103,9 +103,12 @@ def select_version_mismatch_fallback(
|
|||
# don't guess which the user wants.
|
||||
return None
|
||||
|
||||
# First tried = oldest = highest-confidence (the retry walks candidates
|
||||
# best-first). The id is a "<date>_<time>_<name>" timestamp prefix, so the
|
||||
# lexicographically smallest id is the earliest attempt.
|
||||
# First tried = oldest = best (the retry walks candidates best-first; what
|
||||
# "best" means follows the active ordering — confidence-first by default, or
|
||||
# ranked-target quality when best_quality mode / the rank_candidates_by_quality
|
||||
# toggle is on, so this naturally accepts the highest-quality candidate then).
|
||||
# The id is a "<date>_<time>_<name>" timestamp prefix, so the lexicographically
|
||||
# smallest id is the earliest attempt.
|
||||
return min((e for _, e in candidates), key=lambda e: e["id"])
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -119,6 +119,26 @@ def load_search_mode() -> str:
|
|||
return mode if mode in _VALID_SEARCH_MODES else "priority"
|
||||
|
||||
|
||||
def load_rank_candidates_by_quality() -> bool:
|
||||
"""Opt-in: order the priority-mode download walk (and thus the
|
||||
version-mismatch force-import pick, which takes the first-tried = best)
|
||||
by ranked-target quality instead of confidence-first.
|
||||
|
||||
Best-quality search mode is always quality-first regardless of this flag;
|
||||
this toggle only affects *priority* mode. Default ``False`` keeps the
|
||||
byte-for-byte old behaviour (confidence/peer-speed first), so existing
|
||||
installs are unaffected unless they opt in. Any missing value or DB error
|
||||
resolves to ``False``.
|
||||
"""
|
||||
from database.music_database import MusicDatabase
|
||||
|
||||
try:
|
||||
profile = MusicDatabase().get_quality_profile()
|
||||
return bool(profile.get("rank_candidates_by_quality", False))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def rank_for_profile(candidates: list) -> Tuple[list, bool]:
|
||||
"""Load the user's quality profile and rank *candidates* against it.
|
||||
|
||||
|
|
|
|||
|
|
@ -8824,6 +8824,7 @@ class MusicDatabase:
|
|||
"preset": "balanced",
|
||||
"fallback_enabled": True,
|
||||
"search_mode": "priority",
|
||||
"rank_candidates_by_quality": False,
|
||||
"ranked_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},
|
||||
|
|
|
|||
47
tests/downloads/test_worker_quality_ordering.py
Normal file
47
tests/downloads/test_worker_quality_ordering.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"""core.downloads.task_worker._candidate_ordering — decides whether the
|
||||
download walk is ordered quality-first or confidence-first.
|
||||
|
||||
Quality-first applies when:
|
||||
- best_quality search mode is active (always), OR
|
||||
- priority mode AND the rank_candidates_by_quality toggle is on.
|
||||
|
||||
Default (priority mode, toggle off) → confidence-first, the byte-for-byte old
|
||||
behaviour. Any error fails closed to confidence-first so a DB hiccup never
|
||||
blocks a download.
|
||||
"""
|
||||
|
||||
import core.quality.selection as selection
|
||||
import core.downloads.task_worker as task_worker
|
||||
|
||||
_TARGETS = ["t1", "t2"]
|
||||
|
||||
|
||||
def _patch(monkeypatch, *, mode, rank_toggle):
|
||||
monkeypatch.setattr(selection, "load_search_mode", lambda: mode)
|
||||
monkeypatch.setattr(
|
||||
selection, "load_rank_candidates_by_quality", lambda: rank_toggle
|
||||
)
|
||||
monkeypatch.setattr(selection, "load_profile_targets", lambda: (_TARGETS, True))
|
||||
|
||||
|
||||
def test_priority_mode_toggle_off_is_confidence_first(monkeypatch):
|
||||
_patch(monkeypatch, mode="priority", rank_toggle=False)
|
||||
assert task_worker._candidate_ordering() == (False, None)
|
||||
|
||||
|
||||
def test_priority_mode_toggle_on_is_quality_first(monkeypatch):
|
||||
_patch(monkeypatch, mode="priority", rank_toggle=True)
|
||||
assert task_worker._candidate_ordering() == (True, _TARGETS)
|
||||
|
||||
|
||||
def test_best_quality_mode_is_quality_first_regardless_of_toggle(monkeypatch):
|
||||
_patch(monkeypatch, mode="best_quality", rank_toggle=False)
|
||||
assert task_worker._candidate_ordering() == (True, _TARGETS)
|
||||
|
||||
|
||||
def test_fails_closed_to_confidence_first_on_error(monkeypatch):
|
||||
def _boom():
|
||||
raise RuntimeError("db down")
|
||||
|
||||
monkeypatch.setattr(selection, "load_search_mode", _boom)
|
||||
assert task_worker._candidate_ordering() == (False, None)
|
||||
|
|
@ -33,3 +33,32 @@ def test_returns_best_quality_when_set(monkeypatch):
|
|||
def test_unknown_value_falls_back_to_priority(monkeypatch):
|
||||
_patch_profile(monkeypatch, {"version": 3, "search_mode": "nonsense"})
|
||||
assert selection.load_search_mode() == "priority"
|
||||
|
||||
|
||||
# ── rank_candidates_by_quality toggle ───────────────────────────────────────
|
||||
# Opt-in: order the priority-mode download walk by ranked-target quality
|
||||
# instead of confidence-first. Default OFF = byte-for-byte old behaviour.
|
||||
|
||||
|
||||
def test_rank_by_quality_defaults_false_when_key_absent(monkeypatch):
|
||||
_patch_profile(monkeypatch, {"version": 3, "ranked_targets": []})
|
||||
assert selection.load_rank_candidates_by_quality() is False
|
||||
|
||||
|
||||
def test_rank_by_quality_true_when_enabled(monkeypatch):
|
||||
_patch_profile(monkeypatch, {"version": 3, "rank_candidates_by_quality": True})
|
||||
assert selection.load_rank_candidates_by_quality() is True
|
||||
|
||||
|
||||
def test_rank_by_quality_false_when_disabled(monkeypatch):
|
||||
_patch_profile(monkeypatch, {"version": 3, "rank_candidates_by_quality": False})
|
||||
assert selection.load_rank_candidates_by_quality() is False
|
||||
|
||||
|
||||
def test_rank_by_quality_false_on_db_error(monkeypatch):
|
||||
class _BoomDB:
|
||||
def get_quality_profile(self):
|
||||
raise RuntimeError("db down")
|
||||
|
||||
monkeypatch.setattr(music_database, "MusicDatabase", _BoomDB)
|
||||
assert selection.load_rank_candidates_by_quality() is False
|
||||
|
|
|
|||
|
|
@ -4550,9 +4550,12 @@ def apply_quality_preset(preset_name):
|
|||
|
||||
current = db.get_quality_profile()
|
||||
preset = dict(db.get_quality_preset(preset_name))
|
||||
# search_mode is a global search strategy, not a per-preset audio setting —
|
||||
# carry the user's current choice across preset switches.
|
||||
# search_mode + rank_candidates_by_quality are global search/ordering
|
||||
# strategies, not per-preset audio settings — carry the user's current
|
||||
# choices across preset switches.
|
||||
preset['search_mode'] = current.get('search_mode', preset.get('search_mode', 'priority'))
|
||||
preset['rank_candidates_by_quality'] = current.get(
|
||||
'rank_candidates_by_quality', preset.get('rank_candidates_by_quality', False))
|
||||
success = db.set_quality_profile(preset)
|
||||
|
||||
if success:
|
||||
|
|
@ -4580,6 +4583,8 @@ def reset_quality_preset(preset_name):
|
|||
current = db.get_quality_profile()
|
||||
preset = dict(db.reset_quality_preset(preset_name))
|
||||
preset['search_mode'] = current.get('search_mode', preset.get('search_mode', 'priority'))
|
||||
preset['rank_candidates_by_quality'] = current.get(
|
||||
'rank_candidates_by_quality', preset.get('rank_candidates_by_quality', False))
|
||||
success = db.set_quality_profile(preset)
|
||||
|
||||
if success:
|
||||
|
|
|
|||
Loading…
Reference in a new issue