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"
|
# Examples: "season " -> matches "Season 01", "s" -> matches "S01", "season" -> matches "Season01"
|
||||||
TV_SEASON_DIR_PATTERN=season
|
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
|
# LOGGING
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ ALLOW_FILE_DATE_FALLBACK=false
|
|||||||
ENABLE_SMART_DATE_VALIDATION=true
|
ENABLE_SMART_DATE_VALIDATION=true
|
||||||
MAX_RELEASE_DATE_GAP_YEARS=10
|
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
|
# Database connection
|
||||||
RADARR_DB_HOST=radarr-postgres
|
RADARR_DB_HOST=radarr-postgres
|
||||||
RADARR_DB_PORT=5432
|
RADARR_DB_PORT=5432
|
||||||
|
|||||||
+96
-1
@@ -156,6 +156,7 @@ class NFOGuardConfig:
|
|||||||
# TV processing
|
# TV processing
|
||||||
self.tv_season_dir_format = os.environ.get("TV_SEASON_DIR_FORMAT", "Season {season:02d}")
|
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_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()
|
config = NFOGuardConfig()
|
||||||
|
|
||||||
@@ -649,6 +650,89 @@ class TVProcessor:
|
|||||||
current_time = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
current_time = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||||
return None, current_time, "fallback:current_time"
|
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:
|
class MovieProcessor:
|
||||||
"""Handles movie processing"""
|
"""Handles movie processing"""
|
||||||
@@ -1027,6 +1111,15 @@ class WebhookBatcher:
|
|||||||
|
|
||||||
# Process based on media type
|
# Process based on media type
|
||||||
if media_type == 'tv':
|
if media_type == 'tv':
|
||||||
|
# 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)
|
tv_processor.process_series(path_obj)
|
||||||
elif media_type == 'movie':
|
elif media_type == 'movie':
|
||||||
movie_processor.process_movie(path_obj)
|
movie_processor.process_movie(path_obj)
|
||||||
@@ -1149,7 +1242,9 @@ async def sonarr_webhook(request: Request, background_tasks: BackgroundTasks):
|
|||||||
webhook_dict = {
|
webhook_dict = {
|
||||||
'path': str(series_path),
|
'path': str(series_path),
|
||||||
'series_info': series_info,
|
'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')
|
batcher.add_webhook(imdb_id, webhook_dict, 'tv')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user