soulsync/core/personalized
Broque Thomas cc0828e9ff Personalized playlists (4/N): staleness post-filter (exclude_recent_days)
Adds the first quality feature on top of the manager: when
`config.exclude_recent_days > 0`, the manager drops any track from
the generator's output whose primary id was served by this kind
for this profile in the last N days.

Lives at the manager layer, not in each generator, so:
- generators stay focused on selection logic
- staleness behavior stays uniform across every kind
- enabling/disabling per playlist is just a config patch

Implementation:
- New `PersonalizedPlaylistManager._apply_quality_filters` runs after
  generator returns, before `_persist_snapshot`.
- Reads recent ids via existing `recent_track_ids` accessor.
- Tracks without a primary id pass through unchanged (nothing to
  dedupe on -- happens for sourceless tracks during edge cases).
- Returns a new list (never mutates input).

Default `exclude_recent_days = 0` preserves pre-overhaul behavior.
Per-playlist override via `PUT /api/personalized/playlist/<kind>/config`
with `{"exclude_recent_days": N}`. Recommended values:
- Discovery Shuffle: 1-3 days (high churn desired)
- Hidden Gems: 7-14 days (avoid same gems weekly)
- Time Machine / Genre: 30+ days (slow rotation OK, stable view preferred)

4 new boundary tests:
- Zero days = no filter (default behavior preserved)
- Positive days drops tracks served in window
- Filter preserves new tracks alongside dropped ones
- Tracks without primary id pass through unchanged

3369 tests pass total.

Note: listening-history cross-ref + seeded shuffle are deferred to
a future PR. Each requires deeper integration -- listening history
needs a play-events table the discovery pool can query against;
seeded shuffle needs the legacy generators to accept a seed param
without breaking their existing diversity / popularity logic.
2026-05-15 17:18:32 -07:00
..
generators Personalized playlists (2/N): all 8 generators wired through manager 2026-05-15 17:02:08 -07:00
__init__.py Personalized playlists (1/N): unified storage + manager foundation 2026-05-15 16:19:01 -07:00
api.py Personalized playlists (3/N): standardized API endpoints 2026-05-15 17:15:25 -07:00
manager.py Personalized playlists (4/N): staleness post-filter (exclude_recent_days) 2026-05-15 17:18:32 -07:00
specs.py Personalized playlists (1/N): unified storage + manager foundation 2026-05-15 16:19:01 -07:00
types.py Personalized playlists (1/N): unified storage + manager foundation 2026-05-15 16:19:01 -07:00