"""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.routes 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.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) processor_manager = ProcessorManager() @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan manager. Handles startup and shutdown events. """ # Startup logger.info(f"Starting WLED Screen Controller 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) # 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, capture_template_id=device.capture_template_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 Screen Controller") # 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 Screen Controller", 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 Screen Controller", "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 )