diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0de099c..38a0ea4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -347,14 +347,10 @@ fn create_player_backend( } } -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - // Initialize logger - env_logger::Builder::from_default_env() - .filter_level(log::LevelFilter::Info) - .init(); - - let builder = Builder::::new() +/// Construct the tauri-specta command builder. Shared by `run()` and the +/// bindings-export test so the TypeScript bindings always match the handler. +fn specta_builder() -> Builder { + Builder::::new() .commands(tauri_specta::collect_commands![ // Player commands player_play_item, @@ -580,12 +576,22 @@ pub fn run() { convert_ticks_to_seconds, calc_progress, convert_percent_to_volume, - ]); + ]) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + // Initialize logger + env_logger::Builder::from_default_env() + .filter_level(log::LevelFilter::Info) + .init(); + + let builder = specta_builder(); // Export TypeScript bindings (types + typed invoke wrappers) in dev builds. #[cfg(debug_assertions)] builder - .export(specta_typescript::Typescript::default(), "../src/lib/api/bindings.ts") + .export(specta_typescript::Typescript::default().bigint(specta_typescript::BigIntExportBehavior::Number), "../src/lib/api/bindings.ts") .expect("Failed to export typescript bindings"); tauri::Builder::default() @@ -835,3 +841,18 @@ pub fn run() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } + + +#[cfg(test)] +mod specta_bindings { + /// Generates `src/lib/api/bindings.ts`. Run with `cargo test export_typescript_bindings`. + #[test] + fn export_typescript_bindings() { + super::specta_builder() + .export( + specta_typescript::Typescript::default().bigint(specta_typescript::BigIntExportBehavior::Number), + "../src/lib/api/bindings.ts", + ) + .expect("failed to export typescript bindings"); + } +} \ No newline at end of file diff --git a/src/lib/api/bindings.ts b/src/lib/api/bindings.ts new file mode 100644 index 0000000..46d214c --- /dev/null +++ b/src/lib/api/bindings.ts @@ -0,0 +1,2992 @@ + +// 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_play_item", { item }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_play_queue", { request }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Play a track from an album - backend fetches all album tracks and builds queue + */ +async playerPlayAlbumTrack(repositoryHandle: string, request: PlayAlbumTrackRequest) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_play_album_track", { repositoryHandle, request }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Play tracks by ID - backend fetches all metadata + */ +async playerPlayTracks(repositoryHandle: string, request: PlayTracksRequest) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_play_tracks", { repositoryHandle, request }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerPlay() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_play") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerPause() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_pause") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerToggle() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_toggle") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerStop() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_stop") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerNext() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_next") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerPrevious() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_previous") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerSeek(position: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_seek", { position }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_seek_video", { repositoryHandle, position, mediaSourceId, audioStreamIndex, useHtml5 }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerSetVolume(volume: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_set_volume", { volume }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerToggleMute() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_toggle_mute") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerSetAudioTrack(streamIndex: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_set_audio_track", { streamIndex }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_switch_audio_track", { repositoryHandle, streamIndex, arrayIndex, useHtml5, currentPosition, mediaSourceId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerSetSubtitleTrack(streamIndex: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_set_subtitle_track", { streamIndex }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerToggleShuffle() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_toggle_shuffle") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerCycleRepeat() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_cycle_repeat") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerGetStatus() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_get_status") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerGetQueue() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_get_queue") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerAddToQueue(request: AddToQueueRequest) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_add_to_queue", { request }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Add a track to queue by ID - backend fetches metadata and constructs URLs + */ +async playerAddTrackById(repositoryHandle: string, request: AddTrackByIdRequest) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_add_track_by_id", { repositoryHandle, request }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Add multiple tracks to queue by IDs - backend fetches metadata and constructs URLs + */ +async playerAddTracksByIds(repositoryHandle: string, request: AddTracksByIdsRequest) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_add_tracks_by_ids", { repositoryHandle, request }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerRemoveFromQueue(index: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_remove_from_queue", { index }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerMoveInQueue(fromIndex: number, toIndex: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_move_in_queue", { fromIndex, toIndex }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerSkipTo(index: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_skip_to", { index }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerSetAudioSettings(settings: AudioSettings) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_set_audio_settings", { settings }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerGetAudioSettings() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_get_audio_settings") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerSetVideoSettings(settings: VideoSettings) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_set_video_settings", { settings }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playerGetVideoSettings() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_get_video_settings") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Set sleep timer mode + */ +async playerSetSleepTimer(mode: SleepTimerMode) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_set_sleep_timer", { mode }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Cancel sleep timer + */ +async playerCancelSleepTimer() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_cancel_sleep_timer") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get current sleep timer state + */ +async playerGetSleepTimer() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_get_sleep_timer") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get autoplay settings + */ +async playerGetAutoplaySettings() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_get_autoplay_settings") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Set autoplay settings and persist to database + */ +async playerSetAutoplaySettings(userId: string, settings: AutoplaySettings) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_set_autoplay_settings", { userId, settings }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Cancel active autoplay countdown + */ +async playerCancelAutoplayCountdown() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_cancel_autoplay_countdown") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Play next episode (user confirmed from popup) + */ +async playerPlayNextEpisode(item: PlayItemRequest) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_play_next_episode", { item }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_on_playback_ended", { itemId, repositoryHandle }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_preload_upcoming", { userId, downloadBasePath }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Update SmartCache configuration + */ +async playerSetCacheConfig(config: CacheConfig) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_set_cache_config", { config }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get current SmartCache configuration + */ +async playerGetCacheConfig() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_get_cache_config") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Configure Jellyfin API client for automatic playback reporting + */ +async playerConfigureJellyfin(serverUrl: string, accessToken: string, userId: string, deviceId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_configure_jellyfin", { serverUrl, accessToken, userId, deviceId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Disable Jellyfin automatic playback reporting + */ +async playerDisableJellyfin() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_disable_jellyfin") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get the current media session state + */ +async playerGetSession() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_get_session") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Dismiss the current media session (returns to Idle) + */ +async playerDismissSession() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("player_dismiss_session") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Play items on a remote Jellyfin session (casting) + */ +async remotePlayOnSession(sessionId: string, itemIds: string[], startIndex: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("remote_play_on_session", { sessionId, itemIds, startIndex }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Send a playback command to a remote session + */ +async remoteSendCommand(sessionId: string, command: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("remote_send_command", { sessionId, command }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Seek on a remote session + */ +async remoteSessionSeek(sessionId: string, positionTicks: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("remote_session_seek", { sessionId, positionTicks }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Set volume on a remote session + */ +async remoteSessionSetVolume(sessionId: string, volume: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("remote_session_set_volume", { sessionId, volume }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Toggle mute on a remote session + */ +async remoteSessionToggleMute(sessionId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("remote_session_toggle_mute", { sessionId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Set polling frequency hint based on UI state + */ +async sessionsSetPollingHint(hint: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sessions_set_polling_hint", { hint }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Manually trigger a session poll (for refresh button) + */ +async sessionsPollNow() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sessions_poll_now") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get the current playback mode + */ +async playbackModeGetCurrent() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_mode_get_current") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Set the playback mode (internal/testing use) + */ +async playbackModeSet(mode: PlaybackMode) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_mode_set", { mode }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Check if currently transferring between playback modes + */ +async playbackModeIsTransferring() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_mode_is_transferring") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Transfer playback from local device to a remote Jellyfin session + */ +async playbackModeTransferToRemote(sessionId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_mode_transfer_to_remote", { sessionId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get remote session status (for polling position/duration) + */ +async playbackModeGetRemoteStatus() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_mode_get_remote_status") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_mode_transfer_to_local", { currentItemId, positionTicks }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Initialize playback reporter (called after login) + */ +async playbackReporterInit(serverUrl: string, userId: string, accessToken: string, deviceId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_reporter_init", { serverUrl, userId, accessToken, deviceId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Destroy playback reporter (called on logout) + */ +async playbackReporterDestroy() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_reporter_destroy") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Report playback start + */ +async playbackReportStart(itemId: string, positionSeconds: number, contextType: string | null, contextId: string | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_report_start", { itemId, positionSeconds, contextType, contextId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Report playback progress + */ +async playbackReportProgress(itemId: string, positionSeconds: number, isPaused: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_report_progress", { itemId, positionSeconds, isPaused }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Report playback stopped + */ +async playbackReportStopped(itemId: string, positionSeconds: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_report_stopped", { itemId, positionSeconds }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark item as played + */ +async playbackMarkPlayed(itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playback_mark_played", { itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Initialize the auth manager (call on app startup) + * Restores session from storage if available + */ +async authInitialize() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_initialize") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Connect to a Jellyfin server and get server info + */ +async authConnectToServer(serverUrl: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_connect_to_server", { serverUrl }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Login with username and password + */ +async authLogin(serverUrl: string, username: string, password: string, deviceId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_login", { serverUrl, username, password, deviceId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Verify current session + */ +async authVerifySession(serverUrl: string, userId: string, accessToken: string, deviceId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_verify_session", { serverUrl, userId, accessToken, deviceId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Logout (clear session and call Jellyfin logout endpoint) + */ +async authLogout(serverUrl: string, accessToken: string, deviceId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_logout", { serverUrl, accessToken, deviceId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get current session + */ +async authGetSession() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_get_session") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Set current session (for restoration from storage) + */ +async authSetSession(session: Session | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_set_session", { session }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Start background session verification + */ +async authStartVerification(deviceId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_start_verification", { deviceId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Stop background session verification + */ +async authStopVerification() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_stop_verification") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Re-authenticate with password (when session expired) + */ +async authReauthenticate(password: string, deviceId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("auth_reauthenticate", { password, deviceId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("device_get_id") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("device_set_id", { deviceId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Check if the server is currently reachable + */ +async connectivityCheckServer() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("connectivity_check_server") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Set the server URL and trigger an immediate check + */ +async connectivitySetServerUrl(url: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("connectivity_set_server_url", { url }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get the current connectivity status + */ +async connectivityGetStatus() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("connectivity_get_status") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Start monitoring connectivity with adaptive polling + */ +async connectivityStartMonitoring() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("connectivity_start_monitoring") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Stop monitoring connectivity + */ +async connectivityStopMonitoring() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("connectivity_stop_monitoring") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark the server as reachable (called after successful API calls) + */ +async connectivityMarkReachable() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("connectivity_mark_reachable") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark the server as unreachable (called after failed API calls) + */ +async connectivityMarkUnreachable(error: string | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("connectivity_mark_unreachable", { error }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Initialize the database and run migrations + */ +async storageInit() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_init") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get storage directory path (parent directory of the database file) + */ +async storageGetPath() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_path") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get database file size in bytes + */ +async storageGetSize() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_size") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get security status (keyring vs encrypted file fallback) + */ +async storageGetSecurityStatus() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_security_status") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_save_server", { id, name, url, version }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get all saved servers + */ +async storageGetServers() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_servers") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Delete a server and all associated data + */ +async storageDeleteServer(serverId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_delete_server", { serverId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Save a user account (token stored in secure storage, not database) + */ +async storageSaveUser(id: string, serverId: string, username: string, accessToken: string | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_save_user", { id, serverId, username, accessToken }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get users for a server + */ +async storageGetUsers(serverId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_users", { serverId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Set a user as active (and deactivate all other users globally) + */ +async storageSetActiveUser(userId: string, serverId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_set_active_user", { userId, serverId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get the active user for a server + */ +async storageGetActiveUser(serverId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_active_user", { serverId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get the active session (user + server + token) for session restoration + */ +async storageGetActiveSession() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_active_session") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get user's access token from secure storage + */ +async storageGetAccessToken(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_access_token", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Delete a user account and their token from secure storage + */ +async storageDeleteUser(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_delete_user", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_update_playback_progress", { userId, itemId, positionTicks }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_update_playback_context", { userId, itemId, positionTicks, contextType, contextId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark item as played in local database + */ +async storageMarkPlayed(userId: string, itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_mark_played", { userId, itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get playback progress for an item + */ +async storageGetPlaybackProgress(userId: string, itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_playback_progress", { userId, itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark pending sync as completed for an item + */ +async storageMarkSynced(userId: string, itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_mark_synced", { userId, itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_toggle_favorite", { userId, itemId, isFavorite }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Queue a media item for download + */ +async downloadItem(request: DownloadItemRequest) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("download_item", { request }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Queue and start a download in a single atomic operation + * This simplifies the frontend flow by combining multiple steps + */ +async downloadItemAndStart(request: DownloadItemAndStartRequest) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("download_item_and_start", { request }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Queue an entire album for download + */ +async downloadAlbum(albumId: string, userId: string, basePath: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("download_album", { albumId, userId, basePath }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Queue a video item (movie or episode) for download with quality preset + */ +async downloadVideo(request: DownloadVideoRequest) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("download_video", { request }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Queue all episodes of a series for download + */ +async downloadSeries(seriesId: string, seriesName: string, userId: string, basePath: string, qualityPreset: string | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("download_series", { seriesId, seriesName, userId, basePath, qualityPreset }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("download_season", { seasonId, seriesName, seasonName, seasonNumber, userId, basePath, qualityPreset }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get all downloads for a user, optionally filtered by status + */ +async getDownloads(userId: string, statusFilter: string[] | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_downloads", { userId, statusFilter }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Pause a download + */ +async pauseDownload(downloadId: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("pause_download", { downloadId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Resume a paused download + */ +async resumeDownload(downloadId: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("resume_download", { downloadId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Cancel a download + */ +async cancelDownload(downloadId: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("cancel_download", { downloadId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Delete a completed download + */ +async deleteDownload(downloadId: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("delete_download", { downloadId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Delete all downloads for a user + */ +async deleteAllDownloads(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("delete_all_downloads", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Delete all downloads for a specific album + */ +async deleteAlbumDownloads(albumId: string, userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("delete_album_downloads", { albumId, userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Clear all stale pending/failed/paused downloads + */ +async clearStaleDownloads(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("clear_stale_downloads", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get storage statistics for downloads + */ +async getDownloadStorageStats(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_download_storage_stats", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark a download as completed + */ +async markDownloadCompleted(downloadId: number, bytesDownloaded: number, filePath: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("mark_download_completed", { downloadId, bytesDownloaded, filePath }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark a download as failed + */ +async markDownloadFailed(downloadId: number, errorMessage: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("mark_download_failed", { downloadId, errorMessage }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Start downloading a file immediately + * This command actually downloads the file using the worker + */ +async startDownload(downloadId: number, streamUrl: string, targetDir: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("start_download", { downloadId, streamUrl, targetDir }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get download manager statistics + */ +async getDownloadManagerStats() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_download_manager_stats") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Set the maximum concurrent downloads + */ +async setMaxConcurrentDownloads(max: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_max_concurrent_downloads", { max }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get SmartCache statistics + */ +async getSmartCacheStats(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_smart_cache_stats", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Update SmartCache configuration + */ +async updateSmartCacheConfig(config: CacheConfig) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("update_smart_cache_config", { config }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get SmartCache configuration + */ +async getSmartCacheConfig() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_smart_cache_config") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get album recommendations based on play history + */ +async getAlbumRecommendations(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_album_recommendations", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get album affinity status for all tracked albums + * This shows the SmartCache's internal play history and threshold status + */ +async getAlbumAffinityStatus() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_album_affinity_status") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Pin an item's metadata (protects from cache clear) + */ +async pinItem(itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("pin_item", { itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Unpin an item's metadata + */ +async unpinItem(itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("unpin_item", { itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Check if an item is pinned + */ +async isItemPinned(itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_item_pinned", { itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Check if an item is available offline + */ +async offlineIsAvailable(itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("offline_is_available", { itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get all offline items for a user + */ +async offlineGetItems(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("offline_get_items", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Search offline items + */ +async offlineSearch(userId: string, query: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("offline_search", { userId, query }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get cached libraries for a server + */ +async storageGetLibraries(serverId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_libraries", { serverId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_items", { serverId, parentId, libraryId, itemType, limit, offset }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get a single cached item by ID + */ +async storageGetItem(itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_item", { itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Search cached items using FTS + */ +async storageSearchItems(serverId: string, query: string, limit: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_search_items", { serverId, query, limit }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Save a library to the cache + */ +async storageSaveLibrary(id: string, serverId: string, name: string, collectionType: string | null, imageTag: string | null, sortOrder: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_save_library", { id, serverId, name, collectionType, imageTag, sortOrder }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Save an item to the cache + */ +async storageSaveItem(item: CachedItem, serverId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_save_item", { item, serverId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get count of pending sync operations for a user + */ +async storageGetPendingSyncCount(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_pending_sync_count", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Queue a mutation for sync to server + */ +async syncQueueMutation(userId: string, operation: string, itemId: string | null, payload: string | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sync_queue_mutation", { userId, operation, itemId, payload }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get all pending sync operations for a user + */ +async syncGetPending(userId: string, limit: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sync_get_pending", { userId, limit }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark a sync operation as in progress + */ +async syncMarkProcessing(id: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sync_mark_processing", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark a sync operation as completed + */ +async syncMarkCompleted(id: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sync_mark_completed", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark a sync operation as failed with error message + */ +async syncMarkFailed(id: number, error: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sync_mark_failed", { id, error }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get count of pending sync operations for a user + */ +async syncGetPendingCount(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sync_get_pending_count", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Delete completed sync operations older than specified days + */ +async syncCleanupCompleted(daysOld: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sync_cleanup_completed", { daysOld }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Delete all sync operations for a user (used during logout) + */ +async syncClearUser(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sync_clear_user", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("thumbnail_get_cached", { itemId, imageType, tag }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("thumbnail_save", { itemId, imageType, tag, url }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get thumbnail cache statistics + */ +async thumbnailGetStats() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("thumbnail_get_stats") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Set thumbnail cache storage limit in bytes + */ +async thumbnailSetLimit(limitBytes: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("thumbnail_set_limit", { limitBytes }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Clear all cached thumbnails + */ +async thumbnailClearCache() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("thumbnail_clear_cache") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Delete cached thumbnails for a specific item + */ +async thumbnailDeleteItem(itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("thumbnail_delete_item", { itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("image_get_url", { repositoryHandle, request }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Save a person to the cache + */ +async storageSavePerson(person: CachedPerson) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_save_person", { person }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get a cached person by ID + */ +async storageGetPerson(personId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_person", { personId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Save item-person associations (batch) + */ +async storageSaveItemPeople(associations: CachedItemPerson[]) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_save_item_people", { associations }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get people for an item (with person details joined) + */ +async storageGetItemPeople(itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_item_people", { itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_save_series_audio_preference", { userId, seriesId, serverId, audioTrackDisplayTitle, audioTrackLanguage, audioTrackIndex }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get user's preferred audio track for a series + */ +async storageGetSeriesAudioPreference(userId: string, seriesId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("storage_get_series_audio_preference", { userId, seriesId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Create a new repository instance + * Returns a handle (UUID) for accessing the repository + */ +async repositoryCreate(serverUrl: string, userId: string, accessToken: string, serverId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_create", { serverUrl, userId, accessToken, serverId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Destroy a repository instance + */ +async repositoryDestroy(handle: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_destroy", { handle }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get libraries + */ +async repositoryGetLibraries(handle: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_libraries", { handle }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get items in a container (library, folder, album, etc.) + */ +async repositoryGetItems(handle: string, parentId: string, options: GetItemsOptions | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_items", { handle, parentId, options }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get a single item by ID + */ +async repositoryGetItem(handle: string, itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_item", { handle, itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get latest items in a library + */ +async repositoryGetLatestItems(handle: string, parentId: string, limit: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_latest_items", { handle, parentId, limit }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get resume items (continue watching/listening) + */ +async repositoryGetResumeItems(handle: string, parentId: string | null, limit: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_resume_items", { handle, parentId, limit }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get next up episodes + */ +async repositoryGetNextUpEpisodes(handle: string, seriesId: string | null, limit: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_next_up_episodes", { handle, seriesId, limit }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get recently played audio + */ +async repositoryGetRecentlyPlayedAudio(handle: string, limit: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_recently_played_audio", { handle, limit }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get resume movies + */ +async repositoryGetResumeMovies(handle: string, limit: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_resume_movies", { handle, limit }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get genres for a library + */ +async repositoryGetGenres(handle: string, parentId: string | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_genres", { handle, parentId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Search for items + */ +async repositorySearch(handle: string, query: string, options: SearchOptions | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_search", { handle, query, options }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get playback info for an item + */ +async repositoryGetPlaybackInfo(handle: string, itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_playback_info", { handle, itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get video stream URL with optional seeking support + */ +async repositoryGetVideoStreamUrl(handle: string, itemId: string, mediaSourceId: string | null, startTimeSeconds: number | null, audioStreamIndex: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_video_stream_url", { handle, itemId, mediaSourceId, startTimeSeconds, audioStreamIndex }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get audio stream URL for a track + */ +async repositoryGetAudioStreamUrl(handle: string, itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_audio_stream_url", { handle, itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Report playback start + */ +async repositoryReportPlaybackStart(handle: string, itemId: string, positionTicks: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_report_playback_start", { handle, itemId, positionTicks }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Report playback progress + */ +async repositoryReportPlaybackProgress(handle: string, itemId: string, positionTicks: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_report_playback_progress", { handle, itemId, positionTicks }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Report playback stopped + */ +async repositoryReportPlaybackStopped(handle: string, itemId: string, positionTicks: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_report_playback_stopped", { handle, itemId, positionTicks }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get image URL for an item + */ +async repositoryGetImageUrl(handle: string, itemId: string, imageType: ImageType, options: ImageOptions | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_image_url", { handle, itemId, imageType, options }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Mark an item as favorite + */ +async repositoryMarkFavorite(handle: string, itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_mark_favorite", { handle, itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Unmark an item as favorite + */ +async repositoryUnmarkFavorite(handle: string, itemId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_unmark_favorite", { handle, itemId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get person details + */ +async repositoryGetPerson(handle: string, personId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_person", { handle, personId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get items by person (actor, director, etc.) + */ +async repositoryGetItemsByPerson(handle: string, personId: string, options: GetItemsOptions | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_items_by_person", { handle, personId, options }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get similar/related items for a media item + */ +async repositoryGetSimilarItems(handle: string, itemId: string, limit: number | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repository_get_similar_items", { handle, itemId, limit }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Create a new playlist + */ +async playlistCreate(handle: string, name: string, itemIds: string[] | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playlist_create", { handle, name, itemIds }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Delete a playlist + */ +async playlistDelete(handle: string, playlistId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playlist_delete", { handle, playlistId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Rename a playlist + */ +async playlistRename(handle: string, playlistId: string, name: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playlist_rename", { handle, playlistId, name }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Get playlist items with PlaylistItemId + */ +async playlistGetItems(handle: string, playlistId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playlist_get_items", { handle, playlistId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Add items to a playlist + */ +async playlistAddItems(handle: string, playlistId: string, itemIds: string[]) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playlist_add_items", { handle, playlistId, itemIds }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Remove items from a playlist (uses PlaylistItemId entry IDs, NOT media item IDs) + */ +async playlistRemoveItems(handle: string, playlistId: string, entryIds: string[]) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playlist_remove_items", { handle, playlistId, entryIds }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Move a playlist item to a new position + */ +async playlistMoveItem(handle: string, playlistId: string, itemId: string, newIndex: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("playlist_move_item", { handle, playlistId, itemId, newIndex }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * 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 { + 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 { + 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 { + 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 { + 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 { + return await TAURI_INVOKE("convert_percent_to_volume", { percent }); +} +} + +/** user-defined events **/ + + + +/** 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 } +/** + * 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 } +/** + * 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" +/** + * Library (media collection) + */ +export type Library = { id: string; name: string; collectionType: string; imageTag?: string | null } +/** + * Media item + */ +export type MediaItem = { id: string; name: string; type: string; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | 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 } +/** + * Represents a media item that can be played + * + * TRACES: UR-003, UR-004 | DR-002 + */ +export type MediaItem = { +/** + * 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: MediaSource; +/** + * 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 } +/** + * 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: MediaItem | null; is_active: boolean } | +/** + * Movie playback (single video, auto-dismiss on end) + */ +{ type: "movie"; item: MediaItem; is_active: boolean } | +/** + * TV show playback (supports next episode auto-advance) + */ +{ type: "tv_show"; item: MediaItem; 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 } +/** + * TRACES: UR-002, UR-003, UR-004, UR-011 | DR-003 + */ +export type MediaSource = +/** + * 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 } +/** + * 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 } +/** + * 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 } +/** + * 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: MediaItem } | +/** + * Media is playing + */ +{ kind: "playing"; media: MediaItem; position: number; duration: number } | +/** + * Media is paused + */ +{ kind: "paused"; media: MediaItem; position: number; duration: number } | +/** + * Seeking to a new position + */ +{ kind: "seeking"; media: MediaItem; target: number } | +/** + * An error occurred + */ +{ kind: "error"; media: MediaItem | 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 } +/** + * 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; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | 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: MediaItem[]; 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 } +/** + * Server information returned from Jellyfin + */ +export type ServerInfo = { name: string; version: string; id: string; +/** + * Normalized server URL with protocol and no trailing slash + */ +normalizedUrl: string } +/** + * 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__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + 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__) => { + 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); + } + }, + }); + }, + }, + ); +}