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:
3 files changed
+44
-5
No files matched your search
@@ -34,6 +34,9 @@ futures-util = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
|
# HTTP client (for status check + OAuth)
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|||||||
@@ -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)
|
// 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())
|
crate::mpris::start_mpris(daemon.core.clone())
|
||||||
} else {
|
} else {
|
||||||
log::info!("[qbzd] MPRIS disabled via config");
|
log::info!("[qbzd] MPRIS disabled via config");
|
||||||
None
|
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
|
// Register mDNS service for LAN discovery
|
||||||
let _mdns_handle = if config.mdns.enabled {
|
let _mdns_handle = if config.mdns.enabled {
|
||||||
match register_mdns(&config) {
|
match register_mdns(&config) {
|
||||||
@@ -250,10 +250,12 @@ fn register_mdns(config: &DaemonConfig) -> Result<mdns_sd::ServiceDaemon, String
|
|||||||
|
|
||||||
/// Spawn the playback state polling loop.
|
/// Spawn the playback state polling loop.
|
||||||
/// Reads player state and broadcasts PlaybackSnapshot events.
|
/// Reads player state and broadcasts PlaybackSnapshot events.
|
||||||
|
/// Also updates MPRIS metadata when track changes.
|
||||||
/// Adaptive polling: 250ms playing, 1s paused, 5s idle.
|
/// Adaptive polling: 250ms playing, 1s paused, 5s idle.
|
||||||
fn spawn_playback_loop(
|
fn spawn_playback_loop(
|
||||||
core: Arc<QbzCore<DaemonAdapter>>,
|
core: Arc<QbzCore<DaemonAdapter>>,
|
||||||
event_tx: broadcast::Sender<DaemonEvent>,
|
event_tx: broadcast::Sender<DaemonEvent>,
|
||||||
|
mpris: Option<Arc<std::sync::Mutex<souvlaki::MediaControls>>>,
|
||||||
) {
|
) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut last_position: u64 = 0;
|
let mut last_position: u64 = 0;
|
||||||
@@ -296,6 +298,22 @@ fn spawn_playback_loop(
|
|||||||
bit_depth,
|
bit_depth,
|
||||||
};
|
};
|
||||||
let _ = event_tx.send(DaemonEvent::Playback(snapshot));
|
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_position = position;
|
||||||
last_is_playing = is_playing;
|
last_is_playing = is_playing;
|
||||||
last_track_id = track_id;
|
last_track_id = track_id;
|
||||||
|
|||||||
+19
-1
@@ -104,7 +104,25 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Commands::Status) => {
|
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) => {
|
Some(Commands::Token) => {
|
||||||
if cfg.server.token == "auto" {
|
if cfg.server.token == "auto" {
|
||||||
|
|||||||
Reference in new issue
Block a user