feat(db): add M6a rediscover queries (albums + artists, with fallbacks)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -156,6 +156,264 @@ func (q *Queries) ListMostPlayedTracksForUser(ctx context.Context, arg ListMostP
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRediscoverAlbumsFallbackForUser = `-- name: ListRediscoverAlbumsFallbackForUser :many
|
||||
SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, artists.name AS artist_name
|
||||
FROM general_likes_albums gla
|
||||
JOIN albums ON albums.id = gla.album_id
|
||||
JOIN artists ON artists.id = albums.artist_id
|
||||
WHERE gla.user_id = $1
|
||||
ORDER BY random()
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListRediscoverAlbumsFallbackForUserParams struct {
|
||||
UserID pgtype.UUID
|
||||
Limit int32
|
||||
}
|
||||
|
||||
type ListRediscoverAlbumsFallbackForUserRow struct {
|
||||
Album Album
|
||||
ArtistName string
|
||||
}
|
||||
|
||||
// M6a: random sample of liked albums for the user. The Go service uses
|
||||
// this to top up the rediscover row when ListRediscoverAlbumsForUser
|
||||
// returns fewer than `limit` rows.
|
||||
func (q *Queries) ListRediscoverAlbumsFallbackForUser(ctx context.Context, arg ListRediscoverAlbumsFallbackForUserParams) ([]ListRediscoverAlbumsFallbackForUserRow, error) {
|
||||
rows, err := q.db.Query(ctx, listRediscoverAlbumsFallbackForUser, arg.UserID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListRediscoverAlbumsFallbackForUserRow
|
||||
for rows.Next() {
|
||||
var i ListRediscoverAlbumsFallbackForUserRow
|
||||
if err := rows.Scan(
|
||||
&i.Album.ID,
|
||||
&i.Album.Title,
|
||||
&i.Album.SortTitle,
|
||||
&i.Album.ArtistID,
|
||||
&i.Album.ReleaseDate,
|
||||
&i.Album.Mbid,
|
||||
&i.Album.CoverArtPath,
|
||||
&i.Album.CreatedAt,
|
||||
&i.Album.UpdatedAt,
|
||||
&i.ArtistName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRediscoverAlbumsForUser = `-- name: ListRediscoverAlbumsForUser :many
|
||||
WITH eligible AS (
|
||||
SELECT al.id AS album_id,
|
||||
gla.liked_at,
|
||||
max(pe.started_at) AS last_played
|
||||
FROM general_likes_albums gla
|
||||
JOIN albums al ON al.id = gla.album_id
|
||||
LEFT JOIN tracks t ON t.album_id = al.id
|
||||
LEFT JOIN play_events pe ON pe.track_id = t.id AND pe.user_id = $1
|
||||
WHERE gla.user_id = $1
|
||||
AND gla.liked_at < now() - interval '30 days'
|
||||
GROUP BY al.id, gla.liked_at
|
||||
HAVING COALESCE(max(pe.started_at), '1970-01-01'::timestamptz)
|
||||
< now() - interval '14 days'
|
||||
)
|
||||
SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, artists.name AS artist_name
|
||||
FROM eligible e
|
||||
JOIN albums ON albums.id = e.album_id
|
||||
JOIN artists ON artists.id = albums.artist_id
|
||||
ORDER BY (now() - COALESCE(e.last_played, e.liked_at)) DESC, albums.id
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListRediscoverAlbumsForUserParams struct {
|
||||
UserID pgtype.UUID
|
||||
Limit int32
|
||||
}
|
||||
|
||||
type ListRediscoverAlbumsForUserRow struct {
|
||||
Album Album
|
||||
ArtistName string
|
||||
}
|
||||
|
||||
// M6a: albums liked >30 days ago AND not played in the last 14 days,
|
||||
// ordered by longest-since-last-play first. The HAVING clause uses an
|
||||
// epoch sentinel so albums with NO plays count as eligible (their
|
||||
// "last play" is treated as 1970, which is always >14 days ago).
|
||||
func (q *Queries) ListRediscoverAlbumsForUser(ctx context.Context, arg ListRediscoverAlbumsForUserParams) ([]ListRediscoverAlbumsForUserRow, error) {
|
||||
rows, err := q.db.Query(ctx, listRediscoverAlbumsForUser, arg.UserID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListRediscoverAlbumsForUserRow
|
||||
for rows.Next() {
|
||||
var i ListRediscoverAlbumsForUserRow
|
||||
if err := rows.Scan(
|
||||
&i.Album.ID,
|
||||
&i.Album.Title,
|
||||
&i.Album.SortTitle,
|
||||
&i.Album.ArtistID,
|
||||
&i.Album.ReleaseDate,
|
||||
&i.Album.Mbid,
|
||||
&i.Album.CoverArtPath,
|
||||
&i.Album.CreatedAt,
|
||||
&i.Album.UpdatedAt,
|
||||
&i.ArtistName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRediscoverArtistsFallbackForUser = `-- name: ListRediscoverArtistsFallbackForUser :many
|
||||
SELECT artists.id, artists.name, artists.sort_name, artists.mbid, artists.created_at, artists.updated_at,
|
||||
cov.id AS cover_album_id,
|
||||
cnt.album_count::bigint AS album_count
|
||||
FROM general_likes_artists gla
|
||||
JOIN artists ON artists.id = gla.artist_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id FROM albums
|
||||
WHERE artist_id = artists.id AND cover_art_path IS NOT NULL
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
) cov ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT count(*) AS album_count
|
||||
FROM albums WHERE artist_id = artists.id
|
||||
) cnt ON true
|
||||
WHERE gla.user_id = $1
|
||||
ORDER BY random()
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListRediscoverArtistsFallbackForUserParams struct {
|
||||
UserID pgtype.UUID
|
||||
Limit int32
|
||||
}
|
||||
|
||||
type ListRediscoverArtistsFallbackForUserRow struct {
|
||||
Artist Artist
|
||||
CoverAlbumID pgtype.UUID
|
||||
AlbumCount int64
|
||||
}
|
||||
|
||||
func (q *Queries) ListRediscoverArtistsFallbackForUser(ctx context.Context, arg ListRediscoverArtistsFallbackForUserParams) ([]ListRediscoverArtistsFallbackForUserRow, error) {
|
||||
rows, err := q.db.Query(ctx, listRediscoverArtistsFallbackForUser, arg.UserID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListRediscoverArtistsFallbackForUserRow
|
||||
for rows.Next() {
|
||||
var i ListRediscoverArtistsFallbackForUserRow
|
||||
if err := rows.Scan(
|
||||
&i.Artist.ID,
|
||||
&i.Artist.Name,
|
||||
&i.Artist.SortName,
|
||||
&i.Artist.Mbid,
|
||||
&i.Artist.CreatedAt,
|
||||
&i.Artist.UpdatedAt,
|
||||
&i.CoverAlbumID,
|
||||
&i.AlbumCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRediscoverArtistsForUser = `-- name: ListRediscoverArtistsForUser :many
|
||||
WITH eligible AS (
|
||||
SELECT a.id AS artist_id,
|
||||
gla.liked_at,
|
||||
max(pe.started_at) AS last_played
|
||||
FROM general_likes_artists gla
|
||||
JOIN artists a ON a.id = gla.artist_id
|
||||
LEFT JOIN tracks t ON t.artist_id = a.id
|
||||
LEFT JOIN play_events pe ON pe.track_id = t.id AND pe.user_id = $1
|
||||
WHERE gla.user_id = $1
|
||||
AND gla.liked_at < now() - interval '30 days'
|
||||
GROUP BY a.id, gla.liked_at
|
||||
HAVING COALESCE(max(pe.started_at), '1970-01-01'::timestamptz)
|
||||
< now() - interval '14 days'
|
||||
)
|
||||
SELECT artists.id, artists.name, artists.sort_name, artists.mbid, artists.created_at, artists.updated_at,
|
||||
cov.id AS cover_album_id,
|
||||
cnt.album_count::bigint AS album_count
|
||||
FROM eligible e
|
||||
JOIN artists ON artists.id = e.artist_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id FROM albums
|
||||
WHERE artist_id = artists.id AND cover_art_path IS NOT NULL
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
) cov ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT count(*) AS album_count
|
||||
FROM albums WHERE artist_id = artists.id
|
||||
) cnt ON true
|
||||
ORDER BY (now() - COALESCE(e.last_played, e.liked_at)) DESC, artists.id
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListRediscoverArtistsForUserParams struct {
|
||||
UserID pgtype.UUID
|
||||
Limit int32
|
||||
}
|
||||
|
||||
type ListRediscoverArtistsForUserRow struct {
|
||||
Artist Artist
|
||||
CoverAlbumID pgtype.UUID
|
||||
AlbumCount int64
|
||||
}
|
||||
|
||||
// M6a: artists liked >30 days ago AND none of their tracks played in the
|
||||
// last 14 days, ordered by longest-since-last-play. cover_album_id is
|
||||
// derived from a representative album (LEFT JOIN LATERAL).
|
||||
func (q *Queries) ListRediscoverArtistsForUser(ctx context.Context, arg ListRediscoverArtistsForUserParams) ([]ListRediscoverArtistsForUserRow, error) {
|
||||
rows, err := q.db.Query(ctx, listRediscoverArtistsForUser, arg.UserID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListRediscoverArtistsForUserRow
|
||||
for rows.Next() {
|
||||
var i ListRediscoverArtistsForUserRow
|
||||
if err := rows.Scan(
|
||||
&i.Artist.ID,
|
||||
&i.Artist.Name,
|
||||
&i.Artist.SortName,
|
||||
&i.Artist.Mbid,
|
||||
&i.Artist.CreatedAt,
|
||||
&i.Artist.UpdatedAt,
|
||||
&i.CoverAlbumID,
|
||||
&i.AlbumCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const loadRadioCandidates = `-- name: LoadRadioCandidates :many
|
||||
SELECT
|
||||
t.id, t.title, t.album_id, t.artist_id, t.track_number, t.disc_number, t.duration_ms, t.file_path, t.file_size, t.file_format, t.bitrate, t.mbid, t.genre, t.added_at, t.updated_at,
|
||||
|
||||
@@ -253,3 +253,95 @@ LEFT JOIN LATERAL (
|
||||
) cnt ON true
|
||||
ORDER BY max_started.started_at DESC, a.id
|
||||
LIMIT $2;
|
||||
|
||||
-- name: ListRediscoverAlbumsForUser :many
|
||||
-- M6a: albums liked >30 days ago AND not played in the last 14 days,
|
||||
-- ordered by longest-since-last-play first. The HAVING clause uses an
|
||||
-- epoch sentinel so albums with NO plays count as eligible (their
|
||||
-- "last play" is treated as 1970, which is always >14 days ago).
|
||||
WITH eligible AS (
|
||||
SELECT al.id AS album_id,
|
||||
gla.liked_at,
|
||||
max(pe.started_at) AS last_played
|
||||
FROM general_likes_albums gla
|
||||
JOIN albums al ON al.id = gla.album_id
|
||||
LEFT JOIN tracks t ON t.album_id = al.id
|
||||
LEFT JOIN play_events pe ON pe.track_id = t.id AND pe.user_id = $1
|
||||
WHERE gla.user_id = $1
|
||||
AND gla.liked_at < now() - interval '30 days'
|
||||
GROUP BY al.id, gla.liked_at
|
||||
HAVING COALESCE(max(pe.started_at), '1970-01-01'::timestamptz)
|
||||
< now() - interval '14 days'
|
||||
)
|
||||
SELECT sqlc.embed(albums), artists.name AS artist_name
|
||||
FROM eligible e
|
||||
JOIN albums ON albums.id = e.album_id
|
||||
JOIN artists ON artists.id = albums.artist_id
|
||||
ORDER BY (now() - COALESCE(e.last_played, e.liked_at)) DESC, albums.id
|
||||
LIMIT $2;
|
||||
|
||||
-- name: ListRediscoverAlbumsFallbackForUser :many
|
||||
-- M6a: random sample of liked albums for the user. The Go service uses
|
||||
-- this to top up the rediscover row when ListRediscoverAlbumsForUser
|
||||
-- returns fewer than `limit` rows.
|
||||
SELECT sqlc.embed(albums), artists.name AS artist_name
|
||||
FROM general_likes_albums gla
|
||||
JOIN albums ON albums.id = gla.album_id
|
||||
JOIN artists ON artists.id = albums.artist_id
|
||||
WHERE gla.user_id = $1
|
||||
ORDER BY random()
|
||||
LIMIT $2;
|
||||
|
||||
-- name: ListRediscoverArtistsForUser :many
|
||||
-- M6a: artists liked >30 days ago AND none of their tracks played in the
|
||||
-- last 14 days, ordered by longest-since-last-play. cover_album_id is
|
||||
-- derived from a representative album (LEFT JOIN LATERAL).
|
||||
WITH eligible AS (
|
||||
SELECT a.id AS artist_id,
|
||||
gla.liked_at,
|
||||
max(pe.started_at) AS last_played
|
||||
FROM general_likes_artists gla
|
||||
JOIN artists a ON a.id = gla.artist_id
|
||||
LEFT JOIN tracks t ON t.artist_id = a.id
|
||||
LEFT JOIN play_events pe ON pe.track_id = t.id AND pe.user_id = $1
|
||||
WHERE gla.user_id = $1
|
||||
AND gla.liked_at < now() - interval '30 days'
|
||||
GROUP BY a.id, gla.liked_at
|
||||
HAVING COALESCE(max(pe.started_at), '1970-01-01'::timestamptz)
|
||||
< now() - interval '14 days'
|
||||
)
|
||||
SELECT sqlc.embed(artists),
|
||||
cov.id AS cover_album_id,
|
||||
cnt.album_count::bigint AS album_count
|
||||
FROM eligible e
|
||||
JOIN artists ON artists.id = e.artist_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id FROM albums
|
||||
WHERE artist_id = artists.id AND cover_art_path IS NOT NULL
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
) cov ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT count(*) AS album_count
|
||||
FROM albums WHERE artist_id = artists.id
|
||||
) cnt ON true
|
||||
ORDER BY (now() - COALESCE(e.last_played, e.liked_at)) DESC, artists.id
|
||||
LIMIT $2;
|
||||
|
||||
-- name: ListRediscoverArtistsFallbackForUser :many
|
||||
SELECT sqlc.embed(artists),
|
||||
cov.id AS cover_album_id,
|
||||
cnt.album_count::bigint AS album_count
|
||||
FROM general_likes_artists gla
|
||||
JOIN artists ON artists.id = gla.artist_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id FROM albums
|
||||
WHERE artist_id = artists.id AND cover_art_path IS NOT NULL
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
) cov ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT count(*) AS album_count
|
||||
FROM albums WHERE artist_id = artists.id
|
||||
) cnt ON true
|
||||
WHERE gla.user_id = $1
|
||||
ORDER BY random()
|
||||
LIMIT $2;
|
||||
|
||||
Reference in New Issue
Block a user