Files
Deluan cb396f3dba feat(ui): increase cover art size to 600px and use CatmullRom scaling
Increased the UI cover art request size from 300px to 600px for sharper
images on high-DPI displays. Replaced BiLinear with CatmullRom (bicubic)
interpolation for higher quality image resizing. Extracted the hardcoded
size into a COVER_ART_SIZE constant in the frontend and consolidated
backend sizes into a CacheWarmerImageSizes slice. Removed the unused
UIThumbnailSize constant.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-22 14:55:14 -04:00

164 lines
4.0 KiB
Go

package artwork
import (
"context"
"fmt"
"io"
"maps"
"slices"
"sync"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils/cache"
"github.com/navidrome/navidrome/utils/pl"
)
type CacheWarmer interface {
PreCache(artID model.ArtworkID)
}
// NewCacheWarmer creates a new CacheWarmer instance. The CacheWarmer will pre-cache Artwork images in the background
// to speed up the response time when the image is requested by the UI. The cache is pre-populated with the original
// image size, as well as the size defined in the UICoverArtSize constant.
func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer {
// If image cache is disabled, return a NOOP implementation
if conf.Server.ImageCacheSize == "0" || !conf.Server.EnableArtworkPrecache {
return &noopCacheWarmer{}
}
// If the file cache is disabled, return a NOOP implementation
if cache.Disabled(context.Background()) {
log.Debug("Image cache disabled. Cache warmer will not run")
return &noopCacheWarmer{}
}
a := &cacheWarmer{
artwork: artwork,
cache: cache,
buffer: make(map[model.ArtworkID]struct{}),
wakeSignal: make(chan struct{}, 1),
}
// Create a context with a fake admin user, to be able to pre-cache Playlist CoverArts
ctx := request.WithUser(context.TODO(), model.User{IsAdmin: true})
go a.run(ctx)
return a
}
type cacheWarmer struct {
artwork Artwork
buffer map[model.ArtworkID]struct{}
mutex sync.Mutex
cache cache.FileCache
wakeSignal chan struct{}
}
func (a *cacheWarmer) PreCache(artID model.ArtworkID) {
if a.cache.Disabled(context.Background()) {
return
}
a.mutex.Lock()
defer a.mutex.Unlock()
a.buffer[artID] = struct{}{}
a.sendWakeSignal()
}
func (a *cacheWarmer) sendWakeSignal() {
// Don't block if the previous signal was not read yet
select {
case a.wakeSignal <- struct{}{}:
default:
}
}
func (a *cacheWarmer) run(ctx context.Context) {
for {
a.waitSignal(ctx, 10*time.Second)
if ctx.Err() != nil {
break
}
if a.cache.Disabled(ctx) {
a.mutex.Lock()
pending := len(a.buffer)
a.buffer = make(map[model.ArtworkID]struct{})
a.mutex.Unlock()
if pending > 0 {
log.Trace(ctx, "Cache disabled, discarding precache buffer", "bufferLen", pending)
}
return
}
// If cache not available, keep waiting
if !a.cache.Available(ctx) {
a.mutex.Lock()
bufferLen := len(a.buffer)
a.mutex.Unlock()
if bufferLen > 0 {
log.Trace(ctx, "Cache not available, buffering precache request", "bufferLen", bufferLen)
}
continue
}
a.mutex.Lock()
// If there's nothing to send, keep waiting
if len(a.buffer) == 0 {
a.mutex.Unlock()
continue
}
batch := slices.Collect(maps.Keys(a.buffer))
a.buffer = make(map[model.ArtworkID]struct{})
a.mutex.Unlock()
a.processBatch(ctx, batch)
}
}
func (a *cacheWarmer) waitSignal(ctx context.Context, timeout time.Duration) {
select {
case <-time.After(timeout):
case <-a.wakeSignal:
case <-ctx.Done():
}
}
func (a *cacheWarmer) processBatch(ctx context.Context, batch []model.ArtworkID) {
log.Trace(ctx, "PreCaching a new batch of artwork", "batchSize", len(batch))
input := pl.FromSlice(ctx, batch)
errs := pl.Sink(ctx, 4, input, a.doCacheImage)
for err := range errs {
log.Debug(ctx, "Error warming cache", err)
}
}
func (a *cacheWarmer) doCacheImage(ctx context.Context, id model.ArtworkID) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
for _, size := range consts.CacheWarmerImageSizes {
r, _, err := a.artwork.Get(ctx, id, size, true)
if err != nil {
return fmt.Errorf("caching id='%s', size=%d: %w", id, size, err)
}
_, err = io.Copy(io.Discard, r)
r.Close()
return err
}
return nil
}
func NoopCacheWarmer() CacheWarmer {
return &noopCacheWarmer{}
}
type noopCacheWarmer struct{}
func (a *noopCacheWarmer) PreCache(model.ArtworkID) {}