Files
minstrel/internal/wanted/upgrade_test.go
T
bvandeusenandClaude Opus 5.5 c54792c51e
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
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)
- 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>
2026-10-11 12:24:59 -04:00

138 lines
4.4 KiB
Go

package wanted
import (
"context"
"fmt"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
"git.fabledsword.com/bvandeusen/minstrel/internal/release"
)
func TestUpgradeFor(t *testing.T) {
low := release.Held{Tier: release.TierLossyLow}
flac := release.Held{Tier: release.TierLossless}
high := release.Held{Tier: release.TierLossyHigh}
cases := []struct {
name string
held release.Held
judged bool
rips int
until release.Tier
want string
}{
{"below the cutoff", low, true, 0, release.TierLossyHigh, UpgradeQuality},
{"at the cutoff", high, true, 0, release.TierLossyHigh, ""},
{"above the cutoff", flac, true, 0, release.TierLossyHigh, ""},
{"upgrades off", low, true, 0, "", ""},
{"a rip, upgrades off", flac, true, 1, "", UpgradeRip},
{"nothing to judge", release.Held{}, false, 0, release.TierLossless, ""},
}
for _, c := range cases {
if got := UpgradeFor(c.held, c.judged, c.rips, c.until); got != c.want {
t.Errorf("%s: %q, want %q", c.name, got, c.want)
}
}
}
// seedMP3s puts Blur's The Ballad of Darren in the library as MP3s at about
// kbps, two tracks, the first held back as a video rip when rip is set.
func seedMP3s(t *testing.T, pool *pgxpool.Pool, kbps int64, rip bool) dbq.Album {
t.Helper()
ctx := context.Background()
q := dbq.New(pool)
mbid, group, rel := blur, theBallad, "r-ballad-mp3"
artist, err := q.UpsertArtist(ctx, dbq.UpsertArtistParams{Name: "Blur", SortName: "Blur", Mbid: &mbid})
if err != nil {
t.Fatal(err)
}
album, err := q.UpsertAlbum(ctx, dbq.UpsertAlbumParams{Title: "The Ballad of Darren", SortTitle: "Ballad of Darren",
ArtistID: artist.ID, Mbid: &rel, ReleaseGroupMbid: &group})
if err != nil {
t.Fatal(err)
}
for i := range 2 {
num := int32(i + 1)
tr, err := q.UpsertTrack(ctx, dbq.UpsertTrackParams{
Title: fmt.Sprintf("Track %d", i+1), AlbumID: album.ID, ArtistID: artist.ID, TrackNumber: &num,
DurationMs: 200000, FilePath: fmt.Sprintf("/music/ballad/%02d.mp3", i+1),
FileSize: kbps * 200000 / 8, FileFormat: "mp3",
})
if err != nil {
t.Fatal(err)
}
if rip && i == 0 {
if _, err := pool.Exec(ctx, `UPDATE tracks SET source_verdict = 'suspect' WHERE id = $1`, tr.ID); err != nil {
t.Fatal(err)
}
}
}
return album
}
func TestCheckUpgrades_AHeldAlbumBelowTheCutoffIsWantedAgain_Integration(t *testing.T) {
pool := newPool(t)
q := dbq.New(pool)
ctx := context.Background()
s := newService(t, pool, newFakeMB(t))
if _, err := WantWith(ctx, q, WantParams{
GroupMBID: theBallad, ArtistMBID: blur, ArtistName: "Blur", Title: "The Ballad of Darren", Reason: ReasonRequest,
}); err != nil {
t.Fatal(err)
}
seedMP3s(t, pool, 192, false)
res, err := s.Pass(ctx)
if err != nil || res.Held != 1 || res.Upgrades != 1 {
t.Fatalf("pass: %+v, %v", res, err)
}
w, _ := q.GetWantByReleaseGroup(ctx, theBallad)
if w.Upgrade != UpgradeQuality || w.HeldTier != string(release.TierLossyLow) || w.HeldQuality != "MP3, about 192 kbps" {
t.Fatalf("want = upgrade %q, tier %q, %q", w.Upgrade, w.HeldTier, w.HeldQuality)
}
due, err := q.ListWantsDueForSearch(ctx, dbq.ListWantsDueForSearchParams{Hours: 24, MaxRows: 10})
if err != nil || len(due) != 1 || due[0].Upgrade != UpgradeQuality {
t.Fatalf("due for search: %+v, %v", due, err)
}
// Upgrades switched off: no longer wanted again.
if _, err := pool.Exec(ctx, `UPDATE acquisition_settings SET upgrade_until = ''`); err != nil {
t.Fatal(err)
}
s.upgradesChecked = time.Time{}
if _, err := s.Pass(ctx); err != nil {
t.Fatal(err)
}
w, _ = q.GetWantByReleaseGroup(ctx, theBallad)
if w.Upgrade != "" {
t.Errorf("upgrades off: upgrade %q", w.Upgrade)
}
}
func TestCheckUpgrades_ARipIsReplacedWhateverTheSetting_Integration(t *testing.T) {
pool := newPool(t)
q := dbq.New(pool)
ctx := context.Background()
s := newService(t, pool, newFakeMB(t))
if _, err := WantWith(ctx, q, WantParams{
GroupMBID: theBallad, ArtistMBID: blur, ArtistName: "Blur", Title: "The Ballad of Darren", Reason: ReasonRequest,
}); err != nil {
t.Fatal(err)
}
seedMP3s(t, pool, 320, true)
if _, err := pool.Exec(ctx, `UPDATE acquisition_settings SET upgrade_until = ''`); err != nil {
t.Fatal(err)
}
if _, err := s.Pass(ctx); err != nil {
t.Fatal(err)
}
w, _ := q.GetWantByReleaseGroup(ctx, theBallad)
if w.Upgrade != UpgradeRip {
t.Errorf("a rip: upgrade %q", w.Upgrade)
}
}