815 lines
29 KiB
Markdown
815 lines
29 KiB
Markdown
# Architecture Documentation
|
||
|
||
## Document Purpose
|
||
|
||
This document describes the system architecture of the Aniworld anime download manager.
|
||
|
||
---
|
||
|
||
## 1. System Overview
|
||
|
||
Aniworld is a web-based anime download manager built with Python, FastAPI, and SQLite. It provides a REST API and WebSocket interface for managing anime libraries, downloading episodes, and tracking progress.
|
||
|
||
### High-Level Architecture
|
||
|
||
```
|
||
+------------------+ +------------------+ +------------------+
|
||
| Web Browser | | CLI Client | | External |
|
||
| (Frontend) | | (Main.py) | | Providers |
|
||
+--------+---------+ +--------+---------+ +--------+---------+
|
||
| | |
|
||
| HTTP/WebSocket | Direct | HTTP
|
||
| | |
|
||
+--------v---------+ +--------v---------+ +--------v---------+
|
||
| | | | | |
|
||
| FastAPI <-----> Core Layer <-----> Provider |
|
||
| Server Layer | | (SeriesApp) | | Adapters |
|
||
| | | | | |
|
||
+--------+---------+ +--------+---------+ +------------------+
|
||
| |
|
||
| |
|
||
+--------v---------+ +--------v---------+
|
||
| | | |
|
||
| SQLite DB | | File System |
|
||
| (aniworld.db) | | (anime/*/) |
|
||
| - Series data | | - Video files |
|
||
| - Episodes | | - NFO files |
|
||
| - Queue state | | - Media files |
|
||
+------------------+ +------------------+
|
||
```
|
||
|
||
Source: [src/server/fastapi_app.py](../src/server/fastapi_app.py#L1-L252)
|
||
|
||
---
|
||
|
||
## 2. Architectural Layers
|
||
|
||
### 2.1 CLI Layer (`src/cli/`)
|
||
|
||
Legacy command-line interface for direct interaction with the core layer.
|
||
|
||
| Component | File | Purpose |
|
||
| --------- | ----------------------------- | --------------- |
|
||
| Main | [Main.py](../src/cli/Main.py) | CLI entry point |
|
||
|
||
### 2.2 Server Layer (`src/server/`)
|
||
|
||
FastAPI-based REST API and WebSocket server.
|
||
|
||
```
|
||
src/server/
|
||
+-- fastapi_app.py # Application entry point, lifespan management
|
||
+-- api/ # API route handlers
|
||
| +-- anime.py # /api/anime/* endpoints
|
||
| +-- auth.py # /api/auth/* endpoints
|
||
| +-- config.py # /api/config/* endpoints
|
||
| +-- download.py # /api/queue/* endpoints
|
||
| +-- scheduler.py # /api/scheduler/* endpoints
|
||
| +-- nfo.py # /api/nfo/* endpoints
|
||
| +-- websocket.py # /ws/* WebSocket handlers
|
||
| +-- health.py # /health/* endpoints
|
||
+-- controllers/ # Page controllers for HTML rendering
|
||
| +-- page_controller.py # UI page routes
|
||
| +-- health_controller.py# Health check route
|
||
| +-- error_controller.py # Error pages (404, 500)
|
||
+-- services/ # Business logic
|
||
| +-- anime_service.py # Anime operations
|
||
| +-- auth_service.py # Authentication
|
||
| +-- config_service.py # Configuration management
|
||
| +-- download_service.py # Download queue management
|
||
| +-- progress_service.py # Progress tracking
|
||
| +-- websocket_service.py# WebSocket broadcasting
|
||
| +-- queue_repository.py # Database persistence
|
||
| +-- nfo_service.py # NFO metadata management
|
||
+-- models/ # Pydantic models
|
||
| +-- auth.py # Auth request/response models
|
||
| +-- config.py # Configuration models
|
||
| +-- download.py # Download queue models
|
||
| +-- websocket.py # WebSocket message models
|
||
+-- middleware/ # Request processing
|
||
| +-- auth.py # JWT validation, rate limiting
|
||
| +-- error_handler.py # Exception handlers
|
||
| +-- setup_redirect.py # Setup flow redirect
|
||
+-- database/ # SQLAlchemy ORM
|
||
| +-- connection.py # Database connection
|
||
| +-- models.py # ORM models
|
||
| +-- service.py # Database service
|
||
+-- utils/ # Utility modules
|
||
| +-- filesystem.py # Folder sanitization, path safety
|
||
| +-- validators.py # Input validation utilities
|
||
| +-- dependencies.py # FastAPI dependency injection
|
||
+-- web/ # Static files and templates
|
||
+-- static/ # CSS, JS, images
|
||
+-- templates/ # Jinja2 templates
|
||
```
|
||
|
||
Source: [src/server/](../src/server/)
|
||
|
||
### 2.2.1 Frontend Architecture (`src/server/web/static/`)
|
||
|
||
The frontend uses a modular architecture with no build step required. CSS and JavaScript files are organized by responsibility.
|
||
|
||
#### CSS Structure
|
||
|
||
```
|
||
src/server/web/static/css/
|
||
+-- styles.css # Entry point with @import statements
|
||
+-- base/
|
||
| +-- variables.css # CSS custom properties (colors, fonts, spacing)
|
||
| +-- reset.css # CSS reset and normalize styles
|
||
| +-- typography.css # Font styles, headings, text utilities
|
||
+-- components/
|
||
| +-- buttons.css # All button styles
|
||
| +-- cards.css # Card and panel components
|
||
| +-- forms.css # Form inputs, labels, validation styles
|
||
| +-- modals.css # Modal and overlay styles
|
||
| +-- navigation.css # Header, nav, sidebar styles
|
||
| +-- progress.css # Progress bars, loading indicators
|
||
| +-- notifications.css # Toast, alerts, messages
|
||
| +-- tables.css # Table and list styles
|
||
| +-- status.css # Status badges and indicators
|
||
+-- pages/
|
||
| +-- login.css # Login page specific styles
|
||
| +-- index.css # Index/library page specific styles
|
||
| +-- queue.css # Queue page specific styles
|
||
+-- utilities/
|
||
+-- animations.css # Keyframes and animation classes
|
||
+-- responsive.css # Media queries and breakpoints
|
||
+-- helpers.css # Utility classes (hidden, flex, spacing)
|
||
```
|
||
|
||
#### JavaScript Structure
|
||
|
||
JavaScript uses the IIFE pattern with a shared `AniWorld` namespace for browser compatibility without build tools.
|
||
|
||
```
|
||
src/server/web/static/js/
|
||
+-- shared/ # Shared utilities used by all pages
|
||
| +-- constants.js # API endpoints, localStorage keys, defaults
|
||
| +-- auth.js # Token management (getToken, setToken, checkAuth)
|
||
| +-- api-client.js # Fetch wrapper with auto-auth headers
|
||
| +-- theme.js # Dark/light theme toggle
|
||
| +-- ui-utils.js # Toast notifications, format helpers
|
||
| +-- websocket-client.js # Socket.IO wrapper
|
||
+-- index/ # Index page modules
|
||
| +-- series-manager.js # Series list rendering and filtering
|
||
| +-- selection-manager.js# Multi-select and bulk download
|
||
| +-- search.js # Series search functionality
|
||
| +-- scan-manager.js # Library rescan operations
|
||
| +-- scheduler-config.js # Scheduler configuration
|
||
| +-- logging-config.js # Logging configuration
|
||
| +-- advanced-config.js # Advanced settings
|
||
| +-- main-config.js # Main configuration and backup
|
||
| +-- config-manager.js # Config modal orchestrator
|
||
| +-- socket-handler.js # WebSocket event handlers
|
||
| +-- app-init.js # Application initialization
|
||
+-- queue/ # Queue page modules
|
||
+-- queue-api.js # Queue API interactions
|
||
+-- queue-renderer.js # Queue list rendering
|
||
+-- progress-handler.js # Download progress updates
|
||
+-- queue-socket-handler.js # WebSocket events for queue
|
||
+-- queue-init.js # Queue page initialization
|
||
```
|
||
|
||
#### Module Pattern
|
||
|
||
All JavaScript modules follow the IIFE pattern with namespace:
|
||
|
||
```javascript
|
||
var AniWorld = window.AniWorld || {};
|
||
|
||
AniWorld.ModuleName = (function () {
|
||
"use strict";
|
||
|
||
// Private variables and functions
|
||
|
||
// Public API
|
||
return {
|
||
init: init,
|
||
publicMethod: publicMethod,
|
||
};
|
||
})();
|
||
```
|
||
|
||
Source: [src/server/web/static/](../src/server/web/static/)
|
||
|
||
### 2.3 Core Layer (`src/core/`)
|
||
|
||
Domain logic for anime series management.
|
||
|
||
```
|
||
src/core/
|
||
+-- SeriesApp.py # Main application facade
|
||
+-- SerieScanner.py # Directory scanning, targeted single-series scan
|
||
+-- entities/ # Domain entities
|
||
| +-- series.py # Serie class with sanitized_folder property
|
||
| +-- SerieList.py # SerieList collection with sanitized folder support
|
||
| +-- nfo_models.py # Pydantic models for tvshow.nfo (TVShowNFO, ActorInfo…)
|
||
+-- services/ # Domain services
|
||
| +-- nfo_service.py # NFO lifecycle: create / update tvshow.nfo
|
||
| +-- nfo_repair_service.py # Detect & repair incomplete tvshow.nfo files
|
||
| | # (parse_nfo_tags, find_missing_tags, NfoRepairService)
|
||
| +-- tmdb_client.py # Async TMDB API client
|
||
+-- utils/ # Utility helpers (no side-effects)
|
||
| +-- nfo_generator.py # TVShowNFO → XML serialiser
|
||
| +-- nfo_mapper.py # TMDB API dict → TVShowNFO (tmdb_to_nfo_model,
|
||
| | # _extract_rating_by_country, _extract_fsk_rating)
|
||
| +-- image_downloader.py # TMDB image downloader
|
||
+-- providers/ # External provider adapters
|
||
| +-- base_provider.py # Loader interface
|
||
| +-- provider_factory.py # Provider registry
|
||
+-- interfaces/ # Abstract interfaces
|
||
| +-- callbacks.py # Progress callback system
|
||
+-- exceptions/ # Domain exceptions
|
||
+-- Exceptions.py # Custom exceptions
|
||
```
|
||
|
||
**Key Components:**
|
||
|
||
| Component | Purpose |
|
||
| -------------- | -------------------------------------------------------------------------- |
|
||
| `SeriesApp` | Main application facade for anime operations |
|
||
| `SerieScanner` | Scans directories for anime; `scan_single_series()` for targeted scans |
|
||
| `Serie` | Domain entity with `sanitized_folder` property for filesystem-safe names |
|
||
| `SerieList` | Collection management with automatic folder creation using sanitized names |
|
||
|
||
**Initialization:**
|
||
|
||
`SeriesApp` is initialized with `skip_load=True` passed to `SerieList`, preventing automatic loading of series from data files on every instantiation. Series data is loaded once during application setup via `sync_series_from_data_files()` in the FastAPI lifespan, which reads data files and syncs them to the database. Subsequent operations load series from the database through the service layer.
|
||
|
||
Source: [src/core/](../src/core/)
|
||
|
||
### 2.4 Infrastructure Layer (`src/infrastructure/`)
|
||
|
||
Cross-cutting concerns.
|
||
|
||
```
|
||
src/infrastructure/
|
||
+-- logging/ # Structured logging setup
|
||
+-- security/ # Security utilities
|
||
```
|
||
|
||
### 2.5 Configuration Layer (`src/config/`)
|
||
|
||
Application settings management.
|
||
|
||
| Component | File | Purpose |
|
||
| --------- | ---------------------------------------- | ------------------------------- |
|
||
| Settings | [settings.py](../src/config/settings.py) | Environment-based configuration |
|
||
|
||
Source: [src/config/settings.py](../src/config/settings.py#L1-L96)
|
||
|
||
---
|
||
|
||
## 12. Startup Sequence
|
||
|
||
The FastAPI lifespan function (`src/server/fastapi_app.py`) runs the following steps on every server start.
|
||
|
||
### 12.1 Startup Order
|
||
|
||
```
|
||
1. Logging configured
|
||
|
||
2. Temp folder purged ← cleans leftover partial download files
|
||
+-- Iterate ./Temp/ and delete every file and sub-directory
|
||
+-- Create ./Temp/ if it does not exist
|
||
+-- Errors are logged as warnings; startup continues regardless
|
||
|
||
3. Database initialised (required – abort on failure)
|
||
+-- SQLite file created / migrated via init_db()
|
||
|
||
4. Configuration loaded from data/config.json
|
||
+-- Synced to settings (ENV vars take precedence)
|
||
|
||
5. Progress & WebSocket services wired up
|
||
|
||
6. Series loaded from database into memory
|
||
|
||
7. Download service initialised (queue restored from DB)
|
||
|
||
8. Background loader service started
|
||
|
||
9. Scheduler service started
|
||
|
||
10. NFO repair scan (queue incomplete tvshow.nfo files for background reload)
|
||
```
|
||
|
||
### 12.2 Temp Folder Guarantee
|
||
|
||
Every server start begins with a clean `./Temp/` directory. This ensures that partial `.part` files or stale temp videos from a crashed or force-killed previous session are never left behind before new downloads start.
|
||
|
||
Source: [src/server/fastapi_app.py](../src/server/fastapi_app.py)
|
||
|
||
---
|
||
|
||
## 11. Graceful Shutdown
|
||
|
||
The application implements a comprehensive graceful shutdown mechanism that ensures data integrity and proper cleanup when the server is stopped via Ctrl+C (SIGINT) or SIGTERM.
|
||
|
||
### 11.1 Shutdown Sequence
|
||
|
||
```
|
||
1. SIGINT/SIGTERM received
|
||
+-- Uvicorn catches signal
|
||
+-- Stops accepting new requests
|
||
|
||
2. FastAPI lifespan shutdown triggered
|
||
+-- 30 second total timeout
|
||
|
||
3. WebSocket shutdown (5s timeout)
|
||
+-- Broadcast {"type": "server_shutdown"} to all clients
|
||
+-- Close each connection with code 1001 (Going Away)
|
||
+-- Clear connection tracking data
|
||
|
||
4. Download service stop (10s timeout)
|
||
+-- Set shutdown flag
|
||
+-- Persist active download as "pending" in database
|
||
+-- Cancel active download task
|
||
+-- Shutdown ThreadPoolExecutor with wait
|
||
|
||
5. Progress service cleanup
|
||
+-- Clear event subscribers
|
||
+-- Clear active progress tracking
|
||
|
||
6. Database cleanup (10s timeout)
|
||
+-- SQLite: Run PRAGMA wal_checkpoint(TRUNCATE)
|
||
+-- Dispose async engine
|
||
+-- Dispose sync engine
|
||
|
||
7. Process exits cleanly
|
||
```
|
||
|
||
Source: [src/server/fastapi_app.py](../src/server/fastapi_app.py#L142-L210)
|
||
|
||
### 11.2 Key Components
|
||
|
||
| Component | File | Shutdown Method |
|
||
| ------------------- | ------------------------------------------------------------------- | ------------------------------ |
|
||
| WebSocket Service | [websocket_service.py](../src/server/services/websocket_service.py) | `shutdown(timeout=5.0)` |
|
||
| Download Service | [download_service.py](../src/server/services/download_service.py) | `stop(timeout=10.0)` |
|
||
| Database Connection | [connection.py](../src/server/database/connection.py) | `close_db()` |
|
||
| Uvicorn Config | [run_server.py](../run_server.py) | `timeout_graceful_shutdown=30` |
|
||
| Stop Script | [stop_server.sh](../stop_server.sh) | SIGTERM with fallback |
|
||
|
||
### 11.3 Data Integrity Guarantees
|
||
|
||
1. **Active downloads preserved**: In-progress downloads are saved as "pending" and can resume on restart.
|
||
|
||
2. **Database WAL flushed**: SQLite WAL checkpoint ensures all writes are in the main database file.
|
||
|
||
3. **WebSocket clients notified**: Clients receive shutdown message before connection closes.
|
||
|
||
4. **Thread pool cleanup**: Background threads complete or are gracefully cancelled.
|
||
|
||
### 11.4 Manual Stop
|
||
|
||
```bash
|
||
# Graceful stop via script (sends SIGTERM, waits up to 30s)
|
||
./stop_server.sh
|
||
|
||
# Or press Ctrl+C in terminal running the server
|
||
```
|
||
|
||
Source: [stop_server.sh](../stop_server.sh#L1-L80)
|
||
|
||
---
|
||
|
||
## 3. Component Interactions
|
||
|
||
### 3.1 Request Flow (REST API)
|
||
|
||
```
|
||
1. Client sends HTTP request
|
||
2. AuthMiddleware validates JWT token (if required)
|
||
3. Rate limiter checks request frequency
|
||
4. FastAPI router dispatches to endpoint handler
|
||
5. Endpoint calls service layer
|
||
6. Service layer uses core layer or database
|
||
7. Response returned as JSON
|
||
```
|
||
|
||
Source: [src/server/middleware/auth.py](../src/server/middleware/auth.py#L1-L209)
|
||
|
||
### 3.2 Download Flow
|
||
|
||
```
|
||
1. POST /api/queue/add
|
||
+-- DownloadService.add_to_queue()
|
||
+-- QueueRepository.save_item() -> SQLite
|
||
|
||
2. POST /api/queue/start
|
||
+-- DownloadService.start_queue_processing()
|
||
+-- Process pending items sequentially
|
||
+-- ProgressService emits events
|
||
+-- WebSocketService broadcasts to clients
|
||
|
||
3. During download:
|
||
+-- Provider writes to ./Temp/<filename> (+ ./Temp/<filename>.part fragments)
|
||
+-- ProgressService.emit("progress_updated")
|
||
+-- WebSocketService.broadcast_to_room()
|
||
+-- Client receives WebSocket message
|
||
|
||
4. After download attempt (success OR failure):
|
||
+-- _cleanup_temp_file() removes ./Temp/<filename> and all .part fragments
|
||
+-- On success: file was already moved to final destination before cleanup
|
||
+-- On failure / exception: no partial files remain in ./Temp/
|
||
```
|
||
|
||
#### Temp Directory Contract
|
||
|
||
| Situation | Outcome |
|
||
| --------- | ------- |
|
||
| Server start | Entire `./Temp/` directory is purged before any service initialises |
|
||
| Successful download | Temp file moved to destination, then removed from `./Temp/` |
|
||
| Failed download (provider error) | Temp + `.part` fragments removed by `_cleanup_temp_file()` |
|
||
| Exception / cancellation | Temp + `.part` fragments removed in `except` block |
|
||
|
||
Source: [src/server/services/download_service.py](../src/server/services/download_service.py#L1-L150),
|
||
[src/core/providers/aniworld_provider.py](../src/core/providers/aniworld_provider.py),
|
||
[src/core/providers/enhanced_provider.py](../src/core/providers/enhanced_provider.py)
|
||
|
||
### 3.3 WebSocket Event Flow
|
||
|
||
```
|
||
1. Client connects to /ws/connect
|
||
2. Server sends "connected" message
|
||
3. Client joins room: {"action": "join", "data": {"room": "downloads"}}
|
||
4. ProgressService emits events
|
||
5. WebSocketService broadcasts to room subscribers
|
||
6. Client receives real-time updates
|
||
```
|
||
|
||
Source: [src/server/api/websocket.py](../src/server/api/websocket.py#L1-L260)
|
||
|
||
---
|
||
|
||
## 4. Design Patterns
|
||
|
||
### 4.1 Repository Pattern (Service Layer as Repository)
|
||
|
||
**Architecture Decision**: The Service Layer serves as the Repository layer for database access.
|
||
|
||
Database access is abstracted through service classes in `src/server/database/service.py` that provide CRUD operations and act as the repository layer. This eliminates the need for a separate repository layer while maintaining clean separation of concerns.
|
||
|
||
**Service Layer Classes** (acting as repositories):
|
||
|
||
- `AnimeSeriesService` - CRUD operations for anime series
|
||
- `EpisodeService` - CRUD operations for episodes
|
||
- `DownloadQueueService` - CRUD operations for download queue
|
||
- `UserSessionService` - CRUD operations for user sessions
|
||
- `SystemSettingsService` - CRUD operations for system settings
|
||
|
||
**Key Principles**:
|
||
|
||
1. **No Direct Database Queries**: Controllers and business logic services MUST use service layer methods
|
||
2. **Service Layer Encapsulation**: All SQLAlchemy queries are encapsulated in service methods
|
||
3. **Consistent Interface**: Services provide consistent async methods for all database operations
|
||
4. **Single Responsibility**: Each service manages one entity type
|
||
|
||
**Example Usage**:
|
||
|
||
```python
|
||
# CORRECT: Use service layer
|
||
from src.server.database.service import AnimeSeriesService
|
||
|
||
async with get_db_session() as db:
|
||
series = await AnimeSeriesService.get_by_key(db, "attack-on-titan")
|
||
await AnimeSeriesService.update(db, series.id, has_nfo=True)
|
||
|
||
# INCORRECT: Direct database query
|
||
result = await db.execute(select(AnimeSeries).filter(...)) # ❌ Never do this
|
||
```
|
||
|
||
**Special Case - Queue Repository Adapter**:
|
||
|
||
The `QueueRepository` in `src/server/services/queue_repository.py` is an adapter that wraps `DownloadQueueService` to provide domain model conversion between Pydantic models and SQLAlchemy models:
|
||
|
||
```python
|
||
# QueueRepository provides CRUD with model conversion
|
||
class QueueRepository:
|
||
async def save_item(self, item: DownloadItem) -> None: ... # Converts Pydantic → SQLAlchemy
|
||
async def get_all_items(self) -> List[DownloadItem]: ... # Converts SQLAlchemy → Pydantic
|
||
async def delete_item(self, item_id: str) -> bool: ...
|
||
```
|
||
|
||
Source: [src/server/database/service.py](../src/server/database/service.py), [src/server/services/queue_repository.py](../src/server/services/queue_repository.py)
|
||
|
||
### 4.2 Dependency Injection
|
||
|
||
FastAPI's `Depends()` provides constructor injection.
|
||
|
||
```python
|
||
@router.get("/status")
|
||
async def get_status(
|
||
download_service: DownloadService = Depends(get_download_service),
|
||
):
|
||
...
|
||
```
|
||
|
||
Source: [src/server/utils/dependencies.py](../src/server/utils/dependencies.py)
|
||
|
||
### 4.3 Event-Driven Architecture
|
||
|
||
Progress updates use an event subscription model.
|
||
|
||
```python
|
||
# ProgressService publishes events
|
||
progress_service.emit("progress_updated", event)
|
||
|
||
# WebSocketService subscribes
|
||
progress_service.subscribe("progress_updated", ws_handler)
|
||
```
|
||
|
||
Source: [src/server/fastapi_app.py](../src/server/fastapi_app.py#L98-L108)
|
||
|
||
### 4.4 Singleton Pattern
|
||
|
||
Services use module-level singletons for shared state.
|
||
|
||
```python
|
||
# In download_service.py
|
||
_download_service_instance: Optional[DownloadService] = None
|
||
|
||
def get_download_service() -> DownloadService:
|
||
global _download_service_instance
|
||
if _download_service_instance is None:
|
||
_download_service_instance = DownloadService(...)
|
||
return _download_service_instance
|
||
```
|
||
|
||
### 4.5 Error Handling Pattern
|
||
|
||
**Architecture Decision**: Dual error handling approach based on exception source.
|
||
|
||
The application uses two complementary error handling mechanisms:
|
||
|
||
1. **FastAPI HTTPException** - For simple validation and HTTP-level errors
|
||
2. **Custom Exception Hierarchy** - For business logic and service-level errors with rich context
|
||
|
||
#### Exception Hierarchy
|
||
|
||
```python
|
||
# Base exception with HTTP status mapping
|
||
AniWorldAPIException(message, status_code, error_code, details)
|
||
├── AuthenticationError (401)
|
||
├── AuthorizationError (403)
|
||
├── ValidationError (422)
|
||
├── NotFoundError (404)
|
||
├── ConflictError (409)
|
||
├── BadRequestError (400)
|
||
├── RateLimitError (429)
|
||
└── ServerError (500)
|
||
├── DownloadError
|
||
├── ConfigurationError
|
||
├── ProviderError
|
||
└── DatabaseError
|
||
```
|
||
|
||
#### When to Use Each
|
||
|
||
**Use HTTPException for:**
|
||
|
||
- Simple parameter validation (missing fields, wrong type)
|
||
- Direct HTTP-level errors (401, 403, 404 without business context)
|
||
- Quick endpoint-specific failures
|
||
|
||
**Use Custom Exceptions for:**
|
||
|
||
- Service-layer business logic errors (AnimeServiceError, ConfigServiceError)
|
||
- Errors needing rich context (details dict, error codes)
|
||
- Errors that should be logged with specific categorization
|
||
- Cross-cutting concerns (authentication, authorization, rate limiting)
|
||
|
||
**Example:**
|
||
|
||
```python
|
||
# Simple validation - Use HTTPException
|
||
if not series_key:
|
||
raise HTTPException(status_code=400, detail="series_key required")
|
||
|
||
# Business logic error - Use custom exception
|
||
try:
|
||
await anime_service.add_series(series_key)
|
||
except AnimeServiceError as e:
|
||
raise ServerError(
|
||
message=f"Failed to add series: {e}",
|
||
error_code="ANIME_ADD_FAILED",
|
||
details={"series_key": series_key}
|
||
)
|
||
```
|
||
|
||
#### Global Exception Handlers
|
||
|
||
All custom exceptions are automatically handled by global middleware that:
|
||
|
||
- Converts exceptions to structured JSON responses
|
||
- Logs errors with appropriate severity
|
||
- Includes request ID for tracking
|
||
- Provides consistent error format
|
||
|
||
**Source**: [src/server/exceptions/\_\_init\_\_.py](../src/server/exceptions/__init__.py), [src/server/middleware/error_handler.py](../src/server/middleware/error_handler.py)
|
||
|
||
Source: [src/server/services/download_service.py](../src/server/services/download_service.py)
|
||
|
||
---
|
||
|
||
## 5. Data Flow
|
||
|
||
### 5.1 Series Identifier Convention
|
||
|
||
The system uses two identifier fields:
|
||
|
||
| Field | Type | Purpose | Example |
|
||
| -------- | -------- | -------------------------------------- | -------------------------- |
|
||
| `key` | Primary | Provider-assigned, URL-safe identifier | `"attack-on-titan"` |
|
||
| `folder` | Metadata | Filesystem folder name (display only) | `"Attack on Titan (2013)"` |
|
||
|
||
All API operations use `key`. The `folder` is for filesystem operations only.
|
||
|
||
Source: [src/server/database/models.py](../src/server/database/models.py#L26-L50)
|
||
|
||
### 5.2 Database Schema
|
||
|
||
```
|
||
+----------------+ +----------------+ +--------------------+
|
||
| anime_series | | episodes | | download_queue_item|
|
||
+----------------+ +----------------+ +--------------------+
|
||
| id (PK) |<--+ | id (PK) | +-->| id (PK) |
|
||
| key (unique) | | | series_id (FK) |---+ | series_id (FK) |
|
||
| name | +---| season | | status |
|
||
| site | | episode_number | | priority |
|
||
| folder | | title | | progress_percent |
|
||
| created_at | | is_downloaded | | added_at |
|
||
| updated_at | | file_path | | started_at |
|
||
+----------------+ +----------------+ +--------------------+
|
||
```
|
||
|
||
Source: [src/server/database/models.py](../src/server/database/models.py#L1-L200)
|
||
|
||
### 5.3 Configuration Storage
|
||
|
||
Configuration is stored in `data/config.json`:
|
||
|
||
```json
|
||
{
|
||
"name": "Aniworld",
|
||
"data_dir": "data",
|
||
"scheduler": {
|
||
"enabled": true,
|
||
"schedule_time": "03:00",
|
||
"schedule_days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
|
||
"auto_download_after_rescan": false
|
||
},
|
||
"logging": { "level": "INFO" },
|
||
"backup": { "enabled": false, "path": "data/backups" },
|
||
"other": {
|
||
"master_password_hash": "$pbkdf2-sha256$...",
|
||
"anime_directory": "/path/to/anime"
|
||
}
|
||
}
|
||
```
|
||
|
||
Source: [data/config.json](../data/config.json)
|
||
|
||
---
|
||
|
||
## 6. Technology Stack
|
||
|
||
| Layer | Technology | Version | Purpose |
|
||
| ------------- | ------------------- | ------- | ---------------------- |
|
||
| Web Framework | FastAPI | 0.104.1 | REST API, WebSocket |
|
||
| ASGI Server | Uvicorn | 0.24.0 | HTTP server |
|
||
| Database | SQLite + SQLAlchemy | 2.0.35 | Persistence |
|
||
| Auth | python-jose | 3.3.0 | JWT tokens |
|
||
| Password | passlib | 1.7.4 | bcrypt hashing |
|
||
| Validation | Pydantic | 2.5.0 | Data models |
|
||
| Templates | Jinja2 | 3.1.2 | HTML rendering |
|
||
| Logging | structlog | 24.1.0 | Structured logging |
|
||
| Testing | pytest | 7.4.3 | Unit/integration tests |
|
||
|
||
Source: [requirements.txt](../requirements.txt)
|
||
|
||
---
|
||
|
||
## 7. Scalability Considerations
|
||
|
||
### Current Limitations
|
||
|
||
1. **Single-process deployment**: In-memory rate limiting and session state are not shared across processes.
|
||
|
||
2. **SQLite database**: Not suitable for high concurrency. Consider PostgreSQL for production.
|
||
|
||
3. **Sequential downloads**: Only one download processes at a time by design.
|
||
|
||
### Recommended Improvements for Scale
|
||
|
||
| Concern | Current | Recommended |
|
||
| -------------- | --------------- | ----------------- |
|
||
| Rate limiting | In-memory dict | Redis |
|
||
| Session store | In-memory | Redis or database |
|
||
| Database | SQLite | PostgreSQL |
|
||
| Task queue | In-memory deque | Celery + Redis |
|
||
| Load balancing | None | Nginx/HAProxy |
|
||
|
||
---
|
||
|
||
## 8. Integration Points
|
||
|
||
### 8.1 External Providers
|
||
|
||
The system integrates with anime streaming providers via the Loader interface.
|
||
|
||
```python
|
||
class Loader(ABC):
|
||
@abstractmethod
|
||
def search(self, query: str) -> List[Serie]: ...
|
||
|
||
@abstractmethod
|
||
def get_episodes(self, serie: Serie) -> Dict[int, List[int]]: ...
|
||
```
|
||
|
||
Source: [src/core/providers/base_provider.py](../src/core/providers/base_provider.py)
|
||
|
||
### 8.2 Filesystem Integration
|
||
|
||
The scanner reads anime directories to detect downloaded episodes.
|
||
|
||
```python
|
||
SerieScanner(
|
||
basePath="/path/to/anime", # Anime library directory
|
||
loader=provider, # Provider for metadata
|
||
db_session=session # Optional database
|
||
)
|
||
```
|
||
|
||
Source: [src/core/SerieScanner.py](../src/core/SerieScanner.py#L59-L96)
|
||
|
||
---
|
||
|
||
## 9. Security Architecture
|
||
|
||
### 9.1 Authentication Flow
|
||
|
||
```
|
||
1. User sets master password via POST /api/auth/setup
|
||
2. Password hashed with pbkdf2_sha256 (via passlib)
|
||
3. Hash stored in config.json
|
||
4. Login validates password, returns JWT token
|
||
5. JWT contains: session_id, user, created_at, expires_at
|
||
6. Subsequent requests include: Authorization: Bearer <token>
|
||
```
|
||
|
||
Source: [src/server/services/auth_service.py](../src/server/services/auth_service.py#L1-L150)
|
||
|
||
### 9.2 Password Requirements
|
||
|
||
- Minimum 8 characters
|
||
- Mixed case (upper and lower)
|
||
- At least one number
|
||
- At least one special character
|
||
|
||
Source: [src/server/services/auth_service.py](../src/server/services/auth_service.py#L97-L125)
|
||
|
||
### 9.3 Rate Limiting
|
||
|
||
| Endpoint | Limit | Window |
|
||
| ----------------- | ----------- | ---------- |
|
||
| `/api/auth/login` | 5 requests | 60 seconds |
|
||
| `/api/auth/setup` | 5 requests | 60 seconds |
|
||
| All origins | 60 requests | 60 seconds |
|
||
|
||
Source: [src/server/middleware/auth.py](../src/server/middleware/auth.py#L54-L68)
|
||
|
||
---
|
||
|
||
## 10. Deployment Modes
|
||
|
||
### 10.1 Development
|
||
|
||
```bash
|
||
# Run with hot reload
|
||
python -m uvicorn src.server.fastapi_app:app --reload
|
||
```
|
||
|
||
### 10.2 Production
|
||
|
||
```bash
|
||
# Via conda environment
|
||
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app \
|
||
--host 127.0.0.1 --port 8000
|
||
```
|
||
|
||
### 10.3 Configuration
|
||
|
||
Environment variables (via `.env` or shell):
|
||
|
||
| Variable | Default | Description |
|
||
| ----------------- | ------------------------------ | ---------------------- |
|
||
| `JWT_SECRET_KEY` | Random | Secret for JWT signing |
|
||
| `DATABASE_URL` | `sqlite:///./data/aniworld.db` | Database connection |
|
||
| `ANIME_DIRECTORY` | (empty) | Path to anime library |
|
||
| `LOG_LEVEL` | `INFO` | Logging level |
|
||
| `CORS_ORIGINS` | `localhost:3000,8000` | Allowed CORS origins |
|
||
|
||
Source: [src/config/settings.py](../src/config/settings.py#L1-L96)
|