⬆️ version to v0.3.41 — async queue sync refactoring
- Add `SyncCancelled` error variant for non-fatal cancellations - Refactor queue sync to async via `MusicQueue::schedule_sync()` - Extract browse+conversion into internal helper - Update all `QueueBackend::sync_queue()` signatures to accept cancel token and on_ready callback - Implement early-start logic (on pivot preservation or first insert) - Add `QueueReadyToPlay` and ‘ QueueSyncCancelled➔ SSE events - Replace blocking `refresh_attached_queue_for()` with non-blocking async dispatch in control_point.rs - Bump version to v0.3.41
This commit is contained in:
@@ -3,84 +3,297 @@ 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)]
|
||||
pub enum MusicQueue {
|
||||
enum MusicQueueBackend {
|
||||
Internal(InternalQueue),
|
||||
OpenHome(OpenHomeQueue),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MusicQueue {
|
||||
backend: MusicQueueBackend,
|
||||
sync_in_progress: Arc<AtomicBool>,
|
||||
sync_pending: Arc<AtomicBool>,
|
||||
sync_cancel_token: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
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<MusicQueue, ControlPointError> {
|
||||
if info.capabilities().has_oh_playlist() {
|
||||
Ok(MusicQueue::OpenHome(OpenHomeQueue::from_renderer_info(
|
||||
info,
|
||||
)?))
|
||||
let backend = if info.capabilities().has_oh_playlist() {
|
||||
MusicQueueBackend::OpenHome(OpenHomeQueue::from_renderer_info(info)?)
|
||||
} else {
|
||||
Ok(MusicQueue::Internal(InternalQueue::from_renderer_info(
|
||||
info,
|
||||
)?))
|
||||
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<usize, ControlPointError> {
|
||||
match &self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.len(),
|
||||
MusicQueueBackend::OpenHome(q) => q.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn schedule_sync(
|
||||
queue_arc: &Arc<Mutex<MusicQueue>>,
|
||||
renderer_id: &str,
|
||||
items: Vec<PlaybackItem>,
|
||||
pending_items_fn: Box<
|
||||
dyn Fn() -> Result<Vec<PlaybackItem>, ControlPointError> + Send + 'static,
|
||||
>,
|
||||
on_ready: Option<Box<dyn FnOnce() + Send + 'static>>,
|
||||
on_complete: Box<dyn Fn(usize) + Send + 'static>,
|
||||
) -> SyncScheduleOutcome {
|
||||
let (sync_in_progress, sync_pending, sync_cancel_token) = {
|
||||
let q = queue_arc.lock().unwrap();
|
||||
(
|
||||
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 || {
|
||||
struct Guard(Arc<AtomicBool>);
|
||||
impl Drop for Guard {
|
||||
fn drop(&mut self) {
|
||||
self.0.store(false, SeqCst);
|
||||
}
|
||||
}
|
||||
let _guard = Guard(Arc::clone(&sync_in_progress));
|
||||
|
||||
let mut current_items = items;
|
||||
let mut current_on_ready = Some(on_ready);
|
||||
let mut on_complete = Some(on_complete);
|
||||
|
||||
tracing::debug!(thread = %std::thread::current().name().unwrap_or("?"), "queue-sync thread started");
|
||||
|
||||
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<Box<dyn FnOnce() + Send + 'static>> =
|
||||
real_on_ready.as_ref().map(|_| {
|
||||
let flag = Arc::clone(&on_ready_triggered);
|
||||
Box::new(move || {
|
||||
flag.store(true, SeqCst);
|
||||
}) as Box<dyn FnOnce() + Send + 'static>
|
||||
});
|
||||
|
||||
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().unwrap();
|
||||
<MusicQueue as QueueBackend>::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).
|
||||
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();
|
||||
}
|
||||
} else if real_on_ready.is_some() {
|
||||
tracing::debug!(
|
||||
thread = %std::thread::current().name().unwrap_or("?"),
|
||||
"queue-sync: on_ready not triggered (cancelled or skipped)"
|
||||
);
|
||||
}
|
||||
|
||||
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().unwrap().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(None);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("queue-sync pending re-fetch error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(thread = %std::thread::current().name().unwrap_or("?"), "queue-sync thread done");
|
||||
})
|
||||
.expect("Failed to spawn queue-sync thread");
|
||||
|
||||
SyncScheduleOutcome::Scheduled
|
||||
}
|
||||
}
|
||||
|
||||
impl QueueBackend for MusicQueue {
|
||||
// Primitives
|
||||
fn len(&self) -> Result<usize, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.len(),
|
||||
MusicQueue::OpenHome(q) => q.len(),
|
||||
match &self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.len(),
|
||||
MusicQueueBackend::OpenHome(q) => q.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.track_ids(),
|
||||
MusicQueue::OpenHome(q) => q.track_ids(),
|
||||
match &self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.track_ids(),
|
||||
MusicQueueBackend::OpenHome(q) => q.track_ids(),
|
||||
}
|
||||
}
|
||||
|
||||
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.id_to_position(id),
|
||||
MusicQueue::OpenHome(q) => q.id_to_position(id),
|
||||
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<u32, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.position_to_id(id),
|
||||
MusicQueue::OpenHome(q) => q.position_to_id(id),
|
||||
match &self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.position_to_id(id),
|
||||
MusicQueueBackend::OpenHome(q) => q.position_to_id(id),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.current_track(),
|
||||
MusicQueue::OpenHome(q) => q.current_track(),
|
||||
match &self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.current_track(),
|
||||
MusicQueueBackend::OpenHome(q) => q.current_track(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.current_index(),
|
||||
MusicQueue::OpenHome(q) => q.current_index(),
|
||||
match &self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.current_index(),
|
||||
MusicQueueBackend::OpenHome(q) => q.current_index(),
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.queue_snapshot(),
|
||||
MusicQueue::OpenHome(q) => q.queue_snapshot(),
|
||||
match &self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.queue_snapshot(),
|
||||
MusicQueueBackend::OpenHome(q) => q.queue_snapshot(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_index(&mut self, index: Option<usize>) -> Result<(), ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.set_index(index),
|
||||
MusicQueue::OpenHome(q) => q.set_index(index),
|
||||
match &mut self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.set_index(index),
|
||||
MusicQueueBackend::OpenHome(q) => q.set_index(index),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,30 +302,35 @@ impl QueueBackend for MusicQueue {
|
||||
items: Vec<PlaybackItem>,
|
||||
current_index: Option<usize>,
|
||||
) -> Result<(), ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.replace_queue(items, current_index),
|
||||
MusicQueue::OpenHome(q) => q.replace_queue(items, current_index),
|
||||
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<PlaybackItem>) -> Result<(), ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.sync_queue(items),
|
||||
MusicQueue::OpenHome(q) => q.sync_queue(items),
|
||||
fn sync_queue(
|
||||
&mut self,
|
||||
items: Vec<PlaybackItem>,
|
||||
cancel_token: &Arc<AtomicBool>,
|
||||
on_ready: Option<Box<dyn FnOnce() + Send>>,
|
||||
) -> 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<Option<PlaybackItem>, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.get_item(index),
|
||||
MusicQueue::OpenHome(q) => q.get_item(index),
|
||||
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 self {
|
||||
MusicQueue::Internal(q) => q.replace_item(index, item),
|
||||
MusicQueue::OpenHome(q) => q.replace_item(index, item),
|
||||
match &mut self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.replace_item(index, item),
|
||||
MusicQueueBackend::OpenHome(q) => q.replace_item(index, item),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,59 +339,59 @@ impl QueueBackend for MusicQueue {
|
||||
items: Vec<PlaybackItem>,
|
||||
mode: EnqueueMode,
|
||||
) -> Result<(), ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.enqueue_items(items, mode),
|
||||
MusicQueue::OpenHome(q) => q.enqueue_items(items, mode),
|
||||
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 self {
|
||||
MusicQueue::Internal(q) => q.clear_queue(),
|
||||
MusicQueue::OpenHome(q) => q.clear_queue(),
|
||||
match &mut self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.clear_queue(),
|
||||
MusicQueueBackend::OpenHome(q) => q.clear_queue(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> Result<bool, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.is_empty(),
|
||||
MusicQueue::OpenHome(q) => q.is_empty(),
|
||||
match &self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.is_empty(),
|
||||
MusicQueueBackend::OpenHome(q) => q.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn upcoming_len(&self) -> Result<usize, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.upcoming_len(),
|
||||
MusicQueue::OpenHome(q) => q.upcoming_len(),
|
||||
match &self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.upcoming_len(),
|
||||
MusicQueueBackend::OpenHome(q) => q.upcoming_len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn upcoming_items(&self) -> Result<Vec<PlaybackItem>, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.upcoming_items(),
|
||||
MusicQueue::OpenHome(q) => q.upcoming_items(),
|
||||
match &self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.upcoming_items(),
|
||||
MusicQueueBackend::OpenHome(q) => q.upcoming_items(),
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_current(&mut self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.peek_current(),
|
||||
MusicQueue::OpenHome(q) => q.peek_current(),
|
||||
match &mut self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.peek_current(),
|
||||
MusicQueueBackend::OpenHome(q) => q.peek_current(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dequeue_next(&mut self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.dequeue_next(),
|
||||
MusicQueue::OpenHome(q) => q.dequeue_next(),
|
||||
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<PlaybackItem>) -> Result<(), ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.append_or_init_index(items),
|
||||
MusicQueue::OpenHome(q) => q.append_or_init_index(items),
|
||||
match &mut self.backend {
|
||||
MusicQueueBackend::Internal(q) => q.append_or_init_index(items),
|
||||
MusicQueueBackend::OpenHome(q) => q.append_or_init_index(items),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user