import asyncio from collections import defaultdict from datetime import date from marshmallow import Schema from typing import Dict, List, Type from server import config from server.cache.utils import cached from server.client import services from server.constants import DSP from server.constants.charts import COUNTRY_CODE_SORTING_KEY, ChartBreakdown, ChartType, SourceType, TrackStateIncludes from server.domains.charts.misc import fix_weekly from server.schemas.charts.summary import ChartTrackSummary from server.utils.charts.summary import get_charts_summary_top_market from server.utils.pagination import multikeysort from server.utils.parallel import get_awaitable_or_default @cached(ttl=config.CHARTS_CACHE_TLL) async def get_stats( dsp: DSP, isrc_list: List[str], chart_type: ChartType, chart_breakdown: ChartBreakdown, market: str, schema: Type[Schema], ) -> List[dict]: """Get chart tracks stats. Args: dsp: DSP code (Apple or Spotify). isrc_list: ISRC list. chart_type: Regional or viral. chart_breakdown: Daily or weekly. market: Market code. schema: Schema to dump data before caching. Returns: Tracks stats. """ result, chart_date = await asyncio.gather( *[ services.dsp.get_tracks_charts_lifetime( dsp=dsp, isrc=isrc_list, chart_breakdown=[chart_breakdown.value], chart_type=[chart_type.value], chart_country_code=[market], track_state_includes=[TrackStateIncludes.LIFETIME_METRICS.value, TrackStateIncludes.PUBLIC_META.value], ), services.dsp.get_charts_latest_date( dsp, chart_breakdown=chart_breakdown.value, chart_type=chart_type.value, country_code=market ), ] ) result = list({i["public_meta"]["isrc"]: i for i in result}.values()) missing_isrc = set(isrc_list) - set(i["public_meta"]["isrc"] for i in result) for isrc in missing_isrc: result.append( { "public_meta": {"isrc": isrc}, "lifetime_metrics": { "earliest_position_date": None, "min_position": None, "latest_position_date": None, "latest_position": None, }, } ) if chart_date: chart_date = chart_date.isoformat() return schema(many=True, context={"date": chart_date}).dump(result) async def get_apple_data_health_market_last_update_date() -> Dict[str, date]: """Get apple chart last_update_date_time from Del data-health endpoint country_code to date mapping""" chart_dates = await services.dsp.get_charts_latest_date_bulk( dsp=DSP.APPLE, chart_type=ChartType.REGIONAL.value, chart_breakdown=ChartBreakdown.DAILY.value, data_health_chart_updated_key="last_update_date_time", ) return {k.replace("charts_apple_", ""): v for k, v in chart_dates.items()} async def get_summary(dsp: DSP, isrc: str, search: str) -> dict: """Get chart tracks summary. Args: dsp: DSP code (Apple or Spotify). isrc: ISRC. search: Search text. Returns: Tracks stats. """ charts_data, markets_names, update_dates = await asyncio.gather( *[ services.dsp.get_tracks_charts( dsp=dsp, isrc=[isrc], chart_breakdown=[ChartBreakdown.DAILY.value], chart_type=[ChartType.REGIONAL.value], ), services.dsp.get_regions(), get_awaitable_or_default( f=get_apple_data_health_market_last_update_date, condition=dsp.value == DSP.APPLE.value, default={} ), ] ) markets_names = {i["country_code"]: i["country_name"] for i in markets_names} top_market = get_charts_summary_top_market(charts_data) if charts_data else None result_items = [] for item in sorted(charts_data, key=lambda i: i["chart_meta"]["country_code"]): market = item["chart_meta"]["country_code"] market_full_name = markets_names.get(market) if search and market_full_name and search not in market_full_name.lower(): continue metrics = item.get("metrics", {}) position, previous_position, is_entry = ( metrics.get("position"), metrics.get("previous_position"), metrics.get("is_entry"), ) added_date = item["lifetime_metrics"]["earliest_position_date"] current_date = metrics.get("date") if dsp.value == DSP.SPOTIFY.value else update_dates.get(market) rank = item.get("chart_meta", {}).get("rank") peak_position = item.get("lifetime_metrics", {}).get("min_position") result_items.append( { "date": current_date, "country_code": market, "country_name": market_full_name, "streams": metrics.get("date_streams"), "added_date": added_date, "position": position, "is_new": is_entry and added_date == current_date, "is_re_enter": is_entry and added_date != current_date, "change": ( None if is_entry or previous_position is None or position is None else previous_position - position ), "rank": rank, "peak_position": peak_position, } ) return { "top_market": top_market, "items": result_items, } async def get_summary_sorted(dsp: DSP, isrc: str, search: str, sort_by: str) -> dict: """Get chart track sorted summary. Args: dsp: Vendor code. isrc: ISRC code. search: Search text. sort_by: Sort by field. """ result = await get_summary(dsp, isrc, search) if sort_by: result["items"] = multikeysort(result["items"], [sort_by, COUNTRY_CODE_SORTING_KEY]) return result @cached(ttl=config.CHARTS_TRACKS_SUMMARY_CACHE_TLL) async def get_track_summary( dsp: DSP, isrc: str, chart_type_list: List[ChartType], chart_breakdown_list: List[ChartBreakdown], include_empty: bool, ) -> dict: """Track charts summary (dotnet endpoint copy). Args: dsp: Vendor code. isrc: ISRC code. chart_breakdown_list: Daily or weekly. chart_type_list: Regional or viral. include_empty: Include charts that not Returns: Summary. """ chart_type_list = [i.value for i in chart_type_list] chart_breakdown_list = [i.value for i in chart_breakdown_list] available_chart_list, track_charts_data = await asyncio.gather( *[ services.dsp.get_dsp_charts(dsp=dsp, breakdown=chart_breakdown_list, type=chart_type_list), services.dsp.get_tracks_charts_lifetime_with_metrics( dsp=dsp, isrc=[isrc], chart_breakdown=chart_breakdown_list, chart_type=chart_type_list ), ] ) available_chart_mapping = {i["chart_id"]: i for i in available_chart_list} schema = ChartTrackSummary.Response.ChartItem( **({"exclude": ["listType", "timeWindow"]} if dsp.value == DSP.APPLE.value else {}) ) if include_empty: charts_data_mapping = {i["chart_meta"]["chart_id"]: i for i in track_charts_data} current_id_list = [k for k, v in charts_data_mapping.items() if "metrics" in v] missing_id_list = [i for i in available_chart_mapping.keys() if i not in current_id_list] latest_date_mapping = await services.dsp.get_charts_latest_date_bulk( dsp=dsp, chart_breakdown=chart_breakdown_list, chart_type=chart_type_list ) for chart_id in missing_id_list: if chart_id in charts_data_mapping: if chart_id in latest_date_mapping: charts_data_mapping[chart_id]["metrics"] = {"date": latest_date_mapping[chart_id]} else: charts_data_mapping[chart_id] = { "chart_meta": available_chart_mapping[chart_id], **({"metrics": {"date": latest_date_mapping[chart_id]}} if chart_id in latest_date_mapping else {}), } track_charts_data = charts_data_mapping.values() if dsp.value == DSP.SPOTIFY.value: fix_weekly(track_charts_data) track_charts_data = sorted(track_charts_data, key=lambda i: i["chart_meta"]["chart_id"]) result = defaultdict(list) for item in track_charts_data: result[item["chart_meta"]["country_code"]].append(schema.dump(item)) return result