package library import ( "context" "encoding/base64" "encoding/binary" "os" "os/exec" "path/filepath" "strings" "testing" ) func TestFilterComments(t *testing.T) { got := filterComments([]string{ "TRACKNUMBER=1", "GENRE=IDM", "TITLE=Song", "REPLAYGAIN_TRACK_GAIN=-3 dB", "", "no-equals-sign", "ALBUMARTIST=Band", "DATE=2001", }) want := []string{"TRACKNUMBER=1", "TITLE=Song", "ALBUMARTIST=Band", "DATE=2001"} if len(got) != len(want) { t.Fatalf("got %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Errorf("comment %d = %q, want %q", i, got[i], want[i]) } } } func TestPictureCommentStructure(t *testing.T) { dir := t.TempDir() img := filepath.Join(dir, "cover.jpg") payload := []byte{0xFF, 0xD8, 0xFF, 0xE0, 1, 2, 3, 4} if err := os.WriteFile(img, payload, 0o644); err != nil { t.Fatal(err) } comment, err := pictureComment(context.Background(), img) if err != nil { t.Fatal(err) } const prefix = "METADATA_BLOCK_PICTURE=" if !strings.HasPrefix(comment, prefix) { t.Fatalf("comment should start with %s", prefix) } raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(comment, prefix)) if err != nil { t.Fatalf("payload is not valid base64: %v", err) } if got := binary.BigEndian.Uint32(raw[0:4]); got != 3 { t.Errorf("picture type = %d, want 3 (front cover)", got) } mimeLen := int(binary.BigEndian.Uint32(raw[4:8])) mime := string(raw[8 : 8+mimeLen]) if mime != "image/jpeg" { t.Errorf("mime = %q, want image/jpeg", mime) } off := 8 + mimeLen descLen := int(binary.BigEndian.Uint32(raw[off : off+4])) off += 4 + descLen + 16 dataLen := int(binary.BigEndian.Uint32(raw[off : off+4])) off += 4 if dataLen != len(payload) { t.Fatalf("data length = %d, want %d", dataLen, len(payload)) } if string(raw[off:off+dataLen]) != string(payload) { t.Error("image bytes were not preserved") } } // A cover that cannot be read used to be skipped in silence. The usual reason is // a cover that lived in the staging folder and has just been cleared away by the // filing run, so the album gets a library copy with no artwork at all and every // summary still names the cover it thought it used. func TestAMissingCoverIsReported(t *testing.T) { needTools(t, "ffmpeg", "metaflac", "ffprobe") dir := t.TempDir() master := filepath.Join(dir, "master.flac") if out, err := exec.Command("ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=0.1", master).CombinedOutput(); err != nil { t.Fatalf("building a master: %v %s", err, out) } if out, err := exec.Command("metaflac", "--set-tag=TITLE=One", "--set-tag=ALBUM=An Album", "--set-tag=ALBUMARTIST=Band", "--set-tag=TRACKNUMBER=1", master).CombinedOutput(); err != nil { t.Fatalf("tagging the master: %v %s", err, out) } out := filepath.Join(dir, "library", "album") res, err := Transcode(context.Background(), []string{master}, []string{"1-01_one.flac"}, out, Options{ CoverPath: filepath.Join(dir, "staging", "cover.jpg"), }) if err != nil { t.Fatal(err) } found := false for _, w := range res.Warnings { if strings.Contains(w, "cover") { found = true } } if !found { t.Errorf("a cover that could not be read was not reported: %v", res.Warnings) } } // Rebuilding a library used to remove the old directory first and rename the new // one in afterwards, so a failure in between destroyed the old copy and threw // away the new one. Nothing should be left behind when it succeeds, either. func TestRebuildingALibraryLeavesNoLitter(t *testing.T) { needTools(t, "ffmpeg", "metaflac", "ffprobe") dir := t.TempDir() master := filepath.Join(dir, "master.flac") if out, err := exec.Command("ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=0.1", master).CombinedOutput(); err != nil { t.Fatalf("building a master: %v %s", err, out) } if out, err := exec.Command("metaflac", "--set-tag=TITLE=One", "--set-tag=ALBUM=An Album", "--set-tag=ALBUMARTIST=Band", "--set-tag=TRACKNUMBER=1", master).CombinedOutput(); err != nil { t.Fatalf("tagging the master: %v %s", err, out) } out := filepath.Join(dir, "library", "album") for i := 0; i < 2; i++ { if _, err := Transcode(context.Background(), []string{master}, []string{"1-01_one.flac"}, out, Options{}); err != nil { t.Fatal(err) } } entries, err := os.ReadDir(filepath.Dir(out)) if err != nil { t.Fatal(err) } for _, e := range entries { if e.Name() != "album" { t.Errorf("rebuilding left %s behind", e.Name()) } } tracks, err := os.ReadDir(out) if err != nil { t.Fatal(err) } if len(tracks) != 1 || !strings.HasSuffix(tracks[0].Name(), ".ogg") { var names []string for _, e := range tracks { names = append(names, e.Name()) } t.Errorf("library contents after a rebuild: %v", names) } } func needTools(t *testing.T, tools ...string) { t.Helper() for _, tool := range tools { if _, err := exec.LookPath(tool); err != nil { t.Skipf("%s is needed to build a fixture", tool) } } }