release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m29s
release / integration (push) Successful in 5m1s
release / android (push) Successful in 5m5s
release / Build signed APK (releases and dev) (push) Successful in 5m21s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
The importer wrote track=N/M for every format, which ffmpeg stores in a Vorbis comment as TRACKNUMBER=3/20; dhowden/tag reads a Vorbis TRACKNUMBER with a bare Atoi, so it came back 0. All 245 tracks imported since 2026-10-10 (Humanz, Plastic Beach, the Gorillaz super deluxe and twelve more albums) have no track or disc number, and their album pages have no order. - The importer writes a Vorbis track and disc number alone, with TRACKTOTAL and DISCTOTAL, as Picard does. ID3 and MP4 keep N/M. - ReadTrackAndDisc takes the number before a slash when the Vorbis reader returns 0, for the scanner and for the import's own check of a download. - tagReadVersion 6 re-reads the files already held (tag reads only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
240 lines
8.2 KiB
Go
240 lines
8.2 KiB
Go
package importer
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
var ram = Want{GroupMBID: "rg-ram", ArtistMBID: "mb-dp", Artist: "Daft Punk", Title: "Random Access Memories",
|
|
Year: 2013, PrimaryType: "Album"}
|
|
|
|
var ramTitles = []string{"Give Life Back to Music", "The Game of Love", "Giorgio by Moroder", "Within",
|
|
"Instant Crush", "Lose Yourself to Dance", "Touch", "Get Lucky", "Beyond", "Motherboard",
|
|
"Fragments of Time", "Doin' It Right", "Contact"}
|
|
|
|
// edition is RAM's 13-track CD, recordings rec-01.., lengths 4:00 + n seconds.
|
|
func edition(mbid string, n int) Edition {
|
|
e := Edition{MBID: mbid, Title: "Random Access Memories", Status: "Official", Date: "2013-05-17"}
|
|
for i := range n {
|
|
e.Tracks = append(e.Tracks, Track{Medium: 1, Format: "CD", Position: i + 1, Title: ramTitles[i%len(ramTitles)],
|
|
RecordingMBID: fmt.Sprintf("rec-%02d", i+1), LengthMs: 240000 + i*1000})
|
|
}
|
|
return e
|
|
}
|
|
|
|
// download is n files lined up with the edition: tagged positions, lengths
|
|
// within a second, AcoustID naming each file's recording.
|
|
func download(n int) []File {
|
|
var fs []File
|
|
for i := range n {
|
|
fs = append(fs, File{Path: fmt.Sprintf("/dl/%02d.flac", i+1), Rel: fmt.Sprintf("%02d.flac", i+1),
|
|
Disc: 1, Track: i + 1, Title: ramTitles[i%len(ramTitles)], Album: "Random Access Memories",
|
|
LengthMs: 240000 + i*1000 + 400, Recordings: []string{fmt.Sprintf("rec-%02d", i+1)}})
|
|
}
|
|
return fs
|
|
}
|
|
|
|
func TestVerify_TheAlbumAndWhatIsNot(t *testing.T) {
|
|
eds := []Edition{edition("rel-cd", 13), edition("rel-deluxe", 15)}
|
|
cases := []struct {
|
|
name string
|
|
files func() []File
|
|
printed bool
|
|
out Outcome
|
|
code string
|
|
edition string
|
|
}{
|
|
{"the album, fingerprinted", func() []File { return download(13) }, true, Accept, CodeMatched, "rel-cd"},
|
|
{"the deluxe by count", func() []File { return download(15) }, true, Accept, CodeMatched, "rel-deluxe"},
|
|
{"no audio", func() []File { return nil }, true, Refuse, CodeNoAudio, ""},
|
|
{"music videos", func() []File {
|
|
fs := download(13)
|
|
for i := range fs {
|
|
fs[i].Video = i < 10
|
|
}
|
|
return fs
|
|
}, true, Refuse, CodeVideo, ""},
|
|
{"tags name another group", func() []File {
|
|
fs := download(13)
|
|
for i := range fs {
|
|
fs[i].GroupMBID = "rg-homework"
|
|
}
|
|
return fs
|
|
}, true, Refuse, CodeWrongAlbum, ""},
|
|
{"fingerprints say other recordings", func() []File {
|
|
fs := download(13)
|
|
for i := range fs {
|
|
fs[i].Recordings = []string{"rec-other"}
|
|
}
|
|
return fs
|
|
}, true, Refuse, CodeWrongAlbum, "rel-cd"},
|
|
{"the drumless edition: right lengths, album tag says otherwise", func() []File {
|
|
fs := download(13)
|
|
for i := range fs {
|
|
fs[i].Recordings, fs[i].Album = nil, "Random Access Memories (Drumless Edition)"
|
|
}
|
|
return fs
|
|
}, false, Refuse, CodeOtherVersion, "rel-cd"},
|
|
{"fewer files than any edition", func() []File { return download(9) }, true, Refuse, CodeIncomplete, ""},
|
|
{"no edition this long", func() []File { return download(14) }, true, Review, CodeNoEdition, ""},
|
|
{"no fingerprints, lengths and titles agree", func() []File {
|
|
fs := download(13)
|
|
for i := range fs {
|
|
fs[i].Recordings = nil
|
|
}
|
|
return fs
|
|
}, false, Accept, CodeMatched, "rel-cd"},
|
|
{"no fingerprints, no titles, nothing to go on but lengths", func() []File {
|
|
fs := download(13)
|
|
for i := range fs {
|
|
fs[i].Recordings, fs[i].Title, fs[i].Album = nil, "", ""
|
|
}
|
|
return fs
|
|
}, false, Review, CodeWeak, "rel-cd"},
|
|
{"no fingerprints, lengths all wrong", func() []File {
|
|
fs := download(13)
|
|
for i := range fs {
|
|
fs[i].Recordings, fs[i].LengthMs = nil, 600000
|
|
}
|
|
return fs
|
|
}, false, Refuse, CodeWrongAlbum, "rel-cd"},
|
|
{"tags name the group: strong even unprinted", func() []File {
|
|
fs := download(13)
|
|
for i := range fs {
|
|
fs[i].Recordings, fs[i].Title, fs[i].GroupMBID = nil, "", "rg-ram"
|
|
}
|
|
return fs
|
|
}, false, Accept, CodeMatched, "rel-cd"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
v := Verify(ram, eds, c.files(), c.printed, false)
|
|
if v.Outcome != c.out || v.Code != c.code {
|
|
t.Fatalf("got %s/%s (%s), want %s/%s", v.Outcome, v.Code, v.Reason, c.out, c.code)
|
|
}
|
|
got := ""
|
|
if v.Edition >= 0 {
|
|
got = eds[v.Edition].MBID
|
|
}
|
|
if got != c.edition {
|
|
t.Errorf("edition %q, want %q", got, c.edition)
|
|
}
|
|
if v.Reason == "" {
|
|
t.Error("no reason given")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerify_UnnumberedFilesPairByPath(t *testing.T) {
|
|
fs := download(13)
|
|
for i := range fs {
|
|
fs[i].Track, fs[i].Disc = 0, 0
|
|
}
|
|
fs[0], fs[5] = fs[5], fs[0] // listed out of order; their names sort right
|
|
v := Verify(ram, []Edition{edition("rel-cd", 13)}, fs, true, false)
|
|
if v.Outcome != Accept || v.Evidence.Recordings != 13 {
|
|
t.Fatalf("got %+v", v)
|
|
}
|
|
}
|
|
|
|
func TestVerify_ImportAnywayTakesTheBestEdition(t *testing.T) {
|
|
v := Verify(ram, []Edition{edition("rel-cd", 13)}, download(14), true, true)
|
|
if v.Outcome != Accept || v.Code != CodeForced || v.Edition != 0 || len(v.Pairs) != 13 {
|
|
t.Fatalf("got %+v", v)
|
|
}
|
|
if v := Verify(ram, nil, nil, true, true); v.Outcome != Refuse {
|
|
t.Errorf("forced with no audio: %+v", v)
|
|
}
|
|
}
|
|
|
|
func TestVerify_WithoutAcoustIDTheReasonSaysSo(t *testing.T) {
|
|
fs := download(13)
|
|
for i := range fs {
|
|
fs[i].Recordings = nil
|
|
}
|
|
v := Verify(ram, []Edition{edition("rel-cd", 13)}, fs, false, false)
|
|
if !strings.Contains(v.Reason, "AcoustID was not asked") {
|
|
t.Errorf("reason %q", v.Reason)
|
|
}
|
|
}
|
|
|
|
func TestPlan_LibraryNaming(t *testing.T) {
|
|
e := edition("rel-cd", 2)
|
|
e.Tracks[1].Title = "Within: Part 2/3?"
|
|
pl := Plan("/music", ram, &e, download(2), Verify(ram, []Edition{e}, download(2), true, false).Pairs)
|
|
want := []string{
|
|
"/music/Daft Punk/Random Access Memories (2013)/Daft Punk - Random Access Memories - 01 - Give Life Back to Music.flac",
|
|
"/music/Daft Punk/Random Access Memories (2013)/Daft Punk - Random Access Memories - 02 - Within - Part 2+3!.flac",
|
|
}
|
|
for i, p := range pl {
|
|
if p.Dest != want[i] {
|
|
t.Errorf("dest %d = %q, want %q", i, p.Dest, want[i])
|
|
}
|
|
}
|
|
if pl[0].Recording != "rec-01" || pl[0].Tracks != 2 {
|
|
t.Errorf("placement %+v", pl[0])
|
|
}
|
|
}
|
|
|
|
func TestPlan_MultiDiscGetsAMediumFolder(t *testing.T) {
|
|
e := edition("rel-2cd", 4)
|
|
e.Tracks[2].Medium, e.Tracks[2].Position = 2, 1
|
|
e.Tracks[3].Medium, e.Tracks[3].Position = 2, 2
|
|
fs := download(4)
|
|
fs[2].Disc, fs[2].Track, fs[3].Disc, fs[3].Track = 2, 1, 2, 2
|
|
pl := Plan("/music", ram, &e, fs, Verify(ram, []Edition{e}, fs, true, false).Pairs)
|
|
if got := filepath.Dir(pl[3].Dest); got != "/music/Daft Punk/Random Access Memories (2013)/CD 02" {
|
|
t.Errorf("disc folder %q", got)
|
|
}
|
|
if !strings.HasSuffix(pl[3].Dest, " - 02 - Get Lucky.flac") && !strings.Contains(pl[3].Dest, " - 02 - ") {
|
|
t.Errorf("dest %q", pl[3].Dest)
|
|
}
|
|
}
|
|
|
|
func TestSegment(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"AC/DC": "AC+DC", `What?`: "What!", "Title: Sub": "Title - Sub", "a:b": "a-b", " ..x.. ": "x",
|
|
"": "_", "..": "_", `"Quoted" <x> *y* |z|`: "Quoted x -y- z",
|
|
} {
|
|
if got := Segment(in); got != want {
|
|
t.Errorf("Segment(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
if long := Segment(strings.Repeat("é", 200)); len(long) > segmentMax {
|
|
t.Errorf("long segment is %d bytes", len(long))
|
|
}
|
|
}
|
|
|
|
func TestTagsArgs(t *testing.T) {
|
|
tg := Tags{Title: "Get Lucky", Artist: "Daft Punk", Track: 8, Tracks: 13, Disc: 1, Discs: 1, Year: 2013,
|
|
RecordingMBID: "rec-08", GroupMBID: "rg-ram"}
|
|
flac := strings.Join(tg.args(".flac"), " ")
|
|
for _, s := range []string{
|
|
"title=Get Lucky", "track=8 ", "TRACKTOTAL=13", "disc=1 ", "DISCTOTAL=1", "date=2013",
|
|
"MUSICBRAINZ_TRACKID=rec-08", "MUSICBRAINZ_RELEASEGROUPID=rg-ram",
|
|
} {
|
|
if !strings.Contains(flac, s) {
|
|
t.Errorf("flac args lack %q: %s", s, flac)
|
|
}
|
|
}
|
|
// dhowden/tag reads a Vorbis TRACKNUMBER of "8/13" as 0 (#6171).
|
|
if strings.Contains(flac, "/13") {
|
|
t.Errorf("flac track number carries its count: %s", flac)
|
|
}
|
|
if mp3 := strings.Join(tg.args(".mp3"), " "); !strings.Contains(mp3, "track=8/13") {
|
|
t.Errorf("mp3 track should be 8/13, which ID3 readers parse: %s", mp3)
|
|
}
|
|
if strings.Contains(flac, "MUSICBRAINZ_ALBUMID") {
|
|
t.Errorf("an empty release id was written: %s", flac)
|
|
}
|
|
if mp3 := strings.Join(tg.args(".mp3"), " "); !strings.Contains(mp3, "MusicBrainz Track Id=rec-08") {
|
|
t.Errorf("mp3 args %s", mp3)
|
|
}
|
|
if m4a := strings.Join(tg.args(".m4a"), " "); strings.Contains(m4a, "MusicBrainz") {
|
|
t.Errorf("m4a args %s", m4a)
|
|
}
|
|
}
|