diff --git a/core/artwork/blurhash_updater.go b/core/artwork/blurhash_updater.go index 4457ff725..76077e0d1 100644 --- a/core/artwork/blurhash_updater.go +++ b/core/artwork/blurhash_updater.go @@ -148,7 +148,9 @@ func (u *blurHashUpdater) process(ctx context.Context, artID model.ArtworkID, re sig = req.imageUpdatedAt } if !req.force { - if stored != "" && storedAt != nil && storedAt.Equal(version) && !sig.After(*storedAt) { + // Current when computed from this row version or later; the snapshot may exceed the row + // version because file mtimes (which don't move rows) are folded into it on persist. + if stored != "" && storedAt != nil && !storedAt.Before(version) && !sig.After(*storedAt) { return } if last, ok := u.lastNoResult(artID); ok && !sig.After(last) { @@ -168,7 +170,7 @@ func (u *blurHashUpdater) process(ctx context.Context, artID model.ArtworkID, re u.setNoResult(artID, sig) return } - if err := u.persist(ctx, artID, hash, version); err != nil { + if err := u.persist(ctx, artID, hash, sig); err != nil { log.Warn(ctx, "BlurHash: error persisting", "artID", artID, err) return } diff --git a/core/artwork/e2e/blurhash_test.go b/core/artwork/e2e/blurhash_test.go index 9d69ed27c..0ea75d509 100644 --- a/core/artwork/e2e/blurhash_test.go +++ b/core/artwork/e2e/blurhash_test.go @@ -29,8 +29,9 @@ var _ = Describe("BlurHash", func() { g.Expect(err).ToNot(HaveOccurred()) g.Expect(len(updated.BlurHash)).To(BeNumerically(">", 6)) g.Expect(updated.BlurHashUpdatedAt).ToNot(BeNil()) - // The snapshot must match the artwork version, or the DTO layer would treat it as stale. - g.Expect(updated.BlurHashUpdatedAt.Equal(updated.ArtworkUpdatedAt())).To(BeTrue()) + // The snapshot must not be before the artwork version, or the DTO would treat it as + // stale (it may exceed it: image file mtimes are folded in). + g.Expect(updated.BlurHashUpdatedAt.Before(updated.ArtworkUpdatedAt())).To(BeFalse()) }, "10s", "100ms").Should(Succeed()) }) diff --git a/server/jellyfin/dto/blurhash.go b/server/jellyfin/dto/blurhash.go index cc846bb81..d0404d6c3 100644 --- a/server/jellyfin/dto/blurhash.go +++ b/server/jellyfin/dto/blurhash.go @@ -40,10 +40,11 @@ func blurHash(seed string) string { } // primaryBlurHash returns the stored blurhash when it was computed from the entity's current -// artwork version; otherwise a fake seeded by id+version, so the value still rotates on any -// artwork change (Finamp keys its cover caches by this value; tags never reach its image URLs). +// artwork version or later (the snapshot folds in image file mtimes, which can exceed row +// timestamps); otherwise a fake seeded by id+version, so the value still rotates on any artwork +// change (Finamp keys its cover caches by this value; tags never reach its image URLs). func primaryBlurHash(stored string, storedAt *time.Time, id string, version time.Time) string { - if stored != "" && storedAt != nil && storedAt.Equal(version) { + if stored != "" && storedAt != nil && !storedAt.Before(version) { return stored } return blurHash(fmt.Sprintf("%s-%x", id, version.UnixMilli())) diff --git a/server/jellyfin/dto/blurhash_test.go b/server/jellyfin/dto/blurhash_test.go index 78cd242f8..5fc74f293 100644 --- a/server/jellyfin/dto/blurhash_test.go +++ b/server/jellyfin/dto/blurhash_test.go @@ -34,6 +34,11 @@ var _ = Describe("primaryBlurHash", func() { Expect(primaryBlurHash("LEHV6nWB2yk8", &version, "id-1", version)).To(Equal("LEHV6nWB2yk8")) }) + It("returns the stored hash when the snapshot is newer than the version (image mtime)", func() { + newer := version.Add(time.Hour) + Expect(primaryBlurHash("LEHV6nWB2yk8", &newer, "id-1", version)).To(Equal("LEHV6nWB2yk8")) + }) + It("falls back to a fake when there is no stored hash", func() { h := primaryBlurHash("", nil, "id-1", version) Expect(h).To(HaveLen(6))