diff --git a/connect/Cargo.toml b/connect/Cargo.toml index ee076c3e8..74b21b7dc 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 278fc0899..79d48973a 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 11a651863..ebceaaac3 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 000000000..b7bb5f3d5 --- /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 d4773fc04..d1cf9e5b2 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 c06618ae7..73010b257 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 fa78180a4..5233795ed 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 1c1a4b325..659ed92cc 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 b1effb688..077456433 100644 --- a/connect/src/state/metadata.rs +++ b/connect/src/state/metadata.rs @@ -1,84 +1,83 @@ -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 is_from_autoplay(&self) -> bool { - matches!(self.metadata().get(IS_AUTOPLAY), Some(is_autoplay) if is_autoplay.eq("true")) - } - - fn is_hidden(&self) -> bool { - matches!(self.metadata().get(HIDDEN), Some(is_hidden) if is_hidden.eq("true")) - } - - fn get_context_uri(&self) -> Option<&String> { - self.metadata().get(CONTEXT_URI) + fn get_bool(&self, entry: &str) -> bool { + matches!(self.metadata().get(entry), Some(entry) if entry.eq("true")) } - fn get_iteration(&self) -> Option<&String> { - self.metadata().get(ITERATION) + fn get_usize(&self, entry: &str) -> Option { + self.metadata().get(entry)?.parse().ok() } - fn set_queued(&mut self, queued: bool) { - self.metadata_mut() - .insert(IS_QUEUED.to_string(), queued.to_string()); + fn get(&self, entry: &str) -> Option<&String> { + self.metadata().get(entry) } - fn set_autoplay(&mut self, autoplay: bool) { - self.metadata_mut() - .insert(IS_AUTOPLAY.to_string(), autoplay.to_string()); - } + 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 set_hidden(&mut self, hidden: bool) { - self.metadata_mut() - .insert(HIDDEN.to_string(), hidden.to_string()); - } + metadata_entry!(get_context_index use get_usize, set_context_index, remove_context_index (iteration: CUSTOM_CONTEXT_INDEX) -> Option); - 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_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 (iteration: CUSTOM_SHUFFLE_SEED)); } -impl Metadata for ContextTrack { - fn metadata(&self) -> &HashMap { - &self.metadata - } - - fn metadata_mut(&mut self) -> &mut HashMap { - &mut 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 + } + } + }; } -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 97484f675..6f3848107 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 054685757..07d039912 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 7404bf550..1e2f40cf2 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)?; }