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"""