fix(artwork): address blurhash review findings

- force recompute on image-cache miss, so in-place cover/sidecar swaps and
  agent image updates refresh the stored hash even when no entity row moved
- exclude external_info_updated_at from the artwork version: agent TTL
  refreshes bump it with an unchanged image, churning Finamp's cover cache
- lazy-start the worker goroutine and bound each computation with a 30s
  timeout; remember no-result entities per version to avoid re-decoding
  placeholders on every serve
- error (instead of silently skipping) on unknown artwork kinds in persist
This commit is contained in:
Deluan 2026-07-16 08:21:57 -04:00
commit e100c48102
7 changed files with 112 additions and 58 deletions

View file

@ -77,16 +77,14 @@ func (a Album) CoverArtID() ArtworkID {
return artworkIDFromAlbum(a)
}
// ArtworkUpdatedAt is the album's artwork version: the newest row timestamp that can affect
// which cover image is served (scan updates, imports, agent-fetched external images).
// ArtworkUpdatedAt is the album's artwork version. ExternalInfoUpdatedAt is deliberately excluded:
// it bumps on every agent TTL refresh even when the image is unchanged, and actual image changes
// are caught by the image-cache-miss recompute instead.
func (a Album) ArtworkUpdatedAt() time.Time {
t := a.UpdatedAt
if a.ImportedAt.After(t) {
t = a.ImportedAt
}
if a.ExternalInfoUpdatedAt != nil && a.ExternalInfoUpdatedAt.After(t) {
t = *a.ExternalInfoUpdatedAt
}
return t
}

View file

@ -65,8 +65,8 @@ var _ = Describe("Album.ArtworkUpdatedAt", func() {
al := Album{UpdatedAt: base, ImportedAt: later}
Expect(al.ArtworkUpdatedAt()).To(Equal(later))
})
It("returns ExternalInfoUpdatedAt when it is the newest", func() {
It("ignores ExternalInfoUpdatedAt (agent TTL refreshes bump it without an image change)", func() {
al := Album{UpdatedAt: base, ImportedAt: later, ExternalInfoUpdatedAt: &latest}
Expect(al.ArtworkUpdatedAt()).To(Equal(latest))
Expect(al.ArtworkUpdatedAt()).To(Equal(later))
})
})

View file

@ -66,17 +66,14 @@ func (a Artist) CoverArtID() ArtworkID {
return artworkIDFromArtist(a)
}
// ArtworkUpdatedAt is the artist's artwork version; images often arrive via external agents,
// which bump ExternalInfoUpdatedAt rather than UpdatedAt.
// ArtworkUpdatedAt is the artist's artwork version. ExternalInfoUpdatedAt is deliberately
// excluded: it bumps on every agent TTL refresh even when the image is unchanged, and actual
// image changes are caught by the image-cache-miss recompute instead.
func (a Artist) ArtworkUpdatedAt() time.Time {
var t time.Time
if a.UpdatedAt != nil {
t = *a.UpdatedAt
if a.UpdatedAt == nil {
return time.Time{}
}
if a.ExternalInfoUpdatedAt != nil && a.ExternalInfoUpdatedAt.After(t) {
t = *a.ExternalInfoUpdatedAt
}
return t
return *a.UpdatedAt
}
func (a Artist) UploadedImagePath() string {

View file

@ -37,10 +37,10 @@ var _ = Describe("Artist.ArtworkUpdatedAt", func() {
It("handles nil timestamps", func() {
Expect(model.Artist{}.ArtworkUpdatedAt()).To(Equal(time.Time{}))
})
It("returns UpdatedAt when newest", func() {
It("returns UpdatedAt", func() {
Expect(model.Artist{UpdatedAt: &later, ExternalInfoUpdatedAt: &base}.ArtworkUpdatedAt()).To(Equal(later))
})
It("returns ExternalInfoUpdatedAt when newest", func() {
Expect(model.Artist{UpdatedAt: &base, ExternalInfoUpdatedAt: &later}.ArtworkUpdatedAt()).To(Equal(later))
It("ignores ExternalInfoUpdatedAt (agent TTL refreshes bump it without an image change)", func() {
Expect(model.Artist{UpdatedAt: &base, ExternalInfoUpdatedAt: &later}.ArtworkUpdatedAt()).To(Equal(base))
})
})