Fix download + casting breakages from the specta migration

Casting: restore camelCase serialization on SessionInfo/NowPlayingItem/PlayState
(container rename_all = "camelCase" + per-field PascalCase serde aliases so Jellyfin
PascalCase still deserializes). Bindings and the existing frontend are camelCase
again — casting works with no frontend session changes.

Downloads: migrate DownloadButton.svelte and downloads.ts to the typed
commands.downloadItemAndStart / downloadItem / downloadVideo wrappers (request
objects), matching the bundled backend signatures.

Tooling:
- Enable tauri-specta ErrorHandlingMode::Throw so commands.* return Promise<T>
  and throw (drop-in for invoke()).
- Disambiguate specta name collisions: player MediaItem/MediaSource ->
  PlayerMediaItem/PlayerMediaSource, auth ServerInfo -> AuthServerInfo.
- Regenerate bindings.ts; svelte-check passes (0 errors).
This commit is contained in:
2026-06-20 19:27:29 +02:00
parent 7d07b38d50
commit ea342d76e3
7 changed files with 584 additions and 1514 deletions
+1
View File
@@ -12,6 +12,7 @@ pub use session_verifier::SessionVerifier;
/// Server information returned from Jellyfin
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[specta(rename = "AuthServerInfo")]
pub struct ServerInfo {
pub name: String,
pub version: String,
+34 -3
View File
@@ -382,71 +382,102 @@ fn default_true() -> bool {
/// Session information from Jellyfin
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
#[serde(rename_all = "PascalCase")]
#[serde(rename_all = "camelCase")]
pub struct SessionInfo {
#[serde(default)]
#[serde(alias = "Id")]
pub id: Option<String>,
#[serde(default)]
#[serde(alias = "UserId")]
pub user_id: Option<String>,
#[serde(default)]
#[serde(alias = "UserName")]
pub user_name: Option<String>,
#[serde(default)]
#[serde(alias = "Client")]
pub client: Option<String>,
#[serde(default)]
#[serde(alias = "DeviceName")]
pub device_name: Option<String>,
#[serde(default)]
#[serde(alias = "DeviceId")]
pub device_id: Option<String>,
#[serde(default)]
#[serde(alias = "ApplicationVersion")]
pub application_version: Option<String>,
#[serde(default)]
#[serde(alias = "IsActive")]
pub is_active: Option<bool>,
#[serde(default)]
#[serde(alias = "SupportsMediaControl")]
pub supports_media_control: Option<bool>,
#[serde(default = "default_true")]
#[serde(alias = "SupportsRemoteControl")]
pub supports_remote_control: bool,
#[serde(default)]
#[serde(alias = "NowPlayingItem")]
pub now_playing_item: Option<NowPlayingItem>,
#[serde(default)]
#[serde(alias = "PlayState")]
pub play_state: Option<PlayState>,
#[serde(default)]
#[serde(alias = "PlayableMediaTypes")]
pub playable_media_types: Option<Vec<String>>,
#[serde(default)]
#[serde(alias = "SupportedCommands")]
pub supported_commands: Option<Vec<String>>,
}
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
#[serde(rename_all = "PascalCase")]
#[serde(rename_all = "camelCase")]
pub struct NowPlayingItem {
#[serde(alias = "Id")]
pub id: Option<String>,
#[serde(alias = "Name")]
pub name: Option<String>,
#[serde(alias = "RunTimeTicks")]
pub run_time_ticks: Option<i64>,
#[serde(alias = "Album")]
pub album: Option<String>,
#[serde(alias = "AlbumId")]
pub album_id: Option<String>,
#[serde(alias = "AlbumArtist")]
pub album_artist: Option<String>,
#[serde(alias = "Artists")]
pub artists: Option<Vec<String>>,
#[serde(alias = "ImageTags")]
pub image_tags: Option<std::collections::HashMap<String, String>>,
#[serde(alias = "PrimaryImageTag")]
pub primary_image_tag: Option<String>,
#[serde(alias = "AlbumPrimaryImageTag")]
pub album_primary_image_tag: Option<String>,
#[serde(rename = "Type")]
pub item_type: Option<String>,
}
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
#[serde(rename_all = "PascalCase")]
#[serde(rename_all = "camelCase")]
pub struct PlayState {
#[serde(default)]
#[serde(alias = "PositionTicks")]
pub position_ticks: Option<i64>,
#[serde(default)]
#[serde(alias = "CanSeek")]
pub can_seek: Option<bool>,
#[serde(default)]
#[serde(alias = "IsPaused")]
pub is_paused: Option<bool>,
#[serde(default)]
#[serde(alias = "IsMuted")]
pub is_muted: Option<bool>,
#[serde(default)]
#[serde(alias = "VolumeLevel")]
pub volume_level: Option<i32>,
#[serde(default)]
#[serde(alias = "RepeatMode")]
pub repeat_mode: Option<String>,
#[serde(default)]
#[serde(alias = "ShuffleMode")]
pub shuffle_mode: Option<String>,
}
+3
View File
@@ -351,6 +351,9 @@ fn create_player_backend(
/// bindings-export test so the TypeScript bindings always match the handler.
fn specta_builder() -> Builder<tauri::Wry> {
Builder::<tauri::Wry>::new()
// Throw on error so generated `commands.*` return Promise<T> and throw,
// matching the existing frontend's invoke() try/catch convention.
.error_handling(tauri_specta::ErrorHandlingMode::Throw)
.commands(tauri_specta::collect_commands![
// Player commands
player_play_item,
+2
View File
@@ -42,6 +42,7 @@ pub struct SubtitleTrack {
/// TRACES: UR-003, UR-004 | DR-002
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[specta(rename = "PlayerMediaItem")]
pub struct MediaItem {
/// Unique identifier
pub id: String,
@@ -116,6 +117,7 @@ pub enum MediaType {
/// TRACES: UR-002, UR-003, UR-004, UR-011 | DR-003
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
#[specta(rename = "PlayerMediaSource")]
pub enum MediaSource {
/// Streaming from Jellyfin server
Remote {