"""Prep ddex file from s3.""" from datetime import datetime, timedelta import re from typing import List, Union from ddex_ingester_common.constants.artists import VARIOUS_ARTISTS, \ VARIOUS_ARTISTS_REPLACEMENT from ddex_ingester_common.constants.classical import COMPOSER_ROLE from ddex_ingester_common.constants.content_type import \ INSTRUMENTAL_CONTENT_TYPES from ddex_ingester_common.constants.ddex_providers import \ SME_ANALYTICS_PROVIDER, SOM_LIVRE_VENDOR_ID from ddex_ingester_common.constants.explicit_words import \ EXPLICIT_WORDS from ddex_ingester_common.constants.flow_control import \ PROCESS_NON_CONFIDENTIAL_HOURS_BEFORE_RELEASE from ddex_ingester_common.constants.language_codes import \ INSTRUMENTAL_LANGUAGE_CODE from ddex_ingester_common.constants.localizations \ import (JAPANESE, JAPANESE_KATAKANA, JAPANESE_LANGUAGE_CODES, PREFERRED_JAPANESE_LANGUAGE_CODE) from ddex_ingester_common.constants.release_type import VIDEO_RELEASE_TYPES from ddex_ingester_common.constants.roles import CLASSICAL_GENRES from ddex_ingester_common.constants.us_publishing_obligation import \ DEFAULT_US_PUBLISHING_OBLIGATION, PARTIALLY_CONTROLLED from ddex_ingester_common.helpers.list import have_common_elements from ddex_ingester_common.models.s3.body import Body as S3Body from ddex_ingester_common.models.s3.localized_participant import ( LocalizedParticipant) from ddex_ingester_common.models.s3.localized_title import LocalizedTitle from ddex_ingester_common.schemas.s3_schema import ParticipantSchema MAX_ORIGINAL_RELEASE_DATE = 2100 MIN_ORIGINAL_RELEASE_DATE = 1900 def prep_s3_context(ddex_provider, release_type, s3_context: S3Body, artwork_ingestion_only, is_ccm_som_livre_grps_ddex_ingestion_enabled, logger): """Prepare S3Body.""" prep_localizations(ddex_provider, s3_context, logger) prep_lyrics_language(s3_context) prep_us_publishing_obligation(s3_context, is_ccm_som_livre_grps_ddex_ingestion_enabled) prep_artists(s3_context, ddex_provider) prep_original_release_date(s3_context) prep_plines(s3_context, ddex_provider) prep_cline(s3_context) prep_track_lyrics(s3_context, ddex_provider) prep_participants(s3_context, ddex_provider) prep_track_name_length(s3_context) prep_missing_track_original_release_date(s3_context, ddex_provider) prep_preorder_tracks(s3_context, artwork_ingestion_only, ddex_provider, logger) remove_track_isrc_duplicates(s3_context, ddex_provider) prep_track_sequence_and_volume_number(s3_context) prep_forbidden_sequence(s3_context) if is_classical_release(s3_context): prep_composers(s3_context) if release_type in VIDEO_RELEASE_TYPES: prep_video_content_language(s3_context) return s3_context def is_classical_release(s3_context: S3Body) -> bool: """Check if a release is classical.""" genre_names = [genre.genre for genre in s3_context.product.genres] # If any product genre matches the release is classical return have_common_elements(genre_names, CLASSICAL_GENRES) def prep_localizations(ddex_provider, s3_context: S3Body, logger): """Prep localization metadata within the context.""" # Format each localization section individually to match validation rules if s3_context.product.localized_titles: logger.info('Prepping product title localizations') s3_context.product.localized_titles = ( format_localizations(ddex_provider, s3_context.product.localized_titles, logger)) if s3_context.product.display_artists: for display_artist in s3_context.product.display_artists: if display_artist.localized_names: logger.info('Prepping product display artist localizations') display_artist.localized_names = format_localizations( ddex_provider, display_artist.localized_names, logger ) for track in s3_context.tracks: if track.localized_titles: logger.info('Prepping track title localizations') track.localized_titles = format_localizations( ddex_provider, track.localized_titles, logger ) if track.display_artists: for artist in track.display_artists: if artist.localized_names: logger.info('Prepping track artist localizations') artist.localized_names = format_localizations( ddex_provider, artist.localized_names, logger) def format_localizations(ddex_provider, localizations: List[ Union[LocalizedParticipant, LocalizedTitle]], logger) -> List[Union[LocalizedTitle, LocalizedParticipant]]: """Only include preferred japanese localization.""" if ddex_provider == SME_ANALYTICS_PROVIDER: return [] formatted_localizations = [] language_codes = [ localization.language_code.upper() for localization in localizations] if JAPANESE in language_codes and JAPANESE_KATAKANA in language_codes: # We have both japanese language codes in localization logger.info('Duplicate Japanese language codes detected: ' f'in localizations {localizations}') for localization in localizations: language_code = localization.language_code.upper() valid_localization = ( language_code == PREFERRED_JAPANESE_LANGUAGE_CODE or language_code not in JAPANESE_LANGUAGE_CODES ) if valid_localization: formatted_localizations.append(localization) logger.info(f'Formatted localizations: {formatted_localizations}') return formatted_localizations else: # No formatting needed, return original input logger.info(f'No localizations need formatting: {localizations}') return localizations def prep_lyrics_language(s3_context: S3Body): """Prep lyrics language within the context.""" # Instrumental tracks do not have a lyrics language if s3_context.tracks: for track in s3_context.tracks: if not track.lyrics_language: if track.content_type in INSTRUMENTAL_CONTENT_TYPES: track.lyrics_language = INSTRUMENTAL_LANGUAGE_CODE else: track.lyrics_language = \ s3_context.product.metadata_language def prep_us_publishing_obligation(s3_context: S3Body, is_ccm_som_livre_grps_ddex_enabled): """Prep US Publishing Obligation within the context.""" for track in s3_context.tracks or []: # Hard-coding Som Livre default publishing obligation for now if (is_ccm_som_livre_grps_ddex_enabled and s3_context.product.vendor_id == SOM_LIVRE_VENDOR_ID): track.us_publishing_obligation = PARTIALLY_CONTROLLED elif not track.us_publishing_obligation: track.us_publishing_obligation = DEFAULT_US_PUBLISHING_OBLIGATION def prep_video_content_language(s3_context: S3Body): """Prep content language for a video within the context.""" # Instrumental videos do not have a content language if not s3_context.video.content_language: if s3_context.video.content_type in INSTRUMENTAL_CONTENT_TYPES: s3_context.video.content_language = INSTRUMENTAL_LANGUAGE_CODE else: s3_context.video.content_language = \ s3_context.video.title_language def prep_composers(s3_context: S3Body): """Prep composers for a classical release within the context. If there are no composers on a product, use the track composers. """ product_composers = False product_artists = {} for artist in s3_context.product.display_artists or []: product_artists[artist.name] = artist for role in artist.roles or []: if role == COMPOSER_ROLE: product_composers = True if product_composers: return # Dict to avoid duplicates new_composers = {} for track in s3_context.tracks or []: for artist in track.display_artists or []: for role in artist.roles or []: if role == COMPOSER_ROLE: if artist.name in product_artists: if COMPOSER_ROLE not in product_artists[ artist.name].roles: # noqa product_artists[artist.name].roles.append( COMPOSER_ROLE) else: # Serialize to avoid changing track artist object new_composers[artist.name] = \ ParticipantSchema().dump(artist) new_composers[artist.name]['roles'] = [COMPOSER_ROLE] for key in new_composers: s3_context.product.display_artists.append(new_composers[key]) def prep_artists(s3_context: S3Body, ddex_provider): """Replace various artists to Multiple Artists.""" if ddex_provider != SME_ANALYTICS_PROVIDER: return all_participants = [] for track in s3_context.tracks: if track.display_artists: all_participants.extend(track.display_artists) if track.resource_contributors: all_participants.extend(track.resource_contributors) for participant in all_participants: if participant.name.lower() in VARIOUS_ARTISTS: participant.name = VARIOUS_ARTISTS_REPLACEMENT if participant.localized_names: for participant_localized in participant.localized_names: if participant_localized.name.lower() \ in VARIOUS_ARTISTS: participant_localized.name \ = VARIOUS_ARTISTS_REPLACEMENT def prep_plines(s3_context: S3Body, ddex_provider): """Add year to p_line if missing.""" valid_year_regex = r'.*(19|20)\d{2}(,\s(19|20)\d{2})*.*$' any_year_regex = r'.*[0-9]{4}.*' def is_valid_date(date): return date and re.match(valid_year_regex, date, flags=re.ASCII) if ddex_provider == SME_ANALYTICS_PROVIDER: p_line = s3_context.product.p_line if p_line and not \ re.match('[0-9]{4}[ ]+\\S+', p_line, flags=re.ASCII) \ and re.match('[0-9]{4}', p_line, flags=re.ASCII): s3_context.product.p_line = \ s3_context.product.p_line.strip() + ' .' for track in s3_context.tracks or []: p_line = track.p_line if not p_line: continue if re.match(any_year_regex, p_line, flags=re.ASCII) and not re.match(valid_year_regex, p_line, flags=re.ASCII): p_line = re.sub(r'[0-9]{4}', '', p_line).replace(' ', ' ') if not re.match(r'^(19|20)\d{2}', p_line, flags=re.ASCII): if is_valid_date(track.original_release_date): release_date = track.original_release_date elif is_valid_date(s3_context.product.original_release_date): release_date = s3_context.product.original_release_date elif is_valid_date( s3_context.deals[0].deal_terms[0].start_date): release_date = s3_context.deals[0].deal_terms[0].start_date else: release_date = \ s3_context.deals[0].deal_terms[0].start_date_time track.p_line = f'{release_date[:4]} {p_line.lstrip()}' if re.match(r'.*(19|20)\d{2}$', p_line, flags=re.ASCII): track.p_line = track.p_line + ' .' def prep_track_lyrics(s3_context: S3Body, ddex_provider): """Remove swear words from track.lyrics.""" if ddex_provider == SME_ANALYTICS_PROVIDER: for track in s3_context.tracks or []: if track.lyrics: RE_EXPLICIT_WORDS_WORD_BOUNDARIES = [] for explicit_word in EXPLICIT_WORDS: RE_EXPLICIT_WORDS_WORD_BOUNDARIES.append( re.compile(r'\b({0})\b'.format(explicit_word), re.IGNORECASE) ) for word in RE_EXPLICIT_WORDS_WORD_BOUNDARIES: track.lyrics = re.sub(word, '***', track.lyrics) def prep_participants(parsed_ddex: S3Body, ddex_provider): """For participant with different spotify/apple ids set min id to all.""" if ddex_provider == SME_ANALYTICS_PROVIDER: all_participants = get_all_participants(parsed_ddex) for participant in all_participants: if participant.spotify_uri and \ participant.spotify_uri.lower() == 'new': participant.spotify_uri = None name_to_spotify_uri = {} name_to_apple_id = {} for participant in set( [(p.name, p.spotify_uri, p.apple_id) for p in all_participants if p.spotify_uri or p.apple_id]): name = participant[0] if participant[1]: if name in name_to_spotify_uri: name_to_spotify_uri[name] = min(name_to_spotify_uri[name], participant[1]) else: name_to_spotify_uri[name] = participant[1] if participant[2]: if name in name_to_apple_id: name_to_apple_id[name] = min(name_to_apple_id[name], participant[2]) else: name_to_apple_id[name] = participant[2] for participant in all_participants: if participant.spotify_uri: participant.spotify_uri = name_to_spotify_uri[participant.name] if participant.apple_id: participant.apple_id = name_to_apple_id[participant.name] def get_all_participants(parsed_ddex): """Collect all participants from parsed_ddex, including duplicates. Args: parsed_ddex(S3Schema): deserialized ddex from S3 Returns: list """ all_participants = [] if parsed_ddex.project and parsed_ddex.project.artist: all_participants.append(parsed_ddex.project.artist) if parsed_ddex.product.display_artists: all_participants.extend(parsed_ddex.product.display_artists) for track in parsed_ddex.tracks: if track.display_artists: all_participants.extend(track.display_artists) if track.resource_contributors: all_participants.extend(track.resource_contributors) if parsed_ddex.video: if parsed_ddex.video.display_artists: all_participants.extend(parsed_ddex.video.display_artists) if parsed_ddex.video.resource_contributors: all_participants.extend(parsed_ddex.video.resource_contributors) return all_participants def prep_original_release_date(parsed_ddex): """Set original_release_date to None if date in aut of expected range.""" if parsed_ddex.product.original_release_date: release_date_obj = datetime.strptime( parsed_ddex.product.original_release_date, '%Y-%m-%d') if release_date_obj.year < MIN_ORIGINAL_RELEASE_DATE \ or release_date_obj.year > MAX_ORIGINAL_RELEASE_DATE: parsed_ddex.product.original_release_date = None def prep_track_name_length(s3_context: S3Body): """Truncate track names longer than 200 characters.""" for track in s3_context.tracks: if len(track.track_name) > 200: track.track_name = track.track_name[:197] + '...' def prep_forbidden_sequence(s3_context: S3Body): """Break up the '../' sequence to avoid a 403 from GQL.""" def contains_forbidden_sequence(field): return field and ('../' in field or '..\\' in field or '.bak' in field) def fix_sequence(invalid_str: str): if contains_forbidden_sequence(invalid_str): return invalid_str.replace('../ ', '.. / ') \ .replace('../', '.. / ') \ .replace('..\\', '.. \\').replace('.bak', '. bak') else: return invalid_str s3_context.project.description = fix_sequence( s3_context.project.description) s3_context.project.name = fix_sequence(s3_context.project.name) s3_context.product.product_name = fix_sequence( s3_context.product.product_name) s3_context.product.p_line = fix_sequence(s3_context.product.p_line) s3_context.product.c_line = fix_sequence(s3_context.product.c_line) for track in s3_context.tracks: track.track_name = fix_sequence(track.track_name) track.lyrics = fix_sequence(track.lyrics) track.p_line = fix_sequence(track.p_line) def prep_preorder_tracks(s3_context: S3Body, artwork_ingestion_only, ddex_provider, logger): """Filter out preorder tracks with future release date. Since ddex file may contain tracks with different release dates. We have to create tracks only with release date <= release_date_threshold. """ if ddex_provider == SME_ANALYTICS_PROVIDER: tracks = [] if artwork_ingestion_only: logger.info('Artwork ingestion only insertion') s3_context.tracks = [] return if s3_context.product.is_confidential: release_date_threshold = datetime.now() else: release_date_threshold = datetime.now() + timedelta( hours=PROCESS_NON_CONFIDENTIAL_HOURS_BEFORE_RELEASE) for track in s3_context.tracks: if track.original_release_date and datetime.strptime( track.original_release_date, '%Y-%m-%d') <= release_date_threshold: tracks.append(track) s3_context.tracks = tracks def prep_track_sequence_and_volume_number(s3_context: S3Body): """Update sequence/volume after prep_preorder_tracks.""" max_sequence_number_per_volume = {} volumes = set() for track in s3_context.tracks: track_volume = track.volume volumes.add(track_volume) if track_volume not in max_sequence_number_per_volume: max_sequence_number_per_volume[track_volume] = 1 else: max_sequence_number_per_volume[track_volume] = \ max_sequence_number_per_volume[track_volume] + 1 track.sequence_number = max_sequence_number_per_volume[track_volume] # prep_preorder_tracks might remove all tracks for entire volume sorted_volumes = sorted(volumes) for new_volume, volume in enumerate(sorted_volumes): new_volume = new_volume + 1 if new_volume == volume: continue for track in s3_context.tracks: if track.volume == volume: track.volume = new_volume def remove_track_isrc_duplicates(s3_context: S3Body, ddex_provider): """Remove track isrc duplicates.""" if ddex_provider == SME_ANALYTICS_PROVIDER: added_isrc = [] tracks = [] for track in s3_context.tracks: if track.isrc not in added_isrc: added_isrc.append(track.isrc) tracks.append(track) s3_context.tracks = tracks def prep_cline(s3_context: S3Body): """Prep c_line.""" c_line = s3_context.product.c_line if c_line and not re.match('[0-9]{4}[ ]+\\S+', c_line, flags=re.ASCII) and re.match('[0-9]{4}', c_line, flags=re.ASCII): s3_context.product.c_line = s3_context.product.c_line.strip() + ' .' def prep_missing_track_original_release_date(s3_context: S3Body, ddex_provider): """Prep missing track release date.""" if ddex_provider == SME_ANALYTICS_PROVIDER: product_ord = s3_context.product.original_release_date if not product_ord: return for track in s3_context.tracks: if not track.original_release_date: track.original_release_date = product_ord