Repository: beetbox/beets

Issue:
Add the ability to handle error 429 (too many requests) in Spotify plugin
Spotify has a rate-limiting feature. If there are too many requests to the Spotify App it will prevent you from sending further requests. Fortunately, the response includes the `retry-after` value that we should be using to back off. 

I am just creating this issue so that we can keep a track of it based on @wisp3rwind comment [here](https://github.com/beetbox/beets/pull/4352#discussion_r895174817). 

Here are some reference links to implement this feature:
https://github.com/skuethe/MMM-Spotify/issues/172
https://github.com/felix-hilden/tekore/issues/163


Discussion:
Good idea! An ideal implementation of rate limiting would work by measuring the time since the _last_ call to the function—if the calls are naturally spaced out by some other work to be done, then there should be no need to `sleep` at all.

Something along the lines of the rate-limiting in the MusicBrainz library we use:
https://github.com/alastair/python-musicbrainzngs/blob/1638c6271e0beb9560243c2123d354461ec9f842/musicbrainzngs/musicbrainz.py#L376

…although I wrote that code a very long time ago, and maybe there are better Python stdlib facilities for this kind of pattern now?

Resolving patch:
diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py
--- a/beetsplug/spotify.py
+++ b/beetsplug/spotify.py
@@ -17,20 +17,21 @@
 Spotify playlist construction.
 """
 
-import re
-import json
 import base64
+import collections
+import json
+import re
 import time
 import webbrowser
-import collections
 
-import unidecode
-import requests
 import confuse
-
+import requests
+import unidecode
 from beets import ui
 from beets.autotag.hooks import AlbumInfo, TrackInfo
-from beets.plugins import MetadataSourcePlugin, BeetsPlugin
+from beets.plugins import BeetsPlugin, MetadataSourcePlugin
+
+DEFAULT_WAITING_TIME = 5
 
 
 class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
@@ -164,6 +165,13 @@ def _handle_response(self, request_type, url, params=None):
                 )
                 self._authenticate()
                 return self._handle_response(request_type, url, params=params)
+            elif response.status_code == 429:
+                seconds = response.headers.get('Retry-After',
+                                               DEFAULT_WAITING_TIME)
+                self._log.debug('Too many API requests. Retrying after {} \
+                    seconds.', seconds)
+                time.sleep(int(seconds) + 1)
+                return self._handle_response(request_type, url, params=params)
             else:
                 raise ui.UserError(
                     '{} API error:\n{}\nURL:\n{}\nparams:\n{}'.format(
@@ -577,9 +585,6 @@ def _fetch_info(self, items, write, force):
         self._log.debug('Total {} tracks', len(items))
 
         for index, item in enumerate(items, start=1):
-            # Added sleep to avoid API rate limit
-            # https://developer.spotify.com/documentation/web-api/guides/rate-limits/
-            time.sleep(.5)
             self._log.info('Processing {}/{} tracks - {} ',
                            index, len(items), item)
             # If we're not forcing re-downloading for all tracks, check


Test patch: