feat(02-02): add REST audio load endpoint with file validation
- Create lightsync/api/audio.py with POST /load and GET /state endpoints - Validate file existence and extension (.mp3/.wav/.flac/.ogg) - Use asyncio.to_thread for engine.load to avoid blocking event loop - Return 404/400/503 on error conditions - Register audio router at /api/audio prefix in main.py - AUD-02 (YouTube) noted as Phase 6 deferral
This commit is contained in:
47
lightsync/api/audio.py
Normal file
47
lightsync/api/audio.py
Normal file
@@ -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()
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user