Load cover art from file directory

This commit adds support for loading cover art from media file
directories, according to configured filename priorities (of which an
additional, special choice of `embedded` is given).

Cover art paths are resolved during scanning and stored in the database
as part of the `album.cover_art_path` column; if embedded cover art is
matched, this will default to the path of the media file itself, and if
no cover art is matched at all.

Similarly, the `album.cover_art_id` column will default to a reference
to `media_file.id` if embedded cover art is wanted, but if an external
cover art file is matched, this will instead be set to a reference to
the `album.id` value itself, prefixed with the `al-` constant.

Stored cover art paths are once again resolved and matched against
configuration when covers are requested; that is, any change in
configuration between scanning and requesting cover art may not return
correct data until a re-scan is complete.

Tests will be added in future commits.
This commit is contained in:
Alex Palaistras 2020-06-13 18:55:58 +01:00 committed by Deluan Quintão
parent 6563897692
commit 08cd28af2d
3 changed files with 88 additions and 7 deletions

View File

@ -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:""`

View File

@ -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)
}

View File

@ -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)