From 34762f227465281a636e962362f7982ca54f5764 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sun, 2 Feb 2025 22:58:30 +0100 Subject: [PATCH] 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 <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- connect/Cargo.toml | 2 +- connect/src/context_resolver.rs | 25 +++---- connect/src/lib.rs | 1 + connect/src/shuffle_vec.rs | 117 ++++++++++++++++++++++++++++++++ connect/src/spirc.rs | 27 +++----- connect/src/state.rs | 8 +-- connect/src/state/context.rs | 93 +++++++++++++------------ connect/src/state/handle.rs | 18 +++-- connect/src/state/metadata.rs | 114 +++++++++++++++---------------- connect/src/state/options.rs | 39 ++++++----- connect/src/state/tracks.rs | 17 +++-- connect/src/state/transfer.rs | 23 ++++++- 12 files changed, 314 insertions(+), 170 deletions(-) create mode 100644 connect/src/shuffle_vec.rs diff --git a/connect/Cargo.toml b/connect/Cargo.toml index ee076c3e..74b21b7d 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" futures-util = "0.3" log = "0.4" protobuf = "3.5" -rand = "0.8" +rand = { version = "0.8", default-features = false, features = ["small_rng"] } serde_json = "1.0" thiserror = "2.0" tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } diff --git a/connect/src/context_resolver.rs b/connect/src/context_resolver.rs index 278fc089..79d48973 100644 --- a/connect/src/context_resolver.rs +++ b/connect/src/context_resolver.rs @@ -4,13 +4,10 @@ use crate::{ autoplay_context_request::AutoplayContextRequest, context::Context, transfer_state::TransferState, }, - state::{ - context::{ContextType, UpdateContext}, - ConnectState, - }, + state::{context::ContextType, ConnectState}, }; -use std::cmp::PartialEq; use std::{ + cmp::PartialEq, collections::{HashMap, VecDeque}, fmt::{Display, Formatter}, hash::Hash, @@ -35,7 +32,7 @@ pub(super) enum ContextAction { pub(super) struct ResolveContext { resolve: Resolve, fallback: Option, - update: UpdateContext, + update: ContextType, action: ContextAction, } @@ -44,7 +41,7 @@ impl ResolveContext { Self { resolve: Resolve::Uri(uri.into()), fallback: None, - update: UpdateContext::Default, + update: ContextType::Default, action: ContextAction::Append, } } @@ -52,7 +49,7 @@ impl ResolveContext { pub fn from_uri( uri: impl Into, fallback: impl Into, - update: UpdateContext, + update: ContextType, action: ContextAction, ) -> Self { let fallback_uri = fallback.into(); @@ -64,7 +61,7 @@ impl ResolveContext { } } - pub fn from_context(context: Context, update: UpdateContext, action: ContextAction) -> Self { + pub fn from_context(context: Context, update: ContextType, action: ContextAction) -> Self { Self { resolve: Resolve::Context(context), fallback: None, @@ -214,7 +211,7 @@ impl ContextResolver { let (next, resolve_uri, _) = self.find_next().ok_or(ContextResolverError::NoNext)?; match next.update { - UpdateContext::Default => { + ContextType::Default => { let mut ctx = self.session.spclient().get_context(resolve_uri).await; if let Ok(ctx) = ctx.as_mut() { ctx.uri = Some(next.context_uri().to_string()); @@ -223,7 +220,7 @@ impl ContextResolver { ctx } - UpdateContext::Autoplay => { + ContextType::Autoplay => { if resolve_uri.contains("spotify:show:") || resolve_uri.contains("spotify:episode:") { // autoplay is not supported for podcasts @@ -304,13 +301,13 @@ impl ContextResolver { } match (next.update, state.active_context) { - (UpdateContext::Default, ContextType::Default) | (UpdateContext::Autoplay, _) => { + (ContextType::Default, ContextType::Default) | (ContextType::Autoplay, _) => { debug!( "last item of type <{:?}>, finishing state setup", next.update ); } - (UpdateContext::Default, _) => { + (ContextType::Default, _) => { debug!("skipped finishing default, because it isn't the active context"); return false; } @@ -320,7 +317,7 @@ impl ContextResolver { let res = if let Some(transfer_state) = transfer_state.take() { state.finish_transfer(transfer_state) } else if state.shuffling_context() { - state.shuffle() + state.shuffle(None) } else if matches!(active_ctx, Ok(ctx) if ctx.index.track == 0) { // has context, and context is not touched // when the index is not zero, the next index was already evaluated elsewhere diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 11a65186..ebceaaac 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -7,5 +7,6 @@ use librespot_protocol as protocol; mod context_resolver; mod model; +pub mod shuffle_vec; pub mod spirc; pub mod state; diff --git a/connect/src/shuffle_vec.rs b/connect/src/shuffle_vec.rs new file mode 100644 index 00000000..b7bb5f3d --- /dev/null +++ b/connect/src/shuffle_vec.rs @@ -0,0 +1,117 @@ +use rand::{rngs::SmallRng, Rng, SeedableRng}; +use std::{ + ops::{Deref, DerefMut}, + vec::IntoIter, +}; + +#[derive(Debug, Clone, Default)] +pub struct ShuffleVec { + vec: Vec, + indices: Option>, +} + +impl PartialEq for ShuffleVec { + fn eq(&self, other: &Self) -> bool { + self.vec == other.vec + } +} + +impl Deref for ShuffleVec { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.vec + } +} + +impl DerefMut for ShuffleVec { + fn deref_mut(&mut self) -> &mut Self::Target { + self.vec.as_mut() + } +} + +impl IntoIterator for ShuffleVec { + type Item = T; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.vec.into_iter() + } +} + +impl From> for ShuffleVec { + fn from(vec: Vec) -> Self { + Self { vec, indices: None } + } +} + +impl ShuffleVec { + pub fn new() -> Self { + Self { + vec: Vec::new(), + indices: None, + } + } + + pub fn shuffle_with_seed(&mut self, seed: u64) { + self.shuffle_with_rng(SmallRng::seed_from_u64(seed)) + } + + pub fn shuffle_with_rng(&mut self, mut rng: impl Rng) { + if self.indices.is_some() { + self.unshuffle() + } + + let indices = { + (1..self.vec.len()) + .rev() + .map(|i| rng.gen_range(0..i + 1)) + .collect() + }; + + for (i, &rnd_ind) in (1..self.vec.len()).rev().zip(&indices) { + self.vec.swap(i, rnd_ind); + } + + self.indices = Some(indices) + } + + pub fn unshuffle(&mut self) { + let indices = match self.indices.take() { + Some(indices) => indices, + None => return, + }; + + for i in 1..self.vec.len() { + let n = indices[self.vec.len() - i - 1]; + self.vec.swap(n, i); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use rand::Rng; + + #[test] + fn test_shuffle_with_seed() { + let seed = rand::thread_rng().gen_range(0..10000000000000); + + let vec = (0..100).collect::>(); + let base_vec: ShuffleVec = vec.into(); + + let mut shuffled_vec = base_vec.clone(); + shuffled_vec.shuffle_with_seed(seed); + + let mut different_shuffled_vec = base_vec.clone(); + different_shuffled_vec.shuffle_with_seed(seed); + + assert_eq!(shuffled_vec, different_shuffled_vec); + + let mut unshuffled_vec = shuffled_vec.clone(); + unshuffled_vec.unshuffle(); + + assert_eq!(base_vec, unshuffled_vec); + } +} diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index d4773fc0..d1cf9e5b 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -25,9 +25,7 @@ use crate::{ user_attributes::UserAttributesMutation, }, state::{ - context::{ - ResetContext, {ContextType, UpdateContext}, - }, + context::{ContextType, ResetContext}, metadata::Metadata, provider::IsProvider, {ConnectState, ConnectStateConfig}, @@ -37,7 +35,6 @@ use futures_util::StreamExt; use protobuf::MessageField; use std::{ future::Future, - ops::Deref, sync::atomic::{AtomicUsize, Ordering}, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -749,9 +746,6 @@ impl SpircTask { use protobuf::Message; - // todo: handle received pages from transfer, important to not always shuffle the first 10 tracks - // also important when the dealer is restarted, currently we just shuffle again, but at least - // the 10 tracks provided should be used and after that the new shuffle context match TransferState::parse_from_bytes(&cluster.transfer_data) { Ok(transfer_state) => self.handle_transfer(transfer_state)?, Err(why) => error!("failed to take over control: {why}"), @@ -889,7 +883,7 @@ impl SpircTask { } else { self.context_resolver.add(ResolveContext::from_context( update_context.context, - super::state::context::UpdateContext::Default, + ContextType::Default, ContextAction::Replace, )) } @@ -1007,7 +1001,7 @@ impl SpircTask { self.context_resolver.add(ResolveContext::from_uri( ctx_uri.clone(), &fallback, - UpdateContext::Default, + ContextType::Default, ContextAction::Replace, )); @@ -1044,7 +1038,7 @@ impl SpircTask { self.context_resolver.add(ResolveContext::from_uri( ctx_uri, fallback, - UpdateContext::Autoplay, + ContextType::Autoplay, ContextAction::Replace, )) } @@ -1139,13 +1133,12 @@ impl SpircTask { }; let update_context = if cmd.autoplay { - UpdateContext::Autoplay + ContextType::Autoplay } else { - UpdateContext::Default + ContextType::Default }; - self.connect_state - .set_active_context(*update_context.deref()); + self.connect_state.set_active_context(update_context); let current_context_uri = self.connect_state.context_uri(); if current_context_uri == &cmd.context_uri && fallback == cmd.context_uri { @@ -1209,7 +1202,7 @@ impl SpircTask { if self.context_resolver.has_next() { self.connect_state.update_queue_revision() } else { - self.connect_state.shuffle()?; + self.connect_state.shuffle(None)?; self.add_autoplay_resolving_when_required(); } } else { @@ -1366,7 +1359,7 @@ impl SpircTask { let resolve = ResolveContext::from_uri( current_context, fallback, - UpdateContext::Autoplay, + ContextType::Autoplay, if has_tracks { ContextAction::Append } else { @@ -1458,7 +1451,7 @@ impl SpircTask { self.context_resolver.add(ResolveContext::from_uri( uri, self.connect_state.current_track(|t| &t.uri), - UpdateContext::Default, + ContextType::Default, ContextAction::Replace, )); diff --git a/connect/src/state.rs b/connect/src/state.rs index c06618ae..73010b25 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -7,12 +7,12 @@ mod restrictions; mod tracks; mod transfer; -use crate::model::SpircPlayStatus; use crate::{ core::{ config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult, version, Error, Session, }, + model::SpircPlayStatus, protocol::{ connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest}, media::AudioQuality, @@ -26,7 +26,6 @@ use crate::{ provider::{IsProvider, Provider}, }, }; - use log::LevelFilter; use protobuf::{EnumOrUnknown, MessageField}; use std::{ @@ -118,10 +117,9 @@ pub struct ConnectState { /// the context from which we play, is used to top up prev and next tracks context: Option, + /// seed extracted in [ConnectState::handle_initial_transfer] and used in [ConnectState::finish_transfer] + transfer_shuffle_seed: Option, - /// a context to keep track of our shuffled context, - /// should be only available when `player.option.shuffling_context` is true - shuffle_context: Option, /// a context to keep track of the autoplay context autoplay_context: Option, } diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index fa78180a..5233795e 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -7,6 +7,7 @@ use crate::{ player::{ContextIndex, ProvidedTrack}, restrictions::Restrictions, }, + shuffle_vec::ShuffleVec, state::{ metadata::Metadata, provider::{IsProvider, Provider}, @@ -15,46 +16,28 @@ use crate::{ }; use protobuf::MessageField; use std::collections::HashMap; -use std::ops::Deref; use uuid::Uuid; const LOCAL_FILES_IDENTIFIER: &str = "spotify:local-files"; const SEARCH_IDENTIFIER: &str = "spotify:search"; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct StateContext { - pub tracks: Vec, + pub tracks: ShuffleVec, + pub skip_track: Option, pub metadata: HashMap, pub restrictions: Option, /// is used to keep track which tracks are already loaded into the next_tracks pub index: ContextIndex, } -#[derive(Default, Debug, Copy, Clone, PartialEq)] +#[derive(Default, Debug, Copy, Clone, PartialEq, Hash, Eq)] pub enum ContextType { #[default] Default, - Shuffle, Autoplay, } -#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)] -pub enum UpdateContext { - Default, - Autoplay, -} - -impl Deref for UpdateContext { - type Target = ContextType; - - fn deref(&self) -> &Self::Target { - match self { - UpdateContext::Default => &ContextType::Default, - UpdateContext::Autoplay => &ContextType::Autoplay, - } - } -} - pub enum ResetContext<'s> { Completely, DefaultIndex, @@ -96,12 +79,19 @@ impl ConnectState { pub fn get_context(&self, ty: ContextType) -> Result<&StateContext, StateError> { match ty { ContextType::Default => self.context.as_ref(), - ContextType::Shuffle => self.shuffle_context.as_ref(), ContextType::Autoplay => self.autoplay_context.as_ref(), } .ok_or(StateError::NoContext(ty)) } + pub fn get_context_mut(&mut self, ty: ContextType) -> Result<&mut StateContext, StateError> { + match ty { + ContextType::Default => self.context.as_mut(), + ContextType::Autoplay => self.autoplay_context.as_mut(), + } + .ok_or(StateError::NoContext(ty)) + } + pub fn context_uri(&self) -> &String { &self.player().context_uri } @@ -115,14 +105,18 @@ impl ConnectState { if matches!(reset_as, ResetContext::WhenDifferent(ctx) if self.different_context_uri(ctx)) { reset_as = ResetContext::Completely } - self.shuffle_context = None; + + if let Ok(ctx) = self.get_context_mut(ContextType::Default) { + ctx.remove_shuffle_seed(); + ctx.tracks.unshuffle() + } match reset_as { + ResetContext::WhenDifferent(_) => debug!("context didn't change, no reset"), ResetContext::Completely => { self.context = None; self.autoplay_context = None; } - ResetContext::WhenDifferent(_) => debug!("context didn't change, no reset"), ResetContext::DefaultIndex => { for ctx in [self.context.as_mut(), self.autoplay_context.as_mut()] .into_iter() @@ -190,7 +184,7 @@ impl ConnectState { pub fn update_context( &mut self, mut context: Context, - ty: UpdateContext, + ty: ContextType, ) -> Result>, Error> { if context.pages.iter().all(|p| p.tracks.is_empty()) { error!("context didn't have any tracks: {context:#?}"); @@ -221,12 +215,13 @@ impl ConnectState { ); match ty { - UpdateContext::Default => { + ContextType::Default => { let mut new_context = self.state_context_from_page( page, context.metadata, context.restrictions.take(), context.uri.as_deref(), + Some(0), None, ); @@ -245,7 +240,7 @@ impl ConnectState { }; // enforce reloading the context - if let Some(autoplay_ctx) = self.autoplay_context.as_mut() { + if let Ok(autoplay_ctx) = self.get_context_mut(ContextType::Autoplay) { autoplay_ctx.index.track = 0 } self.clear_next_tracks(); @@ -261,12 +256,13 @@ impl ConnectState { } self.player_mut().context_uri = context.uri.take().unwrap_or_default(); } - UpdateContext::Autoplay => { + ContextType::Autoplay => { self.autoplay_context = Some(self.state_context_from_page( page, context.metadata, context.restrictions.take(), context.uri.as_deref(), + None, Some(Provider::Autoplay), )) } @@ -349,6 +345,7 @@ impl ConnectState { metadata: HashMap, restrictions: Option, new_context_uri: Option<&str>, + context_length: Option, provider: Option, ) -> StateContext { let new_context_uri = new_context_uri.unwrap_or(self.context_uri()); @@ -356,10 +353,12 @@ impl ConnectState { let tracks = page .tracks .iter() - .flat_map(|track| { + .enumerate() + .flat_map(|(i, track)| { match self.context_to_provided_track( track, Some(new_context_uri), + context_length.map(|l| l + i), Some(&page.metadata), provider.clone(), ) { @@ -373,20 +372,28 @@ impl ConnectState { .collect::>(); StateContext { - tracks, + tracks: tracks.into(), + skip_track: None, restrictions, metadata, index: ContextIndex::new(), } } + pub fn is_skip_track(&self, track: &ProvidedTrack) -> bool { + self.get_context(self.active_context) + .ok() + .and_then(|t| t.skip_track.as_ref().map(|t| t.uri == track.uri)) + .unwrap_or(false) + } + pub fn merge_context(&mut self, context: Option) -> Option<()> { let mut context = context?; if matches!(context.uri, Some(ref uri) if uri != self.context_uri()) { return None; } - let current_context = self.context.as_mut()?; + let current_context = self.get_context_mut(ContextType::Default).ok()?; let new_page = context.pages.pop()?; for new_track in new_page.tracks { @@ -421,12 +428,7 @@ impl ConnectState { ty: ContextType, new_index: usize, ) -> Result<(), StateError> { - let context = match ty { - ContextType::Default => self.context.as_mut(), - ContextType::Shuffle => self.shuffle_context.as_mut(), - ContextType::Autoplay => self.autoplay_context.as_mut(), - } - .ok_or(StateError::NoContext(ty))?; + let context = self.get_context_mut(ty)?; context.index.track = new_index as u32; Ok(()) @@ -436,6 +438,7 @@ impl ConnectState { &self, ctx_track: &ContextTrack, context_uri: Option<&str>, + context_index: Option, page_metadata: Option<&HashMap>, provider: Option, ) -> Result { @@ -479,19 +482,25 @@ impl ConnectState { }; if let Some(context_uri) = context_uri { - track.set_context_uri(context_uri.to_string()); - track.set_entity_uri(context_uri.to_string()); + track.set_entity_uri(context_uri); + track.set_context_uri(context_uri); + } + + if let Some(index) = context_index { + track.set_context_index(index); } if matches!(provider, Provider::Autoplay) { - track.set_autoplay(true) + track.set_from_autoplay(true) } Ok(track) } pub fn fill_context_from_page(&mut self, page: ContextPage) -> Result<(), Error> { - let context = self.state_context_from_page(page, HashMap::new(), None, None, None); + let ctx_len = self.context.as_ref().map(|c| c.tracks.len()); + let context = self.state_context_from_page(page, HashMap::new(), None, None, ctx_len, None); + let ctx = self .context .as_mut() diff --git a/connect/src/state/handle.rs b/connect/src/state/handle.rs index 1c1a4b32..659ed92c 100644 --- a/connect/src/state/handle.rs +++ b/connect/src/state/handle.rs @@ -2,6 +2,7 @@ use crate::{ core::{dealer::protocol::SetQueueCommand, Error}, state::{ context::{ContextType, ResetContext}, + metadata::Metadata, ConnectState, }, }; @@ -12,7 +13,7 @@ impl ConnectState { self.set_shuffle(shuffle); if shuffle { - return self.shuffle(); + return self.shuffle(None); } self.reset_context(ResetContext::DefaultIndex); @@ -21,11 +22,16 @@ impl ConnectState { return Ok(()); } - let ctx = self.get_context(ContextType::Default)?; - let current_index = - ConnectState::find_index_in_context(ctx, |c| self.current_track(|t| c.uri == t.uri))?; - - self.reset_playback_to_position(Some(current_index)) + match self.current_track(|t| t.get_context_index()) { + Some(current_index) => self.reset_playback_to_position(Some(current_index)), + None => { + let ctx = self.get_context(ContextType::Default)?; + let current_index = ConnectState::find_index_in_context(ctx, |c| { + self.current_track(|t| c.uri == t.uri) + })?; + self.reset_playback_to_position(Some(current_index)) + } + } } pub fn handle_set_queue(&mut self, set_queue: SetQueueCommand) { diff --git a/connect/src/state/metadata.rs b/connect/src/state/metadata.rs index b1effb68..763244b7 100644 --- a/connect/src/state/metadata.rs +++ b/connect/src/state/metadata.rs @@ -1,84 +1,82 @@ -use librespot_protocol::{context_track::ContextTrack, player::ProvidedTrack}; +use crate::{ + protocol::{context::Context, context_track::ContextTrack, player::ProvidedTrack}, + state::context::StateContext, +}; use std::collections::HashMap; +use std::fmt::Display; const CONTEXT_URI: &str = "context_uri"; const ENTITY_URI: &str = "entity_uri"; const IS_QUEUED: &str = "is_queued"; const IS_AUTOPLAY: &str = "autoplay.is_autoplay"; - const HIDDEN: &str = "hidden"; const ITERATION: &str = "iteration"; +const CUSTOM_CONTEXT_INDEX: &str = "context_index"; +const CUSTOM_SHUFFLE_SEED: &str = "shuffle_seed"; + +macro_rules! metadata_entry { + ( $get:ident, $set:ident, $clear:ident ($key:ident: $entry:ident)) => { + metadata_entry!( $get use get, $set, $clear ($key: $entry) -> Option<&String> ); + }; + ( $get_key:ident use $get:ident, $set:ident, $clear:ident ($key:ident: $entry:ident) -> $ty:ty ) => { + fn $get_key (&self) -> $ty { + self.$get($entry) + } + + fn $set (&mut self, $key: impl Display) { + self.metadata_mut().insert($entry.to_string(), $key.to_string()); + } + + fn $clear(&mut self) { + self.metadata_mut().remove($entry); + } + }; +} + #[allow(dead_code)] pub trait Metadata { fn metadata(&self) -> &HashMap; fn metadata_mut(&mut self) -> &mut HashMap; - fn is_from_queue(&self) -> bool { - matches!(self.metadata().get(IS_QUEUED), Some(is_queued) if is_queued.eq("true")) + fn get_bool(&self, entry: &str) -> bool { + matches!(self.metadata().get(entry), Some(entry) if entry.eq("true")) } - fn is_from_autoplay(&self) -> bool { - matches!(self.metadata().get(IS_AUTOPLAY), Some(is_autoplay) if is_autoplay.eq("true")) + fn get_usize(&self, entry: &str) -> Option { + self.metadata().get(entry)?.parse().ok() } - fn is_hidden(&self) -> bool { - matches!(self.metadata().get(HIDDEN), Some(is_hidden) if is_hidden.eq("true")) + fn get(&self, entry: &str) -> Option<&String> { + self.metadata().get(entry) } - fn get_context_uri(&self) -> Option<&String> { - self.metadata().get(CONTEXT_URI) - } + metadata_entry!(is_from_queue use get_bool, set_from_queue, remove_from_queue (is_queued: IS_QUEUED) -> bool); + metadata_entry!(is_from_autoplay use get_bool, set_from_autoplay, remove_from_autoplay (is_autoplay: IS_AUTOPLAY) -> bool); + metadata_entry!(is_hidden use get_bool, set_hidden, remove_hidden (is_hidden: HIDDEN) -> bool); - fn get_iteration(&self) -> Option<&String> { - self.metadata().get(ITERATION) - } - - fn set_queued(&mut self, queued: bool) { - self.metadata_mut() - .insert(IS_QUEUED.to_string(), queued.to_string()); - } - - fn set_autoplay(&mut self, autoplay: bool) { - self.metadata_mut() - .insert(IS_AUTOPLAY.to_string(), autoplay.to_string()); - } - - fn set_hidden(&mut self, hidden: bool) { - self.metadata_mut() - .insert(HIDDEN.to_string(), hidden.to_string()); - } - - fn set_context_uri(&mut self, uri: String) { - self.metadata_mut().insert(CONTEXT_URI.to_string(), uri); - } - - fn set_entity_uri(&mut self, uri: String) { - self.metadata_mut().insert(ENTITY_URI.to_string(), uri); - } - - fn add_iteration(&mut self, iter: i64) { - self.metadata_mut() - .insert(ITERATION.to_string(), iter.to_string()); - } + metadata_entry!(get_context_index use get_usize, set_context_index, remove_context_index (context_index: CUSTOM_CONTEXT_INDEX) -> Option); + metadata_entry!(get_context_uri, set_context_uri, remove_context_uri (context_uri: CONTEXT_URI)); + metadata_entry!(get_entity_uri, set_entity_uri, remove_entity_uri (entity_uri: ENTITY_URI)); + metadata_entry!(get_iteration, set_iteration, remove_iteration (iteration: ITERATION)); + metadata_entry!(get_shuffle_seed, set_shuffle_seed, remove_shuffle_seed (shuffle_seed: CUSTOM_SHUFFLE_SEED)); } -impl Metadata for ContextTrack { - fn metadata(&self) -> &HashMap { - &self.metadata - } +macro_rules! impl_metadata { + ($impl_for:ident) => { + impl Metadata for $impl_for { + fn metadata(&self) -> &HashMap { + &self.metadata + } - fn metadata_mut(&mut self) -> &mut HashMap { - &mut self.metadata - } + fn metadata_mut(&mut self) -> &mut HashMap { + &mut self.metadata + } + } + }; } -impl Metadata for ProvidedTrack { - fn metadata(&self) -> &HashMap { - &self.metadata - } - - fn metadata_mut(&mut self) -> &mut HashMap { - &mut self.metadata - } -} +impl_metadata!(ContextTrack); +impl_metadata!(ProvidedTrack); +impl_metadata!(Context); +impl_metadata!(StateContext); diff --git a/connect/src/state/options.rs b/connect/src/state/options.rs index 97484f67..6f384810 100644 --- a/connect/src/state/options.rs +++ b/connect/src/state/options.rs @@ -1,9 +1,14 @@ -use crate::state::context::ContextType; -use crate::state::{ConnectState, StateError}; -use librespot_core::Error; -use librespot_protocol::player::{ContextIndex, ContextPlayerOptions}; +use crate::{ + core::Error, + protocol::player::ContextPlayerOptions, + state::{ + context::{ContextType, ResetContext}, + metadata::Metadata, + ConnectState, StateError, + }, +}; use protobuf::MessageField; -use rand::prelude::SliceRandom; +use rand::Rng; impl ConnectState { fn add_options_if_empty(&mut self) { @@ -39,7 +44,7 @@ impl ConnectState { self.set_repeat_context(false); } - pub fn shuffle(&mut self) -> Result<(), Error> { + pub fn shuffle(&mut self, seed: Option) -> Result<(), Error> { if let Some(reason) = self .player() .restrictions @@ -55,22 +60,22 @@ impl ConnectState { self.clear_prev_track(); self.clear_next_tracks(); - let current_uri = self.current_track(|t| &t.uri); + let current_track = self.current_track(|t| t.clone().take()); - let ctx = self.get_context(ContextType::Default)?; - let current_track = Self::find_index_in_context(ctx, |t| &t.uri == current_uri)?; + self.reset_context(ResetContext::DefaultIndex); + let ctx = self.get_context_mut(ContextType::Default)?; - let mut shuffle_context = ctx.clone(); // we don't need to include the current track, because it is already being played - shuffle_context.tracks.remove(current_track); + ctx.skip_track = current_track; - let mut rng = rand::thread_rng(); - shuffle_context.tracks.shuffle(&mut rng); - shuffle_context.index = ContextIndex::new(); + let seed = seed + .unwrap_or_else(|| rand::thread_rng().gen_range(100_000_000_000..1_000_000_000_000)); - self.shuffle_context = Some(shuffle_context); - self.set_active_context(ContextType::Shuffle); - self.fill_up_context = ContextType::Shuffle; + ctx.tracks.shuffle_with_seed(seed); + ctx.set_shuffle_seed(seed); + + self.set_active_context(ContextType::Default); + self.fill_up_context = ContextType::Default; self.fill_up_next_tracks()?; Ok(()) diff --git a/connect/src/state/tracks.rs b/connect/src/state/tracks.rs index 05468575..07d03991 100644 --- a/connect/src/state/tracks.rs +++ b/connect/src/state/tracks.rs @@ -23,7 +23,7 @@ impl<'ct> ConnectState { ..Default::default() }; delimiter.set_hidden(true); - delimiter.add_iteration(iteration); + delimiter.set_iteration(iteration); delimiter } @@ -124,6 +124,7 @@ impl<'ct> ConnectState { continue; } Some(next) if next.is_unavailable() => continue, + Some(next) if self.is_skip_track(&next) => continue, other => break other, }; }; @@ -141,12 +142,10 @@ impl<'ct> ConnectState { self.set_active_context(ContextType::Autoplay); None } else { - let ctx = self.get_context(ContextType::Default)?; - let new_index = Self::find_index_in_context(ctx, |c| c.uri == new_track.uri); - match new_index { - Ok(new_index) => Some(new_index as u32), - Err(why) => { - error!("didn't find the track in the current context: {why}"); + match new_track.get_context_index() { + Some(new_index) => Some(new_index as u32), + None => { + error!("the given context track had no set context_index"); None } } @@ -323,7 +322,7 @@ impl<'ct> ConnectState { } } None => break, - Some(ct) if ct.is_unavailable() => { + Some(ct) if ct.is_unavailable() || self.is_skip_track(ct) => { new_index += 1; continue; } @@ -414,7 +413,7 @@ impl<'ct> ConnectState { track.set_provider(Provider::Queue); if !track.is_from_queue() { - track.set_queued(true); + track.set_from_queue(true); } let next_tracks = self.next_tracks_mut(); diff --git a/connect/src/state/transfer.rs b/connect/src/state/transfer.rs index 7404bf55..1e2f40cf 100644 --- a/connect/src/state/transfer.rs +++ b/connect/src/state/transfer.rs @@ -26,6 +26,7 @@ impl ConnectState { track, transfer.current_session.context.uri.as_deref(), None, + None, transfer .queue .is_playing_queue @@ -52,10 +53,25 @@ impl ConnectState { _ => player.playback_speed = 1., } + let mut shuffle_seed = None; if let Some(session) = transfer.current_session.as_mut() { player.play_origin = session.play_origin.take().map(Into::into).into(); player.suppressions = session.suppressions.take().map(Into::into).into(); + // maybe at some point we can use the shuffle seed provided by spotify, + // but I doubt it, as spotify doesn't use true randomness but rather an algorithm + // based shuffle + trace!( + "shuffle_seed: <{:?}> (spotify), <{:?}> (own)", + session.shuffle_seed, + session.context.get_shuffle_seed() + ); + + shuffle_seed = session + .context + .get_shuffle_seed() + .and_then(|seed| seed.parse().ok()); + if let Some(mut ctx) = session.context.take() { player.restrictions = ctx.restrictions.take().map(Into::into).into(); for (key, value) in ctx.metadata { @@ -73,6 +89,8 @@ impl ConnectState { } } + self.transfer_shuffle_seed = shuffle_seed; + self.clear_prev_track(); self.clear_next_tracks(); self.update_queue_revision() @@ -134,6 +152,7 @@ impl ConnectState { track, Some(self.context_uri()), None, + None, Some(Provider::Queue), ) { self.add_to_queue(queued_track, false); @@ -143,7 +162,9 @@ impl ConnectState { if self.shuffling_context() { self.set_current_track(current_index.unwrap_or_default())?; self.set_shuffle(true); - self.shuffle()?; + + let previous_seed = self.transfer_shuffle_seed.take(); + self.shuffle(previous_seed)?; } else { self.reset_playback_to_position(current_index)?; }