Playlists: batch toggle force-rebuilds its own folder (row flag = provenance only) + resolve diagnostics
This commit is contained in:
parent
4d7267e906
commit
c6e077fefe
2 changed files with 38 additions and 8 deletions
|
|
@ -28,6 +28,9 @@ from core.playlists.materialize import (
|
||||||
normalize_mode,
|
normalize_mode,
|
||||||
rebuild_playlist_folder,
|
rebuild_playlist_folder,
|
||||||
)
|
)
|
||||||
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("playlists.materialize")
|
||||||
|
|
||||||
|
|
||||||
def collect_batch_real_paths(batch: dict, download_tasks: dict, *, config_manager) -> List[str]:
|
def collect_batch_real_paths(batch: dict, download_tasks: dict, *, config_manager) -> List[str]:
|
||||||
|
|
@ -90,20 +93,24 @@ def reconcile_batch_playlists(db, batch: dict, download_tasks: dict, config_mana
|
||||||
import json as _json
|
import json as _json
|
||||||
|
|
||||||
# Collect the (ref, source) of every organize playlist this batch touched.
|
# Collect the (ref, source) of every organize playlist this batch touched.
|
||||||
|
# (ref, source, force). force=True for the batch's OWN playlist: the per-download
|
||||||
|
# "organize by playlist" toggle (batch playlist_folder_mode) is the user's
|
||||||
|
# intent, so it rebuilds regardless of the saved row preference. Provenance
|
||||||
|
# playlists (force=False) require the saved organize_by_playlist flag.
|
||||||
wanted, seen_ref = [], set()
|
wanted, seen_ref = [], set()
|
||||||
|
|
||||||
def _want(ref, source):
|
def _want(ref, source, force):
|
||||||
ref = str(ref or "").strip()
|
ref = str(ref or "").strip()
|
||||||
if not ref:
|
if not ref:
|
||||||
return
|
return
|
||||||
key = (ref, source or "spotify")
|
key = (ref, source or "spotify")
|
||||||
if key not in seen_ref:
|
if key not in seen_ref:
|
||||||
seen_ref.add(key)
|
seen_ref.add(key)
|
||||||
wanted.append(key)
|
wanted.append((ref, source or "spotify", force))
|
||||||
|
|
||||||
if batch.get("playlist_folder_mode"):
|
if batch.get("playlist_folder_mode"):
|
||||||
_want(batch.get("source_playlist_ref") or batch.get("playlist_id"),
|
_want(batch.get("source_playlist_ref") or batch.get("playlist_id"),
|
||||||
batch.get("batch_source") or "spotify")
|
batch.get("batch_source") or "spotify", True)
|
||||||
|
|
||||||
for tid in (batch.get("queue") or []):
|
for tid in (batch.get("queue") or []):
|
||||||
task = (download_tasks or {}).get(tid) or {}
|
task = (download_tasks or {}).get(tid) or {}
|
||||||
|
|
@ -116,20 +123,28 @@ def reconcile_batch_playlists(db, batch: dict, download_tasks: dict, config_mana
|
||||||
except Exception:
|
except Exception:
|
||||||
si = {}
|
si = {}
|
||||||
if isinstance(si, dict) and si.get("playlist_id"):
|
if isinstance(si, dict) and si.get("playlist_id"):
|
||||||
_want(si["playlist_id"], si.get("source") or "spotify")
|
_want(si["playlist_id"], si.get("source") or "spotify", False)
|
||||||
|
|
||||||
results, seen_id = [], set()
|
results, seen_id = [], set()
|
||||||
for ref, source in wanted:
|
for ref, source, force in wanted:
|
||||||
try:
|
try:
|
||||||
pl = db.resolve_mirrored_playlist(ref, profile_id, default_source=source)
|
pl = db.resolve_mirrored_playlist(ref, profile_id, default_source=source)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.debug("[Playlist Folder] resolve failed for ref=%s source=%s: %s", ref, source, e)
|
||||||
pl = None
|
pl = None
|
||||||
if not pl or not pl.get("organize_by_playlist") or pl.get("id") in seen_id:
|
if not pl:
|
||||||
|
logger.info("[Playlist Folder] no mirrored playlist resolved for ref=%s source=%s "
|
||||||
|
"(force=%s) — folder not built", ref, source, force)
|
||||||
|
continue
|
||||||
|
if pl.get("id") in seen_id:
|
||||||
|
continue
|
||||||
|
if not force and not pl.get("organize_by_playlist"):
|
||||||
continue
|
continue
|
||||||
seen_id.add(pl.get("id"))
|
seen_id.add(pl.get("id"))
|
||||||
try:
|
try:
|
||||||
results.append(_rebuild_one_from_db(db, config_manager, pl))
|
results.append(_rebuild_one_from_db(db, config_manager, pl))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error("[Playlist Folder] rebuild failed for '%s': %s", pl.get("name"), e)
|
||||||
continue
|
continue
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,21 @@ def test_reconcile_organize_batch_rebuilds_from_library(tmp_path: Path):
|
||||||
assert (tmp_path / "Playlists" / "Mix" / "A.mp3").exists()
|
assert (tmp_path / "Playlists" / "Mix" / "A.mp3").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_reconcile_batch_playlist_rebuilds_even_if_row_flag_off(tmp_path: Path):
|
||||||
|
"""The batch's OWN playlist rebuilds because the per-download toggle (batch
|
||||||
|
playlist_folder_mode) is the intent — even when the saved organize_by_playlist
|
||||||
|
row flag is off (the common case: user flips the download-modal toggle, never
|
||||||
|
the saved preference). 'Off' (PL2) has organize_by_playlist=False on the row."""
|
||||||
|
b = _mk(tmp_path, "B.mp3")[0] # Off's membership = B
|
||||||
|
db = _RebuildDB({"B": b})
|
||||||
|
batch = {"playlist_folder_mode": True, "playlist_name": "Off",
|
||||||
|
"source_playlist_ref": "PL2", "batch_source": "spotify", "queue": []}
|
||||||
|
cfg = _Cfg(str(tmp_path / "Playlists"))
|
||||||
|
results = reconcile_batch_playlists(db, batch, {}, cfg)
|
||||||
|
assert len(results) == 1 and results[0][0] == "Off"
|
||||||
|
assert (tmp_path / "Playlists" / "Off" / "B.mp3").exists()
|
||||||
|
|
||||||
|
|
||||||
def test_reconcile_wishlist_track_rebuilds_its_playlist(tmp_path: Path):
|
def test_reconcile_wishlist_track_rebuilds_its_playlist(tmp_path: Path):
|
||||||
"""The wishlist gap: a wishlist batch (not organize) completes a track whose
|
"""The wishlist gap: a wishlist batch (not organize) completes a track whose
|
||||||
provenance points to an organize playlist → that playlist gets rebuilt from the
|
provenance points to an organize playlist → that playlist gets rebuilt from the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue