fix(02): Alpine libmpv/libsndfile symlinks, upload endpoint, browser file picker

- 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
This commit is contained in:
Claude
2026-04-06 15:34:11 +00:00
parent 7f9efee47a
commit 9239988d38
7 changed files with 197 additions and 9 deletions

View File

@@ -1,13 +1,15 @@
"""Audio loading and status endpoints."""
import asyncio
import shutil
from pathlib import Path
from fastapi import APIRouter, HTTPException, Request
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):
@@ -48,6 +50,38 @@ async def get_audio_state(request: Request):
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).