feat(qbzd): mDNS discovery + registration for LAN pairing

Daemon side: registers _qbz._tcp.local mDNS service on startup
so desktop apps can discover it on the LAN. Uses hostname as
service name (configurable via qbzd.toml).

Desktop side: new qbzd_discovery.rs module with Tauri commands:
- v2_qbzd_start_discovery: browse _qbz._tcp services
- v2_qbzd_stop_discovery: stop browsing
- v2_qbzd_get_devices: list discovered daemons

Uses same mdns-sd crate already used for AirPlay discovery.

cc
This commit is contained in:
vicrodh committed 2026-04-09 04:30:08 -06:00
1 parent 8dac185261
commit 8fc6111782
5 files changed
+186

No files matched your search

+4
View File
@@ -44,6 +44,10 @@ log = { workspace = true }
env_logger = "0.11"
sysinfo = "0.34"
# mDNS discovery
mdns-sd = "0.11"
hostname = "0.4"
# Database
rusqlite = { version = "0.31", features = ["bundled"] }
+46
View File
@@ -102,6 +102,22 @@ pub async fn run(mut config: DaemonConfig) -> Result<(), String> {
// Start playback state polling loop (broadcasts to event bus)
spawn_playback_loop(daemon.core.clone(), daemon.event_bus.clone());
// Register mDNS service for LAN discovery
let _mdns_handle = if config.mdns.enabled {
match register_mdns(&config) {
Ok(handle) => {
log::info!("[qbzd] mDNS registered: _qbz._tcp on port {}", config.server.port);
Some(handle)
}
Err(e) => {
log::warn!("[qbzd] mDNS registration failed: {}", e);
None
}
}
} else {
None
};
log::info!(
"[qbzd] Daemon ready on {}:{}",
config.server.bind,
@@ -195,6 +211,36 @@ fn load_oauth_token() -> Option<String> {
}
}
/// Register the daemon as a `_qbz._tcp` mDNS service for LAN discovery.
fn register_mdns(config: &DaemonConfig) -> Result<mdns_sd::ServiceDaemon, String> {
let mdns = mdns_sd::ServiceDaemon::new()
.map_err(|e| format!("mDNS daemon error: {}", e))?;
let service_name = if config.mdns.name.is_empty() {
hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_else(|| "qbzd".to_string())
} else {
config.mdns.name.clone()
};
let service_info = mdns_sd::ServiceInfo::new(
"_qbz._tcp.local.",
&service_name,
&format!("{}.", hostname::get().ok().and_then(|h| h.into_string().ok()).unwrap_or_else(|| "localhost".to_string())),
"",
config.server.port,
None,
)
.map_err(|e| format!("mDNS service info error: {}", e))?;
mdns.register(service_info)
.map_err(|e| format!("mDNS register error: {}", e))?;
Ok(mdns)
}
/// Spawn the playback state polling loop.
/// Reads player state and broadcasts PlaybackSnapshot events.
/// Adaptive polling: 250ms playing, 1s paused, 5s idle.