Add configurable TV webhook processing modes (targeted vs series)

⚙️ New Configuration Option:
• Added TV_WEBHOOK_PROCESSING_MODE environment variable
• targeted = Only process episodes mentioned in webhook (efficient, default)
• series = Process entire series directory (comprehensive, previous behavior)

🎯 Targeted Episode Processing:
• New process_webhook_episodes() method for precise processing
• Only updates NFOs for downloaded episodes (e.g., just S41E07)
• Reduces unnecessary file operations and Emby notifications
• Falls back to series processing if no episode data in webhook

🔧 Implementation Details:
• Modified Sonarr webhook handler to pass episode data
• Enhanced batch processor with mode detection
• Maintains backward compatibility with existing behavior
• Added comprehensive logging for processing mode selection

 Benefits:
• Efficient: 1 download = 1 episode processed (not entire series)
• Configurable: Users can choose preferred behavior via .env
• Compatible: Works with existing autoscan/Emby refresh systems
• Smart: Falls back gracefully when episode data unavailable
This commit is contained in:
2025-09-11 11:22:36 -04:00
parent 01cd31505a
commit ac2753509d
3 changed files with 107 additions and 2 deletions
+97 -2
View File
@@ -156,6 +156,7 @@ class NFOGuardConfig:
# TV processing
self.tv_season_dir_format = os.environ.get("TV_SEASON_DIR_FORMAT", "Season {season:02d}")
self.tv_season_dir_pattern = os.environ.get("TV_SEASON_DIR_PATTERN", "season ").lower()
self.tv_webhook_processing_mode = os.environ.get("TV_WEBHOOK_PROCESSING_MODE", "targeted").lower()
config = NFOGuardConfig()
@@ -649,6 +650,89 @@ class TVProcessor:
current_time = datetime.now(timezone.utc).isoformat(timespec="seconds")
return None, current_time, "fallback:current_time"
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}")
# 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
aired, dateadded, source = self._get_single_episode_date(imdb_id, season_num, episode_num, series_metadata)
enhanced_metadata = self._get_episode_metadata(series_metadata, season_num, episode_num) if series_metadata else None
# 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)
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)
self.nfo_manager.create_tvshow_nfo(series_path, imdb_id)
_log("INFO", f"Completed targeted processing: {episodes_processed}/{len(webhook_episodes)} episodes processed")
class MovieProcessor:
"""Handles movie processing"""
@@ -1027,7 +1111,16 @@ class WebhookBatcher:
# Process based on media type
if media_type == 'tv':
tv_processor.process_series(path_obj)
# Check processing mode for TV webhooks
processing_mode = webhook_data.get('processing_mode', config.tv_webhook_processing_mode)
episodes_data = webhook_data.get('episodes', [])
if processing_mode == 'targeted' and episodes_data:
_log("INFO", f"Using targeted episode processing for {len(episodes_data)} episodes")
tv_processor.process_webhook_episodes(path_obj, episodes_data)
else:
_log("INFO", f"Using series processing mode (fallback or configured)")
tv_processor.process_series(path_obj)
elif media_type == 'movie':
movie_processor.process_movie(path_obj)
else:
@@ -1149,7 +1242,9 @@ async def sonarr_webhook(request: Request, background_tasks: BackgroundTasks):
webhook_dict = {
'path': str(series_path),
'series_info': series_info,
'event_type': webhook.eventType
'event_type': webhook.eventType,
'episodes': webhook.episodes or [], # Include episode data for targeted processing
'processing_mode': config.tv_webhook_processing_mode
}
batcher.add_webhook(imdb_id, webhook_dict, 'tv')