refactor: update and remove anything to do with NFO files
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:
2025-11-02 13:43:28 -05:00
parent 041caf0d0d
commit 97a9f3431a
13 changed files with 837 additions and 2252 deletions
+53 -226
View File
@@ -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)