This commit is contained in:
+19
-6
@@ -804,7 +804,9 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
|
||||
if path and scan_path.name.lower().startswith('season'):
|
||||
# Single season processing
|
||||
series_path = scan_path.parent
|
||||
if nfo_manager.parse_imdb_from_path(series_path):
|
||||
tv_processor_obj = dependencies.get("tv_processor")
|
||||
sonarr_client = tv_processor_obj.sonarr if tv_processor_obj and hasattr(tv_processor_obj, 'sonarr') else None
|
||||
if nfo_manager.parse_imdb_from_path_with_nfo_fallback(series_path, sonarr_client):
|
||||
print(f"INFO: Processing single season: {scan_path}")
|
||||
try:
|
||||
tv_processor.process_season(series_path, scan_path)
|
||||
@@ -814,15 +816,19 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
|
||||
# Single episode processing
|
||||
season_path = scan_path.parent
|
||||
series_path = season_path.parent
|
||||
if nfo_manager.parse_imdb_from_path(series_path):
|
||||
tv_processor_obj = dependencies.get("tv_processor")
|
||||
sonarr_client = tv_processor_obj.sonarr if tv_processor_obj and hasattr(tv_processor_obj, 'sonarr') else None
|
||||
if nfo_manager.parse_imdb_from_path_with_nfo_fallback(series_path, sonarr_client):
|
||||
print(f"INFO: Processing single episode: {scan_path}")
|
||||
try:
|
||||
tv_processor.process_episode_file(series_path, season_path, scan_path)
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed processing episode {scan_path}: {e}")
|
||||
else:
|
||||
# Check if this path itself is a series (has IMDb ID in the directory name)
|
||||
if nfo_manager.parse_imdb_from_path(scan_path):
|
||||
# Check if this path itself is a series (has IMDb ID in directory name or NFO files)
|
||||
tv_processor_obj = dependencies.get("tv_processor")
|
||||
sonarr_client = tv_processor_obj.sonarr if tv_processor_obj and hasattr(tv_processor_obj, 'sonarr') else None
|
||||
if nfo_manager.parse_imdb_from_path_with_nfo_fallback(scan_path, sonarr_client):
|
||||
try:
|
||||
# Determine force_scan based on scan mode
|
||||
force_scan = (scan_mode == "full")
|
||||
@@ -846,8 +852,10 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
|
||||
not item.name.lower().startswith('season') and
|
||||
not re.match(r'^season\s+\d+$', item.name, re.IGNORECASE)):
|
||||
|
||||
# Check for IMDb ID
|
||||
imdb_id = nfo_manager.parse_imdb_from_path(item)
|
||||
# Check for IMDb ID (enhanced with NFO fallback)
|
||||
tv_processor_obj = dependencies.get("tv_processor")
|
||||
sonarr_client = tv_processor_obj.sonarr if tv_processor_obj and hasattr(tv_processor_obj, 'sonarr') else None
|
||||
imdb_id = nfo_manager.parse_imdb_from_path_with_nfo_fallback(item, sonarr_client)
|
||||
if imdb_id:
|
||||
tv_series_list.append(item)
|
||||
else:
|
||||
@@ -2202,6 +2210,11 @@ def register_routes(app, dependencies: dict):
|
||||
async def _health() -> HealthResponse:
|
||||
return await health(dependencies)
|
||||
|
||||
@app.get("/health/simple")
|
||||
async def _health_simple():
|
||||
"""Simple health check for Docker without external dependencies"""
|
||||
return {"status": "healthy", "service": "nfoguard-core"}
|
||||
|
||||
@app.get("/stats")
|
||||
async def _get_stats():
|
||||
return await get_stats(dependencies)
|
||||
|
||||
@@ -150,6 +150,10 @@ class SonarrClient:
|
||||
_log("WARNING", f"No series found with IMDb ID via direct lookup: {imdb_id}")
|
||||
return None
|
||||
|
||||
def get_series_by_id(self, series_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get series information by Sonarr series ID"""
|
||||
return self._get(f"/series/{series_id}")
|
||||
|
||||
def episodes_for_series(self, series_id: int) -> List[Dict[str, Any]]:
|
||||
"""Get all episodes for a series"""
|
||||
return self._get("/episode", {"seriesId": series_id}) or []
|
||||
|
||||
+60
-4
@@ -50,6 +50,58 @@ class NFOManager:
|
||||
|
||||
return None
|
||||
|
||||
def parse_imdb_from_path_with_nfo_fallback(self, path: Path, sonarr_client=None) -> Optional[str]:
|
||||
"""
|
||||
Enhanced IMDb detection that fallback to Sonarr ID lookup from NFO files
|
||||
|
||||
1. First try to parse IMDb ID from directory path/name
|
||||
2. If not found, scan NFO files in directory for Sonarr series ID
|
||||
3. Use Sonarr API to lookup series and extract IMDb ID
|
||||
"""
|
||||
# Primary: Try directory name first
|
||||
imdb_id = self.parse_imdb_from_path(path)
|
||||
if imdb_id:
|
||||
return imdb_id
|
||||
|
||||
# Fallback: Check NFO files for Sonarr series ID
|
||||
if not sonarr_client or not sonarr_client.enabled:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Look for episode NFO files in the directory (and subdirectories)
|
||||
nfo_files = []
|
||||
if path.is_dir():
|
||||
# Check current directory and season subdirectories
|
||||
nfo_files.extend(list(path.glob("*.nfo")))
|
||||
for subdir in path.iterdir():
|
||||
if subdir.is_dir() and subdir.name.lower().startswith('season'):
|
||||
nfo_files.extend(list(subdir.glob("*.nfo")))
|
||||
|
||||
# Extract Sonarr series ID from any NFO file
|
||||
for nfo_file in nfo_files:
|
||||
try:
|
||||
tree = ET.parse(nfo_file)
|
||||
root = tree.getroot()
|
||||
|
||||
# Look for Sonarr series ID
|
||||
for uniqueid in root.findall('.//uniqueid[@type="sonarr"]'):
|
||||
sonarr_id = uniqueid.text
|
||||
if sonarr_id and sonarr_id.isdigit():
|
||||
# Look up series in Sonarr to get IMDb ID
|
||||
series_data = sonarr_client.get_series_by_id(int(sonarr_id))
|
||||
if series_data:
|
||||
imdb_id = series_data.get('imdbId')
|
||||
if imdb_id:
|
||||
return imdb_id.replace('tt', '') if imdb_id.startswith('tt') else imdb_id
|
||||
|
||||
except Exception as e:
|
||||
continue # Skip this NFO file and try the next one
|
||||
|
||||
except Exception as e:
|
||||
pass # Fallback failed, return None
|
||||
|
||||
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():
|
||||
@@ -154,8 +206,10 @@ class NFOManager:
|
||||
aired_elem = root.find('.//aired') # For TV episodes
|
||||
lockdata_elem = root.find('.//lockdata')
|
||||
|
||||
# Consider it NFOGuard-managed if it has lockdata=true (with or without dateadded)
|
||||
if lockdata_elem is not None and lockdata_elem.text == "true":
|
||||
# Consider it NFOGuard-managed ONLY if it has BOTH lockdata=true AND dateadded
|
||||
# This prevents incomplete NFO files from being marked as "complete"
|
||||
if (lockdata_elem is not None and lockdata_elem.text == "true" and
|
||||
dateadded_elem is not None and dateadded_elem.text):
|
||||
# Extract original source from NFOGuard comment, default to nfo_file_existing
|
||||
source = "nfo_file_existing"
|
||||
|
||||
@@ -206,8 +260,10 @@ class NFOManager:
|
||||
aired_elem = root.find('.//aired')
|
||||
lockdata_elem = root.find('.//lockdata')
|
||||
|
||||
# Consider it NFOGuard-managed if it has lockdata=true (with or without dateadded)
|
||||
if lockdata_elem is not None and lockdata_elem.text == "true":
|
||||
# Consider it NFOGuard-managed ONLY if it has BOTH lockdata=true AND dateadded
|
||||
# This prevents incomplete episode NFO files from being marked as "complete"
|
||||
if (lockdata_elem is not None and lockdata_elem.text == "true" and
|
||||
dateadded_elem is not None and dateadded_elem.text):
|
||||
# Extract original source from NFOGuard comment, default to episode_nfo_existing
|
||||
source = "episode_nfo_existing"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user