featuer: smart scan to skip ones with dates
Local Docker Build (Dev) / build-dev (push) Successful in 4s

This commit is contained in:
2025-10-18 15:20:57 -04:00
parent 55d5f891b5
commit 0734b4d14d
3 changed files with 117 additions and 11 deletions
+1 -1
View File
@@ -1 +1 @@
2.5.0
2.5.1
+30 -3
View File
@@ -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})"}
+86 -7
View File
@@ -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"""