release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m11s
release / go (push) Successful in 1m29s
release / integration (push) Successful in 4m27s
release / android (push) Successful in 5m2s
release / Build signed APK (releases and dev) (push) Successful in 5m17s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m15s
release / Verify release artifacts (tag releases only) (push) Skipped
PickTopPlayedTracksForUser's liked tier read general_likes without joining tracks. With no plays to seed from, For You could pick a liked track whose file is gone. The play tiers were already safe: their play_events join filters missing_since. The same gap was in PickTopPlayedTrackForArtistByUser's fallback, which seeds Songs-like from the artist's newest album when there are no recent plays. It could pick a missing track, and since #5296 a missing track is never fetched for similarity, so that seed has no edges either. The caller already skips an empty seed, so an artist whose tracks are all missing gets no Songs-like mix instead of one aimed at nothing. Integration tests cover both cases: a liked-but-missing track is not a seed, and the Songs-like fallback moves to the next album once the newest one's track goes missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
578 lines
18 KiB
Go
578 lines
18 KiB
Go
// 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 createSystemPlaylist = `-- name: CreateSystemPlaylist :one
|
|
INSERT INTO playlists (
|
|
user_id, name, description, is_public,
|
|
kind, system_variant, seed_artist_id, cover_path
|
|
) VALUES ($1, $2, '', false, 'system', $3, $4, $5)
|
|
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 CreateSystemPlaylistParams struct {
|
|
UserID pgtype.UUID
|
|
Name string
|
|
SystemVariant *string
|
|
SeedArtistID pgtype.UUID
|
|
CoverPath *string
|
|
}
|
|
|
|
// Inserts a system-generated playlist. Used by BuildSystemPlaylists.
|
|
// For 'for_you' variant, pass seed_artist_id as NULL (zero-value pgtype.UUID
|
|
// with Valid=false).
|
|
func (q *Queries) CreateSystemPlaylist(ctx context.Context, arg CreateSystemPlaylistParams) (Playlist, error) {
|
|
row := q.db.QueryRow(ctx, createSystemPlaylist,
|
|
arg.UserID,
|
|
arg.Name,
|
|
arg.SystemVariant,
|
|
arg.SeedArtistID,
|
|
arg.CoverPath,
|
|
)
|
|
var i Playlist
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.UserID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.IsPublic,
|
|
&i.CoverPath,
|
|
&i.TrackCount,
|
|
&i.DurationSec,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Kind,
|
|
&i.SystemVariant,
|
|
&i.SeedArtistID,
|
|
)
|
|
return i, 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 getSystemPlaylistByVariantForUser = `-- name: GetSystemPlaylistByVariantForUser :one
|
|
SELECT p.id, p.user_id, p.name, p.kind, p.system_variant,
|
|
p.seed_artist_id, p.cover_path, p.track_count
|
|
FROM playlists p
|
|
WHERE p.user_id = $1
|
|
AND p.kind = 'system'
|
|
AND p.system_variant = $2
|
|
ORDER BY p.created_at DESC
|
|
LIMIT 1
|
|
`
|
|
|
|
type GetSystemPlaylistByVariantForUserParams struct {
|
|
UserID pgtype.UUID
|
|
SystemVariant *string
|
|
}
|
|
|
|
type GetSystemPlaylistByVariantForUserRow struct {
|
|
ID pgtype.UUID
|
|
UserID pgtype.UUID
|
|
Name string
|
|
Kind string
|
|
SystemVariant *string
|
|
SeedArtistID pgtype.UUID
|
|
CoverPath *string
|
|
TrackCount int32
|
|
}
|
|
|
|
// Returns the user's system playlist for the given variant
|
|
// ('for_you' / 'songs_like_artist' / 'discover'), or pgx.ErrNoRows
|
|
// when the user hasn't built that variant yet. For
|
|
// 'songs_like_artist' (which can have multiple rows per user, one
|
|
// per seed), this returns whichever LIMIT 1 picks — callers that
|
|
// need all instances should use a different query.
|
|
func (q *Queries) GetSystemPlaylistByVariantForUser(ctx context.Context, arg GetSystemPlaylistByVariantForUserParams) (GetSystemPlaylistByVariantForUserRow, error) {
|
|
row := q.db.QueryRow(ctx, getSystemPlaylistByVariantForUser, arg.UserID, arg.SystemVariant)
|
|
var i GetSystemPlaylistByVariantForUserRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.UserID,
|
|
&i.Name,
|
|
&i.Kind,
|
|
&i.SystemVariant,
|
|
&i.SeedArtistID,
|
|
&i.CoverPath,
|
|
&i.TrackCount,
|
|
)
|
|
return i, 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'
|
|
)
|
|
`
|
|
|
|
// Track picks here join `tracks ... AND t.missing_since IS NULL` (#2523): a
|
|
// seed or For-You candidate has to be something that can actually play. Note
|
|
// this only affects newly GENERATED playlists — already-stored system
|
|
// playlists keep their rows until the next daily rebuild, which is why the
|
|
// shared ListPlaylistTracks read path is deliberately left unfiltered (it
|
|
// also serves user-curated playlists, where hiding a track the user added
|
|
// themselves would be wrong).
|
|
// 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
|
|
}
|
|
|
|
// Used in integration tests; production filter applied in Go
|
|
// (api.handleListPlaylists fetches via Service.List and filters by Kind in
|
|
// memory so it can also surface other users' public playlists alongside
|
|
// the caller's own kind-filtered ones).
|
|
// 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 liked AS (
|
|
SELECT gla.artist_id FROM general_likes_artists gla WHERE gla.user_id = $1
|
|
),
|
|
recent7 AS (
|
|
SELECT t.artist_id,
|
|
COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count,
|
|
0 AS tier
|
|
FROM play_events pe
|
|
JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
|
|
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
|
|
),
|
|
recent30 AS (
|
|
SELECT t.artist_id,
|
|
COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count,
|
|
1 AS tier
|
|
FROM play_events pe
|
|
JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
|
|
WHERE pe.user_id = $1
|
|
AND pe.started_at > now() - INTERVAL '30 days'
|
|
AND t.artist_id IS NOT NULL
|
|
GROUP BY t.artist_id
|
|
),
|
|
alltime AS (
|
|
SELECT t.artist_id,
|
|
COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count,
|
|
2 AS tier
|
|
FROM play_events pe
|
|
JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
|
|
WHERE pe.user_id = $1
|
|
AND t.artist_id IS NOT NULL
|
|
GROUP BY t.artist_id
|
|
),
|
|
likedonly AS (
|
|
SELECT l.artist_id, 0::bigint AS play_count, 3 AS tier
|
|
FROM liked l
|
|
),
|
|
chosen AS (
|
|
SELECT artist_id, play_count, tier FROM recent7
|
|
UNION ALL
|
|
SELECT artist_id, play_count, tier FROM recent30
|
|
WHERE NOT EXISTS (SELECT 1 FROM recent7)
|
|
UNION ALL
|
|
SELECT artist_id, play_count, tier FROM alltime
|
|
WHERE NOT EXISTS (SELECT 1 FROM recent7)
|
|
AND NOT EXISTS (SELECT 1 FROM recent30)
|
|
UNION ALL
|
|
SELECT artist_id, play_count, tier FROM likedonly
|
|
WHERE NOT EXISTS (SELECT 1 FROM recent7)
|
|
AND NOT EXISTS (SELECT 1 FROM recent30)
|
|
AND NOT EXISTS (SELECT 1 FROM alltime)
|
|
)
|
|
SELECT c.artist_id,
|
|
(c.play_count + CASE WHEN l.artist_id IS NOT NULL THEN 5 ELSE 0 END)::bigint AS score,
|
|
c.tier::int AS tier
|
|
FROM chosen c
|
|
LEFT JOIN liked l ON l.artist_id = c.artist_id
|
|
ORDER BY score DESC, c.artist_id
|
|
LIMIT 12
|
|
`
|
|
|
|
type PickSeedArtistsRow struct {
|
|
ArtistID pgtype.UUID
|
|
Score int64
|
|
Tier int32
|
|
}
|
|
|
|
// Top-12 most-engaged distinct artist candidates, tiered so the
|
|
// "Songs like X" mixes never silently vanish (#1255): the old hard
|
|
// 7-day window emptied the seed pool after a quiet week, and the
|
|
// daily atomic-replace build then deleted every existing mix until
|
|
// the user played something again. Same fallback shape as
|
|
// PickTopPlayedTracksForUser (For You's seeds):
|
|
//
|
|
// tier 0 engagement in the last 7 days
|
|
// tier 1 (only if tier 0 empty) last 30 days
|
|
// tier 2 (only if tiers 0+1 empty) all-time
|
|
// tier 3 (only if 0-2 empty) liked artists with no play history
|
|
//
|
|
// All returned rows share one tier; produceSeedMixes maps it onto the
|
|
// rule-#131 pick-kind ladder and stamps the built tracks, so metrics
|
|
// can compare mixes seeded from fresh vs stale engagement.
|
|
// Score = unskipped-play count + 5 if user has liked the artist. The
|
|
// Go-side picker (pickSeedArtistsForDay) shuffles the 12
|
|
// daily-deterministically and takes songsLikeSeedCount (6) so the mix
|
|
// set both rotates day-to-day and fills the dedicated Home row (#1491).
|
|
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, &i.Tier); 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 AND t.missing_since IS NULL
|
|
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
|
|
AND t.missing_since IS NULL -- #2701: the play branch filters; this one must too
|
|
ORDER BY a.release_date DESC NULLS LAST,
|
|
t.disc_number NULLS LAST,
|
|
t.track_number NULLS LAST,
|
|
t.id
|
|
LIMIT 1)
|
|
)::uuid AS id
|
|
`
|
|
|
|
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.
|
|
// Cast to uuid so sqlc infers pgtype.UUID rather than interface{}.
|
|
func (q *Queries) PickTopPlayedTrackForArtistByUser(ctx context.Context, arg PickTopPlayedTrackForArtistByUserParams) (pgtype.UUID, error) {
|
|
row := q.db.QueryRow(ctx, pickTopPlayedTrackForArtistByUser, arg.UserID, arg.ArtistID)
|
|
var id pgtype.UUID
|
|
err := row.Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
const pickTopPlayedTracksForUser = `-- name: PickTopPlayedTracksForUser :many
|
|
WITH recent AS (
|
|
SELECT t.id, COUNT(*) AS c, 0 AS tier
|
|
FROM play_events pe
|
|
JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
|
|
WHERE pe.user_id = $1
|
|
AND pe.started_at > now() - INTERVAL '30 days'
|
|
AND pe.was_skipped = false
|
|
GROUP BY t.id
|
|
),
|
|
alltime AS (
|
|
SELECT t.id, COUNT(*) AS c, 1 AS tier
|
|
FROM play_events pe
|
|
JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
|
|
WHERE pe.user_id = $1
|
|
AND pe.was_skipped = false
|
|
GROUP BY t.id
|
|
),
|
|
liked AS (
|
|
SELECT gl.track_id AS id, 0::bigint AS c, 2 AS tier
|
|
FROM general_likes gl
|
|
JOIN tracks t ON t.id = gl.track_id AND t.missing_since IS NULL
|
|
WHERE gl.user_id = $1
|
|
),
|
|
chosen AS (
|
|
SELECT id, c, tier FROM recent
|
|
UNION ALL
|
|
SELECT id, c, tier FROM alltime
|
|
WHERE NOT EXISTS (SELECT 1 FROM recent)
|
|
UNION ALL
|
|
SELECT id, c, tier FROM liked
|
|
WHERE NOT EXISTS (SELECT 1 FROM recent)
|
|
AND NOT EXISTS (SELECT 1 FROM alltime)
|
|
)
|
|
SELECT id
|
|
FROM chosen
|
|
ORDER BY tier, c DESC, id
|
|
LIMIT 5
|
|
`
|
|
|
|
// For-You candidate seeds, tiered so For-You never silently vanishes:
|
|
//
|
|
// tier 0 top non-skip plays in the last 30 days
|
|
// tier 1 (only if tier 0 empty) all-time top non-skip plays
|
|
// tier 2 (only if tiers 0+1 empty) liked tracks
|
|
//
|
|
// Returns up to 5 ids; tie-break by track_id for determinism. The
|
|
// Go-side picker (pickDailySeeds) draws the day's seeds from these.
|
|
// Widened from a hard 7-day window, which made For-You disappear
|
|
// after a week of not listening and never recover on a self-hosted
|
|
// library with sparse history.
|
|
// A likes-only read still joins tracks: a like outlives its file, and a
|
|
// seed whose file is gone points For You at something the user can't hear
|
|
// (#2701). The other two tiers get the same filter from their play_events
|
|
// join.
|
|
func (q *Queries) PickTopPlayedTracksForUser(ctx context.Context, userID pgtype.UUID) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, pickTopPlayedTracksForUser, userID)
|
|
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 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
|
|
}
|