""" backend/routers/scenes.py - Video scene map ingestion. """ from __future__ import annotations import csv import io import asyncpg from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from backend.auth.dependencies import UserPrincipal, require_role from backend.db.pool import get_pool router = APIRouter() @router.post("/upload", summary="Upload a scene CSV for a marketing video") async def upload_scene_map( video_asset_id: str, file: UploadFile = File(...), pool: asyncpg.Pool = Depends(get_pool), user: UserPrincipal = Depends(require_role("SENIOR_BROKER")), ) -> dict[str, int | str]: del user if not file.filename or not file.filename.lower().endswith(".csv"): raise HTTPException(status_code=400, detail="Scene upload must be a CSV file.") raw_bytes = await file.read() try: text = raw_bytes.decode("utf-8-sig") except UnicodeDecodeError as exc: raise HTTPException(status_code=400, detail="CSV must be UTF-8 encoded.") from exc reader = csv.DictReader(io.StringIO(text)) required = {"scene_no", "start_ms", "end_ms", "room_type"} if not reader.fieldnames or not required.issubset(set(reader.fieldnames)): raise HTTPException( status_code=400, detail="CSV must contain scene_no,start_ms,end_ms,room_type columns.", ) rows: list[tuple[str, int, int, int, str, str | None]] = [] for row in reader: try: rows.append( ( video_asset_id, int(row["scene_no"]), int(row["start_ms"]), int(row["end_ms"]), row["room_type"].strip(), (row.get("description") or "").strip() or None, ) ) except (TypeError, ValueError, KeyError) as exc: raise HTTPException(status_code=400, detail=f"Invalid scene row: {row}") from exc if not rows: raise HTTPException(status_code=400, detail="CSV contains no scene rows.") async with pool.acquire() as conn: async with conn.transaction(): await conn.execute( "DELETE FROM video_scene_maps WHERE video_asset_id = $1", video_asset_id, ) await conn.executemany( """ INSERT INTO video_scene_maps (video_asset_id, scene_no, start_ms, end_ms, room_type, description) VALUES ($1, $2, $3, $4, $5, $6) """, rows, ) return {"status": "uploaded", "video_asset_id": video_asset_id, "row_count": len(rows)} @router.get("/{video_asset_id}", summary="List the uploaded scene map for a marketing video") async def get_scene_map( video_asset_id: str, pool: asyncpg.Pool = Depends(get_pool), user: UserPrincipal = Depends(require_role("SENIOR_BROKER")), ) -> dict[str, object]: del user async with pool.acquire() as conn: rows = await conn.fetch( """ SELECT scene_no, start_ms, end_ms, room_type, description FROM video_scene_maps WHERE video_asset_id = $1 ORDER BY scene_no ASC """, video_asset_id, ) return { "video_asset_id": video_asset_id, "row_count": len(rows), "scenes": [dict(row) for row in rows], }