Initial commit — Movie Night media discovery app

AI-powered web app that recommends unwatched movies from a Jellyfin
library based on natural language mood input. Jellyfin auth, modular
LLM backend (Claude/OpenAI/Ollama), two-tier pre-filter + AI ranking,
mobile-responsive dark theme UI with poster cards and deep links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 19:20:56 -07:00
commit 3d5de06b44
30 changed files with 1881 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
import asyncio
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import Response as FastAPIResponse
from fastapi.staticfiles import StaticFiles
from app.database import init_database
from app.routers import auth, library, users
from app.services.jellyfin import get_poster
from app.services.library_sync import run_periodic_sync
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_database()
sync_task = asyncio.create_task(run_periodic_sync())
yield
sync_task.cancel()
try:
await sync_task
except asyncio.CancelledError:
pass
app = FastAPI(title="Movie Night", lifespan=lifespan)
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(library.router, prefix="/api/library", tags=["library"])
app.include_router(users.router, prefix="/api", tags=["users"])
# Mood router imported lazily to avoid circular imports during early phases
try:
from app.routers import mood
app.include_router(mood.router, prefix="/api", tags=["mood"])
except ImportError:
pass
@app.get("/api/poster/{item_id}")
async def poster_proxy(item_id: str, request: Request):
from app.routers.auth import get_current_user
await get_current_user(request)
image_data = await get_poster(item_id)
if image_data is None:
return FastAPIResponse(status_code=404)
return FastAPIResponse(
content=image_data,
media_type="image/jpeg",
headers={"Cache-Control": "public, max-age=86400"},
)
# Static files must be mounted last (catches all non-API routes)
app.mount("/", StaticFiles(directory="app/static", html=True), name="static")