Skip to content

Commit eff5ca3

Browse files
authored
Adjust: Allow repeat in combination with shuffle (#1561)
* fix: incorrect autoplay resolver behavior when shuffling * refactor: store the initial track in the remote context * adjust: shuffle repeat interaction * chore: update .gitignore * chore: rename internal error * adjust: shuffle behavior to ensure consistency * fix: prefer repeat context over autoplay * chore: update changelog * chore: reduce complexity of shuffle * chore: test shuffle with first
1 parent 882ed7c commit eff5ca3

File tree

13 files changed

+193
-65
lines changed

13 files changed

+193
-65
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
target
22
.cargo
33
spotify_appkey.key
4+
.idea/
45
.vagrant/
56
.project
67
.history

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Changed
1313

14+
- [connect] Shuffling was adjusted, so that shuffle and repeat can be used combined
15+
1416
### Deprecated
1517

1618
### Removed
1719

1820
### Fixed
1921

22+
- [connect] Repeat context will not go into autoplay anymore and triggering autoplay while shuffling shouldn't reshuffle anymore
2023
- [connect] Only deletes the connect state on dealer shutdown instead on disconnecting
21-
- [core] Fixed a problem where in `spclient` where a http 411 error was thrown because the header were set wrong
24+
- [core] Fixed a problem where in `spclient` where an http 411 error was thrown because the header were set wrong
2225
- [main] Use the config instead of the type default for values that are not provided by the user
2326

2427

connect/src/context_resolver.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,8 @@ impl ContextResolver {
318318
let active_ctx = state.get_context(state.active_context);
319319
let res = if let Some(transfer_state) = transfer_state.take() {
320320
state.finish_transfer(transfer_state)
321-
} else if state.shuffling_context() {
322-
state.shuffle(None)
321+
} else if state.shuffling_context() && next.update == ContextType::Default {
322+
state.shuffle_new()
323323
} else if matches!(active_ctx, Ok(ctx) if ctx.index.track == 0) {
324324
// has context, and context is not touched
325325
// when the index is not zero, the next index was already evaluated elsewhere

connect/src/shuffle_vec.rs

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ use std::{
88
pub struct ShuffleVec<T> {
99
vec: Vec<T>,
1010
indices: Option<Vec<usize>>,
11+
/// This is primarily necessary to ensure that shuffle does not behave out of place.
12+
///
13+
/// For that reason we swap the first track with the currently playing track. By that we ensure
14+
/// that the shuffle state is consistent between resets of the state because the first track is
15+
/// always the track with which we started playing when switching to shuffle.
16+
original_first_position: Option<usize>,
1117
}
1218

1319
impl<T: PartialEq> PartialEq for ShuffleVec<T> {
@@ -41,16 +47,25 @@ impl<T> IntoIterator for ShuffleVec<T> {
4147

4248
impl<T> From<Vec<T>> for ShuffleVec<T> {
4349
fn from(vec: Vec<T>) -> Self {
44-
Self { vec, indices: None }
50+
Self {
51+
vec,
52+
original_first_position: None,
53+
indices: None,
54+
}
4555
}
4656
}
4757

4858
impl<T> ShuffleVec<T> {
49-
pub fn shuffle_with_seed(&mut self, seed: u64) {
50-
self.shuffle_with_rng(SmallRng::seed_from_u64(seed))
59+
pub fn shuffle_with_seed<F: Fn(&T) -> bool>(&mut self, seed: u64, is_first: F) {
60+
self.shuffle_with_rng(SmallRng::seed_from_u64(seed), is_first)
5161
}
5262

53-
pub fn shuffle_with_rng(&mut self, mut rng: impl Rng) {
63+
pub fn shuffle_with_rng<F: Fn(&T) -> bool>(&mut self, mut rng: impl Rng, is_first: F) {
64+
if self.vec.len() <= 1 {
65+
info!("skipped shuffling for less or equal one item");
66+
return;
67+
}
68+
5469
if self.indices.is_some() {
5570
self.unshuffle()
5671
}
@@ -66,7 +81,12 @@ impl<T> ShuffleVec<T> {
6681
self.vec.swap(i, rnd_ind);
6782
}
6883

69-
self.indices = Some(indices)
84+
self.indices = Some(indices);
85+
86+
self.original_first_position = self.vec.iter().position(is_first);
87+
if let Some(first_pos) = self.original_first_position {
88+
self.vec.swap(0, first_pos)
89+
}
7090
}
7191

7292
pub fn unshuffle(&mut self) {
@@ -75,9 +95,16 @@ impl<T> ShuffleVec<T> {
7595
None => return,
7696
};
7797

98+
if let Some(first_pos) = self.original_first_position {
99+
self.vec.swap(0, first_pos);
100+
self.original_first_position = None;
101+
}
102+
78103
for i in 1..self.vec.len() {
79-
let n = indices[self.vec.len() - i - 1];
80-
self.vec.swap(n, i);
104+
match indices.get(self.vec.len() - i - 1) {
105+
None => return,
106+
Some(n) => self.vec.swap(*n, i),
107+
}
81108
}
82109
}
83110
}
@@ -86,25 +113,86 @@ impl<T> ShuffleVec<T> {
86113
mod test {
87114
use super::*;
88115
use rand::Rng;
116+
use std::ops::Range;
89117

90-
#[test]
91-
fn test_shuffle_with_seed() {
92-
let seed = rand::rng().random_range(0..10000000000000);
118+
fn base(range: Range<usize>) -> (ShuffleVec<usize>, u64) {
119+
let seed = rand::rng().random_range(0..10_000_000_000_000);
93120

94-
let vec = (0..100).collect::<Vec<_>>();
95-
let base_vec: ShuffleVec<i32> = vec.into();
121+
let vec = range.collect::<Vec<_>>();
122+
(vec.into(), seed)
123+
}
124+
125+
#[test]
126+
fn test_shuffle_without_first() {
127+
let (base_vec, seed) = base(0..100);
96128

97129
let mut shuffled_vec = base_vec.clone();
98-
shuffled_vec.shuffle_with_seed(seed);
130+
shuffled_vec.shuffle_with_seed(seed, |_| false);
99131

100132
let mut different_shuffled_vec = base_vec.clone();
101-
different_shuffled_vec.shuffle_with_seed(seed);
133+
different_shuffled_vec.shuffle_with_seed(seed, |_| false);
102134

103-
assert_eq!(shuffled_vec, different_shuffled_vec);
135+
assert_eq!(
136+
shuffled_vec, different_shuffled_vec,
137+
"shuffling with the same seed has the same result"
138+
);
104139

105140
let mut unshuffled_vec = shuffled_vec.clone();
106141
unshuffled_vec.unshuffle();
107142

108-
assert_eq!(base_vec, unshuffled_vec);
143+
assert_eq!(
144+
base_vec, unshuffled_vec,
145+
"unshuffle restores the original state"
146+
);
147+
}
148+
149+
#[test]
150+
fn test_shuffle_with_first() {
151+
const MAX_RANGE: usize = 200;
152+
153+
let (base_vec, seed) = base(0..MAX_RANGE);
154+
let rand_first = rand::rng().random_range(0..MAX_RANGE);
155+
156+
let mut shuffled_with_first = base_vec.clone();
157+
shuffled_with_first.shuffle_with_seed(seed, |i| i == &rand_first);
158+
159+
assert_eq!(
160+
Some(&rand_first),
161+
shuffled_with_first.first(),
162+
"after shuffling the first is expected to be the given item"
163+
);
164+
165+
let mut shuffled_without_first = base_vec.clone();
166+
shuffled_without_first.shuffle_with_seed(seed, |_| false);
167+
168+
let mut switched_positions = Vec::with_capacity(2);
169+
for (i, without_first_value) in shuffled_without_first.iter().enumerate() {
170+
if without_first_value != &shuffled_with_first[i] {
171+
switched_positions.push(i);
172+
} else {
173+
assert_eq!(
174+
without_first_value, &shuffled_with_first[i],
175+
"shuffling with the same seed has the same result"
176+
);
177+
}
178+
}
179+
180+
assert_eq!(
181+
switched_positions.len(),
182+
2,
183+
"only the switched positions should be different"
184+
);
185+
186+
assert_eq!(
187+
shuffled_with_first[switched_positions[0]],
188+
shuffled_without_first[switched_positions[1]],
189+
"the switched values should be equal"
190+
);
191+
192+
assert_eq!(
193+
shuffled_with_first[switched_positions[1]],
194+
shuffled_without_first[switched_positions[0]],
195+
"the switched values should be equal"
196+
)
109197
}
110198
}

connect/src/spirc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1287,7 +1287,7 @@ impl SpircTask {
12871287
if self.context_resolver.has_next() {
12881288
self.connect_state.update_queue_revision()
12891289
} else {
1290-
self.connect_state.shuffle(None)?;
1290+
self.connect_state.shuffle_new()?;
12911291
self.add_autoplay_resolving_when_required();
12921292
}
12931293
} else {

connect/src/state.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::{
2323
},
2424
state::{
2525
context::{ContextType, ResetContext, StateContext},
26+
options::ShuffleState,
2627
provider::{IsProvider, Provider},
2728
},
2829
};
@@ -55,7 +56,7 @@ pub(super) enum StateError {
5556
#[error("the provided context has no tracks")]
5657
ContextHasNoTracks,
5758
#[error("playback of local files is not supported")]
58-
UnsupportedLocalPlayBack,
59+
UnsupportedLocalPlayback,
5960
#[error("track uri <{0:?}> contains invalid characters")]
6061
InvalidTrackUri(Option<String>),
6162
}
@@ -69,7 +70,7 @@ impl From<StateError> for Error {
6970
| CanNotFindTrackInContext(_, _)
7071
| ContextHasNoTracks
7172
| InvalidTrackUri(_) => Error::failed_precondition(err),
72-
CurrentlyDisallowed { .. } | UnsupportedLocalPlayBack => Error::unavailable(err),
73+
CurrentlyDisallowed { .. } | UnsupportedLocalPlayback => Error::unavailable(err),
7374
}
7475
}
7576
}
@@ -123,7 +124,7 @@ pub(super) struct ConnectState {
123124
/// the context from which we play, is used to top up prev and next tracks
124125
context: Option<StateContext>,
125126
/// seed extracted in [ConnectState::handle_initial_transfer] and used in [ConnectState::finish_transfer]
126-
transfer_shuffle_seed: Option<u64>,
127+
transfer_shuffle: Option<ShuffleState>,
127128

128129
/// a context to keep track of the autoplay context
129130
autoplay_context: Option<StateContext>,
@@ -395,7 +396,7 @@ impl ConnectState {
395396
self.update_context_index(self.active_context, new_index + 1)?;
396397
self.fill_up_context = self.active_context;
397398

398-
if !self.current_track(|t| t.is_queue()) {
399+
if !self.current_track(|t| t.is_queue() || self.is_skip_track(t, None)) {
399400
self.set_current_track(new_index)?;
400401
}
401402

connect/src/state/context.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ const SEARCH_IDENTIFIER: &str = "spotify:search";
2424
#[derive(Debug)]
2525
pub struct StateContext {
2626
pub tracks: ShuffleVec<ProvidedTrack>,
27-
pub skip_track: Option<ProvidedTrack>,
2827
pub metadata: HashMap<String, String>,
2928
pub restrictions: Option<Restrictions>,
3029
/// is used to keep track which tracks are already loaded into the next_tracks
@@ -108,6 +107,7 @@ impl ConnectState {
108107

109108
if let Ok(ctx) = self.get_context_mut(ContextType::Default) {
110109
ctx.remove_shuffle_seed();
110+
ctx.remove_initial_track();
111111
ctx.tracks.unshuffle()
112112
}
113113

@@ -194,7 +194,7 @@ impl ConnectState {
194194
error!("context didn't have any tracks: {context:#?}");
195195
Err(StateError::ContextHasNoTracks)?;
196196
} else if matches!(context.uri, Some(ref uri) if uri.starts_with(LOCAL_FILES_IDENTIFIER)) {
197-
Err(StateError::UnsupportedLocalPlayBack)?;
197+
Err(StateError::UnsupportedLocalPlayback)?;
198198
}
199199

200200
let mut next_contexts = Vec::new();
@@ -377,18 +377,23 @@ impl ConnectState {
377377

378378
StateContext {
379379
tracks: tracks.into(),
380-
skip_track: None,
381380
restrictions,
382381
metadata,
383382
index: ContextIndex::new(),
384383
}
385384
}
386385

387-
pub fn is_skip_track(&self, track: &ProvidedTrack) -> bool {
388-
self.get_context(self.active_context)
389-
.ok()
390-
.and_then(|t| t.skip_track.as_ref().map(|t| t.uri == track.uri))
391-
.unwrap_or(false)
386+
pub fn is_skip_track(&self, track: &ProvidedTrack, iteration: Option<u32>) -> bool {
387+
let ctx = match self.get_context(self.active_context).ok() {
388+
None => return false,
389+
Some(ctx) => ctx,
390+
};
391+
392+
if ctx.get_initial_track().is_none_or(|uri| uri != &track.uri) {
393+
return false;
394+
}
395+
396+
iteration.is_none_or(|i| i == 0)
392397
}
393398

394399
pub fn merge_context(&mut self, new_page: Option<ContextPage>) -> Option<()> {

connect/src/state/handle.rs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ impl ConnectState {
1313
self.set_shuffle(shuffle);
1414

1515
if shuffle {
16-
return self.shuffle(None);
16+
return self.shuffle_new();
1717
}
1818

1919
self.reset_context(ResetContext::DefaultIndex);
@@ -44,17 +44,14 @@ impl ConnectState {
4444
self.set_repeat_context(repeat);
4545

4646
if repeat {
47-
self.set_shuffle(false);
48-
self.reset_context(ResetContext::DefaultIndex);
49-
50-
let ctx = self.get_context(ContextType::Default)?;
51-
let current_track = ConnectState::find_index_in_context(ctx, |t| {
52-
self.current_track(|t| &t.uri) == &t.uri
53-
})?;
54-
self.reset_playback_to_position(Some(current_track))
55-
} else {
56-
self.update_restrictions();
57-
Ok(())
47+
if let ContextType::Autoplay = self.fill_up_context {
48+
self.fill_up_context = ContextType::Default;
49+
}
5850
}
51+
52+
let ctx = self.get_context(ContextType::Default)?;
53+
let current_track =
54+
ConnectState::find_index_in_context(ctx, |t| self.current_track(|t| &t.uri) == &t.uri)?;
55+
self.reset_playback_to_position(Some(current_track))
5956
}
6057
}

connect/src/state/metadata.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const ITERATION: &str = "iteration";
1414

1515
const CUSTOM_CONTEXT_INDEX: &str = "context_index";
1616
const CUSTOM_SHUFFLE_SEED: &str = "shuffle_seed";
17+
const CUSTOM_INITIAL_TRACK: &str = "initial_track";
1718

1819
macro_rules! metadata_entry {
1920
( $get:ident, $set:ident, $clear:ident ($key:ident: $entry:ident)) => {
@@ -63,6 +64,7 @@ pub(super) trait Metadata {
6364
metadata_entry!(get_entity_uri, set_entity_uri, remove_entity_uri (entity_uri: ENTITY_URI));
6465
metadata_entry!(get_iteration, set_iteration, remove_iteration (iteration: ITERATION));
6566
metadata_entry!(get_shuffle_seed, set_shuffle_seed, remove_shuffle_seed (shuffle_seed: CUSTOM_SHUFFLE_SEED));
67+
metadata_entry!(get_initial_track, set_initial_track, remove_initial_track (initial_track: CUSTOM_INITIAL_TRACK));
6668
}
6769

6870
macro_rules! impl_metadata {

0 commit comments

Comments
 (0)