This commit is contained in:
+154
-70
@@ -3,6 +3,9 @@ Web interface API routes for NFOGuard database management
|
|||||||
Provides endpoints for the web-based database manipulation interface
|
Provides endpoints for the web-based database manipulation interface
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import multiprocessing
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
from fastapi import HTTPException, Query, BackgroundTasks
|
from fastapi import HTTPException, Query, BackgroundTasks
|
||||||
@@ -11,8 +14,11 @@ from pathlib import Path
|
|||||||
from api.models import *
|
from api.models import *
|
||||||
|
|
||||||
|
|
||||||
# Global status tracking for database population
|
# Status file for cross-process communication
|
||||||
_populate_status = {"running": False, "completed": False}
|
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:
|
def map_source_to_description(source: str) -> str:
|
||||||
@@ -1249,27 +1255,49 @@ async def get_episode_date_options(dependencies: dict, imdb_id: str, season: int
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def populate_database(background_tasks: BackgroundTasks, media_type: str = "both", dependencies: dict = None):
|
def _populate_worker_process(media_type: str, status_file: str):
|
||||||
"""
|
"""
|
||||||
Populate NFOGuard database from Radarr/Sonarr sources
|
Worker process that runs database population in complete isolation.
|
||||||
|
Runs in separate process - keeps web interface responsive.
|
||||||
Args:
|
|
||||||
background_tasks: FastAPI background tasks
|
|
||||||
media_type: Type of media to populate ("movies", "tv", or "both")
|
|
||||||
dependencies: Dictionary with db, radarr_client, sonarr_client
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Status message indicating population has started
|
|
||||||
"""
|
"""
|
||||||
from core.database_populator import DatabasePopulator
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
db = dependencies["db"]
|
# Add parent directory to path for imports
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
# Get Radarr and Sonarr clients
|
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.radarr_client import RadarrClient
|
||||||
from clients.sonarr_client import SonarrClient
|
from clients.sonarr_client import SonarrClient
|
||||||
|
from config.settings import config
|
||||||
|
|
||||||
|
# Initialize components
|
||||||
|
db = NFOGuardDatabase(config)
|
||||||
radarr_client = RadarrClient(
|
radarr_client = RadarrClient(
|
||||||
os.environ.get("RADARR_URL", ""),
|
os.environ.get("RADARR_URL", ""),
|
||||||
os.environ.get("RADARR_API_KEY", "")
|
os.environ.get("RADARR_API_KEY", "")
|
||||||
@@ -1279,11 +1307,77 @@ async def populate_database(background_tasks: BackgroundTasks, media_type: str =
|
|||||||
os.environ.get("SONARR_API_KEY", "")
|
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 in separate process.
|
||||||
|
This keeps the web interface responsive during population.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
background_tasks: FastAPI background tasks (not used - kept for compatibility)
|
||||||
|
media_type: Type of media to populate ("movies", "tv", or "both")
|
||||||
|
dependencies: Dictionary with dependencies (not used in multiprocessing mode)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Status message indicating population has started
|
||||||
|
"""
|
||||||
|
global _populate_process
|
||||||
|
|
||||||
if media_type not in ["both", "movies", "tv"]:
|
if media_type not in ["both", "movies", "tv"]:
|
||||||
raise HTTPException(status_code=400, detail="media_type must be 'both', 'movies', or 'tv'")
|
raise HTTPException(status_code=400, detail="media_type must be 'both', 'movies', or 'tv'")
|
||||||
|
|
||||||
# Create global status tracking
|
# Check if process is already running
|
||||||
populate_status = {
|
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,
|
"running": True,
|
||||||
"media_type": media_type,
|
"media_type": media_type,
|
||||||
"start_time": datetime.now().isoformat(),
|
"start_time": datetime.now().isoformat(),
|
||||||
@@ -1293,71 +1387,61 @@ async def populate_database(background_tasks: BackgroundTasks, media_type: str =
|
|||||||
"error": None
|
"error": None
|
||||||
}
|
}
|
||||||
|
|
||||||
# Store status globally so it can be queried
|
|
||||||
global _populate_status
|
|
||||||
_populate_status = populate_status
|
|
||||||
|
|
||||||
async def run_population():
|
|
||||||
"""Background task to populate the database"""
|
|
||||||
try:
|
try:
|
||||||
populator = DatabasePopulator(db, radarr_client, sonarr_client)
|
with open(POPULATE_STATUS_FILE, 'w') as f:
|
||||||
|
json.dump(initial_status, f)
|
||||||
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:
|
except Exception as e:
|
||||||
print(f"ERROR: Database population failed: {e}")
|
print(f"ERROR: Failed to initialize status file: {e}")
|
||||||
populate_status["error"] = str(e)
|
raise HTTPException(status_code=500, detail=f"Failed to initialize status tracking: {e}")
|
||||||
populate_status["running"] = False
|
|
||||||
populate_status["completed"] = True
|
|
||||||
|
|
||||||
# Add task to background
|
# Start population in separate process
|
||||||
background_tasks.add_task(run_population)
|
_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: Database population started for: {media_type}")
|
print(f"INFO: Database population process started (PID: {_populate_process.pid}) for: {media_type}")
|
||||||
return {
|
return {
|
||||||
"status": "started",
|
"status": "started",
|
||||||
"media_type": media_type,
|
"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():
|
async def get_populate_status():
|
||||||
"""Get the current status of database population"""
|
"""Get the current status of database population from status file"""
|
||||||
global _populate_status
|
global _populate_process
|
||||||
if '_populate_status' not in globals():
|
|
||||||
|
# Check if status file exists
|
||||||
|
if not os.path.exists(POPULATE_STATUS_FILE):
|
||||||
return {"running": False, "completed": False}
|
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):
|
def register_web_routes(app, dependencies):
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def create_web_app() -> FastAPI:
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="NFOGuard Web Interface",
|
title="NFOGuard Web Interface",
|
||||||
description="Web interface for NFOGuard media database management",
|
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,
|
docs_url="/docs" if web_config.web_debug else None,
|
||||||
redoc_url="/redoc" if web_config.web_debug else None
|
redoc_url="/redoc" if web_config.web_debug else None
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -29,7 +29,7 @@ def create_web_app() -> FastAPI:
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="NFOGuard Web Interface",
|
title="NFOGuard Web Interface",
|
||||||
description="Web interface for NFOGuard media database management",
|
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
|
docs_url=None, # Disable docs in production
|
||||||
redoc_url=None
|
redoc_url=None
|
||||||
)
|
)
|
||||||
@@ -94,7 +94,7 @@ def setup_static_files(app: FastAPI) -> None:
|
|||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"service": "nfoguard-web",
|
"service": "nfoguard-web",
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
"version": "2.8.8-movie-id-tuple-fix"
|
"version": "2.8.9-multiprocess-population"
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|||||||
Reference in New Issue
Block a user