Compare commits

...

2 Commits

Author SHA1 Message Date
083eefe697 some routing fixes 2025-09-29 15:53:18 +02:00
54ca564db8 fix routing issue 2025-09-29 15:14:06 +02:00
6 changed files with 7825 additions and 51 deletions

View File

@ -14,7 +14,7 @@ from flask_socketio import SocketIO, emit
import logging import logging
import atexit import atexit
from main import SeriesApp from Main import SeriesApp
from server.core.entities.series import Serie from server.core.entities.series import Serie
from server.core.entities import SerieList from server.core.entities import SerieList
from server.infrastructure.file_system import SerieScanner from server.infrastructure.file_system import SerieScanner
@ -89,11 +89,26 @@ app.register_blueprint(health_bp)
# Keep only series_app for backward compatibility # Keep only series_app for backward compatibility
series_app = None series_app = None
def init_series_app(): def init_series_app(verbose=True):
"""Initialize the SeriesApp with configuration directory.""" """Initialize the SeriesApp with configuration directory."""
global series_app global series_app
directory_to_search = config.anime_directory try:
series_app = SeriesApp(directory_to_search) directory_to_search = config.anime_directory
if verbose:
print(f"Initializing SeriesApp with directory: {directory_to_search}")
series_app = SeriesApp(directory_to_search)
if verbose:
print(f"SeriesApp initialized successfully. List length: {len(series_app.List.GetList()) if series_app.List else 'No List'}")
return series_app
except Exception as e:
print(f"Error initializing SeriesApp: {e}")
import traceback
traceback.print_exc()
return None
def get_series_app():
"""Get the current series app instance."""
global series_app
return series_app return series_app
# Register WebSocket handlers # Register WebSocket handlers
@ -172,10 +187,7 @@ if __name__ == '__main__':
# Only run initialization and logging setup in the main process # Only run initialization and logging setup in the main process
# This prevents duplicate initialization when Flask debug reloader starts # This prevents duplicate initialization when Flask debug reloader starts
# Initialize the series app # Configure enhanced logging system first
init_series_app()
# Configure enhanced logging system
try: try:
from server.infrastructure.logging.config import get_logger, logging_config from server.infrastructure.logging.config import get_logger, logging_config
logger = get_logger(__name__, 'webapp') logger = get_logger(__name__, 'webapp')
@ -186,7 +198,34 @@ if __name__ == '__main__':
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.warning("Using fallback logging - enhanced logging not available") logger.warning("Using fallback logging - enhanced logging not available")
# Only display startup messages if we're not in the reloader child process if __name__ == '__main__':
# Configure enhanced logging system first
try:
from server.infrastructure.logging.config import get_logger, logging_config
logger = get_logger(__name__, 'webapp')
logger.info("Enhanced logging system initialized")
except ImportError:
# Fallback to basic logging with UTF-8 support
import logging
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%H:%M:%S',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
logger.warning("Using fallback logging - enhanced logging not available")
# Try to configure console for UTF-8 on Windows
try:
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
except Exception:
pass
# Only run startup messages and scheduler in the parent process
if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
logger.info("Starting Aniworld Flask server...") logger.info("Starting Aniworld Flask server...")
logger.info(f"Anime directory: {config.anime_directory}") logger.info(f"Anime directory: {config.anime_directory}")
@ -200,6 +239,9 @@ if __name__ == '__main__':
logger.info("Scheduled operations disabled") logger.info("Scheduled operations disabled")
logger.info("Server will be available at http://localhost:5000") logger.info("Server will be available at http://localhost:5000")
else:
# Initialize the series app only in the reloader child process (the actual working process)
init_series_app(verbose=True)
try: try:
# Run with SocketIO # Run with SocketIO

View File

@ -12,6 +12,43 @@ from typing import Optional
from config import config from config import config
class UnicodeStreamHandler(logging.StreamHandler):
"""Custom stream handler that safely handles Unicode characters."""
def __init__(self, stream=None):
super().__init__(stream)
def emit(self, record):
try:
msg = self.format(record)
stream = self.stream
# Handle Unicode encoding issues on Windows
if hasattr(stream, 'encoding') and stream.encoding:
try:
# Try to encode with the stream's encoding
encoded_msg = msg.encode(stream.encoding, errors='replace').decode(stream.encoding)
stream.write(encoded_msg + self.terminator)
except (UnicodeEncodeError, UnicodeDecodeError):
# Fallback: replace problematic characters
safe_msg = msg.encode('ascii', errors='replace').decode('ascii')
stream.write(safe_msg + self.terminator)
else:
# No encoding info, write directly but catch errors
try:
stream.write(msg + self.terminator)
except UnicodeEncodeError:
# Last resort: ASCII-only output
safe_msg = msg.encode('ascii', errors='replace').decode('ascii')
stream.write(safe_msg + self.terminator)
self.flush()
except RecursionError:
raise
except Exception:
self.handleError(record)
class Fail2BanFormatter(logging.Formatter): class Fail2BanFormatter(logging.Formatter):
"""Custom formatter for fail2ban compatible authentication failure logs.""" """Custom formatter for fail2ban compatible authentication failure logs."""
@ -33,8 +70,14 @@ class StructuredFormatter(logging.Formatter):
# Add component info # Add component info
component = getattr(record, 'component', record.name) component = getattr(record, 'component', record.name)
# Safely get message and handle Unicode
try:
message = record.getMessage()
except (UnicodeEncodeError, UnicodeDecodeError):
message = str(record.msg)
# Format: timestamp - level - component - function - message # Format: timestamp - level - component - function - message
formatted = f"{record.asctime} - {record.levelname:8} - {component:15} - {record.funcName:20} - {record.getMessage()}" formatted = f"{record.asctime} - {record.levelname:8} - {component:15} - {record.funcName:20} - {message}"
# Add exception info if present # Add exception info if present
if record.exc_info: if record.exc_info:
@ -49,7 +92,16 @@ class ConsoleOnlyFormatter(logging.Formatter):
def format(self, record): def format(self, record):
# Only show timestamp, level and message for console # Only show timestamp, level and message for console
timestamp = datetime.now().strftime('%H:%M:%S') timestamp = datetime.now().strftime('%H:%M:%S')
return f"[{timestamp}] {record.levelname}: {record.getMessage()}" try:
message = record.getMessage()
# Ensure the message can be safely encoded
if isinstance(message, str):
# Replace problematic Unicode characters with safe alternatives
message = message.encode('ascii', errors='replace').decode('ascii')
except (UnicodeEncodeError, UnicodeDecodeError):
message = str(record.msg)
return f"[{timestamp}] {record.levelname}: {message}"
class LoggingConfig: class LoggingConfig:
@ -96,7 +148,7 @@ class LoggingConfig:
# Console handler (optional, controlled by config) # Console handler (optional, controlled by config)
if console_logging: if console_logging:
console_handler = logging.StreamHandler(sys.stdout) console_handler = UnicodeStreamHandler(sys.stdout)
console_handler.setLevel(numeric_level) console_handler.setLevel(numeric_level)
console_handler.setFormatter(ConsoleOnlyFormatter()) console_handler.setFormatter(ConsoleOnlyFormatter())
root_logger.addHandler(console_handler) root_logger.addHandler(console_handler)

File diff suppressed because it is too large Load Diff

38
src/server/test_api.py Normal file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""
Simple script to test the API endpoint without crashing the server.
"""
import requests
import json
import time
def test_api():
url = "http://localhost:5000/api/series"
try:
print("Testing API endpoint...")
response = requests.get(url, timeout=30)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(f"Response status: {data.get('status', 'unknown')}")
print(f"Total series: {data.get('total_series', 0)}")
print(f"Message: {data.get('message', 'No message')}")
# Print first few series
series = data.get('series', [])
if series:
print(f"\nFirst 3 series:")
for i, serie in enumerate(series[:3]):
print(f" {i+1}. {serie.get('name', 'Unknown')} ({serie.get('folder', 'Unknown folder')})")
else:
print("No series found in response")
else:
print(f"Error: {response.text}")
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
test_api()

View File

@ -79,26 +79,30 @@ def init_series_app():
"""Initialize the SeriesApp with configuration directory.""" """Initialize the SeriesApp with configuration directory."""
global series_app global series_app
from config import config from config import config
from main import SeriesApp from Main import SeriesApp
directory_to_search = config.anime_directory directory_to_search = config.anime_directory
series_app = SeriesApp(directory_to_search) series_app = SeriesApp(directory_to_search)
return series_app return series_app
def get_series_app(): def get_series_app():
"""Get the current series app instance, initializing if needed.""" """Get the current series app instance from the main app."""
global series_app global series_app
if series_app is None: try:
# Try to get it from the main app module first print("API: Attempting to get series app from main app...")
try: import app
import app series_app_from_main = app.get_series_app()
if hasattr(app, 'series_app') and app.series_app is not None: print(f"API: Got series app from main app: {series_app_from_main is not None}")
series_app = app.series_app return series_app_from_main
return series_app except ImportError as ie:
except ImportError: print(f"API: Import error getting app module: {ie}")
pass # Fallback: initialize our own if app module isn't available
# If not available, initialize it if series_app is None:
init_series_app() print("API: Initializing fallback series app...")
return series_app init_series_app()
return series_app
except Exception as e:
print(f"API: Error getting series app: {e}")
return None
# Import socketio instance - this will need to be passed from app.py # Import socketio instance - this will need to be passed from app.py
socketio = None socketio = None
@ -147,8 +151,12 @@ def update_directory():
def get_series(): def get_series():
"""Get all series data.""" """Get all series data."""
try: try:
print("API: Getting series app...")
current_series_app = get_series_app() current_series_app = get_series_app()
print(f"API: Series app obtained: {current_series_app is not None}")
if current_series_app is None or current_series_app.List is None: if current_series_app is None or current_series_app.List is None:
print("API: No series app or list available")
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
'series': [], 'series': [],
@ -156,21 +164,47 @@ def get_series():
'message': 'No series data available. Please perform a scan to load series.' 'message': 'No series data available. Please perform a scan to load series.'
}) })
print(f"API: Getting series list...")
series_list = current_series_app.List.GetList()
print(f"API: Series list length: {len(series_list)}")
# Get series data # Get series data
series_data = [] series_data = []
for serie in current_series_app.List.GetList(): for i, serie in enumerate(series_list):
series_data.append({ try:
'folder': serie.folder, print(f"API: Processing serie {i+1}/{len(series_list)}: {getattr(serie, 'folder', 'unknown')}")
'name': serie.name or serie.folder,
'total_episodes': sum(len(episodes) for episodes in serie.episodeDict.values()), # Safely get serie properties
'missing_episodes': sum(len(episodes) for episodes in serie.episodeDict.values()), folder = getattr(serie, 'folder', f'serie_{i}')
'status': 'ongoing', name = getattr(serie, 'name', None) or folder
'episodes': { episode_dict = getattr(serie, 'episodeDict', {})
season: episodes
for season, episodes in serie.episodeDict.items() # Calculate episodes safely
} total_episodes = 0
}) for season, episodes in episode_dict.items():
if episodes and hasattr(episodes, '__len__'):
total_episodes += len(episodes)
series_data.append({
'folder': folder,
'name': name,
'total_episodes': total_episodes,
'missing_episodes': total_episodes, # For now, assume all are missing
'status': 'ongoing',
'episodes': dict(episode_dict) if episode_dict else {}
})
# Limit to first 50 series to avoid timeout
if len(series_data) >= 50:
print("API: Limiting to first 50 series to prevent timeout")
break
except Exception as serie_error:
print(f"API: Error processing serie {i}: {serie_error}")
# Continue with next serie
continue
print(f"API: Returning {len(series_data)} series")
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
'series': series_data, 'series': series_data,
@ -180,6 +214,8 @@ def get_series():
except Exception as e: except Exception as e:
# Log the error but don't return 500 to prevent page reload loops # Log the error but don't return 500 to prevent page reload loops
print(f"Error in get_series: {e}") print(f"Error in get_series: {e}")
import traceback
traceback.print_exc()
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
'series': [], 'series': [],

View File

@ -9,7 +9,7 @@
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<!-- UX Enhancement and Mobile & Accessibility CSS --> <!-- UX Enhancement and Mobile & Accessibility CSS -->
<link rel="stylesheet" href="{{ url_for('ux_features_css') }}"> <link rel="stylesheet" href="{{ url_for('static.ux_features_css') }}">
</head> </head>
<body> <body>
@ -459,20 +459,20 @@
<script src="{{ url_for('static', filename='js/localization.js') }}"></script> <script src="{{ url_for('static', filename='js/localization.js') }}"></script>
<!-- UX Enhancement Scripts --> <!-- UX Enhancement Scripts -->
<script src="{{ url_for('keyboard_shortcuts_js') }}"></script> <script src="{{ url_for('static.keyboard_shortcuts_js') }}"></script>
<script src="{{ url_for('drag_drop_js') }}"></script> <script src="{{ url_for('static.drag_drop_js') }}"></script>
<script src="{{ url_for('bulk_operations_js') }}"></script> <script src="{{ url_for('static.bulk_operations_js') }}"></script>
<script src="{{ url_for('user_preferences_js') }}"></script> <script src="{{ url_for('static.user_preferences_js') }}"></script>
<script src="{{ url_for('advanced_search_js') }}"></script> <script src="{{ url_for('static.advanced_search_js') }}"></script>
<script src="{{ url_for('undo_redo_js') }}"></script> <script src="{{ url_for('static.undo_redo_js') }}"></script>
<!-- Mobile & Accessibility Scripts --> <!-- Mobile & Accessibility Scripts -->
<script src="{{ url_for('mobile_responsive_js') }}"></script> <script src="{{ url_for('static.mobile_responsive_js') }}"></script>
<script src="{{ url_for('touch_gestures_js') }}"></script> <script src="{{ url_for('static.touch_gestures_js') }}"></script>
<script src="{{ url_for('accessibility_features_js') }}"></script> <script src="{{ url_for('static.accessibility_features_js') }}"></script>
<script src="{{ url_for('screen_reader_support_js') }}"></script> <script src="{{ url_for('static.screen_reader_support_js') }}"></script>
<script src="{{ url_for('color_contrast_compliance_js') }}"></script> <script src="{{ url_for('static.color_contrast_compliance_js') }}"></script>
<script src="{{ url_for('multi_screen_support_js') }}"></script> <script src="{{ url_for('static.multi_screen_support_js') }}"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script> <script src="{{ url_for('static', filename='js/app.js') }}"></script>
</body> </body>