""" Source browser for detection pipeline. Lists available media sources from blob storage (MinIO). GET /detect/sources — list chunk jobs GET /detect/sources/{job_id}/chunks — list chunks for a job GET /detect/sources/{job_id}/chunks/{name}/url — presigned preview URL """ from __future__ import annotations import logging from fastapi import APIRouter, HTTPException from pydantic import BaseModel logger = logging.getLogger(__name__) router = APIRouter(prefix="/detect", tags=["detect"]) class ChunkInfoResponse(BaseModel): filename: str key: str size_bytes: int class SourceInfoResponse(BaseModel): job_id: str source_type: str = "chunk_job" chunk_count: int total_bytes: int = 0 def _list_sources() -> list[SourceInfoResponse]: """List chunk jobs from blob storage.""" from core.storage.blob import get_store store = get_store("out") try: objects = store.list(prefix="chunks/") except Exception as e: logger.warning("Failed to list blob sources: %s", e) return [] jobs: dict[str, int] = {} job_bytes: dict[str, int] = {} for obj in objects: rel_key = obj.key.removeprefix(store.prefix) parts = rel_key.split("/") if len(parts) >= 3 and parts[0] == "chunks": job_id = parts[1] jobs[job_id] = jobs.get(job_id, 0) + 1 job_bytes[job_id] = job_bytes.get(job_id, 0) + obj.size_bytes sources = [] for job_id, count in sorted(jobs.items()): source = SourceInfoResponse( job_id=job_id, source_type="chunk_job", chunk_count=count, total_bytes=job_bytes.get(job_id, 0), ) sources.append(source) return sources @router.get("/sources", response_model=list[SourceInfoResponse]) def list_sources(): """List available chunk jobs from blob storage.""" return _list_sources() @router.get("/sources/{source_job_id}/chunks", response_model=list[ChunkInfoResponse]) def list_chunks(source_job_id: str): """List chunks for a specific source job.""" from core.storage.blob import get_store store = get_store("out") try: objects = store.list(prefix=f"chunks/{source_job_id}/", extensions={".mp4"}) except Exception as e: logger.warning("Failed to list chunks for %s: %s", source_job_id, e) raise HTTPException(status_code=503, detail=f"Blob storage unavailable: {e}") if not objects: raise HTTPException(status_code=404, detail=f"Source not found: {source_job_id}") chunks = [] for obj in objects: info = ChunkInfoResponse(filename=obj.filename, key=obj.key, size_bytes=obj.size_bytes) chunks.append(info) return sorted(chunks, key=lambda c: c.filename) @router.get("/sources/{source_job_id}/chunks/{filename}/url") def get_chunk_url(source_job_id: str, filename: str): """Return a presigned URL for previewing a chunk in the browser.""" from core.storage.blob import get_store store = get_store("out") key = f"chunks/{source_job_id}/{filename}" try: url = store.get_url(key, expires=3600) except Exception as e: raise HTTPException(status_code=503, detail=f"Could not generate URL: {e}") return {"url": url}