feat: Preserve TV episode NFO filenames matching video files
Local Docker Build (Dev) / build-dev (push) Successful in 29s
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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user