diff --git a/Emby-DLL/NFOGuard.Emby.Plugin.dll b/Emby-DLL/NFOGuard.Emby.Plugin.dll index aa3a736..c215455 100644 Binary files a/Emby-DLL/NFOGuard.Emby.Plugin.dll and b/Emby-DLL/NFOGuard.Emby.Plugin.dll differ 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)