dev #62

Merged
sbcrumb merged 56 commits from dev into main 2025-10-26 15:45:15 -04:00
Showing only changes of commit f46f1df7cb - Show all commits
+299 -6
View File
@@ -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"
}
]
}