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
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:
@@ -50,6 +50,9 @@ const (
|
||||
CodeProfile = "profile"
|
||||
CodeNoSeeders = "no_seeders"
|
||||
CodeDoubt = "doubt"
|
||||
// CodeNotBetter: the album, but no better than the copy the library
|
||||
// holds, for a want that is an upgrade (#5524).
|
||||
CodeNotBetter = "not_better"
|
||||
)
|
||||
|
||||
// Reason is one thing a verdict rests on.
|
||||
@@ -64,6 +67,12 @@ type Reason struct {
|
||||
type Want struct {
|
||||
ID pgtype.UUID
|
||||
release.Want
|
||||
// Floor is the tier of the copy the library holds, for an album wanted
|
||||
// for a better one (#5524): a release must rank above it. "" for an
|
||||
// album not held, and for a video rip, which any clean copy beats.
|
||||
Floor release.Tier
|
||||
// Held says the floor in words: "MP3, about 192 kbps".
|
||||
Held string
|
||||
}
|
||||
|
||||
// Candidate is one result with everything decided about it.
|
||||
@@ -188,11 +197,11 @@ func (s *Service) For(ctx context.Context, want Want) Result {
|
||||
|
||||
blocked := s.Memory.blocked(ctx)
|
||||
for i := range cands {
|
||||
decide(&cands[i], want.Want, s.Profile, blocked)
|
||||
decide(&cands[i], want, s.Profile, blocked)
|
||||
}
|
||||
sortCandidates(cands)
|
||||
for _, i := range s.peek(ctx, cands) {
|
||||
decide(&cands[i], want.Want, s.Profile, blocked)
|
||||
decide(&cands[i], want, s.Profile, blocked)
|
||||
r.Looked++
|
||||
}
|
||||
sortCandidates(cands)
|
||||
@@ -209,7 +218,8 @@ func (s *Service) For(ctx context.Context, want Want) Result {
|
||||
// whether it is the album, then whether the profile takes its quality, then
|
||||
// whether anything leaves it to a person. A quality refusal said about the
|
||||
// wrong album would be noise, so identity comes first.
|
||||
func decide(c *Candidate, w release.Want, profile release.Profile, blocked map[string]string) {
|
||||
func decide(c *Candidate, want Want, profile release.Profile, blocked map[string]string) {
|
||||
w := want.Want
|
||||
c.Verdict, c.Reason, c.Reasons = "", "", nil
|
||||
if why, ok := blocked[c.InfoHash]; ok && c.InfoHash != "" {
|
||||
set(c, Blocked, Hard, CodeBlocked, why)
|
||||
@@ -240,6 +250,14 @@ func decide(c *Candidate, w release.Want, profile release.Profile, blocked map[s
|
||||
set(c, Refused, Soft, CodeProfile, "Your quality settings don’t take "+strings.ToLower(release.Label(c.Tier))+".")
|
||||
return
|
||||
}
|
||||
if want.Floor != "" && release.Rank(c.Tier) <= release.Rank(want.Floor) {
|
||||
held := want.Held
|
||||
if held == "" {
|
||||
held = strings.ToLower(release.Label(want.Floor))
|
||||
}
|
||||
set(c, Refused, Soft, CodeNotBetter, "No better than the copy you have ("+held+").")
|
||||
return
|
||||
}
|
||||
if doubt != "" {
|
||||
set(c, Review, Soft, CodeDoubt, doubt)
|
||||
return
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user