diff --git a/core/artwork/worker.go b/core/artwork/worker.go index fab0b96be..499f407d8 100644 --- a/core/artwork/worker.go +++ b/core/artwork/worker.go @@ -250,6 +250,13 @@ func (w *Worker) broadcastRefresh(ctx context.Context, found []model.ArtworkQueu for res, ids := range byResource { event = event.With(res, ids...) } + // A track with no art of its own is served its album's, so an album change moves the track's + // hash too. The dependent id list is unbounded, so refresh the resource as a whole. + if _, ok := byResource["album"]; ok { + if _, ok := byResource["song"]; !ok { + event = event.With("song") + } + } w.broker.SendBroadcastMessage(ctx, event) } diff --git a/core/artwork/worker_test.go b/core/artwork/worker_test.go index bf5bf628e..e2aa8bf82 100644 --- a/core/artwork/worker_test.go +++ b/core/artwork/worker_test.go @@ -18,6 +18,7 @@ import ( "github.com/navidrome/navidrome/server/events" "github.com/navidrome/navidrome/tests" "github.com/navidrome/navidrome/utils/cache" + "github.com/navidrome/navidrome/utils/slice" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "go.uber.org/goleak" @@ -439,8 +440,32 @@ var _ = Describe("Worker", func() { Expect(data).To(ContainSubstring("al2")) Expect(data).ToNot(ContainSubstring("artist"), "a failed (unresolved) artist must not be refreshed") Expect(data).ToNot(ContainSubstring("ar1")) + Expect(data).To(ContainSubstring(`"song"`), "tracks with no art of their own are served the album's") }) + DescribeTable("only pairs songs with album refreshes", + func(kinds []string, wantSong bool) { + items := slice.Map(kinds, func(k string) model.ArtworkQueueItem { + return model.ArtworkQueueItem{ItemKind: k, ItemID: k + "1"} + }) + w.broadcastRefresh(ctx, items) + + evts := broker.getEvents() + Expect(evts).To(HaveLen(1)) + data := evts[0].(*events.RefreshResource).Data(evts[0]) + if wantSong { + Expect(data).To(ContainSubstring(`"song"`)) + } else { + Expect(data).ToNot(ContainSubstring(`"song"`)) + } + }, + Entry("album alone drags songs along", []string{"al"}, true), + Entry("artist alone does not", []string{"ar"}, false), + Entry("playlist alone does not", []string{"pl"}, false), + Entry("album mixed with others still does", []string{"ar", "al"}, true), + Entry("songs resolving on their own stay single-listed", []string{"mf"}, true), + ) + It("broadcasts a refresh when an item resolves to absent (removed cover)", func() { conf.Server.CoverArtPriority = "cover.*" // local-only; no folder image → absent ds.MockedAlbum.(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "al3", Name: "Artless"}})