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:
+44
-1
@@ -167,6 +167,49 @@ Preserve movie and TV import dates in Emby/Jellyfin/Plex by preventing upgraded
|
||||
|
||||
---
|
||||
|
||||
### 🆕 v0.6.3 NFO Management & Date Accuracy Improvements (September 2025)
|
||||
|
||||
**💾 Smart NFO File Management:**
|
||||
- **Problem**: NFOguard was rewriting NFO files for every episode on every scan, even untouched ones
|
||||
- **Solution**: Intelligent content comparison - only updates NFOs when content actually changes
|
||||
- **Performance**: Dramatically reduces unnecessary file system writes and processing time
|
||||
- **Both Media Types**: Applied to both TV episodes and movies for comprehensive efficiency
|
||||
|
||||
**🗓️ Correct Date Field Mapping:**
|
||||
- **Problem**: Episode NFOs showing import dates (2025) for historical fields `<aired>` and `<premiered>`
|
||||
- **Root Cause**: Logic incorrectly used `dateadded` for all date fields instead of proper separation
|
||||
- **Fix**: Clear separation of date meanings:
|
||||
- `<aired>`: Historical air date (e.g., 1951-10-15 for I Love Lucy)
|
||||
- `<premiered>`: Historical premiere date (same as aired)
|
||||
- `<dateadded>`: Actual download/import date (e.g., 2025-09-13T21:39:28+00:00)
|
||||
|
||||
**🎯 Technical Implementation:**
|
||||
- **New Method**: `_nfo_content_matches()` - smart content comparison ignoring timestamps
|
||||
- **Enhanced Logic**: Episodes now correctly separate historical vs import dates
|
||||
- **Skip Logic**: Files marked "already up-to-date" when no meaningful changes detected
|
||||
- **Logging**: Clear debug output showing when files are skipped vs updated
|
||||
|
||||
**🏆 User Experience Improvements:**
|
||||
- **Correct Metadata**: Shows historically accurate air dates in media servers
|
||||
- **Recently Added**: Still works properly using `dateadded` field
|
||||
- **Performance**: Faster scans with fewer unnecessary file writes
|
||||
- **Accuracy**: No more confusion between when show aired vs when you downloaded it
|
||||
|
||||
**📋 Example Before/After:**
|
||||
```xml
|
||||
<!-- Before (Wrong) -->
|
||||
<aired>2025-09-13</aired> <!-- Import date in wrong field -->
|
||||
<dateadded>2025-09-13T21:39:28+00:00</dateadded>
|
||||
<premiered>2025-09-13</premiered> <!-- Import date in wrong field -->
|
||||
|
||||
<!-- After (Correct) -->
|
||||
<aired>1951-10-15</aired> <!-- Historical air date ✅ -->
|
||||
<dateadded>2025-09-13T21:39:28+00:00</dateadded> <!-- When downloaded ✅ -->
|
||||
<premiered>1951-10-15</premiered> <!-- Historical premiere ✅ -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** September 13, 2025
|
||||
**Version:** v0.6.2-dev
|
||||
**Version:** v0.6.3-dev
|
||||
**Status:** Enhanced Production Ready
|
||||
+63
-12
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user