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>
66 lines
2.1 KiB
Python
66 lines
2.1 KiB
Python
"""orchestrator.search routes to the right engine method per search_mode:
|
|
|
|
- priority (default) → engine.search_with_fallback (first source wins)
|
|
- best_quality + hybrid → engine.search_all_sources (pool every source)
|
|
- single-source mode → the single client's search (toggle is a no-op)
|
|
"""
|
|
|
|
import asyncio
|
|
|
|
import core.download_orchestrator as orch_mod
|
|
from core.download_orchestrator import DownloadOrchestrator
|
|
|
|
|
|
def _run(coro):
|
|
loop = asyncio.new_event_loop()
|
|
try:
|
|
return loop.run_until_complete(coro)
|
|
finally:
|
|
loop.close()
|
|
|
|
|
|
class _SpyEngine:
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
async def search_with_fallback(self, query, chain, timeout=None, progress_callback=None):
|
|
self.calls.append(('fallback', tuple(chain)))
|
|
return (['fb'], [])
|
|
|
|
async def search_all_sources(self, query, chain, timeout=None,
|
|
progress_callback=None, exclude_sources=None):
|
|
self.calls.append(('all', tuple(chain)))
|
|
return (['pool'], [])
|
|
|
|
|
|
def _hybrid_orch():
|
|
orch = DownloadOrchestrator.__new__(DownloadOrchestrator)
|
|
orch.mode = 'hybrid'
|
|
orch.hybrid_order = ['soulseek', 'hifi']
|
|
orch.hybrid_primary = 'soulseek'
|
|
orch.hybrid_secondary = 'hifi'
|
|
orch.engine = _SpyEngine()
|
|
# _resolve_source_chain normalizes names through the registry; stub it so the
|
|
# test doesn't need a full registry.
|
|
orch._resolve_source_chain = lambda: ['soulseek', 'hifi']
|
|
return orch
|
|
|
|
|
|
def test_priority_mode_uses_search_with_fallback(monkeypatch):
|
|
monkeypatch.setattr(orch_mod, 'load_search_mode', lambda: 'priority')
|
|
orch = _hybrid_orch()
|
|
|
|
tracks, _ = _run(orch.search('q'))
|
|
|
|
assert orch.engine.calls == [('fallback', ('soulseek', 'hifi'))]
|
|
assert tracks == ['fb']
|
|
|
|
|
|
def test_best_quality_mode_uses_search_all_sources(monkeypatch):
|
|
monkeypatch.setattr(orch_mod, 'load_search_mode', lambda: 'best_quality')
|
|
orch = _hybrid_orch()
|
|
|
|
tracks, _ = _run(orch.search('q'))
|
|
|
|
assert orch.engine.calls == [('all', ('soulseek', 'hifi'))]
|
|
assert tracks == ['pool']
|