web: missing nfo serach updates
Local Docker Build (Dev) / build-dev (push) Successful in 5s

This commit is contained in:
2025-10-26 15:15:29 -04:00
parent d933ac7585
commit 9fbb9e37ab
2 changed files with 331 additions and 10 deletions
+275 -8
View File
@@ -2385,11 +2385,67 @@ def register_routes(app, dependencies: dict):
print(f"📊 Found {len(missing_nfo_files)} NFO files missing dateadded elements") print(f"📊 Found {len(missing_nfo_files)} NFO files missing dateadded elements")
# Convert file paths to episode/movie information # Convert file paths to episode/movie information with direct NFO parsing
def extract_imdb_from_nfo_content(nfo_file, is_episode=True):
"""Extract IMDb ID directly from NFO file content - we already know the file exists"""
imdb_id = "unknown"
try:
# Parse the NFO file we already know exists (since grep found it missing dateadded)
tree = ET.parse(nfo_file)
root = tree.getroot()
# Method 1: Check <imdb> tag
imdb_elem = root.find("imdb")
if imdb_elem is not None and imdb_elem.text:
imdb_text = imdb_elem.text.strip()
if imdb_text.startswith('tt'):
print(f"✅ Found IMDb {imdb_text} in <imdb> tag: {os.path.basename(nfo_file)}")
return imdb_text
# Method 2: Check <uniqueid type="imdb"> tags
for uniqueid in root.findall("uniqueid"):
if uniqueid.get("type") == "imdb" and uniqueid.text:
imdb_text = uniqueid.text.strip()
if imdb_text.startswith('tt'):
print(f"✅ Found IMDb {imdb_text} in <uniqueid type='imdb'>: {os.path.basename(nfo_file)}")
return imdb_text
# Method 3: Check for IMDb pattern anywhere in NFO content
nfo_content = ET.tostring(root, encoding='unicode')
content_match = re.search(r'(tt\d{7,})', nfo_content)
if content_match:
imdb_text = content_match.group(1)
print(f"✅ Found IMDb {imdb_text} in NFO content: {os.path.basename(nfo_file)}")
return imdb_text
# Method 4: Fallback - check folder structure for episodes
if is_episode:
series_folder = os.path.dirname(os.path.dirname(nfo_file))
folder_name = os.path.basename(series_folder)
folder_match = re.search(r'\[imdb-(tt\d+)\]', folder_name, re.IGNORECASE)
if folder_match:
print(f"✅ Found IMDb {folder_match.group(1)} in folder: {folder_name}")
return folder_match.group(1)
else:
# For movies, check movie folder
movie_folder = os.path.dirname(nfo_file)
folder_name = os.path.basename(movie_folder)
folder_match = re.search(r'\[imdb-(tt\d+)\]', folder_name, re.IGNORECASE)
if folder_match:
print(f"✅ Found IMDb {folder_match.group(1)} in folder: {folder_name}")
return folder_match.group(1)
except Exception as e:
print(f"⚠️ Error parsing NFO for IMDb in {nfo_file}: {e}")
print(f"❌ No IMDb ID found in {os.path.basename(nfo_file)}")
return "unknown"
for nfo_file in missing_nfo_files: for nfo_file in missing_nfo_files:
try: try:
# Parse episode info from file path # Parse episode info from file path
# Example: /media/TV/tv/Series Name/Season 08/Series-S08E05-Episode.nfo # Example: /media/TV/tv/Hudson & Rex (2019) [imdb-tt9111220]/Season 08/Hudson & Rex (2019)-S08E05-Episode.nfo
path_parts = nfo_file.split('/') path_parts = nfo_file.split('/')
# Look for season/episode pattern # Look for season/episode pattern
@@ -2400,15 +2456,21 @@ def register_routes(app, dependencies: dict):
season = int(episode_match.group(1)) season = int(episode_match.group(1))
episode = int(episode_match.group(2)) episode = int(episode_match.group(2))
# Extract series name from path # Extract series name and path
series_path = os.path.dirname(os.path.dirname(nfo_file)) series_path = os.path.dirname(os.path.dirname(nfo_file))
series_name = os.path.basename(series_path) series_name = os.path.basename(series_path)
# Extract IMDb ID from NFO content
imdb_id = extract_imdb_from_nfo_content(nfo_file, is_episode=True)
# Clean series name (remove year and imdb parts)
clean_series_name = re.sub(r'\s*\(\d{4}\)\s*\[imdb-[^]]+\]', '', series_name).strip()
missing_items["episodes"].append({ missing_items["episodes"].append({
"imdb_id": "unknown", # We'll lookup later if needed "imdb_id": imdb_id,
"season": season, "season": season,
"episode": episode, "episode": episode,
"series_name": series_name, "series_name": clean_series_name,
"series_path": series_path, "series_path": series_path,
"dateadded": None, "dateadded": None,
"nfo_path": nfo_file, "nfo_path": nfo_file,
@@ -2460,12 +2522,19 @@ def register_routes(app, dependencies: dict):
# If grep returns non-zero, dateadded is missing # If grep returns non-zero, dateadded is missing
if grep_result.returncode != 0: if grep_result.returncode != 0:
# Extract movie info from file path # Extract movie info from file path
# Example: /media/Movies/movies/Knives Out (2019) [imdb-tt8946378]/movie.nfo
movie_dir = os.path.dirname(nfo_file) movie_dir = os.path.dirname(nfo_file)
movie_title = os.path.basename(movie_dir) movie_folder_name = os.path.basename(movie_dir)
# Extract IMDb ID from NFO content
imdb_id = extract_imdb_from_nfo_content(nfo_file, is_episode=False)
# Clean movie title (remove year and imdb parts)
clean_movie_title = re.sub(r'\s*\(\d{4}\)\s*\[imdb-[^]]+\]', '', movie_folder_name).strip()
missing_items["movies"].append({ missing_items["movies"].append({
"imdb_id": "unknown", # We'll lookup later if needed "imdb_id": imdb_id,
"title": movie_title, "title": clean_movie_title,
"path": movie_dir, "path": movie_dir,
"dateadded": None, "dateadded": None,
"nfo_path": nfo_file, "nfo_path": nfo_file,
@@ -2489,6 +2558,55 @@ def register_routes(app, dependencies: dict):
else: else:
print(f"❌ Movie path does not exist: {movie_path}") print(f"❌ Movie path does not exist: {movie_path}")
# Database lookup to get actual dateadded values for missing items
print("🔍 Looking up dateadded values from database for missing items...")
# Enhance episodes with database data
for episode in missing_items["episodes"]:
if episode["imdb_id"] != "unknown":
try:
with db.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT dateadded, source, added_by
FROM episodes
WHERE imdb_id = %s AND season = %s AND episode = %s
LIMIT 1
""", (episode["imdb_id"], episode["season"], episode["episode"]))
result = cursor.fetchone()
if result:
episode["dateadded"] = result["dateadded"]
episode["source"] = result.get("source", "database")
episode["added_by"] = result.get("added_by", "unknown")
else:
print(f"⚠️ No database entry for {episode['imdb_id']} S{episode['season']:02d}E{episode['episode']:02d}")
except Exception as e:
print(f"⚠️ Database lookup failed for episode {episode['imdb_id']}: {e}")
# Enhance movies with database data
for movie in missing_items["movies"]:
if movie["imdb_id"] != "unknown":
try:
with db.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT dateadded, source, added_by
FROM movies
WHERE imdb_id = %s
LIMIT 1
""", (movie["imdb_id"],))
result = cursor.fetchone()
if result:
movie["dateadded"] = result["dateadded"]
movie["source"] = result.get("source", "database")
movie["added_by"] = result.get("added_by", "unknown")
else:
print(f"⚠️ No database entry for movie {movie['imdb_id']}")
except Exception as e:
print(f"⚠️ Database lookup failed for movie {movie['imdb_id']}: {e}")
total_missing = len(missing_items["episodes"]) + len(missing_items["movies"]) total_missing = len(missing_items["episodes"]) + len(missing_items["movies"])
print(f"✅ NFO repair scan complete: {total_missing} items missing dateadded elements") print(f"✅ NFO repair scan complete: {total_missing} items missing dateadded elements")
@@ -2510,6 +2628,155 @@ def register_routes(app, dependencies: dict):
async def _nfo_repair_scan(): async def _nfo_repair_scan():
return await nfo_repair_scan() return await nfo_repair_scan()
async def nfo_repair_fix():
"""Fix missing dateadded elements in NFO files using database values"""
import os
import xml.etree.ElementTree as ET
from datetime import datetime
config = dependencies.get("config")
db = dependencies.get("db")
nfo_manager = dependencies.get("nfo_manager")
if not config or not db:
raise HTTPException(status_code=500, detail=f"Dependencies not available")
print("🔧 Starting NFO repair fix process from core container")
# First, run the scan to identify missing items
scan_result = await nfo_repair_scan()
if scan_result["status"] != "success":
return {"status": "error", "message": "Scan failed"}
missing_items = scan_result["missing_items"]
total_episodes = len(missing_items["episodes"])
total_movies = len(missing_items["movies"])
print(f"🔍 Found {total_episodes} episodes and {total_movies} movies to fix")
fixed_count = 0
failed_count = 0
results = []
# Fix episodes
for episode in missing_items["episodes"]:
if episode.get("dateadded") and episode.get("imdb_id") != "unknown":
try:
nfo_path = episode["nfo_path"]
dateadded_value = episode["dateadded"]
# Format dateadded for NFO file
if isinstance(dateadded_value, str):
# Parse string datetime
dt = datetime.fromisoformat(dateadded_value.replace('Z', '+00:00'))
else:
dt = dateadded_value
# Format as NFO expects: 2024-10-15 12:34:56
nfo_dateadded = dt.strftime('%Y-%m-%d %H:%M:%S')
# Read and modify NFO file
if os.path.exists(nfo_path):
tree = ET.parse(nfo_path)
root = tree.getroot()
# Remove existing dateadded element if present
existing = root.find("dateadded")
if existing is not None:
root.remove(existing)
# Add new dateadded element
dateadded_elem = ET.SubElement(root, "dateadded")
dateadded_elem.text = nfo_dateadded
# Write back to file
tree.write(nfo_path, encoding='utf-8', xml_declaration=True)
fixed_count += 1
results.append({
"type": "episode",
"imdb_id": episode["imdb_id"],
"season": episode["season"],
"episode": episode["episode"],
"nfo_path": nfo_path,
"dateadded": nfo_dateadded,
"status": "fixed"
})
print(f"✅ Fixed episode {episode['imdb_id']} S{episode['season']:02d}E{episode['episode']:02d}")
else:
failed_count += 1
print(f"❌ NFO file not found: {nfo_path}")
except Exception as e:
failed_count += 1
print(f"❌ Failed to fix episode {episode.get('imdb_id', 'unknown')}: {e}")
# Fix movies
for movie in missing_items["movies"]:
if movie.get("dateadded") and movie.get("imdb_id") != "unknown":
try:
nfo_path = movie["nfo_path"]
dateadded_value = movie["dateadded"]
# Format dateadded for NFO file
if isinstance(dateadded_value, str):
dt = datetime.fromisoformat(dateadded_value.replace('Z', '+00:00'))
else:
dt = dateadded_value
nfo_dateadded = dt.strftime('%Y-%m-%d %H:%M:%S')
# Read and modify NFO file
if os.path.exists(nfo_path):
tree = ET.parse(nfo_path)
root = tree.getroot()
# Remove existing dateadded element if present
existing = root.find("dateadded")
if existing is not None:
root.remove(existing)
# Add new dateadded element
dateadded_elem = ET.SubElement(root, "dateadded")
dateadded_elem.text = nfo_dateadded
# Write back to file
tree.write(nfo_path, encoding='utf-8', xml_declaration=True)
fixed_count += 1
results.append({
"type": "movie",
"imdb_id": movie["imdb_id"],
"title": movie["title"],
"nfo_path": nfo_path,
"dateadded": nfo_dateadded,
"status": "fixed"
})
print(f"✅ Fixed movie {movie['imdb_id']} ({movie['title']})")
else:
failed_count += 1
print(f"❌ NFO file not found: {nfo_path}")
except Exception as e:
failed_count += 1
print(f"❌ Failed to fix movie {movie.get('imdb_id', 'unknown')}: {e}")
print(f"✅ NFO repair fix complete: {fixed_count} fixed, {failed_count} failed")
return {
"status": "success",
"total_processed": total_episodes + total_movies,
"fixed_count": fixed_count,
"failed_count": failed_count,
"results": results[:20] # Show first 20 for UI
}
@app.post("/admin/nfo-repair-fix")
async def _nfo_repair_fix():
return await nfo_repair_fix()
# --------------------------- # ---------------------------
# Core API - No Web Interface # Core API - No Web Interface
# --------------------------- # ---------------------------
+56 -2
View File
@@ -1627,6 +1627,55 @@ async def get_episodes_missing_nfo_dateadded(dependencies: dict):
} }
async def fix_nfo_missing_dateadded(dependencies: dict):
"""Fix NFO files missing dateadded elements via core container"""
config = dependencies["config"]
try:
print("🔧 Calling core container for NFO repair fix...")
# Call the core container's NFO repair fix endpoint
import aiohttp
import asyncio
core_url = f"http://nfoguard:{config.core_api_port}/admin/nfo-repair-fix"
print(f"🔍 DEBUG: Calling core container at: {core_url}")
timeout = aiohttp.ClientTimeout(total=600.0) # 10 minute timeout for fix operation
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(core_url) as response:
if response.status == 200:
result = await response.json()
print(f"✅ Core container fix complete: {result.get('fixed_count', 0)} fixed, {result.get('failed_count', 0)} failed")
return {
"success": True,
"fixed_count": result.get("fixed_count", 0),
"failed_count": result.get("failed_count", 0),
"total_processed": result.get("total_processed", 0),
"results": result.get("results", []),
"message": f"Successfully fixed {result.get('fixed_count', 0)} NFO files"
}
else:
error_text = await response.text()
print(f"❌ Core container fix failed: {response.status} - {error_text}")
return {
"success": False,
"error": f"Core container fix failed: {response.status}",
"message": f"Failed to fix NFO files: {error_text}"
}
except Exception as e:
print(f"❌ Error calling core container for NFO repair fix: {str(e)}")
import traceback
traceback.print_exc()
return {
"success": False,
"error": f"Core container communication failed: {str(e)}",
"message": f"Failed to fix NFO files: {str(e)}"
}
async def bulk_update_nfo_files(dependencies: dict, imdb_ids: list = None, fix_all: bool = False): async def bulk_update_nfo_files(dependencies: dict, imdb_ids: list = None, fix_all: bool = False):
"""Update NFO files with missing dateadded elements from database""" """Update NFO files with missing dateadded elements from database"""
db = dependencies["db"] db = dependencies["db"]
@@ -1747,8 +1796,13 @@ def register_database_admin_routes(app, dependencies):
@app.post("/api/admin/nfo/bulk-update") @app.post("/api/admin/nfo/bulk-update")
async def api_bulk_update_nfo_files(imdb_ids: list = None, fix_all: bool = False): async def api_bulk_update_nfo_files(imdb_ids: list = None, fix_all: bool = False):
"""Bulk update NFO files with missing dateadded""" """Bulk update NFO files with missing dateadded via core container"""
return await bulk_update_nfo_files(dependencies, imdb_ids, fix_all) # For now, we only support fix_all mode using the new core container approach
if fix_all or not imdb_ids:
return await fix_nfo_missing_dateadded(dependencies)
else:
# Legacy mode for specific IMDb IDs - fallback to old method
return await bulk_update_nfo_files(dependencies, imdb_ids, fix_all)
@app.get("/api/admin/database/quick-queries") @app.get("/api/admin/database/quick-queries")
async def api_database_quick_queries(): async def api_database_quick_queries():