From 68baa1766cc6a68f9825d5a3756df2add7a19814 Mon Sep 17 00:00:00 2001 From: SBCrumb Date: Wed, 22 Oct 2025 20:27:30 -0400 Subject: [PATCH] web: status dash updates --- VERSION | 2 +- api/routes.py | 224 +++++++++++++++++++++++++++++----- api/web_routes.py | 47 +++---- nfoguard-web/static/js/app.js | 44 ++++++- 4 files changed, 252 insertions(+), 65 deletions(-) diff --git a/VERSION b/VERSION index 860487c..37c2961 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.1 +2.7.2 diff --git a/api/routes.py b/api/routes.py index 95830ba..4625640 100644 --- a/api/routes.py +++ b/api/routes.py @@ -17,6 +17,23 @@ from api.models import ( ) # Web routes removed - handled by separate web container +# Global scan status tracking for detailed progress +scan_status = { + "scanning": False, + "scan_type": None, + "scan_mode": None, + "start_time": None, + "current_operation": None, + "tv_series_processed": 0, + "tv_series_total": 0, + "tv_series_skipped": 0, + "movies_processed": 0, + "movies_total": 0, + "movies_skipped": 0, + "current_item": None, + "last_update": None +} + # --------------------------- # Helper Functions @@ -565,6 +582,9 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N print(f"🚀 MANUAL SCAN STARTED: {scan_type} scan (mode: {scan_mode}) initiated at {local_start.strftime('%Y-%m-%d %H:%M:%S')}{tz_display}") + # Initialize scan tracking + start_scan_tracking(scan_type, scan_mode) + # Initialize counters for scan statistics tv_series_total = 0 tv_series_skipped = 0 @@ -625,57 +645,81 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N else: # Full series processing - scan subdirectories import re - tv_count = 0 + + # Count total series first for progress tracking + tv_series_list = [] for item in scan_path.iterdir(): + if (item.is_dir() and + not item.name.lower().startswith('season') and + not re.match(r'^season\s+\d+$', item.name, re.IGNORECASE) and + nfo_manager.parse_imdb_from_path(item)): + tv_series_list.append(item) + + tv_series_count = len(tv_series_list) + update_scan_status("tv", tv_series_total=tv_series_count) + print(f"INFO: Found {tv_series_count} TV series to process") + + tv_count = 0 + for item in tv_series_list: # Check for shutdown signal at start of each item shutdown_event = dependencies.get("shutdown_event") if shutdown_event and shutdown_event.is_set(): print("INFO: ⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping scan gracefully") return - if (item.is_dir() and - not item.name.lower().startswith('season') and - not re.match(r'^season\s+\d+$', item.name, re.IGNORECASE) and - nfo_manager.parse_imdb_from_path(item)): - tv_count += 1 - try: - # Determine force_scan based on scan mode - force_scan = (scan_mode == "full") - result = tv_processor.process_series(item, force_scan=force_scan) - tv_series_total += 1 - if result == "skipped": - tv_series_skipped += 1 - elif result == "processed": - tv_series_processed += 1 - except Exception as e: - print(f"ERROR: Failed processing TV series {item}: {e}") - tv_series_total += 1 + tv_count += 1 + update_scan_status(current_item=item.name, tv_series_processed=tv_count) + + try: + # Determine force_scan based on scan mode + force_scan = (scan_mode == "full") + result = tv_processor.process_series(item, force_scan=force_scan) + tv_series_total += 1 + if result == "skipped": + tv_series_skipped += 1 + elif result == "processed": + tv_series_processed += 1 + except Exception as e: + print(f"ERROR: Failed processing TV series {item}: {e}") + tv_series_total += 1 + + # Yield control every TV series to allow other requests + if tv_count % 1 == 0: + await asyncio.sleep(0.2) # 200ms yield to process other requests + print(f"INFO: Processed {tv_count} TV series, yielding to other requests...") - # Yield control every TV series to allow other requests - if tv_count % 1 == 0: - await asyncio.sleep(0.2) # 200ms yield to process other requests - print(f"INFO: Processed {tv_count} TV series, yielding to other requests...") - - # Check for shutdown signal - shutdown_event = dependencies.get("shutdown_event") - if shutdown_event and shutdown_event.is_set(): - print("INFO: ⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping scan gracefully") - return + # Check for shutdown signal + shutdown_event = dependencies.get("shutdown_event") + if shutdown_event and shutdown_event.is_set(): + print("INFO: ⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping scan gracefully") + return if scan_type in ["both", "movies"] and scan_path in config.movie_paths: print(f"INFO: Scanning movies in: {scan_path}") - movie_count = 0 + update_scan_status("movies", current_item="Counting movies...") + + # Count total movies first for progress tracking + movie_list = [] for item in scan_path.iterdir(): + if item.is_dir() and nfo_manager.find_movie_imdb_id(item): + movie_list.append(item) + + movie_total_count = len(movie_list) + update_scan_status(movies_total=movie_total_count) + print(f"INFO: Found {movie_total_count} movies to process") + + movie_count = 0 + for item in movie_list: # Check for shutdown signal at start of each movie shutdown_event = dependencies.get("shutdown_event") if shutdown_event and shutdown_event.is_set(): print("INFO: ⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping scan gracefully") return - if item.is_dir() and nfo_manager.find_movie_imdb_id(item): - movie_count += 1 - print(f"INFO: Processing movie: {item.name}") - try: + movie_count += 1 + update_scan_status(current_item=item.name, movies_processed=movie_count) + print(f"INFO: Processing movie: {item.name}") + try: # Determine force_scan based on scan mode force_scan = (scan_mode == "full") shutdown_event = dependencies.get("shutdown_event") @@ -733,6 +777,9 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N print(f"✅ MANUAL SCAN COMPLETED: {scan_type} scan (mode: {scan_mode}) finished at {local_end.strftime('%Y-%m-%d %H:%M:%S')}{tz_display}") print(f"⏱️ MANUAL SCAN DURATION: {duration_str} (total time: {duration.total_seconds():.1f} seconds)") + # Stop scan tracking + stop_scan_tracking() + # Print optimization statistics for TV scans if scan_type in ["both", "tv"] and tv_series_total > 0: print(f"📊 TV SCAN OPTIMIZATION: Total: {tv_series_total}, Processed: {tv_series_processed}, Skipped: {tv_series_skipped}") @@ -1732,6 +1779,113 @@ async def backfill_movie_release_dates(dependencies: dict): } +# --------------------------- +# Scan Status Functions +# --------------------------- + +async def get_scan_status(): + """Get detailed scan status with progress information""" + global scan_status + + if not scan_status["scanning"]: + return {"scanning": False, "message": "No active scan"} + + # Calculate elapsed time + from datetime import datetime + if scan_status["start_time"]: + elapsed_seconds = int((datetime.now() - scan_status["start_time"]).total_seconds()) + if elapsed_seconds >= 60: + minutes = elapsed_seconds // 60 + seconds = elapsed_seconds % 60 + elapsed_str = f"{minutes}m {seconds}s" + else: + elapsed_str = f"{elapsed_seconds}s" + else: + elapsed_str = "unknown" + + # Build detailed status message + if scan_status["current_operation"] == "tv": + if scan_status["tv_series_total"] > 0: + message = f"Processing TV series ({scan_status['tv_series_processed']}/{scan_status['tv_series_total']}) - {elapsed_str} elapsed" + else: + message = f"Processed {scan_status['tv_series_processed']} TV series - {elapsed_str} elapsed" + elif scan_status["current_operation"] == "movies": + if scan_status["movies_total"] > 0: + message = f"Processing movies ({scan_status['movies_processed']}/{scan_status['movies_total']}) - {elapsed_str} elapsed" + else: + message = f"Processed {scan_status['movies_processed']} movies - {elapsed_str} elapsed" + else: + message = f"Scan in progress - {elapsed_str} elapsed" + + # Add current item if available + if scan_status["current_item"]: + message += f" | Current: {scan_status['current_item']}" + + return { + "scanning": True, + "message": message, + "scan_type": scan_status["scan_type"], + "scan_mode": scan_status["scan_mode"], + "elapsed_seconds": elapsed_seconds if scan_status["start_time"] else 0, + "current_operation": scan_status["current_operation"], + "tv_series_processed": scan_status["tv_series_processed"], + "tv_series_total": scan_status["tv_series_total"], + "tv_series_skipped": scan_status["tv_series_skipped"], + "movies_processed": scan_status["movies_processed"], + "movies_total": scan_status["movies_total"], + "movies_skipped": scan_status["movies_skipped"], + "current_item": scan_status["current_item"] + } + +def update_scan_status(operation=None, current_item=None, **kwargs): + """Update scan status with new progress information""" + global scan_status + + if operation: + scan_status["current_operation"] = operation + if current_item: + scan_status["current_item"] = current_item + + # Update counters + for key, value in kwargs.items(): + if key in scan_status: + scan_status[key] = value + + scan_status["last_update"] = datetime.now() + +def start_scan_tracking(scan_type, scan_mode): + """Initialize scan tracking""" + global scan_status + + scan_status.update({ + "scanning": True, + "scan_type": scan_type, + "scan_mode": scan_mode, + "start_time": datetime.now(), + "current_operation": None, + "tv_series_processed": 0, + "tv_series_total": 0, + "tv_series_skipped": 0, + "movies_processed": 0, + "movies_total": 0, + "movies_skipped": 0, + "current_item": None, + "last_update": datetime.now() + }) + +def stop_scan_tracking(): + """Stop scan tracking""" + global scan_status + + scan_status.update({ + "scanning": False, + "scan_type": None, + "scan_mode": None, + "start_time": None, + "current_operation": None, + "current_item": None + }) + # --------------------------- # Route Registration # --------------------------- @@ -1822,6 +1976,10 @@ def register_routes(app, dependencies: dict): async def _manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = None, scan_type: str = "both", scan_mode: str = "smart"): return await manual_scan(background_tasks, path, scan_type, scan_mode, dependencies) + @app.get("/api/scan/status") + async def _scan_status(): + return await get_scan_status() + @app.post("/tv/scan-season") async def _scan_tv_season(background_tasks: BackgroundTasks, request: TVSeasonRequest): return await scan_tv_season(background_tasks, request, dependencies) diff --git a/api/web_routes.py b/api/web_routes.py index d61fb60..81d3732 100644 --- a/api/web_routes.py +++ b/api/web_routes.py @@ -1375,48 +1375,35 @@ def register_web_routes(app, dependencies): @app.get("/api/scan/status") async def api_scan_status(): - """Check scan status using simple time-based tracking""" + """Proxy scan status requests to core container for detailed progress""" import urllib.request import urllib.error import json import os import socket - from datetime import datetime, timedelta - # Check if we have a recent scan - if scan_tracking["last_scan_time"]: - time_since_scan = datetime.now() - scan_tracking["last_scan_time"] - - # Assume scan is active for up to 10 minutes after initiation - # This is a reasonable estimate for most scan operations - if time_since_scan < timedelta(minutes=10) and scan_tracking["scanning"]: - minutes_elapsed = int(time_since_scan.total_seconds() / 60) - seconds_elapsed = int(time_since_scan.total_seconds() % 60) - - if minutes_elapsed > 0: - elapsed_str = f"{minutes_elapsed}m {seconds_elapsed}s" - else: - elapsed_str = f"{seconds_elapsed}s" - - return { - "scanning": True, - "message": f"Scan in progress ({elapsed_str} elapsed) - check core logs for details" - } - - # Check if core container is responsive + # Get core container connection details core_host = os.environ.get("CORE_API_HOST", "nfoguard-core") core_port = os.environ.get("CORE_API_PORT", "8080") try: - health_url = f"http://{core_host}:{core_port}/health" - req = urllib.request.Request(health_url) + # Call core container's detailed scan status endpoint + status_url = f"http://{core_host}:{core_port}/api/scan/status" + req = urllib.request.Request(status_url) with urllib.request.urlopen(req, timeout=5) as response: - # Mark scanning as false if we reach here and enough time has passed - scan_tracking["scanning"] = False - return {"scanning": False, "message": "No active scan detected"} + status_data = json.loads(response.read().decode()) + return status_data + except urllib.error.HTTPError as e: + if e.code == 404: + # Core container doesn't have the endpoint, fallback to simple tracking + return {"scanning": False, "message": "Detailed status not available"} + else: + return {"scanning": False, "message": f"Core container error: {e.code}"} except (urllib.error.URLError, socket.timeout): return {"scanning": False, "message": "Core container unavailable"} - except Exception: - return {"scanning": False, "message": "Unable to check scan status"} \ No newline at end of file + except json.JSONDecodeError: + return {"scanning": False, "message": "Invalid response from core container"} + except Exception as e: + return {"scanning": False, "message": f"Unable to check scan status: {str(e)}"} \ No newline at end of file diff --git a/nfoguard-web/static/js/app.js b/nfoguard-web/static/js/app.js index b8d1de4..79ee839 100644 --- a/nfoguard-web/static/js/app.js +++ b/nfoguard-web/static/js/app.js @@ -1567,7 +1567,39 @@ async function checkScanStatus() { const status = await apiCall('/api/scan/status'); if (status.scanning) { - updateScanStatus(status.message || 'Scan in progress...', true); + // Build detailed status message from core container data + let message = status.message || 'Scan in progress...'; + + // Add progress details if available + if (status.current_operation === 'tv' && status.tv_series_total > 0) { + const progress = `${status.tv_series_processed}/${status.tv_series_total}`; + message = `Processing TV series (${progress})`; + if (status.current_item) { + message += ` | Current: ${status.current_item}`; + } + if (status.elapsed_seconds) { + const elapsed = formatElapsedTime(status.elapsed_seconds); + message += ` | ${elapsed} elapsed`; + } + } else if (status.current_operation === 'movies' && status.movies_total > 0) { + const progress = `${status.movies_processed}/${status.movies_total}`; + message = `Processing movies (${progress})`; + if (status.current_item) { + message += ` | Current: ${status.current_item}`; + } + if (status.elapsed_seconds) { + const elapsed = formatElapsedTime(status.elapsed_seconds); + message += ` | ${elapsed} elapsed`; + } + } else if (status.elapsed_seconds) { + const elapsed = formatElapsedTime(status.elapsed_seconds); + message = `Scan in progress | ${elapsed} elapsed`; + if (status.current_item) { + message += ` | Current: ${status.current_item}`; + } + } + + updateScanStatus(message, true); // Start polling if not already polling if (!scanStatusInterval) { @@ -1586,6 +1618,16 @@ async function checkScanStatus() { } } +function formatElapsedTime(seconds) { + if (seconds >= 60) { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}m ${remainingSeconds}s`; + } else { + return `${seconds}s`; + } +} + function startScanStatusPolling() { // Don't start if already polling if (scanStatusInterval) {