From a7ca7ddfad4f48a676a7f6d89f609befdd050ab8 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 24 May 2026 16:15:36 -0700 Subject: [PATCH] Harden album bundle fallback flow Delay torrent and usenet album-bundle dispatch until missing-track analysis confirms there is work to do, matching the Soulseek album flow and avoiding release downloads for already-owned albums. Clear private album-bundle staging state when a release-level source intentionally falls back to per-track mode so workers can use the normal staging/search path instead of an empty private bundle directory. Verified by user: focused downloads master tests passed, 2 passed. --- core/downloads/album_bundle_dispatch.py | 2 + core/downloads/master.py | 39 ++++++++------- tests/downloads/test_downloads_master.py | 60 ++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 20 deletions(-) diff --git a/core/downloads/album_bundle_dispatch.py b/core/downloads/album_bundle_dispatch.py index 09ec1a46..734d35a9 100644 --- a/core/downloads/album_bundle_dispatch.py +++ b/core/downloads/album_bundle_dispatch.py @@ -179,6 +179,8 @@ def try_dispatch( 'phase': 'analysis', 'album_bundle_state': 'fallback', 'album_bundle_error': err, + 'album_bundle_private_staging': False, + 'album_bundle_staging_path': None, }) return False logger.error("[Album Bundle] %s flow failed for '%s': %s", diff --git a/core/downloads/master.py b/core/downloads/master.py index 83842f17..f80d46b6 100644 --- a/core/downloads/master.py +++ b/core/downloads/master.py @@ -356,26 +356,6 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma if force_download_all: logger.warning(f"[Force Download] Force download mode enabled for batch {batch_id} - treating all tracks as missing") - # Album-bundle gate for torrent / usenet single-source mode. - # See ``core/downloads/album_bundle_dispatch`` for the full - # narrow-gate rationale. Returns True iff the master worker - # should stop (gate fired and failed); False = engaged-and- - # succeeded OR didn't engage, both fall through to per-track. - _bundle_state = _BatchStateAccessImpl() - _album_bundle_source = _resolve_album_bundle_source(deps.config_manager) - if _album_bundle_source and _album_bundle_source != 'soulseek': - if _album_bundle_dispatch.try_dispatch( - batch_id=batch_id, - is_album=batch_is_album, - album_context=batch_album_context, - artist_context=batch_artist_context, - config_get=deps.config_manager.get, - plugin_resolver=deps.download_orchestrator.client, - state=_bundle_state, - source_override=_album_bundle_source, - ): - return - # Allow duplicate tracks across albums — when enabled, only skip tracks already # owned in THIS album, not tracks owned in other albums allow_duplicates = deps.config_manager.get('wishlist.allow_duplicate_tracks', True) @@ -671,6 +651,25 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma batch_playlist_folder_mode = batch.get('playlist_folder_mode', False) batch_playlist_name = batch.get('playlist_name', 'Unknown Playlist') + # Album-bundle sources download a whole release into private staging, + # then the normal per-track workers claim those staged files. Run this + # only after analysis has found missing tracks; otherwise an already + # owned album would still trigger a release download. + _bundle_state = _BatchStateAccessImpl() + _album_bundle_source = _resolve_album_bundle_source(deps.config_manager) + if _album_bundle_source and _album_bundle_source != 'soulseek': + if _album_bundle_dispatch.try_dispatch( + batch_id=batch_id, + is_album=batch_is_album, + album_context=batch_album_context, + artist_context=batch_artist_context, + config_get=deps.config_manager.get, + plugin_resolver=deps.download_orchestrator.client, + state=_bundle_state, + source_override=_album_bundle_source, + ): + return + # === ALBUM PRE-FLIGHT: Search for complete album folder before track-by-track === # Only run pre-flight when Soulseek is the download source (or hybrid with soulseek) preflight_source = None diff --git a/tests/downloads/test_downloads_master.py b/tests/downloads/test_downloads_master.py index a1586028..c2c5b758 100644 --- a/tests/downloads/test_downloads_master.py +++ b/tests/downloads/test_downloads_master.py @@ -526,6 +526,36 @@ def test_no_missing_with_auto_wishlist_submits_completion(monkeypatch): assert args == ('B7',) +def test_no_missing_album_does_not_dispatch_torrent_bundle(monkeypatch): + """Release-level sources must wait until analysis confirms missing tracks.""" + album = _DBAlbum(id_=42, title='Test Album') + db = _FakeDB(album=album, album_tracks=[_DBTrack('T1')]) + monkeypatch.setattr('database.music_database.MusicDatabase', lambda: db) + + plugin = _FakeAlbumBundleSoulseek() + deps = _build_deps( + config=_FakeConfig({'download_source.mode': 'torrent'}), + soulseek=_FakePluginWrapper({'torrent': plugin}), + ) + _seed_batch( + 'B7a', + is_album_download=True, + album_context={'name': 'Test Album', 'total_tracks': 1}, + artist_context={'name': 'Artist'}, + ) + + mw.run_full_missing_tracks_process( + 'B7a', + 'album:1', + [{'name': 'T1', 'artists': ['Artist'], 'track_number': 1}], + deps, + ) + + assert plugin.calls == [] + assert download_batches['B7a']['phase'] == 'complete' + assert 'album_bundle_source' not in download_batches['B7a'] + + # --------------------------------------------------------------------------- # Album fast path # --------------------------------------------------------------------------- @@ -862,6 +892,36 @@ def test_hybrid_first_torrent_uses_album_bundle_before_per_track(monkeypatch): assert download_batches['B27']['album_bundle_source'] == 'torrent' +def test_album_bundle_fallback_clears_private_staging(monkeypatch): + db = _FakeDB() + monkeypatch.setattr('database.music_database.MusicDatabase', lambda: db) + + plugin = _FakeAlbumBundleSoulseek({ + 'success': False, + 'fallback': True, + 'error': 'No release passed validation', + }) + deps = _build_deps( + config=_FakeConfig({'download_source.mode': 'torrent'}), + soulseek=_FakePluginWrapper({'torrent': plugin}), + ) + _seed_batch( + 'B29', + is_album_download=True, + album_context={'name': 'Test Album', 'total_tracks': 1}, + artist_context={'name': 'Artist'}, + ) + tracks = [{'name': 'T1', 'artists': ['Artist'], 'track_number': 1}] + + mw.run_full_missing_tracks_process('B29', 'album:1', tracks, deps) + + assert len(plugin.calls) == 1 + assert download_batches['B29']['album_bundle_state'] == 'fallback' + assert download_batches['B29']['album_bundle_private_staging'] is False + assert download_batches['B29']['album_bundle_staging_path'] is None + assert len(download_batches['B29']['queue']) == 1 + + # --------------------------------------------------------------------------- # Task creation # ---------------------------------------------------------------------------