from collections import defaultdict from datetime import date, datetime, time from typing import Any, Dict, Iterable, List, Optional, Set, Tuple from apollo_notifications.constants import APPLE_CATEGORY_ID, SONY_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, or_, select, union_all from sqlalchemy.orm import Query from apollo_main_db import AppleMusicPlaylistStatistics, ApplePlaylist from apollo_main_db.apollo import ApolloKeyValueStorage, BuzzUser from apollo_main_db.push_notifications import UserDeviceToken, VendorEnum class ApplePlaylistClient(NotificationsPlaylistClient): """Class to get Apple Music playlist data.""" vendor = VendorEnum.APPLE.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_market_to_playlists: Optional[Dict[str, Iterable[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_market_to_playlists (dict): map of markets to list of allowed playlist for them. Returns: Query with playlist_id, playlist_name, playlist_image_url date, country_code. """ filters = [ AppleMusicPlaylistStatistics.tracklist_updated_at > min_datetime, AppleMusicPlaylistStatistics.tracklist_updated_at <= max_datetime, ] if exclude: filters.append(AppleMusicPlaylistStatistics.playlist_id.notin_(exclude)) if filter_by_market_to_playlists: filters.append( or_( *[ and_( AppleMusicPlaylistStatistics.market == _market, AppleMusicPlaylistStatistics.playlist_id.in_(playlists), ) for _market, playlists in filter_by_market_to_playlists.items() ] ) ) playlists = ( self.session.query( AppleMusicPlaylistStatistics.playlist_id.label("playlist_id"), AppleMusicPlaylistStatistics.market.label("country_code"), func.max(AppleMusicPlaylistStatistics.tracklist_updated_at).label("date"), ApplePlaylist.name.label("playlist_name"), ApplePlaylist.artwork.label("playlist_image_url"), ) .filter(*filters) .join(ApplePlaylist, ApplePlaylist.id == AppleMusicPlaylistStatistics.playlist_id) .join(BuzzUser, BuzzUser.user_name == ApplePlaylist.curator_id) .filter(BuzzUser.category_id.in_([APPLE_CATEGORY_ID, SONY_CATEGORY_ID])) .group_by(AppleMusicPlaylistStatistics.playlist_id, AppleMusicPlaylistStatistics.market) ) return playlists def _get_starred_playlists_subquery( self, market_to_playlists_playlists_to_users_map: Dict[str, Dict[str, Set[str]]] ) -> Query: """Create subquery with virtual starred playlists table based on passed mapping. Returns: Query with market, playlist_id, user_id. """ rows = [] for market, playlist_to_users_map in market_to_playlists_playlists_to_users_map.items(): for playlist_id, users in playlist_to_users_map.items(): for user_id in users: rows.append((market, playlist_id, user_id)) statements = [ select( [ cast(literal(m), String).label("country_code"), cast(literal(p), String).label("playlist_id"), cast(literal(u), String).label("user_id"), ] ) if index == 0 else select([literal(m), literal(p), literal(u)]) # no type cast for index, (m, p, u) in enumerate(rows) ] return union_all(*statements).alias("starred_playlists_tbl") def filter_by_starred_and_active( self, playlists_query: Query, market_to_playlists_to_users_map: Dict[str, Dict[str, Set[str]]] ) -> Query: """Filtering playlists query by active users, join with User devices. Args: playlists_query: Query with pla aliased columns. market_to_playlists_to_users_map: Dict of market to dictionaries of playlist_id to interested users set. Returns: Query with playlist_id, playlist_name, date, country_code, user_id, device_tokens. """ playlists_subquery = playlists_query.subquery() starred_playlists_subquery = self._get_starred_playlists_subquery(market_to_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, and_( starred_playlists_subquery.c.playlist_id == playlists_subquery.c.playlist_id, starred_playlists_subquery.c.country_code == playlists_subquery.c.country_code, ), ) .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, market_to_playlists_to_users_map: Dict[str, Dict[str, Set[str]]], exclude: Optional[List[str]] = None, ) -> 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_market_to_playlists={m: pl.keys() for m, pl in market_to_playlists_to_users_map.items()}, exclude=exclude, ) playlists = self.filter_by_starred_and_active( playlists_query=updated_playlists_query, market_to_playlists_to_users_map=market_to_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