from apollo_utils.service.legacy.schemas.mixins.vendor import ( MultipleVendorsMixin, RequiredAllVendorMixin, VendorRequiredMixin, ) from apollo_utils.service.schemas.base import RequestSchema from apollo_utils.service.schemas.fields.datetime import DateStringField from apollo_utils.service.schemas.fields.isrc import IsrcField from apollo_utils.service.schemas.fields.list import SplitUniqueListField from apollo_utils.service.schemas.fields.market import DelphiMarketField from apollo_utils.service.schemas.mixins.country_code import DelphiCountryCodeMixin from apollo_utils.service.schemas.mixins.date import DateRangeMixin from apollo_utils.service.schemas.mixins.isrc import get_isrc_list_mixin from apollo_utils.service.schemas.mixins.market import ( DelphiDefaultMarketMixin, DelphiMarketMixin, DelphiMarketNoneMixin, DelphiMarketsListMixin, DelphiRequiredMarketMixin, ) from marshmallow import Schema, ValidationError, fields, post_load, validate, validates_schema from server.constants.delphi.tiktok import TiktokTrackAnalyticsMetric from server.constants.delphi.videos.only import VideosOnly from server.legacy.analytics.constants import ( DEFAULT_PAGINATION_LIMIT, MarketOrder, MetricsTrendsValuesMetric, YoutubeVideosOrder, ) from server.legacy.analytics.mixins import LatestDateMixin, LimitMixin, MarketMixin, NotEmptyDictField, PerVendorMixin from server.legacy.core.constants import DELPHI_GLOBAL_MARKET, SPOTIFY, TIKTOK, YOUTUBE from server.legacy.core.deserializers import GlobalMarketOutputField from server.legacy.core.utils import convert_market from server.legacy.delphi.constants import VideosInclude from server.schemas.delphi.mixins.content_type import ContentTypeListMixin from server.schemas.fields.dsp import VendorPrefixField def get_pagination_mixin( page_offset: int = 0, page_limit: int or None = DEFAULT_PAGINATION_LIMIT, max_limit: int or None = None, ) -> type: class PaginationParams: offset = fields.Integer(validate=validate.Range(min=0), missing=page_offset) limit = fields.Integer(validate=validate.Range(min=1, max=max_limit), missing=page_limit) return PaginationParams PaginationWithDefaultLimitMixin = get_pagination_mixin() # TODO: Move this mixin to utils_package PaginationMixin = get_pagination_mixin(page_limit=None) class TotalStreamsCountSchemaV1: class RequestSchema(PerVendorMixin, MarketMixin, RequestSchema): class Meta: include = { "end_date": fields.Date(format="iso", description="Date range end date (ISO8601-formatted)."), "isrc": IsrcField(required=True), } class ResponseSchema(Schema): class Meta: class VendorSchema(Schema): class Meta: include = { "global": fields.Int(required=True), "market": fields.Int(required=False), } include = { "global": fields.Int(required=True), "market": fields.Int(required=False), "vendors": fields.Dict(keys=fields.Str(), values=fields.Nested(VendorSchema)), } class TrackStreamsGraphViewV1: class RequestSchema(PerVendorMixin, DelphiDefaultMarketMixin, DateRangeMixin, RequestSchema): isrc = IsrcField(required=True) class ResponseSchema(Schema): class Meta: class CoordinatesSchema(Schema): class Meta: include = { "x": fields.Int(required=True), "y": fields.Int(required=True), } include = { "coordinates": fields.List(fields.Nested(CoordinatesSchema)), "vendors": fields.Dict(keys=fields.Str(), values=fields.Nested(CoordinatesSchema)), } class StreamsMonitoringSchemaV1: class RequestSchema(RequestSchema, DelphiDefaultMarketMixin, DateRangeMixin): isrc = IsrcField(required=True) class ResponseSchema(Schema): class Meta: class LeanSchema(Schema): class Meta: include = { "lean_forward": fields.Int(required=True), "lean_back": fields.Int(required=True), } include = { "apple": fields.Dict(keys=fields.Str(), values=fields.Nested(LeanSchema)), "spotify": fields.Dict(keys=fields.Str(), values=fields.Nested(LeanSchema)), "amazon": fields.Dict(keys=fields.Str(), values=fields.Nested(LeanSchema)), } class DetailedTrackStreamsGraphViewV1: class RequestSchema(DelphiDefaultMarketMixin, DateRangeMixin, RequestSchema, RequiredAllVendorMixin): isrc = IsrcField(required=True) class ResponseSchema(Schema): class Meta: class CoordinatesSchema(Schema): class Meta: include = {"x": fields.Int(required=True), "y": fields.Dict()} include = { "coordinates": fields.List(fields.Nested(CoordinatesSchema)), "vendor": fields.String(required=True), } class DetailedTrackStreamsGraphViewV2(DetailedTrackStreamsGraphViewV1): pass class BulkTracksStreamsSchemaV1: class RequestSchema( RequestSchema, DelphiDefaultMarketMixin, MultipleVendorsMixin, get_isrc_list_mixin(), DateRangeMixin, PerVendorMixin, ): pass class ResponseSchema(Schema): class Meta: include = { "streams": fields.Dict(keys=fields.Str(), values=fields.Int()), "vendors": fields.Dict(keys=fields.Str(), values=fields.Dict()), } class TrackMarketsStreamsListSchemaV1: class RequestSchema(RequestSchema, DateRangeMixin, LimitMixin, MultipleVendorsMixin): isrc = IsrcField(required=True) class ResponseSchema(Schema): class Meta: class TrackMarketSchema(Schema): class Meta: include = { "market": fields.Str(), "start_date": fields.Str(), "end_date": fields.Str(), "current_streams": fields.Int(), "prev_streams": fields.Int(), } include = { "markets": fields.List(fields.Nested(TrackMarketSchema)), "dates": fields.Dict(), } class TrackMarketsListGraphSchemaV1: class RequestSchema(RequestSchema, DateRangeMixin, DelphiMarketsListMixin, PerVendorMixin): isrc = IsrcField(required=True) class ResponseSchema(Schema): class Meta: class MarketCoordinatesSchema(Schema): class Meta: class CoordinatesSchema(Schema): class Meta: include = { "x": fields.Int(required=True), "y": fields.Int(required=True), } include = { "market": fields.Str(), "coordinates": fields.List((fields.Nested(CoordinatesSchema))), "vendors": fields.Dict(keys=fields.Str(), values=fields.Nested(CoordinatesSchema)), } include = { "markets": fields.List(fields.Nested(MarketCoordinatesSchema)), "dates": fields.Dict(), } class TracksStationsStreamsSummarySchema: class RequestSchema(RequestSchema, get_isrc_list_mixin(), MarketMixin): days_list = SplitUniqueListField( fields.Int(), data_key="days", required=False, validate=validate.Length(min=1), missing=[7, 1], ) min_streams = fields.Int(validate=validate.Range(min=0), data_key="min", missing=None, required=False) compact = fields.Boolean(required=False, missing=True) class ResponseSchema(Schema): class Meta: class TrackStationSchema(Schema): class Meta: include = { "id": fields.Str(), "total": fields.List(fields.Int()), "markets": fields.List(fields.Raw()), } include = { "items": fields.List(fields.Nested(TrackStationSchema)), "last_date": fields.Date(), } class TrackInPlaylistStreamsSchema: class RequestSchema( RequestSchema, get_isrc_list_mixin(), RequiredAllVendorMixin, DelphiMarketsListMixin, DateRangeMixin ): playlist_id = fields.Str(required=True) class ResponseSchema(Schema): class Meta: class TrackStreamsInPlaylistSchema(Schema): class Meta: include = { "isrc": fields.Str(), "current_range": fields.Int(), "prev_range": fields.Int(), } include = { "items": fields.List(fields.Nested(TrackStreamsInPlaylistSchema)), "playlist_id": fields.Str(), "dates": fields.Dict(), } class ShortSummaryResponseSchema(Schema): class Meta: include = { "current_date": fields.Raw(), "previous_date": fields.Raw(), "current_week": fields.Raw(), "previous_week": fields.Raw(), } class YoutubeShortSummarySchema: class RequestSchema(DelphiCountryCodeMixin, ContentTypeListMixin, LatestDateMixin, RequestSchema): isrc = IsrcField(required=True) only = SplitUniqueListField( fields.String(required=True, validate=validate.OneOf(VideosOnly.ALL_FIELDS)), missing=VideosInclude.ONLY_MAPPING[VideosInclude.VIEWS], ) class ResponseSchema(ShortSummaryResponseSchema): pass class YoutubeTopMarketsSchema: class RequestSchema(RequestSchema, DateRangeMixin): video_id_list = SplitUniqueListField( VendorPrefixField(vendor=YOUTUBE, required=True), data_key="video_id", validate=validate.Length(min=1), required=True, ) limit = fields.Int(missing=15) sort_by = SplitUniqueListField( fields.String(validate=validate.OneOf(MarketOrder.values())), default=[YoutubeVideosOrder.VIEWS_DESC.value], missing=[YoutubeVideosOrder.VIEWS_DESC.value], ) include_worldwide = fields.Boolean(missing=False) keep_zero_streams = fields.Boolean(missing=True) include = SplitUniqueListField( fields.String(required=True, validate=validate.OneOf(["percentage"])), missing=[], ) class ResponseSchema(Schema): class YoutubeTopMarketsItemSchema(Schema): market = fields.Str() views = fields.Int() percentage = fields.Float(required=False) items = fields.List(fields.Nested(YoutubeTopMarketsItemSchema)) class TrackPlaylistsStreams: class Request(RequestSchema, RequiredAllVendorMixin, DateRangeMixin): isrc = IsrcField(required=False) items = NotEmptyDictField(required=True) chunk_size = fields.Integer(default=20, missing=20, min=1, max=40) @post_load def check_markets(self, data, **kwargs): items = data["items"] for pl_id, markets in items.items(): data["items"][pl_id] = [convert_market(value, DELPHI_GLOBAL_MARKET) for value in markets] return data class Response(Schema): class Meta: class PlaylistStreamsItem(Schema): class Meta: include = { "id": fields.Str(), "current_streams": fields.Int(), "prev_streams": fields.Int(), "country_code": GlobalMarketOutputField(), } include = { "items": fields.List(fields.Nested(PlaylistStreamsItem)), "dates": fields.Dict(), } class TrackTopPlaylistsStreamsSchema: class RequestSchema(RequestSchema, VendorRequiredMixin, DateRangeMixin): isrc = IsrcField(required=True) playlists_ids = SplitUniqueListField(fields.String(), data_key="playlist_id", missing=None, default=None) markets = SplitUniqueListField(DelphiMarketField, data_key="market", default=None, missing=None) chunk_size = fields.Integer(default=20, missing=20, min=1, max=40) class ResponseSchema(Schema): class Meta: class PlaylistStreamsItem(Schema): class Meta: include = { "id": fields.Str(), "current_streams": fields.Int(), "prev_streams": fields.Int(), "country_code": GlobalMarketOutputField(), } include = { "count": fields.Int(), "items": fields.List(fields.Nested(PlaylistStreamsItem)), "dates": fields.Dict(), } class PlaylistsStreamsGraphSchema: class RequestSchema(RequestSchema, DateRangeMixin, RequiredAllVendorMixin, DelphiMarketsListMixin): playlists_ids = SplitUniqueListField(fields.String(), data_key="playlist_id", required=True) group_playlist_id_as = fields.String(missing=None) group_market_as = fields.String(missing=None) class ResponseSchema(Schema): class Meta: class PlaylistStreamsItem(Schema): class Meta: class CoordinatesSchema(Schema): class Meta: include = { "y": fields.Int(missing=None, default=None), "x": fields.Int(required=True), } include = { "playlist_id": fields.String(), "country_code": GlobalMarketOutputField(), "coordinates": fields.List(fields.Nested(CoordinatesSchema)), } include = { "dates": fields.Dict(), "items": fields.List(fields.Nested(PlaylistStreamsItem)), } class SinglePlaylistDateRangeStreamsSchema: class RequestSchema(RequestSchema, DateRangeMixin, DelphiDefaultMarketMixin, RequiredAllVendorMixin): playlist_id = fields.String(required=True) class ResponseSchema(Schema): class Meta: include = { "market": GlobalMarketOutputField(), "dates": fields.Dict(), "prev_range": fields.Integer(default=None, missing=None), "current_range": fields.Integer(default=None, missing=None), } class TiktokShortSummarySchema: class RequestSchema(DelphiCountryCodeMixin, ContentTypeListMixin, LatestDateMixin, RequestSchema): isrc = IsrcField(required=True) only = SplitUniqueListField( fields.String( required=True, validate=validate.OneOf(TiktokTrackAnalyticsMetric.ALL_FIELDS), ), missing=TiktokTrackAnalyticsMetric.ALL_FIELDS, ) class ResponseSchema(ShortSummaryResponseSchema): pass class MetricsRequestSchema(get_isrc_list_mixin(), RequestSchema): latest_date = fields.Date(format="iso", data_key="date", missing=None) dsp_list = SplitUniqueListField( fields.String(required=True, validate=validate.OneOf((SPOTIFY, TIKTOK))), data_key="dsp", missing=(SPOTIFY, TIKTOK), ) metric_list = SplitUniqueListField( fields.String(required=True, validate=validate.OneOf(MetricsTrendsValuesMetric.ALL_FIELDS)), data_key="metric", missing=MetricsTrendsValuesMetric.ALL_FIELDS, ) @validates_schema def validate_data(self, data: dict, *args, **kwargs): dsp_list, metric_list = data["dsp_list"], data["metric_list"] for metric in metric_list: if MetricsTrendsValuesMetric.DSP_MAPPING[metric] not in dsp_list: raise ValidationError(f"Missing {MetricsTrendsValuesMetric.DSP_MAPPING[metric]} in DSP list.") class MetricsTrendsSchema: class RequestSchema(DelphiDefaultMarketMixin, MetricsRequestSchema): pass class ResponseSchema(Schema): class ItemSchema(Schema): class ItemLatestSchema(Schema): date = fields.Date() value = fields.Integer() trend = fields.Integer() class ItemWeekSchema(Schema): class ItemWeekAverageSchema(Schema): value = fields.Integer() trend = fields.Integer() start_date = fields.Date() end_date = fields.Date() value = fields.Integer() trend = fields.Integer() average = fields.Nested(ItemWeekAverageSchema) data = fields.List(fields.Integer()) dsp = fields.String() metric = fields.String() latest = fields.Nested(ItemLatestSchema) week = fields.Nested(ItemWeekSchema) items = fields.List(fields.Nested(ItemSchema)) class TikTokBaseRequestSchema(Schema, DelphiCountryCodeMixin, get_isrc_list_mixin()): latest_date = fields.Date(format="iso", missing=None) class TikTokCountryCodeTopSchema(Schema): class RequestSchema(Schema, get_isrc_list_mixin(), PaginationWithDefaultLimitMixin): latest_date = fields.Date(format="iso", missing=None) class ResponseSchema(Schema): class CountryCodeSchema(Schema): market = fields.String() cnt = fields.Integer() percentage = fields.Integer() video_views = fields.List(fields.Nested(CountryCodeSchema)) creations = fields.List(fields.Nested(CountryCodeSchema)) latest_date = fields.Date(format="iso") class TikTokCountryCodeSchema: class RequestSchema(Schema, get_isrc_list_mixin()): latest_date = fields.Date(format="iso", missing=None) class ResponseSchema(Schema): country_codes = fields.List(DelphiMarketField(required=True, validate=validate.Length(min=2, max=9))) class TikTokTrendsResponseSchema(Schema): class NestedSchema(Schema): class ItemLatestSchema(Schema): date = fields.Date() value = fields.Integer() trend = fields.Integer() class ItemWeekSchema(Schema): start_date = fields.Date() end_date = fields.Date() value = fields.Integer() trend = fields.Integer() latest = fields.Nested(ItemLatestSchema) week = fields.Nested(ItemWeekSchema) video_views = fields.Nested(NestedSchema) creations = fields.Nested(NestedSchema) class TiktokResponseInsightSchema(Schema): peak_date = fields.Date() creations = fields.Integer() views = fields.Integer() class TikTokGraphResponseSchema(Schema): class NestedSchema(Schema): class PointSchema(Schema): x = fields.Integer() y = fields.Integer() total = fields.Integer() graph = fields.List(fields.Nested(PointSchema)) video_views = fields.Nested(NestedSchema) creations = fields.Nested(NestedSchema) class MetricsMarketsWeekValuesSchema: class RequestSchema(DelphiMarketMixin, MetricsRequestSchema): pass class ResponseSchema(Schema): class ItemSchema(Schema): class ItemWeekSchema(Schema): class ItemWeekValueSchema(Schema): worldwide = fields.Integer() market = fields.Integer() start_date = fields.Date() end_date = fields.Date() value = fields.Nested(ItemWeekValueSchema) market = fields.String() percentage = fields.Integer() dsp = fields.String() metric = fields.String() week = fields.Nested(ItemWeekSchema) items = fields.List(fields.Nested(ItemSchema)) class ShazamCountries: class Response(Schema): items = fields.List(fields.String()) class ShazamCities: class Request(DelphiMarketNoneMixin, RequestSchema): pass class Response(Schema): class Item(Schema): id = fields.String(attribute="city_id") name = fields.String(attribute="city_name") market = fields.String() items = fields.List(fields.Nested(Item)) class CityNoneMixin: city = fields.String(missing=None) class ShazamCharts: class Request(DelphiRequiredMarketMixin, CityNoneMixin, RequestSchema): chart_date = fields.Date(format="iso", data_key="date", required=True) is_sony = fields.Boolean(missing=None) class Response(Schema): class Item(Schema): class ItemTrack(Schema): class ItemTrackArtist(Schema): name = fields.String(attribute="full_name") isrc = fields.String(attribute="isrc") cover = fields.String(attribute="image.uri") name = fields.String(attribute="name") id = fields.String(attribute="apple_track_id") artists = fields.List(fields.Nested(ItemTrackArtist)) is_sony = fields.Boolean(attribute="is_sony") release_date = DateStringField(attribute="release_date") class ItemPeak(Schema): position = fields.Integer(attribute="min_position") date = DateStringField(attribute="min_position_date") track = fields.Nested(ItemTrack) position = fields.Integer(attribute="position.current") trend = fields.Function( lambda item: ( -10000 if item["position"]["is_entry"] or item["position"]["is_reentry"] else item["position"]["current"] - item["position"]["previous_position"] ) ) is_new = fields.Boolean(attribute="position.is_entry") is_re_entry = fields.Boolean(attribute="position.is_reentry") peak = fields.Nested(ItemPeak, attribute="summary") shazams = fields.Integer(attribute="summary.total_shazams") days_on_chart = fields.Integer(attribute="summary.total_days") added_date = DateStringField(attribute="summary.earliest_position_date") latest_date = DateStringField(attribute="summary.latest_position_date") items = fields.List(fields.Nested(Item)) class ShazamChartsPositions: class Request(DateRangeMixin, DelphiRequiredMarketMixin, CityNoneMixin, RequestSchema): isrc = IsrcField(required=True) class Response(Schema): class City(Schema): id = fields.String(attribute="city_id") name = fields.String(attribute="city_name") class Position(Schema): peak = fields.Integer() min = fields.Integer() class Value(Schema): position = fields.Integer(attribute="position.current") date = DateStringField(attribute="position.date") is_peak = fields.Function(lambda item, context: item["position"]["current"] == context["min_position"]) is_min = fields.Function(lambda item, context: item["position"]["current"] == context["max_position"]) isrc = fields.String() start_date = DateStringField() end_date = DateStringField() days = fields.Integer() city = fields.Nested(City) market = fields.String() position = fields.Nested(Position) values = fields.List(fields.Nested(Value)) class TrackVideos: class Request(RequestSchema, PaginationMixin): isrc = IsrcField(required=True) views = fields.Integer(required=False, default=1000, missing=1000) sort_by = SplitUniqueListField( fields.String(validate=validate.OneOf(YoutubeVideosOrder.values())), default=[ YoutubeVideosOrder.VIEWS_DESC.value, YoutubeVideosOrder.VIDEO_TITLE.value, ], missing=[ YoutubeVideosOrder.VIEWS_DESC.value, YoutubeVideosOrder.VIDEO_TITLE.value, ], ) class Response(Schema): class YoutubeVideo(Schema): channel_id = fields.String() channel_title = fields.String() content_type = fields.String() date_uploaded = DateStringField() dsp = fields.String() dsp_channel_id = fields.String() dsp_video_id = fields.String() duration_seconds = fields.Integer() thumbnails = fields.Dict() video_id = fields.String() video_title = fields.String() views = fields.Integer() items = fields.List(fields.Nested(YoutubeVideo)) class TrackPerformance: """Visited tracks schema.""" class Request(RequestSchema, DelphiCountryCodeMixin): video_id_list = SplitUniqueListField( VendorPrefixField(vendor=YOUTUBE, required=True), data_key="video_id", validate=validate.Length(min=1), required=True, ) latest_date = DateStringField(required=True) class Response(Schema): class TrafficSource(Schema): sources = fields.List(fields.String()) percentage = fields.Int() class Trends(Schema): weekly_views = fields.Int() weekly_trend = fields.Int() latest_date_views = fields.Int() daily_trend = fields.Int() class DailyMetrics(Schema): class TrafficSourceTypes(Schema): browse_features = fields.Int() direct_or_unknown = fields.Int() external = fields.Int() hashtag_pages = fields.Int() interactive_video_endscreen = fields.Int() notifications = fields.Int() other_youtube_features = fields.Int() playlist_pages = fields.Int() playlists = fields.Int() product_pages = fields.Int() programming_from_claimed_content = fields.Int() shorts = fields.Int() sound_pages = fields.Int() stories = fields.Int() suggested_videos = fields.Int() unknown = fields.Int() video_cards_and_annotations = fields.Int() youtube_advertising = fields.Int() youtube_channels = fields.Int() youtube_search = fields.Int() date = fields.String() timestampt = fields.Int() views = fields.Int() average_view_duration_percentage = fields.Float() average_view_duration_seconds = fields.Float() comments = fields.Int() dislikes = fields.Int() likes = fields.Int() shares = fields.Int() traffic_source_types = fields.Nested(TrafficSourceTypes) biggest_traffic_source = fields.Nested(TrafficSource) trends = fields.Nested(Trends) daily_metrics = fields.List(fields.Nested(DailyMetrics)) class TrackStreamsGraph: class Request(RequestSchema, DelphiCountryCodeMixin): video_id_list = SplitUniqueListField( VendorPrefixField(vendor=YOUTUBE, required=True), data_key="video_id", validate=validate.Length(min=1), required=True, ) latest_date = DateStringField(required=True) class Response(Schema): class CoordinatesSchema(Schema): x = fields.Integer() y = fields.Dict() coordinates = fields.List(fields.Nested(CoordinatesSchema)) class YoutubeDemographics: """Visited tracks schema.""" class Request(RequestSchema, DelphiCountryCodeMixin): video_id_list = SplitUniqueListField( VendorPrefixField(vendor=YOUTUBE, required=True), data_key="video_id", validate=validate.Length(min=1), required=True, ) start_date = DateStringField(required=True) end_date = DateStringField(required=True) class Response(Schema): age_band_most_popular = fields.String() age_band_percentage = fields.String() gender_percentage = fields.String() gender_most_popular = fields.String() class YoutubeTopMarkets: """Top Youtube markets schema.""" class Request(RequestSchema): video_id = fields.String(required=True) limit = fields.Integer(missing=15, default=15) start_date = DateStringField(required=True) end_date = DateStringField(required=True) class Response(Schema): class Item(Schema): market = fields.String() views = fields.Integer() percentage = fields.Float() items = fields.List(fields.Nested(Item))