823 lines
27 KiB
Plaintext
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") |