fix(album): bust public share images on cover edits; drop stale stamp copies

Share.CoverArtID built a bare Album{ID}, so public album-share image URLs
were a constant al-<id>_0 — combined with the 10-year cache header, viewers
kept the pre-edit cover forever. Use the loaded album when available so the
URL carries the real timestamps.

Also skip copying cover_art_updated_at in CopyAttributes when the source has
no uploaded_image (a cover removal leaves the stamp set), so an album-ID
merge can't overwrite the destination's valid cover stamp with a stale one.
This commit is contained in:
Deluan 2026-07-18 00:12:21 -04:00
parent 7c27e9d5a3
commit 5418e88f8e
4 changed files with 47 additions and 1 deletions

View File

@ -37,6 +37,10 @@ func (s Share) CoverArtID() ArtworkID {
}
switch s.ResourceType {
case "album":
// Use the loaded album when available, so the public URL busts on cover edits
if len(s.Albums) > 0 && s.Albums[0].ID == ids[0] {
return s.Albums[0].CoverArtID()
}
return Album{ID: ids[0]}.CoverArtID()
case "playlist":
return Playlist{ID: ids[0]}.CoverArtID()

24
model/share_test.go Normal file
View File

@ -0,0 +1,24 @@
package model_test
import (
"time"
. "github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Share", func() {
Describe("CoverArtID", func() {
It("uses the loaded album, so the public URL busts on cover edits", func() {
s := Share{ResourceType: "album", ResourceIDs: "al-1"}
plain := s.CoverArtID().String()
Expect(s.CoverArtID().ID).To(Equal("al-1"))
stamp := time.Now()
s.Albums = Albums{{ID: "al-1", CoverArtUpdatedAt: &stamp}}
Expect(s.CoverArtID().ID).To(Equal("al-1"))
Expect(s.CoverArtID().String()).ToNot(Equal(plain))
})
})
})

View File

@ -299,10 +299,14 @@ func (r *albumRepository) CopyAttributes(fromID, toID string, columns ...string)
if col == "created_at" && (!v.Valid || v.String == "" || strings.HasPrefix(v.String, "0001-")) {
continue
}
// A source without an uploaded cover must not wipe one the destination has.
// A source without an uploaded cover must not wipe one the destination has,
// nor contribute a stale cover stamp left behind by a cover removal.
if (col == "uploaded_image" || col == "cover_art_updated_at") && (!v.Valid || v.String == "") {
continue
}
if col == "cover_art_updated_at" && (!from["uploaded_image"].Valid || from["uploaded_image"].String == "") {
continue
}
to[col] = v
}
if len(to) == 0 {

View File

@ -174,6 +174,20 @@ var _ = Describe("AlbumRepository", func() {
Expect(got.UploadedImage).To(Equal("copy-dst_cover.jpg"))
Expect(got.CoverArtUpdatedAt).ToNot(BeNil())
})
It("does not copy a stale cover stamp left behind by a cover removal", func() {
// A removal clears uploaded_image but keeps cover_art_updated_at set
Expect(albumRepo.UpdateImage("copy-src", "copy-src_cover.jpg")).To(Succeed())
Expect(albumRepo.UpdateImage("copy-src", "")).To(Succeed())
Expect(albumRepo.UpdateImage("copy-dst", "copy-dst_cover.jpg")).To(Succeed())
before, err := albumRepo.Get("copy-dst")
Expect(err).ToNot(HaveOccurred())
Expect(albumRepo.CopyAttributes("copy-src", "copy-dst", "uploaded_image", "cover_art_updated_at")).To(Succeed())
got, err := albumRepo.Get("copy-dst")
Expect(err).ToNot(HaveOccurred())
Expect(got.UploadedImage).To(Equal("copy-dst_cover.jpg"))
Expect(got.CoverArtUpdatedAt.Equal(*before.CoverArtUpdatedAt)).To(BeTrue())
})
})
Describe("GetCursor", func() {