from flask import jsonify from flask.views import MethodView from flask_apispec import MethodResource, doc, marshal_with, use_kwargs from http import HTTPStatus import src.legacy.spotify.serializers as spotify_serializers from src.constants.core import ZERO_TREND from src.constants.include import NMFTrackInclude, TrackPlaylistHistoryInclude, TrackPlaylistInclude from src.db import values from src.legacy.core.pagination import paginate from src.legacy.core.util import DateRange, generate_coordinates, get_previous_date_range, multikeysort from src.legacy.redis_db import keys from src.legacy.spotify.vendor import SpotifyNewMusicFriday, SpotifyPlaylists, SpotifyStreams from src.swagger import DictTemplate, ListTemplate, StringTemplate @doc("Endpoint to get track's playlists trends and change dates.") class TrackPlaylistsTrendsViewV1(MethodResource): @use_kwargs(spotify_serializers.TrackPlaylistsTrendsViewParams, location="query") @marshal_with(None, code=200, description="Track's playlists trends and change dates.", apply=False) @marshal_with(None, code=401, description="Authentication failed") def get(self, **data): positions_data = SpotifyPlaylists().get_positions_changes_in_playlists(data["playlist_ids"], data["isrc"]) result = {} for pl_id, pl_data in positions_data.items(): curr_position, prev_position = pl_data["current_position"], pl_data["previous_position"] result[pl_id] = { "position": curr_position - prev_position if curr_position and prev_position else ZERO_TREND, "changed_date": pl_data["changed_date"], } return jsonify(result) @doc( "Returns personalized playlists IDs only from passed IDs, or returns all personalized playlists IDs, " "if 'playlists_ids' is not set." ) class PersonalizedPlaylistsView(MethodResource): @use_kwargs(spotify_serializers.PersonalizedPlaylistsViewParams, location="query") @marshal_with(ListTemplate(StringTemplate()), code=200, description="Personalized playlists IDs.", apply=False) def get(self, **data): return jsonify(SpotifyPlaylists().get_personalized_playlists(data.get("playlist_ids"))) class TracksPlaylistsStatsView(MethodView): def get(self): data = spotify_serializers.TracksPlaylistsStatsParams().load_from_request() output_schema = spotify_serializers.TracksPlaylistsStatsOutputSchema() return jsonify( output_schema.dump( SpotifyPlaylists().get_tracks_playlists_stats(data["isrc_list"], data["categories"], data["dates"]), many=True, ) ) @doc("Returns list of tracks in Spotify personalized playlist.") class PersonalizedPlaylistTrackListView(MethodResource): @use_kwargs(spotify_serializers.PersonalizedPlaylistTracklistViewParams, location="query") @marshal_with( spotify_serializers.TracklistPlaylistOutputSchema, code=200, description="Tracklist in Spotify personalized playlist.", apply=True, ) def get(self, **data): playlist_id = data["playlist_id"] playlist_vendor = SpotifyPlaylists() personalized_playlist_tracklist = playlist_vendor.get_personalized_playlist_tracklist(playlist_id) return jsonify({"items": personalized_playlist_tracklist, "count": len(personalized_playlist_tracklist)}) @doc("Returns list of tracks and list of keys in Spotify personalized playlist.") class PersonalizedPlaylistTrackListViewV1(MethodResource): @use_kwargs(spotify_serializers.PersonalizedPlaylistTracklistViewParams, location="query") @marshal_with( spotify_serializers.PersonalizedPlaylistTrackListV1OutputSchema, code=200, description="Tracklist and track_keys in Spotify personalized playlist.", apply=False, ) def get(self, **data): playlist_id = data["playlist_id"] playlist_vendor = SpotifyPlaylists() tracklist, tracks_keys = playlist_vendor.get_personalized_playlist_tracklist_data(playlist_id) return jsonify(tracklist=tracklist, tracks_keys=tracks_keys) @doc("Returns playlist tracks dates in Spotify playlist.") class PlaylistTracksDatesView(MethodResource): @use_kwargs(spotify_serializers.PersonalizedPlaylistTracklistViewParams, location="query") @marshal_with( DictTemplate(DictTemplate("date", example={"earliest_added_date": "2018-11-17", "update_date": "2021-10-18"})), code=200, description="Playlist tracks dates in Spotify personalized playlist.", apply=False, ) def get(self, **data): playlist_id = data["playlist_id"] playlist_vendor = SpotifyPlaylists() result = playlist_vendor.get_playlist_tracks_dates(playlist_id) return jsonify(result) @doc("Returns track data from db") class TrackInPlaylist(MethodResource): @use_kwargs(spotify_serializers.TrackInPlaylistViewParams, location="query") @marshal_with( spotify_serializers.TrackInPlaylistOutputSchema, code=200, description="Track data in Spotify playlist from db.", apply=False, ) @marshal_with(None, code=401, description="Authentication failed.") def get(self, **data): isrc, playlist_id, playlist_type = data["isrc"], data["playlist_id"], data["playlist_type"] playlist_vendor = SpotifyPlaylists() result = playlist_vendor.get_track_data_in_playlist(isrc, playlist_id, playlist_type) return jsonify(result) @doc("Returns track position changes in spotify playlist from db") class TrackPositionChangesInPlaylist(MethodResource): @use_kwargs(spotify_serializers.TrackInPlaylistPositionChangesViewParams, location="query") @marshal_with( DictTemplate(DictTemplate(StringTemplate("date"))), code=200, description="Track position changes data in Spotify playlist from db.", apply=False, ) @marshal_with(None, code=401, description="Authentication failed.") def get(self, **data): isrc, playlist_id = data["isrc"], data["playlist_id"] playlist_vendor = SpotifyPlaylists() result = playlist_vendor.get_track_in_playlists_position_changes(isrc, playlist_id) return jsonify(result) @doc("Returns Spotify track's playlists list.") class TrackPlaylistsViewV1(MethodResource): @use_kwargs(spotify_serializers.TrackPlaylistsV1ViewParams, location="query") @marshal_with(None, code=401, description="Authentication failed.") @marshal_with( spotify_serializers.TrackPlaylistsV1OutputSchema, code=200, description="Track data in Spotify playlist.", apply=False, ) @paginate(cache_key_template=keys.SPOTIFY_TRACK_PLAYLISTS_v1, full_data=True) def get(self, **data): isrc_list = data["isrc_list"] vendor = SpotifyPlaylists() track_playlists_data = vendor.get_track_playlists_data( isrc_list=isrc_list, markets_list=data["markets_list"], search=data["search"], playlists_ids=None, include_track_count=False, include_username=False, include_streams_7_days=False, include_followers=False, streams_markets_list=None, include_owner=False, category_id=None, ) playlists_map, playlist_ids = vendor.flatten_playlists_data(track_playlists_data, True) trends_map = vendor.get_track_playlists_trends_data(isrc_list, playlists_map) items = vendor.extend_playlists_data_with_trends(playlists_map, trends_map) items = vendor.show_recent_adds(items, data["recent_adds_only"]) items = multikeysort(items, data["order_by"]) return items @doc("Returns Spotify track's previous playlists list.") class TrackPreviousPlaylistsViewV1(MethodResource): @use_kwargs(spotify_serializers.TrackPreviousPlaylistsV1ViewParams, location="query") @marshal_with(None, code=401, description="Authentication failed.") @marshal_with( spotify_serializers.TrackPreviousPlaylistsV1OutputSchema, code=200, description="Track data in Spotify playlist.", apply=False, ) @paginate(cache_key_template=keys.SPOTIFY_TRACK_PREVIOUS_PLAYLISTS_v1, full_data=True, response_kwargs=True) def get(self, **data): include = data["include"] vendor = SpotifyPlaylists() track_previous_playlists_data = vendor.get_track_previous_playlists_data( data["isrc_list"], markets_list=data["markets_list"], search=data["search"], exclude_current_playlists=data["exclude_current_playlists"], include_owner=( TrackPlaylistHistoryInclude.OWNER.value in include or TrackPlaylistHistoryInclude.CATEGORIES.value in include ), include_followers=TrackPlaylistHistoryInclude.FOLLOWERS.value in include, category_id=data["category_id"], ) if TrackPlaylistHistoryInclude.CATEGORIES.value in include: categories = dict(categories=vendor.get_playlists_categories(track_previous_playlists_data)) else: categories = dict(categories=[]) items, playlist_ids = vendor.flatten_playlists_data(track_previous_playlists_data) items = vendor.show_recent_removals(items, data["recent_removals_only"]) items = multikeysort(items, data["order_by"]) return items, categories @doc("Returns playlists' markets data.") class PlaylistsMarketsViewV1(MethodResource): @marshal_with(None, code=401, description="Authentication failed.") @use_kwargs(spotify_serializers.PlaylistsMarketsInputSchema, location="query") @marshal_with( spotify_serializers.PlaylistsMarketsOutputSchema, code=200, description="Playlists markets data.", apply=False ) def get(self, **data): items = SpotifyPlaylists().get_playlists_markets(data["playlist_ids"]) return {"items": items} @doc("Returns playlists' data in selected market.") class PlaylistsDataViewV1(MethodResource): @use_kwargs(spotify_serializers.PlaylistsMarketDataInputSchema, location="query") @marshal_with(None, code=401, description="Authentication failed.") @marshal_with( spotify_serializers.PlaylistMarketDataOutputSchema, code=200, description="Playlists data in selected market.", apply=False, ) def get(self, **data): items = SpotifyPlaylists().get_playlists_data(data["playlist_ids"], data["include_owner_data"]) return {"items": items} @doc("Returns a list of track's Spotify playlists.") class TrackPlaylistsViewV2(MethodResource): @use_kwargs(spotify_serializers.TrackPlaylistsV2ViewParams, location="query") @marshal_with(None, code=HTTPStatus.UNAUTHORIZED, description="Authentication failed.") @marshal_with( spotify_serializers.TrackPlaylistsV2OutputSchema, code=HTTPStatus.OK, description="List of track's Spotify playlist.", apply=False, ) @paginate(cache_key_template=keys.SPOTIFY_TRACK_PLAYLISTS_v2, full_data=True, response_kwargs=True) def get(self, **data): isrc_list, include, order_by = sorted(data["isrc_list"]), data["include"], data.get("order_by") vendor = SpotifyPlaylists() track_playlists_data = vendor.get_track_playlists_data( isrc_list=isrc_list, markets_list=data["markets_list"], search=data["search"], playlists_ids=None, include_track_count=(TrackPlaylistInclude.TRACK_COUNT.value in include), include_username=(TrackPlaylistInclude.USERNAME.value in include), include_streams_7_days=(TrackPlaylistInclude.PLAYLIST_STREAMS.value in include), include_followers=(TrackPlaylistInclude.FOLLOWERS.value in include), streams_markets_list=data["streams_markets_list"], include_owner=( TrackPlaylistInclude.OWNER.value in include or TrackPlaylistInclude.CATEGORIES.value in include ), category_id=data["category_id"], ) if TrackPlaylistInclude.CATEGORIES.value in include: categories = dict(categories=vendor.get_playlists_categories(track_playlists_data)) else: categories = dict(categories=[]) items, _ = vendor.flatten_playlists_data(track_playlists_data) if order_by: items = multikeysort(items, data["order_by"]) return items, categories @doc("Returns playlists listeners graph coordinates for selected date range.") class PlaylistsListenersGraphView(MethodResource): @use_kwargs(spotify_serializers.PlaylistsListenersGraphV1ViewParams, location="query") @marshal_with( spotify_serializers.PlaylistsListenersGraphV1OutputSchema, code=HTTPStatus.OK, description="Playlists listeners graph coordinates.", apply=False, ) @paginate(full_data=True, response_kwargs=True) def get(self, **data): vendor = SpotifyPlaylists() date_range = DateRange(start=data["start_date"], end=data["end_date"]) playlists_listeners_items = vendor.get_playlists_listeners_graph_items( data["playlist_id"], data["market"], date_range ) items = [] for playlist_item in playlists_listeners_items: playlist_id = playlist_item["id"] for market, market_coordinates in playlist_item.get("markets", {}).items(): # Fix Redis jsonified coordinates data: cast timestamps to integers. _market_coordinates = {int(ts): listeners for ts, listeners in market_coordinates.items()} items.append( { "playlist_id": playlist_id, "country_code": market, "coordinates": generate_coordinates(_market_coordinates, date_range.start, date_range.end), } ) return items, dict(dates=date_range.to_dict(isoformat=True)) @doc("Returns playlists listeners num in selected date range and market.") class PlaylistListenersDateRangeView(MethodResource): @use_kwargs(spotify_serializers.PlaylistListenersDateRangeV1ViewParams, location="query") @marshal_with( spotify_serializers.PlaylistListenersDateRangeV1OutputSchema, code=HTTPStatus.OK, description="Playlist listeners num in selected date ranges.", ) def get(self, playlist_id: str, **data): vendor = SpotifyPlaylists() current_range = DateRange(start=data["start_date"], end=data["end_date"]) prev_range = get_previous_date_range(current_range) listeners_data = vendor.get_playlist_date_ranges_listeners( playlist_id, data["market"], current_range, prev_range ) result = { **listeners_data, "dates": { "current_range": current_range.to_dict(isoformat=True), "prev_range": prev_range.to_dict(isoformat=True), }, } return result @doc( tags=["spotify", "followers", "graph"], description="Returns playlists followers graph coordinates for selected date range (global market).", ) class PlaylistsFollowersGraphView(MethodResource): @use_kwargs(spotify_serializers.PlaylistsFollowersGraphV1ViewParams, location="query") @marshal_with( spotify_serializers.PlaylistsFollowersGraphV1OutputSchema, code=HTTPStatus.OK, description="Playlists followers graph coordinates.", apply=False, ) @paginate(full_data=True, response_kwargs=True) def get(self, **data): vendor = SpotifyPlaylists() date_range = DateRange(start=data["start_date"], end=data["end_date"]) return ( vendor.get_playlists_followers_graph_items(sorted(data["playlist_id"]), date_range), dict(dates=date_range.to_dict(isoformat=True)), ) @doc( tags=["spotify", "followers", "range"], description="Returns playlists followers total in selected date range (global market).", ) class PlaylistFollowersDateRangeView(MethodResource): @use_kwargs(spotify_serializers.PlaylistFollowersDateRangeV1ViewParams, location="query") @marshal_with( spotify_serializers.PlaylistFollowersDateRangeV1OutputSchema, code=HTTPStatus.OK, description="Playlists followers total in selected date ranges.", ) def get(self, playlist_id, **data): vendor = SpotifyPlaylists() current_range = DateRange(start=data["start_date"], end=data["end_date"]) prev_range = get_previous_date_range(current_range) followers_data = vendor.get_playlist_date_ranges_followers(playlist_id, current_range, prev_range) result = { **followers_data, "dates": { "current_range": current_range.to_dict(isoformat=True), "prev_range": prev_range.to_dict(isoformat=True), }, } return result @doc("New music friday available dates.") class NMFAvailableDates(MethodResource): @marshal_with( ListTemplate(StringTemplate()), code=HTTPStatus.OK, description="NMF available dates list.", apply=False ) @marshal_with(None, code=HTTPStatus.UNAUTHORIZED, description="Authentication failed") def get(self): """Get new music friday available dates. Returns: HTTP 200: Request is accepted. Response successful. Raises: HTTP 401: If authentication failed. """ return SpotifyNewMusicFriday().get_available_dates() @doc("New music friday playlists.") class NMFPlaylists(MethodResource): @marshal_with( spotify_serializers.NMFPlaylistsOutputSchema(many=True), code=HTTPStatus.OK, description="NMF playlists.", apply=True, ) @marshal_with(None, code=HTTPStatus.UNAUTHORIZED, description="Authentication failed") def get(self): """Get new music friday playlists. Returns: HTTP 200: Request is accepted. Response successful. Raises: HTTP 401: If authentication failed. """ return SpotifyNewMusicFriday().get_playlists(with_name=True) @doc("New music friday tracklist.") class NMFTracklist(MethodResource): @use_kwargs(spotify_serializers.NMFTracklistInputSchema, location="query") @marshal_with( spotify_serializers.NMFTracklistOutputSchema, code=HTTPStatus.OK, description="NMF tracks.", apply=False ) @marshal_with(None, code=HTTPStatus.UNAUTHORIZED, description="Authentication failed") @paginate(cache_key_template=keys.SPOTIFY_NMF_TRACKLIST, cache_ttl=keys.SPOTIFY_NMF_TTL, full_data=True) def get(self, **data): """Get new music friday tracks. Returns: HTTP 200: Request is accepted. Response successful. Raises: HTTP 401: If authentication failed. """ result = SpotifyNewMusicFriday().get_tracklist(data["nmf_date"]) return result @doc("Track in New music friday playlists.") class NMFTrackView(MethodResource): @use_kwargs(spotify_serializers.NMFTrackRequest, location="query") @marshal_with( spotify_serializers.NMFTrackResponse, code=HTTPStatus.OK, description="NMF track playlists.", apply=False ) @marshal_with(None, code=HTTPStatus.UNAUTHORIZED, description="Authentication failed") @paginate( cache_key_template=keys.SPOTIFY_NMF_TRACK, cache_ttl=keys.SPOTIFY_NMF_TRACK_TTL, full_data=True, response_kwargs=True, ) def get(self, **data): """Get new music friday tracks. Returns: HTTP 200: Request is accepted. Response successful. Raises: HTTP 401: If authentication failed. """ vendor = SpotifyNewMusicFriday() isrc, region, include, order_by = data["isrc"], data["region"], data["include"], data["order_by"] available_dates = vendor.get_available_dates() latest_date = max(available_dates) items, region_list, region_data, average_position, top_10_total = vendor.get_track_playlists( isrc=isrc, nmf_date=latest_date, include_inactive_markets=(NMFTrackInclude.INACTIVE_MARKETS.value in include), region_code=region, with_name=True, ) items = multikeysort(items, order_by) extra = { "positions_avg": average_position, "positions_top_total": top_10_total, "total": region_data.get("total", 0), } if NMFTrackInclude.TOP_PLAYLIST_IMAGE_URL.value in include: extra["top_playlist_image_url"] = items[0]["image_url"] if items else None if NMFTrackInclude.REGIONS.value in include: extra["regions"] = region_list return items, extra @doc("New music friday playlist update check") class NMFCompletenessView(MethodResource): @marshal_with( spotify_serializers.NMFCompletenessResponse, code=HTTPStatus.OK, description="NMF completeness.", apply=True, ) @marshal_with(None, code=HTTPStatus.UNAUTHORIZED, description="Authentication failed") def get(self): """Get recent update date and delayed playlists. Returns: HTTP 200: Request is accepted. Response successful. Raises: HTTP 401: If authentication failed. """ vendor = SpotifyNewMusicFriday() last_updated = values.get_value("nmf_job_run_ts") available_dates = vendor.get_available_dates() latest_date = max(available_dates) markets_delayed = vendor.get_delayed_playlists(latest_date) return { "last_updated": last_updated, "markets_delayed": markets_delayed, } @doc("Markets list sorted by the last month streams.") class MarketsByStreamsView(MethodResource): @marshal_with(ListTemplate(StringTemplate()), code=HTTPStatus.OK, description="Markets by the last month streams.") @marshal_with(None, code=HTTPStatus.UNAUTHORIZED, description="Authentication failed") def get(self): result = SpotifyStreams().get_markets_by_month_streams() return jsonify(result)