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.
pull/529/merge
Broque Thomas 2 days ago
parent 93743119d9
commit a7ca7ddfad

@ -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",

@ -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

@ -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
# ---------------------------------------------------------------------------

Loading…
Cancel
Save