diff --git a/core/artwork/e2e/blurhash_test.go b/core/artwork/e2e/blurhash_test.go index d11171aaf..f40e83820 100644 --- a/core/artwork/e2e/blurhash_test.go +++ b/core/artwork/e2e/blurhash_test.go @@ -61,6 +61,9 @@ var _ = Describe("BlurHash", func() { // A future file mtime must be capped at now, or the !Before checks would pin the hash // (and the client's cover cache) until wall time caught up. Expect(updated.BlurHashUpdatedAt.After(time.Now())).To(BeFalse()) + // The scanner caps the folder's images_updated_at too, so the artwork version is not future + // and the freshly computed hash is accepted by the DTO instead of the fake. + Expect(updated.BlurHashUpdatedAt.Before(updated.ArtworkUpdatedAt())).To(BeFalse()) }) It("recomputes when the cover is swapped in place", func() { diff --git a/scanner/walk_dir_tree.go b/scanner/walk_dir_tree.go index 887344b1b..b02ec0dde 100644 --- a/scanner/walk_dir_tree.go +++ b/scanner/walk_dir_tree.go @@ -9,6 +9,7 @@ import ( "slices" "sort" "strings" + "time" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/core/storage" @@ -160,7 +161,13 @@ func loadDir(ctx context.Context, job *scanJob, dirPath string, checker *IgnoreC folder.numPlaylists++ case model.IsImageFile(name): folder.imageFiles[entry.Name()] = entry - folder.imagesUpdatedAt = utils.TimeNewest(folder.imagesUpdatedAt, fileInfo.ModTime(), folder.modTime) + imagesAt := utils.TimeNewest(folder.imagesUpdatedAt, fileInfo.ModTime(), folder.modTime) + // Cap at now: a future-stamped image (clock skew) would otherwise become a future artwork + // version that pins the emitted blurhash to the fake until wall time caught up. + if now := time.Now(); imagesAt.After(now) { + imagesAt = now + } + folder.imagesUpdatedAt = imagesAt } } }