fixes: debugs for 422 and delete option
Local Docker Build (Dev) / build-dev (push) Successful in 4s

This commit is contained in:
2025-10-20 08:42:53 -04:00
parent 3c4e47b08e
commit 741a283df0
5 changed files with 298 additions and 1 deletions
+1 -1
View File
@@ -1 +1 @@
2.6.2 2.6.4
+94
View File
@@ -1072,6 +1072,88 @@ async def debug_tmdb_lookup(imdb_id: str, dependencies: dict):
return {"error": str(e), "imdb_id": imdb_id, "traceback": str(e)} return {"error": str(e), "imdb_id": imdb_id, "traceback": str(e)}
# ---------------------------
# Database Cleanup Endpoints
# ---------------------------
async def delete_episode(imdb_id: str, season: int, episode: int, dependencies: dict):
"""Delete a specific episode from the database"""
db = dependencies["db"]
try:
deleted = db.delete_episode(imdb_id, season, episode)
if deleted:
return {
"success": True,
"message": f"Deleted episode S{season:02d}E{episode:02d} from series {imdb_id}",
"imdb_id": imdb_id,
"season": season,
"episode": episode
}
else:
return {
"success": False,
"message": f"Episode S{season:02d}E{episode:02d} not found in series {imdb_id}",
"imdb_id": imdb_id,
"season": season,
"episode": episode
}
except Exception as e:
return {
"success": False,
"error": str(e),
"imdb_id": imdb_id,
"season": season,
"episode": episode
}
async def delete_series_episodes(imdb_id: str, dependencies: dict):
"""Delete all episodes for a series from the database"""
db = dependencies["db"]
try:
deleted_count = db.delete_series_episodes(imdb_id)
return {
"success": True,
"message": f"Deleted {deleted_count} episodes from series {imdb_id}",
"imdb_id": imdb_id,
"deleted_count": deleted_count
}
except Exception as e:
return {
"success": False,
"error": str(e),
"imdb_id": imdb_id
}
async def cleanup_orphaned_episodes(dependencies: dict):
"""Find and delete episodes that don't have corresponding video files"""
db = dependencies["db"]
try:
deleted_episodes = db.delete_orphaned_episodes()
return {
"success": True,
"message": f"Cleaned up {len(deleted_episodes)} orphaned episodes",
"deleted_count": len(deleted_episodes),
"deleted_episodes": deleted_episodes
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": "Failed to cleanup orphaned episodes"
}
# --------------------------- # ---------------------------
# Route Registration # Route Registration
# --------------------------- # ---------------------------
@@ -1122,6 +1204,18 @@ def register_routes(app, dependencies: dict):
async def _debug_movie_history(imdb_id: str): async def _debug_movie_history(imdb_id: str):
return await debug_movie_history(imdb_id, dependencies) return await debug_movie_history(imdb_id, dependencies)
@app.delete("/database/episode/{imdb_id}/{season}/{episode}")
async def _delete_episode(imdb_id: str, season: int, episode: int):
return await delete_episode(imdb_id, season, episode, dependencies)
@app.delete("/database/series/{imdb_id}/episodes")
async def _delete_series_episodes(imdb_id: str):
return await delete_series_episodes(imdb_id, dependencies)
@app.post("/database/cleanup/orphaned-episodes")
async def _cleanup_orphaned_episodes():
return await cleanup_orphaned_episodes(dependencies)
@app.post("/manual/scan") @app.post("/manual/scan")
async def _manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = None, scan_type: str = "both", scan_mode: str = "smart"): async def _manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = None, scan_type: str = "both", scan_mode: str = "smart"):
return await manual_scan(background_tasks, path, scan_type, scan_mode, dependencies) return await manual_scan(background_tasks, path, scan_type, scan_mode, dependencies)
+13
View File
@@ -149,6 +149,19 @@ async def get_tv_series_list(dependencies: dict,
"""Get paginated list of TV series with episode counts""" """Get paginated list of TV series with episode counts"""
db = dependencies["db"] db = dependencies["db"]
# Debug logging for 422 troubleshooting
try:
print(f"DEBUG: Series list params - skip:{skip}, limit:{limit}, search:'{search}', imdb_search:'{imdb_search}', date_filter:'{date_filter}', source_filter:'{source_filter}'")
# Validate date_filter values
if date_filter and date_filter not in ['complete', 'incomplete', 'none']:
print(f"ERROR: Invalid date_filter value: '{date_filter}'")
raise HTTPException(status_code=422, detail=f"Invalid date_filter: must be 'complete', 'incomplete', or 'none', got '{date_filter}'")
except Exception as e:
print(f"ERROR: Parameter validation failed: {e}")
raise
with db.get_connection() as conn: with db.get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
+117
View File
@@ -294,6 +294,123 @@ class NFOGuardDatabase:
"database_type": "postgresql" "database_type": "postgresql"
} }
def delete_episode(self, imdb_id: str, season: int, episode: int) -> bool:
"""
Delete a specific episode from the database
Args:
imdb_id: Series IMDb ID
season: Season number
episode: Episode number
Returns:
True if episode was deleted, False if not found
"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
DELETE FROM episodes
WHERE imdb_id = %s AND season = %s AND episode = %s
""", (imdb_id, season, episode))
deleted_count = cursor.rowcount
conn.commit()
return deleted_count > 0
def delete_series_episodes(self, imdb_id: str) -> int:
"""
Delete all episodes for a series from the database
Args:
imdb_id: Series IMDb ID
Returns:
Number of episodes deleted
"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
DELETE FROM episodes
WHERE imdb_id = %s
""", (imdb_id,))
deleted_count = cursor.rowcount
conn.commit()
return deleted_count
def delete_orphaned_episodes(self) -> List[Dict]:
"""
Find and delete episodes that don't have corresponding video files on disk
This requires checking filesystem for each episode, so use carefully
Returns:
List of deleted episodes with their details
"""
from utils.file_utils import find_episodes_on_disk
from pathlib import Path
deleted_episodes = []
with self.get_connection() as conn:
cursor = conn.cursor()
# Get all series with their paths
cursor.execute("""
SELECT DISTINCT imdb_id, path FROM series
""")
series_list = cursor.fetchall()
for series in series_list:
imdb_id = series['imdb_id']
series_path = Path(series['path'])
if not series_path.exists():
continue
# Get episodes on disk
disk_episodes = find_episodes_on_disk(series_path)
disk_episode_keys = set(disk_episodes.keys())
# Get episodes in database
cursor.execute("""
SELECT season, episode, dateadded, source
FROM episodes
WHERE imdb_id = %s
""", (imdb_id,))
db_episodes = cursor.fetchall()
# Find orphaned episodes (in DB but not on disk)
for db_episode in db_episodes:
season = db_episode['season']
episode = db_episode['episode']
episode_key = (season, episode)
if episode_key not in disk_episode_keys:
# Episode is orphaned - delete it
cursor.execute("""
DELETE FROM episodes
WHERE imdb_id = %s AND season = %s AND episode = %s
""", (imdb_id, season, episode))
deleted_episodes.append({
'imdb_id': imdb_id,
'season': season,
'episode': episode,
'dateadded': db_episode['dateadded'],
'source': db_episode['source'],
'series_path': str(series_path)
})
conn.commit()
return deleted_episodes
def close(self): def close(self):
"""Close all database connections""" """Close all database connections"""
if hasattr(self._local, 'connection'): if hasattr(self._local, 'connection'):
+73
View File
@@ -519,6 +519,9 @@ function showEpisodesModal(data) {
<button class="btn btn-sm btn-primary" onclick="editEpisode('${data.series.imdb_id}', ${episode.season}, ${episode.episode}, '${dateadded}', '${episode.source || ''}')"> <button class="btn btn-sm btn-primary" onclick="editEpisode('${data.series.imdb_id}', ${episode.season}, ${episode.episode}, '${dateadded}', '${episode.source || ''}')">
<i class="fas fa-edit"></i> Edit <i class="fas fa-edit"></i> Edit
</button> </button>
<button class="btn btn-sm btn-danger" onclick="deleteEpisode('${data.series.imdb_id}', ${episode.season}, ${episode.episode})" style="margin-left: 5px;">
<i class="fas fa-trash"></i> Delete
</button>
</td> </td>
</tr> </tr>
`; `;
@@ -1249,4 +1252,74 @@ Analysis:
console.error('Debug failed:', error); console.error('Debug failed:', error);
showToast('Debug failed: ' + error.message, 'error'); showToast('Debug failed: ' + error.message, 'error');
} }
}
// Episode deletion functionality
async function deleteEpisode(imdbId, season, episode) {
const episodeStr = `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`;
// Confirmation dialog
if (!confirm(`⚠️ Delete Episode ${episodeStr}?\n\nThis will permanently remove the episode from the database.\n\nAre you sure you want to continue?`)) {
return;
}
try {
const response = await fetch(`/database/episode/${imdbId}/${season}/${episode}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (response.ok && result.success) {
showToast(`✅ Episode ${episodeStr} deleted successfully`, 'success');
// Remove the row from the table
const rows = document.querySelectorAll('#episodes-table-body tr');
rows.forEach(row => {
const episodeCell = row.querySelector('td:first-child');
if (episodeCell && episodeCell.textContent === episodeStr) {
row.remove();
}
});
// Update episode counts in modal header
updateEpisodeModalCounts();
} else {
const errorMsg = result.message || result.error || 'Unknown error';
showToast(`❌ Failed to delete episode: ${errorMsg}`, 'error');
}
} catch (error) {
console.error('Delete episode failed:', error);
showToast(`❌ Delete failed: ${error.message}`, 'error');
}
}
// Update episode counts in modal after deletion
function updateEpisodeModalCounts() {
const remainingRows = document.querySelectorAll('#episodes-table-body tr');
const totalEpisodes = remainingRows.length;
const episodesWithDates = Array.from(remainingRows).filter(row =>
row.getAttribute('data-has-date') === 'true'
).length;
const episodesWithoutDates = totalEpisodes - episodesWithDates;
// Update the stats in the modal
const statsDiv = document.querySelector('.episode-stats');
if (statsDiv) {
// Keep the existing "With Video" count by finding it
const videoCountDiv = statsDiv.querySelector('div:nth-child(4)');
const videoCountText = videoCountDiv ? videoCountDiv.innerHTML : '<div><strong>With Video:</strong> -</div>';
statsDiv.innerHTML = `
<div><strong>Total Episodes:</strong> ${totalEpisodes}</div>
<div><strong>With Dates:</strong> ${episodesWithDates}</div>
<div style="color: #dc3545;"><strong>Missing Dates:</strong> ${episodesWithoutDates}</div>
${videoCountText}
`;
}
} }