"""Apple Music client.""" from datetime import datetime, timedelta import backoff import jwt import requests from requests.exceptions import HTTPError class AppleMusicClientCredentialsManager: """Credentials manager helper class.""" def __init__(self, client_id, team_id, private_key, token_expires_in=3600): """Credentials manager initialization. Args: client_id (str): Apple 10-character key identifier. team_id (str): Apple 10-character Team ID. private_key (str): MusicKit private key. token_expires_in (int): Lifetime of token in seconds. """ self._client_id = client_id self._team_id = team_id self._private_key = private_key self._expires_in = token_expires_in self._token_info = None self._alg = 'ES256' def get_access_token(self): """Get a valid access token. Generates a new token or returns exists if it's not expired. Returns: str: Access token. """ if self._token_info and not self._is_token_expired(): return self._token_info['access_token'] self._token_info = self._get_token_info() return self._token_info['access_token'] def _get_token_info(self): """Get access token information.""" token_expiration_time = datetime.now() + timedelta(seconds=self._expires_in) token_info = {'token_expiration_time': token_expiration_time} token_info['access_token'] = self._request_access_token(token_expiration_time) return token_info def _request_access_token(self, token_expiration_time): """Actual access token request.""" headers = {'alg': self._alg, 'kid': self._client_id} payload = { 'iss': self._team_id, 'iat': int(datetime.now().timestamp()), 'exp': int(token_expiration_time.timestamp()), } token = jwt.encode( payload, self._private_key, algorithm=self._alg, headers=headers ) return token def _is_token_expired(self): """Check whether the token is expired.""" return self._token_info['token_expiration_time'] < datetime.now() class AppleMusicClient: """Apple Music client class.""" def __init__( self, client_credentials_manager, requests_timeout=5, delay_step=0.5, ): """Client class initialization. Args: client_credentials_manager (AppleMusicClientCredentialsManager): creds manager. requests_timeout (int): In what time request times out. delay_step (float): Additional delay between retries. """ self._client_credentials_manager = client_credentials_manager self._requests_timeout = requests_timeout self._delay_step = delay_step self._search_api_url = 'https://api.music.apple.com/v1/catalog/us/search' self._search_artist_api_url = ( 'https://api.music.apple.com/v1/catalog/us/artists/{}' ) def _auth_headers(self): return { 'Authorization': 'Bearer {}'.format( self._client_credentials_manager.get_access_token() ) } @backoff.on_exception( backoff.expo, HTTPError, max_tries=3, giveup=lambda e: e.response.status_code != 429 and (e.response.status_code < 500 or e.response.status_code >= 600), ) def search_artist(self, query, limit=10, offset=0, localization='en'): """Search artist in Apple Music. Args: query (str): Term for search. limit (int): Max number of matched artists. offset (int): Offset from the beginning of the result. localization (str): Localization for the search. Returns: dict: Apple Music search endpoint result. """ headers = self._auth_headers() headers['Content-Type'] = 'application/json' params = { 'term': query, 'limit': limit, 'offset': offset, 'types': 'artists', 'l': localization, } response = requests.get( self._search_api_url, headers=headers, params=params, timeout=self._requests_timeout, ) response.raise_for_status() return response.json() @backoff.on_exception( backoff.expo, HTTPError, max_tries=3, giveup=lambda e: e.response.status_code != 429 and (e.response.status_code < 500 or e.response.status_code >= 600), ) def get_artist_by_id(self, artist_id): """Search artist by ID in Apple Music. Args: artist_id (str): Artist ID. Returns: dict: Apple Music artist result. """ headers = self._auth_headers() headers['Content-Type'] = 'application/json' response = requests.get( self._search_artist_api_url.format(artist_id), headers=headers, timeout=self._requests_timeout, ) response.raise_for_status() return response.json()