dev #62

Merged
sbcrumb merged 56 commits from dev into main 2025-10-26 15:45:15 -04:00
3 changed files with 404 additions and 315 deletions
Showing only changes of commit ff24e0c431 - Show all commits
+213 -298
View File
@@ -600,44 +600,11 @@ async def update_episode_date(dependencies: dict, imdb_id: str, season: int, epi
has_video_file=episode_data.get('has_video_file', False) has_video_file=episode_data.get('has_video_file', False)
) )
# Create/update NFO file with new data # NFO file management is handled by the core container
nfo_manager = dependencies["nfo_manager"] # Web interface only updates database entries
config = dependencies["config"]
if config.manage_nfo:
try:
# Find the series directory based on IMDb ID
series_path = None
for tv_path in config.tv_paths:
for series_dir in Path(tv_path).iterdir():
if series_dir.is_dir() and imdb_id.lower() in series_dir.name.lower():
series_path = series_dir
break
if series_path:
break
if series_path:
season_dir = series_path / config.tv_season_dir_format.format(season=season)
if season_dir.exists():
nfo_manager.create_episode_nfo(
season_dir=season_dir,
season_num=season,
episode_num=episode,
aired=episode_data.get('aired'),
dateadded=dateadded,
source=source,
lock_metadata=config.lock_metadata
)
print(f"✅ Updated NFO file for {imdb_id} S{season:02d}E{episode:02d}")
else:
print(f"⚠️ Season directory not found: {season_dir}")
else:
print(f"⚠️ Series directory not found for {imdb_id}")
except Exception as e:
print(f"❌ Error updating NFO file for {imdb_id} S{season:02d}E{episode:02d}: {e}")
# Add to processing history # Add to processing history
try:
db.add_processing_history( db.add_processing_history(
imdb_id=imdb_id, imdb_id=imdb_id,
media_type="episode", media_type="episode",
@@ -650,6 +617,9 @@ async def update_episode_date(dependencies: dict, imdb_id: str, season: int, epi
"dateadded": dateadded "dateadded": dateadded
} }
) )
except Exception as e:
print(f"⚠️ Failed to add processing history: {e}")
# Don't fail the entire update for history logging issues
return {"status": "success", "message": f"Updated episode {imdb_id} S{season:02d}E{episode:02d}"} return {"status": "success", "message": f"Updated episode {imdb_id} S{season:02d}E{episode:02d}"}
@@ -882,7 +852,7 @@ async def get_movie_date_options(dependencies: dict, imdb_id: str):
async def get_episode_date_options(dependencies: dict, imdb_id: str, season: int, episode: int): async def get_episode_date_options(dependencies: dict, imdb_id: str, season: int, episode: int):
"""Get available date options for an episode""" """Get available date options for an episode (simplified for web interface)"""
print(f"🔍 DEBUG: get_episode_date_options called with imdb_id={imdb_id}, season={season}, episode={episode}") print(f"🔍 DEBUG: get_episode_date_options called with imdb_id={imdb_id}, season={season}, episode={episode}")
db = dependencies["db"] db = dependencies["db"]
@@ -936,267 +906,7 @@ async def get_episode_date_options(dependencies: dict, imdb_id: str, season: int
"description": f"Use original air date: {episode_data['aired']}" "description": f"Use original air date: {episode_data['aired']}"
}) })
# Option 3: Active lookup from external sources # Option 3: Manual entry
try:
# Get TV processor and clients from dependencies
tv_processor = dependencies.get("tv_processor")
print(f"🔍 DEBUG: tv_processor available: {tv_processor is not None}")
if tv_processor:
print(f"🔍 DEBUG: tv_processor has external_clients: {hasattr(tv_processor, 'external_clients')}")
print(f"🔍 DEBUG: tv_processor has sonarr: {hasattr(tv_processor, 'sonarr')}")
if tv_processor and hasattr(tv_processor, 'external_clients'):
external_clients = tv_processor.external_clients
print(f"🔍 DEBUG: external_clients available: {external_clients is not None}")
if external_clients:
print(f"🔍 DEBUG: TMDB enabled: {external_clients.tmdb.enabled if hasattr(external_clients, 'tmdb') else 'No TMDB client'}")
# Check Sonarr for import dates
if tv_processor.sonarr and tv_processor.sonarr.enabled:
try:
print(f"🔍 DEBUG: Attempting Sonarr lookup for {imdb_id}")
# Look up the series and episode in Sonarr
series_data = tv_processor.sonarr.series_by_imdb(imdb_id)
# If IMDb lookup fails, try direct series lookup as fallback
if not series_data:
print(f"🔍 DEBUG: IMDb lookup failed, trying direct series lookup")
try:
# Let's also debug what series are available
all_series = tv_processor.sonarr.get_all_series()
print(f"🔍 DEBUG: Found {len(all_series)} total series in Sonarr")
# Look for Lincoln Lawyer specifically
lincoln_series = [s for s in all_series if 'lincoln' in s.get('title', '').lower()]
print(f"🔍 DEBUG: Lincoln Lawyer series found: {len(lincoln_series)}")
for ls in lincoln_series:
print(f" - Title: '{ls.get('title')}', IMDb: '{ls.get('imdbId')}', ID: {ls.get('id')}")
# Try direct lookup first
series_data = tv_processor.sonarr.series_by_imdb_direct(imdb_id)
# If still no match but we found Lincoln Lawyer series, try fuzzy matching
if not series_data and lincoln_series:
target_imdb_num = imdb_id.replace('tt', '').lower()
print(f"🔍 DEBUG: Trying fuzzy match for IMDb number: {target_imdb_num}")
for ls in lincoln_series:
ls_imdb = ls.get('imdbId', '')
ls_imdb_num = ls_imdb.replace('tt', '').lower()
print(f" - Comparing {target_imdb_num} vs {ls_imdb_num}")
# Check if IMDb numbers are close (within 10 digits)
if ls_imdb_num and target_imdb_num:
try:
target_num = int(target_imdb_num)
ls_num = int(ls_imdb_num)
diff = abs(target_num - ls_num)
print(f" - Numeric difference: {diff}")
if diff <= 10: # Allow small IMDb ID differences
print(f"✅ Found close IMDb match: {ls_imdb} vs {imdb_id} (diff: {diff})")
series_data = ls
break
except ValueError:
continue
except Exception as e:
print(f"⚠️ Direct series lookup also failed: {e}")
import traceback
print(f" Traceback: {traceback.format_exc()}")
print(f"🔍 DEBUG: Series data found: {series_data is not None}")
if series_data:
series_id = series_data.get('id')
series_title = series_data.get('title', 'Unknown')
print(f"🔍 DEBUG: Found series '{series_title}' with ID {series_id}")
if series_id:
# Get episodes for the series
print(f"🔍 DEBUG: Getting episodes for series {series_id}")
episodes = tv_processor.sonarr.episodes_for_series(series_id)
print(f"🔍 DEBUG: Found {len(episodes)} episodes")
for ep in episodes:
ep_season = ep.get('seasonNumber')
ep_episode = ep.get('episodeNumber')
# Convert to int for proper comparison (handle both string and int from Sonarr)
try:
ep_season = int(ep_season) if ep_season is not None else None
ep_episode = int(ep_episode) if ep_episode is not None else None
except (ValueError, TypeError):
continue # Skip episodes with invalid season/episode numbers
if ep_season == season and ep_episode == episode:
episode_id = ep.get('id')
ep_title = ep.get('title', 'Unknown')
ep_air_date = ep.get('airDate') # Get air date from Sonarr
print(f"🔍 DEBUG: Found target episode '{ep_title}' with ID {episode_id}, airDate: {ep_air_date}")
if episode_id:
# Get import history for this specific episode
print(f"🔍 DEBUG: Getting import history for episode {episode_id}")
import_date = tv_processor.sonarr.get_episode_import_history(episode_id)
print(f"🔍 DEBUG: Import date found: {import_date}")
if import_date:
# Check if this is different from current date
current_dateadded = episode_data.get('dateadded')
current_date_str = current_dateadded.strftime('%Y-%m-%d') if current_dateadded else ''
if not current_dateadded or not current_date_str.startswith(import_date[:10]):
options.append({
"type": "sonarr_import",
"label": "Sonarr Import Date",
"date": import_date,
"source": "sonarr:import_history",
"description": f"Import date from Sonarr: {import_date[:10]}"
})
print(f"✅ Added Sonarr import option: {import_date[:10]}")
# If no import date but we have air date from Sonarr, add as air date option
if not import_date and ep_air_date:
current_aired = episode_data.get('aired', '')
current_dateadded = episode_data.get('dateadded', '')
# Add air date option if different from current or missing
if not current_aired or current_aired != ep_air_date:
options.append({
"type": "sonarr_air",
"label": "Sonarr Air Date",
"date": f"{ep_air_date}T20:00:00",
"source": "sonarr:airdate",
"description": f"Air date from Sonarr: {ep_air_date}"
})
print(f"✅ Added Sonarr air date option: {ep_air_date}")
# If no dateadded, suggest using air date as import date fallback
if not current_dateadded:
options.append({
"type": "sonarr_air_fallback",
"label": "Use Air Date as Import Date",
"date": f"{ep_air_date}T20:00:00",
"source": "sonarr:aired_fallback",
"description": f"Use Sonarr air date as import date: {ep_air_date}"
})
print(f"✅ Added Sonarr air date fallback option: {ep_air_date}")
break
else:
print(f"❌ No series found in Sonarr for {imdb_id}")
except Exception as e:
print(f"⚠️ Failed to get Sonarr import date for {imdb_id} S{season:02d}E{episode:02d}: {e}")
import traceback
print(f" Traceback: {traceback.format_exc()}")
# Check TMDB for episode air dates
if external_clients.tmdb.enabled:
try:
print(f"🔍 DEBUG: Attempting TMDB lookup for {imdb_id}")
# Get TMDB TV series ID from IMDb ID using find endpoint
tv_find_result = external_clients.tmdb._get(f"/find/{imdb_id}", {"external_source": "imdb_id"})
print(f"🔍 DEBUG: TMDB find result: {tv_find_result is not None}")
print(f"🔍 DEBUG: TMDB raw response: {tv_find_result}")
# Check both tv_results and tv_episode_results
tmdb_id = None
tv_title = "Unknown"
if tv_find_result and tv_find_result.get("tv_results"):
tv_results = tv_find_result.get("tv_results", [])
print(f"🔍 DEBUG: Found {len(tv_results)} TV results")
if tv_results:
tv_show = tv_results[0]
tmdb_id = tv_show.get("id")
tv_title = tv_show.get("name", "Unknown")
print(f"🔍 DEBUG: Found TMDB series '{tv_title}' with ID {tmdb_id}")
# Fallback: Check tv_episode_results for show_id
elif tv_find_result and tv_find_result.get("tv_episode_results"):
episode_results = tv_find_result.get("tv_episode_results", [])
print(f"🔍 DEBUG: Found {len(episode_results)} TV episode results")
if episode_results:
tmdb_episode_data = episode_results[0]
tmdb_id = tmdb_episode_data.get("show_id")
episode_name = tmdb_episode_data.get("name", "Unknown")
print(f"🔍 DEBUG: Found TMDB series via episode '{episode_name}' with show_id {tmdb_id}")
if tmdb_id:
print(f"🔍 DEBUG: Using TMDB ID {tmdb_id} for series lookup")
# Get episode air date from TMDB
print(f"🔍 DEBUG: Getting TMDB season {season} episodes for series {tmdb_id}")
episodes = external_clients.tmdb.get_tv_season_episodes(tmdb_id, season)
print(f"🔍 DEBUG: TMDB episodes found: {episodes}")
if episode in episodes:
air_date = episodes[episode]
print(f"🔍 DEBUG: TMDB air date for S{season:02d}E{episode:02d}: {air_date}")
if air_date:
# Check if this is different from current aired date
current_aired = episode_data.get('aired', '')
if not current_aired or current_aired != air_date:
options.append({
"type": "tmdb_air",
"label": "TMDB Air Date",
"date": f"{air_date}T20:00:00", # Default to 8 PM
"source": "tmdb:airdate",
"description": f"Air date from TMDB: {air_date}"
})
print(f"✅ Added TMDB air date option: {air_date}")
# If no aired date in database, also add this as "Use Air Date" option
if not current_aired:
options.insert(1, { # Insert after current option
"type": "airdate_tmdb",
"label": "Use Air Date (TMDB)",
"date": f"{air_date}T20:00:00",
"source": "airdate",
"description": f"Use air date from TMDB: {air_date}"
})
print(f"✅ Added 'Use Air Date' option from TMDB: {air_date}")
else:
print(f"❌ Episode {episode} not found in TMDB season {season} data")
else:
print(f"❌ No TV series ID found in TMDB for {imdb_id}")
except Exception as e:
print(f"⚠️ Failed to get TMDB air date for {imdb_id} S{season:02d}E{episode:02d}: {e}")
import traceback
print(f" Traceback: {traceback.format_exc()}")
# Check external clients for episode air dates (TVDB, OMDb)
if hasattr(external_clients, 'get_episode_air_date'):
try:
air_date = external_clients.get_episode_air_date(imdb_id, season, episode)
if air_date:
# Check if this is different from current aired date
current_aired = episode_data.get('aired', '')
if not current_aired or current_aired != air_date:
options.append({
"type": "external_air",
"label": "External Air Date",
"date": f"{air_date}T20:00:00", # Default to 8 PM
"source": "external:airdate",
"description": f"Air date from external sources: {air_date}"
})
# If no aired date in database and not already added from TMDB, add this as "Use Air Date" option
if not current_aired and not any(opt.get('type') == 'airdate_tmdb' for opt in options):
options.insert(1, { # Insert after current option
"type": "airdate_external",
"label": "Use Air Date (External)",
"date": f"{air_date}T20:00:00",
"source": "airdate",
"description": f"Use air date from external sources: {air_date}"
})
except Exception as e:
print(f"⚠️ Failed to get external air date for {imdb_id} S{season:02d}E{episode:02d}: {e}")
except Exception as e:
print(f"⚠️ External source lookup failed for {imdb_id} S{season:02d}E{episode:02d}: {e}")
# Option 4: Manual entry
options.append({ options.append({
"type": "manual", "type": "manual",
"label": "Manual Entry", "label": "Manual Entry",
@@ -1217,3 +927,208 @@ async def get_episode_date_options(dependencies: dict, imdb_id: str, season: int
"current_data": episode_data, "current_data": episode_data,
"options": options "options": options
} }
async def delete_episode(dependencies: dict, imdb_id: str, season: int, episode: int):
"""Delete an episode from the database"""
db = dependencies["db"]
# Check if episode exists
episode_data = db.get_episode_date(imdb_id, season, episode)
if not episode_data:
raise HTTPException(status_code=404, detail="Episode not found")
# Delete from database
try:
with db.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)
)
conn.commit()
# Add to processing history
try:
db.add_processing_history(
imdb_id=imdb_id,
media_type="episode",
event_type="manual_deletion",
details={
"season": season,
"episode": episode,
"deleted_source": episode_data.get('source'),
"deleted_dateadded": episode_data.get('dateadded')
}
)
except Exception as e:
print(f"⚠️ Failed to add processing history: {e}")
return {"success": True, "status": "success", "message": f"Deleted episode {imdb_id} S{season:02d}E{episode:02d}"}
except Exception as e:
print(f"❌ Error deleting episode: {e}")
raise HTTPException(status_code=500, detail=f"Failed to delete episode: {str(e)}")
async def delete_movie(dependencies: dict, imdb_id: str):
"""Delete a movie from the database"""
db = dependencies["db"]
# Check if movie exists
movie_data = db.get_movie_dates(imdb_id)
if not movie_data:
raise HTTPException(status_code=404, detail="Movie not found")
# Delete from database
try:
with db.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM movies WHERE imdb_id = %s", (imdb_id,))
conn.commit()
# Add to processing history
try:
db.add_processing_history(
imdb_id=imdb_id,
media_type="movie",
event_type="manual_deletion",
details={
"deleted_source": movie_data.get('source'),
"deleted_dateadded": movie_data.get('dateadded'),
"deleted_path": movie_data.get('path')
}
)
except Exception as e:
print(f"⚠️ Failed to add processing history: {e}")
return {"success": True, "status": "success", "message": f"Deleted movie {imdb_id}"}
except Exception as e:
print(f"❌ Error deleting movie: {e}")
raise HTTPException(status_code=500, detail=f"Failed to delete movie: {str(e)}")
def register_web_routes(app, dependencies):
"""Register all web API routes with FastAPI app"""
from fastapi import Request, Response
# Dashboard and stats endpoints
@app.get("/api/dashboard")
async def api_dashboard():
return await get_dashboard_stats(dependencies)
@app.get("/api/dashboard/stats")
async def api_dashboard_stats():
return await get_dashboard_stats(dependencies)
# Movies endpoints
@app.get("/api/movies")
async def api_movies_list(skip: int = 0, limit: int = 100, has_date: bool = None,
source_filter: str = None, search: str = None, imdb_search: str = None):
return await get_movies_list(dependencies, skip, limit, has_date, source_filter, search, imdb_search)
@app.post("/api/movies/{imdb_id}/update-date")
async def api_update_movie_date(imdb_id: str, dateadded: str = None, source: str = "manual"):
return await update_movie_date(dependencies, imdb_id, dateadded, source)
@app.put("/api/movies/{imdb_id}")
async def api_update_movie(imdb_id: str, dateadded: str = None, source: str = "manual"):
return await update_movie_date(dependencies, imdb_id, dateadded, source)
@app.get("/api/movies/{imdb_id}/date-options")
async def api_movie_date_options(imdb_id: str):
return await get_movie_date_options(dependencies, imdb_id)
# TV series endpoints
@app.get("/api/series")
async def api_series_list(skip: int = 0, limit: int = 50, search: str = None,
imdb_search: str = None, date_filter: str = None, source_filter: str = None):
return await get_tv_series_list(dependencies, skip, limit, search, imdb_search, date_filter, source_filter)
@app.get("/api/series/{imdb_id}/episodes")
async def api_series_episodes(imdb_id: str):
return await get_series_episodes(dependencies, imdb_id)
@app.get("/api/series/sources")
async def api_series_sources():
return await get_series_sources(dependencies)
@app.get("/api/series/debug/date-distribution")
async def api_debug_series_date_distribution():
return await debug_series_date_distribution(dependencies)
# Episode endpoints - WORKING VERSIONS
@app.post("/api/episodes/{imdb_id}/{season}/{episode}/update-date")
async def api_update_episode_date(imdb_id: str, season: int, episode: int,
dateadded: str = None, source: str = "manual"):
return await update_episode_date(dependencies, imdb_id, season, episode, dateadded, source)
@app.put("/api/episodes/{imdb_id}/{season}/{episode}")
async def api_update_episode(imdb_id: str, season: int, episode: int,
dateadded: str = None, source: str = "manual"):
return await update_episode_date(dependencies, imdb_id, season, episode, dateadded, source)
@app.get("/api/episodes/{imdb_id}/{season}/{episode}/date-options")
async def api_episode_date_options(imdb_id: str, season: int, episode: int):
return await get_episode_date_options(dependencies, imdb_id, season, episode)
@app.delete("/api/episodes/{imdb_id}/{season}/{episode}")
async def api_delete_episode(imdb_id: str, season: int, episode: int):
return await delete_episode(dependencies, imdb_id, season, episode)
# Movie deletion endpoint
@app.delete("/api/movies/{imdb_id}")
async def api_delete_movie(imdb_id: str):
return await delete_movie(dependencies, imdb_id)
# Bulk operations
@app.post("/api/bulk/update-source")
async def api_bulk_update_source(media_type: str, old_source: str, new_source: str):
return await bulk_update_source(dependencies, media_type, old_source, new_source)
# Reports
@app.get("/api/reports/missing-dates")
async def api_missing_dates_report():
return await get_missing_dates_report(dependencies)
# Authentication endpoints (for web interface compatibility)
@app.get("/api/auth/status")
async def api_auth_status(request: Request):
"""Check authentication status"""
auth_enabled = dependencies.get("auth_enabled", False)
if not auth_enabled:
return {"authenticated": True, "auth_enabled": False, "message": "Authentication disabled"}
session_manager = dependencies.get("session_manager")
if not session_manager:
return {"authenticated": False, "auth_enabled": True, "message": "Session manager not available"}
session_token = request.cookies.get("nfoguard_session")
if session_token:
username = session_manager.get_session_user(session_token)
if username:
return {"authenticated": True, "auth_enabled": True, "username": username}
return {"authenticated": False, "auth_enabled": True, "message": "Not authenticated"}
@app.post("/api/auth/logout")
async def api_auth_logout(request: Request, response: Response):
"""Logout endpoint - clears session"""
session_manager = dependencies.get("session_manager")
if session_manager:
session_token = request.cookies.get("nfoguard_session")
if session_token:
session_manager.delete_session(session_token)
response.delete_cookie("nfoguard_session")
return {"status": "logged_out", "message": "Session cleared"}
# Health endpoint
@app.get("/health")
async def health_check():
"""Health check endpoint for container monitoring"""
return {"status": "healthy", "service": "nfoguard-web"}
print("✅ Web routes registered successfully")
+147
View File
@@ -225,6 +225,153 @@ class WebDatabase:
""" """
return self.execute_query(query) return self.execute_query(query)
# Episode-specific methods for web interface
def get_episode_date(self, imdb_id: str, season: int, episode: int) -> Optional[Dict]:
"""Get episode data including dates"""
query = """
SELECT imdb_id, season, episode, aired, dateadded, source, has_video_file, last_updated
FROM episodes
WHERE imdb_id = %s AND season = %s AND episode = %s
"""
return self.execute_single(query, (imdb_id, season, episode))
def upsert_episode_date(self, imdb_id: str, season: int, episode: int,
aired: Optional[str], dateadded: Optional[str],
source: str, has_video_file: bool = False) -> None:
"""Update or insert episode date information"""
# First check if episode exists
existing = self.get_episode_date(imdb_id, season, episode)
# Temporarily disable autocommit for the transaction
original_autocommit = self.connection.autocommit
self.connection.autocommit = False
try:
with self.connection.cursor() as cursor:
if existing:
# Update existing episode
query = """
UPDATE episodes
SET aired = %s, dateadded = %s, source = %s, has_video_file = %s, last_updated = NOW()
WHERE imdb_id = %s AND season = %s AND episode = %s
"""
cursor.execute(query, (aired, dateadded, source, has_video_file, imdb_id, season, episode))
else:
# Insert new episode
query = """
INSERT INTO episodes (imdb_id, season, episode, aired, dateadded, source, has_video_file, last_updated)
VALUES (%s, %s, %s, %s, %s, %s, %s, NOW())
"""
cursor.execute(query, (imdb_id, season, episode, aired, dateadded, source, has_video_file))
self.connection.commit()
except Exception as e:
self.connection.rollback()
logger.error(f"Failed to upsert episode date: {e}")
raise
finally:
# Restore original autocommit setting
self.connection.autocommit = original_autocommit
def get_movie_dates(self, imdb_id: str) -> Optional[Dict]:
"""Get movie data including dates"""
query = """
SELECT imdb_id, path, released, dateadded, source, has_video_file, last_updated
FROM movies
WHERE imdb_id = %s
"""
return self.execute_single(query, (imdb_id,))
def upsert_movie_dates(self, imdb_id: str, released: Optional[str],
dateadded: Optional[str], source: str,
has_video_file: bool = False, path: str = "") -> None:
"""Update or insert movie date information"""
# First check if movie exists
existing = self.get_movie_dates(imdb_id)
# Temporarily disable autocommit for the transaction
original_autocommit = self.connection.autocommit
self.connection.autocommit = False
try:
with self.connection.cursor() as cursor:
if existing:
# Update existing movie
query = """
UPDATE movies
SET released = %s, dateadded = %s, source = %s, has_video_file = %s, last_updated = NOW()
WHERE imdb_id = %s
"""
cursor.execute(query, (released, dateadded, source, has_video_file, imdb_id))
else:
# Insert new movie
query = """
INSERT INTO movies (imdb_id, path, released, dateadded, source, has_video_file, last_updated)
VALUES (%s, %s, %s, %s, %s, %s, NOW())
"""
cursor.execute(query, (imdb_id, path, released, dateadded, source, has_video_file))
self.connection.commit()
except Exception as e:
self.connection.rollback()
logger.error(f"Failed to upsert movie dates: {e}")
raise
finally:
# Restore original autocommit setting
self.connection.autocommit = original_autocommit
def get_connection(self):
"""Get database connection for advanced operations"""
return self.connection
def _get_first_value(self, row):
"""Extract first value from a database row (compatibility method)"""
if row is None:
return None
if isinstance(row, dict):
return list(row.values())[0] if row else None
return row[0] if row else None
def get_stats(self) -> Dict[str, Any]:
"""Get basic database statistics (compatibility method)"""
return self.get_dashboard_stats()
def add_processing_history(self, imdb_id: str, media_type: str, event_type: str, details: Dict) -> None:
"""Add processing history entry (simplified for web interface)"""
# Temporarily disable autocommit for the transaction
original_autocommit = self.connection.autocommit
self.connection.autocommit = False
try:
with self.connection.cursor() as cursor:
# Check if processing_history table exists
cursor.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'processing_history'
)
""")
table_exists = cursor.fetchone()[0]
if table_exists:
query = """
INSERT INTO processing_history (imdb_id, media_type, event_type, details, processed_at)
VALUES (%s, %s, %s, %s, NOW())
"""
import json
cursor.execute(query, (imdb_id, media_type, event_type, json.dumps(details)))
self.connection.commit()
else:
# Table doesn't exist, skip logging
logger.debug("Processing history table not found, skipping log entry")
except Exception as e:
self.connection.rollback()
logger.error(f"Failed to add processing history: {e}")
# Don't raise, this is non-critical
finally:
# Restore original autocommit setting
self.connection.autocommit = original_autocommit
def close(self): def close(self):
"""Close database connection""" """Close database connection"""
if self.connection: if self.connection:
+31 -4
View File
@@ -676,6 +676,13 @@ async function smartFixMovie(imdbId) {
} }
async function smartFixEpisode(imdbId, season, episode) { async function smartFixEpisode(imdbId, season, episode) {
// Validate parameters
if (!imdbId || season === undefined || season === null || episode === undefined || episode === null) {
console.error('smartFixEpisode: Invalid parameters:', {imdbId, season, episode});
showToast('Invalid episode parameters', 'error');
return;
}
try { try {
const options = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}/date-options`); const options = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}/date-options`);
showSmartFixModal('episode', options); showSmartFixModal('episode', options);
@@ -703,7 +710,10 @@ function showSmartFixModal(mediaType, options) {
if (mediaType === 'movie') { if (mediaType === 'movie') {
title.textContent = `Fix Date for Movie: ${options.imdb_id}`; title.textContent = `Fix Date for Movie: ${options.imdb_id}`;
} else { } else {
title.textContent = `Fix Date for Episode: ${options.imdb_id} S${options.season.toString().padStart(2, '0')}E${options.episode.toString().padStart(2, '0')}`; // Add validation for episode data
const season = options.season || 0;
const episode = options.episode || 0;
title.textContent = `Fix Date for Episode: ${options.imdb_id} S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`;
} }
// Build options HTML // Build options HTML
@@ -878,7 +888,10 @@ function showEnhancedEditModal(mediaType, options, currentDateadded, currentSour
if (mediaType === 'movie') { if (mediaType === 'movie') {
title.textContent = `Edit Movie: ${options.imdb_id}`; title.textContent = `Edit Movie: ${options.imdb_id}`;
} else { } else {
title.textContent = `Edit Episode: ${options.imdb_id} S${options.season.toString().padStart(2, '0')}E${options.episode.toString().padStart(2, '0')}`; // Add validation for episode data
const season = options.season || 0;
const episode = options.episode || 0;
title.textContent = `Edit Episode: ${options.imdb_id} S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`;
} }
// Build enhanced edit form with date options // Build enhanced edit form with date options
@@ -1062,6 +1075,13 @@ async function handleEnhancedEditSubmit(event) {
} }
async function editEpisode(imdbId, season, episode, dateadded, source) { async function editEpisode(imdbId, season, episode, dateadded, source) {
// Validate parameters
if (!imdbId || season === undefined || season === null || episode === undefined || episode === null) {
console.error('editEpisode: Invalid parameters:', {imdbId, season, episode});
showToast('Invalid episode parameters', 'error');
return;
}
try { try {
// Load episode options to populate available dates // Load episode options to populate available dates
const options = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}/date-options`); const options = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}/date-options`);
@@ -1244,6 +1264,13 @@ Analysis:
// Episode deletion functionality // Episode deletion functionality
async function deleteEpisode(imdbId, season, episode) { async function deleteEpisode(imdbId, season, episode) {
// Validate parameters
if (!imdbId || season === undefined || season === null || episode === undefined || episode === null) {
console.error('deleteEpisode: Invalid parameters:', {imdbId, season, episode});
showToast('Invalid episode parameters', 'error');
return;
}
const episodeStr = `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`; const episodeStr = `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`;
// Confirmation dialog // Confirmation dialog
@@ -1252,7 +1279,7 @@ async function deleteEpisode(imdbId, season, episode) {
} }
try { try {
const response = await fetch(`/database/episode/${imdbId}/${season}/${episode}`, { const response = await fetch(`/api/episodes/${imdbId}/${season}/${episode}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -1295,7 +1322,7 @@ async function deleteMovie(imdbId) {
} }
try { try {
const response = await fetch(`/database/movie/${imdbId}`, { const response = await fetch(`/api/movies/${imdbId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'