758
web/backend/routers/scheduler.py
Normal file
758
web/backend/routers/scheduler.py
Normal file
@@ -0,0 +1,758 @@
|
||||
"""
|
||||
Scheduler Router
|
||||
|
||||
Handles all scheduler and service management operations:
|
||||
- Scheduler status and task management
|
||||
- Current activity monitoring
|
||||
- Task pause/resume/skip operations
|
||||
- Service start/stop/restart
|
||||
- Cache builder service management
|
||||
- Dependency updates
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import sqlite3
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
from ..core.dependencies import get_current_user, require_admin, get_app_state
|
||||
from ..core.config import settings
|
||||
from ..core.exceptions import (
|
||||
handle_exceptions,
|
||||
RecordNotFoundError,
|
||||
ServiceError
|
||||
)
|
||||
from ..core.responses import now_iso8601
|
||||
from modules.universal_logger import get_logger
|
||||
|
||||
logger = get_logger('API')
|
||||
|
||||
router = APIRouter(prefix="/api/scheduler", tags=["Scheduler"])
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
# Service names
|
||||
SCHEDULER_SERVICE = 'media-downloader.service'
|
||||
CACHE_BUILDER_SERVICE = 'media-cache-builder.service'
|
||||
|
||||
# Valid platform names for subprocess operations (defense in depth)
|
||||
VALID_PLATFORMS = frozenset(['fastdl', 'imginn', 'imginn_api', 'toolzu', 'snapchat', 'tiktok', 'forums', 'coppermine', 'instagram', 'youtube'])
|
||||
|
||||
# Display name mapping for scheduler task_id prefixes
|
||||
PLATFORM_DISPLAY_NAMES = {
|
||||
'fastdl': 'FastDL',
|
||||
'imginn': 'ImgInn',
|
||||
'imginn_api': 'ImgInn API',
|
||||
'toolzu': 'Toolzu',
|
||||
'snapchat': 'Snapchat',
|
||||
'tiktok': 'TikTok',
|
||||
'forums': 'Forums',
|
||||
'forum': 'Forum',
|
||||
'monitor': 'Forum Monitor',
|
||||
'instagram': 'Instagram',
|
||||
'youtube': 'YouTube',
|
||||
'youtube_channel_monitor': 'YouTube Channels',
|
||||
'youtube_monitor': 'YouTube Monitor',
|
||||
'coppermine': 'Coppermine',
|
||||
'paid_content': 'Paid Content',
|
||||
'appearances': 'Appearances',
|
||||
'easynews_monitor': 'Easynews Monitor',
|
||||
'press_monitor': 'Press Monitor',
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SCHEDULER STATUS ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/status")
|
||||
@limiter.limit("100/minute")
|
||||
@handle_exceptions
|
||||
async def get_scheduler_status(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get detailed scheduler status including all tasks."""
|
||||
app_state = get_app_state()
|
||||
|
||||
# Get enabled forums from config to filter scheduler tasks
|
||||
enabled_forums = set()
|
||||
forums_config = app_state.settings.get('forums')
|
||||
if forums_config and isinstance(forums_config, dict):
|
||||
for forum_cfg in forums_config.get('configs', []):
|
||||
if forum_cfg.get('enabled', False):
|
||||
enabled_forums.add(forum_cfg.get('name'))
|
||||
|
||||
with sqlite3.connect('scheduler_state') as sched_conn:
|
||||
cursor = sched_conn.cursor()
|
||||
|
||||
# Get all tasks
|
||||
cursor.execute("""
|
||||
SELECT task_id, last_run, next_run, run_count, status, last_download_count
|
||||
FROM scheduler_state
|
||||
ORDER BY next_run ASC
|
||||
""")
|
||||
tasks_raw = cursor.fetchall()
|
||||
|
||||
# Clean up stale forum/monitor entries
|
||||
stale_task_ids = []
|
||||
# Platforms that should always have :username suffix
|
||||
platforms_requiring_username = {'tiktok', 'instagram', 'imginn', 'imginn_api', 'toolzu', 'snapchat', 'fastdl'}
|
||||
|
||||
for row in tasks_raw:
|
||||
task_id = row[0]
|
||||
if task_id.startswith('forum:') or task_id.startswith('monitor:'):
|
||||
forum_name = task_id.split(':', 1)[1]
|
||||
if forum_name not in enabled_forums:
|
||||
stale_task_ids.append(task_id)
|
||||
# Clean up legacy platform entries without :username suffix
|
||||
elif task_id in platforms_requiring_username:
|
||||
stale_task_ids.append(task_id)
|
||||
|
||||
# Delete stale entries
|
||||
if stale_task_ids:
|
||||
for stale_id in stale_task_ids:
|
||||
cursor.execute("DELETE FROM scheduler_state WHERE task_id = ?", (stale_id,))
|
||||
sched_conn.commit()
|
||||
|
||||
tasks = []
|
||||
for row in tasks_raw:
|
||||
task_id = row[0]
|
||||
# Skip stale and maintenance tasks
|
||||
if task_id in stale_task_ids:
|
||||
continue
|
||||
if task_id.startswith('maintenance:'):
|
||||
continue
|
||||
|
||||
tasks.append({
|
||||
"task_id": task_id,
|
||||
"last_run": row[1],
|
||||
"next_run": row[2],
|
||||
"run_count": row[3],
|
||||
"status": row[4],
|
||||
"last_download_count": row[5]
|
||||
})
|
||||
|
||||
# Count active tasks
|
||||
active_count = sum(1 for t in tasks if t['status'] == 'active')
|
||||
|
||||
# Get next run time
|
||||
next_run = None
|
||||
for task in sorted(tasks, key=lambda t: t['next_run'] or ''):
|
||||
if task['status'] == 'active' and task['next_run']:
|
||||
next_run = task['next_run']
|
||||
break
|
||||
|
||||
return {
|
||||
"running": active_count > 0,
|
||||
"tasks": tasks,
|
||||
"total_tasks": len(tasks),
|
||||
"active_tasks": active_count,
|
||||
"next_run": next_run
|
||||
}
|
||||
|
||||
|
||||
@router.get("/current-activity")
|
||||
@limiter.limit("100/minute")
|
||||
@handle_exceptions
|
||||
async def get_current_activity(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get current scheduler activity for real-time status."""
|
||||
app_state = get_app_state()
|
||||
|
||||
# Check if scheduler service is running
|
||||
result = subprocess.run(
|
||||
['systemctl', 'is-active', SCHEDULER_SERVICE],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
scheduler_running = result.stdout.strip() == 'active'
|
||||
|
||||
if not scheduler_running:
|
||||
return {
|
||||
"active": False,
|
||||
"scheduler_running": False,
|
||||
"task_id": None,
|
||||
"platform": None,
|
||||
"account": None,
|
||||
"start_time": None,
|
||||
"status": None
|
||||
}
|
||||
|
||||
# Get current activity from database
|
||||
from modules.activity_status import get_activity_manager
|
||||
activity_manager = get_activity_manager(app_state.db)
|
||||
activity = activity_manager.get_current_activity()
|
||||
activity["scheduler_running"] = True
|
||||
return activity
|
||||
|
||||
|
||||
@router.get("/background-tasks")
|
||||
@limiter.limit("100/minute")
|
||||
@handle_exceptions
|
||||
async def get_background_tasks(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get all active background tasks (YouTube monitor, etc.) for real-time status."""
|
||||
app_state = get_app_state()
|
||||
|
||||
from modules.activity_status import get_activity_manager
|
||||
activity_manager = get_activity_manager(app_state.db)
|
||||
tasks = activity_manager.get_active_background_tasks()
|
||||
return {"tasks": tasks}
|
||||
|
||||
|
||||
@router.get("/background-tasks/{task_id}")
|
||||
@limiter.limit("100/minute")
|
||||
@handle_exceptions
|
||||
async def get_background_task(
|
||||
task_id: str,
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get a specific background task status."""
|
||||
app_state = get_app_state()
|
||||
|
||||
from modules.activity_status import get_activity_manager
|
||||
activity_manager = get_activity_manager(app_state.db)
|
||||
task = activity_manager.get_background_task(task_id)
|
||||
|
||||
if not task:
|
||||
return {"active": False, "task_id": task_id}
|
||||
return task
|
||||
|
||||
|
||||
@router.post("/current-activity/stop")
|
||||
@limiter.limit("20/minute")
|
||||
@handle_exceptions
|
||||
async def stop_current_activity(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Stop the currently running download task."""
|
||||
app_state = get_app_state()
|
||||
activity_file = settings.PROJECT_ROOT / 'database' / 'current_activity.json'
|
||||
|
||||
if not activity_file.exists():
|
||||
raise RecordNotFoundError("No active task running")
|
||||
|
||||
with open(activity_file, 'r') as f:
|
||||
activity_data = json.load(f)
|
||||
|
||||
if not activity_data.get('active'):
|
||||
raise RecordNotFoundError("No active task running")
|
||||
|
||||
task_id = activity_data.get('task_id')
|
||||
platform = activity_data.get('platform')
|
||||
|
||||
# Security: Validate platform before using in subprocess (defense in depth)
|
||||
if platform and platform not in VALID_PLATFORMS:
|
||||
logger.warning(f"Invalid platform in activity file: {platform}", module="Security")
|
||||
platform = None
|
||||
|
||||
# Find and kill the process
|
||||
if platform:
|
||||
result = subprocess.run(
|
||||
['pgrep', '-f', f'media-downloader\\.py.*--platform.*{re.escape(platform)}'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
else:
|
||||
# Fallback: find any media-downloader process
|
||||
result = subprocess.run(
|
||||
['pgrep', '-f', 'media-downloader\\.py'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.stdout.strip():
|
||||
pids = [p.strip() for p in result.stdout.strip().split('\n') if p.strip()]
|
||||
for pid in pids:
|
||||
try:
|
||||
os.kill(int(pid), signal.SIGTERM)
|
||||
logger.info(f"Stopped process {pid} for platform {platform}")
|
||||
except (ProcessLookupError, ValueError):
|
||||
pass
|
||||
|
||||
# Clear the current activity
|
||||
inactive_state = {
|
||||
"active": False,
|
||||
"task_id": None,
|
||||
"platform": None,
|
||||
"account": None,
|
||||
"start_time": None,
|
||||
"status": "stopped"
|
||||
}
|
||||
|
||||
with open(activity_file, 'w') as f:
|
||||
json.dump(inactive_state, f)
|
||||
|
||||
# Broadcast stop event
|
||||
try:
|
||||
if hasattr(app_state, 'websocket_manager') and app_state.websocket_manager:
|
||||
await app_state.websocket_manager.broadcast({
|
||||
"type": "download_stopped",
|
||||
"task_id": task_id,
|
||||
"platform": platform,
|
||||
"timestamp": now_iso8601()
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Stopped {platform} download",
|
||||
"task_id": task_id
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TASK MANAGEMENT ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.post("/tasks/{task_id}/pause")
|
||||
@limiter.limit("20/minute")
|
||||
@handle_exceptions
|
||||
async def pause_scheduler_task(
|
||||
request: Request,
|
||||
task_id: str,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Pause a specific scheduler task."""
|
||||
app_state = get_app_state()
|
||||
|
||||
with sqlite3.connect('scheduler_state') as sched_conn:
|
||||
cursor = sched_conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE scheduler_state
|
||||
SET status = 'paused'
|
||||
WHERE task_id = ?
|
||||
""", (task_id,))
|
||||
|
||||
sched_conn.commit()
|
||||
row_count = cursor.rowcount
|
||||
|
||||
if row_count == 0:
|
||||
raise RecordNotFoundError("Task not found", {"task_id": task_id})
|
||||
|
||||
# Broadcast event
|
||||
try:
|
||||
if hasattr(app_state, 'websocket_manager') and app_state.websocket_manager:
|
||||
await app_state.websocket_manager.broadcast({
|
||||
"type": "scheduler_task_paused",
|
||||
"task_id": task_id,
|
||||
"timestamp": now_iso8601()
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"success": True, "task_id": task_id, "status": "paused"}
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/resume")
|
||||
@limiter.limit("20/minute")
|
||||
@handle_exceptions
|
||||
async def resume_scheduler_task(
|
||||
request: Request,
|
||||
task_id: str,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Resume a paused scheduler task."""
|
||||
app_state = get_app_state()
|
||||
|
||||
with sqlite3.connect('scheduler_state') as sched_conn:
|
||||
cursor = sched_conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE scheduler_state
|
||||
SET status = 'active'
|
||||
WHERE task_id = ?
|
||||
""", (task_id,))
|
||||
|
||||
sched_conn.commit()
|
||||
row_count = cursor.rowcount
|
||||
|
||||
if row_count == 0:
|
||||
raise RecordNotFoundError("Task not found", {"task_id": task_id})
|
||||
|
||||
# Broadcast event
|
||||
try:
|
||||
if hasattr(app_state, 'websocket_manager') and app_state.websocket_manager:
|
||||
await app_state.websocket_manager.broadcast({
|
||||
"type": "scheduler_task_resumed",
|
||||
"task_id": task_id,
|
||||
"timestamp": now_iso8601()
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"success": True, "task_id": task_id, "status": "active"}
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/skip")
|
||||
@limiter.limit("20/minute")
|
||||
@handle_exceptions
|
||||
async def skip_next_run(
|
||||
request: Request,
|
||||
task_id: str,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Skip the next scheduled run by advancing next_run time."""
|
||||
app_state = get_app_state()
|
||||
|
||||
with sqlite3.connect('scheduler_state') as sched_conn:
|
||||
cursor = sched_conn.cursor()
|
||||
|
||||
# Get current task info
|
||||
cursor.execute("""
|
||||
SELECT next_run, interval_hours
|
||||
FROM scheduler_state
|
||||
WHERE task_id = ?
|
||||
""", (task_id,))
|
||||
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
raise RecordNotFoundError("Task not found", {"task_id": task_id})
|
||||
|
||||
current_next_run, interval_hours = result
|
||||
|
||||
# Calculate new next_run time
|
||||
current_time = datetime.fromisoformat(current_next_run)
|
||||
new_next_run = current_time + timedelta(hours=interval_hours)
|
||||
|
||||
# Update the next_run time
|
||||
cursor.execute("""
|
||||
UPDATE scheduler_state
|
||||
SET next_run = ?
|
||||
WHERE task_id = ?
|
||||
""", (new_next_run.isoformat(), task_id))
|
||||
|
||||
sched_conn.commit()
|
||||
|
||||
# Broadcast event
|
||||
try:
|
||||
if hasattr(app_state, 'websocket_manager') and app_state.websocket_manager:
|
||||
await app_state.websocket_manager.broadcast({
|
||||
"type": "scheduler_run_skipped",
|
||||
"task_id": task_id,
|
||||
"new_next_run": new_next_run.isoformat(),
|
||||
"timestamp": now_iso8601()
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"task_id": task_id,
|
||||
"skipped_run": current_next_run,
|
||||
"new_next_run": new_next_run.isoformat()
|
||||
}
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/reschedule")
|
||||
@limiter.limit("20/minute")
|
||||
@handle_exceptions
|
||||
async def reschedule_task(
|
||||
request: Request,
|
||||
task_id: str,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Reschedule a task to a new next_run time."""
|
||||
body = await request.json()
|
||||
new_next_run = body.get('next_run')
|
||||
if not new_next_run:
|
||||
raise HTTPException(status_code=400, detail="next_run is required")
|
||||
|
||||
try:
|
||||
parsed = datetime.fromisoformat(new_next_run)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid datetime format")
|
||||
|
||||
with sqlite3.connect('scheduler_state') as sched_conn:
|
||||
cursor = sched_conn.cursor()
|
||||
cursor.execute(
|
||||
"UPDATE scheduler_state SET next_run = ? WHERE task_id = ?",
|
||||
(parsed.isoformat(), task_id)
|
||||
)
|
||||
sched_conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise RecordNotFoundError("Task not found", {"task_id": task_id})
|
||||
|
||||
# Broadcast event
|
||||
try:
|
||||
app_state = get_app_state()
|
||||
if hasattr(app_state, 'websocket_manager') and app_state.websocket_manager:
|
||||
await app_state.websocket_manager.broadcast({
|
||||
"type": "scheduler_task_rescheduled",
|
||||
"task_id": task_id,
|
||||
"new_next_run": parsed.isoformat(),
|
||||
"timestamp": now_iso8601()
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"success": True, "task_id": task_id, "new_next_run": parsed.isoformat()}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CONFIG RELOAD ENDPOINT
|
||||
# ============================================================================
|
||||
|
||||
@router.post("/reload-config")
|
||||
@limiter.limit("10/minute")
|
||||
@handle_exceptions
|
||||
async def reload_scheduler_config(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Reload scheduler config — picks up new/removed accounts and interval changes."""
|
||||
app_state = get_app_state()
|
||||
|
||||
if not hasattr(app_state, 'scheduler') or app_state.scheduler is None:
|
||||
raise ServiceError("Scheduler is not running", {"service": SCHEDULER_SERVICE})
|
||||
|
||||
result = app_state.scheduler.reload_scheduled_tasks()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"added": result['added'],
|
||||
"removed": result['removed'],
|
||||
"modified": result['modified'],
|
||||
"message": (
|
||||
f"Reload complete: {len(result['added'])} added, "
|
||||
f"{len(result['removed'])} removed, "
|
||||
f"{len(result['modified'])} modified"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SERVICE MANAGEMENT ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/service/status")
|
||||
@limiter.limit("100/minute")
|
||||
@handle_exceptions
|
||||
async def get_scheduler_service_status(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Check if scheduler service is running."""
|
||||
result = subprocess.run(
|
||||
['systemctl', 'is-active', SCHEDULER_SERVICE],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
is_running = result.stdout.strip() == 'active'
|
||||
|
||||
return {
|
||||
"running": is_running,
|
||||
"status": result.stdout.strip()
|
||||
}
|
||||
|
||||
|
||||
@router.post("/service/start")
|
||||
@limiter.limit("20/minute")
|
||||
@handle_exceptions
|
||||
async def start_scheduler_service(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(require_admin) # Require admin for service operations
|
||||
):
|
||||
"""Start the scheduler service. Requires admin privileges."""
|
||||
result = subprocess.run(
|
||||
['sudo', 'systemctl', 'start', SCHEDULER_SERVICE],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise ServiceError(
|
||||
f"Failed to start service: {result.stderr}",
|
||||
{"service": SCHEDULER_SERVICE}
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Scheduler service started"}
|
||||
|
||||
|
||||
@router.post("/service/stop")
|
||||
@limiter.limit("20/minute")
|
||||
@handle_exceptions
|
||||
async def stop_scheduler_service(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(require_admin) # Require admin for service operations
|
||||
):
|
||||
"""Stop the scheduler service. Requires admin privileges."""
|
||||
result = subprocess.run(
|
||||
['sudo', 'systemctl', 'stop', SCHEDULER_SERVICE],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise ServiceError(
|
||||
f"Failed to stop service: {result.stderr}",
|
||||
{"service": SCHEDULER_SERVICE}
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Scheduler service stopped"}
|
||||
|
||||
|
||||
@router.post("/service/restart")
|
||||
@limiter.limit("20/minute")
|
||||
@handle_exceptions
|
||||
async def restart_scheduler_service(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(require_admin)
|
||||
):
|
||||
"""Restart the scheduler service. Requires admin privileges."""
|
||||
result = subprocess.run(
|
||||
['sudo', 'systemctl', 'restart', SCHEDULER_SERVICE],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise ServiceError(
|
||||
f"Failed to restart service: {result.stderr}",
|
||||
{"service": SCHEDULER_SERVICE}
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Scheduler service restarted"}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DEPENDENCY MANAGEMENT ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/dependencies/status")
|
||||
@limiter.limit("100/minute")
|
||||
@handle_exceptions
|
||||
async def get_dependencies_status(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get dependency update status."""
|
||||
from modules.dependency_updater import DependencyUpdater
|
||||
updater = DependencyUpdater(scheduler_mode=False)
|
||||
status = updater.get_update_status()
|
||||
return status
|
||||
|
||||
|
||||
@router.post("/dependencies/check")
|
||||
@limiter.limit("20/minute")
|
||||
@handle_exceptions
|
||||
async def check_dependencies(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Force check and update all dependencies."""
|
||||
app_state = get_app_state()
|
||||
|
||||
from modules.dependency_updater import DependencyUpdater
|
||||
from modules.pushover_notifier import create_notifier_from_config
|
||||
|
||||
# Get pushover config
|
||||
pushover = None
|
||||
config = app_state.settings.get_all()
|
||||
if config.get('pushover', {}).get('enabled'):
|
||||
pushover = create_notifier_from_config(config, unified_db=app_state.db)
|
||||
|
||||
updater = DependencyUpdater(
|
||||
config=config.get('dependency_updater', {}),
|
||||
pushover_notifier=pushover,
|
||||
scheduler_mode=True
|
||||
)
|
||||
|
||||
results = updater.force_update_check()
|
||||
return {
|
||||
"success": True,
|
||||
"results": results,
|
||||
"message": "Dependency check completed"
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CACHE BUILDER SERVICE ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.post("/cache-builder/trigger")
|
||||
@limiter.limit("10/minute")
|
||||
@handle_exceptions
|
||||
async def trigger_cache_builder(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Manually trigger the thumbnail cache builder service."""
|
||||
result = subprocess.run(
|
||||
['sudo', 'systemctl', 'start', CACHE_BUILDER_SERVICE],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return {"success": True, "message": "Cache builder started successfully"}
|
||||
else:
|
||||
raise ServiceError(
|
||||
f"Failed to start cache builder: {result.stderr}",
|
||||
{"service": CACHE_BUILDER_SERVICE}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/cache-builder/status")
|
||||
@limiter.limit("30/minute")
|
||||
@handle_exceptions
|
||||
async def get_cache_builder_status(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get detailed cache builder service status."""
|
||||
# Get service status
|
||||
result = subprocess.run(
|
||||
['systemctl', 'status', CACHE_BUILDER_SERVICE, '--no-pager'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
status_output = result.stdout
|
||||
|
||||
# Parse status
|
||||
is_running = 'Active: active (running)' in status_output
|
||||
is_inactive = 'Active: inactive' in status_output
|
||||
last_run = None
|
||||
next_run = None
|
||||
|
||||
# Try to get timer info
|
||||
timer_result = subprocess.run(
|
||||
['systemctl', 'list-timers', '--no-pager', '--all'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if CACHE_BUILDER_SERVICE.replace('.service', '') in timer_result.stdout:
|
||||
for line in timer_result.stdout.split('\n'):
|
||||
if CACHE_BUILDER_SERVICE.replace('.service', '') in line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
# Extract timing info if available
|
||||
pass
|
||||
|
||||
return {
|
||||
"running": is_running,
|
||||
"inactive": is_inactive,
|
||||
"status_output": status_output[:500], # Truncate for brevity
|
||||
"last_run": last_run,
|
||||
"next_run": next_run
|
||||
}
|
||||
Reference in New Issue
Block a user