Add backend/app/api/routes.py

This commit is contained in:
Zac Gaetano 2026-04-14 09:21:08 -04:00
parent 306eb9b9d9
commit 8fee41bc97

259
backend/app/api/routes.py Normal file
View file

@ -0,0 +1,259 @@
"""FastAPI REST API routes for Deltacast SDI recorder control."""
import logging
from fastapi import APIRouter, HTTPException, Depends
from typing import Any, Optional
from pydantic import BaseModel, Field
from ..models import RecorderConfig, PortStatus, SCTE35Marker
from ..recorders.recorder import RecorderManager
from ..recorders.scte35 import SCTE35Manager
logger = logging.getLogger(__name__)
# ============================================================================
# Pydantic Models for Request Validation
# ============================================================================
class SCTE35InjectionRequest(BaseModel):
"""Request model for SCTE35 marker injection endpoint."""
event_id: int = Field(..., description="Unique event identifier")
duration_seconds: float = Field(..., gt=0, description="Duration in seconds")
webhook_url: Optional[str] = Field(None, description="Optional webhook URL")
out_of_network: bool = Field(True, description="Out of network indicator")
splice_immediate: bool = Field(False, description="Splice immediate flag")
router = APIRouter(prefix="/api")
# These will be set by main.py during startup
recorder_manager: RecorderManager | None = None
scte35_manager: SCTE35Manager | None = None
def get_recorder_manager() -> RecorderManager:
"""Dependency to get the RecorderManager instance."""
if recorder_manager is None:
raise HTTPException(status_code=503, detail="Recorder not initialized")
return recorder_manager
def get_scte35_manager() -> SCTE35Manager:
"""Dependency to get the SCTE35Manager instance."""
if scte35_manager is None:
raise HTTPException(status_code=503, detail="SCTE35 manager not initialized")
return scte35_manager
# ============================================================================
# Health and Status Endpoints
# ============================================================================
@router.get("/health")
async def health(manager: RecorderManager = Depends(get_recorder_manager)) -> dict[str, Any]:
"""
Health check endpoint.
Returns:
dict with status, port count, and recording count
"""
try:
all_status = manager.get_all_status()
recording_count = sum(1 for status in all_status if status.is_recording)
return {
"status": "ok",
"ports": len(all_status),
"recording_count": recording_count,
}
except Exception as e:
logger.error(f"Health check error: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
# ============================================================================
# Port Status Endpoints
# ============================================================================
@router.get("/ports", response_model=list[PortStatus])
async def list_ports(manager: RecorderManager = Depends(get_recorder_manager)) -> list[PortStatus]:
"""
Get status of all recorder ports.
Returns:
List of PortStatus objects for all ports
"""
try:
return manager.get_all_status()
except Exception as e:
logger.error(f"Error listing ports: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/ports/{port_index}", response_model=PortStatus)
async def get_port(
port_index: int,
manager: RecorderManager = Depends(get_recorder_manager),
) -> PortStatus:
"""
Get status of a specific recorder port.
Args:
port_index: 0-based port index
Returns:
PortStatus for the specified port
Raises:
HTTPException(404): If port index is invalid
"""
try:
return manager.get_status(port_index)
except ValueError as e:
logger.warning(f"Invalid port index {port_index}: {e}")
raise HTTPException(status_code=404, detail=f"Port {port_index} not found")
except Exception as e:
logger.error(f"Error getting port {port_index}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
# ============================================================================
# Recording Control Endpoints
# ============================================================================
@router.post("/ports/{port_index}/start")
async def start_recording(
port_index: int,
config: RecorderConfig,
manager: RecorderManager = Depends(get_recorder_manager),
) -> dict[str, Any]:
"""
Start recording on a specific port.
Args:
port_index: 0-based port index
config: RecorderConfig with codec and output settings
Returns:
Message confirmation with port number
Raises:
HTTPException(400): If port is already recording or config is invalid
HTTPException(404): If port index is invalid
"""
try:
await manager.start_recording(port_index, config)
logger.info(f"Started recording on port {port_index}")
return {
"message": "Recording started",
"port": port_index,
}
except ValueError as e:
logger.warning(f"Invalid port {port_index}: {e}")
raise HTTPException(status_code=404, detail=f"Port {port_index} not found")
except RuntimeError as e:
logger.warning(f"Cannot start recording on port {port_index}: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error starting recording on port {port_index}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/ports/{port_index}/stop")
async def stop_recording(
port_index: int,
manager: RecorderManager = Depends(get_recorder_manager),
) -> dict[str, Any]:
"""
Stop recording on a specific port.
Args:
port_index: 0-based port index
Returns:
Message confirmation with port number
Raises:
HTTPException(404): If port index is invalid
"""
try:
await manager.stop_recording(port_index)
logger.info(f"Stopped recording on port {port_index}")
return {
"message": "Recording stopped",
"port": port_index,
}
except ValueError as e:
logger.warning(f"Invalid port {port_index}: {e}")
raise HTTPException(status_code=404, detail=f"Port {port_index} not found")
except Exception as e:
logger.error(f"Error stopping recording on port {port_index}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
# ============================================================================
# SCTE35 Ad Break Endpoints
# ============================================================================
@router.post("/scte35/inject", response_model=SCTE35Marker)
async def inject_scte35_marker(
request: SCTE35InjectionRequest,
manager: SCTE35Manager = Depends(get_scte35_manager),
) -> SCTE35Marker:
"""
Inject a SCTE35 ad break marker.
Args:
request: SCTE35InjectionRequest with:
- event_id: int (unique event identifier)
- duration_seconds: float (duration in seconds)
- webhook_url: str | None (optional callback URL)
- out_of_network: bool (whether out-of-network ad)
- splice_immediate: bool (whether splice immediately)
Returns:
SCTE35Marker with timestamp and metadata
Raises:
HTTPException(400): If parameters are invalid
"""
try:
marker = await manager.inject_marker(
event_id=request.event_id,
duration_seconds=request.duration_seconds,
webhook_url=request.webhook_url,
out_of_network=request.out_of_network,
splice_immediate=request.splice_immediate,
)
logger.info(f"Injected SCTE35 marker with event_id {request.event_id}")
return marker
except ValueError as e:
logger.warning(f"Invalid SCTE35 parameters: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error injecting SCTE35 marker: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/scte35/history", response_model=list[SCTE35Marker])
async def get_scte35_history(
manager: SCTE35Manager = Depends(get_scte35_manager),
) -> list[SCTE35Marker]:
"""
Get history of injected SCTE35 markers.
Returns:
List of SCTE35Marker objects in chronological order
"""
try:
return manager.get_marker_history()
except Exception as e:
logger.error(f"Error retrieving SCTE35 history: {e}")
raise HTTPException(status_code=500, detail="Internal server error")