103 lines
3.4 KiB
Python
103 lines
3.4 KiB
Python
"""
|
|
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],
|
|
}
|