From 3c4e47b08eef827508c953eda7e113eb44538ad2 Mon Sep 17 00:00:00 2001 From: sbcrumb Date: Mon, 20 Oct 2025 08:30:55 -0400 Subject: [PATCH] fixes: web interface shutdown --- VERSION | 2 +- api/routes.py | 24 +++++++++++++++++ api/web_routes.py | 10 +++---- main.py | 9 ++++++- processors/tv_processor.py | 55 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/VERSION b/VERSION index e70b452..097a15a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.6.0 +2.6.2 diff --git a/api/routes.py b/api/routes.py index 8d5e8fc..6e758f0 100644 --- a/api/routes.py +++ b/api/routes.py @@ -631,6 +631,12 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N import re tv_count = 0 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 not item.name.lower().startswith('season') 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: 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 if scan_type in ["both", "movies"] and scan_path in config.movie_paths: print(f"INFO: Scanning movies in: {scan_path}") movie_count = 0 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): movie_count += 1 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: await asyncio.sleep(0.2) # 200ms yield to process 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}") diff --git a/api/web_routes.py b/api/web_routes.py index 0dbbba4..78cf429 100644 --- a/api/web_routes.py +++ b/api/web_routes.py @@ -173,13 +173,13 @@ async def get_tv_series_list(dependencies: dict, if date_filter: if date_filter == "complete": # 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": # 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": # 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" 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.path, 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 NULL OR e.dateadded = '' THEN 1 END) as episodes_without_dates + COUNT(CASE WHEN e.dateadded IS NOT NULL THEN 1 END) as episodes_with_dates, + COUNT(CASE WHEN e.dateadded IS NULL THEN 1 END) as episodes_without_dates FROM series s LEFT JOIN episodes e ON s.imdb_id = e.imdb_id GROUP BY s.imdb_id, s.path diff --git a/main.py b/main.py index 6ebb159..e4e3003 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ Modular architecture with webhook processing and intelligent date handling import os import sys import signal +import asyncio from pathlib import Path from datetime import datetime, timezone @@ -34,6 +35,8 @@ from webhooks.webhook_batcher import WebhookBatcher # Import API routes from api.routes import register_routes +# Global shutdown event for graceful shutdown coordination +shutdown_event = asyncio.Event() def get_version() -> str: """Get application version""" @@ -107,7 +110,8 @@ def initialize_components(): "batcher": batcher, "start_time": start_time, "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""" _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 if hasattr(signal_handler, 'dependencies') and signal_handler.dependencies: deps = signal_handler.dependencies diff --git a/processors/tv_processor.py b/processors/tv_processor.py index 6386e43..051d29e 100644 --- a/processors/tv_processor.py +++ b/processors/tv_processor.py @@ -53,6 +53,48 @@ class TVProcessor: 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]: """ 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}") - # 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) _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: should_skip, reason = self.should_skip_series(imdb_id, len(disk_episodes), series_path.name) if should_skip: