Files
jellytau/src/lib/api/bindings.ts
T
dtourolle 68ca1d585d chore: regenerate bindings and the traceability matrix
bindings.ts picks up the library-exclusion commands and types from tauri-specta.
The matrix regenerates because validation.ts and its test are gone — the doc
link checker caught the stale references, which is the first time that gate has
paid for itself on a generated artifact rather than a hand-written link.

Also drops exclusions::is_excluded: a wrapper over is_excluded_by that only a
test called, while the trait impls hoist the snapshot themselves. The test now
calls the same path production does.
2026-08-20 20:14:15 +02:00

3327 lines
122 KiB
TypeScript

// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
/** user-defined commands **/
export const commands = {
/**
* Play a single media item (audio or video)
*
* Accepts a PlayItemRequest with all optional fields properly defaulted.
* This avoids Tauri's Android serialization issues with complex objects.
*
* @req: UR-003 - Play videos
* @req: UR-004 - Play audio uninterrupted
* @req: UR-005 - Control media playback (play operation)
* @req: DR-009 - Audio player UI
*/
async playerPlayItem(item: PlayItemRequest) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_play_item", { item });
},
/**
* Enter background-audio mode: hand playback of the currently-watched video off
* to the native ExoPlayer *audio* path so the audio keeps playing while the app
* is backgrounded/locked, with no client-side video decode (UR-040).
*
* `stream_url` MUST be an audio-only URL (see
* `get_audio_only_stream_url_for_video`). The item is created as
* `MediaType::Audio` so it starts an audio session and loads into the native
* backend with `mediaType="audio"` — the WebView `<video>` is torn down on the
* frontend side, so exactly one audio source is ever active.
*
* This deliberately goes through the queue-based `play_item` path (NOT a
* side-channel) so end-of-track lands in `on_playback_ended`, which already
* honors the sleep timer (Time/Episodes/EndOfTrack) and drives autoplay-next.
* The sleep-timer state is intentionally left untouched by the handoff.
*
* TRACES: UR-040 | DR-052 | UT-061, IT-013
*/
async playerEnterBackgroundAudio(item: PlayItemRequest, positionSeconds: number) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_enter_background_audio", { item, positionSeconds });
},
/**
* Exit background-audio mode: stop the native audio player and return its final
* position so the frontend can reload the WebView `<video>` there (UR-040).
*
* Returns the position in seconds. The sleep timer is intentionally left
* untouched — if it fired while backgrounded, playback is already stopped and
* this simply reports the last position.
*
* TRACES: UR-040 | DR-052 | UT-061, IT-013
*/
async playerExitBackgroundAudio() : Promise<number> {
return await TAURI_INVOKE("player_exit_background_audio");
},
/**
* Play a queue of media items
*
* @req: UR-004 - Play audio uninterrupted
* @req: UR-005 - Control media playback (queue playback)
* @req: UR-015 - View and manage current audio queue
* @req: DR-005 - Queue manager with shuffle, repeat, history
*/
async playerPlayQueue(request: PlayQueueRequest) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_play_queue", { request });
},
/**
* Play a track from an album - backend fetches all album tracks and builds queue
*/
async playerPlayAlbumTrack(repositoryHandle: string, request: PlayAlbumTrackRequest) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_play_album_track", { repositoryHandle, request });
},
/**
* Play tracks by ID - backend fetches all metadata
*/
async playerPlayTracks(repositoryHandle: string, request: PlayTracksRequest) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_play_tracks", { repositoryHandle, request });
},
async playerPlay() : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_play");
},
async playerPause() : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_pause");
},
async playerToggle() : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_toggle");
},
async playerStop() : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_stop");
},
async playerNext() : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_next");
},
async playerPrevious() : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_previous");
},
async playerSeek(position: number) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_seek", { position });
},
/**
* Smart video seeking that decides between native and server-side seeking
*
* This command analyzes the current video stream and automatically chooses
* the best seeking strategy:
* - HLS streams (.m3u8): Use native seeking
* - Direct play streams: Use native seeking
* - Transcoded non-HLS: Request new stream URL from server starting at seek position
*
* For native (non-HTML5) backends, this command handles the entire stream reload
* internally. For HTML5 backends, it returns the new URL for the frontend to handle.
*/
async playerSeekVideo(repositoryHandle: string, position: number, mediaSourceId: string | null, audioStreamIndex: number | null, useHtml5: boolean) : Promise<VideoSeekResponse> {
return await TAURI_INVOKE("player_seek_video", { repositoryHandle, position, mediaSourceId, audioStreamIndex, useHtml5 });
},
async playerSetVolume(volume: number) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_set_volume", { volume });
},
async playerToggleMute() : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_toggle_mute");
},
/**
* Set the active audio track on a native backend directly.
*
* TRACES: UR-021 | IR-019, DR-024
*/
async playerSetAudioTrack(streamIndex: number) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_set_audio_track", { streamIndex });
},
/**
* Switch audio track - handles both HTML5 (stream reload) and native (direct switch)
* Note: Frontend should handle saving series preferences after this command succeeds
*
* The split is the requirement: an HTML5 `<video>` element cannot be told to
* change audio track, so the stream is re-opened at the chosen
* `AudioStreamIndex` and the frontend seeks the reloaded element back to
* `position`; a native backend (ExoPlayer) switches in place by track-group
* index. libmpv implements neither — it is the audio-only backend here and
* leaves `PlayerBackend::set_audio_track` at its `not_implemented()` default,
* which is why IR-019 is met by these two paths rather than by MPV.
*
* TRACES: UR-021 | IR-019, DR-024
*/
async playerSwitchAudioTrack(repositoryHandle: string, streamIndex: number, arrayIndex: number, useHtml5: boolean, currentPosition: number | null, mediaSourceId: string | null) : Promise<AudioTrackSwitchResponse> {
return await TAURI_INVOKE("player_switch_audio_track", { repositoryHandle, streamIndex, arrayIndex, useHtml5, currentPosition, mediaSourceId });
},
/**
* Set (or clear, with `None`) the active subtitle track on a native backend.
*
* On Android this indexes ExoPlayer's *text track groups* — i.e. the position
* of the sideloaded `MediaItem.SubtitleConfiguration`, not the Jellyfin stream
* index. The HTML5 path never reaches here; it toggles its own `<track>`
* children. libmpv implements neither, leaving the trait default in place.
*
* TRACES: UR-020 | IR-018, DR-023
*/
async playerSetSubtitleTrack(streamIndex: number | null) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_set_subtitle_track", { streamIndex });
},
async playerToggleShuffle() : Promise<QueueStatus> {
return await TAURI_INVOKE("player_toggle_shuffle");
},
async playerCycleRepeat() : Promise<QueueStatus> {
return await TAURI_INVOKE("player_cycle_repeat");
},
async playerGetStatus() : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_get_status");
},
async playerGetQueue() : Promise<QueueStatus> {
return await TAURI_INVOKE("player_get_queue");
},
/**
* Report this platform's playback capabilities to the frontend.
*
* TRACES: UR-003, UR-005 | DR-004, DR-023, DR-024
*/
async playerGetCapabilities() : Promise<PlaybackCapabilities> {
return await TAURI_INVOKE("player_get_capabilities");
},
async playerAddToQueue(request: AddToQueueRequest) : Promise<QueueStatus> {
return await TAURI_INVOKE("player_add_to_queue", { request });
},
/**
* Add a track to queue by ID - backend fetches metadata and constructs URLs
*/
async playerAddTrackById(repositoryHandle: string, request: AddTrackByIdRequest) : Promise<QueueStatus> {
return await TAURI_INVOKE("player_add_track_by_id", { repositoryHandle, request });
},
/**
* Add multiple tracks to queue by IDs - backend fetches metadata and constructs URLs
*/
async playerAddTracksByIds(repositoryHandle: string, request: AddTracksByIdsRequest) : Promise<QueueStatus> {
return await TAURI_INVOKE("player_add_tracks_by_ids", { repositoryHandle, request });
},
async playerRemoveFromQueue(index: number) : Promise<QueueStatus> {
return await TAURI_INVOKE("player_remove_from_queue", { index });
},
async playerMoveInQueue(fromIndex: number, toIndex: number) : Promise<QueueStatus> {
return await TAURI_INVOKE("player_move_in_queue", { fromIndex, toIndex });
},
async playerSkipTo(index: number) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_skip_to", { index });
},
async playerSetAudioSettings(settings: AudioSettings) : Promise<AudioSettings> {
return await TAURI_INVOKE("player_set_audio_settings", { settings });
},
async playerGetAudioSettings() : Promise<AudioSettings> {
return await TAURI_INVOKE("player_get_audio_settings");
},
/**
* The built-in equalizer presets and their per-band gain curves (dB), for the
* settings UI. The curve numbers are domain data defined by the band layout,
* so the frontend reads them here rather than encoding them.
*
* TRACES: UR-027 | DR-030
*/
async playerGetEqPresets() : Promise<([EqPreset, number[]])[]> {
return await TAURI_INVOKE("player_get_eq_presets");
},
async playerSetVideoSettings(settings: VideoSettings) : Promise<VideoSettings> {
return await TAURI_INVOKE("player_set_video_settings", { settings });
},
async playerGetVideoSettings() : Promise<VideoSettings> {
return await TAURI_INVOKE("player_get_video_settings");
},
/**
* The bandwidth ceilings the quality picker may offer, each with the label and
* one-line detail to show for it, highest first.
*
* The ladder and its numbers are Jellyfin encoding domain vocabulary, so the
* frontend reads them here rather than encoding them — the same arrangement as
* [`player_get_eq_presets`].
*
* TRACES: UR-074 | DR-162
*/
async playerGetStreamingQualities() : Promise<([StreamingQuality, string, string])[]> {
return await TAURI_INVOKE("player_get_streaming_qualities");
},
/**
* Change the bandwidth ceiling of the video that is playing *right now*.
*
* A cap is a property of the stream the server is producing, so unlike a volume
* change it cannot be applied to a stream already in flight — the stream has to
* be re-opened at the new quality and resumed at the current position. That is
* the same reload the transcoded-seek and audio-track paths use, and the same
* two-sided split: HTML5 gets the URL back and reloads its own element, while a
* native backend is reloaded here.
*
* The change applies to this playback *and* to everything started afterwards
* (it sets the process-wide ceiling), but it is deliberately **not** persisted:
* the in-player picker is a "this film, this connection" control, and the
* durable default belongs to Settings. `player_set_video_settings` is the one
* that writes to the database.
*
* TRACES: UR-074 | DR-162
*/
async playerSetStreamQuality(repositoryHandle: string, quality: StreamingQuality, useHtml5: boolean, currentPosition: number | null, mediaSourceId: string | null, audioStreamIndex: number | null) : Promise<StreamQualityResponse> {
return await TAURI_INVOKE("player_set_stream_quality", { repositoryHandle, quality, useHtml5, currentPosition, mediaSourceId, audioStreamIndex });
},
/**
* Set sleep timer mode
*/
async playerSetSleepTimer(mode: SleepTimerMode) : Promise<SleepTimerState> {
return await TAURI_INVOKE("player_set_sleep_timer", { mode });
},
/**
* Cancel sleep timer
*/
async playerCancelSleepTimer() : Promise<SleepTimerState> {
return await TAURI_INVOKE("player_cancel_sleep_timer");
},
/**
* Get current sleep timer state
*/
async playerGetSleepTimer() : Promise<SleepTimerState> {
return await TAURI_INVOKE("player_get_sleep_timer");
},
/**
* Get autoplay settings
*/
async playerGetAutoplaySettings() : Promise<AutoplaySettings> {
return await TAURI_INVOKE("player_get_autoplay_settings");
},
/**
* Set autoplay settings and persist to database
*/
async playerSetAutoplaySettings(userId: string, settings: AutoplaySettings) : Promise<AutoplaySettings> {
return await TAURI_INVOKE("player_set_autoplay_settings", { userId, settings });
},
/**
* Cancel active autoplay countdown
*/
async playerCancelAutoplayCountdown() : Promise<null> {
return await TAURI_INVOKE("player_cancel_autoplay_countdown");
},
/**
* Play next episode (user confirmed from popup)
*/
async playerPlayNextEpisode(item: PlayItemRequest) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_play_next_episode", { item });
},
/**
* Handle playback ended event - triggers autoplay decision logic
* This is called from:
* - Frontend when HTML5 video ends (Linux/desktop) - passes itemId + repositoryHandle for the video
* - Frontend when audio track ends via backend event - no itemId/repositoryHandle needed
* - Android JNI callback also triggers this logic directly
*
* TRACES: UR-023, UR-026, UR-040 | DR-047, DR-052, DR-129
*/
async playerOnPlaybackEnded(itemId: string | null, repositoryHandle: string | null) : Promise<null> {
return await TAURI_INVOKE("player_on_playback_ended", { itemId, repositoryHandle });
},
/**
* Try to recover playback after a **recoverable** player error, reporting
* whether it was handled.
*
* The frontend's error handler stops the player, which is right for a real
* failure and wrong for a network blip — it turned every hiccup into "playback
* died". This is the echo path for backends that cannot decide in-process:
* MpvBackend is constructed before `PlayerController` exists ([`lib.rs`]), so
* its event thread has no controller to ask. It emits the error, the frontend
* echoes it here, and the decision stays in Rust — the same shape as
* `PlaybackEnded` → `player_on_playback_ended`.
*
* Returns `true` when the stream was re-opened and the caller must NOT stop the
* player; `false` when the error is real and should be surfaced as before.
* Android decides inside its JNI callback and only emits errors it has already
* declined to recover, so this reports `false` for those without a second
* opinion — the shared attempt budget is spent by then either way.
*
* TRACES: UR-004, UR-040 | DR-130 | UT-117
*/
async playerRecoverStream() : Promise<boolean> {
return await TAURI_INVOKE("player_recover_stream");
},
/**
* Report an HTML5 <video> state change (playing/paused/loading/stopped/idle).
*/
async playerReportState(state: string, mediaId: string | null) : Promise<null> {
return await TAURI_INVOKE("player_report_state", { state, mediaId });
},
/**
* Report an HTML5 <video> position tick (seconds). The adapter should throttle
* these to roughly match the native backends' ~250ms cadence.
*/
async playerReportPosition(position: number, duration: number) : Promise<null> {
return await TAURI_INVOKE("player_report_position", { position, duration });
},
/**
* Report that the HTML5 <video> finished loading and knows its duration.
*/
async playerReportMediaLoaded(duration: number) : Promise<null> {
return await TAURI_INVOKE("player_report_media_loaded", { duration });
},
/**
* The on-disk path for a downloaded item, for playback surfaces that resolve
* their own source rather than going through the queue.
*
* The video player is the reason this exists: audio has preferred local files
* since queue construction, but video asks the repository for a stream URL and
* never consults `downloads`, so a downloaded film was still streamed — costing
* bandwidth that had already been spent and failing outright when offline.
*
* Returns `None` when nothing is downloaded *or* the file is missing, so the
* caller falls back to streaming.
*
* TRACES: UR-071 | DR-123 | UT-116
*/
async playerLocalMediaPath(itemId: string) : Promise<string | null> {
return await TAURI_INVOKE("player_local_media_path", { itemId });
},
/**
* Preload upcoming tracks from the queue
* This queues background downloads for the next N tracks that aren't already downloaded
*/
async playerPreloadUpcoming(userId: string, downloadBasePath: string) : Promise<PreloadResult> {
return await TAURI_INVOKE("player_preload_upcoming", { userId, downloadBasePath });
},
/**
* Update SmartCache configuration
*/
async playerSetCacheConfig(config: CacheConfig) : Promise<null> {
return await TAURI_INVOKE("player_set_cache_config", { config });
},
/**
* Get current SmartCache configuration
*/
async playerGetCacheConfig() : Promise<CacheConfig> {
return await TAURI_INVOKE("player_get_cache_config");
},
/**
* Configure Jellyfin API client for automatic playback reporting
*/
async playerConfigureJellyfin(serverUrl: string, accessToken: string, userId: string, deviceId: string) : Promise<null> {
return await TAURI_INVOKE("player_configure_jellyfin", { serverUrl, accessToken, userId, deviceId });
},
/**
* Disable Jellyfin automatic playback reporting
*/
async playerDisableJellyfin() : Promise<null> {
return await TAURI_INVOKE("player_disable_jellyfin");
},
/**
* Get the current media session state
*/
async playerGetSession() : Promise<MediaSessionType> {
return await TAURI_INVOKE("player_get_session");
},
/**
* Dismiss the current media session (returns to Idle)
*/
async playerDismissSession() : Promise<null> {
return await TAURI_INVOKE("player_dismiss_session");
},
/**
* Play items on a remote Jellyfin session (casting)
*/
async remotePlayOnSession(sessionId: string, itemIds: string[], startIndex: number) : Promise<null> {
return await TAURI_INVOKE("remote_play_on_session", { sessionId, itemIds, startIndex });
},
/**
* Send a playback command to a remote session
*/
async remoteSendCommand(sessionId: string, command: string) : Promise<null> {
return await TAURI_INVOKE("remote_send_command", { sessionId, command });
},
/**
* Seek on a remote session
*/
async remoteSessionSeek(sessionId: string, positionTicks: number) : Promise<null> {
return await TAURI_INVOKE("remote_session_seek", { sessionId, positionTicks });
},
/**
* Set volume on a remote session
*/
async remoteSessionSetVolume(sessionId: string, volume: number) : Promise<null> {
return await TAURI_INVOKE("remote_session_set_volume", { sessionId, volume });
},
/**
* Toggle mute on a remote session
*/
async remoteSessionToggleMute(sessionId: string) : Promise<null> {
return await TAURI_INVOKE("remote_session_toggle_mute", { sessionId });
},
/**
* List current LMS sync groups.
*/
async lmsGetSyncGroups() : Promise<LmsSyncGroup[]> {
return await TAURI_INVOKE("lms_get_sync_groups");
},
/**
* Fuse LMS zones into a sync group. `master_mac` keeps playing and the
* `slave_macs` zones join it in sync.
*/
async lmsCreateSyncGroup(masterMac: string, slaveMacs: string[]) : Promise<null> {
return await TAURI_INVOKE("lms_create_sync_group", { masterMac, slaveMacs });
},
/**
* Remove a single LMS zone from its sync group (decouple one player).
*/
async lmsUnsyncPlayer(mac: string) : Promise<null> {
return await TAURI_INVOKE("lms_unsync_player", { mac });
},
/**
* Dissolve an entire LMS sync group, identified by its master's MAC.
*/
async lmsDissolveSyncGroup(masterMac: string) : Promise<null> {
return await TAURI_INVOKE("lms_dissolve_sync_group", { masterMac });
},
/**
* Set polling frequency hint based on UI state
*/
async sessionsSetPollingHint(hint: string) : Promise<null> {
return await TAURI_INVOKE("sessions_set_polling_hint", { hint });
},
/**
* Manually trigger a session poll (for refresh button)
*/
async sessionsPollNow() : Promise<SessionInfo[]> {
return await TAURI_INVOKE("sessions_poll_now");
},
/**
* Get the current playback mode
*/
async playbackModeGetCurrent() : Promise<PlaybackMode> {
return await TAURI_INVOKE("playback_mode_get_current");
},
/**
* Set the playback mode (internal/testing use)
*/
async playbackModeSet(mode: PlaybackMode) : Promise<null> {
return await TAURI_INVOKE("playback_mode_set", { mode });
},
/**
* Check if currently transferring between playback modes
*/
async playbackModeIsTransferring() : Promise<boolean> {
return await TAURI_INVOKE("playback_mode_is_transferring");
},
/**
* Transfer playback from local device to a remote Jellyfin session
*/
async playbackModeTransferToRemote(sessionId: string, position: number | null) : Promise<null> {
return await TAURI_INVOKE("playback_mode_transfer_to_remote", { sessionId, position });
},
/**
* Get remote session status (for polling position/duration)
*/
async playbackModeGetRemoteStatus() : Promise<RemoteSessionStatus> {
return await TAURI_INVOKE("playback_mode_get_remote_status");
},
/**
* Transfer playback from remote session back to local device
*
* Parameters:
* - current_item_id: The Jellyfin item ID currently playing on remote
* - position_ticks: Current playback position in ticks (10,000 ticks = 1ms)
*/
async playbackModeTransferToLocal(currentItemId: string, positionTicks: number) : Promise<null> {
return await TAURI_INVOKE("playback_mode_transfer_to_local", { currentItemId, positionTicks });
},
/**
* Set the transferring flag on the playback mode manager.
*
* Used by the frontend remote->local flow to mark the whole two-step sequence
* as a transfer, so `player_play_tracks` starts LOCAL playback instead of
* casting back to the remote session it's leaving. Always pair `true` with a
* later `false` (including on error) so the flag can't stick.
*/
async playbackModeSetTransferring(transferring: boolean) : Promise<null> {
return await TAURI_INVOKE("playback_mode_set_transferring", { transferring });
},
/**
* Initialize playback reporter (called after login)
*/
async playbackReporterInit(serverUrl: string, userId: string, accessToken: string, deviceId: string) : Promise<null> {
return await TAURI_INVOKE("playback_reporter_init", { serverUrl, userId, accessToken, deviceId });
},
/**
* Destroy playback reporter (called on logout)
*/
async playbackReporterDestroy() : Promise<null> {
return await TAURI_INVOKE("playback_reporter_destroy");
},
/**
* Report playback start
*/
async playbackReportStart(itemId: string, positionSeconds: number, contextType: string | null, contextId: string | null) : Promise<null> {
return await TAURI_INVOKE("playback_report_start", { itemId, positionSeconds, contextType, contextId });
},
/**
* Report playback progress
*/
async playbackReportProgress(itemId: string, positionSeconds: number, isPaused: boolean) : Promise<null> {
return await TAURI_INVOKE("playback_report_progress", { itemId, positionSeconds, isPaused });
},
/**
* Report playback stopped
*/
async playbackReportStopped(itemId: string, positionSeconds: number) : Promise<null> {
return await TAURI_INVOKE("playback_report_stopped", { itemId, positionSeconds });
},
/**
* Mark item as played
*/
async playbackMarkPlayed(itemId: string) : Promise<null> {
return await TAURI_INVOKE("playback_mark_played", { itemId });
},
/**
* Initialize the auth manager (call on app startup)
* Restores session from storage if available
*/
async authInitialize() : Promise<Session | null> {
return await TAURI_INVOKE("auth_initialize");
},
/**
* Connect to a Jellyfin server and get server info
*/
async authConnectToServer(serverUrl: string) : Promise<AuthServerInfo> {
return await TAURI_INVOKE("auth_connect_to_server", { serverUrl });
},
/**
* Login with username and password
*/
async authLogin(serverUrl: string, username: string, password: string, deviceId: string) : Promise<AuthResult> {
return await TAURI_INVOKE("auth_login", { serverUrl, username, password, deviceId });
},
/**
* Verify current session
*/
async authVerifySession(serverUrl: string, userId: string, accessToken: string, deviceId: string) : Promise<boolean> {
return await TAURI_INVOKE("auth_verify_session", { serverUrl, userId, accessToken, deviceId });
},
/**
* Logout (clear session and call Jellyfin logout endpoint)
*/
async authLogout(serverUrl: string, accessToken: string, deviceId: string) : Promise<null> {
return await TAURI_INVOKE("auth_logout", { serverUrl, accessToken, deviceId });
},
/**
* Get current session
*/
async authGetSession() : Promise<Session | null> {
return await TAURI_INVOKE("auth_get_session");
},
/**
* Set current session (for restoration from storage)
*/
async authSetSession(session: Session | null) : Promise<null> {
return await TAURI_INVOKE("auth_set_session", { session });
},
/**
* Start background session verification
*/
async authStartVerification(deviceId: string) : Promise<null> {
return await TAURI_INVOKE("auth_start_verification", { deviceId });
},
/**
* Stop background session verification
*/
async authStopVerification() : Promise<null> {
return await TAURI_INVOKE("auth_stop_verification");
},
/**
* Re-authenticate with password (when session expired)
*/
async authReauthenticate(password: string, deviceId: string) : Promise<AuthResult> {
return await TAURI_INVOKE("auth_reauthenticate", { password, deviceId });
},
/**
* Get or create the device ID.
* Device ID is a UUID v4 that persists across app restarts.
* On first call, generates and stores a new UUID.
* On subsequent calls, retrieves the stored UUID.
*
* # Returns
* - `Ok(String)` - The device ID (UUID v4)
* - `Err(String)` - If database operation fails
*
* TRACES: UR-009 | DR-011
*/
async deviceGetId() : Promise<string> {
return await TAURI_INVOKE("device_get_id");
},
/**
* Set the device ID (primarily for testing or recovery).
* Overwrites any existing device ID.
*
* # Arguments
* * `device_id` - The device ID to store (should be UUID v4 format)
*
* # Returns
* - `Ok(())` - If device ID was stored successfully
* - `Err(String)` - If database operation fails
*
* TRACES: UR-009 | DR-011
*/
async deviceSetId(deviceId: string) : Promise<null> {
return await TAURI_INVOKE("device_set_id", { deviceId });
},
/**
* Check if the server is currently reachable
*/
async connectivityCheckServer() : Promise<boolean> {
return await TAURI_INVOKE("connectivity_check_server");
},
/**
* Set the server URL and trigger an immediate check
*/
async connectivitySetServerUrl(url: string) : Promise<null> {
return await TAURI_INVOKE("connectivity_set_server_url", { url });
},
/**
* Get the current connectivity status
*/
async connectivityGetStatus() : Promise<ConnectivityStatus> {
return await TAURI_INVOKE("connectivity_get_status");
},
/**
* Start monitoring connectivity with adaptive polling
*/
async connectivityStartMonitoring() : Promise<null> {
return await TAURI_INVOKE("connectivity_start_monitoring");
},
/**
* Stop monitoring connectivity
*/
async connectivityStopMonitoring() : Promise<null> {
return await TAURI_INVOKE("connectivity_stop_monitoring");
},
/**
* Mark the server as reachable (called after successful API calls)
*/
async connectivityMarkReachable() : Promise<null> {
return await TAURI_INVOKE("connectivity_mark_reachable");
},
/**
* Mark the server as unreachable (called after failed API calls)
*/
async connectivityMarkUnreachable(error: string | null) : Promise<null> {
return await TAURI_INVOKE("connectivity_mark_unreachable", { error });
},
/**
* Initialize the database and run migrations
*/
async storageInit() : Promise<string> {
return await TAURI_INVOKE("storage_init");
},
/**
* Get storage directory path (parent directory of the database file)
*/
async storageGetPath() : Promise<string> {
return await TAURI_INVOKE("storage_get_path");
},
/**
* Get database file size in bytes
*/
async storageGetSize() : Promise<number | null> {
return await TAURI_INVOKE("storage_get_size");
},
/**
* Get security status (keyring vs encrypted file fallback)
*/
async storageGetSecurityStatus() : Promise<SecurityStatus> {
return await TAURI_INVOKE("storage_get_security_status");
},
/**
* Save a server connection
* Uses INSERT ... ON CONFLICT to avoid triggering CASCADE DELETE on users
*/
async storageSaveServer(id: string, name: string, url: string, version: string | null) : Promise<null> {
return await TAURI_INVOKE("storage_save_server", { id, name, url, version });
},
/**
* Get all saved servers
*/
async storageGetServers() : Promise<ServerInfo[]> {
return await TAURI_INVOKE("storage_get_servers");
},
/**
* Delete a server and all associated data
*/
async storageDeleteServer(serverId: string) : Promise<null> {
return await TAURI_INVOKE("storage_delete_server", { serverId });
},
/**
* Save a user account (token stored in secure storage, not database)
*/
async storageSaveUser(id: string, serverId: string, username: string, accessToken: string | null) : Promise<boolean> {
return await TAURI_INVOKE("storage_save_user", { id, serverId, username, accessToken });
},
/**
* Get users for a server
*/
async storageGetUsers(serverId: string) : Promise<UserInfo[]> {
return await TAURI_INVOKE("storage_get_users", { serverId });
},
/**
* Set a user as active (and deactivate all other users globally)
*/
async storageSetActiveUser(userId: string, serverId: string) : Promise<null> {
return await TAURI_INVOKE("storage_set_active_user", { userId, serverId });
},
/**
* Get the active user for a server
*/
async storageGetActiveUser(serverId: string) : Promise<UserInfo | null> {
return await TAURI_INVOKE("storage_get_active_user", { serverId });
},
/**
* Get the active session (user + server + token) for session restoration
*/
async storageGetActiveSession() : Promise<ActiveSession | null> {
return await TAURI_INVOKE("storage_get_active_session");
},
/**
* Get user's access token from secure storage
*/
async storageGetAccessToken(userId: string) : Promise<string | null> {
return await TAURI_INVOKE("storage_get_access_token", { userId });
},
/**
* Delete a user account and their token from secure storage
*/
async storageDeleteUser(userId: string) : Promise<null> {
return await TAURI_INVOKE("storage_delete_user", { userId });
},
/**
* Update playback progress in local database
* This stores the progress locally for offline access and "continue watching"
*/
async storageUpdatePlaybackProgress(userId: string, itemId: string, positionMs: number) : Promise<null> {
return await TAURI_INVOKE("storage_update_playback_progress", { userId, itemId, positionMs });
},
/**
* Update playback progress with context in local database
* This stores the progress along with playback context (container vs single)
*/
async storageUpdatePlaybackContext(userId: string, itemId: string, positionMs: number, contextType: string | null, contextId: string | null) : Promise<null> {
return await TAURI_INVOKE("storage_update_playback_context", { userId, itemId, positionMs, contextType, contextId });
},
/**
* Mark item as played in local database
*/
async storageMarkPlayed(userId: string, itemId: string) : Promise<null> {
return await TAURI_INVOKE("storage_mark_played", { userId, itemId });
},
/**
* Set the watched flag locally for an item **and everything inside it**.
*
* This backs the watched toggle, and is deliberately separate from
* [`storage_mark_played`] — which reports a single track/episode finishing and
* increments `play_count` — because the toggle has two directions and applies
* to containers.
*
* The recursion is what makes the toggle honest offline. Jellyfin applies
* `POST`/`DELETE /PlayedItems/{id}` recursively over a season or series, so
* online the server fixes up the children on the next read; with no server to
* ask, marking a season watched would otherwise tick the season and leave every
* episode inside it unwatched. Targets are drawn from `items` by the same link
* columns the rest of the offline layer uses, so an id that is not cached
* selects nothing and the statement is a no-op rather than a foreign-key error.
*
* Un-marking clears the resume position too, matching the server, so an item
* un-marked offline does not come back offering to resume from a position it is
* no longer meant to have.
*
* `pending_sync = 1` hands the rows to the sync drain.
*
* TRACES: UR-073 | DR-158
*/
async storageSetWatched(userId: string, itemId: string, watched: boolean) : Promise<null> {
return await TAURI_INVOKE("storage_set_watched", { userId, itemId, watched });
},
/**
* Get playback progress for an item
*/
async storageGetPlaybackProgress(userId: string, itemId: string) : Promise<PlaybackProgress | null> {
return await TAURI_INVOKE("storage_get_playback_progress", { userId, itemId });
},
/**
* Mark pending sync as completed for an item
*/
async storageMarkSynced(userId: string, itemId: string) : Promise<null> {
return await TAURI_INVOKE("storage_mark_synced", { userId, itemId });
},
/**
* Toggle favorite status for an item in local database
* This updates the is_favorite field and marks it for sync to Jellyfin
*/
async storageToggleFavorite(userId: string, itemId: string, isFavorite: boolean) : Promise<boolean> {
return await TAURI_INVOKE("storage_toggle_favorite", { userId, itemId, isFavorite });
},
/**
* Queue a media item for download
*/
async downloadItem(request: DownloadItemRequest) : Promise<number> {
return await TAURI_INVOKE("download_item", { request });
},
/**
* Queue and start a download in a single atomic operation
* This simplifies the frontend flow by combining multiple steps
*/
async downloadItemAndStart(request: DownloadItemAndStartRequest) : Promise<number> {
return await TAURI_INVOKE("download_item_and_start", { request });
},
/**
* Queue an entire album for download.
*
* Owns the whole operation: the album's track list comes from the server (the
* only place that knows all of it), every track is queued and linked to its
* album, each row's stream URL is resolved here, and the queue is pumped.
*
* The frontend used to do the second half — resolve one URL per track and pair
* it with the returned ids **by position**. That pairing had no basis: the ids
* came back in the backend's own order over a different set of rows, so
* whenever the two lists disagreed a row was handed another track's URL, and
* any track past the end of the shorter list was never started at all. Nothing
* crosses the boundary now except the album id.
*
* TRACES: UR-018, UR-055 | DR-173 | UT-170
*/
async downloadAlbum(handle: string, albumId: string, userId: string, basePath: string) : Promise<number[]> {
return await TAURI_INVOKE("download_album", { handle, albumId, userId, basePath });
},
/**
* Queue a video item (movie or episode) for download with quality preset
*/
async downloadVideo(request: DownloadVideoRequest) : Promise<number> {
return await TAURI_INVOKE("download_video", { request });
},
/**
* Queue all episodes of a series for download
*/
async downloadSeries(seriesId: string, seriesName: string, userId: string, basePath: string, qualityPreset: string | null) : Promise<number[]> {
return await TAURI_INVOKE("download_series", { seriesId, seriesName, userId, basePath, qualityPreset });
},
/**
* Queue all episodes of a specific season for download
*/
async downloadSeason(seasonId: string, seriesName: string, seasonName: string, seasonNumber: number, userId: string, basePath: string, qualityPreset: string | null) : Promise<number[]> {
return await TAURI_INVOKE("download_season", { seasonId, seriesName, seasonName, seasonNumber, userId, basePath, qualityPreset });
},
/**
* Get all downloads for a user, optionally filtered by status
*/
async getDownloads(userId: string, statusFilter: string[] | null) : Promise<DownloadsResponse> {
return await TAURI_INVOKE("get_downloads", { userId, statusFilter });
},
/**
* Pause a download.
*
* Writing `status = 'paused'` is only half of it, and used to be all of it: the
* streaming task knew nothing about the row and kept running, then overwrote it
* with `completed`/`failed` when it finished. The row flicked to "paused" and
* undid itself — the reported "pause does not work". Signalling the worker is
* what actually stops the bytes; it leaves the `.part` file in place so
* [`resume_download`] can continue from it.
*
* A queued (not yet started) download has no worker to signal, and the status
* write alone is enough — the pump skips anything that is not `pending`.
*
* TRACES: UR-055 | DR-168
*/
async pauseDownload(downloadId: number) : Promise<null> {
return await TAURI_INVOKE("pause_download", { downloadId });
},
/**
* Resume a paused download.
*
* Flipping the row back to `pending` is likewise not enough on its own: the
* pump is not a poller, it runs when something calls it, so a resumed download
* sat untouched until some unrelated event happened to pump the queue. That is
* the other half of "resume does not work".
*
* TRACES: UR-055 | DR-168
*/
async resumeDownload(downloadId: number) : Promise<null> {
return await TAURI_INVOKE("resume_download", { downloadId });
},
/**
* Cancel a download
*/
async cancelDownload(downloadId: number) : Promise<null> {
return await TAURI_INVOKE("cancel_download", { downloadId });
},
/**
* Delete a completed download
*/
async deleteDownload(downloadId: number) : Promise<null> {
return await TAURI_INVOKE("delete_download", { downloadId });
},
/**
* Delete all downloads for a user
*/
async deleteAllDownloads(userId: string) : Promise<number> {
return await TAURI_INVOKE("delete_all_downloads", { userId });
},
/**
* Delete all downloads for a specific album
*/
async deleteAlbumDownloads(albumId: string, userId: string) : Promise<number> {
return await TAURI_INVOKE("delete_album_downloads", { albumId, userId });
},
/**
* Remove every completed download at or under a container item.
*
* Works at any level of the Downloaded browse: a leaf (removes just that
* download), an album/season/series (removes all downloaded descendants linked
* via album_id/season_id/series_id/parent_id). Deletes the DB rows and the
* on-disk files. Returns the number of downloads removed. Idempotent.
*
* TRACES: UR-055 | DR-083
*/
async deleteDownloadsUnder(itemId: string, userId: string) : Promise<number> {
return await TAURI_INVOKE("delete_downloads_under", { itemId, userId });
},
/**
* Clear all stale pending/failed/paused downloads
*/
async clearStaleDownloads(userId: string) : Promise<number> {
return await TAURI_INVOKE("clear_stale_downloads", { userId });
},
/**
* Get storage statistics for downloads
*/
async getDownloadStorageStats(userId: string) : Promise<StorageStats> {
return await TAURI_INVOKE("get_download_storage_stats", { userId });
},
/**
* Mark a download as completed
*/
async markDownloadCompleted(downloadId: number, bytesDownloaded: number, filePath: string) : Promise<null> {
return await TAURI_INVOKE("mark_download_completed", { downloadId, bytesDownloaded, filePath });
},
/**
* Mark a download as failed
*/
async markDownloadFailed(downloadId: number, errorMessage: string) : Promise<null> {
return await TAURI_INVOKE("mark_download_failed", { downloadId, errorMessage });
},
/**
* A playable URL for a downloaded file on disk.
*
* Local media is served over a loopback HTTP server rather than handed to the
* webview as a `file://`/asset URL, because the asset protocol cannot stream a
* large file — it answers a range-less request with the whole thing, which
* Chromium abandons. See `media_server` for why real HTTP is used.
*
* The returned URL carries the server's per-session token, so it is only valid
* for this run of the app and must not be persisted.
*
* TRACES: UR-071 | DR-137
*/
async mediaLocalUrl(path: string) : Promise<string> {
return await TAURI_INVOKE("media_local_url", { path });
},
/**
* Start downloading a file immediately
* This command actually downloads the file using the worker
*/
async startDownload(downloadId: number, streamUrl: string, targetDir: string) : Promise<null> {
return await TAURI_INVOKE("start_download", { downloadId, streamUrl, targetDir });
},
/**
* Enqueue a download with its resolved stream URL, then let the queue pump
* start it (or a higher-priority pending item) when a slot is free.
*
* Unlike [`start_download`], this never errors when the concurrency limit is
* reached: the URL is persisted on the row and the pump will pick it up once a
* slot frees. This is the path bulk operations (album/series/season) use so
* every queued item eventually downloads without the frontend re-issuing it.
*/
async enqueueDownload(downloadId: number, streamUrl: string, targetDir: string) : Promise<null> {
return await TAURI_INVOKE("enqueue_download", { downloadId, streamUrl, targetDir });
},
/**
* Enqueue a batch of already-queued video downloads, resolving each one's
* transcode URL from the repository using the `quality_preset` stored on the
* row. Then let the pump start them subject to the concurrency limit.
*
* This is the bulk video path (series/season): `download_series`/
* `download_season` insert the rows, then this resolves URLs and enqueues them
* so they actually start. Resolving server-side avoids round-tripping every
* episode URL through the frontend.
*/
async enqueueVideoDownloads(handle: string, downloadIds: number[], targetDir: string) : Promise<null> {
return await TAURI_INVOKE("enqueue_video_downloads", { handle, downloadIds, targetDir });
},
/**
* Walk every library on the server and persist all items to the offline cache
* so the full catalog is browsable offline (greyed out when not downloaded).
*
* Best-effort: a library that fails to fetch is counted and skipped rather than
* aborting the whole sync. Runs libraries sequentially to avoid hammering the
* server. Uses `Recursive=true` so a single request per library returns the
* containers and their playable children.
*/
async syncFullCatalog(handle: string) : Promise<CatalogSyncResult> {
return await TAURI_INVOKE("sync_full_catalog", { handle });
},
/**
* Report the last-synced timestamp so the UI can show a hint / decide whether
* to trigger a fresh sync.
*/
async catalogSyncStatus() : Promise<CatalogSyncStatus> {
return await TAURI_INVOKE("catalog_sync_status");
},
/**
* Control whether offline library queries reveal the full synced catalog
* (greyed-out, non-downloaded media) or only downloaded/local media.
*
* The frontend calls this from the "Show all server media" toggle: pass `true`
* when online, or when offline with the toggle on; pass `false` when offline
* with the toggle off so library pages show downloaded media only. Fixes the
* bug where offline library pages showed every server item regardless of the
* toggle.
*/
async setShowServerCatalog(show: boolean) : Promise<void> {
await TAURI_INVOKE("set_show_server_catalog", { show });
},
/**
* The library preferences currently in force.
*
* Read from the in-memory exclusion set rather than the database: that set is
* what queries actually consult, so reading it is the only answer that cannot
* disagree with what the user is seeing.
*
* TRACES: UR-076 | DR-209
*/
async libraryGetSettings() : Promise<LibrarySettings> {
return await TAURI_INVOKE("library_get_settings");
},
/**
* Replace the library preferences: apply them to every subsequent query and
* persist them.
*
* Returns the sanitised value actually applied, so the picker shows what was
* stored rather than what it sent.
*
* TRACES: UR-076 | DR-209
*/
async librarySetSettings(settings: LibrarySettings) : Promise<LibrarySettings> {
return await TAURI_INVOKE("library_set_settings", { settings });
},
/**
* The folders the user may choose to hide.
*
* Offers each music library and the folders directly inside it. Music is the
* only scope offered because it is the one where a foreign folder — podcasts,
* audiobooks, sound effects — routinely shares a library with the media the
* user actually browses; the scope is decided here rather than in the UI so the
* collection-type table stays out of the frontend
* (see `SearchScope::for_collection_type`).
*
* Reads through `HybridRepository::get_items_unfiltered` so folders that are
* *already* hidden still appear — otherwise the setting could never be undone.
*
* TRACES: UR-076 | DR-209
*/
async libraryGetExclusionCandidates(handle: string) : Promise<ExclusionCandidate[]> {
return await TAURI_INVOKE("library_get_exclusion_candidates", { handle });
},
/**
* Resolve the stream URL for every download row that was queued while offline
* (`status = 'pending' AND stream_url IS NULL`), then pump the queue so they
* start. Call this on reconnect.
*
* Audio rows resolve via `get_audio_stream_url`; video rows (media_type =
* 'video') via the pure `get_video_download_url` builder using the row's stored
* `quality_preset` — mirroring `enqueue_video_downloads`. Rows whose URL can't
* be resolved are left pending (they retry on the next reconnect).
*/
async resumeQueuedDownloads(handle: string) : Promise<ResumeQueuedResult> {
return await TAURI_INVOKE("resume_queued_downloads", { handle });
},
/**
* Get download manager statistics
*/
async getDownloadManagerStats() : Promise<DownloadManagerStats> {
return await TAURI_INVOKE("get_download_manager_stats");
},
/**
* Set the maximum concurrent downloads
*/
async setMaxConcurrentDownloads(max: number) : Promise<null> {
return await TAURI_INVOKE("set_max_concurrent_downloads", { max });
},
/**
* Get SmartCache statistics
*/
async getSmartCacheStats(userId: string) : Promise<SmartCacheStats> {
return await TAURI_INVOKE("get_smart_cache_stats", { userId });
},
/**
* Update SmartCache configuration
*/
async updateSmartCacheConfig(config: CacheConfig) : Promise<null> {
return await TAURI_INVOKE("update_smart_cache_config", { config });
},
/**
* Get SmartCache configuration
*/
async getSmartCacheConfig() : Promise<CacheConfig> {
return await TAURI_INVOKE("get_smart_cache_config");
},
/**
* Report the device's current network transport (Android → Rust).
*
* The frontend calls this on startup and whenever the native network callback
* fires. Updating to an acceptable network re-pumps the download queue, so a
* queue parked on "waiting for WiFi" drains itself without user action.
*
* TRACES: UR-053 | DR-074
*/
async setNetworkState(network: NetworkStateWrapperArg) : Promise<null> {
return await TAURI_INVOKE("set_network_state", { network });
},
/**
* Whether downloads are currently permitted by the WiFi-only gate.
*
* The downloads UI uses this to render "Waiting for WiFi" on pending rows
* rather than leaving them looking silently stuck.
*
* TRACES: UR-053 | DR-074
*/
async getDownloadsAllowed() : Promise<boolean> {
return await TAURI_INVOKE("get_downloads_allowed");
},
/**
* Get album recommendations based on play history
*/
async getAlbumRecommendations(userId: string) : Promise<AlbumRecommendation[]> {
return await TAURI_INVOKE("get_album_recommendations", { userId });
},
/**
* Get album affinity status for all tracked albums
* This shows the SmartCache's internal play history and threshold status
*/
async getAlbumAffinityStatus() : Promise<AlbumAffinityStatus[]> {
return await TAURI_INVOKE("get_album_affinity_status");
},
/**
* Pin an item's metadata (protects from cache clear)
*/
async pinItem(itemId: string) : Promise<null> {
return await TAURI_INVOKE("pin_item", { itemId });
},
/**
* Unpin an item's metadata
*/
async unpinItem(itemId: string) : Promise<null> {
return await TAURI_INVOKE("unpin_item", { itemId });
},
/**
* Check if an item is pinned
*/
async isItemPinned(itemId: string) : Promise<boolean> {
return await TAURI_INVOKE("is_item_pinned", { itemId });
},
/**
* Check if an item is available offline
*/
async offlineIsAvailable(itemId: string) : Promise<boolean> {
return await TAURI_INVOKE("offline_is_available", { itemId });
},
/**
* Get all offline items for a user
*/
async offlineGetItems(userId: string) : Promise<OfflineItem[]> {
return await TAURI_INVOKE("offline_get_items", { userId });
},
/**
* Search offline items
*/
async offlineSearch(userId: string, query: string) : Promise<OfflineItem[]> {
return await TAURI_INVOKE("offline_search", { userId, query });
},
/**
* Get cached libraries for a server
*/
async storageGetLibraries(serverId: string) : Promise<CachedLibrary[]> {
return await TAURI_INVOKE("storage_get_libraries", { serverId });
},
/**
* Get cached items with optional filtering
*/
async storageGetItems(serverId: string, parentId: string | null, libraryId: string | null, itemType: string | null, limit: number | null, offset: number | null) : Promise<CachedItem[]> {
return await TAURI_INVOKE("storage_get_items", { serverId, parentId, libraryId, itemType, limit, offset });
},
/**
* Get a single cached item by ID
*/
async storageGetItem(itemId: string) : Promise<CachedItem | null> {
return await TAURI_INVOKE("storage_get_item", { itemId });
},
/**
* Search cached items using FTS
*/
async storageSearchItems(serverId: string, query: string, limit: number | null) : Promise<CachedItem[]> {
return await TAURI_INVOKE("storage_search_items", { serverId, query, limit });
},
/**
* Save a library to the cache
*/
async storageSaveLibrary(id: string, serverId: string, name: string, collectionType: string | null, imageTag: string | null, sortOrder: number | null) : Promise<null> {
return await TAURI_INVOKE("storage_save_library", { id, serverId, name, collectionType, imageTag, sortOrder });
},
/**
* Save an item to the cache
*/
async storageSaveItem(item: CachedItem, serverId: string) : Promise<null> {
return await TAURI_INVOKE("storage_save_item", { item, serverId });
},
/**
* Get count of pending sync operations for a user
*/
async storageGetPendingSyncCount(userId: string) : Promise<number> {
return await TAURI_INVOKE("storage_get_pending_sync_count", { userId });
},
/**
* Queue a mutation for sync to server
*/
async syncQueueMutation(userId: string, operation: string, itemId: string | null, payload: string | null) : Promise<number> {
return await TAURI_INVOKE("sync_queue_mutation", { userId, operation, itemId, payload });
},
/**
* Get all pending sync operations for a user
*/
async syncGetPending(userId: string, limit: number | null) : Promise<SyncQueueItem[]> {
return await TAURI_INVOKE("sync_get_pending", { userId, limit });
},
/**
* Mark a sync operation as in progress
*/
async syncMarkProcessing(id: number) : Promise<null> {
return await TAURI_INVOKE("sync_mark_processing", { id });
},
/**
* Mark a sync operation as completed
*/
async syncMarkCompleted(id: number) : Promise<null> {
return await TAURI_INVOKE("sync_mark_completed", { id });
},
/**
* Mark a sync operation as failed with error message
*/
async syncMarkFailed(id: number, error: string) : Promise<null> {
return await TAURI_INVOKE("sync_mark_failed", { id, error });
},
/**
* Get count of pending sync operations for a user
*/
async syncGetPendingCount(userId: string) : Promise<number> {
return await TAURI_INVOKE("sync_get_pending_count", { userId });
},
/**
* Push the queue now, on the user's say-so, instead of waiting for a reconnect.
*
* TRACES: UR-025 | DR-132
*/
async syncProcessPending() : Promise<DrainReport> {
return await TAURI_INVOKE("sync_process_pending");
},
/**
* Delete completed sync operations older than specified days
*/
async syncCleanupCompleted(daysOld: number) : Promise<number> {
return await TAURI_INVOKE("sync_cleanup_completed", { daysOld });
},
/**
* Delete all sync operations for a user (used during logout)
*/
async syncClearUser(userId: string) : Promise<null> {
return await TAURI_INVOKE("sync_clear_user", { userId });
},
/**
* Get cached thumbnail path, returns None if not cached
* Also updates last_accessed timestamp for LRU tracking
*/
async thumbnailGetCached(itemId: string, imageType: string, tag: string) : Promise<string | null> {
return await TAURI_INVOKE("thumbnail_get_cached", { itemId, imageType, tag });
},
/**
* Download and save a thumbnail to cache
* Returns the local file path on success
*/
async thumbnailSave(itemId: string, imageType: string, tag: string, url: string) : Promise<string> {
return await TAURI_INVOKE("thumbnail_save", { itemId, imageType, tag, url });
},
/**
* Get thumbnail cache statistics
*/
async thumbnailGetStats() : Promise<ThumbnailCacheStats> {
return await TAURI_INVOKE("thumbnail_get_stats");
},
/**
* Set thumbnail cache storage limit in bytes
*/
async thumbnailSetLimit(limitBytes: number) : Promise<null> {
return await TAURI_INVOKE("thumbnail_set_limit", { limitBytes });
},
/**
* Clear all cached thumbnails
*/
async thumbnailClearCache() : Promise<null> {
return await TAURI_INVOKE("thumbnail_clear_cache");
},
/**
* Delete cached thumbnails for a specific item
*/
async thumbnailDeleteItem(itemId: string) : Promise<null> {
return await TAURI_INVOKE("thumbnail_delete_item", { itemId });
},
/**
* Get image as base64 data URL, caching if not already cached
* This extends the thumbnail system to serve all images through Rust with automatic caching
*/
async imageGetUrl(repositoryHandle: string, request: GetImageRequest) : Promise<string> {
return await TAURI_INVOKE("image_get_url", { repositoryHandle, request });
},
/**
* Save a person to the cache
*/
async storageSavePerson(person: CachedPerson) : Promise<null> {
return await TAURI_INVOKE("storage_save_person", { person });
},
/**
* Get a cached person by ID
*/
async storageGetPerson(personId: string) : Promise<CachedPerson | null> {
return await TAURI_INVOKE("storage_get_person", { personId });
},
/**
* Save item-person associations (batch)
*/
async storageSaveItemPeople(associations: CachedItemPerson[]) : Promise<null> {
return await TAURI_INVOKE("storage_save_item_people", { associations });
},
/**
* Get people for an item (with person details joined)
*/
async storageGetItemPeople(itemId: string) : Promise<CachedItemPerson[]> {
return await TAURI_INVOKE("storage_get_item_people", { itemId });
},
/**
* Save user's preferred audio track for a series
*/
async storageSaveSeriesAudioPreference(userId: string, seriesId: string, serverId: string, audioTrackDisplayTitle: string | null, audioTrackLanguage: string | null, audioTrackIndex: number | null) : Promise<null> {
return await TAURI_INVOKE("storage_save_series_audio_preference", { userId, seriesId, serverId, audioTrackDisplayTitle, audioTrackLanguage, audioTrackIndex });
},
/**
* Get user's preferred audio track for a series
*/
async storageGetSeriesAudioPreference(userId: string, seriesId: string) : Promise<SeriesAudioPreference | null> {
return await TAURI_INVOKE("storage_get_series_audio_preference", { userId, seriesId });
},
/**
* Create a new repository instance
* Returns a handle (UUID) for accessing the repository
*/
async repositoryCreate(serverUrl: string, userId: string, accessToken: string, serverId: string) : Promise<string> {
return await TAURI_INVOKE("repository_create", { serverUrl, userId, accessToken, serverId });
},
/**
* Destroy a repository instance
*/
async repositoryDestroy(handle: string) : Promise<null> {
return await TAURI_INVOKE("repository_destroy", { handle });
},
/**
* Get libraries
*/
async repositoryGetLibraries(handle: string) : Promise<Library[]> {
return await TAURI_INVOKE("repository_get_libraries", { handle });
},
/**
* Get items in a container (library, folder, album, etc.)
*/
async repositoryGetItems(handle: string, parentId: string, options: GetItemsOptions | null) : Promise<SearchResult> {
return await TAURI_INVOKE("repository_get_items", { handle, parentId, options });
},
/**
* Get a single item by ID
*/
async repositoryGetItem(handle: string, itemId: string) : Promise<MediaItem> {
return await TAURI_INVOKE("repository_get_item", { handle, itemId });
},
/**
* Downloaded-only browse: libraries that contain downloaded content.
*
* Backs the Downloads "Downloaded" surface. Never merges server results and is
* authoritative — an empty list means nothing is downloaded.
*
* TRACES: UR-055 | DR-082
*/
async repositoryGetDownloadedLibraries(handle: string) : Promise<Library[]> {
return await TAURI_INVOKE("repository_get_downloaded_libraries", { handle });
},
/**
* Downloaded-only browse: items under a container that are on the device.
*
* TRACES: UR-055 | DR-082, DR-083
*/
async repositoryGetDownloadedItems(handle: string, parentId: string, options: GetItemsOptions | null) : Promise<SearchResult> {
return await TAURI_INVOKE("repository_get_downloaded_items", { handle, parentId, options });
},
/**
* On-disk usage of downloaded content (device total, per-item/container bytes).
*
* TRACES: UR-056 | DR-085
*/
async repositoryGetDownloadDiskUsage(handle: string) : Promise<DownloadDiskUsage> {
return await TAURI_INVOKE("repository_get_download_disk_usage", { handle });
},
/**
* Query the optional JRay plugin for the actors on screen at time `t`
* (seconds) in an item. Returns an empty list when JRay isn't installed or
* has no data for the item, so the caller can render nothing without error.
*/
async repositoryJrayActorsAt(handle: string, itemId: string, t: number) : Promise<JRayActor[]> {
return await TAURI_INVOKE("repository_jray_actors_at", { handle, itemId, t });
},
/**
* Get latest items in a library
*/
async repositoryGetLatestItems(handle: string, parentId: string, limit: number | null) : Promise<MediaItem[]> {
return await TAURI_INVOKE("repository_get_latest_items", { handle, parentId, limit });
},
/**
* Get resume items (continue watching/listening).
*
* The home screen's Continue Watching row and every library's "pick up where
* you left off" hero come through here; each item carries its own resume
* position in `UserData`.
*
* TRACES: UR-019, UR-023, UR-034 | IR-024, JA-013, JA-015 | DR-026, DR-038
*/
async repositoryGetResumeItems(handle: string, parentId: string | null, limit: number | null) : Promise<MediaItem[]> {
return await TAURI_INVOKE("repository_get_resume_items", { handle, parentId, limit });
},
/**
* Get next up episodes.
*
* TRACES: UR-023, UR-034 | IR-024, JA-014 | DR-026
*/
async repositoryGetNextUpEpisodes(handle: string, seriesId: string | null, limit: number | null) : Promise<MediaItem[]> {
return await TAURI_INVOKE("repository_get_next_up_episodes", { handle, seriesId, limit });
},
/**
* Every episode of a series, across all seasons, in series order.
*
* Jellyfin hangs episodes off season folders — except for "flat" series whose
* children are episodes directly. Both shapes are provider vocabulary, so the
* fan-out and its fallback live in Rust rather than being reimplemented in the
* frontend (which is what it used to do).
*
* TRACES: UR-062 | DR-101
*/
async repositoryGetSeriesEpisodes(handle: string, seriesId: string) : Promise<MediaItem[]> {
return await TAURI_INVOKE("repository_get_series_episodes", { handle, seriesId });
},
/**
* The episode a viewer should land on when they open a series.
*
* "Current" is domain policy, not layout: an episode in progress, else the
* server's Next Up for the series, else the first unwatched episode, else the
* first. The third rung is what makes this work offline, where Next Up is
* always empty. Returns `None` only when the series has no episodes at all.
*
* TRACES: UR-062 | DR-101
*/
async repositoryGetSeriesCurrentEpisode(handle: string, seriesId: string) : Promise<MediaItem | null> {
return await TAURI_INVOKE("repository_get_series_current_episode", { handle, seriesId });
},
/**
* Erase the viewer's watch history for an item.
*
* Clears the played flag and the resume position; on a series or season the
* server applies it to everything inside. A series cleared this way is "never
* watched" again, so `repository_get_series_current_episode` returns its
* premiere. Requires the server — offline this fails rather than diverging
* local state the next sync would overwrite.
*
* TRACES: UR-064 | DR-106
*/
async repositoryClearWatchHistory(handle: string, itemId: string) : Promise<null> {
return await TAURI_INVOKE("repository_clear_watch_history", { handle, itemId });
},
/**
* Get recently played audio
*/
async repositoryGetRecentlyPlayedAudio(handle: string, limit: number | null) : Promise<MediaItem[]> {
return await TAURI_INVOKE("repository_get_recently_played_audio", { handle, limit });
},
/**
* Get resume movies
*/
async repositoryGetResumeMovies(handle: string, limit: number | null) : Promise<MediaItem[]> {
return await TAURI_INVOKE("repository_get_resume_movies", { handle, limit });
},
/**
* Get albums the user hasn't listened to recently ("rediscover")
*/
async repositoryGetRediscoverAlbums(handle: string, parentId: string | null, limit: number | null) : Promise<MediaItem[]> {
return await TAURI_INVOKE("repository_get_rediscover_albums", { handle, parentId, limit });
},
/**
* Get genres for a library
*/
async repositoryGetGenres(handle: string, parentId: string | null) : Promise<Genre[]> {
return await TAURI_INVOKE("repository_get_genres", { handle, parentId });
},
/**
* Search for items.
*
* Resolves `SearchOptions::scope` into concrete Jellyfin item types before
* dispatching, so scope taxonomy stays in Rust.
*
* TRACES: UR-049, UR-050 | DR-063
*/
async repositorySearch(handle: string, query: string, options: SearchOptions | null, requestId: number) : Promise<SearchResult> {
return await TAURI_INVOKE("repository_search", { handle, query, options, requestId });
},
/**
* Get playback info for an item
*/
async repositoryGetPlaybackInfo(handle: string, itemId: string) : Promise<PlaybackInfo> {
return await TAURI_INVOKE("repository_get_playback_info", { handle, itemId });
},
/**
* Get a video stream URL.
*
* There is no start-position parameter on purpose: the URL is an HLS playlist
* covering the whole item, and a position on it makes the server reject every
* segment with `400` (DR-181). Callers resume by seeking after load.
*
* TRACES: UR-004 | DR-181 | UT-182
*/
async repositoryGetVideoStreamUrl(handle: string, itemId: string, mediaSourceId: string | null, audioStreamIndex: number | null) : Promise<string> {
return await TAURI_INVOKE("repository_get_video_stream_url", { handle, itemId, mediaSourceId, audioStreamIndex });
},
/**
* Get audio stream URL for a track
*/
async repositoryGetAudioStreamUrl(handle: string, itemId: string) : Promise<string> {
return await TAURI_INVOKE("repository_get_audio_stream_url", { handle, itemId });
},
/**
* Get an audio-only stream URL for a *video* item (background-audio handoff).
*
* TRACES: UR-040 | JA-032 | UT-061
*/
async repositoryGetAudioOnlyStreamUrlForVideo(handle: string, itemId: string, mediaSourceId: string | null, startTimeSeconds: number | null, audioStreamIndex: number | null) : Promise<string> {
return await TAURI_INVOKE("repository_get_audio_only_stream_url_for_video", { handle, itemId, mediaSourceId, startTimeSeconds, audioStreamIndex });
},
/**
* Get Live TV channels (broadcast / IPTV) for browsing
*/
async repositoryGetLiveTvChannels(handle: string) : Promise<MediaItem[]> {
return await TAURI_INVOKE("repository_get_live_tv_channels", { handle });
},
/**
* Get the root list of plugin "Channels"
*/
async repositoryGetChannels(handle: string) : Promise<SearchResult> {
return await TAURI_INVOKE("repository_get_channels", { handle });
},
/**
* Open a live stream for a Live TV channel / live item
*/
async repositoryOpenLiveStream(handle: string, itemId: string) : Promise<LiveStreamInfo> {
return await TAURI_INVOKE("repository_open_live_stream", { handle, itemId });
},
/**
* Report playback start
*/
async repositoryReportPlaybackStart(handle: string, itemId: string, positionMs: number) : Promise<null> {
return await TAURI_INVOKE("repository_report_playback_start", { handle, itemId, positionMs });
},
/**
* Report playback progress
*/
async repositoryReportPlaybackProgress(handle: string, itemId: string, positionMs: number) : Promise<null> {
return await TAURI_INVOKE("repository_report_playback_progress", { handle, itemId, positionMs });
},
/**
* Report playback stopped
*
* A stop-report that cannot reach the server is queued rather than dropped:
* this is the position the resume point is built from, and losing it is
* exactly the "it forgot where I was" the sync queue exists to prevent. The
* drain (DR-131) pushes it on the next reconnect. Queueing is best-effort —
* failing the command because the *queue* write failed would tell the caller
* the report was lost when the local position was already saved.
*
* TRACES: UR-025 | DR-154 | UT-151
*/
async repositoryReportPlaybackStopped(handle: string, itemId: string, positionMs: number) : Promise<null> {
return await TAURI_INVOKE("repository_report_playback_stopped", { handle, itemId, positionMs });
},
/**
* Get image URL for an item
*/
async repositoryGetImageUrl(handle: string, itemId: string, imageType: ImageType, options: ImageOptions | null) : Promise<string> {
return await TAURI_INVOKE("repository_get_image_url", { handle, itemId, imageType, options });
},
/**
* Mark an item as favorite
*/
async repositoryMarkFavorite(handle: string, itemId: string) : Promise<null> {
return await TAURI_INVOKE("repository_mark_favorite", { handle, itemId });
},
/**
* Unmark an item as favorite
*/
async repositoryUnmarkFavorite(handle: string, itemId: string) : Promise<null> {
return await TAURI_INVOKE("repository_unmark_favorite", { handle, itemId });
},
/**
* Everything the viewer has favourited, across libraries, narrowed by scope.
*
* Two-phase like `repository_search`: the local answer returns immediately and
* a background server pass emits `favorites-changed` when the server's set
* differs. Without the second phase a favourite marked in another client shows
* up only on the *second* visit to the page, since the cache-first read hands
* back local rows and the refresh is invisible to the frontend.
*
* TRACES: UR-067 | DR-115, DR-120, JA-033 | UT-107
*/
async repositoryGetFavorites(handle: string, scope: SearchScope, options: GetItemsOptions | null) : Promise<SearchResult> {
return await TAURI_INVOKE("repository_get_favorites", { handle, scope, options });
},
/**
* Get person details
*/
async repositoryGetPerson(handle: string, personId: string) : Promise<MediaItem> {
return await TAURI_INVOKE("repository_get_person", { handle, personId });
},
/**
* Get items by person (actor, director, etc.)
*/
async repositoryGetItemsByPerson(handle: string, personId: string, options: GetItemsOptions | null) : Promise<SearchResult> {
return await TAURI_INVOKE("repository_get_items_by_person", { handle, personId, options });
},
/**
* Get similar/related items for a media item
*/
async repositoryGetSimilarItems(handle: string, itemId: string, limit: number | null) : Promise<SearchResult> {
return await TAURI_INVOKE("repository_get_similar_items", { handle, itemId, limit });
},
/**
* Get subtitle URL for a media item
*/
async repositoryGetSubtitleUrl(handle: string, itemId: string, mediaSourceId: string, streamIndex: number, format: string) : Promise<string> {
return await TAURI_INVOKE("repository_get_subtitle_url", { handle, itemId, mediaSourceId, streamIndex, format });
},
/**
* Get video download URL with quality preset
*/
async repositoryGetVideoDownloadUrl(handle: string, itemId: string, quality: string, mediaSourceId: string | null) : Promise<string> {
return await TAURI_INVOKE("repository_get_video_download_url", { handle, itemId, quality, mediaSourceId });
},
/**
* Create a new playlist
*/
async playlistCreate(handle: string, name: string, itemIds: string[] | null) : Promise<PlaylistCreatedResult> {
return await TAURI_INVOKE("playlist_create", { handle, name, itemIds });
},
/**
* Delete a playlist
*/
async playlistDelete(handle: string, playlistId: string) : Promise<null> {
return await TAURI_INVOKE("playlist_delete", { handle, playlistId });
},
/**
* Rename a playlist
*/
async playlistRename(handle: string, playlistId: string, name: string) : Promise<null> {
return await TAURI_INVOKE("playlist_rename", { handle, playlistId, name });
},
/**
* Get playlist items with PlaylistItemId
*/
async playlistGetItems(handle: string, playlistId: string) : Promise<PlaylistEntry[]> {
return await TAURI_INVOKE("playlist_get_items", { handle, playlistId });
},
/**
* Add items to a playlist
*/
async playlistAddItems(handle: string, playlistId: string, itemIds: string[]) : Promise<null> {
return await TAURI_INVOKE("playlist_add_items", { handle, playlistId, itemIds });
},
/**
* Remove items from a playlist (uses PlaylistItemId entry IDs, NOT media item IDs)
*/
async playlistRemoveItems(handle: string, playlistId: string, entryIds: string[]) : Promise<null> {
return await TAURI_INVOKE("playlist_remove_items", { handle, playlistId, entryIds });
},
/**
* Move a playlist item to a new position
*/
async playlistMoveItem(handle: string, playlistId: string, itemId: string, newIndex: number) : Promise<null> {
return await TAURI_INVOKE("playlist_move_item", { handle, playlistId, itemId, newIndex });
},
/**
* Format time in seconds to MM:SS display string
*
* # Arguments
* * `seconds` - Time in seconds
*
* # Returns
* Formatted string like "3:45" or "12:09"
*/
async formatTimeSeconds(seconds: number) : Promise<string> {
return await TAURI_INVOKE("format_time_seconds", { seconds });
},
/**
* Format time in seconds to HH:MM:SS or MM:SS display string
*
* Automatically chooses format based on duration:
* - Less than 1 hour: Returns MM:SS format
* - 1 hour or more: Returns HH:MM:SS format
*
* # Arguments
* * `seconds` - Time in seconds
*
* # Returns
* Formatted string like "1:23:45" or "3:45"
*/
async formatTimeSecondsLong(seconds: number) : Promise<string> {
return await TAURI_INVOKE("format_time_seconds_long", { seconds });
},
/**
* Convert Jellyfin ticks to seconds
*
* # Arguments
* * `ticks` - Time in Jellyfin ticks (10,000,000 ticks = 1 second)
*
* # Returns
* Time in seconds
*/
async convertTicksToSeconds(ticks: number) : Promise<number> {
return await TAURI_INVOKE("convert_ticks_to_seconds", { ticks });
},
/**
* Calculate progress percentage from position and duration
*
* # Arguments
* * `position` - Current position in seconds
* * `duration` - Total duration in seconds
*
* # Returns
* Progress as percentage (0.0 to 100.0)
*/
async calcProgress(position: number, duration: number) : Promise<number> {
return await TAURI_INVOKE("calc_progress", { position, duration });
},
/**
* Convert percentage volume (0-100) to normalized (0.0-1.0)
*
* # Arguments
* * `percent` - Volume as percentage (0 to 100)
*
* # Returns
* Normalized volume (0.0 to 1.0)
*/
async convertPercentToVolume(percent: number) : Promise<number> {
return await TAURI_INVOKE("convert_percent_to_volume", { percent });
}
}
/** user-defined events **/
export const events = __makeEvents__<{
playerStatusEvent: PlayerStatusEvent
}>({
playerStatusEvent: "player-status-event"
})
/** user-defined constants **/
/** user-defined types **/
/**
* Active session info (for session restoration)
*/
export type ActiveSession = { userId: string; username: string; serverId: string; serverUrl: string; serverName: string; accessToken: string }
/**
* Request to add items to queue
*/
export type AddToQueueRequest = { items: PlayItemRequest[]; position: string }
/**
* Request to add a track by ID - backend fetches metadata
*/
export type AddTrackByIdRequest = { trackId: string; position: string }
/**
* Request to add multiple tracks by IDs - backend fetches metadata
*/
export type AddTracksByIdsRequest = { trackIds: string[]; position: string }
/**
* Album affinity status info
*/
export type AlbumAffinityStatus = { albumId: string; uniqueTracksPlayed: number; threshold: number; thresholdReached: boolean }
/**
* Album recommendation info
*/
export type AlbumRecommendation = { album_id: string; album_name: string; tracks_played: number; total_tracks: number; should_download: boolean }
/**
* Storage info for a single album
*/
export type AlbumStorageInfo = { album_id: string; album_name: string; artist_name: string | null; bytes_used: number; track_count: number }
/**
* Artist item with ID and name (for clickable artist links)
*/
export type ArtistItem = { id: string; name: string }
/**
* Audio playback settings
*/
export type AudioSettings = {
/**
* Crossfade duration in seconds (0 = disabled, max 12)
*/
crossfadeDuration: number;
/**
* Enable gapless playback between tracks
*/
gaplessPlayback: boolean;
/**
* Enable volume normalization
*/
normalizeVolume: boolean;
/**
* Target volume level for normalization
*/
volumeLevel: VolumeLevel;
/**
* Enable the graphic equalizer. When false, no EQ filter is applied.
*/
equalizerEnabled?: boolean;
/**
* Per-band gains in dB, one per [`EQ_BANDS`]. Normalised to 10 entries and
* clamped to [`EQ_GAIN_MIN`, `EQ_GAIN_MAX`] via [`Self::with_equalizer_normalised`].
*/
equalizerBands?: number[] }
/**
* Response for audio track switching operations
*/
export type AudioTrackSwitchResponse =
/**
* Native backend handled it (Android ExoPlayer)
*/
{ strategy: "native"; success: boolean } |
/**
* HTML5 needs to reload stream with new audio track
*/
{ strategy: "reloadStream"; new_url: string; position: number }
/**
* Authentication result
*/
export type AuthResult = { user: User; accessToken: string; serverId: string }
/**
* Server information returned from Jellyfin
*/
export type AuthServerInfo = { name: string; version: string; id: string;
/**
* Normalized server URL with protocol and no trailing slash
*/
normalizedUrl: string }
/**
* Autoplay settings (controls next episode behavior)
*/
export type AutoplaySettings = {
/**
* Whether autoplay is enabled for next episodes
*/
enabled: boolean;
/**
* Countdown duration in seconds before auto-playing next episode
*/
countdownSeconds: number;
/**
* Maximum number of episodes to auto-play consecutively (0 = unlimited)
*/
maxEpisodes?: number }
/**
* Smart caching configuration
*/
export type CacheConfig = {
/**
* Enable queue pre-caching
*/
queuePrecacheEnabled: boolean;
/**
* Number of tracks to pre-cache from queue
*/
queuePrecacheCount: number;
/**
* Enable album affinity detection
*/
albumAffinityEnabled: boolean;
/**
* Threshold for album affinity (tracks played before caching)
*/
albumAffinityThreshold: number;
/**
* Storage limit in bytes (0 = unlimited)
*/
storageLimit: number;
/**
* Only cache on WiFi
*/
wifiOnly: boolean;
/**
* How long a temporary (`download_source = 'auto'`) download lives before
* it is reclaimed, in hours. 0 disables expiry, leaving space pressure as
* the only reclaim trigger.
*
* TRACES: UR-071 | DR-127
*/
temporaryTtlHours: number }
/**
* Cached media item returned to frontend
*/
export type CachedItem = { id: string; name: string; itemType: string; parentId: string | null; libraryId: string | null; overview: string | null; genres: string | null; runtimeTicks: number | null; productionYear: number | null; communityRating: number | null; officialRating: string | null; primaryImageTag: string | null; albumId: string | null; albumName: string | null; albumArtist: string | null; artists: string | null; indexNumber: number | null; seriesId: string | null; seriesName: string | null; seasonId: string | null; seasonName: string | null; parentIndexNumber: number | null }
/**
* Item-person association for caching
*/
export type CachedItemPerson = { itemId: string; personId: string; serverId: string; personType: string; role: string | null; sortOrder: number }
/**
* Cached library info returned to frontend
*/
export type CachedLibrary = { id: string; serverId: string; name: string; collectionType: string | null; imageTag: string | null }
/**
* Cached person info returned to frontend
*/
export type CachedPerson = { id: string; serverId: string; name: string; overview: string | null; primaryImageTag: string | null; premiereDate: string | null; endDate: string | null }
export type CatalogSyncResult = {
/**
* Total items persisted to the offline cache across all libraries.
*/
itemsCached: number;
/**
* Libraries that failed to sync (e.g. server hiccup); best-effort.
*/
librariesFailed: number;
/**
* Entries removed because the server no longer has them. Always 0 when any
* library failed, since a partial crawl cannot prove an item is gone.
*/
itemsPruned: number }
export type CatalogSyncStatus = {
/**
* RFC-3339 timestamp of the last successful sync, if any.
*/
lastSyncedAt: string | null }
/**
* Connectivity status
*/
export type ConnectivityStatus = {
/**
* Whether the Jellyfin server is reachable
*/
isServerReachable: boolean;
/**
* Last time we checked server reachability (ISO 8601 string)
*/
lastChecked: string | null;
/**
* Error message from last connectivity check
*/
connectionError: string | null;
/**
* Whether we're currently checking connectivity
*/
isChecking: boolean }
/**
* On-disk usage of downloaded content, for the Downloads surface.
*
* `sizes` maps an item id (leaf *or* container) to its bytes on disk: a leaf's
* own file size, a container's summed downloaded descendants. `device_total_bytes`
* and `item_count` are the headline figures for the Downloaded surface top bar.
*
* TRACES: UR-056 | DR-085
*/
export type DownloadDiskUsage = {
/**
* item id → bytes on disk (leaf's own size, or a container's subtotal).
*/
sizes: Partial<{ [key in string]: number }>;
/**
* Container id → true when it is only *partially* downloaded (has cached
* children that are not downloaded). Absent/false ⇒ fully downloaded. Lets
* the Downloaded surface badge partial vs. full containers.
*/
partialContainers: Partial<{ [key in string]: boolean }>;
/**
* Sum of all downloaded leaf sizes — the device total.
*/
deviceTotalBytes: number;
/**
* Number of downloaded leaf items (not containers).
*/
itemCount: number }
/**
* Information about a download
*/
export type DownloadInfo = { id: number; itemId: string; userId: string; filePath: string; fileSize: number | null; mimeType: string | null; status: string; progress: number; bytesDownloaded: number; queuedAt: string; startedAt: string | null; completedAt: string | null; errorMessage: string | null; retryCount: number; priority: number; itemName: string | null; artistName: string | null; albumName: string | null; seriesName: string | null; seasonName: string | null; episodeNumber: number | null; seasonNumber: number | null; qualityPreset: string | null; mediaType: string; downloadSource: string }
/**
* Request payload for download_item_and_start (bundled to stay within specta's
* 10-argument command limit).
*/
export type DownloadItemAndStartRequest = { itemId: string; userId: string; streamUrl: string; targetDir: string; itemName: string | null; artistName: string | null; albumName: string | null }
/**
* Request payload for download_item.
*/
export type DownloadItemRequest = { itemId: string; userId: string; filePath: string; mimeType: string | null; priority: number | null; itemName: string | null; artistName: string | null; albumName: string | null; expectedSize: number | null }
/**
* Download manager statistics
*/
export type DownloadManagerStats = { max_concurrent: number; active_count: number; available_slots: number }
/**
* Download statistics computed server-side
*/
export type DownloadStats = { total: number; activeCount: number; queuedCount: number; completedCount: number; failedCount: number; pausedCount: number }
/**
* Request payload for download_video.
*/
export type DownloadVideoRequest = { itemId: string; userId: string; filePath: string; mimeType: string | null; priority: number | null; itemName: string | null; qualityPreset: string | null; seriesName: string | null; seasonName: string | null; episodeNumber: number | null; seasonNumber: number | null }
/**
* Enhanced response with pre-computed stats
*/
export type DownloadsResponse = { downloads: DownloadInfo[]; stats: DownloadStats }
/**
* What a drain did, for logging and for the frontend's "Sync now" button.
*/
export type DrainReport = {
/**
* Rows that reached the server and are now `completed`.
*/
pushed: number;
/**
* Rows that failed and will be retried on the next reconnect.
*/
deferred: number;
/**
* Rows that exhausted `MAX_SYNC_ATTEMPTS` and were given up on.
*/
abandoned: number;
/**
* Rows still waiting afterwards (what the badge counts).
*/
remaining: number }
/**
* Built-in equalizer presets. A preset *is* a gain curve defined by the band
* layout above (a domain concept), not a mere label — the curve numbers live
* in Rust so the frontend never encodes the taxonomy.
*
* TRACES: UR-027 | DR-030
*/
export type EqPreset = "flat" | "rock" | "pop" | "jazz" | "classical" | "bassBoost" | "trebleBoost" | "vocal"
/**
* Something the user may choose to hide: a library, or a folder directly
* inside one.
*
* Which containers are *offerable* is a domain question (it depends on the
* library's Jellyfin collection type and on what counts as a folder), so the
* list is assembled here and the frontend renders it verbatim.
*
* TRACES: UR-076 | DR-209
*/
export type ExclusionCandidate = {
/**
* Stable Jellyfin item id — what gets stored when the user picks it.
*/
id: string;
/**
* Display name of the folder (or of the library, for a whole-library entry).
*/
name: string;
/**
* Library this candidate lives in, so the picker can group and disambiguate
* two folders that share a name.
*/
libraryName: string;
/**
* True when the candidate *is* a library rather than a folder inside one.
*/
isLibrary: boolean }
/**
* Genre
*/
export type Genre = { id: string; name: string;
/**
* Number of albums tagged with this genre, when the backend can supply it
* (online only). Lets the frontend rank/pick genres without probing each
* one. `None` when unknown (e.g. offline).
*/
albumCount: number | null }
/**
* Request to get an image URL (with caching)
*/
export type GetImageRequest = { itemId: string; imageType: string; maxWidth?: number | null; maxHeight?: number | null; tag?: string | null }
/**
* Options for querying items
*/
export type GetItemsOptions = { startIndex?: number | null; limit?: number | null; sortBy?: string | null; sortOrder?: string | null; includeItemTypes?: string[] | null; recursive?: boolean | null; fields?: string[] | null; genres?: string[] | null;
/**
* Restrict the listing to favourited items. Backs the per-library
* favourites toggle; composes with every other filter here.
*
* TRACES: UR-067 | DR-116 | UT-104
*/
favoritesOnly?: boolean | null }
/**
* Image options
*/
export type ImageOptions = { maxWidth?: number | null; maxHeight?: number | null; quality?: number | null; tag?: string | null }
/**
* Image type
*/
export type ImageType = "Primary" | "Backdrop" | "Banner" | "Thumb" | "Logo"
/**
* A single actor returned by the JRay plugin's "context at time t" endpoint.
*
* Mirrors the `actors[]` objects from `GET /Plugins/JRay/Items/{id}/jray?t=`.
* `jellyfin_id` (a Jellyfin Person item GUID) is preferred for navigation;
* the IMDb/TMDb ids are informational fallbacks. Unknown ids are `""`.
*/
export type JRayActor = { name: string; imdb_id?: string; tmdb_id?: string; jellyfin_id?: string }
/**
* Library (media collection)
*/
export type Library = { id: string; name: string; collectionType: string; imageTag?: string | null;
/**
* The favourites scope this library's contents fall under, or `None` for a
* library kind favourites does not carve up (Live TV, channels, books…).
*
* Derived here rather than in the UI: which collection type maps to which
* scope is Jellyfin vocabulary, and the frontend must not hold a
* collection-type → category table any more than an item-type one. See
* `SearchScope::for_collection_type`.
*
* TRACES: UR-075 | DR-175
*/
favoritesScope?: SearchScope | null }
/**
* Library browsing preferences.
*
* Currently a single list: the folders (or whole libraries) the user has asked
* to keep out of browsing. It is a *list of ids*, never names — names are
* unstable, locale-dependent and non-unique, and the hardcoded name filter this
* setting replaced broke on exactly that. What the ids then hide is decided in
* `repository::exclusions`; this struct is only how the choice is carried and
* persisted.
*
* The default is an empty list: nobody inherits another user's folder layout.
*
* TRACES: UR-076 | DR-209
*/
export type LibrarySettings = {
/**
* Stable item ids of the folders/libraries hidden from browsing.
*
* `#[serde(default)]` so settings JSON persisted before this field existed
* loads as the previous behaviour (nothing hidden).
*/
excludedItemIds?: string[] }
/**
* Live stream information returned from opening a Live TV / channel stream.
*
* Unlike on-demand video, a live channel must be "opened" before it can be
* streamed; the server returns a transcoding URL (already absolute) plus a
* `live_stream_id` that can later be used to close the stream.
*/
export type LiveStreamInfo = { streamUrl: string; playSessionId: string | null; liveStreamId: string | null; mediaSourceId: string | null }
/**
* An LMS multi-room sync group, as returned by JellyLMS `/JellyLms/SyncGroups`.
*
* Mirrors the plugin's `SyncGroup` model. The master is the sync source; slaves
* follow it in lockstep.
*/
export type LmsSyncGroup = { masterMac: string; masterName?: string; slaveMacs?: string[]; slaveNames?: string[] }
/**
* Media item
*/
export type MediaItem = { id: string; name: string;
/**
* Legacy Jellyfin item-type string (`"Audio"`, `"MusicAlbum"`, …).
*
* Dual-carry migration (docs/specs/frontend-domain-model.md): `kind` below
* is the neutral replacement. This field stays while the frontend migrates
* off it, then is removed in a later phase. New Rust code should read
* `kind`, not this.
*/
type: string;
/**
* Provider-neutral classification — the replacement for `item_type`.
* Populated by the Jellyfin mapping; defaults to `Other` for the handful of
* construction sites that have not been migrated yet.
*/
kind?: MediaKind;
/**
* Whether this item is a folder/container (vs a playable leaf). Used to
* decide whether a channel item drills into a list or plays directly.
*/
isFolder?: boolean; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null;
/**
* ISO-8601 release/air date (Jellyfin `PremiereDate`). Used to sort
* podcast episodes by release date.
*/
premiereDate?: string | null; communityRating?: number | null; officialRating?: string | null;
/**
* Legacy Jellyfin duration in ticks (100 ns units). Being replaced by
* `duration_ms`; dual-carried while the frontend migrates
* (docs/specs/frontend-domain-model.md). New code should read `duration_ms`.
*/
runTimeTicks?: number | null;
/**
* Duration in milliseconds — the neutral replacement for `runtime_ticks`.
* Ticks never reach the frontend; this does.
*/
durationMs?: number | null;
/**
* Legacy Jellyfin primary image tag. Being replaced by `image_id`;
* dual-carried while the frontend migrates. New code should read `image_id`.
*/
primaryImageTag?: string | null;
/**
* Neutral image identifier the frontend resolves to a URL via the image
* command — the replacement for `primary_image_tag`. Same value today
* (Jellyfin's tag is the id); the rename removes the provider term.
*/
imageId?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }
/**
* The kind of a media item — provider-neutral classification.
*
* Replaces the stringly-typed `item_type` that carried Jellyfin's vocabulary
* (`"Audio"`, `"MusicAlbum"`, …) across the boundary. A closed enum means a
* typo or an unhandled kind is a compile error on the frontend, not a silent
* runtime miss across ~127 comparison sites.
*/
export type MediaKind = "track" | "album" | "artist" | "playlist" | "movie" | "series" | "season" | "episode" | "person" |
/**
* A channel *container* the user drills into (Jellyfin `Channel`).
*/
"channel" | "folder" |
/**
* A live TV channel — playable, but a live stream with no seekable
* timeline (no resume/seek). Jellyfin `TvChannel`/`LiveTvChannel`.
*/
"liveChannel" |
/**
* A playable leaf inside a channel (Jellyfin `ChannelFolderItem` that is
* not itself a folder) — e.g. a plugin-channel VOD item that has no
* dedicated item type but carries its own media streams. Playable and
* seekable, unlike `LiveChannel`. Distinct from `Channel` (the container)
* and from `Other` so the UI can route it to playback.
*/
"channelItem" |
/**
* A kind we do not model explicitly. Reached only for provider item types
* that map to nothing meaningful; consumers treat it like an opaque
* container. The mapping must be *total* — it never panics — so this is the
* safe sink for unknown strings. Also the `Default`, so a defaulted
* `MediaItem` (see the dual-carry migration) is inert rather than a lie.
*/
"other"
/**
* Media session type tracking the high-level playback context
*/
export type MediaSessionType =
/**
* No active session - browsing library
*/
{ type: "idle" } |
/**
* Audio playback session (music, audiobooks, podcasts)
* Persists until explicitly dismissed
*/
{ type: "audio"; last_item: PlayerMediaItem | null; is_active: boolean } |
/**
* Movie playback (single video, auto-dismiss on end)
*/
{ type: "movie"; item: PlayerMediaItem; is_active: boolean } |
/**
* TV show playback (supports next episode auto-advance)
*/
{ type: "tv_show"; item: PlayerMediaItem; series_id: string; is_active: boolean }
/**
* Media source information
*/
export type MediaSource = { id: string; name: string; container?: string | null; size?: number | null; bitrate?: number | null; supportsDirectPlay: boolean; supportsDirectStream: boolean; supportsTranscoding: boolean; directStreamUrl?: string | null }
/**
* Media stream information (audio, video, subtitle tracks)
*/
export type MediaStream = {
/**
* Legacy Jellyfin stream type string ("Audio"/"Video"/"Subtitle"). Being
* replaced by `kind`; dual-carried while the frontend migrates.
*/
type: string;
/**
* Provider-neutral stream classification — replaces `stream_type`.
*/
kind?: StreamKind; codec?: string | null; language?: string | null; displayTitle?: string | null; index: number; isDefault: boolean; isForced: boolean;
/**
* Whether this stream can reach the app as a sidecar it renders itself.
*
* `None` for anything that is not a subtitle — the question does not apply,
* and `false` there would read like a verdict. For a subtitle it is the
* difference between a track the app can draw and one only the server could
* have shown, by burning it into the picture (DR-176) — which this app never
* asks it to do. The vocabulary of *which formats those are* stays in Rust;
* the frontend only reads the answer.
*
* TRACES: UR-020 | DR-176 | UT-168
*/
supportsExternalDelivery?: boolean | null }
export type MediaType = "audio" | "video"
/**
* Lightweight media item for merged playback state
* Converts from both local MediaItem and remote NowPlayingItem
*/
export type MergedMediaItem = { id: string; title: string; artist: string | null; album: string | null; albumId: string | null; duration: number | null; primaryImageTag: string | null;
/**
* Neutral image identifier — replaces `primary_image_tag` (same value).
*/
imageId: string | null; mediaType: string }
/**
* Argument struct for [`set_network_state`].
*
* TRACES: UR-053 | DR-074
*/
export type NetworkStateWrapperArg = { networkType: NetworkType; unmetered: boolean }
/**
* Kind of network transport currently active.
*
* Mirrors the string constants in `NetworkTypeMonitor.kt`; the two must stay
* in sync (the serde rename below is what the frontend sends).
*
* TRACES: UR-053 | DR-074
*/
export type NetworkType =
/**
* No active network.
*/
"none" |
/**
* WiFi (may still be metered — check `unmetered`).
*/
"wifi" |
/**
* Wired ethernet, typical on Android TV and desktop.
*/
"ethernet" |
/**
* Mobile data — never acceptable when wifi-only is enabled.
*/
"cellular" |
/**
* Some other transport (VPN over unknown carrier, Bluetooth tethering, …).
*/
"other" |
/**
* Could not determine the transport.
*/
"unknown"
export type NowPlayingItem = { id: string | null; name: string | null; runTimeTicks: number | null; album: string | null; albumId: string | null; albumArtist: string | null; artists: string[] | null; imageTags: Partial<{ [key in string]: string }> | null; primaryImageTag: string | null; albumPrimaryImageTag: string | null; Type: string | null }
export type OfflineItem = { id: string; name: string; itemType: string; albumId: string | null; albumName: string | null; artists: string | null; runtimeTicks: number | null; primaryImageTag: string | null }
/**
* Person (cast/crew member) - for movies, series, and episodes
*/
export type Person = {
/**
* Deserializes from API's "Id" field (PascalCase), serializes as "id" (camelCase to frontend)
*/
id?: string;
/**
* Deserializes from API's "Name" field (PascalCase), serializes as "name" (camelCase to frontend)
*/
name?: string;
/**
* Person type from Jellyfin API (Actor, Director, Writer, etc.)
* Deserializes from API's "Type" field (PascalCase), serializes as "type" (camelCase to frontend)
*/
type?: string;
/**
* Deserializes from API's "Role" field (PascalCase), serializes as "role" (camelCase to frontend)
*/
role?: string | null;
/**
* Deserializes from API's "PrimaryImageTag" field (PascalCase), serializes as "primaryImageTag" (camelCase to frontend)
*/
primaryImageTag?: string | null }
/**
* Request to play a track from an album (backend fetches all tracks)
*/
export type PlayAlbumTrackRequest = { albumId: string; albumName: string; trackId: string; shuffle: boolean }
/**
* Request to play a single video item
*
* Simplified to video playback only. Audio playback uses player_play_tracks
* to avoid Tauri Android serialization issues with complex objects.
*/
export type PlayItemRequest = { id: string; title: string; streamUrl: string;
/**
* Video codec (e.g., "h264", "hevc") for video media
*/
videoCodec: string;
/**
* Whether the video requires server-side transcoding
*/
needsTranscoding: boolean;
/**
* Optional now-playing metadata. Used by the background-audio handoff so the
* lockscreen/miniplayer show the item (title/subtitle/artwork). Defaulted so
* existing video-only callers need not send them.
*/
artist?: string | null; primaryImageTag?: string | null; serverId?: string | null;
/**
* Total media duration (seconds). Threaded through the background-audio
* handoff so the lockscreen MediaSession advertises a real duration — a
* zero-duration session renders no scrubber, even with ACTION_SEEK_TO set.
*/
durationSeconds?: number | null;
/**
* Item type (e.g. "Episode", "Movie", "Audio"). Carried through the
* background-audio handoff so an episode played as audio-only is still
* recognised as an episode by autoplay (UR-040) and advances to the next one.
*/
itemType?: string | null;
/**
* Series ID for TV episodes. Needed alongside `item_type` so the backend can
* look up the next episode when a background-audio track ends.
*/
seriesId?: string | null;
/**
* Subtitle tracks to sideload, with URLs the frontend has already resolved.
*
* Only the native backends use these: on Android they become the
* `MediaItem.SubtitleConfiguration`s ExoPlayer renders. The HTML5 path
* builds its own `<track>` children instead and ignores this list.
*
* **Order is the contract.** `player_set_subtitle_track(n)` reaches
* `JellyTauPlayer.setSubtitleTrack(n)`, which indexes into ExoPlayer's
* *text track groups* — i.e. the position of the sideloaded configuration,
* not the Jellyfin stream index (which is kept on each entry for the UI's
* benefit). So `n` must be a position in this very array, and the array
* must not be reordered or filtered between building it and sending it.
* `nativeSubtitleArrayIndex()` on the frontend computes `n` from the same
* list that is sent here, for exactly this reason.
*
* Defaulted so the background-audio handoff and the autoplay/next-episode
* callers, which have no subtitles to offer, need not send the field.
*
* TRACES: UR-020 | IR-016, JA-008 | UT-145
*/
subtitles?: SubtitleTrack[] }
/**
* Queue context for remote transfer - what type of queue is this?
*/
export type PlayQueueContext =
/**
* Playing from a specific album
*/
{ type: "album"; albumId: string; albumName: string } |
/**
* Playing from a specific playlist
*/
{ type: "playlist"; playlistId: string; playlistName: string } |
/**
* Custom queue (search results, manual queue, etc.)
*/
{ type: "custom" }
/**
* Request to play a queue of items
*/
export type PlayQueueRequest = { items: PlayItemRequest[]; startIndex: number; shuffle: boolean;
/**
* Optional context for the queue (album, playlist, or custom)
* Used for remote playback transfer
*/
context?: PlayQueueContext | null }
export type PlayState = { positionTicks?: number | null; canSeek?: boolean | null; isPaused?: boolean | null; isMuted?: boolean | null; volumeLevel?: number | null; repeatMode?: string | null; shuffleMode?: string | null }
/**
* Context information for track playback
*/
export type PlayTracksContext = { type: "playlist"; playlistId: string; playlistName: string } | { type: "search"; searchQuery: string } | { type: "custom"; label: string | null }
/**
* Request to play tracks by ID (backend fetches metadata)
*/
export type PlayTracksRequest = { trackIds: string[]; startIndex: number; shuffle: boolean; context: PlayTracksContext;
/**
* Position (seconds) to resume the starting track from. Used when taking
* over playback from a remote session so we don't restart from 0.
*/
startPosition?: number | null }
/**
* What playback facilities this platform's backend actually provides.
*
* The frontend is presentation-only and must not re-derive backend facts from
* `navigator.userAgent` — that sniffing was a second copy of the same platform
* decision Rust already makes with `cfg!`, and it drifted. These flags are the
* single source of truth; the frontend consumes them.
*
* TRACES: UR-003, UR-005 | DR-004, DR-023, DR-024
*/
export type PlaybackCapabilities = {
/**
* True when audio is rendered by a webview `<audio>` element rather than a
* native backend. Native audio exists on Linux (mpv) and Android
* (ExoPlayer); everything else (Windows, future desktops) uses the webview.
*/
usesWebviewAudio: boolean;
/**
* True when video can be rendered by a native surface composited *behind*
* a transparent webview. Android only: ExoPlayer draws into a SurfaceView
* beneath the WebView. Linux cannot do this (WebKitGTK/Wayland
* compositing), so it stays on the HTML5 element.
*/
supportsNativeVideo: boolean }
/**
* Playback information
*/
export type PlaybackInfo = { mediaSourceId: string; playSessionId: string; streamUrl: string; directPlay: boolean; needsTranscoding: boolean }
/**
* Playback mode - local device, remote session, or idle
*/
export type PlaybackMode = { type: "local" } | { type: "remote"; session_id: string } | { type: "idle" }
/**
* Playback progress info
*/
export type PlaybackProgress = { itemId: string;
/**
* Resume position in milliseconds. Stored as Jellyfin ticks in the DB;
* converted here so the frontend never sees ticks.
*/
positionMs: number; isPlayed: boolean; isFavorite: boolean; playCount: number }
/**
* Represents a media item that can be played
*
* TRACES: UR-003, UR-004 | DR-002
*/
export type PlayerMediaItem = {
/**
* Unique identifier
*/
id: string;
/**
* Display title
*/
title: string;
/**
* Name (alias for title - for frontend compatibility)
*/
name?: string | null;
/**
* Artist name(s) for audio
*/
artist: string | null;
/**
* Album name for audio
*/
album: string | null;
/**
* Album name (alias - for frontend compatibility)
*/
albumName?: string | null;
/**
* Album ID (Jellyfin ID) for remote transfer context
*/
albumId?: string | null;
/**
* Artist items with IDs for clickable links
*/
artistItems?: ArtistItem[] | null;
/**
* Artists as array of strings (fallback when artist_items not available)
*/
artists?: string[] | null;
/**
* Primary image tag for artwork.
*
* Legacy Jellyfin name; being replaced by `image_id` (same value). Dual-carried
* while the frontend migrates (docs/specs/frontend-domain-model.md).
*/
primaryImageTag?: string | null;
/**
* Neutral image identifier the frontend resolves to a URL — replaces
* `primary_image_tag`.
*/
imageId?: string | null;
/**
* Item type (Audio, Movie, Episode, etc.)
*/
type?: string | null;
/**
* Playlist ID (Jellyfin ID) for remote transfer context
*/
playlistId?: string | null;
/**
* Duration in seconds
*/
duration: number | null;
/**
* URL or path to artwork image
*/
artworkUrl: string | null;
/**
* Type of media
*/
mediaType: MediaType;
/**
* Source of the media
*/
source: PlayerMediaSource;
/**
* Video codec (e.g., "h264", "hevc") for video media
*/
videoCodec?: string | null;
/**
* Whether the video requires server-side transcoding
*/
needsTranscoding?: boolean;
/**
* Video width in pixels
*/
videoWidth?: number | null;
/**
* Video height in pixels
*/
videoHeight?: number | null;
/**
* Available subtitle tracks
*/
subtitles?: SubtitleTrack[];
/**
* Series ID (for TV show episodes) - used for series audio preferences
*/
seriesId?: string | null;
/**
* Server ID - used for series audio preferences
*/
serverId?: string | null }
/**
* TRACES: UR-002, UR-003, UR-004, UR-011 | DR-003
*/
export type PlayerMediaSource =
/**
* Streaming from Jellyfin server
*/
{ type: "remote"; stream_url: string; jellyfin_item_id: string } |
/**
* Downloaded/cached locally
*/
{ type: "local"; file_path: string; jellyfin_item_id: string | null } |
/**
* Direct URL (e.g., channel plugins)
*/
{ type: "directurl"; url: string }
/**
* Player state machine (6 states: Idle, Loading, Playing, Paused, Seeking, Error)
*
* TRACES: UR-005 | DR-001
*/
export type PlayerState =
/**
* No media loaded
*/
{ kind: "idle" } |
/**
* Media is being loaded/buffered
*/
{ kind: "loading"; media: PlayerMediaItem } |
/**
* Media is playing
*/
{ kind: "playing"; media: PlayerMediaItem; position: number; duration: number } |
/**
* Media is paused
*/
{ kind: "paused"; media: PlayerMediaItem; position: number; duration: number } |
/**
* Seeking to a new position
*/
{ kind: "seeking"; media: PlayerMediaItem; target: number } |
/**
* An error occurred
*/
{ kind: "error"; media: PlayerMediaItem | null; error: string }
/**
* Response for player state queries
*/
export type PlayerStatus = { state: PlayerState; position: number; duration: number | null; volume: number; muted: boolean; shuffle: boolean; repeat: RepeatMode;
/**
* Backend being used (native = ExoPlayer/libmpv, html5 = fallback)
*/
backend: VideoBackend;
/**
* Whether frontend should render HTML5 video element
*/
useHtml5Element: boolean;
/**
* Media item from either local queue or remote session
*/
mergedMedia: MergedMediaItem | null;
/**
* Playing state from either local player or remote session
*/
mergedIsPlaying: boolean;
/**
* Volume from either local player or remote session (0-1 normalized)
*/
mergedVolume: number }
/**
* Events emitted by the player backend to the frontend via Tauri events.
*
* These are distinct from `PlayerEvent` in state.rs, which handles internal
* state machine transitions.
*
* TRACES: UR-005, UR-019, UR-023, UR-026 | DR-001, DR-028, DR-047
*/
export type PlayerStatusEvent =
/**
* Playback position updated (emitted periodically during playback)
*/
{ type: "position_update"; position: number; duration: number } |
/**
* Player state changed
*/
{ type: "state_changed"; state: string; media_id: string | null } |
/**
* Media has finished loading and is ready to play
*/
{ type: "media_loaded"; duration: number } |
/**
* Playback has ended naturally (reached end of media)
*/
{ type: "playback_ended" } |
/**
* Buffering state changed
*/
{ type: "buffering"; percent: number } |
/**
* An error occurred during playback
*/
{ type: "error"; message: string; recoverable: boolean } |
/**
* Volume changed
*/
{ type: "volume_changed"; volume: number; muted: boolean } |
/**
* Sleep timer state changed
*/
{ type: "sleep_timer_changed"; mode: SleepTimerMode; remaining_seconds: number } |
/**
* Time-based sleep timer expired: playback must stop. The backend stops
* its own (MPV/ExoPlayer) playback, but HTML5 video on Linux plays in the
* webview outside the backend's control — the frontend pauses it on this
* event.
*/
{ type: "sleep_timer_expired" } |
/**
* Show next episode popup with countdown
*/
{ type: "show_next_episode_popup"; current_episode: MediaItem; next_episode: MediaItem; countdown_seconds: number; auto_advance: boolean } |
/**
* Countdown tick (emitted every second during autoplay countdown)
*/
{ type: "countdown_tick"; remaining_seconds: number } |
/**
* Queue changed (items added, removed, reordered, or playback mode changed)
*/
{ type: "queue_changed"; items: PlayerMediaItem[]; current_index: number | null; shuffle: boolean; repeat: RepeatMode; has_next: boolean; has_previous: boolean } |
/**
* Media session changed (activity context changed: Audio/Movie/TvShow/Idle)
*/
{ type: "session_changed"; session: MediaSessionType } |
/**
* Remote sessions updated (for cast/remote control UI)
*/
{ type: "sessions_updated"; sessions: SessionInfo[] } |
/**
* The authoritative playback mode changed in the Rust backend.
*
* The Rust `PlaybackModeManager` is the single source of truth for which
* device playback commands route to (local vs a remote session). The
* frontend keeps a mirror store for the UI; without this event that mirror
* drifts out of sync (e.g. a mode transition happens inside a transfer or a
* local stop that the frontend never learns about), and controls then route
* to the wrong device — the classic "it keeps playing on the remote" bug.
* The frontend reconciles its store to this payload whenever it fires.
*/
{ type: "playback_mode_changed"; mode: string; session_id: string | null } |
/**
* The user asked to disconnect from the remote session and resume locally.
*
* Emitted when the lockscreen Stop button is pressed while casting. The
* frontend owns the two-step remote->local transfer (it must reload the
* media item locally), so the native side only signals intent here.
*/
{ type: "remote_disconnect_requested" } |
/**
* Backend-originated control command targeting the active frontend player
* adapter (the HTML5 <video> that lives in the webview, which Rust cannot
* drive directly). Emitted by control paths like the sleep timer, lockscreen,
* or remote so they can pause/play/seek/stop the webview element.
* `playerEvents.ts` routes this to the active PlayerAdapter via the facade.
*/
{ type: "control_command"; action: string; position: number | null } |
/**
* Ask the frontend webview `<audio>` element to load and play a stream.
*
* Emitted by `WebviewAudioBackend` on platforms with no native audio
* backend (e.g. Windows): audio-only playback is rendered by an `<audio>`
* element in the webview, mirroring how all video already renders through
* the webview `<video>`. The element then reports its state/position back
* through the `player_report_*` commands, so the Rust controller stays the
* single source of truth. Subsequent play/pause/seek/stop reach the element
* via `ControlCommand`.
*/
{ type: "webview_audio_load"; url: string; media_id: string | null; position: number; autoplay: boolean }
/**
* Result of creating a playlist
*
* @req: JA-019 - Get/create/update playlists
*/
export type PlaylistCreatedResult = { id: string }
/**
* Playlist entry — wraps a MediaItem with the Jellyfin PlaylistItemId
* needed for remove/reorder operations (distinct from the media item's ID)
*
* @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
*/
export type PlaylistEntry =
/**
* The underlying media item
*/
({ id: string; name: string;
/**
* Legacy Jellyfin item-type string (`"Audio"`, `"MusicAlbum"`, …).
*
* Dual-carry migration (docs/specs/frontend-domain-model.md): `kind` below
* is the neutral replacement. This field stays while the frontend migrates
* off it, then is removed in a later phase. New Rust code should read
* `kind`, not this.
*/
type: string;
/**
* Provider-neutral classification — the replacement for `item_type`.
* Populated by the Jellyfin mapping; defaults to `Other` for the handful of
* construction sites that have not been migrated yet.
*/
kind?: MediaKind;
/**
* Whether this item is a folder/container (vs a playable leaf). Used to
* decide whether a channel item drills into a list or plays directly.
*/
isFolder?: boolean; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null;
/**
* ISO-8601 release/air date (Jellyfin `PremiereDate`). Used to sort
* podcast episodes by release date.
*/
premiereDate?: string | null; communityRating?: number | null; officialRating?: string | null;
/**
* Legacy Jellyfin duration in ticks (100 ns units). Being replaced by
* `duration_ms`; dual-carried while the frontend migrates
* (docs/specs/frontend-domain-model.md). New code should read `duration_ms`.
*/
runTimeTicks?: number | null;
/**
* Duration in milliseconds — the neutral replacement for `runtime_ticks`.
* Ticks never reach the frontend; this does.
*/
durationMs?: number | null;
/**
* Legacy Jellyfin primary image tag. Being replaced by `image_id`;
* dual-carried while the frontend migrates. New code should read `image_id`.
*/
primaryImageTag?: string | null;
/**
* Neutral image identifier the frontend resolves to a URL via the image
* command — the replacement for `primary_image_tag`. Same value today
* (Jellyfin's tag is the id); the rename removes the provider term.
*/
imageId?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }) & {
/**
* The playlist-scoped entry ID (Jellyfin's PlaylistItemId)
*/
playlistItemId: string }
/**
* Response for preload operation
*/
export type PreloadResult = {
/**
* Number of tracks queued for preload
*/
queuedCount: number;
/**
* Number of tracks already downloaded
*/
alreadyDownloaded: number;
/**
* Number of tracks skipped (no jellyfin ID or other reasons)
*/
skipped: number }
/**
* Response for queue queries
*/
export type QueueStatus = { items: PlayerMediaItem[]; currentIndex: number | null; shuffle: boolean; repeat: RepeatMode; hasNext: boolean; hasPrevious: boolean }
/**
* Remote session status for UI updates
*/
export type RemoteSessionStatus = { position: number; duration: number | null; isPlaying: boolean; nowPlayingItem: NowPlayingItem | null }
/**
* Repeat mode for the queue
*
* TRACES: UR-005 | DR-005
*/
export type RepeatMode = "off" | "all" | "one"
export type ResumeQueuedResult = {
/**
* Rows whose stream URL was resolved and are now pump-eligible.
*/
resolved: number;
/**
* Rows that couldn't be resolved (item metadata / URL lookup failed).
*/
failed: number }
/**
* Options for search queries
*/
export type SearchOptions = { limit?: number | null; includeItemTypes?: string[] | null; searchTerm?: string | null;
/**
* Opaque scope selected by the UI. When set it **wins** over
* `include_item_types`, which remains for the non-search `get_items`
* callers that legitimately request a single concrete type.
*/
scope?: SearchScope | null }
/**
* Search result with pagination
*/
export type SearchResult = { items: MediaItem[]; totalRecordCount: number }
/**
* An opaque search scope the frontend selects; Rust owns what it *means*.
*
* The expansion table below is Jellyfin domain vocabulary: it changes when
* Jellyfin adds or renames an item type, never when the UI is redesigned. It
* previously lived in the frontend (`searchScope.ts`), which is the boundary
* leak documented in docs/specs/scoped-search-boundary.md. The frontend now
* sends the enum and never names an item type in connection with search.
*
* TRACES: UR-049 | DR-063
*/
export type SearchScope = "all" | "music" | "movies" | "tv"
/**
* Security status info
*/
export type SecurityStatus = { usingKeyring: boolean; storageType: string }
/**
* Audio track preference for a series
*/
export type SeriesAudioPreference = { seriesId: string; audioTrackDisplayTitle: string | null; audioTrackLanguage: string | null; audioTrackIndex: number | null }
/**
* Server info returned to frontend
*/
export type ServerInfo = { id: string; name: string; url: string; version: string | null }
/**
* Active session for restoration
*/
export type Session = { userId: string; username: string; serverId: string; serverUrl: string; serverName: string; accessToken: string; verified: boolean; needsReauth: boolean }
/**
* Session information from Jellyfin
*/
export type SessionInfo = { id?: string | null; userId?: string | null; userName?: string | null; client?: string | null; deviceName?: string | null; deviceId?: string | null; applicationVersion?: string | null; isActive?: boolean | null; supportsMediaControl?: boolean | null; supportsRemoteControl?: boolean; nowPlayingItem?: NowPlayingItem | null; playState?: PlayState | null; playableMediaTypes?: string[] | null; supportedCommands?: string[] | null }
/**
* Sleep timer mode - determines when playback should stop
* TRACES: UR-026 | DR-029
*/
export type SleepTimerMode =
/**
* Timer is off
*/
{ kind: "off" } |
/**
* Stop after a specific time duration
*/
{ kind: "time"; endTime: number } |
/**
* Stop at the end of current track
*/
{ kind: "endOfTrack" } |
/**
* Stop after N more episodes complete (TV episodes only, not audio tracks)
*/
{ kind: "episodes"; remaining: number }
/**
* Sleep timer state
*/
export type SleepTimerState = { mode: SleepTimerMode; remainingSeconds: number }
/**
* SmartCache statistics
*/
export type SmartCacheStats = { total_size: number; storage_limit: number; available_space: number; items_count: number; config: CacheConfig }
/**
* Storage statistics for downloads
*/
export type StorageStats = { total_bytes: number; total_items: number; albums: AlbumStorageInfo[] }
/**
* The kind of a media stream within an item (audio track, video track,
* subtitle, …) — provider-neutral, replacing the stringly Jellyfin stream type.
*/
export type StreamKind = "audio" | "video" | "subtitle" |
/**
* Any stream kind we do not model explicitly (e.g. embedded image, data).
*/
"other"
/**
* Response for a mid-playback streaming-quality change.
*
* Mirrors [`AudioTrackSwitchResponse`]: the backend decides whether the caller
* has to reload anything, so no strategy branch lives in the UI.
*
* TRACES: UR-074 | DR-162
*/
export type StreamQualityResponse =
/**
* The native backend was reloaded here; nothing left for the frontend.
*/
{ strategy: "native"; position: number } |
/**
* HTML5 must reload its element with this URL.
*/
{ strategy: "reloadStream"; new_url: string; position: number }
/**
* A ceiling on how much bandwidth a *video* stream may consume.
*
* A quality step is a bundle of concrete transcode parameters — total stream
* ceiling, the audio share of it, and the resolution that ceiling can carry —
* not just a label. Those numbers are Jellyfin encoding domain vocabulary, so
* they live here and the frontend only ever names a variant; the labels the
* picker shows are served over IPC by `player_get_streaming_qualities`.
*
* The ladder is deliberately expressed in bandwidth rather than resolution: it
* exists to fit a connection, and the resolution cap is chosen *from* the
* bitrate so the encoder does not spend a small budget on pixels it cannot
* afford. See docs/specs/streaming-bitrate-cap.md.
*
* TRACES: UR-074 | DR-162
*/
export type StreamingQuality =
/**
* No client-imposed cap — the server may direct-play the source as-is.
*/
"original" | "mbps20" | "mbps10" | "mbps8" | "mbps4" | "mbps2" | "mbps1" | "kbps720"
/**
* Represents a subtitle track
*
* 🔴 **Do not add `#[serde(rename_all = "camelCase")]` here.** This is the one
* struct in the player that deliberately keeps snake_case on the wire, because
* the *same* serialization feeds two consumers that both spell `mime_type`:
*
* * the JNI boundary — `player/android/mod.rs` serializes `MediaItem::subtitles`
* with `serde_json` and hands the string to `JellyTauPlayer.loadWithMetadata`,
* whose parser reads `url`, `language`, `label` and `optString("mime_type")`;
* * the IPC boundary — `PlayItemRequest::subtitles` deserializes this same type
* from the frontend, and the generated binding (`SubtitleTrack` in
* `bindings.ts`) therefore also declares `mime_type`.
*
* Renaming would not break the build and would not fail the IPC: Kotlin's
* `optString` would just fall back to its default MIME type for every track, so
* the failure would be silent. UT-146 asserts the serialized keys.
*
* TRACES: UR-020 | IR-016, JA-008 | UT-146
*/
export type SubtitleTrack = {
/**
* Stream index in the media source
*/
index: number;
/**
* Subtitle URL
*/
url: string;
/**
* Language code (e.g., "eng", "spa")
*/
language: string | null;
/**
* Display title
*/
label: string | null;
/**
* MIME type (e.g., "text/vtt", "application/x-subrip").
* Snake_case on purpose — see the note on the struct.
*/
mime_type: string }
/**
* Sync queue item returned to frontend
*/
export type SyncQueueItem = { id: number; userId: string; operation: string; itemId: string | null; payload: string | null; status: string; retryCount: number; createdAt: string | null; errorMessage: string | null;
/**
* Cached title of the item the operation is about, when the catalog knows
* it. Resolved here rather than by a per-row frontend fetch — the queue
* list is otherwise a wall of opaque ids.
*
* TRACES: UR-025 | DR-132
*/
itemName: string | null }
/**
* Statistics about the thumbnail cache
*/
export type ThumbnailCacheStats = { totalSizeBytes: number; itemCount: number; limitBytes: number }
/**
* User information
*/
export type User = { id: string; name: string; serverId: string; primaryImageTag: string | null }
/**
* User-specific data for an item (playback state, favorites, etc.)
*/
export type UserData = {
/**
* Legacy Jellyfin resume position in ticks. Being replaced by
* `playback_position_ms`; dual-carried while the frontend migrates
* (docs/specs/frontend-domain-model.md). New code should read the ms field.
*/
playbackPositionTicks?: number | null;
/**
* Resume position in milliseconds — the neutral replacement for
* `playback_position_ticks`. Populated from ticks by the mapping; the
* frontend never divides ticks itself.
*/
playbackPositionMs?: number | null; isPlayed?: boolean | null; isFavorite?: boolean | null; playCount?: number | null; lastPlayedDate?: string | null; playbackContextType?: string | null; playbackContextId?: string | null }
/**
* User info returned to frontend
*/
export type UserInfo = { id: string; serverId: string; username: string; isActive: boolean }
/**
* Backend type for video playback
*/
export type VideoBackend =
/**
* Native backend (ExoPlayer on Android, libmpv on Linux)
*/
"native" |
/**
* HTML5 video element fallback
*/
"html5"
/**
* Response for video seek operations
*/
export type VideoSeekResponse =
/**
* Use native seeking (HLS or direct stream)
*/
{ strategy: "native"; position: number } |
/**
* Reload stream from new position (transcoded non-HLS)
*/
{ strategy: "reloadStream"; new_url: string; seek_offset: number }
/**
* Video playback settings
*/
export type VideoSettings = {
/**
* Enable auto-play of next episode (with countdown)
*/
autoPlayNextEpisode: boolean;
/**
* Countdown duration in seconds before auto-play (5-30 seconds)
*/
autoPlayCountdownSeconds: number;
/**
* Maximum number of episodes to auto-play consecutively (0 = unlimited)
*/
autoPlayMaxEpisodes?: number;
/**
* Bandwidth ceiling applied to every video stream.
*
* `#[serde(default)]` so settings JSON persisted before this field existed
* loads as the previous behaviour (uncapped).
*
* TRACES: UR-074 | DR-162
*/
streamingQuality?: StreamingQuality }
/**
* Volume normalization levels matching Spotify's presets
*/
export type VolumeLevel =
/**
* Louder output (-11 LUFS)
*/
"loud" |
/**
* Default level (-14 LUFS)
*/
"normal" |
/**
* Quieter output (-23 LUFS)
*/
"quiet"
/** tauri-specta globals **/
import {
invoke as TAURI_INVOKE,
Channel as TAURI_CHANNEL,
} from "@tauri-apps/api/core";
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
type __EventObj__<T> = {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: null extends T
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
export type Result<T, E> =
| { status: "ok"; data: T }
| { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>,
) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
get: (_, event) => {
const name = mappings[event as keyof T];
return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg),
}),
get: (_, command: keyof __EventObj__<any>) => {
switch (command) {
case "listen":
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
case "once":
return (arg: any) => TAURI_API_EVENT.once(name, arg);
case "emit":
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
}
},
});
},
},
);
}