diff --git a/SUMMARY.md b/SUMMARY.md
index bb56df8..2bdc96e 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -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
-
- Annabelle Comes Home
- Plot content...
- 521029
- Horror
- Gary Dauberman
+
+ Movie Title
+ Plot...
...
-
-
+
tt8350360
2019-06-26
2019
@@ -29,12 +27,44 @@
```
-### 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
+
+
+ Show Title
+ Show plot...
+ ...
+
+
+ tt1234567
+ 12345
+ true
+
+```
+
+#### Episode NFO Structure
+```xml
+
+
+ Episode Title
+ Episode plot...
+ ...
+
+
+ 1
+ 5
+ 2019-06-26
+ 2025-09-14T13:50:34-04:00
+ true
+
+```
+
+### 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)
diff --git a/VERSION b/VERSION
index 88c5fb8..347f583 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.4.0
+1.4.1
diff --git a/core/nfo_manager.py b/core/nfo_manager.py
index 2ee5b65..ef30d14 100644
--- a/core/nfo_manager.py
+++ b/core/nfo_manager.py
@@ -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()
+
+ # Ensure root element is
+ if tvshow.tag != "tvshow":
+ raise ValueError("Root element is not ")
+
+ # 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 IMDb ID
+ # 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'\n\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
+ if season.tag != "season":
+ raise ValueError("Root element is not ")
+
+ # 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'\n\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()
+
+ # Ensure root element is
+ if episode.tag != "episodedetails":
+ raise ValueError("Root element is not ")
+
+ # 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")
- # Basic episode info
+ # 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'\n\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"""