dev #62
+299
-6
@@ -1237,15 +1237,15 @@ def register_web_routes(app, dependencies):
|
|||||||
|
|
||||||
@app.post("/api/movies/{imdb_id}/update-date")
|
@app.post("/api/movies/{imdb_id}/update-date")
|
||||||
async def api_update_movie_date(imdb_id: str, dateadded: str = None, source: str = "manual"):
|
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}")
|
@app.put("/api/movies/{imdb_id}")
|
||||||
async def api_update_movie(imdb_id: str, dateadded: str = None, source: str = "manual"):
|
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")
|
@app.get("/api/movies/{imdb_id}/date-options")
|
||||||
async def api_movie_date_options(imdb_id: str):
|
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}")
|
@app.delete("/api/movies/{imdb_id}")
|
||||||
async def api_delete_movie(imdb_id: str):
|
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")
|
@app.post("/api/episodes/{imdb_id}/{season}/{episode}/update-date")
|
||||||
async def api_update_episode_date(imdb_id: str, season: int, episode: int,
|
async def api_update_episode_date(imdb_id: str, season: int, episode: int,
|
||||||
dateadded: str = None, source: str = "manual"):
|
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}")
|
@app.put("/api/episodes/{imdb_id}/{season}/{episode}")
|
||||||
async def api_update_episode(imdb_id: str, season: int, episode: int,
|
async def api_update_episode(imdb_id: str, season: int, episode: int,
|
||||||
dateadded: str = None, source: str = "manual"):
|
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")
|
@app.get("/api/episodes/{imdb_id}/{season}/{episode}/date-options")
|
||||||
async def api_episode_date_options(imdb_id: str, season: int, episode: int):
|
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}")
|
@app.delete("/api/episodes/{imdb_id}/{season}/{episode}")
|
||||||
async def api_delete_episode(imdb_id: str, season: int, episode: int):
|
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"}
|
return {"scanning": False, "message": "Invalid response from core container"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"scanning": False, "message": f"Unable to check scan status: {str(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