dev #62
+299
-6
@@ -1237,15 +1237,15 @@ def register_web_routes(app, dependencies):
|
||||
|
||||
@app.post("/api/movies/{imdb_id}/update-date")
|
||||
async def api_update_movie_date(imdb_id: str, dateadded: str = None, source: str = "manual"):
|
||||
return {"error": "Updates not available in web container. Use core container on port 8085."}
|
||||
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 {"error": "Updates not available in web container. Use core container on port 8085."}
|
||||
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 {"options": [], "message": "Date options not available in web container. Use core container on port 8085."}
|
||||
return await get_movie_date_options(dependencies, imdb_id)
|
||||
|
||||
@app.delete("/api/movies/{imdb_id}")
|
||||
async def api_delete_movie(imdb_id: str):
|
||||
@@ -1294,16 +1294,16 @@ def register_web_routes(app, dependencies):
|
||||
@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 {"error": "Updates not available in web container. Use core container on port 8085."}
|
||||
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 {"error": "Updates not available in web container. Use core container on port 8085."}
|
||||
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 {"options": [], "message": "Date options not available in web container. Use core container on port 8085."}
|
||||
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):
|
||||
@@ -1459,3 +1459,296 @@ def register_web_routes(app, dependencies):
|
||||
return {"scanning": False, "message": "Invalid response from core container"}
|
||||
except Exception as e:
|
||||
return {"scanning": False, "message": f"Unable to check scan status: {str(e)}"}
|
||||
|
||||
# Register database admin routes
|
||||
await register_database_admin_routes(app, dependencies)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DATABASE ADMIN INTERFACE
|
||||
# =============================================================================
|
||||
|
||||
async def execute_database_query(dependencies: dict, query: str, limit: int = 100):
|
||||
"""Execute a database query and return results"""
|
||||
db = dependencies["db"]
|
||||
|
||||
try:
|
||||
with db.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Add LIMIT if not already present in query
|
||||
if "LIMIT" not in query.upper() and not query.strip().endswith(";"):
|
||||
query += f" LIMIT {limit}"
|
||||
elif query.strip().endswith(";"):
|
||||
query = query.strip()[:-1] + f" LIMIT {limit};"
|
||||
|
||||
cursor.execute(query)
|
||||
|
||||
if query.strip().upper().startswith("SELECT"):
|
||||
results = cursor.fetchall()
|
||||
columns = [desc[0] for desc in cursor.description] if cursor.description else []
|
||||
return {
|
||||
"success": True,
|
||||
"query": query,
|
||||
"columns": columns,
|
||||
"rows": [dict(zip(columns, row)) for row in results],
|
||||
"row_count": len(results)
|
||||
}
|
||||
else:
|
||||
conn.commit()
|
||||
return {
|
||||
"success": True,
|
||||
"query": query,
|
||||
"message": f"Query executed successfully. Rows affected: {cursor.rowcount}",
|
||||
"rows_affected": cursor.rowcount
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"query": query,
|
||||
"error": str(e),
|
||||
"message": f"Database query failed: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
async def get_episodes_missing_nfo_dateadded(dependencies: dict):
|
||||
"""Find episodes that have dateadded in database but missing from NFO files"""
|
||||
db = dependencies["db"]
|
||||
config = dependencies["config"]
|
||||
|
||||
try:
|
||||
# Get all episodes with dateadded in database
|
||||
with db.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT e.imdb_id, e.season, e.episode, e.dateadded, e.source, e.has_video_file,
|
||||
s.path as series_path
|
||||
FROM episodes e
|
||||
JOIN series s ON e.imdb_id = s.imdb_id
|
||||
WHERE e.dateadded IS NOT NULL
|
||||
AND e.has_video_file = TRUE
|
||||
ORDER BY e.imdb_id, e.season, e.episode
|
||||
LIMIT 500
|
||||
""")
|
||||
episodes = [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
# Check each episode's NFO file
|
||||
missing_dateadded = []
|
||||
nfo_manager = dependencies["nfo_manager"]
|
||||
|
||||
for ep in episodes:
|
||||
try:
|
||||
# Build expected NFO path
|
||||
series_path = Path(ep['series_path'])
|
||||
season_dir = series_path / config.tv_season_dir_format.format(season=ep['season'])
|
||||
|
||||
if not season_dir.exists():
|
||||
continue
|
||||
|
||||
# Find NFO file for this episode
|
||||
nfo_files = list(season_dir.glob(f"*S{ep['season']:02d}E{ep['episode']:02d}*.nfo"))
|
||||
if not nfo_files:
|
||||
continue
|
||||
|
||||
nfo_path = nfo_files[0] # Use first match
|
||||
|
||||
# Parse NFO and check for dateadded
|
||||
try:
|
||||
import xml.etree.ElementTree as ET
|
||||
tree = ET.parse(nfo_path)
|
||||
root = tree.getroot()
|
||||
dateadded_elem = root.find('dateadded')
|
||||
|
||||
if dateadded_elem is None or not dateadded_elem.text:
|
||||
missing_dateadded.append({
|
||||
**ep,
|
||||
'nfo_path': str(nfo_path),
|
||||
'nfo_exists': True,
|
||||
'dateadded_in_nfo': False
|
||||
})
|
||||
except ET.ParseError:
|
||||
# NFO file is corrupted
|
||||
missing_dateadded.append({
|
||||
**ep,
|
||||
'nfo_path': str(nfo_path),
|
||||
'nfo_exists': True,
|
||||
'dateadded_in_nfo': False,
|
||||
'nfo_corrupt': True
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error checking NFO for {ep['imdb_id']} S{ep['season']:02d}E{ep['episode']:02d}: {e}")
|
||||
continue
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"total_episodes_checked": len(episodes),
|
||||
"missing_dateadded_count": len(missing_dateadded),
|
||||
"episodes": missing_dateadded[:50] # Limit display to first 50
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Failed to check NFO files: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
async def bulk_update_nfo_files(dependencies: dict, imdb_ids: list = None, fix_all: bool = False):
|
||||
"""Update NFO files with missing dateadded elements from database"""
|
||||
db = dependencies["db"]
|
||||
config = dependencies["config"]
|
||||
nfo_manager = dependencies["nfo_manager"]
|
||||
|
||||
if not config.manage_nfo:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "NFO management is disabled in configuration"
|
||||
}
|
||||
|
||||
try:
|
||||
# Build query based on parameters
|
||||
if fix_all:
|
||||
query = """
|
||||
SELECT e.imdb_id, e.season, e.episode, e.aired, e.dateadded, e.source,
|
||||
s.path as series_path
|
||||
FROM episodes e
|
||||
JOIN series s ON e.imdb_id = s.imdb_id
|
||||
WHERE e.dateadded IS NOT NULL
|
||||
AND e.has_video_file = TRUE
|
||||
ORDER BY e.imdb_id, e.season, e.episode
|
||||
"""
|
||||
params = []
|
||||
elif imdb_ids:
|
||||
placeholders = ','.join(['%s'] * len(imdb_ids))
|
||||
query = f"""
|
||||
SELECT e.imdb_id, e.season, e.episode, e.aired, e.dateadded, e.source,
|
||||
s.path as series_path
|
||||
FROM episodes e
|
||||
JOIN series s ON e.imdb_id = s.imdb_id
|
||||
WHERE e.imdb_id IN ({placeholders})
|
||||
AND e.dateadded IS NOT NULL
|
||||
AND e.has_video_file = TRUE
|
||||
ORDER BY e.imdb_id, e.season, e.episode
|
||||
"""
|
||||
params = imdb_ids
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No IMDb IDs specified and fix_all not enabled"
|
||||
}
|
||||
|
||||
# Get episodes to update
|
||||
with db.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, params)
|
||||
episodes = [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
if not episodes:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "No episodes found to update",
|
||||
"updated_count": 0
|
||||
}
|
||||
|
||||
# Update NFO files
|
||||
updated_count = 0
|
||||
errors = []
|
||||
|
||||
for ep in episodes:
|
||||
try:
|
||||
series_path = Path(ep['series_path'])
|
||||
season_dir = series_path / config.tv_season_dir_format.format(season=ep['season'])
|
||||
|
||||
if not season_dir.exists():
|
||||
continue
|
||||
|
||||
# Update NFO file
|
||||
nfo_manager.create_episode_nfo(
|
||||
season_dir=season_dir,
|
||||
season_num=ep['season'],
|
||||
episode_num=ep['episode'],
|
||||
aired=ep['aired'],
|
||||
dateadded=ep['dateadded'],
|
||||
source=ep['source'],
|
||||
lock_metadata=config.lock_metadata
|
||||
)
|
||||
|
||||
updated_count += 1
|
||||
print(f"✅ Updated NFO: {ep['imdb_id']} S{ep['season']:02d}E{ep['episode']:02d}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to update {ep['imdb_id']} S{ep['season']:02d}E{ep['episode']:02d}: {str(e)}"
|
||||
errors.append(error_msg)
|
||||
print(f"❌ {error_msg}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Updated {updated_count} NFO files",
|
||||
"updated_count": updated_count,
|
||||
"total_episodes": len(episodes),
|
||||
"errors": errors[:10] # Limit errors to first 10
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Bulk NFO update failed: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
# Add database admin routes to the web interface
|
||||
async def register_database_admin_routes(app, dependencies):
|
||||
"""Register database admin routes"""
|
||||
|
||||
@app.post("/api/admin/database/query")
|
||||
async def api_database_query(query: str, limit: int = 100):
|
||||
"""Execute a database query"""
|
||||
return await execute_database_query(dependencies, query, limit)
|
||||
|
||||
@app.get("/api/admin/nfo/missing-dateadded")
|
||||
async def api_episodes_missing_nfo_dateadded():
|
||||
"""Get episodes missing dateadded in NFO files"""
|
||||
return await get_episodes_missing_nfo_dateadded(dependencies)
|
||||
|
||||
@app.post("/api/admin/nfo/bulk-update")
|
||||
async def api_bulk_update_nfo_files(imdb_ids: list = None, fix_all: bool = False):
|
||||
"""Bulk update NFO files with missing dateadded"""
|
||||
return await bulk_update_nfo_files(dependencies, imdb_ids, fix_all)
|
||||
|
||||
@app.get("/api/admin/database/quick-queries")
|
||||
async def api_database_quick_queries():
|
||||
"""Get predefined quick queries for common tasks"""
|
||||
return {
|
||||
"success": True,
|
||||
"queries": [
|
||||
{
|
||||
"name": "Episodes missing dateadded in NFO",
|
||||
"description": "Find episodes with dateadded in DB but missing from NFO files",
|
||||
"query": "SELECT e.imdb_id, e.season, e.episode, e.dateadded, e.source, s.path FROM episodes e JOIN series s ON e.imdb_id = s.imdb_id WHERE e.dateadded IS NOT NULL AND e.has_video_file = TRUE ORDER BY e.last_updated DESC"
|
||||
},
|
||||
{
|
||||
"name": "Recent webhook episodes",
|
||||
"description": "Episodes processed in last 24 hours",
|
||||
"query": "SELECT * FROM episodes WHERE last_updated > NOW() - INTERVAL '1 day' ORDER BY last_updated DESC"
|
||||
},
|
||||
{
|
||||
"name": "Episodes by source type",
|
||||
"description": "Count episodes by their date source",
|
||||
"query": "SELECT source, COUNT(*) as count FROM episodes GROUP BY source ORDER BY count DESC"
|
||||
},
|
||||
{
|
||||
"name": "Series with most episodes",
|
||||
"description": "Series sorted by episode count",
|
||||
"query": "SELECT s.imdb_id, s.path, COUNT(e.episode) as episode_count FROM series s LEFT JOIN episodes e ON s.imdb_id = e.imdb_id GROUP BY s.imdb_id, s.path ORDER BY episode_count DESC"
|
||||
},
|
||||
{
|
||||
"name": "Episodes without video files",
|
||||
"description": "Episodes in database without video files",
|
||||
"query": "SELECT imdb_id, season, episode, dateadded, source FROM episodes WHERE has_video_file = FALSE ORDER BY imdb_id, season, episode"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user