Files
bvandeusenandClaude Opus 5.5 3a16732883
release / govulncheck (push) Successful in 15s
release / web (push) Failing after 17s
release / go (push) Successful in 1m13s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m9s
release / Build signed APK (releases and dev) (push) Successful in 5m19s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
feat: Minstrel no longer talks to Lidarr: its connection, Delete via Lidarr and the unmonitor options are gone, and the track reports are plain quarantine (M502 #5525)
Migration 0091 drops lidarr_config and renames the quarantine tables, types,
indexes and the history column off the Lidarr name. The Integrations Lidarr
card, the Reports "Delete via Lidarr" action, the merge and remove-track
unmonitor options, the Lidarr audit actions and error codes go, on the
server, web and Android. The README describes acquisition as it is now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:48:20 -04:00

312 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package release
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"strings"
"testing"
)
func TestParse_ReadsWhatTheNameSays(t *testing.T) {
for _, c := range []struct {
name string
format, vbr, src string
bitrate, depth, yr int
rate float64
tier Tier
}{
// The wrong edition grabbed on 2026-10-09 (note #5527).
{"Daft Punk Random Access Memories (Drumless Edition 2023) [Flac 24-88]", FormatFLAC, "", "", 0, 24, 2023, 88.2, TierLosslessHiRes},
{"Gorillaz - Humanz - (Japanese Edition) - [CD-FLAC] 2017", FormatFLAC, "", SourceCD, 0, 0, 2017, 0, TierLossless},
{"Radiohead - OK Computer (1997) [MP3 320]", FormatMP3, "", "", 320, 0, 1997, 0, TierLossyHigh},
{"Artist - Album 2019 MP3 V0", FormatMP3, "V0", "", 0, 0, 2019, 0, TierLossyHigh},
{"Artist - Album (2020) [WEB] [AAC 256]", FormatAAC, "", SourceWEB, 256, 0, 2020, 0, TierLossyHigh},
{"Artist_-_Album-(2004)-192kbps", FormatMP3, "", "", 192, 0, 2004, 0, TierLossyLow},
{"Artist - Album [24bit-96kHz] FLAC Vinyl", FormatFLAC, "", SourceVinyl, 0, 24, 0, 96, TierLosslessHiRes},
{"Taylor Swift - 1989 (2014) [FLAC]", FormatFLAC, "", "", 0, 0, 2014, 0, TierLossless},
{"Artist - Album", "", "", "", 0, 0, 0, 0, TierUnknown},
} {
r := Parse(c.name)
got := fmt.Sprint(r.Format, r.VBR, r.Source, r.Bitrate, r.BitDepth, r.Year, r.SampleRate, TierOf(r, 0))
want := fmt.Sprint(c.format, c.vbr, c.src, c.bitrate, c.depth, c.yr, c.rate, c.tier)
if got != want {
t.Errorf("Parse(%q)\n got %s\nwant %s", c.name, got, want)
}
}
}
func TestParse_VideoAndDiscographies(t *testing.T) {
if v := Parse("Artist.Album.2018.1080p.BluRay.x264").Video; v != "1080p" {
t.Errorf("video = %q", v)
}
if d := Parse("Artist Discography 1994-2010 FLAC").Discography; d != "discography" {
t.Errorf("discography = %q", d)
}
if d := Parse("Artist - Studio Albums 1994 - 2010 [MP3]").Discography; d != "1994–2010" {
t.Errorf("year range = %q", d)
}
if d := Parse("Artist - Album (2010) [FLAC]").Discography; d != "" {
t.Errorf("one year read as a range: %q", d)
}
}
func judge(w Want, name string) Match { return Judge(w, Parse(name)) }
func TestJudge_TheAlbumAndWhatIsNot(t *testing.T) {
ram := Want{Artist: "Daft Punk", Title: "Random Access Memories", Year: 2013, PrimaryType: "Album"}
for name, veto := range map[string]string{
"Daft Punk - Random Access Memories (2013) [FLAC]": "",
"Daft_Punk-Random_Access_Memories-WEB-2013": "",
"Daft Punk Random Access Memories (Drumless Edition 2023) [Flac 24-88]": VetoVersion,
"Daft Punk - Random Access Memories (Instrumentals) [MP3]": VetoVersion,
"Daft Punk - Random Access Memories Remixes": VetoVersion,
"Daft Punk - Random Access Memories (Live) FLAC": VetoVersion,
"Daft Punk - Random Access Memories (BBC Sessions) FLAC": VetoVersion,
"Daft Punk - Random Access Memories Unplugged [MP3]": VetoVersion,
"Daft Punk - Discovery (2001) [FLAC]": VetoTitle,
"Random Access Memories (2013) [FLAC]": VetoArtist,
"Daft Punk - Discography 1997-2013 FLAC": VetoTitle,
"Daft Punk - Random Access Memories + Discovery + Homework Collection": VetoDiscography,
"Daft Punk - Random Access Memories (2013) 1080p BluRay": VetoVideo,
"Daft Punk - Random Access Memories (Single) 2013": VetoType,
"Daft Punk - Random Access Memories (Deluxe 10th Anniversary Edition) 2023": "",
} {
if got := judge(ram, name).Veto; got != veto {
t.Errorf("%q: veto %q, want %q", name, got, veto)
}
}
if !judge(ram, "Daft Punk - Random Access Memories (2013) [FLAC]").YearAgrees {
t.Error("the year did not agree")
}
}
func TestJudge_WordsTheWantedTitleHoldsDoNotVeto(t *testing.T) {
hole := Want{Artist: "Hole", Title: "Live Through This", PrimaryType: "Album"}
if m := judge(hole, "Hole - Live Through This (1994) [FLAC]"); m.Veto != "" {
t.Errorf("Live Through This: %s", m.Reason)
}
alive := Want{Artist: "Daft Punk", Title: "Alive 2007", PrimaryType: "Album", SecondaryTypes: []string{"Live"}}
if m := judge(alive, "Daft Punk - Alive 2007 (Live) [FLAC]"); m.Veto != "" {
t.Errorf("a wanted live album: %s", m.Reason)
}
alive.SecondaryTypes = nil
if m := judge(alive, "Daft Punk - Alive 2007 (Live) [FLAC]"); m.Veto != VetoVersion {
t.Errorf("live without the Live type: veto %q", m.Veto)
}
}
func TestJudge_SpellingsOfNames(t *testing.T) {
for _, c := range []struct {
w Want
name string
}{
{Want{Artist: "AC/DC", Title: "Back in Black"}, "ACDC - Back In Black (1980) FLAC"},
{Want{Artist: "AC/DC", Title: "Back in Black"}, "AC-DC - Back in Black [MP3 320]"},
{Want{Artist: "Sigur Rós", Title: "Ágætis byrjun"}, "Sigur Ros - Agaetis Byrjun (1999) [FLAC]"},
{Want{Artist: "Simon & Garfunkel", Title: "Bookends"}, "Simon and Garfunkel - Bookends"},
{Want{Artist: "The Beatles", Title: "Abbey Road"}, "Beatles - Abbey Road (Remastered 2009)"},
{Want{Artist: "Florence + the Machine", Title: "Lungs"}, "Florence and The Machine - Lungs (2009)"},
{Want{Artist: "Guns N' Roses", Title: "Appetite for Destruction"}, "Guns N Roses - Appetite For Destruction"},
} {
if m := judge(c.w, c.name); m.Veto != "" {
t.Errorf("%q for %s / %s: %s", c.name, c.w.Artist, c.w.Title, m.Reason)
}
}
// One word is never matched run together: Muse is not inside Museum.
if m := judge(Want{Artist: "Muse", Title: "Absolution"}, "Museum - Absolution"); m.Veto != VetoArtist {
t.Errorf("Museum read as Muse: %q", m.Veto)
}
}
func TestJudge_ASelfTitledAlbumNeedsMoreThanTheName(t *testing.T) {
weezer := Want{Artist: "Weezer", Title: "Weezer", Year: 1994, PrimaryType: "Album"}
if m := judge(weezer, "Weezer - Weezer (Blue Album) [FLAC]"); m.Doubt != "" || m.Veto != "" {
t.Errorf("named twice: %+v", m)
}
if m := judge(weezer, "Weezer (1994) [FLAC]"); m.Doubt != "" {
t.Errorf("the year agrees: %+v", m)
}
if m := judge(weezer, "Weezer (Green Album) FLAC"); m.Doubt == "" {
t.Error("a self-titled name with nothing else was taken as sure")
}
}
func TestTierOf_TheLadder(t *testing.T) {
for _, c := range []struct {
r Release
cat int
want Tier
}{
{Release{Format: FormatMP3, Bitrate: 256}, 0, TierLossyStandard},
{Release{Format: FormatMP3, Bitrate: 128}, 0, TierLossyPoor},
{Release{Format: FormatMP3, Bitrate: 96}, 0, TierLossyTrash},
{Release{Format: FormatMP3, VBR: "V2"}, 0, TierLossyStandard},
{Release{Format: FormatMP3}, 0, TierLossyStandard},
{Release{Format: FormatAAC, Bitrate: 192}, 0, TierLossyStandard},
{Release{Format: FormatALAC, SampleRate: 96}, 0, TierLosslessHiRes},
{Release{Format: FormatDSD}, 0, TierLosslessHiRes},
{Release{}, 3040, TierLossless},
{Release{}, 3010, TierUnknown},
} {
if got := TierOf(c.r, c.cat); got != c.want {
t.Errorf("TierOf(%+v, %d) = %s, want %s", c.r, c.cat, got, c.want)
}
}
if Rank(TierLossless) <= Rank(TierLossyHigh) || Rank(TierUnknown) >= Rank(TierLossyTrash) {
t.Error("the ladder is out of order")
}
if !NewProfile([]string{"lossless"}).Allows(TierLossless) || NewProfile([]string{"lossless"}).Allows(TierUnknown) {
t.Error("profile")
}
}
func files(paths ...string) []File {
out := make([]File, len(paths))
for i, p := range paths {
out[i] = File{Index: i, Path: p, Size: 1 << 20}
}
return out
}
func numbered(n int, ext string) []string {
var out []string
for i := 1; i <= n; i++ {
out = append(out, fmt.Sprintf("Album/%02d - Track.%s", i, ext))
}
return out
}
func TestJudgeFiles_WhatTheTorrentHolds(t *testing.T) {
w := Want{MinTracks: 10, MaxTracks: 13}
if veto, _, _ := JudgeFiles(w, files(numbered(13, "flac")...)); veto != "" {
t.Errorf("a whole album: %s", veto)
}
if veto, reason, _ := JudgeFiles(w, files(numbered(3, "flac")...)); veto != VetoIncomplete || !strings.Contains(reason, "3 tracks") {
t.Errorf("three tracks: %s %s", veto, reason)
}
if veto, _, _ := JudgeFiles(w, files("Concert/Concert.mkv", "Concert/cover.jpg")); veto != VetoVideo {
t.Errorf("a video: %s", veto)
}
if veto, _, _ := JudgeFiles(w, files("Album.rar", "Album.r00")); veto != VetoNoAudio {
t.Errorf("an archive: %s", veto)
}
if _, _, doubt := JudgeFiles(w, files(numbered(60, "mp3")...)); doubt == "" {
t.Error("sixty tracks for a thirteen-track album raised no doubt")
}
if c := Inspect(files(append(numbered(12, "flac"), "Album/cover.jpg", "Album/album.cue")...)); c.Format != FormatFLAC || c.Audio != 12 {
t.Errorf("inspect = %+v", c)
}
}
// bencode writes the few shapes a test torrent needs.
func bencode(v any) string {
switch x := v.(type) {
case string:
return fmt.Sprintf("%d:%s", len(x), x)
case int:
return fmt.Sprintf("i%de", x)
case []any:
var b strings.Builder
b.WriteString("l")
for _, e := range x {
b.WriteString(bencode(e))
}
return b.String() + "e"
case [][2]any: // an ordered dictionary
var b strings.Builder
b.WriteString("d")
for _, kv := range x {
b.WriteString(bencode(kv[0]) + bencode(kv[1]))
}
return b.String() + "e"
}
panic(fmt.Sprintf("bencode %T", v))
}
func TestParseTorrent_FilesAndInfohash(t *testing.T) {
info := bencode([][2]any{
{"files", []any{
[][2]any{{"length", 300}, {"path", []any{"01 - One.flac"}}},
[][2]any{{"attr", "p"}, {"length", 12}, {"path", []any{".pad", "12"}}},
[][2]any{{"length", 200}, {"path", []any{"Scans", "front.jpg"}}},
}},
{"name", "Artist - Album"},
{"piece length", 16384},
{"pieces", strings.Repeat("x", 20)},
})
body := "d" + bencode("announce") + bencode("http://tracker/") + bencode("info") + info + "e"
meta, err := ParseTorrent([]byte(body))
if err != nil {
t.Fatal(err)
}
sum := sha1.Sum([]byte(info))
if meta.InfoHash != hex.EncodeToString(sum[:]) {
t.Errorf("infohash = %s", meta.InfoHash)
}
if len(meta.Files) != 2 || meta.Files[0].Path != "Artist - Album/01 - One.flac" || meta.Files[1].Path != "Artist - Album/Scans/front.jpg" {
t.Errorf("files = %+v", meta.Files)
}
if _, err := ParseTorrent([]byte("<html>not a torrent</html>")); err == nil {
t.Error("HTML parsed as a torrent")
}
}
func TestTierOfFile(t *testing.T) {
cases := []struct {
format string
kbps int
want Tier
ok bool
}{
{"flac", 900, TierLossless, true},
{"FLAC", 2800, TierLosslessHiRes, true},
{"m4a", 750, TierLossless, true},
{"m4a", 256, TierLossyHigh, true},
{"mp3", 320, TierLossyHigh, true},
{"mp3", 245, TierLossyHigh, true}, // a V0, by size
{"mp3", 192, TierLossyLow, true},
{"mp3", 128, TierLossyPoor, true},
{"mp3", 96, TierLossyTrash, true},
{"opus", 160, TierLossyLow, true},
{"wma", 192, TierUnknown, false},
{"mp3", 0, TierLossyStandard, false},
}
for _, c := range cases {
got, ok := TierOfFile(c.format, c.kbps)
if got != c.want || ok != c.ok {
t.Errorf("TierOfFile(%q, %d) = %s, %v; want %s, %v", c.format, c.kbps, got, ok, c.want, c.ok)
}
}
}
func TestFileKbpsAndWords(t *testing.T) {
// 5 MB over 3:30 of audio is about 190 kbps.
k := FileKbps(5_000_000, 210_000)
if k != 190 {
t.Fatalf("FileKbps = %d, want 190", k)
}
if got := HeldWords("mp3", TierLossyLow, 191); got != "MP3, about 192 kbps" {
t.Errorf("HeldWords = %q", got)
}
if got := HeldWords("flac", TierLossless, 900); got != "FLAC" {
t.Errorf("HeldWords = %q", got)
}
if FileKbps(0, 1000) != 0 || FileKbps(1000, 0) != 0 {
t.Error("FileKbps with nothing to measure is 0")
}
}
func TestHeldCopy_TakesTheWorstFormat(t *testing.T) {
h, ok := HeldCopy([]HeldFormat{
{Format: "flac", Bytes: 300_000_000, Ms: 2_400_000},
{Format: "mp3", Bytes: 4_800_000, Ms: 200_000}, // 192 kbps
})
if !ok || h.Tier != TierLossyLow || h.Words != "MP3, about 192 kbps" {
t.Fatalf("HeldCopy = %+v, %v", h, ok)
}
if _, ok := HeldCopy([]HeldFormat{{Format: "wma", Bytes: 1, Ms: 1}}); ok {
t.Error("a format the ladder can't place says nothing")
}
}