"""Main FastAPI application entry point.""" import logging from contextlib import asynccontextmanager from pathlib import Path from typing import AsyncGenerator from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, HTMLResponse from fastapi.staticfiles import StaticFiles from slowapi.errors import RateLimitExceeded from app.core.config import settings from app.core.database import close_db, init_db from app.core.rate_limiter import limiter logger = logging.getLogger(__name__) # Get the base directory BASE_DIR = Path(__file__).resolve().parent.parent @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """ Lifespan context manager for FastAPI application. Handles startup and shutdown events. """ # Startup logger.info("Starting up...") if settings.DEBUG: logger.debug("Debug mode is ON") logger.debug(f"Database URL: {settings.DATABASE_URL}") logger.debug(f"Redis URL: {settings.FULL_REDIS_URL}") # Initialize database await init_db() logger.info("Database initialized") yield # Shutdown logger.info("Shutting down...") await close_db() logger.info("Database connections closed") # Create FastAPI application app = FastAPI( title=settings.APP_NAME, version=settings.APP_VERSION, description="Alternative to Spotify with YouTube streaming", docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/openapi.json", lifespan=lifespan, ) # Set up rate limiting app.state.limiter = limiter # Configure CORS app.add_middleware( CORSMiddleware, allow_origins=settings.BACKEND_CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/", response_class=HTMLResponse) async def root() -> str: """Serve the web application.""" template_path = BASE_DIR / "app" / "templates" / "index.html" with open(template_path, 'r', encoding='utf-8') as f: return f.read() @app.get("/health") async def health_check() -> dict[str, str]: """Health check endpoint.""" return {"status": "healthy"} @app.get("/api/v1") async def api_v1_info() -> dict[str, str]: """API v1 information endpoint.""" return { "version": "v1", "prefix": settings.API_V1_PREFIX, "docs": "/api/docs", } # Exception handlers @app.exception_handler(Exception) async def global_exception_handler(request, exc) -> JSONResponse: """Global exception handler for unhandled exceptions.""" if settings.DEBUG: # In debug mode, return full error details return JSONResponse( status_code=500, content={ "detail": str(exc), "type": type(exc).__name__, }, ) # In production, return generic error message return JSONResponse( status_code=500, content={"detail": "Internal server error"}, ) # API routes from app.api.v1 import auth, music, playlists, library app.include_router(auth.router, prefix=settings.API_V1_PREFIX, tags=["authentication"]) app.include_router(music.router, prefix=settings.API_V1_PREFIX, tags=["music"]) app.include_router(playlists.router, prefix=settings.API_V1_PREFIX, tags=["playlists"]) app.include_router(library.router, prefix=settings.API_V1_PREFIX, tags=["library"]) # Mount static files static_dir = BASE_DIR / "app" / "static" static_dir.mkdir(exist_ok=True) app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") if __name__ == "__main__": import uvicorn uvicorn.run( "app.main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG, log_level=settings.LOG_LEVEL.lower(), )