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//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//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//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//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/', 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