git/intake/main
internal/library/library_test.go
167 lines5 KB last change raw · history
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)
		}
	}
}