"""Collection/franchise ingestion (section 15). Movie ingestion already creates a bare `Collection` stub (name only) the moment it sees a `belongs_to_collection` reference (see `movie_ingestion.py`). This module is the real sync: fetching TMDb's `/collection/{id}` payload for overview, poster/backdrop artwork, and the franchise's member movies. Membership (`CollectionMovie`) is only ever linked for movies Aurora has already ingested -- the same "never fabricate an entity from a bare reference" rule `ingest_movie` applies to recommendations. A newly-ingested collection can list zero movies if none of its parts have been ingested yet; re-running `ingest_collection` after ingesting more of them fills the list in, since it's a full re-sync, not an append. """ from __future__ import annotations import uuid from sqlalchemy import delete from sqlalchemy.ext.asyncio import AsyncSession from app.core.logging import get_logger from app.models.collection import CollectionMovie from app.models.enums import MatchConfidence from app.providers.base import MetadataProvider from app.repositories import polymorphic from app.repositories import reference_repository as refs from app.repositories.provider_repository import get_or_create_provider, upsert_provider_record from app.services.metadata.matcher import match_entity from app.services.metadata.normalizer import normalize_collection logger = get_logger(__name__) async def ingest_collection( session: AsyncSession, provider: MetadataProvider, external_id: str, *, ingestion_job_id: uuid.UUID | None = None, ) -> uuid.UUID: """Fetch, normalize, and upsert a single collection. Returns the Aurora collection UUID (new or existing).""" tmdb_provider = await get_or_create_provider(session, provider.slug, provider.name) aurora_id_existing, confidence = await match_entity( session, provider_id=tmdb_provider.id, entity_type="collection", external_id=external_id ) raw = await provider.get_collection(external_id) normalized = normalize_collection(raw) collection = await refs.get_or_create_collection( session, provider_id=tmdb_provider.id, external_id=external_id, name=normalized.name, overview=normalized.overview, ) await polymorphic.sync_artwork( session, entity_type="collection", entity_id=collection.id, provider_id=tmdb_provider.id, artwork_list=normalized.artwork, ) await session.execute( delete(CollectionMovie).where(CollectionMovie.collection_id == collection.id) ) movie_ids = await refs.find_entities_by_external_ids( session, tmdb_provider.id, "movie", normalized.part_external_ids ) for sort_order, movie_id in enumerate(movie_ids): session.add( CollectionMovie(collection_id=collection.id, movie_id=movie_id, sort_order=sort_order) ) await upsert_provider_record( session, provider_id=tmdb_provider.id, entity_type="collection", external_id=external_id, raw_data=raw, aurora_entity_id=collection.id, match_confidence=confidence if aurora_id_existing else MatchConfidence.NEW, ingestion_job_id=ingestion_job_id, ) logger.info("collection_ingested", collection_id=str(collection.id), external_id=external_id) return collection.id