Aniworld/src/server/app.py.backup

823 lines
27 KiB
Plaintext

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")