web: status dash updates

This commit is contained in:
2025-10-22 20:27:30 -04:00
committed by sbcrumb
parent 413c8ad6f6
commit 68baa1766c
4 changed files with 252 additions and 65 deletions
+1 -1
View File
@@ -1 +1 @@
2.7.1 2.7.2
+165 -7
View File
@@ -17,6 +17,23 @@ from api.models import (
) )
# Web routes removed - handled by separate web container # 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 # 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}") 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 # Initialize counters for scan statistics
tv_series_total = 0 tv_series_total = 0
tv_series_skipped = 0 tv_series_skipped = 0
@@ -625,19 +645,31 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
else: else:
# Full series processing - scan subdirectories # Full series processing - scan subdirectories
import re import re
tv_count = 0
# Count total series first for progress tracking
tv_series_list = []
for item in scan_path.iterdir(): 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 # Check for shutdown signal at start of each item
shutdown_event = dependencies.get("shutdown_event") shutdown_event = dependencies.get("shutdown_event")
if shutdown_event and shutdown_event.is_set(): if shutdown_event and shutdown_event.is_set():
print("INFO: ⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping scan gracefully") print("INFO: ⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping scan gracefully")
return 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 tv_count += 1
update_scan_status(current_item=item.name, tv_series_processed=tv_count)
try: try:
# Determine force_scan based on scan mode # Determine force_scan based on scan mode
force_scan = (scan_mode == "full") force_scan = (scan_mode == "full")
@@ -664,16 +696,28 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
if scan_type in ["both", "movies"] and scan_path in config.movie_paths: if scan_type in ["both", "movies"] and scan_path in config.movie_paths:
print(f"INFO: Scanning movies in: {scan_path}") 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(): 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 # Check for shutdown signal at start of each movie
shutdown_event = dependencies.get("shutdown_event") shutdown_event = dependencies.get("shutdown_event")
if shutdown_event and shutdown_event.is_set(): if shutdown_event and shutdown_event.is_set():
print("INFO: ⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping scan gracefully") print("INFO: ⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping scan gracefully")
return return
if item.is_dir() and nfo_manager.find_movie_imdb_id(item):
movie_count += 1 movie_count += 1
update_scan_status(current_item=item.name, movies_processed=movie_count)
print(f"INFO: Processing movie: {item.name}") print(f"INFO: Processing movie: {item.name}")
try: try:
# Determine force_scan based on scan mode # Determine force_scan based on scan mode
@@ -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 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)") 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 # Print optimization statistics for TV scans
if scan_type in ["both", "tv"] and tv_series_total > 0: 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}") 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 # 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"): 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) 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") @app.post("/tv/scan-season")
async def _scan_tv_season(background_tasks: BackgroundTasks, request: TVSeasonRequest): async def _scan_tv_season(background_tasks: BackgroundTasks, request: TVSeasonRequest):
return await scan_tv_season(background_tasks, request, dependencies) return await scan_tv_season(background_tasks, request, dependencies)
+17 -30
View File
@@ -1375,48 +1375,35 @@ def register_web_routes(app, dependencies):
@app.get("/api/scan/status") @app.get("/api/scan/status")
async def 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.request
import urllib.error import urllib.error
import json import json
import os import os
import socket import socket
from datetime import datetime, timedelta
# Check if we have a recent scan # Get core container connection details
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
core_host = os.environ.get("CORE_API_HOST", "nfoguard-core") core_host = os.environ.get("CORE_API_HOST", "nfoguard-core")
core_port = os.environ.get("CORE_API_PORT", "8080") core_port = os.environ.get("CORE_API_PORT", "8080")
try: try:
health_url = f"http://{core_host}:{core_port}/health" # Call core container's detailed scan status endpoint
req = urllib.request.Request(health_url) 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: with urllib.request.urlopen(req, timeout=5) as response:
# Mark scanning as false if we reach here and enough time has passed status_data = json.loads(response.read().decode())
scan_tracking["scanning"] = False return status_data
return {"scanning": False, "message": "No active scan detected"}
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): except (urllib.error.URLError, socket.timeout):
return {"scanning": False, "message": "Core container unavailable"} return {"scanning": False, "message": "Core container unavailable"}
except Exception: except json.JSONDecodeError:
return {"scanning": False, "message": "Unable to check scan status"} 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)}"}
+43 -1
View File
@@ -1567,7 +1567,39 @@ async function checkScanStatus() {
const status = await apiCall('/api/scan/status'); const status = await apiCall('/api/scan/status');
if (status.scanning) { 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 // Start polling if not already polling
if (!scanStatusInterval) { 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() { function startScanStatusPolling() {
// Don't start if already polling // Don't start if already polling
if (scanStatusInterval) { if (scanStatusInterval) {