release / go (push) Successful in 2m33s
release / web (push) Successful in 1m41s
release / govulncheck (push) Successful in 22s
release / integration (push) Successful in 6m0s
release / android (push) Successful in 6m7s
release / Build signed APK (releases and dev) (push) Successful in 6m8s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m43s
release / Verify release artifacts (tag releases only) (push) Skipped
Lidarr's metadata is keyed by MusicBrainz release group, but re-acquisition requested albums by their release id, so every add came back "not found". - Sweeper requests an album by its tag-supplied release group, else the one MusicBrainz names (cached onto the album). An album MusicBrainz cannot name is skipped and counted, with no attempt spent. - Reconciler: an add refused as not found re-reads the request's album id as a release (library first, then MusicBrainz), rewrites the request to the group and adds again. This repairs the requests already stored. - Completion matches an album by release id or release group, and only once a track of it is on disk, so a re-acquisition request no longer completes against the row of the album it is trying to bring back. Closes #5241. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
942 lines
28 KiB
Go
942 lines
28 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: albums.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"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 countAlbumsByArtist = `-- name: CountAlbumsByArtist :one
|
|
SELECT COUNT(*) FROM albums WHERE artist_id = $1
|
|
`
|
|
|
|
// Used by request-progress reporting to count how many albums have been
|
|
// ingested for a matched artist while a request is still in flight.
|
|
func (q *Queries) CountAlbumsByArtist(ctx context.Context, artistID pgtype.UUID) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countAlbumsByArtist, artistID)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const countAlbumsMatching = `-- name: CountAlbumsMatching :one
|
|
SELECT COUNT(*) FROM albums WHERE title ILIKE '%' || $1::text || '%'
|
|
`
|
|
|
|
func (q *Queries) CountAlbumsMatching(ctx context.Context, dollar_1 string) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countAlbumsMatching, dollar_1)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const deleteAlbumIfEmpty = `-- name: DeleteAlbumIfEmpty :one
|
|
DELETE FROM albums a
|
|
WHERE a.id = $1
|
|
AND NOT EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id)
|
|
RETURNING a.id, a.artist_id
|
|
`
|
|
|
|
type DeleteAlbumIfEmptyRow struct {
|
|
ID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// M7 #372: deletes the album row only when it has no remaining tracks.
|
|
// Used after a track delete to tidy up orphaned albums. RETURNING gives
|
|
// us the artist_id so the service can chain into the artist-empty check.
|
|
// Returns no rows when the album still has tracks; the service treats
|
|
// pgx.ErrNoRows as "album not orphaned, skip cascade".
|
|
func (q *Queries) DeleteAlbumIfEmpty(ctx context.Context, id pgtype.UUID) (DeleteAlbumIfEmptyRow, error) {
|
|
row := q.db.QueryRow(ctx, deleteAlbumIfEmpty, id)
|
|
var i DeleteAlbumIfEmptyRow
|
|
err := row.Scan(&i.ID, &i.ArtistID)
|
|
return i, err
|
|
}
|
|
|
|
const getAlbumByArtistAndTitle = `-- name: GetAlbumByArtistAndTitle :one
|
|
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version, release_group_mbid FROM albums WHERE artist_id = $1 AND title = $2 LIMIT 1
|
|
`
|
|
|
|
type GetAlbumByArtistAndTitleParams struct {
|
|
ArtistID pgtype.UUID
|
|
Title string
|
|
}
|
|
|
|
// Scanner uses this for the no-mbid dedupe path: resolve-or-create.
|
|
func (q *Queries) GetAlbumByArtistAndTitle(ctx context.Context, arg GetAlbumByArtistAndTitleParams) (Album, error) {
|
|
row := q.db.QueryRow(ctx, getAlbumByArtistAndTitle, arg.ArtistID, arg.Title)
|
|
var i Album
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.SortTitle,
|
|
&i.ArtistID,
|
|
&i.ReleaseDate,
|
|
&i.Mbid,
|
|
&i.CoverArtPath,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CoverArtSource,
|
|
&i.CoverArtSourcesVersion,
|
|
&i.ReleaseGroupMbid,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAlbumByID = `-- name: GetAlbumByID :one
|
|
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version, release_group_mbid FROM albums WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetAlbumByID(ctx context.Context, id pgtype.UUID) (Album, error) {
|
|
row := q.db.QueryRow(ctx, getAlbumByID, id)
|
|
var i Album
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.SortTitle,
|
|
&i.ArtistID,
|
|
&i.ReleaseDate,
|
|
&i.Mbid,
|
|
&i.CoverArtPath,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CoverArtSource,
|
|
&i.CoverArtSourcesVersion,
|
|
&i.ReleaseGroupMbid,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAlbumCoverageRollup = `-- name: GetAlbumCoverageRollup :one
|
|
SELECT
|
|
COUNT(*) AS total,
|
|
COUNT(*) FILTER (WHERE cover_art_source IN
|
|
('sidecar','embedded','mbcaa','theaudiodb','deezer','lastfm')) AS with_art,
|
|
COUNT(*) FILTER (WHERE cover_art_source IS NULL) AS pending,
|
|
COUNT(*) FILTER (WHERE cover_art_source = 'none') AS settled,
|
|
COUNT(*) FILTER (WHERE cover_art_source IS NULL AND mbid IS NULL) AS pending_no_mbid
|
|
FROM albums
|
|
`
|
|
|
|
type GetAlbumCoverageRollupRow struct {
|
|
Total int64
|
|
WithArt int64
|
|
Pending int64
|
|
Settled int64
|
|
PendingNoMbid int64
|
|
}
|
|
|
|
// M7 coverage-gauge: library-wide cover-art coverage rollup for the
|
|
// admin gauge. Single sequential scan with FILTER aggregates —
|
|
// sub-millisecond on realistic libraries. pending_no_mbid is a SUBSET
|
|
// of pending; the UI surfaces it separately so operators can see how
|
|
// many "pending" albums are blocked on missing MBID and won't be
|
|
// helped by another scan.
|
|
//
|
|
// Invariant: with_art + pending + settled = total. (pending_no_mbid is
|
|
// not part of the sum — it's a subset of pending, surfaced separately.)
|
|
//
|
|
// The IN list below must stay in sync with the cover_art_source CHECK
|
|
// constraint — currently relaxed by migration 0020 to include 'deezer'
|
|
// and 'lastfm', and migration 0030 further relaxed it to any non-empty
|
|
// string. If a new source value is added without updating this query,
|
|
// with_art will silently undercount. Drift #556 caught the deezer +
|
|
// lastfm omission introduced by migration 0020.
|
|
func (q *Queries) GetAlbumCoverageRollup(ctx context.Context) (GetAlbumCoverageRollupRow, error) {
|
|
row := q.db.QueryRow(ctx, getAlbumCoverageRollup)
|
|
var i GetAlbumCoverageRollupRow
|
|
err := row.Scan(
|
|
&i.Total,
|
|
&i.WithArt,
|
|
&i.Pending,
|
|
&i.Settled,
|
|
&i.PendingNoMbid,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAlbumWithArtist = `-- name: GetAlbumWithArtist :one
|
|
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, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid, artists.name AS artist_name
|
|
FROM albums
|
|
JOIN artists ON artists.id = albums.artist_id
|
|
WHERE albums.id = $1
|
|
`
|
|
|
|
type GetAlbumWithArtistRow struct {
|
|
Album Album
|
|
ArtistName string
|
|
}
|
|
|
|
// Combined fetch for /api/albums/{id}: returns the album row + the
|
|
// joined artist name in a single round trip. Replaces a sequential
|
|
// GetAlbumByID + GetArtistByID pair on the hot detail-page path.
|
|
func (q *Queries) GetAlbumWithArtist(ctx context.Context, id pgtype.UUID) (GetAlbumWithArtistRow, error) {
|
|
row := q.db.QueryRow(ctx, getAlbumWithArtist, id)
|
|
var i GetAlbumWithArtistRow
|
|
err := row.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.Album.CoverArtSource,
|
|
&i.Album.CoverArtSourcesVersion,
|
|
&i.Album.ReleaseGroupMbid,
|
|
&i.ArtistName,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAlbumsByIDs = `-- name: GetAlbumsByIDs :many
|
|
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version, release_group_mbid FROM albums WHERE id = ANY($1::uuid[])
|
|
`
|
|
|
|
// Batched lookup used by /api/library/sync to hydrate upsert payloads
|
|
// (#357). Mirror of GetArtistsByIDs.
|
|
func (q *Queries) GetAlbumsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Album, error) {
|
|
rows, err := q.db.Query(ctx, getAlbumsByIDs, dollar_1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Album
|
|
for rows.Next() {
|
|
var i Album
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.SortTitle,
|
|
&i.ArtistID,
|
|
&i.ReleaseDate,
|
|
&i.Mbid,
|
|
&i.CoverArtPath,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CoverArtSource,
|
|
&i.CoverArtSourcesVersion,
|
|
&i.ReleaseGroupMbid,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getReleaseGroupForReleaseMbid = `-- name: GetReleaseGroupForReleaseMbid :one
|
|
SELECT release_group_mbid::text
|
|
FROM albums
|
|
WHERE mbid = $1 AND release_group_mbid IS NOT NULL
|
|
LIMIT 1
|
|
`
|
|
|
|
// #5241: the release group the library already knows for a release id, so a
|
|
// request carrying a release id can be repaired without asking MusicBrainz.
|
|
func (q *Queries) GetReleaseGroupForReleaseMbid(ctx context.Context, mbid *string) (string, error) {
|
|
row := q.db.QueryRow(ctx, getReleaseGroupForReleaseMbid, mbid)
|
|
var release_group_mbid string
|
|
err := row.Scan(&release_group_mbid)
|
|
return release_group_mbid, err
|
|
}
|
|
|
|
const listAlbumsAlphaByArtist = `-- name: ListAlbumsAlphaByArtist :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, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid, artists.sort_name AS artist_sort_name
|
|
FROM albums
|
|
JOIN artists ON artists.id = albums.artist_id
|
|
ORDER BY artists.sort_name, albums.sort_title
|
|
LIMIT $1 OFFSET $2
|
|
`
|
|
|
|
type ListAlbumsAlphaByArtistParams struct {
|
|
Limit int32
|
|
Offset int32
|
|
}
|
|
|
|
type ListAlbumsAlphaByArtistRow struct {
|
|
Album Album
|
|
ArtistSortName string
|
|
}
|
|
|
|
// Sorted by the owning artist's sort_name. Needed by Subsonic's
|
|
// alphabeticalByArtist album list type.
|
|
func (q *Queries) ListAlbumsAlphaByArtist(ctx context.Context, arg ListAlbumsAlphaByArtistParams) ([]ListAlbumsAlphaByArtistRow, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsAlphaByArtist, arg.Limit, arg.Offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAlbumsAlphaByArtistRow
|
|
for rows.Next() {
|
|
var i ListAlbumsAlphaByArtistRow
|
|
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.Album.CoverArtSource,
|
|
&i.Album.CoverArtSourcesVersion,
|
|
&i.Album.ReleaseGroupMbid,
|
|
&i.ArtistSortName,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAlbumsAlphaByName = `-- name: ListAlbumsAlphaByName :many
|
|
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version, release_group_mbid FROM albums ORDER BY sort_title LIMIT $1 OFFSET $2
|
|
`
|
|
|
|
type ListAlbumsAlphaByNameParams struct {
|
|
Limit int32
|
|
Offset int32
|
|
}
|
|
|
|
func (q *Queries) ListAlbumsAlphaByName(ctx context.Context, arg ListAlbumsAlphaByNameParams) ([]Album, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsAlphaByName, arg.Limit, arg.Offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Album
|
|
for rows.Next() {
|
|
var i Album
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.SortTitle,
|
|
&i.ArtistID,
|
|
&i.ReleaseDate,
|
|
&i.Mbid,
|
|
&i.CoverArtPath,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CoverArtSource,
|
|
&i.CoverArtSourcesVersion,
|
|
&i.ReleaseGroupMbid,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
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, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid, 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.Album.CoverArtSource,
|
|
&i.Album.CoverArtSourcesVersion,
|
|
&i.Album.ReleaseGroupMbid,
|
|
&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, cover_art_source, cover_art_sources_version, release_group_mbid FROM albums WHERE artist_id = $1 ORDER BY release_date NULLS LAST, sort_title
|
|
`
|
|
|
|
func (q *Queries) ListAlbumsByArtist(ctx context.Context, artistID pgtype.UUID) ([]Album, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsByArtist, artistID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Album
|
|
for rows.Next() {
|
|
var i Album
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.SortTitle,
|
|
&i.ArtistID,
|
|
&i.ReleaseDate,
|
|
&i.Mbid,
|
|
&i.CoverArtPath,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CoverArtSource,
|
|
&i.CoverArtSourcesVersion,
|
|
&i.ReleaseGroupMbid,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAlbumsByArtistWithTrackCount = `-- name: ListAlbumsByArtistWithTrackCount :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, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid,
|
|
(SELECT count(*) FROM tracks t WHERE t.album_id = albums.id)::bigint
|
|
AS track_count
|
|
FROM albums
|
|
WHERE albums.artist_id = $1
|
|
ORDER BY release_date NULLS LAST, sort_title
|
|
`
|
|
|
|
type ListAlbumsByArtistWithTrackCountRow struct {
|
|
Album Album
|
|
TrackCount int64
|
|
}
|
|
|
|
// Replaces the N+1 pattern in handleGetArtist (1 + N CountTracksByAlbum
|
|
// per album). Returns each album joined with its track count via a
|
|
// correlated subquery — single round trip regardless of album count.
|
|
func (q *Queries) ListAlbumsByArtistWithTrackCount(ctx context.Context, artistID pgtype.UUID) ([]ListAlbumsByArtistWithTrackCountRow, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsByArtistWithTrackCount, artistID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAlbumsByArtistWithTrackCountRow
|
|
for rows.Next() {
|
|
var i ListAlbumsByArtistWithTrackCountRow
|
|
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.Album.CoverArtSource,
|
|
&i.Album.CoverArtSourcesVersion,
|
|
&i.Album.ReleaseGroupMbid,
|
|
&i.TrackCount,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAlbumsByGenre = `-- name: ListAlbumsByGenre :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, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid
|
|
FROM albums
|
|
WHERE EXISTS (
|
|
SELECT 1
|
|
FROM tracks
|
|
JOIN LATERAL regexp_split_to_table(coalesce(tracks.genre, ''), '[;,]') AS g(genre) ON true
|
|
WHERE tracks.album_id = albums.id
|
|
AND trim(g.genre) = trim($1::text)
|
|
)
|
|
ORDER BY albums.sort_title, albums.id
|
|
LIMIT $3 OFFSET $2
|
|
`
|
|
|
|
type ListAlbumsByGenreParams struct {
|
|
Genre string
|
|
Off int32
|
|
Lim int32
|
|
}
|
|
|
|
// Album "belongs to" a genre if any of its tracks carry that genre.
|
|
// Serves Subsonic getAlbumList?type=byGenre.
|
|
//
|
|
// Splits tracks.genre on [;,] as of #367. It previously compared the whole
|
|
// column verbatim, so a track tagged "Rock;Pop" was unreachable from EITHER
|
|
// "Rock" or "Pop" — a Subsonic client asking for a genre silently missed
|
|
// every multi-genre track. This also aligns the endpoint with
|
|
// recommendation.sql / discover.sql, which have always split, and with the
|
|
// genre browse index that #367 adds.
|
|
//
|
|
// EXISTS rather than JOIN + DISTINCT ON: the lateral split emits one row per
|
|
// (track, genre-fragment), so a join would multiply rows per album and lean
|
|
// on DISTINCT to undo it. EXISTS asks the question directly.
|
|
func (q *Queries) ListAlbumsByGenre(ctx context.Context, arg ListAlbumsByGenreParams) ([]Album, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsByGenre, arg.Genre, arg.Off, arg.Lim)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Album
|
|
for rows.Next() {
|
|
var i Album
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.SortTitle,
|
|
&i.ArtistID,
|
|
&i.ReleaseDate,
|
|
&i.Mbid,
|
|
&i.CoverArtPath,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CoverArtSource,
|
|
&i.CoverArtSourcesVersion,
|
|
&i.ReleaseGroupMbid,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAlbumsMissingMbidWithTrack = `-- name: ListAlbumsMissingMbidWithTrack :many
|
|
SELECT a.id AS album_id,
|
|
a.artist_id AS artist_id,
|
|
a.title AS title,
|
|
t.file_path AS track_file_path
|
|
FROM albums a
|
|
JOIN LATERAL (
|
|
SELECT file_path
|
|
FROM tracks
|
|
WHERE album_id = a.id
|
|
AND missing_since IS NULL
|
|
ORDER BY disc_number NULLS LAST, track_number NULLS LAST, id
|
|
LIMIT 1
|
|
) t ON true
|
|
WHERE a.mbid IS NULL
|
|
ORDER BY a.created_at ASC
|
|
LIMIT $1
|
|
`
|
|
|
|
type ListAlbumsMissingMbidWithTrackRow struct {
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
Title string
|
|
TrackFilePath string
|
|
}
|
|
|
|
// One-shot MBID backfill: returns each album where mbid IS NULL alongside
|
|
// one of its tracks' file_path so the worker can re-read tags. LIMIT
|
|
// supplied by caller for batching/progress purposes. The track read is
|
|
// one still on disk; an album with none left is skipped until a scan
|
|
// finds its files again (#5139).
|
|
func (q *Queries) ListAlbumsMissingMbidWithTrack(ctx context.Context, limit int32) ([]ListAlbumsMissingMbidWithTrackRow, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsMissingMbidWithTrack, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAlbumsMissingMbidWithTrackRow
|
|
for rows.Next() {
|
|
var i ListAlbumsMissingMbidWithTrackRow
|
|
if err := rows.Scan(
|
|
&i.AlbumID,
|
|
&i.ArtistID,
|
|
&i.Title,
|
|
&i.TrackFilePath,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAlbumsNewest = `-- name: ListAlbumsNewest :many
|
|
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version, release_group_mbid FROM albums ORDER BY created_at DESC LIMIT $1 OFFSET $2
|
|
`
|
|
|
|
type ListAlbumsNewestParams struct {
|
|
Limit int32
|
|
Offset int32
|
|
}
|
|
|
|
func (q *Queries) ListAlbumsNewest(ctx context.Context, arg ListAlbumsNewestParams) ([]Album, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsNewest, arg.Limit, arg.Offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Album
|
|
for rows.Next() {
|
|
var i Album
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.SortTitle,
|
|
&i.ArtistID,
|
|
&i.ReleaseDate,
|
|
&i.Mbid,
|
|
&i.CoverArtPath,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CoverArtSource,
|
|
&i.CoverArtSourcesVersion,
|
|
&i.ReleaseGroupMbid,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAlbumsRandom = `-- name: ListAlbumsRandom :many
|
|
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version, release_group_mbid FROM albums ORDER BY random() LIMIT $1
|
|
`
|
|
|
|
func (q *Queries) ListAlbumsRandom(ctx context.Context, limit int32) ([]Album, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsRandom, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Album
|
|
for rows.Next() {
|
|
var i Album
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.SortTitle,
|
|
&i.ArtistID,
|
|
&i.ReleaseDate,
|
|
&i.Mbid,
|
|
&i.CoverArtPath,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CoverArtSource,
|
|
&i.CoverArtSourcesVersion,
|
|
&i.ReleaseGroupMbid,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
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, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid, 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.Album.CoverArtSource,
|
|
&i.Album.CoverArtSourcesVersion,
|
|
&i.Album.ReleaseGroupMbid,
|
|
&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
|
|
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version, release_group_mbid FROM albums
|
|
WHERE title ILIKE '%' || $1 || '%'
|
|
ORDER BY sort_title
|
|
LIMIT $2 OFFSET $3
|
|
`
|
|
|
|
type SearchAlbumsParams struct {
|
|
Column1 *string
|
|
Limit int32
|
|
Offset int32
|
|
}
|
|
|
|
func (q *Queries) SearchAlbums(ctx context.Context, arg SearchAlbumsParams) ([]Album, error) {
|
|
rows, err := q.db.Query(ctx, searchAlbums, arg.Column1, arg.Limit, arg.Offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Album
|
|
for rows.Next() {
|
|
var i Album
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.SortTitle,
|
|
&i.ArtistID,
|
|
&i.ReleaseDate,
|
|
&i.Mbid,
|
|
&i.CoverArtPath,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CoverArtSource,
|
|
&i.CoverArtSourcesVersion,
|
|
&i.ReleaseGroupMbid,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const setAlbumCoverWithVersion = `-- name: SetAlbumCoverWithVersion :exec
|
|
UPDATE albums
|
|
SET cover_art_path = $2,
|
|
cover_art_source = $3,
|
|
cover_art_sources_version = $4,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
`
|
|
|
|
type SetAlbumCoverWithVersionParams struct {
|
|
ID pgtype.UUID
|
|
CoverArtPath *string
|
|
CoverArtSource *string
|
|
CoverArtSourcesVersion int32
|
|
}
|
|
|
|
// M7 cover-sources: records a successful or settled enrichment with the
|
|
// current sources_version stamp. cover_art_path is NULL when source is
|
|
// 'none' (no file written). Atomic; called per album per pass.
|
|
func (q *Queries) SetAlbumCoverWithVersion(ctx context.Context, arg SetAlbumCoverWithVersionParams) error {
|
|
_, err := q.db.Exec(ctx, setAlbumCoverWithVersion,
|
|
arg.ID,
|
|
arg.CoverArtPath,
|
|
arg.CoverArtSource,
|
|
arg.CoverArtSourcesVersion,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const setAlbumMbidIfNull = `-- name: SetAlbumMbidIfNull :exec
|
|
UPDATE albums
|
|
SET mbid = $2,
|
|
updated_at = now()
|
|
WHERE id = $1 AND mbid IS NULL
|
|
`
|
|
|
|
type SetAlbumMbidIfNullParams struct {
|
|
ID pgtype.UUID
|
|
Mbid *string
|
|
}
|
|
|
|
// M7 #379: heal MBID on existing album rows during rescan. Idempotent —
|
|
// only updates when the existing mbid is NULL.
|
|
func (q *Queries) SetAlbumMbidIfNull(ctx context.Context, arg SetAlbumMbidIfNullParams) error {
|
|
_, err := q.db.Exec(ctx, setAlbumMbidIfNull, arg.ID, arg.Mbid)
|
|
return err
|
|
}
|
|
|
|
const setAlbumReleaseGroupMbidIfNull = `-- name: SetAlbumReleaseGroupMbidIfNull :exec
|
|
UPDATE albums
|
|
SET release_group_mbid = $2,
|
|
updated_at = now()
|
|
WHERE id = $1 AND release_group_mbid IS NULL
|
|
`
|
|
|
|
type SetAlbumReleaseGroupMbidIfNullParams struct {
|
|
ID pgtype.UUID
|
|
ReleaseGroupMbid *string
|
|
}
|
|
|
|
// #5241: heal the release-group id on an existing album row, from a rescan's
|
|
// tags or from the re-acquisition sweeper's MusicBrainz lookup. Only fills a
|
|
// NULL, so a tag never gets overwritten by a later guess or vice versa.
|
|
func (q *Queries) SetAlbumReleaseGroupMbidIfNull(ctx context.Context, arg SetAlbumReleaseGroupMbidIfNullParams) error {
|
|
_, err := q.db.Exec(ctx, setAlbumReleaseGroupMbidIfNull, arg.ID, arg.ReleaseGroupMbid)
|
|
return err
|
|
}
|
|
|
|
const setReleaseGroupForReleaseMbidIfNull = `-- name: SetReleaseGroupForReleaseMbidIfNull :exec
|
|
UPDATE albums
|
|
SET release_group_mbid = $1::text,
|
|
updated_at = now()
|
|
WHERE mbid = $2::text AND release_group_mbid IS NULL
|
|
`
|
|
|
|
type SetReleaseGroupForReleaseMbidIfNullParams struct {
|
|
ReleaseGroupMbid string
|
|
ReleaseMbid string
|
|
}
|
|
|
|
// #5241: cache a MusicBrainz-resolved release group onto the album row that
|
|
// holds the release, filling only a NULL.
|
|
func (q *Queries) SetReleaseGroupForReleaseMbidIfNull(ctx context.Context, arg SetReleaseGroupForReleaseMbidIfNullParams) error {
|
|
_, err := q.db.Exec(ctx, setReleaseGroupForReleaseMbidIfNull, arg.ReleaseGroupMbid, arg.ReleaseMbid)
|
|
return err
|
|
}
|
|
|
|
const upsertAlbum = `-- name: UpsertAlbum :one
|
|
INSERT INTO albums (title, sort_title, artist_id, release_date, mbid, cover_art_path, release_group_mbid)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
ON CONFLICT (mbid) WHERE mbid IS NOT NULL
|
|
DO UPDATE SET
|
|
title = EXCLUDED.title,
|
|
sort_title = EXCLUDED.sort_title,
|
|
artist_id = EXCLUDED.artist_id,
|
|
release_date = EXCLUDED.release_date,
|
|
cover_art_path = EXCLUDED.cover_art_path,
|
|
release_group_mbid = COALESCE(EXCLUDED.release_group_mbid, albums.release_group_mbid),
|
|
updated_at = now()
|
|
RETURNING id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version, release_group_mbid
|
|
`
|
|
|
|
type UpsertAlbumParams struct {
|
|
Title string
|
|
SortTitle string
|
|
ArtistID pgtype.UUID
|
|
ReleaseDate pgtype.Date
|
|
Mbid *string
|
|
CoverArtPath *string
|
|
ReleaseGroupMbid *string
|
|
}
|
|
|
|
func (q *Queries) UpsertAlbum(ctx context.Context, arg UpsertAlbumParams) (Album, error) {
|
|
row := q.db.QueryRow(ctx, upsertAlbum,
|
|
arg.Title,
|
|
arg.SortTitle,
|
|
arg.ArtistID,
|
|
arg.ReleaseDate,
|
|
arg.Mbid,
|
|
arg.CoverArtPath,
|
|
arg.ReleaseGroupMbid,
|
|
)
|
|
var i Album
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Title,
|
|
&i.SortTitle,
|
|
&i.ArtistID,
|
|
&i.ReleaseDate,
|
|
&i.Mbid,
|
|
&i.CoverArtPath,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CoverArtSource,
|
|
&i.CoverArtSourcesVersion,
|
|
&i.ReleaseGroupMbid,
|
|
)
|
|
return i, err
|
|
}
|