soulsync/core/playlists/sources
Broque Thomas cf5da04439 Roll LB Weekly / Top series into single rolling mirrors (Phase 1c.2.1)
ListenBrainz publishes "Weekly Jams for X" / "Weekly Exploration
for X" with a fresh MBID every week, and "Top Discoveries of YYYY
for X" / "Top Missed Recordings of YYYY for X" with a fresh MBID
every year. Auto-mirroring those per-period yielded one mirrored-
playlist row per week/year — useless for Auto-Sync schedules
because the underlying LB playlist never updates, only a brand new
playlist replaces it. The user accumulates 100+ dead Weekly Jams
rows per year if they discover regularly.

This commit collapses each family into a single ROLLING mirror
keyed by a synthetic ``source_playlist_id`` (e.g.
``lb_weekly_jams_Nezreka``). Each new period UPSERTs into the same
row, so the user gets one stable Auto-Sync schedule per series
that automatically picks up the latest period's tracks on every
refresh. Non-series LB playlists (user-created, collaborative,
Last.fm radios for a specific seed) continue to mirror under
their per-playlist MBID as before. Per-period LB playlists are
still visible + usable on the LB Sync tab — only the mirror layer
collapses.

- ``core/playlists/lb_series.py`` (new) — series-detect helper
  with regex patterns + canonical-name + LIKE-pattern template
  for each known LB family. Exposes
  ``detect_series(title)``, ``is_series_synthetic_id(id)``, and
  ``list_series_synthetic_ids()`` so both the JS auto-mirror hook
  and the LB adapter can speak the same language.
- ``GET /api/listenbrainz/series-detect?title=...`` — thin HTTP
  shim around ``detect_series`` so the auto-mirror JS doesn't
  duplicate the regex.
- ``ListenBrainzPlaylistSource.get_playlist`` now recognizes
  synthetic series ids — it queries the LB cache for the newest
  cache row whose title matches the series' LIKE pattern and
  resolves to that row's MBID before fetching tracks. The mirror's
  meta keeps the synthetic id so refreshes always re-resolve to
  the latest period.
- ``_mirrorListenBrainzAfterDiscovery`` (sync-services.js) calls
  the new detect endpoint when discovery completes — if a match
  comes back it swaps the per-period MBID for the synthetic id +
  the canonical name. Existing Last.fm radio routing logic stays
  intact (Last.fm radios aren't a series).
- ``ListenBrainzManager._cleanup_per_period_series_mirrors`` —
  one-shot consolidation sweeper runs in ``_cleanup_old_playlists``
  + deletes any legacy per-period mirror rows so the consolidated
  rolling mirror is the only one left. Idempotent — only matches
  per-period titles ("Weekly Jams for ..., week of ...") and never
  the canonical rolling-mirror titles ("ListenBrainz Weekly
  Jams").
- 11 new tests pin the detector + synthetic-id helpers; 236 total
  across adapter + automation + lb-series suites green.
2026-05-26 15:49:49 -07:00
..
__init__.py Refactor refresh_mirrored to use unified PlaylistSource registry 2026-05-26 12:52:39 -07:00
base.py Fold provider-matching into PlaylistSource contract (Phase 1b) 2026-05-26 13:07:01 -07:00
bootstrap.py Fold provider-matching into PlaylistSource contract (Phase 1b) 2026-05-26 13:07:01 -07:00
deezer.py Refactor refresh_mirrored to use unified PlaylistSource registry 2026-05-26 12:52:39 -07:00
itunes_link.py Refactor refresh_mirrored to use unified PlaylistSource registry 2026-05-26 12:52:39 -07:00
lastfm.py Fold provider-matching into PlaylistSource contract (Phase 1b) 2026-05-26 13:07:01 -07:00
listenbrainz.py Roll LB Weekly / Top series into single rolling mirrors (Phase 1c.2.1) 2026-05-26 15:49:49 -07:00
qobuz.py Refactor refresh_mirrored to use unified PlaylistSource registry 2026-05-26 12:52:39 -07:00
registry.py Add unified PlaylistSource adapter layer (Phase 0) 2026-05-26 12:22:09 -07:00
soulsync_discovery.py Add unified PlaylistSource adapter layer (Phase 0) 2026-05-26 12:22:09 -07:00
spotify.py Refactor refresh_mirrored to use unified PlaylistSource registry 2026-05-26 12:52:39 -07:00
spotify_public.py Refactor refresh_mirrored to use unified PlaylistSource registry 2026-05-26 12:52:39 -07:00
tidal.py Refactor refresh_mirrored to use unified PlaylistSource registry 2026-05-26 12:52:39 -07:00
youtube.py Refactor refresh_mirrored to use unified PlaylistSource registry 2026-05-26 12:52:39 -07:00