retry for the AI
Local Docker Build (Dev) / build-dev (push) Successful in 16s

This commit is contained in:
2025-09-26 15:19:37 -04:00
parent c19d0f6290
commit 2c4dd274ff
3 changed files with 90 additions and 389 deletions
+28 -177
View File
@@ -30,7 +30,6 @@ from core.path_mapper import PathMapper
from clients.radarr_client import RadarrClient
from clients.sonarr_client import SonarrClient
from clients.external_clients import ExternalClientManager
from processors.tv_series_processor import TVSeriesProcessor
# ---------------------------
# Configuration & Logging
@@ -441,33 +440,10 @@ class TVProcessor:
if key in disk_episodes: # Only use cached data for episodes we have
episode_dates[key] = (ep["aired"], ep["dateadded"], ep["source"])
# Check for existing NFO files (including long-named ones) for migration
nfo_episodes_found = 0
for (season_num, episode_num) in disk_episodes.keys():
if (season_num, episode_num) not in episode_dates:
# Check if this episode has an existing NFO file that needs migration
season_dir = series_path / config.tv_season_dir_format.format(season=season_num)
existing_nfo = self.nfo_manager.find_existing_episode_nfo(season_dir, season_num, episode_num)
if existing_nfo:
# Force processing of this episode to ensure proper dates
nfo_episodes_found += 1
if nfo_episodes_found > 0:
_log("INFO", f"Found {nfo_episodes_found} episodes with existing NFO files")
# Find missing episodes (not in cache)
# Find missing episodes
cached_keys = set(episode_dates.keys())
missing_keys = set(disk_episodes.keys()) - cached_keys
# Add episodes with existing NFOs to missing_keys so they get proper date lookup
for season_num, episode_num in disk_episodes.keys():
if (season_num, episode_num) not in cached_keys:
season_dir = series_path / config.tv_season_dir_format.format(season=season_num)
existing_nfo = self.nfo_manager.find_existing_episode_nfo(season_dir, season_num, episode_num)
if existing_nfo:
missing_keys.add((season_num, episode_num))
if not missing_keys:
_log("INFO", "All episodes found in cache")
return episode_dates
@@ -575,7 +551,7 @@ class TVProcessor:
for (season, episode), (aired, dateadded, source) in episode_dates.items():
if (season, episode) in disk_episodes:
# Get enhanced episode metadata
enhanced_metadata = self._get_episode_metadata(series_metadata, season, episode, season_path) if series_metadata else self._get_episode_metadata(None, season, episode, season_path)
enhanced_metadata = self._get_episode_metadata(series_metadata, season, episode) if series_metadata else None
# Create NFO
if config.manage_nfo:
@@ -630,21 +606,6 @@ class TVProcessor:
source = nfo_data["source"]
aired = nfo_data.get("aired")
# Check if NFO is missing title and try to enhance it
if not nfo_data.get("has_title", False):
_log("INFO", f"NFO S{season_num:02d}E{episode_num:02d} missing title, attempting to extract from filename")
filename_title = self._extract_title_from_filename(season_path, season_num, episode_num)
if filename_title:
enhanced = self.nfo_manager.enhance_existing_episode_nfo_with_title(
season_path, season_num, episode_num, filename_title
)
if enhanced:
_log("INFO", f"✅ Enhanced NFO S{season_num:02d}E{episode_num:02d} with title: '{filename_title}'")
else:
_log("WARNING", f"Failed to enhance NFO S{season_num:02d}E{episode_num:02d} with title")
else:
_log("WARNING", f"Could not extract title from filename for S{season_num:02d}E{episode_num:02d}")
# Update file mtime if enabled (NFO is already correct)
if config.fix_dir_mtimes and dateadded:
self.nfo_manager.set_file_mtime(episode_file, dateadded)
@@ -661,14 +622,12 @@ class TVProcessor:
source = existing_episode["source"]
aired = existing_episode.get("aired")
# Create NFO with existing data, but try to add title from filename if missing
# Create NFO with existing data (no enhanced metadata needed)
if config.manage_nfo and dateadded:
# Try to extract title from filename as fallback for Tier 2 processing
enhanced_metadata = self._get_episode_metadata(None, season_num, episode_num, season_path)
self.nfo_manager.create_episode_nfo(
season_path,
season_num, episode_num, aired, dateadded, source, config.lock_metadata,
enhanced_metadata # Include filename-extracted title if available
None # No enhanced metadata for database-cached episodes
)
# Update file mtime if enabled
@@ -683,7 +642,7 @@ class TVProcessor:
# Get enhanced metadata from Sonarr
series_metadata = self._get_sonarr_series_metadata(imdb_id)
enhanced_metadata = self._get_episode_metadata(series_metadata, season_num, episode_num, season_path) if series_metadata else self._get_episode_metadata(None, season_num, episode_num, season_path)
enhanced_metadata = self._get_episode_metadata(series_metadata, season_num, episode_num) if series_metadata else None
# Get episode date
aired, dateadded, source = self._get_single_episode_date(imdb_id, season_num, episode_num, series_metadata)
@@ -738,7 +697,6 @@ class TVProcessor:
return None
def _get_sonarr_series_metadata(self, imdb_id: str) -> Optional[Dict[str, Any]]:
"""Get enhanced series metadata from Sonarr API"""
try:
@@ -763,97 +721,20 @@ class TVProcessor:
_log("ERROR", f"Error getting Sonarr metadata for {imdb_id}: {e}")
return None
def _get_episode_metadata(self, series_metadata: Dict[str, Any], season_num: int, episode_num: int, season_dir: Optional[Path] = None) -> Optional[Dict[str, Any]]:
"""Extract specific episode metadata from series data with filename fallback for titles"""
_log("INFO", f"🔍 _get_episode_metadata called for S{season_num:02d}E{episode_num:02d}, season_dir: {season_dir}")
metadata = {
"title": None,
"overview": None,
"runtime": None,
"ratings": {}
}
def _get_episode_metadata(self, series_metadata: Dict[str, Any], season_num: int, episode_num: int) -> Optional[Dict[str, Any]]:
"""Extract specific episode metadata from series data"""
if not series_metadata or "episodes" not in series_metadata:
return None
# Try to get metadata from Sonarr first
if series_metadata and "episodes" in series_metadata:
for episode in series_metadata["episodes"]:
if episode.get("seasonNumber") == season_num and episode.get("episodeNumber") == episode_num:
metadata.update({
"title": episode.get("title"),
"overview": episode.get("overview"),
"runtime": episode.get("runtime"),
"ratings": episode.get("ratings", {})
})
break
for episode in series_metadata["episodes"]:
if episode.get("seasonNumber") == season_num and episode.get("episodeNumber") == episode_num:
return {
"title": episode.get("title"),
"overview": episode.get("overview"),
"runtime": episode.get("runtime"),
"ratings": episode.get("ratings", {})
}
# If no title from Sonarr, try to extract from filename
if not metadata["title"] and season_dir:
_log("INFO", f"📁 No title from Sonarr for S{season_num:02d}E{episode_num:02d}, trying filename extraction")
filename_title = self._extract_title_from_filename(season_dir, season_num, episode_num)
if filename_title:
metadata["title"] = filename_title
_log("INFO", f"✅ Using filename-extracted title for S{season_num:02d}E{episode_num:02d}: '{filename_title}'")
else:
_log("DEBUG", f"⚠️ No filename title extracted for S{season_num:02d}E{episode_num:02d}")
elif metadata["title"]:
_log("DEBUG", f"✅ Using Sonarr title for S{season_num:02d}E{episode_num:02d}: '{metadata['title']}'")
# Return metadata if we have at least some information
if any(metadata.values()):
return metadata
return None
def _extract_title_from_filename(self, season_dir: Path, season_num: int, episode_num: int) -> Optional[str]:
"""Extract episode title from video filename as fallback when Sonarr doesn't provide it"""
try:
import re
# Look for video files matching this episode
season_pattern = f"S{season_num:02d}E{episode_num:02d}"
_log("INFO", f"🔍 Searching for title in files for {season_pattern} in directory: {season_dir}")
for video_file in season_dir.glob("*.mkv"):
filename = video_file.name
_log("DEBUG", f"🔍 Checking file: {filename}")
if season_pattern in filename:
_log("DEBUG", f"✅ Found matching file: {filename}")
# Pattern: SeriesName-S01E01-Episode Title[WEBDL-1080p][AAC2.0][h264].mkv
# Extract the part between season/episode and the first bracket
pattern = rf'{season_pattern}-(.*?)\['
_log("DEBUG", f"🔍 Using regex pattern: {pattern}")
match = re.search(pattern, filename)
if match:
title = match.group(1).strip()
_log("DEBUG", f"🔍 Raw extracted title: '{title}'")
# Clean up common encoding artifacts and separators
title = title.replace('-', ' ').strip()
if title:
_log("INFO", f"✅ Extracted title from filename: '{title}' for {season_pattern}")
return title
else:
_log("DEBUG", f"⚠️ Title was empty after cleanup")
else:
_log("DEBUG", f"⚠️ Regex pattern didn't match filename")
# Also check .mp4 files
for video_file in season_dir.glob("*.mp4"):
filename = video_file.name
_log("DEBUG", f"🔍 Checking .mp4 file: {filename}")
if season_pattern in filename:
_log("DEBUG", f"✅ Found matching .mp4 file: {filename}")
pattern = rf'{season_pattern}-(.*?)\['
match = re.search(pattern, filename)
if match:
title = match.group(1).strip()
_log("DEBUG", f"🔍 Raw extracted title from .mp4: '{title}'")
title = title.replace('-', ' ').strip()
if title:
_log("INFO", f"✅ Extracted title from .mp4 filename: '{title}' for {season_pattern}")
return title
except Exception as e:
_log("ERROR", f"Error extracting title from filename for S{season_num:02d}E{episode_num:02d}: {e}")
_log("DEBUG", f"⚠️ No title found in filenames for {season_pattern}")
return None
def _gather_season_episode_dates(self, series_path: Path, imdb_id: str, season_num: int, disk_episodes: Dict[Tuple[int, int], List[Path]], series_metadata: Optional[Dict[str, Any]] = None) -> Dict[Tuple[int, int], Tuple[Optional[str], Optional[str], str]]:
@@ -970,7 +851,7 @@ class TVProcessor:
# Get episode date information - webhook processing prioritizes existing DB entries
_log("DEBUG", f"Processing webhook episode: IMDb={imdb_id}, S{season_num:02d}E{episode_num:02d}")
aired, dateadded, source = self._get_webhook_episode_date(imdb_id, season_num, episode_num, series_metadata)
enhanced_metadata = self._get_episode_metadata(series_metadata, season_num, episode_num, season_dir) if series_metadata else self._get_episode_metadata(None, season_num, episode_num, season_dir)
enhanced_metadata = self._get_episode_metadata(series_metadata, season_num, episode_num) if series_metadata else None
# Create NFO
if config.manage_nfo:
@@ -1569,13 +1450,12 @@ class MovieProcessor:
class WebhookBatcher:
"""Batches webhook events to avoid processing storms"""
def __init__(self, nfo_manager: NFOManager):
def __init__(self):
self.pending: Dict[str, Dict] = {}
self.timers: Dict[str, threading.Timer] = {}
self.processing: Set[str] = set()
self.lock = threading.Lock()
self.executor = ThreadPoolExecutor(max_workers=config.max_concurrent)
self.nfo_manager = nfo_manager
def add_webhook(self, key: str, webhook_data: Dict, media_type: str):
"""Add webhook to batch queue"""
@@ -1629,23 +1509,11 @@ class WebhookBatcher:
# CRITICAL: Validate that the path contains the expected IMDb ID for movies
if media_type == 'movie':
expected_imdb = key.replace('movie:', '') if key.startswith('movie:') else key
# Use comprehensive IMDb detection (directory, filenames, NFO content)
detected_imdb = self.nfo_manager.find_movie_imdb_id(path_obj)
# Check if detected IMDb matches expected (handle both full IMDb IDs and just numbers)
imdb_match = False
if detected_imdb:
if detected_imdb == expected_imdb:
imdb_match = True
elif detected_imdb.replace('tt', '') == expected_imdb.replace('tt', ''):
imdb_match = True
if not imdb_match:
if expected_imdb not in path_str.lower():
_log("ERROR", f"BATCH VALIDATION FAILED: Expected IMDb {expected_imdb} not found in path {path_str}")
_log("ERROR", f"Detected IMDb: {detected_imdb}, Expected: {expected_imdb}")
_log("ERROR", f"This prevents processing wrong movies due to batch corruption")
return
_log("DEBUG", f"Batch validation passed: Expected IMDb {expected_imdb} matches detected IMDb {detected_imdb}")
_log("DEBUG", f"Batch validation passed: IMDb {expected_imdb} found in path")
# Process based on media type
if media_type == 'tv':
@@ -1726,17 +1594,9 @@ start_time = datetime.now(timezone.utc)
db = NFOGuardDatabase(config.db_path)
nfo_manager = NFOManager(config.manager_brand, config.debug)
path_mapper = PathMapper(config) # FIXED: Pass config to PathMapper
# Initialize Sonarr client using environment variables
sonarr = SonarrClient(
base_url=os.environ.get("SONARR_URL", ""),
api_key=os.environ.get("SONARR_API_KEY", "")
)
# Use new clean TV processor
tv_processor = TVSeriesProcessor(db, nfo_manager, path_mapper, sonarr)
tv_processor = TVProcessor(db, nfo_manager, path_mapper)
movie_processor = MovieProcessor(db, nfo_manager, path_mapper)
batcher = WebhookBatcher(nfo_manager)
batcher = WebhookBatcher()
# ---------------------------
# Webhook Handlers
@@ -1849,18 +1709,9 @@ async def radarr_webhook(request: Request, background_tasks: BackgroundTasks):
_log("ERROR", f"This prevents processing wrong movies due to path mapping issues")
return {"status": "error", "message": f"Mapped movie path does not exist: {container_path}"}
# Verify the path contains the expected IMDb ID using comprehensive detection
detected_imdb = nfo_manager.find_movie_imdb_id(Path(container_path))
imdb_match = False
if detected_imdb:
if detected_imdb == imdb_id or detected_imdb.replace('tt', '') == imdb_id.replace('tt', ''):
imdb_match = True
if not imdb_match:
_log("WARNING", f"IMDb ID {imdb_id} not found via comprehensive detection in {container_path}")
_log("DEBUG", f"Detected IMDb: {detected_imdb}, Expected: {imdb_id}")
else:
_log("DEBUG", f"IMDb ID validated: {imdb_id} matches detected {detected_imdb}")
# Verify the path contains the expected IMDb ID
if imdb_id not in container_path.lower():
_log("WARNING", f"IMDb ID {imdb_id} not found in container path {container_path}")
# Create movie-specific webhook data with proper path validation
movie_webhook_data = {
@@ -2208,11 +2059,11 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
except Exception as e:
_log("ERROR", f"Failed processing episode {scan_path}: {e}")
else:
# Full series processing with new clean processor
# Full series processing
for item in scan_path.iterdir():
if item.is_dir() and nfo_manager.parse_imdb_from_path(item):
try:
tv_processor.process_series_manual_scan(item)
tv_processor.process_series(item)
except Exception as e:
_log("ERROR", f"Failed processing TV series {item}: {e}")