diff --git a/.env.template b/.env.template index fac0ad8..4ee8755 100644 --- a/.env.template +++ b/.env.template @@ -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 # =========================================== diff --git a/README.md b/README.md index d93a8f6..080f071 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/nfoguard.py b/nfoguard.py index 7251f5d..3631f87 100644 --- a/nfoguard.py +++ b/nfoguard.py @@ -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')