- Replace asyncio sleep loop with APScheduler AsyncIOScheduler + CronTrigger
- Add schedule_time (HH:MM), schedule_days (days of week), auto_download_after_rescan fields to SchedulerConfig
- Add _auto_download_missing() to queue missing episodes after rescan
- Reload config live via reload_config(SchedulerConfig) without restart
- Update GET/POST /api/scheduler/config to return {success, config, status} envelope
- Add day-of-week pill toggles to Settings -> Scheduler section in UI
- Update JS loadSchedulerConfig / saveSchedulerConfig for new API shape
- Add 29 unit tests for SchedulerConfig model, 18 unit tests for SchedulerService
- Rewrite 23 endpoint tests and 36 integration tests for APScheduler behaviour
- Coverage: 96% api/scheduler, 95% scheduler_service, 90% total (>= 80% threshold)
- Update docs: API.md, CONFIGURATION.md, features.md, CHANGELOG.md
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: `no_episodes` (shows only series with missing episodes - episodes in DB that haven't been downloaded yet) |
|
||
|
||
**Filter Details:**
|
||
|
||
- `no_episodes`: Returns series that have at least one episode in the database with `is_downloaded=False`
|
||
- Episodes in the database represent MISSING episodes (from episodeDict during scanning)
|
||
- `is_downloaded=False` means the episode file was not found in the folder
|
||
- This effectively shows series where no video files were found for missing episodes
|
||
|
||
**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.0"
|
||
}
|
||
```
|
||
|
||
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.0",
|
||
"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)
|