From 0d11e19ecf0cb487d6dd12b7f9e871c162a54dbb Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 30 Oct 2025 10:13:54 -0400 Subject: [PATCH] Spotify: gracefully handle 403 from deprecated audio-features API Add a dedicated AudioFeaturesUnavailableError and track audio-features availability with an audio_features_available flag. If the audio-features endpoint returns HTTP 403, raise the new error, log a warning once, and disable further audio-features requests for the session. The plugin now skips attempting audio-features lookups when disabled (avoiding repeated failed calls and potential rate-limit issues). Also update changelog to document the behavior. --- beetsplug/spotify.py | 40 ++++++++++++++++++++++++++++++++++------ docs/changelog.rst | 6 ++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 7cb9e330d..dadb0ea4d 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -77,6 +77,11 @@ class APIError(Exception): pass +class AudioFeaturesUnavailableError(Exception): + """Raised when the audio features API returns 403 (deprecated/unavailable).""" + pass + + class SpotifyPlugin( SearchApiMetadataSourcePlugin[ Union[SearchResponseAlbums, SearchResponseTracks] @@ -140,6 +145,7 @@ class SpotifyPlugin( self.config["client_id"].redact = True self.config["client_secret"].redact = True + self.audio_features_available = True # Track if audio features API is available self.setup() def setup(self): @@ -246,6 +252,16 @@ class SpotifyPlugin( f"API Error: {e.response.status_code}\n" f"URL: {url}\nparams: {params}" ) + elif e.response.status_code == 403: + # Check if this is the audio features endpoint + if self.audio_features_url in url: + raise AudioFeaturesUnavailableError( + "Audio features API returned 403 (deprecated or unavailable)" + ) + raise APIError( + f"API Error: {e.response.status_code}\n" + f"URL: {url}\nparams: {params}" + ) elif e.response.status_code == 429: seconds = e.response.headers.get( "Retry-After", DEFAULT_WAITING_TIME @@ -691,13 +707,18 @@ class SpotifyPlugin( item["isrc"] = isrc item["ean"] = ean item["upc"] = upc - audio_features = self.track_audio_features(spotify_track_id) - if audio_features is None: - self._log.info("No audio features found for: {}", item) + + if self.audio_features_available: + audio_features = self.track_audio_features(spotify_track_id) + if audio_features is None: + self._log.info("No audio features found for: {}", item) + else: + for feature, value in audio_features.items(): + if feature in self.spotify_audio_features: + item[self.spotify_audio_features[feature]] = value else: - for feature, value in audio_features.items(): - if feature in self.spotify_audio_features: - item[self.spotify_audio_features[feature]] = value + self._log.debug("Audio features API unavailable, skipping") + item["spotify_updated"] = time.time() item.store() if write: @@ -726,6 +747,13 @@ class SpotifyPlugin( return self._handle_response( "get", f"{self.audio_features_url}{track_id}" ) + except AudioFeaturesUnavailableError as e: + self._log.warning( + "Audio features API is unavailable (403 error). " + "Skipping audio features for remaining tracks." + ) + self.audio_features_available = False + return None except APIError as e: self._log.debug("Spotify API error: {}", e) return None diff --git a/docs/changelog.rst b/docs/changelog.rst index a78787273..e192259b1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,12 @@ New features: Bug fixes: +- :doc:`/plugins/spotify`: The plugin now gracefully handles audio-features API + deprecation (HTTP 403 errors). When a 403 error is encountered from the + audio-features endpoint, the plugin logs a warning once and skips audio + features for all remaining tracks in the session, avoiding unnecessary API + calls and rate limit exhaustion. + For packagers: Other changes: