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)