fix: Add filename-based title extraction for TV episodes
Local Docker Build (Dev) / build-dev (pull_request) Successful in 18s

- 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 <title> 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.
This commit is contained in:
2025-09-25 08:53:56 -04:00
parent c21a0df9c1
commit 16d9956202
+71 -15
View File
@@ -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: