#!/usr/bin/env python3 """ FastAPI routes for NFOGuard """ import json from datetime import datetime, timezone from pathlib import Path from typing import Dict, Any, Optional from fastapi import FastAPI, HTTPException, BackgroundTasks, Request from fastapi.responses import JSONResponse # Import models from .models import SonarrWebhook, RadarrWebhook, HealthResponse, TVSeasonRequest, TVEpisodeRequest # Import core utilities from core.logging import _log from core.database import NFOGuardDatabase from core.fs_cache import fs_cache, clear_all_caches from core.xml_cache import xml_cache from core.batch_operations import optimize_library_scan async def _read_payload(request: Request) -> Optional[Dict[str, Any]]: """Read and validate JSON payload from webhook""" try: content_type = request.headers.get("content-type", "") if "application/json" not in content_type.lower(): _log("WARNING", f"Invalid content type: {content_type}") return None body = await request.body() if not body: _log("WARNING", "Empty request body") return None payload = json.loads(body.decode('utf-8')) _log("DEBUG", f"Raw payload: {json.dumps(payload, indent=2)}") return payload except json.JSONDecodeError as e: _log("ERROR", f"Invalid JSON payload: {e}") return None except Exception as e: _log("ERROR", f"Error reading payload: {e}") return None def register_routes( app: FastAPI, tv_processor, movie_processor, batcher, db: NFOGuardDatabase, start_time: datetime, version: str ): """Register all routes with the FastAPI app""" @app.post("/webhook/sonarr") async def sonarr_webhook(request: Request, background_tasks: BackgroundTasks): """Handle Sonarr webhooks""" try: payload = await _read_payload(request) if not payload: raise HTTPException(status_code=422, detail="Empty Sonarr payload") webhook = SonarrWebhook(**payload) _log("INFO", f"Received Sonarr webhook: {webhook.eventType}") if webhook.eventType not in ["Download", "Upgrade", "Rename"]: return {"status": "ignored", "reason": f"Event type {webhook.eventType} not processed"} if not webhook.series: _log("WARNING", "No series data in Sonarr webhook") return {"status": "error", "message": "No series data"} # Process the webhook using the batcher await batcher.queue_sonarr_webhook(payload) return {"status": "queued", "message": f"Sonarr {webhook.eventType} webhook queued for processing"} except Exception as e: _log("ERROR", f"Sonarr webhook error: {e}") raise HTTPException(status_code=500, detail=f"Webhook processing failed: {str(e)}") @app.post("/webhook/radarr") async def radarr_webhook(request: Request, background_tasks: BackgroundTasks): """Handle Radarr webhooks""" try: payload = await _read_payload(request) if not payload: raise HTTPException(status_code=422, detail="Empty Radarr payload") webhook = RadarrWebhook(**payload) _log("INFO", f"Received Radarr webhook: {webhook.eventType}") if webhook.eventType not in ["Download", "Upgrade", "Rename"]: return {"status": "ignored", "reason": f"Event type {webhook.eventType} not processed"} if not webhook.movie: _log("WARNING", "No movie data in Radarr webhook") return {"status": "error", "message": "No movie data"} # Process the webhook using the batcher await batcher.queue_radarr_webhook(payload) return {"status": "queued", "message": f"Radarr {webhook.eventType} webhook queued for processing"} except Exception as e: _log("ERROR", f"Radarr webhook error: {e}") raise HTTPException(status_code=500, detail=f"Webhook processing failed: {str(e)}") @app.get("/health") async def health() -> HealthResponse: """Health check endpoint with database status""" uptime = datetime.now(timezone.utc) - start_time # Check NFOGuard database try: with db.get_connection() as conn: conn.execute("SELECT 1").fetchone() db_status = "healthy" except Exception as e: db_status = f"error: {e}" # Check Radarr database if available radarr_db_health = None overall_status = "healthy" if db_status == "healthy" else "degraded" # Get Radarr client with database access from movie processor try: if hasattr(movie_processor, 'radarr') and movie_processor.radarr: if hasattr(movie_processor.radarr, 'db_client') and movie_processor.radarr.db_client: radarr_test = movie_processor.radarr.db_client.test_connection() radarr_db_health = { "radarr_db_available": radarr_test["success"], "radarr_db_path": radarr_test.get("db_path"), "radarr_db_error": radarr_test.get("error") } if not radarr_test["success"]: overall_status = "degraded" except Exception as e: _log("DEBUG", f"Radarr database health check failed: {e}") return HealthResponse( status=overall_status, version=version, uptime=str(uptime).split('.')[0], # Remove microseconds database_status=db_status, radarr_database=radarr_db_health ) @app.get("/stats") async def get_stats(): """Get processing statistics""" return { "message": "Stats endpoint - implementation needed", "uptime": str(datetime.now(timezone.utc) - start_time).split('.')[0] } @app.get("/batch/status") async def batch_status(): """Get current batch processing status""" return { "pending_items": batcher.get_pending_count(), "processing_items": batcher.get_processing_count(), "total_queued": batcher.get_pending_count() + batcher.get_processing_count() } @app.post("/manual/scan") async def manual_scan(background_tasks: BackgroundTasks): """Trigger manual library scan""" try: _log("INFO", "Manual scan triggered via API") # Add background task to scan all libraries def scan_libraries(): total_processed = 0 # Scan TV libraries for tv_path in config.tv_paths: if tv_path.exists(): _log("INFO", f"Scanning TV library: {tv_path}") for series_dir in tv_path.iterdir(): if series_dir.is_dir() and "[imdb-" in series_dir.name: try: tv_processor.process_series(series_dir) total_processed += 1 except Exception as e: _log("ERROR", f"Error processing series {series_dir.name}: {e}") # Scan Movie libraries for movie_path in config.movie_paths: if movie_path.exists(): _log("INFO", f"Scanning movie library: {movie_path}") for movie_dir in movie_path.iterdir(): if movie_dir.is_dir(): try: movie_processor.process_movie(movie_dir) total_processed += 1 except Exception as e: _log("ERROR", f"Error processing movie {movie_dir.name}: {e}") _log("INFO", f"Manual scan completed: {total_processed} items processed") background_tasks.add_task(scan_libraries) return {"status": "started", "message": "Manual scan initiated", "note": "Scan running in background - check logs for progress"} except Exception as e: _log("ERROR", f"Manual scan failed: {e}") raise HTTPException(status_code=500, detail=f"Manual scan failed: {str(e)}") @app.post("/tv/scan-season") async def scan_tv_season(request: TVSeasonRequest, background_tasks: BackgroundTasks): """Scan specific TV season""" try: _log("INFO", f"TV season scan requested: {request.series_path}/{request.season_name}") return {"status": "started", "message": f"Scanning season {request.season_name}"} except Exception as e: _log("ERROR", f"TV season scan failed: {e}") raise HTTPException(status_code=500, detail=f"Season scan failed: {str(e)}") @app.post("/tv/scan-episode") async def scan_tv_episode(request: TVEpisodeRequest, background_tasks: BackgroundTasks): """Scan specific TV episode""" try: _log("INFO", f"TV episode scan requested: {request.series_path}/{request.season_name}/{request.episode_name}") return {"status": "started", "message": f"Scanning episode {request.episode_name}"} except Exception as e: _log("ERROR", f"TV episode scan failed: {e}") raise HTTPException(status_code=500, detail=f"Episode scan failed: {str(e)}") # Debug endpoints @app.get("/debug/movie/{imdb_id}") async def debug_movie(imdb_id: str): """Debug movie processing for specific IMDb ID""" return {"message": f"Debug info for movie {imdb_id} - implementation needed"} @app.get("/debug/movie/{imdb_id}/history") async def debug_movie_history(imdb_id: str): """Debug movie processing history""" return {"message": f"Debug history for movie {imdb_id} - implementation needed"} @app.get("/debug/movie/{imdb_id}/priority") async def debug_movie_priority(imdb_id: str): """Debug movie priority processing""" return {"message": f"Debug priority for movie {imdb_id} - implementation needed"} @app.get("/debug/tmdb/{imdb_id}") async def debug_tmdb(imdb_id: str): """Debug TMDB API calls for movie""" return {"message": f"Debug TMDB for movie {imdb_id} - implementation needed"} # Test endpoints @app.post("/test/bulk-update") async def test_bulk_update(): """Test bulk update functionality""" return {"message": "Test bulk update - implementation needed"} @app.post("/test/movie-scan") async def test_movie_scan(): """Test movie scan functionality""" return {"message": "Test movie scan - implementation needed"} @app.post("/bulk/update") async def bulk_update(): """Bulk update functionality""" return {"message": "Bulk update - implementation needed"} # Performance monitoring endpoints @app.get("/cache/stats") async def cache_stats(): """Get comprehensive cache performance statistics""" fs_stats = fs_cache.get_cache_stats() xml_stats = xml_cache.get_cache_stats() # Combine stats combined_stats = { "filesystem_cache": fs_stats, "xml_processing_cache": xml_stats, "overall_hit_rate": round( ((fs_stats["cache_hits"] + xml_stats["xml_cache_hits"]) / (fs_stats["cache_hits"] + fs_stats["cache_misses"] + xml_stats["xml_cache_hits"] + xml_stats["xml_cache_misses"]) * 100 ) if (fs_stats["cache_hits"] + fs_stats["cache_misses"] + xml_stats["xml_cache_hits"] + xml_stats["xml_cache_misses"]) > 0 else 0, 2 ) } return combined_stats @app.post("/cache/clear") async def clear_cache(): """Clear all caches""" clear_all_caches() return {"status": "success", "message": "All caches cleared"} @app.post("/scan/optimized") async def optimized_scan(background_tasks: BackgroundTasks): """Trigger optimized library scan using batch operations""" try: _log("INFO", "Optimized scan triggered via API") def run_optimized_scan(): # Combine all library paths all_paths = list(config.tv_paths) + list(config.movie_paths) existing_paths = [p for p in all_paths if p.exists()] if not existing_paths: _log("WARNING", "No valid library paths found") return # Run optimized scan results = optimize_library_scan(existing_paths) _log("INFO", f"Optimized scan complete: {results}") background_tasks.add_task(run_optimized_scan) return { "status": "started", "message": "Optimized library scan initiated", "note": "Uses batch operations and caching for improved performance" } except Exception as e: _log("ERROR", f"Optimized scan failed: {e}") raise HTTPException(status_code=500, detail=f"Optimized scan failed: {str(e)}") @app.post("/nfo/strip-lockdata") async def strip_lockdata_from_library(background_tasks: BackgroundTasks): """Strip lockdata elements from all NFO files for Emby compatibility""" try: _log("INFO", "Lockdata stripping initiated via API") def strip_lockdata_task(): total_processed = 0 # Process TV libraries for tv_path in config.tv_paths: if tv_path.exists(): _log("INFO", f"Stripping lockdata from TV library: {tv_path}") processed = tv_processor.nfo_manager.strip_lockdata_from_directory(tv_path) total_processed += processed # Process Movie libraries for movie_path in config.movie_paths: if movie_path.exists(): _log("INFO", f"Stripping lockdata from movie library: {movie_path}") processed = movie_processor.nfo_manager.strip_lockdata_from_directory(movie_path) total_processed += processed _log("INFO", f"Lockdata stripping complete: {total_processed} NFO files processed") background_tasks.add_task(strip_lockdata_task) return { "status": "started", "message": "Lockdata stripping initiated for Emby compatibility", "note": "This will remove true from all NFO files" } except Exception as e: _log("ERROR", f"Lockdata stripping failed: {e}") raise HTTPException(status_code=500, detail=f"Lockdata stripping failed: {str(e)}") @app.get("/debug/nfo/{path:path}") async def debug_nfo_content(path: str): """Debug NFO file content to troubleshoot Emby title issues""" try: from pathlib import Path import xml.etree.ElementTree as ET # Construct full path nfo_path = Path("/" + path.lstrip("/")) if not nfo_path.exists(): raise HTTPException(status_code=404, detail=f"NFO file not found: {nfo_path}") if not str(nfo_path).endswith('.nfo'): raise HTTPException(status_code=400, detail="Path must point to a .nfo file") # Parse NFO content try: tree = ET.parse(nfo_path) root = tree.getroot() # Extract key elements that Emby uses result = { "file_path": str(nfo_path), "root_element": root.tag, "elements_found": {}, "nfoguard_data": {}, "raw_xml": None } # Check for key elements key_elements = ["title", "plot", "premiered", "dateadded", "lockdata", "uniqueid", "year", "runtime"] for element_name in key_elements: elem = root.find(f".//{element_name}") if elem is not None: result["elements_found"][element_name] = { "text": elem.text, "attributes": elem.attrib if elem.attrib else None } # Check NFOGuard specific data nfoguard_source = root.find(".//nfoguard_source") if nfoguard_source is not None: result["nfoguard_data"]["source"] = nfoguard_source.text # Get first few lines of raw XML for inspection with open(nfo_path, 'r', encoding='utf-8') as f: raw_content = f.read() lines = raw_content.split('\n') result["raw_xml"] = lines[:20] # First 20 lines return result except ET.ParseError as e: # If XML parsing fails, return raw content with open(nfo_path, 'r', encoding='utf-8') as f: raw_content = f.read() return { "file_path": str(nfo_path), "parse_error": str(e), "raw_content": raw_content[:1000] # First 1000 chars } except Exception as e: _log("ERROR", f"NFO debug failed: {e}") raise HTTPException(status_code=500, detail=f"NFO debug failed: {str(e)}") @app.get("/debug/emby-title-test/{imdb_id}") async def test_emby_title_format(imdb_id: str): """Test different NFO title formats for Emby compatibility""" try: # Create a test NFO with different title positioning to see what Emby prefers test_formats = { "title_first": """ Test Movie Title Test plot description {imdb_id} 2023-01-01T12:00:00 """, "title_after_plot": """ Test plot description Test Movie Title {imdb_id} 2023-01-01T12:00:00 """, "title_before_uniqueid": """ Test plot description {imdb_id} Test Movie Title 2023-01-01T12:00:00 """ } for format_name, xml_content in test_formats.items(): test_formats[format_name] = xml_content.format(imdb_id=imdb_id) return { "imdb_id": imdb_id, "test_formats": test_formats, "recommendation": "Try creating a test NFO file with title as the first element after " } except Exception as e: _log("ERROR", f"Emby title test failed: {e}") raise HTTPException(status_code=500, detail=f"Emby title test failed: {str(e)}") @app.get("/debug/tv-episode/{path:path}") async def debug_tv_episode_nfo(path: str): """Debug TV episode NFO specifically for Emby title issues""" try: from pathlib import Path import xml.etree.ElementTree as ET # Handle paths like "media/TV/ShowName/Season 01/S01E01.nfo" nfo_path = Path("/" + path.lstrip("/")) if not nfo_path.exists(): raise HTTPException(status_code=404, detail=f"Episode NFO not found: {nfo_path}") if not str(nfo_path).endswith('.nfo'): raise HTTPException(status_code=400, detail="Path must point to a .nfo file") try: tree = ET.parse(nfo_path) root = tree.getroot() # Check if it's an episode NFO if root.tag != "episodedetails": return {"error": f"Not a TV episode NFO (root is {root.tag}, expected episodedetails)"} # Extract TV-specific elements result = { "file_path": str(nfo_path), "nfo_type": "TV Episode", "emby_compatibility_check": {}, "elements": {}, "video_file_info": None, "recommendations": [] } # Check critical elements for TV episodes tv_elements = ["title", "season", "episode", "aired", "plot", "uniqueid"] for elem_name in tv_elements: elem = root.find(elem_name) if elem is not None: result["elements"][elem_name] = { "text": elem.text, "attributes": elem.attrib if elem.attrib else {} } # Emby compatibility checks title_elem = root.find("title") if title_elem is not None and title_elem.text: result["emby_compatibility_check"]["has_title"] = True result["emby_compatibility_check"]["title_value"] = title_elem.text.strip() result["emby_compatibility_check"]["title_position"] = list(root).index(title_elem) if list(root).index(title_elem) == 0: result["emby_compatibility_check"]["title_is_first"] = True else: result["emby_compatibility_check"]["title_is_first"] = False result["recommendations"].append("Consider moving to be the first element after <episodedetails>") else: result["emby_compatibility_check"]["has_title"] = False result["recommendations"].append("CRITICAL: No <title> element found!") # Check for video file in same directory season_dir = nfo_path.parent episode_pattern = nfo_path.stem # S01E01 from S01E01.nfo video_exts = [".mkv", ".mp4", ".avi", ".mov", ".m4v"] for video_ext in video_exts: potential_video = season_dir / f"{episode_pattern}{video_ext}" if potential_video.exists(): result["video_file_info"] = { "filename": potential_video.name, "matches_nfo": True } break # Look for any video file with the pattern if not result["video_file_info"]: for video_file in season_dir.glob(f"*{episode_pattern}*"): if video_file.suffix.lower() in video_exts: result["video_file_info"] = { "filename": video_file.name, "matches_nfo": episode_pattern in video_file.stem } if not (episode_pattern in video_file.stem): result["recommendations"].append(f"Video filename '{video_file.name}' doesn't cleanly match NFO pattern") break # Element order analysis element_order = [child.tag for child in root] result["element_order"] = element_order # Emby-specific title positioning checks title_position = element_order.index('title') + 1 if 'title' in element_order else -1 result["title_position"] = title_position if title_position > 1: result["recommendations"].append(f"Title element is at position {title_position}. Emby prefers title as first element.") # Check for common Emby compatibility issues emby_checks = { "title_first": element_order[0] == "title" if element_order else False, "has_plot": "plot" in element_order, "has_aired": "aired" in element_order, "has_season": "season" in element_order, "has_episode": "episode" in element_order, "no_lockdata": "lockdata" not in element_order } result["emby_compatibility"] = emby_checks # Additional Emby recommendations if not emby_checks["title_first"]: result["recommendations"].append("Move <title> to be the first element for best Emby compatibility") if not emby_checks["no_lockdata"]: result["recommendations"].append("Remove <lockdata> elements - they can cause Emby metadata issues") # Check title content quality if title_elem is not None and title_elem.text: title_text = title_elem.text.strip() # Check if title looks like filename video_filename_base = None for video_file in video_files: if episode_pattern in video_file.stem: video_filename_base = video_file.stem break if video_filename_base and title_text.lower() in video_filename_base.lower(): result["recommendations"].append(f"Title '{title_text}' appears to be derived from filename - ensure it's a proper episode title") return result except ET.ParseError as e: with open(nfo_path, 'r', encoding='utf-8') as f: raw_content = f.read() return { "file_path": str(nfo_path), "error": "XML Parse Error", "parse_error": str(e), "raw_content_sample": raw_content[:500] } except Exception as e: _log("ERROR", f"TV episode debug failed: {e}") raise HTTPException(status_code=500, detail=f"TV episode debug failed: {str(e)}") @app.post("/debug/validate-series-nfos") async def validate_series_nfos(request: Dict[str, Any]): """Validate NFO files for an entire TV series for Emby compatibility""" try: series_path_str = request.get("series_path") if not series_path_str: raise HTTPException(status_code=400, detail="series_path required") series_path = Path(series_path_str) if not series_path.exists(): raise HTTPException(status_code=404, detail="Series path not found") _log("INFO", f"Validating series NFOs: {series_path}") validation_results = { "series_path": str(series_path), "series_nfo": None, "seasons": {}, "episodes": [], "summary": { "total_nfos": 0, "emby_compatible": 0, "title_position_issues": 0, "lockdata_issues": 0, "common_issues": [] } } # Check series NFO tvshow_nfo = series_path / "tvshow.nfo" if tvshow_nfo.exists(): validation_results["series_nfo"] = await debug_nfo_structure(str(tvshow_nfo), "series") validation_results["summary"]["total_nfos"] += 1 # Check season NFOs and episodes from core.fs_cache import fs_cache season_dirs = [d for d in fs_cache.get_directory_contents(series_path) if d.is_dir() and d.name.lower().startswith("season")] for season_dir in season_dirs: season_name = season_dir.name season_data = {"path": str(season_dir), "nfo": None, "episodes": []} # Check season NFO season_nfo = season_dir / "season.nfo" if season_nfo.exists(): season_data["nfo"] = await debug_nfo_structure(str(season_nfo), "season") validation_results["summary"]["total_nfos"] += 1 # Check episode NFOs video_files = fs_cache.find_video_files(season_dir) for video_file in video_files: nfo_file = video_file.with_suffix('.nfo') if nfo_file.exists(): episode_debug = await debug_tv_episode_nfo(str(nfo_file).replace(str(series_path) + "/", "")) season_data["episodes"].append(episode_debug) validation_results["episodes"].append(episode_debug) validation_results["summary"]["total_nfos"] += 1 # Analyze for summary if "emby_compatibility" in episode_debug: emby_compat = episode_debug["emby_compatibility"] if all(emby_compat.values()): validation_results["summary"]["emby_compatible"] += 1 if not emby_compat.get("title_first", False): validation_results["summary"]["title_position_issues"] += 1 if not emby_compat.get("no_lockdata", True): validation_results["summary"]["lockdata_issues"] += 1 validation_results["seasons"][season_name] = season_data # Generate common issues summary if validation_results["summary"]["title_position_issues"] > 0: validation_results["summary"]["common_issues"].append( f"{validation_results['summary']['title_position_issues']} NFOs have title positioning issues" ) if validation_results["summary"]["lockdata_issues"] > 0: validation_results["summary"]["common_issues"].append( f"{validation_results['summary']['lockdata_issues']} NFOs contain lockdata elements" ) compat_rate = (validation_results["summary"]["emby_compatible"] / validation_results["summary"]["total_nfos"] * 100) if validation_results["summary"]["total_nfos"] > 0 else 0 validation_results["summary"]["compatibility_rate"] = round(compat_rate, 1) return validation_results except Exception as e: _log("ERROR", f"Series NFO validation failed: {e}") raise HTTPException(status_code=500, detail=f"Series NFO validation failed: {str(e)}") async def debug_nfo_structure(nfo_path_str: str, nfo_type: str) -> Dict[str, Any]: """Helper function to debug NFO structure""" try: nfo_path = Path(nfo_path_str) if not nfo_path.exists(): return {"error": "NFO file not found", "path": nfo_path_str} import xml.etree.ElementTree as ET tree = ET.parse(nfo_path) root = tree.getroot() # Basic structure analysis result = { "file_path": str(nfo_path), "root_element": root.tag, "element_count": len(root), "elements": {}, "recommendations": [] } # Element analysis for child in root: if child.text: result["elements"][child.tag] = child.text.strip()[:100] # Truncate long values else: result["elements"][child.tag] = None # Element order analysis element_order = [child.tag for child in root] result["element_order"] = element_order # Common checks if "title" in element_order: title_position = element_order.index('title') + 1 result["title_position"] = title_position if title_position > 1: result["recommendations"].append(f"Title element is at position {title_position}. Consider moving to position 1.") if "lockdata" in element_order: result["recommendations"].append("Contains lockdata element - consider removing for Emby compatibility") return result except Exception as e: return {"error": str(e), "path": nfo_path_str} @app.post("/debug/fix-emby-compatibility") async def fix_emby_compatibility(request: Dict[str, Any]): """Apply Emby compatibility fixes to NFO files""" try: file_path_str = request.get("file_path") series_path_str = request.get("series_path") if not file_path_str and not series_path_str: raise HTTPException(status_code=400, detail="Either file_path or series_path required") results = { "fixed_files": [], "total_processed": 0, "total_updated": 0, "errors": [] } nfo_files = [] # Single file fix if file_path_str: file_path = Path(file_path_str) if not file_path.exists(): raise HTTPException(status_code=404, detail="File not found") if file_path.suffix.lower() == '.nfo': nfo_files.append(file_path) # Series directory fix if series_path_str: series_path = Path(series_path_str) if not series_path.exists(): raise HTTPException(status_code=404, detail="Series path not found") # Find all NFO files in series directory from core.fs_cache import fs_cache # Series NFO tvshow_nfo = series_path / "tvshow.nfo" if tvshow_nfo.exists(): nfo_files.append(tvshow_nfo) # Season and episode NFOs season_dirs = [d for d in fs_cache.get_directory_contents(series_path) if d.is_dir() and d.name.lower().startswith("season")] for season_dir in season_dirs: # Season NFO season_nfo = season_dir / "season.nfo" if season_nfo.exists(): nfo_files.append(season_nfo) # Episode NFOs video_files = fs_cache.find_video_files(season_dir) for video_file in video_files: nfo_file = video_file.with_suffix('.nfo') if nfo_file.exists(): nfo_files.append(nfo_file) # Apply fixes to all found NFO files for nfo_file in nfo_files: try: fix_results = nfo_manager.fix_nfo_for_emby_compatibility(nfo_file) file_result = { "file_path": str(nfo_file), "lockdata_removed": fix_results["lockdata_removed"], "title_reordered": fix_results["title_reordered"], "updated": fix_results["file_updated"] } results["fixed_files"].append(file_result) results["total_processed"] += 1 if fix_results["file_updated"]: results["total_updated"] += 1 except Exception as e: error_msg = f"Error fixing {nfo_file}: {str(e)}" results["errors"].append(error_msg) _log("ERROR", error_msg) _log("INFO", f"Emby compatibility fixes complete: {results['total_updated']} of {results['total_processed']} files updated") return results except Exception as e: _log("ERROR", f"Emby compatibility fix failed: {e}") raise HTTPException(status_code=500, detail=f"Emby compatibility fix failed: {str(e)}")