import asyncio import logging from collections import defaultdict from http import HTTPStatus from itertools import chain from aiohttp import web from aiohttp_apispec import docs, json_schema, querystring_schema import config from server.apple.constants import US_MARKET from server.core import deserializers from server.core.constants import APPLE, GLOBAL_MARKET, SEARCH_ISRC, SPOTIFY, TRACKS_SEARCH_SUPPORTED_DSP, Service from server.core.utils import (DSP_TO_TRACK_ID_GETTER, DSP_TO_TRACKS_DATA_KEY, get_another_value, get_missing_items, get_track_search_id_type, get_tracks_search_market, handle_requests, search_tracks_extended) logger = logging.getLogger("app") async def lb_check(request: web.Request) -> web.Response: return web.Response(content_type="application/json") @docs(tags=["core"], summary="Check service health.") @querystring_schema(deserializers.HealthCheckParams) async def health(request: web.Request) -> web.Response: data = request["querystring"] include = [i.strip().lower() for i in data["include"]] points_to_check = { Service.APPLE: request.app["apple_api"].check_health, Service.CACHE: request.app["cache_backend"].check_health, Service.SPOTIFY: request.app["spotify_api"].check_health, } errors = [] tasks = {} for service_name, check_func in points_to_check.items(): if service_name not in include and Service.ALL not in include: continue tasks[service_name] = check_func() responses = await asyncio.gather(*tasks.values(), return_exceptions=True) for service_name, response in zip(tasks.keys(), responses): if isinstance(response, Exception): errors.append(dict(name=service_name, details=str(response))) elif not response: errors.append(dict(name=service_name)) if errors: return web.json_response(errors, status=HTTPStatus.BAD_GATEWAY) return web.Response(content_type="application/json") @docs(tags=["core"], summary="Make any request to Spotify or Apple API.") async def handle_other_requests(request: web.Request) -> web.Response: vendor, url = request.match_info.get("vendor"), request.match_info.get("endpoint") api = request.app["apple_api" if vendor == APPLE else "spotify_api"] kwargs = {key: ",".join(request.query.getall(key)) for key in request.query.keys()} result = await api.cached_get(url, **kwargs) return web.json_response(result) @docs(tags=["core"], summary="Get track data by ISRC from Spotify or Apple API.") @querystring_schema(deserializers.TrackIsrcSearchDeserializer) async def track_search_view(request: web.Request) -> web.Response: data = request["querystring"] isrc = data["isrc"] market = data.get("market") spotify_api = request.app["spotify_api"] spotify_data = await spotify_api.tracks([isrc], market, by_isrc=True) tracks = spotify_data.get("tracks", []) spotify_track = tracks[0] if tracks else {} if spotify_track: return web.json_response({"vendor": SPOTIFY, "track": spotify_track}) apple_api = request.app["apple_api"] apple_data = await apple_api.songs_by_isrc([isrc], storefront=market or US_MARKET) tracks = apple_data.get("data", []) apple_track = tracks[0] if tracks else {} if apple_track: return web.json_response({"vendor": APPLE, "track": apple_track}) return web.json_response({}) @docs(tags=["core"], summary="Get tracks data by ISRCs and/or known ids from Spotify or Apple API.") @json_schema(deserializers.TracksSearchV1Deserializer) async def tracks_search_v1_view(request: web.Request) -> web.Response: spotify_api, apple_api = request.app["spotify_api"], request.app["apple_api"] data = request["json"] requested_market = data.pop("market", None) # get all supported markets by dsp available_markets = await handle_requests(((spotify_api.markets, {}), (apple_api.storefronts, {}))) dsp_to_available_markets = {dsp: available_markets[i] for i, dsp in enumerate(TRACKS_SEARCH_SUPPORTED_DSP)} # check if requested market supported dsp_to_requested_market = { dsp: get_tracks_search_market(dsp, requested_market) for dsp in TRACKS_SEARCH_SUPPORTED_DSP } requested_market_supported_dsp = [ # subset of (apple, spotify) dsp for dsp, available in dsp_to_available_markets.items() if dsp_to_requested_market[dsp] in available ] initial_search_by_known_market = not requested_market_supported_dsp unavailable_isrc = [] # not available in requested market, but could be found in known one unresolved_isrc = [] # meta is unavailable at all, or search parameters could not being built # Initial search (by requested or known data) # create parameters collection for tracks searching requests dsp_to_id_type_to_market_to_tracks = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) for track in data["tracks"]: track_dsp = track["dsp"] if (track_dsp in requested_market_supported_dsp) or initial_search_by_known_market: # search by requested dsp search_dsp, search_id_types = track_dsp, config.DSP_TO_TRACK_SEARCH_ID_TYPES[track_dsp] else: # search by alternative dsp search_dsp, search_id_types = requested_market_supported_dsp[0], (SEARCH_ISRC,) search_market = ( dsp_to_requested_market[search_dsp] if requested_market_supported_dsp else get_tracks_search_market( search_dsp, track.get("known_market"), use_defaults_for_unsupported=config.SEARCH_MISSING_TRACKS_BY_DEFAULT_MARKET, dsp_to_available_markets=dsp_to_available_markets, ) ) if search_market is None: unresolved_isrc.append(track["isrc"]) logger.warning( f"tracks_search_v1_view Initial search could not resolve search market for track: {track}\n" f"and parameters: requested_markets={dsp_to_requested_market},\n" f"requested_market_supported_dsp={requested_market_supported_dsp},\n" f"search_missing_tracks_by_default_market={config.SEARCH_MISSING_TRACKS_BY_DEFAULT_MARKET}" ) continue if initial_search_by_known_market: unavailable_isrc.append(track["isrc"]) dsp_to_id_type_to_market_to_tracks[search_dsp][get_track_search_id_type(track, search_id_types)][ search_market ].append(track) results = await search_tracks_extended( dsp_to_id_type_to_market_to_tracks, apple_api=apple_api, spotify_api=spotify_api ) # Secondary search (by known data) # create parameters collection for missing tracks searching requests if config.SEARCH_MISSING_TRACKS_BY_KNOWN_MARKET and not initial_search_by_known_market: dsp_to_id_type_to_market_to_missing_tracks = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) result_idx = 0 for dsp, id_type_to_market_to_tracks in dsp_to_id_type_to_market_to_tracks.items(): result_data_key, result_item_id_getter = DSP_TO_TRACKS_DATA_KEY[dsp], DSP_TO_TRACK_ID_GETTER[dsp] for _, market_to_tracks in id_type_to_market_to_tracks.items(): for market, tracks in market_to_tracks.items(): missing_tracks = get_missing_items( tracks, results[result_idx][result_data_key], id_getter=lambda t: t["isrc"], id_result_getter=result_item_id_getter, ) for mt in missing_tracks: mt_dsp = mt["dsp"] mt_search_market = get_tracks_search_market( mt_dsp, market=mt.get("known_market"), use_defaults_for_unsupported=config.SEARCH_MISSING_TRACKS_BY_DEFAULT_MARKET, dsp_to_available_markets=dsp_to_available_markets, ) if mt_search_market is None: unresolved_isrc.append(mt["isrc"]) logger.warning( f"tracks_search_v1_view Secondary search could not resolve search market for track: " f"{mt}\nand parameters: requested_markets={dsp_to_requested_market},\n" f"requested_market_supported_dsp={requested_market_supported_dsp},\n" f"search_missing_tracks_by_default_market=" f"{config.SEARCH_MISSING_TRACKS_BY_DEFAULT_MARKET}" ) continue unavailable_isrc.append(mt["isrc"]) dsp_to_id_type_to_market_to_missing_tracks[mt_dsp][get_track_search_id_type(mt)][ mt_search_market ].append(mt) result_idx += 1 if dsp_to_id_type_to_market_to_missing_tracks: results_by_known_data = await search_tracks_extended( dsp_to_id_type_to_market_to_missing_tracks, apple_api=apple_api, spotify_api=spotify_api ) results = list(results) + list(results_by_known_data) response = {APPLE: [], SPOTIFY: []} for r in results: for dsp, data_key in DSP_TO_TRACKS_DATA_KEY.items(): data = r.get(data_key) if data: response[dsp].extend(data) break return web.json_response({"data": response, "unavailable": unavailable_isrc, "unresolved": unresolved_isrc})