Add image cache back

This commit is contained in:
Deluan 2022-12-20 11:27:40 -05:00 committed by Deluan Quintão
parent 40bb211b39
commit 0da27e8a3f
5 changed files with 96 additions and 41 deletions

View File

@ -45,7 +45,8 @@ func CreateNativeAPIRouter() *nativeapi.Router {
func CreateSubsonicAPIRouter() *subsonic.Router {
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
artwork := core.NewArtwork(dataStore)
fileCache := core.GetImageCache()
artwork := core.NewArtwork(dataStore, fileCache)
transcoderTranscoder := transcoder.New()
transcodingCache := core.GetTranscodingCache()
mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache)

View File

@ -13,6 +13,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/dhowden/tag"
"github.com/disintegration/imaging"
@ -21,6 +22,8 @@ import (
"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"
)
@ -28,53 +31,60 @@ type Artwork interface {
Get(ctx context.Context, id string, size int) (io.ReadCloser, error)
}
func NewArtwork(ds model.DataStore) Artwork {
return &artwork{ds: ds}
func NewArtwork(ds model.DataStore, cache cache.FileCache) Artwork {
return &artwork{ds: ds, cache: cache}
}
type artwork struct {
ds model.DataStore
ds model.DataStore
cache cache.FileCache
}
func (a *artwork) Get(ctx context.Context, id string, size int) (io.ReadCloser, error) {
r, _, err := a.get(ctx, id, size)
artID, err := model.ParseArtworkID(id)
if err != nil {
return nil, errors.New("invalid ID")
}
key := &artworkKey{a: a, artID: artID, size: size}
r, err := a.cache.Get(ctx, key)
if err != nil {
log.Error(ctx, "Error accessing image cache", "id", id, "size", size, err)
return nil, err
}
return r, err
}
func (a *artwork) get(ctx context.Context, id string, size int) (reader io.ReadCloser, path string, err error) {
artId, err := model.ParseArtworkID(id)
if err != nil {
return nil, "", errors.New("invalid ID")
}
func (a *artwork) get(ctx context.Context, artID model.ArtworkID, size int) (reader io.ReadCloser, path string, err error) {
// If requested a resized image
if size > 0 {
return a.resizedFromOriginal(ctx, id, size)
return a.resizedFromOriginal(ctx, artID, size)
}
switch artId.Kind {
switch artID.Kind {
case model.KindAlbumArtwork:
reader, path = a.extractAlbumImage(ctx, artId)
reader, path = a.extractAlbumImage(ctx, artID)
case model.KindMediaFileArtwork:
reader, path = a.extractMediaFileImage(ctx, artId)
reader, path = a.extractMediaFileImage(ctx, artID)
default:
reader, path = fromPlaceholder()()
}
return reader, path, nil
}
func (a *artwork) extractAlbumImage(ctx context.Context, artId model.ArtworkID) (io.ReadCloser, string) {
al, err := a.ds.Album(ctx).Get(artId.ID)
func (a *artwork) extractAlbumImage(ctx context.Context, artID model.ArtworkID) (io.ReadCloser, string) {
al, err := a.ds.Album(ctx).Get(artID.ID)
if errors.Is(err, model.ErrNotFound) {
r, path := fromPlaceholder()()
return r, path
}
if err != nil {
log.Error(ctx, "Could not retrieve album", "id", artId.ID, err)
log.Error(ctx, "Could not retrieve album", "id", artID.ID, err)
return nil, ""
}
return extractImage(ctx, artId,
return extractImage(ctx, artID,
fromExternalFile(al.ImageFiles, "cover.png", "cover.jpg", "cover.jpeg", "cover.webp"),
fromExternalFile(al.ImageFiles, "folder.png", "folder.jpg", "folder.jpeg", "folder.webp"),
fromExternalFile(al.ImageFiles, "album.png", "album.jpg", "album.jpeg", "album.webp"),
@ -85,18 +95,18 @@ func (a *artwork) extractAlbumImage(ctx context.Context, artId model.ArtworkID)
)
}
func (a *artwork) extractMediaFileImage(ctx context.Context, artId model.ArtworkID) (reader io.ReadCloser, path string) {
mf, err := a.ds.MediaFile(ctx).Get(artId.ID)
func (a *artwork) extractMediaFileImage(ctx context.Context, artID model.ArtworkID) (reader io.ReadCloser, path string) {
mf, err := a.ds.MediaFile(ctx).Get(artID.ID)
if errors.Is(err, model.ErrNotFound) {
r, path := fromPlaceholder()()
return r, path
}
if err != nil {
log.Error(ctx, "Could not retrieve mediafile", "id", artId.ID, err)
log.Error(ctx, "Could not retrieve mediafile", "id", artID.ID, err)
return nil, ""
}
return extractImage(ctx, artId,
return extractImage(ctx, artID,
fromTag(mf.Path),
a.fromAlbum(ctx, mf.AlbumCoverArtID()),
)
@ -104,7 +114,7 @@ func (a *artwork) extractMediaFileImage(ctx context.Context, artId model.Artwork
func (a *artwork) fromAlbum(ctx context.Context, id model.ArtworkID) func() (io.ReadCloser, string) {
return func() (io.ReadCloser, string) {
r, path, err := a.get(ctx, id.String(), 0)
r, path, err := a.get(ctx, id, 0)
if err != nil {
return nil, ""
}
@ -112,8 +122,8 @@ func (a *artwork) fromAlbum(ctx context.Context, id model.ArtworkID) func() (io.
}
}
func (a *artwork) resizedFromOriginal(ctx context.Context, id string, size int) (io.ReadCloser, string, error) {
r, path, err := a.get(ctx, id, 0)
func (a *artwork) resizedFromOriginal(ctx context.Context, artID model.ArtworkID, size int) (io.ReadCloser, string, error) {
r, path, err := a.get(ctx, artID, 0)
if err != nil || r == nil {
return nil, "", err
}
@ -127,15 +137,15 @@ func (a *artwork) resizedFromOriginal(ctx context.Context, id string, size int)
return r, fmt.Sprintf("%s@%d", path, size), nil
}
func extractImage(ctx context.Context, artId model.ArtworkID, extractFuncs ...func() (io.ReadCloser, string)) (io.ReadCloser, string) {
func extractImage(ctx context.Context, artID model.ArtworkID, extractFuncs ...func() (io.ReadCloser, string)) (io.ReadCloser, string) {
for _, f := range extractFuncs {
r, path := f()
if r != nil {
log.Trace(ctx, "Found artwork", "artId", artId, "path", path)
log.Trace(ctx, "Found artwork", "artID", artID, "path", path)
return r, path
}
}
log.Error(ctx, "extractImage should never reach this point!", "artId", artId, "path")
log.Error(ctx, "extractImage should never reach this point!", "artID", artID, "path")
return nil, ""
}
@ -215,3 +225,31 @@ func resizeImage(reader io.Reader, size int, usePng bool) (io.ReadCloser, error)
}
return io.NopCloser(buf), err
}
type ArtworkCache struct {
cache.FileCache
}
type artworkKey struct {
a *artwork
artID model.ArtworkID
size int
lastUpdate time.Time
}
func (k *artworkKey) Key() string {
return fmt.Sprintf("%s.%d.%d.%d", k.artID.ID, k.size, k.artID.LastUpdate.UnixNano(), conf.Server.CoverJpegQuality)
}
func GetImageCache() cache.FileCache {
return singleton.GetInstance(func() *ArtworkCache {
return &ArtworkCache{
FileCache: cache.NewFileCache("Image", conf.Server.ImageCacheSize, consts.ImageCacheDir, consts.DefaultImageCacheMaxItems,
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
info := arg.(*artworkKey)
r, _, err := info.a.get(ctx, info.artID, info.size)
return r, err
}),
}
})
}

View File

@ -4,6 +4,8 @@ import (
"context"
"image"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
@ -31,13 +33,18 @@ var _ = Describe("Artwork", func() {
mfWithEmbed = model.MediaFile{ID: "22", Path: "tests/fixtures/test.mp3", HasCoverArt: true, AlbumID: "222"}
mfWithoutEmbed = model.MediaFile{ID: "44", Path: "tests/fixtures/test.ogg", AlbumID: "444"}
mfCorruptedCover = model.MediaFile{ID: "45", Path: "tests/fixtures/test.ogg", HasCoverArt: true, AlbumID: "444"}
aw = NewArtwork(ds).(*artwork)
DeferCleanup(configtest.SetupConfig())
conf.Server.ImageCacheSize = "0" // Disable cache
cache := GetImageCache()
aw = NewArtwork(ds, cache).(*artwork)
})
Context("Albums", func() {
Context("ID not found", func() {
It("returns placeholder if album is not in the DB", func() {
_, path, err := aw.get(context.Background(), "al-NOT_FOUND-0", 0)
_, path, err := aw.get(context.Background(), model.MustParseArtworkID("al-NOT_FOUND-0"), 0)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal(consts.PlaceholderAlbumArt))
})
@ -50,12 +57,12 @@ var _ = Describe("Artwork", func() {
})
})
It("returns embed cover", func() {
_, path, err := aw.get(context.Background(), alOnlyEmbed.CoverArtID().String(), 0)
_, path, err := aw.get(context.Background(), alOnlyEmbed.CoverArtID(), 0)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("tests/fixtures/test.mp3"))
})
It("returns placeholder if embed path is not available", func() {
_, path, err := aw.get(context.Background(), alEmbedNotFound.CoverArtID().String(), 0)
_, path, err := aw.get(context.Background(), alEmbedNotFound.CoverArtID(), 0)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal(consts.PlaceholderAlbumArt))
})
@ -68,17 +75,17 @@ var _ = Describe("Artwork", func() {
})
})
It("returns external cover", func() {
_, path, err := aw.get(context.Background(), alOnlyExternal.CoverArtID().String(), 0)
_, path, err := aw.get(context.Background(), alOnlyExternal.CoverArtID(), 0)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("tests/fixtures/front.png"))
})
It("returns the first image if more than one is available", func() {
_, path, err := aw.get(context.Background(), alAllOptions.CoverArtID().String(), 0)
_, path, err := aw.get(context.Background(), alAllOptions.CoverArtID(), 0)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("tests/fixtures/cover.jpg"))
})
It("returns placeholder if external file is not available", func() {
_, path, err := aw.get(context.Background(), alExternalNotFound.CoverArtID().String(), 0)
_, path, err := aw.get(context.Background(), alExternalNotFound.CoverArtID(), 0)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal(consts.PlaceholderAlbumArt))
})
@ -87,7 +94,7 @@ var _ = Describe("Artwork", func() {
Context("MediaFiles", func() {
Context("ID not found", func() {
It("returns placeholder if album is not in the DB", func() {
_, path, err := aw.get(context.Background(), "mf-NOT_FOUND-0", 0)
_, path, err := aw.get(context.Background(), model.MustParseArtworkID("mf-NOT_FOUND-0"), 0)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal(consts.PlaceholderAlbumArt))
})
@ -105,17 +112,17 @@ var _ = Describe("Artwork", func() {
})
})
It("returns embed cover", func() {
_, path, err := aw.get(context.Background(), mfWithEmbed.CoverArtID().String(), 0)
_, path, err := aw.get(context.Background(), mfWithEmbed.CoverArtID(), 0)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("tests/fixtures/test.mp3"))
})
It("returns album cover if media file has no cover art", func() {
_, path, err := aw.get(context.Background(), mfWithoutEmbed.CoverArtID().String(), 0)
_, path, err := aw.get(context.Background(), mfWithoutEmbed.CoverArtID(), 0)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("tests/fixtures/front.png"))
})
It("returns album cover if cannot read embed artwork", func() {
_, path, err := aw.get(context.Background(), mfCorruptedCover.CoverArtID().String(), 0)
_, path, err := aw.get(context.Background(), mfCorruptedCover.CoverArtID(), 0)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("tests/fixtures/front.png"))
})
@ -128,7 +135,7 @@ var _ = Describe("Artwork", func() {
})
})
It("returns external cover resized", func() {
r, path, err := aw.get(context.Background(), alOnlyExternal.CoverArtID().String(), 300)
r, path, err := aw.get(context.Background(), alOnlyExternal.CoverArtID(), 300)
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("tests/fixtures/front.png@300"))
img, _, err := image.Decode(r)

View File

@ -11,6 +11,7 @@ var Set = wire.NewSet(
NewArtwork,
NewMediaStreamer,
GetTranscodingCache,
GetImageCache,
NewArchiver,
NewExternalMetadata,
NewPlayers,

View File

@ -48,6 +48,14 @@ func ParseArtworkID(id string) (ArtworkID, error) {
}, nil
}
func MustParseArtworkID(id string) ArtworkID {
artID, err := ParseArtworkID(id)
if err != nil {
panic(artID)
}
return artID
}
func artworkIDFromAlbum(al Album) ArtworkID {
return ArtworkID{
Kind: KindAlbumArtwork,