"""Episode Model. This model represents an Episode """ from datetime import datetime, timedelta import dateutil.parser import sqlalchemy from sqlalchemy import ForeignKey from sqlalchemy.dialects.mysql import INTEGER from sqlalchemy.orm import relationship from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.sql import func from podcast.connectors import mysql from podcast.constants import api as api_const from podcast.constants import error from podcast.models.insertion_point import InsertionPoint from podcast.utils import api_utils from podcast.utils import uuid from podcast.utils.exc import OwsError class Episode(mysql.BaseModel): """Episode model.""" __tablename__ = 'episode' id = sqlalchemy.Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True) # noqa podcast_id = sqlalchemy.Column( INTEGER, ForeignKey('podcast.id'), nullable=False ) title = sqlalchemy.Column(sqlalchemy.VARCHAR(255)) description = sqlalchemy.Column(sqlalchemy.VARCHAR(4000)) season_number = sqlalchemy.Column(sqlalchemy.Integer) episode_number = sqlalchemy.Column(sqlalchemy.Integer) episode_type = sqlalchemy.Column(sqlalchemy.Enum( *['full', 'trailer', 'bonus'])) trailer_type = sqlalchemy.Column(sqlalchemy.Enum(*['regular', 'cross-promo'])) content = sqlalchemy.Column(sqlalchemy.Enum(*['clean', 'explicit'])) planned_pre_roll_count = sqlalchemy.Column(sqlalchemy.Integer, default=0) planned_mid_roll_count = sqlalchemy.Column(sqlalchemy.Integer, default=0) planned_post_roll_count = sqlalchemy.Column(sqlalchemy.Integer, default=0) created_by = sqlalchemy.Column(sqlalchemy.Integer) updated_by = sqlalchemy.Column(sqlalchemy.Integer) created_date = sqlalchemy.Column(sqlalchemy.DateTime, default=func.now()) updated_date = sqlalchemy.Column( sqlalchemy.DateTime, default=func.now(), onupdate=func.now()) is_deleted = sqlalchemy.Column( sqlalchemy.Boolean, nullable=False, default=False) megaphone_id = sqlalchemy.Column(sqlalchemy.VARCHAR(50)) megaphone_uid = sqlalchemy.Column(sqlalchemy.VARCHAR(512)) draft = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True) published_date = sqlalchemy.Column(sqlalchemy.DateTime) insertion_points = relationship(InsertionPoint, lazy='select') uuid = sqlalchemy.Column(sqlalchemy.VARCHAR(36), default=uuid.get_uuid) external_id = sqlalchemy.Column(sqlalchemy.VARCHAR(64)) season_id = sqlalchemy.Column( INTEGER, ForeignKey('podcast_season.id'), nullable=True ) is_reviewed = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True) apple_id = sqlalchemy.Column(sqlalchemy.VARCHAR(20)) def to_dict(self): """Return the object as dictionary.""" result = dict( id=self.id, podcast_id=self.podcast_id, season_id=self.season_id, title=self.title, description=self.description, season_number=self.season_number, episode_number=self.episode_number, episode_type=self.episode_type, trailer_type=self.trailer_type, content=self.content, created_by=self.created_by, megaphone_id=self.megaphone_id, megaphone_uid=self.megaphone_uid, uuid=self.uuid, original_audio_filename=None, planned_pre_roll_count=self.planned_pre_roll_count, planned_mid_roll_count=self.planned_mid_roll_count, planned_post_roll_count=self.planned_post_roll_count, updated_by=self.updated_by, created_date=self.created_date, updated_date=self.updated_date, status=self.computed_status, draft=self.draft, published_date=self.published_date, external_id=self.external_id, is_reviewed=self.is_reviewed, apple_id=self.apple_id ) result['insertion_points'] = [ point.to_dict() for point in self.insertion_points ] return result @property def computed_status(self): """Return calculated status value.""" if self.draft is False and datetime.utcnow() >= self.published_date: return api_const.EPISODE_STATUS_PUBLISHED elif self.draft is False: return api_const.EPISODE_STATUS_SCHEDULED return api_const.EPISODE_STATUS_DRAFT def get_episodes(podcast_id, limit=0, offset=0, filter_by_state='ALL', order_by='published_date', sort_order='asc', start_date=None, end_date=None): """Return all the episodes for a podcast. Args: podcast_id (int): the podcast unique identifier limit (int): how many podcasts to retrieve. offset (int): the offset (for pagination). Returns: dict: containing the paginated episodes. """ if not isinstance(podcast_id, int): raise OwsError.bad_request(error.ERROR_MESSAGE_BAD_PARAMS) with mysql.pod_db_session(read_only=True) as session: query = _filter_by_active_podcast(session.query(Episode), podcast_id) if filter_by_state == 'DONE': query = _filter_by_done(query) if filter_by_state == 'DRAFT': query = _filter_by_draft(query) sort_field = Episode.__table__.c.get(order_by) if sort_field is not None: if sort_order == 'desc': sort_field = sort_field.desc() else: sort_field = sort_field.asc() query = query.order_by(sort_field) if start_date is not None: sd = datetime.strptime(start_date, '%Y-%m-%d').date() query = query.filter(Episode.published_date >= sd) if end_date is not None: ed = datetime.strptime(end_date, '%Y-%m-%d').date() query = query.filter(Episode.published_date <= ed) query = query.offset(offset) if limit != 0: query = query.limit(limit).offset(offset) rows = query.all() items = [row.to_dict() for row in rows] return {'items': items} def get_episodes_by_ids(episode_ids): """Return all the episodes by ids. Args: episode_ids (list): the episode unique identifiers Returns: dict: containing the episodes. """ with mysql.pod_db_session(read_only=True) as session: rows = session.query(Episode).filter( Episode.id.in_(episode_ids), Episode.is_deleted.isnot(True) ).all() items = [row.to_dict() for row in rows] return {'items': items} def get_episode_ids_by_podcast_ids(podcast_ids): """Return all the episodes by podcasts ids. Args: podcast_ids (list): the podcasts identifiers Returns: list: containing the episode ids. """ with mysql.pod_db_session(read_only=True) as session: rows = session.query(Episode).filter( Episode.podcast_id.in_(podcast_ids), Episode.is_deleted.isnot(True) ).all() items = [row.to_dict()['id'] for row in rows] return items def get_episodes_by_megaphone_ids(episode_ids): """Return all the episodes by mp ids. Args: episode_ids (list): megaphone ids Returns: dict: containing the episodes. """ with mysql.pod_db_session(read_only=True) as session: rows = session.query(Episode).filter( Episode.megaphone_id.in_(episode_ids), Episode.is_deleted.isnot(True) ).all() items = [row.to_dict() for row in rows] return {'items': items} def search_episodes(title, podcast_ids): """Return all the episodes where the titles match the search query. Args: title (string): search text to query the podcasts on. network_ids (list): list of network ids to search within Returns: list: list of episodes """ with mysql.pod_db_session(read_only=True) as session: query = session.query(Episode).filter(Episode.is_deleted.isnot(True)) query = query.filter( Episode.podcast_id.in_(podcast_ids), Episode.title.ilike(f'%{title}%', escape='/') ) query = _filter_by_done(query) query = query.order_by(Episode.title) rows = query.all() return [row.to_dict() for row in rows] def get_drafts_count(podcast_id): """Return count of draft episodes for podcast_id. Args: podcast_id (int): the podcast unique identifier Returns: int: number of draft episodes """ with mysql.pod_db_session(read_only=True) as session: query = _filter_by_active_podcast(session.query(Episode), podcast_id) query = _filter_by_draft(query) return query.count() def get_done_count(podcast_id): """Return count of done episodes for podcast_id. Args: podcast_id (int): the podcast unique identifier Returns: int: number of done episodes """ with mysql.pod_db_session(read_only=True) as session: query = _filter_by_active_podcast(session.query(Episode), podcast_id) query = _filter_by_done(query) return query.count() def _filter_by_active_podcast(query, podcast_id): return query.filter( Episode.podcast_id == podcast_id, Episode.is_deleted.isnot(True) ) def _filter_by_done(query): return query.filter(Episode.draft == False) # noqa def _filter_by_draft(query): return query.filter(Episode.draft == True) # noqa def get_episode_by_id(episode_id): """Return single episode by its id. Args: episode_id (int): the unique identifier of episode Returns: dict: containing the updated podcast dict. """ if not isinstance(episode_id, int): raise OwsError.bad_request(error.ERROR_MESSAGE_BAD_PARAMS) with mysql.pod_db_session(read_only=True) as session: try: filters = [ (Episode.id == episode_id), (Episode.is_deleted.isnot(True)) ] row = session.query( Episode).filter(*filters).one() return row.to_dict() except NoResultFound: raise OwsError.not_found(error.ERROR_EPISODE_NOT_FOUND) def create_episode(data, session=None, user_id=None): """Create a new Episode. Args: data (dict): the data from which to create the episode. Returns: dict: containing the created episode dict. """ if not data: raise OwsError.bad_request(error.ERROR_MESSAGE_EMPTY_BODY) if session: episode = _create_episode(data, session, user_id) session.add(episode) session.flush() else: with mysql.pod_db_session() as session: episode = _create_episode(data, session, user_id) session.add(episode) return episode.to_dict() def _create_episode(data, session, user_id): user_id = user_id or api_utils.get_user_id() data['created_by'] = user_id data['updated_by'] = user_id data = _clean_episode_input_data(data) episode = Episode(**data) return episode def get_most_recent_episodes_by_podcast_ids(podcast_ids, limit=5): """Get most recent published episodes for a user. Args: limit (int): max num to return Returns: list(int): list of episode ids """ with mysql.pod_db_session(read_only=True) as session: query = session.query(Episode).filter( Episode.draft == False, # noqa Episode.podcast_id.in_(podcast_ids), Episode.is_deleted.isnot(True), Episode.published_date <= datetime.utcnow() ).order_by(Episode.published_date.desc()).limit(limit) return [{'episode_id': episode.id} for episode in query.all()] def update_episode(episode_id, data, session=None, user_id=None): """Update an Episode. Args: episode_id (int): the episode identifier podcast_id (int): the podcast identifier data (dict): the data with which to update the podcast. session (sqlalchemy.session.Session): live connection to database. Returns: dict: containing the updated podcast dict. """ if not isinstance(episode_id, int): raise OwsError.bad_request(error.ERROR_MESSAGE_BAD_PARAMS) if not data: raise OwsError.bad_request(error.ERROR_MESSAGE_EMPTY_BODY) user_id = user_id or api_utils.get_user_id() if session: return _update_episode(episode_id, data, session, user_id) else: with mysql.pod_db_session() as session: return _update_episode(episode_id, data, session, user_id) def _update_episode(episode_id, data, session, user_id): query = session.query(Episode).filter(Episode.id == episode_id) data = _clean_episode_input_data(data) data['updated_by'] = user_id query.update(data) session.flush() result = query.first() if result: return result.to_dict() else: raise OwsError.not_found(error.ERROR_EPISODE_NOT_FOUND) def delete_episode(podcast_id, episode_id, session): """Delete an episode. Args: episode_id (int): the episode id. podcast_id (int): the podcast id. session (sqlalchemy.session.Session): live connection to database. Returns: dict: containing the deleted podcast dict. """ if not isinstance(podcast_id, int) and not isinstance(episode_id, int): raise OwsError.bad_request(error.ERROR_MESSAGE_BAD_PARAMS) episode = session.query( Episode).filter( Episode.id == episode_id, Episode.podcast_id == podcast_id).first() if not episode: raise OwsError.not_found(error.ERROR_EPISODE_NOT_FOUND) if episode.is_deleted: raise OwsError.not_found(error.ERROR_MESSAGE_EPISODE_IS_DELETED) episode.updated_by = api_utils.get_user_id() episode.is_deleted = True episode.season_id = None return episode.to_dict() def delete_episodes_by_podcast_id(podcast_id, session): """Delete episodes by podcast_id. Args: podcast_id (int): the podcast identifier session (sqlalchemy.orm.Session): configured sqlalchemy session """ episodes = session.query(Episode).filter( Episode.podcast_id == podcast_id, Episode.is_deleted.isnot(True)) episodes.update({'is_deleted': True, 'season_id': None, 'updated_by': api_utils.get_user_id()}) def create_planned_inventory(podcast_id, data): """Create an episode for every date passed in with expected ads. Args: podcast_id (int): the podcast identifier data (dict): the number of ads and a list of dates """ if not isinstance(podcast_id, int): raise OwsError.bad_request(error.ERROR_MESSAGE_BAD_PARAMS) if not data: raise OwsError.bad_request(error.ERROR_MESSAGE_EMPTY_BODY) user_id = api_utils.get_user_id() with mysql.pod_db_session() as session: episodes = [] for date in data['dates']: episode_data = { 'podcast_id': podcast_id, 'published_date': date, 'title': data['title_prefix'], 'created_by': user_id, 'updated_by': user_id, 'planned_mid_roll_count': data['mid_rolls'], 'planned_post_roll_count': data['post_rolls'], 'planned_pre_roll_count': data['pre_rolls'], } episode_data = _clean_episode_input_data(episode_data) episode_data['title'] += episode_data['published_date'].strftime('%m/%d/%Y') episodes.append(Episode(**episode_data)) session.add_all(episodes) items = [episode.to_dict() for episode in episodes] return {'podcast_id': podcast_id, 'items': items} def get_num_episodes_for_podcast_ids(podcast_ids): """Get number of episodes for podcast ids. Args: podcast_ids (list(ints)): the podcast ids to fetch Returns: dict: {[podcast_id]: 5} """ with mysql.pod_db_session(read_only=True) as session: query_result = session.query( Episode.podcast_id, func.count(Episode.podcast_id) ).filter( Episode.podcast_id.in_(podcast_ids), Episode.is_deleted.isnot(True), Episode.draft.isnot(True) ).group_by(Episode.podcast_id).all() final = {} for result in query_result: final[result[0]] = result[1] return final def _clean_episode_input_data(data): """Validate episode data before db insert/update.""" input_data = {**data} if input_data.get('published_date'): input_data['published_date'] = dateutil.parser.parse(input_data['published_date']) if input_data.get('description'): input_data['description'] = api_utils.clean_html(input_data['description']) return input_data def episodes_gone_live_within_hour(): """Return episodes that have gone live within an hour ago.""" with mysql.pod_db_session(read_only=True) as session: now = datetime.utcnow() an_hour_ago = now - timedelta(hours=1) query = session.query(Episode).filter( Episode.draft == False, # noqa Episode.is_deleted.isnot(True), Episode.published_date >= an_hour_ago, Episode.published_date < now ) return [episode.to_dict() for episode in query.all()] def episodes_going_live_tomorrow(): """Return episodes that will go live within 23-24 hours from now.""" with mysql.pod_db_session(read_only=True) as session: now = datetime.utcnow() in_23_hours = now + timedelta(hours=23) in_24_hours = now + timedelta(hours=24) query = session.query(Episode).filter( Episode.draft == False, # noqa Episode.is_deleted.isnot(True), Episode.published_date >= in_23_hours, Episode.published_date < in_24_hours ) return [episode.to_dict() for episode in query.all()]