feat(playlists): #415 stage 1 — rotation-state schema + play ingest
First of three stages for system-playlist sample-history dedup (Fable #415, server-side rotation per the operator's choice). Schema (migration 0027): - play_events.source (nullable text): which surface a play came from. 'for_you' / 'discover' feed rotation; NULL for library / user-playlist / radio / Subsonic. - system_playlist_rotation_state(user_id, playlist_kind, played_track_ids uuid[], rotation_started_at, updated_at): the per-(user,kind) set of already-heard tracks this rotation. Ingest: - New RecordPlayStartedWithSource on the writer; RecordPlayStarted is now a thin source="" wrapper so the frozen Subsonic shim is untouched (no signature ripple). - When source is a known system kind, the same txn appends the track to rotation state (AppendRotationPlayed keeps the array a set via the conflict CASE). - /api/events play_started accepts an optional "source". No serve-behavior change yet — Stage 2 makes shuffle prefer the unplayed tail + resets on exhaustion; Stage 3 wires the clients to send source and consume the rotation-aware order. Tests: rotation appends + dedupes for a system source; source-less play writes no rotation row. (Existing RecordPlayStarted tests are unchanged — same wrapper signature, identical behavior at source="".) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
-- 0027_system_playlist_rotation.down.sql
|
||||
DROP TABLE IF EXISTS system_playlist_rotation_state;
|
||||
|
||||
ALTER TABLE play_events
|
||||
DROP COLUMN IF EXISTS source;
|
||||
@@ -0,0 +1,26 @@
|
||||
-- 0027_system_playlist_rotation.up.sql — sample-history dedup for
|
||||
-- system playlists (Fable #415). Two parts:
|
||||
--
|
||||
-- 1. play_events.source: which system playlist a play originated
|
||||
-- from ('for_you' | 'discover' | future kinds), or NULL for any
|
||||
-- play not launched from a system-playlist surface (library,
|
||||
-- user playlist, radio, Subsonic). Lets the ingest path know a
|
||||
-- play should count against that playlist's rotation.
|
||||
--
|
||||
-- 2. system_playlist_rotation_state: per (user, playlist_kind) set
|
||||
-- of track ids already played in the current rotation window.
|
||||
-- Shuffle-on-play prefers the unplayed tail; when the snapshot
|
||||
-- is exhausted the set resets and rotation_started_at advances.
|
||||
-- Centralized server-side so phone / web / watch share one
|
||||
-- rotation, mirroring how radio dedup already lives server-side.
|
||||
ALTER TABLE play_events
|
||||
ADD COLUMN source text;
|
||||
|
||||
CREATE TABLE system_playlist_rotation_state (
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
playlist_kind text NOT NULL,
|
||||
played_track_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
rotation_started_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, playlist_kind)
|
||||
);
|
||||
Reference in New Issue
Block a user