This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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,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:
|
||||||
|
|||||||
Reference in New Issue
Block a user