feat: one Missing files page under Collection: folders gone, each with its album's want, last search or download, and Search, Releases and Remove; an album held before its files went is no longer shown as held again (M504 #5626)
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 1m30s
release / go (push) Successful in 1m46s
release / integration (push) Successful in 5m23s
release / android (push) Successful in 5m43s
release / Build signed APK (releases and dev) (push) Successful in 6m2s
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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 14:44:33 -04:00
co-authored by Claude Opus 5.5
parent 38b2325d2d
commit ead0abf0cf
20 changed files with 702 additions and 390 deletions
+50 -3
View File
@@ -2,6 +2,7 @@ package api
import (
"net/http"
"time"
"github.com/jackc/pgx/v5/pgtype"
@@ -31,10 +32,13 @@ type missingTrackView struct {
// reacquisitionStateView is what the wanted list says about one album
// (milestone #290, M502 #5520): when it was wanted back, and when the library
// held it again. A missing album with no want yet is still inside the grace
// window, or MusicBrainz cannot name it.
// window, or MusicBrainz cannot name it. Want is that want as the wanted list
// sends it, so the Missing page can show its search and download and act on
// it (M504).
type reacquisitionStateView struct {
WantedAt *string `json:"wanted_at"`
HeldAt *string `json:"held_at"`
WantedAt *string `json:"wanted_at"`
HeldAt *string `json:"held_at"`
Want *wantView `json:"want"`
}
// missingGroupView is a directory's worth of missing tracks.
@@ -174,7 +178,13 @@ func (h *handlers) attachReacquisitionState(
// groupMissingByDirectory folded them in.
dirAlbum := make(map[string]pgtype.UUID, len(groups))
ids := make([]pgtype.UUID, 0, len(groups))
// The latest loss in each folder: a want held before it is the album
// as it was before its files went, not the library holding it again.
dirLost := make(map[string]time.Time, len(groups))
for _, row := range rows {
if row.MissingSince.Valid && row.MissingSince.Time.After(dirLost[row.Directory]) {
dirLost[row.Directory] = row.MissingSince.Time
}
if _, seen := dirAlbum[row.Directory]; seen {
continue
}
@@ -194,6 +204,7 @@ func (h *handlers) attachReacquisitionState(
byAlbum[uuidToString(w.AlbumID)] = w
}
}
full := h.wantViewsByID(r, q, byAlbum)
for i := range groups {
albumID, ok := dirAlbum[groups[i].Directory]
if !ok {
@@ -203,6 +214,12 @@ func (h *handlers) attachReacquisitionState(
if !ok {
continue
}
// Held before its files went (every library album is on the list,
// held, since #5567): not wanted back yet, which the sweep does once
// the grace window has passed.
if w.FulfilledAt.Valid && !w.FulfilledAt.Time.After(dirLost[groups[i].Directory]) {
continue
}
view := &reacquisitionStateView{}
wantedAt := formatTimestamp(w.WantedAt)
view.WantedAt = &wantedAt
@@ -210,6 +227,36 @@ func (h *handlers) attachReacquisitionState(
heldAt := formatTimestamp(w.FulfilledAt)
view.HeldAt = &heldAt
}
if v, ok := full[uuidToString(w.WantID)]; ok {
view.Want = &v
}
groups[i].Reacquisition = view
}
}
// wantViewsByID reads each album's current want in full, keyed by want id.
// Best-effort like the state it decorates: a failure leaves the groups with
// their dates and no want.
func (h *handlers) wantViewsByID(
r *http.Request,
q *dbq.Queries,
byAlbum map[string]dbq.GetWantsForAlbumsRow,
) map[string]wantView {
ids := make([]pgtype.UUID, 0, len(byAlbum))
for _, w := range byAlbum {
ids = append(ids, w.WantID)
}
out := make(map[string]wantView, len(ids))
if len(ids) == 0 {
return out
}
rows, err := q.ListWants(r.Context(), dbq.ListWantsParams{State: "all", WantIds: ids, MaxRows: int32(len(ids))})
if err != nil {
h.logger.Warn("admin missing: want detail lookup failed", "err", err)
return out
}
for _, row := range rows {
out[uuidToString(row.ID)] = wantViewOf(row)
}
return out
}