feat(db/m7-352): sqlc queries for system playlist runs + seed selection
Adds internal/db/queries/system_playlists.sql with 12 named queries covering active-user enumeration, per-user run tracking (claim/finish/fail), seed-artist/track/cover selection, and playlist CRUD by kind. Runs sqlc generate to emit dbq/system_playlists.sql.go; also updates playlists.sql.go and models.go to reflect the new kind/system_variant/ seed_artist_id columns added in migration 0015. Note: plan specified a.cover_path for PickTopAlbumCoverForArtistByUser but albums uses cover_art_path — corrected in the query file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -346,6 +346,9 @@ type Playlist struct {
|
|||||||
DurationSec int32
|
DurationSec int32
|
||||||
CreatedAt pgtype.Timestamptz
|
CreatedAt pgtype.Timestamptz
|
||||||
UpdatedAt pgtype.Timestamptz
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
Kind string
|
||||||
|
SystemVariant *string
|
||||||
|
SeedArtistID pgtype.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaylistTrack struct {
|
type PlaylistTrack struct {
|
||||||
@@ -388,6 +391,14 @@ type SkipEvent struct {
|
|||||||
PositionMs int32
|
PositionMs int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SystemPlaylistRun struct {
|
||||||
|
UserID pgtype.UUID
|
||||||
|
LastRunAt pgtype.Timestamptz
|
||||||
|
LastRunDate pgtype.Date
|
||||||
|
InFlight bool
|
||||||
|
LastError *string
|
||||||
|
}
|
||||||
|
|
||||||
type Track struct {
|
type Track struct {
|
||||||
ID pgtype.UUID
|
ID pgtype.UUID
|
||||||
Title string
|
Title string
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func (q *Queries) AppendPlaylistTrack(ctx context.Context, arg AppendPlaylistTra
|
|||||||
const createPlaylist = `-- name: CreatePlaylist :one
|
const createPlaylist = `-- name: CreatePlaylist :one
|
||||||
INSERT INTO playlists (user_id, name, description, is_public)
|
INSERT INTO playlists (user_id, name, description, is_public)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING id, user_id, name, description, is_public, cover_path, track_count, duration_sec, created_at, updated_at
|
RETURNING id, user_id, name, description, is_public, cover_path, track_count, duration_sec, created_at, updated_at, kind, system_variant, seed_artist_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreatePlaylistParams struct {
|
type CreatePlaylistParams struct {
|
||||||
@@ -84,6 +84,9 @@ func (q *Queries) CreatePlaylist(ctx context.Context, arg CreatePlaylistParams)
|
|||||||
&i.DurationSec,
|
&i.DurationSec,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Kind,
|
||||||
|
&i.SystemVariant,
|
||||||
|
&i.SeedArtistID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -125,7 +128,7 @@ func (q *Queries) DeletePlaylistTrack(ctx context.Context, arg DeletePlaylistTra
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getPlaylist = `-- name: GetPlaylist :one
|
const getPlaylist = `-- name: GetPlaylist :one
|
||||||
SELECT p.id, p.user_id, p.name, p.description, p.is_public, p.cover_path, p.track_count, p.duration_sec, p.created_at, p.updated_at, u.username AS owner_username
|
SELECT p.id, p.user_id, p.name, p.description, p.is_public, p.cover_path, p.track_count, p.duration_sec, p.created_at, p.updated_at, p.kind, p.system_variant, p.seed_artist_id, u.username AS owner_username
|
||||||
FROM playlists p
|
FROM playlists p
|
||||||
JOIN users u ON u.id = p.user_id
|
JOIN users u ON u.id = p.user_id
|
||||||
WHERE p.id = $1
|
WHERE p.id = $1
|
||||||
@@ -142,6 +145,9 @@ type GetPlaylistRow struct {
|
|||||||
DurationSec int32
|
DurationSec int32
|
||||||
CreatedAt pgtype.Timestamptz
|
CreatedAt pgtype.Timestamptz
|
||||||
UpdatedAt pgtype.Timestamptz
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
Kind string
|
||||||
|
SystemVariant *string
|
||||||
|
SeedArtistID pgtype.UUID
|
||||||
OwnerUsername string
|
OwnerUsername string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +165,9 @@ func (q *Queries) GetPlaylist(ctx context.Context, id pgtype.UUID) (GetPlaylistR
|
|||||||
&i.DurationSec,
|
&i.DurationSec,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Kind,
|
||||||
|
&i.SystemVariant,
|
||||||
|
&i.SeedArtistID,
|
||||||
&i.OwnerUsername,
|
&i.OwnerUsername,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
@@ -271,7 +280,7 @@ func (q *Queries) ListPlaylistTracks(ctx context.Context, playlistID pgtype.UUID
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listPlaylistsForUser = `-- name: ListPlaylistsForUser :many
|
const listPlaylistsForUser = `-- name: ListPlaylistsForUser :many
|
||||||
SELECT p.id, p.user_id, p.name, p.description, p.is_public, p.cover_path, p.track_count, p.duration_sec, p.created_at, p.updated_at, u.username AS owner_username
|
SELECT p.id, p.user_id, p.name, p.description, p.is_public, p.cover_path, p.track_count, p.duration_sec, p.created_at, p.updated_at, p.kind, p.system_variant, p.seed_artist_id, u.username AS owner_username
|
||||||
FROM playlists p
|
FROM playlists p
|
||||||
JOIN users u ON u.id = p.user_id
|
JOIN users u ON u.id = p.user_id
|
||||||
WHERE p.user_id = $1 OR p.is_public = true
|
WHERE p.user_id = $1 OR p.is_public = true
|
||||||
@@ -289,6 +298,9 @@ type ListPlaylistsForUserRow struct {
|
|||||||
DurationSec int32
|
DurationSec int32
|
||||||
CreatedAt pgtype.Timestamptz
|
CreatedAt pgtype.Timestamptz
|
||||||
UpdatedAt pgtype.Timestamptz
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
Kind string
|
||||||
|
SystemVariant *string
|
||||||
|
SeedArtistID pgtype.UUID
|
||||||
OwnerUsername string
|
OwnerUsername string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +326,9 @@ func (q *Queries) ListPlaylistsForUser(ctx context.Context, userID pgtype.UUID)
|
|||||||
&i.DurationSec,
|
&i.DurationSec,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Kind,
|
||||||
|
&i.SystemVariant,
|
||||||
|
&i.SeedArtistID,
|
||||||
&i.OwnerUsername,
|
&i.OwnerUsername,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -365,7 +380,7 @@ SET
|
|||||||
is_public = CASE WHEN $5::boolean THEN $6::boolean ELSE is_public END,
|
is_public = CASE WHEN $5::boolean THEN $6::boolean ELSE is_public END,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $7
|
WHERE id = $7
|
||||||
RETURNING id, user_id, name, description, is_public, cover_path, track_count, duration_sec, created_at, updated_at
|
RETURNING id, user_id, name, description, is_public, cover_path, track_count, duration_sec, created_at, updated_at, kind, system_variant, seed_artist_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdatePlaylistParams struct {
|
type UpdatePlaylistParams struct {
|
||||||
@@ -403,6 +418,9 @@ func (q *Queries) UpdatePlaylist(ctx context.Context, arg UpdatePlaylistParams)
|
|||||||
&i.DurationSec,
|
&i.DurationSec,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Kind,
|
||||||
|
&i.SystemVariant,
|
||||||
|
&i.SeedArtistID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: system_playlists.sql
|
||||||
|
|
||||||
|
package dbq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearStaleSystemPlaylistInFlight = `-- name: ClearStaleSystemPlaylistInFlight :exec
|
||||||
|
UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true
|
||||||
|
`
|
||||||
|
|
||||||
|
// Cron startup recovery: clear any in_flight=true rows from a previously
|
||||||
|
// crashed process. Safe because at startup, by definition, nothing is
|
||||||
|
// still building.
|
||||||
|
func (q *Queries) ClearStaleSystemPlaylistInFlight(ctx context.Context) error {
|
||||||
|
_, err := q.db.Exec(ctx, clearStaleSystemPlaylistInFlight)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSystemPlaylistsForUser = `-- name: DeleteSystemPlaylistsForUser :exec
|
||||||
|
DELETE FROM playlists WHERE user_id = $1 AND kind = 'system'
|
||||||
|
`
|
||||||
|
|
||||||
|
// Atomic-replace step. CASCADE on playlist_tracks.playlist_id handles
|
||||||
|
// the join rows.
|
||||||
|
func (q *Queries) DeleteSystemPlaylistsForUser(ctx context.Context, userID pgtype.UUID) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteSystemPlaylistsForUser, userID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const failSystemPlaylistRun = `-- name: FailSystemPlaylistRun :exec
|
||||||
|
UPDATE system_playlist_runs
|
||||||
|
SET in_flight = false,
|
||||||
|
last_error = $2
|
||||||
|
WHERE user_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type FailSystemPlaylistRunParams struct {
|
||||||
|
UserID pgtype.UUID
|
||||||
|
LastError *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark a failed build complete (in_flight=false, error captured).
|
||||||
|
func (q *Queries) FailSystemPlaylistRun(ctx context.Context, arg FailSystemPlaylistRunParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, failSystemPlaylistRun, arg.UserID, arg.LastError)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const finishSystemPlaylistRun = `-- name: FinishSystemPlaylistRun :exec
|
||||||
|
UPDATE system_playlist_runs
|
||||||
|
SET last_run_at = $2,
|
||||||
|
last_run_date = $3,
|
||||||
|
in_flight = false,
|
||||||
|
last_error = NULL
|
||||||
|
WHERE user_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type FinishSystemPlaylistRunParams struct {
|
||||||
|
UserID pgtype.UUID
|
||||||
|
LastRunAt pgtype.Timestamptz
|
||||||
|
LastRunDate pgtype.Date
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark a successful build complete.
|
||||||
|
func (q *Queries) FinishSystemPlaylistRun(ctx context.Context, arg FinishSystemPlaylistRunParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, finishSystemPlaylistRun, arg.UserID, arg.LastRunAt, arg.LastRunDate)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSystemPlaylistRun = `-- name: GetSystemPlaylistRun :one
|
||||||
|
SELECT user_id, last_run_at, last_run_date, in_flight, last_error
|
||||||
|
FROM system_playlist_runs
|
||||||
|
WHERE user_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetSystemPlaylistRun(ctx context.Context, userID pgtype.UUID) (SystemPlaylistRun, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getSystemPlaylistRun, userID)
|
||||||
|
var i SystemPlaylistRun
|
||||||
|
err := row.Scan(
|
||||||
|
&i.UserID,
|
||||||
|
&i.LastRunAt,
|
||||||
|
&i.LastRunDate,
|
||||||
|
&i.InFlight,
|
||||||
|
&i.LastError,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listActiveUsersForSystemPlaylists = `-- name: ListActiveUsersForSystemPlaylists :many
|
||||||
|
|
||||||
|
SELECT u.id FROM users u
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM play_events pe
|
||||||
|
WHERE pe.user_id = u.id
|
||||||
|
AND pe.started_at > now() - INTERVAL '7 days'
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
// M7 #352 slice 2: system-generated playlist queries.
|
||||||
|
// Active = had a play in the last 7 days. The cron iterates this list.
|
||||||
|
func (q *Queries) ListActiveUsersForSystemPlaylists(ctx context.Context) ([]pgtype.UUID, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listActiveUsersForSystemPlaylists)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []pgtype.UUID
|
||||||
|
for rows.Next() {
|
||||||
|
var id pgtype.UUID
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listPlaylistsByUserAndKind = `-- name: ListPlaylistsByUserAndKind :many
|
||||||
|
SELECT id, user_id, name, description, is_public, kind, system_variant,
|
||||||
|
seed_artist_id, cover_path, track_count, duration_sec,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM playlists
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND ($2::text = 'all' OR kind = $2)
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListPlaylistsByUserAndKindParams struct {
|
||||||
|
UserID pgtype.UUID
|
||||||
|
Column2 string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPlaylistsByUserAndKindRow struct {
|
||||||
|
ID pgtype.UUID
|
||||||
|
UserID pgtype.UUID
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
IsPublic bool
|
||||||
|
Kind string
|
||||||
|
SystemVariant *string
|
||||||
|
SeedArtistID pgtype.UUID
|
||||||
|
CoverPath *string
|
||||||
|
TrackCount int32
|
||||||
|
DurationSec int32
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
// Powers GET /api/me/playlists?kind=. Sorted by updated_at DESC.
|
||||||
|
func (q *Queries) ListPlaylistsByUserAndKind(ctx context.Context, arg ListPlaylistsByUserAndKindParams) ([]ListPlaylistsByUserAndKindRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listPlaylistsByUserAndKind, arg.UserID, arg.Column2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListPlaylistsByUserAndKindRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListPlaylistsByUserAndKindRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.IsPublic,
|
||||||
|
&i.Kind,
|
||||||
|
&i.SystemVariant,
|
||||||
|
&i.SeedArtistID,
|
||||||
|
&i.CoverPath,
|
||||||
|
&i.TrackCount,
|
||||||
|
&i.DurationSec,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickSeedArtists = `-- name: PickSeedArtists :many
|
||||||
|
WITH plays AS (
|
||||||
|
SELECT t.artist_id,
|
||||||
|
COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count
|
||||||
|
FROM play_events pe
|
||||||
|
JOIN tracks t ON t.id = pe.track_id
|
||||||
|
WHERE pe.user_id = $1
|
||||||
|
AND pe.started_at > now() - INTERVAL '7 days'
|
||||||
|
AND t.artist_id IS NOT NULL
|
||||||
|
GROUP BY t.artist_id
|
||||||
|
),
|
||||||
|
liked AS (
|
||||||
|
SELECT artist_id FROM general_likes_artists WHERE user_id = $1
|
||||||
|
)
|
||||||
|
SELECT p.artist_id,
|
||||||
|
(p.play_count + CASE WHEN l.artist_id IS NOT NULL THEN 5 ELSE 0 END)::bigint AS score
|
||||||
|
FROM plays p
|
||||||
|
LEFT JOIN liked l ON l.artist_id = p.artist_id
|
||||||
|
ORDER BY score DESC, p.artist_id
|
||||||
|
LIMIT 3
|
||||||
|
`
|
||||||
|
|
||||||
|
type PickSeedArtistsRow struct {
|
||||||
|
ArtistID pgtype.UUID
|
||||||
|
Score int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top-3 most-engaged distinct artists in the user's last 7 days.
|
||||||
|
// Score = unskipped-play count + 5 if user has liked the artist.
|
||||||
|
func (q *Queries) PickSeedArtists(ctx context.Context, userID pgtype.UUID) ([]PickSeedArtistsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, pickSeedArtists, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []PickSeedArtistsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i PickSeedArtistsRow
|
||||||
|
if err := rows.Scan(&i.ArtistID, &i.Score); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickTopAlbumCoverForArtistByUser = `-- name: PickTopAlbumCoverForArtistByUser :one
|
||||||
|
SELECT a.cover_art_path
|
||||||
|
FROM albums a
|
||||||
|
LEFT JOIN tracks t ON t.album_id = a.id
|
||||||
|
LEFT JOIN play_events pe
|
||||||
|
ON pe.track_id = t.id
|
||||||
|
AND pe.user_id = $1
|
||||||
|
AND pe.was_skipped = false
|
||||||
|
WHERE a.artist_id = $2
|
||||||
|
AND a.cover_art_path IS NOT NULL
|
||||||
|
GROUP BY a.id, a.release_date, a.cover_art_path
|
||||||
|
ORDER BY COUNT(pe.id) DESC, a.release_date DESC NULLS LAST, a.id
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
type PickTopAlbumCoverForArtistByUserParams struct {
|
||||||
|
UserID pgtype.UUID
|
||||||
|
ArtistID pgtype.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Songs like X" cover. Most-played album by user; tie-break by most-recent
|
||||||
|
// release. NULL if the artist has no albums or all covers are NULL.
|
||||||
|
// Note: albums uses cover_art_path (not cover_path).
|
||||||
|
func (q *Queries) PickTopAlbumCoverForArtistByUser(ctx context.Context, arg PickTopAlbumCoverForArtistByUserParams) (*string, error) {
|
||||||
|
row := q.db.QueryRow(ctx, pickTopAlbumCoverForArtistByUser, arg.UserID, arg.ArtistID)
|
||||||
|
var cover_art_path *string
|
||||||
|
err := row.Scan(&cover_art_path)
|
||||||
|
return cover_art_path, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickTopPlayedTrackForArtistByUser = `-- name: PickTopPlayedTrackForArtistByUser :one
|
||||||
|
SELECT COALESCE(
|
||||||
|
(SELECT t.id
|
||||||
|
FROM play_events pe
|
||||||
|
JOIN tracks t ON t.id = pe.track_id
|
||||||
|
WHERE pe.user_id = $1
|
||||||
|
AND t.artist_id = $2
|
||||||
|
AND pe.started_at > now() - INTERVAL '7 days'
|
||||||
|
AND pe.was_skipped = false
|
||||||
|
GROUP BY t.id
|
||||||
|
ORDER BY COUNT(*) DESC, t.id
|
||||||
|
LIMIT 1),
|
||||||
|
(SELECT t.id
|
||||||
|
FROM tracks t
|
||||||
|
JOIN albums a ON a.id = t.album_id
|
||||||
|
WHERE t.artist_id = $2
|
||||||
|
ORDER BY a.release_date DESC NULLS LAST,
|
||||||
|
t.disc_number NULLS LAST,
|
||||||
|
t.track_number NULLS LAST,
|
||||||
|
t.id
|
||||||
|
LIMIT 1)
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type PickTopPlayedTrackForArtistByUserParams struct {
|
||||||
|
UserID pgtype.UUID
|
||||||
|
ArtistID pgtype.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Songs like X" seed selection. Returns the user's most-played non-skipped
|
||||||
|
// track by artist X in the last 7 days. Falls back to the artist's most-
|
||||||
|
// recent album's first track if no plays exist.
|
||||||
|
func (q *Queries) PickTopPlayedTrackForArtistByUser(ctx context.Context, arg PickTopPlayedTrackForArtistByUserParams) (interface{}, error) {
|
||||||
|
row := q.db.QueryRow(ctx, pickTopPlayedTrackForArtistByUser, arg.UserID, arg.ArtistID)
|
||||||
|
var coalesce interface{}
|
||||||
|
err := row.Scan(&coalesce)
|
||||||
|
return coalesce, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickTopPlayedTrackForUser = `-- name: PickTopPlayedTrackForUser :one
|
||||||
|
SELECT t.id
|
||||||
|
FROM play_events pe
|
||||||
|
JOIN tracks t ON t.id = pe.track_id
|
||||||
|
WHERE pe.user_id = $1
|
||||||
|
AND pe.started_at > now() - INTERVAL '7 days'
|
||||||
|
AND pe.was_skipped = false
|
||||||
|
GROUP BY t.id
|
||||||
|
ORDER BY COUNT(*) DESC, t.id
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
// For-You seed selection. Returns the user's most-played non-skipped
|
||||||
|
// track in the last 7 days; tie-break by track_id for determinism.
|
||||||
|
func (q *Queries) PickTopPlayedTrackForUser(ctx context.Context, userID pgtype.UUID) (pgtype.UUID, error) {
|
||||||
|
row := q.db.QueryRow(ctx, pickTopPlayedTrackForUser, userID)
|
||||||
|
var id pgtype.UUID
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const tryClaimSystemPlaylistRun = `-- name: TryClaimSystemPlaylistRun :one
|
||||||
|
INSERT INTO system_playlist_runs (user_id, in_flight)
|
||||||
|
VALUES ($1, true)
|
||||||
|
ON CONFLICT (user_id) DO UPDATE
|
||||||
|
SET in_flight = true
|
||||||
|
WHERE system_playlist_runs.in_flight = false
|
||||||
|
RETURNING user_id, last_run_at, last_run_date, in_flight, last_error
|
||||||
|
`
|
||||||
|
|
||||||
|
// Atomic test-and-set on in_flight. Returns the row if we won the claim,
|
||||||
|
// or pgx.ErrNoRows if someone else is already building.
|
||||||
|
func (q *Queries) TryClaimSystemPlaylistRun(ctx context.Context, userID pgtype.UUID) (SystemPlaylistRun, error) {
|
||||||
|
row := q.db.QueryRow(ctx, tryClaimSystemPlaylistRun, userID)
|
||||||
|
var i SystemPlaylistRun
|
||||||
|
err := row.Scan(
|
||||||
|
&i.UserID,
|
||||||
|
&i.LastRunAt,
|
||||||
|
&i.LastRunDate,
|
||||||
|
&i.InFlight,
|
||||||
|
&i.LastError,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
-- M7 #352 slice 2: system-generated playlist queries.
|
||||||
|
|
||||||
|
-- name: ListActiveUsersForSystemPlaylists :many
|
||||||
|
-- Active = had a play in the last 7 days. The cron iterates this list.
|
||||||
|
SELECT u.id FROM users u
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM play_events pe
|
||||||
|
WHERE pe.user_id = u.id
|
||||||
|
AND pe.started_at > now() - INTERVAL '7 days'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- name: GetSystemPlaylistRun :one
|
||||||
|
SELECT user_id, last_run_at, last_run_date, in_flight, last_error
|
||||||
|
FROM system_playlist_runs
|
||||||
|
WHERE user_id = $1;
|
||||||
|
|
||||||
|
-- name: TryClaimSystemPlaylistRun :one
|
||||||
|
-- Atomic test-and-set on in_flight. Returns the row if we won the claim,
|
||||||
|
-- or pgx.ErrNoRows if someone else is already building.
|
||||||
|
INSERT INTO system_playlist_runs (user_id, in_flight)
|
||||||
|
VALUES ($1, true)
|
||||||
|
ON CONFLICT (user_id) DO UPDATE
|
||||||
|
SET in_flight = true
|
||||||
|
WHERE system_playlist_runs.in_flight = false
|
||||||
|
RETURNING user_id, last_run_at, last_run_date, in_flight, last_error;
|
||||||
|
|
||||||
|
-- name: FinishSystemPlaylistRun :exec
|
||||||
|
-- Mark a successful build complete.
|
||||||
|
UPDATE system_playlist_runs
|
||||||
|
SET last_run_at = $2,
|
||||||
|
last_run_date = $3,
|
||||||
|
in_flight = false,
|
||||||
|
last_error = NULL
|
||||||
|
WHERE user_id = $1;
|
||||||
|
|
||||||
|
-- name: FailSystemPlaylistRun :exec
|
||||||
|
-- Mark a failed build complete (in_flight=false, error captured).
|
||||||
|
UPDATE system_playlist_runs
|
||||||
|
SET in_flight = false,
|
||||||
|
last_error = $2
|
||||||
|
WHERE user_id = $1;
|
||||||
|
|
||||||
|
-- name: ClearStaleSystemPlaylistInFlight :exec
|
||||||
|
-- Cron startup recovery: clear any in_flight=true rows from a previously
|
||||||
|
-- crashed process. Safe because at startup, by definition, nothing is
|
||||||
|
-- still building.
|
||||||
|
UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true;
|
||||||
|
|
||||||
|
-- name: PickSeedArtists :many
|
||||||
|
-- Top-3 most-engaged distinct artists in the user's last 7 days.
|
||||||
|
-- Score = unskipped-play count + 5 if user has liked the artist.
|
||||||
|
WITH plays AS (
|
||||||
|
SELECT t.artist_id,
|
||||||
|
COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count
|
||||||
|
FROM play_events pe
|
||||||
|
JOIN tracks t ON t.id = pe.track_id
|
||||||
|
WHERE pe.user_id = $1
|
||||||
|
AND pe.started_at > now() - INTERVAL '7 days'
|
||||||
|
AND t.artist_id IS NOT NULL
|
||||||
|
GROUP BY t.artist_id
|
||||||
|
),
|
||||||
|
liked AS (
|
||||||
|
SELECT artist_id FROM general_likes_artists WHERE user_id = $1
|
||||||
|
)
|
||||||
|
SELECT p.artist_id,
|
||||||
|
(p.play_count + CASE WHEN l.artist_id IS NOT NULL THEN 5 ELSE 0 END)::bigint AS score
|
||||||
|
FROM plays p
|
||||||
|
LEFT JOIN liked l ON l.artist_id = p.artist_id
|
||||||
|
ORDER BY score DESC, p.artist_id
|
||||||
|
LIMIT 3;
|
||||||
|
|
||||||
|
-- name: PickTopPlayedTrackForUser :one
|
||||||
|
-- For-You seed selection. Returns the user's most-played non-skipped
|
||||||
|
-- track in the last 7 days; tie-break by track_id for determinism.
|
||||||
|
SELECT t.id
|
||||||
|
FROM play_events pe
|
||||||
|
JOIN tracks t ON t.id = pe.track_id
|
||||||
|
WHERE pe.user_id = $1
|
||||||
|
AND pe.started_at > now() - INTERVAL '7 days'
|
||||||
|
AND pe.was_skipped = false
|
||||||
|
GROUP BY t.id
|
||||||
|
ORDER BY COUNT(*) DESC, t.id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: PickTopPlayedTrackForArtistByUser :one
|
||||||
|
-- "Songs like X" seed selection. Returns the user's most-played non-skipped
|
||||||
|
-- track by artist X in the last 7 days. Falls back to the artist's most-
|
||||||
|
-- recent album's first track if no plays exist.
|
||||||
|
SELECT COALESCE(
|
||||||
|
(SELECT t.id
|
||||||
|
FROM play_events pe
|
||||||
|
JOIN tracks t ON t.id = pe.track_id
|
||||||
|
WHERE pe.user_id = $1
|
||||||
|
AND t.artist_id = $2
|
||||||
|
AND pe.started_at > now() - INTERVAL '7 days'
|
||||||
|
AND pe.was_skipped = false
|
||||||
|
GROUP BY t.id
|
||||||
|
ORDER BY COUNT(*) DESC, t.id
|
||||||
|
LIMIT 1),
|
||||||
|
(SELECT t.id
|
||||||
|
FROM tracks t
|
||||||
|
JOIN albums a ON a.id = t.album_id
|
||||||
|
WHERE t.artist_id = $2
|
||||||
|
ORDER BY a.release_date DESC NULLS LAST,
|
||||||
|
t.disc_number NULLS LAST,
|
||||||
|
t.track_number NULLS LAST,
|
||||||
|
t.id
|
||||||
|
LIMIT 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- name: PickTopAlbumCoverForArtistByUser :one
|
||||||
|
-- "Songs like X" cover. Most-played album by user; tie-break by most-recent
|
||||||
|
-- release. NULL if the artist has no albums or all covers are NULL.
|
||||||
|
-- Note: albums uses cover_art_path (not cover_path).
|
||||||
|
SELECT a.cover_art_path
|
||||||
|
FROM albums a
|
||||||
|
LEFT JOIN tracks t ON t.album_id = a.id
|
||||||
|
LEFT JOIN play_events pe
|
||||||
|
ON pe.track_id = t.id
|
||||||
|
AND pe.user_id = $1
|
||||||
|
AND pe.was_skipped = false
|
||||||
|
WHERE a.artist_id = $2
|
||||||
|
AND a.cover_art_path IS NOT NULL
|
||||||
|
GROUP BY a.id, a.release_date, a.cover_art_path
|
||||||
|
ORDER BY COUNT(pe.id) DESC, a.release_date DESC NULLS LAST, a.id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: DeleteSystemPlaylistsForUser :exec
|
||||||
|
-- Atomic-replace step. CASCADE on playlist_tracks.playlist_id handles
|
||||||
|
-- the join rows.
|
||||||
|
DELETE FROM playlists WHERE user_id = $1 AND kind = 'system';
|
||||||
|
|
||||||
|
-- name: ListPlaylistsByUserAndKind :many
|
||||||
|
-- Powers GET /api/me/playlists?kind=. Sorted by updated_at DESC.
|
||||||
|
SELECT id, user_id, name, description, is_public, kind, system_variant,
|
||||||
|
seed_artist_id, cover_path, track_count, duration_sec,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM playlists
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND ($2::text = 'all' OR kind = $2)
|
||||||
|
ORDER BY updated_at DESC;
|
||||||
Reference in New Issue
Block a user