"""Audio loading and status endpoints.""" import asyncio import shutil from pathlib import Path from fastapi import APIRouter, HTTPException, Request, UploadFile, File from pydantic import BaseModel from lightsync.audio.waveform import extract_peaks router = APIRouter() SUPPORTED_EXTENSIONS = {".mp3", ".wav", ".flac", ".ogg"} SHOWS_DIR = Path("/app/shows") 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() @router.get("/files") async def list_audio_files(): """List audio files available in the shows directory.""" SHOWS_DIR.mkdir(parents=True, exist_ok=True) files = sorted( p.name for p in SHOWS_DIR.iterdir() if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS ) return {"files": files, "directory": str(SHOWS_DIR)} @router.post("/upload") async def upload_audio(request: Request, file: UploadFile = File(...)): """Upload an audio file to the shows directory and load it.""" engine = request.app.state.engine if engine is None: raise HTTPException(status_code=503, detail="Audio engine not ready") suffix = Path(file.filename).suffix.lower() if suffix not in SUPPORTED_EXTENSIONS: raise HTTPException(status_code=400, detail=f"Unsupported format: {suffix}. Supported: {', '.join(sorted(SUPPORTED_EXTENSIONS))}") SHOWS_DIR.mkdir(parents=True, exist_ok=True) dest = SHOWS_DIR / file.filename with dest.open("wb") as f: shutil.copyfileobj(file.file, f) await asyncio.to_thread(engine.load, str(dest)) return {"status": "loaded", "path": str(dest), "filename": file.filename} @router.get("/waveform") async def get_waveform(request: Request, peaks: int = 1000): """Get waveform peak data for the currently loaded audio file (AUD-05). Query params: peaks: Number of peak samples (default 1000, max 5000) """ engine = request.app.state.engine if engine is None: raise HTTPException(status_code=503, detail="Audio engine not ready") state = engine.get_state() if not state["loaded"] or not state["file"]: raise HTTPException(status_code=400, detail="No audio file loaded") num_peaks = min(max(peaks, 100), 5000) try: data = await asyncio.to_thread(extract_peaks, state["file"], num_peaks) except Exception as e: raise HTTPException(status_code=500, detail=f"Waveform extraction failed: {e}") return {"peaks": data, "count": len(data), "file": state["file"]}