"""Lambda function module for set_track_metadata.""" from typing import Dict, List, Optional from common.constants.role_mappings import ARTIST_ROLE_MAP from common.models.state_machine.grps_ingestion_context import \ GrpsIngestionContext from common.models.state_machine.label_participant import LabelParticipant from common.models.state_machine.participant import Participant from common.models.state_machine.track import Track from common.schemas.state_machine_schema import ProductType, \ StateMachineSchema, TrackSchema from lambdacommon.graphql import graphql import config from config import graphql_gateway from src.constants.errors import KNOWN_GRAPHQL_ERRORS, \ OWS_PRODUCT_USER_IS_FORBIDDEN from src.constants.queries import UPDATE_TRACK_METADATA from src.exceptions import SetTrackMetadataException logger = config.app_logger def handler(event, context): """Set track metadata handler. Called from a Map state — receives the full state machine context and a single track, updates its metadata and label participants in Orchard via GraphQL. """ logger.info(f'Triggered set_track_metadata: {event}') sm_context = StateMachineSchema().load(event.get('context')) track = TrackSchema().load(event.get('track')) correlation_id = sm_context.correlation_id graphql_gateway.set_headers( { 'Orchard-User-Id': config.OA_USER, 'Correlation-Id': correlation_id, } ) unmapped_roles = get_unique_roles(track) try: update_track_metadata( sm_context, sm_context.label_participants, sm_context.product.upc, track.tuid, track, unmapped_roles, ) except graphql.GraphQLError as err: if OWS_PRODUCT_USER_IS_FORBIDDEN in str(err): logger.info(f'Found known GraphQL error: {str(err)}') raise SetTrackMetadataException( OWS_PRODUCT_USER_IS_FORBIDDEN ) from err for known_error in KNOWN_GRAPHQL_ERRORS: if known_error in str(err): logger.info(f'Found known GraphQL error: {str(err)}') graphql_response = err.response if type(graphql_response) is not dict: graphql_response = eval(graphql_response) graphql_response['ISRC'] = track.isrc raise SetTrackMetadataException(graphql_response) from err raise SetTrackMetadataException( f'GraphQL error: {str(err)}' ) from err except Exception as exp: logger.error(f'Error updating track metadata.\n{exp}') raise SetTrackMetadataException( f'Error updating track metadata.\n{exp}' ) from exp if unmapped_roles: message = '' for role in unmapped_roles: message += '\n' message += ( f'on ISRC: "{track.isrc}" participant ' f'"{unmapped_roles[role]}" has unmapped role "{role}"' ) logger.warning('Some roles were not mapped:' + message) return StateMachineSchema().dump(sm_context) def get_unique_roles(track: Track) -> Dict: """Get a dict with every unique howrole along with the participant name. Args: track: Track model object. Returns: dict: Mapping of role string to participant name. """ unique_roles = {} if track.display_artists: for artist in track.display_artists: for role in (artist.roles or []): unique_roles[role] = artist.name return unique_roles def update_track_metadata( context: GrpsIngestionContext, label_participants: Optional[List[LabelParticipant]], upc: str, tuid: int, track: Track, unmapped_roles: Dict) -> None: """Update the metadata of a given track. Args: context: State machine context. label_participants: List of label participants from context. upc: Product UPC. tuid: Track tuid. track: Track model object. unmapped_roles: Roles that have not been mapped yet. """ logger.info( f'Updating track metadata for track: {TrackSchema().dump(track)}') payload = format_track_update_data( context, label_participants, upc, tuid, track, unmapped_roles) logger.info(f'Payload for GraphQL mutation: {payload}') graphql_gateway.execute( UPDATE_TRACK_METADATA, {'data': payload} ) def format_track_update_data( context: GrpsIngestionContext, label_participants: Optional[List[LabelParticipant]], upc: str, tuid: int, track: Track, unmapped_roles: Dict) -> Dict: """Format track update GraphQL payload. Args: context: State machine context. label_participants: List of label participants from context. upc: Product UPC. tuid: Track tuid. track: Track model object. unmapped_roles: Roles that have not been mapped yet. Returns: dict: Formatted payload for the saveTracks GraphQL mutation. """ if context.product_type == ProductType.VIDEO: track_type = 'video' else: track_type = 'music' participations = get_track_participations( label_participants, track, unmapped_roles) body = { 'upc': upc, 'isrc': track.isrc, 'trackName': track.track_name, 'explicit': track.explicit, 'version': track.version, 'participations': participations, 'trackType': track_type, } # Remove keys with None values body = {key: value for key, value in body.items() if value is not None} return { 'update': { 'tracks': [tuid], 'body': body, } } def get_track_participations( label_participants: Optional[List[LabelParticipant]], track: Track, unmapped_roles: Dict) -> List[Dict]: """Get participations payload for saveTracks GraphQL mutation. Args: label_participants: List of label participants from context. track: Track model object. unmapped_roles: Roles that have not been mapped yet. Returns: list: Participations list. """ track_participations = [] if not track.display_artists: return track_participations for artist in track.display_artists: context_participant = retrieve_label_participant( label_participants, artist) for role in (artist.roles or []): add_artist( context_participant, role, track_participations, unmapped_roles, ) return track_participations def add_artist( artist: Optional[LabelParticipant], role: str, participations: List[Dict], unmapped_roles: Dict) -> None: """Add artist to participations list for saveTracks GraphQL mutation. Args: artist: Matched label participant. role: DDEX role string. participations: List to append to. unmapped_roles: Roles that have not been mapped yet. """ if role not in ARTIST_ROLE_MAP: return if not artist: return unmapped_roles.pop(role, None) new_artist = { 'labelParticipantUuid': artist.label_participant_uuid, 'role': ARTIST_ROLE_MAP[role], } if new_artist not in participations: participations.append(new_artist) def retrieve_label_participant( label_participants: Optional[List[LabelParticipant]], artist: Participant) -> Optional[LabelParticipant]: """Retrieve a label participant matching the given artist by name. Args: label_participants: List of label participants from context. artist: Artist participant to look up. Returns: LabelParticipant or None. """ if label_participants and artist: for label_participant in label_participants: if label_participant.name == artist.name: return label_participant return None