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.
This commit is contained in:
Alok Saboo 2025-10-30 10:13:54 -04:00
parent 9608ec0925
commit 0d11e19ecf
2 changed files with 40 additions and 6 deletions

View file

@ -77,6 +77,11 @@ class APIError(Exception):
pass pass
class AudioFeaturesUnavailableError(Exception):
"""Raised when the audio features API returns 403 (deprecated/unavailable)."""
pass
class SpotifyPlugin( class SpotifyPlugin(
SearchApiMetadataSourcePlugin[ SearchApiMetadataSourcePlugin[
Union[SearchResponseAlbums, SearchResponseTracks] Union[SearchResponseAlbums, SearchResponseTracks]
@ -140,6 +145,7 @@ class SpotifyPlugin(
self.config["client_id"].redact = True self.config["client_id"].redact = True
self.config["client_secret"].redact = True self.config["client_secret"].redact = True
self.audio_features_available = True # Track if audio features API is available
self.setup() self.setup()
def setup(self): def setup(self):
@ -246,6 +252,16 @@ class SpotifyPlugin(
f"API Error: {e.response.status_code}\n" f"API Error: {e.response.status_code}\n"
f"URL: {url}\nparams: {params}" 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: elif e.response.status_code == 429:
seconds = e.response.headers.get( seconds = e.response.headers.get(
"Retry-After", DEFAULT_WAITING_TIME "Retry-After", DEFAULT_WAITING_TIME
@ -691,6 +707,8 @@ class SpotifyPlugin(
item["isrc"] = isrc item["isrc"] = isrc
item["ean"] = ean item["ean"] = ean
item["upc"] = upc item["upc"] = upc
if self.audio_features_available:
audio_features = self.track_audio_features(spotify_track_id) audio_features = self.track_audio_features(spotify_track_id)
if audio_features is None: if audio_features is None:
self._log.info("No audio features found for: {}", item) self._log.info("No audio features found for: {}", item)
@ -698,6 +716,9 @@ class SpotifyPlugin(
for feature, value in audio_features.items(): for feature, value in audio_features.items():
if feature in self.spotify_audio_features: if feature in self.spotify_audio_features:
item[self.spotify_audio_features[feature]] = value item[self.spotify_audio_features[feature]] = value
else:
self._log.debug("Audio features API unavailable, skipping")
item["spotify_updated"] = time.time() item["spotify_updated"] = time.time()
item.store() item.store()
if write: if write:
@ -726,6 +747,13 @@ class SpotifyPlugin(
return self._handle_response( return self._handle_response(
"get", f"{self.audio_features_url}{track_id}" "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: except APIError as e:
self._log.debug("Spotify API error: {}", e) self._log.debug("Spotify API error: {}", e)
return None return None

View file

@ -22,6 +22,12 @@ New features:
Bug fixes: 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: For packagers:
Other changes: Other changes: