diff --git a/api/routes.py b/api/routes.py index e9e6209..cd33c43 100644 --- a/api/routes.py +++ b/api/routes.py @@ -844,9 +844,25 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N for item in scan_path.iterdir(): if (item.is_dir() and not item.name.lower().startswith('season') and - not re.match(r'^season\s+\d+$', item.name, re.IGNORECASE) and - nfo_manager.parse_imdb_from_path(item)): - tv_series_list.append(item) + not re.match(r'^season\s+\d+$', item.name, re.IGNORECASE)): + + # Check for IMDb ID + imdb_id = nfo_manager.parse_imdb_from_path(item) + if imdb_id: + tv_series_list.append(item) + else: + # Log missing IMDb ID for TV series + try: + db.add_missing_imdb( + file_path=str(item), + media_type="tv", + folder_name=item.name, + filename=None, + notes=f"TV series directory without IMDb ID detected during scan" + ) + print(f"⚠️ Missing IMDb ID: TV series {item.name} - logged for manual review") + except Exception as e: + print(f"❌ Failed to log missing IMDb for TV series {item.name}: {e}") tv_series_count = len(tv_series_list) update_scan_status("tv", tv_series_total=tv_series_count) @@ -894,8 +910,24 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N # Count total movies first for progress tracking movie_list = [] for item in scan_path.iterdir(): - if item.is_dir() and nfo_manager.find_movie_imdb_id(item): - movie_list.append(item) + if item.is_dir(): + # Check for IMDb ID + imdb_id = nfo_manager.find_movie_imdb_id(item) + if imdb_id: + movie_list.append(item) + else: + # Log missing IMDb ID for movie + try: + db.add_missing_imdb( + file_path=str(item), + media_type="movie", + folder_name=item.name, + filename=None, + notes=f"Movie directory without IMDb ID detected during scan" + ) + print(f"⚠️ Missing IMDb ID: Movie {item.name} - logged for manual review") + except Exception as e: + print(f"❌ Failed to log missing IMDb for movie {item.name}: {e}") movie_total_count = len(movie_list) update_scan_status(movies_total=movie_total_count) @@ -2967,6 +2999,68 @@ def register_routes(app, dependencies: dict): async def _nfo_repair_fix(): return await nfo_repair_fix() + @app.get("/admin/missing-imdb") + async def get_missing_imdb(): + """Get items missing IMDb IDs for manual review""" + config = dependencies.get("config") + db = dependencies.get("db") + + if not config or not db: + raise HTTPException(status_code=500, detail="Dependencies not available") + + print("📋 Retrieving missing IMDb items from core container") + + try: + # Get missing items by type + missing_tv = db.get_missing_imdb_items(media_type="tv", resolved=False) + missing_movies = db.get_missing_imdb_items(media_type="movie", resolved=False) + + # Format for web interface + missing_items = [] + + for item in missing_tv: + missing_items.append({ + "id": item["id"], + "type": "TV Series", + "folder_name": item["folder_name"], + "file_path": item["file_path"], + "discovered_at": item["discovered_at"].isoformat() if item["discovered_at"] else None, + "last_checked": item["last_checked"].isoformat() if item["last_checked"] else None, + "check_count": item["check_count"], + "notes": item["notes"] + }) + + for item in missing_movies: + missing_items.append({ + "id": item["id"], + "type": "Movie", + "folder_name": item["folder_name"], + "file_path": item["file_path"], + "discovered_at": item["discovered_at"].isoformat() if item["discovered_at"] else None, + "last_checked": item["last_checked"].isoformat() if item["last_checked"] else None, + "check_count": item["check_count"], + "notes": item["notes"] + }) + + print(f"✅ Found {len(missing_tv)} TV series and {len(missing_movies)} movies missing IMDb IDs") + + return { + "status": "success", + "missing_items": missing_items, + "summary": { + "total_missing": len(missing_items), + "tv_series": len(missing_tv), + "movies": len(missing_movies) + } + } + + except Exception as e: + print(f"❌ Error retrieving missing IMDb items: {e}") + return { + "status": "error", + "message": f"Failed to retrieve missing IMDb items: {str(e)}" + } + # --------------------------- # Core API - No Web Interface # --------------------------- diff --git a/api/web_routes.py b/api/web_routes.py index fc0a233..e311934 100644 --- a/api/web_routes.py +++ b/api/web_routes.py @@ -1676,6 +1676,58 @@ async def fix_nfo_missing_dateadded(dependencies: dict): } +async def get_missing_imdb_items(dependencies: dict): + """Get items missing IMDb IDs for manual review via core container""" + config = dependencies["config"] + + try: + print("📋 Calling core container for missing IMDb items...") + + # Call the core container's missing IMDb endpoint + import aiohttp + import asyncio + + core_url = f"http://nfoguard:{config.core_api_port}/admin/missing-imdb" + print(f"🔍 DEBUG: Calling core container at: {core_url}") + + timeout = aiohttp.ClientTimeout(total=60.0) # 1 minute timeout + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(core_url) as response: + if response.status == 200: + result = await response.json() + print(f"✅ Core container retrieved missing IMDb items: {result['summary']['total_missing']} items") + + return { + "success": True, + "total_missing": result['summary']['total_missing'], + "tv_series_missing": result['summary']['tv_series'], + "movies_missing": result['summary']['movies'], + "items": result['missing_items'], + "debug_info": { + "scan_method": "core_container_database", + "core_container_response": "success" + } + } + else: + error_text = await response.text() + print(f"❌ Core container missing IMDb retrieval failed: {response.status} - {error_text}") + return { + "success": False, + "error": f"Core container retrieval failed: {response.status}", + "message": f"Failed to retrieve missing IMDb items via core container: {response.status}" + } + + except Exception as e: + print(f"❌ Error calling core container for missing IMDb items: {str(e)}") + import traceback + traceback.print_exc() + return { + "success": False, + "error": f"Core container communication failed: {str(e)}", + "message": f"Failed to retrieve missing IMDb items: {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"] @@ -1794,6 +1846,11 @@ def register_database_admin_routes(app, dependencies): """Get episodes missing dateadded in NFO files""" return await get_episodes_missing_nfo_dateadded(dependencies) + @app.get("/api/admin/missing-imdb") + async def api_missing_imdb_items(): + """Get items missing IMDb IDs for manual review""" + return await get_missing_imdb_items(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 via core container""" diff --git a/core/database.py b/core/database.py index 088f114..de17faf 100644 --- a/core/database.py +++ b/core/database.py @@ -129,11 +129,32 @@ class NFOGuardDatabase: ) """) + # Missing IMDb tracking table + cursor.execute(""" + CREATE TABLE IF NOT EXISTS missing_imdb ( + id SERIAL PRIMARY KEY, + file_path TEXT NOT NULL UNIQUE, + media_type VARCHAR(20) NOT NULL, + folder_name TEXT, + filename TEXT, + discovered_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_checked TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + check_count INTEGER DEFAULT 1, + notes TEXT, + resolved BOOLEAN DEFAULT FALSE, + resolved_at TIMESTAMP, + resolved_imdb_id VARCHAR(20) + ) + """) + # Create indexes for PostgreSQL cursor.execute("CREATE INDEX IF NOT EXISTS idx_episodes_imdb ON episodes(imdb_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_episodes_video ON episodes(has_video_file)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_movies_video ON movies(has_video_file)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_history_imdb ON processing_history(imdb_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_missing_imdb_type ON missing_imdb(media_type)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_missing_imdb_resolved ON missing_imdb(resolved)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_missing_imdb_path ON missing_imdb(file_path)") def upsert_series(self, imdb_id: str, path: str, metadata: Optional[Dict] = None): """Insert or update series record""" with self.get_connection() as conn: @@ -583,6 +604,75 @@ class NFOGuardDatabase: return deleted_series + def add_missing_imdb(self, file_path: str, media_type: str, folder_name: str = None, filename: str = None, notes: str = None): + """Add or update a missing IMDb entry""" + with self.get_connection() as conn: + cursor = conn.cursor() + + cursor.execute(""" + INSERT INTO missing_imdb (file_path, media_type, folder_name, filename, notes) + VALUES (%s, %s, %s, %s, %s) + ON CONFLICT (file_path) DO UPDATE SET + last_checked = CURRENT_TIMESTAMP, + check_count = missing_imdb.check_count + 1, + media_type = EXCLUDED.media_type, + folder_name = EXCLUDED.folder_name, + filename = EXCLUDED.filename, + notes = EXCLUDED.notes + """, (file_path, media_type, folder_name, filename, notes)) + + conn.commit() + + def get_missing_imdb_items(self, media_type: str = None, resolved: bool = False) -> List[Dict]: + """Get missing IMDb items, optionally filtered by type and resolution status""" + with self.get_connection() as conn: + cursor = conn.cursor() + + query = """ + SELECT id, file_path, media_type, folder_name, filename, + discovered_at, last_checked, check_count, notes, + resolved, resolved_at, resolved_imdb_id + FROM missing_imdb + WHERE resolved = %s + """ + params = [resolved] + + if media_type: + query += " AND media_type = %s" + params.append(media_type) + + query += " ORDER BY last_checked DESC" + + cursor.execute(query, params) + return cursor.fetchall() + + def resolve_missing_imdb(self, file_path: str, imdb_id: str): + """Mark a missing IMDb item as resolved""" + with self.get_connection() as conn: + cursor = conn.cursor() + + cursor.execute(""" + UPDATE missing_imdb + SET resolved = TRUE, + resolved_at = CURRENT_TIMESTAMP, + resolved_imdb_id = %s + WHERE file_path = %s + """, (imdb_id, file_path)) + + conn.commit() + return cursor.rowcount > 0 + + def delete_missing_imdb(self, file_path: str) -> bool: + """Delete a missing IMDb entry""" + with self.get_connection() as conn: + cursor = conn.cursor() + + cursor.execute("DELETE FROM missing_imdb WHERE file_path = %s", (file_path,)) + deleted_count = cursor.rowcount + conn.commit() + + return deleted_count > 0 + def close(self): """Close all database connections""" if hasattr(self._local, 'connection'): diff --git a/nfoguard-web/static/css/styles.css b/nfoguard-web/static/css/styles.css index 42e54f1..5c46bb4 100644 --- a/nfoguard-web/static/css/styles.css +++ b/nfoguard-web/static/css/styles.css @@ -1150,6 +1150,105 @@ textarea { border-color: #117a8b; } +/* Missing IMDb Items Styles */ +.missing-imdb-table-container { + overflow-x: auto; + max-height: 600px; + border: 1px solid var(--border-color); + border-radius: 8px; + margin: 1rem 0; +} + +.missing-imdb-table { + width: 100%; + border-collapse: collapse; + min-width: 800px; +} + +.missing-imdb-table thead { + background-color: var(--bg-secondary); + position: sticky; + top: 0; + z-index: 1; +} + +.missing-imdb-table th, +.missing-imdb-table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid var(--border-color); + vertical-align: top; +} + +.missing-imdb-table th { + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; +} + +.missing-imdb-table td { + color: var(--text-secondary); +} + +.missing-imdb-table .file-path { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.85rem; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.media-type-badge { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.media-type-badge.tv-series { + background-color: #e3f2fd; + color: #1976d2; +} + +.media-type-badge.movie { + background-color: #fff3e0; + color: #f57c00; +} + +.missing-imdb-actions { + background-color: var(--bg-secondary); + padding: 1rem; + border-radius: 8px; + margin-top: 1rem; +} + +.missing-imdb-actions ul { + margin: 0.5rem 0 0 1rem; + padding: 0; +} + +.missing-imdb-actions li { + margin-bottom: 0.5rem; +} + +.missing-imdb-actions code { + background-color: var(--bg-primary); + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.85rem; + border: 1px solid var(--border-color); +} + +.btn-small { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + min-width: auto; +} + /* Responsive adjustments for admin tools */ @media (max-width: 768px) { .table-container { @@ -1168,4 +1267,18 @@ textarea { .admin-tools { gap: 0.75rem; } + + .missing-imdb-table { + min-width: 600px; + } + + .missing-imdb-table th, + .missing-imdb-table td { + padding: 0.5rem; + font-size: 0.85rem; + } + + .missing-imdb-table .file-path { + max-width: 200px; + } } \ No newline at end of file diff --git a/nfoguard-web/static/index.html b/nfoguard-web/static/index.html index fbbbcd6..1f5a72a 100644 --- a/nfoguard-web/static/index.html +++ b/nfoguard-web/static/index.html @@ -455,6 +455,9 @@ + diff --git a/nfoguard-web/static/js/app.js b/nfoguard-web/static/js/app.js index 7daf249..6eb5b3d 100644 --- a/nfoguard-web/static/js/app.js +++ b/nfoguard-web/static/js/app.js @@ -2031,6 +2031,90 @@ async function showEpisodesMissingNFODateadded() { } } +async function showMissingImdbItems() { + const resultsDiv = document.getElementById('admin-results'); + const resultsContent = document.getElementById('admin-results-content'); + + try { + resultsContent.innerHTML = '
Found: ${response.total_missing} items missing IMDb IDs
+TV Series: ${response.tv_series_missing} | Movies: ${response.movies_missing}
+ `; + + if (response.total_missing > 0) { + html += ` +Recommendation: These items need manual IMDb ID assignment or folder/filename correction.
+| Type | +Folder Name | +File Path | +Discovered | +Checks | +Action | +
|---|---|---|---|---|---|
| + ${item.type} + | +${item.folder_name} | +${item.file_path} | +${discoveredDate} | +${checkCount} | ++ + | +
How to fix:
+[imdb-tt1234567]Movie.Name.2023.imdb-tt1234567.mkv✅ All items have valid IMDb IDs!
'; + } + + resultsContent.innerHTML = html; + } else { + resultsContent.innerHTML = `