"""Spotify OAuth token management with automatic refresh""" import base64 import requests class SpotifyAuth: """Manages Spotify OAuth tokens with automatic refresh""" def __init__(self, refresh_token: str, client_id: str, client_secret: str, access_token: str = ''): self._access_token = access_token self._refresh_token = refresh_token self._client_id = client_id self._client_secret = client_secret def get_access_token(self) -> str: """Get current access token""" return self._access_token def has_access_token(self) -> bool: """Check if access token exists""" return bool(self._access_token) def refresh_access_token(self) -> str: """ Refresh access token using refresh token Raises: Exception: If token refresh fails """ if not self._refresh_token: raise Exception('No refresh token available') # Create Basic auth header (client_id:client_secret encoded in base64) auth_string = f'{self._client_id}:{self._client_secret}' auth_bytes = auth_string.encode('ascii') auth_b64 = base64.b64encode(auth_bytes).decode('ascii') try: response = requests.post( 'https://accounts.spotify.com/api/token', data={ 'grant_type': 'refresh_token', 'refresh_token': self._refresh_token, }, headers={ 'Authorization': f'Basic {auth_b64}', 'Content-Type': 'application/x-www-form-urlencoded', }, timeout=10 ) response.raise_for_status() token_data = response.json() self._access_token = token_data['access_token'] # Update refresh token if provided if token_data.get('refresh_token'): self._refresh_token = token_data['refresh_token'] return self._access_token except requests.RequestException as e: error_msg = str(e) if hasattr(e, 'response') and e.response is not None: try: error_data = e.response.json() error_msg = error_data.get('error_description', error_msg) except ValueError: # If response is not JSON, we ignore this exception and use the original error message above pass raise Exception(f'Failed to refresh token: {error_msg}') def get_client_credentials_token(self) -> str: """ Get access token using client credentials flow (no user refresh token) Used for looking up public artist/track metadata Returns: str: Access token Raises: Exception: If token request fails """ # Create Basic auth header (client_id:client_secret encoded in base64) auth_string = f'{self._client_id}:{self._client_secret}' auth_bytes = auth_string.encode('ascii') auth_b64 = base64.b64encode(auth_bytes).decode('ascii') try: response = requests.post( 'https://accounts.spotify.com/api/token', data={ 'grant_type': 'client_credentials', }, headers={ 'Authorization': f'Basic {auth_b64}', 'Content-Type': 'application/x-www-form-urlencoded', }, timeout=10 ) response.raise_for_status() token_data = response.json() self._access_token = token_data['access_token'] return self._access_token except requests.RequestException as e: error_msg = str(e) if hasattr(e, 'response') and e.response is not None: try: error_data = e.response.json() error_msg = error_data.get('error_description', error_msg) except ValueError: # If response is not JSON, we ignore this exception and use the original error message above pass raise Exception(f'Failed to get client credentials token: {error_msg}') def ensure_valid_token(self) -> None: """Ensure token is valid, refresh if needed or missing""" if not self.has_access_token(): self.refresh_access_token()