feat: the Wanted view becomes Collection, shaped like Lidarr: an artist catalogue first, the state sections down a left rail (M502 #5577)
release / govulncheck (push) Successful in 21s
release / web (push) Successful in 1m39s
release / go (push) Successful in 1m54s
release / integration (push) Successful in 5m33s
release / android (push) Successful in 5m40s
release / Build signed APK (releases and dev) (push) Successful in 5m56s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped

The section opens on a catalogue of every artist on the list, A–Z with a
letter rail, each card showing how many of their albums are held. An artist
opens on their page: every album of theirs, held or not, with its status,
search, releases and tracks. Wanted, Missing files, Activity, Following and
Blocked sit down a left rail as the sections that show state; Held is gone,
the catalogue shows it. The section moves from /wanted to /collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 21:14:33 -04:00
co-authored by Claude Opus 5.5
parent a4fbe44bd5
commit 358849de77
34 changed files with 994 additions and 356 deletions
+108 -9
View File
@@ -37,6 +37,7 @@ const countWants = `-- 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,
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL)::bigint AS held,
COUNT(DISTINCT artist_mbid)::bigint AS artists,
(SELECT COUNT(*) FROM follows)::bigint AS following,
-- The Wanted view's other tabs (#5522): downloads running, those
-- waiting on a person, and releases set aside.
@@ -52,6 +53,7 @@ type CountWantsRow struct {
Wanted int64
Missing int64
Held int64
Artists int64
Following int64
Activity int64
Attention int64
@@ -65,6 +67,7 @@ func (q *Queries) CountWants(ctx context.Context) (CountWantsRow, error) {
&i.Wanted,
&i.Missing,
&i.Held,
&i.Artists,
&i.Following,
&i.Activity,
&i.Attention,
@@ -852,6 +855,98 @@ func (q *Queries) ListUnmatchedLibraryAlbums(ctx context.Context, maxRows int32)
return items, nil
}
const listWantArtists = `-- name: ListWantArtists :many
WITH a AS (
SELECT w.artist_mbid,
(array_agg(w.artist_name ORDER BY w.wanted_at DESC))[1] AS artist_name,
COUNT(*) FILTER (WHERE w.fulfilled_at IS NOT NULL)::int AS held,
COUNT(*) FILTER (WHERE w.fulfilled_at IS NULL AND w.reason <> 'missing')::int AS wanted,
COUNT(*) FILTER (WHERE w.fulfilled_at IS NULL AND w.reason = 'missing')::int AS missing,
COUNT(*)::int AS albums,
bool_or($1::text <> '' AND strpos(lower(w.title), lower($1::text)) > 0) AS title_match
FROM wants w
WHERE $4::text = '' OR w.artist_mbid = $4::text
GROUP BY w.artist_mbid
)
SELECT a.artist_mbid, a.artist_name::text AS artist_name, a.held, a.wanted, a.missing, a.albums,
COALESCE(NULLIF(la.sort_name, ''), a.artist_name)::text AS sort_name,
la.id AS library_artist_id,
(f.artist_mbid IS NOT NULL)::bool AS following,
(SELECT COUNT(*) FROM grabs g JOIN wants x ON x.id = g.want_id
WHERE x.artist_mbid = a.artist_mbid AND g.ended_at IS NULL)::int AS downloading,
COUNT(*) OVER () AS total
FROM a
LEFT JOIN LATERAL (SELECT ar.id, ar.sort_name FROM artists ar WHERE ar.mbid = a.artist_mbid LIMIT 1) la ON true
LEFT JOIN follows f ON f.artist_mbid = a.artist_mbid
WHERE $1::text = ''
OR strpos(lower(a.artist_name), lower($1::text)) > 0
OR a.title_match
ORDER BY lower(COALESCE(NULLIF(la.sort_name, ''), a.artist_name)), a.artist_mbid
LIMIT $3 OFFSET $2
`
type ListWantArtistsParams struct {
Q string
Skip int32
MaxRows int32
ArtistMbid string
}
type ListWantArtistsRow struct {
ArtistMbid string
ArtistName string
Held int32
Wanted int32
Missing int32
Albums int32
SortName string
LibraryArtistID pgtype.UUID
Following bool
Downloading int32
Total int64
}
// The Wanted view's main page (#5572): every artist on the list, A–Z like
// the library, with how many of their albums are held of how many are on
// the list. q matches the artist's name or any of their albums' titles;
// artist_mbid narrows to one artist, for their page's header.
func (q *Queries) ListWantArtists(ctx context.Context, arg ListWantArtistsParams) ([]ListWantArtistsRow, error) {
rows, err := q.db.Query(ctx, listWantArtists,
arg.Q,
arg.Skip,
arg.MaxRows,
arg.ArtistMbid,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListWantArtistsRow
for rows.Next() {
var i ListWantArtistsRow
if err := rows.Scan(
&i.ArtistMbid,
&i.ArtistName,
&i.Held,
&i.Wanted,
&i.Missing,
&i.Albums,
&i.SortName,
&i.LibraryArtistID,
&i.Following,
&i.Downloading,
&i.Total,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listWants = `-- name: ListWants :many
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,
@@ -879,24 +974,27 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
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
WHEN 'all' THEN true
WHEN 'held' THEN w.fulfilled_at IS NOT NULL
WHEN 'missing' THEN w.fulfilled_at IS NULL AND w.reason = 'missing'
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
END
AND ($2::text = ''
OR strpos(lower(w.artist_name), lower($2::text)) > 0
OR strpos(lower(w.title), lower($2::text)) > 0)
AND ($2::text = '' OR w.artist_mbid = $2::text)
AND ($3::text = ''
OR strpos(lower(w.artist_name), lower($3::text)) > 0
OR strpos(lower(w.title), lower($3::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
LIMIT $5 OFFSET $4
`
type ListWantsParams struct {
State string
Q string
Skip int32
MaxRows int32
State string
ArtistMbid string
Q string
Skip int32
MaxRows int32
}
type ListWantsRow struct {
@@ -937,13 +1035,14 @@ type ListWantsRow struct {
}
// The Wanted page: open wants (all but missing ones, or only missing ones),
// or the held ones. Ordered like the library (#5572): artists A–Z by the
// the held ones, or all of them; artist_mbid narrows to one artist's page. 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.ArtistMbid,
arg.Q,
arg.Skip,
arg.MaxRows,
+37 -1
View File
@@ -177,7 +177,7 @@ DELETE FROM wants WHERE id = $1;
-- name: ListWants :many
-- The Wanted page: open wants (all but missing ones, or only missing ones),
-- or the held ones. Ordered like the library (#5572): artists A–Z by the
-- the held ones, or all of them; artist_mbid narrows to one artist's page. 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.
@@ -207,10 +207,12 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
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
WHEN 'all' THEN true
WHEN 'held' THEN w.fulfilled_at IS NOT NULL
WHEN 'missing' THEN w.fulfilled_at IS NULL AND w.reason = 'missing'
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
END
AND (sqlc.arg(artist_mbid)::text = '' OR w.artist_mbid = sqlc.arg(artist_mbid)::text)
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)
@@ -219,6 +221,39 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
lower(w.title), w.id
LIMIT sqlc.arg(max_rows) OFFSET sqlc.arg(skip);
-- name: ListWantArtists :many
-- The Wanted view's main page (#5572): every artist on the list, A–Z like
-- the library, with how many of their albums are held of how many are on
-- the list. q matches the artist's name or any of their albums' titles;
-- artist_mbid narrows to one artist, for their page's header.
WITH a AS (
SELECT w.artist_mbid,
(array_agg(w.artist_name ORDER BY w.wanted_at DESC))[1] AS artist_name,
COUNT(*) FILTER (WHERE w.fulfilled_at IS NOT NULL)::int AS held,
COUNT(*) FILTER (WHERE w.fulfilled_at IS NULL AND w.reason <> 'missing')::int AS wanted,
COUNT(*) FILTER (WHERE w.fulfilled_at IS NULL AND w.reason = 'missing')::int AS missing,
COUNT(*)::int AS albums,
bool_or(sqlc.arg(q)::text <> '' AND strpos(lower(w.title), lower(sqlc.arg(q)::text)) > 0) AS title_match
FROM wants w
WHERE sqlc.arg(artist_mbid)::text = '' OR w.artist_mbid = sqlc.arg(artist_mbid)::text
GROUP BY w.artist_mbid
)
SELECT a.artist_mbid, a.artist_name::text AS artist_name, a.held, a.wanted, a.missing, a.albums,
COALESCE(NULLIF(la.sort_name, ''), a.artist_name)::text AS sort_name,
la.id AS library_artist_id,
(f.artist_mbid IS NOT NULL)::bool AS following,
(SELECT COUNT(*) FROM grabs g JOIN wants x ON x.id = g.want_id
WHERE x.artist_mbid = a.artist_mbid AND g.ended_at IS NULL)::int AS downloading,
COUNT(*) OVER () AS total
FROM a
LEFT JOIN LATERAL (SELECT ar.id, ar.sort_name FROM artists ar WHERE ar.mbid = a.artist_mbid LIMIT 1) la ON true
LEFT JOIN follows f ON f.artist_mbid = a.artist_mbid
WHERE sqlc.arg(q)::text = ''
OR strpos(lower(a.artist_name), lower(sqlc.arg(q)::text)) > 0
OR a.title_match
ORDER BY lower(COALESCE(NULLIF(la.sort_name, ''), a.artist_name)), a.artist_mbid
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;
@@ -235,6 +270,7 @@ SELECT title, disc_number, track_number, duration_ms, (missing_since IS NOT NULL
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,
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL)::bigint AS held,
COUNT(DISTINCT artist_mbid)::bigint AS artists,
(SELECT COUNT(*) FROM follows)::bigint AS following,
-- The Wanted view's other tabs (#5522): downloads running, those
-- waiting on a person, and releases set aside.