Files
led2/lightsync/main.py
Claude 5e400a6834 feat(02-02): wire WebSocket broadcast loop and command dispatch
- Add broadcast_loop sending position ticks at 10Hz to all clients
- Replace echo stub with play/pause/seek/load command dispatch to MPVEngine
- Start broadcast_loop as asyncio task in lifespan, cancel on shutdown
- Use get_engine callable to avoid circular import at module level
2026-04-06 13:07:18 +00:00

69 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
app.include_router(shows.router, prefix="/api/shows")
app.include_router(devices.router, prefix="/api/devices")
app.include_router(ws.router)
# 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()