Skip to content

Commit 34762f2

Browse files
photovoltexCopilot
andauthored
Shuffle tracks in place (#1445)
* connect: add shuffle_vec.rs * connect: shuffle in place * add shuffle with seed option * reduce complexity to add new metadata fields * add context index to metadata * use seed for shuffle When losing the connection and restarting the dealer, the seed is now stored in the context metadata. So on transfer we can pickup the seed again and shuffle the context as it was previously * add log for shuffle seed * connect: use small_rng, derive Default * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 471735a commit 34762f2

12 files changed

Lines changed: 315 additions & 171 deletions

File tree

connect/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ edition = "2021"
1212
futures-util = "0.3"
1313
log = "0.4"
1414
protobuf = "3.5"
15-
rand = "0.8"
15+
rand = { version = "0.8", default-features = false, features = ["small_rng"] }
1616
serde_json = "1.0"
1717
thiserror = "2.0"
1818
tokio = { version = "1", features = ["macros", "parking_lot", "sync"] }

connect/src/context_resolver.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@ use crate::{
44
autoplay_context_request::AutoplayContextRequest, context::Context,
55
transfer_state::TransferState,
66
},
7-
state::{
8-
context::{ContextType, UpdateContext},
9-
ConnectState,
10-
},
7+
state::{context::ContextType, ConnectState},
118
};
12-
use std::cmp::PartialEq;
139
use std::{
10+
cmp::PartialEq,
1411
collections::{HashMap, VecDeque},
1512
fmt::{Display, Formatter},
1613
hash::Hash,
@@ -35,7 +32,7 @@ pub(super) enum ContextAction {
3532
pub(super) struct ResolveContext {
3633
resolve: Resolve,
3734
fallback: Option<String>,
38-
update: UpdateContext,
35+
update: ContextType,
3936
action: ContextAction,
4037
}
4138

@@ -44,15 +41,15 @@ impl ResolveContext {
4441
Self {
4542
resolve: Resolve::Uri(uri.into()),
4643
fallback: None,
47-
update: UpdateContext::Default,
44+
update: ContextType::Default,
4845
action: ContextAction::Append,
4946
}
5047
}
5148

5249
pub fn from_uri(
5350
uri: impl Into<String>,
5451
fallback: impl Into<String>,
55-
update: UpdateContext,
52+
update: ContextType,
5653
action: ContextAction,
5754
) -> Self {
5855
let fallback_uri = fallback.into();
@@ -64,7 +61,7 @@ impl ResolveContext {
6461
}
6562
}
6663

67-
pub fn from_context(context: Context, update: UpdateContext, action: ContextAction) -> Self {
64+
pub fn from_context(context: Context, update: ContextType, action: ContextAction) -> Self {
6865
Self {
6966
resolve: Resolve::Context(context),
7067
fallback: None,
@@ -214,7 +211,7 @@ impl ContextResolver {
214211
let (next, resolve_uri, _) = self.find_next().ok_or(ContextResolverError::NoNext)?;
215212

216213
match next.update {
217-
UpdateContext::Default => {
214+
ContextType::Default => {
218215
let mut ctx = self.session.spclient().get_context(resolve_uri).await;
219216
if let Ok(ctx) = ctx.as_mut() {
220217
ctx.uri = Some(next.context_uri().to_string());
@@ -223,7 +220,7 @@ impl ContextResolver {
223220

224221
ctx
225222
}
226-
UpdateContext::Autoplay => {
223+
ContextType::Autoplay => {
227224
if resolve_uri.contains("spotify:show:") || resolve_uri.contains("spotify:episode:")
228225
{
229226
// autoplay is not supported for podcasts
@@ -304,13 +301,13 @@ impl ContextResolver {
304301
}
305302

306303
match (next.update, state.active_context) {
307-
(UpdateContext::Default, ContextType::Default) | (UpdateContext::Autoplay, _) => {
304+
(ContextType::Default, ContextType::Default) | (ContextType::Autoplay, _) => {
308305
debug!(
309306
"last item of type <{:?}>, finishing state setup",
310307
next.update
311308
);
312309
}
313-
(UpdateContext::Default, _) => {
310+
(ContextType::Default, _) => {
314311
debug!("skipped finishing default, because it isn't the active context");
315312
return false;
316313
}
@@ -320,7 +317,7 @@ impl ContextResolver {
320317
let res = if let Some(transfer_state) = transfer_state.take() {
321318
state.finish_transfer(transfer_state)
322319
} else if state.shuffling_context() {
323-
state.shuffle()
320+
state.shuffle(None)
324321
} else if matches!(active_ctx, Ok(ctx) if ctx.index.track == 0) {
325322
// has context, and context is not touched
326323
// when the index is not zero, the next index was already evaluated elsewhere

connect/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ use librespot_protocol as protocol;
77

88
mod context_resolver;
99
mod model;
10+
pub mod shuffle_vec;
1011
pub mod spirc;
1112
pub mod state;

connect/src/shuffle_vec.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use rand::{rngs::SmallRng, Rng, SeedableRng};
2+
use std::{
3+
ops::{Deref, DerefMut},
4+
vec::IntoIter,
5+
};
6+
7+
#[derive(Debug, Clone, Default)]
8+
pub struct ShuffleVec<T> {
9+
vec: Vec<T>,
10+
indices: Option<Vec<usize>>,
11+
}
12+
13+
impl<T: PartialEq> PartialEq for ShuffleVec<T> {
14+
fn eq(&self, other: &Self) -> bool {
15+
self.vec == other.vec
16+
}
17+
}
18+
19+
impl<T> Deref for ShuffleVec<T> {
20+
type Target = Vec<T>;
21+
22+
fn deref(&self) -> &Self::Target {
23+
&self.vec
24+
}
25+
}
26+
27+
impl<T> DerefMut for ShuffleVec<T> {
28+
fn deref_mut(&mut self) -> &mut Self::Target {
29+
self.vec.as_mut()
30+
}
31+
}
32+
33+
impl<T> IntoIterator for ShuffleVec<T> {
34+
type Item = T;
35+
type IntoIter = IntoIter<T>;
36+
37+
fn into_iter(self) -> Self::IntoIter {
38+
self.vec.into_iter()
39+
}
40+
}
41+
42+
impl<T> From<Vec<T>> for ShuffleVec<T> {
43+
fn from(vec: Vec<T>) -> Self {
44+
Self { vec, indices: None }
45+
}
46+
}
47+
48+
impl<T> ShuffleVec<T> {
49+
pub fn new() -> Self {
50+
Self {
51+
vec: Vec::new(),
52+
indices: None,
53+
}
54+
}
55+
56+
pub fn shuffle_with_seed(&mut self, seed: u64) {
57+
self.shuffle_with_rng(SmallRng::seed_from_u64(seed))
58+
}
59+
60+
pub fn shuffle_with_rng(&mut self, mut rng: impl Rng) {
61+
if self.indices.is_some() {
62+
self.unshuffle()
63+
}
64+
65+
let indices = {
66+
(1..self.vec.len())
67+
.rev()
68+
.map(|i| rng.gen_range(0..i + 1))
69+
.collect()
70+
};
71+
72+
for (i, &rnd_ind) in (1..self.vec.len()).rev().zip(&indices) {
73+
self.vec.swap(i, rnd_ind);
74+
}
75+
76+
self.indices = Some(indices)
77+
}
78+
79+
pub fn unshuffle(&mut self) {
80+
let indices = match self.indices.take() {
81+
Some(indices) => indices,
82+
None => return,
83+
};
84+
85+
for i in 1..self.vec.len() {
86+
let n = indices[self.vec.len() - i - 1];
87+
self.vec.swap(n, i);
88+
}
89+
}
90+
}
91+
92+
#[cfg(test)]
93+
mod test {
94+
use super::*;
95+
use rand::Rng;
96+
97+
#[test]
98+
fn test_shuffle_with_seed() {
99+
let seed = rand::thread_rng().gen_range(0..10000000000000);
100+
101+
let vec = (0..100).collect::<Vec<_>>();
102+
let base_vec: ShuffleVec<i32> = vec.into();
103+
104+
let mut shuffled_vec = base_vec.clone();
105+
shuffled_vec.shuffle_with_seed(seed);
106+
107+
let mut different_shuffled_vec = base_vec.clone();
108+
different_shuffled_vec.shuffle_with_seed(seed);
109+
110+
assert_eq!(shuffled_vec, different_shuffled_vec);
111+
112+
let mut unshuffled_vec = shuffled_vec.clone();
113+
unshuffled_vec.unshuffle();
114+
115+
assert_eq!(base_vec, unshuffled_vec);
116+
}
117+
}

connect/src/spirc.rs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ use crate::{
2525
user_attributes::UserAttributesMutation,
2626
},
2727
state::{
28-
context::{
29-
ResetContext, {ContextType, UpdateContext},
30-
},
28+
context::{ContextType, ResetContext},
3129
metadata::Metadata,
3230
provider::IsProvider,
3331
{ConnectState, ConnectStateConfig},
@@ -37,7 +35,6 @@ use futures_util::StreamExt;
3735
use protobuf::MessageField;
3836
use std::{
3937
future::Future,
40-
ops::Deref,
4138
sync::atomic::{AtomicUsize, Ordering},
4239
sync::Arc,
4340
time::{Duration, SystemTime, UNIX_EPOCH},
@@ -749,9 +746,6 @@ impl SpircTask {
749746

750747
use protobuf::Message;
751748

752-
// todo: handle received pages from transfer, important to not always shuffle the first 10 tracks
753-
// also important when the dealer is restarted, currently we just shuffle again, but at least
754-
// the 10 tracks provided should be used and after that the new shuffle context
755749
match TransferState::parse_from_bytes(&cluster.transfer_data) {
756750
Ok(transfer_state) => self.handle_transfer(transfer_state)?,
757751
Err(why) => error!("failed to take over control: {why}"),
@@ -889,7 +883,7 @@ impl SpircTask {
889883
} else {
890884
self.context_resolver.add(ResolveContext::from_context(
891885
update_context.context,
892-
super::state::context::UpdateContext::Default,
886+
ContextType::Default,
893887
ContextAction::Replace,
894888
))
895889
}
@@ -1007,7 +1001,7 @@ impl SpircTask {
10071001
self.context_resolver.add(ResolveContext::from_uri(
10081002
ctx_uri.clone(),
10091003
&fallback,
1010-
UpdateContext::Default,
1004+
ContextType::Default,
10111005
ContextAction::Replace,
10121006
));
10131007

@@ -1044,7 +1038,7 @@ impl SpircTask {
10441038
self.context_resolver.add(ResolveContext::from_uri(
10451039
ctx_uri,
10461040
fallback,
1047-
UpdateContext::Autoplay,
1041+
ContextType::Autoplay,
10481042
ContextAction::Replace,
10491043
))
10501044
}
@@ -1139,13 +1133,12 @@ impl SpircTask {
11391133
};
11401134

11411135
let update_context = if cmd.autoplay {
1142-
UpdateContext::Autoplay
1136+
ContextType::Autoplay
11431137
} else {
1144-
UpdateContext::Default
1138+
ContextType::Default
11451139
};
11461140

1147-
self.connect_state
1148-
.set_active_context(*update_context.deref());
1141+
self.connect_state.set_active_context(update_context);
11491142

11501143
let current_context_uri = self.connect_state.context_uri();
11511144
if current_context_uri == &cmd.context_uri && fallback == cmd.context_uri {
@@ -1209,7 +1202,7 @@ impl SpircTask {
12091202
if self.context_resolver.has_next() {
12101203
self.connect_state.update_queue_revision()
12111204
} else {
1212-
self.connect_state.shuffle()?;
1205+
self.connect_state.shuffle(None)?;
12131206
self.add_autoplay_resolving_when_required();
12141207
}
12151208
} else {
@@ -1366,7 +1359,7 @@ impl SpircTask {
13661359
let resolve = ResolveContext::from_uri(
13671360
current_context,
13681361
fallback,
1369-
UpdateContext::Autoplay,
1362+
ContextType::Autoplay,
13701363
if has_tracks {
13711364
ContextAction::Append
13721365
} else {
@@ -1458,7 +1451,7 @@ impl SpircTask {
14581451
self.context_resolver.add(ResolveContext::from_uri(
14591452
uri,
14601453
self.connect_state.current_track(|t| &t.uri),
1461-
UpdateContext::Default,
1454+
ContextType::Default,
14621455
ContextAction::Replace,
14631456
));
14641457

connect/src/state.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ mod restrictions;
77
mod tracks;
88
mod transfer;
99

10-
use crate::model::SpircPlayStatus;
1110
use crate::{
1211
core::{
1312
config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult,
1413
version, Error, Session,
1514
},
15+
model::SpircPlayStatus,
1616
protocol::{
1717
connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest},
1818
media::AudioQuality,
@@ -26,7 +26,6 @@ use crate::{
2626
provider::{IsProvider, Provider},
2727
},
2828
};
29-
3029
use log::LevelFilter;
3130
use protobuf::{EnumOrUnknown, MessageField};
3231
use std::{
@@ -118,10 +117,9 @@ pub struct ConnectState {
118117

119118
/// the context from which we play, is used to top up prev and next tracks
120119
context: Option<StateContext>,
120+
/// seed extracted in [ConnectState::handle_initial_transfer] and used in [ConnectState::finish_transfer]
121+
transfer_shuffle_seed: Option<u64>,
121122

122-
/// a context to keep track of our shuffled context,
123-
/// should be only available when `player.option.shuffling_context` is true
124-
shuffle_context: Option<StateContext>,
125123
/// a context to keep track of the autoplay context
126124
autoplay_context: Option<StateContext>,
127125
}

0 commit comments

Comments
 (0)