1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-03 09:49:31 +02:00

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>
This commit is contained in:
Felix Prillwitz 2025-02-02 22:58:30 +01:00 committed by GitHub
parent 471735aa5a
commit 34762f2274
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 314 additions and 170 deletions

View file

@ -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"] }

View file

@ -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<String>,
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<String>,
fallback: impl Into<String>,
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

View file

@ -7,5 +7,6 @@ use librespot_protocol as protocol;
mod context_resolver;
mod model;
pub mod shuffle_vec;
pub mod spirc;
pub mod state;

117
connect/src/shuffle_vec.rs Normal file
View file

@ -0,0 +1,117 @@
use rand::{rngs::SmallRng, Rng, SeedableRng};
use std::{
ops::{Deref, DerefMut},
vec::IntoIter,
};
#[derive(Debug, Clone, Default)]
pub struct ShuffleVec<T> {
vec: Vec<T>,
indices: Option<Vec<usize>>,
}
impl<T: PartialEq> PartialEq for ShuffleVec<T> {
fn eq(&self, other: &Self) -> bool {
self.vec == other.vec
}
}
impl<T> Deref for ShuffleVec<T> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.vec
}
}
impl<T> DerefMut for ShuffleVec<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.vec.as_mut()
}
}
impl<T> IntoIterator for ShuffleVec<T> {
type Item = T;
type IntoIter = IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.vec.into_iter()
}
}
impl<T> From<Vec<T>> for ShuffleVec<T> {
fn from(vec: Vec<T>) -> Self {
Self { vec, indices: None }
}
}
impl<T> ShuffleVec<T> {
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::<Vec<_>>();
let base_vec: ShuffleVec<i32> = 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);
}
}

View file

@ -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,
));

View file

@ -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<StateContext>,
/// seed extracted in [ConnectState::handle_initial_transfer] and used in [ConnectState::finish_transfer]
transfer_shuffle_seed: Option<u64>,
/// a context to keep track of our shuffled context,
/// should be only available when `player.option.shuffling_context` is true
shuffle_context: Option<StateContext>,
/// a context to keep track of the autoplay context
autoplay_context: Option<StateContext>,
}

View file

@ -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<ProvidedTrack>,
pub tracks: ShuffleVec<ProvidedTrack>,
pub skip_track: Option<ProvidedTrack>,
pub metadata: HashMap<String, String>,
pub restrictions: Option<Restrictions>,
/// 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<Option<Vec<String>>, 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<String, String>,
restrictions: Option<Restrictions>,
new_context_uri: Option<&str>,
context_length: Option<usize>,
provider: Option<Provider>,
) -> 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::<Vec<_>>();
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<Context>) -> 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<usize>,
page_metadata: Option<&HashMap<String, String>>,
provider: Option<Provider>,
) -> Result<ProvidedTrack, Error> {
@ -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()

View file

@ -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) {

View file

@ -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<String, String>;
fn metadata_mut(&mut self) -> &mut HashMap<String, String>;
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<usize> {
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<usize>);
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<String, String> {
&self.metadata
}
macro_rules! impl_metadata {
($impl_for:ident) => {
impl Metadata for $impl_for {
fn metadata(&self) -> &HashMap<String, String> {
&self.metadata
}
fn metadata_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.metadata
}
fn metadata_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.metadata
}
}
};
}
impl Metadata for ProvidedTrack {
fn metadata(&self) -> &HashMap<String, String> {
&self.metadata
}
fn metadata_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.metadata
}
}
impl_metadata!(ContextTrack);
impl_metadata!(ProvidedTrack);
impl_metadata!(Context);
impl_metadata!(StateContext);

View file

@ -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<u64>) -> 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(())

View file

@ -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();

View file

@ -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)?;
}