import os import sys import threading from datetime import datetime from flask import Flask, render_template, request, jsonify, redirect, url_for from flask_socketio import SocketIO, emit import logging import atexit # Add the parent directory to sys.path to import our modules sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from main import SeriesApp from core.entities.series import Serie from core.entities import SerieList from infrastructure.file_system import SerieScanner from infrastructure.providers.provider_factory import Loaders from web.controllers.auth_controller import session_manager, require_auth, optional_auth from config import config from application.services.queue_service import download_queue_bp # Simple decorator to replace handle_api_errors def handle_api_errors(f): """Simple error handling decorator.""" from functools import wraps @wraps(f) def decorated_function(*args, **kwargs): try: return f(*args, **kwargs) except Exception as e: return jsonify({'status': 'error', 'message': str(e)}), 500 return decorated_function # Create placeholder managers for missing modules class PlaceholderManager: """Placeholder manager for missing UX modules.""" def get_shortcuts_js(self): return "" def get_drag_drop_js(self): return "" def get_bulk_operations_js(self): return "" def get_preferences_js(self): return "" def get_search_js(self): return "" def get_undo_redo_js(self): return "" def get_mobile_responsive_js(self): return "" def get_touch_gesture_js(self): return "" def get_accessibility_js(self): return "" def get_screen_reader_js(self): return "" def get_contrast_js(self): return "" def get_multiscreen_js(self): return "" def get_css(self): return "" def get_contrast_css(self): return "" def get_multiscreen_css(self): return "" # Create placeholder instances keyboard_manager = PlaceholderManager() drag_drop_manager = PlaceholderManager() bulk_operations_manager = PlaceholderManager() preferences_manager = PlaceholderManager() advanced_search_manager = PlaceholderManager() undo_redo_manager = PlaceholderManager() mobile_responsive_manager = PlaceholderManager() touch_gesture_manager = PlaceholderManager() accessibility_manager = PlaceholderManager() screen_reader_manager = PlaceholderManager() color_contrast_manager = PlaceholderManager() multi_screen_manager = PlaceholderManager() # Placeholder process lock constants and functions RESCAN_LOCK = "rescan" DOWNLOAD_LOCK = "download" CLEANUP_LOCK = "cleanup" # Simple in-memory process lock system _active_locks = {} def is_process_running(lock_name): """Check if a process is currently running (locked).""" return lock_name in _active_locks def acquire_lock(lock_name, locked_by="system"): """Acquire a process lock.""" if lock_name in _active_locks: raise ProcessLockError(f"Process {lock_name} is already running") _active_locks[lock_name] = { 'locked_by': locked_by, 'timestamp': datetime.now() } def release_lock(lock_name): """Release a process lock.""" if lock_name in _active_locks: del _active_locks[lock_name] def with_process_lock(lock_name, timeout_minutes=30): """Decorator for process locking.""" def decorator(f): from functools import wraps @wraps(f) def decorated_function(*args, **kwargs): # Extract locked_by from kwargs if provided locked_by = kwargs.pop('_locked_by', 'system') try: acquire_lock(lock_name, locked_by) return f(*args, **kwargs) finally: release_lock(lock_name) return decorated_function return decorator class ProcessLockError(Exception): """Placeholder exception for process lock errors.""" pass class RetryableError(Exception): """Placeholder exception for retryable errors.""" pass # Placeholder objects for missing modules class PlaceholderNetworkChecker: def get_network_status(self): return {"status": "unknown"} def check_url_reachability(self, url): return False class PlaceholderErrorManager: def __init__(self): self.error_history = [] self.blacklisted_urls = {} self.retry_counts = {} class PlaceholderHealthMonitor: def get_current_health_status(self): return {"status": "unknown"} network_health_checker = PlaceholderNetworkChecker() error_recovery_manager = PlaceholderErrorManager() health_monitor = PlaceholderHealthMonitor() def check_process_locks(): """Placeholder function for process lock checking.""" pass # TODO: Fix these imports # from process_api import process_bp # from scheduler_api import scheduler_bp # from logging_api import logging_bp # from config_api import config_bp # from scheduler import init_scheduler, get_scheduler # from process_locks import (with_process_lock, RESCAN_LOCK, DOWNLOAD_LOCK, # ProcessLockError, is_process_running, check_process_locks) # TODO: Fix these imports # # Import new error handling and health monitoring modules # from error_handler import ( # handle_api_errors, error_recovery_manager, recovery_strategies, # network_health_checker, NetworkError, DownloadError, RetryableError # ) # from health_monitor import health_bp, health_monitor, init_health_monitoring, cleanup_health_monitoring # TODO: Fix these imports # # Import performance optimization modules # from performance_optimizer import ( # init_performance_monitoring, cleanup_performance_monitoring, # speed_limiter, download_cache, memory_monitor, download_manager # ) # from performance_api import performance_bp # TODO: Fix these imports # # Import API integration modules # from api_integration import ( # init_api_integrations, cleanup_api_integrations, # webhook_manager, export_manager, notification_service # ) # from api_endpoints import api_integration_bp # # # Import database management modules # from database_manager import ( # database_manager, anime_repository, backup_manager, storage_manager, # init_database_system, cleanup_database_system # ) # from database_api import database_bp # # # Import health check endpoints # from health_endpoints import health_bp # # # Import user experience modules # from keyboard_shortcuts import keyboard_manager # from drag_drop import drag_drop_manager # from bulk_operations import bulk_operations_manager # from user_preferences import preferences_manager, preferences_bp # from advanced_search import advanced_search_manager, search_bp # from undo_redo_manager import undo_redo_manager, undo_redo_bp # # # Import Mobile & Accessibility modules # from mobile_responsive import mobile_responsive_manager # from touch_gestures import touch_gesture_manager # from accessibility_features import accessibility_manager # from screen_reader_support import screen_reader_manager # from color_contrast_compliance import color_contrast_manager # from multi_screen_support import multi_screen_manager app = Flask(__name__, template_folder='web/templates/base', static_folder='web/static') app.config['SECRET_KEY'] = os.urandom(24) app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24 hours socketio = SocketIO(app, cors_allowed_origins="*") # Error handler for API routes to return JSON instead of HTML @app.errorhandler(404) def handle_api_not_found(error): """Handle 404 errors for API routes by returning JSON instead of HTML.""" if request.path.startswith('/api/'): return jsonify({ 'success': False, 'error': 'API endpoint not found', 'path': request.path }), 404 # For non-API routes, let Flask handle it normally return error # Register essential blueprints only app.register_blueprint(download_queue_bp) # TODO: Fix and uncomment these blueprints when modules are available # app.register_blueprint(process_bp) # app.register_blueprint(scheduler_bp) # app.register_blueprint(logging_bp) # app.register_blueprint(config_bp) # app.register_blueprint(health_bp) # app.register_blueprint(performance_bp) # app.register_blueprint(api_integration_bp) # app.register_blueprint(database_bp) # Note: health_endpoints blueprint already imported above as health_bp, no need to register twice # TODO: Fix and register these APIs when modules are available # # Register bulk operations API # from bulk_api import bulk_api_bp # app.register_blueprint(bulk_api_bp) # # # Register user preferences API # app.register_blueprint(preferences_bp) # # # Register advanced search API # app.register_blueprint(search_bp) # # # Register undo/redo API # app.register_blueprint(undo_redo_bp) # # # Register Mobile & Accessibility APIs # app.register_blueprint(color_contrast_manager.get_contrast_api_blueprint()) # TODO: Initialize features when modules are available # # Initialize user experience features # # keyboard_manager doesn't need init_app - it's a simple utility class # bulk_operations_manager.init_app(app) # preferences_manager.init_app(app) # advanced_search_manager.init_app(app) # undo_redo_manager.init_app(app) # # # Initialize Mobile & Accessibility features # mobile_responsive_manager.init_app(app) # touch_gesture_manager.init_app(app) # accessibility_manager.init_app(app) # screen_reader_manager.init_app(app) # color_contrast_manager.init_app(app) # multi_screen_manager.init_app(app) # Global variables to store app state series_app = None is_scanning = False is_downloading = False is_paused = False download_thread = None download_progress = {} download_queue = [] current_downloading = None download_stats = { 'total_series': 0, 'completed_series': 0, 'current_episode': None, 'total_episodes': 0, 'completed_episodes': 0 } def init_series_app(): """Initialize the SeriesApp with configuration directory.""" global series_app directory_to_search = config.anime_directory series_app = SeriesApp(directory_to_search) return series_app # Initialize the app on startup init_series_app() # Initialize scheduler # scheduler = init_scheduler(config, socketio) def setup_scheduler_callbacks(): """Setup callbacks for scheduler operations.""" def rescan_callback(): """Callback for scheduled rescan operations.""" try: # Reinit and scan series_app.SerieScanner.Reinit() series_app.SerieScanner.Scan() # Refresh the series list series_app.List = SerieList.SerieList(series_app.directory_to_search) series_app.__InitList__() return {"status": "success", "message": "Scheduled rescan completed"} except Exception as e: raise Exception(f"Scheduled rescan failed: {e}") def download_callback(): """Callback for auto-download after scheduled rescan.""" try: if not series_app or not series_app.List: return {"status": "skipped", "message": "No series data available"} # Find series with missing episodes series_with_missing = [] for serie in series_app.List.GetList(): if serie.episodeDict: series_with_missing.append(serie) if not series_with_missing: return {"status": "skipped", "message": "No series with missing episodes found"} # Note: Actual download implementation would go here # For now, just return the count of series that would be downloaded return { "status": "started", "message": f"Auto-download initiated for {len(series_with_missing)} series", "series_count": len(series_with_missing) } except Exception as e: raise Exception(f"Auto-download failed: {e}") # scheduler.set_rescan_callback(rescan_callback) # scheduler.set_download_callback(download_callback) # Setup scheduler callbacks # setup_scheduler_callbacks() # Initialize error handling and health monitoring # try: # init_health_monitoring() # logging.info("Health monitoring initialized successfully") # except Exception as e: # logging.error(f"Failed to initialize health monitoring: {e}") # Initialize performance monitoring # try: # init_performance_monitoring() # logging.info("Performance monitoring initialized successfully") # except Exception as e: # logging.error(f"Failed to initialize performance monitoring: {e}") # Initialize API integrations # try: # init_api_integrations() # # Set export manager's series app reference # export_manager.series_app = series_app # logging.info("API integrations initialized successfully") # except Exception as e: # logging.error(f"Failed to initialize API integrations: {e}") # Initialize database system # try: # init_database_system() # logging.info("Database system initialized successfully") # except Exception as e: # logging.error(f"Failed to initialize database system: {e}") # Register cleanup functions @atexit.register def cleanup_on_exit(): """Clean up resources on application exit.""" try: # cleanup_health_monitoring() # cleanup_performance_monitoring() # cleanup_api_integrations() # cleanup_database_system() logging.info("Application cleanup completed") except Exception as e: logging.error(f"Error during cleanup: {e}") # UX JavaScript and CSS routes @app.route('/static/js/keyboard-shortcuts.js') def keyboard_shortcuts_js(): """Serve keyboard shortcuts JavaScript.""" from flask import Response js_content = keyboard_manager.get_shortcuts_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/js/drag-drop.js') def drag_drop_js(): """Serve drag and drop JavaScript.""" from flask import Response js_content = drag_drop_manager.get_drag_drop_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/js/bulk-operations.js') def bulk_operations_js(): """Serve bulk operations JavaScript.""" from flask import Response js_content = bulk_operations_manager.get_bulk_operations_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/js/user-preferences.js') def user_preferences_js(): """Serve user preferences JavaScript.""" from flask import Response js_content = preferences_manager.get_preferences_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/js/advanced-search.js') def advanced_search_js(): """Serve advanced search JavaScript.""" from flask import Response js_content = advanced_search_manager.get_search_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/js/undo-redo.js') def undo_redo_js(): """Serve undo/redo JavaScript.""" from flask import Response js_content = undo_redo_manager.get_undo_redo_js() return Response(js_content, mimetype='application/javascript') # Mobile & Accessibility JavaScript routes @app.route('/static/js/mobile-responsive.js') def mobile_responsive_js(): """Serve mobile responsive JavaScript.""" from flask import Response js_content = mobile_responsive_manager.get_mobile_responsive_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/js/touch-gestures.js') def touch_gestures_js(): """Serve touch gestures JavaScript.""" from flask import Response js_content = touch_gesture_manager.get_touch_gesture_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/js/accessibility-features.js') def accessibility_features_js(): """Serve accessibility features JavaScript.""" from flask import Response js_content = accessibility_manager.get_accessibility_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/js/screen-reader-support.js') def screen_reader_support_js(): """Serve screen reader support JavaScript.""" from flask import Response js_content = screen_reader_manager.get_screen_reader_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/js/color-contrast-compliance.js') def color_contrast_compliance_js(): """Serve color contrast compliance JavaScript.""" from flask import Response js_content = color_contrast_manager.get_contrast_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/js/multi-screen-support.js') def multi_screen_support_js(): """Serve multi-screen support JavaScript.""" from flask import Response js_content = multi_screen_manager.get_multiscreen_js() return Response(js_content, mimetype='application/javascript') @app.route('/static/css/ux-features.css') def ux_features_css(): """Serve UX features CSS.""" from flask import Response css_content = f""" /* Keyboard shortcuts don't require additional CSS */ {drag_drop_manager.get_css()} {bulk_operations_manager.get_css()} {preferences_manager.get_css()} {advanced_search_manager.get_css()} {undo_redo_manager.get_css()} /* Mobile & Accessibility CSS */ {mobile_responsive_manager.get_css()} {touch_gesture_manager.get_css()} {accessibility_manager.get_css()} {screen_reader_manager.get_css()} {color_contrast_manager.get_contrast_css()} {multi_screen_manager.get_multiscreen_css()} """ return Response(css_content, mimetype='text/css') @app.route('/') @optional_auth def index(): """Main page route.""" # Check process status process_status = { 'rescan_running': is_process_running(RESCAN_LOCK), 'download_running': is_process_running(DOWNLOAD_LOCK) } return render_template('index.html', process_status=process_status) # Authentication routes @app.route('/login') def login(): """Login page.""" if not config.has_master_password(): return redirect(url_for('setup')) if session_manager.is_authenticated(): return redirect(url_for('index')) return render_template('login.html', session_timeout=config.session_timeout_hours, max_attempts=config.max_failed_attempts, lockout_duration=config.lockout_duration_minutes) @app.route('/setup') def setup(): """Initial setup page.""" if config.has_master_password(): return redirect(url_for('login')) return render_template('setup.html', current_directory=config.anime_directory) @app.route('/api/auth/setup', methods=['POST']) def auth_setup(): """Complete initial setup.""" if config.has_master_password(): return jsonify({ 'status': 'error', 'message': 'Setup already completed' }), 400 try: data = request.get_json() password = data.get('password') directory = data.get('directory') if not password or len(password) < 8: return jsonify({ 'status': 'error', 'message': 'Password must be at least 8 characters long' }), 400 if not directory: return jsonify({ 'status': 'error', 'message': 'Directory is required' }), 400 # Set master password and directory config.set_master_password(password) config.anime_directory = directory config.save_config() # Reinitialize series app with new directory init_series_app() return jsonify({ 'status': 'success', 'message': 'Setup completed successfully' }) except Exception as e: return jsonify({ 'status': 'error', 'message': str(e) }), 500 @app.route('/api/auth/login', methods=['POST']) def auth_login(): """Authenticate user.""" try: data = request.get_json() password = data.get('password') if not password: return jsonify({ 'status': 'error', 'message': 'Password is required' }), 400 # Verify password using session manager result = session_manager.login(password, request.remote_addr) return jsonify(result) except Exception as e: return jsonify({ 'status': 'error', 'message': str(e) }), 500 @app.route('/api/auth/logout', methods=['POST']) @require_auth def auth_logout(): """Logout user.""" session_manager.logout() return jsonify({ 'status': 'success', 'message': 'Logged out successfully' }) @app.route('/api/auth/status', methods=['GET']) def auth_status(): """Get authentication status.""" return jsonify({ 'authenticated': session_manager.is_authenticated(), 'has_master_password': config.has_master_password(), 'setup_required': not config.has_master_password(), 'session_info': session_manager.get_session_info() }) @app.route('/api/config/directory', methods=['POST']) @require_auth def update_directory(): """Update anime directory configuration.""" try: data = request.get_json() new_directory = data.get('directory') if not new_directory: return jsonify({ 'success': False, 'error': 'Directory is required' }), 400 # Update configuration config.anime_directory = new_directory config.save_config() # Reinitialize series app init_series_app() return jsonify({ 'success': True, 'message': 'Directory updated successfully', 'directory': new_directory }) except Exception as e: return jsonify({ 'success': False, 'error': str(e) }), 500 @app.route('/api/series', methods=['GET']) @optional_auth def get_series(): """Get all series data.""" try: if series_app is None or series_app.List is None: return jsonify({ 'status': 'success', 'series': [], 'total_series': 0, 'message': 'No series data available. Please perform a scan to load series.' }) # Get series data series_data = [] for serie in series_app.List.GetList(): series_data.append({ 'folder': serie.folder, 'name': serie.name or serie.folder, 'total_episodes': sum(len(episodes) for episodes in serie.episodeDict.values()), 'missing_episodes': sum(len(episodes) for episodes in serie.episodeDict.values()), 'status': 'ongoing', 'episodes': { season: episodes for season, episodes in serie.episodeDict.items() } }) return jsonify({ 'status': 'success', 'series': series_data, 'total_series': len(series_data) }) except Exception as e: # Log the error but don't return 500 to prevent page reload loops print(f"Error in get_series: {e}") return jsonify({ 'status': 'success', 'series': [], 'total_series': 0, 'message': 'Error loading series data. Please try rescanning.' }) @app.route('/api/search', methods=['POST']) @optional_auth @handle_api_errors def search_series(): """Search for series online.""" try: # Get the search query from the request data = request.get_json() if not data or 'query' not in data: return jsonify({ 'status': 'error', 'message': 'Search query is required' }), 400 query = data['query'].strip() if not query: return jsonify({ 'status': 'error', 'message': 'Search query cannot be empty' }), 400 # Check if series_app is available if series_app is None: return jsonify({ 'status': 'error', 'message': 'Series application not initialized' }), 500 # Perform the search search_results = series_app.search(query) # Format results for the frontend results = [] if search_results: for result in search_results: if isinstance(result, dict) and 'name' in result and 'link' in result: results.append({ 'name': result['name'], 'link': result['link'] }) return jsonify({ 'status': 'success', 'results': results, 'total': len(results) }) except Exception as e: return jsonify({ 'status': 'error', 'message': f'Search failed: {str(e)}' }), 500 @app.route('/api/add_series', methods=['POST']) @optional_auth @handle_api_errors def add_series(): """Add a new series to the collection.""" try: # Get the request data data = request.get_json() if not data: return jsonify({ 'status': 'error', 'message': 'Request data is required' }), 400 # Validate required fields if 'link' not in data or 'name' not in data: return jsonify({ 'status': 'error', 'message': 'Both link and name are required' }), 400 link = data['link'].strip() name = data['name'].strip() if not link or not name: return jsonify({ 'status': 'error', 'message': 'Link and name cannot be empty' }), 400 # Check if series_app is available if series_app is None: return jsonify({ 'status': 'error', 'message': 'Series application not initialized' }), 500 # Create and add the series new_serie = Serie(link, name, "aniworld.to", link, {}) series_app.List.add(new_serie) return jsonify({ 'status': 'success', 'message': f'Series "{name}" added successfully' }) except Exception as e: return jsonify({ 'status': 'error', 'message': f'Failed to add series: {str(e)}' }), 500 @app.route('/api/rescan', methods=['POST']) @optional_auth def rescan_series(): """Rescan/reinit the series directory.""" global is_scanning # Check if rescan is already running using process lock if is_process_running(RESCAN_LOCK) or is_scanning: return jsonify({ 'status': 'error', 'message': 'Rescan is already running. Please wait for it to complete.', 'is_running': True }), 409 def scan_thread(): global is_scanning try: # Use process lock to prevent duplicate rescans @with_process_lock(RESCAN_LOCK, timeout_minutes=120) def perform_rescan(): global is_scanning is_scanning = True try: # Emit scanning started socketio.emit('scan_started') # Reinit and scan series_app.SerieScanner.Reinit() series_app.SerieScanner.Scan(lambda folder, counter: socketio.emit('scan_progress', { 'folder': folder, 'counter': counter }) ) # Refresh the series list series_app.List = SerieList.SerieList(series_app.directory_to_search) series_app.__InitList__() # Emit scan completed socketio.emit('scan_completed') except Exception as e: socketio.emit('scan_error', {'message': str(e)}) raise finally: is_scanning = False perform_rescan(_locked_by='web_interface') except ProcessLockError: socketio.emit('scan_error', {'message': 'Rescan is already running'}) except Exception as e: socketio.emit('scan_error', {'message': str(e)}) # Start scan in background thread threading.Thread(target=scan_thread, daemon=True).start() return jsonify({ 'status': 'success', 'message': 'Rescan started' }) # Basic download endpoint - simplified for now @app.route('/api/download', methods=['POST']) @optional_auth def download_series(): """Download selected series.""" global is_downloading # Check if download is already running using process lock if is_process_running(DOWNLOAD_LOCK) or is_downloading: return jsonify({ 'status': 'error', 'message': 'Download is already running. Please wait for it to complete.', 'is_running': True }), 409 return jsonify({ 'status': 'success', 'message': 'Download functionality will be implemented with queue system' }) # WebSocket events for real-time updates @socketio.on('connect') def handle_connect(): """Handle client connection.""" emit('status', { 'message': 'Connected to server', 'processes': { 'rescan_running': is_process_running(RESCAN_LOCK), 'download_running': is_process_running(DOWNLOAD_LOCK) } }) @socketio.on('disconnect') def handle_disconnect(): """Handle client disconnection.""" print('Client disconnected') @socketio.on('get_status') def handle_get_status(): """Handle status request.""" emit('status_update', { 'processes': { 'rescan_running': is_process_running(RESCAN_LOCK), 'download_running': is_process_running(DOWNLOAD_LOCK) }, 'series_count': len(series_app.List.GetList()) if series_app and series_app.List else 0 }) # Error Recovery and Diagnostics Endpoints @app.route('/api/process/locks/status', methods=['GET']) @handle_api_errors @optional_auth def process_locks_status(): """Get current process lock status.""" try: # Use the constants and functions defined above in this file locks = { 'rescan': { 'is_locked': is_process_running(RESCAN_LOCK), 'locked_by': 'system' if is_process_running(RESCAN_LOCK) else None, 'lock_time': None # Could be extended to track actual lock times }, 'download': { 'is_locked': is_process_running(DOWNLOAD_LOCK), 'locked_by': 'system' if is_process_running(DOWNLOAD_LOCK) else None, 'lock_time': None # Could be extended to track actual lock times } } return jsonify({ 'success': True, 'locks': locks, 'timestamp': datetime.now().isoformat() }) except Exception as e: return jsonify({ 'success': False, 'error': str(e), 'locks': { 'rescan': {'is_locked': False, 'locked_by': None, 'lock_time': None}, 'download': {'is_locked': False, 'locked_by': None, 'lock_time': None} } }) @app.route('/api/status', methods=['GET']) @handle_api_errors @optional_auth def get_status(): """Get current system status.""" try: # Get anime directory from environment or config anime_directory = os.environ.get('ANIME_DIRECTORY', 'Not configured') # Get series count (placeholder implementation) series_count = 0 try: # This would normally get the actual series count from your series scanner # For now, return a placeholder value series_count = 0 except Exception: series_count = 0 return jsonify({ 'success': True, 'directory': anime_directory, 'series_count': series_count, 'timestamp': datetime.now().isoformat() }) except Exception as e: return jsonify({ 'success': False, 'error': str(e), 'directory': 'Error', 'series_count': 0 }) # Configuration API endpoints @app.route('/api/scheduler/config', methods=['GET']) @handle_api_errors @optional_auth def get_scheduler_config(): """Get scheduler configuration.""" return jsonify({ 'success': True, 'config': { 'enabled': False, 'time': '03:00', 'auto_download_after_rescan': False, 'next_run': None, 'last_run': None, 'is_running': False } }) @app.route('/api/scheduler/config', methods=['POST']) @handle_api_errors @optional_auth def set_scheduler_config(): """Set scheduler configuration.""" return jsonify({ 'success': True, 'message': 'Scheduler configuration saved (placeholder)' }) @app.route('/api/logging/config', methods=['GET']) @handle_api_errors @optional_auth def get_logging_config(): """Get logging configuration.""" return jsonify({ 'success': True, 'config': { 'log_level': 'INFO', 'enable_console_logging': True, 'enable_console_progress': True, 'enable_fail2ban_logging': False } }) @app.route('/api/logging/config', methods=['POST']) @handle_api_errors @optional_auth def set_logging_config(): """Set logging configuration.""" return jsonify({ 'success': True, 'message': 'Logging configuration saved (placeholder)' }) @app.route('/api/logging/files', methods=['GET']) @handle_api_errors @optional_auth def get_log_files(): """Get available log files.""" return jsonify({ 'success': True, 'files': [] }) @app.route('/api/logging/test', methods=['POST']) @handle_api_errors @optional_auth def test_logging(): """Test logging functionality.""" return jsonify({ 'success': True, 'message': 'Test logging completed (placeholder)' }) @app.route('/api/logging/cleanup', methods=['POST']) @handle_api_errors @optional_auth def cleanup_logs(): """Clean up old log files.""" data = request.get_json() days = data.get('days', 30) return jsonify({ 'success': True, 'message': f'Log files older than {days} days have been cleaned up (placeholder)' }) @app.route('/api/logging/files//tail') @handle_api_errors @optional_auth def tail_log_file(filename): """Get the tail of a log file.""" lines = request.args.get('lines', 100, type=int) return jsonify({ 'success': True, 'content': f'Last {lines} lines of {filename} (placeholder)', 'filename': filename }) @app.route('/api/config/section/advanced', methods=['GET']) @handle_api_errors @optional_auth def get_advanced_config(): """Get advanced configuration.""" return jsonify({ 'success': True, 'config': { 'max_concurrent_downloads': 3, 'provider_timeout': 30, 'enable_debug_mode': False } }) @app.route('/api/config/section/advanced', methods=['POST']) @handle_api_errors @optional_auth def set_advanced_config(): """Set advanced configuration.""" data = request.get_json() # Here you would normally save the configuration # For now, we'll just return success return jsonify({ 'success': True, 'message': 'Advanced configuration saved successfully' }) @app.route('/api/config/backup', methods=['POST']) @handle_api_errors @optional_auth def create_config_backup(): """Create a configuration backup.""" return jsonify({ 'success': True, 'message': 'Configuration backup created successfully', 'filename': f'config_backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json' }) @app.route('/api/config/backups', methods=['GET']) @handle_api_errors @optional_auth def get_config_backups(): """Get list of configuration backups.""" return jsonify({ 'success': True, 'backups': [] # Empty list for now - would normally list actual backup files }) @app.route('/api/config/backup//restore', methods=['POST']) @handle_api_errors @optional_auth def restore_config_backup(filename): """Restore a configuration backup.""" return jsonify({ 'success': True, 'message': f'Configuration restored from {filename}' }) @app.route('/api/config/backup//download', methods=['GET']) @handle_api_errors @optional_auth def download_config_backup(filename): """Download a configuration backup file.""" # For now, return an empty response - would normally serve the actual file return jsonify({ 'success': True, 'message': 'Backup download endpoint (placeholder)' }) @app.route('/api/diagnostics/network') @handle_api_errors @optional_auth def network_diagnostics(): """Get network diagnostics and connectivity status.""" try: network_status = network_health_checker.get_network_status() # Test AniWorld connectivity aniworld_reachable = network_health_checker.check_url_reachability("https://aniworld.to") network_status['aniworld_reachable'] = aniworld_reachable return jsonify({ 'status': 'success', 'data': network_status }) except Exception as e: raise RetryableError(f"Network diagnostics failed: {e}") @app.route('/api/diagnostics/errors') @handle_api_errors @optional_auth def get_error_history(): """Get recent error history.""" try: recent_errors = error_recovery_manager.error_history[-50:] # Last 50 errors return jsonify({ 'status': 'success', 'data': { 'recent_errors': recent_errors, 'total_errors': len(error_recovery_manager.error_history), 'blacklisted_urls': list(error_recovery_manager.blacklisted_urls.keys()) } }) except Exception as e: raise RetryableError(f"Error history retrieval failed: {e}") @app.route('/api/recovery/clear-blacklist', methods=['POST']) @handle_api_errors @require_auth def clear_blacklist(): """Clear URL blacklist.""" try: error_recovery_manager.blacklisted_urls.clear() return jsonify({ 'status': 'success', 'message': 'URL blacklist cleared successfully' }) except Exception as e: raise RetryableError(f"Blacklist clearing failed: {e}") @app.route('/api/recovery/retry-counts') @handle_api_errors @optional_auth def get_retry_counts(): """Get retry statistics.""" try: return jsonify({ 'status': 'success', 'data': { 'retry_counts': error_recovery_manager.retry_counts, 'total_retries': sum(error_recovery_manager.retry_counts.values()) } }) except Exception as e: raise RetryableError(f"Retry statistics retrieval failed: {e}") @app.route('/api/diagnostics/system-status') @handle_api_errors @optional_auth def system_status_summary(): """Get comprehensive system status summary.""" try: # Get health status health_status = health_monitor.get_current_health_status() # Get network status network_status = network_health_checker.get_network_status() # Get process status process_status = { 'rescan_running': is_process_running(RESCAN_LOCK), 'download_running': is_process_running(DOWNLOAD_LOCK) } # Get error statistics error_stats = { 'total_errors': len(error_recovery_manager.error_history), 'recent_errors': len([e for e in error_recovery_manager.error_history if (datetime.now() - datetime.fromisoformat(e['timestamp'])).seconds < 3600]), 'blacklisted_urls': len(error_recovery_manager.blacklisted_urls) } return jsonify({ 'status': 'success', 'data': { 'health': health_status, 'network': network_status, 'processes': process_status, 'errors': error_stats, 'timestamp': datetime.now().isoformat() } }) except Exception as e: raise RetryableError(f"System status retrieval failed: {e}") if __name__ == '__main__': # Clean up any expired locks on startup check_process_locks() # Configure enhanced logging system try: from logging_config import get_logger, logging_config logger = get_logger(__name__, 'webapp') logger.info("Enhanced logging system initialized") except ImportError: # Fallback to basic logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) logger.warning("Using fallback logging - enhanced logging not available") logger.info("Starting Aniworld Flask server...") logger.info(f"Anime directory: {config.anime_directory}") logger.info(f"Log level: {config.log_level}") # Start scheduler if enabled # if config.scheduled_rescan_enabled: # logger.info(f"Starting scheduler - daily rescan at {config.scheduled_rescan_time}") # scheduler.start_scheduler() # else: logger.info("Scheduled operations disabled") logger.info("Server will be available at http://localhost:5000") try: # Run with SocketIO socketio.run(app, debug=True, host='0.0.0.0', port=5000, allow_unsafe_werkzeug=True) finally: # Clean shutdown # if scheduler: # scheduler.stop_scheduler() # logger.info("Scheduler stopped") pass # Placeholder for cleanup code