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 # 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 # 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 # 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__) app.config['SECRET_KEY'] = os.urandom(24) app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24 hours socketio = SocketIO(app, cors_allowed_origins="*") # Register blueprints app.register_blueprint(download_queue_bp) 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 # 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()) # 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({ 'status': 'error', 'message': 'Directory is required' }), 400 # Update configuration config.anime_directory = new_directory config.save_config() # Reinitialize series app init_series_app() return jsonify({ 'status': 'success', 'message': 'Directory updated successfully', 'directory': new_directory }) except Exception as e: return jsonify({ 'status': 'error', 'message': 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/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/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")