Move artwork handling to its own package

This commit is contained in:
Deluan 2022-12-25 16:07:28 -05:00 committed by Deluan Quintão
parent 8cf78efb9c
commit c1c4645501
10 changed files with 163 additions and 143 deletions

View File

@ -12,6 +12,7 @@ import (
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/core/agents/lastfm"
"github.com/navidrome/navidrome/core/agents/listenbrainz"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/db"
@ -45,9 +46,9 @@ func CreateNativeAPIRouter() *nativeapi.Router {
func CreateSubsonicAPIRouter() *subsonic.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
fileCache := core.GetImageCache()
fileCache := artwork.GetImageCache()
fFmpeg := ffmpeg.New()
artwork := core.NewArtwork(dataStore, fileCache, fFmpeg)
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg)
transcodingCache := core.GetTranscodingCache()
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
archiver := core.NewArchiver(mediaStreamer, dataStore)
@ -58,7 +59,7 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
broker := events.GetBroker()
playlists := core.NewPlaylists(dataStore)
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
router := subsonic.New(dataStore, artwork, mediaStreamer, archiver, players, externalMetadata, scanner, broker, playlists, playTracker)
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, externalMetadata, scanner, broker, playlists, playTracker)
return router
}
@ -80,10 +81,10 @@ func createScanner() scanner.Scanner {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
playlists := core.NewPlaylists(dataStore)
fileCache := core.GetImageCache()
fileCache := artwork.GetImageCache()
fFmpeg := ffmpeg.New()
artwork := core.NewArtwork(dataStore, fileCache, fFmpeg)
cacheWarmer := core.NewArtworkCacheWarmer(artwork)
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg)
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork)
broker := events.GetBroker()
scannerScanner := scanner.New(dataStore, playlists, cacheWarmer, broker)
return scannerScanner

View File

@ -1,4 +1,4 @@
package core
package artwork
import (
"bufio"
@ -12,21 +12,15 @@ import (
"image/png"
"io"
"net/http"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"time"
"github.com/dhowden/tag"
"github.com/disintegration/imaging"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/resources"
"github.com/navidrome/navidrome/utils/cache"
"github.com/navidrome/navidrome/utils/singleton"
_ "golang.org/x/image/webp"
@ -59,24 +53,15 @@ func (a *artwork) Get(ctx context.Context, id string, size int) (io.ReadCloser,
}
}
key := &artworkKey{a: a, artID: artID, size: size}
item := &artItem{a: a, artID: artID, size: size}
r, err := a.cache.Get(ctx, key)
r, err := a.cache.Get(ctx, item)
if err != nil && !errors.Is(err, context.Canceled) {
log.Error(ctx, "Error accessing image cache", "id", id, "size", size, err)
}
return r, err
}
type fromFunc func() (io.ReadCloser, string, error)
func (f fromFunc) String() string {
name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
name = strings.TrimPrefix(name, "github.com/navidrome/navidrome/core.")
name = strings.TrimSuffix(name, ".func1")
return name
}
func (a *artwork) get(ctx context.Context, artID model.ArtworkID, size int) (reader io.ReadCloser, path string, err error) {
// If requested a resized image, get the original (possibly from cache)
if size > 0 {
@ -110,7 +95,7 @@ func (a *artwork) extractAlbumImage(ctx context.Context, artID model.ArtworkID)
log.Error(ctx, "Could not retrieve album", "id", artID.ID, err)
return nil, ""
}
var ff = a.fromCoverArtPriority(ctx, conf.Server.CoverArtPriority, *al)
var ff = fromCoverArtPriority(ctx, a.ffmpeg, conf.Server.CoverArtPriority, *al)
ff = append(ff, fromPlaceholder())
return extractImage(ctx, artID, ff...)
}
@ -126,9 +111,9 @@ func (a *artwork) extractMediaFileImage(ctx context.Context, artID model.Artwork
return nil, ""
}
var ff []fromFunc
var ff []sourceFunc
if mf.CoverArtID().Kind == model.KindMediaFileArtwork {
ff = []fromFunc{
ff = []sourceFunc{
fromTag(mf.Path),
fromFFmpegTag(ctx, a.ffmpeg, mf.Path),
}
@ -152,38 +137,28 @@ func (a *artwork) resizedFromOriginal(ctx context.Context, artID model.ArtworkID
return resized, nil
}
func extractImage(ctx context.Context, artID model.ArtworkID, extractFuncs ...fromFunc) (io.ReadCloser, string) {
func extractImage(ctx context.Context, artID model.ArtworkID, extractFuncs ...sourceFunc) (io.ReadCloser, string) {
for _, f := range extractFuncs {
if ctx.Err() != nil {
return nil, ""
}
r, path, err := f()
if r != nil {
log.Trace(ctx, "Found artwork", "artID", artID, "path", path, "origin", f)
log.Trace(ctx, "Found artwork", "artID", artID, "path", path, "source", f)
return r, path
}
log.Trace(ctx, "Tried to extract artwork", "artID", artID, "origin", f, err)
log.Trace(ctx, "Tried to extract artwork", "artID", artID, "source", f, err)
}
log.Error(ctx, "extractImage should never reach this point!", "artID", artID, "path")
return nil, ""
}
func (a *artwork) fromAlbum(ctx context.Context, id model.ArtworkID) fromFunc {
return func() (io.ReadCloser, string, error) {
r, path, err := a.get(ctx, id, 0)
if err != nil {
return nil, "", err
}
return r, path, nil
}
}
func (a *artwork) fromCoverArtPriority(ctx context.Context, priority string, al model.Album) []fromFunc {
var ff []fromFunc
func fromCoverArtPriority(ctx context.Context, ffmpeg ffmpeg.FFmpeg, priority string, al model.Album) []sourceFunc {
var ff []sourceFunc
for _, pattern := range strings.Split(strings.ToLower(priority), ",") {
pattern = strings.TrimSpace(pattern)
if pattern == "embedded" {
ff = append(ff, fromTag(al.EmbedArtPath), fromFFmpegTag(ctx, a.ffmpeg, al.EmbedArtPath))
ff = append(ff, fromTag(al.EmbedArtPath), fromFFmpegTag(ctx, ffmpeg, al.EmbedArtPath))
continue
}
if al.ImageFiles != "" {
@ -193,79 +168,6 @@ func (a *artwork) fromCoverArtPriority(ctx context.Context, priority string, al
return ff
}
func fromExternalFile(ctx context.Context, files string, pattern string) fromFunc {
return func() (io.ReadCloser, string, error) {
for _, file := range filepath.SplitList(files) {
_, name := filepath.Split(file)
match, err := filepath.Match(pattern, strings.ToLower(name))
if err != nil {
log.Warn(ctx, "Error matching cover art file to pattern", "pattern", pattern, "file", file)
continue
}
if !match {
continue
}
f, err := os.Open(file)
if err != nil {
log.Warn(ctx, "Could not open cover art file", "file", file, err)
continue
}
return f, file, err
}
return nil, "", fmt.Errorf("pattern '%s' not matched by files %v", pattern, files)
}
}
func fromTag(path string) fromFunc {
return func() (io.ReadCloser, string, error) {
if path == "" {
return nil, "", nil
}
f, err := os.Open(path)
if err != nil {
return nil, "", err
}
defer f.Close()
m, err := tag.ReadFrom(f)
if err != nil {
return nil, "", err
}
picture := m.Picture()
if picture == nil {
return nil, "", fmt.Errorf("no embedded image found in %s", path)
}
return io.NopCloser(bytes.NewReader(picture.Data)), path, nil
}
}
func fromFFmpegTag(ctx context.Context, ffmpeg ffmpeg.FFmpeg, path string) fromFunc {
return func() (io.ReadCloser, string, error) {
if path == "" {
return nil, "", nil
}
r, err := ffmpeg.ExtractImage(ctx, path)
if err != nil {
return nil, "", err
}
defer r.Close()
buf := new(bytes.Buffer)
_, err = io.Copy(buf, r)
if err != nil {
return nil, "", err
}
return io.NopCloser(buf), path, nil
}
}
func fromPlaceholder() fromFunc {
return func() (io.ReadCloser, string, error) {
r, _ := resources.FS().Open(consts.PlaceholderAlbumArt)
return r, consts.PlaceholderAlbumArt, nil
}
}
func asImageReader(r io.Reader) (io.Reader, string, error) {
br := bufio.NewReader(r)
buf, err := br.Peek(512)
@ -305,26 +207,26 @@ func resizeImage(reader io.Reader, size int) (io.Reader, error) {
return buf, err
}
type ArtworkCache struct {
type imageCache struct {
cache.FileCache
}
type artworkKey struct {
type artItem struct {
a *artwork
artID model.ArtworkID
size int
}
func (k *artworkKey) Key() string {
func (k *artItem) Key() string {
return fmt.Sprintf("%s.%d.%d", k.artID, k.size, conf.Server.CoverJpegQuality)
}
func GetImageCache() cache.FileCache {
return singleton.GetInstance(func() *ArtworkCache {
return &ArtworkCache{
return singleton.GetInstance(func() *imageCache {
return &imageCache{
FileCache: cache.NewFileCache("Image", conf.Server.ImageCacheSize, consts.ImageCacheDir, consts.DefaultImageCacheMaxItems,
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
info := arg.(*artworkKey)
info := arg.(*artItem)
r, _, err := info.a.get(ctx, info.artID, info.size)
return r, err
}),

View File

@ -1,4 +1,4 @@
package core
package artwork
import (
"context"
@ -11,17 +11,17 @@ import (
"github.com/navidrome/navidrome/utils/pl"
)
type ArtworkCacheWarmer interface {
type CacheWarmer interface {
PreCache(artID model.ArtworkID)
}
func NewArtworkCacheWarmer(artwork Artwork) ArtworkCacheWarmer {
func NewCacheWarmer(artwork Artwork) CacheWarmer {
// If image cache is disabled, return a NOOP implementation
if conf.Server.ImageCacheSize == "0" {
return &noopCacheWarmer{}
}
a := &artworkCacheWarmer{
a := &cacheWarmer{
artwork: artwork,
input: make(chan string),
}
@ -29,23 +29,23 @@ func NewArtworkCacheWarmer(artwork Artwork) ArtworkCacheWarmer {
return a
}
type artworkCacheWarmer struct {
type cacheWarmer struct {
artwork Artwork
input chan string
}
func (a *artworkCacheWarmer) PreCache(artID model.ArtworkID) {
func (a *cacheWarmer) PreCache(artID model.ArtworkID) {
a.input <- artID.String()
}
func (a *artworkCacheWarmer) run(ctx context.Context) {
func (a *cacheWarmer) run(ctx context.Context) {
errs := pl.Sink(ctx, 2, a.input, a.doCacheImage)
for err := range errs {
log.Warn(ctx, "Error warming cache", err)
}
}
func (a *artworkCacheWarmer) doCacheImage(ctx context.Context, id string) error {
func (a *cacheWarmer) doCacheImage(ctx context.Context, id string) error {
r, err := a.artwork.Get(ctx, id, 0)
if err != nil {
return fmt.Errorf("error cacheing id='%s': %w", id, err)

View File

@ -1,4 +1,4 @@
package core
package artwork
import (
"context"

113
core/artwork/sources.go Normal file
View File

@ -0,0 +1,113 @@
package artwork
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"github.com/dhowden/tag"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/resources"
)
type sourceFunc func() (io.ReadCloser, string, error)
func (f sourceFunc) String() string {
name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
name = strings.TrimPrefix(name, "github.com/navidrome/navidrome/core.")
name = strings.TrimPrefix(name, "(*artwork).")
name = strings.TrimSuffix(name, ".func1")
return name
}
func (a *artwork) fromAlbum(ctx context.Context, id model.ArtworkID) sourceFunc {
return func() (io.ReadCloser, string, error) {
r, path, err := a.get(ctx, id, 0)
if err != nil {
return nil, "", err
}
return r, path, nil
}
}
func fromExternalFile(ctx context.Context, files string, pattern string) sourceFunc {
return func() (io.ReadCloser, string, error) {
for _, file := range filepath.SplitList(files) {
_, name := filepath.Split(file)
match, err := filepath.Match(pattern, strings.ToLower(name))
if err != nil {
log.Warn(ctx, "Error matching cover art file to pattern", "pattern", pattern, "file", file)
continue
}
if !match {
continue
}
f, err := os.Open(file)
if err != nil {
log.Warn(ctx, "Could not open cover art file", "file", file, err)
continue
}
return f, file, err
}
return nil, "", fmt.Errorf("pattern '%s' not matched by files %v", pattern, files)
}
}
func fromTag(path string) sourceFunc {
return func() (io.ReadCloser, string, error) {
if path == "" {
return nil, "", nil
}
f, err := os.Open(path)
if err != nil {
return nil, "", err
}
defer f.Close()
m, err := tag.ReadFrom(f)
if err != nil {
return nil, "", err
}
picture := m.Picture()
if picture == nil {
return nil, "", fmt.Errorf("no embedded image found in %s", path)
}
return io.NopCloser(bytes.NewReader(picture.Data)), path, nil
}
}
func fromFFmpegTag(ctx context.Context, ffmpeg ffmpeg.FFmpeg, path string) sourceFunc {
return func() (io.ReadCloser, string, error) {
if path == "" {
return nil, "", nil
}
r, err := ffmpeg.ExtractImage(ctx, path)
if err != nil {
return nil, "", err
}
defer r.Close()
buf := new(bytes.Buffer)
_, err = io.Copy(buf, r)
if err != nil {
return nil, "", err
}
return io.NopCloser(buf), path, nil
}
}
func fromPlaceholder() sourceFunc {
return func() (io.ReadCloser, string, error) {
r, _ := resources.FS().Open(consts.PlaceholderAlbumArt)
return r, consts.PlaceholderAlbumArt, nil
}
}

View File

@ -3,15 +3,16 @@ package core
import (
"github.com/google/wire"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/core/scrobbler"
)
var Set = wire.NewSet(
NewArtwork,
artwork.NewArtwork,
NewMediaStreamer,
GetTranscodingCache,
GetImageCache,
artwork.GetImageCache,
NewArchiver,
NewExternalMetadata,
NewPlayers,
@ -20,5 +21,5 @@ var Set = wire.NewSet(
scrobbler.GetPlayTracker,
NewShare,
NewPlaylists,
NewArtworkCacheWarmer,
artwork.NewCacheWarmer,
)

View File

@ -8,7 +8,7 @@ import (
"time"
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
@ -25,10 +25,10 @@ type refresher struct {
album map[string]struct{}
artist map[string]struct{}
dirMap dirMap
cacheWarmer core.ArtworkCacheWarmer
cacheWarmer artwork.CacheWarmer
}
func newRefresher(ds model.DataStore, cw core.ArtworkCacheWarmer, dirMap dirMap) *refresher {
func newRefresher(ds model.DataStore, cw artwork.CacheWarmer, dirMap dirMap) *refresher {
return &refresher{
ds: ds,
album: map[string]struct{}{},

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server/events"
@ -46,7 +47,7 @@ type scanner struct {
ds model.DataStore
pls core.Playlists
broker events.Broker
cacheWarmer core.ArtworkCacheWarmer
cacheWarmer artwork.CacheWarmer
}
type scanStatus struct {
@ -56,7 +57,7 @@ type scanStatus struct {
lastUpdate time.Time
}
func New(ds model.DataStore, playlists core.Playlists, cacheWarmer core.ArtworkCacheWarmer, broker events.Broker) Scanner {
func New(ds model.DataStore, playlists core.Playlists, cacheWarmer artwork.CacheWarmer, broker events.Broker) Scanner {
s := &scanner{
ds: ds,
pls: playlists,

View File

@ -11,6 +11,7 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
@ -27,10 +28,10 @@ type TagScanner struct {
plsSync *playlistImporter
cnt *counters
mapper *mediaFileMapper
cacheWarmer core.ArtworkCacheWarmer
cacheWarmer artwork.CacheWarmer
}
func NewTagScanner(rootFolder string, ds model.DataStore, playlists core.Playlists, cacheWarmer core.ArtworkCacheWarmer) FolderScanner {
func NewTagScanner(rootFolder string, ds model.DataStore, playlists core.Playlists, cacheWarmer artwork.CacheWarmer) FolderScanner {
s := &TagScanner{
rootFolder: rootFolder,
plsSync: newPlaylistImporter(ds, playlists, rootFolder),

View File

@ -12,6 +12,7 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
@ -30,7 +31,7 @@ type handlerRaw = func(http.ResponseWriter, *http.Request) (*responses.Subsonic,
type Router struct {
http.Handler
ds model.DataStore
artwork core.Artwork
artwork artwork.Artwork
streamer core.MediaStreamer
archiver core.Archiver
players core.Players
@ -41,7 +42,7 @@ type Router struct {
scrobbler scrobbler.PlayTracker
}
func New(ds model.DataStore, artwork core.Artwork, streamer core.MediaStreamer, archiver core.Archiver,
func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, archiver core.Archiver,
players core.Players, externalMetadata core.ExternalMetadata, scanner scanner.Scanner, broker events.Broker,
playlists core.Playlists, scrobbler scrobbler.PlayTracker) *Router {
r := &Router{