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
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:
+132
-50
@@ -69,8 +69,10 @@ type wantGrabView struct {
|
||||
ImportedFiles int32 `json:"imported_files"`
|
||||
}
|
||||
|
||||
// wantCounts sizes each tab of the Wanted page.
|
||||
// wantCounts sizes each tab of the Wanted page. Artists: how many artists
|
||||
// the list holds albums of, its main page (#5572).
|
||||
type wantCounts struct {
|
||||
Artists int64 `json:"artists"`
|
||||
Wanted int64 `json:"wanted"`
|
||||
Missing int64 `json:"missing"`
|
||||
Held int64 `json:"held"`
|
||||
@@ -90,6 +92,34 @@ type wantsPage struct {
|
||||
Counts wantCounts `json:"counts"`
|
||||
// Library is the one-time listing of the library's albums (#5567).
|
||||
Library libraryListingView `json:"library"`
|
||||
// Artist heads one artist's page, when the list is narrowed to them.
|
||||
Artist *wantArtistView `json:"artist,omitempty"`
|
||||
}
|
||||
|
||||
// wantArtistView is one artist on the Wanted view's main page (#5572): how
|
||||
// many of their albums the list holds, of how many, like Lidarr's artist
|
||||
// index.
|
||||
type wantArtistView struct {
|
||||
ArtistMBID string `json:"artist_mbid"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
SortName string `json:"sort_name"`
|
||||
ArtistID string `json:"artist_id,omitempty"`
|
||||
ImageURL string `json:"image_url"`
|
||||
Albums int32 `json:"albums"`
|
||||
Held int32 `json:"held"`
|
||||
Wanted int32 `json:"wanted"`
|
||||
Missing int32 `json:"missing"`
|
||||
Downloading int32 `json:"downloading"`
|
||||
Following bool `json:"following"`
|
||||
}
|
||||
|
||||
type wantArtistsPage struct {
|
||||
Items []wantArtistView `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
Counts wantCounts `json:"counts"`
|
||||
Library libraryListingView `json:"library"`
|
||||
}
|
||||
|
||||
// libraryListingView: whether the library's albums are all on the list
|
||||
@@ -113,77 +143,129 @@ const unmatchedShown = 200
|
||||
|
||||
const wantsPageSize = 50
|
||||
|
||||
// handleListWants implements GET /api/admin/wanted?state=wanted|missing|held&q=&offset=.
|
||||
// wantArtistsPageSize fills a few rows of the artist grid a page.
|
||||
const wantArtistsPageSize = 96
|
||||
|
||||
// wantsHeader is what every Wanted page carries: each tab's count, and how
|
||||
// the one-time library listing went. The albums no match was found for are
|
||||
// named only when asked (the main page's first page).
|
||||
func (h *handlers) wantsHeader(r *http.Request, q *dbq.Queries, unmatched bool) (wantCounts, libraryListingView, error) {
|
||||
counts, err := q.CountWants(r.Context())
|
||||
if err != nil {
|
||||
return wantCounts{}, libraryListingView{}, err
|
||||
}
|
||||
listing, err := q.GetLibraryListing(r.Context())
|
||||
if err != nil {
|
||||
return wantCounts{}, libraryListingView{}, err
|
||||
}
|
||||
lib := libraryListingView{Unlisted: listing.LibraryUnlisted, Unmatched: []unmatchedAlbumView{}}
|
||||
if listing.LibraryListedAt.Valid {
|
||||
at := formatTimestamp(listing.LibraryListedAt)
|
||||
lib.ListedAt = &at
|
||||
}
|
||||
if unmatched && listing.LibraryUnlisted > 0 {
|
||||
rows, err := q.ListUnmatchedLibraryAlbums(r.Context(), unmatchedShown)
|
||||
if err != nil {
|
||||
return wantCounts{}, libraryListingView{}, err
|
||||
}
|
||||
for _, u := range rows {
|
||||
lib.Unmatched = append(lib.Unmatched, unmatchedAlbumView{
|
||||
AlbumID: uuidToString(u.AlbumID), Title: u.Title, Artist: u.ArtistName, Detail: u.Detail,
|
||||
})
|
||||
}
|
||||
}
|
||||
return wantCounts{Artists: counts.Artists, Wanted: counts.Wanted, Missing: counts.Missing, Held: counts.Held,
|
||||
Following: counts.Following, Activity: counts.Activity, Attention: counts.Attention, Blocked: counts.Blocked}, lib, nil
|
||||
}
|
||||
|
||||
func offsetParam(r *http.Request) int {
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
return max(offset, 0)
|
||||
}
|
||||
|
||||
func wantArtistViewOf(row dbq.ListWantArtistsRow) wantArtistView {
|
||||
v := wantArtistView{
|
||||
ArtistMBID: row.ArtistMbid, ArtistName: row.ArtistName, SortName: row.SortName,
|
||||
ImageURL: artistArtURL(row.ArtistMbid, row.ArtistName),
|
||||
Albums: row.Albums, Held: row.Held, Wanted: row.Wanted, Missing: row.Missing,
|
||||
Downloading: row.Downloading, Following: row.Following,
|
||||
}
|
||||
if row.LibraryArtistID.Valid {
|
||||
v.ArtistID = uuidToString(row.LibraryArtistID)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// handleListWantArtists implements GET /api/admin/wanted/artists?q=&offset=:
|
||||
// the Wanted view's main page, every artist on the list A–Z (#5572).
|
||||
func (h *handlers) handleListWantArtists(w http.ResponseWriter, r *http.Request) {
|
||||
offset := offsetParam(r)
|
||||
q := dbq.New(h.pool)
|
||||
rows, err := q.ListWantArtists(r.Context(), dbq.ListWantArtistsParams{
|
||||
Q: strings.TrimSpace(r.URL.Query().Get("q")), Skip: int32(offset), MaxRows: wantArtistsPageSize})
|
||||
if err != nil {
|
||||
writeErrWithLog(w, h.logger, "admin wanted: artists", apierror.Internal(err))
|
||||
return
|
||||
}
|
||||
counts, lib, err := h.wantsHeader(r, q, offset == 0)
|
||||
if err != nil {
|
||||
writeErrWithLog(w, h.logger, "admin wanted: counts", apierror.Internal(err))
|
||||
return
|
||||
}
|
||||
out := wantArtistsPage{Items: make([]wantArtistView, 0, len(rows)), Limit: wantArtistsPageSize, Offset: offset,
|
||||
Counts: counts, Library: lib}
|
||||
for _, row := range rows {
|
||||
out.Total = row.Total
|
||||
out.Items = append(out.Items, wantArtistViewOf(row))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// handleListWants implements
|
||||
// GET /api/admin/wanted?state=wanted|missing|held|all&artist=&q=&offset=.
|
||||
// wanted is everything open but missing files. Each list runs artist by
|
||||
// artist, A–Z, like the library (#5572); q narrows it to an artist or album.
|
||||
// artist, A–Z, like the library (#5572); q narrows it to an artist or
|
||||
// album, and artist to one artist's page, which it then heads.
|
||||
func (h *handlers) handleListWants(w http.ResponseWriter, r *http.Request) {
|
||||
state := r.URL.Query().Get("state")
|
||||
if state == "" {
|
||||
state = "wanted"
|
||||
}
|
||||
if state != "wanted" && state != "missing" && state != "held" {
|
||||
writeErr(w, apierror.BadRequest("bad_state", "state must be wanted, missing or held"))
|
||||
if state != "wanted" && state != "missing" && state != "held" && state != "all" {
|
||||
writeErr(w, apierror.BadRequest("bad_state", "state must be wanted, missing, held or all"))
|
||||
return
|
||||
}
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
offset := offsetParam(r)
|
||||
artist := strings.TrimSpace(r.URL.Query().Get("artist"))
|
||||
q := dbq.New(h.pool)
|
||||
filter := strings.TrimSpace(r.URL.Query().Get("q"))
|
||||
rows, err := q.ListWants(r.Context(), dbq.ListWantsParams{State: state, Q: filter, Skip: int32(offset), MaxRows: wantsPageSize})
|
||||
rows, err := q.ListWants(r.Context(), dbq.ListWantsParams{State: state, ArtistMbid: artist,
|
||||
Q: strings.TrimSpace(r.URL.Query().Get("q")), Skip: int32(offset), MaxRows: wantsPageSize})
|
||||
if err != nil {
|
||||
writeErrWithLog(w, h.logger, "admin wanted: list", apierror.Internal(err))
|
||||
return
|
||||
}
|
||||
counts, err := q.CountWants(r.Context())
|
||||
counts, lib, err := h.wantsHeader(r, q, false)
|
||||
if err != nil {
|
||||
writeErrWithLog(w, h.logger, "admin wanted: count", apierror.Internal(err))
|
||||
writeErrWithLog(w, h.logger, "admin wanted: counts", apierror.Internal(err))
|
||||
return
|
||||
}
|
||||
listing, err := q.GetLibraryListing(r.Context())
|
||||
if err != nil {
|
||||
writeErrWithLog(w, h.logger, "admin wanted: library listing", apierror.Internal(err))
|
||||
return
|
||||
}
|
||||
out := wantsPage{
|
||||
Library: libraryListingView{Unlisted: listing.LibraryUnlisted},
|
||||
Items: make([]wantView, 0, len(rows)), Limit: wantsPageSize, Offset: offset,
|
||||
Counts: wantCounts{Wanted: counts.Wanted, Missing: counts.Missing, Held: counts.Held, Following: counts.Following,
|
||||
Activity: counts.Activity, Attention: counts.Attention, Blocked: counts.Blocked},
|
||||
}
|
||||
out.Library.Unmatched = []unmatchedAlbumView{}
|
||||
if listing.LibraryListedAt.Valid {
|
||||
at := formatTimestamp(listing.LibraryListedAt)
|
||||
out.Library.ListedAt = &at
|
||||
}
|
||||
if state == "held" && offset == 0 && listing.LibraryUnlisted > 0 {
|
||||
unmatched, err := q.ListUnmatchedLibraryAlbums(r.Context(), unmatchedShown)
|
||||
out := wantsPage{Library: lib, Counts: counts,
|
||||
Items: make([]wantView, 0, len(rows)), Limit: wantsPageSize, Offset: offset}
|
||||
if artist != "" {
|
||||
heads, err := q.ListWantArtists(r.Context(), dbq.ListWantArtistsParams{ArtistMbid: artist, MaxRows: 1})
|
||||
if err != nil {
|
||||
writeErrWithLog(w, h.logger, "admin wanted: unmatched albums", apierror.Internal(err))
|
||||
writeErrWithLog(w, h.logger, "admin wanted: artist", apierror.Internal(err))
|
||||
return
|
||||
}
|
||||
for _, u := range unmatched {
|
||||
out.Library.Unmatched = append(out.Library.Unmatched, unmatchedAlbumView{
|
||||
AlbumID: uuidToString(u.AlbumID), Title: u.Title, Artist: u.ArtistName, Detail: u.Detail,
|
||||
})
|
||||
}
|
||||
}
|
||||
switch state {
|
||||
case "missing":
|
||||
out.Total = counts.Missing
|
||||
case "held":
|
||||
out.Total = counts.Held
|
||||
default:
|
||||
out.Total = counts.Wanted
|
||||
}
|
||||
if filter != "" {
|
||||
out.Total = 0
|
||||
if len(rows) > 0 {
|
||||
out.Total = rows[0].Total
|
||||
if len(heads) == 0 {
|
||||
writeErr(w, apierror.NotFound("artist"))
|
||||
return
|
||||
}
|
||||
v := wantArtistViewOf(heads[0])
|
||||
out.Artist = &v
|
||||
}
|
||||
for _, row := range rows {
|
||||
out.Total = row.Total
|
||||
v := wantView{
|
||||
ID: uuidToString(row.ID), ReleaseGroupMBID: row.ReleaseGroupMbid, Title: row.Title,
|
||||
ArtistMBID: row.ArtistMbid, ArtistName: row.ArtistName,
|
||||
|
||||
@@ -24,6 +24,7 @@ func doWantedReq(t *testing.T, h *handlers, path string, user dbq.User) *httptes
|
||||
r.Route("/api/admin", func(admin chi.Router) {
|
||||
admin.Use(auth.RequireAdmin())
|
||||
admin.Get("/wanted", h.handleListWants)
|
||||
admin.Get("/wanted/artists", h.handleListWantArtists)
|
||||
admin.Get("/wanted/{id}/tracks", h.handleWantTracks)
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
@@ -98,6 +99,75 @@ func TestListWants_ArtistsAToZThenTheirAlbumsByDate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The Collection's catalogue (#5572): every artist on the list A–Z, each
|
||||
// with how many of their albums are held; an artist's page lists every album
|
||||
// of theirs, held or not, under that artist.
|
||||
func TestListWantArtists_CatalogueAndAnArtistsPage(t *testing.T) {
|
||||
h, _ := testHandlersWithClientFn(t)
|
||||
admin := seedUser(t, h.pool, "admin", "pw", true)
|
||||
ctx := context.Background()
|
||||
q := dbq.New(h.pool)
|
||||
beatles := "a0000000-0000-4000-8000-000000000002"
|
||||
camel := "a0000000-0000-4000-8000-000000000001"
|
||||
if _, err := q.UpsertArtist(ctx, dbq.UpsertArtistParams{Name: "The Beatles", SortName: "Beatles, The", Mbid: &beatles}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedWant(t, h, "g0000000-0000-4000-8000-000000000002", beatles, "The Beatles", "Abbey Road", "1969")
|
||||
held := seedWant(t, h, "g0000000-0000-4000-8000-000000000003", beatles, "The Beatles", "Please Please Me", "1963")
|
||||
if _, err := h.pool.Exec(ctx, `UPDATE wants SET fulfilled_at = now() WHERE id = $1`, held.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedWant(t, h, "g0000000-0000-4000-8000-000000000004", camel, "Camel", "Moonmadness", "1976")
|
||||
if _, err := h.pool.Exec(ctx, `INSERT INTO follows (artist_mbid, artist_name, profile_id)
|
||||
SELECT $1, 'Camel', id FROM release_type_profiles WHERE is_default`, camel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w := doWantedReq(t, h, "/api/admin/wanted/artists", admin)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d; body = %s", w.Code, w.Body.String())
|
||||
}
|
||||
var page wantArtistsPage
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &page); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(page.Items) != 2 || page.Total != 2 || page.Counts.Artists != 2 {
|
||||
t.Fatalf("catalogue = %+v, total %d, artists %d", page.Items, page.Total, page.Counts.Artists)
|
||||
}
|
||||
b, c := page.Items[0], page.Items[1]
|
||||
if b.ArtistName != "The Beatles" || b.SortName != "Beatles, The" || b.Albums != 2 || b.Held != 1 || b.Wanted != 1 ||
|
||||
b.ArtistID == "" || b.Following || b.ImageURL == "" {
|
||||
t.Errorf("Beatles = %+v", b)
|
||||
}
|
||||
if c.ArtistName != "Camel" || c.Albums != 1 || c.Held != 0 || !c.Following || c.ArtistID != "" {
|
||||
t.Errorf("Camel = %+v", c)
|
||||
}
|
||||
|
||||
// q finds an artist by an album's title.
|
||||
w = doWantedReq(t, h, "/api/admin/wanted/artists?q=moon", admin)
|
||||
page = wantArtistsPage{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &page); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(page.Items) != 1 || page.Items[0].ArtistName != "Camel" {
|
||||
t.Errorf("q=moon: %+v", page.Items)
|
||||
}
|
||||
|
||||
// The Beatles' page: both albums, held or not, headed by the artist.
|
||||
w = doWantedReq(t, h, "/api/admin/wanted?state=all&artist="+beatles, admin)
|
||||
var albums wantsPage
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &albums); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(albums.Items) != 2 || albums.Items[0].Title != "Please Please Me" || albums.Items[0].HeldAt == nil ||
|
||||
albums.Artist == nil || albums.Artist.Held != 1 {
|
||||
t.Errorf("artist page = %+v, head %+v", albums.Items, albums.Artist)
|
||||
}
|
||||
if w := doWantedReq(t, h, "/api/admin/wanted?state=all&artist=nobody", admin); w.Code != http.StatusNotFound {
|
||||
t.Errorf("an artist with nothing on the list: status %d, want 404", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeEditions is a MusicBrainz holding one group with two editions, the
|
||||
// shorter official one the standard, or 503 when down is set.
|
||||
func fakeEditions(t *testing.T, group string, down bool) *httptest.Server {
|
||||
|
||||
@@ -252,6 +252,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev
|
||||
// for, the artists it follows, and the release-type
|
||||
// profiles that decide which of their releases count.
|
||||
admin.Get("/wanted", h.handleListWants)
|
||||
admin.Get("/wanted/artists", h.handleListWantArtists)
|
||||
admin.Delete("/wanted/{id}", h.handleRemoveWant)
|
||||
// Searching and grabbing (M502 #5521).
|
||||
admin.Get("/acquisition/settings", h.handleGetSearchSettings)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user