From b31723c5e2fc5a44f25ef72ffbc5787d5a8c1382 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 1 May 2026 09:03:10 -0400 Subject: [PATCH] feat(db): add M6a library-list + artist-tracks queries Appends ListArtistsAlphaWithCovers, ListAlbumsAlphaWithArtist, CountAlbums, and ListArtistTracksForUser queries; regenerates sqlc. Co-Authored-By: Claude Sonnet 4.6 --- internal/db/dbq/albums.sql.go | 63 +++++++++++++++++++++++++++++ internal/db/dbq/artists.sql.go | 63 +++++++++++++++++++++++++++++ internal/db/dbq/tracks.sql.go | 70 +++++++++++++++++++++++++++++++++ internal/db/queries/albums.sql | 13 ++++++ internal/db/queries/artists.sql | 22 +++++++++++ internal/db/queries/tracks.sql | 20 ++++++++++ 6 files changed, 251 insertions(+) diff --git a/internal/db/dbq/albums.sql.go b/internal/db/dbq/albums.sql.go index 2065580f..e9c47ef1 100644 --- a/internal/db/dbq/albums.sql.go +++ b/internal/db/dbq/albums.sql.go @@ -11,6 +11,18 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const countAlbums = `-- name: CountAlbums :one +SELECT COUNT(*) FROM albums +` + +// M6a: total album count for the /api/library/albums envelope. +func (q *Queries) CountAlbums(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, countAlbums) + var count int64 + err := row.Scan(&count) + return count, err +} + const countAlbumsMatching = `-- name: CountAlbumsMatching :one SELECT COUNT(*) FROM albums WHERE title ILIKE '%' || $1::text || '%' ` @@ -160,6 +172,57 @@ func (q *Queries) ListAlbumsAlphaByName(ctx context.Context, arg ListAlbumsAlpha return items, nil } +const listAlbumsAlphaWithArtist = `-- name: ListAlbumsAlphaWithArtist :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.sort_title, albums.id +LIMIT $1 OFFSET $2 +` + +type ListAlbumsAlphaWithArtistParams struct { + Limit int32 + Offset int32 +} + +type ListAlbumsAlphaWithArtistRow struct { + Album Album + ArtistName string +} + +// M6a: alpha-sorted album list joined with artist_name. Used by +// /api/library/albums for the wrapping-grid page. Stable id-tiebreak. +func (q *Queries) ListAlbumsAlphaWithArtist(ctx context.Context, arg ListAlbumsAlphaWithArtistParams) ([]ListAlbumsAlphaWithArtistRow, error) { + rows, err := q.db.Query(ctx, listAlbumsAlphaWithArtist, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListAlbumsAlphaWithArtistRow + for rows.Next() { + var i ListAlbumsAlphaWithArtistRow + 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 listAlbumsByArtist = `-- name: ListAlbumsByArtist :many SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at FROM albums WHERE artist_id = $1 ORDER BY release_date NULLS LAST, sort_title ` diff --git a/internal/db/dbq/artists.sql.go b/internal/db/dbq/artists.sql.go index 60f059e8..4fcd8172 100644 --- a/internal/db/dbq/artists.sql.go +++ b/internal/db/dbq/artists.sql.go @@ -169,6 +169,69 @@ func (q *Queries) ListArtistsAlpha(ctx context.Context, arg ListArtistsAlphaPara return items, nil } +const listArtistsAlphaWithCovers = `-- name: ListArtistsAlphaWithCovers :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 artists +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 artists.sort_name, artists.name, artists.id +LIMIT $1 OFFSET $2 +` + +type ListArtistsAlphaWithCoversParams struct { + Limit int32 + Offset int32 +} + +type ListArtistsAlphaWithCoversRow struct { + Artist Artist + CoverAlbumID pgtype.UUID + AlbumCount int64 +} + +// M6a: alpha-sorted artist list with derived cover_album_id (most-recent +// album whose cover_art_path is set). Replaces ListArtistsAlpha for the +// /api/artists?sort=alpha branch — the new column is appended, so the +// alpha branch is purely additive on the wire. album_count is computed +// in the same query to drop the old N+1 in handleListArtists. +func (q *Queries) ListArtistsAlphaWithCovers(ctx context.Context, arg ListArtistsAlphaWithCoversParams) ([]ListArtistsAlphaWithCoversRow, error) { + rows, err := q.db.Query(ctx, listArtistsAlphaWithCovers, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListArtistsAlphaWithCoversRow + for rows.Next() { + var i ListArtistsAlphaWithCoversRow + 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 listArtistsNewest = `-- name: ListArtistsNewest :many SELECT id, name, sort_name, mbid, created_at, updated_at FROM artists ORDER BY created_at DESC, id DESC LIMIT $1 OFFSET $2 ` diff --git a/internal/db/dbq/tracks.sql.go b/internal/db/dbq/tracks.sql.go index 65486ad8..c2440931 100644 --- a/internal/db/dbq/tracks.sql.go +++ b/internal/db/dbq/tracks.sql.go @@ -109,6 +109,76 @@ func (q *Queries) GetTrackByPath(ctx context.Context, filePath string) (Track, e return i, err } +const listArtistTracksForUser = `-- name: ListArtistTracksForUser :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 +WHERE t.artist_id = $1 + AND NOT EXISTS ( + SELECT 1 FROM lidarr_quarantine q + WHERE q.user_id = $2 AND q.track_id = t.id + ) +ORDER BY albums.release_date NULLS LAST, albums.sort_title, + t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.id +` + +type ListArtistTracksForUserParams struct { + ArtistID pgtype.UUID + UserID pgtype.UUID +} + +type ListArtistTracksForUserRow struct { + Track Track + AlbumTitle string + ArtistName string +} + +// M6a: every track for the artist across their albums, with album_title +// and artist_name joined. Honors per-user lidarr_quarantine. Used by +// /api/artists/{id}/tracks for the artist-card play affordance, which +// shuffles client-side. Ordering matches album/track natural order so +// the shuffle has a deterministic input. +func (q *Queries) ListArtistTracksForUser(ctx context.Context, arg ListArtistTracksForUserParams) ([]ListArtistTracksForUserRow, error) { + rows, err := q.db.Query(ctx, listArtistTracksForUser, arg.ArtistID, arg.UserID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListArtistTracksForUserRow + for rows.Next() { + var i ListArtistTracksForUserRow + 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 listTracksByAlbum = `-- name: ListTracksByAlbum :many SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks WHERE album_id = $1 ORDER BY disc_number NULLS LAST, track_number NULLS LAST ` diff --git a/internal/db/queries/albums.sql b/internal/db/queries/albums.sql index e56acaa7..10f9eb8c 100644 --- a/internal/db/queries/albums.sql +++ b/internal/db/queries/albums.sql @@ -67,3 +67,16 @@ FROM albums JOIN artists ON artists.id = albums.artist_id ORDER BY albums.created_at DESC, albums.id LIMIT $1; + +-- name: ListAlbumsAlphaWithArtist :many +-- M6a: alpha-sorted album list joined with artist_name. Used by +-- /api/library/albums for the wrapping-grid page. Stable id-tiebreak. +SELECT sqlc.embed(albums), artists.name AS artist_name +FROM albums +JOIN artists ON artists.id = albums.artist_id +ORDER BY albums.sort_title, albums.id +LIMIT $1 OFFSET $2; + +-- name: CountAlbums :one +-- M6a: total album count for the /api/library/albums envelope. +SELECT COUNT(*) FROM albums; diff --git a/internal/db/queries/artists.sql b/internal/db/queries/artists.sql index 50ab2985..6d2bf4f9 100644 --- a/internal/db/queries/artists.sql +++ b/internal/db/queries/artists.sql @@ -42,3 +42,25 @@ SELECT COUNT(*) FROM artists WHERE name ILIKE '%' || $1::text || '%'; -- Batched lookup used by M5c suggestion attribution to resolve top-3 -- contributing seed UUIDs back to artist names in one round-trip. SELECT * FROM artists WHERE id = ANY($1::uuid[]); + +-- name: ListArtistsAlphaWithCovers :many +-- M6a: alpha-sorted artist list with derived cover_album_id (most-recent +-- album whose cover_art_path is set). Replaces ListArtistsAlpha for the +-- /api/artists?sort=alpha branch — the new column is appended, so the +-- alpha branch is purely additive on the wire. album_count is computed +-- in the same query to drop the old N+1 in handleListArtists. +SELECT sqlc.embed(artists), + cov.id AS cover_album_id, + cnt.album_count::bigint AS album_count +FROM artists +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 artists.sort_name, artists.name, artists.id +LIMIT $1 OFFSET $2; diff --git a/internal/db/queries/tracks.sql b/internal/db/queries/tracks.sql index d9b06e1f..e2bf4251 100644 --- a/internal/db/queries/tracks.sql +++ b/internal/db/queries/tracks.sql @@ -70,3 +70,23 @@ WHERE title ILIKE '%' || $1::text || '%' SELECT 1 FROM lidarr_quarantine q WHERE q.user_id = $2 AND q.track_id = tracks.id ); + +-- name: ListArtistTracksForUser :many +-- M6a: every track for the artist across their albums, with album_title +-- and artist_name joined. Honors per-user lidarr_quarantine. Used by +-- /api/artists/{id}/tracks for the artist-card play affordance, which +-- shuffles client-side. Ordering matches album/track natural order so +-- the shuffle has a deterministic input. +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 +WHERE t.artist_id = $1 + AND NOT EXISTS ( + SELECT 1 FROM lidarr_quarantine q + WHERE q.user_id = $2 AND q.track_id = t.id + ) +ORDER BY albums.release_date NULLS LAST, albums.sort_title, + t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.id;