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
+23 -1
View File
@@ -95,7 +95,7 @@ func TestFor_ProfileSeedersAndDoubt(t *testing.T) {
func TestFor_BlockedReleasesAreSetAside(t *testing.T) {
c := Candidate{Release: release.Parse("Daft Punk - Random Access Memories [FLAC]"), InfoHash: "aaaa", Seeders: 3}
decide(&c, ram.Want, anyQuality, map[string]string{"aaaa": "You set this release aside."})
decide(&c, ram, anyQuality, map[string]string{"aaaa": "You set this release aside."})
if c.Verdict != Blocked || c.Reason != "You set this release aside." {
t.Errorf("verdict %s %q", c.Verdict, c.Reason)
}
@@ -239,3 +239,25 @@ func TestLandingFor_OneFolderPerTorrent(t *testing.T) {
t.Error(got)
}
}
func TestDecide_AnUpgradeTakesOnlyABetterCopy(t *testing.T) {
up := ram
up.Floor, up.Held = release.TierLossyLow, "MP3, about 192 kbps"
same := Candidate{Release: release.Parse("Daft Punk - Random Access Memories [MP3 192]"), InfoHash: "aaaa", Seeders: 3}
decide(&same, up, anyQuality, nil)
if same.Verdict != Refused || same.Reasons[0].Code != CodeNotBetter ||
same.Reason != "No better than the copy you have (MP3, about 192 kbps)." {
t.Errorf("same quality: %s %q", same.Verdict, same.Reason)
}
flac := Candidate{Release: release.Parse("Daft Punk - Random Access Memories [FLAC]"), InfoHash: "bbbb", Seeders: 3}
decide(&flac, up, anyQuality, nil)
if flac.Verdict != Grab {
t.Errorf("a FLAC over a 192 MP3: %s %q", flac.Verdict, flac.Reason)
}
// A video rip has no floor: any clean copy is better.
rip := ram
decide(&same, rip, anyQuality, nil)
if same.Verdict != Grab {
t.Errorf("no floor: %s %q", same.Verdict, same.Reason)
}
}