update: webinterface updates
Local Docker Build (Dev) / build-dev (push) Successful in 5s

This commit is contained in:
2025-11-03 14:31:39 -05:00
parent 65896f1974
commit 04b2afdb19
4 changed files with 167 additions and 83 deletions
+1 -1
View File
@@ -1 +1 @@
2.8.8-movie-id-tuple-fix 2.8.9-multiprocess-population
+163 -79
View File
@@ -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,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): 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: 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") 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: Returns:
Status message indicating population has started Status message indicating population has started
""" """
from core.database_populator import DatabasePopulator global _populate_process
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", "")
)
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 try:
global _populate_status with open(POPULATE_STATUS_FILE, 'w') as f:
_populate_status = populate_status 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(): # Start population in separate process
"""Background task to populate the database""" _populate_process = multiprocessing.Process(
try: target=_populate_worker_process,
populator = DatabasePopulator(db, radarr_client, sonarr_client) 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}") print(f"INFO: Database population process started (PID: {_populate_process.pid}) for: {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}")
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):
+1 -1
View File
@@ -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
View File
@@ -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