# Offline Database Design

## Entity Relationship Diagram

    erDiagram
        servers ||--o{ users : "has"
        servers ||--o{ libraries : "has"
        libraries ||--o{ items : "contains"
        items ||--o{ items : "parent_of"
        items ||--o{ user_data : "has"
        items ||--o{ downloads : "has"
        items ||--o{ media_streams : "has"
        items ||--o{ thumbnails : "has"
        users ||--o{ user_data : "owns"
        users ||--o{ downloads : "owns"
        users ||--o{ sync_queue : "owns"

        servers {
            int id PK
            string jellyfin_id UK
            string name
            string url
            string version
            datetime last_sync
        }

        users {
            int id PK
            string jellyfin_id
            int server_id FK
            string name
            boolean is_active
        }

        libraries {
            int id PK
            string jellyfin_id
            int server_id FK
            string name
            string collection_type
            string image_tag
        }

        items {
            int id PK
            string jellyfin_id
            int server_id FK
            int library_id FK
            int parent_id FK
            string type
            string name
            string sort_name
            string overview
            int production_year
            float community_rating
            string official_rating
            int runtime_ticks
            string primary_image_tag
            string backdrop_image_tag
            string album_id
            string album_name
            string album_artist
            json artists
            json genres
            int index_number
            int parent_index_number
            string premiere_date
            json metadata_json
            datetime created_at
            datetime updated_at
            datetime last_sync
        }

        user_data {
            int id PK
            int item_id FK
            int user_id FK
            int position_ticks
            int play_count
            boolean is_favorite
            boolean played
            datetime last_played
            datetime updated_at
            datetime synced_at
        }

        downloads {
            int id PK
            int item_id FK
            int user_id FK
            string file_path
            int file_size
            string status
            float progress
            int priority
            string error_message
            datetime created_at
            datetime completed_at
        }

        media_streams {
            int id PK
            int item_id FK
            int stream_index
            string type
            string codec
            string language
            string display_title
            boolean is_default
            boolean is_forced
            boolean is_external
        }

        sync_queue {
            int id PK
            int user_id FK
            string operation
            string entity_type
            string entity_id
            json payload
            datetime created_at
            int attempts
            datetime last_attempt
            string status
        }

        thumbnails {
            int id PK
            int item_id FK
            string image_type
            string image_tag
            string file_path
            int width
            int height
            datetime cached_at
        }

## Table Definitions

### servers

Stores connected Jellyfin server information.

    CREATE TABLE servers (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        jellyfin_id     TEXT NOT NULL UNIQUE,
        name            TEXT NOT NULL,
        url             TEXT NOT NULL,
        version         TEXT,
        last_sync       DATETIME,
        created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP
    );

### users

Stores user accounts per server. Access tokens are stored separately in secure storage (see 09-security.md).

    CREATE TABLE users (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        jellyfin_id     TEXT NOT NULL,
        server_id       INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
        name            TEXT NOT NULL,
        is_active       BOOLEAN DEFAULT 0,
        created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(jellyfin_id, server_id)
    );

### libraries

Stores library/collection metadata.

    CREATE TABLE libraries (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        jellyfin_id     TEXT NOT NULL,
        server_id       INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
        name            TEXT NOT NULL,
        collection_type TEXT,
        image_tag       TEXT,
        sort_order      INTEGER DEFAULT 0,
        last_sync       DATETIME,
        created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(jellyfin_id, server_id)
    );

    CREATE INDEX idx_libraries_server ON libraries(server_id);

### items

Main table for all media items (movies, episodes, albums, songs, etc.).

    CREATE TABLE items (
        id                   INTEGER PRIMARY KEY AUTOINCREMENT,
        jellyfin_id          TEXT NOT NULL,
        server_id            INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
        library_id           INTEGER REFERENCES libraries(id) ON DELETE SET NULL,
        parent_id            INTEGER REFERENCES items(id) ON DELETE CASCADE,

        -- Basic metadata
        type                 TEXT NOT NULL,
        name                 TEXT NOT NULL,
        sort_name            TEXT,
        overview             TEXT,

        -- Media info
        production_year      INTEGER,
        community_rating     REAL,
        official_rating      TEXT,
        runtime_ticks        INTEGER,

        -- Images
        primary_image_tag    TEXT,
        backdrop_image_tag   TEXT,

        -- Audio-specific
        album_id             TEXT,
        album_name           TEXT,
        album_artist         TEXT,
        artists              TEXT,                   -- JSON array

        -- Series/Season-specific
        index_number         INTEGER,
        parent_index_number  INTEGER,
        series_id            TEXT,
        series_name          TEXT,
        season_id            TEXT,

        -- Additional
        genres               TEXT,                   -- JSON array
        premiere_date        TEXT,
        metadata_json        TEXT,

        -- Sync tracking
        created_at           DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at           DATETIME DEFAULT CURRENT_TIMESTAMP,
        last_sync            DATETIME,

        UNIQUE(jellyfin_id, server_id)
    );

    -- Performance indexes
    CREATE INDEX idx_items_server ON items(server_id);
    CREATE INDEX idx_items_library ON items(library_id);
    CREATE INDEX idx_items_parent ON items(parent_id);
    CREATE INDEX idx_items_type ON items(type);
    CREATE INDEX idx_items_album ON items(album_id);
    CREATE INDEX idx_items_series ON items(series_id);
    CREATE INDEX idx_items_name ON items(name COLLATE NOCASE);

    -- Full-text search
    CREATE VIRTUAL TABLE items_fts USING fts5(
        name,
        overview,
        artists,
        album_name,
        album_artist,
        content='items',
        content_rowid='id'
    );

    -- Triggers to keep FTS in sync
    CREATE TRIGGER items_ai AFTER INSERT ON items BEGIN
        INSERT INTO items_fts(rowid, name, overview, artists, album_name, album_artist)
        VALUES (new.id, new.name, new.overview, new.artists, new.album_name, new.album_artist);
    END;

    CREATE TRIGGER items_ad AFTER DELETE ON items BEGIN
        INSERT INTO items_fts(items_fts, rowid, name, overview, artists, album_name, album_artist)
        VALUES ('delete', old.id, old.name, old.overview, old.artists, old.album_name, old.album_artist);
    END;

    CREATE TRIGGER items_au AFTER UPDATE ON items BEGIN
        INSERT INTO items_fts(items_fts, rowid, name, overview, artists, album_name, album_artist)
        VALUES ('delete', old.id, old.name, old.overview, old.artists, old.album_name, old.album_artist);
        INSERT INTO items_fts(rowid, name, overview, artists, album_name, album_artist)
        VALUES (new.id, new.name, new.overview, new.artists, new.album_name, new.album_artist);
    END;

### media_streams

Stores subtitle and audio track information for items.

    CREATE TABLE media_streams (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        item_id         INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
        stream_index    INTEGER NOT NULL,
        type            TEXT NOT NULL,
        codec           TEXT,
        language        TEXT,
        display_title   TEXT,
        is_default      BOOLEAN DEFAULT 0,
        is_forced       BOOLEAN DEFAULT 0,
        is_external     BOOLEAN DEFAULT 0,
        path            TEXT,
        UNIQUE(item_id, stream_index)
    );

    CREATE INDEX idx_media_streams_item ON media_streams(item_id);

### user_data

Stores per-user data for items (favorites, progress, play count).

    CREATE TABLE user_data (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        item_id         INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
        user_id         INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,

        -- Playback state
        position_ticks  INTEGER DEFAULT 0,
        play_count      INTEGER DEFAULT 0,
        played          BOOLEAN DEFAULT 0,
        last_played     DATETIME,

        -- User preferences
        is_favorite     BOOLEAN DEFAULT 0,
        user_rating     REAL,

        -- Sync tracking
        updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        synced_at       DATETIME,
        needs_sync      BOOLEAN DEFAULT 0,

        UNIQUE(item_id, user_id)
    );

    CREATE INDEX idx_user_data_item ON user_data(item_id);
    CREATE INDEX idx_user_data_user ON user_data(user_id);
    CREATE INDEX idx_user_data_needs_sync ON user_data(needs_sync) WHERE needs_sync = 1;
    CREATE INDEX idx_user_data_favorites ON user_data(user_id, is_favorite) WHERE is_favorite = 1;
    CREATE INDEX idx_user_data_in_progress ON user_data(user_id, position_ticks)
        WHERE position_ticks > 0 AND played = 0;

### downloads

Tracks downloaded media files.

    CREATE TABLE downloads (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        item_id         INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
        user_id         INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,

        file_path       TEXT,
        file_size       INTEGER,
        file_hash       TEXT,

        status          TEXT NOT NULL DEFAULT 'pending',
        progress        REAL DEFAULT 0,
        bytes_downloaded INTEGER DEFAULT 0,

        transcode_profile TEXT,

        priority        INTEGER DEFAULT 0,
        error_message   TEXT,
        retry_count     INTEGER DEFAULT 0,

        created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        started_at      DATETIME,
        completed_at    DATETIME,
        expires_at      DATETIME,

        UNIQUE(item_id, user_id)
    );

    CREATE INDEX idx_downloads_status ON downloads(status);
    CREATE INDEX idx_downloads_user ON downloads(user_id);
    CREATE INDEX idx_downloads_queue ON downloads(status, priority DESC, created_at ASC)
        WHERE status IN ('pending', 'downloading');

### sync_queue

Stores mutations to sync back to server when online.

    CREATE TABLE sync_queue (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id         INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,

        operation       TEXT NOT NULL,
        entity_type     TEXT NOT NULL,
        entity_id       TEXT NOT NULL,
        payload         TEXT,

        status          TEXT DEFAULT 'pending',
        attempts        INTEGER DEFAULT 0,
        max_attempts    INTEGER DEFAULT 5,
        last_attempt    DATETIME,
        error_message   TEXT,

        created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        completed_at    DATETIME
    );

    CREATE INDEX idx_sync_queue_status ON sync_queue(status, created_at ASC)
        WHERE status = 'pending';
    CREATE INDEX idx_sync_queue_user ON sync_queue(user_id);

### thumbnails

Caches downloaded artwork.

    CREATE TABLE thumbnails (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        item_id         INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
        image_type      TEXT NOT NULL,
        image_tag       TEXT,
        file_path       TEXT NOT NULL,
        width           INTEGER,
        height          INTEGER,
        file_size       INTEGER,
        cached_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
        last_accessed   DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(item_id, image_type, width)
    );

    CREATE INDEX idx_thumbnails_item ON thumbnails(item_id);
    CREATE INDEX idx_thumbnails_lru ON thumbnails(last_accessed ASC);

### playlists (for local/synced playlists)

    CREATE TABLE playlists (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        jellyfin_id     TEXT,
        user_id         INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        name            TEXT NOT NULL,
        description     TEXT,
        is_local_only   BOOLEAN DEFAULT 0,
        created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        synced_at       DATETIME,
        needs_sync      BOOLEAN DEFAULT 0
    );

    CREATE TABLE playlist_items (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        playlist_id     INTEGER NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
        item_id         INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
        sort_order      INTEGER NOT NULL,
        added_at        DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(playlist_id, item_id)
    );

    CREATE INDEX idx_playlist_items_playlist ON playlist_items(playlist_id, sort_order);

## Key Queries

### Get items for offline library browsing

    -- Get all albums in a music library
    SELECT * FROM items
    WHERE library_id = ? AND type = 'MusicAlbum'
    ORDER BY sort_name;

    -- Get tracks for an album
    SELECT * FROM items
    WHERE album_id = ? AND type = 'Audio'
    ORDER BY parent_index_number, index_number;

### Resume / Continue Watching

    SELECT i.*, ud.position_ticks, ud.last_played
    FROM items i
    JOIN user_data ud ON ud.item_id = i.id
    WHERE ud.user_id = ?
      AND ud.position_ticks > 0
      AND ud.played = 0
    ORDER BY ud.last_played DESC
    LIMIT 20;

### Offline search

    SELECT i.* FROM items i
    JOIN items_fts fts ON fts.rowid = i.id
    WHERE items_fts MATCH ?
    ORDER BY rank;

### Download queue management

    -- Get next item to download
    SELECT d.*, i.name, i.type
    FROM downloads d
    JOIN items i ON i.id = d.item_id
    WHERE d.status = 'pending'
    ORDER BY d.priority DESC, d.created_at ASC
    LIMIT 1;

    -- Get download progress for UI
    SELECT
        d.status,
        COUNT(*) as count,
        SUM(d.file_size) as total_size,
        SUM(d.bytes_downloaded) as downloaded
    FROM downloads d
    WHERE d.user_id = ?
    GROUP BY d.status;

### Sync queue processing

    -- Get pending sync operations (oldest first)
    SELECT * FROM sync_queue
    WHERE status = 'pending'
      AND attempts < max_attempts
    ORDER BY created_at ASC
    LIMIT 10;

    -- Mark operation complete
    UPDATE sync_queue
    SET status = 'completed', completed_at = CURRENT_TIMESTAMP
    WHERE id = ?;

## Data Flow

### Online Mode

    flowchart TB
        subgraph OnlineMode["Online Mode"]
            JellyfinServer["Jellyfin Server"]
            OnlineRepo["OnlineRepo"]
            SQLite["SQLite"]
            HybridRepo["HybridRepository"]
            UI["UI / Stores"]

            JellyfinServer -->|"API Response"| OnlineRepo
            OnlineRepo -->|"Cache"| SQLite
            SQLite -->|"Sync"| JellyfinServer
            OnlineRepo -->|"Response"| HybridRepo
            SQLite -->|"Fallback"| HybridRepo
            HybridRepo --> UI
        end

### Offline Mode

    flowchart TB
        subgraph OfflineMode["Offline Mode"]
            OfflineRepo["OfflineRepo"]
            SQLite2["SQLite"]
            SyncQueue["sync_queue<br/>(Queued for later)"]
            HybridRepo2["HybridRepository"]
            UI2["UI / Stores"]

            OfflineRepo <-->|"Query"| SQLite2
            SQLite2 -->|"Mutations"| SyncQueue
            OfflineRepo --> HybridRepo2
            HybridRepo2 --> UI2
        end

### Sync on Reconnect

    flowchart LR
        NetworkRestored["Network restored"]
        SyncService["SyncService"]
        SyncQueue2["sync_queue"]
        JellyfinAPI["Jellyfin API"]
        MarkSynced["Mark synced"]

        NetworkRestored --> SyncService
        SyncService -->|"Read"| SyncQueue2
        SyncQueue2 -->|"Send"| JellyfinAPI
        JellyfinAPI -->|"Success"| MarkSynced
        MarkSynced --> SyncService

## Storage Estimates

Content Type | Metadata Size | Thumbnail Size | Media Size
--- | --- | --- | ---
Song | ~2 KB | ~50 KB (300px) | 5-15 MB
Album (12 tracks) | ~30 KB | ~100 KB | 60-180 MB
Movie | ~5 KB | ~200 KB | 1-8 GB
Episode | ~3 KB | ~100 KB | 300 MB - 2 GB
Full music library (5000 songs) | ~10 MB | ~250 MB | 25-75 GB

## Rust Module Structure

    src-tauri/src/storage/
    ├── mod.rs              # Module exports, Database struct
    ├── schema.rs           # Table definitions, migrations
    ├── models.rs           # Rust structs matching tables
    ├── queries/
    │   ├── mod.rs
    │   ├── items.rs        # Item CRUD operations
    │   ├── user_data.rs    # User data operations
    │   ├── downloads.rs    # Download queue operations
    │   └── sync.rs         # Sync queue operations
    └── sync/
        ├── mod.rs          # SyncService
        ├── manager.rs      # Background sync manager
        └── operations.rs   # Individual sync operation handlers