280 lines
8.6 KiB
Python
280 lines
8.6 KiB
Python
from flask import Blueprint, jsonify, request
|
|
from web.controllers.auth_controller import require_auth
|
|
from shared.utils.process_utils import (
|
|
process_lock_manager,
|
|
RESCAN_LOCK,
|
|
DOWNLOAD_LOCK,
|
|
SEARCH_LOCK,
|
|
check_process_locks,
|
|
get_process_status,
|
|
update_process_progress,
|
|
is_process_running,
|
|
episode_deduplicator,
|
|
ProcessLockError
|
|
)
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
process_bp = Blueprint('process', __name__, url_prefix='/api/process')
|
|
|
|
@process_bp.route('/locks/status', methods=['GET'])
|
|
@require_auth
|
|
def get_all_locks_status():
|
|
"""Get status of all process locks."""
|
|
try:
|
|
# Clean up expired locks first
|
|
cleaned = check_process_locks()
|
|
if cleaned > 0:
|
|
logger.info(f"Cleaned up {cleaned} expired locks")
|
|
|
|
status = process_lock_manager.get_all_locks_status()
|
|
|
|
# Add queue deduplication info
|
|
status['queue_info'] = {
|
|
'active_episodes': episode_deduplicator.get_count(),
|
|
'episodes': episode_deduplicator.get_active_episodes()
|
|
}
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'locks': status
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error getting locks status: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
@process_bp.route('/locks/<lock_name>/status', methods=['GET'])
|
|
@require_auth
|
|
def get_lock_status(lock_name):
|
|
"""Get status of a specific process lock."""
|
|
try:
|
|
if lock_name not in [RESCAN_LOCK, DOWNLOAD_LOCK, SEARCH_LOCK]:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Invalid lock name'
|
|
}), 400
|
|
|
|
status = get_process_status(lock_name)
|
|
return jsonify({
|
|
'success': True,
|
|
'status': status
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error getting lock status for {lock_name}: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
@process_bp.route('/locks/<lock_name>/acquire', methods=['POST'])
|
|
@require_auth
|
|
def acquire_lock(lock_name):
|
|
"""Manually acquire a process lock."""
|
|
try:
|
|
if lock_name not in [RESCAN_LOCK, DOWNLOAD_LOCK, SEARCH_LOCK]:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Invalid lock name'
|
|
}), 400
|
|
|
|
data = request.get_json() or {}
|
|
locked_by = data.get('locked_by', 'manual')
|
|
timeout_minutes = data.get('timeout_minutes', 60)
|
|
|
|
success = process_lock_manager.acquire_lock(lock_name, locked_by, timeout_minutes)
|
|
|
|
if success:
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'Lock {lock_name} acquired successfully'
|
|
})
|
|
else:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Lock {lock_name} is already held'
|
|
}), 409
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error acquiring lock {lock_name}: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
@process_bp.route('/locks/<lock_name>/release', methods=['POST'])
|
|
@require_auth
|
|
def release_lock(lock_name):
|
|
"""Manually release a process lock."""
|
|
try:
|
|
if lock_name not in [RESCAN_LOCK, DOWNLOAD_LOCK, SEARCH_LOCK]:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Invalid lock name'
|
|
}), 400
|
|
|
|
success = process_lock_manager.release_lock(lock_name)
|
|
|
|
if success:
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'Lock {lock_name} released successfully'
|
|
})
|
|
else:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Lock {lock_name} was not held'
|
|
}), 404
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error releasing lock {lock_name}: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
@process_bp.route('/locks/cleanup', methods=['POST'])
|
|
@require_auth
|
|
def cleanup_expired_locks():
|
|
"""Manually clean up expired locks."""
|
|
try:
|
|
cleaned = check_process_locks()
|
|
return jsonify({
|
|
'success': True,
|
|
'cleaned_count': cleaned,
|
|
'message': f'Cleaned up {cleaned} expired locks'
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error cleaning up locks: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
@process_bp.route('/locks/force-release-all', methods=['POST'])
|
|
@require_auth
|
|
def force_release_all_locks():
|
|
"""Force release all process locks (emergency use)."""
|
|
try:
|
|
data = request.get_json() or {}
|
|
confirm = data.get('confirm', False)
|
|
|
|
if not confirm:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Confirmation required for force release'
|
|
}), 400
|
|
|
|
released = process_lock_manager.force_release_all()
|
|
|
|
# Also clear queue deduplication
|
|
episode_deduplicator.clear_all()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'released_count': released,
|
|
'message': f'Force released {released} locks and cleared queue deduplication'
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error force releasing locks: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
@process_bp.route('/locks/<lock_name>/progress', methods=['POST'])
|
|
@require_auth
|
|
def update_lock_progress(lock_name):
|
|
"""Update progress for a running process."""
|
|
try:
|
|
if lock_name not in [RESCAN_LOCK, DOWNLOAD_LOCK, SEARCH_LOCK]:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Invalid lock name'
|
|
}), 400
|
|
|
|
if not is_process_running(lock_name):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Process {lock_name} is not running'
|
|
}), 404
|
|
|
|
data = request.get_json() or {}
|
|
progress_data = data.get('progress', {})
|
|
|
|
update_process_progress(lock_name, progress_data)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Progress updated successfully'
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error updating progress for {lock_name}: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
@process_bp.route('/queue/deduplication', methods=['GET'])
|
|
@require_auth
|
|
def get_queue_deduplication():
|
|
"""Get current queue deduplication status."""
|
|
try:
|
|
return jsonify({
|
|
'success': True,
|
|
'deduplication': {
|
|
'active_count': episode_deduplicator.get_count(),
|
|
'active_episodes': episode_deduplicator.get_active_episodes()
|
|
}
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error getting queue deduplication: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
@process_bp.route('/queue/deduplication/clear', methods=['POST'])
|
|
@require_auth
|
|
def clear_queue_deduplication():
|
|
"""Clear all queue deduplication entries."""
|
|
try:
|
|
episode_deduplicator.clear_all()
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Queue deduplication cleared successfully'
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error clearing queue deduplication: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
@process_bp.route('/is-running/<process_name>', methods=['GET'])
|
|
@require_auth
|
|
def check_if_process_running(process_name):
|
|
"""Quick check if a specific process is running."""
|
|
try:
|
|
if process_name not in [RESCAN_LOCK, DOWNLOAD_LOCK, SEARCH_LOCK]:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Invalid process name'
|
|
}), 400
|
|
|
|
is_running = is_process_running(process_name)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'is_running': is_running,
|
|
'process_name': process_name
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error checking if process {process_name} is running: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500 |