update with the ability to add IMDB to skipped
Local Docker Build (Dev) / build-dev (push) Successful in 35s
Local Docker Build (Dev) / build-dev (push) Successful in 35s
This commit is contained in:
@@ -1525,6 +1525,41 @@ def register_web_routes(app, dependencies):
|
|||||||
print(f"❌ Error deleting movie: {e}")
|
print(f"❌ Error deleting movie: {e}")
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to delete movie: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Failed to delete movie: {str(e)}")
|
||||||
|
|
||||||
|
@app.post("/api/movies/{old_imdb_id}/migrate-imdb")
|
||||||
|
async def api_migrate_movie_imdb(old_imdb_id: str, request: Request):
|
||||||
|
"""Migrate a movie from placeholder IMDb ID to real IMDb ID"""
|
||||||
|
db = dependencies["db"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
new_imdb_id = data.get('new_imdb_id')
|
||||||
|
|
||||||
|
if not new_imdb_id:
|
||||||
|
raise HTTPException(status_code=422, detail="new_imdb_id is required")
|
||||||
|
|
||||||
|
# Normalize IMDb ID
|
||||||
|
if not new_imdb_id.startswith('tt'):
|
||||||
|
new_imdb_id = f"tt{new_imdb_id}"
|
||||||
|
|
||||||
|
success = db.migrate_movie_imdb_id(old_imdb_id, new_imdb_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Migrated movie from {old_imdb_id} to {new_imdb_id}",
|
||||||
|
"old_imdb_id": old_imdb_id,
|
||||||
|
"new_imdb_id": new_imdb_id
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Movie not found")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error migrating movie IMDb ID: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to migrate IMDb ID: {str(e)}")
|
||||||
|
|
||||||
# TV series endpoints
|
# TV series endpoints
|
||||||
@app.get("/api/series")
|
@app.get("/api/series")
|
||||||
async def api_series_list(skip: int = 0, limit: int = 50, search: str = None,
|
async def api_series_list(skip: int = 0, limit: int = 50, search: str = None,
|
||||||
@@ -1543,6 +1578,41 @@ def register_web_routes(app, dependencies):
|
|||||||
async def api_debug_series_date_distribution():
|
async def api_debug_series_date_distribution():
|
||||||
return await debug_series_date_distribution(dependencies)
|
return await debug_series_date_distribution(dependencies)
|
||||||
|
|
||||||
|
@app.post("/api/series/{old_imdb_id}/migrate-imdb")
|
||||||
|
async def api_migrate_series_imdb(old_imdb_id: str, request: Request):
|
||||||
|
"""Migrate a series and all its episodes from placeholder IMDb ID to real IMDb ID"""
|
||||||
|
db = dependencies["db"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
new_imdb_id = data.get('new_imdb_id')
|
||||||
|
|
||||||
|
if not new_imdb_id:
|
||||||
|
raise HTTPException(status_code=422, detail="new_imdb_id is required")
|
||||||
|
|
||||||
|
# Normalize IMDb ID
|
||||||
|
if not new_imdb_id.startswith('tt'):
|
||||||
|
new_imdb_id = f"tt{new_imdb_id}"
|
||||||
|
|
||||||
|
success = db.migrate_series_imdb_id(old_imdb_id, new_imdb_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Migrated series and episodes from {old_imdb_id} to {new_imdb_id}",
|
||||||
|
"old_imdb_id": old_imdb_id,
|
||||||
|
"new_imdb_id": new_imdb_id
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Series not found")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error migrating series IMDb ID: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to migrate IMDb ID: {str(e)}")
|
||||||
|
|
||||||
# Episode endpoints
|
# Episode endpoints
|
||||||
@app.post("/api/episodes/{imdb_id}/{season}/{episode}/update-date")
|
@app.post("/api/episodes/{imdb_id}/{season}/{episode}/update-date")
|
||||||
async def api_update_episode_date(imdb_id: str, season: int, episode: int,
|
async def api_update_episode_date(imdb_id: str, season: int, episode: int,
|
||||||
|
|||||||
@@ -437,6 +437,81 @@ class NFOGuardDatabase:
|
|||||||
""", (imdb_id, media_type, event_type, datetime.utcnow().isoformat(),
|
""", (imdb_id, media_type, event_type, datetime.utcnow().isoformat(),
|
||||||
json.dumps(details) if details else None))
|
json.dumps(details) if details else None))
|
||||||
|
|
||||||
|
def migrate_movie_imdb_id(self, old_imdb_id: str, new_imdb_id: str) -> bool:
|
||||||
|
"""Migrate a movie from placeholder IMDb ID to real IMDb ID"""
|
||||||
|
with self.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if old record exists
|
||||||
|
cursor.execute("SELECT * FROM movies WHERE imdb_id = %s", (old_imdb_id,))
|
||||||
|
old_record = cursor.fetchone()
|
||||||
|
if not old_record:
|
||||||
|
return False
|
||||||
|
|
||||||
|
old_data = dict(old_record)
|
||||||
|
|
||||||
|
# Check if new IMDb ID already exists
|
||||||
|
cursor.execute("SELECT * FROM movies WHERE imdb_id = %s", (new_imdb_id,))
|
||||||
|
if cursor.fetchone():
|
||||||
|
# New IMDb ID already exists, just delete the old one
|
||||||
|
cursor.execute("DELETE FROM movies WHERE imdb_id = %s", (old_imdb_id,))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Create new record with correct IMDb ID
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO movies (imdb_id, title, year, path, released, dateadded, source,
|
||||||
|
has_video_file, last_updated, skipped, skip_reason)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""", (new_imdb_id, old_data.get('title'), old_data.get('year'),
|
||||||
|
old_data.get('path'), old_data.get('released'), old_data.get('dateadded'),
|
||||||
|
old_data.get('source'), old_data.get('has_video_file'),
|
||||||
|
datetime.utcnow(), False, None)) # Clear skipped status
|
||||||
|
|
||||||
|
# Delete old placeholder record
|
||||||
|
cursor.execute("DELETE FROM movies WHERE imdb_id = %s", (old_imdb_id,))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def migrate_series_imdb_id(self, old_imdb_id: str, new_imdb_id: str) -> bool:
|
||||||
|
"""Migrate a series and all its episodes from placeholder IMDb ID to real IMDb ID"""
|
||||||
|
with self.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if old series exists
|
||||||
|
cursor.execute("SELECT * FROM series WHERE imdb_id = %s", (old_imdb_id,))
|
||||||
|
old_series = cursor.fetchone()
|
||||||
|
if not old_series:
|
||||||
|
return False
|
||||||
|
|
||||||
|
old_series_data = dict(old_series)
|
||||||
|
|
||||||
|
# Check if new series IMDb ID already exists
|
||||||
|
cursor.execute("SELECT * FROM series WHERE imdb_id = %s", (new_imdb_id,))
|
||||||
|
if cursor.fetchone():
|
||||||
|
# New series already exists, migrate episodes and delete old series
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE episodes SET imdb_id = %s WHERE imdb_id = %s
|
||||||
|
""", (new_imdb_id, old_imdb_id))
|
||||||
|
cursor.execute("DELETE FROM series WHERE imdb_id = %s", (old_imdb_id,))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Create new series record
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO series (imdb_id, path, last_updated, metadata)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
""", (new_imdb_id, old_series_data.get('path'),
|
||||||
|
datetime.utcnow(), old_series_data.get('metadata')))
|
||||||
|
|
||||||
|
# Migrate all episodes to new series IMDb ID and clear skipped status
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE episodes
|
||||||
|
SET imdb_id = %s, skipped = FALSE, skip_reason = NULL
|
||||||
|
WHERE imdb_id = %s
|
||||||
|
""", (new_imdb_id, old_imdb_id))
|
||||||
|
|
||||||
|
# Delete old placeholder series
|
||||||
|
cursor.execute("DELETE FROM series WHERE imdb_id = %s", (old_imdb_id,))
|
||||||
|
return True
|
||||||
|
|
||||||
def get_stats(self) -> Dict[str, Any]:
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
"""Get database statistics"""
|
"""Get database statistics"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>NFOGuard - Database Management</title>
|
<title>NFOGuard - Database Management</title>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=2.10.0-skipped-tracking-v5">
|
<link rel="stylesheet" href="/static/css/styles.css?v=2.10.0-skipped-imdb-edit">
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1><i class="fas fa-shield-alt"></i> NFOGuard <span style="font-size: 0.5em; color: #888;">v2.10.0-skipped-tracking-v5</span></h1>
|
<h1><i class="fas fa-shield-alt"></i> NFOGuard <span style="font-size: 0.5em; color: #888;">v2.10.0-skipped-imdb-edit</span></h1>
|
||||||
<p>Database Management & Reporting</p>
|
<p>Database Management & Reporting</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="auth-status" id="auth-status" style="display: none;">
|
<div class="auth-status" id="auth-status" style="display: none;">
|
||||||
@@ -727,6 +727,6 @@
|
|||||||
<!-- Toast Notifications -->
|
<!-- Toast Notifications -->
|
||||||
<div class="toast-container" id="toast-container"></div>
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
<script src="/static/js/app.js?v=2.10.0-skipped-tracking-v5"></script>
|
<script src="/static/js/app.js?v=2.10.0-skipped-imdb-edit"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -340,10 +340,22 @@ function updateMoviesTable(data) {
|
|||||||
dateTypeBadge = 'badge-warning';
|
dateTypeBadge = 'badge-warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if skipped and if IMDb ID is placeholder
|
||||||
|
const isSkipped = movie.skipped === true;
|
||||||
|
const isPlaceholder = movie.imdb_id && movie.imdb_id.startsWith('missing-');
|
||||||
|
const skipReasonBadge = isSkipped ? `<br><span class="badge badge-warning" title="${escapeHtml(movie.skip_reason || 'Skipped')}"">Skipped</span>` : '';
|
||||||
|
|
||||||
|
// Add Update IMDb ID button for placeholder IDs
|
||||||
|
const updateImdbButton = isPlaceholder ? `
|
||||||
|
<button class="btn btn-sm btn-info" onclick="updateMovieImdbId('${movie.imdb_id}')" title="Update IMDb ID" style="margin-left: 5px;">
|
||||||
|
<i class="fas fa-id-card"></i> Update IMDb
|
||||||
|
</button>
|
||||||
|
` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHtml(movie.title)}</td>
|
<td>${escapeHtml(movie.title)}</td>
|
||||||
<td><code>${movie.imdb_id}</code></td>
|
<td><code>${movie.imdb_id}</code>${skipReasonBadge}</td>
|
||||||
<td>${movie.released || '-'}</td>
|
<td>${movie.released || '-'}</td>
|
||||||
<td>${dateadded || '-'}</td>
|
<td>${dateadded || '-'}</td>
|
||||||
<td><span class="badge badge-secondary">${movie.source_description || movie.source || 'Unknown'}</span></td>
|
<td><span class="badge badge-secondary">${movie.source_description || movie.source || 'Unknown'}</span></td>
|
||||||
@@ -356,6 +368,7 @@ function updateMoviesTable(data) {
|
|||||||
<button class="btn btn-sm btn-secondary" onclick="debugMovie('${movie.imdb_id}')" title="Debug Data">
|
<button class="btn btn-sm btn-secondary" onclick="debugMovie('${movie.imdb_id}')" title="Debug Data">
|
||||||
<i class="fas fa-bug"></i>
|
<i class="fas fa-bug"></i>
|
||||||
</button>
|
</button>
|
||||||
|
${updateImdbButton}
|
||||||
<button class="btn btn-sm btn-danger" onclick="deleteMovie('${movie.imdb_id}')" style="margin-left: 5px;" title="Delete Movie">
|
<button class="btn btn-sm btn-danger" onclick="deleteMovie('${movie.imdb_id}')" style="margin-left: 5px;" title="Delete Movie">
|
||||||
<i class="fas fa-trash"></i> Delete
|
<i class="fas fa-trash"></i> Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -475,6 +488,16 @@ function updateSeriesTable(data) {
|
|||||||
const progressPercent = series.total_episodes > 0 ?
|
const progressPercent = series.total_episodes > 0 ?
|
||||||
((series.episodes_with_dates / series.total_episodes) * 100).toFixed(1) : 0;
|
((series.episodes_with_dates / series.total_episodes) * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
// Check if IMDb ID is placeholder
|
||||||
|
const isPlaceholder = series.imdb_id && series.imdb_id.startsWith('missing-');
|
||||||
|
|
||||||
|
// Add Update IMDb ID button for placeholder IDs
|
||||||
|
const updateImdbButton = isPlaceholder ? `
|
||||||
|
<button class="btn btn-sm btn-info" onclick="updateSeriesImdbId('${series.imdb_id}')" title="Update IMDb ID" style="margin-left: 5px;">
|
||||||
|
<i class="fas fa-id-card"></i> Update IMDb
|
||||||
|
</button>
|
||||||
|
` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHtml(series.title)}</td>
|
<td>${escapeHtml(series.title)}</td>
|
||||||
@@ -490,6 +513,7 @@ function updateSeriesTable(data) {
|
|||||||
<button class="btn btn-sm btn-primary" onclick="viewSeriesEpisodes('${series.imdb_id}')">
|
<button class="btn btn-sm btn-primary" onclick="viewSeriesEpisodes('${series.imdb_id}')">
|
||||||
<i class="fas fa-list"></i> Episodes
|
<i class="fas fa-list"></i> Episodes
|
||||||
</button>
|
</button>
|
||||||
|
${updateImdbButton}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
@@ -1505,6 +1529,92 @@ async function deleteMovie(imdbId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateMovieImdbId(oldImdbId) {
|
||||||
|
// Prompt for new IMDb ID
|
||||||
|
const newImdbId = prompt(`Update IMDb ID\n\nCurrent: ${oldImdbId}\n\nEnter the correct IMDb ID (with or without 'tt' prefix):`);
|
||||||
|
|
||||||
|
if (!newImdbId || newImdbId.trim() === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanImdbId = newImdbId.trim();
|
||||||
|
|
||||||
|
// Confirmation
|
||||||
|
if (!confirm(`Update IMDb ID?\n\nOld: ${oldImdbId}\nNew: ${cleanImdbId}\n\nThis will migrate the movie record to the new IMDb ID. Continue?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/movies/${oldImdbId}/migrate-imdb`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ new_imdb_id: cleanImdbId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
showToast(`✅ IMDb ID updated successfully to ${result.new_imdb_id}`, 'success');
|
||||||
|
|
||||||
|
// Refresh the movies table
|
||||||
|
loadMovies(currentMoviesPage);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const errorMsg = result.message || result.detail || 'Unknown error';
|
||||||
|
showToast(`❌ Failed to update IMDb ID: ${errorMsg}`, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update IMDb ID failed:', error);
|
||||||
|
showToast(`❌ Update failed: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSeriesImdbId(oldImdbId) {
|
||||||
|
// Prompt for new IMDb ID
|
||||||
|
const newImdbId = prompt(`Update Series IMDb ID\n\nCurrent: ${oldImdbId}\n\nEnter the correct IMDb ID (with or without 'tt' prefix):`);
|
||||||
|
|
||||||
|
if (!newImdbId || newImdbId.trim() === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanImdbId = newImdbId.trim();
|
||||||
|
|
||||||
|
// Confirmation
|
||||||
|
if (!confirm(`Update Series IMDb ID?\n\nOld: ${oldImdbId}\nNew: ${cleanImdbId}\n\nThis will migrate the series and ALL its episodes to the new IMDb ID. Continue?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/series/${oldImdbId}/migrate-imdb`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ new_imdb_id: cleanImdbId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
showToast(`✅ Series IMDb ID updated successfully to ${result.new_imdb_id}`, 'success');
|
||||||
|
|
||||||
|
// Refresh the series table
|
||||||
|
loadSeries(currentSeriesPage);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const errorMsg = result.message || result.detail || 'Unknown error';
|
||||||
|
showToast(`❌ Failed to update IMDb ID: ${errorMsg}`, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update series IMDb ID failed:', error);
|
||||||
|
showToast(`❌ Update failed: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update episode counts in modal after deletion
|
// Update episode counts in modal after deletion
|
||||||
function updateEpisodeModalCounts() {
|
function updateEpisodeModalCounts() {
|
||||||
const remainingRows = document.querySelectorAll('#episodes-table-body tr');
|
const remainingRows = document.querySelectorAll('#episodes-table-body tr');
|
||||||
|
|||||||
Reference in New Issue
Block a user