imdb: missing imdge improvments
Local Docker Build (Dev) / build-dev (push) Successful in 5s

This commit is contained in:
2025-10-26 15:48:26 -04:00
parent d6488a99bd
commit 9072c86779
6 changed files with 472 additions and 5 deletions
+99 -5
View File
@@ -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
# ---------------------------
+57
View File
@@ -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"""
+90
View File
@@ -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'):
+113
View File
@@ -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;
}
}
+3
View File
@@ -455,6 +455,9 @@
<button class="btn btn-info" onclick="showEpisodesMissingNFODateadded()">
<i class="fas fa-exclamation-triangle"></i> Episodes Missing NFO dateadded
</button>
<button class="btn btn-warning" onclick="showMissingImdbItems()">
<i class="fas fa-search"></i> Items Missing IMDb IDs
</button>
<button class="btn btn-secondary" onclick="exportDatabaseReport()">
<i class="fas fa-download"></i> Export Database Report
</button>
+110
View File
@@ -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 = '<div class="loading">Checking for items missing IMDb IDs...</div>';
resultsDiv.style.display = 'block';
const response = await apiCall('/api/admin/missing-imdb');
if (response.success) {
let html = `
<h4>Items Missing IMDb IDs</h4>
<p><strong>Found:</strong> ${response.total_missing} items missing IMDb IDs</p>
<p><strong>TV Series:</strong> ${response.tv_series_missing} | <strong>Movies:</strong> ${response.movies_missing}</p>
`;
if (response.total_missing > 0) {
html += `
<p><strong>Recommendation:</strong> These items need manual IMDb ID assignment or folder/filename correction.</p>
<div class="missing-imdb-table-container">
<table class="missing-imdb-table">
<thead>
<tr>
<th style="width: 10%">Type</th>
<th style="width: 25%">Folder Name</th>
<th style="width: 35%">File Path</th>
<th style="width: 15%">Discovered</th>
<th style="width: 10%">Checks</th>
<th style="width: 5%">Action</th>
</tr>
</thead>
<tbody>
`;
response.items.forEach(item => {
const discoveredDate = item.discovered_at ? new Date(item.discovered_at).toLocaleDateString() : 'Unknown';
const checkCount = item.check_count || 1;
html += `
<tr>
<td>
<span class="media-type-badge ${item.type.toLowerCase().replace(' ', '-')}">${item.type}</span>
</td>
<td title="${item.folder_name}">${item.folder_name}</td>
<td title="${item.file_path}" class="file-path">${item.file_path}</td>
<td>${discoveredDate}</td>
<td>${checkCount}</td>
<td>
<button class="btn btn-small btn-info" title="Copy path to clipboard" onclick="copyToClipboard('${item.file_path}')">
<i class="fas fa-copy"></i>
</button>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
<div class="missing-imdb-actions">
<p><strong>How to fix:</strong></p>
<ul>
<li>Add IMDb ID to folder name: <code>[imdb-tt1234567]</code></li>
<li>Add IMDb ID to filename: <code>Movie.Name.2023.imdb-tt1234567.mkv</code></li>
<li>Add IMDb ID to NFO file content</li>
<li>Check folder structure and naming conventions</li>
</ul>
</div>
`;
} else {
html += '<p>✅ All items have valid IMDb IDs!</p>';
}
resultsContent.innerHTML = html;
} else {
resultsContent.innerHTML = `<div class="error">Error: ${response.message}</div>`;
}
} catch (error) {
resultsContent.innerHTML = `<div class="error">Failed to check missing IMDb items: ${error.message}</div>`;
}
}
async function exportDatabaseReport() {
alert('Database export functionality will be implemented in a future update.');
}
@@ -2093,3 +2177,29 @@ async function showRecentWebhookActivity() {
resultsContent.innerHTML = `<div class="error">Failed to load webhook activity: ${error.message}</div>`;
}
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
// Show a temporary success message
const button = event.target.closest('button');
const originalIcon = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
button.style.backgroundColor = '#4CAF50';
setTimeout(() => {
button.innerHTML = originalIcon;
button.style.backgroundColor = '';
}, 1500);
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
alert('Path copied to clipboard!');
}
}