From 16d9956202dd5897051f632aeb028cfb5b7f7659 Mon Sep 17 00:00:00 2001 From: SBCrumb Date: Thu, 25 Sep 2025 08:53:56 -0400 Subject: [PATCH] fix: Add filename-based title extraction for TV episodes - Add _extract_title_from_filename() method to extract episode titles from video filenames - Enhance _get_episode_metadata() to use filename fallback when Sonarr API doesn't provide titles - Update all _get_episode_metadata() calls to pass season directory for filename extraction - Extract titles from patterns like: Series-S01E01-Episode Title[WEBDL-1080p][AAC2.0][h264].mkv - Preserves existing Sonarr API behavior while providing fallback for missing titles - Fixes episodes with missing elements in NFO files when Sonarr data is incomplete Resolves issue where episodes processed via sonarr:history.import had empty NFO titles despite clear title information being available in the video filenames. --- nfoguard.py | 86 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 15 deletions(-) diff --git a/nfoguard.py b/nfoguard.py index 2584c97..6877e8f 100644 --- a/nfoguard.py +++ b/nfoguard.py @@ -568,7 +568,7 @@ class TVProcessor: for (season, episode), (aired, dateadded, source) in episode_dates.items(): if (season, episode) in disk_episodes: # Get enhanced episode metadata - enhanced_metadata = self._get_episode_metadata(series_metadata, season, episode) if series_metadata else None + enhanced_metadata = self._get_episode_metadata(series_metadata, season, episode, season_path) if series_metadata else self._get_episode_metadata(None, season, episode, season_path) # Create NFO if config.manage_nfo: @@ -659,7 +659,7 @@ class TVProcessor: # Get enhanced metadata from Sonarr series_metadata = self._get_sonarr_series_metadata(imdb_id) - enhanced_metadata = self._get_episode_metadata(series_metadata, season_num, episode_num) if series_metadata else None + enhanced_metadata = self._get_episode_metadata(series_metadata, season_num, episode_num, season_path) if series_metadata else self._get_episode_metadata(None, season_num, episode_num, season_path) # Get episode date aired, dateadded, source = self._get_single_episode_date(imdb_id, season_num, episode_num, series_metadata) @@ -739,19 +739,75 @@ class TVProcessor: _log("ERROR", f"Error getting Sonarr metadata for {imdb_id}: {e}") return None - def _get_episode_metadata(self, series_metadata: Dict[str, Any], season_num: int, episode_num: int) -> Optional[Dict[str, Any]]: - """Extract specific episode metadata from series data""" - if not series_metadata or "episodes" not in series_metadata: - return None + def _get_episode_metadata(self, series_metadata: Dict[str, Any], season_num: int, episode_num: int, season_dir: Optional[Path] = None) -> Optional[Dict[str, Any]]: + """Extract specific episode metadata from series data with filename fallback for titles""" + metadata = { + "title": None, + "overview": None, + "runtime": None, + "ratings": {} + } - for episode in series_metadata["episodes"]: - if episode.get("seasonNumber") == season_num and episode.get("episodeNumber") == episode_num: - return { - "title": episode.get("title"), - "overview": episode.get("overview"), - "runtime": episode.get("runtime"), - "ratings": episode.get("ratings", {}) - } + # Try to get metadata from Sonarr first + if series_metadata and "episodes" in series_metadata: + for episode in series_metadata["episodes"]: + if episode.get("seasonNumber") == season_num and episode.get("episodeNumber") == episode_num: + metadata.update({ + "title": episode.get("title"), + "overview": episode.get("overview"), + "runtime": episode.get("runtime"), + "ratings": episode.get("ratings", {}) + }) + break + + # If no title from Sonarr, try to extract from filename + if not metadata["title"] and season_dir: + filename_title = self._extract_title_from_filename(season_dir, season_num, episode_num) + if filename_title: + metadata["title"] = filename_title + _log("INFO", f"Using filename-extracted title for S{season_num:02d}E{episode_num:02d}: '{filename_title}'") + + # Return metadata if we have at least some information + if any(metadata.values()): + return metadata + + return None + + def _extract_title_from_filename(self, season_dir: Path, season_num: int, episode_num: int) -> Optional[str]: + """Extract episode title from video filename as fallback when Sonarr doesn't provide it""" + try: + import re + # Look for video files matching this episode + season_pattern = f"S{season_num:02d}E{episode_num:02d}" + + for video_file in season_dir.glob("*.mkv"): + filename = video_file.name + if season_pattern in filename: + # Pattern: SeriesName-S01E01-Episode Title[WEBDL-1080p][AAC2.0][h264].mkv + # Extract the part between season/episode and the first bracket + match = re.search(rf'{season_pattern}-(.*?)\[', filename) + if match: + title = match.group(1).strip() + # Clean up common encoding artifacts and separators + title = title.replace('-', ' ').strip() + if title: + _log("DEBUG", f"Extracted title from filename: '{title}' for {season_pattern}") + return title + + # Also check .mp4 files + for video_file in season_dir.glob("*.mp4"): + filename = video_file.name + if season_pattern in filename: + match = re.search(rf'{season_pattern}-(.*?)\[', filename) + if match: + title = match.group(1).strip() + title = title.replace('-', ' ').strip() + if title: + _log("DEBUG", f"Extracted title from filename: '{title}' for {season_pattern}") + return title + + except Exception as e: + _log("DEBUG", f"Error extracting title from filename for S{season_num:02d}E{episode_num:02d}: {e}") return None @@ -869,7 +925,7 @@ class TVProcessor: # 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) if series_metadata else None + 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: