refactor: update and remove anything to do with NFO files
Local Docker Build (Dev) / build-dev (push) Successful in 35s
Local Docker Build (Dev) / build-dev (push) Successful in 35s
moving to an all DB approach since radarr overwrites .nfo files
This commit is contained in:
+42
-319
@@ -11,12 +11,12 @@ from datetime import datetime, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from core.database import NFOGuardDatabase
|
||||
from core.nfo_manager import NFOManager
|
||||
from core.path_mapper import PathMapper
|
||||
from clients.radarr_client import RadarrClient
|
||||
from clients.external_clients import ExternalClientManager
|
||||
from config.settings import config
|
||||
from utils.logging import _log
|
||||
from utils.imdb_utils import find_imdb_in_directory # Phase 3: Replaced NFOManager
|
||||
from utils.file_utils import find_media_path_by_imdb_and_title
|
||||
|
||||
|
||||
@@ -68,9 +68,9 @@ def convert_utc_to_local(utc_iso_string: str) -> str:
|
||||
class MovieProcessor:
|
||||
"""Handles movie processing"""
|
||||
|
||||
def __init__(self, db: NFOGuardDatabase, nfo_manager: NFOManager, path_mapper: PathMapper):
|
||||
def __init__(self, db: NFOGuardDatabase, nfo_manager, path_mapper: PathMapper):
|
||||
# nfo_manager parameter kept for backward compatibility but no longer used (Phase 3)
|
||||
self.db = db
|
||||
self.nfo_manager = nfo_manager
|
||||
self.path_mapper = path_mapper
|
||||
self.radarr = RadarrClient(
|
||||
os.environ.get("RADARR_URL", ""),
|
||||
@@ -153,7 +153,7 @@ class MovieProcessor:
|
||||
|
||||
def process_movie(self, movie_path: Path, webhook_mode: bool = False, force_scan: bool = False, scan_mode: str = "smart", shutdown_event=None) -> str:
|
||||
"""Process a movie directory"""
|
||||
imdb_id = self.nfo_manager.find_movie_imdb_id(movie_path)
|
||||
imdb_id = find_imdb_in_directory(movie_path) # Phase 3: Using imdb_utils instead of NFOManager
|
||||
if not imdb_id:
|
||||
_log("ERROR", f"No IMDb ID found in movie directory, filenames, or NFO file: {movie_path}")
|
||||
return "error"
|
||||
@@ -225,106 +225,16 @@ class MovieProcessor:
|
||||
if released and hasattr(released, 'isoformat'):
|
||||
released = released.isoformat()
|
||||
|
||||
# Create NFO with existing data and update files
|
||||
if config.manage_nfo:
|
||||
self.nfo_manager.create_movie_nfo(
|
||||
movie_path, imdb_id, dateadded, released, source, config.lock_metadata
|
||||
)
|
||||
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
self.nfo_manager.update_movie_files_mtime(movie_path, dateadded)
|
||||
# NFO file operations removed - database is now the single source of truth
|
||||
# (Phase 1: Remove NFO file write operations)
|
||||
|
||||
_log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [database-cached]")
|
||||
return "processed"
|
||||
else:
|
||||
_log("INFO", f"🔍 TIER 1 SKIP - {imdb_id}: Database incomplete, proceeding to Tier 2")
|
||||
|
||||
# TIER 2: Check if NFO file has NFOGuard data and cache it in database
|
||||
nfo_path = movie_path / "movie.nfo"
|
||||
_log("INFO", f"🔍 TIER 2 - Checking NFO file: {nfo_path}")
|
||||
_log("INFO", f"🔍 TIER 2 - NFO exists: {nfo_path.exists()}")
|
||||
|
||||
nfo_data = self.nfo_manager.extract_nfoguard_dates_from_nfo(nfo_path)
|
||||
_log("INFO", f"🔍 TIER 2 - NFOGuard data extracted: {nfo_data}")
|
||||
|
||||
if nfo_data:
|
||||
_log("INFO", f"🚀 TIER 2 - Found NFOGuard data in NFO file: {nfo_data['dateadded']} (source: {nfo_data['source']})")
|
||||
dateadded = nfo_data["dateadded"]
|
||||
source = nfo_data["source"]
|
||||
released = nfo_data.get("released")
|
||||
|
||||
# Cache NFO data in database for future lookups
|
||||
# Fixed parameter order: imdb_id, released, dateadded, source
|
||||
self.db.upsert_movie_dates(imdb_id, released, dateadded, source, True)
|
||||
_log("INFO", f"✅ Cached NFO data in database for {imdb_id}")
|
||||
|
||||
# Update file mtimes if enabled (NFO is already correct)
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
self.nfo_manager.update_movie_files_mtime(movie_path, dateadded)
|
||||
|
||||
_log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [nfo-cached]")
|
||||
return "processed"
|
||||
|
||||
# TIER 2.5: Check for any existing valid date data in NFO (even without lockdata marker)
|
||||
# Only use NFO dates if prioritize_nfo is enabled, otherwise check external APIs first
|
||||
existing_nfo_data = self._extract_any_valid_dates_from_nfo(nfo_path)
|
||||
if existing_nfo_data and config.manual_scan_prioritize_nfo:
|
||||
_log("INFO", f"🔍 TIER 2.5 - Found existing date data in NFO (no lockdata): {existing_nfo_data['dateadded']} (source: {existing_nfo_data['source']})")
|
||||
_log("INFO", f"⚡ MANUAL_SCAN_PRIORITIZE_NFO=True - Using NFO date for speed")
|
||||
dateadded = existing_nfo_data["dateadded"]
|
||||
source = existing_nfo_data["source"]
|
||||
released = existing_nfo_data.get("released")
|
||||
|
||||
# Cache existing data in database and add proper NFOGuard formatting
|
||||
# Fixed parameter order: imdb_id, released, dateadded, source
|
||||
self.db.upsert_movie_dates(imdb_id, released, dateadded, source, True)
|
||||
_log("INFO", f"✅ Cached existing NFO data in database for {imdb_id}")
|
||||
|
||||
# Update NFO file to add NFOGuard formatting (lockdata, comment)
|
||||
if config.manage_nfo:
|
||||
self.nfo_manager.create_movie_nfo(
|
||||
movie_path, imdb_id, dateadded, released, source, config.lock_metadata
|
||||
)
|
||||
_log("INFO", f"✅ Added NFOGuard formatting to existing NFO for {imdb_id}")
|
||||
|
||||
# Update file mtimes if enabled
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
self.nfo_manager.update_movie_files_mtime(movie_path, dateadded)
|
||||
|
||||
_log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [existing-nfo-enhanced]")
|
||||
return "processed"
|
||||
elif existing_nfo_data:
|
||||
_log("INFO", f"🔍 TIER 2.5 - Found existing date data in NFO (no lockdata): {existing_nfo_data['dateadded']} (source: {existing_nfo_data['source']})")
|
||||
_log("INFO", f"🎯 MANUAL_SCAN_PRIORITIZE_NFO=False - Will verify against external APIs first")
|
||||
# Store NFO data as fallback but continue to TIER 3 to check external APIs
|
||||
nfo_fallback_data = existing_nfo_data
|
||||
|
||||
# TIER 1.5: Special handling for TMDB-only movies - extract dates from existing NFO
|
||||
if is_tmdb_fallback:
|
||||
tmdb_nfo_data = self._extract_dates_from_tmdb_nfo(nfo_path)
|
||||
if tmdb_nfo_data:
|
||||
_log("INFO", f"🎬 Using TMDB data from existing NFO file: {tmdb_nfo_data['dateadded']} (source: {tmdb_nfo_data['source']})")
|
||||
dateadded = tmdb_nfo_data["dateadded"]
|
||||
source = tmdb_nfo_data["source"]
|
||||
released = tmdb_nfo_data.get("released")
|
||||
|
||||
# Create NFO with NFOGuard fields added
|
||||
if config.manage_nfo:
|
||||
self.nfo_manager.create_movie_nfo(
|
||||
movie_path, imdb_id, dateadded, released, source, config.lock_metadata
|
||||
)
|
||||
|
||||
# Update file mtimes if enabled
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
self.nfo_manager.update_movie_files_mtime(movie_path, dateadded)
|
||||
|
||||
# Save to database
|
||||
self.db.upsert_movie_dates(imdb_id, released, dateadded, source, True)
|
||||
|
||||
_log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [tmdb-nfo]")
|
||||
return "processed"
|
||||
|
||||
# TIER 3: No cached data found - proceed with API lookups and verification
|
||||
# TIER 2: Query external APIs directly (NFO layer removed in Phase 2)
|
||||
_log("INFO", f"🔍 TIER 2 - No database cache, querying external APIs")
|
||||
|
||||
# Check for shutdown signal before expensive API operations
|
||||
if shutdown_event and shutdown_event.is_set():
|
||||
@@ -362,18 +272,8 @@ class MovieProcessor:
|
||||
final_source = f"{source}_as_dateadded" if source else "release_date_fallback"
|
||||
_log("INFO", f"Using release date as dateadded: {final_dateadded} (source: {final_source})")
|
||||
|
||||
# Create NFO regardless of date availability (preserves existing metadata)
|
||||
if config.debug:
|
||||
print(f"🔍 TIER3 - config.manage_nfo: {config.manage_nfo}")
|
||||
if config.manage_nfo:
|
||||
if config.debug:
|
||||
print(f"🔍 TIER3 - Calling create_movie_nfo with final_dateadded: {final_dateadded}")
|
||||
self.nfo_manager.create_movie_nfo(
|
||||
movie_path, imdb_id, final_dateadded, released, final_source, config.lock_metadata
|
||||
)
|
||||
else:
|
||||
if config.debug:
|
||||
print(f"❌ TIER3 - manage_nfo is disabled, skipping NFO creation")
|
||||
# NFO file operations removed - database is now the single source of truth
|
||||
# (Phase 1: Remove NFO file write operations)
|
||||
|
||||
# Skip remaining processing if no valid date found and file dates disabled
|
||||
if final_dateadded is None:
|
||||
@@ -387,9 +287,8 @@ class MovieProcessor:
|
||||
|
||||
_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)
|
||||
# File mtime operations removed - database is now the single source of truth
|
||||
# (Phase 1: Remove NFO file write operations)
|
||||
|
||||
_log("DEBUG", f"Movie processing reached file mtime section: fix_dir_mtimes={config.fix_dir_mtimes}, dateadded={dateadded}")
|
||||
|
||||
@@ -407,76 +306,34 @@ class MovieProcessor:
|
||||
return "processed"
|
||||
|
||||
def _process_movie_nfo_first(self, movie_path: Path, imdb_id: str, shutdown_event=None) -> str:
|
||||
"""Process movie for incomplete mode: Check NFO files first for missing dateadded elements"""
|
||||
_log("INFO", f"🔍 NFO-FIRST MODE: Checking movie for missing dateadded in NFO file: {movie_path.name}")
|
||||
|
||||
"""Process movie for incomplete mode: Database-first then API (NFO checks removed in Phase 2)"""
|
||||
_log("INFO", f"🔍 INCOMPLETE MODE: Checking movie for missing data: {movie_path.name}")
|
||||
|
||||
# Check for shutdown signal
|
||||
if shutdown_event and shutdown_event.is_set():
|
||||
_log("INFO", f"⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping movie NFO-first processing: {movie_path.name}")
|
||||
_log("INFO", f"⚠️ SHUTDOWN SIGNAL RECEIVED - Stopping movie processing: {movie_path.name}")
|
||||
return "shutdown"
|
||||
|
||||
nfo_path = movie_path / "movie.nfo"
|
||||
|
||||
# STEP 1: Check if NFO file exists and has dateadded
|
||||
_log("DEBUG", f"STEP 1 - Checking NFO file for missing dateadded: {nfo_path}")
|
||||
_log("INFO", f"🔍 NFO exists: {nfo_path.exists()}")
|
||||
|
||||
if nfo_path.exists():
|
||||
nfo_data = self.nfo_manager.extract_nfoguard_dates_from_nfo(nfo_path)
|
||||
_log("INFO", f"🔍 NFOGuard data extracted: {nfo_data}")
|
||||
|
||||
if nfo_data and nfo_data.get('dateadded'):
|
||||
# NFO has dateadded - this movie is complete
|
||||
_log("INFO", f"✅ NFO has dateadded={nfo_data['dateadded']}, movie marked as complete")
|
||||
dateadded = nfo_data["dateadded"]
|
||||
source = nfo_data["source"]
|
||||
released = nfo_data.get("released")
|
||||
|
||||
# Cache NFO data in database for future lookups
|
||||
self.db.upsert_movie_dates(imdb_id, released, dateadded, source, True)
|
||||
|
||||
# Update file mtimes if needed
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
self.nfo_manager.update_movie_files_mtime(movie_path, dateadded)
|
||||
|
||||
_log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [nfo-complete]")
|
||||
return "processed"
|
||||
else:
|
||||
# NFO exists but missing dateadded
|
||||
_log("INFO", f"🔍 NFO exists but missing dateadded - needs DB/API lookup")
|
||||
else:
|
||||
# No NFO file found
|
||||
_log("INFO", f"🔍 No NFO file found - needs DB/API lookup")
|
||||
|
||||
# STEP 2: For movies missing dateadded in NFO, check database
|
||||
_log("DEBUG", f"STEP 2 - Checking database for missing dateadded")
|
||||
|
||||
# STEP 1: Check database for existing data (Phase 2: NFO check removed)
|
||||
_log("DEBUG", f"STEP 1 - Checking database for existing data")
|
||||
existing = self.db.get_movie_dates(imdb_id)
|
||||
|
||||
|
||||
if existing and existing.get("dateadded") and existing.get("source") != "no_valid_date_source":
|
||||
# Found in database - use cached data and will add to NFO
|
||||
_log("INFO", f"✅ Database has dateadded={existing['dateadded']} - will add to NFO")
|
||||
# Found in database - data is complete
|
||||
_log("INFO", f"✅ Database has dateadded={existing['dateadded']}")
|
||||
dateadded, source, released = existing["dateadded"], existing["source"], existing.get("released")
|
||||
|
||||
# Convert datetime objects to strings for NFO manager
|
||||
|
||||
# Convert datetime objects to strings
|
||||
if hasattr(dateadded, 'isoformat'):
|
||||
dateadded = dateadded.isoformat()
|
||||
if released and hasattr(released, 'isoformat'):
|
||||
released = released.isoformat()
|
||||
|
||||
# Create NFO with database data
|
||||
if config.manage_nfo:
|
||||
self.nfo_manager.create_movie_nfo(
|
||||
movie_path, imdb_id, dateadded, released, source, config.lock_metadata
|
||||
)
|
||||
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
self.nfo_manager.update_movie_files_mtime(movie_path, dateadded)
|
||||
|
||||
_log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [database-to-nfo]")
|
||||
|
||||
_log("INFO", f"Completed processing movie: {movie_path.name} (source: {source}) [database-cached]")
|
||||
return "processed"
|
||||
|
||||
# STEP 3: For movies still missing dateadded, query APIs
|
||||
_log("DEBUG", f"STEP 3 - Querying APIs for missing dateadded")
|
||||
|
||||
# STEP 2: Database incomplete or missing, query APIs
|
||||
_log("DEBUG", f"STEP 2 - Querying APIs for missing data")
|
||||
|
||||
# Check for shutdown signal before API calls
|
||||
if shutdown_event and shutdown_event.is_set():
|
||||
@@ -487,19 +344,10 @@ class MovieProcessor:
|
||||
is_tmdb_fallback = imdb_id.startswith("tmdb-")
|
||||
|
||||
if is_tmdb_fallback:
|
||||
# TMDB fallback processing
|
||||
# TMDB fallback processing - use file modification time
|
||||
_log("INFO", f"🔍 TMDB fallback processing for {imdb_id}")
|
||||
|
||||
# Check for existing TMDB NFO date data
|
||||
tmdb_data = self._extract_dates_from_tmdb_nfo(nfo_path)
|
||||
if tmdb_data and tmdb_data.get('dateadded'):
|
||||
dateadded = tmdb_data['dateadded']
|
||||
released = tmdb_data.get('released')
|
||||
source = "tmdb_nfo"
|
||||
else:
|
||||
# Use file modification time as fallback for TMDB
|
||||
dateadded, source, released = self._get_file_mtime_date(movie_path)
|
||||
_log("INFO", f"Using file mtime as fallback for TMDB movie: {dateadded}")
|
||||
dateadded, source, released = self._get_file_mtime_date(movie_path)
|
||||
_log("INFO", f"Using file mtime for TMDB movie: {dateadded}")
|
||||
else:
|
||||
# Standard IMDb processing
|
||||
# Try to get digital release date from external APIs
|
||||
@@ -511,116 +359,22 @@ class MovieProcessor:
|
||||
released = digital_date # For movies, digital release is often the key date
|
||||
_log("INFO", f"Got digital release date from APIs: {dateadded} (source: {source})")
|
||||
else:
|
||||
# Check Radarr NFO for premiered date
|
||||
radarr_premiered = self._get_radarr_nfo_premiered_date(movie_path)
|
||||
if radarr_premiered:
|
||||
dateadded = radarr_premiered
|
||||
source = "radarr_nfo"
|
||||
released = radarr_premiered
|
||||
_log("INFO", f"Got premiered date from Radarr NFO: {dateadded}")
|
||||
else:
|
||||
# Last resort: file modification time
|
||||
dateadded, source, released = self._get_file_mtime_date(movie_path)
|
||||
_log("INFO", f"Using file mtime as last resort: {dateadded}")
|
||||
# Last resort: file modification time
|
||||
dateadded, source, released = self._get_file_mtime_date(movie_path)
|
||||
_log("INFO", f"Using file mtime as fallback: {dateadded}")
|
||||
|
||||
# Save to database and create NFO
|
||||
# Save to database only (NFO operations removed in Phase 1)
|
||||
if dateadded:
|
||||
self.db.upsert_movie_dates(imdb_id, released, dateadded, source, True)
|
||||
|
||||
if config.manage_nfo:
|
||||
self.nfo_manager.create_movie_nfo(
|
||||
movie_path, imdb_id, dateadded, released, source, config.lock_metadata
|
||||
)
|
||||
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
self.nfo_manager.update_movie_files_mtime(movie_path, dateadded)
|
||||
|
||||
_log("INFO", f"🔍 NFO-FIRST COMPLETE: {movie_path.name} (source: {source})")
|
||||
_log("INFO", f"🔍 INCOMPLETE MODE COMPLETE: {movie_path.name} (source: {source})")
|
||||
return "processed"
|
||||
else:
|
||||
_log("WARNING", f"Could not determine dateadded for movie: {movie_path.name}")
|
||||
return "error"
|
||||
|
||||
def _extract_dates_from_tmdb_nfo(self, nfo_path: Path) -> Optional[Dict[str, str]]:
|
||||
"""Extract date information from TMDB-based NFO file"""
|
||||
if not nfo_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
root = self.nfo_manager._parse_nfo_with_tolerance(nfo_path)
|
||||
|
||||
# Look for premiered date (from TMDB)
|
||||
premiered_elem = root.find('.//premiered')
|
||||
if premiered_elem is not None and premiered_elem.text:
|
||||
premiered_date = premiered_elem.text.strip()
|
||||
print(f"✅ Found TMDB premiered date: {premiered_date}")
|
||||
|
||||
return {
|
||||
"dateadded": premiered_date,
|
||||
"source": "tmdb:premiered_from_nfo",
|
||||
"released": premiered_date
|
||||
}
|
||||
|
||||
except (ET.ParseError, Exception) as e:
|
||||
print(f"⚠️ Error parsing TMDB NFO for dates: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _extract_any_valid_dates_from_nfo(self, nfo_path: Path) -> Optional[Dict[str, str]]:
|
||||
"""Extract any valid date information from NFO file, even without NFOGuard markers"""
|
||||
if not nfo_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
import xml.etree.ElementTree as ET
|
||||
tree = ET.parse(nfo_path)
|
||||
root = tree.getroot()
|
||||
|
||||
# Look for dateadded element (indicates previously processed by NFOGuard or similar)
|
||||
dateadded_elem = root.find('.//dateadded')
|
||||
premiered_elem = root.find('.//premiered')
|
||||
|
||||
if dateadded_elem is not None and dateadded_elem.text:
|
||||
dateadded = dateadded_elem.text.strip()
|
||||
|
||||
# Try to determine source from NFOGuard comment if present
|
||||
source = "existing_nfo_data"
|
||||
try:
|
||||
nfo_content = nfo_path.read_text(encoding='utf-8')
|
||||
import re
|
||||
# Look for NFOGuard comment pattern
|
||||
source_match = re.search(r'<!--.*?NFOGuard.*?Source:\s*([^-\s]+).*?-->', nfo_content, re.DOTALL | re.IGNORECASE)
|
||||
if source_match:
|
||||
source = source_match.group(1).strip()
|
||||
_log("DEBUG", f"Found source in NFOGuard comment: {source}")
|
||||
else:
|
||||
# Try to infer source from dateadded format/content
|
||||
if "tmdb" in nfo_content.lower() or (premiered_elem and premiered_elem.text):
|
||||
source = "tmdb:digital"
|
||||
elif "radarr" in nfo_content.lower():
|
||||
source = "radarr:db.history.import"
|
||||
else:
|
||||
source = "existing_nfo_data"
|
||||
_log("DEBUG", f"Inferred source from NFO content: {source}")
|
||||
except Exception as e:
|
||||
_log("DEBUG", f"Could not determine source from NFO content: {e}")
|
||||
|
||||
result = {
|
||||
"dateadded": dateadded,
|
||||
"source": source
|
||||
}
|
||||
|
||||
if premiered_elem is not None and premiered_elem.text:
|
||||
result["released"] = premiered_elem.text.strip()
|
||||
|
||||
_log("INFO", f"✅ Found existing date data in NFO: dateadded={dateadded}, source={source}")
|
||||
return result
|
||||
|
||||
except (ET.ParseError, Exception) as e:
|
||||
_log("DEBUG", f"Error parsing NFO for existing date data: {e}")
|
||||
|
||||
return None
|
||||
|
||||
# NFO helper methods removed in Phase 2 - database is the single source of truth
|
||||
|
||||
def _decide_movie_dates(self, imdb_id: str, movie_path: Path, should_query: bool, existing: Optional[Dict]) -> Tuple[str, str, Optional[str]]:
|
||||
"""Decide movie dates based on configuration and available data"""
|
||||
_log("DEBUG", f"_decide_movie_dates for {imdb_id}: should_query={should_query}, existing={existing}")
|
||||
@@ -687,14 +441,7 @@ class MovieProcessor:
|
||||
# When using digital release date, store it as both dateadded and released
|
||||
return digital_date, digital_source, digital_date
|
||||
else:
|
||||
_log("WARNING", f"⚠️ Movie {imdb_id}: No import date OR digital release date found - trying additional fallbacks")
|
||||
|
||||
# Try Radarr's own NFO premiered date as fallback
|
||||
radarr_premiered = self._get_radarr_nfo_premiered_date(movie_path)
|
||||
if radarr_premiered:
|
||||
_log("INFO", f"✅ Movie {imdb_id}: Using Radarr NFO premiered date {radarr_premiered}")
|
||||
# When using Radarr NFO premiered date, store it as both dateadded and released
|
||||
return radarr_premiered, "radarr:nfo.premiered", radarr_premiered
|
||||
_log("WARNING", f"⚠️ Movie {imdb_id}: No import date OR digital release date found")
|
||||
|
||||
else: # digital_then_import
|
||||
# Try digital release first
|
||||
@@ -753,32 +500,8 @@ class MovieProcessor:
|
||||
_log("ERROR", f"External clients error for {imdb_id}: {e}")
|
||||
return None, f"release:error:{str(e)}"
|
||||
|
||||
def _get_radarr_nfo_premiered_date(self, movie_path: Path) -> Optional[str]:
|
||||
"""Extract premiered date from Radarr's existing movie.nfo file"""
|
||||
try:
|
||||
nfo_path = movie_path / "movie.nfo"
|
||||
if not nfo_path.exists():
|
||||
_log("DEBUG", f"No existing NFO file found at {nfo_path}")
|
||||
return None
|
||||
|
||||
nfo_content = nfo_path.read_text(encoding='utf-8')
|
||||
|
||||
# Look for <premiered>YYYY-MM-DD</premiered>
|
||||
match = re.search(r'<premiered>(\d{4}-\d{2}-\d{2})</premiered>', nfo_content)
|
||||
if match:
|
||||
premiered_date = match.group(1)
|
||||
# Convert to ISO format with timezone
|
||||
iso_date = f"{premiered_date}T00:00:00+00:00"
|
||||
_log("INFO", f"✅ Found Radarr NFO premiered date: {premiered_date}")
|
||||
return iso_date
|
||||
else:
|
||||
_log("DEBUG", f"No <premiered> tag found in existing NFO")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
_log("ERROR", f"Error reading Radarr NFO file: {e}")
|
||||
return None
|
||||
|
||||
# _get_radarr_nfo_premiered_date() removed in Phase 2 - no longer reading NFO files
|
||||
|
||||
def _log_failed_movie(self, movie_path: Path, imdb_id: str, reason: str, available_countries: List[str] = None):
|
||||
"""Log movies that failed to get valid dates to a debug file"""
|
||||
try:
|
||||
|
||||
+53
-226
@@ -11,13 +11,12 @@ from typing import Optional, Dict, List, Set, Tuple, Any
|
||||
from datetime import datetime
|
||||
|
||||
from core.database import NFOGuardDatabase
|
||||
from core.nfo_manager import NFOManager
|
||||
from core.async_nfo_manager import AsyncNFOManager
|
||||
from core.path_mapper import PathMapper
|
||||
from clients.sonarr_client import SonarrClient
|
||||
from clients.external_clients import ExternalClientManager
|
||||
from config.settings import config
|
||||
from utils.logging import _log
|
||||
from utils.imdb_utils import parse_imdb_from_path # Phase 3: Replaced NFOManager
|
||||
from utils.file_utils import (
|
||||
find_media_path_by_imdb_and_title,
|
||||
find_episodes_on_disk,
|
||||
@@ -32,10 +31,9 @@ from utils.async_file_utils import (
|
||||
class TVProcessor:
|
||||
"""Handles TV series processing"""
|
||||
|
||||
def __init__(self, db: NFOGuardDatabase, nfo_manager: NFOManager, path_mapper: PathMapper):
|
||||
def __init__(self, db: NFOGuardDatabase, nfo_manager, path_mapper: PathMapper):
|
||||
# nfo_manager parameter kept for backward compatibility but no longer used (Phase 3)
|
||||
self.db = db
|
||||
self.nfo_manager = nfo_manager
|
||||
self.async_nfo_manager = AsyncNFOManager(config.manager_brand, config.debug)
|
||||
self.path_mapper = path_mapper
|
||||
self.sonarr = SonarrClient(
|
||||
os.environ.get("SONARR_URL", ""),
|
||||
@@ -147,7 +145,7 @@ class TVProcessor:
|
||||
|
||||
def process_series(self, series_path: Path, force_scan: bool = False, scan_mode: str = "smart") -> str:
|
||||
"""Process a TV series directory"""
|
||||
imdb_id = self.nfo_manager.parse_imdb_from_path(series_path)
|
||||
imdb_id = parse_imdb_from_path(series_path) # Phase 3: Using imdb_utils
|
||||
if not imdb_id:
|
||||
_log("ERROR", f"No IMDb ID found in series path: {series_path}")
|
||||
return "error"
|
||||
@@ -193,19 +191,8 @@ class TVProcessor:
|
||||
if (season, episode) in disk_episodes:
|
||||
episode_count += 1
|
||||
|
||||
# Create NFO
|
||||
if config.manage_nfo:
|
||||
season_dir = series_path / config.get_season_dir_name(season)
|
||||
self.nfo_manager.create_episode_nfo(
|
||||
season_dir,
|
||||
season, episode, aired, dateadded, source, config.lock_metadata
|
||||
)
|
||||
|
||||
# Update file mtimes
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
video_files = disk_episodes[(season, episode)]
|
||||
for video_file in video_files:
|
||||
self.nfo_manager.set_file_mtime(video_file, dateadded)
|
||||
# NFO file operations removed - database is now the single source of truth
|
||||
# (Phase 1: Remove NFO file write operations)
|
||||
|
||||
# Save to database
|
||||
try:
|
||||
@@ -259,64 +246,12 @@ class TVProcessor:
|
||||
# Not in database or incomplete - needs NFO check
|
||||
episodes_needing_nfo_check.append((season, episode))
|
||||
|
||||
_log("INFO", f"Database cache hits: {db_cache_hits}/{len(disk_episodes)} episodes. Need NFO check: {len(episodes_needing_nfo_check)}")
|
||||
|
||||
# TIER 2: Check NFO files for NFOGuard dates and cache them in database
|
||||
nfo_cache_hits = 0
|
||||
episodes_needing_lookup = []
|
||||
|
||||
if episodes_needing_nfo_check:
|
||||
_log("DEBUG", f"TIER 2 - Checking NFO files for NFOGuard dates for {len(episodes_needing_nfo_check)} episodes")
|
||||
|
||||
for (season, episode) in episodes_needing_nfo_check:
|
||||
# Look for existing NFO files for this episode
|
||||
season_dir = series_path / config.get_season_dir_name(season)
|
||||
episode_files = disk_episodes[(season, episode)]
|
||||
|
||||
nfo_found = False
|
||||
for episode_file in episode_files:
|
||||
# Try to find matching NFO file
|
||||
nfo_path = episode_file.with_suffix('.nfo')
|
||||
if nfo_path.exists():
|
||||
# Extract NFOGuard data from episode NFO
|
||||
nfo_data = self.nfo_manager.extract_nfoguard_dates_from_nfo(nfo_path)
|
||||
if nfo_data:
|
||||
aired = nfo_data.get('aired')
|
||||
dateadded = nfo_data.get('dateadded')
|
||||
source = nfo_data.get('source', 'nfo_cache')
|
||||
|
||||
_log("DEBUG", f"S{season:02d}E{episode:02d}: NFO data found - aired={aired}, dateadded={dateadded}, source={source}")
|
||||
|
||||
# Skip incomplete NFO files with "unknown" source or no useful dates
|
||||
if source == "unknown" and not dateadded and not aired:
|
||||
_log("INFO", f"S{season:02d}E{episode:02d}: Ignoring incomplete NFO file with source 'unknown' and no dates")
|
||||
break # Break out of NFO search to mark episode as needing API lookup
|
||||
|
||||
# Apply fallback logic if NFO has aired but no dateadded
|
||||
if not dateadded and aired:
|
||||
dateadded = aired
|
||||
source = f"{source}_aired_fallback" if source != 'nfo_cache' else 'nfo_aired_fallback'
|
||||
_log("DEBUG", f"S{season:02d}E{episode:02d}: NFO has aired but no dateadded, using aired as fallback: {dateadded}")
|
||||
|
||||
if dateadded:
|
||||
episode_dates[(season, episode)] = (aired, dateadded, source)
|
||||
nfo_cache_hits += 1
|
||||
nfo_found = True
|
||||
|
||||
# Cache NFO data in database for future lookups
|
||||
self.db.upsert_episode_date(imdb_id, season, episode, aired, dateadded, source, True)
|
||||
_log("DEBUG", f"NFO cache hit for S{season:02d}E{episode:02d}: {dateadded} - cached in DB")
|
||||
break
|
||||
|
||||
if not nfo_found:
|
||||
# No NFO data found - needs API lookup
|
||||
episodes_needing_lookup.append((season, episode))
|
||||
|
||||
_log("INFO", f"NFO cache hits: {nfo_cache_hits}/{len(episodes_needing_nfo_check)} episodes. Need API lookup: {len(episodes_needing_lookup)}")
|
||||
|
||||
# TIER 3: Only call Sonarr API for episodes not in database or NFO files
|
||||
_log("INFO", f"TIER 1 - Database cache hits: {db_cache_hits}/{len(disk_episodes)} episodes. Need API lookup: {len(episodes_needing_nfo_check)}")
|
||||
|
||||
# TIER 2: Query Sonarr API for episodes not in database (NFO layer removed in Phase 2)
|
||||
episodes_needing_lookup = episodes_needing_nfo_check # Renamed for clarity
|
||||
if episodes_needing_lookup:
|
||||
_log("DEBUG", f"TIER 3 - Querying Sonarr for {len(episodes_needing_lookup)} episodes missing from database and NFO files")
|
||||
_log("DEBUG", f"TIER 2 - Querying Sonarr for {len(episodes_needing_lookup)} episodes missing from database")
|
||||
sonarr_episodes = self._get_sonarr_episodes(imdb_id, episodes_needing_lookup)
|
||||
|
||||
# Process episodes that needed lookup
|
||||
@@ -385,77 +320,35 @@ class TVProcessor:
|
||||
return episode_dates
|
||||
|
||||
def _gather_episode_dates_nfo_first(self, series_path: Path, imdb_id: str, disk_episodes: Dict[Tuple[int, int], List[Path]]) -> Dict[Tuple[int, int], Tuple[Optional[str], Optional[str], str]]:
|
||||
"""Gather episode dates for incomplete mode: Check NFO files first for missing dateadded elements"""
|
||||
_log("INFO", f"🔍 NFO-FIRST MODE: Checking {len(disk_episodes)} episodes for missing dateadded in NFO files")
|
||||
"""Gather episode dates for incomplete mode: Database-first then API (NFO checks removed in Phase 2)"""
|
||||
_log("INFO", f"🔍 INCOMPLETE MODE: Checking {len(disk_episodes)} episodes for missing data")
|
||||
episode_dates = {}
|
||||
episodes_needing_db_check = []
|
||||
episodes_needing_api_lookup = []
|
||||
|
||||
# STEP 1: Check each NFO file to determine if dateadded is missing
|
||||
_log("DEBUG", f"STEP 1 - Checking NFO files for missing dateadded elements")
|
||||
nfo_complete_count = 0
|
||||
|
||||
|
||||
# STEP 1: Check database for all episodes (NFO check removed in Phase 2)
|
||||
_log("DEBUG", f"STEP 1 - Checking database for {len(disk_episodes)} episodes")
|
||||
db_hits = 0
|
||||
|
||||
for (season, episode) in disk_episodes:
|
||||
season_dir = series_path / config.get_season_dir_name(season)
|
||||
episode_files = disk_episodes[(season, episode)]
|
||||
|
||||
nfo_has_dateadded = False
|
||||
nfo_data = None
|
||||
|
||||
# Look for NFO file for this episode
|
||||
for episode_file in episode_files:
|
||||
nfo_path = episode_file.with_suffix('.nfo')
|
||||
if nfo_path.exists():
|
||||
# Extract NFOGuard data from episode NFO
|
||||
nfo_data = self.nfo_manager.extract_nfoguard_dates_from_nfo(nfo_path)
|
||||
if nfo_data and nfo_data.get('dateadded'):
|
||||
# NFO has dateadded - this episode is complete
|
||||
aired = nfo_data.get('aired')
|
||||
dateadded = nfo_data.get('dateadded')
|
||||
source = nfo_data.get('source', 'nfo_cache')
|
||||
episode_dates[(season, episode)] = (aired, dateadded, source)
|
||||
nfo_complete_count += 1
|
||||
nfo_has_dateadded = True
|
||||
_log("DEBUG", f"S{season:02d}E{episode:02d}: NFO has dateadded={dateadded}, marked as complete")
|
||||
break
|
||||
|
||||
if not nfo_has_dateadded:
|
||||
# NFO missing or missing dateadded - needs further processing
|
||||
if nfo_data:
|
||||
# NFO exists but missing dateadded
|
||||
_log("DEBUG", f"S{season:02d}E{episode:02d}: NFO exists but missing dateadded - needs DB/API lookup")
|
||||
else:
|
||||
# No NFO file found
|
||||
_log("DEBUG", f"S{season:02d}E{episode:02d}: No NFO file found - needs DB/API lookup")
|
||||
episodes_needing_db_check.append((season, episode))
|
||||
|
||||
_log("INFO", f"NFO files with dateadded: {nfo_complete_count}/{len(disk_episodes)} episodes. Need DB check: {len(episodes_needing_db_check)}")
|
||||
|
||||
# STEP 2: For episodes missing dateadded in NFO, check database
|
||||
if episodes_needing_db_check:
|
||||
_log("DEBUG", f"STEP 2 - Checking database for {len(episodes_needing_db_check)} episodes missing dateadded")
|
||||
db_hits = 0
|
||||
|
||||
for (season, episode) in episodes_needing_db_check:
|
||||
db_result = self.db.get_episode_date(imdb_id, season, episode)
|
||||
|
||||
if db_result and db_result.get('dateadded'):
|
||||
# Found in database - use cached data and will add to NFO later
|
||||
aired = db_result.get('aired')
|
||||
dateadded = db_result.get('dateadded')
|
||||
source = db_result.get('source', 'database_cache')
|
||||
episode_dates[(season, episode)] = (aired, dateadded, source)
|
||||
db_hits += 1
|
||||
_log("DEBUG", f"S{season:02d}E{episode:02d}: Database has dateadded={dateadded} - will add to NFO")
|
||||
else:
|
||||
# Not in database or incomplete - needs API lookup
|
||||
episodes_needing_api_lookup.append((season, episode))
|
||||
|
||||
_log("INFO", f"Database hits: {db_hits}/{len(episodes_needing_db_check)} episodes. Need API lookup: {len(episodes_needing_api_lookup)}")
|
||||
|
||||
# STEP 3: For episodes still missing dateadded, query Sonarr
|
||||
db_result = self.db.get_episode_date(imdb_id, season, episode)
|
||||
|
||||
if db_result and db_result.get('dateadded'):
|
||||
# Found in database - use cached data
|
||||
aired = db_result.get('aired')
|
||||
dateadded = db_result.get('dateadded')
|
||||
source = db_result.get('source', 'database_cache')
|
||||
episode_dates[(season, episode)] = (aired, dateadded, source)
|
||||
db_hits += 1
|
||||
_log("DEBUG", f"S{season:02d}E{episode:02d}: Database has dateadded={dateadded}")
|
||||
else:
|
||||
# Not in database or incomplete - needs API lookup
|
||||
episodes_needing_api_lookup.append((season, episode))
|
||||
|
||||
_log("INFO", f"Database hits: {db_hits}/{len(disk_episodes)} episodes. Need API lookup: {len(episodes_needing_api_lookup)}")
|
||||
|
||||
# STEP 2: For episodes missing from database, query Sonarr
|
||||
if episodes_needing_api_lookup:
|
||||
_log("DEBUG", f"STEP 3 - Querying Sonarr for {len(episodes_needing_api_lookup)} episodes missing from NFO and database")
|
||||
_log("DEBUG", f"STEP 2 - Querying Sonarr for {len(episodes_needing_api_lookup)} episodes missing from database")
|
||||
sonarr_episodes = self._get_sonarr_episodes(imdb_id, episodes_needing_api_lookup)
|
||||
|
||||
for (season, episode) in episodes_needing_api_lookup:
|
||||
@@ -503,7 +396,7 @@ class TVProcessor:
|
||||
|
||||
episode_dates[(season, episode)] = (aired, dateadded, source)
|
||||
|
||||
_log("INFO", f"🔍 NFO-FIRST COMPLETE: {len(episode_dates)} episodes processed")
|
||||
_log("INFO", f"🔍 INCOMPLETE MODE COMPLETE: {len(episode_dates)} episodes processed")
|
||||
for (s, e), (aired, dateadded, source) in episode_dates.items():
|
||||
_log("INFO", f" S{s:02d}E{e:02d}: aired={aired}, dateadded={dateadded}, source={source}")
|
||||
|
||||
@@ -648,18 +541,8 @@ class TVProcessor:
|
||||
processed_count = 0
|
||||
for (season, episode), (aired, dateadded, source) in episode_dates.items():
|
||||
if (season, episode) in season_episodes:
|
||||
# Create NFO
|
||||
if config.manage_nfo:
|
||||
self.nfo_manager.create_episode_nfo(
|
||||
season_path,
|
||||
season, episode, aired, dateadded, source, config.lock_metadata
|
||||
)
|
||||
|
||||
# Update file mtimes
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
video_files = season_episodes[(season, episode)]
|
||||
for video_file in video_files:
|
||||
self.nfo_manager.set_file_mtime(video_file, dateadded)
|
||||
# NFO file operations removed - database is now the single source of truth
|
||||
# (Phase 1: Remove NFO file write operations)
|
||||
|
||||
# Save to database
|
||||
try:
|
||||
@@ -728,16 +611,8 @@ class TVProcessor:
|
||||
|
||||
aired, dateadded, source = episode_dates[(season_num, episode_num)]
|
||||
|
||||
# Create NFO
|
||||
if config.manage_nfo:
|
||||
self.nfo_manager.create_episode_nfo(
|
||||
season_path,
|
||||
season_num, episode_num, aired, dateadded, source, config.lock_metadata
|
||||
)
|
||||
|
||||
# Update file mtime
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
self.nfo_manager.set_file_mtime(episode_path, dateadded)
|
||||
# NFO file operations removed - database is now the single source of truth
|
||||
# (Phase 1: Remove NFO file write operations)
|
||||
|
||||
# Save to database
|
||||
self.db.upsert_episode_date(imdb_id, season_num, episode_num, aired, dateadded, source, True)
|
||||
@@ -765,7 +640,7 @@ class TVProcessor:
|
||||
Returns:
|
||||
Dictionary with processing results
|
||||
"""
|
||||
imdb_id = self.nfo_manager.parse_imdb_from_path(series_path)
|
||||
imdb_id = parse_imdb_from_path(series_path) # Phase 3: Using imdb_utils
|
||||
if not imdb_id:
|
||||
return {"status": "error", "reason": f"No IMDb ID found in series path: {series_path}"}
|
||||
|
||||
@@ -787,25 +662,8 @@ class TVProcessor:
|
||||
|
||||
for (season, episode), (aired, dateadded, source) in episode_dates.items():
|
||||
if (season, episode) in disk_episodes:
|
||||
season_dir = series_path / config.get_season_dir_name(season)
|
||||
|
||||
# Prepare NFO creation data
|
||||
if config.manage_nfo:
|
||||
episode_data_list.append({
|
||||
'season_dir': season_dir,
|
||||
'season': season,
|
||||
'episode': episode,
|
||||
'aired': aired,
|
||||
'dateadded': dateadded,
|
||||
'source': source,
|
||||
'lock_metadata': config.lock_metadata
|
||||
})
|
||||
|
||||
# Prepare mtime operations
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
video_files = disk_episodes[(season, episode)]
|
||||
for video_file in video_files:
|
||||
mtime_operations.append((video_file, dateadded))
|
||||
# NFO file operations removed - database is now the single source of truth
|
||||
# (Phase 1: Remove NFO file write operations)
|
||||
|
||||
# Save to database
|
||||
try:
|
||||
@@ -815,27 +673,10 @@ class TVProcessor:
|
||||
_log("ERROR", f"S{season:02d}E{episode:02d}: Database write failed: {e}")
|
||||
# Continue processing other episodes
|
||||
|
||||
# Process NFOs and mtimes concurrently
|
||||
# NFO and mtime operations removed - database is now the single source of truth
|
||||
# (Phase 1: Remove NFO file write operations)
|
||||
results = {}
|
||||
|
||||
if episode_data_list:
|
||||
_log("INFO", f"Creating {len(episode_data_list)} episode NFOs concurrently")
|
||||
nfo_results = await self.async_nfo_manager.async_batch_create_episode_nfos(
|
||||
episode_data_list,
|
||||
max_concurrent=config.max_concurrent
|
||||
)
|
||||
results['nfo_created'] = sum(nfo_results)
|
||||
results['nfo_failed'] = len(nfo_results) - sum(nfo_results)
|
||||
|
||||
if mtime_operations:
|
||||
_log("INFO", f"Setting mtimes for {len(mtime_operations)} files concurrently")
|
||||
mtime_results = await self.async_nfo_manager.async_batch_set_file_mtimes(
|
||||
mtime_operations,
|
||||
max_concurrent=10
|
||||
)
|
||||
results['mtime_updated'] = sum(mtime_results)
|
||||
results['mtime_failed'] = len(mtime_results) - sum(mtime_results)
|
||||
|
||||
_log("INFO", f"Completed async processing TV series: {series_path.name}")
|
||||
|
||||
return {
|
||||
@@ -892,7 +733,7 @@ class TVProcessor:
|
||||
|
||||
def process_webhook_episodes(self, series_path: Path, webhook_episodes: List[Dict[str, Any]]) -> None:
|
||||
"""Process only the specific episodes mentioned in a webhook (targeted mode)"""
|
||||
imdb_id = self.nfo_manager.parse_imdb_from_path(series_path)
|
||||
imdb_id = parse_imdb_from_path(series_path) # Phase 3: Using imdb_utils
|
||||
if not imdb_id:
|
||||
_log("ERROR", f"No IMDb ID found in series path: {series_path}")
|
||||
return
|
||||
@@ -942,18 +783,8 @@ class TVProcessor:
|
||||
aired, dateadded, source = self._get_webhook_episode_date(imdb_id, season_num, episode_num, series_metadata, webhook_episode)
|
||||
enhanced_metadata = self._get_episode_metadata(series_metadata, season_num, episode_num, season_dir) if series_metadata else self._get_episode_metadata(None, season_num, episode_num, season_dir)
|
||||
|
||||
# Create NFO
|
||||
if config.manage_nfo:
|
||||
self.nfo_manager.create_episode_nfo(
|
||||
season_dir,
|
||||
season_num, episode_num, aired, dateadded, source, config.lock_metadata,
|
||||
enhanced_metadata
|
||||
)
|
||||
|
||||
# Update file mtimes
|
||||
if config.fix_dir_mtimes and dateadded:
|
||||
for episode_file in episode_files:
|
||||
self.nfo_manager.set_file_mtime(episode_file, dateadded)
|
||||
# NFO file operations removed - database is now the single source of truth
|
||||
# (Phase 1: Remove NFO file write operations)
|
||||
|
||||
# Save to database
|
||||
self.db.upsert_episode_date(imdb_id, season_num, episode_num, aired, dateadded, source, True)
|
||||
@@ -1208,17 +1039,13 @@ class TVProcessor:
|
||||
dateadded = episode_data.get('dateadded')
|
||||
|
||||
# Get IMDb ID
|
||||
imdb_id = self.nfo_manager.parse_imdb_from_path(series_path)
|
||||
imdb_id = parse_imdb_from_path(series_path) # Phase 3: Using imdb_utils
|
||||
if not imdb_id:
|
||||
return {"status": "error", "reason": "No IMDb ID found"}
|
||||
|
||||
# Create NFO if needed
|
||||
nfo_success = True
|
||||
if config.manage_nfo:
|
||||
season_dir = series_path / config.get_season_dir_name(season)
|
||||
nfo_success = await self.async_nfo_manager.async_create_episode_nfo(
|
||||
season_dir, season, episode, aired, dateadded, "webhook", config.lock_metadata
|
||||
)
|
||||
# NFO file operations removed - database is now the single source of truth
|
||||
# (Phase 1: Remove NFO file write operations)
|
||||
nfo_success = True # Always True since we're not creating NFOs anymore
|
||||
|
||||
# Update database
|
||||
self.db.upsert_episode_date(imdb_id, season, episode, aired, dateadded, "webhook", True)
|
||||
|
||||
Reference in New Issue
Block a user