Files
minstrel/internal/db/dbq/system_playlist_rotation.sql.go
bvandeusen d2a0b7d780 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>
2026-05-15 07:40:17 -04:00

92 lines
2.8 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: system_playlist_rotation.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const appendRotationPlayed = `-- name: AppendRotationPlayed :exec
INSERT INTO system_playlist_rotation_state (
user_id, playlist_kind, played_track_ids, rotation_started_at, updated_at
) VALUES (
$1, $2,
array_append('{}'::uuid[], $3::uuid), now(), now()
)
ON CONFLICT (user_id, playlist_kind) DO UPDATE
SET played_track_ids =
CASE WHEN $3::uuid = ANY(system_playlist_rotation_state.played_track_ids)
THEN system_playlist_rotation_state.played_track_ids
ELSE array_append(system_playlist_rotation_state.played_track_ids, $3::uuid)
END,
updated_at = now()
`
type AppendRotationPlayedParams struct {
UserID pgtype.UUID
PlaylistKind string
TrackID pgtype.UUID
}
// Rotation state for system-playlist sample-history dedup (#415).
// One row per (user, playlist_kind). played_track_ids accumulates
// tracks heard from that playlist in the current rotation; shuffle
// prefers ids NOT in this set, then resets when the snapshot is
// exhausted.
// Records that the given track was played from the (user, kind)
// system playlist. Creates the row on first play; on conflict
// appends only if not already present so the array stays a set.
func (q *Queries) AppendRotationPlayed(ctx context.Context, arg AppendRotationPlayedParams) error {
_, err := q.db.Exec(ctx, appendRotationPlayed, arg.UserID, arg.PlaylistKind, arg.TrackID)
return err
}
const getRotationState = `-- name: GetRotationState :one
SELECT user_id, playlist_kind, played_track_ids, rotation_started_at, updated_at FROM system_playlist_rotation_state
WHERE user_id = $1 AND playlist_kind = $2
`
type GetRotationStateParams struct {
UserID pgtype.UUID
PlaylistKind string
}
func (q *Queries) GetRotationState(ctx context.Context, arg GetRotationStateParams) (SystemPlaylistRotationState, error) {
row := q.db.QueryRow(ctx, getRotationState, arg.UserID, arg.PlaylistKind)
var i SystemPlaylistRotationState
err := row.Scan(
&i.UserID,
&i.PlaylistKind,
&i.PlayedTrackIds,
&i.RotationStartedAt,
&i.UpdatedAt,
)
return i, err
}
const resetRotationState = `-- name: ResetRotationState :exec
UPDATE system_playlist_rotation_state
SET played_track_ids = '{}',
rotation_started_at = now(),
updated_at = now()
WHERE user_id = $1 AND playlist_kind = $2
`
type ResetRotationStateParams struct {
UserID pgtype.UUID
PlaylistKind string
}
// Clears the played set and advances the rotation window. Called
// when the playlist snapshot has been fully sampled (Stage 2).
func (q *Queries) ResetRotationState(ctx context.Context, arg ResetRotationStateParams) error {
_, err := q.db.Exec(ctx, resetRotationState, arg.UserID, arg.PlaylistKind)
return err
}