286 lines
8.3 KiB
Go
286 lines
8.3 KiB
Go
|
|
package artwork
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
|
||
|
|
"github.com/navidrome/navidrome/model"
|
||
|
|
. "github.com/onsi/ginkgo/v2"
|
||
|
|
. "github.com/onsi/gomega"
|
||
|
|
)
|
||
|
|
|
||
|
|
var _ = Describe("Disc Artwork Reader", func() {
|
||
|
|
Describe("extractDiscNumber", func() {
|
||
|
|
DescribeTable("extracts disc number from filename based on glob pattern",
|
||
|
|
func(pattern, filename string, expectedNum int, expectedOk bool) {
|
||
|
|
num, ok := extractDiscNumber(pattern, filename)
|
||
|
|
Expect(ok).To(Equal(expectedOk))
|
||
|
|
if expectedOk {
|
||
|
|
Expect(num).To(Equal(expectedNum))
|
||
|
|
}
|
||
|
|
},
|
||
|
|
// Standard disc patterns
|
||
|
|
Entry("disc1.jpg", "disc*.*", "disc1.jpg", 1, true),
|
||
|
|
Entry("disc2.png", "disc*.*", "disc2.png", 2, true),
|
||
|
|
Entry("disc01.jpg", "disc*.*", "disc01.jpg", 1, true),
|
||
|
|
Entry("disc02.png", "disc*.*", "disc02.png", 2, true),
|
||
|
|
Entry("disc10.jpg", "disc*.*", "disc10.jpg", 10, true),
|
||
|
|
|
||
|
|
// CD patterns
|
||
|
|
Entry("cd1.jpg", "cd*.*", "cd1.jpg", 1, true),
|
||
|
|
Entry("cd02.png", "cd*.*", "cd02.png", 2, true),
|
||
|
|
|
||
|
|
// No number in filename
|
||
|
|
Entry("disc.jpg has no number", "disc*.*", "disc.jpg", 0, false),
|
||
|
|
Entry("cd.jpg has no number", "cd*.*", "cd.jpg", 0, false),
|
||
|
|
|
||
|
|
// Extra text after number
|
||
|
|
Entry("disc2-bonus.jpg", "disc*.*", "disc2-bonus.jpg", 2, true),
|
||
|
|
Entry("disc01_front.png", "disc*.*", "disc01_front.png", 1, true),
|
||
|
|
|
||
|
|
// Case insensitive (filename already lowered by caller)
|
||
|
|
Entry("Disc1.jpg lowered", "disc*.*", "disc1.jpg", 1, true),
|
||
|
|
|
||
|
|
// Pattern doesn't match
|
||
|
|
Entry("cover.jpg doesn't match disc*.*", "disc*.*", "cover.jpg", 0, false),
|
||
|
|
|
||
|
|
// Pattern with no wildcard before dot
|
||
|
|
Entry("front1.jpg with front*.*", "front*.*", "front1.jpg", 1, true),
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
Describe("fromExternalFile", func() {
|
||
|
|
var (
|
||
|
|
ctx context.Context
|
||
|
|
tmpDir string
|
||
|
|
)
|
||
|
|
|
||
|
|
BeforeEach(func() {
|
||
|
|
ctx = context.Background()
|
||
|
|
tmpDir = GinkgoT().TempDir()
|
||
|
|
})
|
||
|
|
|
||
|
|
createFile := func(path string) string {
|
||
|
|
fullPath := filepath.Join(tmpDir, filepath.FromSlash(path))
|
||
|
|
Expect(os.MkdirAll(filepath.Dir(fullPath), 0755)).To(Succeed())
|
||
|
|
Expect(os.WriteFile(fullPath, []byte("image data"), 0600)).To(Succeed())
|
||
|
|
return fullPath
|
||
|
|
}
|
||
|
|
|
||
|
|
It("matches file with disc number in single-folder album", func() {
|
||
|
|
f1 := createFile("album/disc1.jpg")
|
||
|
|
f2 := createFile("album/disc2.jpg")
|
||
|
|
reader := &discArtworkReader{
|
||
|
|
discNumber: 1,
|
||
|
|
imgFiles: []string{f1, f2},
|
||
|
|
discFolders: map[string]bool{filepath.Join(tmpDir, "album"): true},
|
||
|
|
}
|
||
|
|
|
||
|
|
sf := reader.fromExternalFile(ctx, "disc*.*")
|
||
|
|
r, path, err := sf()
|
||
|
|
Expect(err).ToNot(HaveOccurred())
|
||
|
|
Expect(r).ToNot(BeNil())
|
||
|
|
r.Close()
|
||
|
|
Expect(path).To(Equal(f1))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("skips file without number in single-folder album", func() {
|
||
|
|
f1 := createFile("album/disc.jpg")
|
||
|
|
reader := &discArtworkReader{
|
||
|
|
discNumber: 1,
|
||
|
|
imgFiles: []string{f1},
|
||
|
|
discFolders: map[string]bool{filepath.Join(tmpDir, "album"): true},
|
||
|
|
}
|
||
|
|
|
||
|
|
sf := reader.fromExternalFile(ctx, "disc*.*")
|
||
|
|
r, _, _ := sf()
|
||
|
|
Expect(r).To(BeNil())
|
||
|
|
})
|
||
|
|
|
||
|
|
It("matches file without number in multi-folder album by folder", func() {
|
||
|
|
f1 := createFile("album/cd1/disc.jpg")
|
||
|
|
f2 := createFile("album/cd2/disc.jpg")
|
||
|
|
reader := &discArtworkReader{
|
||
|
|
discNumber: 1,
|
||
|
|
imgFiles: []string{f1, f2},
|
||
|
|
discFolders: map[string]bool{filepath.Join(tmpDir, "album", "cd1"): true},
|
||
|
|
isMultiFolder: true,
|
||
|
|
}
|
||
|
|
|
||
|
|
sf := reader.fromExternalFile(ctx, "disc*.*")
|
||
|
|
r, path, err := sf()
|
||
|
|
Expect(err).ToNot(HaveOccurred())
|
||
|
|
Expect(r).ToNot(BeNil())
|
||
|
|
r.Close()
|
||
|
|
Expect(path).To(Equal(f1))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("prefers disc number over folder when number is present", func() {
|
||
|
|
// disc2.jpg in cd1 folder should match disc 2, not disc 1
|
||
|
|
f1 := createFile("album/cd1/disc2.jpg")
|
||
|
|
reader := &discArtworkReader{
|
||
|
|
discNumber: 2,
|
||
|
|
imgFiles: []string{f1},
|
||
|
|
discFolders: map[string]bool{filepath.Join(tmpDir, "album", "cd1"): true},
|
||
|
|
isMultiFolder: true,
|
||
|
|
}
|
||
|
|
|
||
|
|
sf := reader.fromExternalFile(ctx, "disc*.*")
|
||
|
|
r, path, err := sf()
|
||
|
|
Expect(err).ToNot(HaveOccurred())
|
||
|
|
Expect(r).ToNot(BeNil())
|
||
|
|
r.Close()
|
||
|
|
Expect(path).To(Equal(f1))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("does not match disc2.jpg when looking for disc 1", func() {
|
||
|
|
f1 := createFile("album/disc2.jpg")
|
||
|
|
reader := &discArtworkReader{
|
||
|
|
discNumber: 1,
|
||
|
|
imgFiles: []string{f1},
|
||
|
|
discFolders: map[string]bool{filepath.Join(tmpDir, "album"): true},
|
||
|
|
}
|
||
|
|
|
||
|
|
sf := reader.fromExternalFile(ctx, "disc*.*")
|
||
|
|
r, _, _ := sf()
|
||
|
|
Expect(r).To(BeNil())
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
Describe("fromDiscSubtitle", func() {
|
||
|
|
var (
|
||
|
|
ctx context.Context
|
||
|
|
tmpDir string
|
||
|
|
)
|
||
|
|
|
||
|
|
BeforeEach(func() {
|
||
|
|
ctx = context.Background()
|
||
|
|
tmpDir = GinkgoT().TempDir()
|
||
|
|
})
|
||
|
|
|
||
|
|
createFile := func(path string) string {
|
||
|
|
fullPath := filepath.Join(tmpDir, filepath.FromSlash(path))
|
||
|
|
Expect(os.MkdirAll(filepath.Dir(fullPath), 0755)).To(Succeed())
|
||
|
|
Expect(os.WriteFile(fullPath, []byte("image data"), 0600)).To(Succeed())
|
||
|
|
return fullPath
|
||
|
|
}
|
||
|
|
|
||
|
|
It("matches image file whose stem equals the disc subtitle (case-insensitive)", func() {
|
||
|
|
f1 := createFile("album/The Blue Disc.jpg")
|
||
|
|
reader := &discArtworkReader{
|
||
|
|
discNumber: 1,
|
||
|
|
imgFiles: []string{f1},
|
||
|
|
}
|
||
|
|
|
||
|
|
sf := reader.fromDiscSubtitle(ctx, "The Blue Disc")
|
||
|
|
r, path, err := sf()
|
||
|
|
Expect(err).ToNot(HaveOccurred())
|
||
|
|
Expect(r).ToNot(BeNil())
|
||
|
|
r.Close()
|
||
|
|
Expect(path).To(Equal(f1))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("matches case-insensitively", func() {
|
||
|
|
f1 := createFile("album/bonus tracks.png")
|
||
|
|
reader := &discArtworkReader{
|
||
|
|
discNumber: 2,
|
||
|
|
imgFiles: []string{f1},
|
||
|
|
}
|
||
|
|
|
||
|
|
sf := reader.fromDiscSubtitle(ctx, "Bonus Tracks")
|
||
|
|
r, path, err := sf()
|
||
|
|
Expect(err).ToNot(HaveOccurred())
|
||
|
|
Expect(r).ToNot(BeNil())
|
||
|
|
r.Close()
|
||
|
|
Expect(path).To(Equal(f1))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns error when no matching file found", func() {
|
||
|
|
f1 := createFile("album/cover.jpg")
|
||
|
|
reader := &discArtworkReader{
|
||
|
|
discNumber: 1,
|
||
|
|
imgFiles: []string{f1},
|
||
|
|
}
|
||
|
|
|
||
|
|
sf := reader.fromDiscSubtitle(ctx, "The Blue Disc")
|
||
|
|
_, _, err := sf()
|
||
|
|
Expect(err).To(HaveOccurred())
|
||
|
|
})
|
||
|
|
|
||
|
|
It("matches first file when multiple extensions exist", func() {
|
||
|
|
f1 := createFile("album/The Blue Disc.jpg")
|
||
|
|
f2 := createFile("album/The Blue Disc.png")
|
||
|
|
reader := &discArtworkReader{
|
||
|
|
discNumber: 1,
|
||
|
|
imgFiles: []string{f1, f2},
|
||
|
|
}
|
||
|
|
|
||
|
|
sf := reader.fromDiscSubtitle(ctx, "The Blue Disc")
|
||
|
|
r, path, err := sf()
|
||
|
|
Expect(err).ToNot(HaveOccurred())
|
||
|
|
Expect(r).ToNot(BeNil())
|
||
|
|
r.Close()
|
||
|
|
Expect(path).To(Equal(f1))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
Describe("discArtworkReader", func() {
|
||
|
|
Describe("fromDiscArtPriority", func() {
|
||
|
|
var reader *discArtworkReader
|
||
|
|
|
||
|
|
BeforeEach(func() {
|
||
|
|
reader = &discArtworkReader{
|
||
|
|
discNumber: 2,
|
||
|
|
isMultiFolder: true,
|
||
|
|
discFolders: map[string]bool{"/music/album/cd2": true},
|
||
|
|
imgFiles: []string{
|
||
|
|
"/music/album/cd1/disc.jpg",
|
||
|
|
"/music/album/cd2/disc.jpg",
|
||
|
|
"/music/album/cd2/disc2.jpg",
|
||
|
|
},
|
||
|
|
firstTrackPath: "/music/album/cd2/track1.flac",
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns source funcs for glob patterns", func() {
|
||
|
|
ff := reader.fromDiscArtPriority(context.Background(), nil, "disc*.*")
|
||
|
|
Expect(ff).To(HaveLen(1))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns source funcs for embedded pattern", func() {
|
||
|
|
ff := reader.fromDiscArtPriority(context.Background(), nil, "embedded")
|
||
|
|
Expect(ff).To(HaveLen(2)) // fromTag + fromFFmpegTag
|
||
|
|
})
|
||
|
|
|
||
|
|
It("handles multiple comma-separated patterns", func() {
|
||
|
|
ff := reader.fromDiscArtPriority(context.Background(), nil, "disc*.*, cd*.*, embedded")
|
||
|
|
Expect(ff).To(HaveLen(4)) // disc*.* + cd*.* + fromTag + fromFFmpegTag
|
||
|
|
})
|
||
|
|
|
||
|
|
It("ignores 'external' pattern silently", func() {
|
||
|
|
ff := reader.fromDiscArtPriority(context.Background(), nil, "external")
|
||
|
|
Expect(ff).To(HaveLen(0))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns no source funcs when imgFiles is empty and pattern is not embedded", func() {
|
||
|
|
reader.imgFiles = nil
|
||
|
|
ff := reader.fromDiscArtPriority(context.Background(), nil, "disc*.*")
|
||
|
|
Expect(ff).To(HaveLen(0))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns source func for discsubtitle pattern", func() {
|
||
|
|
reader.album = model.Album{Discs: model.Discs{2: "Bonus Tracks"}}
|
||
|
|
ff := reader.fromDiscArtPriority(context.Background(), nil, "discsubtitle")
|
||
|
|
Expect(ff).To(HaveLen(1))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns no source func for discsubtitle when disc has no subtitle", func() {
|
||
|
|
reader.album = model.Album{Discs: model.Discs{2: ""}}
|
||
|
|
ff := reader.fromDiscArtPriority(context.Background(), nil, "discsubtitle")
|
||
|
|
Expect(ff).To(HaveLen(0))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|