huge: update
Local Docker Build (Dev) / build-dev (push) Successful in 28s

This commit is contained in:
2025-09-25 18:41:37 -04:00
parent 740f912b57
commit 70302837a7
4 changed files with 1047 additions and 85 deletions
+384 -28
View File
@@ -3,6 +3,7 @@
Movie processing logic for NFOGuard
"""
import os
import glob
from pathlib import Path
from typing import Optional, Dict, Any, List
from datetime import datetime, timezone
@@ -30,33 +31,388 @@ class MovieProcessor:
)
self.external_clients = ExternalClientManager()
# NOTE: This is a placeholder extraction - the full 500-line class implementation
# needs to be copied from the original nfoguard.py file (lines 1062-1561)
#
# Key methods that need to be extracted include:
# - process_movie()
# - validate_batch_movies()
# - get_movie_dates()
# - _should_query_external_apis()
# - Various helper methods for date handling and processing
#
# This placeholder allows the import structure to work while we continue
# the refactoring process.
def process_movie(self, movie_path: Path, imdb_id: str = None) -> bool:
"""Process a single movie - PLACEHOLDER"""
_log("INFO", f"Processing movie: {movie_path}")
# Full implementation needs to be copied from original file
return True
def find_movie_path(self, movie_title: str, imdb_id: str, radarr_path: str = None) -> Optional[Path]:
"""Find movie directory path"""
# Try webhook path first
if radarr_path:
container_path = self.path_mapper.radarr_path_to_container_path(radarr_path)
path_obj = Path(container_path)
if path_obj.exists():
return path_obj
# Search by IMDb ID or title
for media_path in config.movie_paths:
if not media_path.exists():
continue
# Search by IMDb ID
if imdb_id:
pattern = str(media_path / f"*[imdb-{imdb_id}]*")
matches = glob.glob(pattern)
if matches:
return Path(matches[0])
# Search by title
if movie_title:
title_clean = movie_title.lower().replace(" ", "").replace("-", "")
for item in media_path.iterdir():
if item.is_dir() and "[imdb-" in item.name.lower():
item_clean = item.name.lower().replace(" ", "").replace("-", "")
if title_clean in item_clean:
return item
return None
def process_movie(self, movie_path: Path, webhook_mode: bool = False) -> None:
"""Process a movie directory"""
imdb_id = self.nfo_manager.find_movie_imdb_id(movie_path)
if not imdb_id:
_log("ERROR", f"No IMDb ID found in movie directory, filenames, or NFO file: {movie_path}")
return
# Handle TMDB ID fallback case
is_tmdb_fallback = imdb_id.startswith("tmdb-")
if is_tmdb_fallback:
_log("INFO", f"Processing movie: {movie_path.name} (TMDB: {imdb_id})")
else:
_log("INFO", f"Processing movie: {movie_path.name} (IMDb: {imdb_id})")
# Update database
self.db.upsert_movie(imdb_id, str(movie_path))
# Check for video files
video_exts = (".mkv", ".mp4", ".avi", ".mov", ".m4v")
has_video = any(f.is_file() and f.suffix.lower() in video_exts for f in movie_path.iterdir())
if not has_video:
_log("WARNING", f"No video files found in: {movie_path}")
self.db.upsert_movie_dates(imdb_id, None, None, None, False)
return
# TIER 1: Check if NFO file already has NFOGuard data (fastest - no DB or API calls)
nfo_path = movie_path / "movie.nfo"
nfo_data = self.nfo_manager.extract_nfoguard_dates_from_nfo(nfo_path)
if nfo_data:
_log("INFO", f"🚀 Using existing NFOGuard data from NFO file: {nfo_data['dateadded']} (source: {nfo_data['source']})")
dateadded = nfo_data["dateadded"]
source = nfo_data["source"]
released = nfo_data.get("released")
# 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-only]")
return
# 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 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-enhanced]")
return
# TIER 2: Check database for existing movie data
existing_movie = self.db.get_movie_dates(imdb_id)
if existing_movie and existing_movie.get("dateadded") and existing_movie.get("source") != "no_valid_date_source":
_log("INFO", f"✅ Using complete database data: {existing_movie['dateadded']} (source: {existing_movie['source']})")
# Still create NFO and update files but skip API queries
dateadded = existing_movie["dateadded"]
source = existing_movie["source"]
released = existing_movie.get("released")
if config.manage_nfo and dateadded:
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-only]")
return
# TIER 3: Full processing with API calls (slowest)
_log("DEBUG", f"Movie requires full processing - querying external APIs")
# Get movie dates from various sources
movie_dates = self.get_movie_dates(imdb_id, movie_path, webhook_mode=webhook_mode,
fallback_to_tmdb=(not is_tmdb_fallback))
dateadded = movie_dates.get("dateadded")
released = movie_dates.get("released")
source = movie_dates.get("source", "no_valid_date_source")
# Create NFO
if config.manage_nfo and dateadded:
self.nfo_manager.create_movie_nfo(
movie_path, imdb_id, dateadded, released, source, config.lock_metadata
)
# Update file mtimes
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}) [full-processing]")
def _extract_dates_from_tmdb_nfo(self, nfo_path: Path) -> Optional[Dict]:
"""Extract dates from existing TMDB NFO file"""
if not nfo_path.exists():
return None
try:
with open(nfo_path, 'r', encoding='utf-8') as f:
content = f.read()
# Look for dateadded and premiered elements
dateadded = None
released = None
# Extract dateadded
import re
dateadded_match = re.search(r'<dateadded>([^<]+)</dateadded>', content)
if dateadded_match:
dateadded = dateadded_match.group(1).strip()
# Extract premiered (release date)
premiered_match = re.search(r'<premiered>([^<]+)</premiered>', content)
if premiered_match:
released = premiered_match.group(1).strip()
if dateadded:
return {
"dateadded": dateadded,
"released": released,
"source": "tmdb:nfo.dateadded"
}
except Exception as e:
_log("DEBUG", f"Could not extract dates from TMDB NFO: {e}")
return None
def get_movie_dates(self, imdb_id: str, movie_path: Path = None, webhook_mode: bool = False, fallback_to_tmdb: bool = True) -> Dict[str, Any]:
"""Get movie dates from various sources with priority system"""
# Initialize result
result = {
"dateadded": None,
"released": None,
"source": "no_valid_date_source"
}
# Check if this is a TMDB ID
if imdb_id.startswith("tmdb-"):
return self._get_tmdb_movie_dates(imdb_id, movie_path)
# Priority 1: Radarr import history (most accurate for dateadded)
if self.radarr.enabled and config.movie_priority >= 1:
try:
movie_data = self.radarr.get_movie_by_imdb(imdb_id)
if movie_data:
# Get import history for accurate dateadded
movie_id = movie_data.get("id")
if movie_id:
import_history = self.radarr.get_movie_import_history(movie_id)
if import_history:
result["dateadded"] = self._parse_date_to_iso(import_history)
result["source"] = "radarr:history.import"
_log("INFO", f"Found Radarr import date: {result['dateadded']}")
# Get release date from Radarr
release_date = movie_data.get("digitalRelease") or movie_data.get("physicalRelease") or movie_data.get("inCinemas")
if release_date:
result["released"] = self._parse_date_to_iso(release_date)
# If we have dateadded from import, we're done
if result["dateadded"]:
return result
# Fallback to using dateadded from Radarr API
radarr_dateadded = movie_data.get("added")
if radarr_dateadded:
result["dateadded"] = self._parse_date_to_iso(radarr_dateadded)
result["source"] = "radarr:movie.added"
return result
except Exception as e:
_log("DEBUG", f"Radarr query failed for {imdb_id}: {e}")
# Priority 2: External APIs (TMDB, OMDb, etc.) for release date
if config.movie_priority >= 2 and fallback_to_tmdb:
try:
# Try TMDB
if self.external_clients.tmdb.enabled:
tmdb_data = self.external_clients.tmdb.find_by_imdb(imdb_id)
if tmdb_data:
release_date = tmdb_data.get("release_date")
if release_date:
released_iso = self._parse_date_to_iso(release_date)
result["released"] = released_iso
# Use release date as dateadded fallback
if not result["dateadded"]:
result["dateadded"] = released_iso
result["source"] = "tmdb:release_date"
return result
# Try OMDb as additional fallback
if self.external_clients.omdb.enabled:
omdb_data = self.external_clients.omdb.get_by_imdb(imdb_id)
if omdb_data and omdb_data.get("Released"):
released_date = self._parse_omdb_date(omdb_data["Released"])
if released_date:
result["released"] = released_date
if not result["dateadded"]:
result["dateadded"] = released_date
result["source"] = "omdb:released"
return result
except Exception as e:
_log("DEBUG", f"External API query failed for {imdb_id}: {e}")
# Priority 3: File system dates as absolute fallback
if not result["dateadded"] and movie_path and config.movie_priority >= 3:
try:
# Use directory creation time as last resort
if movie_path.exists():
dir_stat = movie_path.stat()
# Use the earliest of creation or modification time
earliest_time = min(dir_stat.st_ctime, dir_stat.st_mtime)
fs_date = datetime.fromtimestamp(earliest_time, tz=timezone.utc)
result["dateadded"] = fs_date.isoformat(timespec="seconds")
result["source"] = "filesystem:dir.ctime"
_log("DEBUG", f"Using filesystem date as fallback: {result['dateadded']}")
return result
except Exception as e:
_log("DEBUG", f"Filesystem date extraction failed: {e}")
return result
def _get_tmdb_movie_dates(self, tmdb_id: str, movie_path: Path = None) -> Dict[str, Any]:
"""Get movie dates for TMDB-only movies"""
result = {
"dateadded": None,
"released": None,
"source": "no_valid_date_source"
}
# Extract TMDB ID from the string (format: "tmdb-12345")
try:
tmdb_numeric_id = tmdb_id.replace("tmdb-", "")
if self.external_clients.tmdb.enabled:
tmdb_data = self.external_clients.tmdb.get_movie_details(tmdb_numeric_id)
if tmdb_data:
release_date = tmdb_data.get("release_date")
if release_date:
released_iso = self._parse_date_to_iso(release_date)
result["released"] = released_iso
result["dateadded"] = released_iso # Use release date as dateadded
result["source"] = "tmdb:release_date"
return result
except Exception as e:
_log("DEBUG", f"TMDB query failed for {tmdb_id}: {e}")
# Fallback to filesystem date for TMDB movies
if movie_path and movie_path.exists():
try:
dir_stat = movie_path.stat()
earliest_time = min(dir_stat.st_ctime, dir_stat.st_mtime)
fs_date = datetime.fromtimestamp(earliest_time, tz=timezone.utc)
result["dateadded"] = fs_date.isoformat(timespec="seconds")
result["source"] = "filesystem:dir.ctime"
except Exception as e:
_log("DEBUG", f"Filesystem date extraction failed: {e}")
return result
def _parse_date_to_iso(self, date_str: str) -> Optional[str]:
"""Parse date string to ISO format"""
if not date_str:
return None
try:
# Handle different date formats
if len(date_str) == 10 and date_str[4] == "-": # YYYY-MM-DD
dt = datetime.fromisoformat(date_str).replace(tzinfo=timezone.utc)
else: # ISO format with timezone
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00")).astimezone(timezone.utc)
return dt.isoformat(timespec="seconds")
except Exception:
return None
def _parse_omdb_date(self, date_str: str) -> Optional[str]:
"""Parse OMDb date format (e.g., '25 Dec 2020')"""
if not date_str or date_str == "N/A":
return None
try:
# Parse OMDb date format: "25 Dec 2020"
dt = datetime.strptime(date_str, "%d %b %Y").replace(tzinfo=timezone.utc)
return dt.isoformat(timespec="seconds")
except Exception:
return None
def validate_batch_movies(self, movie_paths: List[Path]) -> Dict[str, Any]:
"""Validate batch of movies - PLACEHOLDER"""
_log("INFO", f"Validating {len(movie_paths)} movies")
# Full implementation needs to be copied from original file
return {"processed": 0, "errors": []}
def get_movie_dates(self, imdb_id: str, movie_path: Path = None) -> Dict[str, Any]:
"""Get movie dates from various sources - PLACEHOLDER"""
_log("DEBUG", f"Getting dates for movie {imdb_id}")
# Full implementation needs to be copied from original file
return {"source": "placeholder", "dateadded": None}
"""Validate and process a batch of movies"""
results = {
"processed": 0,
"errors": [],
"skipped": 0
}
_log("INFO", f"Starting batch processing of {len(movie_paths)} movies")
for i, movie_path in enumerate(movie_paths, 1):
try:
_log("INFO", f"Processing movie {i}/{len(movie_paths)}: {movie_path.name}")
# Check if directory exists and has video files
if not movie_path.exists() or not movie_path.is_dir():
results["errors"].append(f"Path does not exist or is not a directory: {movie_path}")
continue
# Check for IMDb ID
imdb_id = self.nfo_manager.find_movie_imdb_id(movie_path)
if not imdb_id:
results["skipped"] += 1
_log("WARNING", f"Skipping {movie_path.name}: No IMDb ID found")
continue
# Process the movie
self.process_movie(movie_path)
results["processed"] += 1
except Exception as e:
error_msg = f"Error processing {movie_path}: {str(e)}"
results["errors"].append(error_msg)
_log("ERROR", error_msg)
continue
_log("INFO", f"Batch processing complete: {results['processed']} processed, "
f"{results['skipped']} skipped, {len(results['errors'])} errors")
return results