package metadata import ( "os" "path/filepath" "testing" "yellowjacket/internal/testfixtures" ) // testMP3Files returns every .mp3 in the generated fixture library // (`make testdata`). Scoped to that manifest so ad-hoc scramble / // autotag fixtures elsewhere under `test_data/` don't get pulled into // the assertion and fail on non-curated codecs. Skips when the // fixtures haven't been generated. func testMP3Files(t *testing.T) []string { t.Helper() m := testfixtures.Load(t) var files []string for _, track := range m.Tracks { if track.Format == "mp3" { files = append(files, m.Abs(track.Path)) } } if len(files) == 0 { t.Skip("no .mp3 fixtures in the manifest") } return files } // TestGetMP3Duration_MatchesBeepDecode verifies that the fast // header-only parser produces a duration within 1 second of the // full decode via beep, for every test MP3 file. func TestGetMP3Duration_MatchesBeepDecode(t *testing.T) { for _, path := range testMP3Files(t) { t.Run(filepath.Base(path), func(t *testing.T) { // Reference value: full beep decode. refMS, err := GetTrackLengthMillis(path) if err != nil { t.Fatalf( "beep decode failed: %v", err, ) } // Fast path. f, err := os.Open(path) if err != nil { t.Fatalf("open: %v", err) } defer func() { _ = f.Close() }() fastMS, _, err := getMP3Duration(f) if err != nil { t.Fatalf( "getMP3Duration failed: %v", err, ) } diffMS := refMS - fastMS if diffMS < 0 { diffMS = -diffMS } // Allow up to 1 second of difference to account // for rounding and the slight inaccuracy of the // CBR fallback for VBR-without-Xing files. const toleranceMS = 1000 t.Logf( "beep=%dms fast=%dms diff=%dms", refMS, fastMS, diffMS, ) if diffMS > toleranceMS { t.Errorf( "duration mismatch: beep=%dms fast=%dms "+ "(diff %dms exceeds %dms tolerance)", refMS, fastMS, diffMS, toleranceMS, ) } }) } } // TestGetMP3Duration_BasicParsing exercises the parser on a single // file and verifies a positive duration is returned. func TestGetMP3Duration_BasicParsing(t *testing.T) { files := testMP3Files(t) f, err := os.Open(files[0]) if err != nil { t.Fatalf("open: %v", err) } defer func() { _ = f.Close() }() ms, _, err := getMP3Duration(f) if err != nil { t.Fatalf("getMP3Duration: %v", err) } if ms <= 0 { t.Errorf("expected positive duration, got %d", ms) } } // TestGetMP3Duration_WithMultipleID3v2 creates a temporary MP3 file // with two consecutive ID3v2 tags prepended and verifies that // getMP3Duration correctly skips both and finds the audio. func TestGetMP3Duration_WithMultipleID3v2(t *testing.T) { files := testMP3Files(t) src := files[0] srcData, err := os.ReadFile(src) if err != nil { t.Fatalf("reading source: %v", err) } // Get reference duration from the original file. origF, err := os.Open(src) if err != nil { t.Fatalf("open original: %v", err) } defer func() { _ = origF.Close() }() origMS, _, err := getMP3Duration(origF) if err != nil { t.Fatalf("getMP3Duration on original: %v", err) } // Build a file with two ID3v2 tags: 1 KB + 2 KB of padding. //nolint:mnd // synthetic tag construction. tag1Size := 1024 tag2Size := 2048 tag1 := buildID3v2Header(tag1Size) tag2 := buildID3v2Header(tag2Size) out := make( []byte, 0, len(tag1)+tag1Size+len(tag2)+tag2Size+len(srcData), ) out = append(out, tag1...) out = append(out, make([]byte, tag1Size)...) out = append(out, tag2...) out = append(out, make([]byte, tag2Size)...) out = append(out, srcData...) tmpDir := t.TempDir() tmpPath := filepath.Join(tmpDir, "multi_id3v2.mp3") if err := os.WriteFile( tmpPath, out, 0o644, ); err != nil { t.Fatalf("writing temp file: %v", err) } tmpF, err := os.Open(tmpPath) if err != nil { t.Fatalf("open temp: %v", err) } defer func() { _ = tmpF.Close() }() wrappedMS, _, err := getMP3Duration(tmpF) if err != nil { t.Fatalf( "getMP3Duration on multi-ID3v2 file: %v", err, ) } diffMS := origMS - wrappedMS if diffMS < 0 { diffMS = -diffMS } // The CBR calculation uses file size, so the prepended tags // will cause a slight overestimate. Allow generous tolerance. const toleranceMS = 5000 t.Logf( "original=%dms wrapped=%dms diff=%dms", origMS, wrappedMS, diffMS, ) if diffMS > toleranceMS { t.Errorf( "duration mismatch: original=%dms "+ "wrapped=%dms (diff %dms "+ "exceeds %dms tolerance)", origMS, wrappedMS, diffMS, toleranceMS, ) } } // TestSkipAdditionalID3v2 verifies that skipAdditionalID3v2 handles // files with no additional tags, one additional tag, and multiple // additional tags. func TestSkipAdditionalID3v2(t *testing.T) { // Build a file: [ID3v2(100)] [ID3v2(200)] [ID3v2(50)] [data] //nolint:mnd // synthetic tag sizes for test. sizes := []int{100, 200, 50} var buf []byte for _, sz := range sizes { buf = append(buf, buildID3v2Header(sz)...) buf = append(buf, make([]byte, sz)...) } buf = append(buf, []byte("audio data here")...) tmpDir := t.TempDir() tmpPath := filepath.Join(tmpDir, "multi_id3.bin") if err := os.WriteFile( tmpPath, buf, 0o644, ); err != nil { t.Fatalf("writing temp file: %v", err) } f, err := os.Open(tmpPath) if err != nil { t.Fatalf("open: %v", err) } defer func() { _ = f.Close() }() // skipID3v2 handles the first tag. firstEnd, err := skipID3v2(f) if err != nil { t.Fatalf("skipID3v2: %v", err) } //nolint:mnd // expected offset after first tag. expectedFirst := int64(10 + 100) if firstEnd != expectedFirst { t.Fatalf( "first tag end: got %d, want %d", firstEnd, expectedFirst, ) } // skipAdditionalID3v2 handles the remaining tags. finalOffset, err := skipAdditionalID3v2(f, firstEnd) if err != nil { t.Fatalf("skipAdditionalID3v2: %v", err) } // Expected: 10+100 + 10+200 + 10+50 = 380 //nolint:mnd // expected offset after all tags. expectedAll := int64(10 + 100 + 10 + 200 + 10 + 50) if finalOffset != expectedAll { t.Errorf( "final offset: got %d, want %d", finalOffset, expectedAll, ) } }