feat: add cron for scans
Local Docker Build (Dev) / build-dev (push) Successful in 7s

This commit is contained in:
2025-10-28 17:33:08 -04:00
parent 6d2435a037
commit f11839e005
6 changed files with 1254 additions and 1 deletions
+322
View File
@@ -1897,4 +1897,326 @@ def register_database_admin_routes(app, dependencies):
"query": "SELECT imdb_id, season, episode, dateadded, source FROM episodes WHERE has_video_file = FALSE ORDER BY imdb_id, season, episode"
}
]
}
# Scheduled Scans Endpoints
@app.get("/api/admin/scheduled-scans")
async def api_get_scheduled_scans():
"""Get all scheduled scans"""
return await get_scheduled_scans(dependencies)
@app.post("/api/admin/scheduled-scans")
async def api_create_scheduled_scan(request: CreateScheduledScanRequest):
"""Create a new scheduled scan"""
return await create_scheduled_scan(dependencies, request)
@app.put("/api/admin/scheduled-scans/{scan_id}")
async def api_update_scheduled_scan(scan_id: int, request: UpdateScheduledScanRequest):
"""Update a scheduled scan"""
return await update_scheduled_scan(dependencies, scan_id, request)
@app.delete("/api/admin/scheduled-scans/{scan_id}")
async def api_delete_scheduled_scan(scan_id: int):
"""Delete a scheduled scan"""
return await delete_scheduled_scan(dependencies, scan_id)
@app.post("/api/admin/scheduled-scans/{scan_id}/toggle")
async def api_toggle_scheduled_scan(scan_id: int):
"""Toggle a scheduled scan enabled/disabled"""
return await toggle_scheduled_scan(dependencies, scan_id)
@app.post("/api/admin/scheduled-scans/{scan_id}/run")
async def api_run_scheduled_scan(scan_id: int):
"""Manually run a scheduled scan"""
return await run_scheduled_scan(dependencies, scan_id)
@app.get("/api/admin/scheduled-scans/executions")
async def api_get_schedule_executions(schedule_id: int = None):
"""Get schedule execution history"""
return await get_schedule_executions(dependencies, schedule_id)
# Scheduled Scans Functions
async def get_scheduled_scans(dependencies: dict):
"""Get all scheduled scans"""
try:
db = dependencies["db"]
scans = db.get_scheduled_scans()
# Convert to response format
scan_list = []
for scan in scans:
scan_dict = dict(scan)
# Convert datetime objects to strings
for field in ['created_at', 'updated_at', 'last_run_at', 'next_run_at']:
if scan_dict.get(field):
scan_dict[field] = scan_dict[field].isoformat()
scan_list.append(scan_dict)
return {
"success": True,
"scans": scan_list
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
async def create_scheduled_scan(dependencies: dict, request: CreateScheduledScanRequest):
"""Create a new scheduled scan"""
try:
db = dependencies["db"]
# Validate cron expression
from croniter import croniter
if not croniter.is_valid(request.cron_expression):
return {
"success": False,
"error": "Invalid cron expression"
}
# Validate media type and scan mode
if request.media_type not in ['tv', 'movies', 'both']:
return {
"success": False,
"error": "Invalid media type. Must be 'tv', 'movies', or 'both'"
}
if request.scan_mode not in ['smart', 'full', 'incomplete']:
return {
"success": False,
"error": "Invalid scan mode. Must be 'smart', 'full', or 'incomplete'"
}
# Create the scheduled scan
scan_id = db.create_scheduled_scan(
name=request.name,
description=request.description,
cron_expression=request.cron_expression,
media_type=request.media_type,
scan_mode=request.scan_mode,
specific_paths=request.specific_paths,
enabled=request.enabled,
created_by="web_interface"
)
# Calculate next run time
from croniter import croniter
from datetime import datetime, timezone
cron = croniter(request.cron_expression, datetime.now(timezone.utc))
next_run = cron.get_next(datetime)
db.update_scan_next_run(scan_id, next_run)
return {
"success": True,
"scan_id": scan_id,
"message": f"Scheduled scan '{request.name}' created successfully"
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
async def update_scheduled_scan(dependencies: dict, scan_id: int, request: UpdateScheduledScanRequest):
"""Update a scheduled scan"""
try:
db = dependencies["db"]
# Check if scan exists
existing_scan = db.get_scheduled_scan(scan_id)
if not existing_scan:
return {
"success": False,
"error": "Scheduled scan not found"
}
# Validate cron expression if provided
if request.cron_expression:
from croniter import croniter
if not croniter.is_valid(request.cron_expression):
return {
"success": False,
"error": "Invalid cron expression"
}
# Validate media type and scan mode if provided
if request.media_type and request.media_type not in ['tv', 'movies', 'both']:
return {
"success": False,
"error": "Invalid media type. Must be 'tv', 'movies', or 'both'"
}
if request.scan_mode and request.scan_mode not in ['smart', 'full', 'incomplete']:
return {
"success": False,
"error": "Invalid scan mode. Must be 'smart', 'full', or 'incomplete'"
}
# Update the scheduled scan
success = db.update_scheduled_scan(
scan_id=scan_id,
name=request.name,
description=request.description,
cron_expression=request.cron_expression,
media_type=request.media_type,
scan_mode=request.scan_mode,
specific_paths=request.specific_paths,
enabled=request.enabled,
updated_by="web_interface"
)
if not success:
return {
"success": False,
"error": "Failed to update scheduled scan"
}
# Update next run time if cron expression changed
if request.cron_expression:
from croniter import croniter
from datetime import datetime, timezone
cron = croniter(request.cron_expression, datetime.now(timezone.utc))
next_run = cron.get_next(datetime)
db.update_scan_next_run(scan_id, next_run)
return {
"success": True,
"message": "Scheduled scan updated successfully"
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
async def delete_scheduled_scan(dependencies: dict, scan_id: int):
"""Delete a scheduled scan"""
try:
db = dependencies["db"]
# Check if scan exists
existing_scan = db.get_scheduled_scan(scan_id)
if not existing_scan:
return {
"success": False,
"error": "Scheduled scan not found"
}
# Delete the scheduled scan
success = db.delete_scheduled_scan(scan_id)
if not success:
return {
"success": False,
"error": "Failed to delete scheduled scan"
}
return {
"success": True,
"message": f"Scheduled scan '{existing_scan['name']}' deleted successfully"
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
async def toggle_scheduled_scan(dependencies: dict, scan_id: int):
"""Toggle a scheduled scan enabled/disabled"""
try:
db = dependencies["db"]
# Check if scan exists
existing_scan = db.get_scheduled_scan(scan_id)
if not existing_scan:
return {
"success": False,
"error": "Scheduled scan not found"
}
# Toggle enabled status
new_enabled = not existing_scan['enabled']
success = db.update_scheduled_scan(
scan_id=scan_id,
enabled=new_enabled,
updated_by="web_interface"
)
if not success:
return {
"success": False,
"error": "Failed to toggle scheduled scan"
}
status = "enabled" if new_enabled else "disabled"
return {
"success": True,
"message": f"Scheduled scan '{existing_scan['name']}' {status} successfully"
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
async def run_scheduled_scan(dependencies: dict, scan_id: int):
"""Manually run a scheduled scan"""
try:
db = dependencies["db"]
# Check if scan exists
existing_scan = db.get_scheduled_scan(scan_id)
if not existing_scan:
return {
"success": False,
"error": "Scheduled scan not found"
}
# TODO: Implement actual scan execution
# For now, just return a success message
return {
"success": True,
"message": f"Manual execution of '{existing_scan['name']}' started",
"note": "Scan execution will be implemented with the background scheduler"
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
async def get_schedule_executions(dependencies: dict, schedule_id: int = None):
"""Get schedule execution history"""
try:
db = dependencies["db"]
executions = db.get_schedule_executions(schedule_id)
# Convert to response format
execution_list = []
for execution in executions:
execution_dict = dict(execution)
# Convert datetime objects to strings
for field in ['started_at', 'completed_at']:
if execution_dict.get(field):
execution_dict[field] = execution_dict[field].isoformat()
execution_list.append(execution_dict)
return {
"success": True,
"executions": execution_list
}
except Exception as e:
return {
"success": False,
"error": str(e)
}