Skip to content

nickblt/qonductor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

38 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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, SetStateExt, report::VolumeChanged},
};

#[tokio::main]
async fn main() -> qonductor::Result<()> {
    // Start the session manager (HTTP server + mDNS)
    let mut manager = SessionManager::start(7864, "your_app_id").await?;

    // Register device and get session handle for bidirectional communication
    let mut session = manager.add_device(
        DeviceConfig::new("Living Room Speaker")
    ).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.state(), cmd.current_position);
                    let mut response = msg::QueueRendererState {
                        current_position: Some(msg::Position::now(cmd.current_position.unwrap_or(0))),
                        ..Default::default()
                    };
                    response
                        .set_state(cmd.state().unwrap_or(PlayingState::Stopped))
                        .set_buffer(BufferState::Ok);
                    respond.send(response);
                }
                Command::SetVolume { cmd, respond } => {
                    println!("Volume: {:?}", cmd.volume);
                    respond.send(VolumeChanged { volume: cmd.volume });
                }
                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, SetVolume, 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

  1. mDNS Advertisement: Each device is advertised via _qobuz-connect._tcp with a unique path in the TXT record.

  2. HTTP Endpoints: A single HTTP server handles all devices via parameterized routes (/devices/{uuid}/*). Qobuz apps hit these endpoints when the device is selected.

  3. 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.

  4. 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

About

Rust implementation of qobuz connect

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages