- Dockerfile: add libmpv.so + libsndfile.so symlinks (Alpine/musl has no ldconfig) - engine.py: monkey-patch ctypes.util.find_library as fallback for Alpine - audio.py: add /upload (multipart) and /files endpoints - pyproject.toml: add python-multipart - frontend: replace path input with upload button + file selector dropdown
108 lines
3.5 KiB
Python
108 lines
3.5 KiB
Python
"""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"]}
|