"""Track Validator functions. Each validator verifies that the track field is complete and valid. This goes beyond the basic validators, which only validates if a field is valid when data has been filled in (i.e. not blank). If a required field is left blank, the validators here will throw an error. """ import json from backend import features from backend.constants import artist_role from backend.constants import error from backend.constants import ownership_type from backend.constants import product as product_consts from backend.constants import services as services_const from backend.constants import track_field as tf from backend.constants import validation from backend.utils import track_utils from backend.utils import validation as validation_utils from backend.utils.validation import format_error from backend.utils.validation import format_missing_error from backend.utils.validation import is_valid_artist_name from backend.validation.track_localization_validators import \ validate_track_localization_set def validate_track_complete( featuring_artists, track, product_genre_type, product_subgenre_type, product_release_date, product_assets, used_track_names, product_meta_language, is_correction_mode, product_tracks, spatial_isrc_by_track_id, track_corrections={}, profile_type='', identity_id=''): """Run all validators for a track. Args: featuring_artists (list): Featuring artists track (dict): Target track data product_genre_type (str): Indicates genre type of product product_subgenre_type (str): Indicates subgenre type of product product_assets (list): List of product assets used_track_names (set): Set of used track names product_meta_language (str): Product meta language is_correction_mode (bool): Correction mode indicator product_tracks (list): Product Tracks data spatial_isrc_by_track_id (dict): mapping of track_id to spatial ISRC track_corrections (dict): Track corrections data Returns: error_obj: errors for track """ error_obj = {} # Validate exclusive classical required fields if product_genre_type == product_consts.CLASSICAL_GENRE: error_obj.update(validate_classical(track)) # Validate exclusive soundtrack required fields # Validate composer field for specific genres/subgenres is_classical = product_genre_type == product_consts.CLASSICAL_GENRE is_composer_required_soundtrack = product_genre_type == product_consts.SOUNDTRACK_GENRE and \ product_subgenre_type in product_consts.SOUNDTRACK_SUBGENRE_IDS.values() is_composer_required_world_music = product_genre_type == product_consts.WORLD_MUSIC_GENRE and \ product_subgenre_type in product_consts.WORLD_MUSIC_SUBGENRE_IDS.values() if (is_classical or is_composer_required_soundtrack or is_composer_required_world_music): error_obj.update(validate_track_composer(track, product_genre_type, product_subgenre_type)) # Validate basic metadata if features.is_classical_check_same_track_name_and_composer_enabled(): error_obj.update(validate_track_name_ff(product_genre_type, track, product_tracks)) else: error_obj.update(validate_track_name(track, used_track_names)) error_obj.update(validate_meta_language_code(track)) error_obj.update(validate_isrc(track)) error_obj.update(validate_pline(track)) error_obj.update(validate_explicit(track)) error_obj.update(validate_master_rights(track)) error_obj.update(validate_preview_start_time( track, product_assets, is_correction_mode)) # Artists and writers error_obj.update(validate_artists(track)) if not is_classical: error_obj.update(validate_writers(track)) attributes = {} if identity_id: attributes['identity_id'] = identity_id if profile_type: attributes['profile_type'] = profile_type validate_missing_producer = features.is_validate_missing_producer_enabled(attributes) if validate_missing_producer and track_utils.is_producer_required_track(product_release_date): error_obj.update(validate_producers(track, is_correction_mode, track_corrections)) error_obj.update(validate_featuring(track, featuring_artists)) # Assets error_obj.update(validate_track_audio_exists( track, product_assets, is_correction_mode)) if features.is_cdam_spatial_isrc_validation_enabled(): error_obj.update(validate_atmos_audio_exists( track, product_assets, spatial_isrc_by_track_id, is_correction_mode)) # Localizations error_obj.update(validate_track_localization_set( track, track.get(tf.LOCALIZATIONS), product_meta_language)) if error_obj: error_obj[tf.TUID] = track[tf.TUID] return error_obj def validate_classical(track): """Validate 'classical' fields. Args: track (dict): Track.to_dict() Returns: dict: errors or empty dict if field is valid """ error_obj = {} work, movement = track_utils.unpack_work_and_movement(track) if not work: error_obj[tf.WORK] = format_missing_error( field_name=tf.WORK, error_code=error.MISSING_WORK ) work_too_long = len(work) > validation.TRACK_NAME_MAX_LENGTH if work_too_long: error_msg = error.VALIDATION_ERROR_FIELD_TOO_LONG.format(tf.WORK) error_obj[tf.WORK] = format_error( validator='max_length_exceeded', message=error_msg, error_code=error.MAX_LENGTH_EXCEEDED.format(field_name='work') ) movement_too_long = len(movement) > validation.TRACK_NAME_MAX_LENGTH if movement_too_long: error_msg = error.VALIDATION_ERROR_FIELD_TOO_LONG.format(tf.MOVEMENT) error_obj[tf.MOVEMENT] = format_error( validator='max_length_exceeded', message=error_msg, error_code=error.MAX_LENGTH_EXCEEDED.format(field_name='movement') ) if not work_too_long and not movement_too_long: classical_track_max_length = validation.CLASSICAL_TRACK_NAME_MAX_LENGTH # noqa if len(track[tf.TRACK_NAME]) > classical_track_max_length: error_obj[tf.WORK] = format_error( validator='max_length_exceeded', message=error.VALIDATION_ERROR_COMBINED_FIELDS_TOO_LONG, error_code=error.MAX_LENGTH_EXCEEDED.format( field_name='combined_work_movement' ) ) if not track_utils.has_role(track, artist_role.PERFORMER): error_obj[artist_role.PERFORMER] = format_missing_error( field_name=artist_role.PERFORMER, error_code=error.MISSING_PERFORMER ) return error_obj def validate_track_composer(track, product_genre_type, product_subgenre_type): """Validate track composer exists with specific genre/subgenre. Args: track (dict): Track.to_dict() Returns: dict: errors or empty dict if field is valid """ error_obj = {} if not track_utils.has_role(track, artist_role.COMPOSER): error_obj[artist_role.COMPOSER] = format_error( validator=error.REQUIRED_CODE, message=json.dumps( { 'genre': product_genre_type, 'subgenre': product_subgenre_type } ), error_code=error.MISSING_COMPOSER ) return error_obj def validate_track_name(track, used_track_names): """Validate 'track_name' field. Args: track (dict): Track dictionary used_track_names (set): Set of unavailable (track_name, version,) """ error_obj = {} prop_set = track_utils.get_track_artists_name_ver_artists_tpl(track) if not track[tf.TRACK_NAME]: error_obj[tf.TRACK_NAME] = format_missing_error( field_name=tf.TRACK_NAME, error_code=error.MISSING_TRACK_NAME ) elif prop_set in used_track_names: error_obj[tf.TRACK_NAME] = format_error( validator='used', message='Track Name + Version already used in a previous track', error_code=error.TRACK_NAME_VERSION_ALREADY_IN_USE ) return error_obj def validate_track_name_ff(product_genre_type, track, product_tracks): """Validate 'track_name' + version fields (+ composers on classical) . Args: product_genre_type (str): Indicates genre type of product track (dict): Track dictionary product_tracks (list): Product Tracks data """ error_obj = {} if not track[tf.TRACK_NAME]: error_obj[tf.TRACK_NAME] = format_missing_error( field_name=tf.TRACK_NAME, error_code=error.MISSING_TRACK_NAME ) return error_obj # compare tracks searching for duplicates name/ver/artists first is_classical_product = product_genre_type == product_consts.CLASSICAL_GENRE this_track_name_tpl = track_utils.get_track_artists_name_ver_artists_tpl(track) # iterate thru tracks checking for the duplicate name/ver/artists tuple for other_track in product_tracks: if track['tuid'] == other_track['tuid']: continue other_track_name_tpl = track_utils.get_track_artists_name_ver_artists_tpl(other_track) # no same name/ver/artists on this track, no validation error at all if set(this_track_name_tpl) != set(other_track_name_tpl): continue # for non-classical, this is already a validation error if not is_classical_product: error_obj[tf.TRACK_NAME] = format_error( validator='used', message='Track Name + Version already used in a previous track', error_code=error.TRACK_NAME_VERSION_ALREADY_IN_USE ) continue # for classical, we need to also check they have same composer this_track_composers = track_utils.get_track_artists_by_role(track, artist_role.COMPOSER) this_track_composers_names = {tc['name'] for tc in this_track_composers} other_track_composers = track_utils.get_track_artists_by_role(other_track, artist_role.COMPOSER) other_track_composers_names = {otc['name'] for otc in other_track_composers} # if they have, this is the validation error if other_track_composers_names == this_track_composers_names: error_obj[tf.TRACK_NAME] = format_error( validator='used', message='Track Name + Version + Composer already used in a previous classical track', error_code=error.TRACK_NAME_VERSION_COMPOSER_CLASSICAL_ALREADY_IN_USE ) # endfor return error_obj def validate_track_name_matches_product_name(track, product_name): """Validate 'track_name' field matches the product name. Args: track (dict): Track dictionary product_name (string): Product name """ error_obj = {} if not track_utils.unicode_normalize_compare(track[tf.TRACK_NAME], product_name): error_obj[tf.TRACK_NAME] = format_error( validator='stNameAndProductNameMismatch', message=error.SINGLE_TRACK_NAME_MUST_MATCH_PRODUCT_NAME_MSG, error_code=error.SINGLE_TRACK_NAME_MUST_MATCH_PRODUCT_NAME ) error_obj[tf.TUID] = track[tf.TUID] return error_obj def validate_track_version_matches_product_version(track, product_version): """Validate 'track_version' field matches the product version. Args: track (dict): Track dictionary product_version (string): Product version """ error_obj = {} if not track_utils.unicode_normalize_compare(track[tf.VERSION], product_version): error_obj[tf.VERSION] = format_error( validator='stVersionAndProductVersionMismatch', message=error.SINGLE_TRACK_VERSION_MUST_MATCH_PRODUCT_VERSION_MSG, error_code=error.SINGLE_TRACK_VERSION_MUST_MATCH_PRODUCT_VERSION ) error_obj[tf.TUID] = track[tf.TUID] return error_obj def validate_track_primary_artists_matches_product_primary_artists(track, product_primary_artists): """Validate track primary artists match the product primary artists""" error_obj = {} track_primary_artists = track_utils.get_track_primary_artist_names( track ) normalized_product_primary_artists = track_utils.unicode_normalize_values(product_primary_artists) normalized_track_primary_artists = track_utils.unicode_normalize_values(track_primary_artists) if set(normalized_product_primary_artists) != set(normalized_track_primary_artists): error_obj[tf.PERFORMER] = format_error( validator='stPrimaryArtistsAndProductPrimaryArtistsMismatch', message=error.SINGLE_TRACK_PRIMARY_ARTISTS_MUST_MATCH_PRODUCT_PRIMARY_ARTISTS_MSG, error_code=error.SINGLE_TRACK_PRIMARY_ARTISTS_MUST_MATCH_PRODUCT_PRIMARY_ARTISTS ) error_obj[tf.TUID] = track[tf.TUID] return error_obj def validate_track_featuring_artists_matches_product_featuring_artists(track, product_featuring_artists): """Validate track featuring artists match the product featuring artists""" error_obj = {} track_featuring_artists = track_utils.get_track_featuring_artist_names( track ) normalized_product_featuring_artists = track_utils.unicode_normalize_values(product_featuring_artists) normalized_track_featuring_artists = track_utils.unicode_normalize_values(track_featuring_artists) if set(normalized_product_featuring_artists) != set(normalized_track_featuring_artists): error_obj[tf.FEATURING] = format_error( validator='stFeaturingArtistsAndProductFeaturingArtistsMismatch', message=error.SINGLE_TRACK_FEATURING_ARTISTS_MUST_MATCH_PRODUCT_FEATURING_ARTISTS_MSG, error_code=error.SINGLE_TRACK_FEATURING_ARTISTS_MUST_MATCH_PRODUCT_FEATURING_ARTISTS ) error_obj[tf.TUID] = track[tf.TUID] return error_obj def validate_track_artists_match_product_artists_for_key(key, track, product_artists): """Validate track artists for the given key match the product artists""" error_obj = {} track_artists = track_utils.get_track_artists_by_key( key, track ) normalized_product_artists = track_utils.unicode_normalize_values(product_artists) normalized_track_artists = track_utils.unicode_normalize_values(track_artists) if set(normalized_product_artists) != set(normalized_track_artists): error_obj[key] = format_error( validator=error.TRACK_ARTISTS_VALIDATORS[key], message=error.TRACK_ARTISTS_MESSAGES[key], error_code=error.TRACK_ARTISTS_ERRORS[key] ) error_obj[tf.TUID] = track[tf.TUID] return error_obj def validate_meta_language_code(track): """Validate `meta_language_code` field. Args: track (dict): Track.to_dict() Returns: dict: errors or empty dict if field is valid """ error_obj = {} if not track[tf.META_LANGUAGE_CODE]: error_obj[tf.META_LANGUAGE_CODE] = format_missing_error( field_name=tf.META_LANGUAGE_CODE, error_code=error.MISSING_META_LANGUAGE_CODE ) return error_obj def validate_isrc(track): """Validate 'isrc' field.""" error_obj = {} if not track[tf.ISRC]: error_obj[tf.ISRC] = format_missing_error( field_name=tf.ISRC, error_code=error.MISSING_ISRC ) else: if not validation_utils.is_isrc(track[tf.ISRC]): error_obj[tf.ISRC] = format_error( validator='isrc_pattern_mismatch', message='This ISRC is invalid.', error_code=error.INVALID_ISRC ) return error_obj def validate_pline(track): """Validate 'p_info' field.""" error_obj = {} if not track[tf.P_INFO]: error_obj[tf.P_INFO] = format_missing_error( field_name=tf.P_INFO, error_code=error.MISSING_P_INFO ) else: if not validation_utils.is_pinfo(track[tf.P_INFO]): error_obj[tf.P_INFO] = format_error( validator='p_info_pattern_mismatch', message='This P Line is invalid.', error_code=error.INVALID_P_INFO ) return error_obj def validate_explicit(track): """Validate 'explicit' field.""" error_obj = {} if not track[tf.EXPLICIT]: error_obj[tf.EXPLICIT] = format_missing_error( field_name=tf.EXPLICIT, error_code=error.MISSING_EXPLICIT ) return error_obj def validate_master_rights(track): """Validate the master rights level for a track is properly set. Args: track (dict): Track.to_dict() Returns: dict: errors or empty dict if field is valid """ if track[tf.OWNERSHIP_RIGHTS] not in ownership_type.OWNERSHIP_TYPES: return {tf.OWNERSHIP_RIGHTS: format_missing_error( field_name=tf.OWNERSHIP_RIGHTS, error_code=error.MISSING_OWNERSHIP_RIGHTS )} is_owner = ownership_type.IS_OWNER_MAP[track[tf.OWNERSHIP_RIGHTS]] return _validate_track_master_rights_additional_fields(track, is_owner) def _validate_track_master_rights_additional_fields(track, is_owner): """Validate additional fields for track master rights. Country of Recording and Nationality of 1st Producer need to be specified when the account has rights for a track. Args: track (dict): Track.to_dict() is_owner (bool): Does account have track master rights Returns: dict: errors or empty dict if field is valid """ error_obj = {} if not is_owner: return error_obj if not track[tf.RECORDING_COUNTRY_ID]: error_obj[tf.RECORDING_COUNTRY_ID] = format_missing_error( field_name=tf.RECORDING_COUNTRY_ID, error_code=error.MISSING_RECORDING_COUNTRY_ID ) if not track[tf.ORIGINAL_RIGHTS_HOLDER_COUNTRY_ID]: error_obj[tf.ORIGINAL_RIGHTS_HOLDER_COUNTRY_ID] = ( format_missing_error( field_name=tf.ORIGINAL_RIGHTS_HOLDER_COUNTRY_ID, error_code=error.MISSING_ORIGINAL_RIGHTS_HOLDER_COUNTRY_ID )) return error_obj def validate_artists(track): """Validate 'artists' fields.""" key = tf.ARTISTS if tf.PRIMARY_ARTIST in track: key = tf.PRIMARY_ARTIST error_obj = {} if track[key]: for artist in track[key]: if validation_utils.is_artist_required(artist['type']) and \ validation_utils.is_artist_name_empty(artist['name']): error_obj[artist['type']] = format_error( validator='artist_empty', message='Artist cannot be empty.', error_code=error.EMPTY_ARTIST_NAME ) # TODO: Subgenre is missing at this point if not validation_utils.is_artist_valid(artist): error_obj[artist_role.PERFORMER] = format_error( validator='genre_artist_mismatch', message='This artist is not eligible for this subgenre.', error_code=error.GENRE_ARTIST_MISMATCH ) if not track_utils.has_role(track, artist_role.PERFORMER): error_obj[artist_role.PERFORMER] = format_missing_error( field_name=artist_role.PERFORMER, error_code=error.MISSING_PERFORMER ) else: error_obj[artist_role.PERFORMER] = format_missing_error( field_name=artist_role.PERFORMER, error_code=error.MISSING_PERFORMER ) return error_obj def validate_featuring(track, featuring_artists): """Validate 'featuring' field.""" error_obj = {} track_level_feat_artists = track_utils.get_track_featuring_artist_names( track ) for feat_artist in featuring_artists: if not is_valid_artist_name(feat_artist): continue if feat_artist not in track_level_feat_artists: error_obj[tf.FEATURING] = format_error( validator='featured_required', message=( 'There are featured artist(s) listed at the release ' 'level (Basics). They must also be listed on all tracks.' ), error_code=error.FEATURED_ARTIST_MISMATCH ) return error_obj def validate_writers(track): """Validate 'writers' field.""" error_obj = {} if not track[tf.WRITERS]: error_obj[tf.WRITERS] = format_missing_error( field_name=tf.WRITERS, error_code=error.MISSING_WRITERS ) return error_obj def validate_producers(track, is_correction_mode=False, track_corrections={}): """Validate 'producers' field.""" error_obj = {} producers = [] correction_data_for_track = track_corrections.get(track.get('tuid'), {}) if is_correction_mode and tf.PRODUCER in correction_data_for_track: producers = correction_data_for_track.get(tf.PRODUCER) if not producers: error_obj[tf.PRODUCER] = format_missing_error( field_name=tf.PRODUCER, error_code=error.MISSING_PRODUCERS ) return error_obj if track[tf.ARTISTS]: for artist in track[tf.ARTISTS]: if artist['type'] == tf.PRODUCER: if validation_utils.is_artist_name_empty(artist['name']): error_obj[artist['type']] = format_error( validator='artist_empty', message=error.EMPTY_PRODUCER_NAME_ERROR, error_code=error.EMPTY_PRODUCER_NAME ) else: producers.append(artist['name']) if not producers: error_obj[tf.PRODUCER] = format_missing_error( field_name=tf.PRODUCER, error_code=error.MISSING_PRODUCERS ) return error_obj def validate_track_audio_exists( track, product_assets, is_correction_mode=False): """Validate track has associated audio file. ows-assets returns information about all assets of particular product. Args: track (dict): Track.to_dict() product_assets (list): list of track asset dicts from ows-assets is_correction_mode (bool): Correction mode indicator Returns: dict: errors or empty dict if field is valid """ error_obj = {} if product_assets is None: error_obj[services_const.AUDIO_FILE] = format_error( validator='status_unknown', message=error.ASSET_STATUS_MISSING, error_code=error.MISSING_ASSET_STATUS ) return error_obj track_asset = _get_track_asset( track, product_assets, is_correction_mode, services_const.ASSET_UPLOAD_TYPE_STEREO) if track_asset and track_asset[services_const.UPLOAD_STATUS] \ == services_const.ASSETS_V2_STATUS_ENCODING_COMPLETED: return {} audio_file_error = format_missing_error( field_name=services_const.AUDIO_FILE, error_code=error.MISSING_AUDIO_FILE ) error_obj[services_const.AUDIO_FILE] = audio_file_error return error_obj def validate_atmos_audio_exists( track, product_assets, spatial_isrc_by_track_id, is_correction_mode=False): """Validate ATMOS asset and spatial ISRC consistency. ATMOS is optional — if absent and no spatial ISRC exists, no errors are returned. If absent but a spatial ISRC is present, an error is returned. When present, it must have encoding_completed status and the track must have a corresponding spatial ISRC. Args: track (dict): Track.to_dict() product_assets (list): list of track asset dicts from ows-assets spatial_isrc_by_track_id (dict): mapping of track_id to spatial ISRC is_correction_mode (bool): Correction mode indicator Returns: dict: errors or empty dict if field is valid """ error_obj = {} if product_assets is None: error_obj[services_const.ATMOS_FILE] = format_error( validator='status_unknown', message=error.ASSET_STATUS_MISSING, error_code=error.MISSING_ASSET_STATUS ) return error_obj atmos_asset = _get_track_asset( track, product_assets, is_correction_mode, services_const.ASSET_UPLOAD_TYPE_ATMOS) spatial_isrc = spatial_isrc_by_track_id.get(track[tf.TUID]) if not atmos_asset: if spatial_isrc: error_obj[services_const.SPATIAL_ISRC] = format_error( validator='unexpected_spatial_isrc', message=error.UNEXPECTED_SPATIAL_ISRC_MSG, error_code=error.UNEXPECTED_SPATIAL_ISRC ) return error_obj if atmos_asset[services_const.UPLOAD_STATUS] != services_const.ASSETS_V2_STATUS_ENCODING_COMPLETED: error_obj[services_const.ATMOS_FILE] = format_error( validator='encoding_not_completed', message=error.ATMOS_ENCODING_NOT_COMPLETED_MSG, error_code=error.MISSING_ATMOS_AUDIO_FILE ) return error_obj if not spatial_isrc: error_obj[services_const.SPATIAL_ISRC] = format_missing_error( field_name=services_const.SPATIAL_ISRC, error_code=error.MISSING_SPATIAL_ISRC ) elif not validation_utils.is_isrc(spatial_isrc): error_obj[services_const.SPATIAL_ISRC] = format_error( validator='isrc_pattern_mismatch', message=error.INVALID_SPATIAL_ISRC_MSG, error_code=error.INVALID_ISRC ) return error_obj def validate_preview_start_time( track, product_assets, is_correction_mode=False): """Preview start time value should be less than track length. Args: track (dict): Track.to_dict() product_assets (list): list of track asset dicts returned from ows-assets is_correction_mode (bool): Correction mode indicator Returns: dict: errors or empty dict if field is valid """ if not track[tf.PREVIEW_START_TIME]: return {} error_obj = {} track_asset = _get_track_asset( track, product_assets, is_correction_mode, services_const.ASSET_UPLOAD_TYPE_STEREO) if track_asset is None or not _is_asset_upload_finished(track_asset): return {} if not track_asset[services_const.DURATION]: error_obj[tf.PREVIEW_START_TIME] = format_error( validator='status_unknown', message=error.ASSET_STATUS_MISSING, error_code=error.MISSING_ASSET_STATUS ) return error_obj if track[tf.PREVIEW_START_TIME] > track_asset[services_const.DURATION]: error_obj[tf.PREVIEW_START_TIME] = format_error( validator='invalid_value', message=error.INVALID_PREVIEW_START_TIME_MSG, error_code=error.INVALID_PREVIEW_START_TIME ) return error_obj return {} def _get_track_asset(track, product_assets, is_correction_mode=False, asset_upload_type=None): """Get asset of given track, optionally filtered by asset_upload_type. Args: track (dict): Track.to_dict() product_assets (list): list of track asset dicts from ows-assets asset_upload_type (str): optional upload type to filter by (e.g. 'stereo', 'atmos') Returns: dict: track asset """ if not product_assets: return None for asset in product_assets: if asset.get(services_const.TRACK_UNIQUE_ID) != track[tf.TUID]: continue if asset_upload_type and asset.get(services_const.ASSET_UPLOAD_TYPE) != asset_upload_type: continue return asset return None def _is_asset_upload_finished(track_asset): """Check whether asset of particular track has status == finished. Args: track_asset (dict): track asset Returns: bool """ if not track_asset: return False asset_upload_status = track_asset.get(services_const.UPLOAD_STATUS) return asset_upload_status == services_const.ASSETS_V2_STATUS_ENCODING_COMPLETED # noqa