fix(qbzd): prevent orchestrator race on play-track + QConnect debug logging

play-track/play-album set skip_auto_advance flag before stop() to
prevent the orchestrator from auto-advancing during the download
window. This fixes the 'track starts then immediately stops' bug.

Added QConnect event type logging to debug why SESSION_STATE never
triggers the deferred renderer join.

cc
This commit is contained in:
vicrodh committed 2026-04-09 14:30:32 -06:00
1 parent ca4480239b
commit c2e8285dfa
3 files changed
+18 -1

No files matched your search

+10
View File
@@ -48,12 +48,18 @@ pub async fn play_track(
log::info!("[qbzd/play] Playing track {} (quality: {:?})", req.track_id, quality);
// Signal orchestrator to not auto-advance during download
daemon.skip_auto_advance.store(true, std::sync::atomic::Ordering::Release);
// Stop current playback
let _ = daemon.core.stop();
// Download audio
let audio_data = crate::daemon::download_track(&daemon, req.track_id).await?;
// Clear the skip flag
daemon.skip_auto_advance.store(false, std::sync::atomic::Ordering::Release);
// Feed to player
daemon.core.player()
.play_data(audio_data, req.track_id)
@@ -122,10 +128,14 @@ pub async fn play_album(
_ => qbz_models::Quality::HiRes,
};
// Signal orchestrator to not auto-advance during download
daemon.skip_auto_advance.store(true, std::sync::atomic::Ordering::Release);
// Stop current playback
let _ = daemon.core.stop();
let audio_data = crate::daemon::download_track(&daemon, first_track_id).await?;
daemon.skip_auto_advance.store(false, std::sync::atomic::Ordering::Release);
daemon.core.player().play_data(audio_data, first_track_id)
.map_err(|e| format!("Player error: {}", e))?;
+4 -1
View File
@@ -22,6 +22,8 @@ pub struct DaemonCore {
pub event_bus: broadcast::Sender<DaemonEvent>,
/// Per-user state, populated after login + session activation
pub user: RwLock<Option<UserSession>>,
/// Flag to prevent orchestrator auto-advance during explicit play commands
pub skip_auto_advance: std::sync::atomic::AtomicBool,
}
/// Run the daemon main loop.
@@ -75,6 +77,7 @@ pub async fn run(mut config: DaemonConfig) -> Result<(), String> {
audio_cache,
event_bus: event_tx.clone(),
user: RwLock::new(None),
skip_auto_advance: std::sync::atomic::AtomicBool::new(false),
});
// Try auto-login from saved OAuth token
@@ -308,7 +311,7 @@ fn spawn_playback_orchestrator(daemon: Arc<DaemonCore>) {
// This handles the case when gapless wasn't available or failed.
let track_ended = was_playing && !is_playing && track_id == 0 && last_track_id != 0;
if track_ended {
if track_ended && !daemon.skip_auto_advance.load(std::sync::atomic::Ordering::Acquire) {
let queue_state = daemon.core.get_queue_state().await;
let repeat_mode = queue_state.repeat;
+4
View File
@@ -204,9 +204,13 @@ async fn run_event_loop(app: &Arc<App>, device_name: &str) {
loop {
match rx.recv().await {
Ok(event) => {
// Log event type for debugging
log::debug!("[qbzd/qconnect] Transport event: {:?}", std::mem::discriminant(&event));
// Wait for SESSION_STATE to do deferred renderer join
if !renderer_joined {
if let qconnect_transport_ws::TransportEvent::InboundQueueServerEvent(ref evt) = event {
log::info!("[qbzd/qconnect] Queue server event: {}", evt.message_type());
if evt.message_type() == "MESSAGE_TYPE_SRVR_CTRL_SESSION_STATE" {
if let Some(session_uuid) = evt.payload.get("session_uuid").and_then(|v| v.as_str()) {
renderer_joined = true;