diff --git a/conf/configuration.go b/conf/configuration.go index 7a52b7c8..703467a9 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -33,6 +33,8 @@ type nd struct { ImageCacheSize string `default:"100MB"` // in MB ProbeCommand string `default:"ffmpeg %s -f ffmetadata"` + CoverArtPriority string `default:"embedded, cover.*, folder.*, front.*"` + // DevFlags. These are used to enable/disable debugging and incomplete features DevLogSourceLine bool `default:"false"` DevAutoCreateAdminPassword string `default:""` diff --git a/engine/cover.go b/engine/cover.go index 00defa32..4be93e10 100644 --- a/engine/cover.go +++ b/engine/cover.go @@ -11,6 +11,7 @@ import ( _ "image/png" "io" "os" + "path/filepath" "strings" "time" @@ -91,6 +92,7 @@ func (c *cover) getCoverPath(ctx context.Context, id string) (path string, lastU if found, err = c.ds.Album(ctx).Exists(id); err != nil { return } + var coverPath string if found { var al *model.Album al, err = c.ds.Album(ctx).Get(id) @@ -102,15 +104,22 @@ func (c *cover) getCoverPath(ctx context.Context, id string) (path string, lastU return } id = al.CoverArtId + coverPath = al.CoverArtPath } var mf *model.MediaFile mf, err = c.ds.MediaFile(ctx).Get(id) - if err != nil { + if err == nil && mf.HasCoverArt { + return mf.Path, mf.UpdatedAt, nil + } else if err != nil && coverPath != "" { + info, err := os.Stat(coverPath) + if err != nil { + return "", time.Time{}, model.ErrNotFound + } + return coverPath, info.ModTime(), nil + } else if err != nil { return } - if mf.HasCoverArt { - return mf.Path, mf.UpdatedAt, nil - } + return "", time.Time{}, model.ErrNotFound } @@ -126,7 +135,17 @@ func (c *cover) getCover(ctx context.Context, path string, size int) (reader io. } }() var data []byte - data, err = readFromTag(path) + for _, p := range strings.Split(conf.Server.CoverArtPriority, ",") { + pat := strings.ToLower(strings.TrimSpace(p)) + if pat == "embedded" { + data, err = readFromTag(path) + } else if ok, _ := filepath.Match(pat, strings.ToLower(filepath.Base(path))); ok { + data, err = readFromFile(path) + } + if err == nil { + break + } + } if err == nil && size > 0 { data, err = resizeImage(bytes.NewReader(data), size) @@ -171,6 +190,21 @@ func readFromTag(path string) ([]byte, error) { return picture.Data, nil } +func readFromFile(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + + defer f.Close() + var buf bytes.Buffer + if _, err := buf.ReadFrom(f); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + func NewImageCache() (ImageCache, error) { return newFileCache("Image", conf.Server.ImageCacheSize, consts.ImageCacheDir, consts.DefaultImageCacheMaxItems) } diff --git a/persistence/album_repository.go b/persistence/album_repository.go index 205f2f53..2776e5e7 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -2,6 +2,8 @@ package persistence import ( "context" + "os" + "path/filepath" "sort" "strconv" "strings" @@ -9,6 +11,7 @@ import ( . "github.com/Masterminds/squirrel" "github.com/astaxie/beego/orm" + "github.com/deluan/navidrome/conf" "github.com/deluan/navidrome/consts" "github.com/deluan/navidrome/log" "github.com/deluan/navidrome/model" @@ -137,9 +140,15 @@ func (r *albumRepository) Refresh(ids ...string) error { toInsert := 0 toUpdate := 0 for _, al := range albums { - if !al.HasCoverArt { - al.CoverArtId = "" + if !al.HasCoverArt || !strings.HasPrefix(conf.Server.CoverArtPriority, "embedded") { + if path := getCoverFromPath(al.CoverArtPath, al.HasCoverArt); path != "" { + al.CoverArtId = "al-" + al.ID + al.CoverArtPath = path + } else if !al.HasCoverArt { + al.CoverArtId = "" + } } + if al.Compilation { al.AlbumArtist = consts.VariousArtists al.AlbumArtistID = consts.VariousArtistsID @@ -184,6 +193,42 @@ func getMinYear(years string) int { return 0 } +// 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. +func getCoverFromPath(path string, hasEmbeddedCover bool) string { + n, err := os.Open(filepath.Dir(path)) + 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 hasEmbeddedCover { + return "" + } + continue + } + + for _, name := range names { + if ok, _ := filepath.Match(pat, strings.ToLower(name)); ok { + return filepath.Join(filepath.Dir(path), name) + } + } + } + + return "" +} + func (r *albumRepository) purgeEmpty() error { del := Delete(r.tableName).Where("id not in (select distinct(album_id) from media_file)") c, err := r.executeSQL(del)