This commit is contained in:
+275
-8
@@ -2385,11 +2385,67 @@ def register_routes(app, dependencies: dict):
|
||||
|
||||
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:
|
||||
try:
|
||||
# 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('/')
|
||||
|
||||
# Look for season/episode pattern
|
||||
@@ -2400,15 +2456,21 @@ def register_routes(app, dependencies: dict):
|
||||
season = int(episode_match.group(1))
|
||||
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_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({
|
||||
"imdb_id": "unknown", # We'll lookup later if needed
|
||||
"imdb_id": imdb_id,
|
||||
"season": season,
|
||||
"episode": episode,
|
||||
"series_name": series_name,
|
||||
"series_name": clean_series_name,
|
||||
"series_path": series_path,
|
||||
"dateadded": None,
|
||||
"nfo_path": nfo_file,
|
||||
@@ -2460,12 +2522,19 @@ def register_routes(app, dependencies: dict):
|
||||
# If grep returns non-zero, dateadded is missing
|
||||
if grep_result.returncode != 0:
|
||||
# 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_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({
|
||||
"imdb_id": "unknown", # We'll lookup later if needed
|
||||
"title": movie_title,
|
||||
"imdb_id": imdb_id,
|
||||
"title": clean_movie_title,
|
||||
"path": movie_dir,
|
||||
"dateadded": None,
|
||||
"nfo_path": nfo_file,
|
||||
@@ -2489,6 +2558,55 @@ def register_routes(app, dependencies: dict):
|
||||
else:
|
||||
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"])
|
||||
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():
|
||||
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
|
||||
# ---------------------------
|
||||
|
||||
+55
-1
@@ -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):
|
||||
"""Update NFO files with missing dateadded elements from database"""
|
||||
db = dependencies["db"]
|
||||
@@ -1747,7 +1796,12 @@ def register_database_admin_routes(app, dependencies):
|
||||
|
||||
@app.post("/api/admin/nfo/bulk-update")
|
||||
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"""
|
||||
# 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")
|
||||
|
||||
Reference in New Issue
Block a user