Playlists: wire materialize triggers + retire per-track routing flag
- Routing (step 5): organize-by-playlist tracks no longer set the per-track _playlist_folder_mode flag, so they import NORMALLY into Artist/Album — exactly what a normal download does. _playlist_name provenance is kept (origin.py). - Triggers (step 4): build the playlist folder from the batch's own payload at both end-of-flow points — the all-owned path in master.py (no downloads, so the lifecycle never runs) and the batch-complete hook in lifecycle.py (after downloads). Both gated on playlist_folder_mode, both non-fatal. Works for the all-owned case (the smack test that did nothing before) and for mixed owned/downloaded, with no source-ID or mirrored-playlist dependency. The materialized folder uses the default ./Playlists root + symlink mode until the Settings UI is added. Updated the master test to assert the new contract (provenance kept, routing flag gone). 979 tests pass.
This commit is contained in:
parent
bef73d855d
commit
aa5d747327
3 changed files with 54 additions and 7 deletions
|
|
@ -549,6 +549,24 @@ def on_download_completed(batch_id: str, task_id: str, success: bool, deps: Life
|
|||
except Exception as m3u_err:
|
||||
logger.error(f"[M3U] Error regenerating M3U on batch complete: {m3u_err}")
|
||||
|
||||
# PLAYLIST MATERIALIZE: if this was an organize-by-playlist batch,
|
||||
# (re)build the playlist's folder of links/copies now that every
|
||||
# track is imported. Built from the batch's own payload (owned +
|
||||
# downloaded real paths) — non-fatal, derived view. (materialize.py)
|
||||
if batch.get('playlist_folder_mode'):
|
||||
try:
|
||||
from core.playlists.materialize_service import materialize_playlist_from_batch
|
||||
_mat = materialize_playlist_from_batch(batch, download_tasks, deps.config_manager)
|
||||
if _mat:
|
||||
logger.info(
|
||||
f"[Playlist Folder] Rebuilt '{_mat.playlist_dir}': "
|
||||
f"{_mat.linked} linked, {_mat.copied} copied, "
|
||||
f"{_mat.unchanged} unchanged, {_mat.removed_stale} stale removed"
|
||||
+ (" (symlinks unsupported here → copied)" if _mat.fellback else "")
|
||||
)
|
||||
except Exception as _mat_err:
|
||||
logger.error(f"[Playlist Folder] Materialize failed (non-fatal): {_mat_err}")
|
||||
|
||||
# REPAIR: Scan all album folders from this batch for track number issues
|
||||
if deps.repair_worker:
|
||||
deps.repair_worker.process_batch(batch_id)
|
||||
|
|
|
|||
|
|
@ -785,6 +785,25 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma
|
|||
logger.warning("[Auto-Wishlist] No missing tracks found - calling auto-completion handler to toggle cycle and reschedule")
|
||||
deps.missing_download_executor.submit(deps.process_failed_tracks_to_wishlist_exact_with_auto_completion, batch_id)
|
||||
|
||||
# Organize-by-playlist with NOTHING to download (every track already
|
||||
# owned): the batch never enters the download/lifecycle path, so build
|
||||
# the playlist folder here from the owned files the analysis matched.
|
||||
# Gated + non-fatal; runs once after analysis, not in the per-track loop.
|
||||
if effective_playlist_folder_mode:
|
||||
try:
|
||||
from core.playlists.materialize_service import materialize_playlist_from_batch
|
||||
_batch = download_batches.get(batch_id)
|
||||
if _batch is not None:
|
||||
_mat = materialize_playlist_from_batch(_batch, download_tasks, deps.config_manager)
|
||||
if _mat:
|
||||
logger.info(
|
||||
f"[Playlist Folder] Rebuilt '{_mat.playlist_dir}' (all owned): "
|
||||
f"{_mat.linked} linked, {_mat.copied} copied, {_mat.removed_stale} stale removed"
|
||||
+ (" (symlinks unsupported here → copied)" if _mat.fellback else "")
|
||||
)
|
||||
except Exception as _mat_err:
|
||||
logger.error(f"[Playlist Folder] All-owned materialize failed (non-fatal): {_mat_err}")
|
||||
|
||||
return
|
||||
|
||||
logger.warning(f" transitioning batch {batch_id} to download phase with {len(missing_tracks)} tracks.")
|
||||
|
|
@ -1163,7 +1182,13 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma
|
|||
task_pl_folder_mode = True
|
||||
task_pl_name = wl_pl_name or wl_mirrored.get('name') or batch_playlist_name
|
||||
if task_pl_folder_mode:
|
||||
track_info['_playlist_folder_mode'] = True
|
||||
# Organize-by-playlist now imports each track NORMALLY into the
|
||||
# Artist/Album library (i.e. exactly what a normal download does)
|
||||
# — the playlist folder is built as links/copies AFTER the batch
|
||||
# from the real library files. So we deliberately DON'T set
|
||||
# `_playlist_folder_mode` (which routed the real file into a flat
|
||||
# Music/<playlist>/ dump). We keep `_playlist_name` + source_info
|
||||
# — they're download provenance (core/downloads/origin.py).
|
||||
track_info['_playlist_name'] = task_pl_name
|
||||
if batch_source_playlist_ref:
|
||||
track_info['source_info'] = {
|
||||
|
|
@ -1172,8 +1197,8 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma
|
|||
'source': batch_source,
|
||||
}
|
||||
logger.info(
|
||||
f"[Task Creation] Added playlist folder mode for: "
|
||||
f"{track_info.get('name')} → {task_pl_name}"
|
||||
f"[Task Creation] Organize-by-playlist (normal import + "
|
||||
f"materialize after batch): {track_info.get('name')} → {task_pl_name}"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
|
|
|
|||
|
|
@ -1048,8 +1048,12 @@ def test_wishlist_album_grouping_uses_shared_rich_album_context(monkeypatch):
|
|||
assert images == {'http://img'}
|
||||
|
||||
|
||||
def test_playlist_folder_mode_propagates(monkeypatch):
|
||||
"""Playlist folder mode flag carried through to track_info."""
|
||||
def test_organize_by_playlist_keeps_provenance_not_routing(monkeypatch):
|
||||
"""Organize-by-playlist no longer routes the file per-track. Each track imports
|
||||
NORMALLY into Artist/Album (what a normal download does); the playlist folder
|
||||
is built as links AFTER the batch. So the old routing flag
|
||||
`_playlist_folder_mode` is NOT set, but `_playlist_name` provenance IS kept
|
||||
(core/downloads/origin.py)."""
|
||||
db = _FakeDB()
|
||||
monkeypatch.setattr('database.music_database.MusicDatabase', lambda: db)
|
||||
|
||||
|
|
@ -1062,8 +1066,8 @@ def test_playlist_folder_mode_propagates(monkeypatch):
|
|||
|
||||
task_id = download_batches['B15']['queue'][0]
|
||||
info = download_tasks[task_id]['track_info']
|
||||
assert info['_playlist_folder_mode'] is True
|
||||
assert info['_playlist_name'] == 'My Mix'
|
||||
assert '_playlist_folder_mode' not in info # routing retired → normal import
|
||||
assert info['_playlist_name'] == 'My Mix' # provenance preserved
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue