Add backend/app/api/routes.py
This commit is contained in:
parent
306eb9b9d9
commit
8fee41bc97
1 changed files with 259 additions and 0 deletions
259
backend/app/api/routes.py
Normal file
259
backend/app/api/routes.py
Normal 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")
|
||||
Loading…
Reference in a new issue