Aniworld/src/server/fastapi_app.py
2025-12-10 21:12:34 +01:00

220 lines
7.8 KiB
Python

"""
FastAPI application for Aniworld anime download manager.
This module provides the main FastAPI application with proper CORS
configuration, middleware setup, static file serving, and Jinja2 template
integration.
"""
from contextlib import asynccontextmanager
from pathlib import Path
import uvicorn
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from src.config.settings import settings
# Import core functionality
from src.infrastructure.logging import setup_logging
from src.server.api.anime import router as anime_router
from src.server.api.auth import router as auth_router
from src.server.api.config import router as config_router
from src.server.api.download import router as download_router
from src.server.api.scheduler import router as scheduler_router
from src.server.api.websocket import router as websocket_router
from src.server.controllers.error_controller import (
not_found_handler,
server_error_handler,
)
# Import controllers
from src.server.controllers.health_controller import router as health_router
from src.server.controllers.page_controller import router as page_router
from src.server.middleware.auth import AuthMiddleware
from src.server.middleware.error_handler import register_exception_handlers
from src.server.middleware.setup_redirect import SetupRedirectMiddleware
from src.server.services.progress_service import get_progress_service
from src.server.services.websocket_service import get_websocket_service
# Prefer storing application-wide singletons on FastAPI.state instead of
# module-level globals. This makes testing and multi-instance hosting safer.
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Manage application lifespan (startup and shutdown)."""
# Setup logging first with DEBUG level
logger = setup_logging(log_level="DEBUG")
# Startup
try:
logger.info("Starting FastAPI application...")
# Initialize database first (required for migration and other services)
try:
from src.server.database.connection import init_db
await init_db()
logger.info("Database initialized successfully")
except Exception as e:
logger.error("Failed to initialize database: %s", e, exc_info=True)
raise # Database is required, fail startup if it fails
# Load configuration from config.json and sync with settings
try:
from src.server.services.config_service import get_config_service
config_service = get_config_service()
config = config_service.load_config()
# Sync anime_directory from config.json to settings
if config.other and config.other.get("anime_directory"):
settings.anime_directory = str(config.other["anime_directory"])
logger.info(
"Loaded anime_directory from config: %s",
settings.anime_directory
)
except Exception as e:
logger.warning("Failed to load config from config.json: %s", e)
# Initialize progress service with event subscription
progress_service = get_progress_service()
ws_service = get_websocket_service()
async def progress_event_handler(event) -> None:
"""Handle progress events and broadcast via WebSocket.
Args:
event: ProgressEvent containing progress update data
"""
message = {
"type": event.event_type,
"data": event.progress.to_dict(),
}
await ws_service.manager.broadcast_to_room(message, event.room)
# Subscribe to progress events
progress_service.subscribe("progress_updated", progress_event_handler)
# Initialize download service and restore queue from database
# Only if anime directory is configured
try:
from src.server.utils.dependencies import get_download_service
if settings.anime_directory:
download_service = get_download_service()
await download_service.initialize()
logger.info("Download service initialized and queue restored")
else:
logger.info(
"Download service initialization skipped - "
"anime directory not configured"
)
except Exception as e:
logger.warning("Failed to initialize download service: %s", e)
# Continue startup - download service can be initialized later
logger.info("FastAPI application started successfully")
logger.info("Server running on http://127.0.0.1:8000")
logger.info(
"API documentation available at http://127.0.0.1:8000/api/docs"
)
except Exception as e:
logger.error("Error during startup: %s", e, exc_info=True)
raise # Re-raise to prevent app from starting in broken state
# Yield control to the application
yield
# Shutdown
logger.info("FastAPI application shutting down")
# Shutdown download service and its thread pool
try:
from src.server.services.download_service import _download_service_instance
if _download_service_instance is not None:
logger.info("Stopping download service...")
await _download_service_instance.stop()
logger.info("Download service stopped successfully")
except Exception as e:
logger.error("Error stopping download service: %s", e, exc_info=True)
# Close database connections
try:
from src.server.database.connection import close_db
await close_db()
logger.info("Database connections closed")
except Exception as e:
logger.error("Error closing database: %s", e, exc_info=True)
logger.info("FastAPI application shutdown complete")
# Initialize FastAPI app with lifespan
app = FastAPI(
title="Aniworld Download Manager",
description="Modern web interface for Aniworld anime download management",
version="1.0.0",
docs_url="/api/docs",
redoc_url="/api/redoc",
lifespan=lifespan
)
# Configure CORS using environment-driven configuration.
allowed_origins = settings.allowed_origins or [
"http://localhost:3000",
"http://localhost:8000",
]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Configure static files
STATIC_DIR = Path(__file__).parent / "web" / "static"
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# Attach setup redirect middleware (runs before auth checks)
app.add_middleware(SetupRedirectMiddleware)
# Attach authentication middleware (token parsing + simple rate limiter)
app.add_middleware(AuthMiddleware, rate_limit_per_minute=5)
# Include routers
app.include_router(health_router)
app.include_router(page_router)
app.include_router(auth_router)
app.include_router(config_router)
app.include_router(scheduler_router)
app.include_router(anime_router)
app.include_router(download_router)
app.include_router(websocket_router)
# Register exception handlers
register_exception_handlers(app)
@app.exception_handler(404)
async def handle_not_found(request: Request, exc: HTTPException):
"""Custom 404 handler."""
return await not_found_handler(request, exc)
@app.exception_handler(500)
async def handle_server_error(request: Request, exc: Exception):
"""Custom 500 handler."""
return await server_error_handler(request, exc)
if __name__ == "__main__":
uvicorn.run(
"fastapi_app:app",
host="127.0.0.1",
port=8000,
reload=True,
log_level="debug"
)