Files
wled-screen-controller-mixed/server/src/wled_controller/main.py
alexei.dolgolyov c3828e10fa Refactor capture engine architecture, rename PictureStream to PictureSource, and split API modules
- Separate CaptureEngine into stateless factory + stateful CaptureStream session
- Add LiveStream/LiveStreamManager for shared capture with reference counting
- Rename PictureStream to PictureSource across storage, API, and UI
- Remove legacy migration logic and unused compatibility code
- Split monolithic routes.py (1935 lines) into 5 focused route modules
- Split schemas.py (480 lines) into 7 schema modules with re-exports
- Extract dependency injection into dedicated dependencies.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 14:27:00 +03:00

186 lines
6.0 KiB
Python

"""FastAPI application entry point."""
import sys
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from wled_controller import __version__
from wled_controller.api import router
from wled_controller.api.dependencies import init_dependencies
from wled_controller.config import get_config
from wled_controller.core.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.utils import setup_logging, get_logger
# Initialize logging
setup_logging()
logger = get_logger(__name__)
# Get configuration
config = get_config()
# Initialize storage and processing
device_store = DeviceStore(config.storage.devices_file)
template_store = TemplateStore(config.storage.templates_file)
pp_template_store = PostprocessingTemplateStore(config.storage.postprocessing_templates_file)
picture_source_store = PictureSourceStore(config.storage.picture_sources_file)
processor_manager = ProcessorManager(
picture_source_store=picture_source_store,
capture_template_store=template_store,
pp_template_store=pp_template_store,
)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan manager.
Handles startup and shutdown events.
"""
# Startup
logger.info(f"Starting WLED Grab v{__version__}")
logger.info(f"Python version: {sys.version}")
logger.info(f"Server listening on {config.server.host}:{config.server.port}")
# Validate authentication configuration
if not config.auth.api_keys:
logger.error("=" * 70)
logger.error("CRITICAL: No API keys configured!")
logger.error("Authentication is REQUIRED for all API requests.")
logger.error("Please add API keys to your configuration:")
logger.error(" 1. Generate keys: openssl rand -hex 32")
logger.error(" 2. Add to config/default_config.yaml under auth.api_keys")
logger.error(" 3. Format: label: \"your-generated-key\"")
logger.error("=" * 70)
raise RuntimeError("No API keys configured - server cannot start without authentication")
# Log authentication status
logger.info(f"API Authentication: ENFORCED ({len(config.auth.api_keys)} clients configured)")
client_labels = ", ".join(config.auth.api_keys.keys())
logger.info(f"Authorized clients: {client_labels}")
logger.info("All API requests require valid Bearer token authentication")
# Initialize API dependencies
init_dependencies(
device_store, template_store, processor_manager,
pp_template_store=pp_template_store,
picture_source_store=picture_source_store,
)
# Load existing devices into processor manager
devices = device_store.get_all_devices()
for device in devices:
try:
processor_manager.add_device(
device_id=device.id,
device_url=device.url,
led_count=device.led_count,
settings=device.settings,
calibration=device.calibration,
picture_source_id=device.picture_source_id,
)
logger.info(f"Loaded device: {device.name} ({device.id})")
except Exception as e:
logger.error(f"Failed to load device {device.id}: {e}")
logger.info(f"Loaded {len(devices)} devices from storage")
# Start background health monitoring for all devices
await processor_manager.start_health_monitoring()
yield
# Shutdown
logger.info("Shutting down WLED Grab")
# Stop all processing
try:
await processor_manager.stop_all()
logger.info("Stopped all processors")
except Exception as e:
logger.error(f"Error stopping processors: {e}")
# Create FastAPI application
app = FastAPI(
title="WLED Grab",
description="Control WLED devices based on screen content for ambient lighting",
version=__version__,
lifespan=lifespan,
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=config.server.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API routes
app.include_router(router)
# Mount static files
static_path = Path(__file__).parent / "static"
if static_path.exists():
app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
logger.info(f"Mounted static files from {static_path}")
else:
logger.warning(f"Static files directory not found: {static_path}")
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
"""Global exception handler for unhandled errors."""
logger.error(f"Unhandled exception: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={
"error": "InternalServerError",
"message": "An unexpected error occurred",
"detail": str(exc) if config.server.log_level == "DEBUG" else None,
},
)
@app.get("/")
async def root():
"""Serve the web UI dashboard."""
static_path = Path(__file__).parent / "static" / "index.html"
if static_path.exists():
return FileResponse(static_path)
# Fallback to JSON if static files not found
return {
"name": "WLED Grab",
"version": __version__,
"docs": "/docs",
"health": "/health",
"api": "/api/v1",
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"wled_controller.main:app",
host=config.server.host,
port=config.server.port,
log_level=config.server.log_level.lower(),
reload=False, # Disabled due to watchfiles infinite reload loop
)