From e5c0261158217100dfa53c33487075db15890dea Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:17:19 -0600 Subject: [PATCH] refactor(qconnect): extract wire types and request payloads to types.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move 22 public types (QconnectConnectOptions, QconnectLifecycleState, QconnectConnectionStatus, QconnectVisibleQueueProjection, the 20-variant QconnectOutboundCommandType + 5 request payload structs, the 4 admission/ handoff types, the 8 device/session/queue payloads) into a dedicated types.rs submodule. mod.rs re-exports via `pub use types::*` so external callers (lib.rs invoke handlers, commands_v2/queue.rs) keep their import paths. Pure data and enum-mapping impls only. Two helper methods on QconnectOutboundCommandType (to_queue_command_type, requires_remote_queue_admission) become pub(super) since they are only used from within the qconnect module tree. mod.rs: 6371 → 6109 lines. types.rs: 273 lines. cc --- src-tauri/src/qconnect/mod.rs | 272 +------------------------------ src-tauri/src/qconnect/types.rs | 273 ++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 267 deletions(-) create mode 100644 src-tauri/src/qconnect/types.rs diff --git a/src-tauri/src/qconnect/mod.rs b/src-tauri/src/qconnect/mod.rs index a2b5187c8..1084f5368 100644 --- a/src-tauri/src/qconnect/mod.rs +++ b/src-tauri/src/qconnect/mod.rs @@ -1,3 +1,6 @@ +mod types; +pub use types::*; + use std::{ collections::HashMap, sync::{Arc, OnceLock}, @@ -7,9 +10,9 @@ use std::{ use async_trait::async_trait; use qbz_models::{Quality, QueueTrack, RepeatMode, Track}; use qconnect_app::{ - evaluate_remote_queue_admission, resolve_handoff_intent, HandoffIntent, QConnectQueueState, + evaluate_remote_queue_admission, resolve_handoff_intent, QConnectQueueState, QConnectRendererState, QconnectApp, QconnectAppEvent, QconnectEventSink, QueueCommandType, - RendererCommand, RendererReport, RendererReportType, TrackOrigin, + RendererCommand, RendererReport, RendererReportType, }; use qconnect_core::QueueItem; use qconnect_transport_ws::{NativeWsTransport, WsTransportConfig}; @@ -54,271 +57,6 @@ const QCONNECT_PLAY_TRACK_HANDOFF_WAIT_MS: u64 = 1_500; const QCONNECT_PLAY_TRACK_HANDOFF_POLL_MS: u64 = 50; static QCONNECT_DEVICE_UUID: OnceLock = OnceLock::new(); -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectConnectOptions { - pub endpoint_url: Option, - pub jwt_qws: Option, - pub reconnect_backoff_ms: Option, - pub reconnect_backoff_max_ms: Option, - pub connect_timeout_ms: Option, - pub keepalive_interval_ms: Option, - pub qcloud_proto: Option, - pub subscribe_channels_hex: Option>, -} - -/// Lifecycle state surfaced to the UI so the toggle can reflect what the user -/// asked for (`running`) separately from what the transport currently has -/// (`Connecting`/`Connected`/`Reconnecting`). Without this distinction the UI -/// reads the toggle as "off" while the backend reconnect loop is alive, -/// leaving the user unable to disable QConnect (issue #358). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum QconnectLifecycleState { - /// User has not enabled QConnect (or it has been fully torn down). - #[default] - Off, - /// `connect()` has been called; transport is establishing the WS but no - /// `SessionEstablished` yet. - Connecting, - /// Transport saw at least one `SESSION_STATE` frame on the active WS — the - /// session-level handshake completed. - Connected, - /// Transport disconnected after at least one successful connect; the - /// reconnect loop is running. - Reconnecting, - /// `MaxReconnectAttemptsExceeded` fired — runtime auto-stopped, last error - /// surfaced. User can re-enable from UI. - Exhausted, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectConnectionStatus { - pub running: bool, - pub transport_connected: bool, - pub endpoint_url: Option, - pub last_error: Option, - /// Granular lifecycle state — the toggle should base its on/off reading on - /// this rather than on `transport_connected`, so a stuck reconnect loop is - /// still visible as "on" and the user can disable it (issue #358). - #[serde(default)] - pub state: QconnectLifecycleState, -} - -#[derive(Debug, Clone, Default)] -pub struct QconnectVisibleQueueProjection { - pub current_track: Option, - pub upcoming_tracks: Vec, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum QconnectOutboundCommandType { - JoinSession, - SetPlayerState, - SetActiveRenderer, - SetVolume, - SetLoopMode, - MuteVolume, - SetMaxAudioQuality, - AskForRendererState, - QueueAddTracks, - QueueLoadTracks, - QueueInsertTracks, - QueueRemoveTracks, - QueueReorderTracks, - ClearQueue, - SetShuffleMode, - SetAutoplayMode, - AutoplayLoadTracks, - AutoplayRemoveTracks, - SetQueueState, - AskForQueueState, -} - -impl QconnectOutboundCommandType { - const fn to_queue_command_type(self) -> QueueCommandType { - match self { - Self::JoinSession => QueueCommandType::CtrlSrvrJoinSession, - Self::SetPlayerState => QueueCommandType::CtrlSrvrSetPlayerState, - Self::SetActiveRenderer => QueueCommandType::CtrlSrvrSetActiveRenderer, - Self::SetVolume => QueueCommandType::CtrlSrvrSetVolume, - Self::SetLoopMode => QueueCommandType::CtrlSrvrSetLoopMode, - Self::MuteVolume => QueueCommandType::CtrlSrvrMuteVolume, - Self::SetMaxAudioQuality => QueueCommandType::CtrlSrvrSetMaxAudioQuality, - Self::AskForRendererState => QueueCommandType::CtrlSrvrAskForRendererState, - Self::QueueAddTracks => QueueCommandType::CtrlSrvrQueueAddTracks, - Self::QueueLoadTracks => QueueCommandType::CtrlSrvrQueueLoadTracks, - Self::QueueInsertTracks => QueueCommandType::CtrlSrvrQueueInsertTracks, - Self::QueueRemoveTracks => QueueCommandType::CtrlSrvrQueueRemoveTracks, - Self::QueueReorderTracks => QueueCommandType::CtrlSrvrQueueReorderTracks, - Self::ClearQueue => QueueCommandType::CtrlSrvrClearQueue, - Self::SetShuffleMode => QueueCommandType::CtrlSrvrSetShuffleMode, - Self::SetAutoplayMode => QueueCommandType::CtrlSrvrSetAutoplayMode, - Self::AutoplayLoadTracks => QueueCommandType::CtrlSrvrAutoplayLoadTracks, - Self::AutoplayRemoveTracks => QueueCommandType::CtrlSrvrAutoplayRemoveTracks, - Self::SetQueueState => QueueCommandType::CtrlSrvrSetQueueState, - Self::AskForQueueState => QueueCommandType::CtrlSrvrAskForQueueState, - } - } - - const fn requires_remote_queue_admission(self) -> bool { - matches!( - self, - Self::QueueAddTracks - | Self::QueueLoadTracks - | Self::QueueInsertTracks - | Self::SetQueueState - | Self::AutoplayLoadTracks - ) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QconnectSendCommandRequest { - pub command_type: QconnectOutboundCommandType, - #[serde(default)] - pub payload: Value, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum QconnectTrackOrigin { - QobuzOnline, - QobuzOfflineCache, - LocalLibrary, - Plex, - ExternalUnknown, -} - -impl QconnectTrackOrigin { - const fn into_core_origin(self) -> TrackOrigin { - match self { - Self::QobuzOnline => TrackOrigin::QobuzOnline, - Self::QobuzOfflineCache => TrackOrigin::QobuzOfflineCache, - Self::LocalLibrary => TrackOrigin::LocalLibrary, - Self::Plex => TrackOrigin::Plex, - Self::ExternalUnknown => TrackOrigin::ExternalUnknown, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum QconnectHandoffIntent { - ContinueLocally, - SendToConnect, -} - -impl QconnectHandoffIntent { - const fn from_core(intent: HandoffIntent) -> Self { - match intent { - HandoffIntent::ContinueLocally => Self::ContinueLocally, - HandoffIntent::SendToConnect => Self::SendToConnect, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QconnectAdmissionResult { - pub accepted: bool, - pub reason: String, - pub origin: QconnectTrackOrigin, - pub handoff_intent: QconnectHandoffIntent, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QconnectSendCommandWithAdmissionRequest { - pub command_type: QconnectOutboundCommandType, - pub origin: QconnectTrackOrigin, - #[serde(default)] - pub payload: Value, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QconnectAdmissionBlockedEvent { - pub command_type: QconnectOutboundCommandType, - pub origin: QconnectTrackOrigin, - pub reason: String, - pub handoff_intent: QconnectHandoffIntent, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectDeviceCapabilitiesPayload { - pub min_audio_quality: Option, - pub max_audio_quality: Option, - pub volume_remote_control: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectDeviceInfoPayload { - pub device_uuid: Option, - pub friendly_name: Option, - pub brand: Option, - pub model: Option, - pub serial_number: Option, - pub device_type: Option, - pub capabilities: Option, - pub software_version: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectJoinSessionRequest { - pub session_uuid: Option, - pub device_info: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QconnectQueueVersionPayload { - pub major: u64, - pub minor: u64, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectSetPlayerStateQueueItemPayload { - pub queue_version: Option, - pub id: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectSetPlayerStateRequest { - pub playing_state: Option, - pub current_position: Option, - pub current_queue_item: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectSetActiveRendererRequest { - pub renderer_id: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectSetVolumeRequest { - pub renderer_id: Option, - pub volume: Option, - pub volume_delta: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectSetLoopModeRequest { - pub loop_mode: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectMuteVolumeRequest { - pub renderer_id: Option, - pub value: bool, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectSetMaxAudioQualityRequest { - pub renderer_id: Option, - pub max_audio_quality: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct QconnectAskForRendererStateRequest { - pub renderer_id: Option, -} struct QconnectRuntime { app: Arc>, diff --git a/src-tauri/src/qconnect/types.rs b/src-tauri/src/qconnect/types.rs new file mode 100644 index 000000000..af4cacefd --- /dev/null +++ b/src-tauri/src/qconnect/types.rs @@ -0,0 +1,273 @@ +//! Wire types and request/response payloads exchanged with the frontend +//! and the QConnect cloud. Pure data — no behavior beyond enum mappings. + +use qconnect_app::{HandoffIntent, QueueCommandType, TrackOrigin}; +use qconnect_core::QueueItem; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectConnectOptions { + pub endpoint_url: Option, + pub jwt_qws: Option, + pub reconnect_backoff_ms: Option, + pub reconnect_backoff_max_ms: Option, + pub connect_timeout_ms: Option, + pub keepalive_interval_ms: Option, + pub qcloud_proto: Option, + pub subscribe_channels_hex: Option>, +} + +/// Lifecycle state surfaced to the UI so the toggle can reflect what the user +/// asked for (`running`) separately from what the transport currently has +/// (`Connecting`/`Connected`/`Reconnecting`). Without this distinction the UI +/// reads the toggle as "off" while the backend reconnect loop is alive, +/// leaving the user unable to disable QConnect (issue #358). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum QconnectLifecycleState { + /// User has not enabled QConnect (or it has been fully torn down). + #[default] + Off, + /// `connect()` has been called; transport is establishing the WS but no + /// `SessionEstablished` yet. + Connecting, + /// Transport saw at least one `SESSION_STATE` frame on the active WS — the + /// session-level handshake completed. + Connected, + /// Transport disconnected after at least one successful connect; the + /// reconnect loop is running. + Reconnecting, + /// `MaxReconnectAttemptsExceeded` fired — runtime auto-stopped, last error + /// surfaced. User can re-enable from UI. + Exhausted, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectConnectionStatus { + pub running: bool, + pub transport_connected: bool, + pub endpoint_url: Option, + pub last_error: Option, + /// Granular lifecycle state — the toggle should base its on/off reading on + /// this rather than on `transport_connected`, so a stuck reconnect loop is + /// still visible as "on" and the user can disable it (issue #358). + #[serde(default)] + pub state: QconnectLifecycleState, +} + +#[derive(Debug, Clone, Default)] +pub struct QconnectVisibleQueueProjection { + pub current_track: Option, + pub upcoming_tracks: Vec, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum QconnectOutboundCommandType { + JoinSession, + SetPlayerState, + SetActiveRenderer, + SetVolume, + SetLoopMode, + MuteVolume, + SetMaxAudioQuality, + AskForRendererState, + QueueAddTracks, + QueueLoadTracks, + QueueInsertTracks, + QueueRemoveTracks, + QueueReorderTracks, + ClearQueue, + SetShuffleMode, + SetAutoplayMode, + AutoplayLoadTracks, + AutoplayRemoveTracks, + SetQueueState, + AskForQueueState, +} + +impl QconnectOutboundCommandType { + pub(super) const fn to_queue_command_type(self) -> QueueCommandType { + match self { + Self::JoinSession => QueueCommandType::CtrlSrvrJoinSession, + Self::SetPlayerState => QueueCommandType::CtrlSrvrSetPlayerState, + Self::SetActiveRenderer => QueueCommandType::CtrlSrvrSetActiveRenderer, + Self::SetVolume => QueueCommandType::CtrlSrvrSetVolume, + Self::SetLoopMode => QueueCommandType::CtrlSrvrSetLoopMode, + Self::MuteVolume => QueueCommandType::CtrlSrvrMuteVolume, + Self::SetMaxAudioQuality => QueueCommandType::CtrlSrvrSetMaxAudioQuality, + Self::AskForRendererState => QueueCommandType::CtrlSrvrAskForRendererState, + Self::QueueAddTracks => QueueCommandType::CtrlSrvrQueueAddTracks, + Self::QueueLoadTracks => QueueCommandType::CtrlSrvrQueueLoadTracks, + Self::QueueInsertTracks => QueueCommandType::CtrlSrvrQueueInsertTracks, + Self::QueueRemoveTracks => QueueCommandType::CtrlSrvrQueueRemoveTracks, + Self::QueueReorderTracks => QueueCommandType::CtrlSrvrQueueReorderTracks, + Self::ClearQueue => QueueCommandType::CtrlSrvrClearQueue, + Self::SetShuffleMode => QueueCommandType::CtrlSrvrSetShuffleMode, + Self::SetAutoplayMode => QueueCommandType::CtrlSrvrSetAutoplayMode, + Self::AutoplayLoadTracks => QueueCommandType::CtrlSrvrAutoplayLoadTracks, + Self::AutoplayRemoveTracks => QueueCommandType::CtrlSrvrAutoplayRemoveTracks, + Self::SetQueueState => QueueCommandType::CtrlSrvrSetQueueState, + Self::AskForQueueState => QueueCommandType::CtrlSrvrAskForQueueState, + } + } + + pub(super) const fn requires_remote_queue_admission(self) -> bool { + matches!( + self, + Self::QueueAddTracks + | Self::QueueLoadTracks + | Self::QueueInsertTracks + | Self::SetQueueState + | Self::AutoplayLoadTracks + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QconnectSendCommandRequest { + pub command_type: QconnectOutboundCommandType, + #[serde(default)] + pub payload: Value, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum QconnectTrackOrigin { + QobuzOnline, + QobuzOfflineCache, + LocalLibrary, + Plex, + ExternalUnknown, +} + +impl QconnectTrackOrigin { + pub(super) const fn into_core_origin(self) -> TrackOrigin { + match self { + Self::QobuzOnline => TrackOrigin::QobuzOnline, + Self::QobuzOfflineCache => TrackOrigin::QobuzOfflineCache, + Self::LocalLibrary => TrackOrigin::LocalLibrary, + Self::Plex => TrackOrigin::Plex, + Self::ExternalUnknown => TrackOrigin::ExternalUnknown, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum QconnectHandoffIntent { + ContinueLocally, + SendToConnect, +} + +impl QconnectHandoffIntent { + pub(super) const fn from_core(intent: HandoffIntent) -> Self { + match intent { + HandoffIntent::ContinueLocally => Self::ContinueLocally, + HandoffIntent::SendToConnect => Self::SendToConnect, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QconnectAdmissionResult { + pub accepted: bool, + pub reason: String, + pub origin: QconnectTrackOrigin, + pub handoff_intent: QconnectHandoffIntent, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QconnectSendCommandWithAdmissionRequest { + pub command_type: QconnectOutboundCommandType, + pub origin: QconnectTrackOrigin, + #[serde(default)] + pub payload: Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QconnectAdmissionBlockedEvent { + pub command_type: QconnectOutboundCommandType, + pub origin: QconnectTrackOrigin, + pub reason: String, + pub handoff_intent: QconnectHandoffIntent, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectDeviceCapabilitiesPayload { + pub min_audio_quality: Option, + pub max_audio_quality: Option, + pub volume_remote_control: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectDeviceInfoPayload { + pub device_uuid: Option, + pub friendly_name: Option, + pub brand: Option, + pub model: Option, + pub serial_number: Option, + pub device_type: Option, + pub capabilities: Option, + pub software_version: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectJoinSessionRequest { + pub session_uuid: Option, + pub device_info: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QconnectQueueVersionPayload { + pub major: u64, + pub minor: u64, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectSetPlayerStateQueueItemPayload { + pub queue_version: Option, + pub id: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectSetPlayerStateRequest { + pub playing_state: Option, + pub current_position: Option, + pub current_queue_item: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectSetActiveRendererRequest { + pub renderer_id: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectSetVolumeRequest { + pub renderer_id: Option, + pub volume: Option, + pub volume_delta: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectSetLoopModeRequest { + pub loop_mode: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectMuteVolumeRequest { + pub renderer_id: Option, + pub value: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectSetMaxAudioQualityRequest { + pub renderer_id: Option, + pub max_audio_quality: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QconnectAskForRendererStateRequest { + pub renderer_id: Option, +}