diff --git a/pmocontrol/src/control_point.rs b/pmocontrol/src/control_point.rs index eff6a832..6f6ddb43 100644 --- a/pmocontrol/src/control_point.rs +++ b/pmocontrol/src/control_point.rs @@ -640,7 +640,7 @@ impl ControlPoint { })?; // Get queue length before clearing - let removed = renderer.get_queue().upcoming_len().unwrap_or(0); + let removed = renderer.upcoming_len().unwrap_or(0); renderer.clear_queue()?; @@ -697,11 +697,8 @@ impl ControlPoint { ControlPointError::SnapshotError(format!("Renderer {} not found", renderer_id.0)) })?; - let new_len = { - let mut queue = renderer.get_queue_mut(); - queue.enqueue_items(items, mode)?; - queue.upcoming_len()? - }; + renderer.enqueue_items(items, mode)?; + let new_len = renderer.upcoming_len()?; debug!( renderer = renderer_id.0.as_str(), @@ -789,7 +786,6 @@ impl ControlPoint { // Get queue from renderer (works for all backends) let queue_snapshot = renderer - .get_queue() .queue_snapshot() .map_err(|e| anyhow!("Failed to get queue snapshot: {}", e))?; @@ -955,7 +951,7 @@ impl ControlPoint { })?; // Use generic queue access (works for all backends) - let Some((item, remaining)) = renderer.get_queue_mut().peek_current()? else { + let Some((item, remaining)) = renderer.peek_current()? else { debug!( renderer = renderer_id.0.as_str(), "play_current_from_queue: queue is empty or no current item" @@ -975,9 +971,8 @@ impl ControlPoint { // when renderer sends Stopped event during SetAVTransportURI renderer.set_playback_source(PlaybackSource::None); - // Start playback - let didl_metadata = playback_item_to_didl(&item); - if let Err(err) = renderer.play_uri(&item.uri, &didl_metadata) { + // Start playback using play_from_queue which preserves the queue + if let Err(err) = renderer.play_from_queue() { error!( renderer = renderer_id.0.as_str(), error = %err, @@ -1008,61 +1003,50 @@ impl ControlPoint { ControlPointError::SnapshotError(format!("Renderer {} not found", renderer_id.0)) })?; - // Dequeue next item - let Some((item, remaining_after)) = renderer.get_queue_mut().dequeue_next()? else { + // Check if queue is empty before trying to play next + if renderer.len()? == 0 { debug!( renderer = renderer_id.0.as_str(), "play_next_from_queue: queue is empty" ); renderer.set_playback_source(PlaybackSource::None); return Ok(()); - }; - - debug!( - renderer = renderer_id.0.as_str(), - queue_after = remaining_after, - uri = item.uri.as_str(), - "Dequeued next playback item" - ); + } // Temporarily disable auto-advance to prevent race condition - // when renderer sends Stopped event during SetAVTransportURI renderer.set_playback_source(PlaybackSource::None); - // Start playback - let didl_metadata = playback_item_to_didl(&item); - if let Err(err) = renderer.play_uri(&item.uri, &didl_metadata) { + // Use the backend's play_next which handles queue advancement correctly for each backend type + if let Err(err) = renderer.play_next_from_queue() { error!( renderer = renderer_id.0.as_str(), error = %err, - "Failed to start playback for queued item" + "Failed to play next item from queue" ); - // Try to requeue the item - let _ = renderer - .get_queue_mut() - .enqueue_items(vec![item.clone()], EnqueueMode::InsertAfterCurrent); renderer.set_playback_source(PlaybackSource::None); return Err(err); } - // Save metadata for current track - let metadata = playback_item_track_metadata(&item); - renderer.set_last_metadata(Some(metadata)); + // Get current item metadata for tracking + if let Some((item, _)) = renderer.peek_current()? { + let metadata = playback_item_track_metadata(&item); + renderer.set_last_metadata(Some(metadata)); + } renderer.set_playback_source(PlaybackSource::FromQueue); debug!( renderer = renderer_id.0.as_str(), - queue_len = remaining_after, - "Started playback from queue" + "Playing next item from queue" ); // Prefetch next track if supported self.prefetch_next_track(&renderer, renderer_id); // Emit QueueUpdated event + let queue_length = renderer.len().unwrap_or(0); self.emit_renderer_event(RendererEvent::QueueUpdated { id: renderer_id.clone(), - queue_length: remaining_after, + queue_length, }); Ok(()) @@ -1076,7 +1060,7 @@ impl ControlPoint { } // Get the next item from the queue using peek_current - let Ok(Some((_, remaining))) = renderer.get_queue_mut().peek_current() else { + let Ok(Some((_, remaining))) = renderer.peek_current() else { return; }; @@ -1084,7 +1068,7 @@ impl ControlPoint { return; } - let queue_snapshot = match renderer.get_queue().queue_snapshot() { + let queue_snapshot = match renderer.queue_snapshot() { Ok(snapshot) => snapshot, Err(_) => return, }; @@ -1119,37 +1103,26 @@ impl ControlPoint { ControlPointError::SnapshotError(format!("Renderer {} not found", renderer_id.0)) })?; - // Set queue index - renderer.get_queue_mut().set_index(Some(index))?; - - let Some((item, remaining)) = renderer.get_queue_mut().peek_current()? else { + // Check if index is valid + if index >= renderer.len()? { debug!( renderer = renderer_id.0.as_str(), - index, "play_queue_index: no item at index" + index, "play_queue_index: index out of bounds" ); renderer.set_playback_source(PlaybackSource::None); return Ok(()); - }; - - debug!( - renderer = renderer_id.0.as_str(), - index, - queue_len = remaining + 1, - uri = item.uri.as_str(), - "Playing item at index from queue" - ); + } // Temporarily disable auto-advance to prevent race condition renderer.set_playback_source(PlaybackSource::None); - // Start playback - let didl_metadata = playback_item_to_didl(&item); - if let Err(err) = renderer.play_uri(&item.uri, &didl_metadata) { + // Use the backend's play_from_index which handles everything correctly + if let Err(err) = renderer.play_from_index(index) { error!( renderer = renderer_id.0.as_str(), index, error = %err, - "Failed to start playback for queued item at index" + "Failed to play from queue index" ); renderer.set_playback_source(PlaybackSource::None); return Err(err); @@ -1157,14 +1130,14 @@ impl ControlPoint { info!( renderer = renderer_id.0.as_str(), - index, - uri = item.uri.as_str(), - "Queue playback started at index" + index, "Queue playback started at index" ); - // Save metadata - let metadata = playback_item_track_metadata(&item); - renderer.set_last_metadata(Some(metadata)); + // Get current item metadata for tracking + if let Some((item, _)) = renderer.peek_current()? { + let metadata = playback_item_track_metadata(&item); + renderer.set_last_metadata(Some(metadata)); + } renderer.set_playback_source(PlaybackSource::FromQueue); Ok(()) @@ -1270,7 +1243,7 @@ impl ControlPoint { // Clear the local queue (detach binding + clear runtime queue structure) self.detach_playlist_binding(renderer_id, "attach_new_playlist"); - renderer.get_queue_mut().clear_queue()?; + renderer.clear_queue()?; debug!( renderer = renderer_id.0.as_str(), diff --git a/pmocontrol/src/music_renderer/arylic_tcp.rs b/pmocontrol/src/music_renderer/arylic_tcp.rs index 7cebd074..234b0bdb 100644 --- a/pmocontrol/src/music_renderer/arylic_tcp.rs +++ b/pmocontrol/src/music_renderer/arylic_tcp.rs @@ -1,3 +1,4 @@ +use std::sync::{Arc, Mutex}; use std::time::Duration; use serde::Deserialize; @@ -13,10 +14,13 @@ use crate::linkplay_client::extract_linkplay_host; use crate::model::{PlaybackState, RendererInfo}; use crate::music_renderer::RendererFromMediaRendererInfo; use crate::music_renderer::capabilities::{ - PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl, VolumeControl, + PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend, + TransportControl, VolumeControl, }; use crate::music_renderer::musicrenderer::MusicRendererBackend; use crate::music_renderer::time_utils::{format_hhmmss, ms_to_seconds, parse_hhmmss_strict}; +use crate::queue::MusicQueue; +use crate::queue::{EnqueueMode, PlaybackItem, QueueBackend, QueueSnapshot}; /// Raw response from Arylic MCU+PINFGET command #[derive(Debug, Deserialize)] @@ -40,6 +44,7 @@ pub struct ArylicTcpRenderer { host: String, port: u16, timeout: Duration, + queue: Arc>, } impl ArylicTcpRenderer { @@ -111,10 +116,13 @@ impl RendererFromMediaRendererInfo for ArylicTcpRenderer { )) })?; + let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?)); + Ok(Self { host, port: ARYLIC_TCP_PORT, timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS), + queue, }) } @@ -209,6 +217,135 @@ impl PlaybackPosition for ArylicTcpRenderer { } } +impl RendererBackend for ArylicTcpRenderer { + fn queue(&self) -> &Arc> { + &self.queue + } +} + +impl QueueTransportControl for ArylicTcpRenderer { + fn play_from_queue(&self) -> Result<(), ControlPointError> { + let mut queue = self.queue.lock().unwrap(); + + let current_index = match queue.current_index()? { + Some(idx) => idx, + None => { + if queue.len()? > 0 { + queue.set_index(Some(0))?; + 0 + } else { + return Err(ControlPointError::QueueError("Queue is empty".into())); + } + } + }; + + let item = queue + .get_item(current_index)? + .ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?; + + let uri = item.uri.clone(); + drop(queue); + + self.play_uri(&uri, "") + } + + fn play_next(&self) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + if !queue.advance()? { + return Err(ControlPointError::QueueError("No next track".into())); + } + } + + self.play_from_queue() + } + + fn play_previous(&self) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + if !queue.rewind()? { + return Err(ControlPointError::QueueError("No previous track".into())); + } + } + + self.play_from_queue() + } + + fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + queue.set_index(Some(index))?; + } + + self.play_from_queue() + } +} + +impl QueueBackend for ArylicTcpRenderer { + fn len(&self) -> Result { + self.queue.lock().unwrap().len() + } + + fn track_ids(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().track_ids() + } + + fn id_to_position(&self, id: u32) -> Result { + self.queue.lock().unwrap().id_to_position(id) + } + + fn position_to_id(&self, id: usize) -> Result { + self.queue.lock().unwrap().position_to_id(id) + } + + fn current_track(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().current_track() + } + + fn current_index(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().current_index() + } + + fn queue_snapshot(&self) -> Result { + self.queue.lock().unwrap().queue_snapshot() + } + + fn set_index(&mut self, index: Option) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().set_index(index) + } + + fn replace_queue( + &mut self, + items: Vec, + current_index: Option, + ) -> Result<(), ControlPointError> { + self.queue + .lock() + .unwrap() + .replace_queue(items, current_index) + } + + fn sync_queue(&mut self, items: Vec) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().sync_queue(items) + } + + fn get_item(&self, index: usize) -> Result, ControlPointError> { + self.queue.lock().unwrap().get_item(index) + } + + fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().replace_item(index, item) + } + + fn enqueue_items( + &mut self, + items: Vec, + mode: EnqueueMode, + ) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().enqueue_items(items, mode) + } +} + #[derive(Debug)] struct ArylicPlaybackInfo { status_raw: String, diff --git a/pmocontrol/src/music_renderer/capabilities.rs b/pmocontrol/src/music_renderer/capabilities.rs index bda9f8cf..c93cad36 100644 --- a/pmocontrol/src/music_renderer/capabilities.rs +++ b/pmocontrol/src/music_renderer/capabilities.rs @@ -1,8 +1,36 @@ // pmocontrol/src/capabilities.rs use anyhow::Result; +use std::sync::{Arc, Mutex}; +use crate::queue::MusicQueue; use crate::{errors::ControlPointError, model::PlaybackState}; +/// Backend-specific operations for renderers. +/// +/// This trait provides access to backend-specific resources like the queue. +pub trait RendererBackend { + /// Returns a reference to the queue associated with this backend. + fn queue(&self) -> &Arc>; +} + +/// Queue-aware transport control operations. +/// +/// These operations combine queue management with transport control, +/// allowing navigation (next/previous) and track selection from the queue. +pub trait QueueTransportControl { + /// Play the next track from the queue. + fn play_next(&self) -> Result<(), ControlPointError>; + + /// Play the previous track from the queue. + fn play_previous(&self) -> Result<(), ControlPointError>; + + /// Play from the queue at the current index (or initialize to 0 if not set). + fn play_from_queue(&self) -> Result<(), ControlPointError>; + + /// Play from a specific index in the queue. + fn play_from_index(&self, index: usize) -> Result<(), ControlPointError>; +} + /// Logical playback position across backends. /// /// Times peuvent être soit en secondes, soit en "HH:MM:SS" selon ce que diff --git a/pmocontrol/src/music_renderer/chromecast_renderer.rs b/pmocontrol/src/music_renderer/chromecast_renderer.rs index bffee820..ff3d2f13 100644 --- a/pmocontrol/src/music_renderer/chromecast_renderer.rs +++ b/pmocontrol/src/music_renderer/chromecast_renderer.rs @@ -17,16 +17,19 @@ use std::thread::JoinHandle; use tracing::debug; use crate::DeviceIdentity; -use crate::music_renderer::capabilities::{ - PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl, - VolumeControl, +use crate::discovery::chromecast_discovery::{ + extract_host_from_location, extract_port_from_location, }; -use crate::music_renderer::time_utils::{format_hhmmss_f64, parse_hhmmss_strict}; -use crate::discovery::chromecast_discovery::{extract_host_from_location, extract_port_from_location}; use crate::errors::ControlPointError; use crate::model::{PlaybackState, RendererInfo}; use crate::music_renderer::RendererFromMediaRendererInfo; +use crate::music_renderer::capabilities::{ + PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend, + TransportControl, VolumeControl, +}; use crate::music_renderer::musicrenderer::MusicRendererBackend; +use crate::music_renderer::time_utils::{format_hhmmss_f64, parse_hhmmss_strict}; +use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot}; use rust_cast::{ CastDevice, ChannelMessage, @@ -55,6 +58,7 @@ pub struct ChromecastRenderer { /// Handle to the active heartbeat thread, if any. /// Wrapped in Arc to allow cloning and proper thread lifecycle management. thread_handle: Arc>>>, + queue: Arc>, } impl std::fmt::Debug for ChromecastRenderer { @@ -88,20 +92,22 @@ fn connect_to_device<'a>(host: &'a str, port: u16) -> Result, Con // Ensure rustls crypto provider is initialized before any TLS connection ensure_crypto_provider_initialized(); - let device = CastDevice::connect_without_host_verification(host, port) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to Chromecast: {}", e)))?; + let device = CastDevice::connect_without_host_verification(host, port).map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to connect to Chromecast: {}", e)) + })?; device .connection .connect(DEFAULT_DESTINATION_ID.to_string()) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect channel: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to connect channel: {}", e)) + })?; // Send initial gre to establish heartbeat communication // This is critical per rust_caster.rs example - device - .heartbeat - .ping() - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to send initial heartbeat ping: {}", e)))?; + device.heartbeat.ping().map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to send initial heartbeat ping: {}", e)) + })?; Ok(device) } @@ -116,7 +122,6 @@ fn map_player_state(player_state: &CastPlayerState) -> PlaybackState { } } - impl RendererFromMediaRendererInfo for ChromecastRenderer { fn from_renderer_info(info: &RendererInfo) -> Result { tracing::info!( @@ -125,13 +130,18 @@ impl RendererFromMediaRendererInfo for ChromecastRenderer { info.friendly_name() ); - let host = extract_host_from_location(info.location()) - .ok_or_else(|| ControlPointError::ChromecastError(format!("Invalid Chromecast location: {}", info.location())))?; + let host = extract_host_from_location(info.location()).ok_or_else(|| { + ControlPointError::ChromecastError(format!( + "Invalid Chromecast location: {}", + info.location() + )) + })?; let port = extract_port_from_location(info.location()).unwrap_or(DEFAULT_CHROMECAST_PORT); let stop_signal = Arc::new(Mutex::new(false)); let thread_handle = Arc::new(Mutex::new(None)); + let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?)); tracing::info!( "ChromecastRenderer created for {} with host={} port={}", @@ -145,13 +155,13 @@ impl RendererFromMediaRendererInfo for ChromecastRenderer { port, stop_signal, thread_handle, + queue, }) } fn to_backend(self) -> MusicRendererBackend { MusicRendererBackend::Chromecast(self) } - } impl TransportControl for ChromecastRenderer { @@ -308,10 +318,9 @@ impl TransportControl for ChromecastRenderer { let device = connect_to_device(&self.host, self.port)?; // Get receiver status to find the active app - let status = device - .receiver - .get_status() - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?; + let status = device.receiver.get_status().map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) + })?; let app = status .applications @@ -322,13 +331,17 @@ impl TransportControl for ChromecastRenderer { device .connection .connect(app.transport_id.as_str()) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)) + })?; // Get media status let media_status = device .media .get_status(app.transport_id.as_str(), None) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)) + })?; let media_entry = media_status .entries @@ -349,10 +362,9 @@ impl TransportControl for ChromecastRenderer { let device = connect_to_device(&self.host, self.port)?; - let status = device - .receiver - .get_status() - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?; + let status = device.receiver.get_status().map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) + })?; let app = status .applications @@ -362,12 +374,16 @@ impl TransportControl for ChromecastRenderer { device .connection .connect(app.transport_id.as_str()) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)) + })?; let media_status = device .media .get_status(app.transport_id.as_str(), None) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)) + })?; let media_entry = media_status .entries @@ -397,10 +413,9 @@ impl TransportControl for ChromecastRenderer { // Also send stop command to the device let device = connect_to_device(&self.host, self.port)?; - let status = device - .receiver - .get_status() - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?; + let status = device.receiver.get_status().map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) + })?; let app = status .applications @@ -410,12 +425,16 @@ impl TransportControl for ChromecastRenderer { device .connection .connect(app.transport_id.as_str()) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)) + })?; let media_status = device .media .get_status(app.transport_id.as_str(), None) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)) + })?; let media_entry = media_status .entries @@ -437,10 +456,9 @@ impl TransportControl for ChromecastRenderer { let device = connect_to_device(&self.host, self.port)?; - let status = device - .receiver - .get_status() - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?; + let status = device.receiver.get_status().map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) + })?; let app = status .applications @@ -450,12 +468,16 @@ impl TransportControl for ChromecastRenderer { device .connection .connect(app.transport_id.as_str()) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)) + })?; let media_status = device .media .get_status(app.transport_id.as_str(), None) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)) + })?; let media_entry = media_status .entries @@ -481,10 +503,9 @@ impl PlaybackStatus for ChromecastRenderer { let device = connect_to_device(&self.host, self.port)?; // Get receiver status to find the active app - let status = device - .receiver - .get_status() - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?; + let status = device.receiver.get_status().map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) + })?; // If no app is running, return NoMedia let app = match status.applications.first() { @@ -496,13 +517,17 @@ impl PlaybackStatus for ChromecastRenderer { device .connection .connect(app.transport_id.as_str()) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)) + })?; // Get media status let media_status = device .media .get_status(app.transport_id.as_str(), None) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)) + })?; // If no media entry, return NoMedia let media_entry = match media_status.entries.first() { @@ -519,10 +544,9 @@ impl PlaybackPosition for ChromecastRenderer { let device = connect_to_device(&self.host, self.port)?; // Get receiver status to find the active app - let status = device - .receiver - .get_status() - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?; + let status = device.receiver.get_status().map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) + })?; let app = status .applications @@ -533,13 +557,17 @@ impl PlaybackPosition for ChromecastRenderer { device .connection .connect(app.transport_id.as_str()) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)) + })?; // Get media status let media_status = device .media .get_status(app.transport_id.as_str(), None) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?; + .map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)) + })?; let media_entry = media_status .entries @@ -635,10 +663,9 @@ impl VolumeControl for ChromecastRenderer { fn volume(&self) -> Result { let device = connect_to_device(&self.host, self.port)?; - let status = device - .receiver - .get_status() - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?; + let status = device.receiver.get_status().map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) + })?; if let Some(level) = status.volume.level { Ok((level * 100.0) as u16) @@ -653,10 +680,9 @@ impl VolumeControl for ChromecastRenderer { let device = connect_to_device(&self.host, self.port)?; let level = (volume as f32) / 100.0; - device - .receiver - .set_volume(level) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to set volume: {}", e)))?; + device.receiver.set_volume(level).map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to set volume: {}", e)) + })?; Ok(()) } @@ -664,10 +690,9 @@ impl VolumeControl for ChromecastRenderer { fn mute(&self) -> Result { let device = connect_to_device(&self.host, self.port)?; - let status = device - .receiver - .get_status() - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?; + let status = device.receiver.get_status().map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) + })?; Ok(status.volume.muted.unwrap_or(false)) } @@ -677,11 +702,139 @@ impl VolumeControl for ChromecastRenderer { let device = connect_to_device(&self.host, self.port)?; - device - .receiver - .set_volume(mute) - .map_err(|e| ControlPointError::ChromecastError(format!("Failed to set mute: {}", e)))?; + device.receiver.set_volume(mute).map_err(|e| { + ControlPointError::ChromecastError(format!("Failed to set mute: {}", e)) + })?; Ok(()) } } + +impl RendererBackend for ChromecastRenderer { + fn queue(&self) -> &Arc> { + &self.queue + } +} + +impl QueueTransportControl for ChromecastRenderer { + fn play_from_queue(&self) -> Result<(), ControlPointError> { + let mut queue = self.queue.lock().unwrap(); + + let current_index = match queue.current_index()? { + Some(idx) => idx, + None => { + if queue.len()? > 0 { + queue.set_index(Some(0))?; + 0 + } else { + return Err(ControlPointError::QueueError("Queue is empty".into())); + } + } + }; + + let item = queue + .get_item(current_index)? + .ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?; + + let uri = item.uri.clone(); + drop(queue); + + self.play_uri(&uri, "") + } + + fn play_next(&self) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + if !queue.advance()? { + return Err(ControlPointError::QueueError("No next track".into())); + } + } + + self.play_from_queue() + } + + fn play_previous(&self) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + if !queue.rewind()? { + return Err(ControlPointError::QueueError("No previous track".into())); + } + } + + self.play_from_queue() + } + + fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + queue.set_index(Some(index))?; + } + + self.play_from_queue() + } +} + +impl QueueBackend for ChromecastRenderer { + fn len(&self) -> Result { + self.queue.lock().unwrap().len() + } + + fn track_ids(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().track_ids() + } + + fn id_to_position(&self, id: u32) -> Result { + self.queue.lock().unwrap().id_to_position(id) + } + + fn position_to_id(&self, id: usize) -> Result { + self.queue.lock().unwrap().position_to_id(id) + } + + fn current_track(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().current_track() + } + + fn current_index(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().current_index() + } + + fn queue_snapshot(&self) -> Result { + self.queue.lock().unwrap().queue_snapshot() + } + + fn set_index(&mut self, index: Option) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().set_index(index) + } + + fn replace_queue( + &mut self, + items: Vec, + current_index: Option, + ) -> Result<(), ControlPointError> { + self.queue + .lock() + .unwrap() + .replace_queue(items, current_index) + } + + fn sync_queue(&mut self, items: Vec) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().sync_queue(items) + } + + fn get_item(&self, index: usize) -> Result, ControlPointError> { + self.queue.lock().unwrap().get_item(index) + } + + fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().replace_item(index, item) + } + + fn enqueue_items( + &mut self, + items: Vec, + mode: EnqueueMode, + ) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().enqueue_items(items, mode) + } +} diff --git a/pmocontrol/src/music_renderer/linkplay_renderer.rs b/pmocontrol/src/music_renderer/linkplay_renderer.rs index f497f32b..002bd0be 100644 --- a/pmocontrol/src/music_renderer/linkplay_renderer.rs +++ b/pmocontrol/src/music_renderer/linkplay_renderer.rs @@ -1,28 +1,33 @@ use std::fmt; +use std::sync::{Arc, Mutex}; use std::time::Duration; use ureq::Agent; use crate::DeviceIdentity; -use crate::linkplay_client::{LinkPlayStatus, build_agent, extract_linkplay_host, fetch_status_for_host, percent_encode}; -use crate::music_renderer::capabilities::{ - PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl, - VolumeControl, -}; -use crate::music_renderer::time_utils::{parse_hhmmss_strict}; use crate::errors::ControlPointError; -use crate::model::{RendererInfo, PlaybackState}; +use crate::linkplay_client::{ + LinkPlayStatus, build_agent, extract_linkplay_host, fetch_status_for_host, percent_encode, +}; +use crate::model::{PlaybackState, RendererInfo}; use crate::music_renderer::RendererFromMediaRendererInfo; +use crate::music_renderer::capabilities::{ + PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend, + TransportControl, VolumeControl, +}; use crate::music_renderer::musicrenderer::MusicRendererBackend; +use crate::music_renderer::time_utils::parse_hhmmss_strict; +use crate::queue::MusicQueue; +use crate::queue::{EnqueueMode, PlaybackItem, QueueBackend, QueueSnapshot}; const DEFAULT_HTTP_TIMEOUT_SECS: u64 = 3; - /// Renderer backend for devices exposing the LinkPlay HTTP API. #[derive(Clone)] pub struct LinkPlayRenderer { host: String, timeout: Duration, + queue: Arc>, } impl fmt::Debug for LinkPlayRenderer { @@ -34,7 +39,6 @@ impl fmt::Debug for LinkPlayRenderer { } impl LinkPlayRenderer { - fn agent(&self) -> Agent { build_agent(self.timeout) } @@ -44,10 +48,12 @@ impl LinkPlayRenderer { "http://{}/httpapi.asp?command=setPlayerCmd:{}", self.host, command ); - self.agent() - .get(&url) - .call() - .map_err(|_| ControlPointError::ArilycTcpError(format!("LinkPlay command {} failed for {}", command, self.host)))?; + self.agent().get(&url).call().map_err(|_| { + ControlPointError::ArilycTcpError(format!( + "LinkPlay command {} failed for {}", + command, self.host + )) + })?; Ok(()) } @@ -58,20 +64,25 @@ impl LinkPlayRenderer { impl RendererFromMediaRendererInfo for LinkPlayRenderer { fn from_renderer_info(info: &RendererInfo) -> Result { - let host = extract_linkplay_host(&info.location()) - .ok_or_else(|| ControlPointError::LinkPlayError(format!("Renderer {} has no valid LOCATION host", info.udn())))?; + let host = extract_linkplay_host(&info.location()).ok_or_else(|| { + ControlPointError::LinkPlayError(format!( + "Renderer {} has no valid LOCATION host", + info.udn() + )) + })?; + + let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?)); Ok(Self { host, timeout: Duration::from_secs(DEFAULT_HTTP_TIMEOUT_SECS), + queue, }) } - fn to_backend(self) -> MusicRendererBackend { MusicRendererBackend::LinkPlay(self) } - } impl TransportControl for LinkPlayRenderer { @@ -129,6 +140,131 @@ impl PlaybackPosition for LinkPlayRenderer { } } +impl RendererBackend for LinkPlayRenderer { + fn queue(&self) -> &Arc> { + &self.queue + } +} +impl QueueTransportControl for LinkPlayRenderer { + fn play_from_queue(&self) -> Result<(), ControlPointError> { + let mut queue = self.queue.lock().unwrap(); + let current_index = match queue.current_index()? { + Some(idx) => idx, + None => { + if queue.len()? > 0 { + queue.set_index(Some(0))?; + 0 + } else { + return Err(ControlPointError::QueueError("Queue is empty".into())); + } + } + }; + let item = queue + .get_item(current_index)? + .ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?; + + let uri = item.uri.clone(); + drop(queue); + + self.play_uri(&uri, "") + } + + fn play_next(&self) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + if !queue.advance()? { + return Err(ControlPointError::QueueError("No next track".into())); + } + } + + self.play_from_queue() + } + + fn play_previous(&self) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + if !queue.rewind()? { + return Err(ControlPointError::QueueError("No previous track".into())); + } + } + + self.play_from_queue() + } + + fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + queue.set_index(Some(index))?; + } + + self.play_from_queue() + } +} + +impl QueueBackend for LinkPlayRenderer { + fn len(&self) -> Result { + self.queue.lock().unwrap().len() + } + + fn track_ids(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().track_ids() + } + + fn id_to_position(&self, id: u32) -> Result { + self.queue.lock().unwrap().id_to_position(id) + } + + fn position_to_id(&self, id: usize) -> Result { + self.queue.lock().unwrap().position_to_id(id) + } + + fn current_track(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().current_track() + } + + fn current_index(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().current_index() + } + + fn queue_snapshot(&self) -> Result { + self.queue.lock().unwrap().queue_snapshot() + } + + fn set_index(&mut self, index: Option) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().set_index(index) + } + + fn replace_queue( + &mut self, + items: Vec, + current_index: Option, + ) -> Result<(), ControlPointError> { + self.queue + .lock() + .unwrap() + .replace_queue(items, current_index) + } + + fn sync_queue(&mut self, items: Vec) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().sync_queue(items) + } + + fn get_item(&self, index: usize) -> Result, ControlPointError> { + self.queue.lock().unwrap().get_item(index) + } + + fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().replace_item(index, item) + } + + fn enqueue_items( + &mut self, + items: Vec, + mode: EnqueueMode, + ) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().enqueue_items(items, mode) + } +} diff --git a/pmocontrol/src/music_renderer/musicrenderer.rs b/pmocontrol/src/music_renderer/musicrenderer.rs index 77f505ec..25b28f45 100644 --- a/pmocontrol/src/music_renderer/musicrenderer.rs +++ b/pmocontrol/src/music_renderer/musicrenderer.rs @@ -14,7 +14,8 @@ use crate::model::{PlaybackSource, PlaybackState, RendererInfo, RendererProtocol use crate::music_renderer::RendererFromMediaRendererInfo; use crate::music_renderer::arylic_tcp::ArylicTcpRenderer; use crate::music_renderer::capabilities::{ - PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl, VolumeControl, + PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend, + TransportControl, VolumeControl, }; use crate::music_renderer::chromecast_renderer::ChromecastRenderer; use crate::music_renderer::linkplay_renderer::LinkPlayRenderer; @@ -87,7 +88,6 @@ pub struct MusicRenderer { info: RendererInfo, connection: Arc>, backend: Arc>, - queue: Arc>, playlist_binding: Arc>>, state: Arc>, } @@ -96,7 +96,6 @@ impl MusicRenderer { pub fn new( info: RendererInfo, backend: Arc>, - queue: Arc>, ) -> Arc { let connection = DeviceConnectionState::new(); @@ -104,7 +103,6 @@ impl MusicRenderer { info, connection: Arc::new(Mutex::new(connection)), backend, - queue, playlist_binding: Arc::new(Mutex::new(None)), state: Arc::new(Mutex::new(MusicRendererState::default())), }; @@ -115,13 +113,11 @@ impl MusicRenderer { pub fn from_renderer_info(info: &RendererInfo) -> Result { let connection = Arc::new(Mutex::new(DeviceConnectionState::new())); let backend = MusicRendererBackend::make_from_renderer_info(info)?; - let queue = MusicQueue::make_from_renderer_info(info)?; let renderer = MusicRenderer { info: info.clone(), connection, backend, - queue, playlist_binding: Arc::new(Mutex::new(None)), state: Arc::new(Mutex::new(MusicRendererState::default())), }; @@ -187,9 +183,9 @@ impl MusicRenderer { /// Prepare the renderer for attaching a new playlist by clearing the queue and stopping playback. pub fn clear_for_playlist_attach(&self) -> Result<(), ControlPointError> { // Clear the queue first - self.queue + self.backend .lock() - .expect("Queue mutex poisoned") + .expect("Backend mutex poisoned") .clear_queue()?; // Then stop playback (ignore errors if already stopped) @@ -210,9 +206,9 @@ impl MusicRenderer { /// Get the current queue snapshot. pub fn queue_snapshot(&self) -> Result { let mut snapshot = self - .queue + .backend .lock() - .expect("Queue mutex poisoned") + .expect("Backend mutex poisoned") .queue_snapshot()?; // Enrich snapshot with playlist_id from binding if available @@ -232,72 +228,42 @@ impl MusicRenderer { /// Get the current queue item without advancing. /// Returns the item and count of remaining items after current. pub fn peek_current(&self) -> Result, ControlPointError> { - self.queue + self.backend .lock() - .expect("Queue mutex poisoned") + .expect("Backend mutex poisoned") .peek_current() } /// Get the count of items remaining after the current index. pub fn upcoming_len(&self) -> Result { - self.queue + self.backend .lock() - .expect("Queue mutex poisoned") + .expect("Backend mutex poisoned") .upcoming_len() } /// Play the current item from the queue. pub fn play_current_from_queue(&self) -> Result<(), ControlPointError> { - let mut queue = self.queue.lock().expect("Queue mutex poisoned"); - let backend = self.backend.lock().expect("Backend mutex poisoned"); - - // Get the current item from the queue - let (item, _remaining) = queue.peek_current()?.ok_or_else(|| { - ControlPointError::QueueError("Queue is empty, cannot play current".to_string()) - })?; - - // Build DIDL metadata if available - let metadata_xml = item - .metadata - .as_ref() - .map(|m| build_didl_lite_metadata(m, &item.uri, &item.protocol_info)) - .unwrap_or_default(); - - // Play the URI using the backend - backend.play_uri(&item.uri, &metadata_xml)?; - - Ok(()) + self.backend + .lock() + .expect("Backend mutex poisoned") + .play_from_queue() } /// Advance to and play the next item from the queue. pub fn play_next_from_queue(&self) -> Result<(), ControlPointError> { - let mut queue = self.queue.lock().expect("Queue mutex poisoned"); - let backend = self.backend.lock().expect("Backend mutex poisoned"); - - // Dequeue the next item - let (item, _remaining) = queue - .dequeue_next()? - .ok_or_else(|| ControlPointError::QueueError("No next item in queue".to_string()))?; - - // Build DIDL metadata if available - let metadata_xml = item - .metadata - .as_ref() - .map(|m| build_didl_lite_metadata(m, &item.uri, &item.protocol_info)) - .unwrap_or_default(); - - // Play the URI using the backend - backend.play_uri(&item.uri, &metadata_xml)?; - - Ok(()) - } - - /// Transport control: play URI with metadata - pub fn play_uri(&self, uri: &str, metadata: &str) -> Result<(), ControlPointError> { self.backend .lock() .expect("Backend mutex poisoned") - .play_uri(uri, metadata) + .play_next() + } + + /// Play from a specific index in the queue. + pub fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> { + self.backend + .lock() + .expect("Backend mutex poisoned") + .play_from_index(index) } /// Transport control: play @@ -306,18 +272,16 @@ impl MusicRenderer { /// joue le track courant de la queue automatiquement (comportement unifié pour tous les backends). pub fn play(&self) -> Result<(), ControlPointError> { // Vérifier si on a une queue non vide - let queue = self.queue.lock().expect("Queue mutex poisoned"); - let queue_not_empty = queue.len().unwrap_or(0) > 0; - drop(queue); // Libérer le lock avant l'appel au backend + let backend = self.backend.lock().expect("Backend mutex poisoned"); + let queue_not_empty = backend.len().unwrap_or(0) > 0; if queue_not_empty { // Si on a des items dans la queue, jouer le track courant (ou le premier si aucun n'est sélectionné) - // peek_current() initialise automatiquement l'index à 0 si nécessaire // Cela fonctionne pour tous les backends (UPnP interne, OpenHome, etc.) - self.play_current_from_queue() + backend.play_from_queue() } else { // Queue vide : déléguer au backend (reprend la lecture en cours, etc.) - self.backend.lock().expect("Backend mutex poisoned").play() + backend.play() } } @@ -506,14 +470,20 @@ impl MusicRenderer { .unwrap_or(false) } + /// Returns the number of items in the queue. + pub fn len(&self) -> Result { + let backend = self.backend.lock().expect("Backend mutex poisoned"); + backend.len() + } + /// Add items to the queue using the specified enqueue mode. pub fn enqueue_items( &self, items: Vec, mode: EnqueueMode, ) -> Result<(), ControlPointError> { - let mut queue = self.queue.lock().expect("Queue mutex poisoned"); - queue.enqueue_items(items, mode) + let mut backend = self.backend.lock().expect("Backend mutex poisoned"); + backend.enqueue_items(items, mode) } /// Synchronize the queue with new items while preserving the current track. @@ -523,21 +493,33 @@ impl MusicRenderer { /// - If the current track is NOT in the new items, it's preserved as the first item /// - If there's no current track, the queue is simply replaced pub fn sync_queue(&self, items: Vec) -> Result<(), ControlPointError> { - let mut queue = self.queue.lock().expect("Queue mutex poisoned"); - queue.sync_queue(items) + let mut backend = self.backend.lock().expect("Backend mutex poisoned"); + backend.sync_queue(items) } /// Set the current queue index (for advanced use). /// Note: This does NOT start playback. Use select_queue_track() to play. pub fn set_queue_index(&self, index: Option) -> Result<(), ControlPointError> { - let mut queue = self.queue.lock().expect("Queue mutex poisoned"); - queue.set_index(index) + let mut backend = self.backend.lock().expect("Backend mutex poisoned"); + backend.set_index(index) } /// Clears the renderer's queue using the generic QueueBackend trait. pub fn clear_queue(&self) -> Result<(), ControlPointError> { - let mut queue = self.queue.lock().expect("Queue mutex poisoned"); - queue.clear_queue() + let mut backend = self.backend.lock().expect("Backend mutex poisoned"); + backend.clear_queue() + } + + /// Dequeues and returns the next item from the queue. + pub fn dequeue_next(&self) -> Result, ControlPointError> { + let mut backend = self.backend.lock().expect("Backend mutex poisoned"); + backend.dequeue_next() + } + + /// Sets the current index in the queue. + pub fn set_index(&self, index: Option) -> Result<(), ControlPointError> { + let mut backend = self.backend.lock().expect("Backend mutex poisoned"); + backend.set_index(index) } /// Replaces the entire queue with new items and sets the current index. @@ -547,8 +529,8 @@ impl MusicRenderer { items: Vec, current_index: Option, ) -> Result<(), ControlPointError> { - let mut queue = self.queue.lock().expect("Queue mutex poisoned"); - queue.replace_queue(items, current_index) + let mut backend = self.backend.lock().expect("Backend mutex poisoned"); + backend.replace_queue(items, current_index) } /// Adds a track to the queue. @@ -578,34 +560,14 @@ impl MusicRenderer { /// Converts the track ID to a position using the generic QueueBackend trait, /// then plays the track. pub fn select_queue_track(&self, track_id: u32) -> Result<(), ControlPointError> { - let queue = self.queue.lock().expect("Queue mutex poisoned"); + // Convert track_id to index + let index = { + let backend = self.backend.lock().expect("Backend mutex poisoned"); + backend.id_to_position(track_id)? + }; - // Convert track ID to position using generic QueueBackend trait - let position = queue.id_to_position(track_id)?; - drop(queue); - - // Set the index - let mut queue = self.queue.lock().expect("Queue mutex poisoned"); - queue.set_index(Some(position))?; - - // Get the item to play - let item = queue - .get_item(position)? - .ok_or_else(|| ControlPointError::QueueError("Track not found".to_string()))?; - drop(queue); - - // Build metadata XML - let metadata_xml = item - .metadata - .as_ref() - .map(|m| build_didl_lite_metadata(m, &item.uri, &item.protocol_info)) - .unwrap_or_default(); - - // Play the item using TransportControl - let backend = self.backend.lock().expect("Backend mutex poisoned"); - backend.play_uri(&item.uri, &metadata_xml)?; - - Ok(()) + // Play from that index + self.play_from_index(index) } /// Synchronizes the queue state with the backend. @@ -613,9 +575,9 @@ impl MusicRenderer { /// For backends with persistent queues (OpenHome), this refreshes the local view. /// For others, this is essentially a no-op (just reads the current state). pub fn sync_queue_state(&self) -> Result<(), ControlPointError> { - let queue = self.queue.lock().expect("Queue mutex poisoned"); + let backend = self.backend.lock().expect("Backend mutex poisoned"); // Calling queue_snapshot() triggers a refresh for backends that need it - let _ = queue.queue_snapshot()?; + let _ = backend.queue_snapshot()?; Ok(()) } @@ -623,29 +585,25 @@ impl MusicRenderer { /// /// This is primarily for backends with persistent queues (OpenHome). pub fn play_current_from_backend_queue(&self) -> Result<(), ControlPointError> { - let queue = self.queue.lock().expect("Queue mutex poisoned"); + let backend = self.backend.lock().expect("Backend mutex poisoned"); // Get current track ID using generic QueueBackend trait - let track_id = queue + let track_id = backend .current_track()? .ok_or_else(|| ControlPointError::QueueError("No current track".to_string()))?; - drop(queue); + drop(backend); // Play it using select_queue_track self.select_queue_track(track_id) } - /// Returns a reference to the queue (read-only access via lock). - /// This is a convenience method to avoid repetitive `queue.lock().unwrap()` patterns. - pub fn get_queue(&self) -> std::sync::MutexGuard<'_, MusicQueue> { - self.queue.lock().unwrap() - } - - /// Returns a mutable reference to the queue (write access via lock). - /// This is a convenience method to avoid repetitive `queue.lock().unwrap()` patterns. - pub fn get_queue_mut(&self) -> std::sync::MutexGuard<'_, MusicQueue> { - self.queue.lock().unwrap() + /// Plays from the queue at the current position. + /// + /// Uses the backend's play_from_queue which preserves the queue for all backends. + pub fn play_from_queue(&self) -> Result<(), ControlPointError> { + let backend = self.backend.lock().expect("Backend mutex poisoned"); + backend.play_from_queue() } // --- Playback State Management --- @@ -771,7 +729,11 @@ impl MusicRenderer { } /// Helper function to build DIDL-Lite metadata XML from TrackMetadata -fn build_didl_lite_metadata(metadata: &TrackMetadata, uri: &str, protocol_info: &str) -> String { +pub(crate) fn build_didl_lite_metadata( + metadata: &TrackMetadata, + uri: &str, + protocol_info: &str, +) -> String { format!( r#" @@ -1087,3 +1049,217 @@ impl PlaybackPosition for MusicRendererBackend { } } } + +impl RendererBackend for MusicRendererBackend { + fn queue(&self) -> &Arc> { + match self { + MusicRendererBackend::Upnp(r) => r.queue(), + MusicRendererBackend::OpenHome(r) => r.queue(), + MusicRendererBackend::LinkPlay(r) => r.queue(), + MusicRendererBackend::ArylicTcp(r) => r.queue(), + MusicRendererBackend::Chromecast(cc) => cc.queue(), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.queue(), + } + } +} + +impl QueueTransportControl for MusicRendererBackend { + fn play_from_queue(&self) -> Result<(), ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.play_from_queue(), + MusicRendererBackend::OpenHome(r) => r.play_from_queue(), + MusicRendererBackend::LinkPlay(r) => r.play_from_queue(), + MusicRendererBackend::ArylicTcp(r) => r.play_from_queue(), + MusicRendererBackend::Chromecast(cc) => cc.play_from_queue(), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_from_queue(), + } + } + + fn play_next(&self) -> Result<(), ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.play_next(), + MusicRendererBackend::OpenHome(r) => r.play_next(), + MusicRendererBackend::LinkPlay(r) => r.play_next(), + MusicRendererBackend::ArylicTcp(r) => r.play_next(), + MusicRendererBackend::Chromecast(cc) => cc.play_next(), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_next(), + } + } + + fn play_previous(&self) -> Result<(), ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.play_previous(), + MusicRendererBackend::OpenHome(r) => r.play_previous(), + MusicRendererBackend::LinkPlay(r) => r.play_previous(), + MusicRendererBackend::ArylicTcp(r) => r.play_previous(), + MusicRendererBackend::Chromecast(cc) => cc.play_previous(), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_previous(), + } + } + + fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.play_from_index(index), + MusicRendererBackend::OpenHome(r) => r.play_from_index(index), + MusicRendererBackend::LinkPlay(r) => r.play_from_index(index), + MusicRendererBackend::ArylicTcp(r) => r.play_from_index(index), + MusicRendererBackend::Chromecast(cc) => cc.play_from_index(index), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_from_index(index), + } + } +} + +impl QueueBackend for MusicRendererBackend { + fn len(&self) -> Result { + match self { + MusicRendererBackend::Upnp(r) => r.len(), + MusicRendererBackend::OpenHome(r) => r.len(), + MusicRendererBackend::LinkPlay(r) => r.len(), + MusicRendererBackend::ArylicTcp(r) => r.len(), + MusicRendererBackend::Chromecast(cc) => cc.len(), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.len(), + } + } + + fn track_ids(&self) -> Result, ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.track_ids(), + MusicRendererBackend::OpenHome(r) => r.track_ids(), + MusicRendererBackend::LinkPlay(r) => r.track_ids(), + MusicRendererBackend::ArylicTcp(r) => r.track_ids(), + MusicRendererBackend::Chromecast(cc) => cc.track_ids(), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.track_ids(), + } + } + + fn id_to_position(&self, id: u32) -> Result { + match self { + MusicRendererBackend::Upnp(r) => r.id_to_position(id), + MusicRendererBackend::OpenHome(r) => r.id_to_position(id), + MusicRendererBackend::LinkPlay(r) => r.id_to_position(id), + MusicRendererBackend::ArylicTcp(r) => r.id_to_position(id), + MusicRendererBackend::Chromecast(cc) => cc.id_to_position(id), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.id_to_position(id), + } + } + + fn position_to_id(&self, id: usize) -> Result { + match self { + MusicRendererBackend::Upnp(r) => r.position_to_id(id), + MusicRendererBackend::OpenHome(r) => r.position_to_id(id), + MusicRendererBackend::LinkPlay(r) => r.position_to_id(id), + MusicRendererBackend::ArylicTcp(r) => r.position_to_id(id), + MusicRendererBackend::Chromecast(cc) => cc.position_to_id(id), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.position_to_id(id), + } + } + + fn current_track(&self) -> Result, ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.current_track(), + MusicRendererBackend::OpenHome(r) => r.current_track(), + MusicRendererBackend::LinkPlay(r) => r.current_track(), + MusicRendererBackend::ArylicTcp(r) => r.current_track(), + MusicRendererBackend::Chromecast(cc) => cc.current_track(), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.current_track(), + } + } + + fn current_index(&self) -> Result, ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.current_index(), + MusicRendererBackend::OpenHome(r) => r.current_index(), + MusicRendererBackend::LinkPlay(r) => r.current_index(), + MusicRendererBackend::ArylicTcp(r) => r.current_index(), + MusicRendererBackend::Chromecast(cc) => cc.current_index(), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.current_index(), + } + } + + fn queue_snapshot(&self) -> Result { + match self { + MusicRendererBackend::Upnp(r) => r.queue_snapshot(), + MusicRendererBackend::OpenHome(r) => r.queue_snapshot(), + MusicRendererBackend::LinkPlay(r) => r.queue_snapshot(), + MusicRendererBackend::ArylicTcp(r) => r.queue_snapshot(), + MusicRendererBackend::Chromecast(cc) => cc.queue_snapshot(), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.queue_snapshot(), + } + } + + fn set_index(&mut self, index: Option) -> Result<(), ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.set_index(index), + MusicRendererBackend::OpenHome(r) => r.set_index(index), + MusicRendererBackend::LinkPlay(r) => r.set_index(index), + MusicRendererBackend::ArylicTcp(r) => r.set_index(index), + MusicRendererBackend::Chromecast(cc) => cc.set_index(index), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.set_index(index), + } + } + + fn replace_queue( + &mut self, + items: Vec, + current_index: Option, + ) -> Result<(), ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.replace_queue(items, current_index), + MusicRendererBackend::OpenHome(r) => r.replace_queue(items, current_index), + MusicRendererBackend::LinkPlay(r) => r.replace_queue(items, current_index), + MusicRendererBackend::ArylicTcp(r) => r.replace_queue(items, current_index), + MusicRendererBackend::Chromecast(cc) => cc.replace_queue(items, current_index), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => { + upnp.replace_queue(items, current_index) + } + } + } + + fn sync_queue(&mut self, items: Vec) -> Result<(), ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.sync_queue(items), + MusicRendererBackend::OpenHome(r) => r.sync_queue(items), + MusicRendererBackend::LinkPlay(r) => r.sync_queue(items), + MusicRendererBackend::ArylicTcp(r) => r.sync_queue(items), + MusicRendererBackend::Chromecast(cc) => cc.sync_queue(items), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.sync_queue(items), + } + } + + fn get_item(&self, index: usize) -> Result, ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.get_item(index), + MusicRendererBackend::OpenHome(r) => r.get_item(index), + MusicRendererBackend::LinkPlay(r) => r.get_item(index), + MusicRendererBackend::ArylicTcp(r) => r.get_item(index), + MusicRendererBackend::Chromecast(cc) => cc.get_item(index), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.get_item(index), + } + } + + fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.replace_item(index, item), + MusicRendererBackend::OpenHome(r) => r.replace_item(index, item), + MusicRendererBackend::LinkPlay(r) => r.replace_item(index, item), + MusicRendererBackend::ArylicTcp(r) => r.replace_item(index, item), + MusicRendererBackend::Chromecast(cc) => cc.replace_item(index, item), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.replace_item(index, item), + } + } + + fn enqueue_items( + &mut self, + items: Vec, + mode: EnqueueMode, + ) -> Result<(), ControlPointError> { + match self { + MusicRendererBackend::Upnp(r) => r.enqueue_items(items, mode), + MusicRendererBackend::OpenHome(r) => r.enqueue_items(items, mode), + MusicRendererBackend::LinkPlay(r) => r.enqueue_items(items, mode), + MusicRendererBackend::ArylicTcp(r) => r.enqueue_items(items, mode), + MusicRendererBackend::Chromecast(cc) => cc.enqueue_items(items, mode), + MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.enqueue_items(items, mode), + } + } +} diff --git a/pmocontrol/src/music_renderer/openhome_renderer.rs b/pmocontrol/src/music_renderer/openhome_renderer.rs index 895ca3e6..1a818e45 100644 --- a/pmocontrol/src/music_renderer/openhome_renderer.rs +++ b/pmocontrol/src/music_renderer/openhome_renderer.rs @@ -1,6 +1,9 @@ +use std::sync::{Arc, Mutex}; + use crate::DeviceIdentity; use crate::music_renderer::capabilities::{ - PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl, VolumeControl, + PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend, + TransportControl, VolumeControl, }; use crate::music_renderer::time_utils::{format_hhmmss_u32, parse_time_flexible}; @@ -12,6 +15,7 @@ use crate::music_renderer::openhome::{ build_info_client, build_playlist_client, build_product_client, build_radio_client, build_time_client, build_volume_client, }; +use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot}; use crate::upnp_clients::{ OPENHOME_PLAYLIST_HEAD_ID, OhInfoClient, OhPlaylistClient, OhProductClient, OhRadioClient, OhTimeClient, OhVolumeClient, @@ -27,6 +31,7 @@ pub struct OpenHomeRenderer { product_client: Option, #[allow(dead_code)] radio_client: Option, + queue: Arc>, } impl OpenHomeRenderer { @@ -37,6 +42,7 @@ impl OpenHomeRenderer { volume_client: Option, product_client: Option, radio_client: Option, + queue: Arc>, ) -> Self { Self { playlist, @@ -45,6 +51,7 @@ impl OpenHomeRenderer { volume_client, product_client, radio_client, + queue, } } @@ -180,6 +187,9 @@ impl OpenHomeRenderer { impl RendererFromMediaRendererInfo for OpenHomeRenderer { fn from_renderer_info(info: &RendererInfo) -> Result { + // Create the OpenHome queue + let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?)); + let renderer = OpenHomeRenderer::new( build_playlist_client(&info), build_info_client(&info), @@ -187,6 +197,7 @@ impl RendererFromMediaRendererInfo for OpenHomeRenderer { build_volume_client(&info), build_product_client(&info), build_radio_client(&info), + queue, ); if renderer.has_any_openhome_service() { @@ -204,6 +215,12 @@ impl RendererFromMediaRendererInfo for OpenHomeRenderer { } } +impl RendererBackend for OpenHomeRenderer { + fn queue(&self) -> &Arc> { + &self.queue + } +} + impl TransportControl for OpenHomeRenderer { fn play_uri(&self, uri: &str, meta: &str) -> Result<(), ControlPointError> { let playlist = self.playlist_client_for("play_uri")?; @@ -330,3 +347,133 @@ pub(crate) fn map_openhome_state(raw: &str) -> PlaybackState { other => PlaybackState::Unknown(other.to_string()), } } + +impl QueueTransportControl for OpenHomeRenderer { + fn play_from_queue(&self) -> Result<(), ControlPointError> { + { + let queue = self.queue.lock().unwrap(); + + if queue.current_index()?.is_none() { + if queue.len()? > 0 { + drop(queue); + let mut queue = self.queue.lock().unwrap(); + queue.set_index(Some(0))?; + } else { + return Err(ControlPointError::QueueError("Queue is empty".into())); + } + } + } + + let playlist = self.playlist_client_for("play_from_queue")?; + playlist.play() + } + + fn play_next(&self) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + if !queue.advance()? { + return Err(ControlPointError::QueueError("No next track".into())); + } + } + + self.play_from_queue() + } + + fn play_previous(&self) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + if !queue.rewind()? { + return Err(ControlPointError::QueueError("No previous track".into())); + } + } + + self.play_from_queue() + } + + fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> { + // For OpenHome, we need to convert index to track_id + let track_id = { + let queue = self.queue.lock().unwrap(); + queue.position_to_id(index)? + }; + + // Seek to the track by ID + let playlist = self.playlist_client_for("play_from_index")?; + playlist.seek_id(track_id)?; + + // Update local queue index + { + let mut queue = self.queue.lock().unwrap(); + queue.set_index(Some(index))?; + } + + // Start playback + playlist.play()?; + Ok(()) + } +} + +impl QueueBackend for OpenHomeRenderer { + fn len(&self) -> Result { + self.queue.lock().unwrap().len() + } + + fn track_ids(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().track_ids() + } + + fn id_to_position(&self, id: u32) -> Result { + self.queue.lock().unwrap().id_to_position(id) + } + + fn position_to_id(&self, id: usize) -> Result { + self.queue.lock().unwrap().position_to_id(id) + } + + fn current_track(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().current_track() + } + + fn current_index(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().current_index() + } + + fn queue_snapshot(&self) -> Result { + self.queue.lock().unwrap().queue_snapshot() + } + + fn set_index(&mut self, index: Option) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().set_index(index) + } + + fn replace_queue( + &mut self, + items: Vec, + current_index: Option, + ) -> Result<(), ControlPointError> { + self.queue + .lock() + .unwrap() + .replace_queue(items, current_index) + } + + fn sync_queue(&mut self, items: Vec) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().sync_queue(items) + } + + fn get_item(&self, index: usize) -> Result, ControlPointError> { + self.queue.lock().unwrap().get_item(index) + } + + fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().replace_item(index, item) + } + + fn enqueue_items( + &mut self, + items: Vec, + mode: EnqueueMode, + ) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().enqueue_items(items, mode) + } +} diff --git a/pmocontrol/src/music_renderer/upnp_renderer.rs b/pmocontrol/src/music_renderer/upnp_renderer.rs index e12901e0..6036ef94 100644 --- a/pmocontrol/src/music_renderer/upnp_renderer.rs +++ b/pmocontrol/src/music_renderer/upnp_renderer.rs @@ -1,11 +1,14 @@ +use std::sync::{Arc, Mutex}; + use crate::errors::ControlPointError; +use crate::model::PlaybackState; use crate::music_renderer::RendererFromMediaRendererInfo; use crate::music_renderer::capabilities::{ - PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl, - VolumeControl, + PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend, + TransportControl, VolumeControl, }; -use crate::model::PlaybackState; -use crate::music_renderer::musicrenderer::MusicRendererBackend; +use crate::music_renderer::musicrenderer::{MusicRendererBackend, build_didl_lite_metadata}; +use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot}; use crate::upnp_clients::{ AvTransportClient, ConnectionInfo, ConnectionManagerClient, PositionInfo, ProtocolInfo, RenderingControlClient, @@ -19,6 +22,7 @@ pub struct UpnpRenderer { rendering_control: Option, connection_manager: Option, has_avtransport_set_next: bool, + queue: Arc>, } impl UpnpRenderer { @@ -85,6 +89,24 @@ impl UpnpRenderer { } } +impl UpnpRenderer { + pub fn new( + avtransport: Option, + rendering_control: Option, + connection_manager: Option, + has_avtransport_set_next: bool, + queue: Arc>, + ) -> Self { + Self { + avtransport, + rendering_control, + connection_manager, + has_avtransport_set_next, + queue, + } + } +} + impl RendererFromMediaRendererInfo for UpnpRenderer { fn from_renderer_info(info: &RendererInfo) -> Result { // Prepare le service AVTTransport @@ -122,11 +144,15 @@ impl RendererFromMediaRendererInfo for UpnpRenderer { ))); } + // Create the internal queue + let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?)); + Ok(Self { avtransport, rendering_control, connection_manager, has_avtransport_set_next: info.capabilities().has_avtransport_set_next(), + queue, }) } @@ -135,6 +161,152 @@ impl RendererFromMediaRendererInfo for UpnpRenderer { } } +impl RendererBackend for UpnpRenderer { + fn queue(&self) -> &Arc> { + &self.queue + } +} + +impl QueueTransportControl for UpnpRenderer { + fn play_from_queue(&self) -> Result<(), ControlPointError> { + let mut queue = self.queue.lock().unwrap(); + + // Get or initialize current index + let current_index = match queue.current_index()? { + Some(idx) => idx, + None => { + if queue.len()? > 0 { + queue.set_index(Some(0))?; + 0 + } else { + return Err(ControlPointError::QueueError("Queue is empty".into())); + } + } + }; + + // Get the item + let item = queue + .get_item(current_index)? + .ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?; + + drop(queue); + + // Build metadata - handle optional TrackMetadata + let metadata = if let Some(ref track_metadata) = item.metadata { + build_didl_lite_metadata(track_metadata, &item.uri, &item.protocol_info) + } else { + // Fallback to minimal DIDL-Lite if no metadata + format!( + r#"{}"#, + item.protocol_info, item.uri + ) + }; + + // UPNP: SetAVTransportURI + Play + let avt = self.avtransport()?; + avt.set_av_transport_uri(&item.uri, &metadata)?; + avt.play(0, "1")?; + + Ok(()) + } + + fn play_next(&self) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + if !queue.advance()? { + return Err(ControlPointError::QueueError("No next track".into())); + } + } + + self.play_from_queue() + } + + fn play_previous(&self) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + if !queue.rewind()? { + return Err(ControlPointError::QueueError("No previous track".into())); + } + } + + self.play_from_queue() + } + + fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> { + { + let mut queue = self.queue.lock().unwrap(); + queue.set_index(Some(index))?; + } + + self.play_from_queue() + } +} + +impl QueueBackend for UpnpRenderer { + fn len(&self) -> Result { + self.queue.lock().unwrap().len() + } + + fn track_ids(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().track_ids() + } + + fn id_to_position(&self, id: u32) -> Result { + self.queue.lock().unwrap().id_to_position(id) + } + + fn position_to_id(&self, id: usize) -> Result { + self.queue.lock().unwrap().position_to_id(id) + } + + fn current_track(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().current_track() + } + + fn current_index(&self) -> Result, ControlPointError> { + self.queue.lock().unwrap().current_index() + } + + fn queue_snapshot(&self) -> Result { + self.queue.lock().unwrap().queue_snapshot() + } + + fn set_index(&mut self, index: Option) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().set_index(index) + } + + fn replace_queue( + &mut self, + items: Vec, + current_index: Option, + ) -> Result<(), ControlPointError> { + self.queue + .lock() + .unwrap() + .replace_queue(items, current_index) + } + + fn sync_queue(&mut self, items: Vec) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().sync_queue(items) + } + + fn get_item(&self, index: usize) -> Result, ControlPointError> { + self.queue.lock().unwrap().get_item(index) + } + + fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().replace_item(index, item) + } + + fn enqueue_items( + &mut self, + items: Vec, + mode: EnqueueMode, + ) -> Result<(), ControlPointError> { + self.queue.lock().unwrap().enqueue_items(items, mode) + } +} + /// Implémentation UPnP AV de `TransportControl` pour [`UpnpRenderer`]. /// /// Cette impl se base sur AVTransport (InstanceID = 0).