"""Track Model.""" from oto.response import create_error_response from oto.response import create_not_found_response from oto.response import Response import sentry_sdk from sqlalchemy import and_ from sqlalchemy import Column from sqlalchemy import Enum from sqlalchemy import exc from sqlalchemy import ForeignKey from sqlalchemy import func from sqlalchemy import Integer from sqlalchemy import String from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship from ows_product_physical.connector import mysql from ows_product_physical.constant import error from ows_product_physical.constant import field from ows_product_physical.models import track_artist as track_artist_model from ows_product_physical.models import ( track_publisher as track_publisher_model) from ows_product_physical.models import track_writer as track_writer_model from ows_product_physical.models.track_artist import TrackArtist from ows_product_physical.models.track_publisher import TrackPublisher from ows_product_physical.models.track_writer import TrackWriter from ows_product_physical.utils.track_utils import fake_pagination from ows_product_physical.utils.track_utils import \ map_fields_to_tracks_columns # noqa from ows_product_physical.utils.track_utils import map_track from ows_product_physical.utils.track_utils import ( map_track_publishing_obligation) from ows_product_physical.validation import track_validation class TrackPhysical(mysql.BaseModel): """TrackPhysical Model. Represents the physical attributes for the track table in art_relations. """ __tablename__ = 'track_physical' track_physical_id = Column( Integer, primary_key=True, autoincrement=True, nullable=False) track_id = Column(Integer, ForeignKey('track.id')) side = Column(String, nullable=False) class Track(mysql.BaseModel): """Track Model. Represents track table in art_relations. """ __tablename__ = 'track' __allow_unmapped__ = True id = Column( # noqa Integer, primary_key=True, autoincrement=True, nullable=False) release_id = Column(Integer, nullable=False) track_id = Column(Integer, nullable=False) # Track Number track_name = Column(String, nullable=False) isrc = Column(String, nullable=False) physical = relationship('TrackPhysical', uselist=False) cd = Column(Integer, nullable=False) length_minute = Column(Integer, nullable=False) length_seconds = Column(Integer, nullable=False) upc = Column(Integer) track_artists = relationship('TrackArtist') track_writers = relationship('TrackWriter') # Publishing Obligation us_publishing_obligation = Column( 'us_publishing_obligation', Enum( *field.OBLIGATION_ENUM), nullable=True) third_party_publisher = Column( 'third_party_publisher', Enum('Y', 'N'), default='N', nullable=True) publishers = relationship( TrackPublisher, order_by='TrackPublisher.track_publisher_id', backref='track', cascade='all, delete-orphan') @hybrid_property def performer(self): """List of performer names.""" return [ track_artist.name for track_artist in self.track_artists if track_artist.type == 'performer'] def to_dict(self): """Dictionary representation of Track.""" publishers = [pub.to_dict() for pub in self.publishers] return { 'id': self.id, 'release_id': self.release_id, 'track_number': self.track_id, 'track_name': self.track_name, 'isrc': self.isrc, 'cd': self.cd, 'length_minute': self.length_minute, 'length_seconds': self.length_seconds, 'upc': self.upc, 'performer': self.performer, 'us_publishing_obligation': self.us_publishing_obligation, 'third_party_publisher': self.third_party_publisher, 'publishers': publishers } def process_tracks(product_id, upc, fields): """Process track as CREATE, UPDATE, or DESTROY. Args: product_id (int): product id upc (int): releases.upc for product. fields (list): updated track fields. Returns: Response: Response containing the processed tracks. """ with mysql.db_session() as session: try: for values in fields: track_id = values.get('track_id') is_deleted = values.get('is_deleted') == 'Y' if track_id and is_deleted: _delete_by_track_id(track_id, session) elif track_id: _update(track_id, values, session) else: _create(product_id, upc, values, session) tracks = _get_tracks(product_id, session) validation_response = \ track_validation.validate_database_state(tracks) if not validation_response: session.rollback() return validation_response return Response(message=fake_pagination(tracks), status=201) except (exc.SQLAlchemyError, exc.DBAPIError) as exception: sentry_sdk.capture_exception(exception) return create_error_response( code=error.INTERNAL_ERROR, message='mysql error', status=500) def _create(product_id, upc, fields, session): """Create a track. Args: upc (str): display upc of product track is assigned to session (object): database connection object product_id (int): id of the current product fields (dict): dictionary of track metadata """ values = { 'release_id': product_id, 'upc': upc } values.update(map_fields_to_tracks_columns(fields)) if 'cd' not in values: values['cd'] = 1 track = Track(**values) session.add(track) session.flush() _update_track_writer(track.id, fields.get('song_writers', None), session) _add_track_physical(track.id, fields.get('side', None), session) _update_track_artists(track.id, fields.get('performer'), session) _update_track_publishers(track.id, fields.get('publisher_names'), session) def _get_tracks(product_id, session): """Retrieve tracks from the database by release_id. Args: session (object): database connection object product_id (int): id of the current product Returns: list: list of dicts with track and artist data """ try: response = (session.query( Track.id, Track.track_id, Track.track_name, TrackPhysical.side, Track.isrc, Track.cd, Track.length_minute, Track.length_seconds, Track.us_publishing_obligation, Track.third_party_publisher, func.group_concat(TrackWriter.writer_name.distinct()), TrackArtist.name, func.group_concat(TrackPublisher.publisher_name.distinct()) ) .join(TrackArtist, and_( TrackArtist.track_id == Track.id, TrackArtist.type == 'performer'), isouter=True) .join(TrackWriter, and_(TrackWriter.unique_track_id == Track.id), isouter=True) .join(TrackPublisher, and_(TrackPublisher.track_id == Track.id), isouter=True) .join(TrackPhysical, and_(TrackPhysical.track_id == Track.id), isouter=True) .filter(Track.release_id == product_id) .group_by(Track.id, TrackArtist.track_id).order_by( Track.cd, TrackPhysical.side, Track.track_id)) return [map_track(item) for item in response] except (exc.SQLAlchemyError, exc.DBAPIError) as exception: sentry_sdk.capture_exception(exception) return create_error_response( code=error.INTERNAL_ERROR, message='mysql error', status=500) def _update(track_id, fields, session): """Update a track. Args: session (object): database connection object track_id (int): id of the track to update fields (dict): dictionary of track metadata """ values = map_fields_to_tracks_columns(fields) if values: session.query(Track).filter(Track.id == track_id).update(values) _update_track_writer(track_id, fields.get('song_writers', None), session) _update_track_artists(track_id, fields.get('performer'), session) if 'side' in fields: _update_track_physical(track_id, fields.get('side'), session) def _delete_by_track_id(track_id, session): """Delete a track. Args: session (object): database connection object track_id (int): id of the track to delete """ track_artist_model.delete_by_track_id(track_id, session) track_publisher_model.delete_by_track_id(track_id, session) track_writer_model.delete_by_track_id(track_id, session) session.query(Track).filter(Track.id == track_id).delete() def _delete_tracks_by_product_id(product_id, session): """Delete a track. Args: product_id (int): id of the product to delete tracks for session: database connection object """ session.query(Track).filter(Track.release_id == product_id).delete() def get_tracks(product_id): """Retrieve tracks from the database by release_id. Args: release_id (int): id of the current product Returns: list: list of dicts with track and artist data """ with mysql.db_session() as session: try: result = fake_pagination(_get_tracks(product_id, session)) return Response(message=result) except (exc.SQLAlchemyError, exc.DBAPIError) as exception: sentry_sdk.capture_exception(exception) return create_error_response( code=error.INTERNAL_ERROR, message='mysql error', status=500) def delete_all_tracks(product_id, session): """Deletes all tracks for a given product. Args: product_id (int): id of the product to delete tracks for session: database connection object """ track_numbers = [ track.get('track_id') for track in _get_tracks(product_id, session) ] track_artist_model.bulk_delete_by_track_id(track_numbers, session) track_publisher_model.bulk_delete_by_track_id(track_numbers, session) track_writer_model.bulk_delete_by_track_id(track_numbers, session) _delete_tracks_by_product_id(product_id, session) def _update_track_artists(track_id, artists, session): """Update a track's artists. Args: session (object): database connection object track_id (int): id of the track to delete artists for artists (dict): list of track artists to create """ if artists: track_artist_model.delete_by_track_id(track_id, session) track_artist_model.create(artists, track_id, session) def _update_track_writer(track_id, track_writers, session): """Update a track's artists. Args: track_id (int): id of the track to delete artists for side (str): side of record session (object): database connection object """ if track_writers: track = session.query(Track).filter(Track.id == track_id)[0] session.query(TrackWriter) \ .filter(TrackWriter.unique_track_id == track.id) \ .delete() for track_writer in track_writers: writer_row = TrackWriter( writer_name=track_writer, cd=track.cd, upc=track.upc, track_id=track.track_id, unique_track_id=track.id ) session.add(writer_row) session.flush() def _add_track_physical(track_id, side, session): """Update a track's artists. Args: track_id (int): id of the track to delete artists for side (str): side of record session (object): database connection object """ physical_track = TrackPhysical(side=side, track_id=track_id) session.add(physical_track) session.flush() def _update_track_physical(track_id, side, session): """Update a track's artists. Args: track_id (int): id of the track to delete artists for side (str): side of record session (object): database connection object """ num_updated = session.query(TrackPhysical).filter( TrackPhysical.track_id == track_id).update({'side': side}) if num_updated == 0: _add_track_physical(track_id, side, session) def _get_publishing_obligations(product_id, session): """Retrieve publishing_obligations from the database by release_id. Args: product_id (int): id of the current product session (object): database connection object Returns: list: list of dicts with publishing_obligation data """ try: response = (session.query( Track.id, Track.track_id, Track.track_name, Track.us_publishing_obligation, Track.third_party_publisher, func.group_concat(TrackPublisher.publisher_name)) .join(TrackPublisher, and_(TrackPublisher.track_id == Track.id), isouter=True) .filter(Track.release_id == product_id) .group_by(Track.id).order_by(Track.cd, Track.track_id).all()) if not response: return create_not_found_response( error.PUBLISHING_OBLIGATION_NOT_FOUND_MESSAGE) return [map_track_publishing_obligation(item) for item in response] except Exception as exception: sentry_sdk.capture_exception(exception) return create_error_response( code=error.INTERNAL_ERROR, message='mysql error', status=500) def get_publishing_obligations(product_id): """Retrieve publishing_obligations from the database by product_id. Args: product_id (int): id of the current product Returns: Response: list of dicts with track and artist data """ with mysql.db_session() as session: result = _get_publishing_obligations(product_id, session) if not result: return result return Response(message={'items': result}) def _update_track_publishers(track_id, publisher_names, session): """Update track's publishers. Args: track_id (int): id of the track to process publishers for. publisher_names (list): list of track publishers to create. session (object): database connection object. Returns: Response: list of dicts with track and artist data in case of success, error response in case of failure. """ if publisher_names is not None: try: track_publisher_model.delete_by_track_id(track_id, session) if publisher_names: track_publisher_model.create( publisher_names, track_id, session) except (exc.SQLAlchemyError, exc.DBAPIError) as exception: sentry_sdk.capture_exception(exception) return create_error_response( code=error.INTERNAL_ERROR, message='mysql error: {}'.format(str(exception)), status=500) return Response() def bulk_update_publishing_obligations(product_id, upc, data): """Update publishing obligation for multiple tracks. Args: product_id (int): id of the product. upc (int): releases.upc for product. data (list): list of publishing obligation values to update for many tracks. Returns: Response: list of dicts with track and artist data in case of success, error response in case of failure. """ with mysql.db_session() as session: for track_item in data: update_result = _update_track_publishers( track_item['track_id'], track_item.get('publisher_names'), session) if not update_result: return update_result process_response = process_tracks(product_id, upc, data) if not process_response.status == 201: return process_response return Response()