Merge branch 'dev'
This commit is contained in:
Binary file not shown.
+132
-13
@@ -163,7 +163,7 @@ class NFOManager:
|
|||||||
|
|
||||||
def extract_nfoguard_dates_from_episode_nfo(self, season_path: Path, season_num: int, episode_num: int) -> Optional[Dict[str, str]]:
|
def extract_nfoguard_dates_from_episode_nfo(self, season_path: Path, season_num: int, episode_num: int) -> Optional[Dict[str, str]]:
|
||||||
"""Extract NFOGuard-managed dates from existing episode NFO file"""
|
"""Extract NFOGuard-managed dates from existing episode NFO file"""
|
||||||
nfo_filename = f"s{season_num:02d}e{episode_num:02d}.nfo"
|
nfo_filename = f"S{season_num:02d}E{episode_num:02d}.nfo"
|
||||||
nfo_path = season_path / nfo_filename
|
nfo_path = season_path / nfo_filename
|
||||||
|
|
||||||
if not nfo_path.exists():
|
if not nfo_path.exists():
|
||||||
@@ -190,7 +190,11 @@ class NFOManager:
|
|||||||
if aired_elem is not None and aired_elem.text:
|
if aired_elem is not None and aired_elem.text:
|
||||||
result["aired"] = aired_elem.text.strip()
|
result["aired"] = aired_elem.text.strip()
|
||||||
|
|
||||||
print(f"✅ Found NFOGuard data in episode NFO S{season_num:02d}E{episode_num:02d}: dateadded={result['dateadded']}, aired={result.get('aired', 'None')}")
|
# Check if title is missing
|
||||||
|
title_elem = root.find('.//title')
|
||||||
|
result["has_title"] = title_elem is not None and title_elem.text and title_elem.text.strip()
|
||||||
|
|
||||||
|
print(f"✅ Found NFOGuard data in episode NFO S{season_num:02d}E{episode_num:02d}: dateadded={result['dateadded']}, aired={result.get('aired', 'None')}, has_title={result['has_title']}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except (ET.ParseError, Exception) as e:
|
except (ET.ParseError, Exception) as e:
|
||||||
@@ -199,6 +203,104 @@ class NFOManager:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def enhance_existing_episode_nfo_with_title(self, season_path: Path, season_num: int, episode_num: int, title: str) -> bool:
|
||||||
|
"""Add title to existing episode NFO file that's missing one"""
|
||||||
|
nfo_filename = f"S{season_num:02d}E{episode_num:02d}.nfo"
|
||||||
|
nfo_path = season_path / nfo_filename
|
||||||
|
|
||||||
|
if not nfo_path.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree = ET.parse(nfo_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Check if title already exists
|
||||||
|
existing_title = root.find('.//title')
|
||||||
|
if existing_title is not None and existing_title.text and existing_title.text.strip():
|
||||||
|
return False # Title already exists
|
||||||
|
|
||||||
|
# Add title element (insert near the top, after any existing plot element)
|
||||||
|
title_elem = ET.Element("title")
|
||||||
|
title_elem.text = title
|
||||||
|
|
||||||
|
# Find the best position to insert title (after plot if it exists, otherwise at the beginning)
|
||||||
|
plot_elem = root.find('.//plot')
|
||||||
|
if plot_elem is not None:
|
||||||
|
# Insert after plot
|
||||||
|
plot_index = list(root).index(plot_elem)
|
||||||
|
root.insert(plot_index + 1, title_elem)
|
||||||
|
else:
|
||||||
|
# Insert at the beginning (after any existing title that might be empty)
|
||||||
|
if existing_title is not None:
|
||||||
|
title_index = list(root).index(existing_title)
|
||||||
|
root.remove(existing_title)
|
||||||
|
root.insert(title_index, title_elem)
|
||||||
|
else:
|
||||||
|
root.insert(0, title_elem)
|
||||||
|
|
||||||
|
# Write the updated NFO
|
||||||
|
tree.write(nfo_path, encoding='utf-8', xml_declaration=True)
|
||||||
|
print(f"📝 Enhanced existing NFO with title: S{season_num:02d}E{episode_num:02d} - '{title}'")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error enhancing NFO with title: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
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 metadata 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}"
|
||||||
|
print(f"🔍 Searching for title in files for {season_pattern} in directory: {season_dir}")
|
||||||
|
|
||||||
|
for video_file in season_dir.glob("*.mkv"):
|
||||||
|
filename = video_file.name
|
||||||
|
print(f"🔍 Checking file: {filename}")
|
||||||
|
if season_pattern in filename:
|
||||||
|
print(f"✅ Found matching file: {filename}")
|
||||||
|
# Pattern: SeriesName-S01E01-Episode Title[WEBDL-1080p][AAC2.0][h264].mkv
|
||||||
|
# Extract the part between season/episode and the first bracket
|
||||||
|
pattern = rf'{season_pattern}-(.*?)\['
|
||||||
|
print(f"🔍 Using regex pattern: {pattern}")
|
||||||
|
match = re.search(pattern, filename)
|
||||||
|
if match:
|
||||||
|
title = match.group(1).strip()
|
||||||
|
print(f"🔍 Raw extracted title: '{title}'")
|
||||||
|
# Clean up common encoding artifacts and separators
|
||||||
|
title = title.replace('-', ' ').strip()
|
||||||
|
if title:
|
||||||
|
print(f"✅ Extracted title from filename: '{title}' for {season_pattern}")
|
||||||
|
return title
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Title was empty after cleanup")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Regex pattern didn't match filename")
|
||||||
|
|
||||||
|
# Also check .mp4 files
|
||||||
|
for video_file in season_dir.glob("*.mp4"):
|
||||||
|
filename = video_file.name
|
||||||
|
print(f"🔍 Checking .mp4 file: {filename}")
|
||||||
|
if season_pattern in filename:
|
||||||
|
print(f"✅ Found matching .mp4 file: {filename}")
|
||||||
|
pattern = rf'{season_pattern}-(.*?)\['
|
||||||
|
match = re.search(pattern, filename)
|
||||||
|
if match:
|
||||||
|
title = match.group(1).strip()
|
||||||
|
print(f"🔍 Raw extracted title from .mp4: '{title}'")
|
||||||
|
title = title.replace('-', ' ').strip()
|
||||||
|
if title:
|
||||||
|
print(f"✅ Extracted title from .mp4 filename: '{title}' for {season_pattern}")
|
||||||
|
return title
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error extracting title from filename for S{season_num:02d}E{episode_num:02d}: {e}")
|
||||||
|
|
||||||
|
print(f"⚠️ No title found in filenames for {season_pattern}")
|
||||||
|
return None
|
||||||
|
|
||||||
def _parse_nfo_with_tolerance(self, nfo_path: Path):
|
def _parse_nfo_with_tolerance(self, nfo_path: Path):
|
||||||
"""Parse NFO file with tolerance for URLs appended after XML"""
|
"""Parse NFO file with tolerance for URLs appended after XML"""
|
||||||
try:
|
try:
|
||||||
@@ -594,6 +696,29 @@ class NFOManager:
|
|||||||
runtime_elem = ET.SubElement(episode, "runtime")
|
runtime_elem = ET.SubElement(episode, "runtime")
|
||||||
runtime_elem.text = str(enhanced_metadata["runtime"])
|
runtime_elem.text = str(enhanced_metadata["runtime"])
|
||||||
|
|
||||||
|
# Fallback: Extract title from filename if no valid title present
|
||||||
|
title_elem = episode.find("title")
|
||||||
|
has_valid_title = title_elem is not None and title_elem.text and title_elem.text.strip()
|
||||||
|
|
||||||
|
print(f"🔍 Title check for S{season_num:02d}E{episode_num:02d}: has_element={title_elem is not None}, has_text={title_elem.text if title_elem is not None else 'N/A'}, has_valid_title={has_valid_title}")
|
||||||
|
|
||||||
|
if not has_valid_title:
|
||||||
|
print(f"🔍 No valid title found in NFO for S{season_num:02d}E{episode_num:02d}, attempting filename extraction")
|
||||||
|
filename_title = self._extract_title_from_filename(season_dir, season_num, episode_num)
|
||||||
|
if filename_title:
|
||||||
|
# Remove existing empty/invalid title element if present
|
||||||
|
if title_elem is not None:
|
||||||
|
episode.remove(title_elem)
|
||||||
|
|
||||||
|
# Add new title element
|
||||||
|
title_elem = ET.SubElement(episode, "title")
|
||||||
|
title_elem.text = filename_title
|
||||||
|
print(f"✅ Added filename-extracted title to NFO: S{season_num:02d}E{episode_num:02d} - '{filename_title}'")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Could not extract title from filename for S{season_num:02d}E{episode_num:02d}")
|
||||||
|
else:
|
||||||
|
print(f"✅ Valid title already exists in NFO for S{season_num:02d}E{episode_num:02d}: '{title_elem.text.strip()}')")
|
||||||
|
|
||||||
# Add NFOGuard fields at the bottom
|
# Add NFOGuard fields at the bottom
|
||||||
|
|
||||||
# Basic episode info at the end
|
# Basic episode info at the end
|
||||||
@@ -617,22 +742,16 @@ class NFOManager:
|
|||||||
lockdata = ET.SubElement(episode, "lockdata")
|
lockdata = ET.SubElement(episode, "lockdata")
|
||||||
lockdata.text = "true"
|
lockdata.text = "true"
|
||||||
|
|
||||||
# Add NFOGuard comment at the beginning
|
# Add NFOGuard comment at the bottom with other NFOGuard fields
|
||||||
comment_text = f" Created by {self.manager_brand} - Source: {source} "
|
nfoguard_comment = ET.Comment(f" Created by {self.manager_brand} - Source: {source} ")
|
||||||
|
episode.append(nfoguard_comment)
|
||||||
|
|
||||||
# Write file with proper formatting
|
# Write file with proper formatting
|
||||||
tree = ET.ElementTree(episode)
|
tree = ET.ElementTree(episode)
|
||||||
ET.indent(tree, space=" ", level=0)
|
ET.indent(tree, space=" ", level=0)
|
||||||
|
|
||||||
# Write to string first to add comment
|
# Write to file normally (ET will handle the comment properly)
|
||||||
xml_str = ET.tostring(episode, encoding='unicode')
|
tree.write(nfo_path, encoding='utf-8', xml_declaration=True)
|
||||||
|
|
||||||
# Add XML declaration and comment
|
|
||||||
full_xml = f'<?xml version="1.0" encoding="utf-8"?>\n<!--{comment_text}-->\n{xml_str}'
|
|
||||||
|
|
||||||
# Write to file
|
|
||||||
with open(nfo_path, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(full_xml)
|
|
||||||
|
|
||||||
print(f"✅ Successfully created/updated episode NFO: {nfo_path}")
|
print(f"✅ Successfully created/updated episode NFO: {nfo_path}")
|
||||||
print(f" S{season_num:02d}E{episode_num:02d}, Aired: {aired}, Date Added: {dateadded}")
|
print(f" S{season_num:02d}E{episode_num:02d}, Aired: {aired}, Date Added: {dateadded}")
|
||||||
|
|||||||
+140
-24
@@ -568,7 +568,7 @@ class TVProcessor:
|
|||||||
for (season, episode), (aired, dateadded, source) in episode_dates.items():
|
for (season, episode), (aired, dateadded, source) in episode_dates.items():
|
||||||
if (season, episode) in disk_episodes:
|
if (season, episode) in disk_episodes:
|
||||||
# Get enhanced episode metadata
|
# 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
|
# Create NFO
|
||||||
if config.manage_nfo:
|
if config.manage_nfo:
|
||||||
@@ -623,6 +623,21 @@ class TVProcessor:
|
|||||||
source = nfo_data["source"]
|
source = nfo_data["source"]
|
||||||
aired = nfo_data.get("aired")
|
aired = nfo_data.get("aired")
|
||||||
|
|
||||||
|
# Check if NFO is missing title and try to enhance it
|
||||||
|
if not nfo_data.get("has_title", False):
|
||||||
|
_log("INFO", f"NFO S{season_num:02d}E{episode_num:02d} missing title, attempting to extract from filename")
|
||||||
|
filename_title = self._extract_title_from_filename(season_path, season_num, episode_num)
|
||||||
|
if filename_title:
|
||||||
|
enhanced = self.nfo_manager.enhance_existing_episode_nfo_with_title(
|
||||||
|
season_path, season_num, episode_num, filename_title
|
||||||
|
)
|
||||||
|
if enhanced:
|
||||||
|
_log("INFO", f"✅ Enhanced NFO S{season_num:02d}E{episode_num:02d} with title: '{filename_title}'")
|
||||||
|
else:
|
||||||
|
_log("WARNING", f"Failed to enhance NFO S{season_num:02d}E{episode_num:02d} with title")
|
||||||
|
else:
|
||||||
|
_log("WARNING", f"Could not extract title from filename for S{season_num:02d}E{episode_num:02d}")
|
||||||
|
|
||||||
# Update file mtime if enabled (NFO is already correct)
|
# Update file mtime if enabled (NFO is already correct)
|
||||||
if config.fix_dir_mtimes and dateadded:
|
if config.fix_dir_mtimes and dateadded:
|
||||||
self.nfo_manager.set_file_mtime(episode_file, dateadded)
|
self.nfo_manager.set_file_mtime(episode_file, dateadded)
|
||||||
@@ -639,12 +654,14 @@ class TVProcessor:
|
|||||||
source = existing_episode["source"]
|
source = existing_episode["source"]
|
||||||
aired = existing_episode.get("aired")
|
aired = existing_episode.get("aired")
|
||||||
|
|
||||||
# Create NFO with existing data (no enhanced metadata needed)
|
# Create NFO with existing data, but try to add title from filename if missing
|
||||||
if config.manage_nfo and dateadded:
|
if config.manage_nfo and dateadded:
|
||||||
|
# Try to extract title from filename as fallback for Tier 2 processing
|
||||||
|
enhanced_metadata = self._get_episode_metadata(None, season_num, episode_num, season_path)
|
||||||
self.nfo_manager.create_episode_nfo(
|
self.nfo_manager.create_episode_nfo(
|
||||||
season_path,
|
season_path,
|
||||||
season_num, episode_num, aired, dateadded, source, config.lock_metadata,
|
season_num, episode_num, aired, dateadded, source, config.lock_metadata,
|
||||||
None # No enhanced metadata for database-cached episodes
|
enhanced_metadata # Include filename-extracted title if available
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update file mtime if enabled
|
# Update file mtime if enabled
|
||||||
@@ -659,7 +676,7 @@ class TVProcessor:
|
|||||||
|
|
||||||
# Get enhanced metadata from Sonarr
|
# Get enhanced metadata from Sonarr
|
||||||
series_metadata = self._get_sonarr_series_metadata(imdb_id)
|
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
|
# Get episode date
|
||||||
aired, dateadded, source = self._get_single_episode_date(imdb_id, season_num, episode_num, series_metadata)
|
aired, dateadded, source = self._get_single_episode_date(imdb_id, season_num, episode_num, series_metadata)
|
||||||
@@ -739,20 +756,97 @@ class TVProcessor:
|
|||||||
_log("ERROR", f"Error getting Sonarr metadata for {imdb_id}: {e}")
|
_log("ERROR", f"Error getting Sonarr metadata for {imdb_id}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_episode_metadata(self, series_metadata: Dict[str, Any], season_num: int, episode_num: int) -> Optional[Dict[str, Any]]:
|
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"""
|
"""Extract specific episode metadata from series data with filename fallback for titles"""
|
||||||
if not series_metadata or "episodes" not in series_metadata:
|
_log("INFO", f"🔍 _get_episode_metadata called for S{season_num:02d}E{episode_num:02d}, season_dir: {season_dir}")
|
||||||
return None
|
metadata = {
|
||||||
|
"title": None,
|
||||||
|
"overview": None,
|
||||||
|
"runtime": None,
|
||||||
|
"ratings": {}
|
||||||
|
}
|
||||||
|
|
||||||
for episode in series_metadata["episodes"]:
|
# Try to get metadata from Sonarr first
|
||||||
if episode.get("seasonNumber") == season_num and episode.get("episodeNumber") == episode_num:
|
if series_metadata and "episodes" in series_metadata:
|
||||||
return {
|
for episode in series_metadata["episodes"]:
|
||||||
"title": episode.get("title"),
|
if episode.get("seasonNumber") == season_num and episode.get("episodeNumber") == episode_num:
|
||||||
"overview": episode.get("overview"),
|
metadata.update({
|
||||||
"runtime": episode.get("runtime"),
|
"title": episode.get("title"),
|
||||||
"ratings": episode.get("ratings", {})
|
"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:
|
||||||
|
_log("INFO", f"📁 No title from Sonarr for S{season_num:02d}E{episode_num:02d}, trying filename extraction")
|
||||||
|
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}'")
|
||||||
|
else:
|
||||||
|
_log("DEBUG", f"⚠️ No filename title extracted for S{season_num:02d}E{episode_num:02d}")
|
||||||
|
elif metadata["title"]:
|
||||||
|
_log("DEBUG", f"✅ Using Sonarr title for S{season_num:02d}E{episode_num:02d}: '{metadata['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}"
|
||||||
|
_log("INFO", f"🔍 Searching for title in files for {season_pattern} in directory: {season_dir}")
|
||||||
|
|
||||||
|
for video_file in season_dir.glob("*.mkv"):
|
||||||
|
filename = video_file.name
|
||||||
|
_log("DEBUG", f"🔍 Checking file: {filename}")
|
||||||
|
if season_pattern in filename:
|
||||||
|
_log("DEBUG", f"✅ Found matching file: {filename}")
|
||||||
|
# Pattern: SeriesName-S01E01-Episode Title[WEBDL-1080p][AAC2.0][h264].mkv
|
||||||
|
# Extract the part between season/episode and the first bracket
|
||||||
|
pattern = rf'{season_pattern}-(.*?)\['
|
||||||
|
_log("DEBUG", f"🔍 Using regex pattern: {pattern}")
|
||||||
|
match = re.search(pattern, filename)
|
||||||
|
if match:
|
||||||
|
title = match.group(1).strip()
|
||||||
|
_log("DEBUG", f"🔍 Raw extracted title: '{title}'")
|
||||||
|
# Clean up common encoding artifacts and separators
|
||||||
|
title = title.replace('-', ' ').strip()
|
||||||
|
if title:
|
||||||
|
_log("INFO", f"✅ Extracted title from filename: '{title}' for {season_pattern}")
|
||||||
|
return title
|
||||||
|
else:
|
||||||
|
_log("DEBUG", f"⚠️ Title was empty after cleanup")
|
||||||
|
else:
|
||||||
|
_log("DEBUG", f"⚠️ Regex pattern didn't match filename")
|
||||||
|
|
||||||
|
# Also check .mp4 files
|
||||||
|
for video_file in season_dir.glob("*.mp4"):
|
||||||
|
filename = video_file.name
|
||||||
|
_log("DEBUG", f"🔍 Checking .mp4 file: {filename}")
|
||||||
|
if season_pattern in filename:
|
||||||
|
_log("DEBUG", f"✅ Found matching .mp4 file: {filename}")
|
||||||
|
pattern = rf'{season_pattern}-(.*?)\['
|
||||||
|
match = re.search(pattern, filename)
|
||||||
|
if match:
|
||||||
|
title = match.group(1).strip()
|
||||||
|
_log("DEBUG", f"🔍 Raw extracted title from .mp4: '{title}'")
|
||||||
|
title = title.replace('-', ' ').strip()
|
||||||
|
if title:
|
||||||
|
_log("INFO", f"✅ Extracted title from .mp4 filename: '{title}' for {season_pattern}")
|
||||||
|
return title
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_log("ERROR", f"Error extracting title from filename for S{season_num:02d}E{episode_num:02d}: {e}")
|
||||||
|
|
||||||
|
_log("DEBUG", f"⚠️ No title found in filenames for {season_pattern}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _gather_season_episode_dates(self, series_path: Path, imdb_id: str, season_num: int, disk_episodes: Dict[Tuple[int, int], List[Path]], series_metadata: Optional[Dict[str, Any]] = None) -> Dict[Tuple[int, int], Tuple[Optional[str], Optional[str], str]]:
|
def _gather_season_episode_dates(self, series_path: Path, imdb_id: str, season_num: int, disk_episodes: Dict[Tuple[int, int], List[Path]], series_metadata: Optional[Dict[str, Any]] = None) -> Dict[Tuple[int, int], Tuple[Optional[str], Optional[str], str]]:
|
||||||
@@ -869,7 +963,7 @@ class TVProcessor:
|
|||||||
# Get episode date information - webhook processing prioritizes existing DB entries
|
# 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}")
|
_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)
|
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
|
# Create NFO
|
||||||
if config.manage_nfo:
|
if config.manage_nfo:
|
||||||
@@ -1468,12 +1562,13 @@ class MovieProcessor:
|
|||||||
class WebhookBatcher:
|
class WebhookBatcher:
|
||||||
"""Batches webhook events to avoid processing storms"""
|
"""Batches webhook events to avoid processing storms"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, nfo_manager: NFOManager):
|
||||||
self.pending: Dict[str, Dict] = {}
|
self.pending: Dict[str, Dict] = {}
|
||||||
self.timers: Dict[str, threading.Timer] = {}
|
self.timers: Dict[str, threading.Timer] = {}
|
||||||
self.processing: Set[str] = set()
|
self.processing: Set[str] = set()
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.executor = ThreadPoolExecutor(max_workers=config.max_concurrent)
|
self.executor = ThreadPoolExecutor(max_workers=config.max_concurrent)
|
||||||
|
self.nfo_manager = nfo_manager
|
||||||
|
|
||||||
def add_webhook(self, key: str, webhook_data: Dict, media_type: str):
|
def add_webhook(self, key: str, webhook_data: Dict, media_type: str):
|
||||||
"""Add webhook to batch queue"""
|
"""Add webhook to batch queue"""
|
||||||
@@ -1527,11 +1622,23 @@ class WebhookBatcher:
|
|||||||
# CRITICAL: Validate that the path contains the expected IMDb ID for movies
|
# CRITICAL: Validate that the path contains the expected IMDb ID for movies
|
||||||
if media_type == 'movie':
|
if media_type == 'movie':
|
||||||
expected_imdb = key.replace('movie:', '') if key.startswith('movie:') else key
|
expected_imdb = key.replace('movie:', '') if key.startswith('movie:') else key
|
||||||
if expected_imdb not in path_str.lower():
|
# Use comprehensive IMDb detection (directory, filenames, NFO content)
|
||||||
|
detected_imdb = self.nfo_manager.find_movie_imdb_id(path_obj)
|
||||||
|
|
||||||
|
# Check if detected IMDb matches expected (handle both full IMDb IDs and just numbers)
|
||||||
|
imdb_match = False
|
||||||
|
if detected_imdb:
|
||||||
|
if detected_imdb == expected_imdb:
|
||||||
|
imdb_match = True
|
||||||
|
elif detected_imdb.replace('tt', '') == expected_imdb.replace('tt', ''):
|
||||||
|
imdb_match = True
|
||||||
|
|
||||||
|
if not imdb_match:
|
||||||
_log("ERROR", f"BATCH VALIDATION FAILED: Expected IMDb {expected_imdb} not found in path {path_str}")
|
_log("ERROR", f"BATCH VALIDATION FAILED: Expected IMDb {expected_imdb} not found in path {path_str}")
|
||||||
|
_log("ERROR", f"Detected IMDb: {detected_imdb}, Expected: {expected_imdb}")
|
||||||
_log("ERROR", f"This prevents processing wrong movies due to batch corruption")
|
_log("ERROR", f"This prevents processing wrong movies due to batch corruption")
|
||||||
return
|
return
|
||||||
_log("DEBUG", f"Batch validation passed: IMDb {expected_imdb} found in path")
|
_log("DEBUG", f"Batch validation passed: Expected IMDb {expected_imdb} matches detected IMDb {detected_imdb}")
|
||||||
|
|
||||||
# Process based on media type
|
# Process based on media type
|
||||||
if media_type == 'tv':
|
if media_type == 'tv':
|
||||||
@@ -1614,7 +1721,7 @@ nfo_manager = NFOManager(config.manager_brand, config.debug)
|
|||||||
path_mapper = PathMapper(config) # FIXED: Pass config to PathMapper
|
path_mapper = PathMapper(config) # FIXED: Pass config to PathMapper
|
||||||
tv_processor = TVProcessor(db, nfo_manager, path_mapper)
|
tv_processor = TVProcessor(db, nfo_manager, path_mapper)
|
||||||
movie_processor = MovieProcessor(db, nfo_manager, path_mapper)
|
movie_processor = MovieProcessor(db, nfo_manager, path_mapper)
|
||||||
batcher = WebhookBatcher()
|
batcher = WebhookBatcher(nfo_manager)
|
||||||
|
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
# Webhook Handlers
|
# Webhook Handlers
|
||||||
@@ -1727,9 +1834,18 @@ async def radarr_webhook(request: Request, background_tasks: BackgroundTasks):
|
|||||||
_log("ERROR", f"This prevents processing wrong movies due to path mapping issues")
|
_log("ERROR", f"This prevents processing wrong movies due to path mapping issues")
|
||||||
return {"status": "error", "message": f"Mapped movie path does not exist: {container_path}"}
|
return {"status": "error", "message": f"Mapped movie path does not exist: {container_path}"}
|
||||||
|
|
||||||
# Verify the path contains the expected IMDb ID
|
# Verify the path contains the expected IMDb ID using comprehensive detection
|
||||||
if imdb_id not in container_path.lower():
|
detected_imdb = nfo_manager.find_movie_imdb_id(Path(container_path))
|
||||||
_log("WARNING", f"IMDb ID {imdb_id} not found in container path {container_path}")
|
imdb_match = False
|
||||||
|
if detected_imdb:
|
||||||
|
if detected_imdb == imdb_id or detected_imdb.replace('tt', '') == imdb_id.replace('tt', ''):
|
||||||
|
imdb_match = True
|
||||||
|
|
||||||
|
if not imdb_match:
|
||||||
|
_log("WARNING", f"IMDb ID {imdb_id} not found via comprehensive detection in {container_path}")
|
||||||
|
_log("DEBUG", f"Detected IMDb: {detected_imdb}, Expected: {imdb_id}")
|
||||||
|
else:
|
||||||
|
_log("DEBUG", f"IMDb ID validated: {imdb_id} matches detected {detected_imdb}")
|
||||||
|
|
||||||
# Create movie-specific webhook data with proper path validation
|
# Create movie-specific webhook data with proper path validation
|
||||||
movie_webhook_data = {
|
movie_webhook_data = {
|
||||||
|
|||||||
Reference in New Issue
Block a user