dev #60

Merged
sbcrumb merged 42 commits from dev into main 2025-10-22 21:27:04 -04:00
4 changed files with 252 additions and 65 deletions
Showing only changes of commit e920487543 - Show all commits
+1 -1
View File
@@ -1 +1 @@
2.7.1
2.7.2
+191 -33
View File
@@ -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)
+17 -30
View File
@@ -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"}
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)}"}
+43 -1
View File
@@ -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) {