1302 lines
42 KiB
Python

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/<filename>/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/<filename>/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/<filename>/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