"""Distribution product model.""" import asyncio import json import re from collections.abc import Coroutine from typing import Any from delivery_metadata.clients import ( art_relations as art_relations_client, direct_delivery as direct_delivery_client, graphql as graphql_client, ows_assets, ows_carveouts, ows_pricing, ows_territories, ows_timed_release, ows_track, ows_video, ) from delivery_metadata.clients.art_relations.distribution_features import ( DistributionFeatures, ) from delivery_metadata.clients.art_relations.product_localized_metadata import ( ArtRelationsProductLocalizedMetadata, ) from delivery_metadata.clients.art_relations.schemas import ArtRelationsArtist from delivery_metadata.clients.art_relations.track import ArtRelationsTrack from delivery_metadata.clients.art_relations.track_localized_metadata import ( ArtRelationsTrackLocalizedMetadata, ) from delivery_metadata.clients.art_relations.track_publisher import ( ArtRelationsTrackPublisher, ) from delivery_metadata.clients.art_relations.track_writer import ArtRelationsTrackWriter from delivery_metadata.clients.graphql.product import ( GraphQlProduct, LabelSoundRecordingParticipation, ) from delivery_metadata.clients.ows_timed_release.timed_release import ( ProductTimedReleaseByStore, TimedRelease, ) from delivery_metadata.clients.ows_track.instant_grats import InstantGrats from delivery_metadata.clients.ows_track.performer import ( TrackPerformer, ) from delivery_metadata.config import USE_MERGED_TIMED_RELEASES_LOGIC from delivery_metadata.constants import ( DeliveryType, DistributionFeatureId, DistributionTypeId, DownloadStreamRights, EncodingProfileType, InstrumentRole, ParticipantRole, TerritoryStandard, TrackOfferType, TrackTypes, ddex as ddex_constants, instrument_to_participant_role_mapping, store_setting_overrides, ) from delivery_metadata.exceptions import ( BundleNotSupported, NoLabelParticipantForParticipant, PricingDataNotFound, ProductIneligible, ) from delivery_metadata.models.schemas import ( AudioAsset, CopyrightLine, Delivery, DeliveryMetadata, LabelParticipantData, LabelParticipantMap, LocalizedMetadata, Participant, ParticipantFullnameLocalization, Product, ProductAudio, ProductBundle, ProductVideo, ReleasePricing, Store, TerritoryCodeA2, Track, TrackReleasePricing, VideoAsset, ) from delivery_metadata.utils.datetime import ( get_datetime_from_string, ) from delivery_metadata.utils.standardized_name import get_standardized_name def get_track_writers( art_relations_track_writers: dict[int, list[ArtRelationsTrackWriter]], track_id: int, ) -> list[str]: return [writer.fullname for writer in art_relations_track_writers.get(track_id, [])] def get_track_publishers( art_relations_track_publishers: dict[int, list[ArtRelationsTrackPublisher]], track_id: int, ) -> list[str]: return [ publisher.fullname for publisher in art_relations_track_publishers.get(track_id, []) ] def get_instant_grats_by_track_id( track_id: int, all_instant_grats_data: set[InstantGrats] ) -> InstantGrats | None: return next( (item for item in all_instant_grats_data if item.tuid == track_id), None, ) def get_product_localization_data( product_localized_metadata: list[ArtRelationsProductLocalizedMetadata], ) -> list[LocalizedMetadata]: return [ LocalizedMetadata( language_code=localized_data.language_code, localized_name=localized_data.localized_product_name, localized_version=localized_data.localized_product_version, ) for localized_data in product_localized_metadata ] def get_track_localized_metadata_by_track_id( track_id: int, track_localized_metadata: list[ArtRelationsTrackLocalizedMetadata] ) -> list[LocalizedMetadata] | None: localized_data = [] if track_localized_metadata: for item in track_localized_metadata: if item.track_id == track_id: localized_data.append( LocalizedMetadata( language_code=item.language_code, localized_name=item.localized_track_name, localized_version=item.localized_track_version, ) ) return localized_data def combine_track_artists_and_contributors( art_relations_track_artists: list[ArtRelationsArtist], art_relations_track_publishers: list[str], art_relations_track_writers: list[str], ) -> list[ArtRelationsArtist]: for publisher in art_relations_track_publishers: art_relations_track_artists.append( ArtRelationsArtist( fullname=publisher, role=ParticipantRole.PUBLISHER, ) ) for track_writer in art_relations_track_writers: art_relations_track_artists.append( ArtRelationsArtist( fullname=track_writer, role=ParticipantRole.TRACK_WRITER, ) ) return art_relations_track_artists def _get_track_distribution_rights( track: ArtRelationsTrack, store: Store, ) -> set[DownloadStreamRights]: # TODO add physical product context check if ( store.ddex_commercial_model_to_use_type_map and not store.is_download_supported and not store.is_streaming_supported ): return { DownloadStreamRights.DOWNLOAD, DownloadStreamRights.STREAM, } track_distribution_rights = set() match track.offer_type: case TrackOfferType.ALL | TrackOfferType.TRACK_DOWNLOAD_STREAM: track_distribution_rights = { DownloadStreamRights.DOWNLOAD, DownloadStreamRights.STREAM, } case TrackOfferType.TRACK_DOWNLOAD_ONLY | TrackOfferType.ALBUM_TRACK_DOWNLOAD: if store.is_download_supported: track_distribution_rights = {DownloadStreamRights.DOWNLOAD} case TrackOfferType.STREAM_ONLY: if store.is_streaming_supported: track_distribution_rights = {DownloadStreamRights.STREAM} case TrackOfferType.ALBUM_DOWNLOAD_STREAM: if store.is_streaming_supported: track_distribution_rights.add(DownloadStreamRights.STREAM) case _: pass return track_distribution_rights def _get_release_distribution_rights( tracks: list[Track], store: Store, delivery_type: DeliveryType, ) -> set[DownloadStreamRights]: # add physical product context check product_distribution_rights: set[DownloadStreamRights] = set() if ( store.ddex_commercial_model_to_use_type_map and not store.is_download_supported and not store.is_streaming_supported ): return { DownloadStreamRights.DOWNLOAD, DownloadStreamRights.STREAM, } for track in tracks: if DownloadStreamRights.DOWNLOAD in track.distribution_rights or ( track.offer_type == TrackOfferType.ALBUM_DOWNLOAD_ONLY and store.is_download_supported ): # track distribution right would be empty if the track offer type # is ALBUM_DOWNLOAD_ONLY, but album distribution right should # be set with DOWNLOAD rights. product_distribution_rights.add(DownloadStreamRights.DOWNLOAD) if DownloadStreamRights.STREAM in track.distribution_rights: product_distribution_rights.add(DownloadStreamRights.STREAM) if ( not product_distribution_rights and delivery_type != DeliveryType.METADATA_UPDATE ): raise ProductIneligible( "Track offer types prevent release availability on this store.", ) return product_distribution_rights def _get_copyright_line(copyright_line: str) -> CopyrightLine: year: str | None = None copyright_text = copyright_line.strip() text_pattern = re.compile(r"^\d{4}(?:\s|,)") if text_pattern.match(copyright_text): year = copyright_text[:4] return CopyrightLine( year=year, text=copyright_text, ) def _hydrate_track( track: ArtRelationsTrack, store: Store, participants: list[Participant], track_pricing_dict: dict[int, set[ReleasePricing]], ows_track_instant_grats_response: dict[int, set[InstantGrats]], art_relations_track_localized_metadata: list[ArtRelationsTrackLocalizedMetadata], track_assets: list[AudioAsset | VideoAsset], ) -> Track: store_instant_grats = ows_track_instant_grats_response.get(store.store_id) return Track( track_id=track.track_id, volume_number=track.volume_number, track_number=track.track_number, preview_start_time_seconds=track.preview_start_time_seconds, duration_seconds=track.duration_seconds, track_type=track.track_type, lyrics_explicitness=track.lyrics_explicitness, upc=track.upc, isrc=track.isrc, p_line=_get_copyright_line(track.p_line), track_name=track.track_name, track_version=track.track_version, language_of_performance_ietf_rfc_5646_code=( track.language_of_performance_ietf_rfc_5646_code ), participants=participants, offer_type=track.offer_type, distribution_rights=_get_track_distribution_rights(track, store), track_territory_pricing=( get_track_pricing_by_track_id(track.track_id, track_pricing_dict) if store.is_pricing_supported else set() ), instant_grats=get_instant_grats_by_track_id(track.track_id, store_instant_grats) if store_instant_grats else None, localized_metadata=get_track_localized_metadata_by_track_id( track.track_id, art_relations_track_localized_metadata ), assets=track_assets, spatial_isrc=track.spatial_isrc, ) async def _fetch_timed_release_or_none( product_id: int, store_id: int ) -> ProductTimedReleaseByStore | TimedRelease | None: """ Will decide to use merged or legacy timed releases logic. Returns timed-release payload or None if upstream returns 404. """ if not USE_MERGED_TIMED_RELEASES_LOGIC: return await ows_timed_release.get_timed_release(product_id) return await ows_timed_release.get_product_timed_release_by_store_id( product_id, store_id ) async def get_delivery_metadata( *, upc: int, store_id: int, delivery_type: DeliveryType, ) -> DeliveryMetadata: """Get delivery metadata.""" # Phase 1: all calls requiring only upc or store_id data_fetching_coroutines: list[Coroutine[Any, Any, Any]] = [ art_relations_client.get_product(upc), art_relations_client.get_tracks(upc), art_relations_client.get_track_writers(upc), art_relations_client.get_track_publishers(upc), art_relations_client.get_store_customer(store_id), art_relations_client.get_distribution_features(store_id), art_relations_client.get_territory_dates(upc), ows_carveouts.get_product_territory_rights(upc, store_id), art_relations_client.get_genre_mapping(store_id), ] ( art_relations_product, art_relations_tracks, art_relations_track_writers, art_relations_track_publishers, art_relations_store_customer, distribution_features, art_relations_territory_dates, ows_carveouts_response, genre_mappings, ) = await asyncio.gather(*data_fetching_coroutines) has_music_track = any( track.track_type == TrackTypes.MUSIC for track in art_relations_tracks ) has_video_track = any( track.track_type == TrackTypes.VIDEO for track in art_relations_tracks ) is_bundle = has_music_track and has_video_track store_overrides = store_setting_overrides.get(store_id, {}) is_bundle_supported = store_overrides.get("is_bundle_supported", False) if is_bundle and not is_bundle_supported: raise BundleNotSupported(f"Bundle not supported by store {store_id}") is_instant_grat_supported = art_relations_store_customer.instant_grat == "Y" is_localization_supported = ( art_relations_store_customer.supports_localization == "Y" ) is_pricing_supported = store_overrides.get("is_pricing_supported", False) territory_standard = store_overrides.get( "territory_standard", TerritoryStandard.ORCHARD_2016 ) # Phase 2: all calls requiring product_id or conditional on phase 1 derived data phase2_coros: dict[str, Coroutine[Any, Any, Any]] = { "timed_release": _fetch_timed_release_or_none( art_relations_product.product_id, store_id ), "store_artists": art_relations_client.get_product_provided_store_artists( art_relations_product.product_id ), "dms_encoding_profiles": direct_delivery_client.get_dms_encoding_profiles( store_id, art_relations_product.product_type_id ), "track_performers": ows_track.get_track_performers_with_role_names( art_relations_product.product_id ), "allowed_territories": get_converted_territories( ows_carveouts_response, territory_standard ), "graphql_product": graphql_client.get_product(art_relations_product.product_id), } if is_instant_grat_supported: phase2_coros["instant_grats"] = ows_track.get_instant_grats( art_relations_product.product_id ) if is_localization_supported: phase2_coros["product_localized_metadata"] = ( art_relations_client.get_product_localized_metadata( art_relations_product.product_id ) ) phase2_coros["track_localized_metadata"] = ( art_relations_client.get_track_localized_metadata( art_relations_product.product_id ) ) if has_music_track: phase2_coros["release_artists"] = art_relations_client.get_release_artists( art_relations_product.product_id ) phase2_coros["track_artists"] = art_relations_client.get_track_artists(upc) phase2_coros["product_assets"] = ows_assets.get_product_assets( art_relations_product.product_id ) if has_music_track and is_localization_supported: phase2_coros["product_artist_localized_metadata"] = ( art_relations_client.get_product_artist_localized_metadata( art_relations_product.product_id ) ) phase2_coros["track_artist_localized_metadata"] = ( art_relations_client.get_track_artist_localized_metadata( art_relations_product.product_id ) ) if has_music_track and is_pricing_supported: phase2_coros["audio_track_pricing"] = ows_pricing.get_track_pricing( art_relations_product.product_id, store_id ) if has_video_track: phase2_coros["product_video"] = art_relations_client.get_product_video(upc) phase2_coros["video_assets"] = ows_video.get_product_video_assets( art_relations_product.product_id ) if has_video_track and is_pricing_supported: phase2_coros["video_track_pricing"] = ows_pricing.get_track_pricing( art_relations_product.product_id, store_id, True ) if is_pricing_supported: phase2_coros["release_pricing"] = ows_pricing.get_product_pricing( art_relations_product.product_id, store_id ) p2_keys = list(phase2_coros.keys()) p2_values = await asyncio.gather(*phase2_coros.values()) p2 = dict(zip(p2_keys, p2_values, strict=True)) store_data: dict[str, Any] = { "store_id": store_id, "sender_ddex_party_id": ddex_constants.ORCHARD_PARTY_ID, "sender_ddex_party_name": ddex_constants.ORCHARD_PARTY_NAME, "recipient_ddex_party_id": art_relations_store_customer.ddex_party_id, "recipient_ddex_party_name": art_relations_store_customer.customer_name, "audio_encoding_profiles": ( p2["dms_encoding_profiles"].get(EncodingProfileType.AUDIO) or None ), "image_encoding_profiles": ( p2["dms_encoding_profiles"].get(EncodingProfileType.IMAGE) or None ), "video_image_encoding_profiles": ( p2["dms_encoding_profiles"].get(EncodingProfileType.VIDEO_IMAGE) or None ), "metadata_encoding_profiles": p2["dms_encoding_profiles"][ EncodingProfileType.METADATA ], "video_encoding_profiles": ( p2["dms_encoding_profiles"].get(EncodingProfileType.VIDEO) or None ), "product_provided_store_artists": p2["store_artists"].spotify, "distribution_feature_ids": get_distribution_feature_ids(distribution_features), "is_localization_supported": is_localization_supported, "is_instant_gratification_supported": is_instant_grat_supported, "is_user_defined_contributors_supported": ( art_relations_store_customer.is_user_defined_contributors_supported == "Y" ), } store_data.update(store_overrides) store = Store(**store_data) delivery = Delivery( delivery_type=delivery_type, allowed_territories=p2["allowed_territories"], store=store, ) assets_response: dict[str, Any] = {"product_assets": [], "track_assets": {}} for key in ("product_assets", "video_assets"): asset_data = p2.get(key) if asset_data: assets_response["product_assets"].extend(asset_data["product_assets"]) for tuid, track_asset_list in asset_data["track_assets"].items(): assets_response["track_assets"].setdefault(tuid, []).extend( track_asset_list ) ows_track_instant_grats_response: dict[int, set[InstantGrats]] = p2.get( "instant_grats", {} ) art_relations_product_localized_metadata: list[ ArtRelationsProductLocalizedMetadata ] = p2.get("product_localized_metadata", []) art_relations_track_localized_metadata: list[ArtRelationsTrackLocalizedMetadata] = ( p2.get("track_localized_metadata", []) ) graphql_product = p2.get("graphql_product") label_participant_map = ( get_label_participant_map(graphql_product) if graphql_product else LabelParticipantMap(map={}) ) release_territory_pricing: set[ReleasePricing] = p2.get("release_pricing", set()) audio_tracks = video_tracks = [] art_relations_product_video = None participants = [] if has_video_track: art_relations_product_video = p2["product_video"] release_artists = [ ArtRelationsArtist( fullname=art_relations_product_video.primary_artist_name, role=ParticipantRole.PERFORMER, ), ] if art_relations_product_video.contributors: contributors = json.loads(art_relations_product_video.contributors) release_artists.extend( [ ArtRelationsArtist( fullname=contributor.get("name"), role=contributor.get("role"), ) for contributor in contributors if contributor.get("role") in ParticipantRole ] ) participants = get_product_participants( release_artists, frozenset(), label_participant_map, store.is_feature_to_primary_artist_supported, ) video_track_pricing_by_id = group_track_pricing_by_track_id( p2.get("video_track_pricing", set()) ) video_tracks = sorted( [ _hydrate_track( track, store, participants, video_track_pricing_by_id, {}, [], assets_response["track_assets"].get(track.track_id, []), ) for track in art_relations_tracks if track.track_type == TrackTypes.VIDEO ], key=lambda t: (t.volume_number, t.track_number), ) if has_music_track: art_relations_release_artists = p2["release_artists"] art_relations_track_artists = p2["track_artists"] art_relations_product_localized_artists: frozenset[ ParticipantFullnameLocalization ] = p2.get("product_artist_localized_metadata", frozenset()) art_relations_track_localized_artists: frozenset[ ParticipantFullnameLocalization ] = p2.get("track_artist_localized_metadata", frozenset()) audio_track_pricing_by_id = group_track_pricing_by_track_id( p2.get("audio_track_pricing", set()) ) participants = get_product_participants( art_relations_release_artists, art_relations_product_localized_artists, label_participant_map, store.is_feature_to_primary_artist_supported, ) graphql_track_map = ( { int(t.tuid): t.labelSoundRecording.participations for t in graphql_product.tracks } if graphql_product else {} ) audio_tracks = sorted( [ _hydrate_track( track, store, get_track_participants( combine_track_artists_and_contributors( art_relations_track_artists[track.track_id], get_track_publishers( art_relations_track_publishers, track.track_id ), get_track_writers( art_relations_track_writers, track.track_id ), ), p2["track_performers"].get(track.track_id, []), graphql_track_map.get(track.track_id, []), art_relations_track_localized_artists, label_participant_map, store.is_feature_to_primary_artist_supported, ), audio_track_pricing_by_id, ows_track_instant_grats_response, art_relations_track_localized_metadata, assets_response["track_assets"].get(track.track_id, []), ) for track in art_relations_tracks if track.track_type == TrackTypes.MUSIC ], key=lambda t: (t.volume_number, t.track_number), ) if is_bundle and store.is_bundle_supported: tracks_list = audio_tracks + video_tracks else: tracks_list = video_tracks if has_video_track else audio_tracks product = Product( upc=art_relations_product.upc, display_upc=art_relations_product.display_upc, tracks=tracks_list, product_id=art_relations_product.product_id, original_release_date=art_relations_product.original_release_date, release_date=art_relations_product.release_date, sale_start_date=art_relations_product.sale_start_date, preorder_date=get_preorder_date_or_none( art_relations_product.preorder_date, get_timed_release_by_dms(store_id, p2["timed_release"]), ), product_name=art_relations_product.product_name, product_type_id=art_relations_product.product_type_id, delivered_version=art_relations_product.delivered_version, release_grid=art_relations_product.release_grid, metadata_language_ietf_rfc_5646_code=( art_relations_product.metadata_language_ietf_rfc_5646_code ), distribution_format_id=art_relations_product.distribution_format_id, product_code=art_relations_product.product_code, release_sony_product_no=art_relations_product.release_sony_product_no, vendor_catalog_number=art_relations_product.vendor_catalog_number, genre_id=art_relations_product.genre_id, genre_name=art_relations_product.genre_name, subgenre_id=art_relations_product.subgenre_id, subgenre_name=art_relations_product.subgenre_name, participants=participants, genre_mappings=genre_mappings, timed_release_datetime=get_timed_release_by_dms(store_id, p2["timed_release"]), staggered_release_date=get_staggered_release_by_dms( store_id, p2["timed_release"] ), imprint=art_relations_product.imprint, p_line=( _get_copyright_line(art_relations_product.p_line) if art_relations_product.p_line else None ), c_line=_get_copyright_line(art_relations_product.c_line), distribution_context=art_relations_product.distribution_context, distribution_rights=_get_release_distribution_rights( tracks_list, store, delivery.delivery_type, ), territory_dates=art_relations_territory_dates, release_territory_pricing=release_territory_pricing, content_id=art_relations_product.content_id, project_id=art_relations_product.project_id, vendor_id=art_relations_product.vendor_id, vendor_owner=art_relations_product.vendor_owner, localized_metadata=get_product_localization_data( art_relations_product_localized_metadata ), assets=assets_response["product_assets"], ) final_product: Product = ProductAudio(**product.__dict__) if has_video_track: product_video_data = art_relations_product_video.__dict__ product_video_data.pop("contributors") product_video_data.pop("primary_artist_name") final_product = ProductVideo(**product.__dict__, **product_video_data) if is_bundle: final_product = ProductBundle(**final_product.__dict__) return DeliveryMetadata(product=final_product, delivery=delivery) async def get_converted_territories( territories: list[TerritoryCodeA2], new_standard: TerritoryStandard ) -> list[TerritoryCodeA2]: if territories and new_standard != TerritoryStandard.ORCHARD_2016: territory_response = await ows_territories.convert_territories( TerritoryStandard.ORCHARD_2016, new_standard, territories, ) return [ TerritoryCodeA2(country_code=x.territory_code_a2) for x in territory_response.items ] return territories def get_preorder_date_or_none( preorder_date: str | None, timed_release_date: str | None ) -> str | None: if not preorder_date: return None if timed_release_date: preorder_date_time = get_datetime_from_string(preorder_date) timed_release_date_time = get_datetime_from_string(timed_release_date) if (timed_release_date_time - preorder_date_time).days <= 2: return None return preorder_date def get_product_participants( art_relations_artists: list[ArtRelationsArtist], participant_fullname_localizations: frozenset[ParticipantFullnameLocalization], lp_map: LabelParticipantMap, is_feature_to_primary_supported: bool, ) -> list[Participant]: return _get_participants( merge_participants(art_relations_artists), participant_fullname_localizations, get_feature_to_primary_names(art_relations_artists) if is_feature_to_primary_supported else set(), lp_map, ) def get_track_participants( art_relations_artists: list[ArtRelationsArtist], track_performers: list[TrackPerformer], lsr_participations: list[LabelSoundRecordingParticipation], participant_fullname_localizations: frozenset[ParticipantFullnameLocalization], lp_map: LabelParticipantMap, is_feature_to_primary_supported: bool, ) -> list[Participant]: return _get_participants( merge_participants( art_relations_artists, track_performers, lsr_participations, ), participant_fullname_localizations, get_feature_to_primary_names(art_relations_artists) if is_feature_to_primary_supported else set(), lp_map, ) def _get_participants( participants: list[Participant], participant_fullname_localizations: frozenset[ParticipantFullnameLocalization], f2p_names: set[str], lp_map: LabelParticipantMap, ) -> list[Participant]: participants = handle_feature_to_primary_role(participants, f2p_names) participants = handle_participant_fullname_localization( participants, participant_fullname_localizations ) participants = handle_label_paricipant_enrichment(participants, lp_map) participants = promote_instruments_to_contributors(participants) check_label_participant_requirements(participants) return participants def promote_instruments_to_contributors( participants: list[Participant], ) -> list[Participant]: promoted_participants: list[Participant] = [] for participant in participants: participant_roles = participant.roles.copy() participant_instruments = participant.instruments.copy() for instrument in participant.instruments: participant_role = instrument_to_participant_role_mapping().get(instrument) if participant_role: participant_roles.add(participant_role) participant_instruments.remove(instrument) promoted_participants.append( Participant( fullname=participant.fullname, roles=participant_roles, instruments=participant_instruments, label_participant_id=participant.label_participant_id, apple_music_id=participant.apple_music_id, spotify_id=participant.spotify_id, spotify_artist_key=participant.spotify_artist_key, global_participant_id=participant.global_participant_id, fullname_localizations=participant.fullname_localizations, ) ) return promoted_participants def get_feature_to_primary_names(artists: list[ArtRelationsArtist]) -> set[str]: return { get_standardized_name(artist.fullname) for artist in artists if artist.is_feature_to_primary } def get_label_participant_map( graphql_product: GraphQlProduct, ) -> LabelParticipantMap: participants = [x.participant for x in graphql_product.participations] for track in graphql_product.tracks: participants.extend( [y.participant for y in track.labelSoundRecording.participations] ) participants.extend([z.participant for z in track.participations]) return LabelParticipantMap( map={ get_standardized_name(participant.name): LabelParticipantData( label_participant_id=participant.uuid, apple_music_id=participant.appleMusicId, spotify_id=participant.spotifyId, spotify_artist_key=participant.spotifyArtistKey, global_participant_id=( participant.globalParticipant.id if participant.globalParticipant else None ), ) for participant in participants } ) def merge_participants( artists: list[ArtRelationsArtist], track_performers: list[TrackPerformer] | None = None, lsr_participations: list[LabelSoundRecordingParticipation] | None = None, ) -> list[Participant]: """Group participants by name and combine them together.""" participants_grouped_by_name: dict[str, list[Participant]] = {} for artist in artists: standardized_fullname = get_standardized_name(artist.fullname) participants_grouped_by_name.setdefault(standardized_fullname, []).append( Participant( fullname=artist.fullname, roles={artist.role}, instruments=set() ) ) if lsr_participations: for participation in lsr_participations: standardized_fullname = get_standardized_name( participation.participant.name ) participants_grouped_by_name.setdefault(standardized_fullname, []).append( Participant( fullname=participation.participant.name, roles={ParticipantRole(participation.role.name.lower())}, instruments=set(), ) ) if track_performers: for track_performer in track_performers: standardized_track_performer_name = get_standardized_name( track_performer.birth_name ) participants_grouped_by_name.setdefault( standardized_track_performer_name, [] ).append( Participant( fullname=track_performer.birth_name, roles=set(), instruments={InstrumentRole(track_performer.role.lower())}, ) ) return [ Participant( fullname=grouped_list[0].fullname, roles={role for participant in grouped_list for role in participant.roles}, instruments={ instrument for participant in grouped_list for instrument in participant.instruments }, ) for _, grouped_list in participants_grouped_by_name.items() ] def check_label_participant_requirements(participants: list[Participant]) -> None: required_roles = {ParticipantRole.PERFORMER, ParticipantRole.FEATURING} error_names = set() for participant in participants: if participant.roles.intersection(required_roles): if not participant.label_participant_id: error_names.add(participant.fullname) if error_names: raise NoLabelParticipantForParticipant( f"No label participant found for {'|'.join(error_names)}", ) def handle_label_paricipant_enrichment( participants: list[Participant], lp_map: LabelParticipantMap ) -> list[Participant]: empty_label_participant_data = LabelParticipantData( label_participant_id=None, apple_music_id=None, spotify_id=None, spotify_artist_key=None, global_participant_id=None, ) return [ Participant( **{ **lp_map.map.get( get_standardized_name(x.fullname), empty_label_participant_data ).__dict__, **{ k: v for k, v in x.__dict__.items() if k not in [ "label_participant_id", "apple_music_id", "spotify_id", "spotify_artist_key", "global_participant_id", ] }, }, ) for x in participants ] def handle_participant_fullname_localization( participants: list[Participant], participant_fullname_localizations: frozenset[ParticipantFullnameLocalization], ) -> list[Participant]: return [ Participant( fullname_localizations=frozenset( participant_fullname_localization for participant_fullname_localization in participant_fullname_localizations if get_standardized_name(participant_fullname_localization.fullname) == get_standardized_name(x.fullname) ), **{k: v for k, v in x.__dict__.items() if k != "fullname_localizations"}, ) for x in participants ] def handle_feature_to_primary_role( participants: list[Participant], f2p_names: set[str], ) -> list[Participant]: return [ Participant( roles=(x.roles | {ParticipantRole.PERFORMER}) - {ParticipantRole.FEATURING}, **{k: v for k, v in x.__dict__.items() if k != "roles"}, ) if get_standardized_name(x.fullname) in f2p_names and ParticipantRole.FEATURING in x.roles else x for x in participants ] def get_timed_release_by_dms( store_id: int, timed_release: TimedRelease | ProductTimedReleaseByStore | None ) -> str | None: if timed_release is None: return None # Handle merged endpoint response (ProductTimedReleaseByStore) if isinstance(timed_release, ProductTimedReleaseByStore): return ( timed_release.timed.get("sales_date_time") if timed_release.timed else None ) # Handle legacy endpoint response (TimedRelease) if not timed_release.timed: return None timed_times = [ timed_timed_release.sale_date_time for timed_timed_release in timed_release.timed if timed_timed_release.store_id == store_id ] return timed_times.pop() if timed_times else None def get_staggered_release_by_dms( store_id: int, staggered_release: TimedRelease | ProductTimedReleaseByStore | None ) -> str | None: if staggered_release is None: return None # Handle merged endpoint response (ProductTimedReleaseByStore) if isinstance(staggered_release, ProductTimedReleaseByStore): return ( staggered_release.staggered.get("sales_date") if staggered_release.staggered else None ) # Handle legacy endpoint response (TimedRelease) if not staggered_release.staggered: return None staggered_times = [ staggered_timed_release.sale_date for staggered_timed_release in staggered_release.staggered if staggered_timed_release.store_id == store_id ] return staggered_times.pop() if staggered_times else None def get_distribution_feature_ids( distribution_features: list[DistributionFeatures], ) -> set[DistributionFeatureId]: distribution_feature_ids = { distribution_feature_id for distribution_feature in distribution_features if distribution_feature.distribution_type_id == DistributionTypeId.FULL_TRACK for distribution_feature_id in distribution_feature.distribution_feature_ids } return distribution_feature_ids def group_track_pricing_by_track_id( track_pricing: set[TrackReleasePricing], ) -> dict[int, set[ReleasePricing]]: track_pricing_by_track_id: dict[int, set[ReleasePricing]] = {} for pricing in track_pricing: for track_id in pricing.track_ids: track_pricing_by_track_id.setdefault(track_id, set()).add( ReleasePricing( price_code=pricing.price_code, country_code=pricing.country_code, custom_pricing=pricing.custom_pricing, start_date=pricing.start_date, end_date=pricing.end_date, ) ) return track_pricing_by_track_id def get_track_pricing_by_track_id( track_id: int, pricing_dict: dict[int, set[ReleasePricing]], ) -> set[ReleasePricing]: try: track_pricing = pricing_dict[track_id] except KeyError as e: raise PricingDataNotFound( f"No track pricing found for track {track_id}." ) from e return track_pricing