diff --git a/lightsync/api/audio.py b/lightsync/api/audio.py new file mode 100644 index 0000000..421d27d --- /dev/null +++ b/lightsync/api/audio.py @@ -0,0 +1,47 @@ +"""Audio loading and status endpoints.""" +import asyncio +from pathlib import Path +from fastapi import APIRouter, HTTPException, Request +from pydantic import BaseModel + +router = APIRouter() + +SUPPORTED_EXTENSIONS = {".mp3", ".wav", ".flac", ".ogg"} + + +class LoadRequest(BaseModel): + path: str + + +class AudioState(BaseModel): + position: float + paused: bool + duration: float | None + loaded: bool + file: str | None + + +@router.post("/load") +async def load_audio(req: LoadRequest, request: Request): + """Load an audio file by server path (AUD-01). YouTube URL deferred to Phase 6 (AUD-02 stub).""" + engine = request.app.state.engine + if engine is None: + raise HTTPException(status_code=503, detail="Audio engine not ready") + + p = Path(req.path) + if not p.exists(): + raise HTTPException(status_code=404, detail=f"File not found: {req.path}") + if p.suffix.lower() not in SUPPORTED_EXTENSIONS: + raise HTTPException(status_code=400, detail=f"Unsupported format: {p.suffix}. Supported: {', '.join(sorted(SUPPORTED_EXTENSIONS))}") + + await asyncio.to_thread(engine.load, req.path) + return {"status": "loaded", "path": req.path} + + +@router.get("/state") +async def get_audio_state(request: Request): + """Get current audio playback state.""" + engine = request.app.state.engine + if engine is None: + raise HTTPException(status_code=503, detail="Audio engine not ready") + return engine.get_state() diff --git a/lightsync/main.py b/lightsync/main.py index 5dbbd00..bf5db44 100644 --- a/lightsync/main.py +++ b/lightsync/main.py @@ -45,10 +45,11 @@ def create_app() -> FastAPI: app = FastAPI(title="LightSync", lifespan=lifespan, redirect_slashes=False) # API routes (registered before static mount — see Pattern 7) - from lightsync.api import shows, devices, ws + from lightsync.api import shows, devices, ws, audio app.include_router(shows.router, prefix="/api/shows") app.include_router(devices.router, prefix="/api/devices") app.include_router(ws.router) + app.include_router(audio.router, prefix="/api/audio") # Static file serving — no-cache headers to prevent stale JS/CSS frontend_dir = Path(__file__).parent / "frontend"