import pytest from collections import defaultdict from datetime import date, datetime, timedelta from flask import url_for from http import HTTPStatus from typing import List from apollo_main_db.spotify.models import ViewPlaylistTypeEnum from src.legacy.spotify.constants import SPOTIFY_PLAYLIST_IMAGE_URL_MASK from tests.legacy.apollo_api import factories as apollo_factories from tests.legacy.spotify import factories as spotify_factories from tests.legacy.util import generate_view_playlists def get_hh_track(isrc: str = None, limit: int = 5, with_name: bool = False) -> List[dict]: def get_market(code): return { 1: "ph", 2: "tw", 3: "id", 4: "my", 5: "th", 6: "hk", 7: "sg", 8: "vn", 9: "jp", 10: "kr", }[code] return [ { "playlist_id": f"pl_{i}", "rank": i + 5, "market": get_market(i), "image_url": f"img_{i}", "region": "Asia", "index": i * 3, "isrc": isrc, "position": i * 2, "previous_position": (i * 2) + 3, "last_date": datetime.now() - timedelta(days=1), **({"name": f"plname_{i}"} if with_name else {}), } for i in range(2, limit + 2) ] @pytest.mark.parametrize( "params,track_data,expected_status,expected_result", ( ( {"isrc": "QZK6P2113983", "region": "as", "include": ["regions"]}, get_hh_track(isrc="QZK6P2113983", limit=3, with_name=False), HTTPStatus.OK, { "count": 3, "previous": None, "next": None, "items": [ { "playlist_id": "pl_2", "market": "tw", "top_market": 7, "isrc": "QZK6P2113983", "entry_date": (date.today() - timedelta(days=5)).strftime("%Y-%m-%d"), "region": "Asia", "position": 5, "previous_position": 8, "latest_date": (date.today() - timedelta(days=1)).strftime("%Y-%m-%d"), "peak_position": 5, "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_2.jpeg", "code": "tw", "name": "TW", "position_change": 3, "added_days": 4, "is_new": False, }, { "playlist_id": "pl_3", "market": "id", "top_market": 8, "isrc": "QZK6P2113983", "entry_date": (date.today() - timedelta(days=5)).strftime("%Y-%m-%d"), "region": "Asia", "position": 7, "previous_position": 10, "latest_date": (date.today() - timedelta(days=1)).strftime("%Y-%m-%d"), "peak_position": 7, "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_3.jpeg", "code": "id", "name": "ID", "position_change": 3, "added_days": 4, "is_new": False, }, { "playlist_id": "pl_4", "market": "my", "top_market": 9, "isrc": "QZK6P2113983", "entry_date": (date.today() - timedelta(days=5)).strftime("%Y-%m-%d"), "region": "Asia", "position": 9, "previous_position": 12, "latest_date": (date.today() - timedelta(days=1)).strftime("%Y-%m-%d"), "peak_position": 9, "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_4.jpeg", "code": "my", "name": "MY", "position_change": 3, "added_days": 4, "is_new": False, }, ], "positions_avg": 7, "positions_top_total": 3, "regions": [ {"name": "All markets", "code": "all", "total": 9, "placements": 3}, {"name": "Asia", "code": "as", "total": 6, "placements": 3}, ], "total": 6, }, ), ), ) def test_tracks_playlists_hot_hits_one( params, track_data, expected_status, expected_result, db_session, user_id, client, patch_auth_user ): patch_auth_user(user_id) entry_date = datetime.now() - timedelta(days=5) latest_date = datetime.now() - timedelta(days=1) apollo_factories.ApolloKeyValueStorageFactory.create( key="hot_hits_latest_ts", value=latest_date.strftime("%Y-%m-%dT%H:%M:%S") ) generate_view_playlists(track_data, ViewPlaylistTypeEnum.HH) for index, item in enumerate(track_data): spotify_factories.SpotifyHotHitsPlaylistTrackStatsFactory.create( isrc=item["isrc"], playlist_id=item["playlist_id"], latest_position=item["position"], previous_position=item["previous_position"], peak_position=item["position"], latest_date=latest_date, previous_date=latest_date, entry_date=entry_date, peak_date=latest_date, ) response = client.get(url_for("tracks_playlists.get_track_playlists_hot_hits", **params)) assert response.status_code == expected_status if expected_status == HTTPStatus.OK: assert response.json == expected_result @pytest.mark.parametrize( "isrc_count,playlist_count,items_count,current_date,dates_count", ( (2, 10, (7, 3), date(2020, 8, 18), 2), (3, 12, (6, 4, 1), date(2020, 8, 20), 4), (4, 6, (0, 0), date(2020, 8, 20), 2), (4, 6, (2, 4), date(2020, 8, 20), 2), (4, 6, (0, 0), date(2020, 8, 20), 2), ), ) def test_tracks_playlists_hot_hits_bulk( isrc_count: int, playlist_count: int, items_count: tuple, current_date: date, dates_count: int, db_session, client, patch_auth_key, ): def filter_playlist_data(data: dict) -> dict: return {k: v for k, v in data.items() if k != "latest_date"} apollo_factories.ApolloKeyValueStorageFactory.create( key="hot_hits_latest_ts", value=current_date.strftime("%Y-%m-%dT%H:%M:%S") ) isrc_list = [f"ISRC_{i}" for i in range(isrc_count)] playlist_ids = [f"PL_ID_{i}" for i in range(playlist_count)] playlist_data = { pl_id: { "playlist_id": pl_id, "market": f"m{i}", "rank": i, "latest_date": current_date - timedelta(days=i if i % 3 == 0 else 0), "region": f"region{i}", "playlist_image": SPOTIFY_PLAYLIST_IMAGE_URL_MASK.format(playlist_id=pl_id), } for i, pl_id in enumerate(playlist_ids) } expected_outdated = { i["playlist_id"]: filter_playlist_data(i) for i in playlist_data.values() if i["latest_date"] != current_date } actual_playlist_ids = [i for i in playlist_ids if i not in expected_outdated] expected_current = defaultdict(dict) expected_previous = defaultdict(dict) for isrc_index, pl_count in enumerate(items_count): isrc = isrc_list[isrc_index] for pl_index in range(pl_count): playlist_id = playlist_ids[pl_index] entry_date = current_date - timedelta(days=(isrc_index + pl_index) * 3) latest_date = current_date - timedelta(days=isrc_index if (isrc_index + pl_index) % 3 == 0 else 0) latest_position = isrc_index * 2 previous_position = ( None if (isrc_index + pl_index) % 4 == 0 else ( (isrc_index * 2) if (isrc_index + pl_index) % 3 == 0 and (isrc_index < 3 or isrc_index % 2 != 0) else (isrc_index + 3 * (-1 if isrc_index % 2 == 0 else 1)) ) ) peak_position = isrc_index * 2 - 10 if (pl_index + isrc_index) == 0 else 0 peak_date = current_date - timedelta(days=(pl_index + isrc_index) * 2) spotify_factories.SpotifyHotHitsPlaylistTrackStatsFactory.create( id=((isrc_index + 1) * 10 + pl_index + 1), isrc=isrc, playlist_id=playlist_id, entry_date=entry_date, latest_date=latest_date, latest_position=latest_position, previous_date=( None if (isrc_index + pl_index) % 4 == 0 else current_date - timedelta(days=1 + isrc_index) ), previous_position=previous_position, peak_position=peak_position, peak_date=peak_date, ) if playlist_id in actual_playlist_ids: data = { "playlist_id": playlist_id, "market": playlist_data[playlist_id]["market"], "rank": playlist_data[playlist_id]["rank"], "region": playlist_data[playlist_id]["region"], "entry_date": str(entry_date), "peak_position": peak_position + 1, "peak_date": str(peak_date), "playlist_image": playlist_data[playlist_id]["playlist_image"], } if latest_date == current_date: data.update( { "position": latest_position + 1, "is_entry": entry_date == latest_date, "trend": None if previous_position is None else (latest_position - previous_position), } ) expected_current[isrc][playlist_id] = data else: data["exit_date"] = str(latest_date) expected_previous[isrc][playlist_id] = data generate_view_playlists( [{"playlist_id": playlist_id, **playlist_data[playlist_id]} for playlist_id in playlist_ids], record_type=ViewPlaylistTypeEnum.HH, last_date_field="latest_date", add_extra=False, region_code_template="rc{}", ) expected_non_featured = defaultdict(list) for isrc in isrc_list: for playlist_id in actual_playlist_ids: if playlist_id not in expected_current[isrc] and playlist_id not in expected_previous[isrc]: expected_non_featured[isrc].append(filter_playlist_data(playlist_data[playlist_id])) url = url_for("tracks_playlists.get_tracks_playlists_hot_hits_bulk", isrc=",".join(isrc_list)) response = client.get(url) assert response.status_code == HTTPStatus.OK for item in playlist_data.values(): item["latest_date"] = str(item["latest_date"]) expected_result = { "items": [ { "isrc": isrc, "current": list(expected_current[isrc].values()), "previous": list(expected_previous[isrc].values()), "non_featured": expected_non_featured[isrc], "current_count": len(expected_current[isrc]), "avg_position": ( round(sum(i["position"] for i in expected_current[isrc].values()) / len(expected_current[isrc])) if expected_current[isrc] else None ), } for index, isrc in enumerate(isrc_list) ], "hot_hits_date": str(current_date), "playlists_by_markets": {i["market"]: i["playlist_id"] for i in playlist_data.values()}, "playlists": list(playlist_data.values()), "outdated": list(expected_outdated.values()), "regions": [i["region"] for i in playlist_data.values()], } def sort_data(data: dict) -> dict: def sort_items(item: dict, sort_key: str = "playlist_id"): for key, value in item.items(): if isinstance(value, list): item[key] = list(sorted(value, key=lambda x: x[sort_key])) data["items"] = list(sorted(data["items"], key=lambda x: x["isrc"])) data["playlists"] = list(sorted(data["playlists"], key=lambda x: x["playlist_id"])) data["outdated"] = list(sorted(data["outdated"], key=lambda x: x["playlist_id"])) data["regions"] = list(sorted(data["regions"])) for item in data["items"]: sort_items(item) return data assert sort_data(response.json) == sort_data(expected_result) def get_nmf_track(isrc: str = None, limit: int = 5, with_name: bool = False) -> List[dict]: def get_market(code): return { 1: "ph", 2: "tw", 3: "id", 4: "my", 5: "th", 6: "hk", 7: "sg", 8: "vn", 9: "jp", 10: "kr", }[code] return [ { "playlist_id": f"pl_{i}", "rank": i + 5, "market": get_market(i), "image_url": f"img_{i}", "dsp": "spotify", "region": "Asia", "index": i * 3, "isrc": isrc, **({"name": f"plname_{i}"} if with_name else {}), } for i in range(2, limit + 2) ] @pytest.mark.parametrize( "params,track_data,expected_status,expected_result", ( ({}, get_nmf_track(with_name=False), HTTPStatus.BAD_REQUEST, None), ( {"isrc": "GBAHT1800503"}, get_nmf_track(isrc="GBAHT1800503", limit=2, with_name=False), HTTPStatus.OK, { "count": 2, "previous": None, "next": None, "items": [ { "playlist_id": "pl_2", "top_market": 7, "position": 7, "code": "tw", "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_2.jpeg", "dsp": "spotify", "region": "as", "name": "TW", }, { "playlist_id": "pl_3", "top_market": 8, "position": 10, "code": "id", "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_3.jpeg", "dsp": "spotify", "region": "as", "name": "ID", }, ], "positions_avg": 8, "positions_top_total": 2, "total": 6, }, ), ( {"isrc": "GBAHT1800503", "include": ["inactive_markets"]}, get_nmf_track(isrc="GBAHT1800503", limit=2, with_name=False), HTTPStatus.OK, { "count": 2, "previous": None, "next": None, "items": [ { "playlist_id": "pl_2", "top_market": 7, "position": 7, "code": "tw", "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_2.jpeg", "dsp": "spotify", "region": "as", "name": "TW", }, { "playlist_id": "pl_3", "top_market": 8, "position": 10, "code": "id", "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_3.jpeg", "dsp": "spotify", "region": "as", "name": "ID", }, ], "positions_avg": 8, "positions_top_total": 2, "total": 2, }, ), ( {"isrc": "GBAHT1800503", "include": ["top_playlist_image_url", "regions"]}, get_nmf_track(isrc="GBAHT1800503", limit=3, with_name=False), HTTPStatus.OK, { "count": 3, "previous": None, "next": None, "items": [ { "playlist_id": "pl_2", "top_market": 7, "position": 7, "code": "tw", "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_2.jpeg", "dsp": "spotify", "region": "as", "name": "TW", }, { "playlist_id": "pl_3", "top_market": 8, "position": 10, "code": "id", "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_3.jpeg", "dsp": "spotify", "region": "as", "name": "ID", }, { "playlist_id": "pl_4", "top_market": 9, "position": 13, "code": "my", "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_4.jpeg", "dsp": "spotify", "region": "as", "name": "MY", }, ], "positions_avg": 10, "positions_top_total": 2, "top_playlist_image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_2.jpeg", "regions": [ {"code": "all", "name": "All markets", "total": 9, "placements": 3}, {"code": "as", "name": "Asia", "total": 6, "placements": 3}, ], "total": 9, }, ), ( {"isrc": "GBAHT1800503", "region": "as", "include": ["regions"]}, get_nmf_track(isrc="GBAHT1800503", limit=3, with_name=False), HTTPStatus.OK, { "count": 3, "previous": None, "next": None, "items": [ { "playlist_id": "pl_2", "top_market": 7, "position": 7, "code": "tw", "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_2.jpeg", "dsp": "spotify", "region": "as", "name": "TW", }, { "playlist_id": "pl_3", "top_market": 8, "position": 10, "code": "id", "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_3.jpeg", "dsp": "spotify", "region": "as", "name": "ID", }, { "playlist_id": "pl_4", "top_market": 9, "position": 13, "code": "my", "image_url": "https://dhfsk7jl2g0km.cloudfront.net/playlist-pl_4.jpeg", "dsp": "spotify", "region": "as", "name": "MY", }, ], "total": 6, "positions_avg": 10, "positions_top_total": 2, "regions": [ {"code": "all", "name": "All markets", "total": 9, "placements": 3}, {"code": "as", "name": "Asia", "total": 6, "placements": 3}, ], }, ), ), ) def test_tracks_playlists_nmf( params, track_data, expected_status, expected_result, db_session, user_id, client, patch_auth_user ): patch_auth_user(user_id) latest_date = date.today() - timedelta(days=1) playlist_data = [{"last_date": latest_date.strftime("%Y-%m-%d"), **item} for item in track_data] playlist_data.append({"playlist_id": "randomId", "market": "kr", "rank": 20}) generate_view_playlists( playlist_data, ViewPlaylistTypeEnum.NMF, add_extra="inactive_markets" not in params.get("include", []) ) for item in track_data: spotify_factories.SpotifyNewMusicFridayPlaylistTrackHistoryFactory.create( playlist_id=item["playlist_id"], isrc=item["isrc"], playlist_index=item["index"], date_id=item["index"] ) spotify_factories.SpotifyNewMusicFridayDateFactory.create(id=item["index"], date=latest_date) spotify_factories.SpotifyPlaylistFactory.create(id="randomId", name="Random Playlist") response = client.get(url_for("tracks_playlists.get_tracks_playlists_nmf", **params)) assert response.status_code == expected_status if expected_status == HTTPStatus.OK: assert response.json == expected_result