from datetime import date, timedelta from typing import Dict, List, Optional from server.client.utils import str_to_date from server.constants.charts import TiktokChartDay, TiktokChartType from server.domains.tracks.common import calc_change_percent def get_chart_id( market: str, chart_days: TiktokChartDay = TiktokChartDay.DAY, chart_type: TiktokChartType = TiktokChartType.CREATIONS, ) -> str: """Generate tiktok chart ID. Args: market: Country code. chart_days: Chart days (1 or 7, affects creations values only, not positions and their dates). chart_type: Creations per day (change) or each day total. """ return f"top50_{chart_days.value}day_product_family_period_{chart_type.value}_{market}" def fix_count_and_positions(data: List[dict], chart_days: TiktokChartDay, chart_type: TiktokChartType) -> List[dict]: """Fix tiktok chart. There could be more than 50 items as tracks with the same amounts of creations have the same positions, and we have top 50 creations counts, not top 50 tracks. Need to sort by creations desc and ISRC asc, take first 50 items, fix positions (= index + 1). Args: data: Tiktok charts data. chart_days: Chart days (day or week). chart_type: Chart type (by creations or by change). Returns: Fixed data. """ data = list(sorted(data, key=lambda i: i["metrics"]["position"]))[:50] for index, item in enumerate(data): item["metrics"]["position"] = index + 1 return data def calc_re_entry(is_entry: bool, current_date: date, previous_date: date) -> bool: """Calculate re-entry based on entry flag and current/previous dates. This won't work if data gaps are possible, but we do not have this field in Delphi API response. Args: is_entry: Is entry or not. current_date: Chart date. previous_date: Previous date. Returns: Is re-entry or not. """ return not is_entry and current_date != (previous_date + timedelta(days=1)) def calc_trend(position: int, previous_position: Optional[int], is_re_entry: bool) -> Optional[int]: """Calculate trend. Args: position: Current position. previous_position: Previous position. is_re_entry: Is re-entry or not. Returns: Trend. """ return None if previous_position is None or is_re_entry else position - previous_position def calc_value_diff(value: int, previous_value: Optional[int], default_value: Optional[int] = None) -> Optional[int]: """Calculate value diff between two days. Args: value: Value. previous_value: Previous value. default_value: Default value. Returns: Values diff. """ return (value - previous_value) if previous_value else default_value def calc_track(isrc: str, item: dict, track: dict, image_size: int) -> dict: """Generate track meta. Args: isrc: ISRC. item: Chart item. track: Track meta. image_size: Image size. Returns: Track data. """ cover = [i for i in track.get("album", {}).get("images", []) if i["height"] == image_size] cover = cover[0]["url"] if cover else None return { "name": track["name"] if track else item["source_meta"]["song_title"], "isrc": isrc, **( {"id": track["id"], "cover": cover, "artists": track["artists"]} if track else {"artists": [{"name": item["source_meta"]["artist"]}]} ), } def calc_selected(item: dict, interval_days: int, previous_creations_field: str) -> dict: """Generate selected data. Args: item: Chart item. interval_days: Chart dates interval (0 for 1 day or 6 for week). previous_creations_field: Previous field name. Returns: Selected data. """ end_date = str_to_date(item["metrics"]["date"]) start_date = end_date - timedelta(days=interval_days) is_entry = item["metrics"]["is_entry"] is_re_entry = calc_re_entry(is_entry, end_date, str_to_date(item["metrics"]["previous_date"])) creations = item["metrics"]["creations"] previous_creations = item["aggregated_metrics"][previous_creations_field] return { "start_date": start_date, "end_date": end_date, "position": { "value": item["metrics"]["position"], "trend": calc_trend(item["metrics"]["position"], item["metrics"]["previous_position"], is_re_entry), "is_new": is_entry, "is_re_entry": is_re_entry, }, "change": { "value": calc_value_diff(creations, previous_creations), "percent": calc_change_percent(creations, previous_creations), "is_new": is_entry, "is_re_entry": is_re_entry, }, "creations": creations, } def calc_previous(item: dict, interval_days: int, previous_creations_field: str): """Generate previous data. Args: item: Chart item. interval_days: Chart dates interval (0 for 1 day or 6 for week). previous_creations_field: Previous field name. Returns: Previous data. """ previous_end_date = str_to_date(item["metrics"]["date"]) - timedelta(days=interval_days + 1) previous_start_date = previous_end_date - timedelta(days=interval_days) return { "start_date": previous_start_date, "end_date": previous_end_date, "creations": item["aggregated_metrics"][previous_creations_field], } def calc_week(item: dict) -> dict: """Generate week data for 1 day chart. Args: item: Chart item. Returns: Week data. """ creations = item["metrics"]["creations"] previous_week_creations = item["aggregated_metrics"]["previous_week_1day_creations"] return { "start_date": str_to_date(item["metrics"]["date"]) - timedelta(days=7), "creations": previous_week_creations, "change": { "value": calc_value_diff(creations, previous_week_creations), "percent": calc_change_percent(creations, previous_week_creations), }, } def get_previous_creations_field(chart_days: TiktokChartDay) -> str: """Get previous creations field name. Args: chart_days: Chart days (day or week). Returns: Field name. """ return "previous_1day_creations" if chart_days.value == TiktokChartDay.DAY.value else "previous_week_7day_creations" def calc_chart( chart_days: TiktokChartDay, image_size: int, top_tracks: List[dict], tracks_meta: Dict[str, dict] ) -> List[dict]: """Calc tiktok chart metrics. Args: chart_days: 1 or 7 days chart. image_size: Image size. top_tracks: Tiktok chart. tracks_meta: Spotify tracks meta. Returns: Calculated chart metrics. """ previous_creations_field = get_previous_creations_field(chart_days) interval_days = int(chart_days.value) - 1 result = [] for item in top_tracks: isrc = item["source_meta"].get("isrc") lifetime_metrics = item.get("lifetime_metrics", {}) result_item = { "track": calc_track(isrc, item, tracks_meta.get(isrc, {}), image_size), "selected": calc_selected(item, interval_days, previous_creations_field), "previous": calc_previous(item, interval_days, previous_creations_field), "top_markets": item["aggregated_metrics"]["last_7day_top_markets"], "days_on_list": lifetime_metrics.get("total_days"), "added_date": lifetime_metrics.get("earliest_position_date"), "latest_date": lifetime_metrics.get("latest_position_date"), } if chart_days.value == TiktokChartDay.DAY.value: result_item.update({"week": calc_week(item)}) result.append(result_item) return result