""" Web interface API routes for NFOGuard database management Provides endpoints for the web-based database manipulation interface """ import json from datetime import datetime, timezone from typing import List, Optional, Dict, Any from fastapi import HTTPException, Query from pathlib import Path from api.models import * # --------------------------- # Database Query Endpoints # --------------------------- async def get_movies_list(dependencies: dict, skip: int = Query(0, ge=0), limit: int = Query(100, le=1000), has_date: Optional[bool] = Query(None), source_filter: Optional[str] = Query(None), search: Optional[str] = Query(None), imdb_search: Optional[str] = Query(None)): """Get paginated list of movies with filtering options""" db = dependencies["db"] with db.get_connection() as conn: cursor = conn.cursor() # Build dynamic query where_conditions = [] params = [] if has_date is not None: if has_date: where_conditions.append("dateadded IS NOT NULL AND dateadded != ''") else: where_conditions.append("(dateadded IS NULL OR dateadded = '')") if source_filter: where_conditions.append("source = ?") params.append(source_filter) if search: where_conditions.append("(imdb_id LIKE ? OR path LIKE ?)") params.extend([f"%{search}%", f"%{search}%"]) if imdb_search: where_conditions.append("imdb_id LIKE ?") params.append(f"%{imdb_search}%") where_clause = " AND ".join(where_conditions) if where_conditions else "1=1" # Get total count count_query = f"SELECT COUNT(*) FROM movies WHERE {where_clause}" cursor.execute(count_query, params) total_count = cursor.fetchone()[0] # Get paginated results query = f""" SELECT imdb_id, path, released, dateadded, source, has_video_file, last_updated FROM movies WHERE {where_clause} ORDER BY last_updated DESC LIMIT ? OFFSET ? """ cursor.execute(query, params + [limit, skip]) movies = [] for row in cursor.fetchall(): movie = dict(row) # Extract title from path for display try: movie['title'] = Path(movie['path']).name if movie['path'] else movie['imdb_id'] except: movie['title'] = movie['imdb_id'] movies.append(movie) return { "movies": movies, "total_count": total_count, "page": skip // limit + 1, "pages": (total_count + limit - 1) // limit, "has_next": skip + limit < total_count, "has_prev": skip > 0 } async def get_tv_series_list(dependencies: dict, skip: int = Query(0, ge=0), limit: int = Query(50, le=500), search: Optional[str] = Query(None)): """Get paginated list of TV series with episode counts""" db = dependencies["db"] with db.get_connection() as conn: cursor = conn.cursor() # Build dynamic query where_conditions = [] params = [] if search: where_conditions.append("(s.imdb_id LIKE ? OR s.path LIKE ?)") params.extend([f"%{search}%", f"%{search}%"]) where_clause = " AND ".join(where_conditions) if where_conditions else "1=1" # Get total count count_query = f"SELECT COUNT(*) FROM series s WHERE {where_clause}" cursor.execute(count_query, params) total_count = cursor.fetchone()[0] # Get series with episode statistics query = f""" SELECT s.imdb_id, s.path, s.last_updated, COUNT(e.episode) as total_episodes, COUNT(CASE WHEN e.dateadded IS NOT NULL AND e.dateadded != '' THEN 1 END) as episodes_with_dates, COUNT(CASE WHEN e.has_video_file = 1 THEN 1 END) as episodes_with_video FROM series s LEFT JOIN episodes e ON s.imdb_id = e.imdb_id WHERE {where_clause} GROUP BY s.imdb_id, s.path, s.last_updated ORDER BY s.last_updated DESC LIMIT ? OFFSET ? """ cursor.execute(query, params + [limit, skip]) series = [] for row in cursor.fetchall(): series_data = dict(row) # Extract title from path try: series_data['title'] = Path(series_data['path']).name if series_data['path'] else series_data['imdb_id'] except: series_data['title'] = series_data['imdb_id'] series.append(series_data) return { "series": series, "total_count": total_count, "page": skip // limit + 1, "pages": (total_count + limit - 1) // limit, "has_next": skip + limit < total_count, "has_prev": skip > 0 } async def get_series_episodes(dependencies: dict, imdb_id: str): """Get all episodes for a specific TV series""" db = dependencies["db"] with db.get_connection() as conn: cursor = conn.cursor() # Get series info cursor.execute("SELECT * FROM series WHERE imdb_id = ?", (imdb_id,)) series_row = cursor.fetchone() if not series_row: raise HTTPException(status_code=404, detail="Series not found") series_info = dict(series_row) try: series_info['title'] = Path(series_info['path']).name if series_info['path'] else imdb_id except: series_info['title'] = imdb_id # Get episodes cursor.execute(""" SELECT season, episode, aired, dateadded, source, has_video_file, last_updated FROM episodes WHERE imdb_id = ? ORDER BY season, episode """, (imdb_id,)) episodes = [dict(row) for row in cursor.fetchall()] return { "series": series_info, "episodes": episodes } async def get_missing_dates_report(dependencies: dict): """Generate report of movies and episodes missing dateadded""" db = dependencies["db"] with db.get_connection() as conn: cursor = conn.cursor() # Movies without dates cursor.execute(""" SELECT imdb_id, path, released, source, last_updated FROM movies WHERE dateadded IS NULL OR dateadded = '' OR source = 'no_valid_date_source' ORDER BY last_updated DESC """) movies_missing = [] for row in cursor.fetchall(): movie = dict(row) try: movie['title'] = Path(movie['path']).name if movie['path'] else movie['imdb_id'] except: movie['title'] = movie['imdb_id'] movies_missing.append(movie) # Episodes without dates cursor.execute(""" SELECT e.imdb_id, e.season, e.episode, e.aired, e.source, e.last_updated, s.path FROM episodes e JOIN series s ON e.imdb_id = s.imdb_id WHERE e.dateadded IS NULL OR e.dateadded = '' OR e.source = 'no_valid_date_source' ORDER BY e.last_updated DESC """) episodes_missing = [] for row in cursor.fetchall(): episode = dict(row) try: episode['series_title'] = Path(episode['path']).name if episode['path'] else episode['imdb_id'] except: episode['series_title'] = episode['imdb_id'] episodes_missing.append(episode) # Summary statistics cursor.execute("SELECT COUNT(*) FROM movies WHERE dateadded IS NOT NULL AND dateadded != ''") movies_with_dates = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM movies") total_movies = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM episodes WHERE dateadded IS NOT NULL AND dateadded != ''") episodes_with_dates = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM episodes") total_episodes = cursor.fetchone()[0] return { "summary": { "movies_with_dates": movies_with_dates, "movies_missing_dates": len(movies_missing), "total_movies": total_movies, "episodes_with_dates": episodes_with_dates, "episodes_missing_dates": len(episodes_missing), "total_episodes": total_episodes }, "movies_missing": movies_missing, "episodes_missing": episodes_missing } async def get_dashboard_stats(dependencies: dict): """Get comprehensive dashboard statistics""" db = dependencies["db"] # Get basic stats from existing method stats = db.get_stats() with db.get_connection() as conn: cursor = conn.cursor() # Enhanced statistics cursor.execute("SELECT COUNT(*) FROM movies WHERE dateadded IS NOT NULL AND dateadded != ''") movies_with_dates = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM episodes WHERE dateadded IS NOT NULL AND dateadded != ''") episodes_with_dates = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM movies WHERE source = 'no_valid_date_source'") movies_no_valid_source = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM episodes WHERE source = 'no_valid_date_source'") episodes_no_valid_source = cursor.fetchone()[0] # Recent activity (last 7 days) cursor.execute(""" SELECT COUNT(*) FROM processing_history WHERE processed_at > datetime('now', '-7 days') """) recent_activity = cursor.fetchone()[0] # Source distribution for movies cursor.execute(""" SELECT source, COUNT(*) as count FROM movies WHERE source IS NOT NULL GROUP BY source ORDER BY count DESC """) movie_sources = [{"source": row[0], "count": row[1]} for row in cursor.fetchall()] # Source distribution for episodes cursor.execute(""" SELECT source, COUNT(*) as count FROM episodes WHERE source IS NOT NULL GROUP BY source ORDER BY count DESC """) episode_sources = [{"source": row[0], "count": row[1]} for row in cursor.fetchall()] # Combine with enhanced stats stats.update({ "movies_with_dates": movies_with_dates, "movies_missing_dates": stats["movies_total"] - movies_with_dates, "episodes_with_dates": episodes_with_dates, "episodes_missing_dates": stats["episodes_total"] - episodes_with_dates, "movies_no_valid_source": movies_no_valid_source, "episodes_no_valid_source": episodes_no_valid_source, "recent_activity_count": recent_activity, "movie_sources": movie_sources, "episode_sources": episode_sources }) return stats # --------------------------- # Database Modification Endpoints # --------------------------- async def update_movie_date(dependencies: dict, imdb_id: str, dateadded: Optional[str], source: str): """Update dateadded for a specific movie""" db = dependencies["db"] # Validate movie exists movie = db.get_movie_dates(imdb_id) if not movie: raise HTTPException(status_code=404, detail="Movie not found") # Update the date db.upsert_movie_dates( imdb_id=imdb_id, released=movie.get('released'), dateadded=dateadded, source=source, has_video_file=movie.get('has_video_file', False) ) # Add to processing history db.add_processing_history( imdb_id=imdb_id, media_type="movie", event_type="manual_date_update", details={"old_source": movie.get('source'), "new_source": source, "dateadded": dateadded} ) return {"status": "success", "message": f"Updated movie {imdb_id}"} async def update_episode_date(dependencies: dict, imdb_id: str, season: int, episode: int, dateadded: Optional[str], source: str): """Update dateadded for a specific episode""" db = dependencies["db"] # Get existing episode episode_data = db.get_episode_date(imdb_id, season, episode) if not episode_data: raise HTTPException(status_code=404, detail="Episode not found") # Update the date db.upsert_episode_date( imdb_id=imdb_id, season=season, episode=episode, aired=episode_data.get('aired'), dateadded=dateadded, source=source, has_video_file=episode_data.get('has_video_file', False) ) # 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 } ) return {"status": "success", "message": f"Updated episode {imdb_id} S{season:02d}E{episode:02d}"} async def bulk_update_source(dependencies: dict, media_type: str, old_source: str, new_source: str): """Bulk update source for movies or episodes""" db = dependencies["db"] if media_type not in ["movies", "episodes"]: raise HTTPException(status_code=400, detail="media_type must be 'movies' or 'episodes'") with db.get_connection() as conn: cursor = conn.cursor() if media_type == "movies": # Update movies cursor.execute("UPDATE movies SET source = ? WHERE source = ?", (new_source, old_source)) updated_count = cursor.rowcount # Add history entries cursor.execute("SELECT imdb_id FROM movies WHERE source = ?", (new_source,)) for row in cursor.fetchall(): db.add_processing_history( imdb_id=row[0], media_type="movie", event_type="bulk_source_update", details={"old_source": old_source, "new_source": new_source} ) else: # Update episodes cursor.execute("UPDATE episodes SET source = ? WHERE source = ?", (new_source, old_source)) updated_count = cursor.rowcount # Add history entries cursor.execute("SELECT imdb_id, season, episode FROM episodes WHERE source = ?", (new_source,)) for row in cursor.fetchall(): db.add_processing_history( imdb_id=row[0], media_type="episode", event_type="bulk_source_update", details={ "season": row[1], "episode": row[2], "old_source": old_source, "new_source": new_source } ) return { "status": "success", "message": f"Updated {updated_count} {media_type} from source '{old_source}' to '{new_source}'" } async def get_movie_date_options(dependencies: dict, imdb_id: str): """Get available date options for a movie (Radarr import, digital release, etc.)""" db = dependencies["db"] nfo_manager = dependencies["nfo_manager"] # Get current movie data movie = db.get_movie_dates(imdb_id) if not movie: raise HTTPException(status_code=404, detail="Movie not found") options = [] # Option 1: Current dateadded (if exists and is different from released) if movie.get('dateadded'): current_source = movie.get('source', 'Unknown') current_date = movie['dateadded'] # Determine what type of current date this is if 'radarr' in current_source.lower() and 'import' in current_source.lower(): label = "Keep Current (Radarr Import Date)" description = f"Keep using Radarr download/import date: {current_date}" elif current_source == 'digital_release': label = "Keep Current (Digital Release)" description = f"Keep using digital release date: {current_date}" elif current_source == 'nfo_file_existing': label = "Keep Current (From Existing NFO)" description = f"Keep using date from existing NFO file: {current_date}" else: label = f"Keep Current ({current_source})" description = f"Keep using current date from {current_source}: {current_date}" options.append({ "type": "current", "label": label, "date": current_date, "source": current_source, "description": description }) # Option 2: Released date as digital release (if different from current) if movie.get('released'): release_date = f"{movie['released']}T00:00:00" # Only add if it's different from current dateadded current_dateadded = movie.get('dateadded', '') if not current_dateadded or not current_dateadded.startswith(movie['released']): options.append({ "type": "digital_release", "label": "Use Actual Release Date", "date": release_date, "source": "digital_release", "description": f"Use the movie's actual release date: {movie['released']} (instead of download date)" }) # Option 3: Manual entry options.append({ "type": "manual", "label": "Manual Entry", "date": None, "source": "manual", "description": "Enter custom date and time" }) # Try to get additional metadata from external sources try: # This would integrate with your existing TMDB/OMDB logic # For now, we'll use the existing released date as an example pass except Exception: pass return { "imdb_id": imdb_id, "current_data": movie, "options": options } async def get_episode_date_options(dependencies: dict, imdb_id: str, season: int, episode: int): """Get available date options for an episode""" db = dependencies["db"] # Get current episode data episode_data = db.get_episode_date(imdb_id, season, episode) if not episode_data: raise HTTPException(status_code=404, detail="Episode not found") options = [] # Option 1: Current dateadded (if exists) if episode_data.get('dateadded'): options.append({ "type": "current", "label": f"Keep Current ({episode_data.get('source', 'Unknown')})", "date": episode_data['dateadded'], "source": episode_data.get('source', 'manual'), "description": f"Currently using: {episode_data.get('source', 'Unknown')}" }) # Option 2: Aired date if episode_data.get('aired'): options.append({ "type": "airdate", "label": "Use Air Date", "date": f"{episode_data['aired']}T20:00:00", # Default to 8 PM "source": "airdate", "description": f"Use original air date: {episode_data['aired']}" }) # Option 3: Manual entry options.append({ "type": "manual", "label": "Manual Entry", "date": None, "source": "manual", "description": "Enter custom date and time" }) return { "imdb_id": imdb_id, "season": season, "episode": episode, "current_data": episode_data, "options": options }