Compare commits

...

6 Commits

Author SHA1 Message Date
39991d9ffc fix: anime api 2025-10-26 19:28:23 +01:00
75aa410f98 fixed: recan issues 2025-10-26 19:14:11 +01:00
12688b9770 better logging 2025-10-25 17:54:18 +02:00
eb4be2926b better logging 2025-10-25 17:44:01 +02:00
94c53e9555 feat: Add comprehensive logging system with console and file output
- Create logging infrastructure in src/infrastructure/logging/
  - logger.py: Main logging setup with console and file handlers
  - uvicorn_config.py: Custom uvicorn logging configuration
  - __init__.py: Export public logging API

- Update FastAPI application to use logging
  - Replace all print() statements with proper logger calls
  - Initialize logging during application startup
  - Add detailed startup/shutdown logging

- Add startup scripts
  - run_server.py: Python script with uvicorn logging config
  - start_server.sh: Bash wrapper script

- Add comprehensive documentation
  - docs/logging.md: User guide for logging system
  - docs/logging_implementation_summary.md: Technical implementation details

Features:
- Console logging with clean, readable format
- File logging with timestamps to logs/fastapi_app.log
- Configurable log level via LOG_LEVEL environment variable
- Proper lazy formatting for performance
- Captures all uvicorn, application, and module logs
- Automatic log directory creation
2025-10-25 17:40:20 +02:00
a41c86f1da refactor: remove GlobalLogger and migrate to standard Python logging
- Remove src/infrastructure/logging/GlobalLogger.py
- Update SerieScanner.py to use standard logging.getLogger()
- Update aniworld_provider.py to remove custom noKeyFound_logger setup
- Fix test_dependencies.py to properly mock config_service
- Fix code style issues (line length, formatting)
- All 846 tests passing
2025-10-25 17:27:49 +02:00
19 changed files with 851 additions and 544 deletions

View File

@ -1,16 +0,0 @@
{
"created_at": "2025-10-23T20:54:38.147564",
"last_updated": "2025-10-23T20:54:38.147574",
"download_stats": {
"total_downloads": 0,
"successful_downloads": 0,
"failed_downloads": 0,
"total_bytes_downloaded": 0,
"average_speed_mbps": 0.0,
"success_rate": 0.0,
"average_duration_seconds": 0.0
},
"series_popularity": [],
"storage_history": [],
"performance_samples": []
}

View File

@ -17,8 +17,8 @@
"keep_days": 30 "keep_days": 30
}, },
"other": { "other": {
"anime_directory": "/home/lukas/Volume/serien/", "master_password_hash": "$pbkdf2-sha256$29000$pRSCMOZcy1mLUeo951zrXQ$8/lWKoHbJJQDk2j7fM9RYrpLyxu3xwJXSpISYfs7jnM",
"master_password_hash": "$pbkdf2-sha256$29000$ZWwtJaQ0ZkxpLUWolRJijA$QcfgTBqgM3ABu9N93/w8naBLdfCKmKFc65Cn/f4fP84" "anime_directory": "/home/lukas/Volume/serien/"
}, },
"version": "1.0.0" "version": "1.0.0"
} }

View File

@ -1,425 +0,0 @@
{
"pending": [
{
"id": "7cc643ca-0b4e-4769-8d25-c99ce539b434",
"serie_id": "workflow-series",
"serie_name": "Workflow Test Series",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "high",
"added_at": "2025-10-24T17:23:26.098284Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "6a017a0d-78e2-4123-9715-80b540e03c41",
"serie_id": "series-2",
"serie_name": "Series 2",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.819219Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "e31ecefa-470a-4ea6-aaa0-c16d38d5ab8b",
"serie_id": "series-1",
"serie_name": "Series 1",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.816100Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "e3b9418c-7b1e-47dc-928c-3746059a0fa8",
"serie_id": "series-0",
"serie_name": "Series 0",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.812680Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "77083b3b-8b7b-4e02-a4c9-0e95652b1865",
"serie_id": "series-high",
"serie_name": "Series High",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "high",
"added_at": "2025-10-24T17:23:25.591277Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "03fa75a1-0641-41e8-be69-c274383d6198",
"serie_id": "test-series-2",
"serie_name": "Another Series",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "high",
"added_at": "2025-10-24T17:23:25.567577Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "bbfa8dd3-0f28-43f3-9f42-03595684e873",
"serie_id": "test-series-1",
"serie_name": "Test Anime Series",
"episode": {
"season": 1,
"episode": 1,
"title": "Episode 1"
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.543811Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "4d462a39-e705-4dd4-a968-e6d995471615",
"serie_id": "test-series-1",
"serie_name": "Test Anime Series",
"episode": {
"season": 1,
"episode": 2,
"title": "Episode 2"
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.543911Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "04e5ce5d-ce4c-4776-a1be-b0c78c17d651",
"serie_id": "series-normal",
"serie_name": "Series Normal",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.593205Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "8a8da509-9bec-4979-aa01-22f726e298ef",
"serie_id": "series-low",
"serie_name": "Series Low",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "low",
"added_at": "2025-10-24T17:23:25.595371Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "b07b9e02-3517-4066-aba0-2ee6b2349580",
"serie_id": "test-series",
"serie_name": "Test Series",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.760199Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "9577295e-7ac6-4786-8601-ac13267aba9f",
"serie_id": "test-series",
"serie_name": "Test Series",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.850731Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "562ce52c-2979-4107-b630-999ff6c095e9",
"serie_id": "invalid-series",
"serie_name": "Invalid Series",
"episode": {
"season": 99,
"episode": 99,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.902493Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "1684fe7f-5755-4064-86ed-a78831e8dc0f",
"serie_id": "test-series",
"serie_name": "Test Series",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.926933Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "c4fe86cb-e6f7-4303-a8b6-2e76c51d7c40",
"serie_id": "series-4",
"serie_name": "Series 4",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.965540Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "94d7d85c-911e-495b-9203-065324594c74",
"serie_id": "series-0",
"serie_name": "Series 0",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.966417Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "1d8e1cda-ff78-4ab8-a040-2f325d53666a",
"serie_id": "series-3",
"serie_name": "Series 3",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.967083Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "f9b4174e-f809-4272-bcd8-f9bd44238d3c",
"serie_id": "series-2",
"serie_name": "Series 2",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.967759Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "b41f4c2a-40d6-4205-b769-c3a77df8df5e",
"serie_id": "series-1",
"serie_name": "Series 1",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:25.968503Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "ae4e67dd-b77f-4fbe-8d4c-19fe979f6783",
"serie_id": "persistent-series",
"serie_name": "Persistent Series",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:26.027365Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "5dc0b529-627c-47ed-8f2a-55112d78de93",
"serie_id": "ws-series",
"serie_name": "WebSocket Series",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:26.073822Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "44f479fd-61f7-4279-ace1-5fbf31dad243",
"serie_id": "pause-test",
"serie_name": "Pause Test Series",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-24T17:23:26.227077Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
}
],
"active": [],
"failed": [],
"timestamp": "2025-10-24T17:23:26.227320+00:00"
}

155
docs/logging.md Normal file
View File

@ -0,0 +1,155 @@
# Logging Configuration
This document describes the logging setup for the Aniworld FastAPI application.
## Overview
The application uses Python's built-in `logging` module with both console and file output. All logs are written to:
- **Console**: Colored output for development
- **Log File**: `logs/fastapi_app.log` with detailed timestamps
## Log Levels
By default, the application logs at `INFO` level. You can change this by setting the `LOG_LEVEL` environment variable:
```bash
export LOG_LEVEL=DEBUG # More verbose
export LOG_LEVEL=INFO # Default
export LOG_LEVEL=WARNING # Less verbose
export LOG_LEVEL=ERROR # Errors only
```
Or in your `.env` file:
```
LOG_LEVEL=INFO
```
## Running the Server
### Option 1: Using the run_server.py script (Recommended)
```bash
conda run -n AniWorld python run_server.py
```
This script uses the custom uvicorn logging configuration that ensures proper console and file logging.
### Option 2: Using the shell script
```bash
./start_server.sh
```
### Option 3: Using uvicorn directly
```bash
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload
```
**Note**: When using `conda run`, console output may not be visible in real-time. The logs will still be written to the file.
## Log File Location
All logs are written to: `logs/fastapi_app.log`
To view logs in real-time:
```bash
tail -f logs/fastapi_app.log
```
## Log Format
### Console Output
```
INFO: Starting FastAPI application...
INFO: Server running on http://127.0.0.1:8000
```
### File Output
```
2025-10-25 17:31:19 - aniworld - INFO - Starting FastAPI application...
2025-10-25 17:31:19 - aniworld - INFO - Server running on http://127.0.0.1:8000
```
## What Gets Logged
The application logs:
- **Startup/Shutdown**: Application lifecycle events
- **Configuration**: Loaded settings and configuration
- **HTTP Requests**: Via uvicorn.access logger
- **Errors**: Exception tracebacks with full context
- **WebSocket Events**: Connection/disconnection events
- **Download Progress**: Progress updates for anime downloads
- **File Operations**: File creation, deletion, scanning
## Logger Names
Different parts of the application use different logger names:
- `aniworld`: Main application logger
- `uvicorn.error`: Uvicorn server errors
- `uvicorn.access`: HTTP request logs
- `src.core.SeriesApp`: Core anime logic
- `src.core.SerieScanner`: File scanning operations
- `src.server.*`: Web API endpoints and services
## Programmatic Usage
To use logging in your code:
```python
from src.infrastructure.logging import get_logger
logger = get_logger(__name__)
logger.info("This is an info message")
logger.warning("This is a warning")
logger.error("This is an error", exc_info=True) # Includes traceback
```
## Log Rotation
Log files can grow large over time. Consider implementing log rotation:
```bash
# Archive old logs
mkdir -p logs/archived
mv logs/fastapi_app.log logs/archived/fastapi_app_$(date +%Y%m%d_%H%M%S).log
```
Or use Python's `RotatingFileHandler` (can be added to `src/infrastructure/logging/logger.py`).
## Troubleshooting
### No console output when using `conda run`
This is a known limitation of `conda run`. The logs are still being written to the file. To see console output:
1. Use the log file: `tail -f logs/fastapi_app.log`
2. Or run without conda: `python run_server.py` (after activating environment with `conda activate AniWorld`)
### Log file not created
- Check that the `logs/` directory exists (it's created automatically)
- Verify write permissions on the `logs/` directory
- Check the `LOG_LEVEL` environment variable
### Too much logging
Set a higher log level:
```bash
export LOG_LEVEL=WARNING
```
### Missing logs
- Check that you're using the logger, not `print()`
- Verify the log level is appropriate for your messages
- Ensure the logger is properly configured (should happen automatically on startup)

View File

@ -0,0 +1,169 @@
# Logging Implementation Summary
## What Was Implemented
### 1. Core Logging Infrastructure (`src/infrastructure/logging/`)
- **`logger.py`**: Main logging configuration module
- `setup_logging()`: Configures both console and file handlers
- `get_logger()`: Retrieves logger instances for specific modules
- Follows Python logging best practices with proper formatters
- **`uvicorn_config.py`**: Uvicorn-specific logging configuration
- Custom logging configuration dictionary for uvicorn
- Ensures uvicorn logs are captured in both console and file
- Configures multiple loggers (uvicorn, uvicorn.error, uvicorn.access, aniworld)
- **`__init__.py`**: Package initialization
- Exports public API: `setup_logging`, `get_logger`, `get_uvicorn_log_config`
### 2. FastAPI Integration
Updated `src/server/fastapi_app.py` to:
- Import and use the logging infrastructure
- Call `setup_logging()` during application startup (in `lifespan()`)
- Replace all `print()` statements with proper logger calls
- Use lazy formatting (`logger.info("Message: %s", value)`)
### 3. Startup Scripts
- **`run_server.py`**: Python startup script
- Uses the custom uvicorn logging configuration
- Recommended way to start the server
- **`start_server.sh`**: Bash startup script
- Wrapper around `run_server.py`
- Made executable with proper shebang
### 4. Documentation
- **`docs/logging.md`**: Comprehensive logging guide
- How to run the server
- Log file locations
- Log format examples
- Troubleshooting guide
- Programmatic usage examples
## Log Outputs
### Console Output
```
INFO: Starting FastAPI application...
INFO: Loaded anime_directory from config: /home/lukas/Volume/serien/
INFO: Server running on http://127.0.0.1:8000
INFO: API documentation available at http://127.0.0.1:8000/api/docs
```
### File Output (`logs/fastapi_app.log`)
```
2025-10-25 17:31:19 - aniworld - INFO - ============================================================
2025-10-25 17:31:19 - aniworld - INFO - Logging configured successfully
2025-10-25 17:31:19 - aniworld - INFO - Log level: INFO
2025-10-25 17:31:19 - aniworld - INFO - Log file: /home/lukas/Volume/repo/Aniworld/logs/fastapi_app.log
2025-10-25 17:31:19 - aniworld - INFO - ============================================================
2025-10-25 17:31:19 - aniworld - INFO - Starting FastAPI application...
2025-10-25 17:31:19 - aniworld - INFO - Loaded anime_directory from config: /home/lukas/Volume/serien/
2025-10-25 17:31:19 - src.core.SeriesApp - INFO - Initializing SeriesApp...
2025-10-25 17:31:19 - src.core.SerieScanner - INFO - Initialized SerieScanner...
2025-10-25 17:31:19 - aniworld - INFO - SeriesApp initialized with directory: /home/lukas/Volume/serien/
2025-10-25 17:31:19 - aniworld - INFO - FastAPI application started successfully
2025-10-25 17:31:19 - aniworld - INFO - Server running on http://127.0.0.1:8000
2025-10-25 17:31:19 - aniworld - INFO - API documentation available at http://127.0.0.1:8000/api/docs
```
## How to Use
### Starting the Server
**Recommended:**
```bash
conda run -n AniWorld python run_server.py
```
**Alternative:**
```bash
./start_server.sh
```
**View logs in real-time:**
```bash
tail -f logs/fastapi_app.log
```
### In Code
```python
from src.infrastructure.logging import get_logger
logger = get_logger(__name__)
logger.info("Message: %s", value)
logger.warning("Warning: %s", warning_msg)
logger.error("Error occurred", exc_info=True)
```
## Configuration
Set log level via environment variable or `.env` file:
```bash
export LOG_LEVEL=INFO # or DEBUG, WARNING, ERROR
```
## Features
**Console logging**: Colored, easy-to-read format
**File logging**: Detailed with timestamps and logger names
**Automatic log directory creation**: `logs/` created if missing
**Uvicorn integration**: All uvicorn logs captured
**Multiple loggers**: Different loggers for different modules
**Configurable log level**: Via environment variable
**Proper formatting**: Uses lazy formatting for performance
**Startup/shutdown logging**: Clear application lifecycle logs
**Error tracebacks**: Full exception context with `exc_info=True`
## Files Created/Modified
### Created:
- `src/infrastructure/logging/logger.py`
- `src/infrastructure/logging/uvicorn_config.py`
- `src/infrastructure/logging/__init__.py`
- `run_server.py`
- `start_server.sh`
- `docs/logging.md`
- `docs/logging_implementation_summary.md` (this file)
### Modified:
- `src/server/fastapi_app.py`: Integrated logging throughout
## Testing
The implementation has been tested and verified:
- ✅ Log file created at `logs/fastapi_app.log`
- ✅ Startup messages logged correctly
- ✅ Application configuration loaded and logged
- ✅ Uvicorn logs captured
- ✅ File watching events logged
- ✅ Shutdown messages logged
## Next Steps
Consider adding:
1. **Log rotation**: Use `RotatingFileHandler` to prevent log files from growing too large
2. **Structured logging**: Use `structlog` for JSON-formatted logs
3. **Log aggregation**: Send logs to a centralized logging service
4. **Performance monitoring**: Add timing logs for slow operations
5. **Request logging middleware**: Log all HTTP requests/responses

View File

27
run_server.py Normal file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""
Startup script for the Aniworld FastAPI application.
This script starts the application with proper logging configuration.
"""
import uvicorn
from src.infrastructure.logging.uvicorn_config import get_uvicorn_log_config
if __name__ == "__main__":
# Get logging configuration
log_config = get_uvicorn_log_config()
# Run the application with logging.
# Only watch .py files in src/, explicitly exclude __pycache__.
# This prevents reload loops from .pyc compilation.
uvicorn.run(
"src.server.fastapi_app:app",
host="127.0.0.1",
port=8000,
reload=True,
reload_dirs=["src"],
reload_includes=["*.py"],
reload_excludes=["*/__pycache__/*", "*.pyc"],
log_config=log_config,
)

View File

@ -23,9 +23,10 @@ from src.core.interfaces.callbacks import (
ProgressPhase, ProgressPhase,
) )
from src.core.providers.base_provider import Loader from src.core.providers.base_provider import Loader
from src.infrastructure.logging.GlobalLogger import error_logger, noKeyFound_logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
error_logger = logging.getLogger("error")
no_key_found_logger = logging.getLogger("series.nokey")
class SerieScanner: class SerieScanner:
@ -174,7 +175,7 @@ class SerieScanner:
# remote metadata, yielding missing episodes per # remote metadata, yielding missing episodes per
# season. Results are saved back to disk so that both # season. Results are saved back to disk so that both
# CLI and API consumers see consistent state. # CLI and API consumers see consistent state.
missing_episodes, site = ( missing_episodes, _site = (
self.__get_missing_episodes_and_season( self.__get_missing_episodes_and_season(
serie.key, mp4_files serie.key, mp4_files
) )
@ -192,14 +193,14 @@ class SerieScanner:
) )
else: else:
self.folderDict[serie.key] = serie self.folderDict[serie.key] = serie
noKeyFound_logger.info( no_key_found_logger.info(
"Saved Serie: '%s'", str(serie) "Saved Serie: '%s'", str(serie)
) )
except NoKeyFoundException as nkfe: except NoKeyFoundException as nkfe:
# Log error and notify via callback # Log error and notify via callback
error_msg = f"Error processing folder '{folder}': {nkfe}" error_msg = f"Error processing folder '{folder}': {nkfe}"
NoKeyFoundException.error(error_msg) logger.error(error_msg)
self._callback_manager.notify_error( self._callback_manager.notify_error(
ErrorContext( ErrorContext(

View File

@ -46,12 +46,7 @@ if not download_error_logger.handlers:
download_error_handler.setLevel(logging.ERROR) download_error_handler.setLevel(logging.ERROR)
download_error_logger.addHandler(download_error_handler) download_error_logger.addHandler(download_error_handler)
noKeyFound_logger = logging.getLogger("NoKeyFound") noKeyFound_logger = logging.getLogger()
if not noKeyFound_logger.handlers:
log_path = _logs_dir / "no_key_found.log"
noKeyFound_handler = logging.FileHandler(str(log_path))
noKeyFound_handler.setLevel(logging.ERROR)
noKeyFound_logger.addHandler(noKeyFound_handler)
class AniworldLoader(Loader): class AniworldLoader(Loader):

View File

@ -1,40 +0,0 @@
import logging
console_handler = None
error_logger = None
noKeyFound_logger = None
noGerFound_logger = None
def setupLogger():
global console_handler, error_logger, noKeyFound_logger, noGerFound_logger
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(funcName)s - %(message)s')
if (console_handler is None):
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter(
"%(asctime)s - %(levelname)s - %(funcName)s - %(message)s")
)
logging.getLogger().addHandler(console_handler)
logging.getLogger("urllib3.connectionpool").setLevel(logging.INFO)
logging.getLogger('charset_normalizer').setLevel(logging.INFO)
logging.getLogger().setLevel(logging.INFO)
if (error_logger is None):
error_logger = logging.getLogger("ErrorLog")
error_handler = logging.FileHandler("../errors.log")
error_handler.setLevel(logging.ERROR)
error_logger.addHandler(error_handler)
if (noKeyFound_logger is None):
noKeyFound_logger = logging.getLogger("NoKeyFound")
noKeyFound_handler = logging.FileHandler("../NoKeyFound.log")
noKeyFound_handler.setLevel(logging.ERROR)
noKeyFound_logger.addHandler(noKeyFound_handler)
if (noGerFound_logger is None):
noGerFound_logger = logging.getLogger("noGerFound")
noGerFound_handler = logging.FileHandler("../noGerFound.log")
noGerFound_handler.setLevel(logging.ERROR)
noGerFound_logger.addHandler(noGerFound_handler)
setupLogger()

View File

@ -0,0 +1,7 @@
"""
Logging infrastructure for the Aniworld application.
"""
from src.infrastructure.logging.logger import get_logger, setup_logging
from src.infrastructure.logging.uvicorn_config import get_uvicorn_log_config
__all__ = ["setup_logging", "get_logger", "get_uvicorn_log_config"]

View File

@ -0,0 +1,100 @@
"""
Logging configuration for the Aniworld application.
This module provides a centralized logging setup with both console and file
logging, following Python logging best practices.
"""
import logging
import sys
from pathlib import Path
from typing import Optional
from src.config.settings import settings
def setup_logging(
log_file: Optional[str] = None,
log_level: Optional[str] = None,
log_dir: Optional[Path] = None
) -> logging.Logger:
"""
Configure application logging with console and file handlers.
Args:
log_file: Name of the log file (default: "fastapi_app.log")
log_level: Logging level (default: from settings or "INFO")
log_dir: Directory for log files (default: "logs" in project root)
Returns:
Configured logger instance
"""
# Determine log level
level_name = log_level or settings.log_level or "INFO"
level = getattr(logging, level_name.upper(), logging.INFO)
# Determine log directory and file
if log_dir is None:
# Default to logs directory in project root
log_dir = Path(__file__).parent.parent.parent.parent / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
if log_file is None:
log_file = "fastapi_app.log"
log_path = log_dir / log_file
# Create formatters
detailed_formatter = logging.Formatter(
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
console_formatter = logging.Formatter(
fmt="%(levelname)s: %(message)s"
)
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(level)
# Remove existing handlers to avoid duplicates
root_logger.handlers.clear()
# Console handler (stdout)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
console_handler.setFormatter(console_formatter)
root_logger.addHandler(console_handler)
# File handler
file_handler = logging.FileHandler(log_path, mode='a', encoding='utf-8')
file_handler.setLevel(level)
file_handler.setFormatter(detailed_formatter)
root_logger.addHandler(file_handler)
# Create application logger
logger = logging.getLogger("aniworld")
logger.setLevel(level)
# Log startup information
logger.info("=" * 60)
logger.info("Logging configured successfully")
logger.info("Log level: %s", level_name.upper())
logger.info("Log file: %s", log_path)
logger.info("=" * 60)
return logger
def get_logger(name: str) -> logging.Logger:
"""
Get a logger instance for a specific module.
Args:
name: Name of the logger (typically __name__)
Returns:
Logger instance
"""
return logging.getLogger(name)

View File

@ -0,0 +1,92 @@
"""
Uvicorn logging configuration for the Aniworld application.
This configuration ensures that uvicorn logs are properly formatted and
written to both console and file.
"""
from pathlib import Path
# Get the logs directory
LOGS_DIR = Path(__file__).parent.parent.parent.parent / "logs"
LOGS_DIR.mkdir(parents=True, exist_ok=True)
LOG_FILE = LOGS_DIR / "fastapi_app.log"
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(message)s",
"use_colors": None,
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": (
'%(levelprefix)s %(client_addr)s - '
'"%(request_line)s" %(status_code)s'
),
},
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "default",
"stream": "ext://sys.stdout",
},
"file": {
"class": "logging.FileHandler",
"level": "INFO",
"formatter": "detailed",
"filename": str(LOG_FILE),
"mode": "a",
"encoding": "utf-8",
},
},
"loggers": {
"uvicorn": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
"uvicorn.error": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
"uvicorn.access": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
"watchfiles.main": {
"handlers": ["console"],
"level": "WARNING",
"propagate": False,
},
"aniworld": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
},
"root": {
"handlers": ["console", "file"],
"level": "INFO",
},
}
def get_uvicorn_log_config() -> dict:
"""
Get the uvicorn logging configuration dictionary.
Returns:
Dictionary containing logging configuration
"""
return LOGGING_CONFIG

View File

@ -95,9 +95,24 @@ async def get_process_locks(
class AnimeSummary(BaseModel): class AnimeSummary(BaseModel):
id: str """Summary of an anime series with missing episodes."""
title: str key: str # Unique identifier (used as id in frontend)
missing_episodes: int name: str # Series name (can be empty)
site: str # Provider site
folder: str # Local folder name
missing_episodes: dict # Episode dictionary: {season: [episode_numbers]}
class Config:
"""Pydantic model configuration."""
json_schema_extra = {
"example": {
"key": "beheneko-the-elf-girls-cat",
"name": "Beheneko",
"site": "aniworld.to",
"folder": "beheneko the elf girls cat (2025)",
"missing_episodes": {"1": [1, 2, 3, 4]}
}
}
class AnimeDetail(BaseModel): class AnimeDetail(BaseModel):
@ -203,26 +218,41 @@ async def list_anime(
series = series_app.List.GetMissingEpisode() series = series_app.List.GetMissingEpisode()
summaries: List[AnimeSummary] = [] summaries: List[AnimeSummary] = []
for serie in series: for serie in series:
episodes_dict = getattr(serie, "episodeDict", {}) or {} # Get all properties from the serie object
missing_episodes = len(episodes_dict) key = getattr(serie, "key", "")
key = getattr(serie, "key", getattr(serie, "folder", "")) name = getattr(serie, "name", "")
title = getattr(serie, "name", "") site = getattr(serie, "site", "")
folder = getattr(serie, "folder", "")
episode_dict = getattr(serie, "episodeDict", {}) or {}
# Convert episode dict keys to strings for JSON serialization
missing_episodes = {str(k): v for k, v in episode_dict.items()}
summaries.append( summaries.append(
AnimeSummary( AnimeSummary(
id=key, key=key,
title=title, name=name,
site=site,
folder=folder,
missing_episodes=missing_episodes, missing_episodes=missing_episodes,
) )
) )
# Apply sorting if requested # Apply sorting if requested
if sort_by: if sort_by:
if sort_by == "title": if sort_by in ["title", "name"]:
summaries.sort(key=lambda x: x.title) summaries.sort(key=lambda x: x.name or x.key)
elif sort_by == "id": elif sort_by == "id":
summaries.sort(key=lambda x: x.id) summaries.sort(key=lambda x: x.key)
elif sort_by == "missing_episodes": elif sort_by == "missing_episodes":
summaries.sort(key=lambda x: x.missing_episodes, reverse=True) # Sort by total number of missing episodes
# (count all episodes across all seasons)
summaries.sort(
key=lambda x: sum(
len(eps) for eps in x.missing_episodes.values()
),
reverse=True
)
return summaries return summaries
except HTTPException: except HTTPException:
@ -239,15 +269,15 @@ async def trigger_rescan(
_auth: dict = Depends(require_auth), _auth: dict = Depends(require_auth),
series_app: Any = Depends(get_series_app), series_app: Any = Depends(get_series_app),
) -> dict: ) -> dict:
"""Kick off a background rescan of the local library. """Kick off a rescan of the local library.
Args: Args:
_auth: Ensures the caller is authenticated (value unused) _auth: Ensures the caller is authenticated (value unused)
series_app: Core `SeriesApp` instance provided via dependency. series_app: Core `SeriesApp` instance provided via dependency.
Returns: Returns:
Dict[str, Any]: Status payload communicating whether the rescan Dict[str, Any]: Status payload with scan results including
launched successfully. number of series found.
Raises: Raises:
HTTPException: If the rescan command is unsupported or fails. HTTPException: If the rescan command is unsupported or fails.
@ -255,8 +285,23 @@ async def trigger_rescan(
try: try:
# SeriesApp.ReScan expects a callback; pass a no-op # SeriesApp.ReScan expects a callback; pass a no-op
if hasattr(series_app, "ReScan"): if hasattr(series_app, "ReScan"):
series_app.ReScan(lambda *args, **kwargs: None) result = series_app.ReScan(lambda *args, **kwargs: None)
return {"success": True, "message": "Rescan started"}
if result.success:
series_count = (
result.data.get("series_count", 0)
if result.data else 0
)
return {
"success": True,
"message": result.message,
"series_count": series_count
}
else:
return {
"success": False,
"message": result.message
}
raise HTTPException( raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, status_code=status.HTTP_501_NOT_IMPLEMENTED,

View File

@ -0,0 +1,122 @@
"""
Logging configuration for the FastAPI server.
This module provides comprehensive logging setup with both console and file
handlers, ensuring all server activity is properly logged.
"""
import logging
import sys
from pathlib import Path
from typing import Dict
from src.config.settings import settings
def setup_logging() -> Dict[str, logging.Logger]:
"""
Configure logging for the FastAPI application.
Creates:
- Console handler for real-time output
- File handler for server.log (general logs)
- File handler for error.log (errors only)
- File handler for access.log (request logs)
Returns:
Dict containing configured loggers
"""
# Create logs directory if it doesn't exist
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
# Define log file paths
server_log_file = log_dir / "server.log"
error_log_file = log_dir / "error.log"
access_log_file = log_dir / "access.log"
# Define log format
detailed_format = logging.Formatter(
fmt="%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
simple_format = logging.Formatter(
fmt="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, settings.log_level.upper(), logging.INFO))
# Remove existing handlers to avoid duplicates
root_logger.handlers.clear()
# Console handler - visible in terminal
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(simple_format)
root_logger.addHandler(console_handler)
# File handler for general server logs
server_file_handler = logging.FileHandler(server_log_file, mode='a', encoding='utf-8')
server_file_handler.setLevel(logging.DEBUG)
server_file_handler.setFormatter(detailed_format)
root_logger.addHandler(server_file_handler)
# File handler for errors only
error_file_handler = logging.FileHandler(error_log_file, mode='a', encoding='utf-8')
error_file_handler.setLevel(logging.ERROR)
error_file_handler.setFormatter(detailed_format)
root_logger.addHandler(error_file_handler)
# Configure uvicorn loggers
uvicorn_logger = logging.getLogger("uvicorn")
uvicorn_logger.setLevel(logging.INFO)
uvicorn_access_logger = logging.getLogger("uvicorn.access")
uvicorn_access_logger.setLevel(logging.INFO)
# Access log file handler
access_file_handler = logging.FileHandler(access_log_file, mode='a', encoding='utf-8')
access_file_handler.setLevel(logging.INFO)
access_file_handler.setFormatter(simple_format)
uvicorn_access_logger.addHandler(access_file_handler)
# Configure FastAPI logger
fastapi_logger = logging.getLogger("fastapi")
fastapi_logger.setLevel(logging.INFO)
# Reduce noise from third-party libraries
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("charset_normalizer").setLevel(logging.WARNING)
logging.getLogger("multipart").setLevel(logging.WARNING)
# Log initial setup
root_logger.info("=" * 80)
root_logger.info("FastAPI Server Logging Initialized")
root_logger.info(f"Log Level: {settings.log_level.upper()}")
root_logger.info(f"Server Log: {server_log_file.absolute()}")
root_logger.info(f"Error Log: {error_log_file.absolute()}")
root_logger.info(f"Access Log: {access_log_file.absolute()}")
root_logger.info("=" * 80)
return {
"root": root_logger,
"uvicorn": uvicorn_logger,
"uvicorn.access": uvicorn_access_logger,
"fastapi": fastapi_logger,
}
def get_logger(name: str) -> logging.Logger:
"""
Get a logger instance for a specific module.
Args:
name: Name of the module/logger
Returns:
Configured logger instance
"""
return logging.getLogger(name)

View File

@ -5,6 +5,7 @@ This module provides the main FastAPI application with proper CORS
configuration, middleware setup, static file serving, and Jinja2 template configuration, middleware setup, static file serving, and Jinja2 template
integration. integration.
""" """
import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@ -18,6 +19,7 @@ from src.config.settings import settings
# Import core functionality # Import core functionality
from src.core.SeriesApp import SeriesApp from src.core.SeriesApp import SeriesApp
from src.infrastructure.logging import setup_logging
from src.server.api.analytics import router as analytics_router from src.server.api.analytics import router as analytics_router
from src.server.api.anime import router as anime_router from src.server.api.anime import router as anime_router
from src.server.api.auth import router as auth_router from src.server.api.auth import router as auth_router
@ -51,8 +53,13 @@ from src.server.services.websocket_service import get_websocket_service
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Manage application lifespan (startup and shutdown).""" """Manage application lifespan (startup and shutdown)."""
# Setup logging first
logger = setup_logging()
# Startup # Startup
try: try:
logger.info("Starting FastAPI application...")
# Load configuration from config.json and sync with settings # Load configuration from config.json and sync with settings
try: try:
from src.server.services.config_service import get_config_service from src.server.services.config_service import get_config_service
@ -62,25 +69,25 @@ async def lifespan(app: FastAPI):
# Sync anime_directory from config.json to settings # Sync anime_directory from config.json to settings
if config.other and config.other.get("anime_directory"): if config.other and config.other.get("anime_directory"):
settings.anime_directory = str(config.other["anime_directory"]) settings.anime_directory = str(config.other["anime_directory"])
print( logger.info(
f"Loaded anime_directory from config: " "Loaded anime_directory from config: %s",
f"{settings.anime_directory}" settings.anime_directory
) )
except Exception as e: except Exception as e:
print(f"Warning: Failed to load config from config.json: {e}") logger.warning("Failed to load config from config.json: %s", e)
# Initialize SeriesApp with configured directory and store it on # Initialize SeriesApp with configured directory and store it on
# application state so it can be injected via dependencies. # application state so it can be injected via dependencies.
if settings.anime_directory: if settings.anime_directory:
app.state.series_app = SeriesApp(settings.anime_directory) app.state.series_app = SeriesApp(settings.anime_directory)
print( logger.info(
f"SeriesApp initialized with directory: " "SeriesApp initialized with directory: %s",
f"{settings.anime_directory}" settings.anime_directory
) )
else: else:
# Log warning when anime directory is not configured # Log warning when anime directory is not configured
print( logger.warning(
"WARNING: ANIME_DIRECTORY not configured. " "ANIME_DIRECTORY not configured. "
"Some features may be unavailable." "Some features may be unavailable."
) )
@ -100,16 +107,20 @@ async def lifespan(app: FastAPI):
progress_service.set_broadcast_callback(broadcast_callback) progress_service.set_broadcast_callback(broadcast_callback)
print("FastAPI application started successfully") logger.info("FastAPI application started successfully")
logger.info("Server running on http://127.0.0.1:8000")
logger.info(
"API documentation available at http://127.0.0.1:8000/api/docs"
)
except Exception as e: except Exception as e:
print(f"Error during startup: {e}") logger.error("Error during startup: %s", e, exc_info=True)
raise # Re-raise to prevent app from starting in broken state raise # Re-raise to prevent app from starting in broken state
# Yield control to the application # Yield control to the application
yield yield
# Shutdown # Shutdown
print("FastAPI application shutting down") logger.info("FastAPI application shutting down")
def get_series_app() -> Optional[SeriesApp]: def get_series_app() -> Optional[SeriesApp]:

View File

@ -567,14 +567,24 @@ class AniWorldApp {
// Check if response has the expected format // Check if response has the expected format
if (Array.isArray(data)) { if (Array.isArray(data)) {
// API returns array of AnimeSummary objects directly // API returns array of AnimeSummary objects with full serie data
this.seriesData = data.map(anime => ({ this.seriesData = data.map(anime => {
id: anime.id, // Count total missing episodes from the episode dictionary
name: anime.title, const episodeDict = anime.missing_episodes || {};
title: anime.title, const totalMissing = Object.values(episodeDict).reduce(
missing_episodes: anime.missing_episodes || 0, (sum, episodes) => sum + (Array.isArray(episodes) ? episodes.length : 0),
episodeDict: {} // Will be populated when needed 0
})); );
return {
key: anime.key,
name: anime.name,
site: anime.site,
folder: anime.folder,
episodeDict: episodeDict,
missing_episodes: totalMissing
};
});
} else if (data.status === 'success') { } else if (data.status === 'success') {
// Legacy format support // Legacy format support
this.seriesData = data.series; this.seriesData = data.series;
@ -633,7 +643,7 @@ class AniWorldApp {
filtered.sort((a, b) => { filtered.sort((a, b) => {
if (this.sortAlphabetical) { if (this.sortAlphabetical) {
// Pure alphabetical sorting when A-Z is enabled // Pure alphabetical sorting when A-Z is enabled
return (a.name || a.folder).localeCompare(b.name || b.folder); return this.getDisplayName(a).localeCompare(this.getDisplayName(b));
} else { } else {
// Default sorting: missing episodes first (descending), then by name // Default sorting: missing episodes first (descending), then by name
// Always show series with missing episodes first // Always show series with missing episodes first
@ -705,7 +715,7 @@ class AniWorldApp {
${isSelected ? 'checked' : ''} ${isSelected ? 'checked' : ''}
${!canBeSelected ? 'disabled' : ''}> ${!canBeSelected ? 'disabled' : ''}>
<div class="series-info"> <div class="series-info">
<h3>${this.escapeHtml(serie.name)}</h3> <h3>${this.escapeHtml(this.getDisplayName(serie))}</h3>
<div class="series-folder">${this.escapeHtml(serie.folder)}</div> <div class="series-folder">${this.escapeHtml(serie.folder)}</div>
</div> </div>
<div class="series-status"> <div class="series-status">
@ -849,8 +859,8 @@ class AniWorldApp {
resultsList.innerHTML = results.map(result => ` resultsList.innerHTML = results.map(result => `
<div class="search-result-item"> <div class="search-result-item">
<span class="search-result-name">${this.escapeHtml(result.name)}</span> <span class="search-result-name">${this.escapeHtml(this.getDisplayName(result))}</span>
<button class="btn btn-small btn-primary" onclick="app.addSeries('${this.escapeHtml(result.link)}', '${this.escapeHtml(result.name)}')"> <button class="btn btn-small btn-primary" onclick="app.addSeries('${this.escapeHtml(result.link)}', '${this.escapeHtml(this.getDisplayName(result))}')">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
Add Add
</button> </button>
@ -924,15 +934,28 @@ class AniWorldApp {
async rescanSeries() { async rescanSeries() {
try { try {
this.showToast('Scanning directory...', 'info');
const response = await this.makeAuthenticatedRequest('/api/anime/rescan', { const response = await this.makeAuthenticatedRequest('/api/anime/rescan', {
method: 'POST' method: 'POST'
}); });
if (!response) return; if (!response) return;
const data = await response.json(); const data = await response.json();
// Debug logging
console.log('Rescan response:', data);
console.log('Success value:', data.success, 'Type:', typeof data.success);
if (data.status === 'success') { if (data.success === true) {
this.showToast('Rescan started', 'success'); const seriesCount = data.series_count || 0;
this.showToast(
`Rescan complete! Found ${seriesCount} series with missing episodes.`,
'success'
);
// Reload the series list to show the updated data
await this.loadSeries();
} else { } else {
this.showToast(`Rescan error: ${data.message}`, 'error'); this.showToast(`Rescan error: ${data.message}`, 'error');
} }
@ -1013,6 +1036,24 @@ class AniWorldApp {
return div.innerHTML; return div.innerHTML;
} }
/**
* Get display name for anime/series object.
* Returns name if available and not empty, otherwise returns key.
* @param {Object} anime - Anime/series object with name and key properties
* @returns {string} Display name
*/
getDisplayName(anime) {
if (!anime) return '';
// Use name if it exists and is not empty (after trimming whitespace)
const name = anime.name || '';
const trimmedName = name.trim();
if (trimmedName) {
return trimmedName;
}
// Fallback to key
return anime.key || anime.folder || '';
}
updateConnectionStatus() { updateConnectionStatus() {
const indicator = document.getElementById('connection-status-display'); const indicator = document.getElementById('connection-status-display');
if (indicator) { if (indicator) {
@ -2004,7 +2045,7 @@ class AniWorldApp {
// Update current downloading // Update current downloading
if (data.current_downloading) { if (data.current_downloading) {
currentDownload.classList.remove('hidden'); currentDownload.classList.remove('hidden');
document.getElementById('current-serie-name').textContent = data.current_downloading.name; document.getElementById('current-serie-name').textContent = this.getDisplayName(data.current_downloading);
document.getElementById('current-episode').textContent = `${data.current_downloading.missing_episodes} episodes remaining`; document.getElementById('current-episode').textContent = `${data.current_downloading.missing_episodes} episodes remaining`;
} else { } else {
currentDownload.classList.add('hidden'); currentDownload.classList.add('hidden');
@ -2015,7 +2056,7 @@ class AniWorldApp {
queueList.innerHTML = data.queue.map((serie, index) => ` queueList.innerHTML = data.queue.map((serie, index) => `
<div class="queue-item"> <div class="queue-item">
<div class="queue-item-index">${index + 1}</div> <div class="queue-item-index">${index + 1}</div>
<div class="queue-item-name">${this.escapeHtml(serie.name)}</div> <div class="queue-item-name">${this.escapeHtml(this.getDisplayName(serie))}</div>
<div class="queue-item-status">Waiting</div> <div class="queue-item-status">Waiting</div>
</div> </div>
`).join(''); `).join('');

5
start_server.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
# Startup script for Aniworld FastAPI server with proper logging
# Activate conda environment and run the server
conda run -n AniWorld python run_server.py

View File

@ -50,11 +50,21 @@ class TestSeriesAppDependency:
assert result == mock_series_app_instance assert result == mock_series_app_instance
mock_series_app_class.assert_called_once_with("/path/to/anime") mock_series_app_class.assert_called_once_with("/path/to/anime")
@patch('src.server.services.config_service.get_config_service')
@patch('src.server.utils.dependencies.settings') @patch('src.server.utils.dependencies.settings')
def test_get_series_app_no_directory_configured(self, mock_settings): def test_get_series_app_no_directory_configured(
self, mock_settings, mock_get_config_service
):
"""Test SeriesApp dependency when directory is not configured.""" """Test SeriesApp dependency when directory is not configured."""
# Arrange # Arrange
mock_settings.anime_directory = "" mock_settings.anime_directory = ""
# Mock config service to return empty config
mock_config_service = Mock()
mock_config = Mock()
mock_config.other = {}
mock_config_service.load_config.return_value = mock_config
mock_get_config_service.return_value = mock_config_service
# Act & Assert # Act & Assert
with pytest.raises(HTTPException) as exc_info: with pytest.raises(HTTPException) as exc_info:
@ -178,8 +188,10 @@ class TestAuthenticationDependencies:
mock_get_current_user.assert_called_once_with(credentials) mock_get_current_user.assert_called_once_with(credentials)
@patch('src.server.utils.dependencies.get_current_user') @patch('src.server.utils.dependencies.get_current_user')
def test_optional_auth_with_invalid_credentials(self, def test_optional_auth_with_invalid_credentials(
mock_get_current_user): self,
mock_get_current_user
):
"""Test optional authentication with invalid credentials.""" """Test optional authentication with invalid credentials."""
# Arrange # Arrange
credentials = HTTPAuthorizationCredentials( credentials = HTTPAuthorizationCredentials(
@ -277,6 +289,8 @@ class TestUtilityDependencies:
await log_request_dependency(mock_request) await log_request_dependency(mock_request)
# Assert - no exception should be raised # Assert - no exception should be raised
class TestIntegrationScenarios: class TestIntegrationScenarios:
"""Integration test scenarios for dependency injection.""" """Integration test scenarios for dependency injection."""
@ -284,14 +298,18 @@ class TestIntegrationScenarios:
"""Test the complete SeriesApp dependency lifecycle.""" """Test the complete SeriesApp dependency lifecycle."""
# Use separate mock instances for each call # Use separate mock instances for each call
with patch('src.server.utils.dependencies.settings') as mock_settings: with patch('src.server.utils.dependencies.settings') as mock_settings:
with patch('src.server.utils.dependencies.SeriesApp') as mock_series_app_class: with patch(
'src.server.utils.dependencies.SeriesApp'
) as mock_series_app_class:
# Arrange # Arrange
mock_settings.anime_directory = "/path/to/anime" mock_settings.anime_directory = "/path/to/anime"
# Create separate mock instances for each instantiation # Create separate mock instances for each instantiation
mock_instance1 = MagicMock() mock_instance1 = MagicMock()
mock_instance2 = MagicMock() mock_instance2 = MagicMock()
mock_series_app_class.side_effect = [mock_instance1, mock_instance2] mock_series_app_class.side_effect = [
mock_instance1, mock_instance2
]
# Act - Get SeriesApp instance # Act - Get SeriesApp instance
app1 = get_series_app() app1 = get_series_app()