fixes: web interface shutdown
Local Docker Build (Dev) / build-dev (push) Successful in 4s

This commit is contained in:
2025-10-20 08:30:55 -04:00
parent eb9bfbf82e
commit 3c4e47b08e
5 changed files with 91 additions and 9 deletions
+1 -1
View File
@@ -1 +1 @@
2.6.0 2.6.2
+24
View File
@@ -631,6 +631,12 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
import re import re
tv_count = 0 tv_count = 0
for item in scan_path.iterdir(): for item in scan_path.iterdir():
# 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 if (item.is_dir() and
not item.name.lower().startswith('season') and not item.name.lower().startswith('season') and
not re.match(r'^season\s+\d+$', item.name, re.IGNORECASE) and not re.match(r'^season\s+\d+$', item.name, re.IGNORECASE) and
@@ -653,11 +659,23 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
if tv_count % 1 == 0: if tv_count % 1 == 0:
await asyncio.sleep(0.2) # 200ms yield to process other requests await asyncio.sleep(0.2) # 200ms yield to process other requests
print(f"INFO: Processed {tv_count} TV series, yielding to 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
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 movie_count = 0
for item in scan_path.iterdir(): for item in scan_path.iterdir():
# 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): if item.is_dir() and nfo_manager.find_movie_imdb_id(item):
movie_count += 1 movie_count += 1
print(f"INFO: Processing movie: {item.name}") print(f"INFO: Processing movie: {item.name}")
@@ -678,6 +696,12 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
if movie_count % 2 == 0: if movie_count % 2 == 0:
await asyncio.sleep(0.2) # 200ms yield to process other requests await asyncio.sleep(0.2) # 200ms yield to process other requests
print(f"INFO: Processed {movie_count} movies, yielding to other requests...") print(f"INFO: Processed {movie_count} movies, 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
print(f"INFO: Completed movie scan: {movie_count} movies processed in {scan_path}") print(f"INFO: Completed movie scan: {movie_count} movies processed in {scan_path}")
+5 -5
View File
@@ -173,13 +173,13 @@ async def get_tv_series_list(dependencies: dict,
if date_filter: if date_filter:
if date_filter == "complete": if date_filter == "complete":
# All episodes have dates # All episodes have dates
having_conditions.append("COUNT(e.episode) > 0 AND COUNT(CASE WHEN e.dateadded IS NULL OR e.dateadded = '' THEN 1 END) = 0") having_conditions.append("COUNT(e.episode) > 0 AND COUNT(CASE WHEN e.dateadded IS NULL THEN 1 END) = 0")
elif date_filter == "incomplete": elif date_filter == "incomplete":
# Some episodes have dates, some don't # Some episodes have dates, some don't
having_conditions.append("COUNT(e.episode) > 0 AND COUNT(CASE WHEN e.dateadded IS NOT NULL AND e.dateadded != '' THEN 1 END) > 0 AND COUNT(CASE WHEN e.dateadded IS NULL OR e.dateadded = '' THEN 1 END) > 0") having_conditions.append("COUNT(e.episode) > 0 AND COUNT(CASE WHEN e.dateadded IS NOT NULL THEN 1 END) > 0 AND COUNT(CASE WHEN e.dateadded IS NULL THEN 1 END) > 0")
elif date_filter == "none": elif date_filter == "none":
# No episodes have dates # No episodes have dates
having_conditions.append("COUNT(e.episode) > 0 AND COUNT(CASE WHEN e.dateadded IS NOT NULL AND e.dateadded != '' THEN 1 END) = 0") having_conditions.append("COUNT(e.episode) > 0 AND COUNT(CASE WHEN e.dateadded IS NOT NULL THEN 1 END) = 0")
where_clause = " AND ".join(where_conditions) if where_conditions else "1=1" where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
having_clause = " AND ".join(having_conditions) if having_conditions else "" having_clause = " AND ".join(having_conditions) if having_conditions else ""
@@ -276,8 +276,8 @@ async def debug_series_date_distribution(dependencies: dict):
s.imdb_id, s.imdb_id,
s.path, s.path,
COUNT(e.episode) as total_episodes, COUNT(e.episode) as total_episodes,
COUNT(CASE WHEN e.dateadded IS NOT NULL AND e.dateadded != '' THEN 1 END) as episodes_with_dates, COUNT(CASE WHEN e.dateadded IS NOT NULL THEN 1 END) as episodes_with_dates,
COUNT(CASE WHEN e.dateadded IS NULL OR e.dateadded = '' THEN 1 END) as episodes_without_dates COUNT(CASE WHEN e.dateadded IS NULL THEN 1 END) as episodes_without_dates
FROM series s FROM series s
LEFT JOIN episodes e ON s.imdb_id = e.imdb_id LEFT JOIN episodes e ON s.imdb_id = e.imdb_id
GROUP BY s.imdb_id, s.path GROUP BY s.imdb_id, s.path
+8 -1
View File
@@ -6,6 +6,7 @@ Modular architecture with webhook processing and intelligent date handling
import os import os
import sys import sys
import signal import signal
import asyncio
from pathlib import Path from pathlib import Path
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -34,6 +35,8 @@ from webhooks.webhook_batcher import WebhookBatcher
# Import API routes # Import API routes
from api.routes import register_routes from api.routes import register_routes
# Global shutdown event for graceful shutdown coordination
shutdown_event = asyncio.Event()
def get_version() -> str: def get_version() -> str:
"""Get application version""" """Get application version"""
@@ -107,7 +110,8 @@ def initialize_components():
"batcher": batcher, "batcher": batcher,
"start_time": start_time, "start_time": start_time,
"config": config, "config": config,
"version": get_version() "version": get_version(),
"shutdown_event": shutdown_event
} }
@@ -115,6 +119,9 @@ def signal_handler(signum, frame):
"""Handle shutdown signals gracefully""" """Handle shutdown signals gracefully"""
_log("INFO", f"Received signal {signum}, shutting down gracefully...") _log("INFO", f"Received signal {signum}, shutting down gracefully...")
# Set shutdown event to notify background tasks
shutdown_event.set()
# Get the global dependencies if they exist # Get the global dependencies if they exist
if hasattr(signal_handler, 'dependencies') and signal_handler.dependencies: if hasattr(signal_handler, 'dependencies') and signal_handler.dependencies:
deps = signal_handler.dependencies deps = signal_handler.dependencies
+53 -2
View File
@@ -53,6 +53,48 @@ class TVProcessor:
path_mapper=self.path_mapper path_mapper=self.path_mapper
) )
def should_skip_series_fast(self, imdb_id: str, series_name: str = "") -> Tuple[bool, str, int]:
"""
Fast preliminary check to skip series without filesystem scan
Args:
imdb_id: Series IMDb ID
series_name: Series name for logging
Returns:
(should_skip: bool, reason: str, episodes_in_db: int)
"""
try:
with self.db.get_connection() as conn:
cursor = conn.cursor()
# Check if we have complete episodes in database
cursor.execute("""
SELECT
COUNT(*) as total_in_db,
COUNT(CASE WHEN dateadded IS NOT NULL AND source IS NOT NULL AND source != 'unknown' AND source != 'no_valid_date_source' THEN 1 END) as complete_episodes
FROM episodes
WHERE imdb_id = %s
""", (imdb_id,))
result = cursor.fetchone()
if not result:
return False, "No database records found", 0
total_in_db = result['total_in_db']
complete_episodes = result['complete_episodes']
# Skip if we have episodes and all are complete
# We'll verify disk count later if needed
if total_in_db > 0 and complete_episodes == total_in_db:
return True, f"Likely complete: {complete_episodes} episodes in DB all have valid dates", total_in_db
else:
return False, f"Needs checking: {complete_episodes}/{total_in_db} episodes complete in DB", total_in_db
except Exception as e:
_log("ERROR", f"Error in fast series check for {imdb_id}: {e}")
return False, f"Error in fast check: {e}", 0
def should_skip_series(self, imdb_id: str, episodes_on_disk: int, series_name: str = "") -> Tuple[bool, str]: def should_skip_series(self, imdb_id: str, episodes_on_disk: int, series_name: str = "") -> Tuple[bool, str]:
""" """
Determine if we should skip processing this series based on completion status Determine if we should skip processing this series based on completion status
@@ -112,11 +154,20 @@ class TVProcessor:
_log("INFO", f"Processing TV series: {series_path.name}") _log("INFO", f"Processing TV series: {series_path.name}")
# Find video files first to check completion # Fast check first - avoid expensive filesystem scan if possible
if not force_scan:
should_skip_fast, reason_fast, episodes_in_db = self.should_skip_series_fast(imdb_id, series_path.name)
if should_skip_fast:
_log("INFO", f"⚡ FAST SKIP: {series_path.name} [{imdb_id}] - {reason_fast}")
# Still update the series record to track that we've seen it
self.db.upsert_series(imdb_id, str(series_path))
return "skipped"
# Need filesystem scan - either force_scan=True or series not complete in DB
disk_episodes = find_episodes_on_disk(series_path) disk_episodes = find_episodes_on_disk(series_path)
_log("INFO", f"Found {len(disk_episodes)} episodes on disk") _log("INFO", f"Found {len(disk_episodes)} episodes on disk")
# Check if we should skip this series (unless forced) # Final skip check with actual episode count (unless forced)
if not force_scan: if not force_scan:
should_skip, reason = self.should_skip_series(imdb_id, len(disk_episodes), series_path.name) should_skip, reason = self.should_skip_series(imdb_id, len(disk_episodes), series_path.name)
if should_skip: if should_skip: