import os from datetime import date, timedelta from typing import Dict, List, Optional, Tuple, Union from urllib.parse import quote from apollo_utils.core.constants.market import Market from jinja2 import Environment, FileSystemLoader from juno_email_messages import config from juno_email_messages.constants import DATE_FORMAT, DAYS_RANGE_VALUE, SPOTIFY_TRACK_URI_PREFIX def get_code_to_name(code_name_mapping: Dict[str, str], country_code_list: List[str]) -> str: if not country_code_list or Market.WORLDWIDE in country_code_list: return "All" if len(country_code_list) == 1: country_code = country_code_list[0] if country_code in code_name_mapping: return code_name_mapping[country_code] else: return country_code return f"{len(country_code_list)} Markets" def get_days_text(days_count: int) -> str: if days_count in DAYS_RANGE_VALUE: return DAYS_RANGE_VALUE[days_count] return f"{days_count} Days" def get_product_sale_text(min_product_sale_days: Optional[int], max_product_sale_days: Optional[int]) -> str: return f"{get_days_text(max_product_sale_days or 0)}—{get_days_text(min_product_sale_days)}" def modify_event_data(event_data: dict, code_name_mapping: Dict[str, str]) -> dict: return { "name": event_data["name"], "country_code": get_code_to_name(code_name_mapping, event_data.get("country_code")), "isrc_country_code": get_code_to_name(code_name_mapping, event_data.get("isrc_country_code")), "product_sale_days": get_product_sale_text( event_data.get("min_product_sale_days"), event_data.get("max_product_sale_days") ), **( {"percent_change": format_number(event_data["percent_change"])} if event_data.get("percent_change") else {} ), } def format_name(name: str, max_length: int = 30) -> str: if not name: return name return name[:max_length].strip(", ") + ("..." if len(name) > max_length else "") def format_number(value: float or None, with_percent_formatting: bool = False) -> str: if value is None and with_percent_formatting: return "All" value = round(value) if value >= 1000000: value = f"{round(value / 1000000)}M" elif value >= 1000: value = f"{round(value / 1000)}k" else: value = str(value) if with_percent_formatting: return f"> {value} %" return value def get_streams_sum(data: dict, with_7_days: bool = False) -> Union[int, Tuple[int, int]]: streams_days_7, streams_days_8_14 = data.get("streams_days_7", 0), data.get("streams_days_8_14", 0) if with_7_days: return streams_days_7 + streams_days_8_14, streams_days_7 return streams_days_7 + streams_days_8_14 def get_track_id(track_uri: Optional[str]) -> Optional[str]: if not track_uri or not track_uri.startswith(SPOTIFY_TRACK_URI_PREFIX): return None return track_uri.replace(SPOTIFY_TRACK_URI_PREFIX, "") def modify_juno_track_list(juno_date: date, juno_track_list: List[dict]) -> List[dict]: juno_date = (juno_date - timedelta(days=6)).isoformat() result = [] for track in juno_track_list: track_meta = track.get("spotify_top_track_meta", {}) track_id = get_track_id(track_meta.get("track_uri")) metrics = track.get("metrics", {}) streams_total, streams_days_7 = get_streams_sum(metrics.get("totals", {}), with_7_days=True) demographics = metrics.get("demographics", {}) streams_male, streams_female = ( get_streams_sum(demographics.get("male", {})), get_streams_sum(demographics.get("female", {})) ) lean_back, lean_forward = ( get_streams_sum(metrics.get("lean_back", {})), get_streams_sum(metrics.get("lean_forward", {})) ) change_data = metrics.get("change", {}) lean_forward_change = metrics.get("lean_forward_change", {}) change_value = change_data.get("streams_change_7_to_14_days", 0) lf_change_value = lean_forward_change.get("streams_change_7_to_14_days", 0) result.append( { "is_entry": track.get("latest_entry_date") and track["latest_entry_date"] >= juno_date, "track_name": format_name(track_meta.get("track_name", ""), config.MAX_TRACK_NAME_LENGTH), "artist_name": format_name( ", ".join(track_meta.get("artist_names", [])), config.MAX_ARTIST_NAME_LENGTH ), "is_male": (streams_male + streams_female) and streams_male / (streams_male + streams_female) >= 0.6, "is_female": ( (streams_male + streams_female) and streams_female / (streams_male + streams_female) >= 0.6 ), "is_lean_back": streams_total and lean_back / streams_total >= 0.6, "is_lean_forward": streams_total and lean_forward / streams_total >= 0.6, "streams": format_number(streams_days_7), "is_change_negative": change_value < 0, "change_value": format_number(abs(change_value)), "change_percent": format_number(abs(change_data.get("streams_change_percent_7_to_14_days", 0))), "is_lf_change_negative": lf_change_value < 0, "lf_change_value": format_number(abs(lf_change_value)), "lf_change_percent": format_number( abs(lean_forward_change.get("streams_change_percent_7_to_14_days", 0)) ), "track_id": track_id, "track_page_url": quote(f"{config.APOLLO_PORTAL_URL}/spotify/track/{track_id}") if track_id else None, "distributed_by": track.get("distributed_by"), } ) return result def render_template(filter_id: str, event_data: dict, juno_date: date, juno_track_list: List[dict]) -> str: template_dir = os.path.join(os.path.dirname(__file__), "templates") env = Environment(loader=FileSystemLoader(template_dir), autoescape=True) template = env.get_template("juno_email.html") return template.render( filter_id=filter_id, filter=event_data, juno_date_formatted=juno_date.strftime(DATE_FORMAT), juno_track_list=juno_track_list, apollo_url=config.APOLLO_PORTAL_URL, ) def generate_email( filter_id: str, event_data: dict, code_name_mapping: Dict[str, str], juno_date: date, juno_track_list: List[dict] ) -> str: event_data = modify_event_data(event_data, code_name_mapping) juno_track_list = modify_juno_track_list(juno_date, juno_track_list) return render_template(filter_id, event_data, juno_date, juno_track_list) def format_country_code_list(country_code_list: List[str]) -> str: return "All" if not country_code_list or Market.WORLDWIDE in country_code_list else country_code_list def generate_amplitude_event(user_id: str, filter_id: str, event_data: dict) -> dict: return { "user_id": user_id, "juno_views": filter_id, "juno_name": event_data["name"], "juno_change_percentage": format_number(event_data.get("percent_change"), with_percent_formatting=True), "juno_country": format_country_code_list(event_data.get("country_code")), "juno_ISRC_country": format_country_code_list(event_data.get("isrc_country_code")), "juno_years_from_release": get_product_sale_text( event_data.get("min_product_sale_days"), event_data.get("max_product_sale_days") ), }