Aniworld/docs/DATABASE.md
2025-12-15 14:07:04 +01:00

13 KiB

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

Connection Configuration

# 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


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

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

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


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

# 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

5.2 Pydantic API Models

# 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

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.

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


7. Database Service

The AnimeSeriesService provides async CRUD operations.

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


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

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.

# 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


10. Common Query Patterns

Get all series with missing episodes

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

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:)