#!/usr/bin/env python3 """ NFO Manager for creating and managing metadata files Handles NFO creation for movies, TV shows, seasons, and episodes """ import os import xml.etree.ElementTree as ET from pathlib import Path from datetime import datetime from typing import Optional, Dict, Any import re class NFOManager: """Manages NFO file creation and updates""" def __init__(self, manager_brand: str = "NFOGuard"): self.manager_brand = manager_brand def parse_imdb_from_path(self, path: Path) -> Optional[str]: """Extract IMDb ID from directory path""" # Look for [imdb-ttXXXXXXX] or [ttXXXXXXX] patterns path_str = str(path).lower() # Try [imdb-ttXXXXXXX] format first match = re.search(r'\[imdb-?(tt\d+)\]', path_str) if match: return match.group(1) # Try standalone [ttXXXXXXX] format match = re.search(r'\[(tt\d+)\]', path_str) if match: return match.group(1) return None def create_movie_nfo(self, movie_dir: Path, imdb_id: str, dateadded: str, released: Optional[str] = None, source: str = "unknown", lock_metadata: bool = True) -> None: """Create or update movie.nfo file preserving existing content""" nfo_path = movie_dir / "movie.nfo" try: # Try to load existing NFO file if nfo_path.exists(): try: tree = ET.parse(nfo_path) movie = tree.getroot() # Ensure root element is if movie.tag != "movie": raise ValueError("Root element is not ") # Remove existing NFOGuard elements to avoid duplicates for elem in movie.findall(".//comment()"): if elem is not None and "NFOGuard" in str(elem): movie.remove(elem) # Remove existing NFOGuard-managed elements for tag in ["dateadded", "lockdata"]: existing = movie.find(tag) if existing is not None: movie.remove(existing) # Remove any existing uniqueid with type="imdb" for uniqueid in movie.findall("uniqueid[@type='imdb']"): movie.remove(uniqueid) except (ET.ParseError, ValueError) as e: print(f"Warning: Could not parse existing NFO {nfo_path}, creating new: {e}") movie = ET.Element("movie") else: # Create new NFO structure movie = ET.Element("movie") # Add/update NFOGuard comment comment = ET.Comment(f" Created by {self.manager_brand} - Source: {source} ") movie.insert(0, comment) # Add/update IMDb uniqueid uniqueid = ET.SubElement(movie, "uniqueid", type="imdb", default="true") uniqueid.text = imdb_id # Add dateadded if dateadded: dateadded_elem = ET.SubElement(movie, "dateadded") dateadded_elem.text = dateadded # Add premiered if we have release date (only if not already present) if released and movie.find("premiered") is None: premiered_elem = ET.SubElement(movie, "premiered") premiered_elem.text = released[:10] if len(released) >= 10 else released # Add lockdata if requested if lock_metadata: lockdata = ET.SubElement(movie, "lockdata") lockdata.text = "true" # Write file tree = ET.ElementTree(movie) ET.indent(tree, space=" ", level=0) tree.write(nfo_path, encoding="utf-8", xml_declaration=True) except Exception as e: 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""" nfo_path = series_dir / "tvshow.nfo" try: tvshow = ET.Element("tvshow") # Add IMDb ID imdb_uniqueid = ET.SubElement(tvshow, "uniqueid", type="imdb", default="true") imdb_uniqueid.text = imdb_id # Add TVDB ID if available if tvdb_id: 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) # Write file tree = ET.ElementTree(tvshow) ET.indent(tree, space=" ", level=0) tree.write(nfo_path, encoding="utf-8", xml_declaration=True) except Exception as e: print(f"Error creating tvshow NFO {nfo_path}: {e}") def create_season_nfo(self, season_dir: Path, season_number: int) -> None: """Create season.nfo file""" nfo_path = season_dir / "season.nfo" try: season_dir.mkdir(exist_ok=True) season = ET.Element("season") 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) # Write file tree = ET.ElementTree(season) ET.indent(tree, space=" ", level=0) tree.write(nfo_path, encoding="utf-8", xml_declaration=True) except Exception as e: print(f"Error creating 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""" # 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") # Basic episode info 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 if aired: aired_elem = ET.SubElement(episode, "aired") aired_elem.text = aired[:10] if len(aired) >= 10 else aired if dateadded: 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 if lock_metadata: lockdata = ET.SubElement(episode, "lockdata") lockdata.text = "true" # Write file tree = ET.ElementTree(episode) ET.indent(tree, space=" ", level=0) tree.write(nfo_path, encoding="utf-8", xml_declaration=True) except Exception as e: print(f"Error creating 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""" try: # Parse ISO timestamp if iso_timestamp.endswith('Z'): dt = datetime.fromisoformat(iso_timestamp.replace('Z', '+00:00')) elif '+' in iso_timestamp or 'T' in iso_timestamp: dt = datetime.fromisoformat(iso_timestamp) else: # Assume it's already a simple date dt = datetime.fromisoformat(iso_timestamp + 'T00:00:00') # Convert to timestamp timestamp = dt.timestamp() # Set both access and modification times os.utime(file_path, (timestamp, timestamp)) except Exception as e: print(f"Error setting mtime for {file_path}: {e}") def update_movie_files_mtime(self, movie_dir: Path, iso_timestamp: str) -> None: """Update modification times for all video files in movie directory""" video_exts = (".mkv", ".mp4", ".avi", ".mov", ".m4v") for file_path in movie_dir.iterdir(): if file_path.is_file() and file_path.suffix.lower() in video_exts: self.set_file_mtime(file_path, iso_timestamp)