feat: enhance NFO processing and movie detection

* fix: NFO processing overhaul - looking GOOD!

  Listen up, buddy! Your boy Cat just made this NFO processing
  smoother than my pompadour on a good hair day!

  WHAT'S THE DEAL?
  • Movie NFOs were getting DISSED when dates went missing
  • Episode NFOs with long names were cluttering up the joint
  • Nobody wants a messy media library - that's just WRONG!

  WHAT I FIXED (because I'm THAT good):
  • Movies: NFO processing now happens FIRST, baby! No more
    skipping when dates are wonky - we standardize EVERYTHING!
  • TV Shows: Long episode NFO names? GONE! We migrate that
    metadata to proper S##E## format and trash the old junk
  • Dates get moved to the bottom where they belong (it's all
    about the STYLE!)
  • NFOGuard signatures added to everything (gotta sign your work!)

  FILES TOUCHED:
  • nfoguard.py:1008-1025 - Fixed that early exit nonsense
  • core/nfo_manager.py - Added find_existing_episode_nfo() function
  • core/nfo_manager.py - Enhanced create_episode_nfo() with migration

  RESULTS:
   Existing movie.nfo files get the full treatment
   Long episode NFO names become sleek S##E## format
   All metadata preserved (we're not ANIMALS!)
   Dates organized properly at the bottom
   NFOGuard branding on everything

  This code is now looking SO good, it should be in GQ!
  What's not to like about perfection?

* testing: Testing database date adds for old files

* database debuging

* fixing database calls as well as ID for imdb

* fix: dirs with no imdb in title

movies with no imdb getting skipped

* update: fix core parsing
This commit is contained in:
sbcrumb
2025-09-20 20:24:02 -04:00
committed by GitHub
parent d5937b13be
commit cd22cd3340
4 changed files with 184 additions and 18 deletions
Binary file not shown.
+6
View File
@@ -173,6 +173,7 @@ class NFOGuardDatabase:
def upsert_movie_dates(self, imdb_id: str, released: Optional[str],
dateadded: Optional[str], source: str, has_video_file: bool = False):
"""Insert or update movie date record"""
print(f"🔍 DATABASE UPSERT: imdb_id={imdb_id}, dateadded={dateadded}, source={source}")
with self.get_connection() as conn:
cursor = conn.cursor()
# Use INSERT OR REPLACE to ensure we always update the dates properly
@@ -185,6 +186,11 @@ class NFOGuardDatabase:
)
""", (imdb_id, imdb_id, released, dateadded, source, has_video_file, datetime.utcnow().isoformat()))
# Debug: Check what was actually saved
cursor.execute("SELECT dateadded, source FROM movies WHERE imdb_id = ?", (imdb_id,))
result = cursor.fetchone()
print(f"🔍 DATABASE VERIFY: After upsert, found dateadded={result[0] if result else 'NOT_FOUND'}, source={result[1] if result else 'NOT_FOUND'}")
def get_series_episodes(self, imdb_id: str, has_video_file_only: bool = False) -> List[Dict]:
"""Get all episodes for a series"""
with self.get_connection() as conn:
+155 -5
View File
@@ -49,6 +49,93 @@ class NFOManager:
return None
def parse_imdb_from_nfo(self, nfo_path: Path) -> Optional[str]:
"""Extract IMDb ID from NFO file content"""
if not nfo_path.exists():
return None
try:
tree = ET.parse(nfo_path)
root = tree.getroot()
# Check for <uniqueid type="imdb">ttXXXXXX</uniqueid>
imdb_uniqueid = root.find('.//uniqueid[@type="imdb"]')
if imdb_uniqueid is not None and imdb_uniqueid.text:
imdb_id = imdb_uniqueid.text.strip()
if imdb_id.startswith('tt'):
return imdb_id
# Check for legacy <imdbid>ttXXXXXX</imdbid>
imdbid_elem = root.find('.//imdbid')
if imdbid_elem is not None and imdbid_elem.text:
imdb_id = imdbid_elem.text.strip()
if imdb_id.startswith('tt'):
return imdb_id
# Check for legacy <imdb>ttXXXXXX</imdb>
imdb_elem = root.find('.//imdb')
if imdb_elem is not None and imdb_elem.text:
imdb_id = imdb_elem.text.strip()
if imdb_id.startswith('tt'):
return imdb_id
except (ET.ParseError, Exception):
# Skip corrupted or non-XML files
pass
return None
def find_movie_imdb_id(self, movie_dir: Path) -> Optional[str]:
"""Find IMDb ID from directory name, filenames, or NFO file"""
# First try directory name
imdb_id = self.parse_imdb_from_path(movie_dir)
if imdb_id:
return imdb_id
# Try all files in the directory for IMDb ID patterns
for file_path in movie_dir.iterdir():
if file_path.is_file():
imdb_id = self.parse_imdb_from_path(file_path)
if imdb_id:
return imdb_id
# Finally, try NFO file content
nfo_path = movie_dir / "movie.nfo"
imdb_id = self.parse_imdb_from_nfo(nfo_path)
if imdb_id:
print(f"🔍 Found IMDb ID in NFO file: {imdb_id} from {nfo_path}")
return imdb_id
return None
def _parse_nfo_with_tolerance(self, nfo_path: Path):
"""Parse NFO file with tolerance for URLs appended after XML"""
try:
# First try normal parsing
tree = ET.parse(nfo_path)
return tree.getroot()
except ET.ParseError as e:
# If parsing fails, try to extract just the XML part
try:
with open(nfo_path, 'r', encoding='utf-8') as f:
content = f.read()
# Find the last </movie> tag and truncate after it
last_movie_end = content.rfind('</movie>')
if last_movie_end != -1:
xml_content = content[:last_movie_end + 8] # +8 for </movie>
# Try parsing the truncated content
root = ET.fromstring(xml_content)
print(f"✅ Successfully parsed NFO after removing trailing content: {nfo_path.name}")
return root
else:
# Re-raise original error if we can't find </movie>
raise e
except Exception:
# Re-raise original error if our fix attempt fails
raise e
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:
@@ -59,8 +146,8 @@ class NFOManager:
# Try to load existing NFO file
if nfo_path.exists():
try:
tree = ET.parse(nfo_path)
movie = tree.getroot()
# Try to parse the XML, handling URLs appended after </movie>
movie = self._parse_nfo_with_tolerance(nfo_path)
# Ensure root element is <movie>
if movie.tag != "movie":
@@ -279,6 +366,48 @@ class NFOManager:
except Exception as e:
print(f"❌ Error creating/updating season NFO {nfo_path}: {e}")
def find_existing_episode_nfo(self, season_dir: Path, season_num: int, episode_num: int) -> Optional[Path]:
"""Find existing episode NFO file that matches season/episode but isn't standardized name"""
if not season_dir.exists():
return None
# Standard filename pattern we're looking to create
standard_pattern = f"S{season_num:02d}E{episode_num:02d}.nfo"
# Look for NFO files in the season directory
for nfo_file in season_dir.glob("*.nfo"):
# Skip if it's already the standard format
if nfo_file.name == standard_pattern:
continue
# Check if this NFO contains the right season/episode
try:
tree = ET.parse(nfo_file)
root = tree.getroot()
if root.tag == "episodedetails":
# Check for season/episode elements
season_elem = root.find("season")
episode_elem = root.find("episode")
if (season_elem is not None and episode_elem is not None and
season_elem.text and episode_elem.text):
try:
file_season = int(season_elem.text)
file_episode = int(episode_elem.text)
if file_season == season_num and file_episode == episode_num:
print(f"🔍 Found existing episode NFO: {nfo_file.name} -> will migrate to {standard_pattern}")
return nfo_file
except ValueError:
continue
except (ET.ParseError, Exception):
# Skip corrupted or non-XML files
continue
return None
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:
@@ -287,17 +416,30 @@ class NFOManager:
episode_filename = f"S{season_num:02d}E{episode_num:02d}.nfo"
nfo_path = season_dir / episode_filename
# Track if we need to delete an old long-named NFO file
old_nfo_to_delete = None
try:
# Try to load existing NFO file
if nfo_path.exists():
# First, check for existing long-named NFO files that need migration
existing_long_nfo = self.find_existing_episode_nfo(season_dir, season_num, episode_num)
# Try to load existing NFO file (either standard or long-named)
source_nfo_path = nfo_path if nfo_path.exists() else existing_long_nfo
if source_nfo_path:
try:
tree = ET.parse(nfo_path)
tree = ET.parse(source_nfo_path)
episode = tree.getroot()
# Ensure root element is <episodedetails>
if episode.tag != "episodedetails":
raise ValueError("Root element is not <episodedetails>")
# If we're migrating from a long-named file, mark it for deletion
if existing_long_nfo and source_nfo_path == existing_long_nfo:
old_nfo_to_delete = existing_long_nfo
print(f"📦 Migrating episode NFO: {existing_long_nfo.name} -> {episode_filename}")
# Remove existing NFOGuard-managed elements to avoid duplicates
# These will be re-added at the bottom
nfoguard_fields = ["aired", "dateadded", "lockdata", "season", "episode"]
@@ -374,6 +516,14 @@ class NFOManager:
print(f"✅ Successfully created/updated episode NFO: {nfo_path}")
print(f" S{season_num:02d}E{episode_num:02d}, Aired: {aired}, Date Added: {dateadded}")
# Clean up old long-named NFO file if we migrated from it
if old_nfo_to_delete and old_nfo_to_delete.exists():
try:
old_nfo_to_delete.unlink()
print(f"🗑️ Cleaned up old NFO file: {old_nfo_to_delete.name}")
except Exception as cleanup_error:
print(f"⚠️ Warning: Could not delete old NFO file {old_nfo_to_delete.name}: {cleanup_error}")
except Exception as e:
print(f"❌ Error creating/updating episode NFO {nfo_path}: {e}")
+22 -12
View File
@@ -949,9 +949,9 @@ class MovieProcessor:
def process_movie(self, movie_path: Path, webhook_mode: bool = False) -> None:
"""Process a movie directory"""
imdb_id = self.nfo_manager.parse_imdb_from_path(movie_path)
imdb_id = self.nfo_manager.find_movie_imdb_id(movie_path)
if not imdb_id:
_log("ERROR", f"No IMDb ID found in movie path: {movie_path}")
_log("ERROR", f"No IMDb ID found in movie directory, filenames, or NFO file: {movie_path}")
return
_log("INFO", f"Processing movie: {movie_path.name} (IMDb: {imdb_id})")
@@ -1005,24 +1005,34 @@ class MovieProcessor:
# Use existing movie date decision logic
dateadded, source, released = self._decide_movie_dates(imdb_id, movie_path, should_query, existing)
# Skip processing if no valid date found and file dates disabled
if dateadded is None:
_log("WARNING", f"Skipping movie {movie_path.name} - no valid date source available")
self.db.upsert_movie_dates(imdb_id, released, None, source, True)
return
# Create NFO
# Create NFO regardless of date availability (preserves existing metadata)
if config.manage_nfo:
self.nfo_manager.create_movie_nfo(
movie_path, imdb_id, dateadded, released, source, config.lock_metadata
)
# Update file mtimes
# Skip remaining processing if no valid date found and file dates disabled
if dateadded is None:
_log("WARNING", f"Movie {movie_path.name} - no valid date source available, but NFO was still processed")
self.db.upsert_movie_dates(imdb_id, released, None, source, True)
return
_log("DEBUG", f"Movie {movie_path.name} proceeding to save: dateadded={dateadded}, source={source}")
# Update file mtimes (only if we have a valid date)
if config.fix_dir_mtimes and dateadded and dateadded != "MANUAL_REVIEW_NEEDED":
self.nfo_manager.update_movie_files_mtime(movie_path, dateadded)
_log("DEBUG", f"Movie processing reached file mtime section: fix_dir_mtimes={config.fix_dir_mtimes}, dateadded={dateadded}")
# Save to database
_log("DEBUG", f"About to save to database: imdb_id={imdb_id}, dateadded={dateadded}")
try:
self.db.upsert_movie_dates(imdb_id, released, dateadded, source, True)
_log("DEBUG", f"Database save completed for {imdb_id}")
except Exception as e:
_log("ERROR", f"Database save failed for {imdb_id}: {e}")
raise
_log("INFO", f"Completed processing movie: {movie_path.name} (source: {source})")
@@ -1874,7 +1884,7 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
_log("INFO", f"Scanning movies in: {scan_path}")
movie_count = 0
for item in scan_path.iterdir():
if item.is_dir() and nfo_manager.parse_imdb_from_path(item):
if item.is_dir() and nfo_manager.find_movie_imdb_id(item):
movie_count += 1
_log("INFO", f"Processing movie: {item.name}")
try:
@@ -1986,7 +1996,7 @@ async def test_movie_scan():
if path.exists():
for item in path.iterdir():
if item.is_dir() and nfo_manager.parse_imdb_from_path(item):
if item.is_dir() and nfo_manager.find_movie_imdb_id(item):
path_result["movies_found"] += 1
results.append(path_result)