dates for TV shows
This commit is contained in:
+51
-21
@@ -1,26 +1,24 @@
|
||||
# NFOGuard Development Summary
|
||||
|
||||
## Current Status: v1.4.0 - Complete Date Field Management
|
||||
## Current Status: v1.4.1 - Complete TV & Movie NFO Management
|
||||
|
||||
### Latest Updates (v1.4.0)
|
||||
- **🎯 ALL DATE FIELDS**: Now moves premiered, year, dateadded, and lockdata to bottom
|
||||
- **📋 COMPREHENSIVE**: Removes existing premiered/year fields and re-adds at bottom
|
||||
- **✅ PRESERVATION**: Preserves existing premiered date if no new release date provided
|
||||
- **🧹 CLEAN STRUCTURE**: All date/metadata fields grouped together at bottom
|
||||
### Latest Updates (v1.4.1)
|
||||
- **📺 TV SHOW SUPPORT**: Extended preservation logic to tvshow.nfo, season.nfo, and episode.nfo
|
||||
- **🎯 CONSISTENT BEHAVIOR**: All NFO types now preserve existing content and append NFOGuard fields
|
||||
- **✅ COMPREHENSIVE**: Movies, TV shows, seasons, and episodes all handle field placement correctly
|
||||
- **🧹 UNIFIED APPROACH**: Same preservation and bottom-placement logic across all media types
|
||||
|
||||
### Complete NFO Structure (Fixed)
|
||||
### Complete NFO Management Coverage
|
||||
|
||||
#### Movie NFO Structure
|
||||
```xml
|
||||
<movie>
|
||||
<!-- All existing Radarr/Jellyfin content preserved -->
|
||||
<title>Annabelle Comes Home</title>
|
||||
<plot>Plot content...</plot>
|
||||
<uniqueid type="tmdb" default="true">521029</uniqueid> <!-- preserved -->
|
||||
<genre>Horror</genre>
|
||||
<director>Gary Dauberman</director>
|
||||
<!-- All existing Radarr content preserved -->
|
||||
<title>Movie Title</title>
|
||||
<plot>Plot...</plot>
|
||||
<actor>...</actor>
|
||||
<!-- ... all existing fields including all actors ... -->
|
||||
|
||||
<!-- ALL date/metadata fields at absolute bottom -->
|
||||
<!-- NFOGuard fields at bottom -->
|
||||
<uniqueid type="imdb" default="true">tt8350360</uniqueid>
|
||||
<premiered>2019-06-26</premiered>
|
||||
<year>2019</year>
|
||||
@@ -29,12 +27,44 @@
|
||||
</movie>
|
||||
```
|
||||
|
||||
### NFOGuard Managed Fields
|
||||
- ✅ `uniqueid[type="imdb"]` - IMDb ID for identification
|
||||
- ✅ `premiered` - Release date (moved to bottom)
|
||||
- ✅ `year` - Extracted from premiered date
|
||||
- ✅ `dateadded` - Import timestamp
|
||||
- ✅ `lockdata` - Prevents metadata overwrites
|
||||
#### TV Show NFO Structure
|
||||
```xml
|
||||
<tvshow>
|
||||
<!-- All existing Sonarr content preserved -->
|
||||
<title>Show Title</title>
|
||||
<plot>Show plot...</plot>
|
||||
<actor>...</actor>
|
||||
|
||||
<!-- NFOGuard fields at bottom -->
|
||||
<uniqueid type="imdb" default="true">tt1234567</uniqueid>
|
||||
<uniqueid type="tvdb">12345</uniqueid>
|
||||
<lockdata>true</lockdata>
|
||||
</tvshow>
|
||||
```
|
||||
|
||||
#### Episode NFO Structure
|
||||
```xml
|
||||
<episodedetails>
|
||||
<!-- All existing Sonarr content preserved -->
|
||||
<title>Episode Title</title>
|
||||
<plot>Episode plot...</plot>
|
||||
<director>...</director>
|
||||
|
||||
<!-- NFOGuard fields at bottom -->
|
||||
<season>1</season>
|
||||
<episode>5</episode>
|
||||
<aired>2019-06-26</aired>
|
||||
<dateadded>2025-09-14T13:50:34-04:00</dateadded>
|
||||
<lockdata>true</lockdata>
|
||||
</episodedetails>
|
||||
```
|
||||
|
||||
### Preservation Features
|
||||
- ✅ **Movies**: Preserves title, plot, actors, ratings, all existing metadata
|
||||
- ✅ **TV Shows**: Preserves show metadata, existing uniqueid[tvdb], actor info
|
||||
- ✅ **Episodes**: Preserves episode title, plot, director, existing aired dates
|
||||
- ✅ **Seasons**: Preserves any existing season metadata
|
||||
- ✅ **All Types**: Existing content stays in original order, NFOGuard fields at bottom
|
||||
|
||||
### Complete Path Processing Pipeline
|
||||
- ✅ **Path Mapping**: Fixed substring matching (movies/movies6, tv/tv6)
|
||||
|
||||
+155
-45
@@ -127,76 +127,192 @@ class NFOManager:
|
||||
print(f"Error creating/updating movie NFO {nfo_path}: {e}")
|
||||
|
||||
def create_tvshow_nfo(self, series_dir: Path, imdb_id: str, tvdb_id: Optional[str] = None) -> None:
|
||||
"""Create tvshow.nfo file"""
|
||||
"""Create or update tvshow.nfo file preserving existing content"""
|
||||
nfo_path = series_dir / "tvshow.nfo"
|
||||
|
||||
try:
|
||||
tvshow = ET.Element("tvshow")
|
||||
# Try to load existing NFO file
|
||||
if nfo_path.exists():
|
||||
try:
|
||||
tree = ET.parse(nfo_path)
|
||||
tvshow = tree.getroot()
|
||||
|
||||
# Add IMDb ID
|
||||
# Ensure root element is <tvshow>
|
||||
if tvshow.tag != "tvshow":
|
||||
raise ValueError("Root element is not <tvshow>")
|
||||
|
||||
# Remove existing NFOGuard-managed elements to avoid duplicates
|
||||
# These will be re-added at the bottom
|
||||
for tag in ["lockdata"]:
|
||||
existing = tvshow.find(tag)
|
||||
if existing is not None:
|
||||
tvshow.remove(existing)
|
||||
|
||||
# Remove ALL existing uniqueid with type="imdb" regardless of attributes
|
||||
for uniqueid in tvshow.findall("uniqueid[@type='imdb']"):
|
||||
tvshow.remove(uniqueid)
|
||||
|
||||
except (ET.ParseError, ValueError) as e:
|
||||
print(f"Warning: Could not parse existing tvshow NFO {nfo_path}, creating new: {e}")
|
||||
tvshow = ET.Element("tvshow")
|
||||
else:
|
||||
# Create new NFO structure
|
||||
tvshow = ET.Element("tvshow")
|
||||
|
||||
# Add NFOGuard fields at the bottom
|
||||
|
||||
# Add IMDb uniqueid at the end
|
||||
imdb_uniqueid = ET.SubElement(tvshow, "uniqueid", type="imdb", default="true")
|
||||
imdb_uniqueid.text = imdb_id
|
||||
|
||||
# Add TVDB ID if available
|
||||
if tvdb_id:
|
||||
# Add TVDB ID if available (preserve existing or add new)
|
||||
if tvdb_id and not tvshow.find("uniqueid[@type='tvdb']"):
|
||||
tvdb_uniqueid = ET.SubElement(tvshow, "uniqueid", type="tvdb")
|
||||
tvdb_uniqueid.text = tvdb_id
|
||||
|
||||
# Add source comment
|
||||
comment = ET.Comment(f" Created by {self.manager_brand} ")
|
||||
tvshow.insert(0, comment)
|
||||
# Add lockdata at the very end
|
||||
lockdata = ET.SubElement(tvshow, "lockdata")
|
||||
lockdata.text = "true"
|
||||
|
||||
# Write file
|
||||
# Add NFOGuard comment at the beginning
|
||||
comment_text = f" Created by {self.manager_brand} "
|
||||
|
||||
# Write file with proper formatting
|
||||
tree = ET.ElementTree(tvshow)
|
||||
ET.indent(tree, space=" ", level=0)
|
||||
tree.write(nfo_path, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
# Write to string first to add comment
|
||||
xml_str = ET.tostring(tvshow, encoding='unicode')
|
||||
|
||||
# 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)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating tvshow NFO {nfo_path}: {e}")
|
||||
print(f"Error creating/updating tvshow NFO {nfo_path}: {e}")
|
||||
|
||||
def create_season_nfo(self, season_dir: Path, season_number: int) -> None:
|
||||
"""Create season.nfo file"""
|
||||
"""Create or update season.nfo file preserving existing content"""
|
||||
nfo_path = season_dir / "season.nfo"
|
||||
|
||||
try:
|
||||
season_dir.mkdir(exist_ok=True)
|
||||
|
||||
season = ET.Element("season")
|
||||
# Try to load existing NFO file
|
||||
if nfo_path.exists():
|
||||
try:
|
||||
tree = ET.parse(nfo_path)
|
||||
season = tree.getroot()
|
||||
|
||||
# Ensure root element is <season>
|
||||
if season.tag != "season":
|
||||
raise ValueError("Root element is not <season>")
|
||||
|
||||
# Remove existing NFOGuard-managed elements
|
||||
for tag in ["seasonnumber", "lockdata"]:
|
||||
existing = season.find(tag)
|
||||
if existing is not None:
|
||||
season.remove(existing)
|
||||
|
||||
except (ET.ParseError, ValueError) as e:
|
||||
print(f"Warning: Could not parse existing season NFO {nfo_path}, creating new: {e}")
|
||||
season = ET.Element("season")
|
||||
else:
|
||||
# Create new NFO structure
|
||||
season = ET.Element("season")
|
||||
|
||||
# Add NFOGuard fields at the bottom
|
||||
seasonnumber = ET.SubElement(season, "seasonnumber")
|
||||
seasonnumber.text = str(season_number)
|
||||
|
||||
# Add source comment
|
||||
comment = ET.Comment(f" Created by {self.manager_brand} ")
|
||||
season.insert(0, comment)
|
||||
# Add lockdata at the end
|
||||
lockdata = ET.SubElement(season, "lockdata")
|
||||
lockdata.text = "true"
|
||||
|
||||
# Write file
|
||||
# Add NFOGuard comment at the beginning
|
||||
comment_text = f" Created by {self.manager_brand} "
|
||||
|
||||
# Write file with proper formatting
|
||||
tree = ET.ElementTree(season)
|
||||
ET.indent(tree, space=" ", level=0)
|
||||
tree.write(nfo_path, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
# Write to string first to add comment
|
||||
xml_str = ET.tostring(season, encoding='unicode')
|
||||
|
||||
# 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)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating season NFO {nfo_path}: {e}")
|
||||
print(f"Error creating/updating season NFO {nfo_path}: {e}")
|
||||
|
||||
def create_episode_nfo(self, season_dir: Path, season_num: int, episode_num: int,
|
||||
aired: Optional[str], dateadded: Optional[str], source: str,
|
||||
lock_metadata: bool = True, enhanced_metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""Create episode NFO file"""
|
||||
"""Create or update episode NFO file preserving existing content"""
|
||||
# Generate episode filename pattern
|
||||
episode_filename = f"S{season_num:02d}E{episode_num:02d}.nfo"
|
||||
nfo_path = season_dir / episode_filename
|
||||
|
||||
try:
|
||||
episode = ET.Element("episodedetails")
|
||||
# Try to load existing NFO file
|
||||
if nfo_path.exists():
|
||||
try:
|
||||
tree = ET.parse(nfo_path)
|
||||
episode = tree.getroot()
|
||||
|
||||
# Basic episode info
|
||||
# Ensure root element is <episodedetails>
|
||||
if episode.tag != "episodedetails":
|
||||
raise ValueError("Root element is not <episodedetails>")
|
||||
|
||||
# Remove existing NFOGuard-managed elements to avoid duplicates
|
||||
# These will be re-added at the bottom
|
||||
nfoguard_fields = ["aired", "dateadded", "lockdata", "season", "episode"]
|
||||
for tag in nfoguard_fields:
|
||||
existing = episode.find(tag)
|
||||
if existing is not None:
|
||||
# Store the aired value before removing
|
||||
if tag == "aired" and not aired:
|
||||
aired = existing.text # Preserve existing aired date
|
||||
episode.remove(existing)
|
||||
|
||||
except (ET.ParseError, ValueError) as e:
|
||||
print(f"Warning: Could not parse existing episode NFO {nfo_path}, creating new: {e}")
|
||||
episode = ET.Element("episodedetails")
|
||||
else:
|
||||
# Create new NFO structure
|
||||
episode = ET.Element("episodedetails")
|
||||
|
||||
# Enhanced metadata should be preserved - only add if not already present
|
||||
if enhanced_metadata:
|
||||
if enhanced_metadata.get("title") and not episode.find("title"):
|
||||
title_elem = ET.SubElement(episode, "title")
|
||||
title_elem.text = enhanced_metadata["title"]
|
||||
|
||||
if enhanced_metadata.get("overview") and not episode.find("plot"):
|
||||
plot_elem = ET.SubElement(episode, "plot")
|
||||
plot_elem.text = enhanced_metadata["overview"]
|
||||
|
||||
if enhanced_metadata.get("runtime") and not episode.find("runtime"):
|
||||
runtime_elem = ET.SubElement(episode, "runtime")
|
||||
runtime_elem.text = str(enhanced_metadata["runtime"])
|
||||
|
||||
# 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
|
||||
# Dates at the end
|
||||
if aired:
|
||||
aired_elem = ET.SubElement(episode, "aired")
|
||||
aired_elem.text = aired[:10] if len(aired) >= 10 else aired
|
||||
@@ -205,36 +321,30 @@ class NFOManager:
|
||||
dateadded_elem = ET.SubElement(episode, "dateadded")
|
||||
dateadded_elem.text = dateadded
|
||||
|
||||
# Enhanced metadata if available
|
||||
if enhanced_metadata:
|
||||
if enhanced_metadata.get("title"):
|
||||
title_elem = ET.SubElement(episode, "title")
|
||||
title_elem.text = enhanced_metadata["title"]
|
||||
|
||||
if enhanced_metadata.get("overview"):
|
||||
plot_elem = ET.SubElement(episode, "plot")
|
||||
plot_elem.text = enhanced_metadata["overview"]
|
||||
|
||||
if enhanced_metadata.get("runtime"):
|
||||
runtime_elem = ET.SubElement(episode, "runtime")
|
||||
runtime_elem.text = str(enhanced_metadata["runtime"])
|
||||
|
||||
# Add source comment
|
||||
comment = ET.Comment(f" Created by {self.manager_brand} - Source: {source} ")
|
||||
episode.insert(0, comment)
|
||||
|
||||
# Add lockdata if requested
|
||||
# Add lockdata at the very end
|
||||
if lock_metadata:
|
||||
lockdata = ET.SubElement(episode, "lockdata")
|
||||
lockdata.text = "true"
|
||||
|
||||
# Write file
|
||||
# Add NFOGuard comment at the beginning
|
||||
comment_text = f" Created by {self.manager_brand} - Source: {source} "
|
||||
|
||||
# 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)
|
||||
|
||||
# Write to string first to add comment
|
||||
xml_str = ET.tostring(episode, encoding='unicode')
|
||||
|
||||
# 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)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating episode NFO {nfo_path}: {e}")
|
||||
print(f"Error creating/updating episode NFO {nfo_path}: {e}")
|
||||
|
||||
def set_file_mtime(self, file_path: Path, iso_timestamp: str) -> None:
|
||||
"""Set file modification time to match import date"""
|
||||
|
||||
Reference in New Issue
Block a user