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
+12 -2
View File
@@ -66,6 +66,9 @@ type Importer struct {
// Roots are the library's folders (the scan paths). New music is filed
// under the one the settings name, or the first.
Roots []string
// DataDir is where cached art lives, for tidying an artist an upgrade
// leaves with nothing (#5524). Empty skips it.
DataDir string
// ClientFor builds the download client a grab went to. Defaults to
// acquire.DownloadClientFor.
ClientFor func(ctx context.Context, code string) (downloader.Driver, error)
@@ -338,6 +341,9 @@ func (im *Importer) place(ctx context.Context, q *dbq.Queries, g dbq.Grab, d dow
e = &editions[v.Edition]
}
plan := Plan(root, w, e, files, v.Pairs)
if w.Upgrade != "" {
return im.placeUpgrade(ctx, q, g, d, w, e, files, plan, v, det, record)
}
heldRec, heldPos, err := im.held(ctx, q, w, plan)
if err != nil {
return record(StateWaiting, "Couldn't read what the library holds: "+err.Error(), "", det, 0, "")
@@ -504,10 +510,14 @@ func (im *Importer) want(ctx context.Context, q *dbq.Queries, id pgtype.UUID) (W
if err != nil {
return Want{}, err
}
return Want{
w := Want{
GroupMBID: row.ReleaseGroupMbid, ArtistMBID: row.ArtistMbid, Artist: row.ArtistName, Title: row.Title,
Year: yearOf(row.FirstReleaseDate), PrimaryType: row.PrimaryType, SecondaryTypes: row.SecondaryTypes,
}, nil
}
if row.FulfilledAt.Valid && row.Upgrade != "" && row.AlbumID.Valid {
w.Upgrade, w.AlbumID = row.Upgrade, row.AlbumID
}
return w, nil
}
func (im *Importer) editions(ctx context.Context, group string) ([]Edition, error) {