fix(artwork): return imagesUpdatedAt in LastUpdated when cover art changes

When cover art (cover.jpg) is updated in an album folder, the HTTP
Last-Modified header was incorrectly returning album.UpdatedAt (which
only tracks media file changes) instead of imagesUpdatedAt (which
tracks cover art changes).

This caused browsers to use their cached cover art because the
Last-Modified header didn't change, even though the actual cover art
image data was new (due to cache key changing based on imagesUpdatedAt).

The fix ensures LastUpdated() returns a.lastUpdate (which is the max of
album.UpdatedAt and imagesUpdatedAt) instead of always returning
album.UpdatedAt.

Fixes navidrome/navidrome#5377
This commit is contained in:
bobo-xxx 2026-04-17 23:30:06 +00:00
parent 155e293f4d
commit 9a741859f6
2 changed files with 39 additions and 2 deletions

View file

@ -7,9 +7,9 @@ import (
"image/jpeg"
"image/png"
"io"
"os"
"path/filepath"
"time"
_ "github.com/gen2brain/webp"
@ -146,6 +146,43 @@ var _ = Describe("Artwork", func() {
Entry(nil, " embedded , front.* , cover.*,folder.*", "tests/fixtures/artist/an-album/test.mp3"),
)
})
Context("LastUpdated returns the correct timestamp", func() {
It("returns album UpdatedAt when imagesUpdatedAt is older", func() {
// Set up album with recent UpdatedAt but folder with older ImagesUpdatedAt
now := time.Now().Truncate(time.Second)
albumOld := model.Album{ID: "old-album", Name: "Old Album", UpdatedAt: now, FolderIDs: []string{"folder1"}}
folderRepo.result = []model.Folder{{
Path: "tests/fixtures/artist/an-album",
ImagesUpdatedAt: now.Add(-1 * time.Hour), // older than album.UpdatedAt
ImageFiles: []string{"cover.jpg"},
}}
ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{albumOld})
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{})
ar, err := newAlbumArtworkReader(ctx, aw, albumOld.CoverArtID(), nil)
Expect(err).ToNot(HaveOccurred())
Expect(ar.LastUpdated()).To(Equal(now)) // should return album.UpdatedAt (now)
})
It("returns imagesUpdatedAt when it is newer than album UpdatedAt", func() {
// Set up album with old UpdatedAt but folder with recent ImagesUpdatedAt
// This simulates the case where cover art was updated but no media files changed
now := time.Now().Truncate(time.Second)
albumOld := model.Album{ID: "old-album2", Name: "Old Album 2", UpdatedAt: now.Add(-24 * time.Hour), FolderIDs: []string{"folder1"}}
newerImagesUpdatedAt := now.Add(-1 * time.Hour) // cover art was updated 1 hour ago
folderRepo.result = []model.Folder{{
Path: "tests/fixtures/artist/an-album",
ImagesUpdatedAt: newerImagesUpdatedAt,
ImageFiles: []string{"cover.jpg"},
}}
ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{albumOld})
ds.MediaFile(ctx).(*tests.MockMediaFileRepo).SetData(model.MediaFiles{})
ar, err := newAlbumArtworkReader(ctx, aw, albumOld.CoverArtID(), nil)
Expect(err).ToNot(HaveOccurred())
// Should return imagesUpdatedAt (newer), not album.UpdatedAt (older)
Expect(ar.LastUpdated()).To(Equal(newerImagesUpdatedAt))
})
})
})
Describe("artistArtworkReader", func() {
Context("Multiple covers", func() {

View file

@ -72,7 +72,7 @@ func (a *albumArtworkReader) Key() string {
)
}
func (a *albumArtworkReader) LastUpdated() time.Time {
return a.album.UpdatedAt
return a.lastUpdate
}
func (a *albumArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) {