327 lines
13 KiB
Markdown
327 lines
13 KiB
Markdown
# Database Documentation
|
|
|
|
## Document Purpose
|
|
|
|
This document describes the database schema, models, and data layer of the Aniworld application.
|
|
|
|
---
|
|
|
|
## 1. Database Overview
|
|
|
|
### Technology
|
|
|
|
- **Database Engine**: SQLite 3 (default), PostgreSQL supported
|
|
- **ORM**: SQLAlchemy 2.0 with async support (aiosqlite)
|
|
- **Location**: `data/aniworld.db` (configurable via `DATABASE_URL`)
|
|
|
|
Source: [src/config/settings.py](../src/config/settings.py#L53-L55)
|
|
|
|
### Connection Configuration
|
|
|
|
```python
|
|
# Default connection string
|
|
DATABASE_URL = "sqlite+aiosqlite:///./data/aniworld.db"
|
|
|
|
# PostgreSQL alternative
|
|
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/aniworld"
|
|
```
|
|
|
|
Source: [src/server/database/connection.py](../src/server/database/connection.py)
|
|
|
|
---
|
|
|
|
## 2. Entity Relationship Diagram
|
|
|
|
```
|
|
+-------------------+ +-------------------+ +------------------------+
|
|
| anime_series | | episodes | | download_queue_item |
|
|
+-------------------+ +-------------------+ +------------------------+
|
|
| id (PK) |<--+ | id (PK) | +-->| id (PK, VARCHAR) |
|
|
| key (UNIQUE) | | | series_id (FK)----+---+ | series_id (FK)---------+
|
|
| name | +---| | | status |
|
|
| site | | season | | priority |
|
|
| folder | | episode_number | | season |
|
|
| created_at | | title | | episode |
|
|
| updated_at | | file_path | | progress_percent |
|
|
+-------------------+ | is_downloaded | | error_message |
|
|
| created_at | | retry_count |
|
|
| updated_at | | added_at |
|
|
+-------------------+ | started_at |
|
|
| completed_at |
|
|
| created_at |
|
|
| updated_at |
|
|
+------------------------+
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Table Schemas
|
|
|
|
### 3.1 anime_series
|
|
|
|
Stores anime series metadata.
|
|
|
|
| Column | Type | Constraints | Description |
|
|
| ------------ | ------------- | -------------------------- | ------------------------------------------------------- |
|
|
| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Internal database ID |
|
|
| `key` | VARCHAR(255) | UNIQUE, NOT NULL, INDEX | **Primary identifier** - provider-assigned URL-safe key |
|
|
| `name` | VARCHAR(500) | NOT NULL, INDEX | Display name of the series |
|
|
| `site` | VARCHAR(500) | NOT NULL | Provider site URL |
|
|
| `folder` | VARCHAR(1000) | NOT NULL | Filesystem folder name (metadata only) |
|
|
| `created_at` | DATETIME | NOT NULL, DEFAULT NOW | Record creation timestamp |
|
|
| `updated_at` | DATETIME | NOT NULL, ON UPDATE NOW | Last update timestamp |
|
|
|
|
**Identifier Convention:**
|
|
|
|
- `key` is the **primary identifier** for all operations (e.g., `"attack-on-titan"`)
|
|
- `folder` is **metadata only** for filesystem operations (e.g., `"Attack on Titan (2013)"`)
|
|
- `id` is used only for database relationships
|
|
|
|
Source: [src/server/database/models.py](../src/server/database/models.py#L23-L87)
|
|
|
|
### 3.2 episodes
|
|
|
|
Stores individual episode information.
|
|
|
|
| Column | Type | Constraints | Description |
|
|
| ---------------- | ------------- | ---------------------------- | ----------------------------- |
|
|
| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Internal database ID |
|
|
| `series_id` | INTEGER | FOREIGN KEY, NOT NULL, INDEX | Reference to anime_series.id |
|
|
| `season` | INTEGER | NOT NULL | Season number (1-based) |
|
|
| `episode_number` | INTEGER | NOT NULL | Episode number within season |
|
|
| `title` | VARCHAR(500) | NULLABLE | Episode title if known |
|
|
| `file_path` | VARCHAR(1000) | NULLABLE | Local file path if downloaded |
|
|
| `is_downloaded` | BOOLEAN | NOT NULL, DEFAULT FALSE | Download status flag |
|
|
| `created_at` | DATETIME | NOT NULL, DEFAULT NOW | Record creation timestamp |
|
|
| `updated_at` | DATETIME | NOT NULL, ON UPDATE NOW | Last update timestamp |
|
|
|
|
**Foreign Key:**
|
|
|
|
- `series_id` -> `anime_series.id` (ON DELETE CASCADE)
|
|
|
|
Source: [src/server/database/models.py](../src/server/database/models.py#L122-L181)
|
|
|
|
### 3.3 download_queue_item
|
|
|
|
Stores download queue items with status tracking.
|
|
|
|
| Column | Type | Constraints | Description |
|
|
| ------------------ | ------------- | --------------------------- | ------------------------------ |
|
|
| `id` | VARCHAR(36) | PRIMARY KEY | UUID identifier |
|
|
| `series_id` | INTEGER | FOREIGN KEY, NOT NULL | Reference to anime_series.id |
|
|
| `season` | INTEGER | NOT NULL | Season number |
|
|
| `episode` | INTEGER | NOT NULL | Episode number |
|
|
| `status` | VARCHAR(20) | NOT NULL, DEFAULT 'pending' | Download status |
|
|
| `priority` | VARCHAR(10) | NOT NULL, DEFAULT 'NORMAL' | Queue priority |
|
|
| `progress_percent` | FLOAT | NULLABLE | Download progress (0-100) |
|
|
| `error_message` | TEXT | NULLABLE | Error description if failed |
|
|
| `retry_count` | INTEGER | NOT NULL, DEFAULT 0 | Number of retry attempts |
|
|
| `source_url` | VARCHAR(2000) | NULLABLE | Download source URL |
|
|
| `added_at` | DATETIME | NOT NULL, DEFAULT NOW | When added to queue |
|
|
| `started_at` | DATETIME | NULLABLE | When download started |
|
|
| `completed_at` | DATETIME | NULLABLE | When download completed/failed |
|
|
| `created_at` | DATETIME | NOT NULL, DEFAULT NOW | Record creation timestamp |
|
|
| `updated_at` | DATETIME | NOT NULL, ON UPDATE NOW | Last update timestamp |
|
|
|
|
**Status Values:** `pending`, `downloading`, `paused`, `completed`, `failed`, `cancelled`
|
|
|
|
**Priority Values:** `LOW`, `NORMAL`, `HIGH`
|
|
|
|
**Foreign Key:**
|
|
|
|
- `series_id` -> `anime_series.id` (ON DELETE CASCADE)
|
|
|
|
Source: [src/server/database/models.py](../src/server/database/models.py#L200-L300)
|
|
|
|
---
|
|
|
|
## 4. Indexes
|
|
|
|
| Table | Index Name | Columns | Purpose |
|
|
| --------------------- | ----------------------- | ----------- | --------------------------------- |
|
|
| `anime_series` | `ix_anime_series_key` | `key` | Fast lookup by primary identifier |
|
|
| `anime_series` | `ix_anime_series_name` | `name` | Search by name |
|
|
| `episodes` | `ix_episodes_series_id` | `series_id` | Join with series |
|
|
| `download_queue_item` | `ix_download_series_id` | `series_id` | Filter by series |
|
|
| `download_queue_item` | `ix_download_status` | `status` | Filter by status |
|
|
|
|
---
|
|
|
|
## 5. Model Layer
|
|
|
|
### 5.1 SQLAlchemy ORM Models
|
|
|
|
```python
|
|
# src/server/database/models.py
|
|
|
|
class AnimeSeries(Base, TimestampMixin):
|
|
__tablename__ = "anime_series"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
key: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
|
name: Mapped[str] = mapped_column(String(500), index=True)
|
|
site: Mapped[str] = mapped_column(String(500))
|
|
folder: Mapped[str] = mapped_column(String(1000))
|
|
|
|
episodes: Mapped[List["Episode"]] = relationship(
|
|
"Episode", back_populates="series", cascade="all, delete-orphan"
|
|
)
|
|
```
|
|
|
|
Source: [src/server/database/models.py](../src/server/database/models.py#L23-L87)
|
|
|
|
### 5.2 Pydantic API Models
|
|
|
|
```python
|
|
# src/server/models/download.py
|
|
|
|
class DownloadItem(BaseModel):
|
|
id: str
|
|
serie_id: str # Maps to anime_series.key
|
|
serie_folder: str # Metadata only
|
|
serie_name: str
|
|
episode: EpisodeIdentifier
|
|
status: DownloadStatus
|
|
priority: DownloadPriority
|
|
```
|
|
|
|
Source: [src/server/models/download.py](../src/server/models/download.py#L63-L118)
|
|
|
|
### 5.3 Model Mapping
|
|
|
|
| API Field | Database Column | Notes |
|
|
| -------------- | --------------------- | ------------------ |
|
|
| `serie_id` | `anime_series.key` | Primary identifier |
|
|
| `serie_folder` | `anime_series.folder` | Metadata only |
|
|
| `serie_name` | `anime_series.name` | Display name |
|
|
|
|
---
|
|
|
|
## 6. Repository Pattern
|
|
|
|
The `QueueRepository` class provides data access abstraction.
|
|
|
|
```python
|
|
class QueueRepository:
|
|
async def save_item(self, item: DownloadItem) -> None:
|
|
"""Save or update a download item."""
|
|
|
|
async def get_all_items(self) -> List[DownloadItem]:
|
|
"""Get all items from database."""
|
|
|
|
async def delete_item(self, item_id: str) -> bool:
|
|
"""Delete item by ID."""
|
|
|
|
async def get_items_by_status(
|
|
self, status: DownloadStatus
|
|
) -> List[DownloadItem]:
|
|
"""Get items filtered by status."""
|
|
```
|
|
|
|
Source: [src/server/services/queue_repository.py](../src/server/services/queue_repository.py)
|
|
|
|
---
|
|
|
|
## 7. Database Service
|
|
|
|
The `AnimeSeriesService` provides async CRUD operations.
|
|
|
|
```python
|
|
class AnimeSeriesService:
|
|
@staticmethod
|
|
async def create(
|
|
db: AsyncSession,
|
|
key: str,
|
|
name: str,
|
|
site: str,
|
|
folder: str
|
|
) -> AnimeSeries:
|
|
"""Create a new anime series."""
|
|
|
|
@staticmethod
|
|
async def get_by_key(
|
|
db: AsyncSession,
|
|
key: str
|
|
) -> Optional[AnimeSeries]:
|
|
"""Get series by primary key identifier."""
|
|
```
|
|
|
|
Source: [src/server/database/service.py](../src/server/database/service.py)
|
|
|
|
---
|
|
|
|
## 8. Data Integrity Rules
|
|
|
|
### Validation Constraints
|
|
|
|
| Field | Rule | Error Message |
|
|
| ------------------------- | ------------------------ | ------------------------------------- |
|
|
| `anime_series.key` | Non-empty, max 255 chars | "Series key cannot be empty" |
|
|
| `anime_series.name` | Non-empty, max 500 chars | "Series name cannot be empty" |
|
|
| `episodes.season` | 0-1000 | "Season number must be non-negative" |
|
|
| `episodes.episode_number` | 0-10000 | "Episode number must be non-negative" |
|
|
|
|
Source: [src/server/database/models.py](../src/server/database/models.py#L89-L119)
|
|
|
|
### Cascade Rules
|
|
|
|
- Deleting `anime_series` deletes all related `episodes` and `download_queue_item`
|
|
|
|
---
|
|
|
|
## 9. Migration Strategy
|
|
|
|
Currently, SQLAlchemy's `create_all()` is used for schema creation.
|
|
|
|
```python
|
|
# src/server/database/connection.py
|
|
async def init_db():
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
```
|
|
|
|
For production migrations, Alembic is recommended but not yet implemented.
|
|
|
|
Source: [src/server/database/connection.py](../src/server/database/connection.py)
|
|
|
|
---
|
|
|
|
## 10. Common Query Patterns
|
|
|
|
### Get all series with missing episodes
|
|
|
|
```python
|
|
series = await db.execute(
|
|
select(AnimeSeries).options(selectinload(AnimeSeries.episodes))
|
|
)
|
|
for serie in series.scalars():
|
|
downloaded = [e for e in serie.episodes if e.is_downloaded]
|
|
```
|
|
|
|
### Get pending downloads ordered by priority
|
|
|
|
```python
|
|
items = await db.execute(
|
|
select(DownloadQueueItem)
|
|
.where(DownloadQueueItem.status == "pending")
|
|
.order_by(
|
|
case(
|
|
(DownloadQueueItem.priority == "HIGH", 1),
|
|
(DownloadQueueItem.priority == "NORMAL", 2),
|
|
(DownloadQueueItem.priority == "LOW", 3),
|
|
),
|
|
DownloadQueueItem.added_at
|
|
)
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Database Location
|
|
|
|
| Environment | Default Location |
|
|
| ----------- | ------------------------------------------------- |
|
|
| Development | `./data/aniworld.db` |
|
|
| Production | Via `DATABASE_URL` environment variable |
|
|
| Testing | In-memory SQLite (`sqlite+aiosqlite:///:memory:`) |
|