Added documentation for API, architecture, configuration, database, development guide, testing, and navigation. Includes helper scripts, diagrams, and guides for NFO files and migration.
1597 lines
35 KiB
Markdown
1597 lines
35 KiB
Markdown
# API Documentation
|
||
|
||
## Document Purpose
|
||
|
||
This document provides comprehensive REST API and WebSocket reference for the Aniworld application.
|
||
|
||
Source: [src/server/fastapi_app.py](../src/server/fastapi_app.py#L1-L252)
|
||
|
||
---
|
||
|
||
## 1. API Overview
|
||
|
||
### Base URL and Versioning
|
||
|
||
| Environment | Base URL |
|
||
| ------------------- | --------------------------------- |
|
||
| Local Development | `http://127.0.0.1:8000` |
|
||
| API Documentation | `http://127.0.0.1:8000/api/docs` |
|
||
| ReDoc Documentation | `http://127.0.0.1:8000/api/redoc` |
|
||
|
||
The API does not use versioning prefixes. All endpoints are available under `/api/*`.
|
||
|
||
Source: [src/server/fastapi_app.py](../src/server/fastapi_app.py#L177-L184)
|
||
|
||
### Authentication
|
||
|
||
The API uses JWT Bearer Token authentication.
|
||
|
||
**Header Format:**
|
||
|
||
```
|
||
Authorization: Bearer <jwt_token>
|
||
```
|
||
|
||
**Public Endpoints (no authentication required):**
|
||
|
||
- `/api/auth/*` - Authentication endpoints
|
||
- `/api/health` - Health check endpoints
|
||
- `/api/docs`, `/api/redoc` - API documentation
|
||
- `/static/*` - Static files
|
||
- `/`, `/login`, `/setup`, `/queue` - UI pages
|
||
|
||
Source: [src/server/middleware/auth.py](../src/server/middleware/auth.py#L39-L52)
|
||
|
||
### Content Types
|
||
|
||
| Direction | Content-Type |
|
||
| --------- | ----------------------------- |
|
||
| Request | `application/json` |
|
||
| Response | `application/json` |
|
||
| WebSocket | `application/json` (messages) |
|
||
|
||
### Common Headers
|
||
|
||
| Header | Required | Description |
|
||
| --------------- | -------- | ------------------------------------ |
|
||
| `Authorization` | Yes\* | Bearer token for protected endpoints |
|
||
| `Content-Type` | Yes | `application/json` for POST/PUT |
|
||
| `Origin` | No | Required for CORS preflight |
|
||
|
||
\*Not required for public endpoints listed above.
|
||
|
||
---
|
||
|
||
## 2. Authentication Endpoints
|
||
|
||
Prefix: `/api/auth`
|
||
|
||
Source: [src/server/api/auth.py](../src/server/api/auth.py#L1-L180)
|
||
|
||
### POST /api/auth/setup
|
||
|
||
Initial setup endpoint to configure the master password. Can only be called once.
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"master_password": "string (min 8 chars, mixed case, number, special char)",
|
||
"anime_directory": "string (optional, path to anime folder)"
|
||
}
|
||
```
|
||
|
||
**Response (201 Created):**
|
||
|
||
```json
|
||
{
|
||
"status": "ok"
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
- `400 Bad Request` - Master password already configured or invalid password
|
||
|
||
Source: [src/server/api/auth.py](../src/server/api/auth.py#L28-L90)
|
||
|
||
### POST /api/auth/login
|
||
|
||
Validate master password and return JWT token.
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"password": "string",
|
||
"remember": false
|
||
}
|
||
```
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"access_token": "eyJ...",
|
||
"token_type": "bearer",
|
||
"expires_at": "2025-12-14T10:30:00Z"
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
- `401 Unauthorized` - Invalid credentials
|
||
- `429 Too Many Requests` - Account locked due to failed attempts
|
||
|
||
Source: [src/server/api/auth.py](../src/server/api/auth.py#L93-L124)
|
||
|
||
### POST /api/auth/logout
|
||
|
||
Logout by revoking token.
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"message": "Logged out successfully"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/auth.py](../src/server/api/auth.py#L127-L140)
|
||
|
||
### GET /api/auth/status
|
||
|
||
Return whether master password is configured and if caller is authenticated.
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"configured": true,
|
||
"authenticated": true
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/auth.py](../src/server/api/auth.py#L157-L162)
|
||
|
||
---
|
||
|
||
## 3. Anime Endpoints
|
||
|
||
Prefix: `/api/anime`
|
||
|
||
Source: [src/server/api/anime.py](../src/server/api/anime.py#L1-L812)
|
||
|
||
### Series Identifier Convention
|
||
|
||
The API uses two identifier fields:
|
||
|
||
| Field | Purpose | Example |
|
||
| -------- | ---------------------------------------------------- | -------------------------- |
|
||
| `key` | **Primary identifier** - provider-assigned, URL-safe | `"attack-on-titan"` |
|
||
| `folder` | Metadata only - filesystem folder name | `"Attack on Titan (2013)"` |
|
||
|
||
Use `key` for all API operations. The `folder` field is for display purposes only.
|
||
|
||
### GET /api/anime/status
|
||
|
||
Get anime library status information.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"directory": "/path/to/anime",
|
||
"series_count": 42
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/anime.py](../src/server/api/anime.py#L28-L58)
|
||
|
||
### GET /api/anime
|
||
|
||
List library series that have missing episodes.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Query Parameters:**
|
||
| Parameter | Type | Default | Description |
|
||
|-----------|------|---------|-------------|
|
||
| `page` | int | 1 | Page number (must be positive) |
|
||
| `per_page` | int | 20 | Items per page (max 1000) |
|
||
| `sort_by` | string | null | Sort field: `title`, `id`, `name`, `missing_episodes` |
|
||
| `filter` | string | null | Filter: `missing_episodes` (shows series with any missing episodes), `no_episodes` (shows series with zero downloaded episodes) |
|
||
|
||
**Filter Details:**
|
||
|
||
- `missing_episodes`: Returns series that have at least one missing episode recorded in the database (`is_downloaded=False`)
|
||
- `no_episodes`: Returns series that have missing episodes and no downloaded episodes (i.e., only missing episodes exist in the database)
|
||
- Episodes in the database represent MISSING episodes (from episodeDict during scanning)
|
||
- `is_downloaded=False` means the episode file was not found in the folder
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
[
|
||
{
|
||
"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] },
|
||
"link": ""
|
||
}
|
||
]
|
||
```
|
||
|
||
**Example with filter:**
|
||
|
||
```bash
|
||
GET /api/anime?filter=no_episodes
|
||
```
|
||
|
||
Source: [src/server/api/anime.py](../src/server/api/anime.py#L155-L303)
|
||
|
||
### GET /api/anime/search
|
||
|
||
Search the provider for anime series matching a query.
|
||
|
||
**Authentication:** Not required
|
||
|
||
**Query Parameters:**
|
||
| Parameter | Type | Required | Description |
|
||
|-----------|------|----------|-------------|
|
||
| `query` | string | Yes | Search term (max 200 chars) |
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
[
|
||
{
|
||
"key": "attack-on-titan",
|
||
"name": "Attack on Titan",
|
||
"site": "aniworld.to",
|
||
"folder": "Attack on Titan (2013)",
|
||
"missing_episodes": {},
|
||
"link": "https://aniworld.to/anime/stream/attack-on-titan"
|
||
}
|
||
]
|
||
```
|
||
|
||
Source: [src/server/api/anime.py](../src/server/api/anime.py#L431-L474)
|
||
|
||
### POST /api/anime/search
|
||
|
||
Search via POST body.
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"query": "attack on titan"
|
||
}
|
||
```
|
||
|
||
**Response:** Same as GET /api/anime/search
|
||
|
||
Source: [src/server/api/anime.py](../src/server/api/anime.py#L477-L495)
|
||
|
||
### POST /api/anime/add
|
||
|
||
Add a new series to the library with automatic database persistence, folder creation, and episode scanning.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"link": "https://aniworld.to/anime/stream/attack-on-titan",
|
||
"name": "Attack on Titan"
|
||
}
|
||
```
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"message": "Successfully added series: Attack on Titan",
|
||
"key": "attack-on-titan",
|
||
"folder": "Attack on Titan",
|
||
"db_id": 1,
|
||
"missing_episodes": ["1-1", "1-2", "1-3"],
|
||
"total_missing": 3
|
||
}
|
||
```
|
||
|
||
**Enhanced Flow:**
|
||
|
||
1. Validates the request (link format, name)
|
||
2. Creates Serie object with sanitized folder name
|
||
3. Saves to database via AnimeDBService
|
||
4. Creates folder using sanitized display name (not internal key)
|
||
5. Performs targeted episode scan for this anime only
|
||
6. Returns response with missing episodes count
|
||
|
||
**Folder Name Sanitization:**
|
||
|
||
- Removes invalid filesystem characters: `< > : " / \ | ? *`
|
||
- Trims leading/trailing whitespace and dots
|
||
- Preserves Unicode characters (for Japanese titles)
|
||
- Example: `"Attack on Titan: Final Season"` → `"Attack on Titan Final Season"`
|
||
|
||
Source: [src/server/api/anime.py](../src/server/api/anime.py#L604-L710)
|
||
|
||
### POST /api/anime/rescan
|
||
|
||
Trigger a rescan of the local library.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Rescan started successfully"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/anime.py](../src/server/api/anime.py#L306-L337)
|
||
|
||
### GET /api/anime/{anime_id}
|
||
|
||
Return detailed information about a specific series.
|
||
|
||
**Authentication:** Not required
|
||
|
||
**Path Parameters:**
|
||
| Parameter | Description |
|
||
|-----------|-------------|
|
||
| `anime_id` | Series `key` (primary) or `folder` (deprecated fallback) |
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"key": "attack-on-titan",
|
||
"title": "Attack on Titan",
|
||
"folder": "Attack on Titan (2013)",
|
||
"episodes": ["1-1", "1-2", "1-3"],
|
||
"description": null
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/anime.py](../src/server/api/anime.py#L713-L793)
|
||
|
||
---
|
||
|
||
## 4. Download Queue Endpoints
|
||
|
||
Prefix: `/api/queue`
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L1-L529)
|
||
|
||
### GET /api/queue/status
|
||
|
||
Get current download queue status and statistics.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": {
|
||
"is_running": false,
|
||
"is_paused": false,
|
||
"active_downloads": [],
|
||
"pending_queue": [],
|
||
"completed_downloads": [],
|
||
"failed_downloads": []
|
||
},
|
||
"statistics": {
|
||
"total_items": 5,
|
||
"pending_count": 3,
|
||
"active_count": 1,
|
||
"completed_count": 1,
|
||
"failed_count": 0,
|
||
"total_downloaded_mb": 1024.5,
|
||
"average_speed_mbps": 2.5,
|
||
"estimated_time_remaining": 3600
|
||
}
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L21-L56)
|
||
|
||
### POST /api/queue/add
|
||
|
||
Add episodes to the download queue.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"serie_id": "attack-on-titan",
|
||
"serie_folder": "Attack on Titan (2013)",
|
||
"serie_name": "Attack on Titan",
|
||
"episodes": [
|
||
{ "season": 1, "episode": 1, "title": "Episode 1" },
|
||
{ "season": 1, "episode": 2, "title": "Episode 2" }
|
||
],
|
||
"priority": "NORMAL"
|
||
}
|
||
```
|
||
|
||
**Priority Values:** `LOW`, `NORMAL`, `HIGH`
|
||
|
||
**Response (201 Created):**
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"message": "Added 2 episode(s) to download queue",
|
||
"added_items": ["uuid1", "uuid2"],
|
||
"item_ids": ["uuid1", "uuid2"],
|
||
"failed_items": []
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L59-L120)
|
||
|
||
### POST /api/queue/start
|
||
|
||
Start automatic queue processing.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"message": "Queue processing started"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L293-L331)
|
||
|
||
### POST /api/queue/stop
|
||
|
||
Stop processing new downloads from queue.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"message": "Queue processing stopped (current download will continue)"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L334-L387)
|
||
|
||
### POST /api/queue/pause
|
||
|
||
Pause queue processing (alias for stop).
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"message": "Queue processing paused"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L416-L445)
|
||
|
||
### DELETE /api/queue/{item_id}
|
||
|
||
Remove a specific item from the download queue.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Path Parameters:**
|
||
| Parameter | Description |
|
||
|-----------|-------------|
|
||
| `item_id` | Download item UUID |
|
||
|
||
**Response (204 No Content)**
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L225-L256)
|
||
|
||
### DELETE /api/queue
|
||
|
||
Remove multiple items from the download queue.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"item_ids": ["uuid1", "uuid2"]
|
||
}
|
||
```
|
||
|
||
**Response (204 No Content)**
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L259-L290)
|
||
|
||
### DELETE /api/queue/completed
|
||
|
||
Clear completed downloads from history.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"message": "Cleared 5 completed item(s)",
|
||
"count": 5
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L123-L149)
|
||
|
||
### DELETE /api/queue/failed
|
||
|
||
Clear failed downloads from history.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"message": "Cleared 2 failed item(s)",
|
||
"count": 2
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L152-L178)
|
||
|
||
### DELETE /api/queue/pending
|
||
|
||
Clear all pending downloads from the queue.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"message": "Removed 10 pending item(s)",
|
||
"count": 10
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L181-L207)
|
||
|
||
### POST /api/queue/reorder
|
||
|
||
Reorder items in the pending queue.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"item_ids": ["uuid3", "uuid1", "uuid2"]
|
||
}
|
||
```
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"message": "Queue reordered with 3 items"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L448-L477)
|
||
|
||
### POST /api/queue/retry
|
||
|
||
Retry failed downloads.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"item_ids": ["uuid1", "uuid2"]
|
||
}
|
||
```
|
||
|
||
Pass empty `item_ids` array to retry all failed items.
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"message": "Retrying 2 failed item(s)",
|
||
"retried_count": 2,
|
||
"retried_ids": ["uuid1", "uuid2"]
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/download.py](../src/server/api/download.py#L480-L514)
|
||
|
||
---
|
||
|
||
## 5. Configuration Endpoints
|
||
|
||
Prefix: `/api/config`
|
||
|
||
Source: [src/server/api/config.py](../src/server/api/config.py#L1-L374)
|
||
|
||
### GET /api/config
|
||
|
||
Return current application configuration.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"name": "Aniworld",
|
||
"data_dir": "data",
|
||
"scheduler": {
|
||
"enabled": true,
|
||
"interval_minutes": 60,
|
||
"schedule_time": "03:00",
|
||
"schedule_days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
|
||
"auto_download_after_rescan": false
|
||
},
|
||
"logging": {
|
||
"level": "INFO",
|
||
"file": null,
|
||
"max_bytes": null,
|
||
"backup_count": 3
|
||
},
|
||
"backup": {
|
||
"enabled": false,
|
||
"path": "data/backups",
|
||
"keep_days": 30
|
||
},
|
||
"other": {}
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/config.py](../src/server/api/config.py#L16-L27)
|
||
|
||
### PUT /api/config
|
||
|
||
Apply an update to the configuration.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"scheduler": {
|
||
"enabled": true,
|
||
"interval_minutes": 60,
|
||
"schedule_time": "06:30",
|
||
"schedule_days": ["mon", "wed", "fri"]
|
||
},
|
||
"logging": {
|
||
"level": "DEBUG"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response (200 OK):** Updated configuration object
|
||
|
||
Source: [src/server/api/config.py](../src/server/api/config.py#L30-L47)
|
||
|
||
### POST /api/config/validate
|
||
|
||
Validate a configuration without applying it.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Request Body:** Full `AppConfig` object
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"valid": true,
|
||
"errors": []
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/config.py](../src/server/api/config.py#L50-L64)
|
||
|
||
### GET /api/config/backups
|
||
|
||
List all available configuration backups.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
[
|
||
{
|
||
"name": "config_backup_20251213_090130.json",
|
||
"size": 1024,
|
||
"created": "2025-12-13T09:01:30Z"
|
||
}
|
||
]
|
||
```
|
||
|
||
Source: [src/server/api/config.py](../src/server/api/config.py#L67-L81)
|
||
|
||
### POST /api/config/backups
|
||
|
||
Create a backup of the current configuration.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Query Parameters:**
|
||
| Parameter | Type | Required | Description |
|
||
|-----------|------|----------|-------------|
|
||
| `name` | string | No | Custom backup name |
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"name": "config_backup_20251213_090130.json",
|
||
"message": "Backup created successfully"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/config.py](../src/server/api/config.py#L84-L102)
|
||
|
||
### POST /api/config/backups/{backup_name}/restore
|
||
|
||
Restore configuration from a backup.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):** Restored configuration object
|
||
|
||
Source: [src/server/api/config.py](../src/server/api/config.py#L105-L123)
|
||
|
||
### DELETE /api/config/backups/{backup_name}
|
||
|
||
Delete a configuration backup.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"message": "Backup 'config_backup_20251213.json' deleted successfully"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/config.py](../src/server/api/config.py#L126-L142)
|
||
|
||
### POST /api/config/directory
|
||
|
||
Update anime directory configuration.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"directory": "/path/to/anime"
|
||
}
|
||
```
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"message": "Anime directory updated successfully",
|
||
"synced_series": 15
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/config.py](../src/server/api/config.py#L189-L247)
|
||
|
||
---
|
||
|
||
## 6. NFO Management Endpoints
|
||
|
||
Prefix: `/api/nfo`
|
||
|
||
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L1-L684)
|
||
|
||
These endpoints manage tvshow.nfo metadata files and associated media (poster, logo, fanart) for anime series. NFO files use Kodi/XBMC format and are scraped from TMDB API.
|
||
|
||
**Prerequisites:**
|
||
|
||
- TMDB API key must be configured in settings
|
||
- NFO service returns 503 if API key not configured
|
||
|
||
### GET /api/nfo/{serie_id}/check
|
||
|
||
Check if NFO file and media files exist for a series.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Path Parameters:**
|
||
|
||
- `serie_id` (string): Series identifier
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"serie_id": "one-piece",
|
||
"serie_folder": "One Piece (1999)",
|
||
"has_nfo": true,
|
||
"nfo_path": "/path/to/anime/One Piece (1999)/tvshow.nfo",
|
||
"media_files": {
|
||
"has_poster": true,
|
||
"has_logo": false,
|
||
"has_fanart": true,
|
||
"poster_path": "/path/to/anime/One Piece (1999)/poster.jpg",
|
||
"logo_path": null,
|
||
"fanart_path": "/path/to/anime/One Piece (1999)/fanart.jpg"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
- `401 Unauthorized` - Not authenticated
|
||
- `404 Not Found` - Series not found
|
||
- `503 Service Unavailable` - TMDB API key not configured
|
||
|
||
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L90-L147)
|
||
|
||
### POST /api/nfo/{serie_id}/create
|
||
|
||
Create NFO file and download media for a series.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Path Parameters:**
|
||
|
||
- `serie_id` (string): Series identifier
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"serie_name": "One Piece",
|
||
"year": 1999,
|
||
"download_poster": true,
|
||
"download_logo": true,
|
||
"download_fanart": true,
|
||
"overwrite_existing": false
|
||
}
|
||
```
|
||
|
||
**Fields:**
|
||
|
||
- `serie_name` (string, optional): Series name for TMDB search (defaults to folder name)
|
||
- `year` (integer, optional): Series year to help narrow TMDB search
|
||
- `download_poster` (boolean, default: true): Download poster.jpg
|
||
- `download_logo` (boolean, default: true): Download logo.png
|
||
- `download_fanart` (boolean, default: true): Download fanart.jpg
|
||
- `overwrite_existing` (boolean, default: false): Overwrite existing NFO
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"serie_id": "one-piece",
|
||
"serie_folder": "One Piece (1999)",
|
||
"nfo_path": "/path/to/anime/One Piece (1999)/tvshow.nfo",
|
||
"media_files": {
|
||
"has_poster": true,
|
||
"has_logo": true,
|
||
"has_fanart": true,
|
||
"poster_path": "/path/to/anime/One Piece (1999)/poster.jpg",
|
||
"logo_path": "/path/to/anime/One Piece (1999)/logo.png",
|
||
"fanart_path": "/path/to/anime/One Piece (1999)/fanart.jpg"
|
||
},
|
||
"message": "NFO and media files created successfully"
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
- `401 Unauthorized` - Not authenticated
|
||
- `404 Not Found` - Series not found
|
||
- `409 Conflict` - NFO already exists (use `overwrite_existing: true`)
|
||
- `503 Service Unavailable` - TMDB API error or key not configured
|
||
|
||
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L150-L240)
|
||
|
||
### PUT /api/nfo/{serie_id}/update
|
||
|
||
Update existing NFO file with fresh TMDB data.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Path Parameters:**
|
||
|
||
- `serie_id` (string): Series identifier
|
||
|
||
**Query Parameters:**
|
||
|
||
- `download_media` (boolean, default: true): Re-download media files
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"serie_id": "one-piece",
|
||
"serie_folder": "One Piece (1999)",
|
||
"nfo_path": "/path/to/anime/One Piece (1999)/tvshow.nfo",
|
||
"media_files": {
|
||
"has_poster": true,
|
||
"has_logo": true,
|
||
"has_fanart": true,
|
||
"poster_path": "/path/to/anime/One Piece (1999)/poster.jpg",
|
||
"logo_path": "/path/to/anime/One Piece (1999)/logo.png",
|
||
"fanart_path": "/path/to/anime/One Piece (1999)/fanart.jpg"
|
||
},
|
||
"message": "NFO updated successfully"
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
- `401 Unauthorized` - Not authenticated
|
||
- `404 Not Found` - Series or NFO not found (use create endpoint)
|
||
- `503 Service Unavailable` - TMDB API error
|
||
|
||
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L243-L325)
|
||
|
||
### GET /api/nfo/{serie_id}/content
|
||
|
||
Get NFO file XML content for a series.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Path Parameters:**
|
||
|
||
- `serie_id` (string): Series identifier
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"serie_id": "one-piece",
|
||
"serie_folder": "One Piece (1999)",
|
||
"content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tvshow>...</tvshow>",
|
||
"file_size": 2048,
|
||
"last_modified": "2026-01-15T10:30:00"
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
- `401 Unauthorized` - Not authenticated
|
||
- `404 Not Found` - Series or NFO not found
|
||
|
||
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L328-L397)
|
||
|
||
### GET /api/nfo/{serie_id}/media/status
|
||
|
||
Get media files status for a series.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Path Parameters:**
|
||
|
||
- `serie_id` (string): Series identifier
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"has_poster": true,
|
||
"has_logo": false,
|
||
"has_fanart": true,
|
||
"poster_path": "/path/to/anime/One Piece (1999)/poster.jpg",
|
||
"logo_path": null,
|
||
"fanart_path": "/path/to/anime/One Piece (1999)/fanart.jpg"
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
- `401 Unauthorized` - Not authenticated
|
||
- `404 Not Found` - Series not found
|
||
|
||
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L400-L447)
|
||
|
||
### POST /api/nfo/{serie_id}/media/download
|
||
|
||
Download missing media files for a series.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Path Parameters:**
|
||
|
||
- `serie_id` (string): Series identifier
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"download_poster": true,
|
||
"download_logo": true,
|
||
"download_fanart": true
|
||
}
|
||
```
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"has_poster": true,
|
||
"has_logo": true,
|
||
"has_fanart": true,
|
||
"poster_path": "/path/to/anime/One Piece (1999)/poster.jpg",
|
||
"logo_path": "/path/to/anime/One Piece (1999)/logo.png",
|
||
"fanart_path": "/path/to/anime/One Piece (1999)/fanart.jpg"
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
- `401 Unauthorized` - Not authenticated
|
||
- `404 Not Found` - Series or NFO not found (NFO required for TMDB ID)
|
||
- `503 Service Unavailable` - TMDB API error
|
||
|
||
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L450-L519)
|
||
|
||
### POST /api/nfo/batch/create
|
||
|
||
Batch create NFO files for multiple series.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Request Body:**
|
||
|
||
```json
|
||
{
|
||
"serie_ids": ["one-piece", "naruto", "bleach"],
|
||
"download_media": true,
|
||
"skip_existing": true,
|
||
"max_concurrent": 3
|
||
}
|
||
```
|
||
|
||
**Fields:**
|
||
|
||
- `serie_ids` (array of strings): Series identifiers to process
|
||
- `download_media` (boolean, default: true): Download media files
|
||
- `skip_existing` (boolean, default: true): Skip series with existing NFOs
|
||
- `max_concurrent` (integer, 1-10, default: 3): Number of concurrent operations
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"total": 3,
|
||
"successful": 2,
|
||
"failed": 0,
|
||
"skipped": 1,
|
||
"results": [
|
||
{
|
||
"serie_id": "one-piece",
|
||
"serie_folder": "One Piece (1999)",
|
||
"success": true,
|
||
"message": "NFO created successfully",
|
||
"nfo_path": "/path/to/anime/One Piece (1999)/tvshow.nfo"
|
||
},
|
||
{
|
||
"serie_id": "naruto",
|
||
"serie_folder": "Naruto (2002)",
|
||
"success": false,
|
||
"message": "Skipped - NFO already exists",
|
||
"nfo_path": null
|
||
},
|
||
{
|
||
"serie_id": "bleach",
|
||
"serie_folder": "Bleach (2004)",
|
||
"success": true,
|
||
"message": "NFO created successfully",
|
||
"nfo_path": "/path/to/anime/Bleach (2004)/tvshow.nfo"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
- `401 Unauthorized` - Not authenticated
|
||
- `503 Service Unavailable` - TMDB API key not configured
|
||
|
||
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L522-L634)
|
||
|
||
### GET /api/nfo/missing
|
||
|
||
Get list of series without NFO files.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"total_series": 150,
|
||
"missing_nfo_count": 23,
|
||
"series": [
|
||
{
|
||
"serie_id": "dragon-ball",
|
||
"serie_folder": "Dragon Ball (1986)",
|
||
"serie_name": "Dragon Ball",
|
||
"has_media": false,
|
||
"media_files": {
|
||
"has_poster": false,
|
||
"has_logo": false,
|
||
"has_fanart": false,
|
||
"poster_path": null,
|
||
"logo_path": null,
|
||
"fanart_path": null
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
- `401 Unauthorized` - Not authenticated
|
||
- `503 Service Unavailable` - TMDB API key not configured
|
||
|
||
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L637-L684)
|
||
|
||
---
|
||
|
||
## 7. Scheduler Endpoints
|
||
|
||
Prefix: `/api/scheduler`
|
||
|
||
All GET/POST config responses share the same envelope:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"config": { ... },
|
||
"status": { ... }
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/scheduler.py](../src/server/api/scheduler.py)
|
||
|
||
### GET /api/scheduler/config
|
||
|
||
Get current scheduler configuration and runtime status.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"config": {
|
||
"enabled": true,
|
||
"interval_minutes": 60,
|
||
"schedule_time": "03:00",
|
||
"schedule_days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
|
||
"auto_download_after_rescan": false
|
||
},
|
||
"status": {
|
||
"is_running": true,
|
||
"next_run": "2025-07-15T03:00:00+00:00",
|
||
"last_run": null,
|
||
"scan_in_progress": false
|
||
}
|
||
}
|
||
```
|
||
|
||
### POST /api/scheduler/config
|
||
|
||
Update scheduler configuration and apply changes immediately.
|
||
|
||
**Authentication:** Required
|
||
|
||
**Request Body (all fields optional, uses model defaults):**
|
||
|
||
```json
|
||
{
|
||
"enabled": true,
|
||
"schedule_time": "06:30",
|
||
"schedule_days": ["mon", "wed", "fri"],
|
||
"auto_download_after_rescan": true
|
||
}
|
||
```
|
||
|
||
**Response (200 OK):** Same envelope as GET, reflecting saved values.
|
||
|
||
**Validation errors (422):**
|
||
|
||
- `schedule_time` must match `HH:MM` (00:00–23:59)
|
||
- `schedule_days` entries must be one of `mon tue wed thu fri sat sun`
|
||
- `interval_minutes` must be ≥ 1
|
||
|
||
### POST /api/scheduler/trigger-rescan
|
||
|
||
Manually trigger a library rescan (and auto-download if configured).
|
||
|
||
**Authentication:** Required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"message": "Rescan started successfully"
|
||
}
|
||
```
|
||
|
||
**Error responses:**
|
||
|
||
- `503` — SeriesApp not yet initialised
|
||
- `500` — Rescan failed unexpectedly
|
||
|
||
---
|
||
|
||
## 8. Health Check Endpoints
|
||
|
||
Prefix: `/health`
|
||
|
||
Source: [src/server/api/health.py](../src/server/api/health.py#L1-L267)
|
||
|
||
### GET /health
|
||
|
||
Basic health check endpoint.
|
||
|
||
**Authentication:** Not required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "healthy",
|
||
"timestamp": "2025-12-13T10:30:00.000Z",
|
||
"version": "1.0.1"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/health.py](../src/server/api/health.py#L151-L161)
|
||
|
||
### GET /health/detailed
|
||
|
||
Comprehensive health check with database, filesystem, and system metrics.
|
||
|
||
**Authentication:** Not required
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "healthy",
|
||
"timestamp": "2025-12-13T10:30:00.000Z",
|
||
"version": "1.0.1",
|
||
"dependencies": {
|
||
"database": {
|
||
"status": "healthy",
|
||
"connection_time_ms": 1.5,
|
||
"message": "Database connection successful"
|
||
},
|
||
"filesystem": {
|
||
"status": "healthy",
|
||
"data_dir_writable": true,
|
||
"logs_dir_writable": true
|
||
},
|
||
"system": {
|
||
"cpu_percent": 25.0,
|
||
"memory_percent": 45.0,
|
||
"memory_available_mb": 8192.0,
|
||
"disk_percent": 60.0,
|
||
"disk_free_mb": 102400.0,
|
||
"uptime_seconds": 86400.0
|
||
}
|
||
},
|
||
"startup_time": "2025-12-13T08:00:00.000Z"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/health.py](../src/server/api/health.py#L164-L200)
|
||
|
||
---
|
||
|
||
## 9. WebSocket Protocol
|
||
|
||
Endpoint: `/ws/connect`
|
||
|
||
Source: [src/server/api/websocket.py](../src/server/api/websocket.py#L1-L260)
|
||
|
||
### Connection
|
||
|
||
**URL:** `ws://127.0.0.1:8000/ws/connect`
|
||
|
||
**Query Parameters:**
|
||
| Parameter | Required | Description |
|
||
|-----------|----------|-------------|
|
||
| `token` | No | JWT token for authenticated access |
|
||
|
||
### Message Types
|
||
|
||
| Type | Direction | Description |
|
||
| ------------------- | ---------------- | -------------------------- |
|
||
| `connected` | Server -> Client | Connection confirmation |
|
||
| `ping` | Client -> Server | Keepalive request |
|
||
| `pong` | Server -> Client | Keepalive response |
|
||
| `download_progress` | Server -> Client | Download progress update |
|
||
| `download_complete` | Server -> Client | Download completed |
|
||
| `download_failed` | Server -> Client | Download failed |
|
||
| `download_added` | Server -> Client | Item added to queue |
|
||
| `download_removed` | Server -> Client | Item removed from queue |
|
||
| `queue_status` | Server -> Client | Queue status update |
|
||
| `queue_started` | Server -> Client | Queue processing started |
|
||
| `queue_stopped` | Server -> Client | Queue processing stopped |
|
||
| `scan_progress` | Server -> Client | Library scan progress |
|
||
| `scan_complete` | Server -> Client | Library scan completed |
|
||
| `system_info` | Server -> Client | System information message |
|
||
| `error` | Server -> Client | Error message |
|
||
|
||
Source: [src/server/models/websocket.py](../src/server/models/websocket.py#L25-L57)
|
||
|
||
### Room Subscriptions
|
||
|
||
Clients can join/leave rooms to receive specific updates.
|
||
|
||
**Join Room:**
|
||
|
||
```json
|
||
{
|
||
"action": "join",
|
||
"data": { "room": "downloads" }
|
||
}
|
||
```
|
||
|
||
**Leave Room:**
|
||
|
||
```json
|
||
{
|
||
"action": "leave",
|
||
"data": { "room": "downloads" }
|
||
}
|
||
```
|
||
|
||
**Available Rooms:**
|
||
|
||
- `downloads` - Download progress and status updates
|
||
|
||
### Server Message Format
|
||
|
||
```json
|
||
{
|
||
"type": "download_progress",
|
||
"timestamp": "2025-12-13T10:30:00.000Z",
|
||
"data": {
|
||
"download_id": "uuid-here",
|
||
"key": "attack-on-titan",
|
||
"folder": "Attack on Titan (2013)",
|
||
"percent": 45.2,
|
||
"speed_mbps": 2.5,
|
||
"eta_seconds": 180
|
||
}
|
||
}
|
||
```
|
||
|
||
### WebSocket Status Endpoint
|
||
|
||
**GET /ws/status**
|
||
|
||
Returns WebSocket service status.
|
||
|
||
**Response (200 OK):**
|
||
|
||
```json
|
||
{
|
||
"status": "operational",
|
||
"active_connections": 5,
|
||
"supported_message_types": [
|
||
"download_progress",
|
||
"download_complete",
|
||
"download_failed",
|
||
"queue_status",
|
||
"connected",
|
||
"ping",
|
||
"pong",
|
||
"error"
|
||
]
|
||
}
|
||
```
|
||
|
||
Source: [src/server/api/websocket.py](../src/server/api/websocket.py#L238-L260)
|
||
|
||
---
|
||
|
||
## 10. Data Models
|
||
|
||
### Download Item
|
||
|
||
```json
|
||
{
|
||
"id": "uuid-string",
|
||
"serie_id": "attack-on-titan",
|
||
"serie_folder": "Attack on Titan (2013)",
|
||
"serie_name": "Attack on Titan",
|
||
"episode": {
|
||
"season": 1,
|
||
"episode": 1,
|
||
"title": "To You, in 2000 Years"
|
||
},
|
||
"status": "pending",
|
||
"priority": "NORMAL",
|
||
"added_at": "2025-12-13T10:00:00Z",
|
||
"started_at": null,
|
||
"completed_at": null,
|
||
"progress": null,
|
||
"error": null,
|
||
"retry_count": 0,
|
||
"source_url": null
|
||
}
|
||
```
|
||
|
||
**Status Values:** `pending`, `downloading`, `paused`, `completed`, `failed`, `cancelled`
|
||
|
||
**Priority Values:** `LOW`, `NORMAL`, `HIGH`
|
||
|
||
Source: [src/server/models/download.py](../src/server/models/download.py#L63-L118)
|
||
|
||
### Episode Identifier
|
||
|
||
```json
|
||
{
|
||
"season": 1,
|
||
"episode": 1,
|
||
"title": "Episode Title"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/models/download.py](../src/server/models/download.py#L36-L41)
|
||
|
||
### Download Progress
|
||
|
||
```json
|
||
{
|
||
"percent": 45.2,
|
||
"downloaded_mb": 256.0,
|
||
"total_mb": 512.0,
|
||
"speed_mbps": 2.5,
|
||
"eta_seconds": 180
|
||
}
|
||
```
|
||
|
||
Source: [src/server/models/download.py](../src/server/models/download.py#L44-L60)
|
||
|
||
---
|
||
|
||
## 11. Error Handling
|
||
|
||
### HTTP Status Codes
|
||
|
||
| Code | Meaning | When Used |
|
||
| ---- | --------------------- | --------------------------------- |
|
||
| 200 | OK | Successful request |
|
||
| 201 | Created | Resource created |
|
||
| 204 | No Content | Successful deletion |
|
||
| 400 | Bad Request | Invalid request body/parameters |
|
||
| 401 | Unauthorized | Missing or invalid authentication |
|
||
| 403 | Forbidden | Insufficient permissions |
|
||
| 404 | Not Found | Resource does not exist |
|
||
| 422 | Unprocessable Entity | Validation error |
|
||
| 429 | Too Many Requests | Rate limit exceeded |
|
||
| 500 | Internal Server Error | Server-side error |
|
||
|
||
### Error Response Format
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": "VALIDATION_ERROR",
|
||
"message": "Human-readable error message",
|
||
"details": {
|
||
"field": "Additional context"
|
||
},
|
||
"request_id": "uuid-for-tracking"
|
||
}
|
||
```
|
||
|
||
Source: [src/server/middleware/error_handler.py](../src/server/middleware/error_handler.py#L26-L56)
|
||
|
||
### Common Error Codes
|
||
|
||
| Error Code | HTTP Status | Description |
|
||
| ---------------------- | ----------- | ------------------------------ |
|
||
| `AUTHENTICATION_ERROR` | 401 | Invalid or missing credentials |
|
||
| `AUTHORIZATION_ERROR` | 403 | Insufficient permissions |
|
||
| `VALIDATION_ERROR` | 422 | Request validation failed |
|
||
| `NOT_FOUND_ERROR` | 404 | Resource not found |
|
||
| `CONFLICT_ERROR` | 409 | Resource conflict |
|
||
| `RATE_LIMIT_ERROR` | 429 | Rate limit exceeded |
|
||
|
||
---
|
||
|
||
## 12. Rate Limiting
|
||
|
||
### Authentication Endpoints
|
||
|
||
| Endpoint | Limit | Window |
|
||
| ---------------------- | ---------- | ---------- |
|
||
| `POST /api/auth/login` | 5 requests | 60 seconds |
|
||
| `POST /api/auth/setup` | 5 requests | 60 seconds |
|
||
|
||
Source: [src/server/middleware/auth.py](../src/server/middleware/auth.py#L143-L162)
|
||
|
||
### Origin-Based Limiting
|
||
|
||
All endpoints from the same origin are limited to 60 requests per minute per origin.
|
||
|
||
Source: [src/server/middleware/auth.py](../src/server/middleware/auth.py#L115-L133)
|
||
|
||
### Rate Limit Response
|
||
|
||
```json
|
||
{
|
||
"detail": "Too many authentication attempts, try again later"
|
||
}
|
||
```
|
||
|
||
HTTP Status: 429 Too Many Requests
|
||
|
||
---
|
||
|
||
## 13. Pagination
|
||
|
||
The anime list endpoint supports pagination.
|
||
|
||
**Query Parameters:**
|
||
| Parameter | Default | Max | Description |
|
||
|-----------|---------|-----|-------------|
|
||
| `page` | 1 | - | Page number (1-indexed) |
|
||
| `per_page` | 20 | 1000 | Items per page |
|
||
|
||
**Example:**
|
||
|
||
```
|
||
GET /api/anime?page=2&per_page=50
|
||
```
|
||
|
||
Source: [src/server/api/anime.py](../src/server/api/anime.py#L180-L220)
|