diff --git a/VERSION b/VERSION index 437459c..73462a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.5.0 +2.5.1 diff --git a/api/routes.py b/api/routes.py index 777f59a..8d5e8fc 100644 --- a/api/routes.py +++ b/api/routes.py @@ -573,6 +573,9 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N tv_series_total = 0 tv_series_skipped = 0 tv_series_processed = 0 + movie_total = 0 + movie_skipped = 0 + movie_processed = 0 paths_to_scan = [] if path: @@ -659,9 +662,17 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N movie_count += 1 print(f"INFO: Processing movie: {item.name}") try: - movie_processor.process_movie(item) + # 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) + movie_total += 1 + if result == "skipped": + movie_skipped += 1 + elif result == "processed": + movie_processed += 1 except Exception as e: print(f"ERROR: Failed processing movie {item}: {e}") + movie_total += 1 # Yield control every 2 movies to allow other requests (webhooks, web interface) if movie_count % 2 == 0: @@ -697,10 +708,26 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N # Print optimization statistics for TV scans if scan_type in ["both", "tv"] and tv_series_total > 0: - print(f"📊 SCAN OPTIMIZATION STATS: 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}") if tv_series_skipped > 0: skip_percentage = (tv_series_skipped / tv_series_total) * 100 - print(f"⚡ PERFORMANCE BOOST: {tv_series_skipped}/{tv_series_total} series skipped ({skip_percentage:.1f}% time saved!)") + print(f"⚡ TV PERFORMANCE BOOST: {tv_series_skipped}/{tv_series_total} series skipped ({skip_percentage:.1f}% time saved!)") + + # Print optimization statistics for movie scans + if scan_type in ["both", "movies"] and movie_total > 0: + print(f"📊 MOVIE SCAN OPTIMIZATION: Total: {movie_total}, Processed: {movie_processed}, Skipped: {movie_skipped}") + if movie_skipped > 0: + skip_percentage = (movie_skipped / movie_total) * 100 + print(f"⚡ MOVIE PERFORMANCE BOOST: {movie_skipped}/{movie_total} movies skipped ({skip_percentage:.1f}% time saved!)") + + # Print combined optimization statistics for "both" scans + if scan_type == "both" and (tv_series_total > 0 or movie_total > 0): + total_items = tv_series_total + movie_total + total_skipped = tv_series_skipped + movie_skipped + total_processed = tv_series_processed + movie_processed + if total_skipped > 0: + overall_skip_percentage = (total_skipped / total_items) * 100 + print(f"🎯 OVERALL OPTIMIZATION: {total_skipped}/{total_items} items skipped ({overall_skip_percentage:.1f}% total time saved!)") background_tasks.add_task(run_scan) return {"status": "started", "message": f"Manual {scan_type} scan started (mode: {scan_mode})"} diff --git a/processors/movie_processor.py b/processors/movie_processor.py index 91c49d4..ca5dd86 100644 --- a/processors/movie_processor.py +++ b/processors/movie_processor.py @@ -88,12 +88,75 @@ class MovieProcessor: path_mapper=self.path_mapper ) - def process_movie(self, movie_path: Path, webhook_mode: bool = False) -> None: + def should_skip_movie(self, imdb_id: str, movie_name: str = "") -> Tuple[bool, str]: + """ + Determine if we should skip processing this movie based on completion status + + Args: + imdb_id: Movie IMDb ID + movie_name: Movie name for logging + + Returns: + (should_skip: bool, reason: str) + """ + try: + with self.db.get_connection() as conn: + cursor = conn.cursor() + + if self.db.db_type == "postgresql": + cursor.execute(""" + SELECT dateadded, source, has_video_file + FROM movies + WHERE imdb_id = %s + """, (imdb_id,)) + else: + cursor.execute(""" + SELECT dateadded, source, has_video_file + FROM movies + WHERE imdb_id = ? + """, (imdb_id,)) + + result = cursor.fetchone() + if not result: + return False, "No database record found" + + if self.db.db_type == "postgresql": + dateadded = result['dateadded'] + source = result['source'] + has_video_file = result['has_video_file'] + else: + dateadded = result[0] if result[0] else None + source = result[1] if result[1] else None + has_video_file = result[2] if result[2] else False + + # Skip if: + # 1. Movie has a valid dateadded timestamp + # 2. Source is valid (not 'unknown' or 'no_valid_date_source') + # 3. Has video file on disk + if (dateadded and + source and + source not in ['unknown', 'no_valid_date_source'] and + has_video_file): + return True, f"Complete: Has valid date '{dateadded}' from source '{source}'" + elif not dateadded: + return False, "Missing dateadded" + elif not source or source in ['unknown', 'no_valid_date_source']: + return False, f"Invalid source: '{source}'" + elif not has_video_file: + return False, "No video file detected" + else: + return False, "Incomplete movie data" + + except Exception as e: + _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: """Process a movie directory""" imdb_id = self.nfo_manager.find_movie_imdb_id(movie_path) if not imdb_id: _log("ERROR", f"No IMDb ID found in movie directory, filenames, or NFO file: {movie_path}") - return + return "error" # Handle TMDB ID fallback case is_tmdb_fallback = imdb_id.startswith("tmdb-") @@ -102,6 +165,21 @@ class MovieProcessor: else: _log("INFO", f"Processing movie: {movie_path.name} (IMDb: {imdb_id})") + # Check if we should skip this movie (unless forced or webhook mode) + if not force_scan and not webhook_mode: + should_skip, reason = self.should_skip_movie(imdb_id, movie_path.name) + if should_skip: + _log("INFO", f"⏭️ SKIPPING MOVIE: {movie_path.name} [{imdb_id}] - {reason}") + # Still update the movie record to track that we've seen it + self.db.upsert_movie(imdb_id, str(movie_path)) + return "skipped" + else: + _log("INFO", f"🎬 PROCESSING MOVIE: {movie_path.name} [{imdb_id}] - {reason}") + elif force_scan: + _log("INFO", f"🔄 FORCE PROCESSING MOVIE: {movie_path.name} [{imdb_id}] - Force scan enabled") + else: + _log("INFO", f"📥 WEBHOOK PROCESSING MOVIE: {movie_path.name} [{imdb_id}] - Webhook mode") + # Update database self.db.upsert_movie(imdb_id, str(movie_path)) @@ -141,7 +219,7 @@ class MovieProcessor: self.nfo_manager.update_movie_files_mtime(movie_path, dateadded) _log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [database-cached]") - return + return "processed" else: _log("INFO", f"🔍 TIER 1 SKIP - {imdb_id}: Database incomplete, proceeding to Tier 2") @@ -169,7 +247,7 @@ class MovieProcessor: self.nfo_manager.update_movie_files_mtime(movie_path, dateadded) _log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [nfo-cached]") - return + return "processed" # TIER 2.5: Check for any existing valid date data in NFO (even without lockdata marker) # Only use NFO dates if prioritize_nfo is enabled, otherwise check external APIs first @@ -198,7 +276,7 @@ class MovieProcessor: self.nfo_manager.update_movie_files_mtime(movie_path, dateadded) _log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [existing-nfo-enhanced]") - return + return "processed" elif existing_nfo_data: _log("INFO", f"🔍 TIER 2.5 - Found existing date data in NFO (no lockdata): {existing_nfo_data['dateadded']} (source: {existing_nfo_data['source']})") _log("INFO", f"🎯 MANUAL_SCAN_PRIORITIZE_NFO=False - Will verify against external APIs first") @@ -228,7 +306,7 @@ class MovieProcessor: self.db.upsert_movie_dates(imdb_id, released, dateadded, source, True) _log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [tmdb-nfo]") - return + return "processed" # TIER 3: No cached data found - proceed with API lookups and verification @@ -277,7 +355,7 @@ class MovieProcessor: if final_dateadded is None: _log("WARNING", f"Movie {movie_path.name} - no valid date source available, but NFO was still processed") self.db.upsert_movie_dates(imdb_id, released, None, source, True) - return + return "processed" # Update dateadded and source for the rest of processing dateadded = final_dateadded @@ -305,6 +383,7 @@ class MovieProcessor: raise _log("INFO", f"Completed processing movie: {movie_path.name} (source: {source})") + return "processed" def _extract_dates_from_tmdb_nfo(self, nfo_path: Path) -> Optional[Dict[str, str]]: """Extract date information from TMDB-based NFO file"""