diff --git a/backend/server.py b/backend/server.py index c7e3118..cedbef6 100644 --- a/backend/server.py +++ b/backend/server.py @@ -1798,37 +1798,64 @@ async def opensubs_download(payload: dict, user: dict = Depends(require_admin)): # ============ BULK TMDB ENRICHMENT ============ +_movie_enrich_status = {"running": False, "total": 0, "done": 0, "enriched": 0, "skipped": 0, "failed": 0, "error": ""} + + +async def _run_movie_enrich(key: str): + _movie_enrich_status.update(running=True, total=0, done=0, enriched=0, skipped=0, failed=0, error="") + try: + movies = await db.movies.find({}, {"_id": 0}).to_list(20000) + _movie_enrich_status["total"] = len(movies) + for m in movies: + needs = not m.get("cast") or not m.get("director") or not m.get("description") or not m.get("poster_url") + if not needs: + _movie_enrich_status["skipped"] += 1 + _movie_enrich_status["done"] += 1 + continue + try: + if m.get("tmdb_id"): + data = await asyncio.to_thread(tmdb_client.get_movie, key, m["tmdb_id"]) + else: + results = await asyncio.to_thread(tmdb_client.search_movies, key, m["title"]) + if not results: + _movie_enrich_status["failed"] += 1 + _movie_enrich_status["done"] += 1 + continue + data = await asyncio.to_thread(tmdb_client.get_movie, key, results[0]["tmdb_id"]) + updates = {} + for field in ("description", "poster_url", "backdrop_url", "director", "duration_minutes", "rating"): + if data.get(field) and not m.get(field): updates[field] = data[field] + if data.get("cast") and not m.get("cast"): updates["cast"] = data["cast"] + if data.get("genres") and not m.get("genres"): updates["genres"] = data["genres"] + if data.get("tmdb_id"): updates["tmdb_id"] = data["tmdb_id"] + if updates: + await db.movies.update_one({"id": m["id"]}, {"$set": updates}) + _movie_enrich_status["enriched"] += 1 + except Exception as e: + logger.warning(f"Enrich failed for {m.get('title')}: {e}") + _movie_enrich_status["failed"] += 1 + _movie_enrich_status["done"] += 1 + except Exception as e: + _movie_enrich_status["error"] = str(e) + finally: + _movie_enrich_status["running"] = False + + @api.post("/tmdb/enrich-all") async def tmdb_enrich_all(user: dict = Depends(require_admin)): - """Sweep movies with missing metadata and fill from TMDB (by title match).""" + """Kicks off a background sweep of movies with missing metadata, filled from TMDB (by title match).""" + if _movie_enrich_status["running"]: + raise HTTPException(status_code=409, detail="Enrichment already running") s = await get_settings() key = s.get("tmdb_api_key", "") if not key: raise HTTPException(status_code=400, detail="TMDB not configured") - enriched = 0; skipped = 0; failed = 0 - async for m in db.movies.find({}, {"_id": 0}): - needs = not m.get("cast") or not m.get("director") or not m.get("description") or not m.get("poster_url") - if not needs: - skipped += 1; continue - try: - if m.get("tmdb_id"): - data = await asyncio.to_thread(tmdb_client.get_movie, key, m["tmdb_id"]) - else: - results = await asyncio.to_thread(tmdb_client.search_movies, key, m["title"]) - if not results: failed += 1; continue - data = await asyncio.to_thread(tmdb_client.get_movie, key, results[0]["tmdb_id"]) - updates = {} - for field in ("description", "poster_url", "backdrop_url", "director", "duration_minutes", "rating"): - if data.get(field) and not m.get(field): updates[field] = data[field] - if data.get("cast") and not m.get("cast"): updates["cast"] = data["cast"] - if data.get("genres") and not m.get("genres"): updates["genres"] = data["genres"] - if data.get("tmdb_id"): updates["tmdb_id"] = data["tmdb_id"] - if updates: - await db.movies.update_one({"id": m["id"]}, {"$set": updates}) - enriched += 1 - except Exception as e: - logger.warning(f"Enrich failed for {m.get('title')}: {e}") - failed += 1 - return {"ok": True, "enriched": enriched, "skipped": skipped, "failed": failed} + asyncio.create_task(_run_movie_enrich(key)) + return {"ok": True} + + +@api.get("/tmdb/enrich-all/status") +async def tmdb_enrich_all_status(user: dict = Depends(require_admin)): + return _movie_enrich_status async def _auto_fetch_subtitle(content_type: str, content_id: str, tmdb_id: Optional[int], diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 3e4751f..9014da5 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -57,6 +57,7 @@ export default function Settings() { const [pw, setPw] = useState({ current_password: "", new_password: "", confirm: "" }); const [pwShow, setPwShow] = useState(false); const [pwSaving, setPwSaving] = useState(false); + const [enrich, setEnrich] = useState(null); const load = async () => { if (!isAdmin) return; @@ -119,10 +120,32 @@ export default function Settings() { const enrichAll = async () => { if (!window.confirm("Sweep all movies with missing metadata and fill from TMDB? This may take a while.")) return; - try { const { data } = await api.post("/tmdb/enrich-all"); toast.success(`Enriched ${data.enriched}, skipped ${data.skipped}, failed ${data.failed}`); } - catch (err) { toast.error(err.response?.data?.detail || "Enrichment failed"); } + try { + await api.post("/tmdb/enrich-all"); + const { data } = await api.get("/tmdb/enrich-all/status"); + setEnrich(data); + toast.success("Enrichment started"); + } catch (err) { toast.error(err.response?.data?.detail || "Enrichment failed"); } }; + useEffect(() => { + if (!isAdmin) return; + api.get("/tmdb/enrich-all/status").then(({ data }) => setEnrich(data)).catch(() => {}); + }, [isAdmin]); + + useEffect(() => { + if (!enrich?.running) return; + const t = setInterval(async () => { + const { data } = await api.get("/tmdb/enrich-all/status"); + setEnrich(data); + if (!data.running) { + clearInterval(t); + toast.success(`Enrichment done — ${data.enriched} enriched, ${data.skipped} already complete, ${data.failed} not found`); + } + }, 2000); + return () => clearInterval(t); + }, [enrich?.running]); + return (
@@ -183,9 +206,16 @@ export default function Settings() {

Free key at themoviedb.org

{info.tmdb_configured && ( - +
+ + {enrich && !enrich.running && enrich.total > 0 && ( + + Last run: {enrich.enriched} enriched · {enrich.skipped} already complete · {enrich.failed} not found on TMDB + + )} +
)}