Readme
Qonductor
UNDER ACTIVE DEVELOPMENT, EVERYTHING WILL BREAK
Rust implementation of the Qobuz Connect protocol.
Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ SessionManager │
│ - Main entry point │
│ - Owns DeviceRegistry and SessionHandles │
│ - Routes events to user via mpsc channel │
└─────────────────────────────────────────────────────────────────────────────┘
│ owns │ owns
▼ ▼
┌─────────────────────────────┐ ┌──────────────────────────────┐
│ DeviceRegistry │ │ SessionHandle ( per session) │
│ - 1 HTTP server ( axum) │ │ - Lightweight handle │
│ - N mDNS announcements │ │ - Sends commands via channel│
│ - Emits DeviceSelected │ │ - Spawns SessionRunner task │
└─────────────────────────────┘ └──────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────┐ ┌──────────────────────────────┐
│ Per- Device mDNS Service │ │ SessionRunner ( spawned) │
│ _qobuz- connect. _tcp. local │ │ - Owns WebSocket connection │
│ TXT : path, device_uuid │ │ - tokio:: select! loop │
└─────────────────────────────┘ │ - Handles WS + commands │
└──────────────────────────────┘
Device Discovery Flow
1. Device Registration
When you call manager. add_device ( config) :
┌──────────┐ ┌────────────────┐ ┌─────────────────┐
│ User │ │ DeviceRegistry │ │ mDNS ( Avahi) │
└────┬─────┘ └───────┬────────┘ └────────┬────────┘
│ │ │
│ add_device ( config) │ │
│──────────────────────> │ │
│ │ │
│ │ Register service │
│ │ " _qobuz-connect._tcp" │
│ │──────────────────────────> │
│ │ │
│ │ TXT records: │
│ │ - path= / devices/ { uuid} │
│ │ - device_uuid= { uuid} │
│ │ - type = SPEAKER │
│ │──────────────────────────> │
│ │ │
│ Ok ( ( ) ) │ │
│< ──────────────────────│ │
The device is now discoverable on the local network via mDNS/Zeroconf.
2. Device Selection (Qobuz App → Your Device)
When a user selects your device in the Qobuz app:
┌────────────┐ ┌────────────────┐ ┌────────────────┐ ┌─────────────┐
│ Qobuz App │ │ HTTP Server │ │ DeviceRegistry │ │ Manager │
└─────┬──────┘ └───────┬────────┘ └───────┬────────┘ └──────┬──────┘
│ │ │ │
│ GET / devices/ { uuid} / get- display- info │ │
│────────────────────> │ │ │
│ { name, type } │ │ │
│< ────────────────────│ │ │
│ │ │ │
│ GET / devices/ { uuid} / get- connect- info │ │
│────────────────────> │ │ │
│ { app_id } │ │ │
│< ────────────────────│ │ │
│ │ │ │
│ POST / devices/ { uuid} / connect- to- qconnect │ │
│ { session_id, jwt_qconnect, jwt_api } │ │
│────────────────────> │ │ │
│ │ DeviceSelected │ │
│ │──────────────────────> │ │
│ │ │ DeviceSelected │
│ │ │─────────────────────> │
│ { success } │ │ │
│< ────────────────────│ │ │
3. WebSocket Session Creation
When SessionManager receives DeviceSelected:
┌─────────────┐ ┌───────────────┐ ┌────────────────┐ ┌─────────────┐
│ Manager │ │ SessionHandle │ │ SessionRunner │ │ Qobuz WS │
└──────┬──────┘ └───────┬───────┘ └───────┬────────┘ └──────┬──────┘
│ │ │ │
│ SessionHandle:: connect( session_info, device_config) │
│────────────────────> │ │ │
│ │ │ │
│ │ Connect WebSocket │ │
│ │────────────────────────────────────────────> │
│ │ │ │
│ │ Subscribe + Join │ │
│ │────────────────────────────────────────────> │
│ │ │ │
│ │ Spawn runner task │ │
│ │─────────────────────> │ │
│ │ │ │
│ SessionHandle │ │ tokio:: select! { │
│< ────────────────────│ │ ws. recv ( ) │
│ │ │ cmd_rx. recv ( ) │
│ │ │ } │
│ │ │< ────────────────────> │
4. Event Flow
Events from Qobuz server flow to user code:
┌─────────────┐ ┌───────────────┐ ┌─────────────┐ ┌──────────┐
│ Qobuz WS │ │ SessionRunner │ │ Manager │ │ User │
└──────┬──────┘ └───────┬───────┘ └──────┬──────┘ └────┬─────┘
│ │ │ │
│ PlaybackCommand │ │ │
│────────────────────> │ │ │
│ │ │ │
│ │ event_tx. send ( ) │ │
│ │────────────────────> │ │
│ │ │ │
│ │ │ events. recv ( ) │
│ │ │──────────────────> │
│ │ │ │
│ │ │ SessionEvent:: │
│ │ │ PlaybackCommand │
│ │ │──────────────────> │
Usage
use qonductor:: {
SessionManager, DeviceConfig, SessionEvent, Command, Notification,
ActivationState, msg, PlayingState, BufferState,
msg:: { PositionExt, QueueRendererStateExt} ,
} ;
# [ tokio ::main ]
async fn main ( ) -> qonductor:: Result < ( ) > {
// Start the session manager (HTTP server + mDNS)
let mut manager = SessionManager:: start( 7864 ) . await? ;
// Register device and get session handle for bidirectional communication
let mut session = manager. add_device (
DeviceConfig:: new( " Living Room Speaker" , " your_app_id" )
) . await? ;
// Spawn manager to handle device selections
tokio:: spawn( async move { manager. run ( ) . await } ) ;
// Handle events for this device
while let Some ( event) = session. recv ( ) . await {
match event {
// Commands require a response via the Responder
SessionEvent:: Command( cmd) => match cmd {
Command:: SetState { cmd, respond } => {
println! ( " Play {:?} at {:?} ms" , cmd. playing_state, cmd. current_position) ;
let mut response = msg:: QueueRendererState:: default( ) ;
response. set_state ( PlayingState:: Playing) . set_buffer ( BufferState:: Ok) ;
respond. send ( response) ;
}
Command:: SetActive { respond, .. } => {
println! ( " Device activated!" ) ;
respond. send ( ActivationState {
muted: false ,
volume: 100 ,
max_quality: 4 ,
playback: msg:: QueueRendererState:: default( ) ,
} ) ;
}
Command:: Heartbeat { respond } => {
respond. send ( None ) ; // or Some(state) if playing
}
} ,
// Notifications are informational (use _ => for forward compatibility)
SessionEvent:: Notification( n) => match n {
Notification:: Connected => println! ( " Connected!" ) ,
Notification:: DeviceRegistered { renderer_id, .. } => {
println! ( " Registered as renderer {} " , renderer_id) ;
}
Notification:: QueueState( queue) => {
println! ( " Queue has {} tracks" , queue. tracks. len ( ) ) ;
}
_ => { }
} ,
}
}
Ok ( ( ) )
}
Key Types
Type
Description
SessionManager
Main entry point. Manages devices and sessions.
DeviceConfig
Configuration for a discoverable device.
DeviceSession
Bidirectional session handle returned by add_device ( ) .
SessionEvent
Wrapper: Command( Command) or Notification( Notification)
Command
Events requiring response: SetState , SetActive , Heartbeat
Notification
Informational events: Connected , QueueState , etc.
Responder< T>
Used to send required responses back to the server.
PlayingState
Playback state: Playing , Paused , Stopped
How It Works
mDNS Advertisement : Each device is advertised via _qobuz- connect. _tcp with a unique path in the TXT record.
HTTP Endpoints : A single HTTP server handles all devices via parameterized routes (/devices/{uuid}/* ). Qobuz apps hit these endpoints when the device is selected.
1:1 Device-Session Mapping : When a device is selected in the Qobuz app, a dedicated session is created for that device between Qonductor and the Qobuz servers.
Actor Pattern : Each WebSocket session runs in its own spawned task, communicating with the manager via channels.
Building
cargo build
cargo run -- example discovery_server
License
MIT