From 70defdb5bc531fa5170985442e783ef9f6f28077 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 4 May 2026 14:45:20 -0400 Subject: [PATCH] feat(server/m7-353): coverart.FindSidecar shared helper + tests --- internal/coverart/sidecar.go | 35 ++++++++++++++++++++ internal/coverart/sidecar_test.go | 54 +++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 internal/coverart/sidecar.go create mode 100644 internal/coverart/sidecar_test.go diff --git a/internal/coverart/sidecar.go b/internal/coverart/sidecar.go new file mode 100644 index 00000000..e5191c3a --- /dev/null +++ b/internal/coverart/sidecar.go @@ -0,0 +1,35 @@ +// Package coverart finds and fetches album cover artwork. +// +// Two-layer model: a directory-walking sidecar lookup (cover.jpg / folder.jpg +// next to the audio files) and a MusicBrainz Cover Art Archive (MBCAA) +// fallback for albums with an MBID but no sidecar. The Enricher orchestrates +// the two; the API and Subsonic packages reuse the sidecar helper directly. +package coverart + +import ( + "os" + "path/filepath" +) + +// SidecarNames is the lookup order for cover art living next to audio files. +// Matches common library conventions: cover.* wins over folder.*; JPEG wins +// over PNG when both are present. +var SidecarNames = []string{ + "cover.jpg", "cover.jpeg", "cover.png", + "folder.jpg", "folder.jpeg", "folder.png", +} + +// FindSidecar returns the absolute path to a sidecar cover image in +// albumDir, or "" when none of the conventional names are present as a +// regular file. Directories named like sidecar files are skipped. +func FindSidecar(albumDir string) string { + for _, name := range SidecarNames { + candidate := filepath.Join(albumDir, name) + info, err := os.Stat(candidate) + if err != nil || info.IsDir() { + continue + } + return candidate + } + return "" +} diff --git a/internal/coverart/sidecar_test.go b/internal/coverart/sidecar_test.go new file mode 100644 index 00000000..33f15847 --- /dev/null +++ b/internal/coverart/sidecar_test.go @@ -0,0 +1,54 @@ +package coverart + +import ( + "os" + "path/filepath" + "testing" +) + +func TestFindSidecar_PrefersCoverJpgOverFolderJpg(t *testing.T) { + dir := t.TempDir() + must(t, os.WriteFile(filepath.Join(dir, "folder.jpg"), []byte("f"), 0o644)) + must(t, os.WriteFile(filepath.Join(dir, "cover.jpg"), []byte("c"), 0o644)) + got := FindSidecar(dir) + want := filepath.Join(dir, "cover.jpg") + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestFindSidecar_PrefersJpegOverPng(t *testing.T) { + dir := t.TempDir() + must(t, os.WriteFile(filepath.Join(dir, "cover.png"), []byte("p"), 0o644)) + must(t, os.WriteFile(filepath.Join(dir, "cover.jpeg"), []byte("j"), 0o644)) + got := FindSidecar(dir) + want := filepath.Join(dir, "cover.jpeg") + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestFindSidecar_EmptyDir_ReturnsEmpty(t *testing.T) { + dir := t.TempDir() + if got := FindSidecar(dir); got != "" { + t.Errorf("expected empty string, got %q", got) + } +} + +func TestFindSidecar_DirectoryNamedCoverJpg_Skipped(t *testing.T) { + dir := t.TempDir() + must(t, os.MkdirAll(filepath.Join(dir, "cover.jpg"), 0o755)) + must(t, os.WriteFile(filepath.Join(dir, "folder.png"), []byte("p"), 0o644)) + got := FindSidecar(dir) + want := filepath.Join(dir, "folder.png") + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func must(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("setup: %v", err) + } +}