soulsync/tests/quality/test_search_all_sources.py
dev d484046523 feat(downloads): best-quality search mode + clearer fallback UI
Adds an opt-in search strategy toggle in the Quality Profile:

- priority (default): unchanged — first source in the hybrid chain that
  meets a quality target wins.
- best_quality: pool candidates from EVERY source per query and download
  them best→worst by actual audio quality; source order only breaks ties.

Implementation reuses existing plumbing so the retry system is untouched:
- engine.search_all_sources pools raw tracks across all configured,
  non-exhausted sources (no first-source short-circuit).
- candidates.order_candidates: new quality_first sort path — profile
  quality rank dominates, confidence/peer signals break ties. Priority
  path is byte-for-byte unchanged (regression-locked by tests).
- task_worker passes quality_first + targets through; skips the redundant
  hybrid-fallback block in best-quality mode (pool already covered it).
- Per-source retry budgets unchanged: a source that spends its budget is
  added to exhausted_download_sources and thus dropped from the whole
  pool. Independent of post_processing.retry_exhaustive.
- Query generator NOT touched.

Also clarifies the "Allow fallback" setting wording: it accepts OFF-LIST
quality as a last resort (not "walk down my list"), and notes that
lossy_copy.downsample_hires also bypasses the quality gate — the cause of
16-bit/MP3 files slipping through a 24-bit-only profile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:46:01 +02:00

82 lines
2.3 KiB
Python

"""Engine.search_all_sources pools candidates from EVERY configured source
(best-quality mode), instead of stopping at the first satisfying one like
search_with_fallback. Ranking happens later in the orchestrator — this just
combines raw tracks. Excluded/exhausted and raising sources are skipped.
"""
import asyncio
class _Cand:
def __init__(self, name):
self.name = name
class _FakePlugin:
def __init__(self, tracks, configured=True, raises=False):
self._tracks = tracks
self._configured = configured
self._raises = raises
self.searched = False
def is_configured(self):
return self._configured
async def search(self, query, timeout=None, progress_callback=None):
self.searched = True
if self._raises:
raise RuntimeError("boom")
return (list(self._tracks), [])
def _engine_with(plugins):
from core.download_engine.engine import DownloadEngine
eng = object.__new__(DownloadEngine)
eng._plugins = plugins
return eng
def test_pools_candidates_from_all_sources():
a = _FakePlugin([_Cand('a1'), _Cand('a2')])
b = _FakePlugin([_Cand('b1')])
eng = _engine_with({'a': a, 'b': b})
tracks, albums = asyncio.run(eng.search_all_sources('q', ['a', 'b']))
assert {t.name for t in tracks} == {'a1', 'a2', 'b1'}
assert a.searched and b.searched
assert albums == []
def test_skips_excluded_sources():
a = _FakePlugin([_Cand('a1')])
b = _FakePlugin([_Cand('b1')])
eng = _engine_with({'a': a, 'b': b})
tracks, _ = asyncio.run(
eng.search_all_sources('q', ['a', 'b'], exclude_sources=['b'])
)
assert {t.name for t in tracks} == {'a1'}
assert b.searched is False
def test_skips_unconfigured_and_swallows_raising_source():
a = _FakePlugin([_Cand('a1')], configured=False)
b = _FakePlugin([_Cand('b1')], raises=True)
c = _FakePlugin([_Cand('c1')])
eng = _engine_with({'a': a, 'b': b, 'c': c})
tracks, _ = asyncio.run(eng.search_all_sources('q', ['a', 'b', 'c']))
assert {t.name for t in tracks} == {'c1'} # a skipped, b raised, c survives
def test_empty_when_all_sources_empty():
a = _FakePlugin([])
eng = _engine_with({'a': a})
tracks, albums = asyncio.run(eng.search_all_sources('q', ['a']))
assert tracks == []
assert albums == []