"""Localization helper functions.""" from collections import defaultdict from copy import deepcopy from flask import g from backend.constants import api as api_constants from backend.constants import error from backend.constants import track_field from backend.constants import track_role from backend.constants.field import NAMES_FIELD_MODIFIER from backend.models import ows_product from backend.utils import api as api_utils def patch_tracks_localizations(tracks_response, localizations_delta): """Patch localizations for serveral tracks. Args: tracks_response (Response): Tracks response object. localizations_delta (list): List of localization updates to apply. Returns: Response: A Response obj with track JSON data """ if not localizations_delta: return tracks_response tracks_response = \ add_track_localizations_to_response(tracks_response) if not tracks_response: return tracks_response localizations_delta_map = {} for loc in localizations_delta: localizations_delta_map[loc[track_field.LANGUAGE_ID]] = loc tracks = tracks_response.message[api_constants.ITEMS] bulk_localization_update_list = [] new_track_list = [] for track in tracks: track_with_loc = create_updates_for_track_localizations( track, localizations_delta_map) bulk_localization_update_list += \ track_with_loc[track_field.LOCALIZATIONS] new_track_list.append(track_with_loc) # Send localization updates to OWS Product localization_response = ows_product.update_localizations( bulk_localization_update_list) if localization_response: tracks_response = api_utils.create_get_list_response(new_track_list) else: error_msg = '{message} {error}'.format( message=error.OWS_PRODUCT_FAILED_TO_UPDATE_LOCALIZATIONS_MSG, error=localization_response.errors.get('message')) g.log.error(error_msg) return tracks_response def create_updates_for_track_localizations(track, localizations_delta_map): """Create update list and new track object for applied localization delta. Any localizations for languages not specified for fields in the delta map will be cleared out. track = { 'tuid': 1, 'version': 'remix', ... 'localizations': [ {'language_id': 2, 'version': 'red', ...}, {'language_id': 3, 'version': 'green', ...}, {'language_id': 4, 'version': 'blue', ...}] } localizations_delta_map = { 2: {'language_id': 2, 'version': 'red'}, 4: {'language_id': 4, 'version': 'blue2'}, 5: {'language_id': 5, 'version': 'orange'} } Args: Track (dict): track data with localizations localizations_delta_map (list): Localizations to apply Return: tuple: Updated track object and localizations updates that need to be applied """ track = deepcopy(track) track_language_id = code_to_language_id( track[track_field.META_LANGUAGE_CODE]) localization_updates = [] all_language_ids = set(localizations_delta_map.keys()) processed_language_ids = set() # Get fields in each dict of localizations_delta_map and validate update_fields = set(next(iter(localizations_delta_map.values())).keys()) for language_id, localizations_delta in localizations_delta_map.items(): if localizations_delta.keys() != update_fields: raise ValueError( 'Localization fields to update must be the same') # Iterate through existing track localizations and update for track_loc in track[track_field.LOCALIZATIONS]: language_id = track_loc[track_field.LANGUAGE_ID] processed_language_ids.add(language_id) if language_id in localizations_delta_map: # Update existing track localization _update_track_localization_with_delta( track_loc, track.get('artists'), localizations_delta_map[language_id]) else: # No localization was specified for this language, # so clear out any fields that are being updated for field in update_fields: if field.endswith(NAMES_FIELD_MODIFIER): artist_role = field[:-len(NAMES_FIELD_MODIFIER)] if artist_role not in track_role.ARTISTS: raise ValueError('Invalid Artist Role') track_localized_artists = [] for localized_artist in track_loc[track_field.ARTISTS]: if localized_artist[track_field.TYPE] != artist_role: track_localized_artists.append(localized_artist) track_loc[track_field.ARTISTS] = track_localized_artists elif field not in (track_field.TUID, track_field.LANGUAGE_ID,): track_loc[field] = '' localization_updates.append(track_loc) # Add new localizations to track as needed new_language_ids = all_language_ids - processed_language_ids for language_id in new_language_ids: # Don't add localization if track's meta language is the same if language_id == track_language_id: continue track_loc = _make_localization_dict( tuid=track[track_field.TUID], language_id=language_id) _update_track_localization_with_delta( track_loc, track.get('artists'), localizations_delta_map[language_id]) track[track_field.LOCALIZATIONS].append(track_loc) localization_updates.append(track_loc) return track def _update_track_localization_with_delta( track_localization, track_artists, localization_delta): """Update single track localization with delta updates. The order of the artist data is preserved. Args: track_localization (dict): A single track localization track_artists (list): List of all track artists localization_delta (dict): Changes to make to track localization """ for field in localization_delta.keys(): if field.endswith(NAMES_FIELD_MODIFIER): if track_artists is None: raise Exception('Internal Error: Track artists not provided') artist_role = field[:-len(NAMES_FIELD_MODIFIER)] if artist_role not in track_role.ARTISTS: raise ValueError('Invalid Artist Role') # We want constant lookups in the next step artist_loc_lookup_map = {} for localized_artist in track_localization[track_field.ARTISTS]: track_artist_id = localized_artist[track_field.TRACK_ARTIST_ID] artist_loc_lookup_map[track_artist_id] = localized_artist track_localized_artists = [] names = localization_delta[field] names_index = 0 # Make updated list of track artists for artist in track_artists: if artist[track_field.TYPE] != artist_role: # Preserve any existing artists track_artist_id = artist[track_field.TRACK_ARTIST_ID] if track_artist_id in artist_loc_lookup_map: loc_artist = artist_loc_lookup_map[track_artist_id] track_localized_artists.append(loc_artist) elif names_index < len(names): loc_artist = artist.copy() loc_artist['name'] = names[names_index] track_localized_artists.append(loc_artist) names_index += 1 track_localization[track_field.ARTISTS] = track_localized_artists else: track_localization[field] = localization_delta[field] def add_track_localizations_to_response(tracks_response): """Add localization data to tracks response object. Args: tracks (object): Track Response object to update Returns: Response: An updated Response obj with localizations """ if not tracks_response: return tracks_response if api_constants.ITEMS in tracks_response.message: tracks = tracks_response.message[api_constants.ITEMS] else: tracks = [tracks_response.message] if not tracks: return tracks_response res = _add_localizations_to_tracks_list(tracks) if not res: return res return tracks_response def _add_localizations_to_tracks_list(tracks): """Add localization data to list of track. Args: tracks (list): List of track dictionaries Returns: response.Response """ # Get track localizations tuids = [track[track_field.TUID] for track in tracks] track_localization_response = ows_product.get_track_localizations(tuids) if not track_localization_response: return track_localization_response all_tracks_localizations = track_localization_response.message[ api_constants.ITEMS] track_localizations_map = _group_localizations_by_tuid( all_tracks_localizations) # Append localization information to each track for track in tracks: _update_track_localization_response( track, track_localizations_map.get(track['tuid'])) return api_utils.create_ok_response() def _update_track_localization_response(track, track_localizations): """Update track localization response. Args: track (dict): track data track_localizations (list): list of localizations for track """ if not track_localizations: track[track_field.LOCALIZATIONS] = [] return # Expand out artists fields for track localizations for localization in track_localizations: if track_field.ARTISTS not in track: # If this exception is being raised, you need to update your code # to pass the track artist data raise ValueError( 'Artist Data must be provided when localizing track response') artists_with_ids = [] artists = localization[track_field.ARTISTS] # Add artist type information for track_artist_id, name in artists.items(): track_artist_id = int(track_artist_id) # Determine artist type by looking at track metadata artist_type = next( (artist[track_field.TYPE] for artist in track[track_field.ARTISTS] if artist[track_field.TRACK_ARTIST_ID] == track_artist_id), '') artists_with_ids.append({ track_field.TRACK_ARTIST_ID: track_artist_id, track_field.TYPE: artist_type, track_field.NAME: name }) # Artists list needs to be sorted by id artists_with_ids.sort( key=lambda artist: artist[track_field.TRACK_ARTIST_ID]) localization[track_field.ARTISTS] = artists_with_ids track[track_field.LOCALIZATIONS] = track_localizations def _make_localization_dict( tuid, language_id, track_name='', version='', artists=None): """Make track localization dict. Args: tuid (int): Primary key of track language_id (int): Language Id track_name (string): Track name localization (optional) version (version): Track version localization (optional) artists (list): List of track artists localizations (optional) """ return { track_field.TUID: tuid, track_field.LANGUAGE_ID: language_id, track_field.TRACK_NAME: track_name, track_field.VERSION: version, track_field.ARTISTS: artists or [] } def copy_tracks_localizations(source_dest_track_list): """Copy localizations from source tracks to destination tracks. The source_dest_track_list has the following format: [{'source': source_track_dict, 'destination': destination_track_dict}, ...] Args: source_dest_track_list (list): List of source->dest tracks mapping Returns: list: successfully copied localizations {tuid: localizations_list} """ # Add localizations to source tracks source_tracks = [ import_dict['source'] for import_dict in source_dest_track_list] localizations_response = _add_localizations_to_tracks_list(source_tracks) if not localizations_response: tuids = [track[track_field.TUID] for track in source_tracks] g.log.error( 'Failed to get localizatons for TUIDs {}. Response {}'.format( list(tuids), localizations_response.message)) return localizations_response new_track_list = [] bulk_localization_update_list = [] # Go through each source and destination pair and copy the localizations for import_dict in source_dest_track_list: track_with_loc = _copy_localizations( from_track=import_dict['source'], to_track=import_dict['destination']) new_track_list.append(track_with_loc) bulk_localization_update_list += \ track_with_loc[track_field.LOCALIZATIONS] # Send localization updates to OWS Product localization_response = ows_product.update_localizations( bulk_localization_update_list) if localization_response: return api_utils.create_get_list_response(new_track_list) else: # Log error error_msg = '{message} {error}'.format( message=error.OWS_PRODUCT_FAILED_TO_UPDATE_LOCALIZATIONS_MSG, error=localization_response.errors.get('message')) g.log.error(error_msg) return localization_response def _copy_localizations(from_track, to_track): """Copy localizations from from_track to to_track. Data is not persisted - this must be done in the calling function. Args: from_track (dict): Track dictionary with localizations to copy to_track (dict): Destination track dictionary Returns: dict: A copy of destination track with copied localizations """ # Build a lookup that maps a tuple of artist (type, name) to artist_id src_artist_id_lookup = {} for src_artist in from_track[track_field.ARTISTS]: key = (src_artist[track_field.TYPE], src_artist[track_field.NAME],) src_artist_id_lookup[key] = src_artist[track_field.TRACK_ARTIST_ID] # Build a lookup that maps the source artist_id to destination artist_id src_to_dest_artist_id_map = {} for dest_artist in to_track[track_field.ARTISTS]: key = (dest_artist[track_field.TYPE], dest_artist[track_field.NAME],) src_artist_id = src_artist_id_lookup.get(key) dest_artist_id = dest_artist[track_field.TRACK_ARTIST_ID] src_to_dest_artist_id_map[src_artist_id] = dest_artist_id # Build localizations dest_localizations = [] for src_loc in from_track[track_field.LOCALIZATIONS]: # Go through each localized source artist and make a new artist # localization using the destination artist_id artists = [] for src_artist in src_loc[track_field.ARTISTS]: src_artist_id = src_artist[track_field.TRACK_ARTIST_ID] dest_artist_id = src_to_dest_artist_id_map[src_artist_id] dest_artist = src_artist.copy() dest_artist[track_field.TRACK_ARTIST_ID] = dest_artist_id artists.append(dest_artist) dest_localizations.append(_make_localization_dict( tuid=to_track[track_field.TUID], language_id=src_loc[track_field.LANGUAGE_ID], track_name=src_loc[track_field.TRACK_NAME], version=src_loc[track_field.VERSION], artists=artists)) updated_to_track = deepcopy(to_track) updated_to_track[track_field.LOCALIZATIONS] = dest_localizations return updated_to_track def _group_localizations_by_tuid(localizations): """ Group localizations by tuid. Args: localizations (list): list of localization dicts Returns: dict: {tuid: localizations_list} """ result = defaultdict(list) for loc in localizations: result[loc[track_field.TUID]].append(loc) return result def get_localization_languages(): """Get a cached list of localization languages. Returns: list: list of language dictionaries. """ result = ows_product.get_localization_languages() if result: return tuple(result.message['items']) return () def language_id_to_code(language_id): """Convert language id to a language code. Args: language_id (int): language id. Returns: str: language code """ result = None languages = get_localization_languages() language = next( (language_item for language_item in languages if language_item['language_id'] == language_id), None) if language: result = language['meta_language_code'] return result def code_to_language_id(code): """Convert meta_language_code to language id. Args: code (str): language code. Returns: int: language id """ result = None languages = get_localization_languages() language = next( (language_item for language_item in languages if language_item['meta_language_code'] == code), None) if language: result = language['language_id'] return result