fix(qconnect): materialize remote shuffle atomically
cx
This commit is contained in:
4 files changed
+104
-2
No files matched your search
@@ -228,6 +228,22 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace queue contents and playback order atomically.
|
||||||
|
pub async fn set_queue_with_order(
|
||||||
|
&self,
|
||||||
|
tracks: Vec<QueueTrack>,
|
||||||
|
start_index: Option<usize>,
|
||||||
|
shuffle_enabled: bool,
|
||||||
|
shuffle_order: Option<Vec<usize>>,
|
||||||
|
) {
|
||||||
|
let queue = self.queue.write().await;
|
||||||
|
queue.set_queue_with_order(tracks, start_index, shuffle_enabled, shuffle_order);
|
||||||
|
self.emit(CoreEvent::QueueUpdated {
|
||||||
|
state: queue.get_state(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove a track by index
|
/// Remove a track by index
|
||||||
pub async fn remove_track(&self, index: usize) -> Option<QueueTrack> {
|
pub async fn remove_track(&self, index: usize) -> Option<QueueTrack> {
|
||||||
let queue = self.queue.write().await;
|
let queue = self.queue.write().await;
|
||||||
|
|||||||
@@ -149,6 +149,56 @@ impl QueueManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace the queue and playback order in a single atomic update.
|
||||||
|
/// This avoids emitting an intermediate locally reshuffled state before an
|
||||||
|
/// authoritative remote shuffle order has been applied.
|
||||||
|
pub fn set_queue_with_order(
|
||||||
|
&self,
|
||||||
|
new_tracks: Vec<QueueTrack>,
|
||||||
|
start_index: Option<usize>,
|
||||||
|
shuffle_enabled: bool,
|
||||||
|
shuffle_order: Option<Vec<usize>>,
|
||||||
|
) {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.tracks = new_tracks;
|
||||||
|
state.current_index = start_index;
|
||||||
|
state.history.clear();
|
||||||
|
state.shuffle = shuffle_enabled;
|
||||||
|
|
||||||
|
if !shuffle_enabled {
|
||||||
|
state.shuffle_order.clear();
|
||||||
|
state.shuffle_position = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(order) =
|
||||||
|
shuffle_order.filter(|order| Self::is_valid_shuffle_order(order, state.tracks.len()))
|
||||||
|
{
|
||||||
|
state.shuffle_order = order;
|
||||||
|
if let Some(curr_idx) = state.current_index {
|
||||||
|
if let Some(pos) = state.shuffle_order.iter().position(|&idx| idx == curr_idx) {
|
||||||
|
state.shuffle_position = pos;
|
||||||
|
} else {
|
||||||
|
state.shuffle_position = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.shuffle_position = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::regenerate_shuffle_order_internal(&mut state);
|
||||||
|
|
||||||
|
if let Some(curr_idx) = state.current_index {
|
||||||
|
if let Some(pos) = state.shuffle_order.iter().position(|&idx| idx == curr_idx) {
|
||||||
|
if pos != 0 {
|
||||||
|
state.shuffle_order.swap(0, pos);
|
||||||
|
}
|
||||||
|
state.shuffle_position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear the queue
|
/// Clear the queue
|
||||||
pub fn clear(&self) {
|
pub fn clear(&self) {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
@@ -1096,4 +1146,23 @@ mod tests {
|
|||||||
assert_eq!(state.shuffle_order.len(), 4);
|
assert_eq!(state.shuffle_order.len(), 4);
|
||||||
assert_eq!(state.shuffle_order[0], 1);
|
assert_eq!(state.shuffle_order[0], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_queue_with_order_applies_authoritative_shuffle_before_snapshot() {
|
||||||
|
let queue = QueueManager::new();
|
||||||
|
let tracks = (1..=5).map(create_test_track).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
queue.set_queue_with_order(tracks, Some(0), true, Some(vec![0, 3, 1, 4, 2]));
|
||||||
|
|
||||||
|
let state = queue.get_state();
|
||||||
|
assert!(state.shuffle);
|
||||||
|
assert_eq!(
|
||||||
|
state
|
||||||
|
.upcoming
|
||||||
|
.iter()
|
||||||
|
.map(|track| track.id)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![4, 2, 5, 3]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +169,19 @@ impl CoreBridge {
|
|||||||
self.core.set_queue(tracks, start_index).await
|
self.core.set_queue(tracks, start_index).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the entire queue and playback order atomically.
|
||||||
|
pub async fn set_queue_with_order(
|
||||||
|
&self,
|
||||||
|
tracks: Vec<QueueTrack>,
|
||||||
|
start_index: Option<usize>,
|
||||||
|
shuffle_enabled: bool,
|
||||||
|
shuffle_order: Option<Vec<usize>>,
|
||||||
|
) {
|
||||||
|
self.core
|
||||||
|
.set_queue_with_order(tracks, start_index, shuffle_enabled, shuffle_order)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove a track by index
|
/// Remove a track by index
|
||||||
pub async fn remove_track(&self, index: usize) -> Option<QueueTrack> {
|
pub async fn remove_track(&self, index: usize) -> Option<QueueTrack> {
|
||||||
self.core.remove_track(index).await
|
self.core.remove_track(index).await
|
||||||
|
|||||||
@@ -2540,9 +2540,13 @@ async fn materialize_remote_queue_to_corebridge(
|
|||||||
start_index,
|
start_index,
|
||||||
current_playback_track_id
|
current_playback_track_id
|
||||||
);
|
);
|
||||||
bridge.set_queue(queue_tracks, start_index).await;
|
|
||||||
bridge
|
bridge
|
||||||
.set_shuffle_with_order(queue_state.shuffle_mode, queue_state.shuffle_order.clone())
|
.set_queue_with_order(
|
||||||
|
queue_tracks,
|
||||||
|
start_index,
|
||||||
|
queue_state.shuffle_mode,
|
||||||
|
queue_state.shuffle_order.clone(),
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let local_track_missing_from_remote = current_playback_track_id
|
let local_track_missing_from_remote = current_playback_track_id
|
||||||
|
|||||||
Reference in new issue
Block a user