jellytau/src/lib/api/bindings.ts
Duncan Tourolle a64e1b1fb4 Introduce PlayerAdapter contract; decision logic shared in Rust backend
Establish a decoupled player boundary so UI and backend interact with video
through one contract, with the HTML5 (Linux/interim-Android) and native
(ExoPlayer) providers as interchangeable primitive-executor adapters.

- PlayerAdapter interface + AdapterHost callback bag (adapters/types.ts): the
  adapter owns only decision-free element PRIMITIVES (seekElement, reloadSource,
  play/pause, setVolume, selectSubtitle); it never branches on strategy.
- Seek/audio-track DECISIONS stay in Rust (player_seek_video / _switch_audio_track
  return a strategy); the facade dispatches the chosen primitive to the active
  adapter. Both providers share the one decision path — logic lives once, in Rust.
- Facade holds the active adapter; a new ControlCommand PlayerStatusEvent lets
  backend control (lockscreen/remote/sleep) drive the webview <video> element.
- Html5PlayerAdapter resolves the LIVE element via the bridge (fixes play/pause
  silently no-opping when the element was re-bound).
- Do not emit a "stopped" player state on natural end-of-video: it flipped the
  player/mode to idle mid-handoff and suppressed next-episode auto-advance under
  a sleep timer. Jellyfin progress reporting is preserved; the backend's
  on_video_playback_ended owns the transition.
- VideoPlayer net -300 lines (strategy/HLS-reload logic relocated to the adapter).
- Adds 20 adapter unit tests; existing suites stay green (vitest 457, cargo 416).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:56:20 +02:00

2290 lines
80 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 });
},
/**
* 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");
},
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
*/
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 });
},
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");
},
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");
},
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");
},
/**
* 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
*/
async playerOnPlaybackEnded(itemId: string | null, repositoryHandle: string | null) : Promise<null> {
return await TAURI_INVOKE("player_on_playback_ended", { itemId, repositoryHandle });
},
/**
* 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 });
},
/**
* 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, positionTicks: number) : Promise<null> {
return await TAURI_INVOKE("storage_update_playback_progress", { userId, itemId, positionTicks });
},
/**
* 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, positionTicks: number, contextType: string | null, contextId: string | null) : Promise<null> {
return await TAURI_INVOKE("storage_update_playback_context", { userId, itemId, positionTicks, 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 });
},
/**
* 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
*/
async downloadAlbum(albumId: string, userId: string, basePath: string) : Promise<number[]> {
return await TAURI_INVOKE("download_album", { 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
*/
async pauseDownload(downloadId: number) : Promise<null> {
return await TAURI_INVOKE("pause_download", { downloadId });
},
/**
* Resume a paused download
*/
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 });
},
/**
* 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 });
},
/**
* 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 });
},
/**
* 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");
},
/**
* 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 });
},
/**
* 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 });
},
/**
* 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)
*/
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
*/
async repositoryGetNextUpEpisodes(handle: string, seriesId: string | null, limit: number | null) : Promise<MediaItem[]> {
return await TAURI_INVOKE("repository_get_next_up_episodes", { handle, seriesId, limit });
},
/**
* 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
*/
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 video stream URL with optional seeking support
*/
async repositoryGetVideoStreamUrl(handle: string, itemId: string, mediaSourceId: string | null, startTimeSeconds: number | null, audioStreamIndex: number | null) : Promise<string> {
return await TAURI_INVOKE("repository_get_video_stream_url", { handle, itemId, mediaSourceId, startTimeSeconds, 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 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, positionTicks: number) : Promise<null> {
return await TAURI_INVOKE("repository_report_playback_start", { handle, itemId, positionTicks });
},
/**
* Report playback progress
*/
async repositoryReportPlaybackProgress(handle: string, itemId: string, positionTicks: number) : Promise<null> {
return await TAURI_INVOKE("repository_report_playback_progress", { handle, itemId, positionTicks });
},
/**
* Report playback stopped
*/
async repositoryReportPlaybackStopped(handle: string, itemId: string, positionTicks: number) : Promise<null> {
return await TAURI_INVOKE("repository_report_playback_stopped", { handle, itemId, positionTicks });
},
/**
* 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 });
},
/**
* 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 }
/**
* 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 }
/**
* 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 }
/**
* 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 }
/**
* 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 }
/**
* 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 }
/**
* 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 }
/**
* 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; type: string;
/**
* 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; runTimeTicks?: number | null; primaryImageTag?: 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 }
/**
* 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 = { type: string; codec?: string | null; language?: string | null; displayTitle?: string | null; index: number; isDefault: boolean; isForced: boolean }
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; mediaType: string }
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 }
/**
* 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 }
/**
* 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; positionTicks: 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
*/
primaryImageTag?: 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 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 }
/**
* 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; type: string;
/**
* 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; runTimeTicks?: number | null; primaryImageTag?: 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"
/**
* Options for search queries
*/
export type SearchOptions = { limit?: number | null; includeItemTypes?: string[] | null; searchTerm?: string | null }
/**
* Search result with pagination
*/
export type SearchResult = { items: MediaItem[]; totalRecordCount: number }
/**
* 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[] }
/**
* Represents a subtitle track
*/
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")
*/
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 }
/**
* 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 = { playbackPositionTicks?: 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 }
/**
* 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);
}
},
});
},
},
);
}