From 1e2cf49c1060c0f9145ba938d08e6789985e98c4 Mon Sep 17 00:00:00 2001 From: sbcrumb Date: Mon, 20 Oct 2025 10:36:23 -0400 Subject: [PATCH] fix: not shutting down during scan --- api/routes.py | 32 ++++++++++++++++++++- core/database.py | 52 +++++++++++++++++++++++++++++++++++ processors/movie_processor.py | 12 +++++++- 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/api/routes.py b/api/routes.py index 35df7b8..210a1b9 100644 --- a/api/routes.py +++ b/api/routes.py @@ -682,7 +682,8 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N try: # Determine force_scan based on scan mode force_scan = (scan_mode == "full") - result = movie_processor.process_movie(item, webhook_mode=False, force_scan=force_scan) + shutdown_event = dependencies.get("shutdown_event") + result = movie_processor.process_movie(item, webhook_mode=False, force_scan=force_scan, shutdown_event=shutdown_event) movie_total += 1 if result == "skipped": movie_skipped += 1 @@ -691,6 +692,9 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N elif result == "no_video_files": print(f"INFO: Skipped empty directory: {item.name}") movie_skipped += 1 + elif result == "shutdown": + print("INFO: ⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping movie scan gracefully") + return except Exception as e: print(f"ERROR: Failed processing movie {item}: {e}") movie_total += 1 @@ -1207,6 +1211,28 @@ async def cleanup_orphaned_movies(dependencies: dict): } +async def cleanup_orphaned_series(dependencies: dict): + """Find and delete TV series that don't have corresponding directories""" + db = dependencies["db"] + + try: + deleted_series = db.delete_orphaned_series() + + return { + "success": True, + "message": f"Cleaned up {len(deleted_series)} orphaned TV series", + "deleted_count": len(deleted_series), + "deleted_series": deleted_series + } + + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to cleanup orphaned TV series" + } + + # --------------------------- # Route Registration # --------------------------- @@ -1277,6 +1303,10 @@ def register_routes(app, dependencies: dict): async def _cleanup_orphaned_movies(): return await cleanup_orphaned_movies(dependencies) + @app.post("/database/cleanup/orphaned-series") + async def _cleanup_orphaned_series(): + return await cleanup_orphaned_series(dependencies) + @app.post("/manual/scan") 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) diff --git a/core/database.py b/core/database.py index a80b15c..f8dc57c 100644 --- a/core/database.py +++ b/core/database.py @@ -501,6 +501,58 @@ class NFOGuardDatabase: return deleted_movies + def delete_orphaned_series(self) -> List[Dict]: + """ + Find and delete TV series that don't have corresponding directories on disk + This requires checking filesystem for each series, so use carefully + + Returns: + List of deleted series with their details + """ + from pathlib import Path + + deleted_series = [] + + with self.get_connection() as conn: + cursor = conn.cursor() + + # Get all series with their paths + cursor.execute(""" + SELECT imdb_id, path, last_updated, metadata + FROM series + """) + + series_list = cursor.fetchall() + + for series in series_list: + imdb_id = series['imdb_id'] + series_path = Path(series['path']) + + if not series_path.exists(): + # Series directory doesn't exist - delete the series and all its episodes + cursor.execute(""" + DELETE FROM episodes + WHERE imdb_id = %s + """, (imdb_id,)) + episodes_deleted = cursor.rowcount + + cursor.execute(""" + DELETE FROM series + WHERE imdb_id = %s + """, (imdb_id,)) + + deleted_series.append({ + 'imdb_id': imdb_id, + 'reason': 'directory_not_found', + 'path': str(series_path), + 'last_updated': series['last_updated'], + 'episodes_deleted': episodes_deleted + }) + + conn.commit() + + return deleted_series + def close(self): """Close all database connections""" if hasattr(self._local, 'connection'): diff --git a/processors/movie_processor.py b/processors/movie_processor.py index b5daf61..df4a00c 100644 --- a/processors/movie_processor.py +++ b/processors/movie_processor.py @@ -151,7 +151,7 @@ class MovieProcessor: _log("ERROR", f"Error checking movie completion for {imdb_id}: {e}") return False, f"Error checking completion: {e}" - def process_movie(self, movie_path: Path, webhook_mode: bool = False, force_scan: bool = False) -> str: + def process_movie(self, movie_path: Path, webhook_mode: bool = False, force_scan: bool = False, shutdown_event=None) -> str: """Process a movie directory""" imdb_id = self.nfo_manager.find_movie_imdb_id(movie_path) if not imdb_id: @@ -180,6 +180,11 @@ class MovieProcessor: else: _log("INFO", f"📥 WEBHOOK PROCESSING MOVIE: {movie_path.name} [{imdb_id}] - Webhook mode") + # Check for shutdown signal early in processing + if shutdown_event and shutdown_event.is_set(): + _log("INFO", f"⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping movie processing: {movie_path.name}") + return "shutdown" + # Update database self.db.upsert_movie(imdb_id, str(movie_path)) @@ -309,6 +314,11 @@ class MovieProcessor: # TIER 3: No cached data found - proceed with API lookups and verification + # Check for shutdown signal before expensive API operations + if shutdown_event and shutdown_event.is_set(): + _log("INFO", f"⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping movie processing before API calls: {movie_path.name}") + return "shutdown" + # TIER 3: No cached data found - determine if we should query APIs if webhook_mode: _log("INFO", f"Webhook processing - no cached data found, using full date decision logic")