package e2e
import (
"testing/fstest"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Album artwork resolution", func() {
BeforeEach(func() {
setupResolutionHarness()
})
When("an album has a single folder with cover.jpg at the album root", func() {
It("returns the album-root cover", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/cover.jpg": smallPNG("album-root"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/cover.jpg")
})
})
When("a multi-disc album has a cover.jpg at the album root and per-disc covers", func() {
It("prefers the album-root cover over per-disc covers", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/Album/CD1/01 - Track.mp3": trackFile(1, "Track CD1"),
"Artist/Album/CD2/01 - Track.mp3": trackFile(1, "Track CD2"),
"Artist/Album/cover.jpg": smallPNG("album-root"),
"Artist/Album/CD1/cover.jpg": smallPNG("disc1"),
"Artist/Album/CD2/cover.jpg": smallPNG("disc2"),
})
scan()
al := firstAlbum()
Expect(al.FolderIDs).To(HaveLen(2),
"sanity check: the two disc subfolders should form one multi-disc album")
expectAlbumFolderCover(al, "Artist/Album/cover.jpg")
})
})
When("a multi-disc album has folder.jpg at the album root AND in each disc subfolder", func() {
It("prefers the album-root folder.jpg over per-disc folder.jpg", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/Album/CD1/01 - Track.mp3": trackFile(1, "Track CD1"),
"Artist/Album/CD2/01 - Track.mp3": trackFile(1, "Track CD2"),
"Artist/Album/folder.jpg": smallPNG("album-root"),
"Artist/Album/CD1/folder.jpg": smallPNG("disc1"),
"Artist/Album/CD2/folder.jpg": smallPNG("disc2"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/folder.jpg")
})
})
When("an album lives entirely under a single disc subfolder with cover.jpg at the parent", func() {
It("uses the parent-folder cover for single-disc-subfolder albums", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/Album/disc1/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/cover.jpg": smallPNG("album-root"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/cover.jpg")
})
})
When("a top-level multi-disc album has cover.jpg at the album root and per-disc folder.jpg", func() {
It("prefers the album-root cover.jpg", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Album/CD1/01 - Track.mp3": trackFile(1, "Track CD1"),
"Album/CD2/01 - Track.mp3": trackFile(1, "Track CD2"),
"Album/cover.jpg": smallPNG("album-root"),
"Album/CD1/folder.jpg": smallPNG("disc1"),
"Album/CD2/folder.jpg": smallPNG("disc2"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Album/cover.jpg")
})
})
When("CoverArtPriority puts embedded first and the album has both embedded and external art", func() {
It("returns the embedded image", func() {
conf.Server.CoverArtPriority = "embedded, cover.*, folder.*, front.*, external"
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track", map[string]any{"has_picture": "true"}),
"Artist/Album/cover.jpg": smallPNG("external"),
})
scan()
replaceWithRealMP3("Artist/Album/01 - Track.mp3")
ia := acquire(model.KindAlbumArtwork, firstAlbum().ID)
Expect(ia.Source).To(Equal("embedded"))
Expect(storedBytes(ia)).To(Equal(embeddedArtBytes))
})
})
When("CoverArtPriority lists external first but no external file is present", func() {
It("falls through to embedded artwork", func() {
conf.Server.CoverArtPriority = "external, embedded"
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track", map[string]any{"has_picture": "true"}),
})
scan()
replaceWithRealMP3("Artist/Album/01 - Track.mp3")
ia := acquire(model.KindAlbumArtwork, firstAlbum().ID)
Expect(ia.Source).To(Equal("embedded"))
Expect(storedBytes(ia)).To(Equal(embeddedArtBytes))
})
})
When("the only cover file uses uppercase extension and a different case in its name", func() {
It("matches case-insensitively against cover.*", func() {
conf.Server.CoverArtPriority = "cover.*, folder.*"
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/Cover.JPG": smallPNG("case-insensitive"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/Cover.JPG")
})
})
When("two cover files have basenames that tie under the natural-sort tiebreaker", func() {
It("prefers the file without a numeric suffix", func() {
conf.Server.CoverArtPriority = "cover.*"
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/cover.jpg": smallPNG("primary"),
"Artist/Album/cover.1.jpg": smallPNG("secondary"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/cover.jpg")
})
})
When("the album has no cover and CoverArtPriority lists only file patterns", func() {
It("settles absent", func() {
conf.Server.CoverArtPriority = "cover.*, folder.*"
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
})
scan()
expectAlbumAbsent(firstAlbum())
})
})
When("only folder.jpg is present (cover.* and front.* missing)", func() {
It("falls through to folder.jpg", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/folder.jpg": smallPNG("folder"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/folder.jpg")
})
})
When("only front.jpg is present (cover.* and folder.* missing)", func() {
It("falls through to front.jpg", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/front.jpg": smallPNG("front"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/front.jpg")
})
})
When("cover.*, folder.*, and front.* all exist in the same folder", func() {
It("prefers cover.* (first in CoverArtPriority)", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/cover.jpg": smallPNG("cover"),
"Artist/Album/folder.jpg": smallPNG("folder"),
"Artist/Album/front.jpg": smallPNG("front"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/cover.jpg")
})
})
When("only folder.* and front.* exist (priority order check)", func() {
It("prefers folder.* over front.*", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/folder.jpg": smallPNG("folder"),
"Artist/Album/front.jpg": smallPNG("front"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/folder.jpg")
})
})
When("three cover files tie by basename and differ only by numeric suffix", func() {
It("selects the unsuffixed file first regardless of numeric-suffix order", func() {
conf.Server.CoverArtPriority = "cover.*"
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/cover.2.jpg": smallPNG("second"),
"Artist/Album/cover.jpg": smallPNG("primary"),
"Artist/Album/cover.1.jpg": smallPNG("first"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/cover.jpg")
})
})
When("CoverArtPriority contains an unknown pattern before a matching one", func() {
It("skips the unknown pattern and falls through to the matching one", func() {
conf.Server.CoverArtPriority = "bogus.*, cover.*"
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/cover.jpg": smallPNG("cover"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/cover.jpg")
})
})
When("an album has no images and the artist folder has folder.jpg", func() {
It("does not use the artist image as album art", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/folder.jpg": smallPNG("artist-thumbnail"),
"Artist/Album A/01 - Track.mp3": trackFile(1, "Track A", map[string]any{"album": "Album A", "albumartist": "Artist"}),
"Artist/Album B/01 - Track.mp3": trackFile(1, "Track B", map[string]any{"album": "Album B", "albumartist": "Artist"}),
"Artist/Album B/cover.jpg": smallPNG("album-b"),
})
scan()
expectAlbumFolderCover(albumByName("Album B"), "Artist/Album B/cover.jpg")
expectAlbumAbsent(albumByName("Album A"))
})
})
When("a single-disc album is spread across sibling folders under the artist folder", func() {
It("does not use the artist image as album art for the spread album", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/folder.jpg": smallPNG("artist-thumbnail"),
"Artist/Album A/01 - Track.mp3": trackFile(1, "Track A1", map[string]any{"album": "Album A", "albumartist": "Artist"}),
"Artist/Album A bonus/02 - Track.mp3": trackFile(2, "Track A2", map[string]any{"album": "Album A", "albumartist": "Artist"}),
"Artist/Album B/01 - Track.mp3": trackFile(1, "Track B", map[string]any{"album": "Album B", "albumartist": "Artist"}),
"Artist/Album B/cover.jpg": smallPNG("album-b"),
})
scan()
alA := albumByName("Album A")
Expect(alA.FolderIDs).To(HaveLen(2),
"sanity check: the two sibling folders should form one spread album")
expectAlbumAbsent(alA)
})
})
When("a multi-disc album sits directly at the library root with a cover.jpg beside it", func() {
It("does not adopt the library-root image as album art", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"cover.jpg": smallPNG("library-root"),
"CD1/01 - Track.mp3": trackFile(1, "T1", map[string]any{"album": "Rootless", "disc": "1"}),
"CD2/01 - Track.mp3": trackFile(1, "T2", map[string]any{"album": "Rootless", "disc": "2"}),
})
scan()
expectAlbumAbsent(firstAlbum())
})
})
When("a single-folder album has its own cover.jpg and the artist folder has one too", func() {
It("prefers the album's own cover over the shallower artist-folder cover", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/cover.jpg": smallPNG("artist-image"),
"Artist/Album/01 - Track.mp3": trackFile(1, "Track", map[string]any{"albumartist": "Artist"}),
"Artist/Album/cover.jpg": smallPNG("album-own"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/cover.jpg")
})
})
When("a spread album has its own front.jpg but the artist folder has cover.jpg", func() {
It("prefers the album's own art over the artist image", func() {
conf.Server.CoverArtPriority = defaultCoverPriority
setLayout(fstest.MapFS{
"Artist/cover.jpg": smallPNG("artist-image"),
"Artist/Album A/01 - Track.mp3": trackFile(1, "Track A1", map[string]any{"album": "Album A", "albumartist": "Artist"}),
"Artist/Album A/front.jpg": smallPNG("album-a-front"),
"Artist/Album A bonus/02 - Track.mp3": trackFile(2, "Track A2", map[string]any{"album": "Album A", "albumartist": "Artist"}),
"Artist/Album B/01 - Track.mp3": trackFile(1, "Track B", map[string]any{"album": "Album B", "albumartist": "Artist"}),
})
scan()
alA := albumByName("Album A")
Expect(alA.FolderIDs).To(HaveLen(2),
"sanity check: the two sibling folders should form one spread album")
expectAlbumFolderCover(alA, "Artist/Album A/front.jpg")
})
})
When("embedded is first in CoverArtPriority but the track has no embedded art", func() {
It("falls through to the next priority entry", func() {
conf.Server.CoverArtPriority = "embedded, cover.*"
setLayout(fstest.MapFS{
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
"Artist/Album/cover.jpg": smallPNG("cover"),
})
scan()
expectAlbumFolderCover(firstAlbum(), "Artist/Album/cover.jpg")
})
})
})