fix(scanner): cap folder images_updated_at at scan time

A future-stamped image file (clock skew on NAS mounts) previously flowed verbatim
into folder.images_updated_at and from there into the album artwork version, while
the stored blur_hash_updated_at is capped at now on write. The DTO staleness gate
then kept emitting the fake blurhash until wall time caught up with the file mtime.
Capping at the source keeps future values out of the DB entirely; rotation is
preserved because any later change is capped to a later scan time. Capping in the
DTO instead would be worse: the version would become a moving target and the fake
seed would rotate on every request.
This commit is contained in:
Deluan 2026-07-17 22:47:43 -04:00
parent 08d83a7785
commit f0fe070ba1
2 changed files with 11 additions and 1 deletions

View File

@ -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() {

View File

@ -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
}
}
}