mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
ed4178a6 gated serveDisc on len(album.Discs) > 1, claiming parity with the legacy reader. The legacy reader has no such gate: artwork.go dispatches every dc- id to newDiscArtworkReader, whose Reader() walks DiscArtPriority unconditionally. The gate also lost art. For a single-disc album whose only image is disc1.jpg, the disc request skipped the chain and fell through to album art, which does not match CoverArtPriority — so tracks tagged disc 1 (whose CoverArtID is a dc- id) served nothing at all, where before they served disc1.jpg. A single disc can legitimately have its own cover, distinct from the album's, and DiscArtPriority is what expresses that preference. Drop the gate and restore the single-disc e2e scenarios that covered it.
335 lines
13 KiB
Go
335 lines
13 KiB
Go
package e2e
|
|
|
|
import (
|
|
"fmt"
|
|
"testing/fstest"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/core/artwork"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// Disc art is a serve-time read through the library FS (no worker state row), so per-disc images
|
|
// are asserted byte-for-byte. Single-disc albums run the disc chain too — a disc can carry art
|
|
// distinct from the album cover. Album-root covers are folder-backed and asserted on the state row.
|
|
var _ = Describe("Disc artwork resolution", func() {
|
|
BeforeEach(func() {
|
|
setupResolutionHarness()
|
|
})
|
|
|
|
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": smallPNG("disc1-image"),
|
|
})
|
|
scan()
|
|
expectDiscImage(firstAlbum(), 1, "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 — nothing to serve)
|
|
It("reports the disc lookup as unavailable", func() {
|
|
conf.Server.DiscArtPriority = "disc*.*, cd*.*"
|
|
conf.Server.CoverArtPriority = "cover.*, folder.*"
|
|
setLayout(fstest.MapFS{
|
|
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
|
|
})
|
|
scan()
|
|
Expect(serveErr(discArtID(firstAlbum(), 1))).To(MatchError(artwork.ErrUnavailable))
|
|
})
|
|
})
|
|
|
|
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": smallPNG("album-cover"),
|
|
})
|
|
scan()
|
|
expectDiscImage(firstAlbum(), 1, "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": smallPNG("disc-one"),
|
|
"Artist/Album/disc10.jpg": smallPNG("disc-ten"),
|
|
})
|
|
scan()
|
|
expectDiscImage(firstAlbum(), 1, "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": smallPNG("disc-1"),
|
|
"Artist/Album/CD2/disc2.jpg": smallPNG("disc-2"),
|
|
})
|
|
scan()
|
|
expectDiscImage(firstAlbum(), 2, "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": smallPNG("disc-1"),
|
|
"Artist/Album/CD2/cd2.png": smallPNG("cd-2"),
|
|
})
|
|
scan()
|
|
expectDiscImage(firstAlbum(), 2, "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": smallPNG("disc1-cover"),
|
|
"Artist/Album/CD2/cover.jpg": smallPNG("disc2-cover"),
|
|
})
|
|
scan()
|
|
expectDiscImage(firstAlbum(), 1, "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": smallPNG("disc-1"),
|
|
"Artist/Album/CD2/cd2.png": smallPNG("cd-2"),
|
|
"Artist/Album/cover.jpg": smallPNG("album-cover"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
for _, n := range []int{1, 2} {
|
|
expectDiscImage(al, n, "album-cover")
|
|
}
|
|
})
|
|
})
|
|
|
|
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": smallPNG("disc-1"),
|
|
"Artist/Album/disc2/cd2.png": smallPNG("cd-2"),
|
|
"Artist/Album/cover.jpg": smallPNG("album-root"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
expectDiscImage(al, 1, "disc-1")
|
|
expectDiscImage(al, 2, "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": smallPNG("bonus-tracks"),
|
|
})
|
|
scan()
|
|
expectDiscImage(firstAlbum(), 1, "bonus-tracks")
|
|
})
|
|
})
|
|
|
|
// Reproduces https://github.com/navidrome/navidrome/issues/5456
|
|
// Deeply nested layout matching the reporter's actual structure.
|
|
When("a deeply nested multi-disc album has cover.jpg and per-disc folder.jpg", func() {
|
|
// Genre/Artist/Album/ ← album root with cover.jpg
|
|
// ├── cover.jpg ← album-level cover
|
|
// ├── Disc 01 (Subtitle)/
|
|
// │ ├── 01 - Track.mp3
|
|
// │ └── folder.jpg ← disc 1 art
|
|
// ├── Disc 02 (Subtitle)/
|
|
// │ ├── 01 - Track.mp3
|
|
// │ └── folder.jpg
|
|
// └── ... (12 discs)
|
|
It("uses album-root cover.jpg for album art and per-disc folder.jpg for each disc", func() {
|
|
conf.Server.DiscArtPriority = defaultDiscPriority
|
|
conf.Server.CoverArtPriority = defaultCoverPriority
|
|
discNames := []string{
|
|
"Disc 01 (Birth of the Dead - The Studio Sides)",
|
|
"Disc 02 (Birth of the Dead - The Live Sides)",
|
|
"Disc 03 (The Grateful Dead)",
|
|
"Disc 04 (Anthem of the Sun)",
|
|
"Disc 05 (Aoxomoxoa)",
|
|
"Disc 06 (Live; Dead)",
|
|
"Disc 07 (Workingman's Dead)",
|
|
"Disc 08 (American Beauty)",
|
|
"Disc 09 (Grateful Dead)",
|
|
"Disc 10 (Europe '72)",
|
|
"Disc 11 (Europe '72)",
|
|
"Disc 12 (History of the Grateful Dead, Volume One (Bear's Choice))",
|
|
}
|
|
layout := fstest.MapFS{
|
|
"Pop; Rock/Grateful Dead/(2001) The Golden Road/cover.jpg": smallPNG("album-root-cover"),
|
|
}
|
|
for i, name := range discNames {
|
|
discNum := i + 1
|
|
prefix := fmt.Sprintf("Pop; Rock/Grateful Dead/(2001) The Golden Road/%s/", name)
|
|
layout[prefix+"01 - Track.mp3"] = trackFile(1, fmt.Sprintf("T%d", discNum), map[string]any{"disc": fmt.Sprintf("%d", discNum)})
|
|
layout[prefix+"folder.jpg"] = smallPNG(fmt.Sprintf("disc-%02d-folder", discNum))
|
|
}
|
|
setLayout(layout)
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
expectAlbumFolderCover(al, "(2001) The Golden Road/cover.jpg")
|
|
for i := range discNames {
|
|
discNum := i + 1
|
|
expectDiscImage(al, discNum, fmt.Sprintf("disc-%02d-folder", discNum))
|
|
}
|
|
})
|
|
})
|
|
|
|
// https://github.com/navidrome/navidrome/issues/5456
|
|
// Top-level album variant — album folder at library root (Path=".").
|
|
When("a top-level multi-disc album has cover.jpg and per-disc folder.jpg", func() {
|
|
// Album/ (top-level, Path=".")
|
|
// ├── cover.jpg ← album-level cover
|
|
// ├── Disc 01/
|
|
// │ ├── 01 - Track.mp3
|
|
// │ └── folder.jpg ← disc 1 art
|
|
// ├── Disc 02/
|
|
// │ ├── 01 - Track.mp3
|
|
// │ └── folder.jpg
|
|
// └── Disc 03/
|
|
// ├── 01 - Track.mp3
|
|
// └── folder.jpg
|
|
It("uses album-root cover.jpg for album art and per-disc folder.jpg for each disc", func() {
|
|
conf.Server.DiscArtPriority = defaultDiscPriority
|
|
conf.Server.CoverArtPriority = defaultCoverPriority
|
|
layout := fstest.MapFS{
|
|
"Album/cover.jpg": smallPNG("album-root-cover"),
|
|
}
|
|
for i := 1; i <= 3; i++ {
|
|
prefix := fmt.Sprintf("Album/Disc %02d/", i)
|
|
layout[prefix+"01 - Track.mp3"] = trackFile(1, fmt.Sprintf("T%d", i), map[string]any{"disc": fmt.Sprintf("%d", i)})
|
|
layout[prefix+"folder.jpg"] = smallPNG(fmt.Sprintf("disc-%02d-folder", i))
|
|
}
|
|
setLayout(layout)
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
expectAlbumFolderCover(al, "Album/cover.jpg")
|
|
for i := 1; i <= 3; i++ {
|
|
expectDiscImage(al, i, fmt.Sprintf("disc-%02d-folder", i))
|
|
}
|
|
})
|
|
})
|
|
|
|
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": smallPNG("cover"),
|
|
})
|
|
scan()
|
|
expectDiscImage(firstAlbum(), 1, "cover")
|
|
})
|
|
})
|
|
})
|