Fix: torrent/usenet album bundle hard-fails on 'no results' instead of falling back
A torrent-first (or usenet-first) hybrid download would freeze at
"Torrent searching for release 0%" and never move to the next source when
Prowlarr returned no results for the album. Reported by Cezar:
[Album Bundle] torrent flow failed for '...': No torrent results found
Cause: the album-bundle dispatch only returns to the per-track flow (which,
in hybrid mode, tries the next configured source) when the plugin's failure
outcome carries fallback=True; otherwise it marks the batch failed and stops.
Both plugins set fallback=True on their 'results found but none matched the
album' branch, but the adjacent 'no results at all' branch set only an error
and no fallback flag -- so zero results hard-failed while wrong results fell
back. Backwards, and soulseek's plugin already defaults fallback=True for
exactly this reason.
Fix: set fallback=True on the no-results branch in torrent.py and usenet.py.
The dispatch's fallback handling (return False -> per-track flow) was already
correct and is unchanged.
The only consumer of download_album_to_staging is the dispatch, which reads
the result via .get('fallback'), so the change is additive and locally
contained.
Tests: new test_torrent_album_to_staging_no_results_flags_fallback and
test_usenet_album_to_staging_no_results_flags_fallback assert the plugins now
emit fallback=True on an empty search; the existing torrent no-results test is
extended with the same assertion. Existing dispatch tests already pin
fallback=True -> per-track flow. Full downloads/plugins/adapters sweep: 690
passed.
This commit is contained in:
parent
6e7948b642
commit
95d6ad4bc9
3 changed files with 48 additions and 0 deletions
|
|
@ -494,7 +494,14 @@ class TorrentDownloadPlugin(DownloadSourcePlugin):
|
|||
candidates = [r for r in search_results
|
||||
if r.protocol == 'torrent' and (r.magnet_uri or r.download_url)]
|
||||
if not candidates:
|
||||
# Album isn't available on this source. Mark the failure as
|
||||
# fallback-eligible so the dispatch returns to the per-track flow
|
||||
# instead of hard-failing the batch — in hybrid mode that lets the
|
||||
# next configured source take over. Without this flag a torrent-first
|
||||
# hybrid would get stuck at "searching" forever when Prowlarr
|
||||
# returns nothing, never trying the other sources.
|
||||
result['error'] = f'No torrent results found for "{query}"'
|
||||
result['fallback'] = True
|
||||
return result
|
||||
|
||||
picked = pick_best_album_release(
|
||||
|
|
|
|||
|
|
@ -465,7 +465,12 @@ class UsenetDownloadPlugin(DownloadSourcePlugin):
|
|||
candidates = [r for r in search_results
|
||||
if r.protocol == 'usenet' and r.download_url]
|
||||
if not candidates:
|
||||
# Album isn't available on this source — fall back to the per-track
|
||||
# flow (next configured source in hybrid mode) rather than hard-
|
||||
# failing the whole batch. Mirrors the torrent plugin + soulseek's
|
||||
# default fallback contract.
|
||||
result['error'] = f'No usenet results found for "{query}"'
|
||||
result['fallback'] = True
|
||||
return result
|
||||
|
||||
picked = pick_best_album_release(
|
||||
|
|
|
|||
|
|
@ -589,9 +589,45 @@ def test_torrent_album_to_staging_ignores_candidates_without_download_url(tmp_pa
|
|||
|
||||
assert outcome['success'] is False
|
||||
assert 'No torrent results' in outcome['error']
|
||||
# Regression (Cezar): "no results" must be fallback-eligible so a
|
||||
# torrent-first hybrid returns to the per-track flow (next source)
|
||||
# instead of the dispatch marking the batch failed and freezing at
|
||||
# "Torrent searching for release 0%".
|
||||
assert outcome.get('fallback') is True
|
||||
fake_adapter.add_torrent.assert_not_called()
|
||||
|
||||
|
||||
def test_torrent_album_to_staging_no_results_flags_fallback(tmp_path: Path) -> None:
|
||||
"""Empty Prowlarr search → fallback-eligible failure, not terminal."""
|
||||
plugin = TorrentDownloadPlugin()
|
||||
fake_adapter = MagicMock()
|
||||
fake_adapter.is_configured.return_value = True
|
||||
with patch.object(plugin, 'is_configured', return_value=True), \
|
||||
patch.object(plugin._prowlarr, 'search', new=AsyncMock(return_value=[])), \
|
||||
patch('core.download_plugins.torrent.get_active_torrent_adapter', return_value=fake_adapter):
|
||||
outcome = plugin.download_album_to_staging('GNX', 'Kendrick Lamar', str(tmp_path))
|
||||
assert outcome['success'] is False
|
||||
assert 'No torrent results' in outcome['error']
|
||||
assert outcome.get('fallback') is True
|
||||
fake_adapter.add_torrent.assert_not_called()
|
||||
|
||||
|
||||
def test_usenet_album_to_staging_no_results_flags_fallback(tmp_path: Path) -> None:
|
||||
"""Same contract for usenet: an empty search must fall back to the
|
||||
per-track flow rather than hard-failing the album batch."""
|
||||
plugin = UsenetDownloadPlugin()
|
||||
fake_adapter = MagicMock()
|
||||
fake_adapter.is_configured.return_value = True
|
||||
with patch.object(plugin, 'is_configured', return_value=True), \
|
||||
patch.object(plugin._prowlarr, 'search', new=AsyncMock(return_value=[])), \
|
||||
patch('core.download_plugins.usenet.get_active_usenet_adapter', return_value=fake_adapter):
|
||||
outcome = plugin.download_album_to_staging('GNX', 'Kendrick Lamar', str(tmp_path))
|
||||
assert outcome['success'] is False
|
||||
assert 'No usenet results' in outcome['error']
|
||||
assert outcome.get('fallback') is True
|
||||
fake_adapter.add_nzb.assert_not_called()
|
||||
|
||||
|
||||
def test_registry_includes_torrent_and_usenet() -> None:
|
||||
"""The registry decides what shows up in the orchestrator's
|
||||
iteration helpers. If we forget to register a new plugin the
|
||||
|
|
|
|||
Loading…
Reference in a new issue