feat(db): add M6a home-section queries (recently added, most played, last played)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -313,6 +313,54 @@ func (q *Queries) ListAlbumsRandom(ctx context.Context, limit int32) ([]Album, e
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listRecentlyAddedAlbumsWithArtist = `-- name: ListRecentlyAddedAlbumsWithArtist :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 albums
|
||||||
|
JOIN artists ON artists.id = albums.artist_id
|
||||||
|
ORDER BY albums.created_at DESC, albums.id
|
||||||
|
LIMIT $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListRecentlyAddedAlbumsWithArtistRow struct {
|
||||||
|
Album Album
|
||||||
|
ArtistName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// M6a: recently-added albums joined with artist_name + artist_id for the
|
||||||
|
// home-page section. created_at is the row-insert timestamp from the
|
||||||
|
// scanner — the closest proxy to "added to library". Stable secondary
|
||||||
|
// ordering by id keeps pagination tie-break deterministic.
|
||||||
|
func (q *Queries) ListRecentlyAddedAlbumsWithArtist(ctx context.Context, limit int32) ([]ListRecentlyAddedAlbumsWithArtistRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listRecentlyAddedAlbumsWithArtist, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListRecentlyAddedAlbumsWithArtistRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListRecentlyAddedAlbumsWithArtistRow
|
||||||
|
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 searchAlbums = `-- name: SearchAlbums :many
|
const searchAlbums = `-- name: SearchAlbums :many
|
||||||
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at FROM albums
|
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at FROM albums
|
||||||
WHERE title ILIKE '%' || $1 || '%'
|
WHERE title ILIKE '%' || $1 || '%'
|
||||||
|
|||||||
@@ -11,6 +11,151 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const listLastPlayedArtistsForUser = `-- name: ListLastPlayedArtistsForUser :many
|
||||||
|
SELECT a.id, a.name, a.sort_name, a.mbid, a.created_at, a.updated_at,
|
||||||
|
cov.id AS cover_album_id,
|
||||||
|
cnt.album_count::bigint AS album_count,
|
||||||
|
max_started.started_at::timestamptz AS last_played_at
|
||||||
|
FROM artists a
|
||||||
|
JOIN LATERAL (
|
||||||
|
SELECT max(pe.started_at) AS started_at
|
||||||
|
FROM play_events pe
|
||||||
|
JOIN tracks t ON t.id = pe.track_id
|
||||||
|
WHERE pe.user_id = $1 AND t.artist_id = a.id
|
||||||
|
) max_started ON max_started.started_at IS NOT NULL
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT id FROM albums
|
||||||
|
WHERE artist_id = a.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 = a.id
|
||||||
|
) cnt ON true
|
||||||
|
ORDER BY max_started.started_at DESC, a.id
|
||||||
|
LIMIT $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListLastPlayedArtistsForUserParams struct {
|
||||||
|
UserID pgtype.UUID
|
||||||
|
Limit int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLastPlayedArtistsForUserRow struct {
|
||||||
|
Artist Artist
|
||||||
|
CoverAlbumID pgtype.UUID
|
||||||
|
AlbumCount int64
|
||||||
|
LastPlayedAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
// M6a: artists ranked by max(play_events.started_at) for the user, with
|
||||||
|
// a derived cover_album_id via a representative-album lateral join (most
|
||||||
|
// recent album that has cover_art_path set). album_count joined for the
|
||||||
|
// ArtistRef wire shape.
|
||||||
|
func (q *Queries) ListLastPlayedArtistsForUser(ctx context.Context, arg ListLastPlayedArtistsForUserParams) ([]ListLastPlayedArtistsForUserRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listLastPlayedArtistsForUser, arg.UserID, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListLastPlayedArtistsForUserRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListLastPlayedArtistsForUserRow
|
||||||
|
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,
|
||||||
|
&i.LastPlayedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listMostPlayedTracksForUser = `-- name: ListMostPlayedTracksForUser :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,
|
||||||
|
albums.title AS album_title,
|
||||||
|
artists.name AS artist_name
|
||||||
|
FROM tracks t
|
||||||
|
JOIN albums ON albums.id = t.album_id
|
||||||
|
JOIN artists ON artists.id = t.artist_id
|
||||||
|
JOIN play_events pe ON pe.track_id = t.id
|
||||||
|
WHERE pe.user_id = $1
|
||||||
|
AND pe.was_skipped = false
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM lidarr_quarantine q
|
||||||
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||||
|
)
|
||||||
|
GROUP BY t.id, t.title, t.album_id, t.artist_id, t.duration_ms, t.file_path,
|
||||||
|
t.file_format, t.file_size, t.bitrate, t.track_number, t.disc_number,
|
||||||
|
t.mbid, t.genre, t.added_at, t.updated_at,
|
||||||
|
albums.title, artists.name
|
||||||
|
ORDER BY count(*) DESC, t.id
|
||||||
|
LIMIT $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListMostPlayedTracksForUserParams struct {
|
||||||
|
UserID pgtype.UUID
|
||||||
|
Limit int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListMostPlayedTracksForUserRow struct {
|
||||||
|
Track Track
|
||||||
|
AlbumTitle string
|
||||||
|
ArtistName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// M6a: top-N tracks by completed-play count for the user. was_skipped
|
||||||
|
// excludes skips so a user spamming next doesn't fabricate a top track.
|
||||||
|
// Quarantined tracks (per-user soft-hide from M5b) are filtered out.
|
||||||
|
func (q *Queries) ListMostPlayedTracksForUser(ctx context.Context, arg ListMostPlayedTracksForUserParams) ([]ListMostPlayedTracksForUserRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listMostPlayedTracksForUser, arg.UserID, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListMostPlayedTracksForUserRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListMostPlayedTracksForUserRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Track.ID,
|
||||||
|
&i.Track.Title,
|
||||||
|
&i.Track.AlbumID,
|
||||||
|
&i.Track.ArtistID,
|
||||||
|
&i.Track.TrackNumber,
|
||||||
|
&i.Track.DiscNumber,
|
||||||
|
&i.Track.DurationMs,
|
||||||
|
&i.Track.FilePath,
|
||||||
|
&i.Track.FileSize,
|
||||||
|
&i.Track.FileFormat,
|
||||||
|
&i.Track.Bitrate,
|
||||||
|
&i.Track.Mbid,
|
||||||
|
&i.Track.Genre,
|
||||||
|
&i.Track.AddedAt,
|
||||||
|
&i.Track.UpdatedAt,
|
||||||
|
&i.AlbumTitle,
|
||||||
|
&i.ArtistName,
|
||||||
|
); 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
|
const loadRadioCandidates = `-- name: LoadRadioCandidates :many
|
||||||
SELECT
|
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,
|
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,
|
||||||
|
|||||||
@@ -56,3 +56,14 @@ LIMIT $2 OFFSET $3;
|
|||||||
|
|
||||||
-- name: CountAlbumsMatching :one
|
-- name: CountAlbumsMatching :one
|
||||||
SELECT COUNT(*) FROM albums WHERE title ILIKE '%' || $1::text || '%';
|
SELECT COUNT(*) FROM albums WHERE title ILIKE '%' || $1::text || '%';
|
||||||
|
|
||||||
|
-- name: ListRecentlyAddedAlbumsWithArtist :many
|
||||||
|
-- M6a: recently-added albums joined with artist_name + artist_id for the
|
||||||
|
-- home-page section. created_at is the row-insert timestamp from the
|
||||||
|
-- scanner — the closest proxy to "added to library". Stable secondary
|
||||||
|
-- ordering by id keeps pagination tie-break deterministic.
|
||||||
|
SELECT sqlc.embed(albums), artists.name AS artist_name
|
||||||
|
FROM albums
|
||||||
|
JOIN artists ON artists.id = albums.artist_id
|
||||||
|
ORDER BY albums.created_at DESC, albums.id
|
||||||
|
LIMIT $1;
|
||||||
|
|||||||
@@ -201,3 +201,55 @@ FROM contributions
|
|||||||
GROUP BY candidate_mbid, candidate_name
|
GROUP BY candidate_mbid, candidate_name
|
||||||
ORDER BY total_score DESC
|
ORDER BY total_score DESC
|
||||||
LIMIT $3;
|
LIMIT $3;
|
||||||
|
|
||||||
|
-- name: ListMostPlayedTracksForUser :many
|
||||||
|
-- M6a: top-N tracks by completed-play count for the user. was_skipped
|
||||||
|
-- excludes skips so a user spamming next doesn't fabricate a top track.
|
||||||
|
-- Quarantined tracks (per-user soft-hide from M5b) are filtered out.
|
||||||
|
SELECT sqlc.embed(t),
|
||||||
|
albums.title AS album_title,
|
||||||
|
artists.name AS artist_name
|
||||||
|
FROM tracks t
|
||||||
|
JOIN albums ON albums.id = t.album_id
|
||||||
|
JOIN artists ON artists.id = t.artist_id
|
||||||
|
JOIN play_events pe ON pe.track_id = t.id
|
||||||
|
WHERE pe.user_id = $1
|
||||||
|
AND pe.was_skipped = false
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM lidarr_quarantine q
|
||||||
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||||
|
)
|
||||||
|
GROUP BY t.id, t.title, t.album_id, t.artist_id, t.duration_ms, t.file_path,
|
||||||
|
t.file_format, t.file_size, t.bitrate, t.track_number, t.disc_number,
|
||||||
|
t.mbid, t.genre, t.added_at, t.updated_at,
|
||||||
|
albums.title, artists.name
|
||||||
|
ORDER BY count(*) DESC, t.id
|
||||||
|
LIMIT $2;
|
||||||
|
|
||||||
|
-- name: ListLastPlayedArtistsForUser :many
|
||||||
|
-- M6a: artists ranked by max(play_events.started_at) for the user, with
|
||||||
|
-- a derived cover_album_id via a representative-album lateral join (most
|
||||||
|
-- recent album that has cover_art_path set). album_count joined for the
|
||||||
|
-- ArtistRef wire shape.
|
||||||
|
SELECT sqlc.embed(a),
|
||||||
|
cov.id AS cover_album_id,
|
||||||
|
cnt.album_count::bigint AS album_count,
|
||||||
|
max_started.started_at::timestamptz AS last_played_at
|
||||||
|
FROM artists a
|
||||||
|
JOIN LATERAL (
|
||||||
|
SELECT max(pe.started_at) AS started_at
|
||||||
|
FROM play_events pe
|
||||||
|
JOIN tracks t ON t.id = pe.track_id
|
||||||
|
WHERE pe.user_id = $1 AND t.artist_id = a.id
|
||||||
|
) max_started ON max_started.started_at IS NOT NULL
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT id FROM albums
|
||||||
|
WHERE artist_id = a.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 = a.id
|
||||||
|
) cnt ON true
|
||||||
|
ORDER BY max_started.started_at DESC, a.id
|
||||||
|
LIMIT $2;
|
||||||
|
|||||||
Reference in New Issue
Block a user