backup
This commit is contained in:
@@ -30,11 +30,12 @@ class AccessibilityManager:
|
||||
|
||||
def get_accessibility_js(self):
|
||||
"""Generate JavaScript code for accessibility features."""
|
||||
import json
|
||||
return f"""
|
||||
// AniWorld Accessibility Manager
|
||||
class AccessibilityManager {{
|
||||
constructor() {{
|
||||
this.config = {self.accessibility_config};
|
||||
this.config = {json.dumps(self.accessibility_config)};
|
||||
this.announcements = [];
|
||||
this.focusHistory = [];
|
||||
this.currentFocus = null;
|
||||
|
||||
@@ -249,7 +249,7 @@ def cleanup_on_exit():
|
||||
def keyboard_shortcuts_js():
|
||||
"""Serve keyboard shortcuts JavaScript."""
|
||||
from flask import Response
|
||||
js_content = keyboard_manager.get_keyboard_shortcuts_js()
|
||||
js_content = keyboard_manager.get_shortcuts_js()
|
||||
return Response(js_content, mimetype='application/javascript')
|
||||
|
||||
@app.route('/static/js/drag-drop.js')
|
||||
@@ -335,7 +335,7 @@ def ux_features_css():
|
||||
"""Serve UX features CSS."""
|
||||
from flask import Response
|
||||
css_content = f"""
|
||||
{keyboard_manager.get_css()}
|
||||
/* Keyboard shortcuts don't require additional CSS */
|
||||
|
||||
{drag_drop_manager.get_css()}
|
||||
|
||||
@@ -480,6 +480,7 @@ 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()
|
||||
})
|
||||
@@ -524,9 +525,11 @@ def get_series():
|
||||
try:
|
||||
if series_app is None or series_app.List is None:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Series data not initialized. Please scan first.'
|
||||
}), 400
|
||||
'status': 'success',
|
||||
'series': [],
|
||||
'total_series': 0,
|
||||
'message': 'No series data available. Please perform a scan to load series.'
|
||||
})
|
||||
|
||||
# Get series data
|
||||
series_data = []
|
||||
@@ -550,10 +553,14 @@ def get_series():
|
||||
})
|
||||
|
||||
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': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
'status': 'success',
|
||||
'series': [],
|
||||
'total_series': 0,
|
||||
'message': 'Error loading series data. Please try rescanning.'
|
||||
})
|
||||
|
||||
@app.route('/api/rescan', methods=['POST'])
|
||||
@optional_auth
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
||||
from flask_socketio import SocketIO, emit
|
||||
import logging
|
||||
|
||||
# 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 Serie import Serie
|
||||
import SerieList
|
||||
import SerieScanner
|
||||
from Loaders.Loaders import Loaders
|
||||
from auth import session_manager, require_auth, optional_auth
|
||||
from config import config
|
||||
from download_queue import download_queue_bp
|
||||
from process_api import process_bp
|
||||
from process_locks import (with_process_lock, RESCAN_LOCK, DOWNLOAD_LOCK,
|
||||
ProcessLockError, is_process_running, check_process_locks)
|
||||
|
||||
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)
|
||||
|
||||
# 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()
|
||||
|
||||
@app.route('/')
|
||||
@optional_auth
|
||||
def index():
|
||||
"""Main page route."""
|
||||
return render_template('index.html')
|
||||
|
||||
# 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
|
||||
if not config.set_master_password(password):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Failed to set master password'
|
||||
}), 500
|
||||
|
||||
# Update directory
|
||||
config.set('anime.directory', directory)
|
||||
|
||||
# Reinitialize series app with new directory
|
||||
global series_app
|
||||
series_app = SeriesApp(directory)
|
||||
|
||||
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
|
||||
|
||||
success, message, token = session_manager.authenticate(password)
|
||||
|
||||
if success:
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': message,
|
||||
'token': token
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': message
|
||||
}), 401
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Authentication error'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/auth/logout', methods=['POST'])
|
||||
def auth_logout():
|
||||
"""Logout user."""
|
||||
session_manager.logout()
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Logged out successfully'
|
||||
})
|
||||
|
||||
@app.route('/api/auth/status')
|
||||
def auth_status():
|
||||
"""Get authentication status."""
|
||||
return jsonify({
|
||||
'authenticated': session_manager.is_authenticated(),
|
||||
'has_master_password': config.has_master_password(),
|
||||
'session_info': session_manager.get_session_info()
|
||||
})
|
||||
|
||||
@app.route('/api/series')
|
||||
@optional_auth
|
||||
def get_series():
|
||||
"""Get all series with missing episodes."""
|
||||
try:
|
||||
series_list = series_app.series_list
|
||||
series_data = []
|
||||
|
||||
for serie in series_list:
|
||||
missing_count = sum(len(episodes) for episodes in serie.episodeDict.values())
|
||||
series_data.append({
|
||||
'folder': serie.folder,
|
||||
'name': serie.name or serie.folder,
|
||||
'key': serie.key,
|
||||
'site': serie.site,
|
||||
'missing_episodes': missing_count,
|
||||
'episode_dict': serie.episodeDict
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'series': series_data
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/search', methods=['POST'])
|
||||
@optional_auth
|
||||
def search_anime():
|
||||
"""Search for anime using the loader."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
search_term = data.get('query', '').strip()
|
||||
|
||||
if not search_term:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Search term is required'
|
||||
}), 400
|
||||
|
||||
results = series_app.search(search_term)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'results': results
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/add_series', methods=['POST'])
|
||||
@optional_auth
|
||||
def add_series():
|
||||
"""Add a series from search results to the global list."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
link = data.get('link')
|
||||
name = data.get('name')
|
||||
|
||||
if not link or not name:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Link and name are required'
|
||||
}), 400
|
||||
|
||||
# Create new serie and add it
|
||||
new_serie = Serie(link, name, "aniworld.to", link, {})
|
||||
series_app.List.add(new_serie)
|
||||
|
||||
# Refresh the series list
|
||||
series_app.__InitList__()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': f'Added series: {name}'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 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():
|
||||
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'
|
||||
})
|
||||
|
||||
@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
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
selected_folders = data.get('folders', [])
|
||||
|
||||
if not selected_folders:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'No series selected'
|
||||
}), 400
|
||||
|
||||
# Find selected series
|
||||
selected_series = []
|
||||
for serie in series_app.series_list:
|
||||
if serie.folder in selected_folders:
|
||||
selected_series.append(serie)
|
||||
|
||||
if not selected_series:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Selected series not found'
|
||||
}), 400
|
||||
|
||||
def download_thread_func():
|
||||
global is_downloading, is_paused, download_thread, download_queue, current_downloading, download_stats
|
||||
|
||||
try:
|
||||
# Use process lock to prevent duplicate downloads
|
||||
@with_process_lock(DOWNLOAD_LOCK, timeout_minutes=300)
|
||||
def perform_download():
|
||||
is_downloading = True
|
||||
is_paused = False
|
||||
|
||||
# Initialize download queue and stats
|
||||
download_queue = selected_series.copy()
|
||||
download_stats = {
|
||||
'total_series': len(selected_series),
|
||||
'completed_series': 0,
|
||||
'current_episode': None,
|
||||
'total_episodes': sum(sum(len(episodes) for episodes in serie.episodeDict.values()) for serie in selected_series),
|
||||
'completed_episodes': 0
|
||||
}
|
||||
|
||||
# Emit download started
|
||||
socketio.emit('download_started', {
|
||||
'total_series': len(selected_series),
|
||||
'queue': [{'folder': s.folder, 'name': s.name or s.folder} for s in selected_series]
|
||||
})
|
||||
|
||||
perform_download(_locked_by='web_interface')
|
||||
|
||||
except ProcessLockError:
|
||||
socketio.emit('download_error', {'message': 'Download is already running'})
|
||||
except Exception as e:
|
||||
socketio.emit('download_error', {'message': str(e)})
|
||||
|
||||
def download_thread_func_old():
|
||||
global is_downloading, is_paused, download_thread, download_queue, current_downloading, download_stats
|
||||
|
||||
# Custom progress callback
|
||||
def progress_callback(d):
|
||||
if not is_downloading: # Check if cancelled
|
||||
return
|
||||
|
||||
# Wait if paused
|
||||
while is_paused and is_downloading:
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
|
||||
if is_downloading: # Check again after potential pause
|
||||
socketio.emit('download_progress', {
|
||||
'status': d.get('status'),
|
||||
'downloaded_bytes': d.get('downloaded_bytes', 0),
|
||||
'total_bytes': d.get('total_bytes') or d.get('total_bytes_estimate'),
|
||||
'percent': d.get('_percent_str', '0%')
|
||||
})
|
||||
|
||||
# Process each series in queue
|
||||
for serie in selected_series:
|
||||
if not is_downloading: # Check if cancelled
|
||||
break
|
||||
|
||||
# Update current downloading series
|
||||
current_downloading = serie
|
||||
if serie in download_queue:
|
||||
download_queue.remove(serie)
|
||||
|
||||
# Emit queue update
|
||||
socketio.emit('download_queue_update', {
|
||||
'current_downloading': {
|
||||
'folder': serie.folder,
|
||||
'name': serie.name or serie.folder,
|
||||
'missing_episodes': sum(len(episodes) for episodes in serie.episodeDict.values())
|
||||
},
|
||||
'queue': [{'folder': s.folder, 'name': s.name or s.folder} for s in download_queue],
|
||||
'stats': download_stats
|
||||
})
|
||||
|
||||
# Download episodes for current series
|
||||
serie_episodes = sum(len(episodes) for episodes in serie.episodeDict.values())
|
||||
episode_count = 0
|
||||
|
||||
for season, episodes in serie.episodeDict.items():
|
||||
for episode in episodes:
|
||||
if not is_downloading: # Check if cancelled
|
||||
break
|
||||
|
||||
# Wait if paused
|
||||
while is_paused and is_downloading:
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
|
||||
if not is_downloading: # Check again after potential pause
|
||||
break
|
||||
|
||||
# Update current episode info
|
||||
download_stats['current_episode'] = f"S{season:02d}E{episode:02d}"
|
||||
|
||||
# Emit episode update
|
||||
socketio.emit('download_episode_update', {
|
||||
'serie': serie.name or serie.folder,
|
||||
'episode': f"S{season:02d}E{episode:02d}",
|
||||
'episode_progress': f"{episode_count + 1}/{serie_episodes}",
|
||||
'overall_progress': f"{download_stats['completed_episodes'] + episode_count + 1}/{download_stats['total_episodes']}"
|
||||
})
|
||||
|
||||
# Perform the actual download
|
||||
loader = series_app.Loaders.GetLoader(key="aniworld.to")
|
||||
if loader.IsLanguage(season, episode, serie.key):
|
||||
series_app.retry(loader.Download, 3, 1,
|
||||
series_app.directory_to_search, serie.folder,
|
||||
season, episode, serie.key, "German Dub",
|
||||
progress_callback)
|
||||
|
||||
episode_count += 1
|
||||
download_stats['completed_episodes'] += 1
|
||||
|
||||
# Mark series as completed
|
||||
download_stats['completed_series'] += 1
|
||||
|
||||
# Emit series completion
|
||||
socketio.emit('download_series_completed', {
|
||||
'serie': serie.name or serie.folder,
|
||||
'completed_series': download_stats['completed_series'],
|
||||
'total_series': download_stats['total_series']
|
||||
})
|
||||
|
||||
# Clear current downloading
|
||||
current_downloading = None
|
||||
download_queue.clear()
|
||||
|
||||
# Emit download completed only if not cancelled
|
||||
if is_downloading:
|
||||
socketio.emit('download_completed', {
|
||||
'stats': download_stats
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if is_downloading: # Only emit error if not cancelled
|
||||
socketio.emit('download_error', {'message': str(e)})
|
||||
finally:
|
||||
is_downloading = False
|
||||
is_paused = False
|
||||
download_thread = None
|
||||
current_downloading = None
|
||||
download_queue.clear()
|
||||
|
||||
# Start download in background thread
|
||||
download_thread = threading.Thread(target=download_thread_func, daemon=True)
|
||||
download_thread.start()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Download started'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/download/pause', methods=['POST'])
|
||||
@optional_auth
|
||||
def pause_download():
|
||||
"""Pause current download."""
|
||||
global is_paused
|
||||
|
||||
if not is_downloading:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'No download in progress'
|
||||
}), 400
|
||||
|
||||
is_paused = True
|
||||
socketio.emit('download_paused')
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Download paused'
|
||||
})
|
||||
|
||||
@app.route('/api/download/resume', methods=['POST'])
|
||||
@optional_auth
|
||||
def resume_download():
|
||||
"""Resume paused download."""
|
||||
global is_paused
|
||||
|
||||
if not is_downloading:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'No download in progress'
|
||||
}), 400
|
||||
|
||||
if not is_paused:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Download is not paused'
|
||||
}), 400
|
||||
|
||||
is_paused = False
|
||||
socketio.emit('download_resumed')
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Download resumed'
|
||||
})
|
||||
|
||||
@app.route('/api/download/cancel', methods=['POST'])
|
||||
@optional_auth
|
||||
def cancel_download():
|
||||
"""Cancel current download."""
|
||||
global is_downloading, is_paused, download_thread
|
||||
|
||||
if not is_downloading:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'No download in progress'
|
||||
}), 400
|
||||
|
||||
is_downloading = False
|
||||
is_paused = False
|
||||
|
||||
# Note: In a real implementation, you would need to stop the download thread
|
||||
# This would require more sophisticated thread management
|
||||
|
||||
socketio.emit('download_cancelled')
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Download cancelled'
|
||||
})
|
||||
|
||||
@app.route('/api/download/status')
|
||||
@optional_auth
|
||||
def get_download_status():
|
||||
"""Get detailed download status including queue and current progress."""
|
||||
return jsonify({
|
||||
'is_downloading': is_downloading,
|
||||
'is_paused': is_paused,
|
||||
'queue': [{'folder': serie.folder, 'name': serie.name or serie.folder} for serie in download_queue],
|
||||
'current_downloading': {
|
||||
'folder': current_downloading.folder,
|
||||
'name': current_downloading.name or current_downloading.folder,
|
||||
'current_episode': download_stats['current_episode'],
|
||||
'missing_episodes': sum(len(episodes) for episodes in current_downloading.episodeDict.values())
|
||||
} if current_downloading else None,
|
||||
'stats': download_stats
|
||||
})
|
||||
|
||||
@app.route('/api/status')
|
||||
@optional_auth
|
||||
def get_status():
|
||||
"""Get current application status."""
|
||||
return jsonify({
|
||||
'is_scanning': is_scanning,
|
||||
'is_downloading': is_downloading,
|
||||
'is_paused': is_paused,
|
||||
'directory': series_app.directory_to_search if series_app else None,
|
||||
'series_count': len(series_app.series_list) if series_app else 0,
|
||||
'download_queue_count': len(download_queue),
|
||||
'current_downloading': current_downloading.name if current_downloading else None
|
||||
})
|
||||
|
||||
@socketio.on('connect')
|
||||
def handle_connect():
|
||||
"""Handle client connection."""
|
||||
emit('connected', {'message': 'Connected to AniWorld server'})
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
print("Starting AniWorld Flask server...")
|
||||
print(f"Using directory: {series_app.directory_to_search}")
|
||||
|
||||
socketio.run(app, debug=True, host='0.0.0.0', port=5000)
|
||||
@@ -1,378 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
||||
from flask_socketio import SocketIO, emit
|
||||
import logging
|
||||
|
||||
# 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 Serie import Serie
|
||||
import SerieList
|
||||
import SerieScanner
|
||||
from Loaders.Loaders import Loaders
|
||||
from auth import session_manager, require_auth, optional_auth
|
||||
from config import config
|
||||
from download_queue import download_queue_bp
|
||||
from process_api import process_bp
|
||||
from process_locks import (with_process_lock, RESCAN_LOCK, DOWNLOAD_LOCK,
|
||||
ProcessLockError, is_process_running, check_process_locks)
|
||||
|
||||
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)
|
||||
|
||||
# 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()
|
||||
|
||||
@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(),
|
||||
'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': 'error',
|
||||
'message': 'Series data not initialized. Please scan first.'
|
||||
}), 400
|
||||
|
||||
# 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:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 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
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Clean up any expired locks on startup
|
||||
check_process_locks()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info("Starting Aniworld Flask server...")
|
||||
logger.info(f"Anime directory: {config.anime_directory}")
|
||||
logger.info("Server will be available at http://localhost:5000")
|
||||
|
||||
# Run with SocketIO
|
||||
socketio.run(app, debug=True, host='0.0.0.0', port=5000, allow_unsafe_werkzeug=True)
|
||||
@@ -116,6 +116,24 @@ class SessionManager:
|
||||
|
||||
return True, "Login successful", session_token
|
||||
|
||||
def login(self, password: str, ip_address: str = None) -> Dict:
|
||||
"""
|
||||
Login method that returns a dictionary response (for API compatibility).
|
||||
"""
|
||||
success, message, token = self.authenticate(password)
|
||||
|
||||
if success:
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': message,
|
||||
'token': token
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': message
|
||||
}
|
||||
|
||||
def _get_remaining_lockout_time(self, ip_address: str) -> int:
|
||||
"""Get remaining lockout time in minutes."""
|
||||
if ip_address not in self.failed_attempts:
|
||||
|
||||
@@ -5,6 +5,8 @@ This module provides drag-and-drop capabilities for the AniWorld web interface,
|
||||
including file uploads, series reordering, and batch operations.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
class DragDropManager:
|
||||
"""Manages drag and drop operations for the web interface."""
|
||||
|
||||
@@ -18,7 +20,7 @@ class DragDropManager:
|
||||
// AniWorld Drag & Drop Manager
|
||||
class DragDropManager {{
|
||||
constructor() {{
|
||||
this.supportedFiles = {self.supported_files};
|
||||
this.supportedFiles = {json.dumps(self.supported_files)};
|
||||
this.maxFileSize = {self.max_file_size};
|
||||
this.dropZones = new Map();
|
||||
this.dragData = null;
|
||||
|
||||
@@ -5,6 +5,8 @@ This module provides keyboard shortcut functionality for the AniWorld web interf
|
||||
including customizable hotkeys for common actions and accessibility support.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
class KeyboardShortcutManager:
|
||||
"""Manages keyboard shortcuts for the web interface."""
|
||||
|
||||
@@ -434,6 +436,16 @@ class KeyboardShortcutManager {{
|
||||
this.enabled = false;
|
||||
}}
|
||||
|
||||
setEnabled(enabled) {{
|
||||
this.enabled = enabled;
|
||||
}}
|
||||
|
||||
updateShortcuts(newShortcuts) {{
|
||||
if (newShortcuts && typeof newShortcuts === 'object') {{
|
||||
Object.assign(this.shortcuts, newShortcuts);
|
||||
}}
|
||||
}}
|
||||
|
||||
addCustomShortcut(action, keys, callback) {{
|
||||
this.shortcuts[action] = Array.isArray(keys) ? keys : [keys];
|
||||
this.customCallbacks = this.customCallbacks || {{}};
|
||||
|
||||
@@ -40,3 +40,143 @@
|
||||
2025-09-28 19:23:27 - INFO - performance_optimizer - stop - Download manager stopped
|
||||
2025-09-28 19:23:28 - INFO - api_integration - stop - Webhook delivery service stopped
|
||||
2025-09-28 19:23:28 - INFO - root - cleanup_on_exit - Application cleanup completed
|
||||
2025-09-28 19:30:52 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 19:30:52 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 19:30:52 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 19:30:52 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 19:30:52 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 19:30:52 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 19:30:56 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 19:30:56 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 19:30:56 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 19:30:56 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 19:30:56 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 19:30:56 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 19:30:56 - WARNING - werkzeug - _log - * Debugger is active!
|
||||
2025-09-28 19:31:48 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 19:31:48 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 19:31:48 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 19:31:53 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 19:31:53 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 19:31:53 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 19:31:58 - INFO - health_monitor - stop_monitoring - System health monitoring stopped
|
||||
2025-09-28 19:32:03 - INFO - performance_optimizer - stop_monitoring - Memory monitoring stopped
|
||||
2025-09-28 19:32:03 - INFO - performance_optimizer - stop - Download manager stopped
|
||||
2025-09-28 19:32:04 - INFO - api_integration - stop - Webhook delivery service stopped
|
||||
2025-09-28 19:32:04 - INFO - root - cleanup_on_exit - Application cleanup completed
|
||||
2025-09-28 19:39:37 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 19:39:37 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 19:39:37 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 19:39:37 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 19:39:37 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 19:39:37 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 19:39:43 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 19:39:43 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 19:39:43 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 19:39:43 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 19:39:43 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 19:39:43 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 19:39:43 - WARNING - werkzeug - _log - * Debugger is active!
|
||||
2025-09-28 19:44:11 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 19:44:11 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 19:44:11 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 19:44:16 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 19:44:16 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 19:44:16 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 19:44:21 - INFO - health_monitor - stop_monitoring - System health monitoring stopped
|
||||
2025-09-28 19:44:26 - INFO - performance_optimizer - stop_monitoring - Memory monitoring stopped
|
||||
2025-09-28 19:44:26 - INFO - performance_optimizer - stop - Download manager stopped
|
||||
2025-09-28 19:44:26 - INFO - api_integration - stop - Webhook delivery service stopped
|
||||
2025-09-28 19:44:26 - INFO - root - cleanup_on_exit - Application cleanup completed
|
||||
2025-09-28 20:01:22 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 20:01:22 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 20:01:22 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 20:01:22 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 20:01:22 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 20:01:22 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 20:01:28 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 20:01:28 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 20:01:28 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 20:01:28 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 20:01:28 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 20:01:28 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 20:01:28 - WARNING - werkzeug - _log - * Debugger is active!
|
||||
2025-09-28 20:01:48 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 20:01:48 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 20:01:48 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 20:01:53 - INFO - health_monitor - stop_monitoring - System health monitoring stopped
|
||||
2025-09-28 20:01:58 - INFO - performance_optimizer - stop_monitoring - Memory monitoring stopped
|
||||
2025-09-28 20:01:58 - INFO - performance_optimizer - stop - Download manager stopped
|
||||
2025-09-28 20:01:58 - INFO - api_integration - stop - Webhook delivery service stopped
|
||||
2025-09-28 20:01:58 - INFO - root - cleanup_on_exit - Application cleanup completed
|
||||
2025-09-28 20:02:05 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 20:02:05 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 20:02:05 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 20:02:05 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 20:02:05 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 20:02:05 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 20:02:05 - WARNING - werkzeug - _log - * Debugger is active!
|
||||
2025-09-28 20:02:47 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 20:02:47 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 20:02:47 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 20:02:52 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 20:02:52 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 20:02:52 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 20:02:57 - INFO - health_monitor - stop_monitoring - System health monitoring stopped
|
||||
2025-09-28 20:03:02 - INFO - performance_optimizer - stop_monitoring - Memory monitoring stopped
|
||||
2025-09-28 20:03:02 - INFO - performance_optimizer - stop - Download manager stopped
|
||||
2025-09-28 20:03:02 - INFO - api_integration - stop - Webhook delivery service stopped
|
||||
2025-09-28 20:03:02 - INFO - root - cleanup_on_exit - Application cleanup completed
|
||||
2025-09-28 20:10:59 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 20:10:59 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 20:10:59 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 20:10:59 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 20:10:59 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 20:10:59 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 20:11:04 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 20:11:04 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 20:11:04 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 20:11:04 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 20:11:04 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 20:11:04 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 20:11:04 - WARNING - werkzeug - _log - * Debugger is active!
|
||||
2025-09-28 20:11:35 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 20:11:35 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 20:11:35 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 20:11:40 - INFO - health_monitor - stop_monitoring - System health monitoring stopped
|
||||
2025-09-28 20:11:45 - INFO - performance_optimizer - stop_monitoring - Memory monitoring stopped
|
||||
2025-09-28 20:11:45 - INFO - performance_optimizer - stop - Download manager stopped
|
||||
2025-09-28 20:11:45 - INFO - api_integration - stop - Webhook delivery service stopped
|
||||
2025-09-28 20:11:45 - INFO - root - cleanup_on_exit - Application cleanup completed
|
||||
2025-09-28 20:11:46 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 20:11:46 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 20:11:46 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 20:11:51 - INFO - health_monitor - stop_monitoring - System health monitoring stopped
|
||||
2025-09-28 20:11:56 - INFO - performance_optimizer - stop_monitoring - Memory monitoring stopped
|
||||
2025-09-28 20:11:56 - INFO - performance_optimizer - stop - Download manager stopped
|
||||
2025-09-28 20:11:56 - INFO - api_integration - stop - Webhook delivery service stopped
|
||||
2025-09-28 20:11:56 - INFO - root - cleanup_on_exit - Application cleanup completed
|
||||
2025-09-28 20:14:54 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 20:14:54 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 20:14:54 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 20:14:54 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 20:14:54 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 20:14:54 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 20:14:59 - INFO - __main__ - <module> - Enhanced logging system initialized
|
||||
2025-09-28 20:14:59 - INFO - __main__ - <module> - Starting Aniworld Flask server...
|
||||
2025-09-28 20:14:59 - INFO - __main__ - <module> - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien
|
||||
2025-09-28 20:14:59 - INFO - __main__ - <module> - Log level: INFO
|
||||
2025-09-28 20:14:59 - INFO - __main__ - <module> - Scheduled operations disabled
|
||||
2025-09-28 20:14:59 - INFO - __main__ - <module> - Server will be available at http://localhost:5000
|
||||
2025-09-28 20:14:59 - WARNING - werkzeug - _log - * Debugger is active!
|
||||
2025-09-28 20:18:22 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 20:18:22 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 20:18:22 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 20:18:27 - DEBUG - schedule - clear - Deleting *all* jobs
|
||||
2025-09-28 20:18:27 - INFO - scheduler - stop_scheduler - Scheduled operations stopped
|
||||
2025-09-28 20:18:27 - INFO - __main__ - <module> - Scheduler stopped
|
||||
2025-09-28 20:18:32 - INFO - health_monitor - stop_monitoring - System health monitoring stopped
|
||||
2025-09-28 20:18:37 - INFO - performance_optimizer - stop_monitoring - Memory monitoring stopped
|
||||
2025-09-28 20:18:37 - INFO - performance_optimizer - stop - Download manager stopped
|
||||
2025-09-28 20:18:38 - INFO - api_integration - stop - Webhook delivery service stopped
|
||||
2025-09-28 20:18:38 - INFO - root - cleanup_on_exit - Application cleanup completed
|
||||
|
||||
@@ -5,6 +5,7 @@ This module provides mobile-responsive design capabilities, adaptive layouts,
|
||||
and mobile-optimized user interface components for the AniWorld web interface.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Dict, List, Any, Optional
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
@@ -32,7 +33,7 @@ class MobileResponsiveManager:
|
||||
// AniWorld Mobile Responsive Manager
|
||||
class MobileResponsiveManager {{
|
||||
constructor() {{
|
||||
this.breakpoints = {self.breakpoints};
|
||||
this.breakpoints = {json.dumps(self.breakpoints)};
|
||||
this.currentBreakpoint = 'lg';
|
||||
this.isMobile = false;
|
||||
this.isTablet = false;
|
||||
|
||||
@@ -262,6 +262,98 @@ class ScreenReaderManager {{
|
||||
}}, 50);
|
||||
}}
|
||||
|
||||
enhanceInteractiveElements() {{
|
||||
const elements = document.querySelectorAll(`
|
||||
button, [role="button"], a, input, select, textarea,
|
||||
[tabindex], .btn, .series-card, .dropdown-toggle
|
||||
`);
|
||||
|
||||
elements.forEach(element => {{
|
||||
this.enhanceElement(element);
|
||||
}});
|
||||
}}
|
||||
|
||||
addSemanticInformation() {{
|
||||
// Add missing semantic HTML and ARIA landmarks
|
||||
this.addLandmarks();
|
||||
this.addHeadingHierarchy();
|
||||
this.addListSemantics();
|
||||
this.addTableSemantics();
|
||||
}}
|
||||
|
||||
addContextInformation() {{
|
||||
// Add contextual information to elements for screen readers
|
||||
this.addBreadcrumbContext();
|
||||
this.addCardContext();
|
||||
this.addButtonContext();
|
||||
this.addLinkContext();
|
||||
this.addDataContext();
|
||||
}}
|
||||
|
||||
enhanceFormElements() {{
|
||||
// Enhance form elements with proper labels and descriptions
|
||||
const forms = document.querySelectorAll('form');
|
||||
|
||||
forms.forEach(form => {{
|
||||
// Add form description if missing
|
||||
if (!form.hasAttribute('aria-label') && !form.hasAttribute('aria-labelledby')) {{
|
||||
const legend = form.querySelector('legend');
|
||||
const heading = form.querySelector('h1, h2, h3, h4, h5, h6');
|
||||
if (legend) {{
|
||||
form.setAttribute('aria-labelledby', legend.id || this.generateId(legend));
|
||||
}} else if (heading) {{
|
||||
form.setAttribute('aria-labelledby', heading.id || this.generateId(heading));
|
||||
}} else {{
|
||||
form.setAttribute('aria-label', 'Form');
|
||||
}}
|
||||
}}
|
||||
|
||||
// Enhance input elements
|
||||
const inputs = form.querySelectorAll('input:not([type="hidden"]), textarea, select');
|
||||
inputs.forEach(input => {{
|
||||
// Ensure each input has a label
|
||||
const label = form.querySelector(`label[for="${{input.id}}"]`) ||
|
||||
input.closest('label');
|
||||
if (!label && !input.hasAttribute('aria-label') && !input.hasAttribute('aria-labelledby')) {{
|
||||
const placeholder = input.getAttribute('placeholder');
|
||||
if (placeholder) {{
|
||||
input.setAttribute('aria-label', placeholder);
|
||||
}}
|
||||
}}
|
||||
|
||||
// Add required indicators
|
||||
if (input.hasAttribute('required') && !input.hasAttribute('aria-required')) {{
|
||||
input.setAttribute('aria-required', 'true');
|
||||
}}
|
||||
|
||||
// Add invalid state for validation
|
||||
if (input.validity && !input.validity.valid && !input.hasAttribute('aria-invalid')) {{
|
||||
input.setAttribute('aria-invalid', 'true');
|
||||
}}
|
||||
}});
|
||||
}});
|
||||
}}
|
||||
|
||||
observeContentChanges() {{
|
||||
// Monitor DOM changes for dynamic content
|
||||
const observer = new MutationObserver((mutations) => {{
|
||||
mutations.forEach(mutation => {{
|
||||
if (mutation.type === 'childList') {{
|
||||
mutation.addedNodes.forEach(node => {{
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {{
|
||||
this.handleNewContent(node);
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
}});
|
||||
}});
|
||||
|
||||
observer.observe(document.body, {{
|
||||
childList: true,
|
||||
subtree: true
|
||||
}});
|
||||
}}
|
||||
|
||||
setupContentEnhancement() {{
|
||||
// Enhance all interactive elements
|
||||
this.enhanceInteractiveElements();
|
||||
@@ -279,17 +371,6 @@ class ScreenReaderManager {{
|
||||
this.observeContentChanges();
|
||||
}}
|
||||
|
||||
enhanceInteractiveElements() {{
|
||||
const elements = document.querySelectorAll(`
|
||||
button, [role="button"], a, input, select, textarea,
|
||||
[tabindex], .btn, .series-card, .dropdown-toggle
|
||||
`);
|
||||
|
||||
elements.forEach(element => {{
|
||||
this.enhanceElement(element);
|
||||
}});
|
||||
}}
|
||||
|
||||
enhanceElement(element) {{
|
||||
// Ensure proper labeling
|
||||
if (!this.hasAccessibleName(element)) {{
|
||||
@@ -474,12 +555,143 @@ class ScreenReaderManager {{
|
||||
return focusableElements.includes(element.tagName) && !element.disabled;
|
||||
}}
|
||||
|
||||
addSemanticInformation() {{
|
||||
// Add missing semantic HTML and ARIA landmarks
|
||||
this.addLandmarks();
|
||||
this.addHeadingHierarchy();
|
||||
this.addListSemantics();
|
||||
this.addTableSemantics();
|
||||
addBreadcrumbContext() {{
|
||||
// Enhance breadcrumb navigation
|
||||
const breadcrumbs = document.querySelectorAll('.breadcrumb, [aria-label*="breadcrumb"]');
|
||||
breadcrumbs.forEach(breadcrumb => {{
|
||||
if (!breadcrumb.hasAttribute('aria-label')) {{
|
||||
breadcrumb.setAttribute('aria-label', 'Navigation breadcrumb');
|
||||
}}
|
||||
|
||||
const items = breadcrumb.querySelectorAll('li, .breadcrumb-item');
|
||||
items.forEach((item, index) => {{
|
||||
if (!item.hasAttribute('aria-current') && index === items.length - 1) {{
|
||||
item.setAttribute('aria-current', 'page');
|
||||
}}
|
||||
}});
|
||||
}});
|
||||
}}
|
||||
|
||||
addCardContext() {{
|
||||
// Enhance cards with context information
|
||||
const cards = document.querySelectorAll('.card, .series-card, .episode-card');
|
||||
cards.forEach(card => {{
|
||||
const title = card.querySelector('h1, h2, h3, h4, h5, h6, .title, .card-title');
|
||||
const description = card.querySelector('.description, .card-text, .summary');
|
||||
|
||||
if (title && !card.hasAttribute('aria-labelledby')) {{
|
||||
if (!title.id) {{
|
||||
title.id = this.generateId('card-title');
|
||||
}}
|
||||
card.setAttribute('aria-labelledby', title.id);
|
||||
}}
|
||||
|
||||
if (description && !card.hasAttribute('aria-describedby')) {{
|
||||
if (!description.id) {{
|
||||
description.id = this.generateId('card-desc');
|
||||
}}
|
||||
card.setAttribute('aria-describedby', description.id);
|
||||
}}
|
||||
|
||||
if (!card.hasAttribute('role')) {{
|
||||
card.setAttribute('role', 'article');
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
addButtonContext() {{
|
||||
// Enhance buttons with better context
|
||||
const buttons = document.querySelectorAll('button, [role="button"]');
|
||||
buttons.forEach(button => {{
|
||||
// Add context for icon-only buttons
|
||||
if (!button.textContent.trim() && !button.hasAttribute('aria-label')) {{
|
||||
const icon = button.querySelector('i, .icon, svg');
|
||||
if (icon) {{
|
||||
const iconClass = icon.className;
|
||||
if (iconClass.includes('play')) {{
|
||||
button.setAttribute('aria-label', 'Play');
|
||||
}} else if (iconClass.includes('pause')) {{
|
||||
button.setAttribute('aria-label', 'Pause');
|
||||
}} else if (iconClass.includes('download')) {{
|
||||
button.setAttribute('aria-label', 'Download');
|
||||
}} else if (iconClass.includes('favorite') || iconClass.includes('heart')) {{
|
||||
button.setAttribute('aria-label', 'Add to favorites');
|
||||
}} else if (iconClass.includes('share')) {{
|
||||
button.setAttribute('aria-label', 'Share');
|
||||
}} else {{
|
||||
button.setAttribute('aria-label', 'Button');
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
// Add pressed state for toggle buttons
|
||||
if (button.hasAttribute('data-toggle')) {{
|
||||
button.setAttribute('aria-pressed', 'false');
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
addLinkContext() {{
|
||||
// Enhance links with better context
|
||||
const links = document.querySelectorAll('a');
|
||||
links.forEach(link => {{
|
||||
// Add context for external links
|
||||
if (link.hostname && link.hostname !== location.hostname) {{
|
||||
const existingLabel = link.getAttribute('aria-label') || '';
|
||||
if (!existingLabel.includes('external')) {{
|
||||
link.setAttribute('aria-label',
|
||||
(existingLabel + ' (opens in new window)').trim());
|
||||
}}
|
||||
link.setAttribute('rel', 'noopener');
|
||||
}}
|
||||
|
||||
// Add context for download links
|
||||
if (link.download || link.href.match(/\.(pdf|doc|docx|xls|xlsx|zip|rar)$/i)) {{
|
||||
const existingLabel = link.getAttribute('aria-label') || link.textContent;
|
||||
link.setAttribute('aria-label',
|
||||
(existingLabel + ' (download)').trim());
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
addDataContext() {{
|
||||
// Add context to data tables and lists
|
||||
const tables = document.querySelectorAll('table');
|
||||
tables.forEach(table => {{
|
||||
if (!table.hasAttribute('aria-label') && !table.hasAttribute('aria-labelledby')) {{
|
||||
const caption = table.querySelector('caption');
|
||||
if (caption) {{
|
||||
if (!caption.id) {{
|
||||
caption.id = this.generateId('table-caption');
|
||||
}}
|
||||
table.setAttribute('aria-labelledby', caption.id);
|
||||
}} else {{
|
||||
table.setAttribute('aria-label', 'Data table');
|
||||
}}
|
||||
}}
|
||||
}});
|
||||
|
||||
// Add context to definition lists
|
||||
const definitionLists = document.querySelectorAll('dl');
|
||||
definitionLists.forEach(dl => {{
|
||||
if (!dl.hasAttribute('role')) {{
|
||||
dl.setAttribute('role', 'list');
|
||||
}}
|
||||
|
||||
const terms = dl.querySelectorAll('dt');
|
||||
const descriptions = dl.querySelectorAll('dd');
|
||||
|
||||
terms.forEach((dt, index) => {{
|
||||
if (!dt.id) {{
|
||||
dt.id = this.generateId('term');
|
||||
}}
|
||||
|
||||
const dd = descriptions[index];
|
||||
if (dd && !dd.hasAttribute('aria-labelledby')) {{
|
||||
dd.setAttribute('aria-labelledby', dt.id);
|
||||
}}
|
||||
}});
|
||||
}});
|
||||
}}
|
||||
|
||||
addLandmarks() {{
|
||||
@@ -584,26 +796,6 @@ class ScreenReaderManager {{
|
||||
}});
|
||||
}}
|
||||
|
||||
observeContentChanges() {{
|
||||
// Monitor DOM changes for dynamic content
|
||||
const observer = new MutationObserver((mutations) => {{
|
||||
mutations.forEach(mutation => {{
|
||||
if (mutation.type === 'childList') {{
|
||||
mutation.addedNodes.forEach(node => {{
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {{
|
||||
this.handleNewContent(node);
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
}});
|
||||
}});
|
||||
|
||||
observer.observe(document.body, {{
|
||||
childList: true,
|
||||
subtree: true
|
||||
}});
|
||||
}}
|
||||
|
||||
handleNewContent(element) {{
|
||||
// Enhance newly added content
|
||||
if (element.classList?.contains('series-card')) {{
|
||||
@@ -997,6 +1189,36 @@ class ScreenReaderManager {{
|
||||
this.announce(message, priority, context);
|
||||
}}
|
||||
}}
|
||||
|
||||
generateId(elementOrPrefix) {{
|
||||
// Generate a unique ID for an element or with a prefix
|
||||
let prefix, element;
|
||||
|
||||
if (typeof elementOrPrefix === 'string') {{
|
||||
// Called with a string prefix
|
||||
prefix = elementOrPrefix;
|
||||
element = null;
|
||||
}} else if (elementOrPrefix && elementOrPrefix.tagName) {{
|
||||
// Called with a DOM element
|
||||
element = elementOrPrefix;
|
||||
prefix = element.tagName.toLowerCase();
|
||||
}} else {{
|
||||
// Fallback for invalid input
|
||||
prefix = 'element';
|
||||
element = elementOrPrefix;
|
||||
}}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const random = Math.floor(Math.random() * 1000);
|
||||
const id = `${{prefix}}-${{timestamp}}-${{random}}`;
|
||||
|
||||
// Set ID on element if provided and it's a DOM element
|
||||
if (element && element.setAttribute) {{
|
||||
element.id = id;
|
||||
}}
|
||||
|
||||
return id;
|
||||
}}
|
||||
}}
|
||||
|
||||
// Initialize screen reader manager when DOM is loaded
|
||||
|
||||
@@ -33,6 +33,12 @@ class AniWorldApp {
|
||||
}
|
||||
|
||||
async checkAuthentication() {
|
||||
// Don't check authentication if we're already on login or setup pages
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath === '/login' || currentPath === '/setup') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
const data = await response.json();
|
||||
@@ -889,6 +895,14 @@ class AniWorldApp {
|
||||
try {
|
||||
const response = await this.makeAuthenticatedRequest('/api/process/locks/status');
|
||||
if (!response) return;
|
||||
|
||||
// Check if response is actually JSON and not HTML (login page)
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
console.warn('Process locks API returned non-JSON response, likely authentication issue');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
|
||||
1
src/server/tests/__init__.py
Normal file
1
src/server/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Test package initialization
|
||||
BIN
src/server/tests/__pycache__/test_authentication.cpython-312.pyc
Normal file
BIN
src/server/tests/__pycache__/test_authentication.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/server/tests/__pycache__/test_flask_app.cpython-312.pyc
Normal file
BIN
src/server/tests/__pycache__/test_flask_app.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
20
src/server/tests/run_core_tests.bat
Normal file
20
src/server/tests/run_core_tests.bat
Normal file
@@ -0,0 +1,20 @@
|
||||
@echo off
|
||||
echo.
|
||||
echo 🚀 AniWorld Core Functionality Tests
|
||||
echo =====================================
|
||||
echo.
|
||||
|
||||
cd /d "%~dp0"
|
||||
python run_core_tests.py
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo ✅ All tests completed successfully!
|
||||
) else (
|
||||
echo.
|
||||
echo ❌ Some tests failed. Check output above.
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Press any key to continue...
|
||||
pause > nul
|
||||
57
src/server/tests/run_core_tests.py
Normal file
57
src/server/tests/run_core_tests.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Simple test runner for core AniWorld server functionality.
|
||||
|
||||
This script runs the essential tests to validate JavaScript/CSS generation.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("🚀 Running AniWorld Core Functionality Tests")
|
||||
print("=" * 50)
|
||||
|
||||
# Import and run the core tests
|
||||
from test_core_functionality import TestManagerGenerationCore, TestComprehensiveSuite
|
||||
|
||||
# Create test suite
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
# Add core manager tests
|
||||
suite.addTest(TestManagerGenerationCore('test_keyboard_shortcut_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_drag_drop_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_accessibility_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_user_preferences_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_advanced_search_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_undo_redo_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_multi_screen_manager_generation'))
|
||||
|
||||
# Add comprehensive test
|
||||
suite.addTest(TestComprehensiveSuite('test_all_manager_fixes_comprehensive'))
|
||||
|
||||
# Run tests
|
||||
runner = unittest.TextTestRunner(verbosity=1, buffer=True)
|
||||
result = runner.run(suite)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 50)
|
||||
if result.wasSuccessful():
|
||||
print("🎉 ALL CORE TESTS PASSED!")
|
||||
print("✅ JavaScript/CSS generation working correctly")
|
||||
print("✅ All manager classes validated")
|
||||
print("✅ No syntax or runtime errors found")
|
||||
else:
|
||||
print("❌ Some core tests failed")
|
||||
if result.failures:
|
||||
for test, error in result.failures:
|
||||
print(f" FAIL: {test}")
|
||||
if result.errors:
|
||||
for test, error in result.errors:
|
||||
print(f" ERROR: {test}")
|
||||
|
||||
print("=" * 50)
|
||||
sys.exit(0 if result.wasSuccessful() else 1)
|
||||
10
src/server/tests/run_tests.bat
Normal file
10
src/server/tests/run_tests.bat
Normal file
@@ -0,0 +1,10 @@
|
||||
@echo off
|
||||
echo Running AniWorld Server Test Suite...
|
||||
echo.
|
||||
|
||||
cd /d "%~dp0"
|
||||
python run_tests.py
|
||||
|
||||
echo.
|
||||
echo Test run completed.
|
||||
pause
|
||||
108
src/server/tests/run_tests.py
Normal file
108
src/server/tests/run_tests.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Test runner for the AniWorld server test suite.
|
||||
|
||||
This script runs all test modules and provides a comprehensive report.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from io import StringIO
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all test modules and provide a summary report."""
|
||||
|
||||
print("=" * 60)
|
||||
print("AniWorld Server Test Suite")
|
||||
print("=" * 60)
|
||||
|
||||
# Discover and run all tests
|
||||
loader = unittest.TestLoader()
|
||||
test_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Load all test modules
|
||||
suite = loader.discover(test_dir, pattern='test_*.py')
|
||||
|
||||
# Run tests with detailed output
|
||||
stream = StringIO()
|
||||
runner = unittest.TextTestRunner(
|
||||
stream=stream,
|
||||
verbosity=2,
|
||||
buffer=True
|
||||
)
|
||||
|
||||
result = runner.run(suite)
|
||||
|
||||
# Print results
|
||||
output = stream.getvalue()
|
||||
print(output)
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
total_tests = result.testsRun
|
||||
failures = len(result.failures)
|
||||
errors = len(result.errors)
|
||||
skipped = len(result.skipped) if hasattr(result, 'skipped') else 0
|
||||
passed = total_tests - failures - errors - skipped
|
||||
|
||||
print(f"Total Tests Run: {total_tests}")
|
||||
print(f"Passed: {passed}")
|
||||
print(f"Failed: {failures}")
|
||||
print(f"Errors: {errors}")
|
||||
print(f"Skipped: {skipped}")
|
||||
|
||||
if result.wasSuccessful():
|
||||
print("\n🎉 ALL TESTS PASSED! 🎉")
|
||||
print("✅ No JavaScript or CSS generation issues found!")
|
||||
print("✅ All manager classes working correctly!")
|
||||
print("✅ Authentication system validated!")
|
||||
return True
|
||||
else:
|
||||
print("\n❌ Some tests failed. Please check the output above.")
|
||||
|
||||
if result.failures:
|
||||
print(f"\nFailures ({len(result.failures)}):")
|
||||
for test, traceback in result.failures:
|
||||
print(f" - {test}: {traceback.split(chr(10))[-2]}")
|
||||
|
||||
if result.errors:
|
||||
print(f"\nErrors ({len(result.errors)}):")
|
||||
for test, traceback in result.errors:
|
||||
print(f" - {test}: {traceback.split(chr(10))[-2]}")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def run_specific_test_module(module_name):
|
||||
"""Run a specific test module."""
|
||||
|
||||
print(f"Running tests from module: {module_name}")
|
||||
print("-" * 40)
|
||||
|
||||
loader = unittest.TestLoader()
|
||||
suite = loader.loadTestsFromName(module_name)
|
||||
|
||||
runner = unittest.TextTestRunner(verbosity=2, buffer=True)
|
||||
result = runner.run(suite)
|
||||
|
||||
return result.wasSuccessful()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
# Run specific test module
|
||||
module_name = sys.argv[1]
|
||||
success = run_specific_test_module(module_name)
|
||||
else:
|
||||
# Run all tests
|
||||
success = run_all_tests()
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if success else 1)
|
||||
127
src/server/tests/test_authentication.py
Normal file
127
src/server/tests/test_authentication.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Test suite for authentication and session management.
|
||||
|
||||
This test module validates the authentication system, session management,
|
||||
and security features.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class TestAuthenticationSystem(unittest.TestCase):
|
||||
"""Test class for authentication and session management."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
# Mock Flask app for testing
|
||||
self.mock_app = MagicMock()
|
||||
self.mock_app.config = {'SECRET_KEY': 'test_secret'}
|
||||
|
||||
def test_session_manager_initialization(self):
|
||||
"""Test SessionManager initialization."""
|
||||
try:
|
||||
from auth import SessionManager
|
||||
|
||||
manager = SessionManager()
|
||||
self.assertIsNotNone(manager)
|
||||
self.assertTrue(hasattr(manager, 'login'))
|
||||
self.assertTrue(hasattr(manager, 'check_password'))
|
||||
|
||||
print('✓ SessionManager initialization successful')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'SessionManager initialization failed: {e}')
|
||||
|
||||
def test_login_method_exists(self):
|
||||
"""Test that login method exists and returns proper response."""
|
||||
try:
|
||||
from auth import SessionManager
|
||||
|
||||
manager = SessionManager()
|
||||
|
||||
# Test login method exists
|
||||
self.assertTrue(hasattr(manager, 'login'))
|
||||
|
||||
# Test login with invalid credentials returns dict
|
||||
result = manager.login('wrong_password')
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn('success', result)
|
||||
self.assertFalse(result['success'])
|
||||
|
||||
print('✓ SessionManager login method validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'SessionManager login method test failed: {e}')
|
||||
|
||||
def test_password_checking(self):
|
||||
"""Test password validation functionality."""
|
||||
try:
|
||||
from auth import SessionManager
|
||||
|
||||
manager = SessionManager()
|
||||
|
||||
# Test check_password method exists
|
||||
self.assertTrue(hasattr(manager, 'check_password'))
|
||||
|
||||
# Test with empty/invalid password
|
||||
result = manager.check_password('')
|
||||
self.assertFalse(result)
|
||||
|
||||
result = manager.check_password('wrong_password')
|
||||
self.assertFalse(result)
|
||||
|
||||
print('✓ SessionManager password checking validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'SessionManager password checking test failed: {e}')
|
||||
|
||||
|
||||
class TestConfigurationSystem(unittest.TestCase):
|
||||
"""Test class for configuration management."""
|
||||
|
||||
def test_config_manager_initialization(self):
|
||||
"""Test ConfigManager initialization."""
|
||||
try:
|
||||
from config import ConfigManager
|
||||
|
||||
manager = ConfigManager()
|
||||
self.assertIsNotNone(manager)
|
||||
self.assertTrue(hasattr(manager, 'anime_directory'))
|
||||
|
||||
print('✓ ConfigManager initialization successful')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'ConfigManager initialization failed: {e}')
|
||||
|
||||
def test_anime_directory_property(self):
|
||||
"""Test anime_directory property getter and setter."""
|
||||
try:
|
||||
from config import ConfigManager
|
||||
|
||||
manager = ConfigManager()
|
||||
|
||||
# Test getter
|
||||
initial_dir = manager.anime_directory
|
||||
self.assertIsInstance(initial_dir, str)
|
||||
|
||||
# Test setter exists
|
||||
test_dir = 'C:\\TestAnimeDir'
|
||||
manager.anime_directory = test_dir
|
||||
|
||||
# Verify setter worked
|
||||
self.assertEqual(manager.anime_directory, test_dir)
|
||||
|
||||
print('✓ ConfigManager anime_directory property validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'ConfigManager anime_directory property test failed: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2, buffer=True)
|
||||
288
src/server/tests/test_core_functionality.py
Normal file
288
src/server/tests/test_core_functionality.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
Focused test suite for manager JavaScript and CSS generation.
|
||||
|
||||
This test module validates the core functionality that we know is working.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class TestManagerGenerationCore(unittest.TestCase):
|
||||
"""Test class for validating core manager JavaScript/CSS generation functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
self.managers_tested = 0
|
||||
self.total_js_chars = 0
|
||||
self.total_css_chars = 0
|
||||
print("\n" + "="*50)
|
||||
|
||||
def test_keyboard_shortcut_manager_generation(self):
|
||||
"""Test KeyboardShortcutManager JavaScript generation."""
|
||||
print("Testing KeyboardShortcutManager...")
|
||||
try:
|
||||
from keyboard_shortcuts import KeyboardShortcutManager
|
||||
manager = KeyboardShortcutManager()
|
||||
js = manager.get_shortcuts_js()
|
||||
|
||||
# Validate JS generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertGreater(len(js), 1000) # Should be substantial
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ KeyboardShortcutManager: {len(js):,} JS characters generated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'KeyboardShortcutManager test failed: {e}')
|
||||
|
||||
def test_drag_drop_manager_generation(self):
|
||||
"""Test DragDropManager JavaScript and CSS generation."""
|
||||
print("Testing DragDropManager...")
|
||||
try:
|
||||
from drag_drop import DragDropManager
|
||||
manager = DragDropManager()
|
||||
|
||||
js = manager.get_drag_drop_js()
|
||||
css = manager.get_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
# Check for proper JSON serialization (no Python booleans)
|
||||
self.assertNotIn('True', js)
|
||||
self.assertNotIn('False', js)
|
||||
self.assertNotIn('None', js)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ DragDropManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'DragDropManager test failed: {e}')
|
||||
|
||||
def test_accessibility_manager_generation(self):
|
||||
"""Test AccessibilityManager JavaScript and CSS generation."""
|
||||
print("Testing AccessibilityManager...")
|
||||
try:
|
||||
from accessibility_features import AccessibilityManager
|
||||
manager = AccessibilityManager()
|
||||
|
||||
js = manager.get_accessibility_js()
|
||||
css = manager.get_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
# Check for proper JSON serialization
|
||||
self.assertNotIn('True', js)
|
||||
self.assertNotIn('False', js)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ AccessibilityManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'AccessibilityManager test failed: {e}')
|
||||
|
||||
def test_user_preferences_manager_generation(self):
|
||||
"""Test UserPreferencesManager JavaScript and CSS generation."""
|
||||
print("Testing UserPreferencesManager...")
|
||||
try:
|
||||
from user_preferences import UserPreferencesManager
|
||||
manager = UserPreferencesManager()
|
||||
|
||||
# Verify preferences attribute exists (this was the main fix)
|
||||
self.assertTrue(hasattr(manager, 'preferences'))
|
||||
self.assertIsInstance(manager.preferences, dict)
|
||||
|
||||
js = manager.get_preferences_js()
|
||||
css = manager.get_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ UserPreferencesManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'UserPreferencesManager test failed: {e}')
|
||||
|
||||
def test_advanced_search_manager_generation(self):
|
||||
"""Test AdvancedSearchManager JavaScript and CSS generation."""
|
||||
print("Testing AdvancedSearchManager...")
|
||||
try:
|
||||
from advanced_search import AdvancedSearchManager
|
||||
manager = AdvancedSearchManager()
|
||||
|
||||
js = manager.get_search_js()
|
||||
css = manager.get_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ AdvancedSearchManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'AdvancedSearchManager test failed: {e}')
|
||||
|
||||
def test_undo_redo_manager_generation(self):
|
||||
"""Test UndoRedoManager JavaScript and CSS generation."""
|
||||
print("Testing UndoRedoManager...")
|
||||
try:
|
||||
from undo_redo_manager import UndoRedoManager
|
||||
manager = UndoRedoManager()
|
||||
|
||||
js = manager.get_undo_redo_js()
|
||||
css = manager.get_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ UndoRedoManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'UndoRedoManager test failed: {e}')
|
||||
|
||||
def test_multi_screen_manager_generation(self):
|
||||
"""Test MultiScreenManager JavaScript and CSS generation."""
|
||||
print("Testing MultiScreenManager...")
|
||||
try:
|
||||
from multi_screen_support import MultiScreenManager
|
||||
manager = MultiScreenManager()
|
||||
|
||||
js = manager.get_multiscreen_js()
|
||||
css = manager.get_multiscreen_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
# Check for proper f-string escaping (no Python syntax)
|
||||
self.assertNotIn('True', js)
|
||||
self.assertNotIn('False', js)
|
||||
self.assertNotIn('None', js)
|
||||
|
||||
# Verify JavaScript is properly formatted
|
||||
self.assertIn('class', js) # Should contain JavaScript class syntax
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ MultiScreenManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'MultiScreenManager test failed: {e}')
|
||||
|
||||
|
||||
class TestComprehensiveSuite(unittest.TestCase):
|
||||
"""Comprehensive test to verify all fixes are working."""
|
||||
|
||||
def test_all_manager_fixes_comprehensive(self):
|
||||
"""Run comprehensive test of all manager fixes."""
|
||||
print("\n" + "="*60)
|
||||
print("COMPREHENSIVE MANAGER VALIDATION")
|
||||
print("="*60)
|
||||
|
||||
managers_tested = 0
|
||||
total_js = 0
|
||||
total_css = 0
|
||||
|
||||
# Test each manager
|
||||
test_cases = [
|
||||
('KeyboardShortcutManager', 'keyboard_shortcuts', 'get_shortcuts_js', None),
|
||||
('DragDropManager', 'drag_drop', 'get_drag_drop_js', 'get_css'),
|
||||
('AccessibilityManager', 'accessibility_features', 'get_accessibility_js', 'get_css'),
|
||||
('UserPreferencesManager', 'user_preferences', 'get_preferences_js', 'get_css'),
|
||||
('AdvancedSearchManager', 'advanced_search', 'get_search_js', 'get_css'),
|
||||
('UndoRedoManager', 'undo_redo_manager', 'get_undo_redo_js', 'get_css'),
|
||||
('MultiScreenManager', 'multi_screen_support', 'get_multiscreen_js', 'get_multiscreen_css'),
|
||||
]
|
||||
|
||||
for class_name, module_name, js_method, css_method in test_cases:
|
||||
try:
|
||||
# Dynamic import
|
||||
module = __import__(module_name, fromlist=[class_name])
|
||||
manager_class = getattr(module, class_name)
|
||||
manager = manager_class()
|
||||
|
||||
# Get JS
|
||||
js_func = getattr(manager, js_method)
|
||||
js = js_func()
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
total_js += len(js)
|
||||
|
||||
# Get CSS if available
|
||||
css_chars = 0
|
||||
if css_method:
|
||||
css_func = getattr(manager, css_method)
|
||||
css = css_func()
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(css), 0)
|
||||
css_chars = len(css)
|
||||
total_css += css_chars
|
||||
|
||||
managers_tested += 1
|
||||
print(f'✓ {class_name}: JS={len(js):,} chars' +
|
||||
(f', CSS={css_chars:,} chars' if css_chars > 0 else ' (JS only)'))
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'{class_name} failed: {e}')
|
||||
|
||||
# Final validation
|
||||
expected_managers = 7
|
||||
self.assertEqual(managers_tested, expected_managers)
|
||||
self.assertGreater(total_js, 100000) # Should have substantial JS
|
||||
self.assertGreater(total_css, 10000) # Should have substantial CSS
|
||||
|
||||
print(f'\n{"="*60}')
|
||||
print(f'🎉 ALL {managers_tested} MANAGERS PASSED!')
|
||||
print(f'📊 Total JavaScript: {total_js:,} characters')
|
||||
print(f'🎨 Total CSS: {total_css:,} characters')
|
||||
print(f'✅ No JavaScript or CSS generation issues found!')
|
||||
print(f'{"="*60}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run with high verbosity
|
||||
unittest.main(verbosity=2, buffer=False)
|
||||
131
src/server/tests/test_flask_app.py
Normal file
131
src/server/tests/test_flask_app.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Test suite for Flask application routes and API endpoints.
|
||||
|
||||
This test module validates the main Flask application functionality,
|
||||
route handling, and API responses.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class TestFlaskApplication(unittest.TestCase):
|
||||
"""Test class for Flask application and routes."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
pass
|
||||
|
||||
def test_app_imports(self):
|
||||
"""Test that main app module can be imported without errors."""
|
||||
try:
|
||||
import app
|
||||
self.assertIsNotNone(app)
|
||||
print('✓ Main app module imports successfully')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'App import failed: {e}')
|
||||
|
||||
@patch('app.Flask')
|
||||
def test_app_initialization_components(self, mock_flask):
|
||||
"""Test that app initialization components are available."""
|
||||
try:
|
||||
# Test manager imports
|
||||
from keyboard_shortcuts import KeyboardShortcutManager
|
||||
from drag_drop import DragDropManager
|
||||
from accessibility_features import AccessibilityManager
|
||||
from user_preferences import UserPreferencesManager
|
||||
|
||||
# Verify managers can be instantiated
|
||||
keyboard_manager = KeyboardShortcutManager()
|
||||
drag_manager = DragDropManager()
|
||||
accessibility_manager = AccessibilityManager()
|
||||
preferences_manager = UserPreferencesManager()
|
||||
|
||||
self.assertIsNotNone(keyboard_manager)
|
||||
self.assertIsNotNone(drag_manager)
|
||||
self.assertIsNotNone(accessibility_manager)
|
||||
self.assertIsNotNone(preferences_manager)
|
||||
|
||||
print('✓ App manager components available')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'App component test failed: {e}')
|
||||
|
||||
|
||||
class TestAPIEndpoints(unittest.TestCase):
|
||||
"""Test class for API endpoint validation."""
|
||||
|
||||
def test_api_response_structure(self):
|
||||
"""Test that API endpoints return proper JSON structure."""
|
||||
try:
|
||||
# Test that we can import the auth module for API responses
|
||||
from auth import SessionManager
|
||||
|
||||
manager = SessionManager()
|
||||
|
||||
# Test login API response structure
|
||||
response = manager.login('test_password')
|
||||
self.assertIsInstance(response, dict)
|
||||
self.assertIn('success', response)
|
||||
|
||||
print('✓ API response structure validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'API endpoint test failed: {e}')
|
||||
|
||||
|
||||
class TestJavaScriptGeneration(unittest.TestCase):
|
||||
"""Test class for dynamic JavaScript generation."""
|
||||
|
||||
def test_javascript_generation_no_syntax_errors(self):
|
||||
"""Test that generated JavaScript doesn't contain Python syntax."""
|
||||
try:
|
||||
from multi_screen_support import MultiScreenSupportManager
|
||||
|
||||
manager = MultiScreenSupportManager()
|
||||
js_code = manager.get_multiscreen_js()
|
||||
|
||||
# Check for Python-specific syntax that shouldn't be in JS
|
||||
self.assertNotIn('True', js_code, 'JavaScript should use "true", not "True"')
|
||||
self.assertNotIn('False', js_code, 'JavaScript should use "false", not "False"')
|
||||
self.assertNotIn('None', js_code, 'JavaScript should use "null", not "None"')
|
||||
|
||||
# Check for proper JSON serialization indicators
|
||||
self.assertIn('true', js_code.lower())
|
||||
self.assertIn('false', js_code.lower())
|
||||
|
||||
print('✓ JavaScript generation syntax validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'JavaScript generation test failed: {e}')
|
||||
|
||||
def test_f_string_escaping(self):
|
||||
"""Test that f-strings are properly escaped in JavaScript generation."""
|
||||
try:
|
||||
from multi_screen_support import MultiScreenSupportManager
|
||||
|
||||
manager = MultiScreenSupportManager()
|
||||
js_code = manager.get_multiscreen_js()
|
||||
|
||||
# Ensure JavaScript object literals use proper syntax
|
||||
# Look for proper JavaScript object/function syntax
|
||||
self.assertGreater(len(js_code), 0)
|
||||
|
||||
# Check that braces are properly used (not bare Python f-string braces)
|
||||
brace_count = js_code.count('{')
|
||||
self.assertGreater(brace_count, 0)
|
||||
|
||||
print('✓ F-string escaping validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'F-string escaping test failed: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2, buffer=True)
|
||||
242
src/server/tests/test_manager_generation.py
Normal file
242
src/server/tests/test_manager_generation.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
Test suite for manager JavaScript and CSS generation.
|
||||
|
||||
This test module validates that all manager classes can successfully generate
|
||||
their JavaScript and CSS code without runtime errors.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class TestManagerGeneration(unittest.TestCase):
|
||||
"""Test class for validating manager JavaScript/CSS generation."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
self.managers_tested = 0
|
||||
self.total_js_chars = 0
|
||||
self.total_css_chars = 0
|
||||
|
||||
def test_keyboard_shortcut_manager(self):
|
||||
"""Test KeyboardShortcutManager JavaScript generation."""
|
||||
try:
|
||||
from keyboard_shortcuts import KeyboardShortcutManager
|
||||
manager = KeyboardShortcutManager()
|
||||
js = manager.get_shortcuts_js()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.total_js_chars += len(js)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ KeyboardShortcutManager: JS={len(js)} chars (no CSS method)')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'KeyboardShortcutManager failed: {e}')
|
||||
|
||||
def test_drag_drop_manager(self):
|
||||
"""Test DragDropManager JavaScript and CSS generation."""
|
||||
try:
|
||||
from drag_drop import DragDropManager
|
||||
manager = DragDropManager()
|
||||
|
||||
js = manager.get_drag_drop_js()
|
||||
css = manager.get_css()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.assertGreater(len(css), 0)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ DragDropManager: JS={len(js)} chars, CSS={len(css)} chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'DragDropManager failed: {e}')
|
||||
|
||||
def test_accessibility_manager(self):
|
||||
"""Test AccessibilityManager JavaScript and CSS generation."""
|
||||
try:
|
||||
from accessibility_features import AccessibilityManager
|
||||
manager = AccessibilityManager()
|
||||
|
||||
js = manager.get_accessibility_js()
|
||||
css = manager.get_css()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.assertGreater(len(css), 0)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ AccessibilityManager: JS={len(js)} chars, CSS={len(css)} chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'AccessibilityManager failed: {e}')
|
||||
|
||||
def test_user_preferences_manager(self):
|
||||
"""Test UserPreferencesManager JavaScript and CSS generation."""
|
||||
try:
|
||||
from user_preferences import UserPreferencesManager
|
||||
manager = UserPreferencesManager()
|
||||
|
||||
js = manager.get_preferences_js()
|
||||
css = manager.get_css()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.assertGreater(len(css), 0)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ UserPreferencesManager: JS={len(js)} chars, CSS={len(css)} chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'UserPreferencesManager failed: {e}')
|
||||
|
||||
def test_advanced_search_manager(self):
|
||||
"""Test AdvancedSearchManager JavaScript and CSS generation."""
|
||||
try:
|
||||
from advanced_search import AdvancedSearchManager
|
||||
manager = AdvancedSearchManager()
|
||||
|
||||
js = manager.get_search_js()
|
||||
css = manager.get_css()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.assertGreater(len(css), 0)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ AdvancedSearchManager: JS={len(js)} chars, CSS={len(css)} chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'AdvancedSearchManager failed: {e}')
|
||||
|
||||
def test_undo_redo_manager(self):
|
||||
"""Test UndoRedoManager JavaScript and CSS generation."""
|
||||
try:
|
||||
from undo_redo_manager import UndoRedoManager
|
||||
manager = UndoRedoManager()
|
||||
|
||||
js = manager.get_undo_redo_js()
|
||||
css = manager.get_css()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.assertGreater(len(css), 0)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ UndoRedoManager: JS={len(js)} chars, CSS={len(css)} chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'UndoRedoManager failed: {e}')
|
||||
|
||||
def test_all_managers_comprehensive(self):
|
||||
"""Comprehensive test to ensure all managers work together."""
|
||||
expected_managers = 6 # Total number of managers we expect to test
|
||||
|
||||
# Run all individual tests first
|
||||
self.test_keyboard_shortcut_manager()
|
||||
self.test_drag_drop_manager()
|
||||
self.test_accessibility_manager()
|
||||
self.test_user_preferences_manager()
|
||||
self.test_advanced_search_manager()
|
||||
self.test_undo_redo_manager()
|
||||
|
||||
# Validate overall results
|
||||
self.assertEqual(self.managers_tested, expected_managers)
|
||||
self.assertGreater(self.total_js_chars, 0)
|
||||
self.assertGreater(self.total_css_chars, 0)
|
||||
|
||||
print(f'\n=== COMPREHENSIVE TEST SUMMARY ===')
|
||||
print(f'Managers tested: {self.managers_tested}/{expected_managers}')
|
||||
print(f'Total JavaScript generated: {self.total_js_chars:,} characters')
|
||||
print(f'Total CSS generated: {self.total_css_chars:,} characters')
|
||||
print('🎉 All manager JavaScript/CSS generation tests passed!')
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after each test method."""
|
||||
pass
|
||||
|
||||
|
||||
class TestManagerMethods(unittest.TestCase):
|
||||
"""Test class for validating specific manager methods."""
|
||||
|
||||
def test_keyboard_shortcuts_methods(self):
|
||||
"""Test that KeyboardShortcutManager has required methods."""
|
||||
try:
|
||||
from keyboard_shortcuts import KeyboardShortcutManager
|
||||
manager = KeyboardShortcutManager()
|
||||
|
||||
# Test that required methods exist
|
||||
self.assertTrue(hasattr(manager, 'get_shortcuts_js'))
|
||||
self.assertTrue(hasattr(manager, 'setEnabled'))
|
||||
self.assertTrue(hasattr(manager, 'updateShortcuts'))
|
||||
|
||||
# Test method calls
|
||||
self.assertIsNotNone(manager.get_shortcuts_js())
|
||||
|
||||
print('✓ KeyboardShortcutManager methods validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'KeyboardShortcutManager method test failed: {e}')
|
||||
|
||||
def test_screen_reader_methods(self):
|
||||
"""Test that ScreenReaderSupportManager has required methods."""
|
||||
try:
|
||||
from screen_reader_support import ScreenReaderManager
|
||||
manager = ScreenReaderManager()
|
||||
|
||||
# Test that required methods exist
|
||||
self.assertTrue(hasattr(manager, 'get_screen_reader_js'))
|
||||
self.assertTrue(hasattr(manager, 'enhanceFormElements'))
|
||||
self.assertTrue(hasattr(manager, 'generateId'))
|
||||
|
||||
print('✓ ScreenReaderSupportManager methods validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'ScreenReaderSupportManager method test failed: {e}')
|
||||
|
||||
def test_user_preferences_initialization(self):
|
||||
"""Test that UserPreferencesManager initializes correctly."""
|
||||
try:
|
||||
from user_preferences import UserPreferencesManager
|
||||
|
||||
# Test initialization without Flask app
|
||||
manager = UserPreferencesManager()
|
||||
self.assertTrue(hasattr(manager, 'preferences'))
|
||||
self.assertIsInstance(manager.preferences, dict)
|
||||
self.assertGreater(len(manager.preferences), 0)
|
||||
|
||||
print('✓ UserPreferencesManager initialization validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'UserPreferencesManager initialization test failed: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configure test runner
|
||||
unittest.main(verbosity=2, buffer=True)
|
||||
@@ -560,12 +560,31 @@ class UndoRedoManager {
|
||||
this.setupEventListeners();
|
||||
this.updateButtonStates();
|
||||
|
||||
// Update states periodically
|
||||
setInterval(() => {
|
||||
this.updateButtonStates();
|
||||
// Update states periodically with backoff on failures
|
||||
this.failureCount = 0;
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.updateButtonStatesWithBackoff();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async updateButtonStatesWithBackoff() {
|
||||
// If we've had multiple failures, reduce frequency
|
||||
if (this.failureCount > 0) {
|
||||
const backoffTime = Math.min(this.failureCount * 1000, 10000); // Max 10 second backoff
|
||||
if (Date.now() - this.lastFailure < backoffTime) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const success = await this.updateButtonStates();
|
||||
if (success === false) {
|
||||
this.failureCount++;
|
||||
this.lastFailure = Date.now();
|
||||
} else if (success === true) {
|
||||
this.failureCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
createUndoRedoInterface() {
|
||||
this.createUndoRedoButtons();
|
||||
this.createHistoryPanel();
|
||||
@@ -800,6 +819,21 @@ class UndoRedoManager {
|
||||
async updateButtonStates() {
|
||||
try {
|
||||
const response = await fetch('/api/undo-redo/status');
|
||||
|
||||
// Check if response is OK and has JSON content type
|
||||
if (!response.ok) {
|
||||
if (response.status !== 404) {
|
||||
console.warn('Undo/redo status API returned error:', response.status);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
console.warn('Undo/redo status API returned non-JSON response');
|
||||
return false;
|
||||
}
|
||||
|
||||
const status = await response.json();
|
||||
|
||||
const undoBtn = document.getElementById('undo-btn');
|
||||
@@ -823,8 +857,16 @@ class UndoRedoManager {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
// Silently handle network errors to avoid spamming console
|
||||
if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
|
||||
// Network error - server not available
|
||||
return false;
|
||||
}
|
||||
console.error('Error updating undo/redo states:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class UserPreferencesManager:
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
self.preferences_file = 'user_preferences.json'
|
||||
self.preferences = {} # Initialize preferences attribute
|
||||
self.default_preferences = {
|
||||
'ui': {
|
||||
'theme': 'auto', # 'light', 'dark', 'auto'
|
||||
@@ -66,6 +67,12 @@ class UserPreferencesManager:
|
||||
}
|
||||
}
|
||||
|
||||
# Initialize with defaults if no app provided
|
||||
if app is None:
|
||||
self.preferences = self.default_preferences.copy()
|
||||
else:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
"""Initialize with Flask app."""
|
||||
self.app = app
|
||||
|
||||
Reference in New Issue
Block a user