From 9a360470969780b5f16b3088d00e5e60a049aa32 Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 17 Jul 2026 22:30:38 -0400 Subject: [PATCH] fix(artwork): version the resized cache key to backfill blurhashes on upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resized entries cached by a pre-blurhash version serve straight from the cache: the resized reader never runs, the original is never read, and the tee never gets a chance to compute a hash — clients that only request sized images (Jellyfin maxwidth) would keep receiving the fake blurhash indefinitely for those items. Versioning the resized cache key makes every post-upgrade sized request miss and refill. The refill pulls the original through Get, which usually hits the cached original (original keys are unchanged), so the backfill costs one decode+re-encode per resized variant with no source or external-provider I/O. Orphaned entries are evicted by the cache's LRU as usual. --- core/artwork/reader_resized.go | 6 +++++- core/artwork/reader_resized_test.go | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/artwork/reader_resized.go b/core/artwork/reader_resized.go index cd16cbada..0cbe6abd0 100644 --- a/core/artwork/reader_resized.go +++ b/core/artwork/reader_resized.go @@ -66,8 +66,12 @@ func resizedFromOriginal(ctx context.Context, a *artwork, artID model.ArtworkID, return r, nil } +// resizedKeyVersion invalidates resized entries cached by pre-blurhash versions: the refill is what +// pulls the original through the tee, so warm entries would otherwise never backfill a hash. +const resizedKeyVersion = "v1" + func (a *resizedArtworkReader) Key() string { - baseKey := fmt.Sprintf("%s.%d", a.cacheKey, a.size) + baseKey := fmt.Sprintf("%s.%d.%s", a.cacheKey, a.size, resizedKeyVersion) if a.square { return baseKey + ".square" } diff --git a/core/artwork/reader_resized_test.go b/core/artwork/reader_resized_test.go index 7c14f5e44..6b419c8bc 100644 --- a/core/artwork/reader_resized_test.go +++ b/core/artwork/reader_resized_test.go @@ -6,12 +6,28 @@ import ( "errors" "io" + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/core/ffmpeg" "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) +var _ = Describe("resizedArtworkReader.Key", func() { + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + conf.Server.CoverArtQuality = 75 + }) + + It("includes the cache version so pre-blurhash resized entries are invalidated", func() { + r := &resizedArtworkReader{cacheKey: "al-1.123", size: 300} + Expect(r.Key()).To(Equal("al-1.123.300.v1.75")) + r.square = true + Expect(r.Key()).To(Equal("al-1.123.300.v1.square")) + }) +}) + var _ = Describe("resizeImage", func() { var mockFF *tests.MockFFmpeg var r *resizedArtworkReader