# Download Manager & Offline Architecture

## Overview

**Location**: `src-tauri/src/download/`

The download manager provides offline media support with priority-based queue management, progress tracking, retry logic, and smart caching.

```mermaid
flowchart TB
    subgraph Frontend["Frontend"]
        DownloadButton["DownloadButton.svelte"]
        DownloadsPage["/downloads"]
        DownloadsStore["downloads.ts store"]
    end

    subgraph Backend["Rust Backend"]
        Commands["Download Commands"]
        DownloadManager["DownloadManager"]
        DownloadWorker["DownloadWorker"]
        SmartCache["SmartCache Engine"]
    end

    subgraph Storage["Storage"]
        SQLite[("SQLite DB")]
        MediaFiles[("Downloaded Files")]
    end

    DownloadButton -->|"invoke('download_item')"| Commands
    DownloadsPage -->|"invoke('get_downloads')"| Commands
    Commands --> DownloadManager
    DownloadManager --> DownloadWorker
    DownloadManager --> SmartCache
    DownloadWorker -->|"HTTP Stream"| MediaFiles
    DownloadWorker -->|"Events"| DownloadsStore
    Commands <--> SQLite
    SmartCache <--> SQLite
```

## Download Worker

**Location**: `src-tauri/src/download/worker.rs`

The download worker handles HTTP streaming with retry logic and resume support:

```rust
pub struct DownloadWorker {
    client: reqwest::Client,
    max_retries: u32,
}

pub struct DownloadTask {
    pub id: i64,
    pub item_id: String,
    pub user_id: String,
    pub priority: i32,
    pub url: String,
    pub target_path: PathBuf,
    pub mime_type: Option<String>,
    pub expected_size: Option<i64>,
}
```

**Retry Strategy**:
- Exponential backoff: 5s, 15s, 45s
- Maximum 3 retry attempts
- HTTP Range requests for resume support
- Progress events emitted every 1MB

**Download Flow**:

```mermaid
sequenceDiagram
    participant UI
    participant Command as download_item
    participant DB as SQLite
    participant Worker as DownloadWorker
    participant Jellyfin as Jellyfin Server
    participant Store as downloads store

    UI->>Command: download_item(itemId, userId)
    Command->>DB: INSERT INTO downloads
    Command->>Worker: Start download task
    Worker->>Jellyfin: GET /Items/{id}/Download

    loop Progress Updates
        Jellyfin->>Worker: Stream chunks
        Worker->>Worker: Write to .part file
        Worker->>Store: Emit progress event
        Store->>UI: Update progress bar
    end

    Worker->>Worker: Rename .part to final
    Worker->>DB: UPDATE status='completed'
    Worker->>Store: Emit completed event
    Store->>UI: Show completed
```

## Smart Caching Engine

**Location**: `src-tauri/src/download/cache.rs`

The smart caching system provides predictive downloads based on listening patterns:

```rust
pub struct SmartCache {
    config: Arc<Mutex<CacheConfig>>,
    album_play_history: Arc<Mutex<HashMap<String, Vec<String>>>>,
}

pub struct CacheConfig {
    pub queue_precache_enabled: bool,
    pub queue_precache_count: usize,        // Default: 5
    pub album_affinity_enabled: bool,
    pub album_affinity_threshold: usize,    // Default: 3
    pub storage_limit: u64,                 // Default: 10GB
    pub wifi_only: bool,                    // Default: true
}
```

**Caching Strategies**:

1. **Queue Pre-caching**: Auto-download next 5 tracks when playing (WiFi only)
2. **Album Affinity**: If user plays 3+ tracks from album, cache entire album
3. **LRU Eviction**: Remove least recently accessed when storage limit reached

```mermaid
flowchart TB
    Play["Track Played"] --> CheckQueue{"Queue<br/>Pre-cache?"}
    CheckQueue -->|"Yes"| CacheNext5["Download<br/>Next 5 Tracks"]

    Play --> TrackHistory["Track Play History"]
    TrackHistory --> CheckAlbum{"3+ Tracks<br/>from Album?"}
    CheckAlbum -->|"Yes"| CacheAlbum["Download<br/>Full Album"]

    CacheNext5 --> CheckStorage{"Storage<br/>Limit?"}
    CacheAlbum --> CheckStorage
    CheckStorage -->|"Exceeded"| EvictLRU["Evict LRU Items"]
    CheckStorage -->|"OK"| Download["Queue Download"]
```

## Download Commands

**Location**: `src-tauri/src/commands/download.rs`

| Command | Parameters | Description |
|---------|------------|-------------|
| `download_item` | `item_id, user_id, file_path` | Queue single item download |
| `download_album` | `album_id, user_id` | Queue all tracks in album |
| `get_downloads` | `user_id, status_filter` | Get download list |
| `pause_download` | `download_id` | Pause active download |
| `resume_download` | `download_id` | Resume paused download |
| `cancel_download` | `download_id` | Cancel and delete partial |
| `delete_download` | `download_id` | Delete completed download |

## Offline Commands

**Location**: `src-tauri/src/commands/offline.rs`

| Command | Parameters | Description |
|---------|------------|-------------|
| `offline_is_available` | `item_id` | Check if item downloaded |
| `offline_get_items` | `user_id` | Get all offline items |
| `offline_search` | `user_id, query` | Search downloaded items |

## Player Integration

**Location**: `src-tauri/src/commands/player.rs` (modified)

The player checks for local downloads before streaming:

```rust
fn create_media_item(req: PlayItemRequest, db: Option<&DatabaseWrapper>) -> MediaItem {
    let local_path = db.and_then(|db_wrapper| {
        check_for_local_download(db_wrapper, &jellyfin_id).ok().flatten()
    });

    let source = if let Some(path) = local_path {
        MediaSource::Local {
            file_path: PathBuf::from(path),
            jellyfin_item_id: Some(jellyfin_id.clone())
        }
    } else {
        MediaSource::Remote {
            stream_url: req.stream_url,
            jellyfin_item_id: jellyfin_id.clone()
        }
    };

    MediaItem { source, /* ... */ }
}
```

## Frontend Downloads Store

**Location**: `src/lib/stores/downloads.ts`

```typescript
interface DownloadsState {
  downloads: Record<number, DownloadInfo>;
  activeCount: number;
  queuedCount: number;
}

const downloads = createDownloadsStore();

// Actions
downloads.downloadItem(itemId, userId, filePath)
downloads.downloadAlbum(albumId, userId)
downloads.pause(downloadId)
downloads.resume(downloadId)
downloads.cancel(downloadId)
downloads.delete(downloadId)
downloads.refresh(userId, statusFilter)

// Derived stores
export const activeDownloads = derived(downloads, ($d) =>
  Object.values($d.downloads).filter((d) => d.status === 'downloading')
);
```

**Event Handling**:

The store listens to Tauri events for real-time updates:

```typescript
listen<DownloadEvent>('download-event', (event) => {
  const payload = event.payload;

  switch (payload.type) {
    case 'started':
      // Update status to 'downloading'
    case 'progress':
      // Update progress and bytes_downloaded
    case 'completed':
      // Update status to 'completed', progress to 1.0
    case 'failed':
      // Update status to 'failed', store error message
  }
});
```

## Download UI Components

**DownloadButton** (`src/lib/components/library/DownloadButton.svelte`):
- Multiple states: available, downloading, completed, failed, paused
- Circular progress ring during download
- Size variants: sm, md, lg
- Integrated into TrackList with `showDownload={true}` prop

**DownloadItem** (`src/lib/components/downloads/DownloadItem.svelte`):
- Individual download list item with progress bar
- Action buttons: pause, resume, cancel, delete
- Status indicators with color coding

**Downloads Page** (`src/routes/downloads/+page.svelte`):
- Active/Completed tabs
- Bulk actions: Pause All, Resume All, Clear Completed
- Empty states with helpful instructions

## Database Schema

**downloads table**:

```sql
CREATE TABLE downloads (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    item_id         TEXT NOT NULL,
    user_id         TEXT NOT NULL,
    file_path       TEXT,
    file_size       INTEGER,
    mime_type       TEXT,
    status          TEXT DEFAULT 'pending',  -- pending, downloading, completed, failed, paused
    progress        REAL DEFAULT 0.0,
    bytes_downloaded INTEGER DEFAULT 0,
    priority        INTEGER DEFAULT 0,
    error_message   TEXT,
    retry_count     INTEGER DEFAULT 0,
    queued_at       TEXT DEFAULT CURRENT_TIMESTAMP,
    started_at      TEXT,
    completed_at    TEXT
);

CREATE INDEX idx_downloads_queue
  ON downloads(status, priority DESC, queued_at ASC)
  WHERE status IN ('pending', 'downloading');
```
