feat(qbzd): MPRIS playback updates + status subcommand

Playback loop now updates MPRIS state (playing/paused/position)
on every emit cycle so media controls always reflect current state.

qbzd status: queries running daemon via HTTP and prints status.
Shows logged_in state, port, and daemon info.

cc
This commit is contained in:
vicrodh committed 2026-04-09 05:26:37 -06:00
1 parent f9d6e551e2
commit e0301b6a29
3 files changed
+44 -5

No files matched your search

+3
View File
@@ -34,6 +34,9 @@ futures-util = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
# HTTP client (for status check + OAuth)
reqwest = { workspace = true }
# Config
toml = "0.8"
clap = { version = "4", features = ["derive"] }
+22 -4
View File
@@ -97,17 +97,17 @@ 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());
// Start MPRIS media controls (Linux D-Bus, headless)
let _mpris = if config.mpris.enabled {
let mpris_handle = if config.mpris.enabled {
crate::mpris::start_mpris(daemon.core.clone())
} else {
log::info!("[qbzd] MPRIS disabled via config");
None
};
// Start playback state polling loop (broadcasts to event bus + MPRIS)
spawn_playback_loop(daemon.core.clone(), daemon.event_bus.clone(), mpris_handle);
// Register mDNS service for LAN discovery
let _mdns_handle = if config.mdns.enabled {
match register_mdns(&config) {
@@ -250,10 +250,12 @@ fn register_mdns(config: &DaemonConfig) -> Result<mdns_sd::ServiceDaemon, String
/// Spawn the playback state polling loop.
/// Reads player state and broadcasts PlaybackSnapshot events.
/// Also updates MPRIS metadata when track changes.
/// Adaptive polling: 250ms playing, 1s paused, 5s idle.
fn spawn_playback_loop(
core: Arc<QbzCore<DaemonAdapter>>,
event_tx: broadcast::Sender<DaemonEvent>,
mpris: Option<Arc<std::sync::Mutex<souvlaki::MediaControls>>>,
) {
tokio::spawn(async move {
let mut last_position: u64 = 0;
@@ -296,6 +298,22 @@ fn spawn_playback_loop(
bit_depth,
};
let _ = event_tx.send(DaemonEvent::Playback(snapshot));
// Update MPRIS
if let Some(ref mc) = mpris {
if track_id != last_track_id && track_id != 0 {
// Track changed — update metadata
// We don't have track title here (just ID), so use a
// placeholder. Full metadata comes from CoreEvent::TrackStarted.
crate::mpris::update_mpris_playback(mc, is_playing, position);
} else {
crate::mpris::update_mpris_playback(mc, is_playing, position);
}
if track_id == 0 && last_track_id != 0 {
crate::mpris::update_mpris_playback(mc, false, 0);
}
}
last_position = position;
last_is_playing = is_playing;
last_track_id = track_id;
+19 -1
View File
@@ -104,7 +104,25 @@ async fn main() {
}
}
Some(Commands::Status) => {
println!("qbzd status: not running (status check requires daemon to be active)");
let port = cfg.server.port;
match reqwest::Client::new()
.get(format!("http://127.0.0.1:{}/api/status", port))
.timeout(std::time::Duration::from_secs(3))
.send()
.await
{
Ok(resp) if resp.status().is_success() => {
let body: serde_json::Value = resp.json().await.unwrap_or_default();
println!("qbzd running on port {}", port);
println!("{}", serde_json::to_string_pretty(&body).unwrap_or_default());
}
Ok(resp) => {
println!("qbzd responded with HTTP {}", resp.status());
}
Err(_) => {
println!("qbzd not running (no response on port {})", port);
}
}
}
Some(Commands::Token) => {
if cfg.server.token == "auto" {