""" Integration with Vendor API. Vendor API: https://github.com/filtr/vendor-api. """ import requests from apollo_utils.service.exceptions import BadRequest from flask import current_app as app from typing import List, Union from apollo_main_db.apollo.models import ApolloMarket from src.constants.core import DEFAULT_IMAGE_SIZE, VENDOR_APPLE, VENDOR_SPOTIFY, Service, VendorBitMask from src.db.base import session from src.legacy.core.client_base import APIInvalidResponse, BaseInternalAPIClient class VendorAPI(BaseInternalAPIClient): """Object oriented interface to Vendor API. It provides methods for accessing the API, which prepare HTTP requests, parse and validate responses, maintain session etc. """ service_name = Service.VENDOR_API env_host_name = "VENDOR_HOSTNAME" env_app_key = "VENDOR_APPKEY" def _send_request(self, request: requests.Request, data_ttl: int or None = None) -> Union[List[dict], dict]: """Send HTTP request to Vendor API endpoint with ttl header. Args: request: Request to send. data_ttl: Cached data actuality timeout. Returns: dict: Response JSON content from Vendor API. """ if data_ttl is None: data_ttl = app.config["VENDOR_DEFAULT_DATA_TTL"] if data_ttl and data_ttl.isdigit(): ttl_header = {"X-Data-Timeout": data_ttl} if request.headers: request.headers.update(ttl_header) else: request.headers = ttl_header return super()._send_request(request) def _get_market(self, market: str, vendor: str) -> str: """Check market and return default if it is not valid. Args: market (str): Current market value. vendor (str): Vendor name (spotify or apple). Returns: str: Correct market code. """ if market and len(market) == 2: return market if vendor == VENDOR_SPOTIFY: return "" elif vendor == VENDOR_APPLE: return "us" else: raise NotImplementedError() @staticmethod def check_market_is_supported(market: str, vendor: str, by_isrc: bool) -> bool: """Check market is supported by vendor API. Args: market (str): Current market value. vendor (str): Vendor name (spotify or apple). by_isrc (bool): Pass data via ISRC list. Returns: bool: True if market is supported by vendor, False if not. Raises: BadRequest: if there is no market record with particular code in database. """ if not market or not by_isrc and vendor == VENDOR_SPOTIFY: # Market validation logic should work only for Spotify search endpoint, which is using when by_isrc is True return True vendors_flag = session.query(ApolloMarket.vendors).filter_by(code=market.lower()).scalar() if vendors_flag is None: raise BadRequest(f"Market {market} is not defined.") return VendorBitMask.get_mask(vendor).is_set(vendors_flag) def _get_images_base(self, ids: List[str], url: str, vendor: str, market: str, size: int = None) -> List[dict]: """Get images urls by list of ids. Args: ids: list of ids. url: url to get data from. vendor: 'apple' or 'spotify' market: Country 2 letter code or generic markets. Returns: List of image url data items. """ request_data = {"ids": ids, "market": self._get_market(market, vendor)} if size: request_data["image_size"] = size request = requests.Request("POST", url, json=request_data) return self._send_request(request) def get_track(self, track_id: str or int, vendor: str, market: str, raw_results: bool = False) -> dict: """Get track data from src.legacy.spotify/Apple. Args: track_id: Track ID. vendor (str): 'apple' or 'spotify'. market (str): Country 2 letter code or generic markets. raw_results (bool): Return results as is without post processing. Returns: Track data. """ if not raw_results: raise NotImplementedError() base_url = app.config["VENDOR_TRACKS_URL"][vendor] request = requests.Request("GET", f"{base_url}/{track_id}") request.params = [("market", self._get_market(market, vendor))] response_json = self._send_request(request) # for Apple even 1 song request returns {"data": [{...}]} if vendor == VENDOR_APPLE: response_json = response_json.get("data") if response_json: response_json = response_json[0] return response_json def get_tracks( self, track_ids: List[str] or List[int], vendor: str, market: str, image_size: int = DEFAULT_IMAGE_SIZE, by_isrc: bool = False, raw_results: bool = False, ) -> List[dict]: """Get tracks data. Args: track_ids: (List[str] or List[int]): A list of track ids or isrc. vendor (str): 'apple' or 'spotify'. market (str): Country 2 letter code or generic markets. image_size: desirable size of track image. For Spotify 64, 300, 640 are allowed only. For Apple if image_size is None, methods returns url image mask. by_isrc (bool): By track id or isrc list. raw_results (bool): Return results as is without post processing. Returns: List[dict]: List of track data items. Raises: APIInvalidResponse: If Vendor API returned response which cannot be handled. """ not_empty_ids = [str(_id) for _id in track_ids if _id] result = [] if not not_empty_ids: return result market = self._get_market(market, vendor) market_supported = self.check_market_is_supported(market, vendor, by_isrc) if market_supported is False: return result request = requests.Request("GET", app.config["VENDOR_TRACKS_URL"][vendor]) request.params = [("isrc" if by_isrc else "ids", ",".join(not_empty_ids)), ("market", market)] if vendor == VENDOR_APPLE: request.params.append(("include", "artists")) response_json = self._send_request(request) if raw_results: return response_json.get("tracks") or response_json.get("data") try: if vendor == VENDOR_APPLE: for track in response_json.get("data", []): image_url = track.get("attributes").get("artwork").get("url") if image_url and image_size: image_url = image_url.format(w=image_size, h=image_size) albums = track["relationships"].get("albums", {}).get("data", []) artists = track["relationships"].get("artists", {}).get("data", []) result.append( { "id": track.get("id"), "vendor": vendor, "isrc": track.get("attributes", {}).get("isrc"), "name": track.get("attributes", {}).get("name"), "release_date": track.get("attributes").get("releaseDate"), "image": image_url, "album_ids": [a["id"] for a in albums], "artists": [ {"id": artist.get("id"), "name": artist.get("attributes", {}).get("name")} for artist in artists ], } ) elif vendor == VENDOR_SPOTIFY: for track in response_json.get("tracks", []): result.append( { "id": track.get("id"), "vendor": vendor, "isrc": track.get("external_ids", {}).get("isrc"), "name": track.get("name"), "release_date": track.get("album", {}).get("release_date"), "image": next( ( image for image in track.get("album", {}).get("images", []) if image["height"] == image_size ), {}, ).get("url"), "album_ids": [track.get("album", {}).get("id")], "artists": [ {"id": artist.get("id"), "name": artist.get("name"), "uri": artist.get("uri")} for artist in track.get("artists", []) ], } ) else: raise NotImplementedError() except (KeyError, TypeError) as ex: raise APIInvalidResponse(exception=ex) return result def get_tracks_images(self, track_ids: List[str], vendor: str, market: str, size: int = None) -> List[dict]: return self._get_images_base( track_ids, app.config["VENDOR_TRACKS_IMAGES_URL"][vendor], vendor, market, size=size ) def get_playlists_images(self, playlists_ids: List[str], vendor: str, market: str, size: int = None) -> List[dict]: return self._get_images_base( playlists_ids, app.config["VENDOR_PLAYLISTS_IMAGES_URL"][vendor], vendor, market, size=size ) def get_albums(self, album_ids: List[str], vendor: str, market: str) -> List[dict]: """Get albums data. Args: album_ids (List[str]): (List[str]): List of album IDs. vendor (str): 'apple' or 'spotify'. market (str): Country 2 letter code or generic markets. Returns: List[dict]: List of album data items. Raises: APIInvalidResponse: If Vendor API returned response which cannot be handled. """ result = [] if not album_ids: return result request = requests.Request("GET", app.config["VENDOR_ALBUMS_URL"][vendor]) request.params = [("ids", ",".join(album_ids)), ("market", self._get_market(market, vendor))] response_json = self._send_request(request) try: if vendor == VENDOR_APPLE: for album in response_json["data"]: result.append({"id": album["id"], "name": album.get("attributes", {}).get("name")}) elif vendor == VENDOR_SPOTIFY: for album in response_json["albums"]: result.append( {"id": album["id"], "upc": album.get("external_ids", {}).get("upc"), "name": album["name"]} ) else: raise NotImplementedError() except (KeyError, TypeError) as ex: raise APIInvalidResponse(exception=ex) return result