d2a0b7d780
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>
92 lines
2.8 KiB
Go
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
|
|
}
|