navidrome/core/artwork/reader_mediafile.go
Deluan f498dfde7d fix(album): make cover cache keys immune to future file mtimes
All cover cache keys used max(updated_at, cover_art_updated_at), so an album
whose updated_at is in the future (bad file mtimes) would mask a cover edit
entirely — id unchanged, reader cache key unchanged, UI URL unchanged — and
keep serving the old image.

Treat the keys as discriminators instead of dates: the album artwork id adds
the two timestamps, the reader cache keys (album/disc/mediafile) carry the
cover stamp as a separate component, and the UI cache-buster joins both
timestamps instead of picking the newest.
2026-07-17 23:03:20 -04:00

86 lines
2.3 KiB
Go

package artwork
import (
"context"
"fmt"
"io"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/model"
)
type mediafileArtworkReader struct {
cacheKey
a *artwork
mediafile model.MediaFile
album model.Album
lib libraryView
}
func newMediafileArtworkReader(ctx context.Context, artwork *artwork, artID model.ArtworkID) (*mediafileArtworkReader, error) {
mf, err := artwork.ds.MediaFile(ctx).Get(artID.ID)
if err != nil {
return nil, err
}
al, err := artwork.ds.Album(ctx).Get(mf.AlbumID)
if err != nil {
return nil, err
}
_, _, imagesUpdatedAt, err := loadAlbumFoldersPaths(ctx, artwork.ds, *al)
if err != nil {
return nil, err
}
lib, err := loadLibraryView(ctx, artwork.ds, mf.LibraryID)
if err != nil {
return nil, err
}
a := &mediafileArtworkReader{
a: artwork,
mediafile: *mf,
album: *al,
lib: lib,
}
a.cacheKey.artID = artID
a.cacheKey.lastUpdate = mf.UpdatedAt
if al.UpdatedAt.After(a.cacheKey.lastUpdate) {
a.cacheKey.lastUpdate = al.UpdatedAt
}
if imagesUpdatedAt != nil && imagesUpdatedAt.After(a.cacheKey.lastUpdate) {
a.cacheKey.lastUpdate = *imagesUpdatedAt
}
if al.CoverArtUpdatedAt != nil && al.CoverArtUpdatedAt.After(a.cacheKey.lastUpdate) {
a.cacheKey.lastUpdate = *al.CoverArtUpdatedAt
}
return a, nil
}
func (a *mediafileArtworkReader) Key() string {
return fmt.Sprintf(
"%s.%t.%d",
a.cacheKey.Key(),
conf.Server.EnableMediaFileCoverArt,
coverStamp(a.album.CoverArtUpdatedAt),
)
}
func (a *mediafileArtworkReader) LastUpdated() time.Time {
return a.lastUpdate
}
func (a *mediafileArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) {
var ff []sourceFunc
if a.mediafile.CoverArtID().Kind == model.KindMediaFileArtwork {
ff = []sourceFunc{
fromTag(ctx, a.lib.FS, a.mediafile.Path),
fromFFmpegTag(ctx, a.a.ffmpeg, a.lib.Abs(a.mediafile.Path)),
}
}
// For multi-disc albums, fall back to disc artwork first; for single-disc albums,
// skip disc resolution (it would just fall through to album art anyway).
if len(a.album.Discs) > 1 {
ff = append(ff, fromAlbum(ctx, a.a, a.mediafile.DiscCoverArtID()))
} else {
ff = append(ff, fromAlbum(ctx, a.a, a.mediafile.AlbumCoverArtID()))
}
return selectImageReader(ctx, a.artID, ff...)
}