diff --git a/adapters/lastfm/agent.go b/adapters/lastfm/agent.go index 98850fdde..f967595e3 100644 --- a/adapters/lastfm/agent.go +++ b/adapters/lastfm/agent.go @@ -291,12 +291,13 @@ func (l *lastfmAgent) GetArtistImages(ctx context.Context, _, name, mbid string) func (l *lastfmAgent) callAlbumGetInfo(ctx context.Context, name, artist, lang string) (*Album, error) { a, err := l.client.albumGetInfo(ctx, name, artist, "", lang) if err != nil { - var lfErr *lastFMError - if errors.As(err, &lfErr) && lfErr.Code == 6 { - log.Debug(ctx, "Album not found", "album", name, "artist", artist, err) - } else { - log.Error(ctx, "Error calling LastFM/album.getInfo", "album", name, "artist", artist, err) + if lfErr, ok := errors.AsType[*lastFMError](err); ok && lfErr.Code == 6 { + // A not-found is a definitive absence, not a fault: return the shared sentinel so the + // artwork worker's breaker/transient checks don't retry it, and log it at Debug. + log.Debug(ctx, "Album not found in Last.fm", "album", name, "artist", artist) + return nil, agents.ErrNotFound } + log.Error(ctx, "Error calling LastFM/album.getInfo", "album", name, "artist", artist, err) return nil, err } return a, nil @@ -308,6 +309,12 @@ func (l *lastfmAgent) callArtistGetInfo(ctx context.Context, name string, lang s a, err := l.client.artistGetInfo(ctx, name, lang) if err != nil { + if lfErr, ok := errors.AsType[*lastFMError](err); ok && lfErr.Code == 6 { + // A not-found is a definitive absence, not a fault: return the shared sentinel so it + // doesn't trip the artwork worker's breaker, and log at Debug instead of Error. + log.Debug(ctx, "Artist not found in Last.fm", "artist", name) + return nil, agents.ErrNotFound + } log.Error(ctx, "Error calling LastFM/artist.getInfo", "artist", name, err) return nil, err } diff --git a/adapters/lastfm/agent_test.go b/adapters/lastfm/agent_test.go index 1d1400620..6e94a075d 100644 --- a/adapters/lastfm/agent_test.go +++ b/adapters/lastfm/agent_test.go @@ -576,6 +576,9 @@ var _ = Describe("lastfmAgent", func() { httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200} _, err := agent.GetAlbumInfo(ctx, "123", "U2", "mbid-1234") Expect(err).To(HaveOccurred()) + // error 6 is a definitive not-found, so it must satisfy the shared sentinel (else it + // would trip the artwork worker's circuit breaker and be retried as a transient fault). + Expect(errors.Is(err, agents.ErrNotFound)).To(BeTrue()) // No MBID retry: album.getInfo is queried by name+artist only, in a single call. Expect(httpClient.RequestCount).To(Equal(1)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty()) @@ -609,6 +612,14 @@ var _ = Describe("lastfmAgent", func() { Expect(images[0].URL).To(Equal("https://lastfm.freetls.fastly.net/i/u/ar0/818148bf682d429dc21b59a73ef6f68e.png")) }) + It("maps a Last.fm error 6 (artist not found) to the shared not-found sentinel", func() { + apiClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200} + _, err := agent.GetArtistImages(ctx, "123", "Nonexistent Artist", "") + // Definitive not-found, not a fault — must satisfy the sentinel through the %w wrap so + // runs of missing artists never trip the artwork worker's circuit breaker. + Expect(errors.Is(err, agents.ErrNotFound)).To(BeTrue()) + }) + It("returns empty list if image is the ignored default image", func() { fApi, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json") apiClient.Res = http.Response{Body: fApi, StatusCode: 200}