Fix TV webhook processing: Add missing process_webhook_episodes method

- Add process_webhook_episodes method to TVProcessor class for targeted episode processing
- Implement helper methods: _parse_episode_from_filename, _get_sonarr_series_metadata, _get_episode_metadata, _extract_title_from_filename, _get_webhook_episode_date
- Enables webhook-based episode processing with title extraction and metadata handling
- Resolves 'TVProcessor' object has no attribute 'process_webhook_episodes' error
- Version bump to 2.0.10
This commit is contained in:
2025-10-09 18:02:24 -04:00
parent 7ff2de6e66
commit ed78a81a8e
2 changed files with 224 additions and 1 deletions
+223
View File
@@ -459,6 +459,229 @@ class TVProcessor:
return processed_results
def process_webhook_episodes(self, series_path: Path, webhook_episodes: List[Dict[str, Any]]) -> None:
"""Process only the specific episodes mentioned in a webhook (targeted mode)"""
imdb_id = self.nfo_manager.parse_imdb_from_path(series_path)
if not imdb_id:
_log("ERROR", f"No IMDb ID found in series path: {series_path}")
return
if not webhook_episodes:
_log("WARNING", f"No episodes in webhook, falling back to series processing: {series_path}")
self.process_series(series_path)
return
_log("INFO", f"Processing {len(webhook_episodes)} webhook episodes for: {series_path.name} (IMDb: {imdb_id})")
# Update database
self.db.upsert_series(imdb_id, str(series_path))
# Get enhanced metadata from Sonarr
series_metadata = self._get_sonarr_series_metadata(imdb_id)
episodes_processed = 0
for webhook_episode in webhook_episodes:
season_num = webhook_episode.get("seasonNumber")
episode_num = webhook_episode.get("episodeNumber")
if not season_num or not episode_num:
_log("WARNING", f"Invalid episode data in webhook: {webhook_episode}")
continue
# Check if episode file exists on disk
season_dir = series_path / config.tv_season_dir_format.format(season=season_num)
if not season_dir.exists():
_log("WARNING", f"Season directory not found: {season_dir}")
continue
# Find matching episode files
episode_files = []
for file_path in season_dir.iterdir():
if file_path.is_file() and file_path.suffix.lower() in ('.mkv', '.mp4', '.avi', '.mov', '.m4v'):
parsed = self._parse_episode_from_filename(file_path.name)
if parsed and parsed == (season_num, episode_num):
episode_files.append(file_path)
if not episode_files:
_log("WARNING", f"No video files found for S{season_num:02d}E{episode_num:02d}")
continue
# 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)
# Create NFO
if config.manage_nfo:
self.nfo_manager.create_episode_nfo(
season_dir,
season_num, episode_num, aired, dateadded, source, config.lock_metadata,
enhanced_metadata
)
# Update file mtimes
if config.fix_dir_mtimes and dateadded:
for episode_file in episode_files:
self.nfo_manager.set_file_mtime(episode_file, dateadded)
# Save to database
self.db.upsert_episode_date(imdb_id, season_num, episode_num, aired, dateadded, source, True)
# Verify database entry was saved (debug)
verification = self.db.get_episode_date(imdb_id, season_num, episode_num)
if verification:
_log("DEBUG", f"Verified database entry saved: S{season_num:02d}E{episode_num:02d} -> {verification['dateadded']}")
else:
_log("ERROR", f"Failed to save episode to database: S{season_num:02d}E{episode_num:02d}")
episodes_processed += 1
# Create season/tvshow NFOs if any episodes were processed
if episodes_processed > 0 and config.manage_nfo:
seasons_processed = set()
for webhook_episode in webhook_episodes:
season_num = webhook_episode.get("seasonNumber")
if season_num and season_num not in seasons_processed:
season_dir = series_path / config.tv_season_dir_format.format(season=season_num)
if season_dir.exists():
self.nfo_manager.create_season_nfo(season_dir, season_num)
seasons_processed.add(season_num)
# Get TVDB ID for better Emby compatibility
tvdb_id = self.external_clients.get_tvdb_series_id(imdb_id)
self.nfo_manager.create_tvshow_nfo(series_path, imdb_id, tvdb_id)
_log("INFO", f"Completed targeted processing: {episodes_processed}/{len(webhook_episodes)} episodes processed")
def _parse_episode_from_filename(self, filename: str) -> Optional[Tuple[int, int]]:
"""Parse season and episode numbers from filename"""
# Try SxxExx format
match = re.search(r'S(\d{1,2})E(\d{1,2})', filename, re.IGNORECASE)
if match:
return int(match.group(1)), int(match.group(2))
# Try season.episode format
match = re.search(r'(\d{1,2})\.(\d{1,2})', filename)
if match:
return int(match.group(1)), int(match.group(2))
return None
def _get_sonarr_series_metadata(self, imdb_id: str) -> Optional[Dict[str, Any]]:
"""Get enhanced series metadata from Sonarr API"""
try:
if not self.sonarr.enabled:
return None
series_data = self.sonarr.series_by_imdb(imdb_id)
if not series_data:
return None
series_id = series_data.get('id')
if not series_id:
return None
# Get all episodes for this series
episodes = self.sonarr.episodes_for_series(series_id)
# Organize episodes by season/episode
episode_map = {}
for episode in episodes:
season = episode.get('seasonNumber', 0)
episode_num = episode.get('episodeNumber', 0)
if season > 0 and episode_num > 0:
episode_map[(season, episode_num)] = episode
return {
'series': series_data,
'episodes': episode_map
}
except Exception as e:
_log("ERROR", f"Failed to get Sonarr series metadata for {imdb_id}: {e}")
return None
def _get_episode_metadata(self, series_metadata: Optional[Dict[str, Any]], season_num: int, episode_num: int, season_dir: Optional[Path] = None) -> Optional[Dict[str, Any]]:
"""Get enhanced episode metadata including title extraction from filename"""
_log("DEBUG", f"Getting episode metadata for S{season_num:02d}E{episode_num:02d}, season_dir: {season_dir}")
metadata = {}
# Try to get title from Sonarr first
if series_metadata and 'episodes' in series_metadata:
episode_data = series_metadata['episodes'].get((season_num, episode_num))
if episode_data:
title = episode_data.get('title')
if title and title != 'TBA':
metadata['title'] = title
_log("DEBUG", f"Got title from Sonarr for S{season_num:02d}E{episode_num:02d}: {title}")
# If no title from Sonarr, try to extract from filename
if 'title' not in metadata and season_dir:
title = self._extract_title_from_filename(season_num, episode_num, season_dir)
if title:
metadata['title'] = title
_log("DEBUG", f"Extracted title from filename for S{season_num:02d}E{episode_num:02d}: {title}")
return metadata if metadata else None
def _extract_title_from_filename(self, season_num: int, episode_num: int, season_dir: Path) -> Optional[str]:
"""Extract episode title from video filename using regex pattern"""
season_pattern = f"S{season_num:02d}E{episode_num:02d}"
try:
# Look for video files in the season directory
for file_path in season_dir.iterdir():
if file_path.is_file() and file_path.suffix.lower() in ('.mkv', '.mp4', '.avi', '.mov', '.m4v'):
filename = file_path.name
# Check if this file matches our season/episode
if season_pattern in filename.upper():
# Extract title using regex pattern: S01E01-Title[WEBDL-1080p]
match = re.search(rf'{season_pattern}-(.*?)\[', filename, re.IGNORECASE)
if match:
title = match.group(1)
# Clean up the title
title = title.replace('-', ' ').strip()
if title:
_log("DEBUG", f"Extracted title '{title}' from filename: {filename}")
return title
except Exception as e:
_log("ERROR", f"Error extracting title from filename for S{season_num:02d}E{episode_num:02d}: {e}")
return None
def _get_webhook_episode_date(self, imdb_id: str, season_num: int, episode_num: int, series_metadata: Optional[Dict[str, Any]] = None) -> Tuple[Optional[str], Optional[str], str]:
"""
Get episode date for webhook processing with correct priority:
1. Check existing NFOGuard database entry (preserve previous import dates)
2. If not found, use current time (webhook = new download)
3. Get aired date from Sonarr/TMDB for reference
"""
# Check if we already have this episode in our database (preserve existing dates)
existing = self.db.get_episode_date(imdb_id, season_num, episode_num)
if existing and existing.get('dateadded'):
_log("DEBUG", f"Found existing database entry for S{season_num:02d}E{episode_num:02d}: {existing['dateadded']}")
return existing.get('aired'), existing.get('dateadded'), existing.get('source', 'nfoguard:database')
# Get aired date from Sonarr if available
aired = None
if series_metadata and 'episodes' in series_metadata:
episode_data = series_metadata['episodes'].get((season_num, episode_num))
if episode_data:
aired = episode_data.get('airDate')
_log("DEBUG", f"Got aired date from Sonarr for S{season_num:02d}E{episode_num:02d}: {aired}")
# For webhook processing, use current time as dateadded (new download)
dateadded = datetime.now().isoformat()
source = "sonarr:webhook"
_log("DEBUG", f"Using webhook date for S{season_num:02d}E{episode_num:02d}: {dateadded}")
return aired, dateadded, source
async def async_batch_episode_processing(
self,
episodes_data: List[Dict[str, Any]],