fix(album): make the cover art id collision-proof against updated_at drops

The additive id (updated_at + cover stamp) could re-emit a previously used
value when updated_at decreases (the newest-mtime track leaves the album) by
the same delta the cover stamp advanced — re-serving a stale client cover.
Mix the timestamps instead, so the id changes whenever either does.
This commit is contained in:
Deluan 2026-07-18 02:07:07 -04:00
parent a79e670740
commit 5a90e17693
2 changed files with 15 additions and 3 deletions

View File

@ -50,6 +50,17 @@ var _ = Describe("Album", func() {
a.CoverArtUpdatedAt = &stamp
Expect(a.CoverArtID().String()).ToNot(Equal(before))
})
It("does not collide when updated_at decreases by the cover-stamp delta", func() {
// With an additive combination, updated_at dropping by N while the cover
// stamp advances by N re-emits the same id — the mix must not.
u1 := time.Unix(1_000_000_000, 0)
c1 := time.Unix(2_000_000_000, 0)
u2 := u1.Add(-100 * time.Second)
c2 := c1.Add(100 * time.Second)
id1 := Album{ID: "al-1", UpdatedAt: u1, CoverArtUpdatedAt: &c1}.CoverArtID().String()
id2 := Album{ID: "al-1", UpdatedAt: u2, CoverArtUpdatedAt: &c2}.CoverArtID().String()
Expect(id1).ToNot(Equal(id2))
})
})
})

View File

@ -112,11 +112,12 @@ func ParseDiscArtworkID(id string) (albumID string, discNumber int, err error) {
}
func artworkIDFromAlbum(al Album) ArtworkID {
// The suffix is a cache discriminator, not a date: cover edits are added (not max'd)
// so the id changes even when updated_at is newer, e.g. files with future mtimes.
// The suffix is a cache discriminator, not a date: mix (not max/add) the timestamps so
// the id changes whenever either does — updated_at can decrease and cancel a plain sum.
lastUpdate := al.UpdatedAt
if al.CoverArtUpdatedAt != nil {
lastUpdate = time.Unix(max(al.UpdatedAt.Unix(), 0)+al.CoverArtUpdatedAt.Unix(), 0)
mixed := (uint64(al.UpdatedAt.Unix()) ^ (uint64(al.CoverArtUpdatedAt.Unix()) * 0x9E3779B97F4A7C15)) & 0x3FFFFFFFFFFFFFFF
lastUpdate = time.Unix(int64(max(mixed, 1)), 0)
}
return ArtworkID{
Kind: KindAlbumArtwork,