277 lines
11 KiB
Go
277 lines
11 KiB
Go
|
|
package artworke2e_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"testing/fstest"
|
||
|
|
|
||
|
|
"github.com/navidrome/navidrome/conf"
|
||
|
|
"github.com/navidrome/navidrome/model"
|
||
|
|
. "github.com/onsi/ginkgo/v2"
|
||
|
|
. "github.com/onsi/gomega"
|
||
|
|
)
|
||
|
|
|
||
|
|
var _ = Describe("Disc artwork resolution", func() {
|
||
|
|
BeforeEach(func() {
|
||
|
|
setupHarness()
|
||
|
|
})
|
||
|
|
|
||
|
|
When("the album is single-disc with a disc1.jpg in the only folder", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// ├── 01 - Track.mp3
|
||
|
|
// └── disc1.jpg ← matched by disc*.*
|
||
|
|
It("returns the disc1.jpg image (matched as disc*.*)", func() {
|
||
|
|
conf.Server.DiscArtPriority = "disc*.*, cd*.*, cover.*, folder.*, front.*, embedded"
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
|
||
|
|
"Artist/Album/disc1.jpg": imageFile("disc1-image"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
discID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 1), &al.UpdatedAt)
|
||
|
|
Expect(readArtwork(discID)).To(Equal(imageBytes("disc1-image")))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
When("the album has no per-disc image and no album cover", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// └── 01 - Track.mp3 (no disc or album art — returns ErrUnavailable)
|
||
|
|
It("returns ErrUnavailable for the disc lookup", func() {
|
||
|
|
conf.Server.DiscArtPriority = "disc*.*, cd*.*"
|
||
|
|
conf.Server.CoverArtPriority = "cover.*, folder.*"
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
discID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 1), &al.UpdatedAt)
|
||
|
|
_, err := readArtworkOrErr(discID)
|
||
|
|
Expect(err).To(HaveOccurred())
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
When("the album has no per-disc image but has an album cover", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// ├── 01 - Track.mp3
|
||
|
|
// └── cover.jpg ← album-level fallback (no disc art present)
|
||
|
|
It("falls back to the album cover", func() {
|
||
|
|
conf.Server.DiscArtPriority = "disc*.*, cd*.*"
|
||
|
|
conf.Server.CoverArtPriority = defaultCoverPriority
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
|
||
|
|
"Artist/Album/cover.jpg": imageFile("album-cover"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
discID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 1), &al.UpdatedAt)
|
||
|
|
Expect(readArtwork(discID)).To(Equal(imageBytes("album-cover")))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
When("multiple disc images exist in the same folder (disc1 vs disc10)", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// ├── 01 - Track.mp3
|
||
|
|
// ├── disc1.jpg ← matches request for disc 1
|
||
|
|
// └── disc10.jpg
|
||
|
|
It("matches the requested disc number, not a higher-numbered one", func() {
|
||
|
|
conf.Server.DiscArtPriority = "disc*.*"
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
|
||
|
|
"Artist/Album/disc1.jpg": imageFile("disc-one"),
|
||
|
|
"Artist/Album/disc10.jpg": imageFile("disc-ten"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
discID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 1), &al.UpdatedAt)
|
||
|
|
Expect(readArtwork(discID)).To(Equal(imageBytes("disc-one")))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
When("a multi-disc album has per-disc covers", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// ├── CD1/
|
||
|
|
// │ ├── 01 - Track.mp3
|
||
|
|
// │ └── disc1.jpg ← matches request for disc 1
|
||
|
|
// └── CD2/
|
||
|
|
// ├── 01 - Track.mp3
|
||
|
|
// └── disc2.jpg ← matches request for disc 2
|
||
|
|
It("returns the requested disc's image", func() {
|
||
|
|
conf.Server.DiscArtPriority = "disc*.*"
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/CD1/01 - Track.mp3": trackFile(1, "T1", map[string]any{"disc": "1"}),
|
||
|
|
"Artist/Album/CD2/01 - Track.mp3": trackFile(1, "T2", map[string]any{"disc": "2"}),
|
||
|
|
"Artist/Album/CD1/disc1.jpg": imageFile("disc-1"),
|
||
|
|
"Artist/Album/CD2/disc2.jpg": imageFile("disc-2"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
discID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 2), &al.UpdatedAt)
|
||
|
|
Expect(readArtwork(discID)).To(Equal(imageBytes("disc-2")))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
// Doc scenarios from:
|
||
|
|
// https://www.navidrome.org/docs/usage/library/artwork/#disc-cover-art
|
||
|
|
// Default DiscArtPriority is "disc*.*, cd*.*, cover.*, folder.*, front.*, discsubtitle, embedded".
|
||
|
|
When("a disc subfolder has a cd2.png image", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// ├── CD1/
|
||
|
|
// │ ├── 01 - Track.mp3
|
||
|
|
// │ └── disc1.jpg
|
||
|
|
// └── CD2/
|
||
|
|
// ├── 01 - Track.mp3
|
||
|
|
// └── cd2.png ← matched by cd*.* for disc 2
|
||
|
|
It("matches via the cd*.* pattern", func() {
|
||
|
|
conf.Server.DiscArtPriority = defaultDiscPriority
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/CD1/01 - Track.mp3": trackFile(1, "T1", map[string]any{"disc": "1"}),
|
||
|
|
"Artist/Album/CD2/01 - Track.mp3": trackFile(1, "T2", map[string]any{"disc": "2"}),
|
||
|
|
"Artist/Album/CD1/disc1.jpg": imageFile("disc-1"),
|
||
|
|
"Artist/Album/CD2/cd2.png": imageFile("cd-2"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
discID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 2), &al.UpdatedAt)
|
||
|
|
Expect(readArtwork(discID)).To(Equal(imageBytes("cd-2")))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
When("a disc subfolder has cover.jpg but no disc*.*/cd*.* image", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// ├── CD1/
|
||
|
|
// │ ├── 01 - Track.mp3
|
||
|
|
// │ └── cover.jpg ← matched by cover.* inside disc folder
|
||
|
|
// └── CD2/
|
||
|
|
// ├── 01 - Track.mp3
|
||
|
|
// └── cover.jpg
|
||
|
|
It("falls through to cover.* inside the disc folder", func() {
|
||
|
|
conf.Server.DiscArtPriority = defaultDiscPriority
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/CD1/01 - Track.mp3": trackFile(1, "T1", map[string]any{"disc": "1"}),
|
||
|
|
"Artist/Album/CD2/01 - Track.mp3": trackFile(1, "T2", map[string]any{"disc": "2"}),
|
||
|
|
"Artist/Album/CD1/cover.jpg": imageFile("disc1-cover"),
|
||
|
|
"Artist/Album/CD2/cover.jpg": imageFile("disc2-cover"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
discID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 1), &al.UpdatedAt)
|
||
|
|
Expect(readArtwork(discID)).To(Equal(imageBytes("disc1-cover")))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
When("DiscArtPriority is the empty string", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// ├── CD1/
|
||
|
|
// │ ├── 01 - Track.mp3
|
||
|
|
// │ └── disc1.jpg (ignored — DiscArtPriority is empty)
|
||
|
|
// ├── CD2/
|
||
|
|
// │ ├── 01 - Track.mp3
|
||
|
|
// │ └── cd2.png (ignored — DiscArtPriority is empty)
|
||
|
|
// └── cover.jpg ← used for every disc (album-level fallback)
|
||
|
|
It("skips every disc-level source and returns the album cover", func() {
|
||
|
|
conf.Server.DiscArtPriority = ""
|
||
|
|
conf.Server.CoverArtPriority = defaultCoverPriority
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/CD1/01 - Track.mp3": trackFile(1, "T1", map[string]any{"disc": "1"}),
|
||
|
|
"Artist/Album/CD2/01 - Track.mp3": trackFile(1, "T2", map[string]any{"disc": "2"}),
|
||
|
|
"Artist/Album/CD1/disc1.jpg": imageFile("disc-1"),
|
||
|
|
"Artist/Album/CD2/cd2.png": imageFile("cd-2"),
|
||
|
|
"Artist/Album/cover.jpg": imageFile("album-cover"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
for _, n := range []int{1, 2} {
|
||
|
|
discID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, n), &al.UpdatedAt)
|
||
|
|
Expect(readArtwork(discID)).To(Equal(imageBytes("album-cover")),
|
||
|
|
"disc %d should use the album cover when DiscArtPriority is empty", n)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
When("the documented multi-disc layout is used (disc1.jpg + cd2.png + album-root cover.jpg)", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// ├── disc1/
|
||
|
|
// │ ├── disc1.jpg ← matched by disc*.* for disc 1
|
||
|
|
// │ ├── 01 - Track.mp3
|
||
|
|
// │ └── 02 - Track.mp3
|
||
|
|
// ├── disc2/
|
||
|
|
// │ ├── cd2.png ← matched by cd*.* for disc 2
|
||
|
|
// │ ├── 01 - Track.mp3
|
||
|
|
// │ └── 02 - Track.mp3
|
||
|
|
// └── cover.jpg (album-level fallback, unused here)
|
||
|
|
It("matches the per-disc image for each disc", func() {
|
||
|
|
conf.Server.DiscArtPriority = defaultDiscPriority
|
||
|
|
conf.Server.CoverArtPriority = defaultCoverPriority
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/disc1/01 - Track.mp3": trackFile(1, "T1", map[string]any{"disc": "1"}),
|
||
|
|
"Artist/Album/disc1/02 - Track.mp3": trackFile(2, "T2", map[string]any{"disc": "1"}),
|
||
|
|
"Artist/Album/disc2/01 - Track.mp3": trackFile(1, "T3", map[string]any{"disc": "2"}),
|
||
|
|
"Artist/Album/disc2/02 - Track.mp3": trackFile(2, "T4", map[string]any{"disc": "2"}),
|
||
|
|
"Artist/Album/disc1/disc1.jpg": imageFile("disc-1"),
|
||
|
|
"Artist/Album/disc2/cd2.png": imageFile("cd-2"),
|
||
|
|
"Artist/Album/cover.jpg": imageFile("album-root"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
disc1ID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 1), &al.UpdatedAt)
|
||
|
|
disc2ID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 2), &al.UpdatedAt)
|
||
|
|
Expect(readArtwork(disc1ID)).To(Equal(imageBytes("disc-1")))
|
||
|
|
Expect(readArtwork(disc2ID)).To(Equal(imageBytes("cd-2")))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
When("discsubtitle keyword matches an image whose stem equals the disc's subtitle", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// ├── 01 - Track.mp3 (discsubtitle="Bonus Tracks")
|
||
|
|
// └── Bonus Tracks.jpg ← matched by "discsubtitle" keyword
|
||
|
|
It("selects the subtitle-named image", func() {
|
||
|
|
conf.Server.DiscArtPriority = "discsubtitle"
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/01 - Track.mp3": trackFile(1, "T1", map[string]any{"disc": "1", "discsubtitle": "Bonus Tracks"}),
|
||
|
|
"Artist/Album/Bonus Tracks.jpg": imageFile("bonus-tracks"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
discID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 1), &al.UpdatedAt)
|
||
|
|
Expect(readArtwork(discID)).To(Equal(imageBytes("bonus-tracks")))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
When("discsubtitle is set but no image filename matches the subtitle", func() {
|
||
|
|
// Artist/
|
||
|
|
// └── Album/
|
||
|
|
// ├── 01 - Track.mp3 (discsubtitle="Bonus Tracks")
|
||
|
|
// └── cover.jpg ← wins (discsubtitle has no match, falls through)
|
||
|
|
It("falls through to the next priority entry", func() {
|
||
|
|
conf.Server.DiscArtPriority = "discsubtitle, cover.*"
|
||
|
|
setLayout(fstest.MapFS{
|
||
|
|
"Artist/Album/01 - Track.mp3": trackFile(1, "T1", map[string]any{"disc": "1", "discsubtitle": "Bonus Tracks"}),
|
||
|
|
"Artist/Album/cover.jpg": imageFile("cover"),
|
||
|
|
})
|
||
|
|
scan()
|
||
|
|
|
||
|
|
al := firstAlbum()
|
||
|
|
discID := model.NewArtworkID(model.KindDiscArtwork, model.DiscArtworkID(al.ID, 1), &al.UpdatedAt)
|
||
|
|
Expect(readArtwork(discID)).To(Equal(imageBytes("cover")))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|