mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-12 04:17:16 +02:00
Replace cache.Transient with an artwork-local *deferredAnimation error returned by resizedItem.Reader. The file cache already stores nothing when its loader fails, so the generic cache no longer needs a transient mode. The error carries the GIF bytes and the static stand-in, replacing the deferAnimated callback. The cache's loader-error path now cancels the entry writer, so readers that joined the miss fail with the loader's error instead of reading an empty entry, and the removal no longer blocks on them. The background job resizes the bytes it already holds instead of re-reading them, orOriginal centralizes the serve-the-original fallback, and warmCache shares the store-and-drain step with the worker's precache.
115 lines
3.4 KiB
Go
115 lines
3.4 KiB
Go
package artwork
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/core/ffmpeg"
|
|
"github.com/navidrome/navidrome/utils/cache"
|
|
"github.com/navidrome/navidrome/utils/singleton"
|
|
)
|
|
|
|
// artworkReader is the cache.Item the image cache loader dispatches on: Reader
|
|
// produces the (possibly resized) bytes to store under Key.
|
|
type artworkReader interface {
|
|
cache.Item
|
|
Reader(ctx context.Context) (io.ReadCloser, error)
|
|
}
|
|
|
|
type imageCache struct {
|
|
cache.FileCache
|
|
}
|
|
|
|
func GetImageCache() cache.FileCache {
|
|
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) {
|
|
return arg.(artworkReader).Reader(ctx)
|
|
}),
|
|
}
|
|
})
|
|
}
|
|
|
|
// resizedItem is an artworkReader that resizes bytes opened by open() and caches the
|
|
// result under a hash-derived key.
|
|
type resizedItem struct {
|
|
hash string
|
|
size int
|
|
square bool
|
|
ffmpeg ffmpeg.FFmpeg
|
|
open func() (io.ReadCloser, error)
|
|
// deferAnimated makes Reader return a *deferredAnimation for animated GIFs instead of converting inline.
|
|
deferAnimated bool
|
|
}
|
|
|
|
// Key is the ETag namespaced for the cache, so the validator a client holds and the entry it
|
|
// validates can never drift apart.
|
|
func (r *resizedItem) Key() string {
|
|
return "h-" + representationTag(r.hash, r.size, r.square)
|
|
}
|
|
|
|
func (r *resizedItem) Reader(ctx context.Context) (io.ReadCloser, error) {
|
|
orig, err := r.open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// An open() that reports "no image" as a nil reader would otherwise panic on the Close below.
|
|
if orig == nil {
|
|
return nil, ErrUnavailable
|
|
}
|
|
defer orig.Close()
|
|
data, err := readCapped(orig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r.deferAnimated && isAnimatedGIF(data) && r.ffmpeg.IsAvailable() {
|
|
static, _, err := resizeStaticImage(data, r.size, r.square)
|
|
return nil, &deferredAnimation{data: data, standIn: orOriginal(data, static, err)}
|
|
}
|
|
resized, _, err := resizeImageData(ctx, r.ffmpeg, data, r.size, r.square)
|
|
return io.NopCloser(orOriginal(data, resized, err)), nil
|
|
}
|
|
|
|
// orOriginal serves the original bytes when the resize failed or the image was already within bounds.
|
|
func orOriginal(data []byte, resized io.Reader, err error) io.Reader {
|
|
if err != nil || resized == nil {
|
|
return bytes.NewReader(data)
|
|
}
|
|
return resized
|
|
}
|
|
|
|
// deferredAnimation is returned as an error so the cache stores nothing: it carries the GIF bytes
|
|
// to convert in the background and a static stand-in to serve meanwhile.
|
|
type deferredAnimation struct {
|
|
data []byte
|
|
standIn io.Reader
|
|
}
|
|
|
|
func (*deferredAnimation) Error() string { return "animated image conversion deferred" }
|
|
|
|
// warmCache stores item in c, returning once the entry is written.
|
|
func warmCache(ctx context.Context, c cache.FileCache, item cache.Item) error {
|
|
stream, err := c.Get(ctx, item)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stream.Close()
|
|
_, err = io.Copy(io.Discard, stream)
|
|
return err
|
|
}
|
|
|
|
// storedItem caches bytes produced outside the cache.
|
|
type storedItem struct {
|
|
key string
|
|
data []byte
|
|
}
|
|
|
|
func (i *storedItem) Key() string { return i.key }
|
|
|
|
func (i *storedItem) Reader(context.Context) (io.ReadCloser, error) {
|
|
return io.NopCloser(bytes.NewReader(i.data)), nil
|
|
}
|