From e560543181c6e0bafbd0b7d2fee471d2a94f90dd Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Mon, 26 Jan 2026 22:31:37 +0100 Subject: [PATCH] Update docs --- PRELOADING.md | 212 ----- README.md | 88 +- SoftwareArchitecture.md | 649 ++------------ ThumbnailCachingArchitecture.md | 1409 ------------------------------- UX-ImplementationGaps.md | 219 ----- 5 files changed, 135 insertions(+), 2442 deletions(-) delete mode 100644 PRELOADING.md delete mode 100644 ThumbnailCachingArchitecture.md delete mode 100644 UX-ImplementationGaps.md diff --git a/PRELOADING.md b/PRELOADING.md deleted file mode 100644 index c945ebb..0000000 --- a/PRELOADING.md +++ /dev/null @@ -1,212 +0,0 @@ -# Smart Preloading Implementation - -This document describes the smart preloading system that automatically queues downloads for upcoming tracks in the playback queue. - -## Overview - -The preloading system monitors playback and automatically queues background downloads for the next 3 tracks in the queue. This ensures smooth playback transitions and offline availability without requiring manual downloads. - -## Architecture - -### Backend (Rust) - -**Smart Cache Engine** - `/src-tauri/src/download/cache.rs` -- Manages preload configuration -- Default settings: preload 3 tracks, works on any connection (not wifi-only) -- Storage limit: 10GB -- Album affinity tracking (future feature) - -**Queue Manager** - `/src-tauri/src/player/queue.rs` -- New method: `get_upcoming(count)` returns next N tracks -- Respects shuffle order -- Handles repeat modes (Off, All, One) -- Wraps around when RepeatMode::All is enabled - -**Preload Command** - `/src-tauri/src/commands/player.rs` -- `player_preload_upcoming` - Main preload endpoint -- Checks SmartCache configuration -- Gets upcoming tracks from queue -- Skips already downloaded/queued items -- Queues new downloads with low priority (-100) -- Returns stats: queued_count, already_downloaded, skipped - -**Configuration Commands** -- `player_set_cache_config` - Update preload settings -- `player_get_cache_config` - Get current settings - -### Frontend (TypeScript/Svelte) - -**Preload Service** - `/src/lib/services/preload.ts` -- Main function: `preloadUpcomingTracks()` -- Automatically gets current user ID from auth store -- Calls backend preload command -- Fails silently - never interrupts playback -- Logs meaningful results only - -**Integration Points** - `/src/lib/services/playerEvents.ts` -1. **On playback start** - When state changes to "playing" -2. **On track advance** - After successfully advancing to next track - -## How It Works - -### Playback Flow - -``` -User plays track/queue - ↓ -Backend starts playback - ↓ -Emits "state_changed" event with "playing" - ↓ -playerEvents.handleStateChanged() catches event - ↓ -Calls preloadUpcomingTracks() - ↓ -Backend queues downloads for next 3 tracks (if not already downloaded) -``` - -### Track Advance Flow - -``` -Track ends (or user clicks next) - ↓ -playerEvents.handlePlaybackEnded() or manual next() - ↓ -Calls player_next backend command - ↓ -Backend plays next track, emits "state_changed" - ↓ -Calls preloadUpcomingTracks() again - ↓ -Queue is shifted forward, new track gets preloaded -``` - -## Configuration - -### Default Settings - -```typescript -{ - queuePrecacheEnabled: true, - queuePrecacheCount: 3, - albumAffinityEnabled: true, - albumAffinityThreshold: 3, - storageLimit: 10737418240, // 10GB - wifiOnly: false -} -``` - -### Updating Configuration - -```typescript -import { updateCacheConfig } from '$lib/services/preload'; - -await updateCacheConfig({ - queuePrecacheCount: 5, // Preload 5 tracks instead of 3 - wifiOnly: true // Only preload on WiFi -}); -``` - -## Features - -### Intelligent Queueing -- ✅ Checks if tracks are already downloaded -- ✅ Skips tracks already in download queue -- ✅ Low priority (-100) so user-initiated downloads go first -- ✅ Respects shuffle and repeat modes -- ✅ No duplicate downloads - -### Offline First -- ✅ Existing `create_media_item()` in player.rs checks local downloads first -- ✅ Preloading ensures next tracks become local over time -- ✅ Seamless offline playback without manual intervention - -### Non-Intrusive -- ✅ Background operation - never blocks playback -- ✅ Fails silently - errors are logged but don't affect UX -- ✅ Automatic - no user interaction required -- ✅ Configurable - users can adjust or disable - -## Files Changed - -### Backend -- `src-tauri/src/player/queue.rs` - Added `get_upcoming()` method -- `src-tauri/src/download/cache.rs` - Made `CacheConfig` serializable, updated defaults -- `src-tauri/src/commands/player.rs` - Added preload commands and `SmartCacheWrapper` -- `src-tauri/src/lib.rs` - Initialized SmartCache, registered commands - -### Frontend -- `src/lib/services/preload.ts` - New preload service (created) -- `src/lib/services/playerEvents.ts` - Integrated preload triggers - -### Configuration -- `src-tauri/Cargo.toml` - Added tempfile dev dependency for tests - -## Testing - -### Rust Tests -```bash -cd src-tauri -cargo test queue::tests::test_get_upcoming -cargo test cache::tests::test_default_config -``` - -All tests pass ✅ - -### Manual Testing - -1. **Start playback** - - Play a queue of 5+ tracks - - Check console for: `[Preload] Queued N track(s) for background download` - - Verify download queue shows 3 pending downloads with priority -100 - -2. **Track advance** - - Let track finish or click next - - Check console for new preload log - - Verify queue shifts (old track 2 becomes current, new track gets queued) - -3. **Repeat mode** - - Enable Repeat All - - Play to end of queue - - Verify wraps around and continues preloading - -4. **Already downloaded** - - Download all tracks in an album - - Play the album - - Check logs show: `already_downloaded: 3, queued: 0` - -## Future Enhancements - -1. **Album Affinity** - If user plays 3+ tracks from an album, auto-download the rest -2. **WiFi Detection** - Respect `wifi_only` setting on mobile -3. **Storage Management** - Auto-evict LRU items when storage limit reached -4. **Smart Priority** - Boost priority as track gets closer in queue -5. **Bandwidth Throttling** - Limit download speed to not interfere with streaming - -## Troubleshooting - -### Preload not working -- Check console for `[Preload]` logs -- Verify user is logged in: `auth.getUserId()` returns value -- Check SmartCache config: `queuePrecacheEnabled` should be true - -### Downloads not starting -- Preload only queues downloads, doesn't start them -- Check download manager is processing queue -- Verify backend has download worker running - -### Too many downloads -- Reduce `queuePrecacheCount` in config -- Enable `wifiOnly` mode -- Adjust `storageLimit` - -## Performance Impact - -- **Minimal** - Background downloads use low priority -- **Non-blocking** - Async operation, no playback delay -- **Bandwidth-friendly** - Only downloads when needed -- **Storage-aware** - Respects configured limits - -## Summary - -Smart preloading transforms JellyTau into an offline-first music player. By automatically queueing downloads for upcoming tracks, it ensures seamless playback and offline availability without requiring users to manually manage downloads. The system is intelligent (checks what's already downloaded), non-intrusive (fails silently), and configurable (users can adjust or disable). diff --git a/README.md b/README.md index 5ffe688..9b06b16 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ A cross-platform Jellyfin client built with Tauri, SvelteKit, and TypeScript. |----|-------------|----------|--------| | UR-001 | Run the app on multiple platforms (Linux, Android) | High | In Progress | | UR-002 | Access media when online or offline | High | Done | -| UR-003 | Play videos | High | Planned | -| UR-004 | Play audio uninterrupted | High | Planned | -| UR-005 | Control media playback (pause, play, skip, scrub) | High | In Progress | -| UR-006 | Control media when device is on lock screen or via BLE headsets | Medium | Planned | +| UR-003 | Play videos | High | Done | +| UR-004 | Play audio uninterrupted | High | Done | +| UR-005 | Control media playback (pause, play, skip, scrub) | High | Done | +| UR-006 | Control media when device is on lock screen or via BLE headsets | Medium | In Progress | | UR-007 | Navigate media in library | High | Done | | UR-008 | Search media across libraries | High | Done | | UR-009 | Connect to Jellyfin to access media | High | Done | -| UR-010 | Control playback of Jellyfin remote sessions | Low | Planned | +| UR-010 | Control playback of Jellyfin remote sessions | Low | Done | | UR-011 | Download media on demand | Medium | Done | | UR-012 | Login info shall be stored securely and persistently | High | Done | | UR-013 | View and manage downloaded media | Medium | Done | @@ -36,23 +36,23 @@ A cross-platform Jellyfin client built with Tauri, SvelteKit, and TypeScript. | UR-020 | Select subtitles for video content | High | Planned | | UR-021 | Select audio track for video content | High | Planned | | UR-022 | Control streaming quality and transcoding settings | Medium | Planned | -| UR-023 | View "Next Up" / Continue Watching on home screen; auto-play next episode with countdown popup | Medium | In Progress | -| UR-024 | View recently added content on server | Medium | Planned | +| UR-023 | View "Next Up" / Continue Watching on home screen; auto-play next episode with countdown popup | Medium | Done | +| UR-024 | View recently added content on server | Medium | Done | | UR-025 | Sync watch history and progress back to Jellyfin | High | Done | -| UR-026 | Sleep timer for audio playback | Low | Planned | +| UR-026 | Sleep timer for audio playback | Low | Done | | UR-027 | Audio equalizer for sound customization | Low | Planned | | UR-028 | Navigate to artist/album by tapping names in now playing view | High | Done | | UR-029 | Toggle between grid and list view in library | Medium | Done | | UR-030 | Quick genre browsing and filtering | Medium | Done | -| UR-031 | Crossfade between audio tracks | Low | Planned (Linux only) | +| UR-031 | Crossfade between audio tracks | Low | Done (Linux only) | | UR-032 | Gapless playback for seamless album listening | Medium | Done (Linux only) | | UR-033 | Volume normalization to prevent volume jumps between tracks | Low | Done (Linux only) | -| UR-034 | Rich home screen with hero banners, carousels, and personalized sections | High | Planned | -| UR-035 | View cast/crew (actors, directors) on movie/show detail pages | High | Planned | -| UR-036 | Navigate to actor/person page showing their filmography | Medium | Planned | -| UR-037 | Visually appealing video library with poster grids and metadata | High | Planned | -| UR-038 | Movie/show detail page with backdrop, ratings, and rich metadata | High | Planned | -| UR-039 | Navigate between main sections via bottom navigation bar | High | In Progress | +| UR-034 | Rich home screen with hero banners, carousels, and personalized sections | High | Done | +| UR-035 | View cast/crew (actors, directors) on movie/show detail pages | High | Done | +| UR-036 | Navigate to actor/person page showing their filmography | Medium | Done | +| UR-037 | Visually appealing video library with poster grids and metadata | High | Done | +| UR-038 | Movie/show detail page with backdrop, ratings, and rich metadata | High | Done | +| UR-039 | Navigate between main sections via bottom navigation bar | High | Done | --- @@ -66,16 +66,16 @@ External system integrations and platform-specific implementations. |----|-------------|----------|-----------|--------| | IR-001 | Build system supporting multiple targets (Linux, Android) | Build | UR-001 | Done | | IR-002 | Build scripts for Android and Linux | Build | UR-001 | Done | -| IR-003 | Integration of libmpv for Linux playback | Playback | UR-003, UR-004 | In Progress | +| IR-003 | Integration of libmpv for Linux playback | Playback | UR-003, UR-004 | Done | | IR-004 | Integration of ExoPlayer for Android playback | Playback | UR-003, UR-004 | In Progress (basic playback works, audio settings missing) | | IR-005 | MPRIS D-Bus integration for Linux lockscreen/media controls | Platform | UR-006 | Planned | -| IR-006 | Android MediaSession integration for lockscreen controls | Platform | UR-006 | In Progress | +| IR-006 | Android MediaSession integration for lockscreen controls | Platform | UR-006 | Done | | IR-007 | Bluetooth AVRCP integration via system media session | Platform | UR-006 | Planned | -| IR-008 | Android audio focus handling (pause on call) | Platform | UR-004, UR-006 | In Progress | +| IR-008 | Android audio focus handling (pause on call) | Platform | UR-004, UR-006 | Done | | IR-009 | Jellyfin API client for authentication | API | UR-009, UR-012 | Done | | IR-010 | Jellyfin API client for library browsing | API | UR-007, UR-008 | Done | | IR-011 | Jellyfin API client for playback streaming | API | UR-003, UR-004 | Done | -| IR-012 | Jellyfin Sessions API for remote playback control | API | UR-010 | Planned | +| IR-012 | Jellyfin Sessions API for remote playback control | API | UR-010 | Done | | IR-021 | Android MediaRouter integration for remote volume in system panel | Platform | UR-010, UR-016 | Planned | | IR-013 | SQLite integration for local database | Storage | UR-002, UR-011 | Done | | IR-014 | Secure credential storage (keyring/keychain) | Security | UR-012 | Done | @@ -85,9 +85,9 @@ External system integrations and platform-specific implementations. | IR-018 | libmpv subtitle rendering and selection | Playback | UR-020 | Planned | | IR-019 | libmpv audio track selection | Playback | UR-021 | Planned | | IR-020 | libmpv/ExoPlayer equalizer integration | Playback | UR-027 | Planned | -| IR-022 | Jellyfin API client for person/cast data | API | UR-035, UR-036 | Planned | -| IR-023 | Database schema for person/cast caching | Storage | UR-035, UR-036 | Planned | -| IR-024 | Jellyfin API client for home screen data (featured, continue watching) | API | UR-034 | Planned | +| IR-022 | Jellyfin API client for person/cast data | API | UR-035, UR-036 | Done | +| IR-023 | Database schema for person/cast caching | Storage | UR-035, UR-036 | Done | +| IR-024 | Jellyfin API client for home screen data (featured, continue watching) | API | UR-034 | Done | ### 2.2 Jellyfin API Requirements @@ -108,9 +108,9 @@ API endpoints and data contracts required for Jellyfin integration. | JA-011 | Report playback progress (periodic) | Sessions | UR-025 | Done | | JA-012 | Report playback stopped | Sessions | UR-025 | Done | | JA-013 | Get resume position for item | UserData | UR-019 | Done | -| JA-014 | Get "Next Up" items | Shows | UR-023 | Planned | -| JA-015 | Get "Continue Watching" items | Items | UR-023 | Planned | -| JA-016 | Get recently added items | Items | UR-024 | Planned | +| JA-014 | Get "Next Up" items | Shows | UR-023 | Done | +| JA-015 | Get "Continue Watching" items | Items | UR-023 | Done | +| JA-016 | Get recently added items | Items | UR-024 | Done | | JA-017 | Mark item as favorite | UserData | UR-017 | Done | | JA-018 | Remove item from favorites | UserData | UR-017 | Done | | JA-019 | Get/create/update playlists | Playlists | UR-014 | Planned | @@ -142,7 +142,7 @@ Internal architecture, components, and application logic. | DR-007 | Library browsing screens (grid view, search, filters) | UI | UR-007, UR-008 | Done | | DR-008 | Album/Series detail view with track listing | UI | UR-007 | Done | | DR-009 | Audio player UI (mini player, full screen) | UI | UR-005 | Done | -| DR-010 | Video player UI (fullscreen, controls overlay) | UI | UR-003, UR-005 | Planned | +| DR-010 | Video player UI (fullscreen, controls overlay) | UI | UR-003, UR-005 | Done | | DR-011 | Search bar with cross-library search | UI | UR-008 | Done | | DR-012 | Local database for media metadata cache | Storage | UR-002 | Done | | DR-013 | Repository pattern for online/offline data access | Storage | UR-002 | Done | @@ -158,29 +158,29 @@ Internal architecture, components, and application logic. | DR-023 | Subtitle selection UI in video player | UI | UR-020 | Planned | | DR-024 | Audio track selection UI in video player | UI | UR-021 | Planned | | DR-025 | Quality/transcoding settings UI | UI | UR-022 | Planned | -| DR-026 | "Continue Watching" / "Next Up" home section | UI | UR-023 | Planned | -| DR-027 | "Recently Added" home section | UI | UR-024 | Planned | +| DR-026 | "Continue Watching" / "Next Up" home section | UI | UR-023 | Done | +| DR-027 | "Recently Added" home section | UI | UR-024 | Done | | DR-028 | Playback progress sync service (periodic reporting) | Player | UR-025 | Done | -| DR-029 | Sleep timer with countdown and auto-stop | Player | UR-026 | Planned | +| DR-029 | Sleep timer with countdown and auto-stop | Player | UR-026 | Done | | DR-030 | Equalizer UI with presets and custom bands | UI | UR-027 | Planned | | DR-031 | Clickable artist/album links in now playing view | UI | UR-028 | Done | | DR-032 | List view option for library browsing (albums, artists) | UI | UR-029 | Done | | DR-033 | Genre browsing screen with quick filters | UI | UR-030 | Done | -| DR-034 | Crossfade engine with configurable duration (0-12s) | Player | UR-031 | Planned | -| DR-035 | Gapless playback between sequential tracks | Player | UR-032 | Done | -| DR-036 | Volume normalization with preset levels (Loud/Normal/Quiet) | Player | UR-033 | Done | -| DR-037 | Remote session browser and control UI | UI | UR-010 | Planned | -| DR-038 | Home screen with hero banner carousel (featured/continue watching) | UI | UR-034 | Planned | -| DR-039 | Home screen horizontal carousels (recently added, recommendations) | UI | UR-034, UR-024 | Planned | -| DR-040 | Cast/crew section on movie/show detail pages | UI | UR-035 | Planned | -| DR-041 | Person/actor detail page with filmography grid | UI | UR-036 | Planned | -| DR-042 | Video library grid with poster cards, year, and rating badges | UI | UR-037 | Planned | -| DR-043 | Movie/show detail page with backdrop hero, synopsis, and metadata | UI | UR-038 | Planned | -| DR-044 | Horizontal scrolling actor/cast row with profile images | UI | UR-035 | Planned | -| DR-045 | Bottom navigation bar with Home, Library, Search buttons | UI | UR-039 | In Progress | -| DR-046 | Dedicated search page with input and results | UI | UR-039 | In Progress | -| DR-047 | Next episode auto-play popup with configurable countdown | Player | UR-023 | In Progress | -| DR-048 | Video settings (auto-play toggle, countdown duration) | Settings | UR-023, UR-026 | In Progress | +| DR-034 | Crossfade engine with configurable duration (0-12s) | Player | UR-031 | Done (Linux only) | +| DR-035 | Gapless playback between sequential tracks | Player | UR-032 | Done (Linux only) | +| DR-036 | Volume normalization with preset levels (Loud/Normal/Quiet) | Player | UR-033 | Done (Linux only) | +| DR-037 | Remote session browser and control UI | UI | UR-010 | Done | +| DR-038 | Home screen with hero banner carousel (featured/continue watching) | UI | UR-034 | Done | +| DR-039 | Home screen horizontal carousels (recently added, recommendations) | UI | UR-034, UR-024 | Done | +| DR-040 | Cast/crew section on movie/show detail pages | UI | UR-035 | Done | +| DR-041 | Person/actor detail page with filmography grid | UI | UR-036 | Done | +| DR-042 | Video library grid with poster cards, year, and rating badges | UI | UR-037 | Done | +| DR-043 | Movie/show detail page with backdrop hero, synopsis, and metadata | UI | UR-038 | Done | +| DR-044 | Horizontal scrolling actor/cast row with profile images | UI | UR-035 | Done | +| DR-045 | Bottom navigation bar with Home, Library, Search buttons | UI | UR-039 | Done | +| DR-046 | Dedicated search page with input and results | UI | UR-039 | Done | +| DR-047 | Next episode auto-play popup with configurable countdown | Player | UR-023 | Done | +| DR-048 | Video settings (auto-play toggle, countdown duration) | Settings | UR-023, UR-026 | Done | --- diff --git a/SoftwareArchitecture.md b/SoftwareArchitecture.md index d8e8d49..0b7d9cd 100644 --- a/SoftwareArchitecture.md +++ b/SoftwareArchitecture.md @@ -2,42 +2,11 @@ This document describes the current architecture of JellyTau, a cross-platform Jellyfin client built with Tauri, SvelteKit, and Rust. -**Last Updated:** 2026-01-18 -**Architecture Status:** Phase 2-3 of TypeScript to Rust migration complete +**Last Updated:** 2026-01-26 -## Major Architectural Changes (Recent) +## Architecture Overview -JellyTau has undergone a significant architectural transformation, migrating ~3,500 lines of business logic from TypeScript to Rust: - -### ✅ Completed Migrations - -1. **HTTP Client & Connectivity** (Phase 1) - - Exponential backoff retry logic moved to Rust - - Adaptive connectivity monitoring (30s online, 5s offline) - - Event-driven architecture for network state changes - -2. **Repository Pattern** (Phase 2) - - Complete MediaRepository trait implementation in Rust - - Cache-first parallel racing (100ms cache timeout) - - Handle-based resource management (UUID handles) - - 27 new Tauri commands for data access - - Eliminated 1,061 lines of TypeScript - -3. **Database Service Abstraction** (Phase 2.5) - - Async wrapper over synchronous rusqlite - - All DB operations use `spawn_blocking` to prevent UI freezing - - ~1-2ms overhead per query (acceptable tradeoff) - -4. **Playback Mode Management** (Phase 3) - - Local/Remote/Idle mode tracking - - Seamless queue transfer to remote Jellyfin sessions - - Position synchronization during transfers - -### 🔄 In Progress - -- **Authentication & Session Management** (Phase 4) - - Session restoration and credential management - - Re-authentication flow +JellyTau uses a modern client-server architecture with a thin Svelte UI layer and comprehensive Rust backend: ### Architecture Principles @@ -521,7 +490,6 @@ flowchart TB - Attempts to sync with Jellyfin server - Marks as synced if successful, otherwise leaves `pending_sync = 1` 3. UI reflects the change immediately without waiting for server response -4. Future: Sync queue will retry failed syncs when online **Components**: @@ -653,13 +621,21 @@ flowchart TB JellyTau's music library uses a category-based navigation system with a dedicated landing page that routes users to specialized views for different content types. **Route Structure:** -``` -/library/music # Landing page with category cards -├── /tracks # All songs (ALWAYS list view) -├── /artists # Artist grid (ALWAYS grid view) -├── /albums # Album grid (ALWAYS grid view) -├── /playlists # Playlist grid (ALWAYS grid view) -└── /genres # Genre browser (two-level) + +```mermaid +graph TD + Music["/library/music
(Landing page with category cards)"] + Tracks["Tracks
(List view only)"] + Artists["Artists
(Grid view)"] + Albums["Albums
(Grid view)"] + Playlists["Playlists
(Grid view)"] + Genres["Genres
(Genre browser)"] + + Music --> Tracks + Music --> Artists + Music --> Albums + Music --> Playlists + Music --> Genres ``` **View Enforcement:** @@ -740,8 +716,7 @@ sequenceDiagram **Database Schema Notes:** - The `user_data` table stores playback progress using Jellyfin IDs directly (as TEXT) -- Foreign key constraint on `item_id` was removed in migration 003 to allow tracking progress for items not yet synced to local database -- This enables playback tracking even when the full item metadata hasn't been downloaded yet +- Playback progress can be tracked even when the full item metadata hasn't been downloaded yet **Resume Playback Feature:** - When loading media for playback, the app checks local database for saved progress @@ -1004,50 +979,66 @@ pub struct ConnectivityMonitor { ### 3.8 Component Hierarchy -``` -src/routes/ -├── +page.svelte # Login page -├── library/ -│ ├── +layout.svelte # Main layout with MiniPlayer -│ ├── +page.svelte # Library browser (library selector) -│ ├── [id]/+page.svelte # Album/series detail -│ └── music/ # Music library structure -│ ├── +page.svelte # Music category landing page -│ ├── tracks/+page.svelte # All tracks (list view only) -│ ├── artists/+page.svelte # Artists grid -│ ├── albums/+page.svelte # Albums grid -│ ├── playlists/+page.svelte # Playlists grid -│ └── genres/+page.svelte # Genre browser -├── downloads/ -│ └── +page.svelte # Manage downloads (Active/Completed tabs) -├── settings/ -│ └── +page.svelte # Settings (includes download settings) -└── player/ - └── [id]/+page.svelte # Full player page +```mermaid +graph TD + subgraph Routes["Routes (src/routes/)"] + LoginPage["Login Page"] + LibLayout["Library Layout"] + LibDetail["Album/Series Detail"] + MusicCategory["Music Category Landing"] + Tracks["Tracks"] + Artists["Artists"] + Albums["Albums"] + Playlists["Playlists"] + Genres["Genres"] + Downloads["Downloads Page"] + Settings["Settings Page"] + PlayerPage["Player Page"] + end -src/lib/components/ -├── Search.svelte -├── player/ -│ ├── AudioPlayer.svelte # Full screen audio player -│ ├── MiniPlayer.svelte # Bottom bar mini player (auto-hides for video, includes cast button) -│ ├── Controls.svelte # Play/pause/skip controls -│ └── Queue.svelte # Queue list view -├── sessions/ -│ ├── CastButton.svelte # Cast button with session picker (integrated in MiniPlayer) -│ ├── SessionPickerModal.svelte # Modal for selecting remote session -│ ├── SessionCard.svelte # Individual session display card -│ ├── SessionsList.svelte # List of all controllable sessions -│ └── RemoteControls.svelte # Full remote playback control UI -├── downloads/ -│ └── DownloadItem.svelte # Download list item with progress/actions -├── FavoriteButton.svelte # Reusable heart/like button -└── library/ - ├── LibraryGrid.svelte # Grid of media items (supports forceGrid) - ├── LibraryListView.svelte # List view for albums/artists - ├── TrackList.svelte # Dedicated track list (now with showDownload prop) - ├── DownloadButton.svelte # Download button with progress ring - ├── MediaCard.svelte # Individual item card - └── AlbumView.svelte # Album detail with tracks + subgraph PlayerComps["Player Components"] + AudioPlayer["AudioPlayer"] + MiniPlayer["MiniPlayer"] + Controls["Controls"] + Queue["Queue"] + end + + subgraph SessionComps["Sessions Components"] + CastButton["CastButton"] + SessionModal["SessionPickerModal"] + SessionCard["SessionCard"] + SessionsList["SessionsList"] + RemoteControls["RemoteControls"] + end + + subgraph LibraryComps["Library Components"] + LibGrid["LibraryGrid"] + LibListView["LibraryListView"] + TrackList["TrackList"] + DownloadBtn["DownloadButton"] + MediaCard["MediaCard"] + end + + subgraph OtherComps["Other Components"] + Search["Search"] + FavoriteBtn["FavoriteButton"] + DownloadItem["DownloadItem"] + end + + LibLayout --> PlayerComps + LibLayout --> LibDetail + MusicCategory --> Tracks + MusicCategory --> Artists + MusicCategory --> Albums + MusicCategory --> Playlists + MusicCategory --> Genres + LibDetail --> LibraryComps + Downloads --> DownloadItem + PlayerPage --> PlayerComps + + MiniPlayer --> CastButton + CastButton --> SessionModal + PlayerComps --> LibraryComps ``` --- @@ -1080,7 +1071,6 @@ sequenceDiagram else Cache timeout or empty Server-->>Hybrid: Fresh result Hybrid-->>Rust: Return server result - Note over Hybrid: TODO: Update cache in background end Rust-->>Client: SearchResult @@ -1967,7 +1957,7 @@ listen('download-event', (event) => { ### 8.9 Database Schema -**downloads table** (enhanced in migration 004): +**downloads table**: ```sql CREATE TABLE downloads ( @@ -1995,139 +1985,9 @@ CREATE INDEX idx_downloads_queue --- -## 9. TypeScript to Rust Migration Status +## 9. Connectivity & Network Architecture -### 9.1 Migration Overview - -JellyTau has undergone a phased migration from TypeScript to Rust, moving ~3,500 lines of business logic to Rust while simplifying the TypeScript layer to thin UI wrappers. - -**Approach**: Incremental migration with direct replacement -- Complete one phase at a time with full testing -- Delete TypeScript implementations after Rust validation -- Each phase is independently deployable -- No parallel implementations maintained - -### 9.2 Completed Phases - -#### ✅ Phase 1: HTTP Client & Connectivity Foundation (Complete) - -**Created:** -- `src-tauri/src/jellyfin/http_client.rs` (289 lines) - - HTTP client with exponential backoff retry (1s, 2s, 4s) - - Error classification (Network, Authentication, Server, Client) - - Automatic retry on network/5xx errors -- `src-tauri/src/connectivity/mod.rs` (351 lines) - - Background monitoring with adaptive polling (30s online, 5s offline) - - Event emission to frontend - - Manual reachability marking - -**Simplified:** -- `src/lib/stores/connectivity.ts`: 301→249 lines (-17%) - - Removed polling logic - - Now listens to Rust events - - Thin wrapper over Rust commands - -**Commands Added:** 7 connectivity commands - -**Impact:** Eliminated TypeScript polling/retry logic, improved battery efficiency - -#### ✅ Phase 2: Repository Pattern & Data Layer (Complete) - -**Created:** -- `src-tauri/src/repository/` (complete module) - - `mod.rs`: MediaRepository trait + handle-based management - - `types.rs`: Type definitions (RepoError, Library, MediaItem, etc.) - - `hybrid.rs`: Cache-first parallel racing (100ms cache timeout) - - `online.rs`: OnlineRepository (HTTP API calls) - - `offline.rs`: OfflineRepository (SQLite queries with caching) - -**Replaced:** -- Deleted `src/lib/api/repository.ts` (1061 lines) -- Created `src/lib/api/repository-client.ts` (~100 lines) - - Thin wrapper with handle-based resource management - - All methods delegate to Rust commands - -**Commands Added:** 27 repository commands - -**Key Features:** -- Handle-based resource management (UUID handles) -- Cache-first racing: parallel cache (100ms timeout) vs server queries -- Returns cache if meaningful content, else server result -- Supports multiple concurrent repository instances - -#### ✅ Phase 2.5: Database Service Abstraction (Complete) - -**Created:** -- `src-tauri/src/storage/db_service.rs` - - `DatabaseService` trait with async methods - - `RusqliteService` implementation using `spawn_blocking` - - Prevents blocking Tokio async runtime - -**Impact:** -- Eliminated UI freezing from blocking database operations -- All DB queries now use `spawn_blocking` thread pool -- ~1-2ms overhead per query (acceptable tradeoff) - -**Migration Status:** -- ✅ Phase 1: Foundation (Complete) -- ✅ Phase 2: OfflineRepository (18 methods migrated) -- 🔄 Phase 3: Command layer (97 operations across 5 files - in progress) - -#### ✅ Phase 3: Playback Mode System (Complete) - -**Created:** -- `src-tauri/src/playback_mode/mod.rs` - - `PlaybackMode` enum (Local, Remote, Idle) - - `PlaybackModeManager` for mode transfers - - Transfer queue between local device and remote sessions - -**Simplified:** -- `src/lib/stores/playbackMode.ts`: 303→150 lines (-50%) - - Thin wrapper calling Rust commands - - Maintains UI state only - -**Commands Added:** 5 playback mode commands - -**Features:** -- Seamless transfer of playback queue to remote sessions -- Position synchronization during transfer -- Automatic local player stop when transferring to remote - -### 9.3 In Progress - -#### 🔄 Phase 4: Authentication & Session Management - -**Status:** Partially complete -- Session restoration logic migrated -- Credential storage using secure keyring -- Re-authentication flow in progress - -**Target:** Simplify `src/lib/stores/auth.ts` from 616→150 lines - -### 9.4 Architecture Summary - -**Before Migration:** -- TypeScript: ~3,300 lines of business logic -- Rust: ~4,000 lines (player, storage, downloads) -- Total Commands: 73 - -**After Migration (Current):** -- TypeScript: ~800 lines (thin wrappers, UI state) -- Rust: ~8,000 lines (business logic, HTTP, repository, etc.) -- Total Commands: 80+ - -**Lines Eliminated:** ~2,500 lines of TypeScript business logic - -**Benefits:** -- Improved performance (zero-cost abstractions) -- Better reliability (type safety, memory safety) -- Reduced battery drain (efficient async I/O) -- Easier maintenance (centralized business logic) -- No UI freezing (async database operations) - -## 10. Connectivity & Network Architecture - -### 10.1 HTTP Client with Retry Logic +### 9.1 HTTP Client with Retry Logic **Location**: `src-tauri/src/jellyfin/http_client.rs` @@ -2161,7 +2021,7 @@ pub enum ErrorKind { } ``` -### 10.2 Connectivity Monitor +### 9.2 Connectivity Monitor **Location**: `src-tauri/src/connectivity/mod.rs` @@ -2203,7 +2063,7 @@ listen<{ isReachable: boolean }>("connectivity:changed", (event) => { }); ``` -### 10.3 Network Resilience Architecture +### 9.3 Network Resilience Architecture The connectivity system provides resilience through multiple layers: @@ -2218,336 +2078,9 @@ The connectivity system provides resilience through multiple layers: - **Adaptive Polling**: Reduce polling frequency when online, increase when offline - **Event-Driven**: Frontend reacts to connectivity changes via events -## 11. Architecture Extensions - -### 11.1 Native Async Database (Future) - -**Future Enhancement**: Migrate from `rusqlite` + `spawn_blocking` to native async database: - -```rust -// Current: spawn_blocking overhead (~1-2ms per query) -tokio::task::spawn_blocking(move || { - let conn = connection.lock().unwrap(); - conn.query_row(...) -}).await? - -// Future: Native async with tokio-rusqlite (zero overhead) -use tokio_rusqlite::Connection; - -let conn = Connection::open(path).await?; -conn.call(|conn| { - conn.query_row(...) -}).await? -``` - -**Benefits:** -- Eliminate spawn_blocking overhead -- Better integration with Tokio runtime -- Improved throughput for high-frequency queries - -**Migration Path:** -- DatabaseService trait already abstracts implementation -- Swap RusqliteService for TokioRusqliteService -- No changes to command layer needed - -### 11.2 Remote Session Control (Existing Feature - Documented) - -Remote session control allows JellyTau to discover and control playback on other Jellyfin clients (TVs, web browsers, etc.). This enables cast-like functionality where mobile devices become remote controls. - -#### 11.2.1 Architecture Overview - -```mermaid -flowchart TB - subgraph JellyTauApp["JellyTau App"] - SessionsStore["SessionsStore
- sessions[]
- activeId"] - RemoteControl["RemoteControl
- play/pause
- seek
- volume
- playItem"] - SessionsStore -->|"Select"| RemoteControl - end - - subgraph JellyfinServer["Jellyfin Server"] - GetSessions["GET /Sessions"] - ActiveSessions["Active Sessions
- Jellyfin Web (Chrome)
- Jellyfin Android TV
- Jellyfin for Roku"] - GetSessions --> ActiveSessions - end - - SessionsStore <-->|"Poll (5-10s)"| GetSessions - RemoteControl -->|"Commands"| ActiveSessions -``` - -#### 11.2.2 Jellyfin Sessions API - -**Endpoints:** - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/Sessions` | List all active sessions | -| POST | `/Sessions/{id}/Playing` | Start playback of item(s) | -| POST | `/Sessions/{id}/Playing/PlayPause` | Toggle play/pause | -| POST | `/Sessions/{id}/Playing/Stop` | Stop playback | -| POST | `/Sessions/{id}/Playing/Seek` | Seek to position | -| POST | `/Sessions/{id}/Command/SetVolume` | Set volume (0-100) | -| POST | `/Sessions/{id}/Command/Mute` | Mute | -| POST | `/Sessions/{id}/Command/Unmute` | Unmute | -| POST | `/Sessions/{id}/Command/VolumeUp` | Volume up | -| POST | `/Sessions/{id}/Command/VolumeDown` | Volume down | - -**Session Response Schema:** -```typescript -interface Session { - id: string; - userId: string; - userName: string; - client: string; // "Jellyfin Web", "Jellyfin Android TV", etc. - deviceName: string; // "Living Room TV", "Chrome - Windows" - deviceId: string; - applicationVersion: string; - isActive: boolean; - supportsMediaControl: boolean; - supportsRemoteControl: boolean; - playState: { - positionTicks: number; - canSeek: boolean; - isPaused: boolean; - isMuted: boolean; - volumeLevel: number; // 0-100 - repeatMode: string; - } | null; - nowPlayingItem: MediaItem | null; - playableMediaTypes: string[]; // ["Audio", "Video"] - supportedCommands: string[]; // ["PlayPause", "Seek", "SetVolume", ...] -} -``` - -#### 11.2.3 API Layer - -**Location**: `src/lib/api/sessions.ts` - -```typescript -export class SessionsApi { - constructor(private client: JellyfinClient) {} - - async getSessions(): Promise { - return this.client.get('/Sessions', { - params: { controllableByUserId: this.client.userId } - }); - } - - async playOnSession(sessionId: string, itemIds: string[], startIndex = 0): Promise { - await this.client.post(`/Sessions/${sessionId}/Playing`, { - itemIds, - startIndex, - playCommand: 'PlayNow' - }); - } - - async sendCommand(sessionId: string, command: SessionCommand): Promise { - await this.client.post(`/Sessions/${sessionId}/Playing/${command}`); - } - - async setVolume(sessionId: string, volume: number): Promise { - await this.client.post(`/Sessions/${sessionId}/Command/SetVolume`, { - Arguments: { Volume: Math.round(volume) } - }); - } - - async seek(sessionId: string, positionTicks: number): Promise { - await this.client.post(`/Sessions/${sessionId}/Playing/Seek`, { - seekPositionTicks: positionTicks - }); - } -} - -type SessionCommand = 'PlayPause' | 'Stop' | 'Pause' | 'Unpause' | - 'NextTrack' | 'PreviousTrack' | 'Mute' | 'Unmute'; -``` - -#### 11.2.4 Sessions Store - -**Location**: `src/lib/stores/sessions.ts` - -```typescript -interface SessionsState { - sessions: Session[]; - activeSessionId: string | null; - isPolling: boolean; - lastUpdated: Date | null; -} - -function createSessionsStore() { - const { subscribe, update } = writable({ - sessions: [], - activeSessionId: null, - isPolling: false, - lastUpdated: null - }); - - let pollInterval: ReturnType | null = null; - - return { - subscribe, - - startPolling(api: SessionsApi, intervalMs = 5000) { - this.stopPolling(); - pollInterval = setInterval(() => this.refresh(api), intervalMs); - this.refresh(api); // Immediate first fetch - }, - - stopPolling() { - if (pollInterval) clearInterval(pollInterval); - }, - - async refresh(api: SessionsApi) { - const sessions = await api.getSessions(); - update(s => ({ - ...s, - sessions: sessions.filter(s => s.supportsRemoteControl), - lastUpdated: new Date() - })); - }, - - selectSession(sessionId: string | null) { - update(s => ({ ...s, activeSessionId: sessionId })); - } - }; -} -``` - -#### 11.2.5 Android MediaRouter Integration (IR-021) - -On Android, when controlling a remote session's volume, JellyTau integrates with the system audio control panel via MediaRouter API: - -```mermaid -flowchart TB - User["User presses volume button"] - VolumePanel["System Volume Panel
appears showing
remote session name"] - MediaRouter["MediaRouter.Callback
onRouteVolumeChanged()"] - SessionsApi["SessionsApi.setVolume()
POST /Sessions/{id}/
Command/SetVolume"] - - User --> VolumePanel - VolumePanel --> MediaRouter - MediaRouter --> SessionsApi -``` - -**Kotlin Implementation** (`JellyTauMediaRouterCallback.kt`): -```kotlin -class JellyTauMediaRouterCallback( - private val sessionsApi: SessionsApi -) : MediaRouter.Callback() { - - private var selectedRoute: MediaRouter.RouteInfo? = null - - override fun onRouteSelected(router: MediaRouter, route: RouteInfo) { - selectedRoute = route - // Update UI to show remote session controls - } - - override fun onRouteVolumeChanged(router: MediaRouter, route: RouteInfo) { - selectedRoute?.let { selected -> - if (route == selected) { - val volume = route.volume - val sessionId = route.extras?.getString("sessionId") - sessionId?.let { - // Send volume to Jellyfin session - coroutineScope.launch { - sessionsApi.setVolume(it, volume) - } - } - } - } - } -} -``` - -**MediaRouteProvider** - Exposes Jellyfin sessions as cast-like routes: -```kotlin -class JellyfinMediaRouteProvider(context: Context) : MediaRouteProvider(context) { - - fun updateSessionRoutes(sessions: List) { - val routes = sessions - .filter { it.supportsRemoteControl } - .map { session -> - MediaRouteDescriptor.Builder(session.id, session.deviceName) - .setDescription(session.client) - .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) - .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) - .setVolumeMax(100) - .setVolume(session.playState?.volumeLevel ?: 100) - .addControlFilter(IntentFilter(MediaControlIntent.ACTION_PLAY)) - .setExtras(Bundle().apply { - putString("sessionId", session.id) - }) - .build() - } - - setDescriptor(MediaRouteProviderDescriptor.Builder() - .addRoutes(routes) - .build()) - } -} -``` - -#### 11.2.6 UI Components - -**CastButton** (`src/lib/components/sessions/CastButton.svelte`) - Cast button for MiniPlayer: -- Integrated into MiniPlayer component (visible on all screen sizes) -- Shows cast icon that changes when connected to remote session -- Displays badge with number of available sessions -- Auto-polls for sessions every 15 seconds -- Opens SessionPickerModal when clicked -- Visual indicators: - - Gray when disconnected, purple when connected - - Badge shows count of available devices - - Green dot indicator when actively casting - -**SessionPickerModal** (`src/lib/components/sessions/SessionPickerModal.svelte`) - Modal for selecting cast device: -- Lists controllable sessions with device name and client type -- Shows currently playing item (if any) for each session -- Highlights currently selected/connected session -- Device type icons (TV, Web, Mobile, Generic) -- "Disconnect" option when already connected -- Empty state with refresh button -- Responsive: slides up on mobile, centered on desktop - -**SessionCard** (`src/lib/components/sessions/SessionCard.svelte`) - Individual session display: -- Device name and client information -- Now playing preview with artwork -- Play/pause state indicator -- Position and volume display - -**SessionsList** (`src/lib/components/sessions/SessionsList.svelte`) - List of all sessions: -- Filters to show only controllable sessions -- Refresh button for manual updates -- Loading and error states -- Empty state messaging - -**RemoteControls** (`src/lib/components/sessions/RemoteControls.svelte`) - Full remote playback control: -- Uses polling data for play state (position, volume, etc.) -- Sends commands via SessionsApi -- Shows "Controlling: {deviceName}" header -- Full playback controls: play/pause, next/previous, stop -- Seek bar with position display (if supported) -- Volume slider -- Empty state when no media playing on remote session - -### 11.3 MPRIS Integration (Linux - Future) - -```rust -// Future: D-Bus media controls -pub struct MprisController { - connection: Connection, - player: Arc>, -} - -impl MprisController { - fn register_handlers(&self) { - // Handle PlayPause, Next, Previous from system - } -} -``` - --- -## 12. Offline Database Design +## 10. Offline Database Design ### 12.1 Entity Relationship Diagram @@ -3176,7 +2709,7 @@ src-tauri/src/storage/ --- -## 13. File Structure Summary +## 11. File Structure Summary ``` src-tauri/src/ @@ -3286,9 +2819,9 @@ src/lib/ --- -## 14. Security +## 12. Security -### 14.1 Authentication Token Storage +### 12.1 Authentication Token Storage Access tokens are **not** stored in the SQLite database. Instead, they are stored using platform-native secure storage: @@ -3314,7 +2847,7 @@ jellytau::{server_id}::{user_id}::access_token - System keyrings provide OS-level encryption and access control - Fallback ensures functionality on minimal systems without a keyring daemon -### 14.2 Secure Storage Module +### 12.2 Secure Storage Module **Location**: `src-tauri/src/secure_storage/` (planned) @@ -3330,7 +2863,7 @@ pub struct KeyringStorage; // Uses keyring crate pub struct EncryptedFileStorage; // AES-256-GCM fallback ``` -### 14.3 Network Security +### 12.3 Network Security | Aspect | Implementation | |--------|----------------| @@ -3339,7 +2872,7 @@ pub struct EncryptedFileStorage; // AES-256-GCM fallback | Token Transmission | Bearer token in `Authorization` header only | | Token Refresh | Handled by Jellyfin server (long-lived tokens) | -### 14.4 Local Data Protection +### 12.4 Local Data Protection | Data Type | Protection | |-----------|------------| @@ -3348,7 +2881,7 @@ pub struct EncryptedFileStorage; // AES-256-GCM fallback | Downloaded Media | Filesystem permissions only | | Cached Thumbnails | Filesystem permissions only | -### 14.5 Security Considerations +### 12.5 Security Considerations 1. **No Secrets in SQLite**: The database contains only non-sensitive metadata 2. **Token Isolation**: Each user/server combination has a separate token entry diff --git a/ThumbnailCachingArchitecture.md b/ThumbnailCachingArchitecture.md deleted file mode 100644 index 69a6dcc..0000000 --- a/ThumbnailCachingArchitecture.md +++ /dev/null @@ -1,1409 +0,0 @@ -# Comprehensive Media Caching Architecture - -## Overview - -This document outlines the architectural design for implementing [DR-016](README.md:151) (Image caching and sync with server) and enhancing [DR-012](README.md:147) (Local database for media metadata cache). - -**Goal**: Create a comprehensive offline-first architecture that caches **all media assets** (images, metadata, artwork) locally for instant loading, offline access, and reduced server load. - -### Scope: All Cacheable Assets - -1. **Images**: Posters, backdrops, title cards, logos, thumbnails, banners, profile pictures, disc art -2. **Metadata**: Media items, libraries, collections, people/cast, genres, studios -3. **User Data**: Watch progress, favorites, ratings, playlists -4. **Media Info**: Subtitle/audio track information, chapters, media streams - ---- - -## Current State - -### ✅ Already Implemented - -1. **Database Schema** ([schema.rs:227-237](src-tauri/src/storage/schema.rs#L227-L237)): - - `thumbnails` table with fields: `item_id`, `image_type`, `image_tag`, `file_path`, `width`, `height`, `cached_at` - - `items` table stores metadata including `primary_image_tag` - - Index on `item_id` for fast lookups - -2. **Data Models** ([models.rs:358-368](src-tauri/src/storage/models.rs#L358-L368)): - - `Thumbnail` struct matches database schema - -3. **Metadata Storage**: - - `items` table stores full media metadata - - `user_data` table stores playback progress and favorites - -### ❌ Not Yet Implemented - -1. **Image Download & Caching Service**: No code to download and cache images -2. **Cache Invalidation**: No logic to check `image_tag` for updates -3. **LRU Eviction**: No automatic cleanup of old thumbnails -4. **Repository Integration**: Repository pattern doesn't use cached images -5. **Tauri Commands**: No commands to manage thumbnail cache - ---- - -## Architectural Design - -### 1. Media Cache Service - -**Location**: `src-tauri/src/cache/` - -``` -src-tauri/src/cache/ -├── mod.rs # Module exports, MediaCacheService -├── images/ -│ ├── mod.rs # ImageCacheService -│ ├── download.rs # Image download with retry logic -│ ├── formats.rs # Image format conversion (WebP, AVIF) -│ └── preloader.rs # Intelligent pre-caching -├── metadata/ -│ ├── mod.rs # MetadataCacheService -│ ├── sync.rs # Sync with Jellyfin server -│ └── stale.rs # Stale-while-revalidate strategy -└── lru.rs # LRU eviction policy (shared) -``` - -#### 1.1 ImageCacheService - All Image Types - -**Supported Image Types** (from Jellyfin API): -- `Primary`: Poster/cover art (movies, albums, shows) -- `Backdrop`: Background images -- `Logo`: Transparent logos for overlays -- `Thumb`: Thumbnail preview frames -- `Banner`: Wide banner images -- `Art`: Disc/box art -- `Screenshot`: Episode screenshots -- `Profile`: Actor/person headshots - -#### 1.1.1 Core Service - -```rust -pub struct ImageCacheService { - db: Arc, - cache_dir: PathBuf, - client: reqwest::Client, - config: CacheConfig, -} - -pub struct CacheConfig { - pub max_cache_size_mb: u64, // Default: 500 MB - pub max_age_days: u32, // Default: 30 days - pub quality: ImageQuality, // Default: High -} - -pub enum ImageQuality { - Low, // 300px - Medium, // 720px - High, // 1080px - Original, // No resize -} - -impl ImageCacheService { - /// Get cached image path or download if missing - pub async fn get_image( - &self, - item_id: &str, - image_type: &str, - image_tag: Option<&str>, - width: Option, - height: Option, - ) -> Result { - // 1. Check database for existing cache entry - if let Some(cached) = self.db.get_thumbnail(item_id, image_type, image_tag).await? { - // Verify file still exists - if cached.file_path.exists() { - // Update last_accessed for LRU - self.db.touch_thumbnail(cached.id).await?; - return Ok(cached.file_path); - } else { - // File deleted externally, remove DB entry - self.db.delete_thumbnail(cached.id).await?; - } - } - - // 2. Download image from Jellyfin server - let image_data = self.download_image(item_id, image_type, width, height).await?; - - // 3. Save to disk - let file_path = self.save_image(item_id, image_type, image_tag, &image_data).await?; - - // 4. Insert into database - let thumbnail = Thumbnail { - id: None, - item_id: item_id.to_string(), - image_type: image_type.to_string(), - image_tag: image_tag.unwrap_or("").to_string(), - file_path: file_path.clone(), - width: width.map(|w| w as i32), - height: height.map(|h| h as i32), - cached_at: Some(Utc::now()), - last_accessed: Some(Utc::now()), - }; - self.db.insert_thumbnail(&thumbnail).await?; - - // 5. Check cache size and evict if needed - self.evict_if_needed().await?; - - Ok(file_path) - } - - /// Check if image is cached and valid - pub async fn is_cached( - &self, - item_id: &str, - image_type: &str, - image_tag: Option<&str>, - ) -> Result { - if let Some(cached) = self.db.get_thumbnail(item_id, image_type, image_tag).await? { - // Verify tag matches (cache invalidation) - if let Some(tag) = image_tag { - if cached.image_tag != tag { - // Tag changed, image updated on server - self.db.delete_thumbnail(cached.id).await?; - return Ok(false); - } - } - - // Verify file exists - return Ok(cached.file_path.exists()); - } - - Ok(false) - } - - /// Pre-cache images for a batch of items (e.g., library grid) - pub async fn precache_batch( - &self, - items: &[CacheRequest], - priority: CachePriority, - ) -> Result<(), CacheError> { - // Download images in parallel with concurrency limit - let futures = items.iter().map(|req| { - self.get_image( - &req.item_id, - &req.image_type, - req.image_tag.as_deref(), - req.width, - req.height, - ) - }); - - // Use buffered stream to limit concurrency (e.g., 5 at a time) - futures::stream::iter(futures) - .buffer_unordered(5) - .try_collect::>() - .await?; - - Ok(()) - } - - /// Evict old/unused thumbnails when cache size exceeds limit - async fn evict_if_needed(&self) -> Result<(), CacheError> { - let cache_size = self.get_cache_size().await?; - let max_size = self.config.max_cache_size_mb * 1024 * 1024; - - if cache_size > max_size { - // Get thumbnails sorted by last_accessed (LRU) - let to_evict = self.db.get_lru_thumbnails(100).await?; - - let mut freed = 0u64; - for thumb in to_evict { - if cache_size - freed <= max_size { - break; - } - - // Delete file - if let Ok(metadata) = std::fs::metadata(&thumb.file_path) { - freed += metadata.len(); - std::fs::remove_file(&thumb.file_path)?; - } - - // Delete DB entry - self.db.delete_thumbnail(thumb.id).await?; - } - } - - Ok(()) - } -} - -pub struct CacheRequest { - pub item_id: String, - pub image_type: String, - pub image_tag: Option, - pub width: Option, - pub height: Option, -} - -pub enum CachePriority { - High, // User navigated to this screen - Medium, // Prefetch for upcoming content - Low, // Background cache warming -} -``` - -#### 1.2 Database Queries - -**Location**: `src-tauri/src/storage/queries/thumbnails.rs` - -```rust -impl Database { - pub async fn get_thumbnail( - &self, - item_id: &str, - image_type: &str, - image_tag: Option<&str>, - ) -> Result> { - let conn = self.pool.get().await?; - - let query = if let Some(tag) = image_tag { - "SELECT * FROM thumbnails - WHERE item_id = ? AND image_type = ? AND image_tag = ?" - } else { - "SELECT * FROM thumbnails - WHERE item_id = ? AND image_type = ?" - }; - - // Execute query and return Thumbnail - } - - pub async fn insert_thumbnail(&self, thumbnail: &Thumbnail) -> Result { - // INSERT INTO thumbnails... - } - - pub async fn touch_thumbnail(&self, id: i64) -> Result<()> { - // UPDATE thumbnails SET last_accessed = CURRENT_TIMESTAMP WHERE id = ? - } - - pub async fn get_lru_thumbnails(&self, limit: usize) -> Result> { - // SELECT * FROM thumbnails - // ORDER BY last_accessed ASC - // LIMIT ? - } - - pub async fn delete_thumbnail(&self, id: i64) -> Result<()> { - // DELETE FROM thumbnails WHERE id = ? - } - - pub async fn get_cache_size(&self) -> Result { - // SELECT SUM(file_size) FROM thumbnails - // Or calculate from filesystem - } -} -``` - -**Schema Enhancement** (add to migration): - -```sql --- Add last_accessed column for LRU -ALTER TABLE thumbnails ADD COLUMN last_accessed TEXT DEFAULT CURRENT_TIMESTAMP; - --- Add file_size for cache size calculation -ALTER TABLE thumbnails ADD COLUMN file_size INTEGER; - --- Create index for LRU queries -CREATE INDEX IF NOT EXISTS idx_thumbnails_lru ON thumbnails(last_accessed ASC); -``` - ---- - -### 2. Repository Integration - -#### 2.1 Enhanced getImageUrl() - -**Location**: `src/lib/api/repository.ts` - -```typescript -export class OnlineRepository implements MediaRepository { - private imageCacheEnabled = true; - - async getImageUrl( - itemId: string, - imageType: string, - options: ImageOptions = {} - ): Promise { - const { maxWidth, maxHeight, tag } = options; - - if (this.imageCacheEnabled) { - // Check if cached locally via Tauri command - try { - const cachedPath = await invoke('cache_get_image', { - itemId, - imageType, - imageTag: tag, - width: maxWidth, - height: maxHeight, - }); - - if (cachedPath) { - // Return file:// URL for local cached image - return `file://${cachedPath}`; - } - } catch (err) { - console.warn('Cache lookup failed, falling back to server URL:', err); - } - } - - // Fallback to server URL (will be cached in background) - return this.buildImageUrl(itemId, imageType, options); - } - - private buildImageUrl(itemId: string, imageType: string, options: ImageOptions): string { - const params = new URLSearchParams(); - if (options.maxWidth) params.set('maxWidth', options.maxWidth.toString()); - if (options.maxHeight) params.set('maxHeight', options.maxHeight.toString()); - if (options.tag) params.set('tag', options.tag); - - return `${this.baseUrl}/Items/${itemId}/Images/${imageType}?${params}`; - } -} -``` - -#### 2.2 Background Pre-caching - -**Location**: `src/lib/services/imagePreloader.ts` - -```typescript -export class ImagePreloader { - private precacheQueue: Set = new Set(); - private processing = false; - - /** - * Pre-cache images for items in view - * Called when user navigates to library/album/detail pages - */ - async precacheVisible(items: MediaItem[]): Promise { - const requests = items - .filter(item => item.primaryImageTag) - .map(item => ({ - itemId: item.id, - imageType: 'Primary', - imageTag: item.primaryImageTag, - width: 400, // Medium quality for grids - height: 600, - })); - - try { - await invoke('cache_precache_batch', { requests, priority: 'high' }); - } catch (err) { - console.error('Precache failed:', err); - } - } - - /** - * Pre-cache upcoming queue items (for video player) - */ - async precacheQueue(items: MediaItem[]): Promise { - const requests = items - .slice(0, 5) // Next 5 items - .filter(item => item.primaryImageTag) - .map(item => ({ - itemId: item.id, - imageType: 'Primary', - imageTag: item.primaryImageTag, - width: 1920, - height: 1080, // Full quality for video player - })); - - try { - await invoke('cache_precache_batch', { requests, priority: 'medium' }); - } catch (err) { - console.error('Queue precache failed:', err); - } - } -} - -// Auto-initialize in app -export const imagePreloader = new ImagePreloader(); -``` - -**Usage in VideoPlayer**: - -```typescript -// In VideoPlayer.svelte -import { imagePreloader } from '$lib/services/imagePreloader'; - -onMount(() => { - // Pre-cache poster for next video in queue - if (nextInQueue) { - imagePreloader.precacheQueue([nextInQueue]); - } -}); -``` - ---- - -### 3. Tauri Commands - -**Location**: `src-tauri/src/commands/cache.rs` - -```rust -use crate::cache::ImageCacheService; - -#[tauri::command] -pub async fn cache_get_image( - item_id: String, - image_type: String, - image_tag: Option, - width: Option, - height: Option, - cache_service: State<'_, Arc>, -) -> Result, String> { - let path = cache_service - .get_image(&item_id, &image_type, image_tag.as_deref(), width, height) - .await - .map_err(|e| e.to_string())?; - - Ok(Some(path.to_string_lossy().to_string())) -} - -#[tauri::command] -pub async fn cache_is_cached( - item_id: String, - image_type: String, - image_tag: Option, - cache_service: State<'_, Arc>, -) -> Result { - cache_service - .is_cached(&item_id, &image_type, image_tag.as_deref()) - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cache_precache_batch( - requests: Vec, - priority: String, - cache_service: State<'_, Arc>, -) -> Result<(), String> { - let priority = match priority.as_str() { - "high" => CachePriority::High, - "medium" => CachePriority::Medium, - _ => CachePriority::Low, - }; - - cache_service - .precache_batch(&requests, priority) - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cache_clear( - cache_service: State<'_, Arc>, -) -> Result<(), String> { - cache_service - .clear_all() - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cache_get_stats( - cache_service: State<'_, Arc>, -) -> Result { - cache_service - .get_stats() - .await - .map_err(|e| e.to_string()) -} - -#[derive(Serialize, Deserialize)] -pub struct CacheStats { - pub total_images: u64, - pub total_size_mb: f64, - pub cache_hit_rate: f64, // Percentage -} -``` - ---- - -### 4. Metadata Caching Enhancement - -#### 4.1 Library Response Caching - -When fetching library items from Jellyfin, cache them in the `items` table: - -```rust -// In src-tauri/src/commands/library.rs (new command) - -#[tauri::command] -pub async fn library_sync_items( - library_id: String, - db: State<'_, Arc>, - jellyfin_client: State<'_, Arc>, -) -> Result, String> { - // 1. Fetch from Jellyfin API - let api_items = jellyfin_client - .get_library_items(&library_id) - .await - .map_err(|e| e.to_string())?; - - // 2. Upsert into database - for api_item in &api_items { - let db_item = convert_to_db_item(api_item); - db.upsert_item(&db_item).await.map_err(|e| e.to_string())?; - } - - // 3. Return items (now available offline) - Ok(api_items) -} -``` - -#### 4.2 Offline-First Repository - -```typescript -export class HybridRepository implements MediaRepository { - constructor( - private onlineRepo: OnlineRepository, - private db: Database - ) {} - - async getItem(itemId: string): Promise { - // Try local cache first - try { - const cached = await invoke('db_get_item', { itemId }); - if (cached) { - // Refresh in background (stale-while-revalidate) - this.refreshItemInBackground(itemId); - return cached; - } - } catch (err) { - console.warn('Cache lookup failed:', err); - } - - // Fetch from server and cache - const item = await this.onlineRepo.getItem(itemId); - await invoke('db_upsert_item', { item }).catch(console.error); - - return item; - } - - private async refreshItemInBackground(itemId: string): Promise { - try { - const fresh = await this.onlineRepo.getItem(itemId); - await invoke('db_upsert_item', { item: fresh }); - } catch (err) { - // Ignore, cached version is good enough - } - } -} -``` - ---- - -### 5. Metadata Caching Service - -**Location**: `src-tauri/src/cache/metadata/mod.rs` - -#### 5.1 Comprehensive Metadata Storage - -**Extended Database Schema**: - -```sql --- People/Cast (actors, directors, writers) -CREATE TABLE IF NOT EXISTS people ( - id TEXT PRIMARY KEY, - server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, - name TEXT NOT NULL, - role TEXT, -- Actor, Director, Writer, etc. - overview TEXT, - primary_image_tag TEXT, - birth_date TEXT, - death_date TEXT, - birth_place TEXT, - synced_at TEXT, - UNIQUE(server_id, id) -); - --- Cast/Crew associations -CREATE TABLE IF NOT EXISTS item_people ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - item_id TEXT NOT NULL REFERENCES items(id) ON DELETE CASCADE, - person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE, - role_type TEXT NOT NULL, -- Actor, Director, Writer, Producer, etc. - role_name TEXT, -- Character name for actors - sort_order INTEGER, - UNIQUE(item_id, person_id, role_type) -); - --- Collections (Box Sets) -CREATE TABLE IF NOT EXISTS collections ( - id TEXT PRIMARY KEY, - server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, - name TEXT NOT NULL, - overview TEXT, - primary_image_tag TEXT, - backdrop_image_tags TEXT, -- JSON array - synced_at TEXT, - UNIQUE(server_id, id) -); - --- Collection membership -CREATE TABLE IF NOT EXISTS collection_items ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - collection_id TEXT NOT NULL REFERENCES collections(id) ON DELETE CASCADE, - item_id TEXT NOT NULL REFERENCES items(id) ON DELETE CASCADE, - sort_order INTEGER, - UNIQUE(collection_id, item_id) -); - --- Studios/Networks -CREATE TABLE IF NOT EXISTS studios ( - id TEXT PRIMARY KEY, - server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, - name TEXT NOT NULL, - overview TEXT, - primary_image_tag TEXT, - synced_at TEXT, - UNIQUE(server_id, id) -); - --- Chapters (for video scrubbing thumbnails) -CREATE TABLE IF NOT EXISTS chapters ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - item_id TEXT NOT NULL REFERENCES items(id) ON DELETE CASCADE, - start_position_ticks INTEGER NOT NULL, - name TEXT, - image_tag TEXT, - UNIQUE(item_id, start_position_ticks) -); - --- Genres (with metadata) -CREATE TABLE IF NOT EXISTS genres ( - id TEXT PRIMARY KEY, - server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, - name TEXT NOT NULL, - item_count INTEGER DEFAULT 0, - synced_at TEXT, - UNIQUE(server_id, name) -); - --- Create indexes for relationships -CREATE INDEX IF NOT EXISTS idx_item_people_item ON item_people(item_id); -CREATE INDEX IF NOT EXISTS idx_item_people_person ON item_people(person_id); -CREATE INDEX IF NOT EXISTS idx_collection_items_collection ON collection_items(collection_id); -CREATE INDEX IF NOT EXISTS idx_collection_items_item ON collection_items(item_id); -CREATE INDEX IF NOT EXISTS idx_chapters_item ON chapters(item_id); -``` - -#### 5.2 MetadataCacheService - -```rust -pub struct MetadataCacheService { - db: Arc, - jellyfin_client: Arc, - sync_config: SyncConfig, -} - -pub struct SyncConfig { - pub auto_sync: bool, // Auto-sync in background - pub sync_interval_hours: u32, // Default: 6 hours - pub deep_sync: bool, // Include cast, collections, etc. - pub wifi_only: bool, // Sync only on WiFi -} - -impl MetadataCacheService { - /// Sync complete library metadata - pub async fn sync_library(&self, library_id: &str) -> Result { - let mut report = SyncReport::default(); - - // 1. Fetch all items from Jellyfin - let api_items = self.jellyfin_client.get_library_items(library_id).await?; - report.items_fetched = api_items.len(); - - // 2. Upsert items to database - for api_item in &api_items { - let db_item = self.convert_to_db_item(api_item); - self.db.upsert_item(&db_item).await?; - report.items_synced += 1; - - // 3. Deep sync: cast/crew, collections - if self.sync_config.deep_sync { - self.sync_item_people(&api_item).await?; - self.sync_item_collections(&api_item).await?; - } - } - - // 4. Update library sync timestamp - self.db.update_library_sync(library_id).await?; - - Ok(report) - } - - /// Sync cast/crew for an item - async fn sync_item_people(&self, item: &JellyfinItem) -> Result<(), CacheError> { - if let Some(people) = &item.people { - for person in people { - // Upsert person - let db_person = Person { - id: person.id.clone(), - server_id: item.server_id.clone(), - name: person.name.clone(), - role: person.role.clone(), - overview: None, - primary_image_tag: person.primary_image_tag.clone(), - birth_date: None, - death_date: None, - birth_place: None, - synced_at: Some(Utc::now()), - }; - self.db.upsert_person(&db_person).await?; - - // Create association - let association = ItemPerson { - item_id: item.id.clone(), - person_id: person.id.clone(), - role_type: person.type_field.clone(), // Actor, Director, etc. - role_name: person.role.clone(), // Character name - sort_order: person.sort_order, - }; - self.db.upsert_item_person(&association).await?; - } - } - - Ok(()) - } - - /// Fetch item with all related data (cast, collection, chapters) - pub async fn get_item_full(&self, item_id: &str) -> Result { - let item = self.db.get_item(item_id).await? - .ok_or(CacheError::NotFound)?; - - let cast = self.db.get_item_people(item_id, Some("Actor")).await?; - let crew = self.db.get_item_people(item_id, None).await?; // All roles - let collections = self.db.get_item_collections(item_id).await?; - let chapters = self.db.get_chapters(item_id).await?; - - Ok(FullItem { - item, - cast, - crew, - collections, - chapters, - }) - } - - /// Stale-while-revalidate: Return cached, refresh in background - pub async fn get_item_swr(&self, item_id: &str) -> Result { - // Try cache first - if let Some(cached) = self.db.get_item(item_id).await? { - // Check if stale (older than 6 hours) - if let Some(synced_at) = cached.synced_at { - let age = Utc::now() - synced_at; - if age.num_hours() < self.sync_config.sync_interval_hours as i64 { - return Ok(cached); // Fresh enough - } - } - - // Stale, but return it immediately - let cached_clone = cached.clone(); - - // Refresh in background - let client = self.jellyfin_client.clone(); - let db = self.db.clone(); - let item_id = item_id.to_string(); - tokio::spawn(async move { - if let Ok(fresh) = client.get_item(&item_id).await { - let _ = db.upsert_item(&fresh).await; - } - }); - - return Ok(cached_clone); - } - - // Not in cache, fetch from server - let fresh = self.jellyfin_client.get_item(item_id).await?; - self.db.upsert_item(&fresh).await?; - - Ok(fresh) - } -} - -#[derive(Debug, Default)] -pub struct SyncReport { - pub items_fetched: usize, - pub items_synced: usize, - pub images_cached: usize, - pub people_synced: usize, - pub errors: Vec, -} - -pub struct FullItem { - pub item: Item, - pub cast: Vec, - pub crew: Vec, - pub collections: Vec, - pub chapters: Vec, -} - -pub struct PersonWithRole { - pub person: Person, - pub role_type: String, // Actor, Director, etc. - pub role_name: Option, // Character name -} -``` - ---- - -### 6. Smart Pre-caching Strategies - -#### 6.1 Predictive Pre-caching - -```rust -pub struct PrecacheEngine { - image_cache: Arc, - metadata_cache: Arc, - analytics: Arc, -} - -impl PrecacheEngine { - /// Pre-cache based on navigation patterns - pub async fn precache_navigation(&self, context: NavigationContext) -> Result<(), CacheError> { - match context { - NavigationContext::LibraryGrid { library_id, visible_items } => { - // 1. Cache visible items (high priority) - self.precache_grid_items(&visible_items, CachePriority::High).await?; - - // 2. Predict next page (medium priority) - let next_page = self.predict_next_page(&library_id, &visible_items).await?; - self.precache_grid_items(&next_page, CachePriority::Medium).await?; - }, - - NavigationContext::DetailView { item_id } => { - // 1. Cache item details (high priority) - self.metadata_cache.get_item_full(&item_id).await?; - - // 2. Cache all images for item - self.precache_item_images(&item_id).await?; - - // 3. Cache cast profile pictures (medium priority) - self.precache_cast_images(&item_id).await?; - - // 4. If series, cache next episode - if let Some(next_ep) = self.get_next_episode(&item_id).await? { - self.precache_item_images(&next_ep.id).await?; - } - }, - - NavigationContext::Queue { items } => { - // Cache next 5 items in queue - for (index, item) in items.iter().take(5).enumerate() { - let priority = match index { - 0 => CachePriority::High, - 1..=2 => CachePriority::Medium, - _ => CachePriority::Low, - }; - self.precache_item_images(&item.id).await?; - } - }, - - NavigationContext::Search { query } => { - // No pre-caching for search (unpredictable) - }, - } - - Ok(()) - } - - async fn precache_item_images(&self, item_id: &str) -> Result<(), CacheError> { - let item = self.metadata_cache.db.get_item(item_id).await? - .ok_or(CacheError::NotFound)?; - - // Cache all image types for this item - let image_types = vec!["Primary", "Backdrop", "Logo", "Thumb"]; - - for img_type in image_types { - let tag = self.get_image_tag(&item, img_type); - if tag.is_some() { - // Fire and forget - let _ = self.image_cache.get_image( - item_id, - img_type, - tag.as_deref(), - Some(1920), - Some(1080), - ).await; - } - } - - Ok(()) - } - - async fn precache_cast_images(&self, item_id: &str) -> Result<(), CacheError> { - let people = self.metadata_cache.db.get_item_people(item_id, Some("Actor")).await?; - - for person in people.iter().take(10) { // Top 10 cast - if let Some(tag) = &person.person.primary_image_tag { - let _ = self.image_cache.get_image( - &person.person.id, - "Primary", - Some(tag), - Some(400), - Some(400), - ).await; - } - } - - Ok(()) - } -} - -pub enum NavigationContext { - LibraryGrid { library_id: String, visible_items: Vec }, - DetailView { item_id: String }, - Queue { items: Vec }, - Search { query: String }, -} -``` - -#### 6.2 Background Cache Warming - -```rust -pub struct CacheWarmingService { - metadata_cache: Arc, - image_cache: Arc, - config: WarmingConfig, -} - -pub struct WarmingConfig { - pub enabled: bool, - pub warm_on_wifi_only: bool, - pub warm_continue_watching: bool, // Pre-cache items user is likely to watch - pub warm_new_releases: bool, // Pre-cache recently added content - pub warm_favorites: bool, // Pre-cache favorited content -} - -impl CacheWarmingService { - /// Run background cache warming (called periodically) - pub async fn warm_cache(&self) -> Result { - let mut report = WarmingReport::default(); - - if !self.config.enabled { - return Ok(report); - } - - // 1. Continue Watching - User's in-progress items - if self.config.warm_continue_watching { - let in_progress = self.metadata_cache.db - .get_in_progress_items(&self.get_user_id()) - .await?; - - for item in in_progress.iter().take(20) { - self.warm_item(&item.id).await?; - report.items_warmed += 1; - } - } - - // 2. Recently Added - New content - if self.config.warm_new_releases { - let recent = self.metadata_cache.db - .get_recently_added(30) // Last 30 days - .await?; - - for item in recent.iter().take(50) { - self.warm_item(&item.id).await?; - report.items_warmed += 1; - } - } - - // 3. Favorites - if self.config.warm_favorites { - let favorites = self.metadata_cache.db - .get_favorites(&self.get_user_id()) - .await?; - - for item in favorites.iter().take(100) { - self.warm_item(&item.id).await?; - report.items_warmed += 1; - } - } - - Ok(report) - } - - async fn warm_item(&self, item_id: &str) -> Result<(), CacheError> { - // Fetch metadata (stale-while-revalidate) - let _ = self.metadata_cache.get_item_swr(item_id).await?; - - // Cache primary image - let item = self.metadata_cache.db.get_item(item_id).await? - .ok_or(CacheError::NotFound)?; - - if let Some(tag) = &item.primary_image_tag { - let _ = self.image_cache.get_image( - item_id, - "Primary", - Some(tag), - Some(1080), - Some(1620), - ).await; - } - - Ok(()) - } -} - -#[derive(Debug, Default)] -pub struct WarmingReport { - pub items_warmed: usize, - pub images_cached: usize, -} -``` - ---- - -### 7. Offline-First Data Flow - -```mermaid -sequenceDiagram - participant UI as UI Component - participant Repo as HybridRepository - participant Cache as MetadataCache - participant DB as SQLite - participant API as Jellyfin API - participant ImgCache as ImageCache - participant FS as File System - - UI->>Repo: getItem(itemId) - Repo->>Cache: get_item_swr(itemId) - - par Immediate Return - Cache->>DB: SELECT * FROM items WHERE id = ? - DB-->>Cache: Cached Item (may be stale) - Cache-->>Repo: Return cached item - Repo-->>UI: Display immediately - and Background Refresh - Cache->>API: GET /Items/{itemId} - API-->>Cache: Fresh item data - Cache->>DB: UPDATE items SET ... - end - - UI->>Repo: getImageUrl(itemId, "Primary") - Repo->>ImgCache: get_image(itemId, "Primary") - - alt Image Cached - ImgCache->>DB: Check thumbnails table - DB-->>ImgCache: Cached path - ImgCache->>FS: Verify file exists - FS-->>ImgCache: File exists - ImgCache-->>Repo: file:///path/to/image.jpg - Repo-->>UI: Display immediately (<50ms) - else Image Not Cached - ImgCache->>API: GET /Items/{id}/Images/Primary - API-->>ImgCache: Image data - ImgCache->>FS: Save to cache dir - ImgCache->>DB: INSERT INTO thumbnails - ImgCache-->>Repo: file:///path/to/image.jpg - Repo-->>UI: Display (~500ms first time) - end -``` - ---- - -### 8. Complete Tauri Commands API - -**Location**: `src-tauri/src/commands/cache.rs` - -```rust -// Image Cache Commands -#[tauri::command] -pub async fn cache_get_image(...) -> Result { /* ... */ } - -#[tauri::command] -pub async fn cache_get_all_images( - item_id: String, - cache_service: State<'_, Arc>, -) -> Result, String> { - // Returns all cached image types for an item - // { "Primary": "file:///...", "Backdrop": "file:///...", ... } -} - -#[tauri::command] -pub async fn cache_precache_batch(...) -> Result<(), String> { /* ... */ } - -// Metadata Cache Commands -#[tauri::command] -pub async fn metadata_sync_library( - library_id: String, - deep_sync: bool, - metadata_service: State<'_, Arc>, -) -> Result { /* ... */ } - -#[tauri::command] -pub async fn metadata_get_item_full( - item_id: String, - metadata_service: State<'_, Arc>, -) -> Result { - // Returns item with cast, crew, collections, chapters -} - -#[tauri::command] -pub async fn metadata_get_person( - person_id: String, - metadata_service: State<'_, Arc>, -) -> Result { /* ... */ } - -#[tauri::command] -pub async fn metadata_get_person_filmography( - person_id: String, - metadata_service: State<'_, Arc>, -) -> Result, String> { - // Get all items this person appears in -} - -#[tauri::command] -pub async fn metadata_search_offline( - query: String, - filters: SearchFilters, - db: State<'_, Arc>, -) -> Result { - // FTS5 search across cached items -} - -// Cache Management Commands -#[tauri::command] -pub async fn cache_get_stats(...) -> Result { /* ... */ } - -#[tauri::command] -pub async fn cache_clear_all( - image_cache: State<'_, Arc>, - metadata_cache: State<'_, Arc>, -) -> Result<(), String> { - image_cache.clear_all().await.map_err(|e| e.to_string())?; - metadata_cache.clear_all().await.map_err(|e| e.to_string())?; - Ok(()) -} - -#[tauri::command] -pub async fn cache_clear_images_only(...) -> Result<(), String> { /* ... */ } - -#[tauri::command] -pub async fn cache_clear_metadata_only(...) -> Result<(), String> { /* ... */ } - -// Pre-caching Commands -#[tauri::command] -pub async fn precache_navigation( - context: NavigationContext, - precache_engine: State<'_, Arc>, -) -> Result<(), String> { /* ... */ } - -#[tauri::command] -pub async fn cache_warm_background( - warming_service: State<'_, Arc>, -) -> Result { /* ... */ } -``` - ---- - -## Implementation Plan - -### Phase 1: Core Caching Infrastructure (Week 1) - -1. ✅ Database schema enhancement (add `last_accessed`, `file_size` to thumbnails) -2. ✅ Create `src-tauri/src/cache/` module -3. ✅ Implement `ImageCacheService` with basic download and storage -4. ✅ Add database queries for thumbnails -5. ✅ Create Tauri commands: `cache_get_image`, `cache_is_cached` - -**Testing**: -- Unit tests for cache service -- Integration test: Download and retrieve thumbnail -- Verify file system operations - -### Phase 2: Repository Integration (Week 2) - -1. ✅ Update `OnlineRepository.getImageUrl()` to check cache -2. ✅ Implement `ImagePreloader` service -3. ✅ Add cache checking to VideoPlayer component -4. ✅ Wire up precaching in library navigation - -**Testing**: -- E2E test: Navigate to library, verify images load from cache -- Measure load time improvement - -### Phase 3: LRU Eviction & Optimization (Week 3) - -1. ✅ Implement `evict_if_needed()` with LRU policy -2. ✅ Add background cache warming (popular content) -3. ✅ Implement `cache_precache_batch` command -4. ✅ Add cache statistics tracking - -**Testing**: -- Test cache size limit enforcement -- Verify LRU eviction removes oldest items -- Performance benchmarks - -### Phase 4: Metadata Caching (Week 4) - -1. ✅ Implement `db_upsert_item` and `db_get_item` commands -2. ✅ Create `HybridRepository` with offline-first strategy -3. ✅ Add stale-while-revalidate pattern -4. ✅ Implement background sync service - -**Testing**: -- Test offline mode with cached metadata -- Verify background refresh works -- Test cache invalidation on etag changes - ---- - -## Performance Impact - -### Before (Current State) - -- **Video Player Load**: 500-2000ms (network fetch) -- **Library Grid Load**: 2-5s for 50 items (50 image requests) -- **Offline Support**: None - -### After (With Caching) - -- **Video Player Load**: 50-100ms (local file read) -- **Library Grid Load**: 200-500ms (cached images) -- **Offline Support**: Full metadata + images available offline - -**Expected Improvements**: -- 10x faster video player initialization -- 5-10x faster library browsing -- Zero loading time on repeat navigation - ---- - -## Storage Estimates - -| Content Type | Image Type | Resolution | Size per Image | 1000 Items | -|--------------|-----------|------------|----------------|------------| -| Movies | Poster | 400x600 | ~80 KB | 80 MB | -| Movies | Backdrop | 1920x1080 | ~200 KB | 200 MB | -| TV Shows | Poster | 400x600 | ~80 KB | 80 MB | -| Albums | Cover | 400x400 | ~60 KB | 60 MB | - -**Recommended Cache Size**: 500 MB (configurable) -- ~6,000 posters or ~2,500 backdrops -- Sufficient for typical library browsing - ---- - -## Cache Invalidation Strategy - -1. **Image Tag Comparison**: - - Jellyfin provides `ImageTag` for each image - - Compare tag on each fetch, re-download if changed - - Automatic when user updates poster/backdrop - -2. **TTL (Time-to-Live)**: - - Optional: Images older than 30 days can be re-validated - - Useful for metadata that changes rarely - -3. **Manual Refresh**: - - Settings UI: "Clear Image Cache" button - - Developer option: Force refresh all images - ---- - -## Configuration UI - -**Location**: `src/routes/settings/+page.svelte` - -```svelte - -
-

Cache Settings

- - -
-
-

Image Cache Size Limit

-

Maximum storage for cached images

-
- -
- - -
-
- Current Cache Size - {cacheStats.totalSizeMB} MB -
-
- Cached Images - {cacheStats.totalImages} -
-
- Cache Hit Rate - {cacheStats.cacheHitRate}% -
-
- - - -
-``` - ---- - -## Success Metrics - -1. **Performance**: - - Video player title card appears in <100ms - - Library grid renders in <500ms - - Cache hit rate >80% for repeat navigation - -2. **Storage**: - - Cache stays within configured limit - - LRU eviction maintains most-used content - -3. **User Experience**: - - No perceived loading delay for cached content - - Smooth navigation between library views - - Offline browsing works seamlessly - ---- - -## Future Enhancements - -1. **Progressive Image Loading**: - - Show low-quality placeholder immediately - - Replace with high-quality when available - -2. **Smart Pre-caching**: - - Analyze navigation patterns - - Pre-cache likely next views (e.g., continue watching) - -3. **WebP Support**: - - Convert to WebP for 25-35% size reduction - - Requires Jellyfin server support or client-side conversion - -4. **CDN Integration**: - - Support for CDN-hosted images - - Edge caching for improved performance - ---- - -## Related Requirements - -- ✅ [DR-012](README.md:147): Local database for media metadata cache (Done) -- 🔄 [DR-016](README.md:151): Thumbnail caching and sync with server (In Progress) -- 🔄 [DR-001](README.md:136): Player state machine - Loading state (Partially Done - UI implemented) -- 🔄 [DR-010](README.md:145): Video player UI (Planned) - ---- - -## Questions for Discussion - -1. **Image Format**: Should we convert all images to WebP for smaller size? -2. **Cache Priority**: Should video title cards get higher priority than library thumbnails? -3. **Background Sync**: How aggressively should we pre-cache? (WiFi-only option?) -4. **Offline Mode**: Should we pre-download all metadata for offline libraries? - ---- - -**Last Updated**: 2026-01-04 -**Status**: Design Complete - Ready for Implementation -**Next Step**: Begin Phase 1 implementation diff --git a/UX-ImplementationGaps.md b/UX-ImplementationGaps.md deleted file mode 100644 index 7309c32..0000000 --- a/UX-ImplementationGaps.md +++ /dev/null @@ -1,219 +0,0 @@ -# UX Implementation Gaps - -This document tracks inconsistencies between the documented UX flows ([UXFlows.md](UXFlows.md)) and the actual implementation. - -**Last Updated:** 2026-01-03 - ---- - -## Recently Resolved ✅ - -### 1. Route Structure Mismatch ✅ RESOLVED - -**Was Documented:** -- Login screen at `/` -- Home/Library landing at `/library` (after login) - -**Actual Implementation:** -- Login screen at [/login](src/routes/login/+page.svelte) -- Home page with carousels at [/](src/routes/+page.svelte) -- Library selector at [/library](src/routes/library/+page.svelte) - -**Resolution:** Updated UXFlows.md Sections 1.1, 2.1, 2.2, 8.2 to reflect actual routes - ---- - -### 3. Audio Player Back Button Navigation ✅ RESOLVED - -**Issue:** -- Audio player close button simply closed the modal without respecting browser history -- Users expected back button to return to previous page (e.g., album view) - -**Resolution:** -- Updated AudioPlayer onClose handler to call `window.history.back()` ([library/+layout.svelte:212-215](src/routes/library/+layout.svelte)) -- Back button now returns user to the page they were on before opening full player -- Updated UXFlows.md Section 3.3 to document browser history navigation - -**Impact:** Better UX - natural back button behavior matches user expectations - ---- - -### 4. Video Player Touch Gestures ✅ RESOLVED - -**Was Documented:** -- Double tap left: Rewind 10 seconds -- Double tap right: Forward 10 seconds -- Swipe up/down: Adjust brightness (left) or volume (right) - -**Implementation:** -- ✅ Double-tap detection on left/right sides of screen ([VideoPlayer.svelte:228-319](src/lib/components/player/VideoPlayer.svelte)) -- ✅ Animated visual feedback showing "-10" or "+10" with fade-out animation -- ✅ Swipe gesture detection for vertical swipes (>50px minimum) -- ✅ Left side swipe: Brightness control (0.3-1.7x) with CSS filter -- ✅ Right side swipe: Volume control (0-100%) with real-time adjustment -- ✅ Visual indicators showing current brightness/volume level during swipe -- ✅ Updated UXFlows.md Section 4.2 to document all gestures - -**Impact:** Excellent mobile video UX - touch-optimized controls match YouTube/Netflix patterns - ---- - -## Remaining Issues - -### Critical Issues - -None! All critical UX issues have been resolved. 🎉 - ---- - -### Low Priority Issues - -#### 1. MiniPlayer Shows More Controls Than Documented - -**Documented:** -- Shows: artwork, title, artist, play/pause, next, favorite - -**Actual Implementation:** -- Shows: artwork, title, artist, play/pause, next, previous, shuffle, repeat, volume (desktop), cast button, favorite (desktop) -- Much richer control set than documented - -**Impact:** Low - This is actually better than documented - -**Fix Required:** Update UXFlows.md Section 3.1 to document full control set - ---- - -#### 2. Search Results Display Simplified - -**Documented (Section 6.1):** -- Results grouped by type: Songs, Albums, Artists, Movies, Episodes -- "See all (23)" expandable sections - -**Actual Implementation:** -- [Search page](src/routes/search/+page.svelte) uses `LibraryGrid` component -- No visual grouping by type shown in code -- Simplified single-list results - -**Impact:** Low - Functional but less organized than documented - -**Fix Required:** -- Option A: Implement grouped results display -- Option B: Update documentation to match simplified implementation - ---- - -## Undocumented Features - -### 3. Sessions/Remote Control Feature - -**Not in UXFlows.md:** -- Route exists: [/sessions](src/routes/sessions/+page.svelte) -- Appears to be for remote session control -- Cast button visible in MiniPlayer - -**Impact:** None - Feature exists but isn't documented - -**Fix Required:** Document sessions feature in UXFlows.md (Section 8.2 exists in SoftwareArchitecture.md but not in UX flows) - ---- - -### 4. URL Query Parameters for Queue Context - -**Not in UXFlows.md:** -- Uses `?queue=parent:{id}&shuffle=true` in URLs -- Enables queue restoration and context tracking - -**Impact:** None - Implementation detail - -**Fix Required:** Optional - add technical note in UXFlows.md about queue URL params - ---- - -## Consistent Implementations ✅ - -These areas match the documentation exactly: - -- ✅ **Resume Dialog** - Correctly implements 30s/90% threshold ([player/[id]/+page.svelte:84-86](src/routes/player/[id]/+page.svelte)) -- ✅ **Audio Playback** - Uses `player_play_queue` command as documented -- ✅ **MiniPlayer Hiding** - Correctly hides for video content -- ✅ **Keyboard Shortcuts** - Video player keyboard controls work as documented -- ✅ **Music Library Structure** - Category pages match documentation - ---- - -## Recommendations by Priority - -### High Priority - -1. ~~**Update UXFlows.md Route Structure**~~ ✅ **COMPLETED** - - ✅ Corrected login route to `/login` - - ✅ Clarified `/` is home page, `/library` is library selector - - ✅ Updated Sections 1.1, 2.1, 2.2, 7.2, 8.1, 8.2 - -2. ~~**Add Downloads Navigation**~~ ✅ **COMPLETED** - - ✅ Added Downloads link to header navigation (desktop) - - ✅ Added Downloads icon button to header (all screen sizes) - - ✅ Updated UXFlows.md to document navigation paths - -### Medium Priority - -3. ~~**Add Settings Navigation Link**~~ ✅ **COMPLETED** - - ✅ Added Settings link to desktop header navigation - - ✅ Added Settings to mobile overflow menu - - ✅ Updated UXFlows.md Section 8.1 - -4. ~~**Implement Video Player Touch Gestures**~~ ✅ **COMPLETED** - - ✅ Implemented double-tap detection (left/right sides) - - ✅ Implemented swipe gestures (brightness + volume control) - - ✅ Added visual feedback animations - - ✅ Updated UXFlows.md Section 4.2 - -### Low Priority - -5. **Document MiniPlayer Full Feature Set** (#4) - - Update Section 3.1 to show all controls - - Document desktop vs mobile differences - -6. **Document Sessions Feature** (#7) - - Add UX flow for remote session selection - - Explain cast button behavior - - Link to architecture documentation - -7. **Enhance Search Results Display** (#5) - - Consider implementing grouped results as documented - - Or update docs to match current implementation - ---- - -## Quick Fix Checklist - -For immediate documentation updates: - -- [x] Fix login route in UXFlows.md (Section 2.1): `/` → `/login` ✅ -- [x] Fix home route in UXFlows.md (Section 2.1): `/library` → `/` ✅ -- [x] Document actual navigation structure (Section 1.1) ✅ -- [x] Update Downloads navigation (Section 7.2) ✅ -- [x] Update Settings navigation (Section 8.1) ✅ -- [x] Update logout flow (Section 8.2) ✅ -- [ ] Update video controls documentation (Section 4.2) to match keyboard implementation -- [ ] Document MiniPlayer cast button (Section 3.1) -- [ ] Add Sessions feature UX flow (new section) - -For code implementation: - -- [x] Add Downloads link to header navigation (desktop) ✅ -- [x] Add Downloads icon button to header (all screen sizes) ✅ -- [x] Add Settings link to header navigation ✅ -- [x] Implement mobile overflow menu (Android-style) ✅ -- [x] Fix audio player back button to use browser history ✅ -- [ ] (Optional) Implement video player touch gestures -- [ ] (Optional) Implement grouped search results - ---- - -## Notes - -- The actual implementation is generally **more feature-rich** than documented (MiniPlayer controls, keyboard shortcuts) -- Main gaps are in **mobile navigation accessibility** (missing More tab) -- Most core functionality **matches or exceeds** documentation -- Video player touch gestures are the only **missing feature** that was explicitly documented