From cd22cd3340fb2a43743d28a8fa5cdd52dce6ed11 Mon Sep 17 00:00:00 2001 From: sbcrumb <5639759+sbcrumb@users.noreply.github.com> Date: Sat, 20 Sep 2025 20:24:02 -0400 Subject: [PATCH] feat: enhance NFO processing and movie detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- Emby-DLL/NFOGuard.Emby.Plugin.dll | Bin 1027072 -> 1028608 bytes core/database.py | 6 ++ core/nfo_manager.py | 160 +++++++++++++++++++++++++++++- nfoguard.py | 36 ++++--- 4 files changed, 184 insertions(+), 18 deletions(-) diff --git a/Emby-DLL/NFOGuard.Emby.Plugin.dll b/Emby-DLL/NFOGuard.Emby.Plugin.dll index aa3a7360601105e65e6589e2dd21612bfb3e6d44..c21545562d3a8c5df4b23f57ab35564051749c86 100644 GIT binary patch delta 29922 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 diff --git a/core/database.py b/core/database.py index 33f2905..80503de 100644 --- a/core/database.py +++ b/core/database.py @@ -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 @@ -184,6 +185,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""" diff --git a/core/nfo_manager.py b/core/nfo_manager.py index 71f45c7..8baac7c 100644 --- a/core/nfo_manager.py +++ b/core/nfo_manager.py @@ -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 ttXXXXXX + 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 ttXXXXXX + 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 ttXXXXXX + 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 tag and truncate after it + last_movie_end = content.rfind('') + if last_movie_end != -1: + xml_content = content[:last_movie_end + 8] # +8 for + + # 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 + 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 = self._parse_nfo_with_tolerance(nfo_path) # Ensure root element is 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 if episode.tag != "episodedetails": raise ValueError("Root element is not ") + # 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}") diff --git a/nfoguard.py b/nfoguard.py index 1940419..2b1bd82 100644 --- a/nfoguard.py +++ b/nfoguard.py @@ -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 - self.db.upsert_movie_dates(imdb_id, released, dateadded, source, True) + _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)