soulsync/database
Broque Thomas 79224ed294 Personalized playlists (1/N): unified storage + manager foundation
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.
2026-05-15 16:19:01 -07:00
..
__init__.py basic db structure 2025-08-05 22:43:04 -07:00
music_database.py Personalized playlists (1/N): unified storage + manager foundation 2026-05-15 16:19:01 -07:00
personalized_schema.py Personalized playlists (1/N): unified storage + manager foundation 2026-05-15 16:19:01 -07:00