# JellyTau Software Architecture
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
## Major Architectural Changes (Recent)
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
### Architecture Principles
- **Thin UI Layer**: TypeScript reduced from ~3,300 to ~800 lines
- **Business Logic in Rust**: Performance, reliability, type safety
- **Event-Driven**: Rust emits events, TypeScript listens and updates UI
- **Handle-Based Resources**: UUID handles for stateful Rust objects
- **Cache-First**: Parallel queries with intelligent fallback
---
## 1. High-Level Overview
```mermaid
flowchart TB
subgraph Frontend["Svelte Frontend"]
subgraph Stores["Stores (Thin Wrappers)"]
auth["auth"]
player["player"]
queue["queue"]
library["library"]
connectivity["connectivity"]
playbackMode["playbackMode"]
end
subgraph Components
playerComp["player/"]
libraryComp["library/"]
Search["Search"]
end
subgraph Routes
routeLibrary["/library"]
routePlayer["/player"]
routeRoot["/"]
end
subgraph API["API Layer (Thin Client)"]
RepositoryClient["RepositoryClient
(Handle-based)"]
JellyfinClient["JellyfinClient
(Helper)"]
end
end
Frontend -->|"Tauri IPC (invoke)"| Backend
subgraph Backend["Rust Backend (Business Logic)"]
subgraph Commands["Tauri Commands (80+)"]
PlayerCmds["player.rs"]
RepoCmds["repository.rs (27)"]
PlaybackModeCmds["playback_mode.rs (5)"]
StorageCmds["storage.rs"]
ConnectivityCmds["connectivity.rs (7)"]
end
subgraph Core["Core Modules"]
MediaSessionManager["MediaSessionManager
(Audio/Movie/TvShow/Idle)"]
PlayerController["PlayerController
+ PlayerBackend
+ QueueManager"]
Repository["Repository Layer
HybridRepository (cache-first)
OnlineRepository (HTTP)
OfflineRepository (SQLite)"]
PlaybackModeManager["PlaybackModeManager
(Local/Remote/Idle)"]
ConnectivityMonitor["ConnectivityMonitor
(Adaptive polling)"]
HttpClient["HttpClient
(Exponential backoff retry)"]
end
subgraph Storage["Storage Layer"]
DatabaseService["DatabaseService
(Async trait)"]
SQLite["SQLite Database
(13 tables)"]
end
Commands --> Core
Core --> Storage
Repository --> HttpClient
Repository --> DatabaseService
end
```
---
## 2. Rust Backend Architecture
### 2.1 Media Session State Machine
**Location**: `src-tauri/src/player/session.rs` ✅ **IMPLEMENTED**
The media session tracks the high-level playback context (what kind of media is being consumed) and persists beyond individual playback states. This enables persistent UI (miniplayer for audio) and proper transitions between content types.
**Architecture Note:** The session manager is a separate app-level state manager (not inside PlayerController), coordinated by the commands layer. This maintains clean separation of concerns.
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> AudioActive : play_queue(audio)
Idle --> MovieActive : play_item(movie)
Idle --> TvShowActive : play_item(episode)
state "Audio Session" as AudioSession {
[*] --> AudioActive
AudioActive --> AudioInactive : playback_ended
AudioInactive --> AudioActive : resume/play
AudioActive --> AudioActive : next/previous
}
state "Movie Session" as MovieSession {
[*] --> MovieActive
MovieActive --> MovieInactive : playback_ended
MovieInactive --> MovieActive : resume
}
state "TV Show Session" as TvShowSession {
[*] --> TvShowActive
TvShowActive --> TvShowInactive : playback_ended
TvShowInactive --> TvShowActive : next_episode/resume
}
AudioSession --> Idle : dismiss/clear_queue
AudioSession --> MovieSession : play_item(movie)
AudioSession --> TvShowSession : play_item(episode)
MovieSession --> Idle : dismiss/playback_complete
MovieSession --> AudioSession : play_queue(audio)
TvShowSession --> Idle : dismiss/series_complete
TvShowSession --> AudioSession : play_queue(audio)
note right of Idle
No active media session
Queue may exist but not playing
No miniplayer/video player shown
end note
note right of AudioSession
SHOW: Miniplayer (always visible)
- Active: Play/pause/skip controls enabled
- Inactive: Play button to resume queue
Persists until explicit dismiss
end note
note right of MovieSession
SHOW: Full video player
- Active: Video playing/paused
- Inactive: Resume dialog
Auto-dismiss when playback ends
end note
note right of TvShowSession
SHOW: Full video player + Next Episode UI
- Active: Video playing/paused
- Inactive: Next episode prompt
Auto-dismiss when series ends
end note
```
**Session State Enum:**
```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MediaSessionType {
/// No active session - browsing library
Idle,
/// Audio playback session (music, audiobooks, podcasts)
/// Persists until explicitly dismissed
Audio {
/// Last/current track being played
last_item: Option,
/// True = playing/paused, False = stopped/ended
is_active: bool,
},
/// Movie playback (single video, auto-dismiss on end)
Movie {
item: MediaItem,
is_active: bool, // true = playing/paused, false = ended
},
/// TV show playback (supports next episode auto-advance)
TvShow {
item: MediaItem,
series_id: String,
is_active: bool, // true = playing/paused, false = ended
},
}
```
**State Transitions & Rules:**
| From State | Event | To State | UI Behavior | Notes |
|------------|-------|----------|-------------|-------|
| Idle | `play_queue(audio)` | Audio (active) | Show miniplayer | Creates audio session |
| Idle | `play_item(movie)` | Movie (active) | Show video player | Creates movie session |
| Idle | `play_item(episode)` | TvShow (active) | Show video player | Creates TV session |
| Audio (active) | `playback_ended` | Audio (inactive) | Miniplayer stays visible | Queue preserved |
| Audio (inactive) | `play/resume` | Audio (active) | Miniplayer enabled | Resume from queue |
| Audio (active/inactive) | `dismiss` | Idle | Hide miniplayer | Clear session |
| Audio (active/inactive) | `play_item(movie)` | Movie (active) | Switch to video player | Replace session |
| Movie (active) | `playback_ended` | Idle | Hide video player | Auto-dismiss |
| Movie (active) | `dismiss` | Idle | Hide video player | User dismiss |
| TvShow (active) | `playback_ended` | TvShow (inactive) | Show next episode UI | Wait for user choice |
| TvShow (inactive) | `next_episode` | TvShow (active) | Play next episode | Stay in session |
| TvShow (inactive) | `series_complete` | Idle | Hide video player | No more episodes |
**Key Design Decisions:**
1. **Audio Sessions Persist**: Miniplayer stays visible even when queue ends, allows easy resume
2. **Video Sessions Auto-Dismiss**: Movies auto-close when finished (unless paused)
3. **Single Active Session**: Playing new content type replaces current session
4. **Explicit Dismiss for Audio**: User must click close button to clear audio session
5. **Session ≠ PlayerState**: Session is higher-level, PlayerState tracks playing/paused/seeking
**Edge Cases Handled:**
- ✅ Album finishes: Session goes inactive, miniplayer shows last track with play disabled
- ✅ User wants to dismiss: Close button clears session → Idle
- ✅ Switch content types: New session replaces old (audio → movie)
- ✅ Paused for extended time: Session persists indefinitely
- ✅ Playback errors: Session stays inactive, allows retry
- ✅ Queue operations while idle: Queue exists but no session created until play
### 2.2 Player State Machine (Low-Level Playback)
**Location**: `src-tauri/src/player/state.rs`
The player uses a deterministic state machine with 6 states (operates within a media session):
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Loading : Load
Loading --> Playing : MediaLoaded
Playing --> Paused : Pause
Paused --> Playing : Play
Paused --> Seeking : Seek
Seeking --> Playing : PositionUpdate
Playing --> Idle : Stop
Paused --> Idle : Stop
Idle --> Error : Error
Loading --> Error : Error
Playing --> Error : Error
Paused --> Error : Error
Seeking --> Error : Error
state Playing {
[*] : position, duration
}
state Paused {
[*] : position, duration
}
state Seeking {
[*] : target
}
state Error {
[*] : error message
}
```
**State Enum:**
```rust
pub enum PlayerState {
Idle,
Loading { media: MediaItem },
Playing { media: MediaItem, position: f64, duration: f64 },
Paused { media: MediaItem, position: f64, duration: f64 },
Seeking { media: MediaItem, target: f64 },
Error { media: Option, error: String },
}
```
**Event Enum:**
```rust
pub enum PlayerEvent {
Load(MediaItem),
Play,
Pause,
Stop,
Seek(f64),
Next,
Previous,
MediaLoaded(f64), // duration
PositionUpdate(f64), // position
PlaybackEnded,
Error(String),
}
```
### 2.3 Playback Mode State Machine
**Location**: `src-tauri/src/playback_mode/mod.rs`
The playback mode manages whether media is playing locally on the device or remotely on another Jellyfin session (TV, browser, etc.):
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Local : play_queue()
Idle --> Remote : transfer_to_remote(session_id)
Local --> Remote : transfer_to_remote(session_id)
Local --> Idle : stop()
Remote --> Local : transfer_to_local()
Remote --> Idle : session_disconnected()
Remote --> Idle : stop()
state Local {
[*] : Playing on device
[*] : ExoPlayer active
[*] : Volume buttons → device
}
state Remote {
[*] : Controlling session
[*] : session_id
[*] : Volume buttons → remote
[*] : Android: VolumeProvider active
}
state Idle {
[*] : No active playback
}
```
**State Enum:**
```rust
pub enum PlaybackMode {
Local, // Playing on local device
Remote { session_id: String }, // Controlling remote Jellyfin session
Idle, // No active playback
}
```
**State Transitions:**
| From | Event | To | Side Effects |
|------|-------|-----|--------------|
| Idle | `play_queue()` | Local | Start local playback |
| Idle | `transfer_to_remote(session_id)` | Remote | Send queue to remote session |
| Local | `transfer_to_remote(session_id)` | Remote | Stop local, send queue to remote, enable remote volume (Android) |
| Local | `stop()` | Idle | Stop local playback |
| Remote | `transfer_to_local()` | Local | Get remote state, stop remote, start local at same position, disable remote volume |
| Remote | `stop()` | Idle | Stop remote playback, disable remote volume |
| Remote | `session_disconnected()` | Idle | Session lost, disable remote volume |
**Integration with Player State Machine:**
- When `PlaybackMode = Local`: Player state machine is active (Idle/Loading/Playing/Paused/etc.)
- When `PlaybackMode = Remote`: Player state is typically Idle (remote session controls playback)
- When `PlaybackMode = Idle`: Player state is Idle
**Android Volume Control Integration:**
When transitioning to `Remote` mode on Android:
1. Call `enable_remote_volume(initial_volume)`
2. VolumeProviderCompat intercepts hardware volume buttons
3. PlaybackStateCompat is set to STATE_PLAYING (shows volume UI)
4. Volume commands routed to remote session via Jellyfin API
When transitioning away from `Remote` mode:
1. Call `disable_remote_volume()`
2. Volume buttons return to controlling device volume
3. PlaybackStateCompat set to STATE_NONE
4. VolumeProviderCompat is cleared
### 2.4 Media Item & Source
**Location**: `src-tauri/src/player/media.rs`
```rust
pub struct MediaItem {
pub id: String,
pub title: String,
pub artist: Option,
pub album: Option,
pub duration: Option,
pub artwork_url: Option,
pub media_type: MediaType,
pub source: MediaSource,
}
pub enum MediaType {
Audio,
Video,
}
pub enum MediaSource {
Remote {
stream_url: String,
jellyfin_item_id: String,
},
Local {
file_path: PathBuf,
jellyfin_item_id: Option,
},
DirectUrl {
url: String,
},
}
```
The `MediaSource` enum enables:
- **Remote**: Streaming from Jellyfin server
- **Local**: Downloaded/cached files (future offline support)
- **DirectUrl**: Direct URLs (channel plugins, external sources)
### 2.5 Queue Manager
**Location**: `src-tauri/src/player/queue.rs`
```rust
pub struct QueueManager {
items: Vec,
current_index: Option,
shuffle: bool,
repeat: RepeatMode,
shuffle_order: Vec, // Fisher-Yates permutation
history: Vec, // For back navigation in shuffle
}
pub enum RepeatMode {
Off,
All,
One,
}
```
**Queue Navigation Logic:**
```mermaid
flowchart TB
QM[QueueManager]
QM --> Shuffle
QM --> Repeat
QM --> History
subgraph Shuffle["Shuffle Mode"]
ShuffleOff["OFF
next() returns index + 1"]
ShuffleOn["ON
next() follows shuffle_order[]"]
end
subgraph Repeat["Repeat Mode"]
RepeatOff["OFF
next() at end: → None"]
RepeatAll["ALL
next() at end: → wrap to index 0"]
RepeatOne["ONE
next() returns same item"]
end
subgraph History["History"]
HistoryDesc["Used for previous()
in shuffle mode"]
end
```
### 2.6 Favorites System
**Location**:
- Service: `src/lib/services/favorites.ts`
- Component: `src/lib/components/FavoriteButton.svelte`
- Backend: `src-tauri/src/commands/storage.rs`
**Architecture**:
The favorites system implements optimistic updates with server synchronization to provide instant UI feedback while maintaining data consistency with the Jellyfin server.
```mermaid
flowchart TB
UI[FavoriteButton] -->|Click| Service[toggleFavorite]
Service -->|1. Optimistic| LocalDB[(SQLite user_data)]
Service -->|2. Sync| JellyfinAPI[Jellyfin API]
Service -->|3. Mark Synced| LocalDB
JellyfinAPI -->|POST| MarkFav["/Users/{id}/FavoriteItems/{itemId}"]
JellyfinAPI -->|DELETE| UnmarkFav["/Users/{id}/FavoriteItems/{itemId}"]
LocalDB -->|is_favorite
pending_sync| UserData[user_data table]
```
**Flow**:
1. User clicks heart button in UI (MiniPlayer, AudioPlayer, or detail pages)
2. `toggleFavorite()` service function handles the logic:
- Updates local SQLite database immediately (optimistic update)
- 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**:
- **FavoriteButton.svelte**: Reusable heart button component
- Configurable size (sm/md/lg)
- Red when favorited, gray when not
- Loading state during toggle
- Bindable `isFavorite` prop for two-way binding
- **Integration Points**:
- MiniPlayer: Shows favorite button for audio tracks (hidden on small screens)
- Full AudioPlayer: Shows favorite button (planned)
- Album/Artist detail pages: Shows favorite button (planned)
**Database Schema**:
- `user_data.is_favorite`: Boolean flag (stored as INTEGER 0/1)
- `user_data.pending_sync`: Indicates if local changes need syncing
**Tauri Commands**:
- `storage_toggle_favorite`: Updates favorite status in local database
- `storage_mark_synced`: Clears pending_sync flag after successful sync
**API Methods**:
- `LibraryApi.markFavorite(itemId)`: POST to Jellyfin
- `LibraryApi.unmarkFavorite(itemId)`: DELETE from Jellyfin
### 2.7 Player Backend Trait
**Location**: `src-tauri/src/player/backend.rs`
```rust
pub trait PlayerBackend: Send + Sync {
fn load(&mut self, media: &MediaItem) -> Result<(), PlayerError>;
fn play(&mut self) -> Result<(), PlayerError>;
fn pause(&mut self) -> Result<(), PlayerError>;
fn stop(&mut self) -> Result<(), PlayerError>;
fn seek(&mut self, position: f64) -> Result<(), PlayerError>;
fn set_volume(&mut self, volume: f32) -> Result<(), PlayerError>;
fn position(&self) -> f64;
fn duration(&self) -> Option;
fn state(&self) -> PlayerState;
fn is_loaded(&self) -> bool;
fn volume(&self) -> f32;
}
```
**Implementations:**
- `NullBackend` - Mock backend for testing
- `MpvBackend` - Linux playback via libmpv (see Section 7.1)
- `ExoPlayerBackend` - Android playback via ExoPlayer/Media3 (see Section 7.2)
### 2.8 Player Controller
**Location**: `src-tauri/src/player/mod.rs`
The `PlayerController` orchestrates playback:
```rust
pub struct PlayerController {
backend: Arc>>,
queue: Arc>,
volume: f32,
muted: bool,
}
```
**Key Methods:**
- `play_item(item)`: Load and play single item
- `play_queue(items, start_index)`: Load queue and start playback
- `next()` / `previous()`: Queue navigation
- `toggle_shuffle()` / `cycle_repeat()`: Mode changes
### 2.9 Tauri Commands
**Location**: `src-tauri/src/commands/player.rs`
| Command | Parameters | Returns |
|---------|------------|---------|
| `player_play_item` | `PlayItemRequest` | `PlayerStatus` |
| `player_play_queue` | `items, start_index, shuffle` | `PlayerStatus` |
| `player_play` | - | `PlayerStatus` |
| `player_pause` | - | `PlayerStatus` |
| `player_toggle` | - | `PlayerStatus` |
| `player_stop` | - | `PlayerStatus` |
| `player_next` | - | `PlayerStatus` |
| `player_previous` | - | `PlayerStatus` |
| `player_seek` | `position: f64` | `PlayerStatus` |
| `player_set_volume` | `volume: f32` | `PlayerStatus` |
| `player_toggle_shuffle` | - | `QueueStatus` |
| `player_cycle_repeat` | - | `QueueStatus` |
| `player_get_status` | - | `PlayerStatus` |
| `player_get_queue` | - | `QueueStatus` |
| `player_get_session` | - | `MediaSessionType` |
| `player_dismiss_session` | - | `()` |
---
## 3. Svelte Frontend Architecture
### 3.1 Store Structure
**Location**: `src/lib/stores/`
```mermaid
flowchart TB
subgraph Stores
subgraph auth["auth.ts"]
AuthState["AuthState
- user
- serverUrl
- token
- isLoading"]
end
subgraph playerStore["player.ts"]
PlayerStoreState["PlayerState
- kind
- media
- position
- duration"]
end
subgraph queueStore["queue.ts"]
QueueState["QueueState
- items
- index
- shuffle
- repeat"]
end
subgraph libraryStore["library.ts"]
LibraryState["LibraryState
- libraries
- items
- loading"]
end
subgraph Derived["Derived Stores"]
DerivedList["isAuthenticated, currentUser
isPlaying, isPaused, currentMedia
hasNext, hasPrevious, isShuffle
libraryItems, isLibraryLoading"]
end
end
```
### 3.2 Music Library Architecture
**Category-Based Navigation:**
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)
```
**View Enforcement:**
| Content Type | View Mode | Toggle Visible | Component Used |
|--------------|-----------|----------------|----------------|
| Tracks | List (forced) | No | `TrackList` |
| Artists | Grid (forced) | No | `LibraryGrid` with `forceGrid={true}` |
| Albums | Grid (forced) | No | `LibraryGrid` with `forceGrid={true}` |
| Playlists | Grid (forced) | No | `LibraryGrid` with `forceGrid={true}` |
| Genres | Grid (both levels) | No | `LibraryGrid` with `forceGrid={true}` |
| Album Detail Tracks | List (forced) | No | `TrackList` |
**TrackList Component:**
The `TrackList` component (`src/lib/components/library/TrackList.svelte`) is a dedicated component for displaying songs in list format:
- **No Thumbnails**: Track numbers only (transform to play button on hover)
- **Desktop Layout**: Table with columns: #, Title, Artist, Album, Duration
- **Mobile Layout**: Compact rows with track number and metadata
- **Configurable Columns**: `showArtist` and `showAlbum` props control column visibility
- **Click Behavior**: Clicking a track plays it and queues all filtered tracks
**Example Usage:**
```svelte
```
**LibraryGrid forceGrid Prop:**
The `forceGrid` prop prevents the grid/list view toggle from appearing and forces grid view regardless of user preference. This ensures visual content (artists, albums, playlists) is always displayed as cards with artwork.
### 3.3 Playback Reporting Service
**Location**: `src/lib/services/playbackReporting.ts`
The playback reporting service ensures playback progress is synced to both the Jellyfin server AND the local SQLite database. This dual-write approach enables:
- Offline "Continue Watching" functionality
- Sync queue for when network is unavailable
- Consistent progress across app restarts
```mermaid
sequenceDiagram
participant VideoPlayer
participant PlaybackService as playbackReporting.ts
participant LocalDB as Local SQLite
(Tauri Commands)
participant Jellyfin as Jellyfin Server
VideoPlayer->>PlaybackService: reportPlaybackProgress(itemId, position)
par Local Storage (always works)
PlaybackService->>LocalDB: invoke("storage_update_playback_progress")
LocalDB-->>PlaybackService: Ok (pending_sync = true)
and Server Sync (if online)
PlaybackService->>Jellyfin: POST /Sessions/Playing/Progress
Jellyfin-->>PlaybackService: Ok
PlaybackService->>LocalDB: invoke("storage_mark_synced")
end
```
**Service Functions:**
- `reportPlaybackStart(itemId, positionSeconds)` - Called when playback begins
- `reportPlaybackProgress(itemId, positionSeconds, isPaused)` - Called periodically (every 10s)
- `reportPlaybackStopped(itemId, positionSeconds)` - Called when player closes or video ends
**Tauri Commands:**
| Command | Description |
|---------|-------------|
| `storage_update_playback_progress` | Update position in local DB (marks `pending_sync = true`) |
| `storage_mark_played` | Mark item as played, increment play count |
| `storage_get_playback_progress` | Get stored progress for an item |
| `storage_mark_synced` | Clear `pending_sync` flag after successful server sync |
**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
**Resume Playback Feature:**
- When loading media for playback, the app checks local database for saved progress
- If progress exists (>30 seconds watched and <90% complete), shows resume dialog
- User can choose to "Resume" from saved position or "Start from Beginning"
- For video: Uses `startTimeSeconds` parameter in stream URL to begin transcoding from resume point
- For audio: Seeks to resume position after loading via MPV backend
- Implemented in [src/routes/player/[id]/+page.svelte](src/routes/player/[id]/+page.svelte:54-167)
### 3.4 Repository Architecture (Rust-Based)
**Location**: `src-tauri/src/repository/`
```mermaid
classDiagram
class MediaRepository {
<>
+get_libraries()
+get_items(parent_id, options)
+get_item(item_id)
+search(query, options)
+get_latest_items(parent_id, limit)
+get_resume_items(parent_id, limit)
+get_next_up_episodes(series_id, limit)
+get_genres(parent_id)
+get_playback_info(item_id)
+report_playback_start(item_id, position_ticks)
+report_playback_progress(item_id, position_ticks, is_paused)
+report_playback_stopped(item_id, position_ticks)
+mark_favorite(item_id)
+unmark_favorite(item_id)
+get_person(person_id)
+get_items_by_person(person_id, options)
+get_image_url(item_id, image_type, options)
}
class OnlineRepository {
-http_client: Arc~HttpClient~
-server_url: String
-user_id: String
-access_token: String
+new()
}
class OfflineRepository {
-db_service: Arc~DatabaseService~
-server_id: String
-user_id: String
+new()
+cache_library()
+cache_items()
+cache_item()
}
class HybridRepository {
-online: Arc~OnlineRepository~
-offline: Arc~OfflineRepository~
-connectivity: Arc~ConnectivityMonitor~
+new()
-parallel_query()
-has_meaningful_content()
}
MediaRepository <|.. OnlineRepository
MediaRepository <|.. OfflineRepository
MediaRepository <|.. HybridRepository
HybridRepository --> OnlineRepository
HybridRepository --> OfflineRepository
```
**Key Implementation Details:**
1. **Cache-First Racing Strategy** (`hybrid.rs`):
- Runs cache (SQLite) and server (HTTP) queries in parallel
- Cache has 100ms timeout
- Returns cache result if it has meaningful content
- Falls back to server result otherwise
- Background cache updates planned
2. **Handle-Based Resource Management** (`repository.rs` commands):
```rust
// Frontend creates repository with UUID handle
repository_create(server_url, user_id, access_token, server_id) -> String (UUID)
// All operations use handle for identification
repository_get_libraries(handle: String) -> Vec
repository_get_items(handle: String, ...) -> SearchResult
// Cleanup when done
repository_destroy(handle: String)
```
- Enables multiple concurrent repository instances
- Thread-safe with `Arc>>>`
- No global state conflicts
3. **Frontend API Layer** (`src/lib/api/repository-client.ts`):
- Thin TypeScript wrapper over Rust commands
- Maintains handle throughout session
- All methods: `invoke("repository_operation", { handle, ...args })`
- ~100 lines (down from 1061 lines)
### 3.5 Playback Mode System
**Location**: `src-tauri/src/playback_mode/mod.rs`
The playback mode system manages transitions between local device playback and remote Jellyfin session control:
```rust
pub enum PlaybackMode {
Local, // Playing on local device
Remote { session_id: String }, // Controlling remote session
Idle, // Not playing
}
pub struct PlaybackModeManager {
current_mode: PlaybackMode,
player_controller: Arc>,
jellyfin_client: Arc,
}
```
**Key Operations:**
1. **Transfer to Remote** (`transfer_to_remote(session_id)`):
```mermaid
sequenceDiagram
participant UI
participant Manager as PlaybackModeManager
participant Player as PlayerController
participant Jellyfin as Jellyfin API
UI->>Manager: transfer_to_remote(session_id)
Manager->>Player: Extract queue items
Manager->>Manager: Get Jellyfin IDs from queue
Manager->>Jellyfin: POST /Sessions/{id}/Playing
Note over Jellyfin: Start playback with queue
Manager->>Jellyfin: POST /Sessions/{id}/Playing/Seek
Note over Jellyfin: Seek to current position
Manager->>Player: Stop local playback
Manager->>Manager: Set mode to Remote
```
2. **Transfer to Local** (`transfer_to_local(item_id, position_ticks)`):
- Stops remote session playback
- Prepares local player to resume
- Sets mode to Local
**Tauri Commands** (`playback_mode.rs`):
- `playback_mode_get_current()` → Returns current PlaybackMode
- `playback_mode_transfer_to_remote(session_id)` → Async transfer
- `playback_mode_transfer_to_local(item_id, position_ticks)` → Async transfer back
- `playback_mode_is_transferring()` → Check transfer state
- `playback_mode_set(mode)` → Direct mode setting
**Frontend Store** (`src/lib/stores/playbackMode.ts`):
- Thin wrapper calling Rust commands
- Maintains UI state (isTransferring, transferError)
- Listens to mode change events from Rust
### 3.6 Database Service Abstraction
**Location**: `src-tauri/src/storage/db_service.rs`
Async database interface wrapping synchronous `rusqlite` to prevent blocking the Tokio runtime:
```rust
#[async_trait]
pub trait DatabaseService: Send + Sync {
async fn execute(&self, query: Query) -> Result;
async fn execute_batch(&self, queries: Vec) -> Result<(), DatabaseError>;
async fn query_one(&self, query: Query, mapper: F) -> Result
where F: FnOnce(&Row) -> Result + Send + 'static;
async fn query_optional(&self, query: Query, mapper: F) -> Result