feat: Preserve TV episode NFO filenames matching video files
Local Docker Build (Dev) / build-dev (push) Successful in 29s

Instead of migrating long NFO names to standardized S01E01.nfo format,
now preserves original filenames that match video files while still
populating NFOGuard metadata (dateadded, aired, season, episode).

Changes:
- Added update_episode_nfo_preserving_name() method to NFOManager
- Added find_episode_nfo_matching_video() to locate NFO files by video name pattern
- Modified TVProcessor to update existing NFOs in-place rather than migrate names
- Episodes with existing NFOs now get "nfo_update_required" source for special handling

This maintains the user's preferred naming convention where NFO files
match their corresponding video files exactly.
This commit is contained in:
2025-09-26 08:16:42 -04:00
parent 24d94306c1
commit 4980d1ae04
3 changed files with 684 additions and 12 deletions
+173
View File
@@ -9,6 +9,7 @@ from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, Any
import re
from .logging import _log
class NFOManager:
@@ -858,6 +859,71 @@ class NFOManager:
return False
def reorder_nfo_for_emby(self, nfo_path: Path) -> bool:
"""Reorder NFO elements to put title first for better Emby compatibility"""
if not nfo_path.exists():
return False
try:
tree = ET.parse(nfo_path)
root = tree.getroot()
# Find title element
title_elem = root.find('title')
if title_elem is None:
return False # No title element to reorder
# Check if title is already first
if len(root) > 0 and root[0].tag == 'title':
return False # Title is already first
# Remove title from current position
root.remove(title_elem)
# Insert title as first element
root.insert(0, title_elem)
# Write back the modified NFO
ET.indent(tree, space=" ", level=0)
tree.write(nfo_path, encoding="utf-8", xml_declaration=True)
_log("INFO", f"Reordered NFO elements - moved title to first position: {nfo_path.name}")
return True
except Exception as e:
_log("ERROR", f"Error reordering NFO {nfo_path}: {e}")
return False
def fix_nfo_for_emby_compatibility(self, nfo_path: Path) -> Dict[str, bool]:
"""Apply all Emby compatibility fixes to an NFO file"""
results = {
"lockdata_removed": False,
"title_reordered": False,
"file_updated": False
}
if not nfo_path.exists():
return results
try:
# Strip lockdata elements
results["lockdata_removed"] = self.strip_lockdata_from_nfo(nfo_path)
# Reorder title to first position
results["title_reordered"] = self.reorder_nfo_for_emby(nfo_path)
results["file_updated"] = results["lockdata_removed"] or results["title_reordered"]
if results["file_updated"]:
_log("INFO", f"Applied Emby compatibility fixes to {nfo_path.name}: "
f"lockdata_removed={results['lockdata_removed']}, "
f"title_reordered={results['title_reordered']}")
except Exception as e:
_log("ERROR", f"Error applying Emby fixes to {nfo_path}: {e}")
return results
def strip_lockdata_from_directory(self, directory_path: Path) -> int:
"""Strip lockdata from all NFO files in a directory"""
if not directory_path.exists():
@@ -874,4 +940,111 @@ class NFOManager:
_log("INFO", f"Stripped lockdata from {processed_count} NFO files in {directory_path}")
return processed_count
def update_episode_nfo_preserving_name(self, nfo_path: Path, season_num: int, episode_num: int,
aired: Optional[str], dateadded: Optional[str], source: str) -> bool:
"""Update existing TV episode NFO file while preserving its original filename"""
if not nfo_path.exists():
return False
try:
# Parse the existing NFO file
tree = ET.parse(nfo_path)
episode = tree.getroot()
# Ensure root element is <episodedetails>
if episode.tag != "episodedetails":
print(f"⚠️ NFO file {nfo_path.name} does not have episodedetails root element")
return False
# Remove any existing NFOGuard fields to re-add them at the bottom
nfoguard_fields = ["aired", "dateadded", "season", "episode"]
for field_name in nfoguard_fields:
existing_field = episode.find(field_name)
if existing_field is not None:
episode.remove(existing_field)
# Remove any existing NFOGuard comments
for child in episode:
if isinstance(child, ET.Comment):
if self.manager_brand in str(child):
episode.remove(child)
# Add NFOGuard fields at the bottom
# Basic episode info at the end
season_elem = ET.SubElement(episode, "season")
season_elem.text = str(season_num)
episode_elem = ET.SubElement(episode, "episode")
episode_elem.text = str(episode_num)
# Dates at the end
if aired:
aired_elem = ET.SubElement(episode, "aired")
aired_elem.text = aired[:10] if len(aired) >= 10 else aired
if dateadded:
dateadded_elem = ET.SubElement(episode, "dateadded")
dateadded_elem.text = dateadded
# Add NFOGuard comment at the bottom with other NFOGuard fields
nfoguard_comment = ET.Comment(f" Updated by {self.manager_brand} - Source: {source} ")
episode.append(nfoguard_comment)
# Write file with proper formatting
tree = ET.ElementTree(episode)
ET.indent(tree, space=" ", level=0)
tree.write(nfo_path, encoding='utf-8', xml_declaration=True)
print(f"✅ Successfully updated episode NFO (preserving name): {nfo_path.name}")
print(f" S{season_num:02d}E{episode_num:02d}, Aired: {aired}, Date Added: {dateadded}")
return True
except Exception as e:
print(f"❌ Error updating episode NFO {nfo_path.name}: {e}")
return False
def find_episode_nfo_matching_video(self, season_dir: Path, season_num: int, episode_num: int) -> Optional[Path]:
"""Find episode NFO that matches the naming pattern of corresponding video files"""
if not season_dir.exists():
return None
# Find video files for this episode (check common patterns)
season_episode_pattern = f"S{season_num:02d}E{episode_num:02d}"
# Look for video files with season/episode pattern
video_extensions = ['.mkv', '.mp4', '.avi', '.m4v']
for ext in video_extensions:
video_files = season_dir.glob(f"*{season_episode_pattern}*{ext}")
for video_file in video_files:
# For each video file, look for an NFO with the same base name
nfo_path = video_file.with_suffix('.nfo')
if nfo_path.exists():
# Verify this NFO contains the right season/episode
try:
tree = ET.parse(nfo_path)
root = tree.getroot()
if root.tag == "episodedetails":
season_elem = root.find("season")
episode_elem = root.find("episode")
if (season_elem is not None and episode_elem is not None and
season_elem.text and episode_elem.text):
try:
file_season = int(season_elem.text)
file_episode = int(episode_elem.text)
if file_season == season_num and file_episode == episode_num:
print(f"🎯 Found episode NFO matching video file: {nfo_path.name}")
return nfo_path
except ValueError:
continue
except (ET.ParseError, Exception):
continue
return None