feat: a held album below the quality you settle for, or holding a video rip, stays wanted for a better copy, and the import replaces the old tracks with their likes and plays carried over (M502 #5524)
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m23s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 5m20s
release / android (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 5m30s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 27s
release / Verify release artifacts (tag releases only) (push) Skipped

- acquisition_settings.upgrade_until (default High quality lossy, Lidarr's
  cutoff, note #5527; "" is off), an "Upgrade until" row under Searching.
- The wanted worker judges held albums hourly: tier per format from size per
  second (the scanner records no bitrate), the album's worst. Below the
  cutoff it is wanted for quality; a video rip among its tracks always.
- The schedule searches upgrades weekly, after open wants; a release no
  better than the copy held is refused ("not_better").
- The import matches each file to the track it would replace (recording,
  then disc/track) and replaces it when it is a rip or the file is a better
  tier: over the same path in one rename, or filed beside it and the old
  track folded in (library.ReplaceTrack: file first, then foldTrackInto).
  Nothing better: the release is set aside.
- Collection › Upgrades (Lidarr's Cutoff Unmet); Suspect sources says when a
  clean copy was looked for instead of Lidarr's search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 12:24:59 -04:00
co-authored by Claude Opus 5.5
parent a5ad301c94
commit c54792c51e
45 changed files with 1574 additions and 99 deletions
+13 -6
View File
@@ -39,6 +39,10 @@ type wantView struct {
WantedAt string `json:"wanted_at"`
HeldAt *string `json:"held_at"`
ImageURL string `json:"image_url"`
// Upgrade: why a held album is wanted again, quality or rip, and
// HeldQuality what the library's copy is (#5524).
Upgrade string `json:"upgrade,omitempty"`
HeldQuality string `json:"held_quality,omitempty"`
// Search is the schedule's last look; Grab the download running (#5521).
Search *wantSearchView `json:"search"`
Grab *wantGrabView `json:"grab"`
@@ -80,6 +84,8 @@ type wantCounts struct {
Wanted int64 `json:"wanted"`
Missing int64 `json:"missing"`
Held int64 `json:"held"`
// Upgrades: held albums wanted for a better copy (#5524).
Upgrades int64 `json:"upgrades"`
Following int64 `json:"following"`
// Activity: downloads running; Attention: of those, the ones waiting on
// a person; Blocked: releases set aside (#5522).
@@ -173,7 +179,7 @@ func (h *handlers) wantsHeader(r *http.Request, q *dbq.Queries) (wantCounts, lib
lib.ListedAt = &at
}
return wantCounts{Artists: counts.Artists, Unmatched: counts.Unmatched, Wanted: counts.Wanted, Missing: counts.Missing, Held: counts.Held,
Following: counts.Following, Activity: counts.Activity, Attention: counts.Attention, Blocked: counts.Blocked}, lib, nil
Upgrades: counts.Upgrades, Following: counts.Following, Activity: counts.Activity, Attention: counts.Attention, Blocked: counts.Blocked}, lib, nil
}
func offsetParam(r *http.Request) int {
@@ -220,8 +226,9 @@ func (h *handlers) handleListWantArtists(w http.ResponseWriter, r *http.Request)
}
// 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
// GET /api/admin/wanted?state=wanted|missing|upgrade|held|all&artist=&q=&offset=.
// wanted is everything open but missing files; upgrade the held albums
// wanted for a better copy (#5524). Each list runs artist by
// 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) {
@@ -229,8 +236,8 @@ func (h *handlers) handleListWants(w http.ResponseWriter, r *http.Request) {
if state == "" {
state = "wanted"
}
if state != "wanted" && state != "missing" && state != "held" && state != "all" {
writeErr(w, apierror.BadRequest("bad_state", "state must be wanted, missing, held or all"))
if state != "wanted" && state != "missing" && state != "upgrade" && state != "held" && state != "all" {
writeErr(w, apierror.BadRequest("bad_state", "state must be wanted, missing, upgrade, held or all"))
return
}
offset := offsetParam(r)
@@ -269,7 +276,7 @@ func (h *handlers) handleListWants(w http.ResponseWriter, r *http.Request) {
ArtistMBID: row.ArtistMbid, ArtistName: row.ArtistName,
PrimaryType: row.PrimaryType, SecondaryTypes: row.SecondaryTypes,
Year: first4(row.FirstReleaseDate), Reason: row.Reason,
WantedAt: formatTimestamp(row.WantedAt),
WantedAt: formatTimestamp(row.WantedAt), Upgrade: row.Upgrade, HeldQuality: row.HeldQuality,
ImageURL: groupArtURL(musicbrainz.ReleaseGroup{MBID: row.ReleaseGroupMbid, Title: row.Title, ArtistCredit: row.ArtistName}),
}
if row.LibraryArtistID.Valid {