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.protocol.udp_sender import UDPSender # Module-level containers populated in lifespan registry: DeviceRegistry | None = None show_store: ShowStore | None = None @asynccontextmanager async def lifespan(app: FastAPI): global registry, show_store # Startup registry = DeviceRegistry(Path("devices.json")) await registry.load() show_store = ShowStore(Path("shows")) show_store.ensure_dir() # Beat analysis cache: filepath -> {"tempo": float, "beats": [float, ...]} app.state.beats = {} # UDP sender for device communication (Phase 3) udp_sender = UDPSender() await udp_sender.start() app.state.udp_sender = udp_sender yield # Shutdown await app.state.udp_sender.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, timeline 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") app.include_router(timeline.router, prefix="/api") # Serve uploaded audio files so