Move cover art discovery (temporarily) to model
This commit is contained in:
+54
-1
@@ -2,9 +2,12 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"mime"
|
"mime"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/consts"
|
"github.com/navidrome/navidrome/consts"
|
||||||
"github.com/navidrome/navidrome/utils"
|
"github.com/navidrome/navidrome/utils"
|
||||||
"github.com/navidrome/navidrome/utils/number"
|
"github.com/navidrome/navidrome/utils/number"
|
||||||
@@ -75,6 +78,7 @@ func (mfs MediaFiles) ToAlbum() Album {
|
|||||||
var songArtistIds []string
|
var songArtistIds []string
|
||||||
var mbzAlbumIds []string
|
var mbzAlbumIds []string
|
||||||
var comments []string
|
var comments []string
|
||||||
|
var firstPath string
|
||||||
for _, m := range mfs {
|
for _, m := range mfs {
|
||||||
// We assume these attributes are all the same for all songs on an album
|
// We assume these attributes are all the same for all songs on an album
|
||||||
a.ID = m.AlbumID
|
a.ID = m.AlbumID
|
||||||
@@ -115,8 +119,11 @@ func (mfs MediaFiles) ToAlbum() Album {
|
|||||||
m.SortAlbumName, m.SortAlbumArtistName, m.SortArtistName,
|
m.SortAlbumName, m.SortAlbumArtistName, m.SortArtistName,
|
||||||
m.DiscSubtitle)
|
m.DiscSubtitle)
|
||||||
if m.HasCoverArt {
|
if m.HasCoverArt {
|
||||||
// TODO CoverArtPriority
|
|
||||||
a.CoverArtId = m.ID
|
a.CoverArtId = m.ID
|
||||||
|
a.CoverArtPath = m.Path
|
||||||
|
}
|
||||||
|
if firstPath == "" {
|
||||||
|
firstPath = m.Path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
comments = slices.Compact(comments)
|
comments = slices.Compact(comments)
|
||||||
@@ -132,6 +139,14 @@ func (mfs MediaFiles) ToAlbum() Album {
|
|||||||
slices.Sort(songArtistIds)
|
slices.Sort(songArtistIds)
|
||||||
a.AllArtistIDs = strings.Join(slices.Compact(songArtistIds), " ")
|
a.AllArtistIDs = strings.Join(slices.Compact(songArtistIds), " ")
|
||||||
a.MbzAlbumID = slice.MostFrequent(mbzAlbumIds)
|
a.MbzAlbumID = slice.MostFrequent(mbzAlbumIds)
|
||||||
|
|
||||||
|
if a.CoverArtPath == "" || !strings.HasPrefix(conf.Server.CoverArtPriority, "embedded") {
|
||||||
|
if path := getCoverFromPath(firstPath, a.CoverArtPath); path != "" {
|
||||||
|
a.CoverArtId = "al-" + a.ID
|
||||||
|
a.CoverArtPath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +184,44 @@ func fixAlbumArtist(a Album, albumArtistIds []string) Album {
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCoverFromPath accepts a path to a file, and returns a path to an eligible cover image from the
|
||||||
|
// file's directory (as configured with CoverArtPriority). If no cover file is found, among
|
||||||
|
// available choices, or an error occurs, an empty string is returned. If HasEmbeddedCover is true,
|
||||||
|
// and 'embedded' is matched among eligible choices, GetCoverFromPath will return early with an
|
||||||
|
// empty path.
|
||||||
|
// TODO: Move to scanner (or at least out of here)
|
||||||
|
func getCoverFromPath(mediaPath string, embeddedPath string) string {
|
||||||
|
n, err := os.Open(filepath.Dir(mediaPath))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
defer n.Close()
|
||||||
|
names, err := n.Readdirnames(-1)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range strings.Split(conf.Server.CoverArtPriority, ",") {
|
||||||
|
pat := strings.ToLower(strings.TrimSpace(p))
|
||||||
|
if pat == "embedded" {
|
||||||
|
if embeddedPath != "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
match, _ := filepath.Match(pat, strings.ToLower(name))
|
||||||
|
if match && utils.IsImageFile(name) {
|
||||||
|
return filepath.Join(filepath.Dir(mediaPath), name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type MediaFileRepository interface {
|
type MediaFileRepository interface {
|
||||||
CountAll(options ...QueryOptions) (int64, error)
|
CountAll(options ...QueryOptions) (int64, error)
|
||||||
Exists(id string) (bool, error)
|
Exists(id string) (bool, error)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/consts"
|
"github.com/navidrome/navidrome/consts"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
@@ -51,3 +55,57 @@ var _ = Describe("fixAlbumArtist", func() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var _ = Describe("getCoverFromPath", func() {
|
||||||
|
var testFolder, testPath, embeddedPath string
|
||||||
|
BeforeEach(func() {
|
||||||
|
testFolder, _ = os.MkdirTemp("", "album_persistence_tests")
|
||||||
|
if err := os.MkdirAll(testFolder, 0777); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if _, err := os.Create(filepath.Join(testFolder, "Cover.jpeg")); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if _, err := os.Create(filepath.Join(testFolder, "FRONT.PNG")); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
testPath = filepath.Join(testFolder, "somefile.test")
|
||||||
|
embeddedPath = filepath.Join(testFolder, "somefile.mp3")
|
||||||
|
})
|
||||||
|
AfterEach(func() {
|
||||||
|
_ = os.RemoveAll(testFolder)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns audio file for embedded cover", func() {
|
||||||
|
conf.Server.CoverArtPriority = "embedded, cover.*, front.*"
|
||||||
|
Expect(getCoverFromPath(testPath, embeddedPath)).To(Equal(""))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns external file when no embedded cover exists", func() {
|
||||||
|
conf.Server.CoverArtPriority = "embedded, cover.*, front.*"
|
||||||
|
Expect(getCoverFromPath(testPath, "")).To(Equal(filepath.Join(testFolder, "Cover.jpeg")))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns embedded cover even if not first choice", func() {
|
||||||
|
conf.Server.CoverArtPriority = "something.png, embedded, cover.*, front.*"
|
||||||
|
Expect(getCoverFromPath(testPath, embeddedPath)).To(Equal(""))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns first correct match case-insensitively", func() {
|
||||||
|
conf.Server.CoverArtPriority = "embedded, cover.jpg, front.svg, front.png"
|
||||||
|
Expect(getCoverFromPath(testPath, "")).To(Equal(filepath.Join(testFolder, "FRONT.PNG")))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns match for embedded pattern", func() {
|
||||||
|
conf.Server.CoverArtPriority = "embedded, cover.jp?g, front.png"
|
||||||
|
Expect(getCoverFromPath(testPath, "")).To(Equal(filepath.Join(testFolder, "Cover.jpeg")))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns empty string if no match was found", func() {
|
||||||
|
conf.Server.CoverArtPriority = "embedded, cover.jpg, front.apng"
|
||||||
|
Expect(getCoverFromPath(testPath, "")).To(Equal(""))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset configuration to default.
|
||||||
|
conf.Server.CoverArtPriority = "embedded, cover.*, front.*"
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user