From 9a741859f665cbfedd28865f9f54ebdf03e17b04 Mon Sep 17 00:00:00 2001 From: bobo-xxx <111567133+bobo-xxx@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:30:06 +0000 Subject: [PATCH] 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 --- core/artwork/artwork_internal_test.go | 39 ++++++++++++++++++++++++++- core/artwork/reader_album.go | 2 +- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/core/artwork/artwork_internal_test.go b/core/artwork/artwork_internal_test.go index 380352d3f..739f84aaf 100644 --- a/core/artwork/artwork_internal_test.go +++ b/core/artwork/artwork_internal_test.go @@ -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() { diff --git a/core/artwork/reader_album.go b/core/artwork/reader_album.go index 641b12b33..35d489b6c 100644 --- a/core/artwork/reader_album.go +++ b/core/artwork/reader_album.go @@ -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) {