"""Apple Music Kit related helpers.""" import datetime import os import jwt from oto import response import requests from analytics import config from analytics.connectors import sentry from analytics.models import utils metadata_playlists_url_template = ( 'https://api.music.apple.com/v1/catalog/{storefront}/playlists?ids={ids}') metadata_stations_url_template = ( 'https://api.music.apple.com/v1/catalog/{storefront}/stations?ids={ids}') _api_playlist_link_templates = { 'playlist': ( 'https://api.music.apple.com/v1/catalog/us/playlists?ids={ids}'), 'station': ( 'https://api.music.apple.com/v1/catalog/us/stations?ids={ids}')} _playlist_link_templates = { 'playlist': 'https://itunes.apple.com/us/playlist/id{}', 'station': 'https://itunes.apple.com/us/station/id{}'} def _obtain_access_token(): """Obtain access token for further Apple Music Kit API calls. Returns: str: access token. """ alg = 'ES256' time_now = datetime.datetime.utcnow() time_expired = time_now + datetime.timedelta(hours=1) headers = {'alg': alg, 'kid': config.APPLE_MUSIC_KEY_ID} payload = { 'iss': config.APPLE_MUSIC_TEAM_ID, 'exp': int(time_expired.strftime('%s')), 'iat': int(time_now.strftime('%s')) } return jwt.encode( payload, config.APPLE_MUSIC_PRIVATE_KEY, algorithm=alg, headers=headers ) def _auth_headers(access_token): """Form Apple Music Kit API headers. Args: access_token (str): access token for Apple Music Kit API calls. Returns: dict: headers dictionary. """ return {'Authorization': 'Bearer {}'.format(access_token)} def _square_image(image): """Square image from Apple Music Kit API response. In links like '...source/{w}x{h}bb.jpg' 'bb' part should be updated to 'cc' for making any image square. Args: image (str): image link. Returns: str: updated image link. """ image = image.format(w=300, h=300, c='cc') img_url, img_extension = os.path.splitext(image) if img_url.endswith('bb'): img_url = ''.join([img_url[:-2], 'cc']) return ''.join([img_url, img_extension]) def _get_metadata_from_response(data, playlist_type): """Form playlist or station metadata from Apple Music Kit API response. Args: data (dict): Apple Music API response data. playlist_type (str): 'playlist' or 'station'. Returns: dict: Playlist metadata. """ playlist_id = data['id'] playlist_link = _playlist_link_templates[playlist_type].format(playlist_id) attributes = data['attributes'] playlist_author = attributes.get('curatorName', 'Radio Station') playlist_title = attributes.get('name') image = attributes.get('artwork') if image is not None: image = _square_image(image['url']) return { playlist_id: { 'playlist_link': playlist_link, 'playlist_author': playlist_author, 'playlist_title': playlist_title, 'image': image}} def _process_response_data(response_data, playlists_type): """Process Apple Music API response. Args: response_data (dict): Apple Music API response. playlists_type (str): 'playlist' or 'station'. Returns dict: Key is playlist id, valur dict with metadata of playlist. """ result = {} if response_data.get('data'): for playlist in response_data['data']: result.update( _get_metadata_from_response(playlist, playlists_type)) return result def _prepare_playlists_metadata(playlists, found_playlists): """Prepare metadata of playlists. If particular playlist was represented in Apple Music API response then add the playlist metadata, if not so then add 404 status. Args: playlists (list): List of requested playlists. found_playlists (dict): Playlists from Apple Music API response. Returns: list: List with metadata of playlists. """ result = [] for playlist in playlists: if playlist['playlist_id'] in found_playlists: playlist_data = found_playlists[ playlist['playlist_id']] playlist_data['status_code'] = 200 result.append(playlist_data) else: result.append({ 'playlist_link': playlist['playlist_link'], 'status_code': 404}) return result def get_playlists_metadata(playlists, logger, feed_ids, distributors): """Get metadata for each playlist. Args: playlists (list): list of playlist. logger (OwsLoggingAdapter): Application logger. It is passed explicitly since flask `g` object is not available in spawned threads. feed_ids (list): list of available feed_ids. distributors (list): list of distributors names. Returns: oto.Response: Response with list of metadata of playlists. """ access_token = _obtain_access_token() headers = _auth_headers(access_token) playlist_ids = ','.join( [playlist['playlist_id'] for playlist in playlists]) playlists_type = playlists[0]['type'] api_url = _api_playlist_link_templates[playlists_type].format( ids=playlist_ids) api_response_status_code = 500 try: api_response = requests.get(api_url, headers=headers) api_response_status_code = api_response.status_code if api_response_status_code == 200: response_data = api_response.json() found_playlists = _process_response_data( response_data, playlists_type) result = _prepare_playlists_metadata(playlists, found_playlists) return response.Response(result) elif api_response_status_code == 429: sentry.sentry_client.captureMessage( 'Apple Music API limit was reached.', extra={ 'api_url': api_url, 'playlists_type': playlists_type}) except Exception as e: if isinstance(e, requests.exceptions.ReadTimeout): api_response_status_code = 504 sentry.sentry_client.captureException() result = utils.playlists_with_status(playlists, api_response_status_code) return response.Response(result)