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