Three fixes from on-device testing of best-quality mode: 1. clear_completed_local no longer prunes terminal tasks that belong to a STILL-ACTIVE batch (one with non-terminal work remaining). The 5-min "Clean Completed Downloads" automation was yanking completed/failed/ unverified rows out of download_tasks mid-run — and failed/cancelled aren't in library_history — so they only reappeared after the batch ended. Now the whole active batch stays intact until it finishes. 2. search_all_sources runs every source CONCURRENTLY (asyncio.gather) instead of sequentially, so the pool waits only for the slowest source (e.g. usenet/Prowlarr) in parallel rather than summing all latencies. 3. The pool log now reports per-source contribution counts (e.g. "usenet=0, hifi=11, soulseek=1") instead of just echoing the chain, so a release-level source that returns nothing for a track-title query is visible rather than appearing to have been searched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
113 lines
3.3 KiB
Python
113 lines
3.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_searches_run_concurrently_and_wait_for_slow_source():
|
|
import time
|
|
|
|
class _SlowPlugin:
|
|
def __init__(self, name, delay):
|
|
self._name = name
|
|
self._delay = delay
|
|
self.searched = False
|
|
|
|
def is_configured(self):
|
|
return True
|
|
|
|
async def search(self, query, timeout=None, progress_callback=None):
|
|
self.searched = True
|
|
await asyncio.sleep(self._delay)
|
|
return ([_Cand(self._name)], [])
|
|
|
|
slow = _SlowPlugin('usenet', 0.20) # a slow release-level source
|
|
fast = _SlowPlugin('hifi', 0.05)
|
|
eng = _engine_with({'usenet': slow, 'hifi': fast})
|
|
|
|
start = time.monotonic()
|
|
tracks, _ = asyncio.run(eng.search_all_sources('q', ['usenet', 'hifi']))
|
|
elapsed = time.monotonic() - start
|
|
|
|
# Both included — the pool waits for the slow source.
|
|
assert {t.name for t in tracks} == {'usenet', 'hifi'}
|
|
# Concurrent: total ≈ max(0.20, 0.05), well under the sequential sum 0.25.
|
|
assert elapsed < 0.20 + 0.04
|
|
|
|
|
|
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 == []
|