244 lines
9.9 KiB
Python
244 lines
9.9 KiB
Python
#!/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 <movie>
|
|
if movie.tag != "movie":
|
|
raise ValueError("Root element is not <movie>")
|
|
|
|
# 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) |