web: status dash updates
This commit is contained in:
+191
-33
@@ -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,57 +645,81 @@ 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
|
tv_count += 1
|
||||||
not item.name.lower().startswith('season') and
|
update_scan_status(current_item=item.name, tv_series_processed=tv_count)
|
||||||
not re.match(r'^season\s+\d+$', item.name, re.IGNORECASE) and
|
|
||||||
nfo_manager.parse_imdb_from_path(item)):
|
try:
|
||||||
tv_count += 1
|
# Determine force_scan based on scan mode
|
||||||
try:
|
force_scan = (scan_mode == "full")
|
||||||
# Determine force_scan based on scan mode
|
result = tv_processor.process_series(item, force_scan=force_scan)
|
||||||
force_scan = (scan_mode == "full")
|
tv_series_total += 1
|
||||||
result = tv_processor.process_series(item, force_scan=force_scan)
|
if result == "skipped":
|
||||||
tv_series_total += 1
|
tv_series_skipped += 1
|
||||||
if result == "skipped":
|
elif result == "processed":
|
||||||
tv_series_skipped += 1
|
tv_series_processed += 1
|
||||||
elif result == "processed":
|
except Exception as e:
|
||||||
tv_series_processed += 1
|
print(f"ERROR: Failed processing TV series {item}: {e}")
|
||||||
except Exception as e:
|
tv_series_total += 1
|
||||||
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
|
# Check for shutdown signal
|
||||||
if tv_count % 1 == 0:
|
shutdown_event = dependencies.get("shutdown_event")
|
||||||
await asyncio.sleep(0.2) # 200ms yield to process other requests
|
if shutdown_event and shutdown_event.is_set():
|
||||||
print(f"INFO: Processed {tv_count} TV series, yielding to other requests...")
|
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:
|
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
|
||||||
force_scan = (scan_mode == "full")
|
force_scan = (scan_mode == "full")
|
||||||
shutdown_event = dependencies.get("shutdown_event")
|
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 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
@@ -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)}"}
|
||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user