From 87a72a060fa82153f1995c9f9e563baf1aca3282 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 22 Jan 2020 21:01:30 +0100 Subject: [PATCH] Automatic refresh of Authorization Code Flow Tokens in long-running Applications (#428) * auto-refresh user token * example for a long-running user-request app * wrap long lines * combine duplicate code into _refresh_token_if_expired method * add changelog entry --- CHANGELOG.md | 2 ++ examples/long_running.py | 42 ++++++++++++++++++++++++++++++++++++++++ spotipy/client.py | 6 ++++-- spotipy/oauth2.py | 26 ++++++++++++++++--------- spotipy/util.py | 8 +++----- 5 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 examples/long_running.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 729541b..45fbc13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] + - Auto-Refresh token in long-running apps when using Authorization Code Flow + ## [2.7.1] - 2020-01-20 ### Changed diff --git a/examples/long_running.py b/examples/long_running.py new file mode 100644 index 0000000..2552efd --- /dev/null +++ b/examples/long_running.py @@ -0,0 +1,42 @@ +# long running example (need to be authenticated via oauth) + +import sys +import time +from datetime import datetime + +import spotipy +import spotipy.util as util + +if len(sys.argv) > 1: + username = sys.argv[1] +else: + print("Whoops, need your username!") + print("usage: python user_playlists.py [username]") + sys.exit() + +SCOPE = ','.join([ + 'user-modify-playback-state', + 'user-read-playback-state', + 'user-read-currently-playing' +]) + +oauth = util.prompt_for_user_token( + username, + redirect_uri="http://localhost/", + scope=SCOPE) + +if oauth: + sp = spotipy.Spotify(auth=oauth) + # sp.trace = True + # sp.trace_out = True + start = datetime.now() + while True: + current_playback = sp.current_playback() + print(datetime.now() - start, + "is_playing?", + None if current_playback is None + else current_playback['is_playing']) + + time.sleep(60) +else: + print("Can't get token for", username) diff --git a/spotipy/client.py b/spotipy/client.py index 893fcb5..ef4e351 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -63,7 +63,7 @@ class Spotify(object): """ Creates a Spotify API client. - :param auth: An authorization token (optional) + :param auth: An authenticated OAuth2 Instance :param requests_session: A Requests session object or a truthy value to create one. A falsy value disables sessions. @@ -94,7 +94,9 @@ class Spotify(object): def _auth_headers(self): if self._auth: - return {'Authorization': 'Bearer {0}'.format(self._auth)} + token_info = self._auth.get_cached_token() + access_token = token_info['access_token'] + return {'Authorization': 'Bearer {0}'.format(access_token)} elif self.client_credentials_manager: token = self.client_credentials_manager.get_access_token() return {'Authorization': 'Bearer {0}'.format(token)} diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index f749974..c7979d3 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -16,7 +16,6 @@ import sys import time import requests - # Workaround to support both python 2 & 3 import six import six.moves.urllib.parse as urllibparse @@ -135,32 +134,41 @@ class SpotifyOAuth(object): self.cache_path = cache_path self.scope = self._normalize_scope(scope) self.proxies = proxies + self.token_info = None def get_cached_token(self): ''' Gets a cached auth token ''' - token_info = None + if self.token_info is not None: + self._refresh_token_if_expired() + return self.token_info + if self.cache_path: try: f = open(self.cache_path) token_info_string = f.read() f.close() - token_info = json.loads(token_info_string) + self.token_info = json.loads(token_info_string) # if scopes don't match, then bail - if 'scope' not in token_info or not self._is_scope_subset( - self.scope, token_info['scope']): + if 'scope' not in self.token_info or not self._is_scope_subset( + self.scope, self.token_info['scope']): return None - if self.is_token_expired(token_info): - token_info = self.refresh_access_token( - token_info['refresh_token']) + self._refresh_token_if_expired() except IOError: pass - return token_info + + return self.token_info + + def _refresh_token_if_expired(self): + if self.is_token_expired(self.token_info): + self.token_info = self.refresh_access_token( + self.token_info['refresh_token']) def _save_token_info(self, token_info): + self.token_info = token_info if self.cache_path: try: f = open(self.cache_path, 'w') diff --git a/spotipy/util.py b/spotipy/util.py index d8b7585..ef75074 100644 --- a/spotipy/util.py +++ b/spotipy/util.py @@ -103,9 +103,7 @@ def prompt_for_user_token(username, scope=None, client_id=None, print() code = sp_oauth.parse_response_code(response) - token_info = sp_oauth.get_access_token(code) + sp_oauth.get_access_token(code) + # Auth'ed API request - if token_info: - return token_info['access_token'] - else: - return None + return sp_oauth