"""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") srt_destination_url: Optional[str] = Field(None, description="Target specific SRT destination (None = all)") 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 globally (all ports). Args: request: SCTE35InjectionRequest 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, port_index=None, srt_destination_url=request.srt_destination_url, ) logger.info(f"Injected global SCTE35 marker 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.post("/ports/{port_index}/scte35/inject", response_model=SCTE35Marker) async def inject_scte35_marker_for_port( port_index: int, request: SCTE35InjectionRequest, manager: SCTE35Manager = Depends(get_scte35_manager), ) -> SCTE35Marker: """ Inject a SCTE35 ad break marker on a specific port's SRT output(s). Args: port_index: 0-based port index request: SCTE35InjectionRequest Returns: SCTE35Marker with timestamp, port_index, and metadata """ 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, port_index=port_index, srt_destination_url=request.srt_destination_url, ) logger.info(f"Injected SCTE35 marker event_id={request.event_id} on port {port_index}") 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 on port {port_index}: {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") @router.get("/ports/{port_index}/scte35/history", response_model=list[SCTE35Marker]) async def get_scte35_history_for_port( port_index: int, manager: SCTE35Manager = Depends(get_scte35_manager), ) -> list[SCTE35Marker]: """ Get SCTE35 marker history for a specific port. """ try: return [m for m in manager.get_marker_history() if m.port_index == port_index] except Exception as e: logger.error(f"Error retrieving SCTE35 history for port {port_index}: {e}") raise HTTPException(status_code=500, detail="Internal server error")