This commit is contained in:
+226
-311
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user