diff --git a/core/artwork/artwork.go b/core/artwork/artwork.go index 7b99d89ee..f3dcb7593 100644 --- a/core/artwork/artwork.go +++ b/core/artwork/artwork.go @@ -87,10 +87,11 @@ func (a *artwork) Get(ctx context.Context, artID model.ArtworkID, size int, squa } return nil, time.Time{}, err } - if a.blurHashes != nil && size == 0 && !square && !r.Cached { - // An original-size cache fill is exactly when the served bytes change: the single recompute - // trigger. Resized fills recurse through Get(size=0); a disabled cache reports every serve as - // a fill (the worker's unchanged-hash guard keeps that write-free). + if a.blurHashes != nil && size == 0 && !square { + // Every original-size serve carries the reader's true version; the worker's freshness guard + // turns already-hashed rows into a cheap read and only recomputes when the hash is stale or + // missing. Enqueuing on cache hits too (not just fills) is what backfills warm caches adopted + // from a pre-blurhash version, whose rows migrated in with an empty hash. a.blurHashes.Enqueue(artID, artReader.LastUpdated()) } return r, artReader.LastUpdated(), nil diff --git a/core/artwork/blurhash_updater.go b/core/artwork/blurhash_updater.go index eb2f9f40a..37a39fdd9 100644 --- a/core/artwork/blurhash_updater.go +++ b/core/artwork/blurhash_updater.go @@ -83,10 +83,16 @@ func (u *blurHashUpdater) enqueue(artID model.ArtworkID, req enqueueRequest) { go u.run(ctx) } prev := u.buffer[artID] - if req.snapshot.After(prev.snapshot) { - prev.snapshot = req.snapshot + if !req.snapshot.IsZero() { + // A successful serve proves the artwork exists, so it supersedes any pending gone request for + // the same artwork (a cover restored right after a missing-art serve must still recompute). + prev.gone = false + if req.snapshot.After(prev.snapshot) { + prev.snapshot = req.snapshot + } + } else if req.gone { + prev.gone = true } - prev.gone = prev.gone || req.gone u.buffer[artID] = prev u.mutex.Unlock() select { diff --git a/core/artwork/blurhash_updater_internal_test.go b/core/artwork/blurhash_updater_internal_test.go index d2e5960bb..8a27a2c9e 100644 --- a/core/artwork/blurhash_updater_internal_test.go +++ b/core/artwork/blurhash_updater_internal_test.go @@ -45,7 +45,7 @@ var _ = Describe("blurHashUpdater", func() { Expect(u.buffer[id].gone).To(BeFalse()) }) - It("merges a gone flag onto a pending snapshot for the same artwork", func() { + It("keeps a gone flag when it follows a pending fill (cover then vanished)", func() { id := model.Album{ID: "al-1"}.CoverArtID() t1 := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) u.Enqueue(id, t1) @@ -54,6 +54,15 @@ var _ = Describe("blurHashUpdater", func() { Expect(u.buffer[id].gone).To(BeTrue()) }) + It("lets a successful fill supersede a pending gone (cover restored)", func() { + id := model.Album{ID: "al-1"}.CoverArtID() + t1 := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + u.EnqueueGone(id) + u.Enqueue(id, t1) + Expect(u.buffer[id].snapshot).To(Equal(t1)) + Expect(u.buffer[id].gone).To(BeFalse()) + }) + It("ignores other artwork kinds", func() { u.Enqueue(model.ArtworkID{Kind: model.KindMediaFileArtwork, ID: "mf-1"}, time.Time{}) u.EnqueueGone(model.ArtworkID{Kind: model.KindRadioArtwork, ID: "ra-1"})