import os from abc import ABC, abstractmethod from lxml import etree from lxml.etree import LxmlError from delivery_metadata.constants import ( ROLE_PRIORITIES, VARIOUS_ARTISTS, ParticipantRole, ) from delivery_metadata.exceptions import PrimaryArtistsMissing, SchemaValidationError from delivery_metadata.models.delivery_rights import ( DeliveryRights, ) from delivery_metadata.models.schemas import DeliveryMetadata, Participant class DeliveryMetadataFormatter(ABC): delivery_metadata: DeliveryMetadata delivery_rights: DeliveryRights various_artists_roles: set[ParticipantRole] def __init__(self, metadata: DeliveryMetadata, rights: DeliveryRights): self.delivery_metadata = metadata self.delivery_rights = rights self.various_artists_roles = {ParticipantRole.PERFORMER} @property @abstractmethod def media_type(self) -> str: ... @abstractmethod def format(self) -> str: ... def _schema_validate(self, xml_doc: etree._Element, schema_location: str) -> str: schema_class_mapping = { ".xsd": etree.XMLSchema, ".rng": etree.RelaxNG, } _, file_extension = os.path.splitext(schema_location) with open(schema_location, "rb") as schema_file: schema_doc = etree.parse(schema_file) schema = schema_class_mapping[file_extension](schema_doc) xml_content = etree.tostring( xml_doc, pretty_print=True, xml_declaration=True, encoding="UTF-8", ).decode("UTF-8") try: schema.assertValid(xml_doc) except LxmlError as e: raise SchemaValidationError( "Schema validation error - " + str(e), xml_content=xml_content, ) from e return xml_content def _get_main_artist_roles(self) -> set[ParticipantRole]: return ( { ParticipantRole.PERFORMER, ParticipantRole.FEATURING, } if not self.delivery_metadata.product.is_classical else { ParticipantRole.PERFORMER, ParticipantRole.COMPOSER, } ) def _get_contributor_roles(self) -> set[ParticipantRole]: return set(ParticipantRole).difference(self._get_main_artist_roles()) def _get_display_artists( self, participants: list[Participant] ) -> list[Participant]: return [ participant for participant in participants if participant.roles.intersection(self._get_main_artist_roles()) ] def _get_contributors(self, participants: list[Participant]) -> list[Participant]: return [ participant for participant in participants if ( participant.roles and participant.roles.issubset(self._get_contributor_roles()) ) or participant.instruments ] def _get_release_display_artist_candidates(self) -> list[Participant]: return self._get_display_artists(self.delivery_metadata.product.participants) def _get_release_display_artists(self) -> list[Participant]: """Get release level display artists.""" display_artists = self._get_primary_artists( self._get_release_display_artist_candidates() ) if ( len(display_artists) >= self.delivery_metadata.delivery.store.various_artists_limit ): return [ Participant( fullname=VARIOUS_ARTISTS, roles={ParticipantRole.PERFORMER}, instruments=set(), label_participant_id=None, apple_music_id=None, spotify_id=None, global_participant_id=None, fullname_localizations=display_artists.fullname_localizations if hasattr(display_artists, "fullname_localizations") else None, ), ] return display_artists def _get_release_non_primary_display_artists(self) -> list[Participant]: return [ candidate for candidate in self._get_release_display_artist_candidates() if self.various_artists_roles.isdisjoint(candidate.roles) ] def _format_display_artist_name(self, display_artists: list[Participant]) -> str: primary_artists = self._get_primary_artists(display_artists) if not primary_artists: raise PrimaryArtistsMissing("Primary artist missing.") display_artist_names = [artist.fullname for artist in primary_artists] last_artist = display_artist_names.pop() return ( " & ".join([", ".join(display_artist_names), last_artist]) if display_artist_names else last_artist ) def _get_primary_artists( self, participants: list[Participant] ) -> list[Participant]: unsorted_participants = [ participant for participant in participants if self.various_artists_roles.intersection(participant.roles) ] sorted_participants = unsorted_participants del unsorted_participants sorted_participants.sort(key=self._get_artist_role_priority) return sorted_participants def _has_various_artists(self) -> bool: release_display_artists = self._get_release_display_artists() return ( len(release_display_artists) == 1 and release_display_artists.pop().fullname == VARIOUS_ARTISTS ) def _get_artist_role_priority(self, artist: Participant) -> int: """Sort participants by role.""" for role in ROLE_PRIORITIES: if role in artist.roles: return ROLE_PRIORITIES[role] return len(ROLE_PRIORITIES) + 1 def _get_artist_role_priority_for_roles(self, artist_role: ParticipantRole) -> int: """Sort participant roles.""" return ( ROLE_PRIORITIES[artist_role] if artist_role in ROLE_PRIORITIES else len(ROLE_PRIORITIES) + 1 )