diff --git a/adapters/lastfm/agent.go b/adapters/lastfm/agent.go index eb8f3d36e..7a1125167 100644 --- a/adapters/lastfm/agent.go +++ b/adapters/lastfm/agent.go @@ -370,11 +370,26 @@ func (l *lastfmAgent) NowPlaying(ctx context.Context, userId string, track *mode }) if err != nil { log.Warn(ctx, "Last.fm client.updateNowPlaying returned error", "track", track.Title, err) + l.dropSessionKeyIfInvalid(ctx, userId, err) return errors.Join(err, scrobbler.ErrUnrecoverable) } return nil } +// dropSessionKeyIfInvalid discards a session key that Last.fm has rejected, so the +// account stops reporting as linked and the user is prompted to re-authenticate +// instead of scrobbles failing silently. +func (l *lastfmAgent) dropSessionKeyIfInvalid(ctx context.Context, userId string, err error) { + var lfErr *lastFMError + if !errors.As(err, &lfErr) || lfErr.Code != errorInvalidSessionKey { + return + } + log.Warn(ctx, "Last.fm session key was rejected, unlinking account", "userId", userId, err) + if delErr := l.sessionKeys.Delete(ctx, userId); delErr != nil { + log.Error(ctx, "Could not delete rejected Last.fm session key", "userId", userId, delErr) + } +} + func (l *lastfmAgent) Scrobble(ctx context.Context, userId string, s scrobbler.Scrobble) error { sk, err := l.sessionKeys.Get(ctx, userId) if err != nil || sk == "" { @@ -407,6 +422,7 @@ func (l *lastfmAgent) Scrobble(ctx context.Context, userId string, s scrobbler.S if lfErr.Code == 11 || lfErr.Code == 16 { return errors.Join(err, scrobbler.ErrRetryLater) } + l.dropSessionKeyIfInvalid(ctx, userId, err) return errors.Join(err, scrobbler.ErrUnrecoverable) } diff --git a/adapters/lastfm/agent_test.go b/adapters/lastfm/agent_test.go index 7e4e29294..b9d178905 100644 --- a/adapters/lastfm/agent_test.go +++ b/adapters/lastfm/agent_test.go @@ -24,6 +24,7 @@ import ( const ( lastfmError3 = `{"error":3,"message":"Invalid Method - No method with that name in this package","links":[]}` lastfmError6 = `{"error":6,"message":"The artist you supplied could not be found","links":[]}` + lastfmError9 = `{"error":9,"message":"Invalid session key - Please re-authenticate","links":[]}` ) var _ = Describe("lastfmAgent", func() { @@ -516,6 +517,32 @@ var _ = Describe("lastfmAgent", func() { err := agent.Scrobble(ctx, "user-1", scrobbler.Scrobble{MediaFile: *track, TimeStamp: time.Now()}) Expect(err).To(MatchError(scrobbler.ErrUnrecoverable)) }) + + It("unlinks the account when the session key is rejected", func() { + httpClient.Res = http.Response{ + Body: io.NopCloser(bytes.NewBufferString(lastfmError9)), + StatusCode: 403, + } + + err := agent.Scrobble(ctx, "user-1", scrobbler.Scrobble{MediaFile: *track, TimeStamp: time.Now()}) + + Expect(err).To(MatchError(scrobbler.ErrUnrecoverable)) + Expect(agent.IsAuthorized(ctx, "user-1")).To(BeFalse()) + }) + }) + + Describe("session key rejected during NowPlaying", func() { + It("unlinks the account", func() { + httpClient.Res = http.Response{ + Body: io.NopCloser(bytes.NewBufferString(lastfmError9)), + StatusCode: 403, + } + + err := agent.NowPlaying(ctx, "user-1", track, 0) + + Expect(err).To(HaveOccurred()) + Expect(agent.IsAuthorized(ctx, "user-1")).To(BeFalse()) + }) }) }) diff --git a/adapters/lastfm/client.go b/adapters/lastfm/client.go index 726df1360..c14ce7340 100644 --- a/adapters/lastfm/client.go +++ b/adapters/lastfm/client.go @@ -19,6 +19,9 @@ import ( const ( apiBaseUrl = "https://ws.audioscrobbler.com/2.0/" + + // https://www.last.fm/api/errorcodes + errorInvalidSessionKey = 9 ) type lastFMError struct {