Built the Sentinel Tab
This commit is contained in:
102
backend/routers/scenes.py
Normal file
102
backend/routers/scenes.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
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],
|
||||
}
|
||||
Reference in New Issue
Block a user