""" Mulita - Photo Management Application Main FastAPI application entry point """ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware import logging import os from app.config import settings from app.database import init_db from app.routers import photos, folders, heaps, tags, discard, library, search, auth, admin, sharing, download, nextcloud, nc_webhook from app.services.scanner import start_initial_scan, bootstrap_default_source_root from app.services.cleanup import cleanup_data_integrity from app.services.nextcloud_dav import init_preview_client, close_preview_client # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): """Manage application lifecycle""" logger.info("Starting Mulita application...") # Initialize database await init_db() # Pooled httpx client to Nextcloud — keepalive + HTTP/2 means every # thumbnail / Memories-info call after the first reuses one socket # instead of paying TCP+TLS handshake per request. await init_preview_client() # First-boot convenience: if there are no source roots in the DB yet, # create one for the default /photos mount so the user sees their # library immediately without configuring anything in the UI. try: await bootstrap_default_source_root() except Exception as e: logger.error(f"Bootstrap source root failed (continuing): {e}") # One-shot cleanup of duplicate source_roots / folders left over from # earlier scanner versions that didn't normalize paths. Idempotent. try: await cleanup_data_integrity() except Exception as e: logger.error(f"Startup cleanup failed (continuing): {e}") # Start initial scan if configured if settings.scanner.initial_scan_on_start: logger.info("Starting initial library scan...") await start_initial_scan() yield logger.info("Shutting down Mulita application...") await close_preview_client() # Create FastAPI app app = FastAPI( title="Mulita Photo Management API", description="Self-hosted photo management application inspired by Lightroom", version="1.0.0", lifespan=lifespan ) # Configure CORS. The frontend normally talks to the backend through the # nginx (prod) or vite (dev) proxy, so requests are same-origin and never # trip CORS. ALLOWED_ORIGINS in .env controls the fallback for direct # browser access from other origins (LAN IP, reverse proxy under a # different host). Defaults to "*" since this is a single-user homelab # tool; lock it down by setting e.g. ALLOWED_ORIGINS=https://photos.your.tld # in production deployments. _origins = settings.cors_origins app.add_middleware( CORSMiddleware, allow_origins=_origins, # Wildcard origins can't be combined with credentials per the CORS # spec, so credentials get auto-disabled in that case. allow_credentials=_origins != ["*"], allow_methods=["*"], allow_headers=["*"], ) # Session middleware — only used by Authlib to hold PKCE state during # the OIDC round-trip. max_age is short because the cookie is only # meaningful between /auth/oidc/login and /auth/oidc/callback; the app # itself still runs on JWTs. app.add_middleware( SessionMiddleware, secret_key=settings.effective_session_secret, session_cookie="mulita_oidc", max_age=600, same_site="lax", https_only=False, ) # Mount static files for serving thumbnails (with X-Accel-Redirect support) if os.path.exists("/data/thumbs"): app.mount("/thumbs", StaticFiles(directory="/data/thumbs"), name="thumbs") # Include routers app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"]) app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"]) app.include_router(sharing.router, prefix="/api/v1", tags=["sharing"]) app.include_router(photos.router, prefix="/api/v1/photos", tags=["photos"]) app.include_router(folders.router, prefix="/api/v1/folders", tags=["folders"]) app.include_router(heaps.router, prefix="/api/v1/heaps", tags=["heaps"]) app.include_router(tags.router, prefix="/api/v1/tags", tags=["tags"]) app.include_router(discard.router, prefix="/api/v1/discard", tags=["discard"]) app.include_router(library.router, prefix="/api/v1/library", tags=["library"]) app.include_router(search.router, prefix="/api/v1/photos/search", tags=["search"]) app.include_router(download.router, prefix="/api/v1/download", tags=["download"]) app.include_router(nextcloud.router, prefix="/api/v1/nextcloud", tags=["nextcloud"]) app.include_router(nc_webhook.router, prefix="/api/v1/internal", tags=["nc-webhook"]) @app.get("/") async def root(): """Root endpoint""" return { "name": "Mulita Photo Management API", "version": "1.0.0", "status": "running" } @app.get("/health") async def health_check(): """Health check endpoint for Docker""" return {"status": "healthy"}