diff --git a/pmocontrol/src/control_point.rs b/pmocontrol/src/control_point.rs index 11051e5e..d07eeaaa 100644 --- a/pmocontrol/src/control_point.rs +++ b/pmocontrol/src/control_point.rs @@ -1696,18 +1696,34 @@ impl ControlPoint { auto_play: bool, ) -> anyhow::Result<()> { // CRITICAL: When attaching a new playlist to a renderer, we must UNCONDITIONALLY - // clear the queue first. This is different from refreshing an already-attached - // playlist (which uses gentle sync to avoid interrupting playback). + // clear the RENDERER queue first (but NOT the local queue cache, which will be + // replaced by refresh_attached_queue_for() using replace_entire_playlist()). // - // Attach workflow: Stop (if playing) → Clear → Fill → Play + // Attach workflow: Clear renderer → Fill with new playlist // Update workflow: Gentle sync (preserve current item, use LCS) info!( renderer = renderer_id.0.as_str(), server = server_id.0.as_str(), container = container_id, - "Attaching new playlist: clearing queue first" + "Attaching new playlist: clearing renderer queue" ); - self.clear_queue(renderer_id)?; + + // Clear the renderer's queue for OpenHome renderers + // We also sync the local cache to reflect the empty state, which will trigger + // refresh_attached_queue_for() to use replace_entire_playlist() instead of gentle sync + if self.runtime.uses_openhome_playlist(renderer_id) { + let renderer = self.openhome_renderer(renderer_id)?; + renderer.openhome_playlist_clear()?; + // Sync local cache to reflect the empty renderer state + self.sync_openhome_playlist_for(renderer_id)?; + debug!( + renderer = renderer_id.0.as_str(), + "Cleared OpenHome renderer playlist and synced local cache" + ); + } else { + // For non-OpenHome renderers, use the standard clear_queue + self.clear_queue(renderer_id)?; + } let binding = PlaylistBinding { server_id: server_id.clone(), @@ -2622,6 +2638,9 @@ fn refresh_attached_queue_for( let current_item = current_idx.and_then(|idx| full_queue.get(idx).cloned()); // Check if renderer is playing - if so, we MUST preserve the current track + // We use multiple signals to determine playback state: + // 1. Direct playback_state() query (may fail on some renderers like upmpdcli) + // 2. Presence of current_idx (if we have a current track, likely playing) let is_playing = { let renderer_info = { let reg = registry.read().unwrap(); @@ -2630,7 +2649,20 @@ fn refresh_attached_queue_for( if let Some(info) = renderer_info { if let Some(renderer) = MusicRenderer::from_registry_info(info, registry) { - matches!(renderer.playback_state(), Ok(PlaybackState::Playing)) + // Try direct query first + if matches!(renderer.playback_state(), Ok(PlaybackState::Playing)) { + true + } else if current_idx.is_some() && !full_queue.is_empty() { + // Fallback: if we have a current index and non-empty queue, + // assume playback is happening (handles renderers where playback_state() fails) + debug!( + renderer = renderer_id.0.as_str(), + "playback_state() failed or not Playing, but current_idx is set - assuming playback" + ); + true + } else { + false + } } else { false } diff --git a/pmocontrol/src/openhome_client.rs b/pmocontrol/src/openhome_client.rs index bbcbb832..d820ecf5 100644 --- a/pmocontrol/src/openhome_client.rs +++ b/pmocontrol/src/openhome_client.rs @@ -121,12 +121,12 @@ impl OhPlaylistClient { pub fn play_id(&self, id: u32) -> Result<()> { let id_str = id.to_string(); - let args = [("Id", id_str.as_str())]; + let args = [("Value", id_str.as_str())]; let call_result = - invoke_upnp_action(&self.control_url, &self.service_type, "PlayId", &args)?; + invoke_upnp_action(&self.control_url, &self.service_type, "SeekId", &args)?; - handle_action_response("PlayId", &call_result) + handle_action_response("SeekId", &call_result) } pub fn play(&self) -> Result<()> { @@ -170,7 +170,7 @@ impl OhPlaylistClient { pub fn delete_id(&self, id: u32) -> Result<()> { let id_str = id.to_string(); - let args = [("Id", id_str.as_str())]; + let args = [("Value", id_str.as_str())]; let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "DeleteId", &args)?; @@ -221,6 +221,16 @@ impl OhPlaylistClient { handle_action_response("DeleteAll", &call_result) } + pub fn current_id(&self) -> Result { + let call_result = + invoke_upnp_action(&self.control_url, &self.service_type, "Id", &[])?; + let envelope = ensure_success("Id", &call_result)?; + let response = find_child_with_suffix(&envelope.body.content, "IdResponse") + .ok_or_else(|| anyhow!("Missing IdResponse element in SOAP body"))?; + let value: String = extract_child_text_any(response, &["aValue", "Value"])?; + Ok(value) + } + pub fn tracks_max(&self) -> Result { let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "TracksMax", &[])?; @@ -228,7 +238,7 @@ impl OhPlaylistClient { let envelope = ensure_success("TracksMax", &call_result)?; let response = find_child_with_suffix(&envelope.body.content, "TracksMaxResponse") .ok_or_else(|| anyhow!("Missing TracksMaxResponse element in SOAP body"))?; - let value_text = extract_child_text_any(response, &["aValue", "Value"])?; + let value_text: String = extract_child_text_any(response, &["aValue", "Value"])?; let value = value_text .parse::() .map_err(|_| anyhow!("Invalid TracksMax value: {}", value_text))?;