docu
This commit is contained in:
326
docs/DATABASE.md
Normal file
326
docs/DATABASE.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# 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:`) |
|
||||
Reference in New Issue
Block a user