
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.

/** user-defined commands **/


export const commands = {
/**
 * Play a single media item (audio or video)
 * 
 * Accepts a PlayItemRequest with all optional fields properly defaulted.
 * This avoids Tauri's Android serialization issues with complex objects.
 * 
 * @req: UR-003 - Play videos
 * @req: UR-004 - Play audio uninterrupted
 * @req: UR-005 - Control media playback (play operation)
 * @req: DR-009 - Audio player UI
 */
async playerPlayItem(item: PlayItemRequest) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_play_item", { item });
},
/**
 * Enter background-audio mode: hand playback of the currently-watched video off
 * to the native ExoPlayer *audio* path so the audio keeps playing while the app
 * is backgrounded/locked, with no client-side video decode (UR-040).
 * 
 * `stream_url` MUST be an audio-only URL (see
 * `get_audio_only_stream_url_for_video`). The item is created as
 * `MediaType::Audio` so it starts an audio session and loads into the native
 * backend with `mediaType="audio"` — the WebView `<video>` is torn down on the
 * frontend side, so exactly one audio source is ever active.
 * 
 * This deliberately goes through the queue-based `play_item` path (NOT a
 * side-channel) so end-of-track lands in `on_playback_ended`, which already
 * honors the sleep timer (Time/Episodes/EndOfTrack) and drives autoplay-next.
 * The sleep-timer state is intentionally left untouched by the handoff.
 * 
 * TRACES: UR-040 | DR-052 | UT-061, IT-013
 */
async playerEnterBackgroundAudio(item: PlayItemRequest, positionSeconds: number) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_enter_background_audio", { item, positionSeconds });
},
/**
 * Exit background-audio mode: stop the native audio player and return its final
 * position so the frontend can reload the WebView `<video>` there (UR-040).
 * 
 * Returns the position in seconds. The sleep timer is intentionally left
 * untouched — if it fired while backgrounded, playback is already stopped and
 * this simply reports the last position.
 * 
 * TRACES: UR-040 | DR-052 | UT-061, IT-013
 */
async playerExitBackgroundAudio() : Promise<number> {
    return await TAURI_INVOKE("player_exit_background_audio");
},
/**
 * Play a queue of media items
 * 
 * @req: UR-004 - Play audio uninterrupted
 * @req: UR-005 - Control media playback (queue playback)
 * @req: UR-015 - View and manage current audio queue
 * @req: DR-005 - Queue manager with shuffle, repeat, history
 */
async playerPlayQueue(request: PlayQueueRequest) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_play_queue", { request });
},
/**
 * Play a track from an album - backend fetches all album tracks and builds queue
 */
async playerPlayAlbumTrack(repositoryHandle: string, request: PlayAlbumTrackRequest) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_play_album_track", { repositoryHandle, request });
},
/**
 * Play tracks by ID - backend fetches all metadata
 */
async playerPlayTracks(repositoryHandle: string, request: PlayTracksRequest) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_play_tracks", { repositoryHandle, request });
},
async playerPlay() : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_play");
},
async playerPause() : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_pause");
},
async playerToggle() : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_toggle");
},
async playerStop() : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_stop");
},
async playerNext() : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_next");
},
async playerPrevious() : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_previous");
},
async playerSeek(position: number) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_seek", { position });
},
/**
 * Smart video seeking that decides between native and server-side seeking
 * 
 * This command analyzes the current video stream and automatically chooses
 * the best seeking strategy:
 * - HLS streams (.m3u8): Use native seeking
 * - Direct play streams: Use native seeking
 * - Transcoded non-HLS: Request new stream URL from server starting at seek position
 * 
 * For native (non-HTML5) backends, this command handles the entire stream reload
 * internally. For HTML5 backends, it returns the new URL for the frontend to handle.
 */
async playerSeekVideo(repositoryHandle: string, position: number, mediaSourceId: string | null, audioStreamIndex: number | null, useHtml5: boolean) : Promise<VideoSeekResponse> {
    return await TAURI_INVOKE("player_seek_video", { repositoryHandle, position, mediaSourceId, audioStreamIndex, useHtml5 });
},
async playerSetVolume(volume: number) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_set_volume", { volume });
},
async playerToggleMute() : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_toggle_mute");
},
async playerSetAudioTrack(streamIndex: number) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_set_audio_track", { streamIndex });
},
/**
 * Switch audio track - handles both HTML5 (stream reload) and native (direct switch)
 * Note: Frontend should handle saving series preferences after this command succeeds
 */
async playerSwitchAudioTrack(repositoryHandle: string, streamIndex: number, arrayIndex: number, useHtml5: boolean, currentPosition: number | null, mediaSourceId: string | null) : Promise<AudioTrackSwitchResponse> {
    return await TAURI_INVOKE("player_switch_audio_track", { repositoryHandle, streamIndex, arrayIndex, useHtml5, currentPosition, mediaSourceId });
},
async playerSetSubtitleTrack(streamIndex: number | null) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_set_subtitle_track", { streamIndex });
},
async playerToggleShuffle() : Promise<QueueStatus> {
    return await TAURI_INVOKE("player_toggle_shuffle");
},
async playerCycleRepeat() : Promise<QueueStatus> {
    return await TAURI_INVOKE("player_cycle_repeat");
},
async playerGetStatus() : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_get_status");
},
async playerGetQueue() : Promise<QueueStatus> {
    return await TAURI_INVOKE("player_get_queue");
},
/**
 * Report this platform's playback capabilities to the frontend.
 * 
 * TRACES: UR-003, UR-005 | DR-004, DR-023, DR-024
 */
async playerGetCapabilities() : Promise<PlaybackCapabilities> {
    return await TAURI_INVOKE("player_get_capabilities");
},
async playerAddToQueue(request: AddToQueueRequest) : Promise<QueueStatus> {
    return await TAURI_INVOKE("player_add_to_queue", { request });
},
/**
 * Add a track to queue by ID - backend fetches metadata and constructs URLs
 */
async playerAddTrackById(repositoryHandle: string, request: AddTrackByIdRequest) : Promise<QueueStatus> {
    return await TAURI_INVOKE("player_add_track_by_id", { repositoryHandle, request });
},
/**
 * Add multiple tracks to queue by IDs - backend fetches metadata and constructs URLs
 */
async playerAddTracksByIds(repositoryHandle: string, request: AddTracksByIdsRequest) : Promise<QueueStatus> {
    return await TAURI_INVOKE("player_add_tracks_by_ids", { repositoryHandle, request });
},
async playerRemoveFromQueue(index: number) : Promise<QueueStatus> {
    return await TAURI_INVOKE("player_remove_from_queue", { index });
},
async playerMoveInQueue(fromIndex: number, toIndex: number) : Promise<QueueStatus> {
    return await TAURI_INVOKE("player_move_in_queue", { fromIndex, toIndex });
},
async playerSkipTo(index: number) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_skip_to", { index });
},
async playerSetAudioSettings(settings: AudioSettings) : Promise<AudioSettings> {
    return await TAURI_INVOKE("player_set_audio_settings", { settings });
},
async playerGetAudioSettings() : Promise<AudioSettings> {
    return await TAURI_INVOKE("player_get_audio_settings");
},
/**
 * The built-in equalizer presets and their per-band gain curves (dB), for the
 * settings UI. The curve numbers are domain data defined by the band layout,
 * so the frontend reads them here rather than encoding them.
 * 
 * TRACES: UR-027 | DR-030
 */
async playerGetEqPresets() : Promise<([EqPreset, number[]])[]> {
    return await TAURI_INVOKE("player_get_eq_presets");
},
async playerSetVideoSettings(settings: VideoSettings) : Promise<VideoSettings> {
    return await TAURI_INVOKE("player_set_video_settings", { settings });
},
async playerGetVideoSettings() : Promise<VideoSettings> {
    return await TAURI_INVOKE("player_get_video_settings");
},
/**
 * Set sleep timer mode
 */
async playerSetSleepTimer(mode: SleepTimerMode) : Promise<SleepTimerState> {
    return await TAURI_INVOKE("player_set_sleep_timer", { mode });
},
/**
 * Cancel sleep timer
 */
async playerCancelSleepTimer() : Promise<SleepTimerState> {
    return await TAURI_INVOKE("player_cancel_sleep_timer");
},
/**
 * Get current sleep timer state
 */
async playerGetSleepTimer() : Promise<SleepTimerState> {
    return await TAURI_INVOKE("player_get_sleep_timer");
},
/**
 * Get autoplay settings
 */
async playerGetAutoplaySettings() : Promise<AutoplaySettings> {
    return await TAURI_INVOKE("player_get_autoplay_settings");
},
/**
 * Set autoplay settings and persist to database
 */
async playerSetAutoplaySettings(userId: string, settings: AutoplaySettings) : Promise<AutoplaySettings> {
    return await TAURI_INVOKE("player_set_autoplay_settings", { userId, settings });
},
/**
 * Cancel active autoplay countdown
 */
async playerCancelAutoplayCountdown() : Promise<null> {
    return await TAURI_INVOKE("player_cancel_autoplay_countdown");
},
/**
 * Play next episode (user confirmed from popup)
 */
async playerPlayNextEpisode(item: PlayItemRequest) : Promise<PlayerStatus> {
    return await TAURI_INVOKE("player_play_next_episode", { item });
},
/**
 * Handle playback ended event - triggers autoplay decision logic
 * This is called from:
 * - Frontend when HTML5 video ends (Linux/desktop) - passes itemId + repositoryHandle for the video
 * - Frontend when audio track ends via backend event - no itemId/repositoryHandle needed
 * - Android JNI callback also triggers this logic directly
 * 
 * TRACES: UR-023, UR-026, UR-040 | DR-047, DR-052, DR-129
 */
async playerOnPlaybackEnded(itemId: string | null, repositoryHandle: string | null) : Promise<null> {
    return await TAURI_INVOKE("player_on_playback_ended", { itemId, repositoryHandle });
},
/**
 * Try to recover playback after a **recoverable** player error, reporting
 * whether it was handled.
 * 
 * The frontend's error handler stops the player, which is right for a real
 * failure and wrong for a network blip — it turned every hiccup into "playback
 * died". This is the echo path for backends that cannot decide in-process:
 * MpvBackend is constructed before `PlayerController` exists ([`lib.rs`]), so
 * its event thread has no controller to ask. It emits the error, the frontend
 * echoes it here, and the decision stays in Rust — the same shape as
 * `PlaybackEnded` → `player_on_playback_ended`.
 * 
 * Returns `true` when the stream was re-opened and the caller must NOT stop the
 * player; `false` when the error is real and should be surfaced as before.
 * Android decides inside its JNI callback and only emits errors it has already
 * declined to recover, so this reports `false` for those without a second
 * opinion — the shared attempt budget is spent by then either way.
 * 
 * TRACES: UR-004, UR-040 | DR-130 | UT-117
 */
async playerRecoverStream() : Promise<boolean> {
    return await TAURI_INVOKE("player_recover_stream");
},
/**
 * Report an HTML5 <video> state change (playing/paused/loading/stopped/idle).
 */
async playerReportState(state: string, mediaId: string | null) : Promise<null> {
    return await TAURI_INVOKE("player_report_state", { state, mediaId });
},
/**
 * Report an HTML5 <video> position tick (seconds). The adapter should throttle
 * these to roughly match the native backends' ~250ms cadence.
 */
async playerReportPosition(position: number, duration: number) : Promise<null> {
    return await TAURI_INVOKE("player_report_position", { position, duration });
},
/**
 * Report that the HTML5 <video> finished loading and knows its duration.
 */
async playerReportMediaLoaded(duration: number) : Promise<null> {
    return await TAURI_INVOKE("player_report_media_loaded", { duration });
},
/**
 * The on-disk path for a downloaded item, for playback surfaces that resolve
 * their own source rather than going through the queue.
 * 
 * The video player is the reason this exists: audio has preferred local files
 * since queue construction, but video asks the repository for a stream URL and
 * never consults `downloads`, so a downloaded film was still streamed — costing
 * bandwidth that had already been spent and failing outright when offline.
 * 
 * Returns `None` when nothing is downloaded *or* the file is missing, so the
 * caller falls back to streaming.
 * 
 * TRACES: UR-071 | DR-123 | UT-116
 */
async playerLocalMediaPath(itemId: string) : Promise<string | null> {
    return await TAURI_INVOKE("player_local_media_path", { itemId });
},
/**
 * Preload upcoming tracks from the queue
 * This queues background downloads for the next N tracks that aren't already downloaded
 */
async playerPreloadUpcoming(userId: string, downloadBasePath: string) : Promise<PreloadResult> {
    return await TAURI_INVOKE("player_preload_upcoming", { userId, downloadBasePath });
},
/**
 * Update SmartCache configuration
 */
async playerSetCacheConfig(config: CacheConfig) : Promise<null> {
    return await TAURI_INVOKE("player_set_cache_config", { config });
},
/**
 * Get current SmartCache configuration
 */
async playerGetCacheConfig() : Promise<CacheConfig> {
    return await TAURI_INVOKE("player_get_cache_config");
},
/**
 * Configure Jellyfin API client for automatic playback reporting
 */
async playerConfigureJellyfin(serverUrl: string, accessToken: string, userId: string, deviceId: string) : Promise<null> {
    return await TAURI_INVOKE("player_configure_jellyfin", { serverUrl, accessToken, userId, deviceId });
},
/**
 * Disable Jellyfin automatic playback reporting
 */
async playerDisableJellyfin() : Promise<null> {
    return await TAURI_INVOKE("player_disable_jellyfin");
},
/**
 * Get the current media session state
 */
async playerGetSession() : Promise<MediaSessionType> {
    return await TAURI_INVOKE("player_get_session");
},
/**
 * Dismiss the current media session (returns to Idle)
 */
async playerDismissSession() : Promise<null> {
    return await TAURI_INVOKE("player_dismiss_session");
},
/**
 * Play items on a remote Jellyfin session (casting)
 */
async remotePlayOnSession(sessionId: string, itemIds: string[], startIndex: number) : Promise<null> {
    return await TAURI_INVOKE("remote_play_on_session", { sessionId, itemIds, startIndex });
},
/**
 * Send a playback command to a remote session
 */
async remoteSendCommand(sessionId: string, command: string) : Promise<null> {
    return await TAURI_INVOKE("remote_send_command", { sessionId, command });
},
/**
 * Seek on a remote session
 */
async remoteSessionSeek(sessionId: string, positionTicks: number) : Promise<null> {
    return await TAURI_INVOKE("remote_session_seek", { sessionId, positionTicks });
},
/**
 * Set volume on a remote session
 */
async remoteSessionSetVolume(sessionId: string, volume: number) : Promise<null> {
    return await TAURI_INVOKE("remote_session_set_volume", { sessionId, volume });
},
/**
 * Toggle mute on a remote session
 */
async remoteSessionToggleMute(sessionId: string) : Promise<null> {
    return await TAURI_INVOKE("remote_session_toggle_mute", { sessionId });
},
/**
 * List current LMS sync groups.
 */
async lmsGetSyncGroups() : Promise<LmsSyncGroup[]> {
    return await TAURI_INVOKE("lms_get_sync_groups");
},
/**
 * Fuse LMS zones into a sync group. `master_mac` keeps playing and the
 * `slave_macs` zones join it in sync.
 */
async lmsCreateSyncGroup(masterMac: string, slaveMacs: string[]) : Promise<null> {
    return await TAURI_INVOKE("lms_create_sync_group", { masterMac, slaveMacs });
},
/**
 * Remove a single LMS zone from its sync group (decouple one player).
 */
async lmsUnsyncPlayer(mac: string) : Promise<null> {
    return await TAURI_INVOKE("lms_unsync_player", { mac });
},
/**
 * Dissolve an entire LMS sync group, identified by its master's MAC.
 */
async lmsDissolveSyncGroup(masterMac: string) : Promise<null> {
    return await TAURI_INVOKE("lms_dissolve_sync_group", { masterMac });
},
/**
 * Set polling frequency hint based on UI state
 */
async sessionsSetPollingHint(hint: string) : Promise<null> {
    return await TAURI_INVOKE("sessions_set_polling_hint", { hint });
},
/**
 * Manually trigger a session poll (for refresh button)
 */
async sessionsPollNow() : Promise<SessionInfo[]> {
    return await TAURI_INVOKE("sessions_poll_now");
},
/**
 * Get the current playback mode
 */
async playbackModeGetCurrent() : Promise<PlaybackMode> {
    return await TAURI_INVOKE("playback_mode_get_current");
},
/**
 * Set the playback mode (internal/testing use)
 */
async playbackModeSet(mode: PlaybackMode) : Promise<null> {
    return await TAURI_INVOKE("playback_mode_set", { mode });
},
/**
 * Check if currently transferring between playback modes
 */
async playbackModeIsTransferring() : Promise<boolean> {
    return await TAURI_INVOKE("playback_mode_is_transferring");
},
/**
 * Transfer playback from local device to a remote Jellyfin session
 */
async playbackModeTransferToRemote(sessionId: string, position: number | null) : Promise<null> {
    return await TAURI_INVOKE("playback_mode_transfer_to_remote", { sessionId, position });
},
/**
 * Get remote session status (for polling position/duration)
 */
async playbackModeGetRemoteStatus() : Promise<RemoteSessionStatus> {
    return await TAURI_INVOKE("playback_mode_get_remote_status");
},
/**
 * Transfer playback from remote session back to local device
 * 
 * Parameters:
 * - current_item_id: The Jellyfin item ID currently playing on remote
 * - position_ticks: Current playback position in ticks (10,000 ticks = 1ms)
 */
async playbackModeTransferToLocal(currentItemId: string, positionTicks: number) : Promise<null> {
    return await TAURI_INVOKE("playback_mode_transfer_to_local", { currentItemId, positionTicks });
},
/**
 * Set the transferring flag on the playback mode manager.
 * 
 * Used by the frontend remote->local flow to mark the whole two-step sequence
 * as a transfer, so `player_play_tracks` starts LOCAL playback instead of
 * casting back to the remote session it's leaving. Always pair `true` with a
 * later `false` (including on error) so the flag can't stick.
 */
async playbackModeSetTransferring(transferring: boolean) : Promise<null> {
    return await TAURI_INVOKE("playback_mode_set_transferring", { transferring });
},
/**
 * Initialize playback reporter (called after login)
 */
async playbackReporterInit(serverUrl: string, userId: string, accessToken: string, deviceId: string) : Promise<null> {
    return await TAURI_INVOKE("playback_reporter_init", { serverUrl, userId, accessToken, deviceId });
},
/**
 * Destroy playback reporter (called on logout)
 */
async playbackReporterDestroy() : Promise<null> {
    return await TAURI_INVOKE("playback_reporter_destroy");
},
/**
 * Report playback start
 */
async playbackReportStart(itemId: string, positionSeconds: number, contextType: string | null, contextId: string | null) : Promise<null> {
    return await TAURI_INVOKE("playback_report_start", { itemId, positionSeconds, contextType, contextId });
},
/**
 * Report playback progress
 */
async playbackReportProgress(itemId: string, positionSeconds: number, isPaused: boolean) : Promise<null> {
    return await TAURI_INVOKE("playback_report_progress", { itemId, positionSeconds, isPaused });
},
/**
 * Report playback stopped
 */
async playbackReportStopped(itemId: string, positionSeconds: number) : Promise<null> {
    return await TAURI_INVOKE("playback_report_stopped", { itemId, positionSeconds });
},
/**
 * Mark item as played
 */
async playbackMarkPlayed(itemId: string) : Promise<null> {
    return await TAURI_INVOKE("playback_mark_played", { itemId });
},
/**
 * Initialize the auth manager (call on app startup)
 * Restores session from storage if available
 */
async authInitialize() : Promise<Session | null> {
    return await TAURI_INVOKE("auth_initialize");
},
/**
 * Connect to a Jellyfin server and get server info
 */
async authConnectToServer(serverUrl: string) : Promise<AuthServerInfo> {
    return await TAURI_INVOKE("auth_connect_to_server", { serverUrl });
},
/**
 * Login with username and password
 */
async authLogin(serverUrl: string, username: string, password: string, deviceId: string) : Promise<AuthResult> {
    return await TAURI_INVOKE("auth_login", { serverUrl, username, password, deviceId });
},
/**
 * Verify current session
 */
async authVerifySession(serverUrl: string, userId: string, accessToken: string, deviceId: string) : Promise<boolean> {
    return await TAURI_INVOKE("auth_verify_session", { serverUrl, userId, accessToken, deviceId });
},
/**
 * Logout (clear session and call Jellyfin logout endpoint)
 */
async authLogout(serverUrl: string, accessToken: string, deviceId: string) : Promise<null> {
    return await TAURI_INVOKE("auth_logout", { serverUrl, accessToken, deviceId });
},
/**
 * Get current session
 */
async authGetSession() : Promise<Session | null> {
    return await TAURI_INVOKE("auth_get_session");
},
/**
 * Set current session (for restoration from storage)
 */
async authSetSession(session: Session | null) : Promise<null> {
    return await TAURI_INVOKE("auth_set_session", { session });
},
/**
 * Start background session verification
 */
async authStartVerification(deviceId: string) : Promise<null> {
    return await TAURI_INVOKE("auth_start_verification", { deviceId });
},
/**
 * Stop background session verification
 */
async authStopVerification() : Promise<null> {
    return await TAURI_INVOKE("auth_stop_verification");
},
/**
 * Re-authenticate with password (when session expired)
 */
async authReauthenticate(password: string, deviceId: string) : Promise<AuthResult> {
    return await TAURI_INVOKE("auth_reauthenticate", { password, deviceId });
},
/**
 * Get or create the device ID.
 * Device ID is a UUID v4 that persists across app restarts.
 * On first call, generates and stores a new UUID.
 * On subsequent calls, retrieves the stored UUID.
 * 
 * # Returns
 * - `Ok(String)` - The device ID (UUID v4)
 * - `Err(String)` - If database operation fails
 * 
 * TRACES: UR-009 | DR-011
 */
async deviceGetId() : Promise<string> {
    return await TAURI_INVOKE("device_get_id");
},
/**
 * Set the device ID (primarily for testing or recovery).
 * Overwrites any existing device ID.
 * 
 * # Arguments
 * * `device_id` - The device ID to store (should be UUID v4 format)
 * 
 * # Returns
 * - `Ok(())` - If device ID was stored successfully
 * - `Err(String)` - If database operation fails
 * 
 * TRACES: UR-009 | DR-011
 */
async deviceSetId(deviceId: string) : Promise<null> {
    return await TAURI_INVOKE("device_set_id", { deviceId });
},
/**
 * Check if the server is currently reachable
 */
async connectivityCheckServer() : Promise<boolean> {
    return await TAURI_INVOKE("connectivity_check_server");
},
/**
 * Set the server URL and trigger an immediate check
 */
async connectivitySetServerUrl(url: string) : Promise<null> {
    return await TAURI_INVOKE("connectivity_set_server_url", { url });
},
/**
 * Get the current connectivity status
 */
async connectivityGetStatus() : Promise<ConnectivityStatus> {
    return await TAURI_INVOKE("connectivity_get_status");
},
/**
 * Start monitoring connectivity with adaptive polling
 */
async connectivityStartMonitoring() : Promise<null> {
    return await TAURI_INVOKE("connectivity_start_monitoring");
},
/**
 * Stop monitoring connectivity
 */
async connectivityStopMonitoring() : Promise<null> {
    return await TAURI_INVOKE("connectivity_stop_monitoring");
},
/**
 * Mark the server as reachable (called after successful API calls)
 */
async connectivityMarkReachable() : Promise<null> {
    return await TAURI_INVOKE("connectivity_mark_reachable");
},
/**
 * Mark the server as unreachable (called after failed API calls)
 */
async connectivityMarkUnreachable(error: string | null) : Promise<null> {
    return await TAURI_INVOKE("connectivity_mark_unreachable", { error });
},
/**
 * Initialize the database and run migrations
 */
async storageInit() : Promise<string> {
    return await TAURI_INVOKE("storage_init");
},
/**
 * Get storage directory path (parent directory of the database file)
 */
async storageGetPath() : Promise<string> {
    return await TAURI_INVOKE("storage_get_path");
},
/**
 * Get database file size in bytes
 */
async storageGetSize() : Promise<number | null> {
    return await TAURI_INVOKE("storage_get_size");
},
/**
 * Get security status (keyring vs encrypted file fallback)
 */
async storageGetSecurityStatus() : Promise<SecurityStatus> {
    return await TAURI_INVOKE("storage_get_security_status");
},
/**
 * Save a server connection
 * Uses INSERT ... ON CONFLICT to avoid triggering CASCADE DELETE on users
 */
async storageSaveServer(id: string, name: string, url: string, version: string | null) : Promise<null> {
    return await TAURI_INVOKE("storage_save_server", { id, name, url, version });
},
/**
 * Get all saved servers
 */
async storageGetServers() : Promise<ServerInfo[]> {
    return await TAURI_INVOKE("storage_get_servers");
},
/**
 * Delete a server and all associated data
 */
async storageDeleteServer(serverId: string) : Promise<null> {
    return await TAURI_INVOKE("storage_delete_server", { serverId });
},
/**
 * Save a user account (token stored in secure storage, not database)
 */
async storageSaveUser(id: string, serverId: string, username: string, accessToken: string | null) : Promise<boolean> {
    return await TAURI_INVOKE("storage_save_user", { id, serverId, username, accessToken });
},
/**
 * Get users for a server
 */
async storageGetUsers(serverId: string) : Promise<UserInfo[]> {
    return await TAURI_INVOKE("storage_get_users", { serverId });
},
/**
 * Set a user as active (and deactivate all other users globally)
 */
async storageSetActiveUser(userId: string, serverId: string) : Promise<null> {
    return await TAURI_INVOKE("storage_set_active_user", { userId, serverId });
},
/**
 * Get the active user for a server
 */
async storageGetActiveUser(serverId: string) : Promise<UserInfo | null> {
    return await TAURI_INVOKE("storage_get_active_user", { serverId });
},
/**
 * Get the active session (user + server + token) for session restoration
 */
async storageGetActiveSession() : Promise<ActiveSession | null> {
    return await TAURI_INVOKE("storage_get_active_session");
},
/**
 * Get user's access token from secure storage
 */
async storageGetAccessToken(userId: string) : Promise<string | null> {
    return await TAURI_INVOKE("storage_get_access_token", { userId });
},
/**
 * Delete a user account and their token from secure storage
 */
async storageDeleteUser(userId: string) : Promise<null> {
    return await TAURI_INVOKE("storage_delete_user", { userId });
},
/**
 * Update playback progress in local database
 * This stores the progress locally for offline access and "continue watching"
 */
async storageUpdatePlaybackProgress(userId: string, itemId: string, positionMs: number) : Promise<null> {
    return await TAURI_INVOKE("storage_update_playback_progress", { userId, itemId, positionMs });
},
/**
 * Update playback progress with context in local database
 * This stores the progress along with playback context (container vs single)
 */
async storageUpdatePlaybackContext(userId: string, itemId: string, positionMs: number, contextType: string | null, contextId: string | null) : Promise<null> {
    return await TAURI_INVOKE("storage_update_playback_context", { userId, itemId, positionMs, contextType, contextId });
},
/**
 * Mark item as played in local database
 */
async storageMarkPlayed(userId: string, itemId: string) : Promise<null> {
    return await TAURI_INVOKE("storage_mark_played", { userId, itemId });
},
/**
 * Get playback progress for an item
 */
async storageGetPlaybackProgress(userId: string, itemId: string) : Promise<PlaybackProgress | null> {
    return await TAURI_INVOKE("storage_get_playback_progress", { userId, itemId });
},
/**
 * Mark pending sync as completed for an item
 */
async storageMarkSynced(userId: string, itemId: string) : Promise<null> {
    return await TAURI_INVOKE("storage_mark_synced", { userId, itemId });
},
/**
 * Toggle favorite status for an item in local database
 * This updates the is_favorite field and marks it for sync to Jellyfin
 */
async storageToggleFavorite(userId: string, itemId: string, isFavorite: boolean) : Promise<boolean> {
    return await TAURI_INVOKE("storage_toggle_favorite", { userId, itemId, isFavorite });
},
/**
 * Queue a media item for download
 */
async downloadItem(request: DownloadItemRequest) : Promise<number> {
    return await TAURI_INVOKE("download_item", { request });
},
/**
 * Queue and start a download in a single atomic operation
 * This simplifies the frontend flow by combining multiple steps
 */
async downloadItemAndStart(request: DownloadItemAndStartRequest) : Promise<number> {
    return await TAURI_INVOKE("download_item_and_start", { request });
},
/**
 * Queue an entire album for download
 */
async downloadAlbum(albumId: string, userId: string, basePath: string) : Promise<number[]> {
    return await TAURI_INVOKE("download_album", { albumId, userId, basePath });
},
/**
 * Queue a video item (movie or episode) for download with quality preset
 */
async downloadVideo(request: DownloadVideoRequest) : Promise<number> {
    return await TAURI_INVOKE("download_video", { request });
},
/**
 * Queue all episodes of a series for download
 */
async downloadSeries(seriesId: string, seriesName: string, userId: string, basePath: string, qualityPreset: string | null) : Promise<number[]> {
    return await TAURI_INVOKE("download_series", { seriesId, seriesName, userId, basePath, qualityPreset });
},
/**
 * Queue all episodes of a specific season for download
 */
async downloadSeason(seasonId: string, seriesName: string, seasonName: string, seasonNumber: number, userId: string, basePath: string, qualityPreset: string | null) : Promise<number[]> {
    return await TAURI_INVOKE("download_season", { seasonId, seriesName, seasonName, seasonNumber, userId, basePath, qualityPreset });
},
/**
 * Get all downloads for a user, optionally filtered by status
 */
async getDownloads(userId: string, statusFilter: string[] | null) : Promise<DownloadsResponse> {
    return await TAURI_INVOKE("get_downloads", { userId, statusFilter });
},
/**
 * Pause a download
 */
async pauseDownload(downloadId: number) : Promise<null> {
    return await TAURI_INVOKE("pause_download", { downloadId });
},
/**
 * Resume a paused download
 */
async resumeDownload(downloadId: number) : Promise<null> {
    return await TAURI_INVOKE("resume_download", { downloadId });
},
/**
 * Cancel a download
 */
async cancelDownload(downloadId: number) : Promise<null> {
    return await TAURI_INVOKE("cancel_download", { downloadId });
},
/**
 * Delete a completed download
 */
async deleteDownload(downloadId: number) : Promise<null> {
    return await TAURI_INVOKE("delete_download", { downloadId });
},
/**
 * Delete all downloads for a user
 */
async deleteAllDownloads(userId: string) : Promise<number> {
    return await TAURI_INVOKE("delete_all_downloads", { userId });
},
/**
 * Delete all downloads for a specific album
 */
async deleteAlbumDownloads(albumId: string, userId: string) : Promise<number> {
    return await TAURI_INVOKE("delete_album_downloads", { albumId, userId });
},
/**
 * Remove every completed download at or under a container item.
 * 
 * Works at any level of the Downloaded browse: a leaf (removes just that
 * download), an album/season/series (removes all downloaded descendants linked
 * via album_id/season_id/series_id/parent_id). Deletes the DB rows and the
 * on-disk files. Returns the number of downloads removed. Idempotent.
 * 
 * TRACES: UR-055 | DR-083
 */
async deleteDownloadsUnder(itemId: string, userId: string) : Promise<number> {
    return await TAURI_INVOKE("delete_downloads_under", { itemId, userId });
},
/**
 * Clear all stale pending/failed/paused downloads
 */
async clearStaleDownloads(userId: string) : Promise<number> {
    return await TAURI_INVOKE("clear_stale_downloads", { userId });
},
/**
 * Get storage statistics for downloads
 */
async getDownloadStorageStats(userId: string) : Promise<StorageStats> {
    return await TAURI_INVOKE("get_download_storage_stats", { userId });
},
/**
 * Mark a download as completed
 */
async markDownloadCompleted(downloadId: number, bytesDownloaded: number, filePath: string) : Promise<null> {
    return await TAURI_INVOKE("mark_download_completed", { downloadId, bytesDownloaded, filePath });
},
/**
 * Mark a download as failed
 */
async markDownloadFailed(downloadId: number, errorMessage: string) : Promise<null> {
    return await TAURI_INVOKE("mark_download_failed", { downloadId, errorMessage });
},
/**
 * A playable URL for a downloaded file on disk.
 * 
 * Local media is served over a loopback HTTP server rather than handed to the
 * webview as a `file://`/asset URL, because the asset protocol cannot stream a
 * large file — it answers a range-less request with the whole thing, which
 * Chromium abandons. See `media_server` for why real HTTP is used.
 * 
 * The returned URL carries the server's per-session token, so it is only valid
 * for this run of the app and must not be persisted.
 * 
 * TRACES: UR-071 | DR-137
 */
async mediaLocalUrl(path: string) : Promise<string> {
    return await TAURI_INVOKE("media_local_url", { path });
},
/**
 * Start downloading a file immediately
 * This command actually downloads the file using the worker
 */
async startDownload(downloadId: number, streamUrl: string, targetDir: string) : Promise<null> {
    return await TAURI_INVOKE("start_download", { downloadId, streamUrl, targetDir });
},
/**
 * Enqueue a download with its resolved stream URL, then let the queue pump
 * start it (or a higher-priority pending item) when a slot is free.
 * 
 * Unlike [`start_download`], this never errors when the concurrency limit is
 * reached: the URL is persisted on the row and the pump will pick it up once a
 * slot frees. This is the path bulk operations (album/series/season) use so
 * every queued item eventually downloads without the frontend re-issuing it.
 */
async enqueueDownload(downloadId: number, streamUrl: string, targetDir: string) : Promise<null> {
    return await TAURI_INVOKE("enqueue_download", { downloadId, streamUrl, targetDir });
},
/**
 * Enqueue a batch of already-queued video downloads, resolving each one's
 * transcode URL from the repository using the `quality_preset` stored on the
 * row. Then let the pump start them subject to the concurrency limit.
 * 
 * This is the bulk video path (series/season): `download_series`/
 * `download_season` insert the rows, then this resolves URLs and enqueues them
 * so they actually start. Resolving server-side avoids round-tripping every
 * episode URL through the frontend.
 */
async enqueueVideoDownloads(handle: string, downloadIds: number[], targetDir: string) : Promise<null> {
    return await TAURI_INVOKE("enqueue_video_downloads", { handle, downloadIds, targetDir });
},
/**
 * Walk every library on the server and persist all items to the offline cache
 * so the full catalog is browsable offline (greyed out when not downloaded).
 * 
 * Best-effort: a library that fails to fetch is counted and skipped rather than
 * aborting the whole sync. Runs libraries sequentially to avoid hammering the
 * server. Uses `Recursive=true` so a single request per library returns the
 * containers and their playable children.
 */
async syncFullCatalog(handle: string) : Promise<CatalogSyncResult> {
    return await TAURI_INVOKE("sync_full_catalog", { handle });
},
/**
 * Report the last-synced timestamp so the UI can show a hint / decide whether
 * to trigger a fresh sync.
 */
async catalogSyncStatus() : Promise<CatalogSyncStatus> {
    return await TAURI_INVOKE("catalog_sync_status");
},
/**
 * Control whether offline library queries reveal the full synced catalog
 * (greyed-out, non-downloaded media) or only downloaded/local media.
 * 
 * The frontend calls this from the "Show all server media" toggle: pass `true`
 * when online, or when offline with the toggle on; pass `false` when offline
 * with the toggle off so library pages show downloaded media only. Fixes the
 * bug where offline library pages showed every server item regardless of the
 * toggle.
 */
async setShowServerCatalog(show: boolean) : Promise<void> {
    await TAURI_INVOKE("set_show_server_catalog", { show });
},
/**
 * Resolve the stream URL for every download row that was queued while offline
 * (`status = 'pending' AND stream_url IS NULL`), then pump the queue so they
 * start. Call this on reconnect.
 * 
 * Audio rows resolve via `get_audio_stream_url`; video rows (media_type =
 * 'video') via the pure `get_video_download_url` builder using the row's stored
 * `quality_preset` — mirroring `enqueue_video_downloads`. Rows whose URL can't
 * be resolved are left pending (they retry on the next reconnect).
 */
async resumeQueuedDownloads(handle: string) : Promise<ResumeQueuedResult> {
    return await TAURI_INVOKE("resume_queued_downloads", { handle });
},
/**
 * Get download manager statistics
 */
async getDownloadManagerStats() : Promise<DownloadManagerStats> {
    return await TAURI_INVOKE("get_download_manager_stats");
},
/**
 * Set the maximum concurrent downloads
 */
async setMaxConcurrentDownloads(max: number) : Promise<null> {
    return await TAURI_INVOKE("set_max_concurrent_downloads", { max });
},
/**
 * Get SmartCache statistics
 */
async getSmartCacheStats(userId: string) : Promise<SmartCacheStats> {
    return await TAURI_INVOKE("get_smart_cache_stats", { userId });
},
/**
 * Update SmartCache configuration
 */
async updateSmartCacheConfig(config: CacheConfig) : Promise<null> {
    return await TAURI_INVOKE("update_smart_cache_config", { config });
},
/**
 * Get SmartCache configuration
 */
async getSmartCacheConfig() : Promise<CacheConfig> {
    return await TAURI_INVOKE("get_smart_cache_config");
},
/**
 * Report the device's current network transport (Android → Rust).
 * 
 * The frontend calls this on startup and whenever the native network callback
 * fires. Updating to an acceptable network re-pumps the download queue, so a
 * queue parked on "waiting for WiFi" drains itself without user action.
 * 
 * TRACES: UR-053 | DR-074
 */
async setNetworkState(network: NetworkStateWrapperArg) : Promise<null> {
    return await TAURI_INVOKE("set_network_state", { network });
},
/**
 * Whether downloads are currently permitted by the WiFi-only gate.
 * 
 * The downloads UI uses this to render "Waiting for WiFi" on pending rows
 * rather than leaving them looking silently stuck.
 * 
 * TRACES: UR-053 | DR-074
 */
async getDownloadsAllowed() : Promise<boolean> {
    return await TAURI_INVOKE("get_downloads_allowed");
},
/**
 * Get album recommendations based on play history
 */
async getAlbumRecommendations(userId: string) : Promise<AlbumRecommendation[]> {
    return await TAURI_INVOKE("get_album_recommendations", { userId });
},
/**
 * Get album affinity status for all tracked albums
 * This shows the SmartCache's internal play history and threshold status
 */
async getAlbumAffinityStatus() : Promise<AlbumAffinityStatus[]> {
    return await TAURI_INVOKE("get_album_affinity_status");
},
/**
 * Pin an item's metadata (protects from cache clear)
 */
async pinItem(itemId: string) : Promise<null> {
    return await TAURI_INVOKE("pin_item", { itemId });
},
/**
 * Unpin an item's metadata
 */
async unpinItem(itemId: string) : Promise<null> {
    return await TAURI_INVOKE("unpin_item", { itemId });
},
/**
 * Check if an item is pinned
 */
async isItemPinned(itemId: string) : Promise<boolean> {
    return await TAURI_INVOKE("is_item_pinned", { itemId });
},
/**
 * Check if an item is available offline
 */
async offlineIsAvailable(itemId: string) : Promise<boolean> {
    return await TAURI_INVOKE("offline_is_available", { itemId });
},
/**
 * Get all offline items for a user
 */
async offlineGetItems(userId: string) : Promise<OfflineItem[]> {
    return await TAURI_INVOKE("offline_get_items", { userId });
},
/**
 * Search offline items
 */
async offlineSearch(userId: string, query: string) : Promise<OfflineItem[]> {
    return await TAURI_INVOKE("offline_search", { userId, query });
},
/**
 * Get cached libraries for a server
 */
async storageGetLibraries(serverId: string) : Promise<CachedLibrary[]> {
    return await TAURI_INVOKE("storage_get_libraries", { serverId });
},
/**
 * Get cached items with optional filtering
 */
async storageGetItems(serverId: string, parentId: string | null, libraryId: string | null, itemType: string | null, limit: number | null, offset: number | null) : Promise<CachedItem[]> {
    return await TAURI_INVOKE("storage_get_items", { serverId, parentId, libraryId, itemType, limit, offset });
},
/**
 * Get a single cached item by ID
 */
async storageGetItem(itemId: string) : Promise<CachedItem | null> {
    return await TAURI_INVOKE("storage_get_item", { itemId });
},
/**
 * Search cached items using FTS
 */
async storageSearchItems(serverId: string, query: string, limit: number | null) : Promise<CachedItem[]> {
    return await TAURI_INVOKE("storage_search_items", { serverId, query, limit });
},
/**
 * Save a library to the cache
 */
async storageSaveLibrary(id: string, serverId: string, name: string, collectionType: string | null, imageTag: string | null, sortOrder: number | null) : Promise<null> {
    return await TAURI_INVOKE("storage_save_library", { id, serverId, name, collectionType, imageTag, sortOrder });
},
/**
 * Save an item to the cache
 */
async storageSaveItem(item: CachedItem, serverId: string) : Promise<null> {
    return await TAURI_INVOKE("storage_save_item", { item, serverId });
},
/**
 * Get count of pending sync operations for a user
 */
async storageGetPendingSyncCount(userId: string) : Promise<number> {
    return await TAURI_INVOKE("storage_get_pending_sync_count", { userId });
},
/**
 * Queue a mutation for sync to server
 */
async syncQueueMutation(userId: string, operation: string, itemId: string | null, payload: string | null) : Promise<number> {
    return await TAURI_INVOKE("sync_queue_mutation", { userId, operation, itemId, payload });
},
/**
 * Get all pending sync operations for a user
 */
async syncGetPending(userId: string, limit: number | null) : Promise<SyncQueueItem[]> {
    return await TAURI_INVOKE("sync_get_pending", { userId, limit });
},
/**
 * Mark a sync operation as in progress
 */
async syncMarkProcessing(id: number) : Promise<null> {
    return await TAURI_INVOKE("sync_mark_processing", { id });
},
/**
 * Mark a sync operation as completed
 */
async syncMarkCompleted(id: number) : Promise<null> {
    return await TAURI_INVOKE("sync_mark_completed", { id });
},
/**
 * Mark a sync operation as failed with error message
 */
async syncMarkFailed(id: number, error: string) : Promise<null> {
    return await TAURI_INVOKE("sync_mark_failed", { id, error });
},
/**
 * Get count of pending sync operations for a user
 */
async syncGetPendingCount(userId: string) : Promise<number> {
    return await TAURI_INVOKE("sync_get_pending_count", { userId });
},
/**
 * Push the queue now, on the user's say-so, instead of waiting for a reconnect.
 * 
 * TRACES: UR-025 | DR-132
 */
async syncProcessPending() : Promise<DrainReport> {
    return await TAURI_INVOKE("sync_process_pending");
},
/**
 * Delete completed sync operations older than specified days
 */
async syncCleanupCompleted(daysOld: number) : Promise<number> {
    return await TAURI_INVOKE("sync_cleanup_completed", { daysOld });
},
/**
 * Delete all sync operations for a user (used during logout)
 */
async syncClearUser(userId: string) : Promise<null> {
    return await TAURI_INVOKE("sync_clear_user", { userId });
},
/**
 * Get cached thumbnail path, returns None if not cached
 * Also updates last_accessed timestamp for LRU tracking
 */
async thumbnailGetCached(itemId: string, imageType: string, tag: string) : Promise<string | null> {
    return await TAURI_INVOKE("thumbnail_get_cached", { itemId, imageType, tag });
},
/**
 * Download and save a thumbnail to cache
 * Returns the local file path on success
 */
async thumbnailSave(itemId: string, imageType: string, tag: string, url: string) : Promise<string> {
    return await TAURI_INVOKE("thumbnail_save", { itemId, imageType, tag, url });
},
/**
 * Get thumbnail cache statistics
 */
async thumbnailGetStats() : Promise<ThumbnailCacheStats> {
    return await TAURI_INVOKE("thumbnail_get_stats");
},
/**
 * Set thumbnail cache storage limit in bytes
 */
async thumbnailSetLimit(limitBytes: number) : Promise<null> {
    return await TAURI_INVOKE("thumbnail_set_limit", { limitBytes });
},
/**
 * Clear all cached thumbnails
 */
async thumbnailClearCache() : Promise<null> {
    return await TAURI_INVOKE("thumbnail_clear_cache");
},
/**
 * Delete cached thumbnails for a specific item
 */
async thumbnailDeleteItem(itemId: string) : Promise<null> {
    return await TAURI_INVOKE("thumbnail_delete_item", { itemId });
},
/**
 * Get image as base64 data URL, caching if not already cached
 * This extends the thumbnail system to serve all images through Rust with automatic caching
 */
async imageGetUrl(repositoryHandle: string, request: GetImageRequest) : Promise<string> {
    return await TAURI_INVOKE("image_get_url", { repositoryHandle, request });
},
/**
 * Save a person to the cache
 */
async storageSavePerson(person: CachedPerson) : Promise<null> {
    return await TAURI_INVOKE("storage_save_person", { person });
},
/**
 * Get a cached person by ID
 */
async storageGetPerson(personId: string) : Promise<CachedPerson | null> {
    return await TAURI_INVOKE("storage_get_person", { personId });
},
/**
 * Save item-person associations (batch)
 */
async storageSaveItemPeople(associations: CachedItemPerson[]) : Promise<null> {
    return await TAURI_INVOKE("storage_save_item_people", { associations });
},
/**
 * Get people for an item (with person details joined)
 */
async storageGetItemPeople(itemId: string) : Promise<CachedItemPerson[]> {
    return await TAURI_INVOKE("storage_get_item_people", { itemId });
},
/**
 * Save user's preferred audio track for a series
 */
async storageSaveSeriesAudioPreference(userId: string, seriesId: string, serverId: string, audioTrackDisplayTitle: string | null, audioTrackLanguage: string | null, audioTrackIndex: number | null) : Promise<null> {
    return await TAURI_INVOKE("storage_save_series_audio_preference", { userId, seriesId, serverId, audioTrackDisplayTitle, audioTrackLanguage, audioTrackIndex });
},
/**
 * Get user's preferred audio track for a series
 */
async storageGetSeriesAudioPreference(userId: string, seriesId: string) : Promise<SeriesAudioPreference | null> {
    return await TAURI_INVOKE("storage_get_series_audio_preference", { userId, seriesId });
},
/**
 * Create a new repository instance
 * Returns a handle (UUID) for accessing the repository
 */
async repositoryCreate(serverUrl: string, userId: string, accessToken: string, serverId: string) : Promise<string> {
    return await TAURI_INVOKE("repository_create", { serverUrl, userId, accessToken, serverId });
},
/**
 * Destroy a repository instance
 */
async repositoryDestroy(handle: string) : Promise<null> {
    return await TAURI_INVOKE("repository_destroy", { handle });
},
/**
 * Get libraries
 */
async repositoryGetLibraries(handle: string) : Promise<Library[]> {
    return await TAURI_INVOKE("repository_get_libraries", { handle });
},
/**
 * Get items in a container (library, folder, album, etc.)
 */
async repositoryGetItems(handle: string, parentId: string, options: GetItemsOptions | null) : Promise<SearchResult> {
    return await TAURI_INVOKE("repository_get_items", { handle, parentId, options });
},
/**
 * Get a single item by ID
 */
async repositoryGetItem(handle: string, itemId: string) : Promise<MediaItem> {
    return await TAURI_INVOKE("repository_get_item", { handle, itemId });
},
/**
 * Downloaded-only browse: libraries that contain downloaded content.
 * 
 * Backs the Downloads "Downloaded" surface. Never merges server results and is
 * authoritative — an empty list means nothing is downloaded.
 * 
 * TRACES: UR-055 | DR-082
 */
async repositoryGetDownloadedLibraries(handle: string) : Promise<Library[]> {
    return await TAURI_INVOKE("repository_get_downloaded_libraries", { handle });
},
/**
 * Downloaded-only browse: items under a container that are on the device.
 * 
 * TRACES: UR-055 | DR-082, DR-083
 */
async repositoryGetDownloadedItems(handle: string, parentId: string, options: GetItemsOptions | null) : Promise<SearchResult> {
    return await TAURI_INVOKE("repository_get_downloaded_items", { handle, parentId, options });
},
/**
 * On-disk usage of downloaded content (device total, per-item/container bytes).
 * 
 * TRACES: UR-056 | DR-085
 */
async repositoryGetDownloadDiskUsage(handle: string) : Promise<DownloadDiskUsage> {
    return await TAURI_INVOKE("repository_get_download_disk_usage", { handle });
},
/**
 * Query the optional JRay plugin for the actors on screen at time `t`
 * (seconds) in an item. Returns an empty list when JRay isn't installed or
 * has no data for the item, so the caller can render nothing without error.
 */
async repositoryJrayActorsAt(handle: string, itemId: string, t: number) : Promise<JRayActor[]> {
    return await TAURI_INVOKE("repository_jray_actors_at", { handle, itemId, t });
},
/**
 * Get latest items in a library
 */
async repositoryGetLatestItems(handle: string, parentId: string, limit: number | null) : Promise<MediaItem[]> {
    return await TAURI_INVOKE("repository_get_latest_items", { handle, parentId, limit });
},
/**
 * Get resume items (continue watching/listening)
 */
async repositoryGetResumeItems(handle: string, parentId: string | null, limit: number | null) : Promise<MediaItem[]> {
    return await TAURI_INVOKE("repository_get_resume_items", { handle, parentId, limit });
},
/**
 * Get next up episodes
 */
async repositoryGetNextUpEpisodes(handle: string, seriesId: string | null, limit: number | null) : Promise<MediaItem[]> {
    return await TAURI_INVOKE("repository_get_next_up_episodes", { handle, seriesId, limit });
},
/**
 * Every episode of a series, across all seasons, in series order.
 * 
 * Jellyfin hangs episodes off season folders — except for "flat" series whose
 * children are episodes directly. Both shapes are provider vocabulary, so the
 * fan-out and its fallback live in Rust rather than being reimplemented in the
 * frontend (which is what it used to do).
 * 
 * TRACES: UR-062 | DR-101
 */
async repositoryGetSeriesEpisodes(handle: string, seriesId: string) : Promise<MediaItem[]> {
    return await TAURI_INVOKE("repository_get_series_episodes", { handle, seriesId });
},
/**
 * The episode a viewer should land on when they open a series.
 * 
 * "Current" is domain policy, not layout: an episode in progress, else the
 * server's Next Up for the series, else the first unwatched episode, else the
 * first. The third rung is what makes this work offline, where Next Up is
 * always empty. Returns `None` only when the series has no episodes at all.
 * 
 * TRACES: UR-062 | DR-101
 */
async repositoryGetSeriesCurrentEpisode(handle: string, seriesId: string) : Promise<MediaItem | null> {
    return await TAURI_INVOKE("repository_get_series_current_episode", { handle, seriesId });
},
/**
 * Erase the viewer's watch history for an item.
 * 
 * Clears the played flag and the resume position; on a series or season the
 * server applies it to everything inside. A series cleared this way is "never
 * watched" again, so `repository_get_series_current_episode` returns its
 * premiere. Requires the server — offline this fails rather than diverging
 * local state the next sync would overwrite.
 * 
 * TRACES: UR-064 | DR-106
 */
async repositoryClearWatchHistory(handle: string, itemId: string) : Promise<null> {
    return await TAURI_INVOKE("repository_clear_watch_history", { handle, itemId });
},
/**
 * Get recently played audio
 */
async repositoryGetRecentlyPlayedAudio(handle: string, limit: number | null) : Promise<MediaItem[]> {
    return await TAURI_INVOKE("repository_get_recently_played_audio", { handle, limit });
},
/**
 * Get resume movies
 */
async repositoryGetResumeMovies(handle: string, limit: number | null) : Promise<MediaItem[]> {
    return await TAURI_INVOKE("repository_get_resume_movies", { handle, limit });
},
/**
 * Get albums the user hasn't listened to recently ("rediscover")
 */
async repositoryGetRediscoverAlbums(handle: string, parentId: string | null, limit: number | null) : Promise<MediaItem[]> {
    return await TAURI_INVOKE("repository_get_rediscover_albums", { handle, parentId, limit });
},
/**
 * Get genres for a library
 */
async repositoryGetGenres(handle: string, parentId: string | null) : Promise<Genre[]> {
    return await TAURI_INVOKE("repository_get_genres", { handle, parentId });
},
/**
 * Search for items.
 * 
 * Resolves `SearchOptions::scope` into concrete Jellyfin item types before
 * dispatching, so scope taxonomy stays in Rust.
 * 
 * TRACES: UR-049, UR-050 | DR-063
 */
async repositorySearch(handle: string, query: string, options: SearchOptions | null, requestId: number) : Promise<SearchResult> {
    return await TAURI_INVOKE("repository_search", { handle, query, options, requestId });
},
/**
 * Get playback info for an item
 */
async repositoryGetPlaybackInfo(handle: string, itemId: string) : Promise<PlaybackInfo> {
    return await TAURI_INVOKE("repository_get_playback_info", { handle, itemId });
},
/**
 * Get video stream URL with optional seeking support
 */
async repositoryGetVideoStreamUrl(handle: string, itemId: string, mediaSourceId: string | null, startTimeSeconds: number | null, audioStreamIndex: number | null) : Promise<string> {
    return await TAURI_INVOKE("repository_get_video_stream_url", { handle, itemId, mediaSourceId, startTimeSeconds, audioStreamIndex });
},
/**
 * Get audio stream URL for a track
 */
async repositoryGetAudioStreamUrl(handle: string, itemId: string) : Promise<string> {
    return await TAURI_INVOKE("repository_get_audio_stream_url", { handle, itemId });
},
/**
 * Get an audio-only stream URL for a *video* item (background-audio handoff).
 * 
 * TRACES: UR-040 | JA-032 | UT-061
 */
async repositoryGetAudioOnlyStreamUrlForVideo(handle: string, itemId: string, mediaSourceId: string | null, startTimeSeconds: number | null, audioStreamIndex: number | null) : Promise<string> {
    return await TAURI_INVOKE("repository_get_audio_only_stream_url_for_video", { handle, itemId, mediaSourceId, startTimeSeconds, audioStreamIndex });
},
/**
 * Get Live TV channels (broadcast / IPTV) for browsing
 */
async repositoryGetLiveTvChannels(handle: string) : Promise<MediaItem[]> {
    return await TAURI_INVOKE("repository_get_live_tv_channels", { handle });
},
/**
 * Get the root list of plugin "Channels"
 */
async repositoryGetChannels(handle: string) : Promise<SearchResult> {
    return await TAURI_INVOKE("repository_get_channels", { handle });
},
/**
 * Open a live stream for a Live TV channel / live item
 */
async repositoryOpenLiveStream(handle: string, itemId: string) : Promise<LiveStreamInfo> {
    return await TAURI_INVOKE("repository_open_live_stream", { handle, itemId });
},
/**
 * Report playback start
 */
async repositoryReportPlaybackStart(handle: string, itemId: string, positionMs: number) : Promise<null> {
    return await TAURI_INVOKE("repository_report_playback_start", { handle, itemId, positionMs });
},
/**
 * Report playback progress
 */
async repositoryReportPlaybackProgress(handle: string, itemId: string, positionMs: number) : Promise<null> {
    return await TAURI_INVOKE("repository_report_playback_progress", { handle, itemId, positionMs });
},
/**
 * Report playback stopped
 * 
 * A stop-report that cannot reach the server is queued rather than dropped:
 * this is the position the resume point is built from, and losing it is
 * exactly the "it forgot where I was" the sync queue exists to prevent. The
 * drain (DR-131) pushes it on the next reconnect. Queueing is best-effort —
 * failing the command because the *queue* write failed would tell the caller
 * the report was lost when the local position was already saved.
 * 
 * TRACES: UR-025 | DR-154 | UT-151
 */
async repositoryReportPlaybackStopped(handle: string, itemId: string, positionMs: number) : Promise<null> {
    return await TAURI_INVOKE("repository_report_playback_stopped", { handle, itemId, positionMs });
},
/**
 * Get image URL for an item
 */
async repositoryGetImageUrl(handle: string, itemId: string, imageType: ImageType, options: ImageOptions | null) : Promise<string> {
    return await TAURI_INVOKE("repository_get_image_url", { handle, itemId, imageType, options });
},
/**
 * Mark an item as favorite
 */
async repositoryMarkFavorite(handle: string, itemId: string) : Promise<null> {
    return await TAURI_INVOKE("repository_mark_favorite", { handle, itemId });
},
/**
 * Unmark an item as favorite
 */
async repositoryUnmarkFavorite(handle: string, itemId: string) : Promise<null> {
    return await TAURI_INVOKE("repository_unmark_favorite", { handle, itemId });
},
/**
 * Everything the viewer has favourited, across libraries, narrowed by scope.
 * 
 * Two-phase like `repository_search`: the local answer returns immediately and
 * a background server pass emits `favorites-changed` when the server's set
 * differs. Without the second phase a favourite marked in another client shows
 * up only on the *second* visit to the page, since the cache-first read hands
 * back local rows and the refresh is invisible to the frontend.
 * 
 * TRACES: UR-067 | DR-115, DR-120, JA-033 | UT-107
 */
async repositoryGetFavorites(handle: string, scope: SearchScope, options: GetItemsOptions | null) : Promise<SearchResult> {
    return await TAURI_INVOKE("repository_get_favorites", { handle, scope, options });
},
/**
 * Get person details
 */
async repositoryGetPerson(handle: string, personId: string) : Promise<MediaItem> {
    return await TAURI_INVOKE("repository_get_person", { handle, personId });
},
/**
 * Get items by person (actor, director, etc.)
 */
async repositoryGetItemsByPerson(handle: string, personId: string, options: GetItemsOptions | null) : Promise<SearchResult> {
    return await TAURI_INVOKE("repository_get_items_by_person", { handle, personId, options });
},
/**
 * Get similar/related items for a media item
 */
async repositoryGetSimilarItems(handle: string, itemId: string, limit: number | null) : Promise<SearchResult> {
    return await TAURI_INVOKE("repository_get_similar_items", { handle, itemId, limit });
},
/**
 * Get subtitle URL for a media item
 */
async repositoryGetSubtitleUrl(handle: string, itemId: string, mediaSourceId: string, streamIndex: number, format: string) : Promise<string> {
    return await TAURI_INVOKE("repository_get_subtitle_url", { handle, itemId, mediaSourceId, streamIndex, format });
},
/**
 * Get video download URL with quality preset
 */
async repositoryGetVideoDownloadUrl(handle: string, itemId: string, quality: string, mediaSourceId: string | null) : Promise<string> {
    return await TAURI_INVOKE("repository_get_video_download_url", { handle, itemId, quality, mediaSourceId });
},
/**
 * Create a new playlist
 */
async playlistCreate(handle: string, name: string, itemIds: string[] | null) : Promise<PlaylistCreatedResult> {
    return await TAURI_INVOKE("playlist_create", { handle, name, itemIds });
},
/**
 * Delete a playlist
 */
async playlistDelete(handle: string, playlistId: string) : Promise<null> {
    return await TAURI_INVOKE("playlist_delete", { handle, playlistId });
},
/**
 * Rename a playlist
 */
async playlistRename(handle: string, playlistId: string, name: string) : Promise<null> {
    return await TAURI_INVOKE("playlist_rename", { handle, playlistId, name });
},
/**
 * Get playlist items with PlaylistItemId
 */
async playlistGetItems(handle: string, playlistId: string) : Promise<PlaylistEntry[]> {
    return await TAURI_INVOKE("playlist_get_items", { handle, playlistId });
},
/**
 * Add items to a playlist
 */
async playlistAddItems(handle: string, playlistId: string, itemIds: string[]) : Promise<null> {
    return await TAURI_INVOKE("playlist_add_items", { handle, playlistId, itemIds });
},
/**
 * Remove items from a playlist (uses PlaylistItemId entry IDs, NOT media item IDs)
 */
async playlistRemoveItems(handle: string, playlistId: string, entryIds: string[]) : Promise<null> {
    return await TAURI_INVOKE("playlist_remove_items", { handle, playlistId, entryIds });
},
/**
 * Move a playlist item to a new position
 */
async playlistMoveItem(handle: string, playlistId: string, itemId: string, newIndex: number) : Promise<null> {
    return await TAURI_INVOKE("playlist_move_item", { handle, playlistId, itemId, newIndex });
},
/**
 * Format time in seconds to MM:SS display string
 * 
 * # Arguments
 * * `seconds` - Time in seconds
 * 
 * # Returns
 * Formatted string like "3:45" or "12:09"
 */
async formatTimeSeconds(seconds: number) : Promise<string> {
    return await TAURI_INVOKE("format_time_seconds", { seconds });
},
/**
 * Format time in seconds to HH:MM:SS or MM:SS display string
 * 
 * Automatically chooses format based on duration:
 * - Less than 1 hour: Returns MM:SS format
 * - 1 hour or more: Returns HH:MM:SS format
 * 
 * # Arguments
 * * `seconds` - Time in seconds
 * 
 * # Returns
 * Formatted string like "1:23:45" or "3:45"
 */
async formatTimeSecondsLong(seconds: number) : Promise<string> {
    return await TAURI_INVOKE("format_time_seconds_long", { seconds });
},
/**
 * Convert Jellyfin ticks to seconds
 * 
 * # Arguments
 * * `ticks` - Time in Jellyfin ticks (10,000,000 ticks = 1 second)
 * 
 * # Returns
 * Time in seconds
 */
async convertTicksToSeconds(ticks: number) : Promise<number> {
    return await TAURI_INVOKE("convert_ticks_to_seconds", { ticks });
},
/**
 * Calculate progress percentage from position and duration
 * 
 * # Arguments
 * * `position` - Current position in seconds
 * * `duration` - Total duration in seconds
 * 
 * # Returns
 * Progress as percentage (0.0 to 100.0)
 */
async calcProgress(position: number, duration: number) : Promise<number> {
    return await TAURI_INVOKE("calc_progress", { position, duration });
},
/**
 * Convert percentage volume (0-100) to normalized (0.0-1.0)
 * 
 * # Arguments
 * * `percent` - Volume as percentage (0 to 100)
 * 
 * # Returns
 * Normalized volume (0.0 to 1.0)
 */
async convertPercentToVolume(percent: number) : Promise<number> {
    return await TAURI_INVOKE("convert_percent_to_volume", { percent });
}
}

/** user-defined events **/


export const events = __makeEvents__<{
playerStatusEvent: PlayerStatusEvent
}>({
playerStatusEvent: "player-status-event"
})

/** user-defined constants **/



/** user-defined types **/

/**
 * Active session info (for session restoration)
 */
export type ActiveSession = { userId: string; username: string; serverId: string; serverUrl: string; serverName: string; accessToken: string }
/**
 * Request to add items to queue
 */
export type AddToQueueRequest = { items: PlayItemRequest[]; position: string }
/**
 * Request to add a track by ID - backend fetches metadata
 */
export type AddTrackByIdRequest = { trackId: string; position: string }
/**
 * Request to add multiple tracks by IDs - backend fetches metadata
 */
export type AddTracksByIdsRequest = { trackIds: string[]; position: string }
/**
 * Album affinity status info
 */
export type AlbumAffinityStatus = { albumId: string; uniqueTracksPlayed: number; threshold: number; thresholdReached: boolean }
/**
 * Album recommendation info
 */
export type AlbumRecommendation = { album_id: string; album_name: string; tracks_played: number; total_tracks: number; should_download: boolean }
/**
 * Storage info for a single album
 */
export type AlbumStorageInfo = { album_id: string; album_name: string; artist_name: string | null; bytes_used: number; track_count: number }
/**
 * Artist item with ID and name (for clickable artist links)
 */
export type ArtistItem = { id: string; name: string }
/**
 * Audio playback settings
 */
export type AudioSettings = { 
/**
 * Crossfade duration in seconds (0 = disabled, max 12)
 */
crossfadeDuration: number; 
/**
 * Enable gapless playback between tracks
 */
gaplessPlayback: boolean; 
/**
 * Enable volume normalization
 */
normalizeVolume: boolean; 
/**
 * Target volume level for normalization
 */
volumeLevel: VolumeLevel; 
/**
 * Enable the graphic equalizer. When false, no EQ filter is applied.
 */
equalizerEnabled?: boolean; 
/**
 * Per-band gains in dB, one per [`EQ_BANDS`]. Normalised to 10 entries and
 * clamped to [`EQ_GAIN_MIN`, `EQ_GAIN_MAX`] via [`Self::with_equalizer_normalised`].
 */
equalizerBands?: number[] }
/**
 * Response for audio track switching operations
 */
export type AudioTrackSwitchResponse = 
/**
 * Native backend handled it (Android ExoPlayer)
 */
{ strategy: "native"; success: boolean } | 
/**
 * HTML5 needs to reload stream with new audio track
 */
{ strategy: "reloadStream"; new_url: string; position: number }
/**
 * Authentication result
 */
export type AuthResult = { user: User; accessToken: string; serverId: string }
/**
 * Server information returned from Jellyfin
 */
export type AuthServerInfo = { name: string; version: string; id: string; 
/**
 * Normalized server URL with protocol and no trailing slash
 */
normalizedUrl: string }
/**
 * Autoplay settings (controls next episode behavior)
 */
export type AutoplaySettings = { 
/**
 * Whether autoplay is enabled for next episodes
 */
enabled: boolean; 
/**
 * Countdown duration in seconds before auto-playing next episode
 */
countdownSeconds: number; 
/**
 * Maximum number of episodes to auto-play consecutively (0 = unlimited)
 */
maxEpisodes?: number }
/**
 * Smart caching configuration
 */
export type CacheConfig = { 
/**
 * Enable queue pre-caching
 */
queuePrecacheEnabled: boolean; 
/**
 * Number of tracks to pre-cache from queue
 */
queuePrecacheCount: number; 
/**
 * Enable album affinity detection
 */
albumAffinityEnabled: boolean; 
/**
 * Threshold for album affinity (tracks played before caching)
 */
albumAffinityThreshold: number; 
/**
 * Storage limit in bytes (0 = unlimited)
 */
storageLimit: number; 
/**
 * Only cache on WiFi
 */
wifiOnly: boolean; 
/**
 * How long a temporary (`download_source = 'auto'`) download lives before
 * it is reclaimed, in hours. 0 disables expiry, leaving space pressure as
 * the only reclaim trigger.
 * 
 * TRACES: UR-071 | DR-127
 */
temporaryTtlHours: number }
/**
 * Cached media item returned to frontend
 */
export type CachedItem = { id: string; name: string; itemType: string; parentId: string | null; libraryId: string | null; overview: string | null; genres: string | null; runtimeTicks: number | null; productionYear: number | null; communityRating: number | null; officialRating: string | null; primaryImageTag: string | null; albumId: string | null; albumName: string | null; albumArtist: string | null; artists: string | null; indexNumber: number | null; seriesId: string | null; seriesName: string | null; seasonId: string | null; seasonName: string | null; parentIndexNumber: number | null }
/**
 * Item-person association for caching
 */
export type CachedItemPerson = { itemId: string; personId: string; serverId: string; personType: string; role: string | null; sortOrder: number }
/**
 * Cached library info returned to frontend
 */
export type CachedLibrary = { id: string; serverId: string; name: string; collectionType: string | null; imageTag: string | null }
/**
 * Cached person info returned to frontend
 */
export type CachedPerson = { id: string; serverId: string; name: string; overview: string | null; primaryImageTag: string | null; premiereDate: string | null; endDate: string | null }
export type CatalogSyncResult = { 
/**
 * Total items persisted to the offline cache across all libraries.
 */
itemsCached: number; 
/**
 * Libraries that failed to sync (e.g. server hiccup); best-effort.
 */
librariesFailed: number; 
/**
 * Entries removed because the server no longer has them. Always 0 when any
 * library failed, since a partial crawl cannot prove an item is gone.
 */
itemsPruned: number }
export type CatalogSyncStatus = { 
/**
 * RFC-3339 timestamp of the last successful sync, if any.
 */
lastSyncedAt: string | null }
/**
 * Connectivity status
 */
export type ConnectivityStatus = { 
/**
 * Whether the Jellyfin server is reachable
 */
isServerReachable: boolean; 
/**
 * Last time we checked server reachability (ISO 8601 string)
 */
lastChecked: string | null; 
/**
 * Error message from last connectivity check
 */
connectionError: string | null; 
/**
 * Whether we're currently checking connectivity
 */
isChecking: boolean }
/**
 * On-disk usage of downloaded content, for the Downloads surface.
 * 
 * `sizes` maps an item id (leaf *or* container) to its bytes on disk: a leaf's
 * own file size, a container's summed downloaded descendants. `device_total_bytes`
 * and `item_count` are the headline figures for the Downloaded surface top bar.
 * 
 * TRACES: UR-056 | DR-085
 */
export type DownloadDiskUsage = { 
/**
 * item id → bytes on disk (leaf's own size, or a container's subtotal).
 */
sizes: Partial<{ [key in string]: number }>; 
/**
 * Container id → true when it is only *partially* downloaded (has cached
 * children that are not downloaded). Absent/false ⇒ fully downloaded. Lets
 * the Downloaded surface badge partial vs. full containers.
 */
partialContainers: Partial<{ [key in string]: boolean }>; 
/**
 * Sum of all downloaded leaf sizes — the device total.
 */
deviceTotalBytes: number; 
/**
 * Number of downloaded leaf items (not containers).
 */
itemCount: number }
/**
 * Information about a download
 */
export type DownloadInfo = { id: number; itemId: string; userId: string; filePath: string; fileSize: number | null; mimeType: string | null; status: string; progress: number; bytesDownloaded: number; queuedAt: string; startedAt: string | null; completedAt: string | null; errorMessage: string | null; retryCount: number; priority: number; itemName: string | null; artistName: string | null; albumName: string | null; seriesName: string | null; seasonName: string | null; episodeNumber: number | null; seasonNumber: number | null; qualityPreset: string | null; mediaType: string; downloadSource: string }
/**
 * Request payload for download_item_and_start (bundled to stay within specta's
 * 10-argument command limit).
 */
export type DownloadItemAndStartRequest = { itemId: string; userId: string; streamUrl: string; targetDir: string; itemName: string | null; artistName: string | null; albumName: string | null }
/**
 * Request payload for download_item.
 */
export type DownloadItemRequest = { itemId: string; userId: string; filePath: string; mimeType: string | null; priority: number | null; itemName: string | null; artistName: string | null; albumName: string | null; expectedSize: number | null }
/**
 * Download manager statistics
 */
export type DownloadManagerStats = { max_concurrent: number; active_count: number; available_slots: number }
/**
 * Download statistics computed server-side
 */
export type DownloadStats = { total: number; activeCount: number; queuedCount: number; completedCount: number; failedCount: number; pausedCount: number }
/**
 * Request payload for download_video.
 */
export type DownloadVideoRequest = { itemId: string; userId: string; filePath: string; mimeType: string | null; priority: number | null; itemName: string | null; qualityPreset: string | null; seriesName: string | null; seasonName: string | null; episodeNumber: number | null; seasonNumber: number | null }
/**
 * Enhanced response with pre-computed stats
 */
export type DownloadsResponse = { downloads: DownloadInfo[]; stats: DownloadStats }
/**
 * What a drain did, for logging and for the frontend's "Sync now" button.
 */
export type DrainReport = { 
/**
 * Rows that reached the server and are now `completed`.
 */
pushed: number; 
/**
 * Rows that failed and will be retried on the next reconnect.
 */
deferred: number; 
/**
 * Rows that exhausted `MAX_SYNC_ATTEMPTS` and were given up on.
 */
abandoned: number; 
/**
 * Rows still waiting afterwards (what the badge counts).
 */
remaining: number }
/**
 * Built-in equalizer presets. A preset *is* a gain curve defined by the band
 * layout above (a domain concept), not a mere label — the curve numbers live
 * in Rust so the frontend never encodes the taxonomy.
 * 
 * TRACES: UR-027 | DR-030
 */
export type EqPreset = "flat" | "rock" | "pop" | "jazz" | "classical" | "bassBoost" | "trebleBoost" | "vocal"
/**
 * Genre
 */
export type Genre = { id: string; name: string; 
/**
 * Number of albums tagged with this genre, when the backend can supply it
 * (online only). Lets the frontend rank/pick genres without probing each
 * one. `None` when unknown (e.g. offline).
 */
albumCount: number | null }
/**
 * Request to get an image URL (with caching)
 */
export type GetImageRequest = { itemId: string; imageType: string; maxWidth?: number | null; maxHeight?: number | null; tag?: string | null }
/**
 * Options for querying items
 */
export type GetItemsOptions = { startIndex?: number | null; limit?: number | null; sortBy?: string | null; sortOrder?: string | null; includeItemTypes?: string[] | null; recursive?: boolean | null; fields?: string[] | null; genres?: string[] | null; 
/**
 * Restrict the listing to favourited items. Backs the per-library
 * favourites toggle; composes with every other filter here.
 * 
 * TRACES: UR-067 | DR-116 | UT-104
 */
favoritesOnly?: boolean | null }
/**
 * Image options
 */
export type ImageOptions = { maxWidth?: number | null; maxHeight?: number | null; quality?: number | null; tag?: string | null }
/**
 * Image type
 */
export type ImageType = "Primary" | "Backdrop" | "Banner" | "Thumb" | "Logo"
/**
 * A single actor returned by the JRay plugin's "context at time t" endpoint.
 * 
 * Mirrors the `actors[]` objects from `GET /Plugins/JRay/Items/{id}/jray?t=`.
 * `jellyfin_id` (a Jellyfin Person item GUID) is preferred for navigation;
 * the IMDb/TMDb ids are informational fallbacks. Unknown ids are `""`.
 */
export type JRayActor = { name: string; imdb_id?: string; tmdb_id?: string; jellyfin_id?: string }
/**
 * Library (media collection)
 */
export type Library = { id: string; name: string; collectionType: string; imageTag?: string | null }
/**
 * Live stream information returned from opening a Live TV / channel stream.
 * 
 * Unlike on-demand video, a live channel must be "opened" before it can be
 * streamed; the server returns a transcoding URL (already absolute) plus a
 * `live_stream_id` that can later be used to close the stream.
 */
export type LiveStreamInfo = { streamUrl: string; playSessionId: string | null; liveStreamId: string | null; mediaSourceId: string | null }
/**
 * An LMS multi-room sync group, as returned by JellyLMS `/JellyLms/SyncGroups`.
 * 
 * Mirrors the plugin's `SyncGroup` model. The master is the sync source; slaves
 * follow it in lockstep.
 */
export type LmsSyncGroup = { masterMac: string; masterName?: string; slaveMacs?: string[]; slaveNames?: string[] }
/**
 * Media item
 */
export type MediaItem = { id: string; name: string; 
/**
 * Legacy Jellyfin item-type string (`"Audio"`, `"MusicAlbum"`, …).
 * 
 * Dual-carry migration (docs/specs/frontend-domain-model.md): `kind` below
 * is the neutral replacement. This field stays while the frontend migrates
 * off it, then is removed in a later phase. New Rust code should read
 * `kind`, not this.
 */
type: string; 
/**
 * Provider-neutral classification — the replacement for `item_type`.
 * Populated by the Jellyfin mapping; defaults to `Other` for the handful of
 * construction sites that have not been migrated yet.
 */
kind?: MediaKind; 
/**
 * Whether this item is a folder/container (vs a playable leaf). Used to
 * decide whether a channel item drills into a list or plays directly.
 */
isFolder?: boolean; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null; 
/**
 * ISO-8601 release/air date (Jellyfin `PremiereDate`). Used to sort
 * podcast episodes by release date.
 */
premiereDate?: string | null; communityRating?: number | null; officialRating?: string | null; 
/**
 * Legacy Jellyfin duration in ticks (100 ns units). Being replaced by
 * `duration_ms`; dual-carried while the frontend migrates
 * (docs/specs/frontend-domain-model.md). New code should read `duration_ms`.
 */
runTimeTicks?: number | null; 
/**
 * Duration in milliseconds — the neutral replacement for `runtime_ticks`.
 * Ticks never reach the frontend; this does.
 */
durationMs?: number | null; 
/**
 * Legacy Jellyfin primary image tag. Being replaced by `image_id`;
 * dual-carried while the frontend migrates. New code should read `image_id`.
 */
primaryImageTag?: string | null; 
/**
 * Neutral image identifier the frontend resolves to a URL via the image
 * command — the replacement for `primary_image_tag`. Same value today
 * (Jellyfin's tag is the id); the rename removes the provider term.
 */
imageId?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }
/**
 * The kind of a media item — provider-neutral classification.
 * 
 * Replaces the stringly-typed `item_type` that carried Jellyfin's vocabulary
 * (`"Audio"`, `"MusicAlbum"`, …) across the boundary. A closed enum means a
 * typo or an unhandled kind is a compile error on the frontend, not a silent
 * runtime miss across ~127 comparison sites.
 */
export type MediaKind = "track" | "album" | "artist" | "playlist" | "movie" | "series" | "season" | "episode" | "person" | 
/**
 * A channel *container* the user drills into (Jellyfin `Channel`).
 */
"channel" | "folder" | 
/**
 * A live TV channel — playable, but a live stream with no seekable
 * timeline (no resume/seek). Jellyfin `TvChannel`/`LiveTvChannel`.
 */
"liveChannel" | 
/**
 * A playable leaf inside a channel (Jellyfin `ChannelFolderItem` that is
 * not itself a folder) — e.g. a plugin-channel VOD item that has no
 * dedicated item type but carries its own media streams. Playable and
 * seekable, unlike `LiveChannel`. Distinct from `Channel` (the container)
 * and from `Other` so the UI can route it to playback.
 */
"channelItem" | 
/**
 * A kind we do not model explicitly. Reached only for provider item types
 * that map to nothing meaningful; consumers treat it like an opaque
 * container. The mapping must be *total* — it never panics — so this is the
 * safe sink for unknown strings. Also the `Default`, so a defaulted
 * `MediaItem` (see the dual-carry migration) is inert rather than a lie.
 */
"other"
/**
 * Media session type tracking the high-level playback context
 */
export type MediaSessionType = 
/**
 * No active session - browsing library
 */
{ type: "idle" } | 
/**
 * Audio playback session (music, audiobooks, podcasts)
 * Persists until explicitly dismissed
 */
{ type: "audio"; last_item: PlayerMediaItem | null; is_active: boolean } | 
/**
 * Movie playback (single video, auto-dismiss on end)
 */
{ type: "movie"; item: PlayerMediaItem; is_active: boolean } | 
/**
 * TV show playback (supports next episode auto-advance)
 */
{ type: "tv_show"; item: PlayerMediaItem; series_id: string; is_active: boolean }
/**
 * Media source information
 */
export type MediaSource = { id: string; name: string; container?: string | null; size?: number | null; bitrate?: number | null; supportsDirectPlay: boolean; supportsDirectStream: boolean; supportsTranscoding: boolean; directStreamUrl?: string | null }
/**
 * Media stream information (audio, video, subtitle tracks)
 */
export type MediaStream = { 
/**
 * Legacy Jellyfin stream type string ("Audio"/"Video"/"Subtitle"). Being
 * replaced by `kind`; dual-carried while the frontend migrates.
 */
type: string; 
/**
 * Provider-neutral stream classification — replaces `stream_type`.
 */
kind?: StreamKind; codec?: string | null; language?: string | null; displayTitle?: string | null; index: number; isDefault: boolean; isForced: boolean }
export type MediaType = "audio" | "video"
/**
 * Lightweight media item for merged playback state
 * Converts from both local MediaItem and remote NowPlayingItem
 */
export type MergedMediaItem = { id: string; title: string; artist: string | null; album: string | null; albumId: string | null; duration: number | null; primaryImageTag: string | null; 
/**
 * Neutral image identifier — replaces `primary_image_tag` (same value).
 */
imageId: string | null; mediaType: string }
/**
 * Argument struct for [`set_network_state`].
 * 
 * TRACES: UR-053 | DR-074
 */
export type NetworkStateWrapperArg = { networkType: NetworkType; unmetered: boolean }
/**
 * Kind of network transport currently active.
 * 
 * Mirrors the string constants in `NetworkTypeMonitor.kt`; the two must stay
 * in sync (the serde rename below is what the frontend sends).
 * 
 * TRACES: UR-053 | DR-074
 */
export type NetworkType = 
/**
 * No active network.
 */
"none" | 
/**
 * WiFi (may still be metered — check `unmetered`).
 */
"wifi" | 
/**
 * Wired ethernet, typical on Android TV and desktop.
 */
"ethernet" | 
/**
 * Mobile data — never acceptable when wifi-only is enabled.
 */
"cellular" | 
/**
 * Some other transport (VPN over unknown carrier, Bluetooth tethering, …).
 */
"other" | 
/**
 * Could not determine the transport.
 */
"unknown"
export type NowPlayingItem = { id: string | null; name: string | null; runTimeTicks: number | null; album: string | null; albumId: string | null; albumArtist: string | null; artists: string[] | null; imageTags: Partial<{ [key in string]: string }> | null; primaryImageTag: string | null; albumPrimaryImageTag: string | null; Type: string | null }
export type OfflineItem = { id: string; name: string; itemType: string; albumId: string | null; albumName: string | null; artists: string | null; runtimeTicks: number | null; primaryImageTag: string | null }
/**
 * Person (cast/crew member) - for movies, series, and episodes
 */
export type Person = { 
/**
 * Deserializes from API's "Id" field (PascalCase), serializes as "id" (camelCase to frontend)
 */
id?: string; 
/**
 * Deserializes from API's "Name" field (PascalCase), serializes as "name" (camelCase to frontend)
 */
name?: string; 
/**
 * Person type from Jellyfin API (Actor, Director, Writer, etc.)
 * Deserializes from API's "Type" field (PascalCase), serializes as "type" (camelCase to frontend)
 */
type?: string; 
/**
 * Deserializes from API's "Role" field (PascalCase), serializes as "role" (camelCase to frontend)
 */
role?: string | null; 
/**
 * Deserializes from API's "PrimaryImageTag" field (PascalCase), serializes as "primaryImageTag" (camelCase to frontend)
 */
primaryImageTag?: string | null }
/**
 * Request to play a track from an album (backend fetches all tracks)
 */
export type PlayAlbumTrackRequest = { albumId: string; albumName: string; trackId: string; shuffle: boolean }
/**
 * Request to play a single video item
 * 
 * Simplified to video playback only. Audio playback uses player_play_tracks
 * to avoid Tauri Android serialization issues with complex objects.
 */
export type PlayItemRequest = { id: string; title: string; streamUrl: string; 
/**
 * Video codec (e.g., "h264", "hevc") for video media
 */
videoCodec: string; 
/**
 * Whether the video requires server-side transcoding
 */
needsTranscoding: boolean; 
/**
 * Optional now-playing metadata. Used by the background-audio handoff so the
 * lockscreen/miniplayer show the item (title/subtitle/artwork). Defaulted so
 * existing video-only callers need not send them.
 */
artist?: string | null; primaryImageTag?: string | null; serverId?: string | null; 
/**
 * Total media duration (seconds). Threaded through the background-audio
 * handoff so the lockscreen MediaSession advertises a real duration — a
 * zero-duration session renders no scrubber, even with ACTION_SEEK_TO set.
 */
durationSeconds?: number | null; 
/**
 * Item type (e.g. "Episode", "Movie", "Audio"). Carried through the
 * background-audio handoff so an episode played as audio-only is still
 * recognised as an episode by autoplay (UR-040) and advances to the next one.
 */
itemType?: string | null; 
/**
 * Series ID for TV episodes. Needed alongside `item_type` so the backend can
 * look up the next episode when a background-audio track ends.
 */
seriesId?: string | null; 
/**
 * Subtitle tracks to sideload, with URLs the frontend has already resolved.
 * 
 * Only the native backends use these: on Android they become the
 * `MediaItem.SubtitleConfiguration`s ExoPlayer renders. The HTML5 path
 * builds its own `<track>` children instead and ignores this list.
 * 
 * **Order is the contract.** `player_set_subtitle_track(n)` reaches
 * `JellyTauPlayer.setSubtitleTrack(n)`, which indexes into ExoPlayer's
 * *text track groups* — i.e. the position of the sideloaded configuration,
 * not the Jellyfin stream index (which is kept on each entry for the UI's
 * benefit). So `n` must be a position in this very array, and the array
 * must not be reordered or filtered between building it and sending it.
 * `nativeSubtitleArrayIndex()` on the frontend computes `n` from the same
 * list that is sent here, for exactly this reason.
 * 
 * Defaulted so the background-audio handoff and the autoplay/next-episode
 * callers, which have no subtitles to offer, need not send the field.
 * 
 * TRACES: UR-020 | IR-016, JA-008 | UT-145
 */
subtitles?: SubtitleTrack[] }
/**
 * Queue context for remote transfer - what type of queue is this?
 */
export type PlayQueueContext = 
/**
 * Playing from a specific album
 */
{ type: "album"; albumId: string; albumName: string } | 
/**
 * Playing from a specific playlist
 */
{ type: "playlist"; playlistId: string; playlistName: string } | 
/**
 * Custom queue (search results, manual queue, etc.)
 */
{ type: "custom" }
/**
 * Request to play a queue of items
 */
export type PlayQueueRequest = { items: PlayItemRequest[]; startIndex: number; shuffle: boolean; 
/**
 * Optional context for the queue (album, playlist, or custom)
 * Used for remote playback transfer
 */
context?: PlayQueueContext | null }
export type PlayState = { positionTicks?: number | null; canSeek?: boolean | null; isPaused?: boolean | null; isMuted?: boolean | null; volumeLevel?: number | null; repeatMode?: string | null; shuffleMode?: string | null }
/**
 * Context information for track playback
 */
export type PlayTracksContext = { type: "playlist"; playlistId: string; playlistName: string } | { type: "search"; searchQuery: string } | { type: "custom"; label: string | null }
/**
 * Request to play tracks by ID (backend fetches metadata)
 */
export type PlayTracksRequest = { trackIds: string[]; startIndex: number; shuffle: boolean; context: PlayTracksContext; 
/**
 * Position (seconds) to resume the starting track from. Used when taking
 * over playback from a remote session so we don't restart from 0.
 */
startPosition?: number | null }
/**
 * What playback facilities this platform's backend actually provides.
 * 
 * The frontend is presentation-only and must not re-derive backend facts from
 * `navigator.userAgent` — that sniffing was a second copy of the same platform
 * decision Rust already makes with `cfg!`, and it drifted. These flags are the
 * single source of truth; the frontend consumes them.
 * 
 * TRACES: UR-003, UR-005 | DR-004, DR-023, DR-024
 */
export type PlaybackCapabilities = { 
/**
 * True when audio is rendered by a webview `<audio>` element rather than a
 * native backend. Native audio exists on Linux (mpv) and Android
 * (ExoPlayer); everything else (Windows, future desktops) uses the webview.
 */
usesWebviewAudio: boolean; 
/**
 * True when video can be rendered by a native surface composited *behind*
 * a transparent webview. Android only: ExoPlayer draws into a SurfaceView
 * beneath the WebView. Linux cannot do this (WebKitGTK/Wayland
 * compositing), so it stays on the HTML5 element.
 */
supportsNativeVideo: boolean }
/**
 * Playback information
 */
export type PlaybackInfo = { mediaSourceId: string; playSessionId: string; streamUrl: string; directPlay: boolean; needsTranscoding: boolean }
/**
 * Playback mode - local device, remote session, or idle
 */
export type PlaybackMode = { type: "local" } | { type: "remote"; session_id: string } | { type: "idle" }
/**
 * Playback progress info
 */
export type PlaybackProgress = { itemId: string; 
/**
 * Resume position in milliseconds. Stored as Jellyfin ticks in the DB;
 * converted here so the frontend never sees ticks.
 */
positionMs: number; isPlayed: boolean; isFavorite: boolean; playCount: number }
/**
 * Represents a media item that can be played
 * 
 * TRACES: UR-003, UR-004 | DR-002
 */
export type PlayerMediaItem = { 
/**
 * Unique identifier
 */
id: string; 
/**
 * Display title
 */
title: string; 
/**
 * Name (alias for title - for frontend compatibility)
 */
name?: string | null; 
/**
 * Artist name(s) for audio
 */
artist: string | null; 
/**
 * Album name for audio
 */
album: string | null; 
/**
 * Album name (alias - for frontend compatibility)
 */
albumName?: string | null; 
/**
 * Album ID (Jellyfin ID) for remote transfer context
 */
albumId?: string | null; 
/**
 * Artist items with IDs for clickable links
 */
artistItems?: ArtistItem[] | null; 
/**
 * Artists as array of strings (fallback when artist_items not available)
 */
artists?: string[] | null; 
/**
 * Primary image tag for artwork.
 * 
 * Legacy Jellyfin name; being replaced by `image_id` (same value). Dual-carried
 * while the frontend migrates (docs/specs/frontend-domain-model.md).
 */
primaryImageTag?: string | null; 
/**
 * Neutral image identifier the frontend resolves to a URL — replaces
 * `primary_image_tag`.
 */
imageId?: string | null; 
/**
 * Item type (Audio, Movie, Episode, etc.)
 */
type?: string | null; 
/**
 * Playlist ID (Jellyfin ID) for remote transfer context
 */
playlistId?: string | null; 
/**
 * Duration in seconds
 */
duration: number | null; 
/**
 * URL or path to artwork image
 */
artworkUrl: string | null; 
/**
 * Type of media
 */
mediaType: MediaType; 
/**
 * Source of the media
 */
source: PlayerMediaSource; 
/**
 * Video codec (e.g., "h264", "hevc") for video media
 */
videoCodec?: string | null; 
/**
 * Whether the video requires server-side transcoding
 */
needsTranscoding?: boolean; 
/**
 * Video width in pixels
 */
videoWidth?: number | null; 
/**
 * Video height in pixels
 */
videoHeight?: number | null; 
/**
 * Available subtitle tracks
 */
subtitles?: SubtitleTrack[]; 
/**
 * Series ID (for TV show episodes) - used for series audio preferences
 */
seriesId?: string | null; 
/**
 * Server ID - used for series audio preferences
 */
serverId?: string | null }
/**
 * TRACES: UR-002, UR-003, UR-004, UR-011 | DR-003
 */
export type PlayerMediaSource = 
/**
 * Streaming from Jellyfin server
 */
{ type: "remote"; stream_url: string; jellyfin_item_id: string } | 
/**
 * Downloaded/cached locally
 */
{ type: "local"; file_path: string; jellyfin_item_id: string | null } | 
/**
 * Direct URL (e.g., channel plugins)
 */
{ type: "directurl"; url: string }
/**
 * Player state machine (6 states: Idle, Loading, Playing, Paused, Seeking, Error)
 * 
 * TRACES: UR-005 | DR-001
 */
export type PlayerState = 
/**
 * No media loaded
 */
{ kind: "idle" } | 
/**
 * Media is being loaded/buffered
 */
{ kind: "loading"; media: PlayerMediaItem } | 
/**
 * Media is playing
 */
{ kind: "playing"; media: PlayerMediaItem; position: number; duration: number } | 
/**
 * Media is paused
 */
{ kind: "paused"; media: PlayerMediaItem; position: number; duration: number } | 
/**
 * Seeking to a new position
 */
{ kind: "seeking"; media: PlayerMediaItem; target: number } | 
/**
 * An error occurred
 */
{ kind: "error"; media: PlayerMediaItem | null; error: string }
/**
 * Response for player state queries
 */
export type PlayerStatus = { state: PlayerState; position: number; duration: number | null; volume: number; muted: boolean; shuffle: boolean; repeat: RepeatMode; 
/**
 * Backend being used (native = ExoPlayer/libmpv, html5 = fallback)
 */
backend: VideoBackend; 
/**
 * Whether frontend should render HTML5 video element
 */
useHtml5Element: boolean; 
/**
 * Media item from either local queue or remote session
 */
mergedMedia: MergedMediaItem | null; 
/**
 * Playing state from either local player or remote session
 */
mergedIsPlaying: boolean; 
/**
 * Volume from either local player or remote session (0-1 normalized)
 */
mergedVolume: number }
/**
 * Events emitted by the player backend to the frontend via Tauri events.
 * 
 * These are distinct from `PlayerEvent` in state.rs, which handles internal
 * state machine transitions.
 * 
 * TRACES: UR-005, UR-019, UR-023, UR-026 | DR-001, DR-028, DR-047
 */
export type PlayerStatusEvent = 
/**
 * Playback position updated (emitted periodically during playback)
 */
{ type: "position_update"; position: number; duration: number } | 
/**
 * Player state changed
 */
{ type: "state_changed"; state: string; media_id: string | null } | 
/**
 * Media has finished loading and is ready to play
 */
{ type: "media_loaded"; duration: number } | 
/**
 * Playback has ended naturally (reached end of media)
 */
{ type: "playback_ended" } | 
/**
 * Buffering state changed
 */
{ type: "buffering"; percent: number } | 
/**
 * An error occurred during playback
 */
{ type: "error"; message: string; recoverable: boolean } | 
/**
 * Volume changed
 */
{ type: "volume_changed"; volume: number; muted: boolean } | 
/**
 * Sleep timer state changed
 */
{ type: "sleep_timer_changed"; mode: SleepTimerMode; remaining_seconds: number } | 
/**
 * Time-based sleep timer expired: playback must stop. The backend stops
 * its own (MPV/ExoPlayer) playback, but HTML5 video on Linux plays in the
 * webview outside the backend's control — the frontend pauses it on this
 * event.
 */
{ type: "sleep_timer_expired" } | 
/**
 * Show next episode popup with countdown
 */
{ type: "show_next_episode_popup"; current_episode: MediaItem; next_episode: MediaItem; countdown_seconds: number; auto_advance: boolean } | 
/**
 * Countdown tick (emitted every second during autoplay countdown)
 */
{ type: "countdown_tick"; remaining_seconds: number } | 
/**
 * Queue changed (items added, removed, reordered, or playback mode changed)
 */
{ type: "queue_changed"; items: PlayerMediaItem[]; current_index: number | null; shuffle: boolean; repeat: RepeatMode; has_next: boolean; has_previous: boolean } | 
/**
 * Media session changed (activity context changed: Audio/Movie/TvShow/Idle)
 */
{ type: "session_changed"; session: MediaSessionType } | 
/**
 * Remote sessions updated (for cast/remote control UI)
 */
{ type: "sessions_updated"; sessions: SessionInfo[] } | 
/**
 * The authoritative playback mode changed in the Rust backend.
 * 
 * The Rust `PlaybackModeManager` is the single source of truth for which
 * device playback commands route to (local vs a remote session). The
 * frontend keeps a mirror store for the UI; without this event that mirror
 * drifts out of sync (e.g. a mode transition happens inside a transfer or a
 * local stop that the frontend never learns about), and controls then route
 * to the wrong device — the classic "it keeps playing on the remote" bug.
 * The frontend reconciles its store to this payload whenever it fires.
 */
{ type: "playback_mode_changed"; mode: string; session_id: string | null } | 
/**
 * The user asked to disconnect from the remote session and resume locally.
 * 
 * Emitted when the lockscreen Stop button is pressed while casting. The
 * frontend owns the two-step remote->local transfer (it must reload the
 * media item locally), so the native side only signals intent here.
 */
{ type: "remote_disconnect_requested" } | 
/**
 * Backend-originated control command targeting the active frontend player
 * adapter (the HTML5 <video> that lives in the webview, which Rust cannot
 * drive directly). Emitted by control paths like the sleep timer, lockscreen,
 * or remote so they can pause/play/seek/stop the webview element.
 * `playerEvents.ts` routes this to the active PlayerAdapter via the facade.
 */
{ type: "control_command"; action: string; position: number | null } | 
/**
 * Ask the frontend webview `<audio>` element to load and play a stream.
 * 
 * Emitted by `WebviewAudioBackend` on platforms with no native audio
 * backend (e.g. Windows): audio-only playback is rendered by an `<audio>`
 * element in the webview, mirroring how all video already renders through
 * the webview `<video>`. The element then reports its state/position back
 * through the `player_report_*` commands, so the Rust controller stays the
 * single source of truth. Subsequent play/pause/seek/stop reach the element
 * via `ControlCommand`.
 */
{ type: "webview_audio_load"; url: string; media_id: string | null; position: number; autoplay: boolean }
/**
 * Result of creating a playlist
 * 
 * @req: JA-019 - Get/create/update playlists
 */
export type PlaylistCreatedResult = { id: string }
/**
 * Playlist entry — wraps a MediaItem with the Jellyfin PlaylistItemId
 * needed for remove/reorder operations (distinct from the media item's ID)
 * 
 * @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
 */
export type PlaylistEntry = 
/**
 * The underlying media item
 */
({ id: string; name: string; 
/**
 * Legacy Jellyfin item-type string (`"Audio"`, `"MusicAlbum"`, …).
 * 
 * Dual-carry migration (docs/specs/frontend-domain-model.md): `kind` below
 * is the neutral replacement. This field stays while the frontend migrates
 * off it, then is removed in a later phase. New Rust code should read
 * `kind`, not this.
 */
type: string; 
/**
 * Provider-neutral classification — the replacement for `item_type`.
 * Populated by the Jellyfin mapping; defaults to `Other` for the handful of
 * construction sites that have not been migrated yet.
 */
kind?: MediaKind; 
/**
 * Whether this item is a folder/container (vs a playable leaf). Used to
 * decide whether a channel item drills into a list or plays directly.
 */
isFolder?: boolean; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null; 
/**
 * ISO-8601 release/air date (Jellyfin `PremiereDate`). Used to sort
 * podcast episodes by release date.
 */
premiereDate?: string | null; communityRating?: number | null; officialRating?: string | null; 
/**
 * Legacy Jellyfin duration in ticks (100 ns units). Being replaced by
 * `duration_ms`; dual-carried while the frontend migrates
 * (docs/specs/frontend-domain-model.md). New code should read `duration_ms`.
 */
runTimeTicks?: number | null; 
/**
 * Duration in milliseconds — the neutral replacement for `runtime_ticks`.
 * Ticks never reach the frontend; this does.
 */
durationMs?: number | null; 
/**
 * Legacy Jellyfin primary image tag. Being replaced by `image_id`;
 * dual-carried while the frontend migrates. New code should read `image_id`.
 */
primaryImageTag?: string | null; 
/**
 * Neutral image identifier the frontend resolves to a URL via the image
 * command — the replacement for `primary_image_tag`. Same value today
 * (Jellyfin's tag is the id); the rename removes the provider term.
 */
imageId?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }) & { 
/**
 * The playlist-scoped entry ID (Jellyfin's PlaylistItemId)
 */
playlistItemId: string }
/**
 * Response for preload operation
 */
export type PreloadResult = { 
/**
 * Number of tracks queued for preload
 */
queuedCount: number; 
/**
 * Number of tracks already downloaded
 */
alreadyDownloaded: number; 
/**
 * Number of tracks skipped (no jellyfin ID or other reasons)
 */
skipped: number }
/**
 * Response for queue queries
 */
export type QueueStatus = { items: PlayerMediaItem[]; currentIndex: number | null; shuffle: boolean; repeat: RepeatMode; hasNext: boolean; hasPrevious: boolean }
/**
 * Remote session status for UI updates
 */
export type RemoteSessionStatus = { position: number; duration: number | null; isPlaying: boolean; nowPlayingItem: NowPlayingItem | null }
/**
 * Repeat mode for the queue
 * 
 * TRACES: UR-005 | DR-005
 */
export type RepeatMode = "off" | "all" | "one"
export type ResumeQueuedResult = { 
/**
 * Rows whose stream URL was resolved and are now pump-eligible.
 */
resolved: number; 
/**
 * Rows that couldn't be resolved (item metadata / URL lookup failed).
 */
failed: number }
/**
 * Options for search queries
 */
export type SearchOptions = { limit?: number | null; includeItemTypes?: string[] | null; searchTerm?: string | null; 
/**
 * Opaque scope selected by the UI. When set it **wins** over
 * `include_item_types`, which remains for the non-search `get_items`
 * callers that legitimately request a single concrete type.
 */
scope?: SearchScope | null }
/**
 * Search result with pagination
 */
export type SearchResult = { items: MediaItem[]; totalRecordCount: number }
/**
 * An opaque search scope the frontend selects; Rust owns what it *means*.
 * 
 * The expansion table below is Jellyfin domain vocabulary: it changes when
 * Jellyfin adds or renames an item type, never when the UI is redesigned. It
 * previously lived in the frontend (`searchScope.ts`), which is the boundary
 * leak documented in docs/specs/scoped-search-boundary.md. The frontend now
 * sends the enum and never names an item type in connection with search.
 * 
 * TRACES: UR-049 | DR-063
 */
export type SearchScope = "all" | "music" | "movies" | "tv"
/**
 * Security status info
 */
export type SecurityStatus = { usingKeyring: boolean; storageType: string }
/**
 * Audio track preference for a series
 */
export type SeriesAudioPreference = { seriesId: string; audioTrackDisplayTitle: string | null; audioTrackLanguage: string | null; audioTrackIndex: number | null }
/**
 * Server info returned to frontend
 */
export type ServerInfo = { id: string; name: string; url: string; version: string | null }
/**
 * Active session for restoration
 */
export type Session = { userId: string; username: string; serverId: string; serverUrl: string; serverName: string; accessToken: string; verified: boolean; needsReauth: boolean }
/**
 * Session information from Jellyfin
 */
export type SessionInfo = { id?: string | null; userId?: string | null; userName?: string | null; client?: string | null; deviceName?: string | null; deviceId?: string | null; applicationVersion?: string | null; isActive?: boolean | null; supportsMediaControl?: boolean | null; supportsRemoteControl?: boolean; nowPlayingItem?: NowPlayingItem | null; playState?: PlayState | null; playableMediaTypes?: string[] | null; supportedCommands?: string[] | null }
/**
 * Sleep timer mode - determines when playback should stop
 * TRACES: UR-026 | DR-029
 */
export type SleepTimerMode = 
/**
 * Timer is off
 */
{ kind: "off" } | 
/**
 * Stop after a specific time duration
 */
{ kind: "time"; endTime: number } | 
/**
 * Stop at the end of current track
 */
{ kind: "endOfTrack" } | 
/**
 * Stop after N more episodes complete (TV episodes only, not audio tracks)
 */
{ kind: "episodes"; remaining: number }
/**
 * Sleep timer state
 */
export type SleepTimerState = { mode: SleepTimerMode; remainingSeconds: number }
/**
 * SmartCache statistics
 */
export type SmartCacheStats = { total_size: number; storage_limit: number; available_space: number; items_count: number; config: CacheConfig }
/**
 * Storage statistics for downloads
 */
export type StorageStats = { total_bytes: number; total_items: number; albums: AlbumStorageInfo[] }
/**
 * The kind of a media stream within an item (audio track, video track,
 * subtitle, …) — provider-neutral, replacing the stringly Jellyfin stream type.
 */
export type StreamKind = "audio" | "video" | "subtitle" | 
/**
 * Any stream kind we do not model explicitly (e.g. embedded image, data).
 */
"other"
/**
 * Represents a subtitle track
 * 
 * 🔴 **Do not add `#[serde(rename_all = "camelCase")]` here.** This is the one
 * struct in the player that deliberately keeps snake_case on the wire, because
 * the *same* serialization feeds two consumers that both spell `mime_type`:
 * 
 * * the JNI boundary — `player/android/mod.rs` serializes `MediaItem::subtitles`
 * with `serde_json` and hands the string to `JellyTauPlayer.loadWithMetadata`,
 * whose parser reads `url`, `language`, `label` and `optString("mime_type")`;
 * * the IPC boundary — `PlayItemRequest::subtitles` deserializes this same type
 * from the frontend, and the generated binding (`SubtitleTrack` in
 * `bindings.ts`) therefore also declares `mime_type`.
 * 
 * Renaming would not break the build and would not fail the IPC: Kotlin's
 * `optString` would just fall back to its default MIME type for every track, so
 * the failure would be silent. UT-146 asserts the serialized keys.
 * 
 * TRACES: UR-020 | IR-016, JA-008 | UT-146
 */
export type SubtitleTrack = { 
/**
 * Stream index in the media source
 */
index: number; 
/**
 * Subtitle URL
 */
url: string; 
/**
 * Language code (e.g., "eng", "spa")
 */
language: string | null; 
/**
 * Display title
 */
label: string | null; 
/**
 * MIME type (e.g., "text/vtt", "application/x-subrip").
 * Snake_case on purpose — see the note on the struct.
 */
mime_type: string }
/**
 * Sync queue item returned to frontend
 */
export type SyncQueueItem = { id: number; userId: string; operation: string; itemId: string | null; payload: string | null; status: string; retryCount: number; createdAt: string | null; errorMessage: string | null; 
/**
 * Cached title of the item the operation is about, when the catalog knows
 * it. Resolved here rather than by a per-row frontend fetch — the queue
 * list is otherwise a wall of opaque ids.
 * 
 * TRACES: UR-025 | DR-132
 */
itemName: string | null }
/**
 * Statistics about the thumbnail cache
 */
export type ThumbnailCacheStats = { totalSizeBytes: number; itemCount: number; limitBytes: number }
/**
 * User information
 */
export type User = { id: string; name: string; serverId: string; primaryImageTag: string | null }
/**
 * User-specific data for an item (playback state, favorites, etc.)
 */
export type UserData = { 
/**
 * Legacy Jellyfin resume position in ticks. Being replaced by
 * `playback_position_ms`; dual-carried while the frontend migrates
 * (docs/specs/frontend-domain-model.md). New code should read the ms field.
 */
playbackPositionTicks?: number | null; 
/**
 * Resume position in milliseconds — the neutral replacement for
 * `playback_position_ticks`. Populated from ticks by the mapping; the
 * frontend never divides ticks itself.
 */
playbackPositionMs?: number | null; isPlayed?: boolean | null; isFavorite?: boolean | null; playCount?: number | null; lastPlayedDate?: string | null; playbackContextType?: string | null; playbackContextId?: string | null }
/**
 * User info returned to frontend
 */
export type UserInfo = { id: string; serverId: string; username: string; isActive: boolean }
/**
 * Backend type for video playback
 */
export type VideoBackend = 
/**
 * Native backend (ExoPlayer on Android, libmpv on Linux)
 */
"native" | 
/**
 * HTML5 video element fallback
 */
"html5"
/**
 * Response for video seek operations
 */
export type VideoSeekResponse = 
/**
 * Use native seeking (HLS or direct stream)
 */
{ strategy: "native"; position: number } | 
/**
 * Reload stream from new position (transcoded non-HLS)
 */
{ strategy: "reloadStream"; new_url: string; seek_offset: number }
/**
 * Video playback settings
 */
export type VideoSettings = { 
/**
 * Enable auto-play of next episode (with countdown)
 */
autoPlayNextEpisode: boolean; 
/**
 * Countdown duration in seconds before auto-play (5-30 seconds)
 */
autoPlayCountdownSeconds: number; 
/**
 * Maximum number of episodes to auto-play consecutively (0 = unlimited)
 */
autoPlayMaxEpisodes?: number }
/**
 * Volume normalization levels matching Spotify's presets
 */
export type VolumeLevel = 
/**
 * Louder output (-11 LUFS)
 */
"loud" | 
/**
 * Default level (-14 LUFS)
 */
"normal" | 
/**
 * Quieter output (-23 LUFS)
 */
"quiet"

/** tauri-specta globals **/

import {
	invoke as TAURI_INVOKE,
	Channel as TAURI_CHANNEL,
} from "@tauri-apps/api/core";
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";

type __EventObj__<T> = {
	listen: (
		cb: TAURI_API_EVENT.EventCallback<T>,
	) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
	once: (
		cb: TAURI_API_EVENT.EventCallback<T>,
	) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
	emit: null extends T
		? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
		: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};

export type Result<T, E> =
	| { status: "ok"; data: T }
	| { status: "error"; error: E };

function __makeEvents__<T extends Record<string, any>>(
	mappings: Record<keyof T, string>,
) {
	return new Proxy(
		{} as unknown as {
			[K in keyof T]: __EventObj__<T[K]> & {
				(handle: __WebviewWindow__): __EventObj__<T[K]>;
			};
		},
		{
			get: (_, event) => {
				const name = mappings[event as keyof T];

				return new Proxy((() => {}) as any, {
					apply: (_, __, [window]: [__WebviewWindow__]) => ({
						listen: (arg: any) => window.listen(name, arg),
						once: (arg: any) => window.once(name, arg),
						emit: (arg: any) => window.emit(name, arg),
					}),
					get: (_, command: keyof __EventObj__<any>) => {
						switch (command) {
							case "listen":
								return (arg: any) => TAURI_API_EVENT.listen(name, arg);
							case "once":
								return (arg: any) => TAURI_API_EVENT.once(name, arg);
							case "emit":
								return (arg: any) => TAURI_API_EVENT.emit(name, arg);
						}
					},
				});
			},
		},
	);
}
