use crate::errors::ControlPointError; use crate::queue::{ EnqueueMode, InternalQueue, OpenHomeQueue, QueueBackend, QueueFromRendererInfo, }; use crate::{PlaybackItem, QueueSnapshot, RendererInfo}; use std::sync::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, Mutex, }; use std::thread; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SyncScheduleOutcome { Scheduled, AlreadyRunning, } #[derive(Debug)] enum MusicQueueBackend { Internal(InternalQueue), OpenHome(OpenHomeQueue), } #[derive(Debug)] pub struct MusicQueue { backend: MusicQueueBackend, sync_in_progress: Arc, sync_pending: Arc, sync_cancel_token: Arc, } impl MusicQueue { /// Creates a queue appropriate for the given renderer. /// This is the factory method used by QueueFromRendererInfo trait. pub fn from_renderer_info(info: &RendererInfo) -> Result { let backend = if info.capabilities().has_oh_playlist() { MusicQueueBackend::OpenHome(OpenHomeQueue::from_renderer_info(info)?) } else { MusicQueueBackend::Internal(InternalQueue::from_renderer_info(info)?) }; Ok(MusicQueue { backend, sync_in_progress: Arc::new(AtomicBool::new(false)), sync_pending: Arc::new(AtomicBool::new(false)), sync_cancel_token: Arc::new(AtomicBool::new(false)), }) } /// Creates a MusicQueue from an InternalQueue backend. pub fn from_internal(queue: InternalQueue) -> MusicQueue { MusicQueue { backend: MusicQueueBackend::Internal(queue), sync_in_progress: Arc::new(AtomicBool::new(false)), sync_pending: Arc::new(AtomicBool::new(false)), sync_cancel_token: Arc::new(AtomicBool::new(false)), } } /// Creates a MusicQueue from an OpenHomeQueue backend. pub fn from_openhome(queue: OpenHomeQueue) -> MusicQueue { MusicQueue { backend: MusicQueueBackend::OpenHome(queue), sync_in_progress: Arc::new(AtomicBool::new(false)), sync_pending: Arc::new(AtomicBool::new(false)), sync_cancel_token: Arc::new(AtomicBool::new(false)), } } /// Returns true if this is an OpenHome backend. pub fn is_openhome(&self) -> bool { matches!(self.backend, MusicQueueBackend::OpenHome(_)) } /// Returns a reference to the OpenHome queue if this is an OpenHome backend. pub fn as_openhome(&self) -> Option<&OpenHomeQueue> { match &self.backend { MusicQueueBackend::OpenHome(q) => Some(q), MusicQueueBackend::Internal(_) => None, } } /// Returns a mutable reference to the OpenHome queue if this is an OpenHome backend. pub fn as_openhome_mut(&mut self) -> Option<&mut OpenHomeQueue> { match &mut self.backend { MusicQueueBackend::OpenHome(q) => Some(q), MusicQueueBackend::Internal(_) => None, } } pub fn len(&self) -> Result { match &self.backend { MusicQueueBackend::Internal(q) => q.len(), MusicQueueBackend::OpenHome(q) => q.len(), } } pub fn schedule_sync( queue_arc: &Arc>, renderer_id: &str, items: Vec, pending_items_fn: Box< dyn Fn() -> Result, ControlPointError> + Send + 'static, >, on_ready: Option>, on_complete: Box, ) -> SyncScheduleOutcome { let (sync_in_progress, sync_pending, sync_cancel_token) = { let q = queue_arc.lock().expect("MusicQueue mutex poisoned"); ( Arc::clone(&q.sync_in_progress), Arc::clone(&q.sync_pending), Arc::clone(&q.sync_cancel_token), ) }; if sync_in_progress.swap(true, SeqCst) { sync_cancel_token.store(true, SeqCst); sync_pending.store(true, SeqCst); return SyncScheduleOutcome::AlreadyRunning; } sync_cancel_token.store(false, SeqCst); sync_pending.store(false, SeqCst); let queue_arc = Arc::clone(queue_arc); let thread_name = format!("queue-sync-{}", renderer_id); thread::Builder::new() .name(thread_name) .spawn(move || { // Protocol for the three AtomicBools: // sync_in_progress : set to true before spawn, cleared on Drop via Guard. // sync_pending : set to true by a concurrent caller that arrives while // a sync is already running. The worker re-fetches items // and loops when it detects this flag on exit. // sync_cancel_token: set to true when a new sync request interrupts an // in-progress one. Passed into QueueBackend::sync_queue // so it can abort early. struct Guard(Arc); impl Drop for Guard { fn drop(&mut self) { self.0.store(false, SeqCst); } } let _guard = Guard(Arc::clone(&sync_in_progress)); // Error policy: sync errors are logged by sync_worker_loop (warn level) and // the thread exits normally. No restart — a new sync can be scheduled via // schedule_sync. The Guard Drop clears sync_in_progress unconditionally, // even on panic, keeping the AtomicBool protocol consistent. tracing::debug!(thread = %std::thread::current().name().unwrap_or("?"), "queue-sync thread started"); Self::sync_worker_loop( queue_arc, items, pending_items_fn, on_ready, on_complete, sync_pending, sync_cancel_token, ); tracing::debug!(thread = %std::thread::current().name().unwrap_or("?"), "queue-sync thread done"); }) .expect("Failed to spawn queue-sync thread"); SyncScheduleOutcome::Scheduled } /// Inner loop executed by the sync worker thread. /// /// Runs at least once with `initial_items`. If a new sync request arrives while the /// loop is running (`sync_pending` becomes true), it re-fetches items via /// `pending_items_fn` and iterates again, allowing the latest playlist state to win. fn sync_worker_loop( queue_arc: Arc>, initial_items: Vec, pending_items_fn: Box Result, ControlPointError> + Send>, initial_on_ready: Option>, on_complete: Box, sync_pending: Arc, sync_cancel_token: Arc, ) { let mut current_items = initial_items; let mut current_on_ready = Some(initial_on_ready); let mut on_complete = Some(on_complete); loop { sync_pending.store(false, SeqCst); sync_cancel_token.store(false, SeqCst); // Extract the real on_ready BEFORE locking the queue. // on_ready may call play_from_queue() which re-locks the queue, // so we must NOT call it while holding queue_arc. let real_on_ready = current_on_ready.take().flatten(); let on_ready_triggered = Arc::new(AtomicBool::new(false)); let proxy_on_ready: Option> = real_on_ready.as_ref().map(|_| { let flag = Arc::clone(&on_ready_triggered); Box::new(move || { flag.store(true, SeqCst); }) as Box }); tracing::debug!( thread = %std::thread::current().name().unwrap_or("?"), items = current_items.len(), has_on_ready = real_on_ready.is_some(), "queue-sync: calling sync_queue" ); let result = { let mut q = queue_arc.lock().expect("MusicQueue mutex poisoned"); ::sync_queue( &mut q, current_items, &sync_cancel_token, proxy_on_ready, ) }; // Queue lock is released here. // Now safe to call on_ready (which may re-lock the queue). let carry_on_ready = if on_ready_triggered.load(SeqCst) { tracing::debug!( thread = %std::thread::current().name().unwrap_or("?"), "queue-sync: on_ready triggered, calling callback" ); if let Some(f) = real_on_ready { f(); } None } else { if real_on_ready.is_some() { tracing::debug!( thread = %std::thread::current().name().unwrap_or("?"), "queue-sync: on_ready not triggered, carrying to next attempt" ); } real_on_ready }; match result { Err(ControlPointError::SyncCancelled) => { tracing::debug!( thread = %std::thread::current().name().unwrap_or("?"), "queue-sync: cancelled" ); } Err(e) => { tracing::warn!("queue-sync error: {}", e); } Ok(()) => { tracing::debug!( thread = %std::thread::current().name().unwrap_or("?"), "queue-sync: completed successfully" ); if let Some(cb) = on_complete.take() { let queue_len = queue_arc.lock().expect("MusicQueue mutex poisoned").len().unwrap_or(0); cb(queue_len); } } } if !sync_pending.load(SeqCst) { break; } match pending_items_fn() { Ok(new_items) => { current_items = new_items; current_on_ready = Some(carry_on_ready); } Err(e) => { tracing::warn!("queue-sync pending re-fetch error: {}", e); break; } } } } } impl QueueBackend for MusicQueue { // Primitives fn len(&self) -> Result { match &self.backend { MusicQueueBackend::Internal(q) => q.len(), MusicQueueBackend::OpenHome(q) => q.len(), } } fn track_ids(&self) -> Result, ControlPointError> { match &self.backend { MusicQueueBackend::Internal(q) => q.track_ids(), MusicQueueBackend::OpenHome(q) => q.track_ids(), } } fn id_to_position(&self, id: u32) -> Result { match &self.backend { MusicQueueBackend::Internal(q) => q.id_to_position(id), MusicQueueBackend::OpenHome(q) => q.id_to_position(id), } } fn position_to_id(&self, id: usize) -> Result { match &self.backend { MusicQueueBackend::Internal(q) => q.position_to_id(id), MusicQueueBackend::OpenHome(q) => q.position_to_id(id), } } fn current_track(&self) -> Result, ControlPointError> { match &self.backend { MusicQueueBackend::Internal(q) => q.current_track(), MusicQueueBackend::OpenHome(q) => q.current_track(), } } fn current_index(&self) -> Result, ControlPointError> { match &self.backend { MusicQueueBackend::Internal(q) => q.current_index(), MusicQueueBackend::OpenHome(q) => q.current_index(), } } fn queue_snapshot(&self) -> Result { match &self.backend { MusicQueueBackend::Internal(q) => q.queue_snapshot(), MusicQueueBackend::OpenHome(q) => q.queue_snapshot(), } } fn set_index(&mut self, index: Option) -> Result<(), ControlPointError> { match &mut self.backend { MusicQueueBackend::Internal(q) => q.set_index(index), MusicQueueBackend::OpenHome(q) => q.set_index(index), } } fn replace_queue( &mut self, items: Vec, current_index: Option, ) -> Result<(), ControlPointError> { match &mut self.backend { MusicQueueBackend::Internal(q) => q.replace_queue(items, current_index), MusicQueueBackend::OpenHome(q) => q.replace_queue(items, current_index), } } fn sync_queue( &mut self, items: Vec, cancel_token: &Arc, on_ready: Option>, ) -> Result<(), ControlPointError> { match &mut self.backend { MusicQueueBackend::Internal(q) => q.sync_queue(items, cancel_token, on_ready), MusicQueueBackend::OpenHome(q) => q.sync_queue(items, cancel_token, on_ready), } } fn get_item(&self, index: usize) -> Result, ControlPointError> { match &self.backend { MusicQueueBackend::Internal(q) => q.get_item(index), MusicQueueBackend::OpenHome(q) => q.get_item(index), } } fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> { match &mut self.backend { MusicQueueBackend::Internal(q) => q.replace_item(index, item), MusicQueueBackend::OpenHome(q) => q.replace_item(index, item), } } fn enqueue_items( &mut self, items: Vec, mode: EnqueueMode, ) -> Result<(), ControlPointError> { match &mut self.backend { MusicQueueBackend::Internal(q) => q.enqueue_items(items, mode), MusicQueueBackend::OpenHome(q) => q.enqueue_items(items, mode), } } // Optimized helpers fn clear_queue(&mut self) -> Result<(), ControlPointError> { match &mut self.backend { MusicQueueBackend::Internal(q) => q.clear_queue(), MusicQueueBackend::OpenHome(q) => q.clear_queue(), } } fn is_empty(&self) -> Result { match &self.backend { MusicQueueBackend::Internal(q) => q.is_empty(), MusicQueueBackend::OpenHome(q) => q.is_empty(), } } fn upcoming_len(&self) -> Result { match &self.backend { MusicQueueBackend::Internal(q) => q.upcoming_len(), MusicQueueBackend::OpenHome(q) => q.upcoming_len(), } } fn upcoming_items(&self) -> Result, ControlPointError> { match &self.backend { MusicQueueBackend::Internal(q) => q.upcoming_items(), MusicQueueBackend::OpenHome(q) => q.upcoming_items(), } } fn peek_current(&mut self) -> Result, ControlPointError> { match &mut self.backend { MusicQueueBackend::Internal(q) => q.peek_current(), MusicQueueBackend::OpenHome(q) => q.peek_current(), } } fn dequeue_next(&mut self) -> Result, ControlPointError> { match &mut self.backend { MusicQueueBackend::Internal(q) => q.dequeue_next(), MusicQueueBackend::OpenHome(q) => q.dequeue_next(), } } fn append_or_init_index(&mut self, items: Vec) -> Result<(), ControlPointError> { match &mut self.backend { MusicQueueBackend::Internal(q) => q.append_or_init_index(items), MusicQueueBackend::OpenHome(q) => q.append_or_init_index(items), } } } impl QueueFromRendererInfo for MusicQueue { fn from_renderer_info(renderer: &RendererInfo) -> Result { MusicQueue::from_renderer_info(renderer) } fn to_backend(self) -> MusicQueue { self } }