from collections import defaultdict from datetime import date, datetime, time from typing import Any, Dict, List, Optional, Set, Tuple from apollo_notifications.constants import SONY_CATEGORY_ID, SPOTIFY_CATEGORY_ID from apollo_notifications.playlists.client import NotificationsPlaylistClient from apollo_notifications.utils import dump_datetime, parse_gl_to_global from sqlalchemy import String, and_, cast, func, literal, select, union_all from sqlalchemy.orm import Query from apollo_main_db.apollo import ApolloKeyValueStorage from apollo_main_db.push_notifications import UserDeviceToken, VendorEnum from apollo_main_db.spotify import SpotifyPlaylist, SpotifyPlaylistStatistics class SpotifyPlaylistClient(NotificationsPlaylistClient): """Class to get Spotify playlist data.""" vendor = VendorEnum.SPOTIFY.name.lower() def __init__(self, config): super().__init__(config) self.last_update_key = config.LAST_RUN_DATE_KEY def get_last_update_datetime(self) -> Tuple[str, bool]: not_found = False last_datetime_str = self.session.query( ApolloKeyValueStorage.value).filter(ApolloKeyValueStorage.key == self.last_update_key).first() if last_datetime_str: return last_datetime_str.value, not_found return dump_datetime(datetime.combine(date.today(), time(hour=0))), True def set_last_update_datetime(self, last_update_str: str, create: bool = False): if create: self.session.add(ApolloKeyValueStorage(key=self.last_update_key, value=last_update_str)) else: self.session.query(ApolloKeyValueStorage).filter(ApolloKeyValueStorage.key == self.last_update_key)\ .update({ApolloKeyValueStorage.value: last_update_str}) self.session.commit() def get_updated_playlists_query( self, min_datetime: str, max_datetime: str, exclude: Optional[List[str]] = None, filter_by_playlists: Optional[List[str]] = None ) -> Query: """Get updated playlist by date Args: min_datetime (str): min datetime to filter playlists update after. max_datetime (str): max datetime to filter playlists update before. exclude (list): list of playlist ids to exclude. filter_by_playlists (list): list of playlist ids to filter by. Returns: Query with playlist_id, playlist_name, date, country_code (global is stored as '_gl' here). """ filters = [ SpotifyPlaylistStatistics.tracklist_updated_at > min_datetime, SpotifyPlaylistStatistics.tracklist_updated_at <= max_datetime, ] if exclude: filters.append( SpotifyPlaylistStatistics.playlist_id.notin_(exclude) ) if filter_by_playlists: filters.append( SpotifyPlaylistStatistics.playlist_id.in_(filter_by_playlists) ) playlists = self.session.query( SpotifyPlaylistStatistics.playlist_id.label('playlist_id'), SpotifyPlaylistStatistics.market.label('country_code'), func.max(SpotifyPlaylistStatistics.tracklist_updated_at).label('date'), SpotifyPlaylist.name.label('playlist_name') ).filter( *filters ).join( SpotifyPlaylist, and_( SpotifyPlaylist.id == SpotifyPlaylistStatistics.playlist_id, SpotifyPlaylist.buzz_category_id.in_((SONY_CATEGORY_ID, SPOTIFY_CATEGORY_ID)), ) ).group_by( SpotifyPlaylistStatistics.playlist_id, SpotifyPlaylistStatistics.market ) return playlists def _get_starred_playlists_subquery(self, starred_playlists_to_users_map: Dict[str, Set[str]]) -> Query: """Create subquery with virtual starred playlists table based on passed mapping. Returns: Query with playlist_id, user_id. """ rows = [] for playlist_id, users in starred_playlists_to_users_map.items(): for user_id in users: rows.append((playlist_id, user_id)) statements = [ select([cast(literal(i), String).label("playlist_id"), cast(literal(s), String).label("user_id")]) if index == 0 else select([literal(i), literal(s)]) # no type cast for index, (i, s) in enumerate(rows) ] return union_all(*statements).alias("starred_playlists_tbl") def filter_by_starred_and_active( self, playlists_query: Query, starred_playlists_to_users_map: Dict[str, Set[str]]) -> Query: """Filtering playlists query by active users, join with User devices. Args: playlists_query: Query with pla aliased columns. starred_playlists_to_users_map: Dict of playlist_id to interested users set. Returns: Query with playlist_id, playlist_name, date, country_code (global is stored as '_gl' here), user_id, device_tokens. """ playlists_subquery = playlists_query.subquery() starred_playlists_subquery = self._get_starred_playlists_subquery(starred_playlists_to_users_map) return self.session.query( starred_playlists_subquery.c.user_id.label('user_id'), func.group_concat( UserDeviceToken.token.op('ORDER BY')(UserDeviceToken.created_at.desc()) ).label('device_tokens'), playlists_subquery ).join( starred_playlists_subquery, starred_playlists_subquery.c.playlist_id == playlists_subquery.c.playlist_id ).join( UserDeviceToken, starred_playlists_subquery.c.user_id == UserDeviceToken.user_id ).filter( UserDeviceToken.is_active.is_(True), ).group_by( starred_playlists_subquery.c.user_id, playlists_subquery.c.playlist_id, ) def get_market_to_updated_playlists_map( self, min_datetime: str, max_datetime: str, starred_playlists_to_users_map: Dict[str, Set[str]], ) -> Dict[str, List[Any]]: """Get market to filtered updated playlists items map.""" updated_playlists_query = self.get_updated_playlists_query( min_datetime=min_datetime, max_datetime=max_datetime, filter_by_playlists=starred_playlists_to_users_map.keys() ) playlists = self.filter_by_starred_and_active( playlists_query=updated_playlists_query, starred_playlists_to_users_map=starred_playlists_to_users_map) market_to_playlists_map = defaultdict(list) for playlist in playlists: market_to_playlists_map[parse_gl_to_global(playlist.country_code)].append(playlist) return market_to_playlists_map