import json from pydantic import ValidationError, ValidationInfo, computed_field, field_validator from pydantic_core import InitErrorDetails, PydanticCustomError from sqlalchemy import text from delivery_metadata.api.app import app from delivery_metadata.constants import ( LyricsExplicitness, TrackOfferType, TrackTypes, ) from delivery_metadata.models import Model INSTRUMENTAL_ORCHARD_LANGUAGE_CODE = "n/a" NO_LINGUISTIC_CONTENT_ISO_639_3_CODE = "zxx" class ArtRelationsTrackDuration(Model): length_minute: int length_seconds: int @computed_field # type: ignore[prop-decorator] @property def duration_seconds(self) -> int: return (self.length_minute * 60) + self.length_seconds class ArtRelationsTrack(Model): track_id: int upc: int isrc: str volume_number: int track_number: int track_name: str track_version: str | None duration_seconds: int preview_start_time_seconds: int lyrics_explicitness: LyricsExplicitness p_line: str track_type: TrackTypes offer_type: TrackOfferType language_of_performance_ietf_rfc_5646_code: str | None spatial_isrc: str | None @field_validator("preview_start_time_seconds", mode="after") @classmethod def validate_preview(cls, value: int, info: ValidationInfo) -> int: if value >= info.data["duration_seconds"]: raise ValueError( "preview_start_time_seconds must be less than duration_seconds" ) return value async def get_tracks(upc: int) -> list[ArtRelationsTrack]: async with app.state.art_relations_connector.db_session() as session: result = await session.execute( text( """ SELECT t.id AS track_id, t.upc, t.isrc, t.cd AS volume_number, t.track_id AS track_number, t.track_name AS track_name, t.version AS track_version, t.length_minute, t.length_seconds, t.preview_start_time AS preview_start_time_milliseconds, t.explicit_lyrics AS lyrics_explicitness, t.p_line, t.track_type, t.offer_type, lower(t.meta_language) as language_of_performance_orchard_language_code, lower(language_of_performance.iso_639_1_code) as language_of_performance_iso_639_1_code, lower(language_of_performance.iso_code_639_3_code) as language_of_performance_iso_639_3_code, ts.isrc AS spatial_isrc FROM track t LEFT JOIN language language_of_performance -- We store language of performance in the track.meta_language column. -- Metadata language is stored in the releases.meta_language column. -- track.meta_language is labeled as "Lyrics Language" on the product builder track form -- but the help text says it should contain the language of performance. -- https://github.com/theorchard/frontend-distribution/blob/5dbf17b55f980b3b10f4015326f4b3c1edc34d88/src/components/track-builder/track-edit-form/i18n/index.js#L305-L316 ON t.meta_language = language_of_performance.language_code LEFT JOIN track_additional_isrc ts ON ts.track_id = t.id AND ts.type = 'atmos' AND ts.deleted_at IS NULL WHERE t.upc = :upc ORDER BY volume_number, track_number """ ), { "upc": upc, }, ) tracks: list[ArtRelationsTrack] = [] all_error_details: list[InitErrorDetails] = [] for track_result in result.mappings().all(): track_label = f"{track_result.upc}_{track_result.volume_number}_{track_result.track_number}" try: duration_seconds = ArtRelationsTrackDuration( length_minute=track_result.length_minute, length_seconds=track_result.length_seconds, ).duration_seconds except ValidationError as e: for error in json.loads(e.json()): all_error_details.append( InitErrorDetails( type=PydanticCustomError(error["type"], error["msg"]), loc=(track_label, *error["loc"]), input=error.get("input"), ) ) continue try: tracks.append( ArtRelationsTrack( track_id=track_result.track_id, upc=track_result.upc, isrc=track_result.isrc, volume_number=track_result.volume_number, track_number=track_result.track_number, track_name=track_result.track_name, track_version=track_result.track_version, duration_seconds=duration_seconds, preview_start_time_seconds=( (track_result.preview_start_time_milliseconds or 0) // 1000 ), lyrics_explicitness=track_result.lyrics_explicitness, p_line=track_result.p_line, track_type=track_result.track_type, offer_type=track_result.offer_type, language_of_performance_ietf_rfc_5646_code=( NO_LINGUISTIC_CONTENT_ISO_639_3_CODE if ( track_result.language_of_performance_orchard_language_code == INSTRUMENTAL_ORCHARD_LANGUAGE_CODE ) else ( track_result.language_of_performance_iso_639_1_code or track_result.language_of_performance_iso_639_3_code ) ), spatial_isrc=track_result.spatial_isrc, ) ) except ValidationError as e: for error in json.loads(e.json()): all_error_details.append( InitErrorDetails( type=PydanticCustomError(error["type"], error["msg"]), loc=(track_label, *error["loc"]), input=error.get("input"), ) ) if all_error_details: raise ValidationError.from_exception_data( "ArtRelationsTrack", all_error_details ) return tracks