From c6e077fefec07954fec668dcb5a88bd8788ad498 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 12 Jun 2026 17:19:45 -0700 Subject: [PATCH] Playlists: batch toggle force-rebuilds its own folder (row flag = provenance only) + resolve diagnostics --- core/playlists/materialize_service.py | 31 ++++++++++++++++------ tests/test_playlist_materialize_service.py | 15 +++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/core/playlists/materialize_service.py b/core/playlists/materialize_service.py index 02219858..c8121b86 100644 --- a/core/playlists/materialize_service.py +++ b/core/playlists/materialize_service.py @@ -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 diff --git a/tests/test_playlist_materialize_service.py b/tests/test_playlist_materialize_service.py index 4280d71b..fbb3332e 100644 --- a/tests/test_playlist_materialize_service.py +++ b/tests/test_playlist_materialize_service.py @@ -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