"""Logic for Track Roles.""" from oto import response from oto import status as response_code from backend.constants import api as api_const from backend.constants import artist_role from backend.constants import error from backend.constants import product as product_consts from backend.constants import track_field from backend.constants import track_role from backend.models import ows_product from backend.models import ows_product_digital from backend.models import ows_product_workflow from backend.models.track_persister import TrackPersister from backend.utils import api as api_utils from backend.utils import localization as loc_util from backend.utils import logic as logic_util from backend.utils import validation as track_validation @logic_util.verify_product_ownership def create(tuid, role, data, account_type, account_id): """Create a new track role. Args: tuid (int): track primary key role (str): type of track role data (dict): track role data to add Returns: Response: A Response obj with track role JSON data """ if 'name' not in data or not data['name'].strip(): return api_utils.create_validation_error_response( error.VALIDATION_ERROR_MISSING_FIELD_MSG, 'name') if role in track_role.ARTISTS: return TrackPersister.add_track_artist(tuid, role, data['name']) if role in track_role.WRITERS: return TrackPersister.add_track_writer(tuid, data['name']) return _invalid_role_response(role) @logic_util.verify_product_ownership def delete(tuid, role, track_role_id, account_type, account_id): """Delete track role. Args: tuid (int): track primary key role (str): type of role track_role_id (int): primary key of role Returns: Response: A Response with status 200 if deleted """ if role in track_role.ARTISTS: return TrackPersister.remove_track_artist(tuid, role, track_role_id) if role in track_role.WRITERS: return TrackPersister.remove_track_writer(tuid, track_role_id) return _invalid_role_response(role) @logic_util.verify_product_ownership def update(tuid, role, role_id, data, account_type, account_id): """Update track role. Args: tuid (int): track primary key role (str): type of role role_id (int): primary key of role data (dict): values to update role with Returns: Response: A Response with 200 status if updated. """ if role in track_role.ARTISTS: return TrackPersister.update_track_artist( tuid, role, role_id, data ) elif role in track_role.WRITERS: return TrackPersister.update_track_writer(tuid, role_id, data) return _invalid_role_response(role) @logic_util.verify_product_ownership def update_for_all_tracks_in_product( product_id, role_type, data, account_type, account_id): """Update role for all tracks in in product. Args: product_id (int): Primary key of product the tracks belong to role_type (str): type of track role data (dict): track roles and role localizations to apply account_type (str): the user account type in the header. account_id (int): the user account id in the header. Returns: Response: A Response obj with track JSON data """ role_names = data['names'] localizations = data.get(track_field.LOCALIZATIONS, []) if role_type in track_role.ARTISTS: valid_response = track_validation.validate_artist_names(role_names) if not valid_response: return valid_response if localizations and role_type not in track_role.ARTISTS: return api_utils.create_validation_error_response( error.VALIDATION_ERROR_ROLE_ARTISTS_ONLY_LOCALIZATIONS_MSG) # Make sure localizations are valid localizations_delta = [] localizations_with_role = '{}_names'.format(role_type) for loc in localizations: # Number of localizations must match number of role if provided if role_names and len(loc['names']) != len(role_names): msg = error.VALIDATION_ERROR_ROLE_LOCALIZATIONS_MISMATCH_MSG return api_utils.create_validation_error_response(msg, role_type) localizations_delta.append({ 'language_id': loc['language_id'], localizations_with_role: loc['names']}) track_response = TrackPersister.sync_role_for_all_tracks_in_product( product_id, role_type, role_names) result = loc_util.patch_tracks_localizations( track_response, localizations_delta) if result: for item in result.message[api_const.ITEMS]: del item[track_field.META_LANGUAGE_CODE] return result def _invalid_role_response(role): """Return invalid role response. Args: role (str): type of role Returns: Response: Invalid role response with status 400 """ return response.create_error_response( code=error.INVALID_ROLE_ERROR_CODE, message=error.INVALID_ROLE_ERROR_MSG.format(role)) @logic_util.verify_product_ownership def update_artist(product_id, request_data, account_id, account_type, correlation_id, orchard_user_id): """Re-arrange track artist data to keep valid artist roles for given genre. Args: product_id (int): Unique of product table. request_data (dict): post data account_id (int): label user id account_type (string): type of label user, alw or oa. correlation_id (string): correlation identifier orchard_user_id(string): user id and type of the user requesting changes Returns: response """ product_response = ows_product.get_product_by_product_id(product_id) if not product_response: return product_response product_data = product_response.message genre_id = request_data.get(product_consts.GENRE_ID) subgenre_id = request_data.get(product_consts.SUBGENRE_ID) if not (genre_id and subgenre_id): return api_utils.create_validation_error_response( error.VALIDATION_ERROR_MSG) valid_artist_roles = \ _get_artist_roles_by_genre_and_subgenre(genre_id, subgenre_id) tracks = TrackPersister.get_all_by_product_id_light(product_id) if api_const.ITEMS not in tracks.message: return response.create_not_found_response(error.ERROR_MESSAGE_TRACK_NOT_FOUND) tuids = [item[track_field.TUID] for item in tracks.message.get(api_const.ITEMS)] correction_id = request_data.get(product_consts.CORRECTION_ID) if correction_id and product_data.get('status') == product_consts.RELEASE_IN_CONTENT_STATUS: correction_details_data = _format_track_artist_in_correction( product_id, tracks, valid_artist_roles, correlation_id, tuids) if correction_details_data: return ows_product_digital.post_track_correction( product_id, correction_id, correction_details_data, correlation_id, orchard_user_id) track_artists = _delete_track_artist(tracks, valid_artist_roles) if not track_artists: return response.Response(status=response_code.NO_CONTENT, message=track_artists) return response.Response(status=response_code.OK, message=track_artists) def _get_artist_roles_by_genre_and_subgenre(genre_id, subgenre_id): """Fetch valid track artist roles for given genre and subgenre. Args: genre_id (int): Unique identifier of genre table subgenre_id (int): Unique identifier of subgenre table Returns: List of valid artist roles """ if subgenre_id in product_consts.SOUNDTRACK_SUBGENRE_IDS: return artist_role.SOUNDTRACK_AND_WORLDMUSIC_ARTIST_ROLES if genre_id == product_consts.CLASSICAL_GENRE_ID: return artist_role.CLASSICAL_ARTIST_ROLES return artist_role.NON_CLASSICAL_ARTIST_ROLES def _delete_track_artist(tracks, valid_artist_roles): """Delete track artist which are not required. Args: tracks (list): list of all tracks valid_artist_roles (list): list of track artist roles. Returns: list of valid track artist """ valid_track_artists = [] for track in tracks.message[api_const.ITEMS]: track_artist = TrackPersister.get_track_artist_by_track_id(track.get('tuid')) if not track_artist: continue for track_artist in track_artist.message: if track_artist.get('type') not in valid_artist_roles: delete( track.get('tuid'), track_artist.get('type'), track_artist.get('track_artist_id')) else: valid_track_artists.append(track_artist) return valid_track_artists def _filter_specific_artists(artists, artist_roles): """Filter specific type from set of all artist roles. Args: artists (list): list of all artist objects artist_roles (list): list of all artist roles to be picked Returns: List of artist objects for given role type """ return list(filter(lambda item: item['type'] in artist_roles, artists)) def _format_individual_roles(artists, role, valid_artist_roles, track_id): """Re-structure artist data for individual role types. for eg. remixer, featuring. Args: artists (int): list of all artist objects role (int): artist role valid_artist_roles(list): list of valid artist roles based on requested genre Returns: None or updated artist object for given role """ role_specific_artists = _filter_specific_artists(artists, [role]) if role_specific_artists and role not in valid_artist_roles: return _format_artist_object([], role, track_id) def _format_combined_roles(artists, valid_artist_roles, track_id): """Fetch valid track artist roles for given genre and subgenre. Args: artists (int): list of artist object with key track_artist valid_artist_roles (int): list of valid artist roles based on requested genre track_id (int): Unique identifier of track table Returns: List of valid artist objects """ valid_track_artists = [] for track_artist in artists: if track_artist.get('type') not in valid_artist_roles: continue valid_track_artists.append({ 'type': track_artist.get('type'), 'name': track_artist.get('name') }) return _format_artist_object(valid_track_artists, 'track_artist', track_id) def _format_artist_object(key_value, role, track_id): """Create release_correction_detail friendly structure for artist object. Args: key_value (list): list of artist objects role (string): artist role track_id(int): unique identifier of track table Returns: Artist dictionary """ return { 'key_value': key_value, 'field_name': role, 'key_id': track_id, 'table_name': 'track' } def _get_new_correction_detail_entries(tracks, valid_artist_roles): """Returns manipulated list of artist objects based on valid artist roles for genre. Args: tracks (list): list of all artist objects valid_artist_roles (list): list of valid artist roles based on requested genre Returns: List of valid artist objects """ all_track_artists = [] for track in tracks: track_artist = TrackPersister.get_track_artist_by_track_id(track.get('tuid')) track_id = track.get('tuid') if not track_artist: continue featuring_artists = _format_individual_roles( track_artist.message, artist_role.FEATURING, valid_artist_roles, track_id) if featuring_artists: all_track_artists.append(featuring_artists) remixer_artists = _format_individual_roles( track_artist.message, artist_role.REMIXER, valid_artist_roles, track_id) if remixer_artists: all_track_artists.append(remixer_artists) producer_artists = _format_individual_roles( track_artist.message, artist_role.PRODUCER, valid_artist_roles, track_id) if producer_artists: all_track_artists.append(producer_artists) other_track_artists = _filter_specific_artists(track_artist.message, artist_role.TRACK_ARTIST) if other_track_artists: valid_track_artists = _format_combined_roles( other_track_artists, valid_artist_roles, track_id) all_track_artists.append(valid_track_artists) return all_track_artists def _format_track_artist_in_correction(product_id, tracks, valid_artist_roles, correlation_id, tuids): """Returns manipulated list of artist objects based on valid artist roles for genre. Args: product_id (int): Unique identifier of releases table tracks (list): list of all artist objects valid_artist_roles (list): list of valid artist roles based on requested genre correction_id (int): Unique identifier of release_correction table tuids (list): list of track ids Returns: List of valid artist objects """ track_corrections = False tracks_with_no_records_in_cd = False correction_details_data = False new_correction_details_data = [] correction_data = ows_product_workflow.get_last_release_correction(product_id, correlation_id) if correction_data.message.get(api_const.ITEMS): track_corrections = _filter_artists_from_correction_data( correction_data.message.get(api_const.ITEMS), [track_field.TRACK], 'table_name') tracks_in_correction = [item[track_field.KEY_ID] for item in track_corrections] tracks_with_no_records_in_cd = list(set(tuids) - set(tracks_in_correction)) if track_corrections: correction_details_data = _format_old_correction_detail_entries(track_corrections, valid_artist_roles) if tracks_with_no_records_in_cd: tracks_with_no_records_in_cd = _filter_artists_from_correction_data( tracks.message[api_const.ITEMS], tracks_with_no_records_in_cd, 'tuid') new_correction_details_data = _get_new_correction_detail_entries( tracks_with_no_records_in_cd, valid_artist_roles) if correction_details_data or new_correction_details_data: return correction_details_data + new_correction_details_data return _get_new_correction_detail_entries(tracks.message[api_const.ITEMS], valid_artist_roles) def _format_old_correction_detail_entries(correction_data, valid_artist_roles): """Returns list of valid artist objects based on genre. Args: correction_data (list): list of all artist objects in correction valid_artist_roles (list): list of valid artist roles based on requested genre Returns: List of valid artist objects """ correction_details_data = [] track_artists = _filter_artists_from_correction_data(correction_data, [artist_role.TRACK_ARTIST_ROLE]) correction_details_data = _process_track_artists(track_artists, valid_artist_roles) updated_featuring_artists = _format_individual_roles_in_correction( correction_data, artist_role.FEATURING, valid_artist_roles) if updated_featuring_artists: for artist in updated_featuring_artists: correction_details_data.append(artist) updated_remixer_artists = _format_individual_roles_in_correction( correction_data, artist_role.REMIXER, valid_artist_roles) if updated_remixer_artists: for artist in updated_remixer_artists: correction_details_data.append(artist) updated_producer_artists = _format_individual_roles_in_correction( correction_data, artist_role.PRODUCER, valid_artist_roles) if updated_producer_artists: for artist in updated_producer_artists: correction_details_data.append(artist) return correction_details_data def _filter_artists_from_correction_data(correction_data, artist_keys, field_type='field_name'): """Filter specific field type from set of all artist roles. Args: correction_data (list): list of correction artist objects artist_keys (list): list of all artist objects field_type (string): field type filter Returns: List of artist objects based on filter criteria """ return list(filter(lambda item: item[field_type] in artist_keys, correction_data)) def _process_track_artists(track_artists, valid_artist_roles): """Process list of artist objects based on valid artist roles for genre. Args: track_artists (list): list of track artist objects valid_artist_roles (list): list of valid artist roles based on requested genre Returns: List of valid artist objects """ all_track_artists = [] for track_artist in track_artists: valid_track_artists = _filter_specific_artists(track_artist['key_value'], valid_artist_roles) single_track = _format_artist_object( valid_track_artists, artist_role.TRACK_ARTIST_ROLE, track_artist.get('key_id')) all_track_artists.append(single_track) return all_track_artists def _updated_data_for_individual_roles(track_artists, role, valid_artist_roles): """Process artist roles which are saved as individual keys in correction detail table. Args: track_artists (list): List of track artists role (string): role of the artist valid_artist_roles (list): list of valid artist roles based on requested genre Returns: List of individual role artist objects """ single_role_artists = [] for artist in track_artists: updated_artist_data = _format_artist_object([], role, artist.get('key_id')) single_role_artists.append(updated_artist_data) return single_role_artists def _format_individual_roles_in_correction(correction_data, artist_role, valid_artist_roles): """Format individual artist roles data in correction. Args: correction_data (list): list of artist objects in correction artist_role (string): artist role valid_artist_roles (list): list of valid artist roles based on requested genre Returns: List of valid artist objects with individual roles """ artists = _filter_artists_from_correction_data(correction_data, [artist_role]) if artists and artist_role not in valid_artist_roles: return _updated_data_for_individual_roles( artists, artist_role, valid_artist_roles)