"""Utils and helpers related to streams, skips and saves.""" import datetime from ddtrace import tracer from sound_recordings.constants import source as source_constants from sound_recordings.constants.parameters import ALL_TIME from sound_recordings.utils import store_availability AGGREGATE_STREAMS_FIELDS = ["isrc", "streams_all_time", "streams_7_day_growth"] STREAMS_ALL_FIELDS = ["date", "streams", "streams_with_skips", "skips", "saves"] STREAMS_BY_TRACK_FIELDS = [ "track_unique_id", "date", "streams", "streams_with_skips", "skips", "saves", ] STREAMS_BY_COUNTRY_FIELDS = [ "country_code", "date", "streams", "streams_with_skips", "skips", "saves", ] STREAMS_BY_PRODUCT_FIELDS = [ "product_id", "date", "streams", "streams_with_skips", "skips", "saves", ] STREAMS_BY_SOS_FIELDS = [ "date", "streams_passive", "streams_active", "streams_collection", "streams", ] STREAMS_BY_SOUND_RECORDING_FIELDS = [ "isrc", "date", "streams", "streams_with_skips", "skips", "saves", ] STREAMS_BY_STORE_FIELDS = [ "store_id", "date", "streams", "streams_with_skips", "skips", "saves", ] def calc_date_skip_rate(date_item): """Calculate Skip Rate for a given date item. Args: date_item (dict): Dict representing streams, skips and saves for a given date. Returns: Skip Rate for the Streams and Skips. """ if not date_item or date_item["skips"] is None: return None skips = date_item["skips"] streams_with_skips = date_item["streams_with_skips"] return skips / (skips + streams_with_skips) if skips else 0 def get_empty_streams_for_period(start_date, end_date): """Get list of empty streams for given date range. Args: start_date (datetime.date): Start date end_date (datetime.date): End date Returns: List of dictionaries with keys date (str): Date in YYYY-MM-DD format streams (None): Total streams """ empty_streams = [] for n in range((end_date - start_date).days + 1): empty_streams.append( { "date": (start_date + datetime.timedelta(n)).strftime("%Y-%m-%d"), "streams": 0, "streams_with_skips": 0, "skips": None, "skip_rate": None, "saves": None, } ) return empty_streams def get_streams_for_store(stream_records, store_id, start_date, end_date): """Get streams for store for given date range. Args: stream_records (list): List of dicts representing streams with values store_id (int): Store ID date (datetime.date): Date of streams streams (int): Total streams for date streams_with_skips (int | None): Total streams with skips for date saves (int | None): Total saves for date skips (int | None): Total skips for date store_id (int): Store to get streams for start_date (datetime.date): Start date end_date (datetime.date): End date Returns: List of dictionaries with keys date (str): Date in YYYY-MM-DD format streams (int | None): Total streams for date streams_with_skips (int | None): Total streams with skips for date saves (int | None): Total saves for date skips (int | None): Total skips for date skip_rate (float | None): Total skip rate for date """ streams = get_empty_streams_for_period(start_date, end_date) for record in stream_records: if record["store_id"] == store_id: date_idx = (record["date"] - start_date).days if 0 <= date_idx < len(streams): streams[date_idx]["streams"] = record["streams"] streams[date_idx]["streams_with_skips"] = record["streams_with_skips"] streams[date_idx]["skips"] = record["skips"] streams[date_idx]["saves"] = record["saves"] streams[date_idx]["skip_rate"] = calc_date_skip_rate(record) return streams def get_streams_by_store(streams, start_date, end_date): """Format streams by store for given date range. Args: streams (list): streams result start_date (datetime.date | str): Start date or ALL_TIME keyword end_date (datetime.date): End date Returns: Formatted list of dictionaries with keys id (int): Store ID name (str): Store Name items (list): List of dicts representing streams with values date (str): Date in YYYY-MM-DD format streams (int | None): Total streams for date streams_with_skips (int | None): Total streams with skips saves (int | None): Total saves for date skips (int | None): Total skips for date skip_rate (float | None): Total skip rate for date """ if len(streams) == 0: return streams # The records are ordered by ascending date # so we can pick the first one as our start_date if start_date == ALL_TIME: start_date = streams[0]["date"] store_names = store_availability.get_store_names() all_store_ids = list(set([record["store_id"] for record in streams])) active_store_ids = set(filter(lambda e: e in store_names, all_store_ids)) streams_by_store = [] for store_id in active_store_ids: streams_by_store.append( { "id": store_id, "name": store_names.get(store_id), "items": get_streams_for_store(streams, store_id, start_date, end_date), } ) return streams_by_store def _format_streams_for_single_country(stream_records, country, start_date, end_date): """Get streams timeseries for given country for given date range. Args: stream_records (list): List of dicts representing streams with values country_code (str): Country code date (datetime.date): Date of streams streams (int): Total streams for date streams_with_skips (int | None): Total streams with skips for date saves (int | None): Total saves for date skips (int | None): Total skips for date country (str): Country code start_date (datetime.date): Start date end_date (datetime.date): End date Returns: List of dictionaries with keys: date (str): Date in YYYY-MM-DD format streams (int | None): Total streams for date saves (int | None): Total saves for date skip_rate (float | None): Total skip rate for date """ streams = get_empty_streams_for_period(start_date, end_date) for record in stream_records: if record["country_code"] == country: date_idx = (record["date"] - start_date).days if 0 <= date_idx < len(streams): streams[date_idx]["streams"] = record["streams"] streams[date_idx]["saves"] = record["saves"] streams[date_idx]["skip_rate"] = calc_date_skip_rate(record) return streams @tracer.wrap(name="format_streams_by_country") def format_streams_by_country(streams, start_date, end_date): """Streams by country timeseries for given date range. Args: streams (list): List of dicts representing streams with values country_code (str): Country code date (datetime.date): Date of streams streams (int): Total streams for date streams_with_skips (int | None): Total streams with skips for date saves (int | None): Total saves for date skips (int | None): Total skips for date start_date (datetime.date | str): Start date or ALL_TIME keyword end_date (datetime.date): End date Return: Formatted streams by country timeseries """ if not streams: return None # The records are ordered by ascending date # so we can pick the first one as our start_date if start_date == ALL_TIME: start_date = streams[0]["date"] all_countries = list(set([record["country_code"] for record in streams])) streams_by_country = [] for country in all_countries: items = _format_streams_for_single_country( streams, country, start_date, end_date ) streams_by_country.append({"country_code": country, "items": items}) return streams_by_country def get_streams_for_sos(stream_records, source, start_date, end_date): """Get streams for source of streams for given date range. Args: stream_records (list): List of dicts representing streams with values source (string): Source of streams date (datetime.date): Date of streams streams (int): Total streams for date source (string): Source of streams start_date (datetime.date): Start date end_date (datetime.date): End date Returns: List of dictionaries with keys date (str): Date in YYYY-MM-DD format streams (int | None): Total streams for date """ streams = get_empty_streams_for_period(start_date, end_date) for record in stream_records: if record["source"] == source: date_idx = (record["date"] - start_date).days if 0 <= date_idx < len(streams): streams[date_idx]["streams"] = record["streams"] return streams def get_streams_by_sos(streams, start_date, end_date): """Streams by source of streams timeseries for given date range. Args: streams (list): List of dicts representing streams with values source (str): Source of stream date (datetime.date): Date of streams streams (int): Total streams for date start_date (datetime.date | str): Start date or ALL_TIME keyword end_date (datetime.date): End date Returns: Formatted streams by source of streams timeseries """ if not streams: return None # The records are ordered by ascending date # so we can pick the first one as our start_date if start_date == ALL_TIME: start_date = streams[0]["date"] all_sources = list(set([record["source"] for record in streams])) streams_by_sos = [] for source in all_sources: items = get_streams_for_sos(streams, source, start_date, end_date) streams_by_sos.append({"source": source, "items": items}) return streams_by_sos def format_streams_by_sos_model(stream_records, fields): """Format streams timeseries model data by source. Args: stream_records (list): List of tuples of streams timeseries data fields (list): List of keys to assign to stream_records values Returns: List of dictionaries with keys: source (str): Source of streams date (str): Date in YYYY-MM-DD format streams (int): Total streams for date """ timeseries = [] for record in stream_records: x = dict(zip(fields, record)) date = x["date"] streams_unknown = ( x["streams"] - x["streams_active"] - x["streams_passive"] - x["streams_collection"] ) timeseries.append( { "source": source_constants.SOURCE_ACTIVE, "date": date, "streams": x["streams_active"], } ) timeseries.append( { "source": source_constants.SOURCE_PASSIVE, "date": date, "streams": x["streams_passive"], } ) timeseries.append( { "source": source_constants.SOURCE_COLLECTION, "date": date, "streams": x["streams_collection"], } ) timeseries.append( { "source": source_constants.SOURCE_UNKNOWN, "date": date, "streams": streams_unknown, } ) return timeseries def get_streams_all(stream_records, start_date, end_date): """Get aggregate streams for store for given date range. Args: stream_records (list): List of dicts representing streams with values date (datetime.date): Date of streams streams (int): Total streams for date streams_with_skips (int | None): Total streams with skips for date saves (int | None): Total saves for date skips (int | None): Total skips for date start_date (datetime.date | str): Start date or ALL_TIME keyword end_date (datetime.date): End date Returns: List of dictionaries with keys date (str): Date in YYYY-MM-DD format streams (int | None): Total streams for date saves (int | None): Total saves for date skip_rate (float | None): Total skip rate for date """ if len(stream_records) == 0: return stream_records # The records are ordered by ascending date # so we can pick the first one as our start_date if start_date == ALL_TIME: start_date = stream_records[0]["date"] streams = get_empty_streams_for_period(start_date, end_date) for record in stream_records: date_idx = (record["date"] - start_date).days if 0 <= date_idx < len(streams): streams[date_idx]["streams"] = record["streams"] streams[date_idx]["saves"] = record["saves"] streams[date_idx]["skip_rate"] = calc_date_skip_rate(record) return streams def get_streams_for_product(stream_records, product_id, start_date, end_date): """Get streams timeseries for given product_id for given date range. Args: stream_records (list): List of dicts representing streams with values product_id (str): Product ID date (datetime.date): Date of streams streams (int): Total streams for date streams_with_skips (int | None): Total streams with skips for date saves (int | None): Total saves for date skips (int | None): Total skips for date product_id (str): Product ID start_date (datetime.date): Start date end_date (datetime.date): End date Returns: List of dictionaries with keys: date (str): Date in YYYY-MM-DD format streams (int | None): Total streams for date saves (int | None): Total saves for date skip_rate (float | None): Total skip rate for date """ streams = get_empty_streams_for_period(start_date, end_date) for record in stream_records: if record["product_id"] == product_id: date_idx = (record["date"] - start_date).days if 0 <= date_idx < len(streams): streams[date_idx]["streams"] = record["streams"] streams[date_idx]["saves"] = record["saves"] streams[date_idx]["skip_rate"] = calc_date_skip_rate(record) return streams def get_streams_by_product(streams, start_date, end_date): """Get streams by product timeseries for given date range. Args: streams (list): List of dicts representing streams with values product_id (str): Product ID date (datetime.date): Date of streams streams (int): Total streams for date streams_with_skips (int | None): Total streams with skips for date saves (int | None): Total saves for date skips (int | None): Total skips for date start_date (datetime.date): Start date end_date (datetime.date): End date Returns: Formatted streams by product timeseries """ if not streams: return None all_product_ids = list(set([record["product_id"] for record in streams])) streams_by_product = [] for product_id in all_product_ids: items = get_streams_for_product(streams, product_id, start_date, end_date) streams_by_product.append({"product_id": product_id, "items": items}) return streams_by_product def get_streams_for_sound_recording(stream_records, isrc, start_date, end_date): """Get streams timeseries for given isrc for given date range. Args: stream_records (list): List of dicts representing streams with values isrc (str): ISRC of sound-recording date (datetime.date): Date of streams streams (int): Total streams for date streams_with_skips (int | None): Total streams with skips for date saves (int | None): Total saves for date skips (int | None): Total skips for date isrc (str): ISRC of sound-recording start_date (datetime.date): Start date end_date (datetime.date): End date Returns: List of dictionaries with keys: date (str): Date in YYYY-MM-DD format streams (int | None): Total streams for date saves (int | None): Total saves for date skip_rate (float | None): Total skip rate for date """ streams = get_empty_streams_for_period(start_date, end_date) for record in stream_records: if record["isrc"] == isrc: date_idx = (record["date"] - start_date).days if 0 <= date_idx < len(streams): streams[date_idx]["streams"] = record["streams"] streams[date_idx]["saves"] = record["saves"] streams[date_idx]["skip_rate"] = calc_date_skip_rate(record) return streams def get_streams_by_sound_recording(streams, start_date, end_date): """Streams by sound-recording timeseries for given date range. Args: streams (list): List of dicts representing streams with values isrc (str): ISRC of sound-recording date (datetime.date): Date of streams streams (int): Total streams for date streams_with_skips (int | None): Total streams with skips for date saves (int | None): Total saves for date skips (int | None): Total skips for date start_date (datetime.date | str): Start date or ALL_TIME keyword end_date (datetime.date): End date Return: Formatted streams by sound-recording timeseries """ if not streams: return None # The records are ordered by ascending date # so we can pick the first one as our start_date if start_date == ALL_TIME: start_date = streams[0]["date"] all_sound_recordings = list(set([record["isrc"] for record in streams])) streams_by_sound_recording = [] for isrc in all_sound_recordings: items = get_streams_for_sound_recording(streams, isrc, start_date, end_date) streams_by_sound_recording.append({"isrc": isrc, "items": items}) return streams_by_sound_recording def get_streams_for_track(stream_records, tuid, start_date, end_date): """Get streams timeseries for given tuid for given date range. Args: stream_records (list): List of dicts representing streams with values tuid (int): Tuid start_date (datetime.date): Start date end_date (datetime.date): End date Returns: List of dicts with keys track_unique_id (int): tuid items (list): List of dicts representing streams with values date (str): Date in YYYY-MM-DD format streams (int | None): Total streams for date saves (int | None): Total saves for date skip_rate (float | None): Total skip rate for date """ result = {"track_unique_id": None, "items": []} streams = get_empty_streams_for_period(start_date, end_date) for record in stream_records: if record["track_unique_id"] == tuid: result["track_unique_id"] = record["track_unique_id"] date_idx = (record["date"] - start_date).days if 0 <= date_idx < len(streams): streams[date_idx]["streams"] = record["streams"] streams[date_idx]["saves"] = record["saves"] streams[date_idx]["skip_rate"] = calc_date_skip_rate(record) result["items"] = streams return result def get_streams_by_track(streams, start_date, end_date): """Streams by track timeseries for given date range. Args: streams (list): List of dicts representing streams with values track_unique_id (int): tuid date (datetime.date): Date of streams streams (int): Total streams for date streams_with_skips (int | None): Total streams with skips for date saves (int | None): Total saves for date skips (int | None): Total skips for date start_date (datetime.date): Start date end_date (datetime.date): End date Return: Formatted streams by track timeseries """ if len(streams) == 0: return streams all_tuids = list(set([record["track_unique_id"] for record in streams])) streams_by_track = [] for tuid in all_tuids: streams_for_track = get_streams_for_track(streams, tuid, start_date, end_date) streams_by_track.append(streams_for_track) return streams_by_track