"""Assembles Series/Season/Episode API responses from repository data.""" from __future__ import annotations import uuid from sqlalchemy.ext.asyncio import AsyncSession from app.core.ids import aurora_id, parse_aurora_id from app.models.enums import ArtworkType, CompanyType from app.repositories import polymorphic, tv_repository from app.schemas.common import CompanyBriefOut, GenreOut, KeywordOut from app.schemas.tv import ( EpisodeBriefOut, EpisodeDetailOut, SeasonBriefOut, SeasonDetailOut, SeriesDetailOut, SeriesSeasonsOut, SeriesSummaryOut, ) from app.services.common import primary_artwork_path, split_credits, to_images_out, to_video_out class SeriesNotFoundError(Exception): pass class SeasonNotFoundError(Exception): pass class EpisodeNotFoundError(Exception): pass def _resolve_id(aid: str, expected_type: str, error_cls: type[Exception]) -> uuid.UUID: try: return uuid.UUID(parse_aurora_id(aid, expected_type=expected_type)) except ValueError as exc: raise error_cls(str(exc)) from exc async def _season_brief(session: AsyncSession, season) -> SeasonBriefOut: posters = await polymorphic.get_artwork(session, "season", season.id, ArtworkType.POSTER) return SeasonBriefOut( id=aurora_id("season", str(season.id)), season_number=season.season_number, title=season.title, episode_count=season.episode_count, air_date=season.air_date, poster_path=primary_artwork_path(posters, ArtworkType.POSTER), ) async def _company_brief(session: AsyncSession, company) -> CompanyBriefOut: logos = await polymorphic.get_artwork(session, "company", company.id, ArtworkType.LOGO) return CompanyBriefOut( id=aurora_id("company", str(company.id)), name=company.name, company_type=company.company_type.value, logo_path=primary_artwork_path(logos, ArtworkType.LOGO), ) async def get_series_detail(session: AsyncSession, series_aurora_id: str) -> SeriesDetailOut: series_id = _resolve_id(series_aurora_id, "series", SeriesNotFoundError) series = await tv_repository.get_series_by_id(session, series_id) if series is None: raise SeriesNotFoundError(series_aurora_id) genres = await tv_repository.get_series_genres(session, series_id) keywords = await tv_repository.get_series_keywords(session, series_id) networks = await tv_repository.get_series_companies(session, series_id, CompanyType.NETWORK) companies = await tv_repository.get_series_companies( session, series_id, CompanyType.PRODUCTION_COMPANY ) seasons = await tv_repository.get_seasons_for_series(session, series_id) credits_ = await polymorphic.get_credits(session, "series", series_id) artwork = await polymorphic.get_artwork(session, "series", series_id) videos = await polymorphic.get_videos(session, "series", series_id) external_ids = await polymorphic.get_external_ids(session, "series", series_id) cast, crew = split_credits([c for c in credits_ if c.department != "Creator"]) _, creators = split_credits([c for c in credits_ if c.department == "Creator"]) season_briefs = [await _season_brief(session, s) for s in seasons] network_briefs = [await _company_brief(session, c) for c, _role in networks] company_briefs = [await _company_brief(session, c) for c, _role in companies] return SeriesDetailOut( id=aurora_id("series", str(series.id)), title=series.title, original_title=series.original_title, overview=series.overview, tagline=series.tagline, status=series.status.value, original_language=series.original_language_code, first_air_date=series.first_air_date, last_air_date=series.last_air_date, number_of_seasons=series.number_of_seasons, number_of_episodes=series.number_of_episodes, popularity=series.popularity, vote_average=series.vote_average, vote_count=series.vote_count, genres=[GenreOut(id=aurora_id("genre", str(g.id)), name=g.name) for g in genres], keywords=[KeywordOut(id=aurora_id("keyword", str(k.id)), name=k.name) for k in keywords], creators=creators, cast=cast, crew=crew, networks=network_briefs, companies=company_briefs, seasons=season_briefs, images=to_images_out(artwork), videos=[to_video_out(v) for v in videos], external_ids=external_ids, created_at=series.created_at, updated_at=series.updated_at, ) async def get_series_seasons(session: AsyncSession, series_aurora_id: str) -> SeriesSeasonsOut: series_id = _resolve_id(series_aurora_id, "series", SeriesNotFoundError) series = await tv_repository.get_series_by_id(session, series_id) if series is None: raise SeriesNotFoundError(series_aurora_id) seasons = await tv_repository.get_seasons_for_series(session, series_id) briefs = [await _season_brief(session, s) for s in seasons] return SeriesSeasonsOut(id=aurora_id("series", str(series.id)), seasons=briefs) async def get_season_detail(session: AsyncSession, season_aurora_id: str) -> SeasonDetailOut: season_id = _resolve_id(season_aurora_id, "season", SeasonNotFoundError) season = await tv_repository.get_season_by_id(session, season_id) if season is None: raise SeasonNotFoundError(season_aurora_id) episodes = await tv_repository.get_episodes_for_season(session, season_id) artwork = await polymorphic.get_artwork(session, "season", season_id) external_ids = await polymorphic.get_external_ids(session, "season", season_id) episode_briefs: list[EpisodeBriefOut] = [] for ep in episodes: stills = await polymorphic.get_artwork(session, "episode", ep.id, ArtworkType.STILL) episode_briefs.append( EpisodeBriefOut( id=aurora_id("episode", str(ep.id)), episode_number=ep.episode_number, title=ep.title, air_date=ep.air_date, still_path=primary_artwork_path(stills, ArtworkType.STILL), ) ) return SeasonDetailOut( id=aurora_id("season", str(season.id)), series_id=aurora_id("series", str(season.series_id)), season_number=season.season_number, title=season.title, overview=season.overview, air_date=season.air_date, episode_count=season.episode_count, episodes=episode_briefs, images=to_images_out(artwork), external_ids=external_ids, created_at=season.created_at, updated_at=season.updated_at, ) async def get_episode_detail(session: AsyncSession, episode_aurora_id: str) -> EpisodeDetailOut: episode_id = _resolve_id(episode_aurora_id, "episode", EpisodeNotFoundError) episode = await tv_repository.get_episode_by_id(session, episode_id) if episode is None: raise EpisodeNotFoundError(episode_aurora_id) credits_ = await polymorphic.get_credits(session, "episode", episode_id) artwork = await polymorphic.get_artwork(session, "episode", episode_id) external_ids = await polymorphic.get_external_ids(session, "episode", episode_id) main_cast, crew = split_credits([c for c in credits_ if not c.is_guest]) guest_cast, _ = split_credits([c for c in credits_ if c.is_guest]) return EpisodeDetailOut( id=aurora_id("episode", str(episode.id)), series_id=aurora_id("series", str(episode.series_id)), season_id=aurora_id("season", str(episode.season_id)), season_number=episode.season_number, episode_number=episode.episode_number, title=episode.title, overview=episode.overview, air_date=episode.air_date, runtime=episode.runtime_minutes, cast=main_cast, guest_stars=guest_cast, crew=crew, images=to_images_out(artwork), external_ids=external_ids, created_at=episode.created_at, updated_at=episode.updated_at, ) async def build_series_summary( session: AsyncSession, series_id: uuid.UUID ) -> SeriesSummaryOut | None: series = await tv_repository.get_series_by_id(session, series_id) if series is None: return None posters = await polymorphic.get_artwork(session, "series", series_id, ArtworkType.POSTER) return SeriesSummaryOut( id=aurora_id("series", str(series.id)), title=series.title, original_title=series.original_title, overview=series.overview, first_air_date=series.first_air_date, poster_path=primary_artwork_path(posters, ArtworkType.POSTER), popularity=series.popularity, vote_average=series.vote_average, ) async def build_series_summaries( session: AsyncSession, series_ids: list[uuid.UUID] ) -> list[SeriesSummaryOut]: out: list[SeriesSummaryOut] = [] for series_id in series_ids: summary = await build_series_summary(session, series_id) if summary is not None: out.append(summary) return out