feat: smart NFO management and correct episode date field mapping

💾 Smart NFO File Management:
- Add intelligent content comparison to prevent unnecessary file overwrites
- Only update NFO files when content actually changes (ignoring timestamp differences)
- Dramatically reduce file system writes and improve scan performance
- Applied to both TV episodes and movies for comprehensive efficiency
- New _nfo_content_matches() method for smart content comparison

🗓️ Fix Episode Date Field Mapping:
- BREAKING: Correct date field usage in episode NFOs
- <aired> and <premiered>: Now use historical air dates (e.g., 1951-10-15)
- <dateadded>: Correctly shows actual download/import date (e.g., 2025-09-13)
- Resolves confusion between when show aired vs when user downloaded it
- Media servers now show historically accurate metadata with working "Recently Added"

🎯 Technical Changes:
- Enhanced create_episode_nfo() with content comparison logic
- Enhanced create_movie_nfo() with content comparison logic
- Separated historical date logic from import date logic
- Added comprehensive debug logging for file skip/update decisions

🏆 User Experience:
- Faster scans with fewer unnecessary file operations
- Correct historical context in media servers
- "Recently Added" functionality preserved and working
- No more metadata confusion between air dates and import dates

Example NFO output:
- <aired>1951-10-15</aired> (historical)
- <dateadded>2025-09-13T21:39:28+00:00</dateadded> (when downloaded)
- <premiered>1951-10-15</premiered> (historical)
This commit is contained in:
2025-09-13 19:52:43 -04:00
parent 2d89e87191
commit f4c56f6cbe
2 changed files with 107 additions and 13 deletions
+63 -12
View File
@@ -204,12 +204,28 @@ class NFOManager:
# Pretty-print and save
self._indent_xml(root)
# Generate new content
import io
new_content_buffer = io.StringIO()
ET.ElementTree(root).write(new_content_buffer, encoding="unicode", xml_declaration=True)
new_content = new_content_buffer.getvalue()
# Write to both canonical path and alt path if it exists
for target in [canonical_path] + ([alt_path] if alt_path.exists() else []):
# Check if content has meaningfully changed
if target.exists():
try:
existing_content = target.read_text(encoding="utf-8")
if self._nfo_content_matches(existing_content, new_content):
_log("DEBUG", f"Movie NFO already up-to-date: {target}")
continue
except Exception as e:
_log("DEBUG", f"Could not read existing NFO {target}: {e}")
try:
target.parent.mkdir(parents=True, exist_ok=True)
ET.ElementTree(root).write(str(target), encoding="utf-8", xml_declaration=True)
_log("DEBUG", f"Updated NFO: {target}")
target.write_text(new_content, encoding="utf-8")
_log("DEBUG", f"Created/updated movie NFO: {target}")
except Exception as e:
_log("ERROR", f"Failed writing {target}: {e}")
@@ -261,17 +277,15 @@ class NFOManager:
xml_lines.append(f" <rating>{imdb_rating}</rating>")
xml_lines.append(f" <votes>{imdb_votes}</votes>")
if dateadded:
# For Emby TV episodes: Use dateadded (import date) as aired date
# This ensures Emby displays the import date instead of scan date
import_date = dateadded.split('T')[0]
xml_lines.append(f" <aired>{import_date}</aired>")
xml_lines.append(f" <dateadded>{dateadded}</dateadded>")
xml_lines.append(f" <premiered>{import_date}</premiered>")
elif aired:
# Fallback: Use actual air date if no import date available
# Add historical air date and premiere date (should be the same)
if aired:
air_date = aired.split('T')[0]
xml_lines.append(f" <aired>{air_date}</aired>")
xml_lines.append(f" <premiered>{air_date}</premiered>")
# Add import/download date (when user actually got the episode)
if dateadded:
xml_lines.append(f" <dateadded>{dateadded}</dateadded>")
if lock_metadata:
xml_lines.append(" <lockdata>true</lockdata>")
@@ -287,12 +301,49 @@ class NFOManager:
content = "\n".join(xml_lines)
# Check if NFO already exists and has correct content
if nfo_path.exists():
try:
existing_content = nfo_path.read_text(encoding="utf-8")
if self._nfo_content_matches(existing_content, content):
_log("DEBUG", f"Episode NFO already up-to-date: {nfo_path}")
return
except Exception as e:
_log("DEBUG", f"Could not read existing NFO {nfo_path}: {e}")
try:
nfo_path.write_text(content, encoding="utf-8")
_log("DEBUG", f"Created episode NFO: {nfo_path}")
_log("DEBUG", f"Created/updated episode NFO: {nfo_path}")
except Exception as e:
_log("ERROR", f"Failed writing {nfo_path}: {e}")
def _nfo_content_matches(self, existing_content: str, new_content: str) -> bool:
"""
Compare NFO content intelligently, ignoring timestamp differences
but checking for meaningful changes in episode data.
"""
try:
# Remove timestamp comments as they will always differ
def clean_content(content: str) -> str:
lines = content.split('\n')
cleaned_lines = []
for line in lines:
# Skip lines with timestamps (managed by NFOGuard at ...)
if 'managed by' in line and 'at ' in line:
continue
cleaned_lines.append(line.strip())
return '\n'.join(cleaned_lines)
cleaned_existing = clean_content(existing_content)
cleaned_new = clean_content(new_content)
# Compare the essential content (ignoring whitespace differences)
return cleaned_existing == cleaned_new
except Exception as e:
_log("DEBUG", f"Error comparing NFO content: {e}")
return False
def create_season_nfo(self, season_dir: Path, season_num: int):
"""Create season.nfo if it doesn't exist"""
season_nfo = season_dir / "season.nfo"