mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-09 03:38:48 +01:00
fix(jellyfin): fold folder image mtimes into the album blurhash version
An in-place cover-file swap followed by a quick scan updates only the folder's images_updated_at: no tracks are imported, so the album row never moves and ArtworkUpdatedAt() stayed at the old value. The Jellyfin DTO then kept emitting the previous stored blurhash, and clients that key their cover caches on it never refetched the image, so the served-bytes recompute could never run. The album select now surfaces the newest folder images_updated_at (bare-column correlated subquery over json_each(folder_ids), so the datetime decltype survives and scans as time.Time) and ArtworkUpdatedAt() folds it in. Benchmarked on a 96K track production copy: ~90us/page added, no query-plan change; the subquery is a PK point lookup per folder with at most two rows to order. Artist images and playlist sidecars have the same theoretical gap but their timestamps cannot be derived in SQL; they remain a documented follow-up.
This commit is contained in:
parent
2499de2bac
commit
07af29fbb4
6 changed files with 104 additions and 2 deletions
|
|
@ -135,6 +135,35 @@ var _ = Describe("BlurHash", func() {
|
|||
Expect(storedAlbum(al.ID).BlurHash).ToNot(Equal(firstHash))
|
||||
})
|
||||
|
||||
It("advances the album artwork version when only the cover file changes (quick scan)", func() {
|
||||
setLayout(fstest.MapFS{
|
||||
"Artist/Album/01 - Song.mp3": trackFile(1, "Song"),
|
||||
"Artist/Album/cover.png": realPNG("p1-orig"),
|
||||
})
|
||||
scan()
|
||||
al := firstAlbum()
|
||||
readArtwork(al.CoverArtID())
|
||||
first := storedAlbum(al.ID)
|
||||
Expect(first.BlurHash).ToNot(BeEmpty())
|
||||
Expect(first.BlurHashUpdatedAt.Before(first.ArtworkUpdatedAt())).To(BeFalse())
|
||||
|
||||
// Replace only the cover and quick-scan: the album row stays untouched while the folder's
|
||||
// images_updated_at advances the artwork version, so hash-keyed clients refetch.
|
||||
fakeFS.Add("Artist/Album/cover.png", realPNG("p1-swapped"), time.Now())
|
||||
quickScan()
|
||||
|
||||
stale := storedAlbum(al.ID)
|
||||
Expect(stale.UpdatedAt).To(Equal(first.UpdatedAt), "premise: image-only change must not touch the album row")
|
||||
Expect(stale.BlurHash).To(Equal(first.BlurHash))
|
||||
Expect(stale.BlurHashUpdatedAt.Before(stale.ArtworkUpdatedAt())).To(BeTrue(), "stored hash must read as stale")
|
||||
|
||||
// The refetch serves the new bytes; the tee rotates the hash and its version catches up.
|
||||
readArtwork(al.CoverArtID())
|
||||
fresh := storedAlbum(al.ID)
|
||||
Expect(fresh.BlurHash).ToNot(Equal(first.BlurHash))
|
||||
Expect(fresh.BlurHashUpdatedAt.Before(fresh.ArtworkUpdatedAt())).To(BeFalse())
|
||||
})
|
||||
|
||||
It("clears a stored playlist hash when it falls back to the placeholder", func() {
|
||||
// A playlist with a sidecar cover gets a real hash; removing the sidecar makes the reader chain
|
||||
// fall through to fromAlbumPlaceholder(), whose bytes flow through the tee on Get and clear it.
|
||||
|
|
|
|||
|
|
@ -91,10 +91,22 @@ func setupHarness() {
|
|||
}
|
||||
|
||||
func scan() {
|
||||
GinkgoHelper()
|
||||
doScan(true)
|
||||
}
|
||||
|
||||
// quickScan runs a non-full scan: only outdated folders are processed and unchanged audio files are
|
||||
// not reimported, so an image-only change reaches the folder row without touching the album row.
|
||||
func quickScan() {
|
||||
GinkgoHelper()
|
||||
doScan(false)
|
||||
}
|
||||
|
||||
func doScan(full bool) {
|
||||
GinkgoHelper()
|
||||
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||
playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance())
|
||||
_, err := s.ScanAll(ctx, true)
|
||||
_, err := s.ScanAll(ctx, full)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue