* test(artwork): add e2e suite documenting album/disc resolution Adds core/artwork/e2e/ with a real-tempdir + scanner harness that exercises artwork resolution end-to-end. Covers album and disc kinds; pending (PIt) cases document two known bugs in reader_album.go for regression-guard flipping once they are fixed. * refactor(artwork): add libraryFS helper to resolve MusicFS for a library * test(artwork): tighten libraryFS test isolation and add scheme-error case * test(artwork): update libraryFS test description to match implementation * refactor(artwork): convert fromExternalFile to use fs.FS Add a temporary fromExternalFileAbs shim so existing absolute-path callers still compile; the shim is removed once all readers are migrated. * refactor(artwork): make fromExternalFileAbs a thin delegator Introduce a minimal osDirectFS adapter so the shim no longer duplicates the matching loop. Both will be removed in Task 9. * refactor(artwork): convert fromTag to taglib.OpenStream over fs.FS Add a temporary fromTagAbs shim so existing absolute-path callers still compile; removed in Task 9. Reuses the osDirectFS adapter from Task 2. * refactor(artwork): defer fs.File close until after taglib reads finish Mirror the lifetime pattern used by adapters/gotaglib/gotaglib.go: keep the underlying fs.File open until taglib.File is closed, and pass WithFilename so format detection doesn't rely on content sniffing. * docs(artwork): note ffmpeg's path-based API limitation * refactor(artwork): migrate album reader to MusicFS - Add libFS (storage.MusicFS) field to albumArtworkReader; resolved once at construction time via libraryFS() - Switch fromCoverArtPriority from abs-path shims to FS-based fromTag/fromExternalFile; only fromFFmpegTag retains absolute path - Build imgFiles as library-relative forward-slash paths in loadAlbumFoldersPaths using path.Join(f.Path, f.Name, img) - Guard embedAbs so that an empty EmbedArtPath never produces a non-empty absolute path (prevents accidental ffmpeg invocation) - Register testfile:// storage scheme in artwork test suite to provide an os.DirFS-backed MusicFS without requiring the taglib extractor - Update test assertions from filepath.FromSlash(abs) to bare forward-slash relative strings * fix(artwork): use path package in compareImageFiles for forward-slash relative paths * refactor(artwork): migrate disc reader to MusicFS Replace os.Open absolute-path access with libFS.Open on library-relative forward-slash paths. Rename discFolders→discFoldersRel, split firstTrackPath into firstTrackRelPath (for fromTag) and firstTrackAbsPath (for fromFFmpegTag), and switch path.Dir/Base/Ext for forward-slash safety. * refactor(artwork): build discFoldersRel directly and guard empty first track * refactor(artwork): migrate mediafile reader to MusicFS * refactor(artwork): migrate artist album-art lookup to MusicFS * refactor(artwork): remove temporary path-based shims All readers now use the FS-based fromTag and fromExternalFile directly, so the absolute-path adapters and the osDirectFS helper that backed them can go away. * test(artwork): rewrite e2e suite to use storagetest.FakeFS Switches from real-tempdir + local storage to FakeFS via the storage registry. Adds a proper multi-disc scenario using the disc tag, which previously required curated MP3 fixtures we did not have. * test(artwork): use maps.Copy in trackFile tag merge Lint cleanup: replace the manual map-copy loop flagged by mapsloop. * test(artwork): reuse tests.MockFFmpeg in e2e harness Replace the hand-rolled noopFFmpeg stub with tests.NewMockFFmpeg, which already satisfies the full ffmpeg.FFmpeg interface and won't drift when new methods are added. Also tie imageBytes to imageFile so they cannot silently disagree on the on-disk encoding. * test(artwork): add e2e scenarios from artwork documentation Covers the behaviors documented at https://www.navidrome.org/docs/usage/library/artwork/: - Album: folder.*/front.* fallbacks and priority order with cover.*. - Disc: cd*.* match, cover.* inside disc folder, DiscArtPriority="" skip path, the documented multi-disc layout, and the discsubtitle keyword. - MediaFile: disc-level fallback for multi-disc tracks and album-level fallback for single-disc tracks (doc section "MediaFiles" items 2-3). - Artist: album/artist.* lookup via libFS (passes). The artist-folder branch is XIt-marked because fromArtistFolder still calls os.DirFS directly on an absolute path and can't read from a FakeFS-backed library — migrating that to storage.MusicFS is a follow-up. Signed-off-by: Deluan <deluan@navidrome.org> * refactor(artwork): scope artist folder traversal to library root Route fromArtistFolder reads through storage.MusicFS and bound the parent-directory walk at the library root. This keeps artwork resolution scoped to the configured library and unblocks FakeFS-backed e2e scenarios that depend on the artist folder. Also consolidate the libraryFS + core.AbsolutePath pairing (used by three readers) into a single libraryFSAndRoot helper. * test(artwork): add ASCII file-tree diagrams to e2e scenarios Each It/PIt block now shows the on-disk layout it exercises, with arrows indicating which file wins (or should win, for the known-bug PIt cases). Makes scenarios readable at a glance without having to parse the MapFS map. * test(artwork): add e2e tests for playlist and radio artwork resolution Signed-off-by: Deluan <deluan@navidrome.org> * test(artwork): enhance e2e tests with real MP3 fixtures for embedded artwork Signed-off-by: Deluan <deluan@navidrome.org> * test(ffmpeg): add support for animated WebP encoder detection and fallback handling Signed-off-by: Deluan <deluan@navidrome.org> * test(artwork): cover additional edge cases in e2e suite Add high-value scenarios uncovered by the existing specs: - Album: three-way basename tie (unsuffixed wins), unknown pattern in CoverArtPriority is skipped, embedded-first with no embedded art falls through. - Disc: discsubtitle with no matching image falls through. - Artist: ArtistArtPriority can reach images via album/<pattern>. - Playlist: generates a 2x2 tiled cover from album art when the playlist has no uploaded/sidecar/external image. New helper realPNG() produces real taglib/image-decodable bytes so the tiled-cover test can exercise the generator's decode + compose path. * test(artwork): refactor image upload logic in e2e tests for consistency Signed-off-by: Deluan <deluan@navidrome.org> * test(ffmpeg): simplify animated WebP encoder check by removing context parameter Signed-off-by: Deluan <deluan@navidrome.org> * fix(artwork): normalize rel path for fs.Glob on Windows filepath.Rel returns backslash-separated paths on Windows, but fs.Glob and path.Join require forward slashes. Convert with filepath.ToSlash after computing the relative path and use path.Dir for the parent walk so the artist-folder lookup works cross-platform. * fix(ffmpeg): retry animated WebP probe on transient failure The probe previously used the caller's request context inside sync.Once, so a single cancelled first request would permanently disable animated WebP for the rest of the process. Switch to a mutex + probed flag, use a fresh background context with its own timeout, and only cache the result when the probe actually succeeds. * test(ffmpeg): reset ffOnce so ConvertAnimatedImage test is order-independent The ConvertAnimatedImage stand-in test sets ffmpegPath directly but does not reset ffOnce. If ffmpegCmd() has not been called earlier in the test process, the next call inside hasAnimatedWebPEncoder runs ffOnce.Do and re-resolves the real ffmpeg binary, overwriting the stand-in and breaking the test. Reset ffOnce and conf.Server.FFmpegPath alongside the other globals to pin resolution to the stand-in. * test(artwork): unblock Windows CI — forward-slash fs paths and suite-level DB lifetime The internal artwork test planted a Windows absolute path (backslashes) into Folder.Path and then fed it through libFS.Open, which fs.ValidPath rejects. Rooting the testfile library at the temp dir directly and using filepath.ToSlash keeps the path model library-relative and forward-slash, matching production. The e2e suite opened a per-spec DB in a per-spec TempDir, but the go-sqlite3 singleton kept the file open across specs. Ginkgo's per-spec TempDir cleanup then tried to unlink a file still held by that handle — fine on POSIX, fails on Windows. Moving the DB to a suite-level tempdir and closing it in AfterSuite avoids the race. * test(artwork): keep Windows drive letters intact in testfile library URLs url.Parse on `testfile://C:/path` reads `C` as the host and the path loses the drive letter, so Windows libFS lookups go to `/path` and fail. testFileLibPath now prepends a `/` when the OS path has no leading slash, and the testfile constructor strips that extra slash back off before handing the path to os.Stat / os.DirFS. * refactor(artwork): consolidate libFS + root into libraryView helper Collapses the per-reader libFS/libPath/rootFolder/firstTrackAbsPath fields into a single libraryView{FS, absRoot} with an Abs(rel) method. Also folds the two library lookups (ds.Library.Get + core.AbsolutePath) into one, and uses mf.Path directly instead of stripping libRoot off an absolute path. * refactor(ffmpeg): replace hasAnimatedWebPEncoder with encoderProbe for state management Signed-off-by: Deluan <deluan@navidrome.org> * fix: escape artist folder names in artwork glob Escape glob metacharacters in the library-relative artist folder path before composing the fs.Glob pattern for artist image lookup. This preserves literal folder names such as Artist [Live] while keeping the configured filename pattern behavior unchanged, and adds a regression test for bracketed artist folders. Signed-off-by: Deluan <deluan@navidrome.org> * fix(artwork): correct test path assertions after MusicFS migration Source functions (fromTag, fromExternalFile) now return forward-slash fs.FS-relative paths, so test assertions should compare against plain forward-slash strings, not filepath.FromSlash(). The artistArtPriority test needs filepath.FromSlash() on the suffix because findImageInFolder returns OS-native absolute paths via filepath.Join. * fix(artwork): normalize path separators in artistArtPriority assertion The two table entries exercise different code paths: entry 1 goes through fromArtistFolder (returns OS-native paths via filepath.Join), while entry 2 goes through fromExternalFile (returns forward-slash fs.FS paths). Using filepath.FromSlash on the expected value only works for entry 1. Normalize the actual path to forward slashes with filepath.ToSlash so a single HaveSuffix assertion works for both code paths on all platforms. --------- Signed-off-by: Deluan <deluan@navidrome.org>
354 lines
13 KiB
Go
354 lines
13 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"
|
|
)
|
|
|
|
const (
|
|
defaultCoverPriority = "cover.*, folder.*, front.*, embedded, external"
|
|
defaultDiscPriority = "disc*.*, cd*.*, cover.*, folder.*, front.*, discsubtitle, embedded"
|
|
)
|
|
|
|
var _ = Describe("Album artwork resolution", func() {
|
|
BeforeEach(func() {
|
|
setupHarness()
|
|
})
|
|
|
|
When("an album has a single folder with cover.jpg at the album root", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3
|
|
// └── cover.jpg ← matched by cover.*
|
|
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": imageFile("album-root"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("album-root")))
|
|
})
|
|
})
|
|
|
|
// Bug 2 variant: cover.* basenames tie across album-root and per-disc folders;
|
|
// compareImageFiles' lexicographic full-path tiebreaker ranks disc-subfolder
|
|
// files first. Flip from PIt to It once it prefers shorter/parent paths.
|
|
When("a multi-disc album has a cover.jpg at the album root and per-disc covers", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── CD1/
|
|
// │ ├── 01 - Track.mp3
|
|
// │ └── cover.jpg ← currently wins (bug)
|
|
// ├── CD2/
|
|
// │ ├── 01 - Track.mp3
|
|
// │ └── cover.jpg
|
|
// └── cover.jpg ← should win (album-root fallback)
|
|
PIt("uses the album-root cover (currently picks a disc subfolder image — bug)", 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": imageFile("album-root"),
|
|
"Artist/Album/CD1/cover.jpg": imageFile("disc1"),
|
|
"Artist/Album/CD2/cover.jpg": imageFile("disc2"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(al.FolderIDs).To(HaveLen(2),
|
|
"sanity check: scanner should treat the two disc subfolders as one multi-disc album")
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("album-root")))
|
|
})
|
|
})
|
|
|
|
// Bug 2: folder.jpg basenames tie across album-root and per-disc folders;
|
|
// the lexicographic full-path tiebreaker in compareImageFiles ranks
|
|
// "Artist/Album/CD1/folder.jpg" ahead of "Artist/Album/folder.jpg".
|
|
// Flip from PIt to It once compareImageFiles prefers shorter/parent paths.
|
|
When("a multi-disc album has folder.jpg at the album root AND in each disc subfolder", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── CD1/
|
|
// │ ├── 01 - Track.mp3
|
|
// │ └── folder.jpg ← currently wins (bug)
|
|
// ├── CD2/
|
|
// │ ├── 01 - Track.mp3
|
|
// │ └── folder.jpg
|
|
// └── folder.jpg ← should win (album-root fallback)
|
|
PIt("uses the album-root folder.jpg (currently picks a disc subfolder image — bug)", 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": imageFile("album-root"),
|
|
"Artist/Album/CD1/folder.jpg": imageFile("disc1"),
|
|
"Artist/Album/CD2/folder.jpg": imageFile("disc2"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("album-root")))
|
|
})
|
|
})
|
|
|
|
// Bug 1: commonParentFolder's `len(folders) < 2` guard skips the parent-folder
|
|
// lookup whenever an album lives entirely under a single subfolder, so an
|
|
// album-root cover is never considered. Flip from PIt to It once the guard
|
|
// accepts single-folder albums whose parent isn't already in the folder set.
|
|
When("an album lives entirely under a single disc subfolder with cover.jpg at the parent", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── disc1/
|
|
// │ └── 01 - Track.mp3
|
|
// └── cover.jpg ← should win (parent-folder fallback, currently ignored — bug)
|
|
PIt("uses the parent-folder cover (currently ignored — bug)", func() {
|
|
conf.Server.CoverArtPriority = defaultCoverPriority
|
|
setLayout(fstest.MapFS{
|
|
"Artist/Album/disc1/01 - Track.mp3": trackFile(1, "Track"),
|
|
"Artist/Album/cover.jpg": imageFile("album-root"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("album-root")))
|
|
})
|
|
})
|
|
|
|
When("CoverArtPriority puts embedded first and the album has both embedded and external art", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3 ← has embedded picture (wins via "embedded")
|
|
// └── cover.jpg
|
|
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": imageFile("external"),
|
|
})
|
|
scan()
|
|
// Swap in real MP3 bytes so libFS.Open returns a taglib-readable stream.
|
|
replaceWithRealMP3("Artist/Album/01 - Track.mp3")
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(embeddedArtBytes))
|
|
})
|
|
})
|
|
|
|
When("CoverArtPriority lists external first but no external file is present", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// └── 01 - Track.mp3 ← has embedded picture (falls through to "embedded")
|
|
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")
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(embeddedArtBytes))
|
|
})
|
|
})
|
|
|
|
When("the only cover file uses uppercase extension and a different case in its name", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3
|
|
// └── Cover.JPG ← matched case-insensitively by cover.*
|
|
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": imageFile("case-insensitive"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("case-insensitive")))
|
|
})
|
|
})
|
|
|
|
When("two cover files have basenames that tie under the natural-sort tiebreaker", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3
|
|
// ├── cover.jpg ← wins (no numeric suffix)
|
|
// └── cover.1.jpg
|
|
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": imageFile("primary"),
|
|
"Artist/Album/cover.1.jpg": imageFile("secondary"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("primary")))
|
|
})
|
|
})
|
|
|
|
When("the album has no cover and CoverArtPriority lists only file patterns", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// └── 01 - Track.mp3 (no image files — returns ErrUnavailable)
|
|
It("returns ErrUnavailable", func() {
|
|
conf.Server.CoverArtPriority = "cover.*, folder.*"
|
|
setLayout(fstest.MapFS{
|
|
"Artist/Album/01 - Track.mp3": trackFile(1, "Track"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
_, err := readArtworkOrErr(model.NewArtworkID(model.KindAlbumArtwork, al.ID, &al.UpdatedAt))
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
// Doc scenarios from:
|
|
// https://www.navidrome.org/docs/usage/library/artwork/#albums
|
|
// Default CoverArtPriority is "cover.*, folder.*, front.*, embedded, external".
|
|
When("only folder.jpg is present (cover.* and front.* missing)", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3
|
|
// └── folder.jpg ← matched by folder.*
|
|
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": imageFile("folder"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("folder")))
|
|
})
|
|
})
|
|
|
|
When("only front.jpg is present (cover.* and folder.* missing)", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3
|
|
// └── front.jpg ← matched by front.*
|
|
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": imageFile("front"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("front")))
|
|
})
|
|
})
|
|
|
|
When("cover.*, folder.*, and front.* all exist in the same folder", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3
|
|
// ├── cover.jpg ← wins (cover.* is first in priority)
|
|
// ├── folder.jpg
|
|
// └── front.jpg
|
|
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": imageFile("cover"),
|
|
"Artist/Album/folder.jpg": imageFile("folder"),
|
|
"Artist/Album/front.jpg": imageFile("front"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("cover")))
|
|
})
|
|
})
|
|
|
|
When("only folder.* and front.* exist (priority order check)", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3
|
|
// ├── folder.jpg ← wins (folder.* comes before front.*)
|
|
// └── front.jpg
|
|
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": imageFile("folder"),
|
|
"Artist/Album/front.jpg": imageFile("front"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("folder")))
|
|
})
|
|
})
|
|
|
|
When("three cover files tie by basename and differ only by numeric suffix", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3
|
|
// ├── cover.jpg ← wins (no numeric suffix)
|
|
// ├── cover.1.jpg
|
|
// └── cover.2.jpg
|
|
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": imageFile("second"),
|
|
"Artist/Album/cover.jpg": imageFile("primary"),
|
|
"Artist/Album/cover.1.jpg": imageFile("first"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("primary")))
|
|
})
|
|
})
|
|
|
|
When("CoverArtPriority contains an unknown pattern before a matching one", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3
|
|
// └── cover.jpg ← wins (unknown "bogus.*" is skipped)
|
|
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": imageFile("cover"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("cover")))
|
|
})
|
|
})
|
|
|
|
When("embedded is first in CoverArtPriority but the track has no embedded art", func() {
|
|
// Artist/
|
|
// └── Album/
|
|
// ├── 01 - Track.mp3 (no embedded picture)
|
|
// └── cover.jpg ← wins (embedded skipped, falls through)
|
|
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": imageFile("cover"),
|
|
})
|
|
scan()
|
|
|
|
al := firstAlbum()
|
|
Expect(readArtwork(al.CoverArtID())).To(Equal(imageBytes("cover")))
|
|
})
|
|
})
|
|
})
|