Files
led2/lightsync/main.py
Claude de455400d2 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
2026-04-06 13:07:45 +00:00

70 lines
2.1 KiB
Python

import asyncio
import os
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from lightsync.devices.registry import DeviceRegistry
from lightsync.store.show_store import ShowStore
from lightsync.audio.engine import MPVEngine
# Module-level containers populated in lifespan
registry: DeviceRegistry | None = None
show_store: ShowStore | None = None
engine: MPVEngine | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global registry, show_store, engine
# Startup
registry = DeviceRegistry(Path("devices.json"))
await registry.load()
show_store = ShowStore(Path("shows"))
show_store.ensure_dir()
ao = os.environ.get("MPV_AO", "null")
engine = MPVEngine(ao=ao)
engine.start()
app.state.engine = engine
from lightsync.api.ws import broadcast_loop
task = asyncio.create_task(broadcast_loop(lambda: engine))
yield
# Shutdown
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
engine.stop()
await registry.save()
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, 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"
@app.get("/{filename:path}")
async def static_files(request: Request, filename: str):
path = frontend_dir / (filename or "index.html")
if not path.exists() or not path.is_file():
path = frontend_dir / "index.html"
response = FileResponse(path)
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
return response
return app
app = create_app()