diff --git a/VERSION b/VERSION index 6c7a25b..33e3f83 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.8.8-movie-id-tuple-fix +2.8.9-multiprocess-population diff --git a/api/web_routes.py b/api/web_routes.py index 8c55fa8..5b3abda 100644 --- a/api/web_routes.py +++ b/api/web_routes.py @@ -3,6 +3,9 @@ Web interface API routes for NFOGuard database management Provides endpoints for the web-based database manipulation interface """ import json +import os +import tempfile +import multiprocessing from datetime import datetime, timezone from typing import List, Optional, Dict, Any from fastapi import HTTPException, Query, BackgroundTasks @@ -11,8 +14,11 @@ from pathlib import Path from api.models import * -# Global status tracking for database population -_populate_status = {"running": False, "completed": False} +# Status file for cross-process communication +POPULATE_STATUS_FILE = os.path.join(tempfile.gettempdir(), "nfoguard_populate_status.json") + +# Process tracking +_populate_process = None def map_source_to_description(source: str) -> str: @@ -1249,41 +1255,129 @@ async def get_episode_date_options(dependencies: dict, imdb_id: str, season: int } +def _populate_worker_process(media_type: str, status_file: str): + """ + Worker process that runs database population in complete isolation. + Runs in separate process - keeps web interface responsive. + """ + import os + import sys + import json + from datetime import datetime + + # Add parent directory to path for imports + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + def update_status(status_data): + """Write status to file for main process to read""" + try: + with open(status_file, 'w') as f: + json.dump(status_data, f) + except Exception as e: + print(f"ERROR: Failed to update status file: {e}") + + # Initialize status + status = { + "running": True, + "media_type": media_type, + "start_time": datetime.now().isoformat(), + "movies": {"status": "pending", "stats": None}, + "tv": {"status": "pending", "stats": None}, + "completed": False, + "error": None + } + update_status(status) + + try: + # Import here (inside process) to avoid pickling issues + from core.database import NFOGuardDatabase + from core.database_populator import DatabasePopulator + from clients.radarr_client import RadarrClient + from clients.sonarr_client import SonarrClient + from config.settings import config + + # Initialize components + db = NFOGuardDatabase(config) + radarr_client = RadarrClient( + os.environ.get("RADARR_URL", ""), + os.environ.get("RADARR_API_KEY", "") + ) + sonarr_client = SonarrClient( + os.environ.get("SONARR_URL", ""), + os.environ.get("SONARR_API_KEY", "") + ) + + populator = DatabasePopulator(db, radarr_client, sonarr_client) + + print(f"INFO: [Worker Process] Starting database population: {media_type}") + + # Run population based on media type + if media_type in ["movies", "both"]: + status["movies"]["status"] = "running" + update_status(status) + + movie_stats = populator.populate_movies() + + status["movies"]["status"] = "completed" + status["movies"]["stats"] = movie_stats + update_status(status) + print(f"INFO: [Worker Process] Movie population completed: {movie_stats}") + + if media_type in ["tv", "both"]: + status["tv"]["status"] = "running" + update_status(status) + + tv_stats = populator.populate_tv_episodes() + + status["tv"]["status"] = "completed" + status["tv"]["stats"] = tv_stats + update_status(status) + print(f"INFO: [Worker Process] TV population completed: {tv_stats}") + + # Mark as completed + status["completed"] = True + status["running"] = False + update_status(status) + print("INFO: [Worker Process] Database population completed successfully") + + except Exception as e: + print(f"ERROR: [Worker Process] Database population failed: {e}") + import traceback + traceback.print_exc() + + status["error"] = str(e) + status["running"] = False + status["completed"] = True + update_status(status) + + async def populate_database(background_tasks: BackgroundTasks, media_type: str = "both", dependencies: dict = None): """ - Populate NFOGuard database from Radarr/Sonarr sources + Populate NFOGuard database from Radarr/Sonarr sources in separate process. + This keeps the web interface responsive during population. Args: - background_tasks: FastAPI background tasks + background_tasks: FastAPI background tasks (not used - kept for compatibility) media_type: Type of media to populate ("movies", "tv", or "both") - dependencies: Dictionary with db, radarr_client, sonarr_client + dependencies: Dictionary with dependencies (not used in multiprocessing mode) Returns: Status message indicating population has started """ - from core.database_populator import DatabasePopulator - import os - - db = dependencies["db"] - - # Get Radarr and Sonarr clients - from clients.radarr_client import RadarrClient - from clients.sonarr_client import SonarrClient - - radarr_client = RadarrClient( - os.environ.get("RADARR_URL", ""), - os.environ.get("RADARR_API_KEY", "") - ) - sonarr_client = SonarrClient( - os.environ.get("SONARR_URL", ""), - os.environ.get("SONARR_API_KEY", "") - ) + global _populate_process if media_type not in ["both", "movies", "tv"]: raise HTTPException(status_code=400, detail="media_type must be 'both', 'movies', or 'tv'") - # Create global status tracking - populate_status = { + # Check if process is already running + if _populate_process and _populate_process.is_alive(): + return { + "status": "already_running", + "message": "Database population is already in progress" + } + + # Initialize status file + initial_status = { "running": True, "media_type": media_type, "start_time": datetime.now().isoformat(), @@ -1293,71 +1387,61 @@ async def populate_database(background_tasks: BackgroundTasks, media_type: str = "error": None } - # Store status globally so it can be queried - global _populate_status - _populate_status = populate_status + try: + with open(POPULATE_STATUS_FILE, 'w') as f: + json.dump(initial_status, f) + except Exception as e: + print(f"ERROR: Failed to initialize status file: {e}") + raise HTTPException(status_code=500, detail=f"Failed to initialize status tracking: {e}") - async def run_population(): - """Background task to populate the database""" - try: - populator = DatabasePopulator(db, radarr_client, sonarr_client) + # Start population in separate process + _populate_process = multiprocessing.Process( + target=_populate_worker_process, + args=(media_type, POPULATE_STATUS_FILE), + daemon=False # Keep process alive even if parent exits + ) + _populate_process.start() - print(f"INFO: Starting database population: {media_type}") - - if media_type == "movies": - populate_status["movies"]["status"] = "running" - movie_stats = populator.populate_movies() - populate_status["movies"]["status"] = "completed" - populate_status["movies"]["stats"] = movie_stats - print(f"INFO: Movie population completed: {movie_stats}") - - elif media_type == "tv": - populate_status["tv"]["status"] = "running" - tv_stats = populator.populate_tv_episodes() - populate_status["tv"]["status"] = "completed" - populate_status["tv"]["stats"] = tv_stats - print(f"INFO: TV population completed: {tv_stats}") - - elif media_type == "both": - populate_status["movies"]["status"] = "running" - movie_stats = populator.populate_movies() - populate_status["movies"]["status"] = "completed" - populate_status["movies"]["stats"] = movie_stats - print(f"INFO: Movie population completed: {movie_stats}") - - populate_status["tv"]["status"] = "running" - tv_stats = populator.populate_tv_episodes() - populate_status["tv"]["status"] = "completed" - populate_status["tv"]["stats"] = tv_stats - print(f"INFO: TV population completed: {tv_stats}") - - populate_status["completed"] = True - populate_status["running"] = False - print("INFO: Database population completed successfully") - - except Exception as e: - print(f"ERROR: Database population failed: {e}") - populate_status["error"] = str(e) - populate_status["running"] = False - populate_status["completed"] = True - - # Add task to background - background_tasks.add_task(run_population) - - print(f"INFO: Database population started for: {media_type}") + print(f"INFO: Database population process started (PID: {_populate_process.pid}) for: {media_type}") return { "status": "started", "media_type": media_type, - "message": f"Database population started for {media_type}" + "message": f"Database population started for {media_type} in separate process (PID: {_populate_process.pid})" } async def get_populate_status(): - """Get the current status of database population""" - global _populate_status - if '_populate_status' not in globals(): + """Get the current status of database population from status file""" + global _populate_process + + # Check if status file exists + if not os.path.exists(POPULATE_STATUS_FILE): return {"running": False, "completed": False} - return _populate_status + + # Read status from file + try: + with open(POPULATE_STATUS_FILE, 'r') as f: + status = json.load(f) + + # Check if process is still alive + if _populate_process: + status["process_alive"] = _populate_process.is_alive() + if not _populate_process.is_alive() and status.get("running"): + # Process died unexpectedly + status["running"] = False + status["completed"] = True + if not status.get("error"): + status["error"] = "Process terminated unexpectedly" + + return status + + except Exception as e: + print(f"ERROR: Failed to read status file: {e}") + return { + "running": False, + "completed": True, + "error": f"Failed to read status: {e}" + } def register_web_routes(app, dependencies): diff --git a/nfoguard-web/main_web.py b/nfoguard-web/main_web.py index a8f1069..278e77f 100644 --- a/nfoguard-web/main_web.py +++ b/nfoguard-web/main_web.py @@ -33,7 +33,7 @@ def create_web_app() -> FastAPI: app = FastAPI( title="NFOGuard Web Interface", description="Web interface for NFOGuard media database management", - version="2.8.8-movie-id-tuple-fix", + version="2.8.9-multiprocess-population", docs_url="/docs" if web_config.web_debug else None, redoc_url="/redoc" if web_config.web_debug else None ) diff --git a/start_web.py b/start_web.py index 4479e98..ee400a2 100644 --- a/start_web.py +++ b/start_web.py @@ -29,7 +29,7 @@ def create_web_app() -> FastAPI: app = FastAPI( title="NFOGuard Web Interface", description="Web interface for NFOGuard media database management", - version="2.8.8-movie-id-tuple-fix", + version="2.8.9-multiprocess-population", docs_url=None, # Disable docs in production redoc_url=None ) @@ -94,7 +94,7 @@ def setup_static_files(app: FastAPI) -> None: "status": "healthy", "service": "nfoguard-web", "timestamp": time.time(), - "version": "2.8.8-movie-id-tuple-fix" + "version": "2.8.9-multiprocess-population" } except Exception as e: from fastapi import HTTPException