feat: the Wanted view lists artists A–Z with their albums inside, each album's tracks a click away (M502 #5572)
release / govulncheck (push) Successful in 21s
release / web (push) Successful in 1m20s
release / go (push) Successful in 1m38s
release / integration (push) Successful in 5m35s
release / android (push) Successful in 5m59s
release / Build signed APK (releases and dev) (push) Successful in 6m10s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 26s
release / Verify release artifacts (tag releases only) (push) Skipped

The list runs like the library: artists by the library's sort name, each
artist's albums oldest first, grouped under the artist (linked into the
library when it holds them). A filter narrows it to an artist or album by
name, on the server, so it finds what has not been scrolled to. Tracks
opens an album's track list: the library's own for an album it holds, a
lost file marked, otherwise MusicBrainz's standard edition, named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 18:43:32 -04:00
co-authored by Claude Opus 5.5
parent ca57133c8a
commit a4fbe44bd5
11 changed files with 753 additions and 115 deletions
+84 -5
View File
@@ -273,6 +273,24 @@ func (q *Queries) GetReleaseTypeProfile(ctx context.Context, id pgtype.UUID) (Re
return i, err
}
const getWantAlbum = `-- name: GetWantAlbum :one
SELECT id, release_group_mbid, album_id FROM wants WHERE id = $1
`
type GetWantAlbumRow struct {
ID pgtype.UUID
ReleaseGroupMbid string
AlbumID pgtype.UUID
}
// What the Wanted view's track list needs of a want (#5572).
func (q *Queries) GetWantAlbum(ctx context.Context, id pgtype.UUID) (GetWantAlbumRow, error) {
row := q.db.QueryRow(ctx, getWantAlbum, id)
var i GetWantAlbumRow
err := row.Scan(&i.ID, &i.ReleaseGroupMbid, &i.AlbumID)
return i, err
}
const getWantByReleaseGroup = `-- name: GetWantByReleaseGroup :one
SELECT id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error FROM wants WHERE release_group_mbid = $1
`
@@ -616,6 +634,49 @@ func (q *Queries) ListFollowsDue(ctx context.Context, arg ListFollowsDueParams)
return items, nil
}
const listLibraryAlbumTracks = `-- name: ListLibraryAlbumTracks :many
SELECT title, disc_number, track_number, duration_ms, (missing_since IS NOT NULL)::bool AS missing
FROM tracks
WHERE album_id = $1
ORDER BY disc_number NULLS FIRST, track_number NULLS LAST, title
`
type ListLibraryAlbumTracksRow struct {
Title string
DiscNumber *int32
TrackNumber *int32
DurationMs int32
Missing bool
}
// A held or missing album's own tracks, for the Wanted view's track list;
// missing says its file is gone.
func (q *Queries) ListLibraryAlbumTracks(ctx context.Context, albumID pgtype.UUID) ([]ListLibraryAlbumTracksRow, error) {
rows, err := q.db.Query(ctx, listLibraryAlbumTracks, albumID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListLibraryAlbumTracksRow
for rows.Next() {
var i ListLibraryAlbumTracksRow
if err := rows.Scan(
&i.Title,
&i.DiscNumber,
&i.TrackNumber,
&i.DurationMs,
&i.Missing,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listLibraryAlbums = `-- name: ListLibraryAlbums :execrows
INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, reason, album_id, wanted_at, fulfilled_at)
SELECT DISTINCT ON (a.release_group_mbid)
@@ -800,7 +861,8 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date,
u.username AS requested_by,
(SELECT ar.id FROM artists ar WHERE ar.mbid = w.artist_mbid) AS library_artist_id,
la.id AS library_artist_id,
COUNT(*) OVER () AS total,
-- The search schedule's last look (#5521), and the download running.
w.searched_at, w.search_outcome, w.search_considered, w.best_release, w.best_verdict, w.search_error,
-- Its latest download, running or ended, and what the import made of
@@ -813,6 +875,7 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
FROM wants w
LEFT JOIN users u ON u.id = w.created_by
LEFT JOIN mb_release_groups g ON g.mbid = w.release_group_mbid
LEFT JOIN LATERAL (SELECT ar.id, ar.sort_name FROM artists ar WHERE ar.mbid = w.artist_mbid LIMIT 1) la ON true
LEFT JOIN grabs gr ON gr.id = (SELECT x.id FROM grabs x WHERE x.want_id = w.id
ORDER BY x.ended_at IS NULL DESC, x.grabbed_at DESC LIMIT 1)
WHERE CASE $1::text
@@ -820,12 +883,18 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
WHEN 'missing' THEN w.fulfilled_at IS NULL AND w.reason = 'missing'
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
END
ORDER BY COALESCE(w.fulfilled_at, w.wanted_at) DESC, w.id
LIMIT $3 OFFSET $2
AND ($2::text = ''
OR strpos(lower(w.artist_name), lower($2::text)) > 0
OR strpos(lower(w.title), lower($2::text)) > 0)
ORDER BY lower(COALESCE(NULLIF(la.sort_name, ''), w.artist_name)), w.artist_mbid,
NULLIF(COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, ''), '') NULLS LAST,
lower(w.title), w.id
LIMIT $4 OFFSET $3
`
type ListWantsParams struct {
State string
Q string
Skip int32
MaxRows int32
}
@@ -845,6 +914,7 @@ type ListWantsRow struct {
FirstReleaseDate string
RequestedBy *string
LibraryArtistID pgtype.UUID
Total int64
SearchedAt pgtype.Timestamptz
SearchOutcome string
SearchConsidered int32
@@ -867,9 +937,17 @@ type ListWantsRow struct {
}
// The Wanted page: open wants (all but missing ones, or only missing ones),
// or what was held most recently.
// or the held ones. Ordered like the library (#5572): artists A–Z by the
// library's sort name when it holds the artist, each artist's albums by
// release date, so the page groups them as they arrive. q narrows to an
// artist or album title; total counts every match, past the page.
func (q *Queries) ListWants(ctx context.Context, arg ListWantsParams) ([]ListWantsRow, error) {
rows, err := q.db.Query(ctx, listWants, arg.State, arg.Skip, arg.MaxRows)
rows, err := q.db.Query(ctx, listWants,
arg.State,
arg.Q,
arg.Skip,
arg.MaxRows,
)
if err != nil {
return nil, err
}
@@ -892,6 +970,7 @@ func (q *Queries) ListWants(ctx context.Context, arg ListWantsParams) ([]ListWan
&i.FirstReleaseDate,
&i.RequestedBy,
&i.LibraryArtistID,
&i.Total,
&i.SearchedAt,
&i.SearchOutcome,
&i.SearchConsidered,
+25 -3
View File
@@ -177,7 +177,10 @@ DELETE FROM wants WHERE id = $1;
-- name: ListWants :many
-- The Wanted page: open wants (all but missing ones, or only missing ones),
-- or what was held most recently.
-- or the held ones. Ordered like the library (#5572): artists A–Z by the
-- library's sort name when it holds the artist, each artist's albums by
-- release date, so the page groups them as they arrive. q narrows to an
-- artist or album title; total counts every match, past the page.
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reason,
w.album_id, w.wanted_at, w.fulfilled_at,
-- A request names an album before the catalogue has read it: the
@@ -186,7 +189,8 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date,
u.username AS requested_by,
(SELECT ar.id FROM artists ar WHERE ar.mbid = w.artist_mbid) AS library_artist_id,
la.id AS library_artist_id,
COUNT(*) OVER () AS total,
-- The search schedule's last look (#5521), and the download running.
w.searched_at, w.search_outcome, w.search_considered, w.best_release, w.best_verdict, w.search_error,
-- Its latest download, running or ended, and what the import made of
@@ -199,6 +203,7 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
FROM wants w
LEFT JOIN users u ON u.id = w.created_by
LEFT JOIN mb_release_groups g ON g.mbid = w.release_group_mbid
LEFT JOIN LATERAL (SELECT ar.id, ar.sort_name FROM artists ar WHERE ar.mbid = w.artist_mbid LIMIT 1) la ON true
LEFT JOIN grabs gr ON gr.id = (SELECT x.id FROM grabs x WHERE x.want_id = w.id
ORDER BY x.ended_at IS NULL DESC, x.grabbed_at DESC LIMIT 1)
WHERE CASE sqlc.arg(state)::text
@@ -206,9 +211,26 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
WHEN 'missing' THEN w.fulfilled_at IS NULL AND w.reason = 'missing'
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
END
ORDER BY COALESCE(w.fulfilled_at, w.wanted_at) DESC, w.id
AND (sqlc.arg(q)::text = ''
OR strpos(lower(w.artist_name), lower(sqlc.arg(q)::text)) > 0
OR strpos(lower(w.title), lower(sqlc.arg(q)::text)) > 0)
ORDER BY lower(COALESCE(NULLIF(la.sort_name, ''), w.artist_name)), w.artist_mbid,
NULLIF(COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, ''), '') NULLS LAST,
lower(w.title), w.id
LIMIT sqlc.arg(max_rows) OFFSET sqlc.arg(skip);
-- name: GetWantAlbum :one
-- What the Wanted view's track list needs of a want (#5572).
SELECT id, release_group_mbid, album_id FROM wants WHERE id = $1;
-- name: ListLibraryAlbumTracks :many
-- A held or missing album's own tracks, for the Wanted view's track list;
-- missing says its file is gone.
SELECT title, disc_number, track_number, duration_ms, (missing_since IS NOT NULL)::bool AS missing
FROM tracks
WHERE album_id = $1
ORDER BY disc_number NULLS FIRST, track_number NULLS LAST, title;
-- name: CountWants :one
SELECT COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason <> 'missing')::bigint AS wanted,
COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason = 'missing')::bigint AS missing,