Files
mule-image/backend/app/main.py
root 5c531f11da feat: runtime feature flags, upload/download, RAW decoding
Adds Redis-backed feature flags for vision stages with admin UI toggles
and manual backfill trigger, photo upload and download routers with
frontend upload modal, and rawpy-based RAW decoding with JPEG fallback
for misnamed DNGs. Fixes pgvector serialization, is_trashed filter, and
naive-datetime bind in incremental duplicate regrouping; bumps Celery
time limits on regroup tasks beyond the 5-minute default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:31:52 +02:00

114 lines
4.3 KiB
Python

"""
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
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, upload, download, features
from app.services.scanner import start_initial_scan, bootstrap_default_source_root
from app.services.cleanup import cleanup_data_integrity
# 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()
# 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...")
# 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=["*"],
)
# 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(upload.router, prefix="/api/v1/upload", tags=["upload"])
app.include_router(download.router, prefix="/api/v1/download", tags=["download"])
app.include_router(features.router, prefix="/api/v1/features", tags=["features"])
@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"}