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 Lidarr 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("not a torrent")); err == nil { t.Error("HTML parsed as a torrent") } }