diff --git a/api/routes.py b/api/routes.py index babcca4..e5c25c9 100644 --- a/api/routes.py +++ b/api/routes.py @@ -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 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 tag: {os.path.basename(nfo_file)}") + return imdb_text + + # Method 2: Check 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 : {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 # --------------------------- diff --git a/api/web_routes.py b/api/web_routes.py index 3fc9c65..fc0a233 100644 --- a/api/web_routes.py +++ b/api/web_routes.py @@ -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,8 +1796,13 @@ 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""" - return await bulk_update_nfo_files(dependencies, imdb_ids, fix_all) + """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") async def api_database_quick_queries():