diff --git a/api/routes.py b/api/routes.py index 6e5642e..cd3e519 100644 --- a/api/routes.py +++ b/api/routes.py @@ -2288,10 +2288,188 @@ async def lookup_episode(imdb_id: str, season: int, episode: int, dependencies: } except Exception as e: - print(f"ERROR: Episode lookup failed for {imdb_id} S{season:02d}E{episode:02d}: {e}") + _log("ERROR", f"Episode lookup failed for {imdb_id} S{season:02d}E{episode:02d}: {e}") raise HTTPException(status_code=500, detail=f"Episode lookup failed: {str(e)}") +def extract_title_and_year_from_path(nfo_path: str) -> tuple: + """ + Extract movie title and year from NFO file path + Examples: + - "/path/Scream (1996)/movie.nfo" -> ("Scream", "1996") + - "/path/Witchboard (2025)/movie.nfo" -> ("Witchboard", "2025") + - "/path/The Conjuring 2 (2016) [tt3065204]/movie.nfo" -> ("The Conjuring 2", "2016") + """ + import re + from pathlib import Path + + # Get the directory name (movie folder) + dir_name = Path(nfo_path).parent.name + + # Pattern to extract title and year: "Title (YYYY)" + pattern = r'^(.+?)\s*\((\d{4})\)' + match = re.search(pattern, dir_name) + + if match: + title = match.group(1).strip() + year = match.group(2) + return title, year + + # Fallback: try to find year anywhere in the path + year_pattern = r'\((\d{4})\)' + year_match = re.search(year_pattern, dir_name) + year = year_match.group(1) if year_match else None + + # Remove any [imdb-*] or [tt*] patterns and year patterns + clean_title = re.sub(r'\s*\[\s*(?:imdb-)?tt\d+\s*\]', '', dir_name) + clean_title = re.sub(r'\s*\(\d{4}\)', '', clean_title) + clean_title = clean_title.strip() + + return clean_title, year + + +async def lookup_movie_comprehensive(nfo_path: str, dependencies: dict): + """ + Comprehensive movie lookup that tries multiple methods: + 1. Extract IMDb ID from path/NFO -> lookup by IMDb + 2. Extract title/year from path -> lookup by title + 3. Extract title only -> fuzzy lookup + """ + try: + _log("DEBUG", f"Comprehensive movie lookup for: {nfo_path}") + + # First try to extract IMDb ID from path + from utils.nfo_patterns import extract_imdb_id_from_text + imdb_id = extract_imdb_id_from_text(nfo_path) + + if imdb_id: + _log("DEBUG", f"Found IMDb ID in path: {imdb_id}") + result = await lookup_movie(imdb_id, dependencies) + if result.get("found"): + result["lookup_method"] = "imdb_from_path" + return result + + # If IMDb lookup failed, try title-based lookup + title, year = extract_title_and_year_from_path(nfo_path) + _log("DEBUG", f"Extracted title: '{title}', year: '{year}' from path") + + if title: + result = await lookup_movie_by_title(title, year, dependencies) + if result.get("found"): + return result + + # All methods failed + return { + "found": False, + "nfo_path": nfo_path, + "extracted_title": title, + "extracted_year": year, + "extracted_imdb": imdb_id, + "lookup_method": "comprehensive_failed" + } + + except Exception as e: + _log("ERROR", f"Comprehensive movie lookup failed for {nfo_path}: {e}") + return { + "found": False, + "error": str(e), + "lookup_method": "comprehensive_error" + } + + +async def lookup_movie_by_title(title: str, year: str, dependencies: dict): + """ + Lookup movie by title and year when IMDb ID is not available + + This is a fallback for when Radarr overwrites NFO files and IMDb IDs are lost. + """ + try: + db = dependencies.get("db") + if not db: + raise HTTPException(status_code=500, detail="Database not available") + + _log("DEBUG", f"Movie lookup by title: '{title}' year: '{year}'") + + # Clean up the title (remove common brackets, extra spaces) + clean_title = title.strip() + + # Search database by title pattern matching + with db.get_connection() as conn: + cursor = conn.cursor() + + # Try exact title match first + if year: + cursor.execute(""" + SELECT * FROM movies + WHERE LOWER(title) = LOWER(%s) AND released::text LIKE %s + ORDER BY dateadded DESC + LIMIT 1 + """, (clean_title, f"{year}%")) + else: + cursor.execute(""" + SELECT * FROM movies + WHERE LOWER(title) = LOWER(%s) + ORDER BY dateadded DESC + LIMIT 1 + """, (clean_title,)) + + row = cursor.fetchone() + + # If exact match fails, try fuzzy matching + if not row: + _log("DEBUG", f"Exact match failed, trying fuzzy match for '{clean_title}'") + + if year: + cursor.execute(""" + SELECT * FROM movies + WHERE LOWER(title) LIKE LOWER(%s) AND released::text LIKE %s + ORDER BY dateadded DESC + LIMIT 1 + """, (f"%{clean_title}%", f"{year}%")) + else: + cursor.execute(""" + SELECT * FROM movies + WHERE LOWER(title) LIKE LOWER(%s) + ORDER BY dateadded DESC + LIMIT 1 + """, (f"%{clean_title}%",)) + + row = cursor.fetchone() + + if row: + result = dict(row) + _log("DEBUG", f"Found movie by title search: {result}") + + if result.get('dateadded'): + dateadded = result['dateadded'] + if hasattr(dateadded, 'isoformat'): + dateadded_str = dateadded.isoformat() + else: + dateadded_str = str(dateadded) + + return { + "found": True, + "imdb_id": result.get('imdb_id'), + "title": result.get('title'), + "dateadded": dateadded_str, + "source": result.get('source', 'database'), + "released": result.get('released') if result.get('released') else None, + "lookup_method": "title_search" + } + + _log("DEBUG", f"No movie found for title '{clean_title}' year '{year}'") + return { + "found": False, + "title": clean_title, + "year": year, + "lookup_method": "title_search" + } + + except Exception as e: + _log("ERROR", f"Movie title lookup failed: {e}") + raise HTTPException(status_code=500, detail=f"Movie title lookup failed: {str(e)}") + + async def lookup_movie(imdb_id: str, dependencies: dict): """ Lookup movie dateadded from NFOGuard database for Emby plugin integration @@ -2304,12 +2482,26 @@ async def lookup_movie(imdb_id: str, dependencies: dict): if not db: raise HTTPException(status_code=500, detail="Database not available") + _log("DEBUG", f"Movie lookup called for IMDb ID: {imdb_id}") + # Normalize IMDb ID (ensure tt prefix) if not imdb_id.startswith('tt'): imdb_id = f"tt{imdb_id}" + _log("DEBUG", f"Normalized IMDb ID: {imdb_id}") + # Query database for movie result = db.get_movie_dates(imdb_id) + _log("DEBUG", f"Movie lookup for {imdb_id}: database result = {result}") + + # If not found, let's see what movies we DO have in the database + if not result: + _log("DEBUG", f"Movie {imdb_id} not found, checking what movies exist in database...") + with db.get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT imdb_id, title FROM movies ORDER BY dateadded DESC LIMIT 10") + recent_movies = cursor.fetchall() + _log("DEBUG", f"Recent movies in database: {[dict(row) for row in recent_movies]}") if result and result.get('dateadded'): # Format response for Emby plugin @@ -2342,7 +2534,7 @@ async def lookup_movie(imdb_id: str, dependencies: dict): } except Exception as e: - print(f"ERROR: Movie lookup failed for {imdb_id}: {e}") + _log("ERROR", f"Movie lookup failed for {imdb_id}: {e}") raise HTTPException(status_code=500, detail=f"Movie lookup failed: {str(e)}") @@ -2488,6 +2680,28 @@ def register_routes(app, dependencies: dict): @app.get("/api/lookup/movie/{imdb_id}") async def _lookup_movie(imdb_id: str): return await lookup_movie(imdb_id, dependencies) + + @app.get("/api/lookup/movie/title/{title}") + async def _lookup_movie_by_title(title: str, year: str = None): + return await lookup_movie_by_title(title, year, dependencies) + + @app.post("/api/lookup/movie/path") + async def _lookup_movie_by_path(request: Request): + data = await request.json() + nfo_path = data.get("nfo_path") + if not nfo_path: + raise HTTPException(status_code=400, detail="nfo_path required") + return await lookup_movie_comprehensive(nfo_path, dependencies) + + @app.get("/api/debug/movie/{title}") + async def _debug_movie_lookup(title: str): + """Debug endpoint to check what movies exist for a given title""" + db = dependencies.get("db") + with db.get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT imdb_id, title, dateadded, source FROM movies WHERE LOWER(title) LIKE LOWER(%s)", (f"%{title}%",)) + movies = cursor.fetchall() + return {"title_search": title, "matches": [dict(row) for row in movies]} # Include monitoring routes from api.monitoring_routes import router as monitoring_router diff --git a/api/web_routes.py b/api/web_routes.py index 94e8fe4..faa86fc 100644 --- a/api/web_routes.py +++ b/api/web_routes.py @@ -1425,8 +1425,8 @@ def register_web_routes(app, dependencies): # Create request with timeout (no body needed for query parameters) req = urllib.request.Request(core_url, method='POST') - # Make request with timeout - with urllib.request.urlopen(req, timeout=30) as response: + # Make request with timeout (increased for manual scans which can take several minutes) + with urllib.request.urlopen(req, timeout=300) as response: response_data = response.read().decode('utf-8') return json.loads(response_data)