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
+154 -70
View File
@@ -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,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
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
Worker process that runs database population in complete isolation.
Runs in separate process - keeps web interface responsive.
"""
from core.database_populator import DatabasePopulator
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.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", "")
@@ -1279,11 +1307,77 @@ async def populate_database(background_tasks: BackgroundTasks, media_type: str =
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"]:
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
async def run_population():
"""Background task to populate the database"""
try:
populator = DatabasePopulator(db, radarr_client, sonarr_client)
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")
with open(POPULATE_STATUS_FILE, 'w') as f:
json.dump(initial_status, f)
except Exception as e:
print(f"ERROR: Database population failed: {e}")
populate_status["error"] = str(e)
populate_status["running"] = False
populate_status["completed"] = True
print(f"ERROR: Failed to initialize status file: {e}")
raise HTTPException(status_code=500, detail=f"Failed to initialize status tracking: {e}")
# Add task to background
background_tasks.add_task(run_population)
# 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: 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):
+1 -1
View File
@@ -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
)
+2 -2
View File
@@ -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