diff --git a/nfoguard-web/api/web_routes.py b/nfoguard-web/api/web_routes.py index 6e197a5..3026213 100644 --- a/nfoguard-web/api/web_routes.py +++ b/nfoguard-web/api/web_routes.py @@ -600,56 +600,26 @@ async def update_episode_date(dependencies: dict, imdb_id: str, season: int, epi has_video_file=episode_data.get('has_video_file', False) ) - # Create/update NFO file with new data - nfo_manager = dependencies["nfo_manager"] - 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}") + # NFO file management is handled by the core container + # Web interface only updates database entries # Add to processing history - db.add_processing_history( - imdb_id=imdb_id, - media_type="episode", - event_type="manual_date_update", - details={ - "season": season, - "episode": episode, - "old_source": episode_data.get('source'), - "new_source": source, - "dateadded": dateadded - } - ) + try: + db.add_processing_history( + imdb_id=imdb_id, + media_type="episode", + event_type="manual_date_update", + details={ + "season": season, + "episode": episode, + "old_source": episode_data.get('source'), + "new_source": source, + "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}"} @@ -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): - """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}") 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']}" }) - # Option 3: Active lookup from external sources - 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 + # Option 3: Manual entry options.append({ "type": "manual", "label": "Manual Entry", @@ -1216,4 +926,209 @@ async def get_episode_date_options(dependencies: dict, imdb_id: str, season: int "episode": episode, "current_data": episode_data, "options": options - } \ No newline at end of file + } + + +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") \ No newline at end of file diff --git a/nfoguard-web/core/web_database.py b/nfoguard-web/core/web_database.py index 5c95e40..68f2c45 100644 --- a/nfoguard-web/core/web_database.py +++ b/nfoguard-web/core/web_database.py @@ -225,6 +225,153 @@ class WebDatabase: """ 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): """Close database connection""" if self.connection: diff --git a/nfoguard-web/static/js/app.js b/nfoguard-web/static/js/app.js index 79ee839..21867de 100644 --- a/nfoguard-web/static/js/app.js +++ b/nfoguard-web/static/js/app.js @@ -676,6 +676,13 @@ async function smartFixMovie(imdbId) { } 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 { const options = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}/date-options`); showSmartFixModal('episode', options); @@ -703,7 +710,10 @@ function showSmartFixModal(mediaType, options) { if (mediaType === 'movie') { title.textContent = `Fix Date for Movie: ${options.imdb_id}`; } 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 @@ -878,7 +888,10 @@ function showEnhancedEditModal(mediaType, options, currentDateadded, currentSour if (mediaType === 'movie') { title.textContent = `Edit Movie: ${options.imdb_id}`; } 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 @@ -1062,6 +1075,13 @@ async function handleEnhancedEditSubmit(event) { } 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 { // Load episode options to populate available dates const options = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}/date-options`); @@ -1244,6 +1264,13 @@ Analysis: // Episode deletion functionality 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')}`; // Confirmation dialog @@ -1252,7 +1279,7 @@ async function deleteEpisode(imdbId, season, episode) { } try { - const response = await fetch(`/database/episode/${imdbId}/${season}/${episode}`, { + const response = await fetch(`/api/episodes/${imdbId}/${season}/${episode}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' @@ -1295,7 +1322,7 @@ async function deleteMovie(imdbId) { } try { - const response = await fetch(`/database/movie/${imdbId}`, { + const response = await fetch(`/api/movies/${imdbId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json'