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.
This commit is contained in:
parent
93743119d9
commit
a7ca7ddfad
3 changed files with 81 additions and 20 deletions
|
|
@ -179,6 +179,8 @@ def try_dispatch(
|
||||||
'phase': 'analysis',
|
'phase': 'analysis',
|
||||||
'album_bundle_state': 'fallback',
|
'album_bundle_state': 'fallback',
|
||||||
'album_bundle_error': err,
|
'album_bundle_error': err,
|
||||||
|
'album_bundle_private_staging': False,
|
||||||
|
'album_bundle_staging_path': None,
|
||||||
})
|
})
|
||||||
return False
|
return False
|
||||||
logger.error("[Album Bundle] %s flow failed for '%s': %s",
|
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:
|
if force_download_all:
|
||||||
logger.warning(f"[Force Download] Force download mode enabled for batch {batch_id} - treating all tracks as missing")
|
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
|
# Allow duplicate tracks across albums — when enabled, only skip tracks already
|
||||||
# owned in THIS album, not tracks owned in other albums
|
# owned in THIS album, not tracks owned in other albums
|
||||||
allow_duplicates = deps.config_manager.get('wishlist.allow_duplicate_tracks', True)
|
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_folder_mode = batch.get('playlist_folder_mode', False)
|
||||||
batch_playlist_name = batch.get('playlist_name', 'Unknown Playlist')
|
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 ===
|
# === 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)
|
# Only run pre-flight when Soulseek is the download source (or hybrid with soulseek)
|
||||||
preflight_source = None
|
preflight_source = None
|
||||||
|
|
|
||||||
|
|
@ -526,6 +526,36 @@ def test_no_missing_with_auto_wishlist_submits_completion(monkeypatch):
|
||||||
assert args == ('B7',)
|
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
|
# 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'
|
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
|
# Task creation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue