# Database Layer

SQLAlchemy-based database layer for the Aniworld web application.

## Overview

This package provides persistent storage for anime series, episodes, download queue, and user sessions using SQLAlchemy ORM with comprehensive service layer for CRUD operations.

## Quick Start

### Installation

Install required dependencies:

```bash
pip install sqlalchemy aiosqlite
```

Or use the project requirements:

```bash
pip install -r requirements.txt
```

### Initialization

Initialize the database on application startup:

```python
from src.server.database import init_db, close_db

# Startup
await init_db()

# Shutdown
await close_db()
```

### Usage in FastAPI

Use the database session dependency in your endpoints:

```python
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from src.server.database import get_db_session, AnimeSeries
from sqlalchemy import select

@app.get("/anime")
async def get_anime(db: AsyncSession = Depends(get_db_session)):
    result = await db.execute(select(AnimeSeries))
    return result.scalars().all()
```

## Models

### AnimeSeries

Represents an anime series with metadata and relationships.

```python
series = AnimeSeries(
    key="attack-on-titan",
    name="Attack on Titan",
    site="https://aniworld.to",
    folder="/anime/attack-on-titan",
    description="Epic anime about titans",
    status="completed",
    total_episodes=75
)
```

### Episode

Individual episodes linked to series.

```python
episode = Episode(
    series_id=series.id,
    season=1,
    episode_number=5,
    title="The Fifth Episode",
    is_downloaded=True
)
```

### DownloadQueueItem

Download queue with progress tracking.

```python
from src.server.database.models import DownloadStatus, DownloadPriority

item = DownloadQueueItem(
    series_id=series.id,
    season=1,
    episode_number=3,
    status=DownloadStatus.DOWNLOADING,
    priority=DownloadPriority.HIGH,
    progress_percent=45.5
)
```

### UserSession

User authentication sessions.

```python
from datetime import datetime, timedelta

session = UserSession(
    session_id="unique-session-id",
    token_hash="hashed-jwt-token",
    expires_at=datetime.utcnow() + timedelta(hours=24),
    is_active=True
)
```

## Mixins

### TimestampMixin

Adds automatic timestamp tracking:

```python
from src.server.database.base import Base, TimestampMixin

class MyModel(Base, TimestampMixin):
    __tablename__ = "my_table"
    # created_at and updated_at automatically added
```

### SoftDeleteMixin

Provides soft delete functionality:

```python
from src.server.database.base import Base, SoftDeleteMixin

class MyModel(Base, SoftDeleteMixin):
    __tablename__ = "my_table"

    # Usage
    instance.soft_delete()  # Mark as deleted
    instance.is_deleted     # Check if deleted
    instance.restore()      # Restore deleted record
```

## Configuration

Configure database via environment variables:

```bash
DATABASE_URL=sqlite:///./data/aniworld.db
LOG_LEVEL=DEBUG  # Enables SQL query logging
```

Or in code:

```python
from src.config.settings import settings

settings.database_url = "sqlite:///./data/aniworld.db"
```

## Testing

Run database tests:

```bash
pytest tests/unit/test_database_models.py -v
```

The test suite uses an in-memory SQLite database for isolation and speed.

## Architecture

-   **base.py**: Base declarative class and mixins
-   **models.py**: SQLAlchemy ORM models (4 models)
-   **connection.py**: Engine, session factory, dependency injection
-   \***\*init**.py\*\*: Package exports
-   **service.py**: Service layer with CRUD operations

## Service Layer

The service layer provides high-level CRUD operations for all models:

### AnimeSeriesService

```python
from src.server.database import AnimeSeriesService

# Create series
series = await AnimeSeriesService.create(
    db,
    key="my-anime",
    name="My Anime",
    site="https://example.com",
    folder="/path/to/anime"
)

# Get by ID or key
series = await AnimeSeriesService.get_by_id(db, series_id)
series = await AnimeSeriesService.get_by_key(db, "my-anime")

# Get all with pagination
all_series = await AnimeSeriesService.get_all(db, limit=50, offset=0)

# Update
updated = await AnimeSeriesService.update(db, series_id, name="Updated Name")

# Delete (cascades to episodes and downloads)
deleted = await AnimeSeriesService.delete(db, series_id)

# Search
results = await AnimeSeriesService.search(db, "naruto", limit=10)
```

### EpisodeService

```python
from src.server.database import EpisodeService

# Create episode
episode = await EpisodeService.create(
    db,
    series_id=1,
    season=1,
    episode_number=5,
    title="Episode 5"
)

# Get episodes for series
episodes = await EpisodeService.get_by_series(db, series_id, season=1)

# Get specific episode
episode = await EpisodeService.get_by_episode(db, series_id, season=1, episode_number=5)

# Mark as downloaded
updated = await EpisodeService.mark_downloaded(
    db,
    episode_id,
    file_path="/path/to/file.mp4",
    file_size=1024000
)
```

### DownloadQueueService

```python
from src.server.database import DownloadQueueService
from src.server.database.models import DownloadPriority, DownloadStatus

# Add to queue
item = await DownloadQueueService.create(
    db,
    series_id=1,
    season=1,
    episode_number=5,
    priority=DownloadPriority.HIGH
)

# Get pending downloads (ordered by priority)
pending = await DownloadQueueService.get_pending(db, limit=10)

# Get active downloads
active = await DownloadQueueService.get_active(db)

# Update status
updated = await DownloadQueueService.update_status(
    db,
    item_id,
    DownloadStatus.DOWNLOADING
)

# Update progress
updated = await DownloadQueueService.update_progress(
    db,
    item_id,
    progress_percent=50.0,
    downloaded_bytes=500000,
    total_bytes=1000000,
    download_speed=50000.0
)

# Clear completed
count = await DownloadQueueService.clear_completed(db)

# Retry failed downloads
retried = await DownloadQueueService.retry_failed(db, max_retries=3)
```

### UserSessionService

```python
from src.server.database import UserSessionService
from datetime import datetime, timedelta

# Create session
expires_at = datetime.utcnow() + timedelta(hours=24)
session = await UserSessionService.create(
    db,
    session_id="unique-session-id",
    token_hash="hashed-jwt-token",
    expires_at=expires_at,
    user_id="user123",
    ip_address="127.0.0.1"
)

# Get session
session = await UserSessionService.get_by_session_id(db, "session-id")

# Get active sessions
active = await UserSessionService.get_active_sessions(db, user_id="user123")

# Update activity
updated = await UserSessionService.update_activity(db, "session-id")

# Revoke session
revoked = await UserSessionService.revoke(db, "session-id")

# Cleanup expired sessions
count = await UserSessionService.cleanup_expired(db)
```

## Database Schema

```
anime_series (id, key, name, site, folder, ...)
├── episodes (id, series_id, season, episode_number, ...)
└── download_queue (id, series_id, season, episode_number, status, ...)

user_sessions (id, session_id, token_hash, expires_at, ...)
```

## Production Considerations

### SQLite (Current)

-   Single file: `data/aniworld.db`
-   WAL mode for concurrency
-   Foreign keys enabled
-   Static connection pool

### PostgreSQL/MySQL (Future)

For multi-process deployments:

```python
DATABASE_URL=postgresql+asyncpg://user:pass@host/db
# or
DATABASE_URL=mysql+aiomysql://user:pass@host/db
```

Configure connection pooling:

```python
engine = create_async_engine(
    url,
    pool_size=10,
    max_overflow=20,
    pool_pre_ping=True
)
```

## Performance Tips

1. **Indexes**: Models have indexes on frequently queried columns
2. **Relationships**: Use `selectinload()` or `joinedload()` for eager loading
3. **Batching**: Use bulk operations for multiple inserts/updates
4. **Query Optimization**: Profile slow queries in DEBUG mode

Example with eager loading:

```python
from sqlalchemy.orm import selectinload

result = await db.execute(
    select(AnimeSeries)
    .options(selectinload(AnimeSeries.episodes))
    .where(AnimeSeries.key == "attack-on-titan")
)
series = result.scalar_one()
# episodes already loaded, no additional queries
```

## Troubleshooting

### Database not initialized

```
RuntimeError: Database not initialized. Call init_db() first.
```

Solution: Call `await init_db()` during application startup.

### Table does not exist

```
sqlalchemy.exc.OperationalError: no such table: anime_series
```

Solution: `Base.metadata.create_all()` is called automatically by `init_db()`.

### Foreign key constraint failed

```
sqlalchemy.exc.IntegrityError: FOREIGN KEY constraint failed
```

Solution: Ensure referenced records exist before creating relationships.

## Further Reading

-   [SQLAlchemy 2.0 Documentation](https://docs.sqlalchemy.org/en/20/)
-   [FastAPI with Databases](https://fastapi.tiangolo.com/tutorial/sql-databases/)
