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:
@@ -123,6 +123,11 @@ TV_SEASON_DIR_FORMAT=Season {season:02d}
|
||||
# Examples: "season " -> matches "Season 01", "s" -> matches "S01", "season" -> matches "Season01"
|
||||
TV_SEASON_DIR_PATTERN=season
|
||||
|
||||
# TV webhook processing mode (v0.6.0+)
|
||||
# targeted = Only process episodes mentioned in webhook (efficient, recommended)
|
||||
# series = Process entire series directory (comprehensive, current default)
|
||||
TV_WEBHOOK_PROCESSING_MODE=targeted
|
||||
|
||||
# ===========================================
|
||||
# LOGGING
|
||||
# ===========================================
|
||||
|
||||
@@ -115,6 +115,11 @@ ALLOW_FILE_DATE_FALLBACK=false
|
||||
ENABLE_SMART_DATE_VALIDATION=true
|
||||
MAX_RELEASE_DATE_GAP_YEARS=10
|
||||
|
||||
# TV webhook processing mode (v0.6.0+)
|
||||
# targeted = Only process episodes in webhook (efficient)
|
||||
# series = Process entire series directory (comprehensive)
|
||||
TV_WEBHOOK_PROCESSING_MODE=targeted
|
||||
|
||||
# Database connection
|
||||
RADARR_DB_HOST=radarr-postgres
|
||||
RADARR_DB_PORT=5432
|
||||
|
||||
+97
-2
@@ -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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user