# 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, pub expected_size: Option, } ``` **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>, album_play_history: Arc>>>, } 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
Pre-cache?"} CheckQueue -->|"Yes"| CacheNext5["Download
Next 5 Tracks"] Play --> TrackHistory["Track Play History"] TrackHistory --> CheckAlbum{"3+ Tracks
from Album?"} CheckAlbum -->|"Yes"| CacheAlbum["Download
Full Album"] CacheNext5 --> CheckStorage{"Storage
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; 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('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'); ```