From 50ada9ad29381b45dc94ff5e24f9c8b5c6965605 Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 23 Jul 2026 08:01:01 -0400 Subject: [PATCH] fix(artwork): invalidate artwork when an uploaded image is deleted Deleting an artist/radio/playlist upload cleared the filename but left the found item_artwork row and its hash, so lists kept advertising the deleted cover's hash-suffixed immutable URL and clients could display it indefinitely. Call EnqueueArtwork after the delete-side Put, symmetric with upload, so the state is cleared and re-resolved to the next source (or absent). --- core/playlists/playlists.go | 6 +++++- core/playlists/playlists_test.go | 18 ++++++++++++++++++ server/nativeapi/artists.go | 6 +++++- server/nativeapi/radios.go | 6 +++++- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/core/playlists/playlists.go b/core/playlists/playlists.go index a4a26c0f5..57e3e944d 100644 --- a/core/playlists/playlists.go +++ b/core/playlists/playlists.go @@ -339,5 +339,9 @@ func (s *playlists) RemoveImage(ctx context.Context, playlistID string) error { } pls.UploadedImage = "" - return s.ds.Playlist(ctx).Put(pls) + if err := s.ds.Playlist(ctx).Put(pls); err != nil { + return err + } + s.imgUpload.EnqueueArtwork(ctx, consts.EntityPlaylist, pls.ID) + return nil } diff --git a/core/playlists/playlists_test.go b/core/playlists/playlists_test.go index 4120fd2f5..0aeed5cd7 100644 --- a/core/playlists/playlists_test.go +++ b/core/playlists/playlists_test.go @@ -420,6 +420,24 @@ var _ = Describe("Playlists", func() { Expect(mockPlsRepo.Last.UploadedImage).To(BeEmpty()) }) + It("clears the resolved artwork state and re-queues after removing an upload", func() { + ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false}) + Expect(ds.Artwork(ctx).PutItemArtwork(&model.ItemArtwork{ + ItemKind: "pl", ItemID: "pls-1", Hash: "oldhash", Source: "upload", + })).To(Succeed()) + + Expect(ps.RemoveImage(ctx, "pls-1")).To(Succeed()) + + _, err := ds.Artwork(ctx).GetItemArtwork("pl", "pls-1", model.ImageTypePrimary) + Expect(err).To(MatchError(model.ErrNotFound)) + queued, _ := ds.ArtworkQueue(ctx).DequeueBatch(100) + Expect(queued).To(ContainElement(SatisfyAll( + HaveField("ItemKind", "pl"), + HaveField("ItemID", "pls-1"), + HaveField("Priority", model.ArtworkPriorityBump), + ))) + }) + It("denies non-owner", func() { ctx = request.WithUser(ctx, model.User{ID: "other-user", IsAdmin: false}) err := ps.RemoveImage(ctx, "pls-1") diff --git a/server/nativeapi/artists.go b/server/nativeapi/artists.go index 90d4c0d27..193f88eda 100644 --- a/server/nativeapi/artists.go +++ b/server/nativeapi/artists.go @@ -69,6 +69,10 @@ func (api *Router) deleteArtistImage() http.HandlerFunc { } ar.UploadedImage = "" ar.UpdatedAt = new(time.Now()) - return api.ds.Artist(ctx).Put(ar, "uploaded_image", "updated_at") + if err := api.ds.Artist(ctx).Put(ar, "uploaded_image", "updated_at"); err != nil { + return err + } + api.imgUpload.EnqueueArtwork(ctx, consts.EntityArtist, ar.ID) + return nil }) } diff --git a/server/nativeapi/radios.go b/server/nativeapi/radios.go index 3868a3501..3e88af287 100644 --- a/server/nativeapi/radios.go +++ b/server/nativeapi/radios.go @@ -69,6 +69,10 @@ func (api *Router) deleteRadioImage() http.HandlerFunc { return err } radio.UploadedImage = "" - return api.ds.Radio(ctx).Put(radio, "UploadedImage") + if err := api.ds.Radio(ctx).Put(radio, "UploadedImage"); err != nil { + return err + } + api.imgUpload.EnqueueArtwork(ctx, consts.EntityRadio, radio.ID) + return nil }) }