Playlists: batch toggle force-rebuilds its own folder (row flag = provenance only) + resolve diagnostics

This commit is contained in:
BoulderBadgeDad 2026-06-12 17:19:45 -07:00
parent 4d7267e906
commit c6e077fefe
2 changed files with 38 additions and 8 deletions

View file

@ -28,6 +28,9 @@ from core.playlists.materialize import (
normalize_mode,
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]:
@ -90,20 +93,24 @@ def reconcile_batch_playlists(db, batch: dict, download_tasks: dict, config_mana
import json as _json
# 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()
def _want(ref, source):
def _want(ref, source, force):
ref = str(ref or "").strip()
if not ref:
return
key = (ref, source or "spotify")
if key not in seen_ref:
seen_ref.add(key)
wanted.append(key)
wanted.append((ref, source or "spotify", force))
if batch.get("playlist_folder_mode"):
_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 []):
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:
si = {}
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()
for ref, source in wanted:
for ref, source, force in wanted:
try:
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
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
seen_id.add(pl.get("id"))
try:
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
return results

View file

@ -164,6 +164,21 @@ def test_reconcile_organize_batch_rebuilds_from_library(tmp_path: Path):
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):
"""The wishlist gap: a wishlist batch (not organize) completes a track whose
provenance points to an organize playlist that playlist gets rebuilt from the