import logging import uuid from datetime import datetime, timedelta from typing import Any, List from apollo_push_client import Push, PushClient, PushData from botocore.client import BaseClient from redis import StrictRedis from sqlalchemy import and_, func from sqlalchemy.orm import Query, Session from apollo_main_db.apollo.models import ApolloMarket, StarredContent, UserMarket from apollo_main_db.apple.models import AppleMusicSong, ApplePlaylist, ApplePlaylistTracklistHistory, \ AppleWeeklyTopPlaylist from apollo_main_db.push_notifications.models import TopicEnum, UserDeviceToken, VendorEnum from apple_playlists_additions_notifications import constants from apple_playlists_additions_notifications.config import Config from apple_playlists_additions_notifications.main_db import session_scope from apple_playlists_additions_notifications.redis_db import get_key from apple_playlists_additions_notifications.utils import add_to_already_sent, build_push_message, is_new_track, \ get_blacklisted_ids, get_country_code, get_country_codes_from_db, get_country_flag def get_top_playlists_by_date(session: Session, market: str, current_date: str) -> List[str]: """Get top playlist by date Args: session (Session): DB session. market (str): Market. current_date (str): Date "%Y-%m-%d" format. Returns: List of playlist_id """ top_playlists_query = session.query( AppleWeeklyTopPlaylist.playlist_id, ).filter( AppleWeeklyTopPlaylist.country_code == market, AppleWeeklyTopPlaylist.rank <= constants.TOP_PLAYLIST_THRESHOLD, AppleWeeklyTopPlaylist.date.in_(( current_date, datetime.strptime(current_date, '%Y-%m-%d') - timedelta(days=1) )) ) return top_playlists_query def get_added_tracks_query( session: Session, current_date: str, previous_date: str, market: str) -> Query: """Get added tracks to top playlists between two dates. Args: session (Session): DB session. current_date (str): Current date for track history "%Y-%m-%d" format. previous_date (str): Previous date for track history "%Y-%m-%d" format. market (str): Market. Returns: List of tuple(playlist_id, song_id,) """ top_playlists_for_date = get_top_playlists_by_date(session, market, current_date) top_playlists_for_prev_date = get_top_playlists_by_date(session, market, previous_date) end_date_tracks_query = session.query( ApplePlaylistTracklistHistory.playlist_id.label('playlist_id'), ApplePlaylistTracklistHistory.song_id.label('song_id'), ).filter( ApplePlaylistTracklistHistory.playlist_id.in_(top_playlists_for_prev_date), ApplePlaylistTracklistHistory.storefront == market, ApplePlaylistTracklistHistory.date == previous_date ).subquery() start_date_tracks_query = session.query( ApplePlaylistTracklistHistory.playlist_id.label('playlist_id'), ApplePlaylistTracklistHistory.song_id.label('song_id'), ApplePlaylistTracklistHistory.position.label('position'), AppleMusicSong.name.label('song_name'), AppleMusicSong.isrc.label('isrc'), AppleMusicSong.artist_name.label('artist_name'), ApplePlaylist.name.label('playlist_name'), ).join( AppleMusicSong, AppleMusicSong.id == ApplePlaylistTracklistHistory.song_id ).join( ApplePlaylist, ApplePlaylist.id == ApplePlaylistTracklistHistory.playlist_id ).filter( ApplePlaylistTracklistHistory.playlist_id.in_(top_playlists_for_date), ApplePlaylistTracklistHistory.storefront == market, ApplePlaylistTracklistHistory.date == current_date, AppleMusicSong.storefront == market, ).subquery() return session.query( start_date_tracks_query ).outerjoin( end_date_tracks_query, and_( start_date_tracks_query.c.playlist_id == end_date_tracks_query.c.playlist_id, start_date_tracks_query.c.song_id == end_date_tracks_query.c.song_id ) ).filter( end_date_tracks_query.c.song_id.is_(None) ) def get_only_starred_tracks_with_device_tokens(session: Session, tracks_sub_query: Query, market: str) -> Query: """Filtering tracks query by starred tracks and join with User devices Args: session (Session): DB session. tracks_sub_query: Query with track_id, position, track_name, artist_name, playlist_name, playlist_id aliased columns. market: str market. """ filters = [UserDeviceToken.is_active.is_(True)] if market == constants.GLOBAL_MARKET: filters.extend([ ApolloMarket.code == constants.US_MARKET, UserMarket.market_id == constants.GLOBAL_MARKET_ID ]) join_value = constants.US_MARKET_ID else: filters.extend([ ApolloMarket.code == market, UserMarket.market_id != constants.GLOBAL_MARKET_ID ]) join_value = UserMarket.market_id return session.query( StarredContent.user_id, ApolloMarket.code.label("market_name"), func.group_concat( UserDeviceToken.token.op('ORDER BY')(UserDeviceToken.created_at.desc()) ).label('device_tokens'), tracks_sub_query ).join( tracks_sub_query, func.upper(StarredContent.isrc) == func.upper(tracks_sub_query.c.isrc) ).join( UserDeviceToken, UserDeviceToken.user_id == StarredContent.user_id ).join( UserMarket, UserMarket.user_id == StarredContent.user_id ).join( ApolloMarket, ApolloMarket.id == join_value ).filter( *filters ).group_by( StarredContent.user_id, tracks_sub_query.c.playlist_id, tracks_sub_query.c.song_id ) def process_added_tracks(market: str, today: str, yesterday: str, push_client: PushClient, redis_client: StrictRedis, session: Session, logger: logging.Logger, blacklisted_ids: list): """Send push notifications about added tracks. Args: market (str): Market code. today (str): Current date. yesterday (str): Previous date. push_client (PushClient): Push client instance. redis_client (StrictRedis): Redis client. session (Session): DB session. logger (Logger): Logging instance. blacklisted_ids (list): List with playlists ids. """ is_global = bool(market == constants.GLOBAL_MARKET) _market = constants.US_MARKET if is_global else market added_tracks_query = get_added_tracks_query(session, today, yesterday, _market).subquery() starred_tracks = get_only_starred_tracks_with_device_tokens(session, added_tracks_query, market) push_messages = [] for t in starred_tracks: _is_blacklisted = bool(blacklisted_ids and str(t.playlist_id) in blacklisted_ids) if _is_blacklisted is True: logger.info(f'Track <{t.song_id}> is skipped! <{t.playlist_id}> is in blacklist.') continue _is_new_track = is_new_track(today, t.playlist_id, t.song_id, _market, t.user_id, redis_client) if _is_new_track is False: continue add_to_already_sent(today, t.playlist_id, t.song_id, constants.GLOBAL_MARKET, t.user_id, redis_client) _country_code = get_country_code(market) _message_id = str(uuid.uuid4()) push_message = Push( id=_message_id, title='Added to a Top 250 Playlist', message=build_push_message(track=t, country_flag=get_country_flag(_country_code)), tokens=t.device_tokens.split(','), user_id=t.user_id, data=PushData( id=_message_id, isrc=t.isrc, country_code=_country_code, artist_name=t.artist_name, track_id=str(t.song_id), track_name=t.song_name, topic=TopicEnum.PLAYLIST_ADDITIONS.name.lower(), position=t.position + 1, target=t.playlist_name, vendor=VendorEnum.APPLE.name.lower(), playlist_id=t.playlist_id ) ) push_messages.append(push_message) for sent_messages in push_client.send_messages(push_messages): push_client.save_messages_to_db(sent_messages) def handler(logger: logging.Logger, sqs_client: BaseClient, redis_client, app_config: Config) -> Any: """Job for fetching new tracks in top Apple playlists. Args: logger: Logger instance. sqs_client: Client for SQS AWS. redis_client: Redis client. app_config: Config object. Returns: JSON serializable response """ today = datetime.today().strftime("%Y-%m-%d") yesterday = (datetime.today().date() - timedelta(days=1)).strftime("%Y-%m-%d") # set lock for 15 m to avoid possible race condition between lambdas lock = redis_client.lock(get_key('lock'), timeout=constants.REDIS_LOCK_TTL) if not lock.acquire(blocking=False): logger.info('Locked') return {} with session_scope() as session: country_codes = get_country_codes_from_db(session) blacklisted_ids = get_blacklisted_ids(session, redis_client) or [] logger.info(f"Blacklist: {blacklisted_ids}") for country_code in country_codes: with session_scope() as session: push_client = PushClient( logger_client=logger, sqs_client=sqs_client, db_session=session, config=app_config) process_added_tracks(country_code, today, yesterday, push_client, redis_client, session, logger, blacklisted_ids) lock.release() return {}