release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m31s
release / go (push) Successful in 1m51s
release / android (push) Successful in 5m18s
release / integration (push) Successful in 5m23s
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 21s
release / Verify release artifacts (tag releases only) (push) Skipped
MusicBrainz types My Chemical Romance's "Common People (Recorded For BBC Radio 1)" as a plain Single, so a studio-only follow wanted it. Passes now reads the title's qualifier (brackets, or after a dash) for the version it names and treats it as a secondary type; a version no type names, an acoustic or instrumental one, no profile passes. The follow check takes off the list an open follow want the rule now refuses. Judge counts bbc, sessions and unplugged as live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
254 lines
10 KiB
Go
254 lines
10 KiB
Go
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("<html>not a torrent</html>")); err == nil {
|
||
t.Error("HTML parsed as a torrent")
|
||
}
|
||
}
|