Files
led2/lightsync/main.py
Claude 5f75a19868 feat(02-01): implement MPVEngine with position polling and lifespan integration
- Create lightsync/audio/__init__.py package marker
- Create lightsync/audio/engine.py with MPVEngine class
  - Headless mpv wrapper (vo=null, ao from MPV_AO env var default null)
  - 10Hz polling loop in daemon thread (time.sleep(0.1), not observe_property)
  - Thread-safe get_state() returning position, paused, duration, loaded, file
  - load/play/pause/seek/stop methods (AUD-01, AUD-03)
  - apply_timer_fix() with sys.platform == win32 guard (INF-03)
- Update main.py lifespan to create/start/stop MPVEngine
  - MPV_AO env var controls audio output (default null for server mode)
  - engine exposed via app.state.engine for API routes
2026-04-06 13:03:43 +00:00

61 lines
1.9 KiB
Python

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
yield
# Shutdown
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()