mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-14 11:17:19 +03:00
fix(artwork): always select the coverArt of the first disc in multi-disc albums (#3950)
* feat(artwork): sort image files for consistent cover art selection Signed-off-by: Deluan <deluan@navidrome.org> * feat(artwork): improve album cover art selection by considering disc numbers Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
bcea8b832a
commit
49b8cfc261
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -113,5 +114,10 @@ func loadAlbumFoldersPaths(ctx context.Context, ds model.DataStore, albums ...mo
|
|||||||
imgFiles = append(imgFiles, filepath.Join(path, img))
|
imgFiles = append(imgFiles, filepath.Join(path, img))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort image files to ensure consistent selection of cover art
|
||||||
|
// This prioritizes files from lower-numbered disc folders by sorting the paths
|
||||||
|
slices.Sort(imgFiles)
|
||||||
|
|
||||||
return paths, imgFiles, &updatedAt, nil
|
return paths, imgFiles, &updatedAt, nil
|
||||||
}
|
}
|
||||||
|
76
core/artwork/reader_album_test.go
Normal file
76
core/artwork/reader_album_test.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package artwork
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Album Artwork Reader", func() {
|
||||||
|
Describe("loadAlbumFoldersPaths", func() {
|
||||||
|
var (
|
||||||
|
ctx context.Context
|
||||||
|
ds *fakeDataStore
|
||||||
|
repo *fakeFolderRepo
|
||||||
|
album model.Album
|
||||||
|
now time.Time
|
||||||
|
expectedAt time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = context.Background()
|
||||||
|
now = time.Now().Truncate(time.Second)
|
||||||
|
expectedAt = now.Add(5 * time.Minute)
|
||||||
|
|
||||||
|
// Set up the test folders with image files
|
||||||
|
repo = &fakeFolderRepo{
|
||||||
|
result: []model.Folder{
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/Disc1",
|
||||||
|
ImagesUpdatedAt: expectedAt,
|
||||||
|
ImageFiles: []string{"cover.jpg", "back.jpg"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/Disc2",
|
||||||
|
ImagesUpdatedAt: now,
|
||||||
|
ImageFiles: []string{"cover.jpg"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/Disc10",
|
||||||
|
ImagesUpdatedAt: now,
|
||||||
|
ImageFiles: []string{"cover.jpg"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
}
|
||||||
|
ds = &fakeDataStore{
|
||||||
|
folderRepo: repo,
|
||||||
|
}
|
||||||
|
album = model.Album{
|
||||||
|
ID: "album1",
|
||||||
|
Name: "Album",
|
||||||
|
FolderIDs: []string{"folder1", "folder2", "folder3"},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns sorted image files", func() {
|
||||||
|
_, imgFiles, imagesUpdatedAt, err := loadAlbumFoldersPaths(ctx, ds, album)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(*imagesUpdatedAt).To(Equal(expectedAt))
|
||||||
|
|
||||||
|
// Check that image files are sorted alphabetically
|
||||||
|
Expect(imgFiles).To(HaveLen(4))
|
||||||
|
|
||||||
|
// The files should be sorted by full path
|
||||||
|
Expect(imgFiles[0]).To(Equal(filepath.FromSlash("Artist/Album/Disc1/back.jpg")))
|
||||||
|
Expect(imgFiles[1]).To(Equal(filepath.FromSlash("Artist/Album/Disc1/cover.jpg")))
|
||||||
|
Expect(imgFiles[2]).To(Equal(filepath.FromSlash("Artist/Album/Disc10/cover.jpg")))
|
||||||
|
Expect(imgFiles[3]).To(Equal(filepath.FromSlash("Artist/Album/Disc2/cover.jpg")))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -183,6 +183,8 @@ func (mfs MediaFiles) ToAlbum() Album {
|
|||||||
tags := make(TagList, 0, len(mfs[0].Tags)*len(mfs))
|
tags := make(TagList, 0, len(mfs[0].Tags)*len(mfs))
|
||||||
|
|
||||||
a.Missing = true
|
a.Missing = true
|
||||||
|
embedArtPath := ""
|
||||||
|
embedArtDisc := 0
|
||||||
for _, m := range mfs {
|
for _, m := range mfs {
|
||||||
// We assume these attributes are all the same for all songs in an album
|
// We assume these attributes are all the same for all songs in an album
|
||||||
a.ID = m.AlbumID
|
a.ID = m.AlbumID
|
||||||
@ -211,15 +213,15 @@ func (mfs MediaFiles) ToAlbum() Album {
|
|||||||
comments = append(comments, m.Comment)
|
comments = append(comments, m.Comment)
|
||||||
mbzAlbumIds = append(mbzAlbumIds, m.MbzAlbumID)
|
mbzAlbumIds = append(mbzAlbumIds, m.MbzAlbumID)
|
||||||
mbzReleaseGroupIds = append(mbzReleaseGroupIds, m.MbzReleaseGroupID)
|
mbzReleaseGroupIds = append(mbzReleaseGroupIds, m.MbzReleaseGroupID)
|
||||||
if m.HasCoverArt && a.EmbedArtPath == "" {
|
|
||||||
a.EmbedArtPath = m.Path
|
|
||||||
}
|
|
||||||
if m.DiscNumber > 0 {
|
if m.DiscNumber > 0 {
|
||||||
a.Discs.Add(m.DiscNumber, m.DiscSubtitle)
|
a.Discs.Add(m.DiscNumber, m.DiscSubtitle)
|
||||||
}
|
}
|
||||||
tags = append(tags, m.Tags.FlattenAll()...)
|
tags = append(tags, m.Tags.FlattenAll()...)
|
||||||
a.Participants.Merge(m.Participants)
|
a.Participants.Merge(m.Participants)
|
||||||
|
|
||||||
|
// Find the MediaFile with cover art and the lowest disc number to use for album cover
|
||||||
|
embedArtPath, embedArtDisc = firstArtPath(embedArtPath, embedArtDisc, m)
|
||||||
|
|
||||||
if m.ExplicitStatus == "c" && a.ExplicitStatus != "e" {
|
if m.ExplicitStatus == "c" && a.ExplicitStatus != "e" {
|
||||||
a.ExplicitStatus = "c"
|
a.ExplicitStatus = "c"
|
||||||
} else if m.ExplicitStatus == "e" {
|
} else if m.ExplicitStatus == "e" {
|
||||||
@ -231,6 +233,7 @@ func (mfs MediaFiles) ToAlbum() Album {
|
|||||||
a.Missing = a.Missing && m.Missing
|
a.Missing = a.Missing && m.Missing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.EmbedArtPath = embedArtPath
|
||||||
a.SetTags(tags)
|
a.SetTags(tags)
|
||||||
a.FolderIDs = slice.Unique(slice.Map(mfs, func(m MediaFile) string { return m.FolderID }))
|
a.FolderIDs = slice.Unique(slice.Map(mfs, func(m MediaFile) string { return m.FolderID }))
|
||||||
a.Date, _ = allOrNothing(dates)
|
a.Date, _ = allOrNothing(dates)
|
||||||
@ -305,6 +308,28 @@ func fixAlbumArtist(a *Album) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// firstArtPath determines which media file path should be used for album artwork
|
||||||
|
// based on disc number (preferring lower disc numbers) and path (for consistency)
|
||||||
|
func firstArtPath(currentPath string, currentDisc int, m MediaFile) (string, int) {
|
||||||
|
if !m.HasCoverArt {
|
||||||
|
return currentPath, currentDisc
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current has no disc number (currentDisc == 0) or new file has lower disc number
|
||||||
|
if currentDisc == 0 || (m.DiscNumber < currentDisc && m.DiscNumber > 0) {
|
||||||
|
return m.Path, m.DiscNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
// If disc numbers are equal, use path for ordering
|
||||||
|
if m.DiscNumber == currentDisc {
|
||||||
|
if m.Path < currentPath || currentPath == "" {
|
||||||
|
return m.Path, m.DiscNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentPath, currentDisc
|
||||||
|
}
|
||||||
|
|
||||||
type MediaFileCursor iter.Seq2[MediaFile, error]
|
type MediaFileCursor iter.Seq2[MediaFile, error]
|
||||||
|
|
||||||
type MediaFileRepository interface {
|
type MediaFileRepository interface {
|
||||||
|
@ -305,6 +305,101 @@ var _ = Describe("MediaFiles", func() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Context("Album Art", func() {
|
||||||
|
When("we have media files with cover art from multiple discs", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
mfs = MediaFiles{
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/Disc2/01.mp3",
|
||||||
|
HasCoverArt: true,
|
||||||
|
DiscNumber: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/Disc1/01.mp3",
|
||||||
|
HasCoverArt: true,
|
||||||
|
DiscNumber: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/Disc3/01.mp3",
|
||||||
|
HasCoverArt: true,
|
||||||
|
DiscNumber: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
It("selects the cover art from the lowest disc number", func() {
|
||||||
|
album := mfs.ToAlbum()
|
||||||
|
Expect(album.EmbedArtPath).To(Equal("Artist/Album/Disc1/01.mp3"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("we have media files with cover art from the same disc number", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
mfs = MediaFiles{
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/Disc1/02.mp3",
|
||||||
|
HasCoverArt: true,
|
||||||
|
DiscNumber: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/Disc1/01.mp3",
|
||||||
|
HasCoverArt: true,
|
||||||
|
DiscNumber: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
It("selects the cover art with the lowest path alphabetically", func() {
|
||||||
|
album := mfs.ToAlbum()
|
||||||
|
Expect(album.EmbedArtPath).To(Equal("Artist/Album/Disc1/01.mp3"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("we have media files with some missing cover art", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
mfs = MediaFiles{
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/Disc1/01.mp3",
|
||||||
|
HasCoverArt: false,
|
||||||
|
DiscNumber: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/Disc2/01.mp3",
|
||||||
|
HasCoverArt: true,
|
||||||
|
DiscNumber: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
It("selects the file with cover art even if from a higher disc number", func() {
|
||||||
|
album := mfs.ToAlbum()
|
||||||
|
Expect(album.EmbedArtPath).To(Equal("Artist/Album/Disc2/01.mp3"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("we have media files with path names that don't correlate with disc numbers", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
mfs = MediaFiles{
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/file-z.mp3", // Path would be sorted last alphabetically
|
||||||
|
HasCoverArt: true,
|
||||||
|
DiscNumber: 1, // But it has lowest disc number
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/file-a.mp3", // Path would be sorted first alphabetically
|
||||||
|
HasCoverArt: true,
|
||||||
|
DiscNumber: 2, // But it has higher disc number
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "Artist/Album/file-m.mp3",
|
||||||
|
HasCoverArt: true,
|
||||||
|
DiscNumber: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
It("selects the cover art from the lowest disc number regardless of path", func() {
|
||||||
|
album := mfs.ToAlbum()
|
||||||
|
Expect(album.EmbedArtPath).To(Equal("Artist/Album/file-z.mp3"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user