Begins the standardization of the personalized-playlist subsystem.
Pre-existing state was a patchwork: Group A (Fresh Tape / Archives /
Seasonal Mix) lived in `discovery_curated_playlists` and
`curated_seasonal_playlists` with inconsistent shapes; Group B
(Hidden Gems / Discovery Shuffle / Time Machine / Popular Picks /
Genre / Daily Mixes) was computed on-demand by
`PersonalizedPlaylistsService` with no persistence -- every call
reran the generator with `ORDER BY RANDOM()` so results rotated.
Post-overhaul (this PR) every personalized playlist lands in one
unified storage layer with stable identity, persistent track lists,
explicit refresh, and per-playlist user-tweakable config.
Foundation in this commit (no behavior change yet):
- `database/personalized_schema.py`: 3 tables created idempotently
at app startup (wired into `MusicDatabase._initialize_database`).
- `personalized_playlists`: one row per (profile, kind, variant)
with config_json, track_count, last_generated_at,
last_synced_at, last_generation_source, last_generation_error.
Variant '' (empty string) for singletons; non-empty for
time_machine / seasonal_mix / genre_playlist / daily_mix.
- `personalized_playlist_tracks`: current snapshot per playlist.
Atomically replaced on refresh.
- `personalized_track_history`: append-only log powering the
`exclude_recent_days` config knob.
- `core/personalized/types.py`: `Track`, `PlaylistConfig`,
`PlaylistRecord` dataclasses. `PlaylistConfig.merged()` for
partial-update PATCH semantics; `Track.from_dict()` accepts the
legacy generator output shape unchanged.
- `core/personalized/specs.py`: `PlaylistKindSpec` (kind,
name_template, default_config, generator, variant_resolver) and a
module-level registry. Generators register at import time;
manager dispatches by kind.
- `core/personalized/manager.py`: `PersonalizedPlaylistManager` --
the only thing that touches the new tables. Owns:
- ensure_playlist (auto-create row from kind defaults)
- get_playlist / list_playlists
- refresh_playlist (atomic snapshot replace; generator exception
preserves previous good snapshot + records error on row)
- get_playlist_tracks
- update_config (deep-merge with stored config, including extra dict)
- recent_track_ids (staleness lookup for generators)
35 boundary tests in `tests/test_personalized_manager.py` pin every
shape: config round-trip / merge semantics / extra deep-merge /
defaults; Track.from_dict tolerance + primary_id fallback chain;
registry dedup / display_name with+without variant; manager
ensure_playlist auto-create + idempotency, variant separation,
required-variant enforcement, unknown-kind error; refresh persists
+ replaces atomically + survives generator exception with previous
snapshot intact + records source from first track + round-trips
nested track_data_json; update_config patch semantics; list_playlists
profile scoping; staleness history scoped to (profile, kind, days).
3304 tests pass total. Generators ship in subsequent commits on this
branch -- each kind migrated one at a time with its own per-kind
boundary tests.
24 lines
1.1 KiB
Python
24 lines
1.1 KiB
Python
"""Standardized personalized-playlist subsystem.
|
|
|
|
Replaces the patchwork of `PersonalizedPlaylistsService` (computed-on-
|
|
demand views, no persistence) + `discovery_curated_playlists` (ID-only
|
|
storage) + `curated_seasonal_playlists` (full storage) with a single
|
|
unified abstraction:
|
|
|
|
- ``manager.PersonalizedPlaylistManager`` — owns the storage layer +
|
|
generator dispatch + refresh lifecycle.
|
|
- ``specs.PlaylistKindSpec`` — one spec per playlist KIND
|
|
(``hidden_gems``, ``time_machine``, ``seasonal_mix``, etc.) with
|
|
generator function, default config, variant resolver, and display-
|
|
name template.
|
|
- ``types.Track`` / ``types.PlaylistConfig`` — shared dataclasses.
|
|
|
|
The legacy ``PersonalizedPlaylistsService`` keeps its existing
|
|
generator implementations — they're called BY the manager rather than
|
|
duplicated. This means:
|
|
- The improved diversity logic / popularity thresholds / blacklist
|
|
filtering all stays.
|
|
- New behavior layered on top: persistence, refresh-on-demand,
|
|
per-playlist user-tweakable config, staleness windows, listening-
|
|
history cross-reference, seeded randomization.
|
|
"""
|