import asyncio from datetime import date from marshmallow import Schema from typing import List, Optional, Tuple, 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 ChartBreakdown, ChartInclude, ChartType, SourceType, TrackStateIncludes from server.constants.distributors import DISTRIBUTORS, DISTRIBUTORS_FILTERS from server.constants.favorites import EntityType from server.domains.charts.misc import get_chart_id from server.scenarios.distributors import get_tracks_distributors_map from server.scenarios.users.favorites import get_favorites_map from server.schemas.charts.digest import ChartBase from server.utils.pagination import multikeysort async def request_data( source_type: SourceType, chart_id: str, dsp: DSP, chart_date: Optional[date or str], min_position: Optional[int] = None, max_position: Optional[int] = None, ) -> List[dict]: """Call main charts data function depending on action type. Args: source_type: Source type (chart/additions/removals). chart_id: Chart ID (concatenated types and market). dsp: DSP enum value. chart_date: Chart date if selected. min_position: Min track position in chart or None for all. max_position: Max track position in chart of None for all. Returns: List of chart's tracks. """ func_mapping = { SourceType.CHART: services.dsp.get_charts_tracks, SourceType.ADDITIONS: services.dsp.get_charts_additions, SourceType.REMOVALS: services.dsp.get_charts_removals, } if source_type not in func_mapping: raise NotImplementedError("Unknown action") func = func_mapping[source_type] return await func( dsp=dsp, chart_id=chart_id, date=chart_date, track_state_includes=[ TrackStateIncludes.METRICS.value, TrackStateIncludes.LIFETIME_METRICS.value, TrackStateIncludes.PUBLIC_META.value, TrackStateIncludes.LICENSORS.value, ], min_position=min_position, max_position=max_position, ) async def get_data( source_type: SourceType, dsp: DSP, chart_breakdown: ChartBreakdown, chart_date: Optional[date], market: str, min_position: Optional[int], max_position: Optional[int], ) -> Tuple[bool, List[dict], Optional[date]]: """Get chart data from Delphi API. Args: source_type: Source type (chart/additions/removals). dsp: DSP enum value. chart_breakdown: Daily or weekly. chart_date: Chart date if selected. market: Market code. min_position: Min track position in chart or None for all. max_position: Max track position in chart of None for all. Returns: List of chart's tracks, chart availability and selected or latest date. """ chart_id = get_chart_id(market, chart_breakdown=chart_breakdown, dsp=dsp) if not chart_date: chart_date = await services.dsp.get_charts_latest_date( dsp=dsp, chart_type=ChartType.REGIONAL.value, chart_breakdown=ChartBreakdown.DAILY.value, country_code=market, source_type=source_type, ) if not chart_date: return False, [], None chart_available, chart_tracks = await asyncio.gather( *[ services.dsp.get_dsp_charts(dsp=dsp, chart_id=chart_id), request_data(source_type, chart_id, dsp, chart_date, min_position, max_position), ] ) return bool(chart_available), chart_tracks, chart_date def apply_additional_filters( chart_tracks: List[dict], previous_min_position: Optional[int], previous_max_position: Optional[int], change: Optional[int], ) -> List[dict]: """Apply additional action specific filters like previous position range for out or trend above some value for moves. Args: chart_tracks: List of chart tracks. previous_min_position: Range start, get tracks that previous position is within the range, but current is not. previous_max_position: Range end, get tracks that previous position is within the range, but current is not. change: Min position change. Returns: List of filtered chart tracks. """ if previous_min_position is not None and previous_max_position is not None: chart_tracks = [ i for i in chart_tracks if ( i["public_meta"].get("isrc") and not i["metrics"]["is_entry"] and previous_min_position <= i["metrics"]["previous_position"] <= previous_max_position and ( i["metrics"]["position"] > previous_max_position or i["metrics"]["position"] < previous_min_position ) ) ] if change is not None: chart_tracks = [ i for i in chart_tracks if ( i["public_meta"].get("isrc") and not i["metrics"]["is_entry"] and abs(i["metrics"]["previous_position"] - i["metrics"]["position"]) >= change ) ] return chart_tracks async def apply_and_filter_distributors( chart_tracks: List[dict], dsp: DSP, market: str, distributors: Optional[List[DISTRIBUTORS]] = None, filter_distributors: List[DISTRIBUTORS_FILTERS] = None, ) -> List[dict]: """Get information if chart tracks distributor and filter by this value if needed. Args: chart_tracks: List of chart tracks. dsp: Spotify or Apple. market: Market code. distributors: List of DISTRIBUTORS to calculate 'distributed_by' values for filter_distributors: List of DISTRIBUTORS_FILTERS to filter 'distributed_by' values by, empty 'filter_distributors' means no filtering is needed. Returns: List of filtered chart tracks with additional data field. """ if not distributors: return chart_tracks if filter_distributors: filter_distributors = set( (f.value if f is not DISTRIBUTORS_FILTERS.OTHER else None) for f in filter_distributors ) distributors_mapping = await get_tracks_distributors_map( country_code=market, dsp_track_id=[f'{dsp.value}_{i["public_meta"]["track_id"]}' for i in chart_tracks], distributors=distributors, remove_result_dsp_prefix=True, ) for index in range(len(chart_tracks) - 1, -1, -1): track = chart_tracks[index] track_id = track["public_meta"]["track_id"] track_distributor = distributors_mapping.get(track_id) if filter_distributors and track_distributor not in filter_distributors: del chart_tracks[index] continue track["distributed_by"] = track_distributor track["is_sony"] = track_distributor == DISTRIBUTORS.SME.value return chart_tracks async def apply_starred( chart_tracks: List[dict], field_list: List[ChartInclude], only_starred_tracks: bool, ) -> List[dict]: """Get information if chart tracks is starred and filter by this flag if needed. Args: chart_tracks: List of chart tracks. field_list: Fields to additionally include in results. only_starred_tracks: Only starred tracks. Returns: List of filtered chart tracks with additional data field. """ if ChartInclude.IS_STARRED in field_list: isrc_list = [i["isrc"] for i in chart_tracks if i.get("isrc")] starred_isrc_to_data = await get_favorites_map(entity_id=isrc_list, entity_type=[EntityType.TRACK.value]) for index in range(len(chart_tracks) - 1, -1, -1): track = chart_tracks[index] isrc = track.get("isrc") starred_data = starred_isrc_to_data.get(isrc, {}) if isrc else {} is_starred = bool(starred_data) if only_starred_tracks and not is_starred: del chart_tracks[index] track.update({"is_starred_track": is_starred, "favorites_id": starred_data.get("favorites_id")}) return chart_tracks def generate_response( source_type: SourceType, chart_available: bool, chart_tracks: Optional[List[dict]], chart_date: Optional[date], dsp: DSP, chart_breakdown: ChartBreakdown, schema: Type[Schema], ): """Generate charts response, dump and sort data. Args: source_type: Source type (chart/additions/removals). chart_tracks: List of chart tracks. chart_available: If chart exists for chosen market/type or not. chart_date: Chart date. dsp: DSP code (Spotify or Apple). chart_breakdown: Daily or weekly. schema: Schema that is used to dump response. """ chart_date = chart_tracks[0]["metrics"]["date"] if chart_tracks else (chart_date.isoformat() if chart_date else "") return schema( context={ "dsp": dsp.value, "chart_breakdown": chart_breakdown.value, "removals": source_type == SourceType.REMOVALS, } ).dump({"chart_date": chart_date, "available": chart_available, "tracks": chart_tracks or []}) @cached(ttl=config.CHARTS_CACHE_TLL) async def get_not_sorted_chart( dsp: DSP, chart_breakdown: ChartBreakdown, chart_date: Optional[date], market: str, min_position: Optional[int], max_position: Optional[int], previous_min_position: Optional[int], previous_max_position: Optional[int], change: Optional[int], schema: Type[Schema], source_type: SourceType, distributors: Optional[List[DISTRIBUTORS]], filter_distributors: List[DISTRIBUTORS_FILTERS] = None, ) -> dict: """Get non-sorted chart tracks. Args: dsp: DSP code (Apple or Spotify). chart_breakdown: Daily or weekly. chart_date: Chart date or None for the latest. market: Market code. min_position: Current position range filter start. max_position: Current position range filter end. previous_min_position: Out of range filter start. previous_max_position: Out of range filter end. change: Position change value filter. schema: Schema to dump with. source_type: Source type (chart/additions/removals). distributors: List of DISTRIBUTORS to calculate 'distributed_by' values for filter_distributors: List of DISTRIBUTORS_FILTERS to filter 'distributed_by' values by, empty 'filter_distributors' means no filtering is needed. """ chart_available, chart_tracks, chart_date = await get_data( source_type, dsp, chart_breakdown, chart_date, market, min_position, max_position ) if chart_available: chart_tracks = apply_additional_filters(chart_tracks, previous_min_position, previous_max_position, change) if distributors and chart_tracks: chart_tracks = await apply_and_filter_distributors( chart_tracks, dsp, market, distributors, filter_distributors ) return generate_response(source_type, chart_available, chart_tracks, chart_date, dsp, chart_breakdown, schema) async def get_chart( dsp: DSP, chart_breakdown: ChartBreakdown, chart_date: Optional[date], market: str, field_list: List[ChartInclude], only_starred_tracks: bool, order_by_list: List[str], min_position: Optional[int] = None, max_position: Optional[int] = None, previous_min_position: Optional[int] = None, previous_max_position: Optional[int] = None, change: Optional[int] = None, schema: Type[Schema] = ChartBase.Response, source_type: SourceType = SourceType.CHART, distributors: List[DISTRIBUTORS] = None, filter_distributors: List[DISTRIBUTORS_FILTERS] = None, ) -> dict: """Get chart tracks. Args: dsp: DSP code (Apple or Spotify). chart_breakdown: Daily or weekly. chart_date: Chart date or None for the latest. market: Market code. field_list: List of additional fields to include. only_starred_tracks: Only tracks that were starred by the current user. order_by_list: Order by fields. min_position: Current position range filter start. max_position: Current position range filter end. previous_min_position: Out of range filter start. previous_max_position: Out of range filter end. change: Position change value filter. schema: Schema to dump with. source_type: Source type (chart/additions/removals). distributors: List of DISTRIBUTORS to calculate 'distributed_by' values for filter_distributors: List of DISTRIBUTORS_FILTERS to filter 'distributed_by' values by, empty 'filter_distributors' means no filtering is needed. """ result = await get_not_sorted_chart( dsp=dsp, chart_breakdown=chart_breakdown, chart_date=chart_date, market=market, min_position=min_position, max_position=max_position, previous_min_position=previous_min_position, previous_max_position=previous_max_position, change=change, schema=schema, source_type=source_type, distributors=distributors, filter_distributors=filter_distributors, ) await apply_starred(result["tracks"], field_list, only_starred_tracks) if result["tracks"]: result["tracks"] = multikeysort(result["tracks"], order_by_list) return result