30 KiB
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
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 | 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/
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:
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/
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/
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 | Environment-based configuration |
Source: src/config/settings.py
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
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
11.2 Key Components
| Component | File | Shutdown Method |
|---|---|---|
| WebSocket Service | websocket_service.py | shutdown(timeout=5.0) |
| Download Service | download_service.py | stop(timeout=10.0) |
| Database Connection | connection.py | close_db() |
| Uvicorn Config | run_server.py | timeout_graceful_shutdown=30 |
| Stop Script | stop_server.sh | SIGTERM with fallback |
11.3 Data Integrity Guarantees
-
Active downloads preserved: In-progress downloads are saved as "pending" and can resume on restart.
-
Database WAL flushed: SQLite WAL checkpoint ensures all writes are in the main database file.
-
WebSocket clients notified: Clients receive shutdown message before connection closes.
-
Thread pool cleanup: Background threads complete or are gracefully cancelled.
11.4 Manual Stop
# 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
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
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/core/providers/aniworld_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
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 seriesEpisodeService- CRUD operations for episodesDownloadQueueService- CRUD operations for download queueUserSessionService- CRUD operations for user sessionsSystemSettingsService- CRUD operations for system settings
Key Principles:
- No Direct Database Queries: Controllers and business logic services MUST use service layer methods
- Service Layer Encapsulation: All SQLAlchemy queries are encapsulated in service methods
- Consistent Interface: Services provide consistent async methods for all database operations
- Single Responsibility: Each service manages one entity type
Example Usage:
# 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:
# 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/services/queue_repository.py
4.2 Dependency Injection
FastAPI's Depends() provides constructor injection.
@router.get("/status")
async def get_status(
download_service: DownloadService = Depends(get_download_service),
):
...
Source: src/server/utils/dependencies.py
4.3 Event-Driven Architecture
Progress updates use an event subscription model.
# ProgressService publishes events
progress_service.emit("progress_updated", event)
# WebSocketService subscribes
progress_service.subscribe("progress_updated", ws_handler)
Source: src/server/fastapi_app.py
4.4 Singleton Pattern
Services use module-level singletons for shared state.
# 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:
- FastAPI HTTPException - For simple validation and HTTP-level errors
- Custom Exception Hierarchy - For business logic and service-level errors with rich context
Exception Hierarchy
# 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:
# 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/middleware/error_handler.py
Source: 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
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
5.3 Configuration Storage
Configuration is stored in data/config.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
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
7. Scalability Considerations
Current Limitations
-
Single-process deployment: In-memory rate limiting and session state are not shared across processes.
-
SQLite database: Not suitable for high concurrency. Consider PostgreSQL for production.
-
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.
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
8.2 Filesystem Integration
The scanner reads anime directories to detect downloaded episodes.
SerieScanner(
basePath="/path/to/anime", # Anime library directory
loader=provider, # Provider for metadata
db_session=session # Optional database
)
Source: src/core/SerieScanner.py
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
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
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
10. Deployment Modes
10.1 Development
# Run with hot reload
python -m uvicorn src.server.fastapi_app:app --reload
10.2 Production
# 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