from datetime import date, datetime from typing import List, Optional, Union from unittest.mock import AsyncMock from apollo_utils.core.constants.dsp import DSP from apollo_utils.core.constants.market import Market from http import HTTPStatus import pytest from server.client.clients.apollo_api import ApolloApiClient from server.client.clients.dsp_api import DspApiClient from server.client.clients.vendor_api import VendorApiClient from server.config import CORE_IMAGE_SERVICE from server.constants import SPOTIFY_PLAYLIST_URI_PREFIX from server.constants.core_image_service import CoreImageEntityMapping, CoreImageByEntityMapping from server.constants.market import ISRC_MARKET_PREFIX_MAPPING from server.constants.playlists.tracks import AUDIO_FEATURES_MAPPING from server.constants.tracks import SPOTIFY_TRACK_URI_PREFIX COUNTRY_CODE_LIST = ("us", "ca", "gb", "nl", "se", "au") ISRC_PREFIX_LIST = list(ISRC_MARKET_PREFIX_MAPPING.keys()) def get_dsp(dsp: Optional[DSP], index: int) -> DSP: if dsp: return dsp return DSP.SPOTIFY if index % 2 else DSP.APPLE def get_playlist_id(dsp: DSP, index: int, as_uri: bool = False) -> str: if dsp.value == DSP.SPOTIFY.value: playlist_id = f"plid{index:02d}" elif dsp.value == DSP.APPLE.value: playlist_id = f"pl.id{index:02d}" else: raise NotImplementedError("Playlist ID: unknown DSP") return f"{SPOTIFY_PLAYLIST_URI_PREFIX}{playlist_id}" if as_uri else playlist_id def get_playlist_index(dsp: DSP, index: int, same_playlist: bool = True) -> int: return (int(dsp.value == DSP.SPOTIFY.value) + 1) if same_playlist else index def get_track_id(dsp: DSP, index: int, with_prefix: bool = False, as_uri: bool = False) -> Union[str, int]: if dsp.value == DSP.SPOTIFY.value: track_id = f"trid:{index:02d}" elif dsp.value == DSP.APPLE.value: track_id = 10000 + index else: raise NotImplementedError("Track ID: unknown DSP") if with_prefix: return f"{dsp.value}_{track_id}" elif as_uri: return f"{SPOTIFY_TRACK_URI_PREFIX}{track_id}" return track_id def get_track_id_list(dsp: DSP, count: int = 10) -> List[str]: return [get_track_id(dsp, i) for i in range(1, count + 1)] def get_upc(index: int) -> str: return f"upc{index:02d}" def get_upc_list(count: int = 10) -> List[str]: return [get_upc(i) for i in range(1, count + 1)] def get_isrc(index: int) -> str: return f"{ISRC_PREFIX_LIST[index % len(ISRC_PREFIX_LIST)]}BBCCDDEE{index:02d}" def get_album_id(dsp: DSP, index: int) -> Union[str, int]: if dsp.value == DSP.SPOTIFY.value: return f"alid:{index:02d}" elif dsp.value == DSP.APPLE.value: return 50000 + index raise NotImplementedError("Album ID: unknown DSP") def get_artist_id(dsp: DSP, index1: int, index2: int) -> Union[str, int]: if dsp.value == DSP.SPOTIFY.value: return f"arid:{index1:02d}" elif dsp.value == DSP.APPLE.value: return 2000 + index1 * 10 + index2 raise NotImplementedError("Artist ID: unknown DSP") def get_playlist(dsp: DSP, playlist_index: int, is_personalized: Optional[bool] = None) -> dict: dsp_data = {"name": dsp.value.capitalize(), "dsp_id": dsp.value, "slug": dsp.value} playlist_id = get_playlist_id(dsp, playlist_index) delphi_playlist_id = f"{dsp.value}_{playlist_id}" country_code = COUNTRY_CODE_LIST[(playlist_index - 1) % len(COUNTRY_CODE_LIST)] item_is_personalized = bool(playlist_index % 2) if is_personalized is None else is_personalized return { "name": f"Pl {playlist_index}", "playlist_id": delphi_playlist_id, "playlist_type": f"pl_type_{playlist_index}", "uri": f"spotify:playlist:{playlist_id}", "num_tracks": 10 + playlist_index, "country_code": country_code, "rank": 5 + playlist_index, "dsp": dsp_data, "dsp_playlist_id": playlist_id, "amazon_playlist_id": f"am_pl:{playlist_index}", "image": {"uri": f"https://url/playlists/{playlist_id}.img", "height": 300, "width": 300}, "description": f"Info {playlist_index}", "followers": 80000 + playlist_index, "is_ignored": bool(playlist_index % 3 == 0), "is_removed": bool(playlist_index % 4 == 0), "is_personalised": item_is_personalized, "is_public": bool(playlist_index % 3), "owner": { "username": f"owner_{playlist_index}", "display_name": f"Owner {playlist_index}", "owner_country_code": country_code, "owner_category": { "owner_category_id": str(playlist_index), "name": f"Cat {playlist_index}" }, "dsp": dsp_data, }, } def get_track(dsp: DSP, index: int) -> dict: track_id = get_track_id(dsp, index) return { "track_id": str(track_id), "name": f"Tr_{index}", "image_url": f"https://url/tracks/{get_track_id(dsp, index)}.img", "release_date": f"2021-06-0{index % 9 + 1}", "is_sony": bool(index % 5), "isrc": get_isrc(index), "artists": [ { "artist_id": str(get_artist_id(dsp, index, j)), "name": f"Ar_{index}_{j}", "url": f"https://url/artists/ar_{index}_{j}.img", } for j in range(1, min(index, 3)) ], "album": { "album_id": str(get_album_id(dsp, index)), "name": f"Al {index}", "image": f"https://url/albums/al_{index}.img", "upc": get_upc(index), "release_date": f"2021-11-1{index % 10}", }, **({"track_popularity": 50 + index} if dsp.value == DSP.SPOTIFY.value else {}), } def get_sony_upc_list(count: int = 5, *args) -> List[str]: if not args: args = [i * 2 for i in range(1, count + 1)] return [get_upc(index) for index in args] def get_audio_features(count: int = 6) -> dict: return { "audio_features": [ { "id": get_track_id(DSP.SPOTIFY, index), "danceability": 0.09 * index, "energy": 0.07 * index, "key": 0, "loudness": -0.56 * index, "mode": 0, "speechiness": 0.02 * index, "acousticness": 0.04 * index, "instrumentalness": 0.007 * index, "liveness": 0.009 * index, "valence": 0.08 * index, "tempo": 2.07 * index, } for index in range(1, count + 1) ], } def get_tracklist( streams_country_code_list: List[str] = None, dsp: Optional[DSP] = None, same_playlist: bool = True, item_list: List[int] = None, count: int = 10, collection_name: str = "items", is_personalized: Optional[bool] = None, result_data: Optional[dict] = None, is_current: bool = False, ) -> dict: if not item_list: item_list = list(range(1, count + 1)) result = [] for index in item_list: dsp = get_dsp(dsp, index) playlist_index = get_playlist_index(dsp, index, same_playlist) delphi_playlist_id = f"{dsp.value}_{get_playlist_id(dsp, playlist_index)}" is_top_track = (index + 1) % 3 is_not_new = index % 4 item_is_personalized = bool(playlist_index % 2) if is_personalized is None else is_personalized previous_position = ( (10 + index * (-1 if index % 3 else 1)) if not item_is_personalized and is_not_new and is_top_track else None ) result.append( { "playlist_id": delphi_playlist_id, "country_code": COUNTRY_CODE_LIST[(playlist_index - 1) % len(COUNTRY_CODE_LIST)], "isrc": get_isrc(index), ("track_id" if is_current else "dsp_track_id"): f"{dsp.value}_{get_track_id(dsp, index)}", f"{dsp.value}_track": get_track(dsp, index), "earliest_position_date_time": f"2022-04-0{index}T11:08:52.123Z", "earliest_position": 19 + index, "last_added_date_time": f"2022-11-0{index}T14:35:26.105Z", "current": 10 + index, "is_top_track_for_isrc": is_top_track, "previous_position_change_14_days": ( previous_position - 1 if previous_position is not None and is_not_new else None ), "last_date_change_14_days": f"2022-11-1{index - 1}T16:37:41.205Z" if is_not_new else None, "trend_change_14_days": (index + index * (-1 if index % 3 else 1)) if is_not_new else None, "num_days_on": (5 + index) if index % 3 else 0, "streaming_info": { country_code: { "1": {"isrc_in_playlist": 50 + index * 5 + j, "playlist": 200 + index * 10 + j}, "7": {"isrc_in_playlist": 250 + index * 10 + j, "playlist": 5000 + index * 100 + j}, "14": {"isrc_in_playlist": 450 + index * 15 + j, "playlist": 10000 + index * 200 + j}, } for j, country_code in enumerate(streams_country_code_list, 1) } if streams_country_code_list else None, **({} if is_current else {"playlist": get_playlist(dsp, playlist_index, is_personalized)}), "num_tracks": 10 + playlist_index, } ) return {collection_name: result, **(result_data or {})} def get_dsp_tracklist_result( dsp: DSP, item_list: List[int] = None, count: int = 10, result_data: Optional[dict] = None, ) -> dict: entity = "track" if not item_list: item_list = list(range(1, count + 1)) result = [] for index in item_list: dsp = get_dsp(dsp, index) track_id = get_track_id(dsp, index) item_is_personalized = bool(get_playlist_index(dsp, index, True) % 2) has_previous = not item_is_personalized and index % 4 and (index + 1) % 3 image_url = f"https://url/tracks/{track_id}.img" \ if dsp.value == DSP.APPLE.value \ else f"{CORE_IMAGE_SERVICE}{CoreImageEntityMapping[entity]}/{CoreImageByEntityMapping[entity][dsp.value]}" \ f"/{track_id}?resolution=large" result_item = { "current_position": 11 + index, "added_date": f"2022-04-0{str(index)[:1]}", "name": f"Tr_{index}", "isrc": get_isrc(index), "artists": [f"Ar_{index}_{j}" for j in range(1, min(index, 3))], "image_url": image_url, "previous_position": (10 + index * (-1 if index % 3 else 1)) if has_previous else None, "trend": (index + index * (-1 if index % 3 else 1)) if has_previous else None, "position_change_date": f"2022-11-1{index - 1}" if has_previous else None, "is_new": bool(index % 3 == 0 and has_previous), } if dsp.value == DSP.SPOTIFY.value: result_item.update( { "track_id": track_id, "album_id": get_album_id(dsp, index), "update_date": datetime.utcnow().date().isoformat(), } ) else: result_item["track_id"] = track_id _utc_now = datetime.utcnow().date() result_item["is_new"] = result_item["is_new"] and \ datetime.fromisoformat(result_item["added_date"][:19]).date() == _utc_now result.append(result_item) result = sorted(result, key=lambda i: (i["added_date"], i["current_position"])) return {"items": result, **(result_data or {})} def get_apple_tracks_result( streams_country_code_list: List[str] = None, dsp: Optional[DSP] = None, item_list: List[int] = None, count: int = 10, is_personalized: Optional[bool] = None, result_country_code: Optional[str] = None, is_current: bool = False, ) -> dict: entity = "track" if not item_list: item_list = list(range(1, count + 1)) result = [] for index in item_list: dsp = get_dsp(dsp, index) track_id = get_track_id(dsp, index) result_item = { "id": track_id, "storefront": result_country_code, "artworkUrl": f"https://url/tracks/{track_id}.img", "albumId": get_album_id(dsp, index), "albumName": f"Al {index}", "artistId": get_artist_id(dsp, index, 1) if index > 1 else None, "artistIds": [get_artist_id(dsp, index, j) for j in range(1, min(index, 3))], "artistName": f"Ar_{index}_1" if index > 1 else None, "releaseDate": f"2021-06-0{index % 9 + 1}", "name": f"Tr_{index}", "isrc": get_isrc(index), ("addedDate" if is_current else "added"): f"2022-11-0{index}T14:35:26.105Z", "albumReleaseDate": f"2021-11-1{index % 10}", "position": 11 + index, "marketStreams": ( ((251 if is_current else 51) + index * (10 if is_current else 5)) if streams_country_code_list else None ), } if is_current: item_is_personalized = ( bool(get_playlist_index(dsp, index, True) % 2) if is_personalized is None else is_personalized ) has_previous = not item_is_personalized and index % 4 and (index + 1) % 3 result_item.update( { "earliestAdded": f"2022-04-0{index}T11:08:52.123Z", "earliestPosition": 20 + index, "positionChange": (index + index * (-1 if index % 3 else 1)) if has_previous else None, "positionChangeDate": f"2022-11-1{index - 1}T16:37:41.205Z" if has_previous else None, "isEntry": bool(index % 3 == 0 and has_previous), } ) result.append(result_item) return {"tracklist": result} def get_spotify_tracks_result( streams_country_code_list: List[str], dsp: DSP, item_list: List[int] = None, count: int = 10, is_personalized: Optional[bool] = None, is_current: bool = False, ) -> dict: if not item_list: item_list = list(range(1, count + 1)) entity = "track" result = [] for index in item_list: dsp = get_dsp(dsp, index) track_id = get_track_id(dsp, index) result_item = { "trackUri": f"{SPOTIFY_TRACK_URI_PREFIX}{track_id}", "artistName": ",".join(f"Ar_{index}_{j}" for j in range(1, min(index, 3))), "isSony": bool(index % 2 == 0), "distributed_by": "sme" if bool(index % 2 == 0) else None, "albumImageUrl": f"{CORE_IMAGE_SERVICE}{CoreImageEntityMapping[entity]}/" f"{CoreImageByEntityMapping[entity][dsp.value]}/{track_id}", "artists": [ {"name": f"Ar_{index}_{j}", "uri": f"https://url/artists/ar_{index}_{j}.img"} for j in range(1, min(index, 3)) ], "earliestAdded": f"2022-04-0{index}T11:08:52.123Z", "globalStreams": ( ((252 if is_current else 52) + index * (10 if is_current else 5)) if streams_country_code_list else None ), "name": f"Tr_{index}", "isrc": get_isrc(index), ("addedDate" if is_current else "added"): f"2022-11-0{index}T14:35:26.105Z", "albumReleaseDate": f"2021-11-1{index % 10}", "position": 11 + index, "marketStreams": ( ((251 if is_current else 51) + index * (10 if is_current else 5)) if streams_country_code_list else None ), "popularity": 50 + index, } if is_current: item_is_personalized = ( bool(get_playlist_index(dsp, index, True) % 2) if is_personalized is None else is_personalized ) has_previous = not item_is_personalized and index % 4 and (index + 1) % 3 result_item.update( { "positionChange": (index + index * (-1 if index % 3 else 1)) if has_previous else None, "positionChangeDate": f"2022-11-1{index - 1}T16:37:41.205Z" if has_previous else None, "isEntry": bool(index % 3 == 0 and has_previous), } ) else: result_item["albumName"] = f"Al {index}" if index <= 6: result_item.update( { "danceability": 0.09 * index, "energy": 0.07 * index, "audioKey": 0, "loudness": -0.56 * index, "audioMode": 0, "speechiness": 0.02 * index, "acousticness": 0.04 * index, "instrumentalness": 0.007 * index, "liveness": 0.009 * index, "valence": 0.08 * index, "tempo": 2.07 * index, } ) else: result_item.update({key: None for key in AUDIO_FEATURES_MAPPING.values()}) result.append(result_item) return {"tracks" if is_current else "items": result} @pytest.mark.parametrize( "country_code_list,is_personalized,params,status_code", ( ([], False, {}, HTTPStatus.BAD_REQUEST), ([], False, {"country": "us"}, HTTPStatus.BAD_REQUEST), ([], False, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "country": "us"}, HTTPStatus.OK), (["us"], False, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "country": "us"}, HTTPStatus.OK), ( ["worldwide"], False, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "country": "worldwide"}, HTTPStatus.OK, ), ([], True, {"playlist_id": get_playlist_id(DSP.APPLE, 1)}, HTTPStatus.OK), ), ) async def test_get_playlists_apple_tracks_current( country_code_list: Optional[List[str]], is_personalized: Optional[bool], params: dict, status_code: HTTPStatus, mocker, client, auth, ): playlist_id, country_code = params.get("playlist_id"), params.get("country") async_dsp_mock = AsyncMock( return_value=get_tracklist(country_code_list, DSP.APPLE, is_personalized=is_personalized, is_current=True) ) dsp_send_mocked = mocker.patch.object(DspApiClient, "send_request", side_effect=async_dsp_mock) response = await client.get("/api/v1/playlists/apple/tracks/", params=params, headers=auth) assert response.status == status_code if status_code != HTTPStatus.OK: return if country_code: dsp_send_mocked.assert_called_once_with( "api/delphi/public/track-positions/playlists", params={ "playlist_id": f"{DSP.APPLE.value}_{playlist_id}", "dsp": DSP.APPLE.value, "country_code": [country_code] if country_code else None, "streams_country_code": [country_code] if country_code else None, "include": ["track_info", "streams_for_period"], "end_date": None, "expand_isrc_by_track_positions": True, "isrc": None, "min_added_date": None, "playlist_name_contains": None, "playlist_owner_category_id": None, "sort_by": None, "sort_order": None, "start_date": None, "apple_country_code": None, }, ) else: assert dsp_send_mocked.call_count == 0 response = await response.json() assert response == get_apple_tracks_result( country_code_list, DSP.APPLE, is_personalized=is_personalized, count=10 if country_code else 0, result_country_code=country_code, is_current=True, ) @pytest.mark.parametrize( "country_code_list,is_personalized,params,status_code", ( ([], False, {}, HTTPStatus.BAD_REQUEST), ([], False, {"country": "us"}, HTTPStatus.BAD_REQUEST), ([], False, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "country": "us"}, HTTPStatus.BAD_REQUEST), ( [], False, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "country": "us", "date": "2022-06-01"}, HTTPStatus.OK, ), ( ["us"], False, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "country": "us", "date": "2022-06-01"}, HTTPStatus.OK, ), ( ["worldwide"], True, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "country": "worldwide", "date": "2022-06-01"}, HTTPStatus.OK, ), ([], True, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "date": "2022-06-01"}, HTTPStatus.OK), ), ) async def test_get_playlists_apple_tracks_history( country_code_list: Optional[List[str]], is_personalized: Optional[bool], params: dict, status_code: HTTPStatus, mocker, client, auth, ): playlist_id, tracklist_date, country_code = params.get("playlist_id"), params.get("date"), params.get("country") tracklist_date = date.fromisoformat(tracklist_date) if tracklist_date else None async_dsp_mock = AsyncMock( return_value=get_tracklist(country_code_list, DSP.APPLE, is_personalized=is_personalized, is_current=False) ) dsp_send_mocked = mocker.patch.object(DspApiClient, "send_request", side_effect=async_dsp_mock) response = await client.get("/api/v1/playlists/apple/tracks/history/", params=params, headers=auth) assert response.status == status_code if status_code != HTTPStatus.OK: return if country_code: dsp_send_mocked.assert_called_once_with( "api/delphi/public/track-positions/playlists", params={ "start_date": tracklist_date, "end_date": tracklist_date, "expand_isrc_by_track_positions": True, "playlist_id": f"{DSP.APPLE.value}_{playlist_id}", "isrc": None, "min_added_date": None, "dsp": DSP.APPLE.value, "playlist_name_contains": None, "playlist_owner_category_id": None, "sort_by": None, "sort_order": None, "country_code": [country_code] if country_code else None, "streams_country_code": [country_code] if country_code else None, "include": ["playlists", "track_info", "streams_for_period"], "apple_country_code": None, }, ) else: assert dsp_send_mocked.call_count == 0 response = await response.json() assert response == get_apple_tracks_result( country_code_list, DSP.APPLE, is_personalized=is_personalized, count=10 if country_code else 0, result_country_code=country_code, is_current=False, ) @pytest.mark.parametrize( "country_code_list,is_personalized,params,status_code", ( ([], False, {}, HTTPStatus.BAD_REQUEST), ([], False, {"region": "us"}, HTTPStatus.BAD_REQUEST), ([], False, {"playlistUri": get_playlist_id(DSP.SPOTIFY, 2), "region": "us"}, HTTPStatus.OK), ( ["us", Market.WORLDWIDE], False, {"playlistUri": get_playlist_id(DSP.SPOTIFY, 2, as_uri=True), "region": "us"}, HTTPStatus.OK, ), ( ["gb", "worldwide"], True, {"playlistUri": get_playlist_id(DSP.SPOTIFY, 2), "region": "gb"}, HTTPStatus.OK, ), ( ["local", "worldwide"], True, {"playlistUri": get_playlist_id(DSP.SPOTIFY, 2, as_uri=True)}, HTTPStatus.OK, ), ), ) async def test_get_playlists_spotify_tracks_current( country_code_list: Optional[List[str]], is_personalized: Optional[bool], params: dict, status_code: HTTPStatus, mocker, client, auth, ): playlist_id, country_code = params.get("playlistUri"), params.get("region") playlist_id = playlist_id.replace(SPOTIFY_PLAYLIST_URI_PREFIX, "") if playlist_id else None async def async_dsp_mock(url, *args, **kwargs): if url == "api/delphi/public/track-positions/playlists/current-tracklist": return get_tracklist( country_code_list, DSP.SPOTIFY, is_personalized=is_personalized, is_current=True ) elif "api/delphi/public/playlists/" in url: playlist_index = int(url.replace("api/delphi/public/playlists/spotify_plid", "")) return get_playlist(DSP.SPOTIFY, playlist_index, is_personalized) dsp_send_mocked = mocker.patch.object(DspApiClient, "send_request", side_effect=async_dsp_mock) async_vendor_mock = AsyncMock(return_value=get_audio_features()) get_audio_features_mock = mocker.patch.object(VendorApiClient, "send_request", side_effect=async_vendor_mock) async_apollo_mock = AsyncMock(return_value=get_sony_upc_list()) is_sony_mocked = mocker.patch.object(ApolloApiClient, "send_request", side_effect=async_apollo_mock) response = await client.get("/api/v1/playlists/spotify/tracks/", params=params, headers=auth) assert response.status == status_code if status_code != HTTPStatus.OK: return assert dsp_send_mocked.call_count == 2 assert dsp_send_mocked.call_args_list[0] == ( ("api/delphi/public/track-positions/playlists/current-tracklist",), dict( params={ "playlist_id": f"{DSP.SPOTIFY.value}_{playlist_id}", "dsp": DSP.SPOTIFY.value, "streams_country_code": [country_code if country_code else "local", Market.WORLDWIDE], "include": ["track_info", "streams_for_period"], "country_code": None, } ), ) assert dsp_send_mocked.call_args_list[1] == (("api/delphi/public/playlists/spotify_plid02",), dict(params={})) get_audio_features_mock.call_args.kwargs["params"]["ids"] = list( sorted(get_audio_features_mock.call_args.kwargs["params"]["ids"]) ) get_audio_features_mock.assert_called_once_with( "api/spotify/v1/audio-features", params={"ids": get_track_id_list(DSP.SPOTIFY)} ) is_sony_mocked.call_args.kwargs["params"]["upc"] = list(sorted(is_sony_mocked.call_args.kwargs["params"]["upc"])) is_sony_mocked.assert_called_once_with("api/albums/is-sony/", params=dict(upc=get_upc_list(), market=country_code or "global")) response = await response.json() assert response == get_spotify_tracks_result( country_code_list, DSP.SPOTIFY, is_personalized=is_personalized, is_current=True, ) @pytest.mark.parametrize( "country_code_list,is_personalized,params,status_code", ( ([], False, {}, HTTPStatus.BAD_REQUEST), ([], False, {"region": "us"}, HTTPStatus.BAD_REQUEST), ([], False, {"playlistUri": get_playlist_id(DSP.SPOTIFY, 2), "region": "us"}, HTTPStatus.BAD_REQUEST), ( [], False, {"playlistUri": get_playlist_id(DSP.SPOTIFY, 2), "region": "us", "date": "2022-08-01"}, HTTPStatus.OK, ), ( ["us", Market.WORLDWIDE], False, {"playlistUri": get_playlist_id(DSP.SPOTIFY, 2, as_uri=True), "region": "us", "date": "2022-08-01"}, HTTPStatus.OK, ), ( ["gb", "worldwide"], True, {"playlistUri": get_playlist_id(DSP.SPOTIFY, 2), "region": "gb", "date": "2022-08-01"}, HTTPStatus.OK, ), ( ["local", "worldwide"], True, {"playlistUri": get_playlist_id(DSP.SPOTIFY, 2, as_uri=True), "date": "2022-08-01"}, HTTPStatus.OK, ), ), ) async def test_get_playlists_spotify_tracks_history( country_code_list: Optional[List[str]], is_personalized: Optional[bool], params: dict, status_code: HTTPStatus, mocker, client, auth, ): playlist_id, tracklist_date, country_code = params.get("playlistUri"), params.get("date"), params.get("region") playlist_id = playlist_id.replace(SPOTIFY_PLAYLIST_URI_PREFIX, "") if playlist_id else None tracklist_date = date.fromisoformat(tracklist_date) if tracklist_date else None async_dsp_mock = AsyncMock( return_value=get_tracklist(country_code_list, DSP.SPOTIFY, is_personalized=is_personalized, is_current=False) ) dsp_send_mocked = mocker.patch.object(DspApiClient, "send_request", side_effect=async_dsp_mock) vendor_mock = AsyncMock(return_value=get_audio_features()) get_audio_features_mock = mocker.patch.object(VendorApiClient, "send_request", side_effect=vendor_mock) async_apollo_mock = AsyncMock(return_value=get_sony_upc_list()) is_sony_mocked = mocker.patch.object(ApolloApiClient, "send_request", side_effect=async_apollo_mock) response = await client.get("/api/v1/playlists/spotify/tracks/history/", params=params, headers=auth) assert response.status == status_code if status_code != HTTPStatus.OK: return dsp_send_mocked.assert_called_once_with( "api/delphi/public/track-positions/playlists", params={ "start_date": tracklist_date, "end_date": tracklist_date, "expand_isrc_by_track_positions": True, "isrc": None, "min_added_date": None, "playlist_name_contains": None, "playlist_owner_category_id": None, "sort_by": None, "sort_order": None, "playlist_id": f"{DSP.SPOTIFY.value}_{playlist_id}", "dsp": DSP.SPOTIFY.value, "streams_country_code": [country_code if country_code else "local", Market.WORLDWIDE], "include": ["playlists", "track_info", "streams_for_period"], "country_code": None, "apple_country_code": None, }, ) get_audio_features_mock.call_args.kwargs["params"]["ids"] = list( sorted(get_audio_features_mock.call_args.kwargs["params"]["ids"]) ) get_audio_features_mock.assert_called_once_with( "api/spotify/v1/audio-features", params={"ids": get_track_id_list(DSP.SPOTIFY)} ) is_sony_mocked.call_args.kwargs["params"]["upc"] = list(sorted(is_sony_mocked.call_args.kwargs["params"]["upc"])) is_sony_mocked.assert_called_once_with("api/albums/is-sony/", params=dict(upc=get_upc_list(), market=country_code or "global")) response = await response.json() assert response == get_spotify_tracks_result( country_code_list, DSP.SPOTIFY, is_personalized=is_personalized, is_current=False ) @pytest.mark.parametrize( "dsp,params,status_code,index_list", ( (DSP.SPOTIFY, {}, HTTPStatus.BAD_REQUEST, None), (DSP.SPOTIFY, {"country_code": "us"}, HTTPStatus.BAD_REQUEST, None), (DSP.SPOTIFY, {"playlist_id": get_playlist_id(DSP.SPOTIFY, 2), "country_code": "us"}, HTTPStatus.OK, None), (DSP.SPOTIFY, {"playlist_id": get_playlist_id(DSP.SPOTIFY, 2), "search": "Ar_3_1,Ar_3_2"}, HTTPStatus.OK, [3]), (DSP.SPOTIFY, {"playlist_id": get_playlist_id(DSP.SPOTIFY, 2), "search": "Tr_1"}, HTTPStatus.OK, [1, 10]), (DSP.SPOTIFY, {"playlist_id": get_playlist_id(DSP.SPOTIFY, 2), "search": "Tr"}, HTTPStatus.OK, None), ( DSP.SPOTIFY, {"playlist_id": get_playlist_id(DSP.SPOTIFY, 2), "search": "Tr_2", "source_track_isrc": "FXBBCCDDEE04"}, HTTPStatus.OK, [2, 4], ), (DSP.APPLE, {}, HTTPStatus.BAD_REQUEST, None), (DSP.APPLE, {"country_code": "us"}, HTTPStatus.BAD_REQUEST, None), (DSP.APPLE, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "country_code": "us"}, HTTPStatus.OK, None), (DSP.APPLE, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "search": "Ar_3_1,Ar_3_2"}, HTTPStatus.OK, [3]), (DSP.APPLE, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "search": "Tr_1"}, HTTPStatus.OK, [1, 10]), (DSP.APPLE, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "search": "Tr"}, HTTPStatus.OK, None), ( DSP.APPLE, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "search": "Tr_2", "source_track_isrc": "FXBBCCDDEE04"}, HTTPStatus.OK, [2, 4], ), ), ) async def test_get_playlists_dsp_tracklist( dsp: DSP, params: dict, status_code: HTTPStatus, index_list: List[int], mocker, client, auth ): playlist_id, country_code = params.get("playlist_id"), params.get("region") async def async_dsp_mock(url, *args, **kwargs): if url == "api/delphi/public/track-positions/playlists/current-tracklist": return get_tracklist(dsp=dsp, is_current=True) elif "api/delphi/public/playlists/" in url: return get_playlist(dsp, 2 if dsp.value == DSP.SPOTIFY.value else 1) dsp_send_mocked = mocker.patch.object(DspApiClient, "send_request", side_effect=async_dsp_mock) response = await client.get(f"/api/v1/playlists/{dsp.value}/tracklist/", params=params, headers=auth) assert response.status == status_code if status_code != HTTPStatus.OK: return assert dsp_send_mocked.call_count == 2 assert dsp_send_mocked.call_args_list[0] == ( ("api/delphi/public/track-positions/playlists/current-tracklist",), dict( params={ "playlist_id": f"{dsp.value}_{playlist_id}", "dsp": dsp.value, "include": ["track_info"], "streams_country_code": None, "country_code": ["us"] if dsp == DSP.APPLE else None, } ), ) assert dsp_send_mocked.call_args_list[1] == ( (f"api/delphi/public/playlists/{dsp.value}_{playlist_id}",), dict(params={}) ) response = await response.json() assert response == get_dsp_tracklist_result( dsp=dsp, result_data={"count": len(index_list) if index_list else 10, "next": None, "previous": None}, item_list=index_list, ) @pytest.mark.parametrize( "dsp,params,status_code,expected_result", ( (DSP.SPOTIFY, {}, HTTPStatus.BAD_REQUEST, {}), ( DSP.SPOTIFY, {"playlist_id": get_playlist_id(DSP.SPOTIFY, 2)}, HTTPStatus.OK, {"frontline_tracks": 7, "local_tracks": 0, "sony_tracks": 5, "tracks_num": 10}, ), ( DSP.SPOTIFY, {"playlist_id": get_playlist_id(DSP.SPOTIFY, 2), "market": "us"}, HTTPStatus.OK, {"frontline_tracks": 7, "local_tracks": 3, "sony_tracks": 5, "tracks_num": 10}, ), (DSP.APPLE, {}, HTTPStatus.BAD_REQUEST, {}), ( DSP.APPLE, {"playlist_id": get_playlist_id(DSP.APPLE, 1)}, HTTPStatus.OK, {"frontline_tracks": 5, "local_tracks": 3, "tracks_num": 10}, ), ( DSP.APPLE, {"playlist_id": get_playlist_id(DSP.APPLE, 1), "market": "fr"}, HTTPStatus.OK, {"frontline_tracks": 5, "local_tracks": 1, "tracks_num": 10}, ), ), ) async def test_get_playlists_tracklist_graph( dsp: DSP, params: dict, status_code: HTTPStatus, expected_result: dict, mocker, auth, client ): playlist_id, country_code = ( params.get("playlist_id"), params.get("market") or (Market.WORLDWIDE if dsp.value == DSP.SPOTIFY.value else Market.US), ) async_apollo_mock = AsyncMock(return_value=get_sony_upc_list()) is_sony_mocked = mocker.patch.object(ApolloApiClient, "send_request", side_effect=async_apollo_mock) async_dsp_mock = AsyncMock(return_value=get_tracklist(dsp=dsp)) dsp_send_mocked = mocker.patch.object(DspApiClient, "send_request", side_effect=async_dsp_mock) date_mock = mocker.patch(f"server.api.playlists.{dsp.value}.tracklist_graph.date") date_mock.today.return_value = date(2024, 5, 13) if dsp.value == DSP.SPOTIFY.value else date(2023, 12, 5) response = await client.get(f"/api/v1/playlists/{dsp.value}/tracklist-graph/", params=params, headers=auth) assert response.status == status_code if status_code != HTTPStatus.OK: return dsp_send_mocked.assert_called_once_with( "api/delphi/public/track-positions/playlists/current-tracklist", params={ "country_code": None if dsp.value == DSP.SPOTIFY.value else country_code, "dsp": dsp.value, "include": ["track_info"], "playlist_id": f"{dsp.value}_{playlist_id}", 'streams_country_code': None, }, ) if dsp.value == DSP.SPOTIFY.value: is_sony_mocked.call_args.kwargs["params"]["upc"] = list( sorted(is_sony_mocked.call_args.kwargs["params"]["upc"]) ) is_sony_mocked.assert_called_once_with( "api/albums/is-sony/", params=dict(upc=get_upc_list(), market=country_code) ) else: assert is_sony_mocked.called == 0 response = await response.json() assert response == expected_result