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" futures-util = "0.3"
log = "0.4" log = "0.4"
protobuf = "3.5" protobuf = "3.5"
rand = "0.8" rand = { version = "0.8", default-features = false, features = ["small_rng"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "2.0" thiserror = "2.0"
tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } tokio = { version = "1", features = ["macros", "parking_lot", "sync"] }

View file

@ -4,13 +4,10 @@ use crate::{
autoplay_context_request::AutoplayContextRequest, context::Context, autoplay_context_request::AutoplayContextRequest, context::Context,
transfer_state::TransferState, transfer_state::TransferState,
}, },
state::{ state::{context::ContextType, ConnectState},
context::{ContextType, UpdateContext},
ConnectState,
},
}; };
use std::cmp::PartialEq;
use std::{ use std::{
cmp::PartialEq,
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
fmt::{Display, Formatter}, fmt::{Display, Formatter},
hash::Hash, hash::Hash,
@ -35,7 +32,7 @@ pub(super) enum ContextAction {
pub(super) struct ResolveContext { pub(super) struct ResolveContext {
resolve: Resolve, resolve: Resolve,
fallback: Option<String>, fallback: Option<String>,
update: UpdateContext, update: ContextType,
action: ContextAction, action: ContextAction,
} }
@ -44,7 +41,7 @@ impl ResolveContext {
Self { Self {
resolve: Resolve::Uri(uri.into()), resolve: Resolve::Uri(uri.into()),
fallback: None, fallback: None,
update: UpdateContext::Default, update: ContextType::Default,
action: ContextAction::Append, action: ContextAction::Append,
} }
} }
@ -52,7 +49,7 @@ impl ResolveContext {
pub fn from_uri( pub fn from_uri(
uri: impl Into<String>, uri: impl Into<String>,
fallback: impl Into<String>, fallback: impl Into<String>,
update: UpdateContext, update: ContextType,
action: ContextAction, action: ContextAction,
) -> Self { ) -> Self {
let fallback_uri = fallback.into(); 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 { Self {
resolve: Resolve::Context(context), resolve: Resolve::Context(context),
fallback: None, fallback: None,
@ -214,7 +211,7 @@ impl ContextResolver {
let (next, resolve_uri, _) = self.find_next().ok_or(ContextResolverError::NoNext)?; let (next, resolve_uri, _) = self.find_next().ok_or(ContextResolverError::NoNext)?;
match next.update { match next.update {
UpdateContext::Default => { ContextType::Default => {
let mut ctx = self.session.spclient().get_context(resolve_uri).await; let mut ctx = self.session.spclient().get_context(resolve_uri).await;
if let Ok(ctx) = ctx.as_mut() { if let Ok(ctx) = ctx.as_mut() {
ctx.uri = Some(next.context_uri().to_string()); ctx.uri = Some(next.context_uri().to_string());
@ -223,7 +220,7 @@ impl ContextResolver {
ctx ctx
} }
UpdateContext::Autoplay => { ContextType::Autoplay => {
if resolve_uri.contains("spotify:show:") || resolve_uri.contains("spotify:episode:") if resolve_uri.contains("spotify:show:") || resolve_uri.contains("spotify:episode:")
{ {
// autoplay is not supported for podcasts // autoplay is not supported for podcasts
@ -304,13 +301,13 @@ impl ContextResolver {
} }
match (next.update, state.active_context) { match (next.update, state.active_context) {
(UpdateContext::Default, ContextType::Default) | (UpdateContext::Autoplay, _) => { (ContextType::Default, ContextType::Default) | (ContextType::Autoplay, _) => {
debug!( debug!(
"last item of type <{:?}>, finishing state setup", "last item of type <{:?}>, finishing state setup",
next.update next.update
); );
} }
(UpdateContext::Default, _) => { (ContextType::Default, _) => {
debug!("skipped finishing default, because it isn't the active context"); debug!("skipped finishing default, because it isn't the active context");
return false; return false;
} }
@ -320,7 +317,7 @@ impl ContextResolver {
let res = if let Some(transfer_state) = transfer_state.take() { let res = if let Some(transfer_state) = transfer_state.take() {
state.finish_transfer(transfer_state) state.finish_transfer(transfer_state)
} else if state.shuffling_context() { } else if state.shuffling_context() {
state.shuffle() state.shuffle(None)
} else if matches!(active_ctx, Ok(ctx) if ctx.index.track == 0) { } else if matches!(active_ctx, Ok(ctx) if ctx.index.track == 0) {
// has context, and context is not touched // has context, and context is not touched
// when the index is not zero, the next index was already evaluated elsewhere // 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 context_resolver;
mod model; mod model;
pub mod shuffle_vec;
pub mod spirc; pub mod spirc;
pub mod state; 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, user_attributes::UserAttributesMutation,
}, },
state::{ state::{
context::{ context::{ContextType, ResetContext},
ResetContext, {ContextType, UpdateContext},
},
metadata::Metadata, metadata::Metadata,
provider::IsProvider, provider::IsProvider,
{ConnectState, ConnectStateConfig}, {ConnectState, ConnectStateConfig},
@ -37,7 +35,6 @@ use futures_util::StreamExt;
use protobuf::MessageField; use protobuf::MessageField;
use std::{ use std::{
future::Future, future::Future,
ops::Deref,
sync::atomic::{AtomicUsize, Ordering}, sync::atomic::{AtomicUsize, Ordering},
sync::Arc, sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH}, time::{Duration, SystemTime, UNIX_EPOCH},
@ -749,9 +746,6 @@ impl SpircTask {
use protobuf::Message; 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) { match TransferState::parse_from_bytes(&cluster.transfer_data) {
Ok(transfer_state) => self.handle_transfer(transfer_state)?, Ok(transfer_state) => self.handle_transfer(transfer_state)?,
Err(why) => error!("failed to take over control: {why}"), Err(why) => error!("failed to take over control: {why}"),
@ -889,7 +883,7 @@ impl SpircTask {
} else { } else {
self.context_resolver.add(ResolveContext::from_context( self.context_resolver.add(ResolveContext::from_context(
update_context.context, update_context.context,
super::state::context::UpdateContext::Default, ContextType::Default,
ContextAction::Replace, ContextAction::Replace,
)) ))
} }
@ -1007,7 +1001,7 @@ impl SpircTask {
self.context_resolver.add(ResolveContext::from_uri( self.context_resolver.add(ResolveContext::from_uri(
ctx_uri.clone(), ctx_uri.clone(),
&fallback, &fallback,
UpdateContext::Default, ContextType::Default,
ContextAction::Replace, ContextAction::Replace,
)); ));
@ -1044,7 +1038,7 @@ impl SpircTask {
self.context_resolver.add(ResolveContext::from_uri( self.context_resolver.add(ResolveContext::from_uri(
ctx_uri, ctx_uri,
fallback, fallback,
UpdateContext::Autoplay, ContextType::Autoplay,
ContextAction::Replace, ContextAction::Replace,
)) ))
} }
@ -1139,13 +1133,12 @@ impl SpircTask {
}; };
let update_context = if cmd.autoplay { let update_context = if cmd.autoplay {
UpdateContext::Autoplay ContextType::Autoplay
} else { } else {
UpdateContext::Default ContextType::Default
}; };
self.connect_state self.connect_state.set_active_context(update_context);
.set_active_context(*update_context.deref());
let current_context_uri = self.connect_state.context_uri(); let current_context_uri = self.connect_state.context_uri();
if current_context_uri == &cmd.context_uri && fallback == cmd.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() { if self.context_resolver.has_next() {
self.connect_state.update_queue_revision() self.connect_state.update_queue_revision()
} else { } else {
self.connect_state.shuffle()?; self.connect_state.shuffle(None)?;
self.add_autoplay_resolving_when_required(); self.add_autoplay_resolving_when_required();
} }
} else { } else {
@ -1366,7 +1359,7 @@ impl SpircTask {
let resolve = ResolveContext::from_uri( let resolve = ResolveContext::from_uri(
current_context, current_context,
fallback, fallback,
UpdateContext::Autoplay, ContextType::Autoplay,
if has_tracks { if has_tracks {
ContextAction::Append ContextAction::Append
} else { } else {
@ -1458,7 +1451,7 @@ impl SpircTask {
self.context_resolver.add(ResolveContext::from_uri( self.context_resolver.add(ResolveContext::from_uri(
uri, uri,
self.connect_state.current_track(|t| &t.uri), self.connect_state.current_track(|t| &t.uri),
UpdateContext::Default, ContextType::Default,
ContextAction::Replace, ContextAction::Replace,
)); ));

View file

@ -7,12 +7,12 @@ mod restrictions;
mod tracks; mod tracks;
mod transfer; mod transfer;
use crate::model::SpircPlayStatus;
use crate::{ use crate::{
core::{ core::{
config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult, config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult,
version, Error, Session, version, Error, Session,
}, },
model::SpircPlayStatus,
protocol::{ protocol::{
connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest}, connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest},
media::AudioQuality, media::AudioQuality,
@ -26,7 +26,6 @@ use crate::{
provider::{IsProvider, Provider}, provider::{IsProvider, Provider},
}, },
}; };
use log::LevelFilter; use log::LevelFilter;
use protobuf::{EnumOrUnknown, MessageField}; use protobuf::{EnumOrUnknown, MessageField};
use std::{ use std::{
@ -118,10 +117,9 @@ pub struct ConnectState {
/// the context from which we play, is used to top up prev and next tracks /// the context from which we play, is used to top up prev and next tracks
context: Option<StateContext>, 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 /// a context to keep track of the autoplay context
autoplay_context: Option<StateContext>, autoplay_context: Option<StateContext>,
} }

View file

@ -7,6 +7,7 @@ use crate::{
player::{ContextIndex, ProvidedTrack}, player::{ContextIndex, ProvidedTrack},
restrictions::Restrictions, restrictions::Restrictions,
}, },
shuffle_vec::ShuffleVec,
state::{ state::{
metadata::Metadata, metadata::Metadata,
provider::{IsProvider, Provider}, provider::{IsProvider, Provider},
@ -15,46 +16,28 @@ use crate::{
}; };
use protobuf::MessageField; use protobuf::MessageField;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Deref;
use uuid::Uuid; use uuid::Uuid;
const LOCAL_FILES_IDENTIFIER: &str = "spotify:local-files"; const LOCAL_FILES_IDENTIFIER: &str = "spotify:local-files";
const SEARCH_IDENTIFIER: &str = "spotify:search"; const SEARCH_IDENTIFIER: &str = "spotify:search";
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct StateContext { pub struct StateContext {
pub tracks: Vec<ProvidedTrack>, pub tracks: ShuffleVec<ProvidedTrack>,
pub skip_track: Option<ProvidedTrack>,
pub metadata: HashMap<String, String>, pub metadata: HashMap<String, String>,
pub restrictions: Option<Restrictions>, pub restrictions: Option<Restrictions>,
/// is used to keep track which tracks are already loaded into the next_tracks /// is used to keep track which tracks are already loaded into the next_tracks
pub index: ContextIndex, pub index: ContextIndex,
} }
#[derive(Default, Debug, Copy, Clone, PartialEq)] #[derive(Default, Debug, Copy, Clone, PartialEq, Hash, Eq)]
pub enum ContextType { pub enum ContextType {
#[default] #[default]
Default, Default,
Shuffle,
Autoplay, 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> { pub enum ResetContext<'s> {
Completely, Completely,
DefaultIndex, DefaultIndex,
@ -96,12 +79,19 @@ impl ConnectState {
pub fn get_context(&self, ty: ContextType) -> Result<&StateContext, StateError> { pub fn get_context(&self, ty: ContextType) -> Result<&StateContext, StateError> {
match ty { match ty {
ContextType::Default => self.context.as_ref(), ContextType::Default => self.context.as_ref(),
ContextType::Shuffle => self.shuffle_context.as_ref(),
ContextType::Autoplay => self.autoplay_context.as_ref(), ContextType::Autoplay => self.autoplay_context.as_ref(),
} }
.ok_or(StateError::NoContext(ty)) .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 { pub fn context_uri(&self) -> &String {
&self.player().context_uri &self.player().context_uri
} }
@ -115,14 +105,18 @@ impl ConnectState {
if matches!(reset_as, ResetContext::WhenDifferent(ctx) if self.different_context_uri(ctx)) { if matches!(reset_as, ResetContext::WhenDifferent(ctx) if self.different_context_uri(ctx)) {
reset_as = ResetContext::Completely 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 { match reset_as {
ResetContext::WhenDifferent(_) => debug!("context didn't change, no reset"),
ResetContext::Completely => { ResetContext::Completely => {
self.context = None; self.context = None;
self.autoplay_context = None; self.autoplay_context = None;
} }
ResetContext::WhenDifferent(_) => debug!("context didn't change, no reset"),
ResetContext::DefaultIndex => { ResetContext::DefaultIndex => {
for ctx in [self.context.as_mut(), self.autoplay_context.as_mut()] for ctx in [self.context.as_mut(), self.autoplay_context.as_mut()]
.into_iter() .into_iter()
@ -190,7 +184,7 @@ impl ConnectState {
pub fn update_context( pub fn update_context(
&mut self, &mut self,
mut context: Context, mut context: Context,
ty: UpdateContext, ty: ContextType,
) -> Result<Option<Vec<String>>, Error> { ) -> Result<Option<Vec<String>>, Error> {
if context.pages.iter().all(|p| p.tracks.is_empty()) { if context.pages.iter().all(|p| p.tracks.is_empty()) {
error!("context didn't have any tracks: {context:#?}"); error!("context didn't have any tracks: {context:#?}");
@ -221,12 +215,13 @@ impl ConnectState {
); );
match ty { match ty {
UpdateContext::Default => { ContextType::Default => {
let mut new_context = self.state_context_from_page( let mut new_context = self.state_context_from_page(
page, page,
context.metadata, context.metadata,
context.restrictions.take(), context.restrictions.take(),
context.uri.as_deref(), context.uri.as_deref(),
Some(0),
None, None,
); );
@ -245,7 +240,7 @@ impl ConnectState {
}; };
// enforce reloading the context // 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 autoplay_ctx.index.track = 0
} }
self.clear_next_tracks(); self.clear_next_tracks();
@ -261,12 +256,13 @@ impl ConnectState {
} }
self.player_mut().context_uri = context.uri.take().unwrap_or_default(); self.player_mut().context_uri = context.uri.take().unwrap_or_default();
} }
UpdateContext::Autoplay => { ContextType::Autoplay => {
self.autoplay_context = Some(self.state_context_from_page( self.autoplay_context = Some(self.state_context_from_page(
page, page,
context.metadata, context.metadata,
context.restrictions.take(), context.restrictions.take(),
context.uri.as_deref(), context.uri.as_deref(),
None,
Some(Provider::Autoplay), Some(Provider::Autoplay),
)) ))
} }
@ -349,6 +345,7 @@ impl ConnectState {
metadata: HashMap<String, String>, metadata: HashMap<String, String>,
restrictions: Option<Restrictions>, restrictions: Option<Restrictions>,
new_context_uri: Option<&str>, new_context_uri: Option<&str>,
context_length: Option<usize>,
provider: Option<Provider>, provider: Option<Provider>,
) -> StateContext { ) -> StateContext {
let new_context_uri = new_context_uri.unwrap_or(self.context_uri()); let new_context_uri = new_context_uri.unwrap_or(self.context_uri());
@ -356,10 +353,12 @@ impl ConnectState {
let tracks = page let tracks = page
.tracks .tracks
.iter() .iter()
.flat_map(|track| { .enumerate()
.flat_map(|(i, track)| {
match self.context_to_provided_track( match self.context_to_provided_track(
track, track,
Some(new_context_uri), Some(new_context_uri),
context_length.map(|l| l + i),
Some(&page.metadata), Some(&page.metadata),
provider.clone(), provider.clone(),
) { ) {
@ -373,20 +372,28 @@ impl ConnectState {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
StateContext { StateContext {
tracks, tracks: tracks.into(),
skip_track: None,
restrictions, restrictions,
metadata, metadata,
index: ContextIndex::new(), 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<()> { pub fn merge_context(&mut self, context: Option<Context>) -> Option<()> {
let mut context = context?; let mut context = context?;
if matches!(context.uri, Some(ref uri) if uri != self.context_uri()) { if matches!(context.uri, Some(ref uri) if uri != self.context_uri()) {
return None; 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()?; let new_page = context.pages.pop()?;
for new_track in new_page.tracks { for new_track in new_page.tracks {
@ -421,12 +428,7 @@ impl ConnectState {
ty: ContextType, ty: ContextType,
new_index: usize, new_index: usize,
) -> Result<(), StateError> { ) -> Result<(), StateError> {
let context = match ty { let context = self.get_context_mut(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))?;
context.index.track = new_index as u32; context.index.track = new_index as u32;
Ok(()) Ok(())
@ -436,6 +438,7 @@ impl ConnectState {
&self, &self,
ctx_track: &ContextTrack, ctx_track: &ContextTrack,
context_uri: Option<&str>, context_uri: Option<&str>,
context_index: Option<usize>,
page_metadata: Option<&HashMap<String, String>>, page_metadata: Option<&HashMap<String, String>>,
provider: Option<Provider>, provider: Option<Provider>,
) -> Result<ProvidedTrack, Error> { ) -> Result<ProvidedTrack, Error> {
@ -479,19 +482,25 @@ impl ConnectState {
}; };
if let Some(context_uri) = context_uri { if let Some(context_uri) = context_uri {
track.set_context_uri(context_uri.to_string()); track.set_entity_uri(context_uri);
track.set_entity_uri(context_uri.to_string()); track.set_context_uri(context_uri);
}
if let Some(index) = context_index {
track.set_context_index(index);
} }
if matches!(provider, Provider::Autoplay) { if matches!(provider, Provider::Autoplay) {
track.set_autoplay(true) track.set_from_autoplay(true)
} }
Ok(track) Ok(track)
} }
pub fn fill_context_from_page(&mut self, page: ContextPage) -> Result<(), Error> { 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 let ctx = self
.context .context
.as_mut() .as_mut()

View file

@ -2,6 +2,7 @@ use crate::{
core::{dealer::protocol::SetQueueCommand, Error}, core::{dealer::protocol::SetQueueCommand, Error},
state::{ state::{
context::{ContextType, ResetContext}, context::{ContextType, ResetContext},
metadata::Metadata,
ConnectState, ConnectState,
}, },
}; };
@ -12,7 +13,7 @@ impl ConnectState {
self.set_shuffle(shuffle); self.set_shuffle(shuffle);
if shuffle { if shuffle {
return self.shuffle(); return self.shuffle(None);
} }
self.reset_context(ResetContext::DefaultIndex); self.reset_context(ResetContext::DefaultIndex);
@ -21,12 +22,17 @@ impl ConnectState {
return Ok(()); return Ok(());
} }
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 ctx = self.get_context(ContextType::Default)?;
let current_index = let current_index = ConnectState::find_index_in_context(ctx, |c| {
ConnectState::find_index_in_context(ctx, |c| self.current_track(|t| c.uri == t.uri))?; self.current_track(|t| c.uri == t.uri)
})?;
self.reset_playback_to_position(Some(current_index)) self.reset_playback_to_position(Some(current_index))
} }
}
}
pub fn handle_set_queue(&mut self, set_queue: SetQueueCommand) { pub fn handle_set_queue(&mut self, set_queue: SetQueueCommand) {
self.set_next_tracks(set_queue.next_tracks); self.set_next_tracks(set_queue.next_tracks);

View file

@ -1,69 +1,70 @@
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::collections::HashMap;
use std::fmt::Display;
const CONTEXT_URI: &str = "context_uri"; const CONTEXT_URI: &str = "context_uri";
const ENTITY_URI: &str = "entity_uri"; const ENTITY_URI: &str = "entity_uri";
const IS_QUEUED: &str = "is_queued"; const IS_QUEUED: &str = "is_queued";
const IS_AUTOPLAY: &str = "autoplay.is_autoplay"; const IS_AUTOPLAY: &str = "autoplay.is_autoplay";
const HIDDEN: &str = "hidden"; const HIDDEN: &str = "hidden";
const ITERATION: &str = "iteration"; 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)] #[allow(dead_code)]
pub trait Metadata { pub trait Metadata {
fn metadata(&self) -> &HashMap<String, String>; fn metadata(&self) -> &HashMap<String, String>;
fn metadata_mut(&mut self) -> &mut HashMap<String, String>; fn metadata_mut(&mut self) -> &mut HashMap<String, String>;
fn is_from_queue(&self) -> bool { fn get_bool(&self, entry: &str) -> bool {
matches!(self.metadata().get(IS_QUEUED), Some(is_queued) if is_queued.eq("true")) matches!(self.metadata().get(entry), Some(entry) if entry.eq("true"))
} }
fn is_from_autoplay(&self) -> bool { fn get_usize(&self, entry: &str) -> Option<usize> {
matches!(self.metadata().get(IS_AUTOPLAY), Some(is_autoplay) if is_autoplay.eq("true")) self.metadata().get(entry)?.parse().ok()
} }
fn is_hidden(&self) -> bool { fn get(&self, entry: &str) -> Option<&String> {
matches!(self.metadata().get(HIDDEN), Some(is_hidden) if is_hidden.eq("true")) self.metadata().get(entry)
} }
fn get_context_uri(&self) -> Option<&String> { metadata_entry!(is_from_queue use get_bool, set_from_queue, remove_from_queue (is_queued: IS_QUEUED) -> bool);
self.metadata().get(CONTEXT_URI) 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> { metadata_entry!(get_context_index use get_usize, set_context_index, remove_context_index (context_index: CUSTOM_CONTEXT_INDEX) -> Option<usize>);
self.metadata().get(ITERATION) 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));
fn set_queued(&mut self, queued: bool) { metadata_entry!(get_shuffle_seed, set_shuffle_seed, remove_shuffle_seed (shuffle_seed: CUSTOM_SHUFFLE_SEED));
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());
}
} }
impl Metadata for ContextTrack { macro_rules! impl_metadata {
($impl_for:ident) => {
impl Metadata for $impl_for {
fn metadata(&self) -> &HashMap<String, String> { fn metadata(&self) -> &HashMap<String, String> {
&self.metadata &self.metadata
} }
@ -71,14 +72,11 @@ impl Metadata for ContextTrack {
fn metadata_mut(&mut self) -> &mut HashMap<String, String> { fn metadata_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.metadata &mut self.metadata
} }
}
};
} }
impl Metadata for ProvidedTrack { impl_metadata!(ContextTrack);
fn metadata(&self) -> &HashMap<String, String> { impl_metadata!(ProvidedTrack);
&self.metadata impl_metadata!(Context);
} impl_metadata!(StateContext);
fn metadata_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.metadata
}
}

View file

@ -1,9 +1,14 @@
use crate::state::context::ContextType; use crate::{
use crate::state::{ConnectState, StateError}; core::Error,
use librespot_core::Error; protocol::player::ContextPlayerOptions,
use librespot_protocol::player::{ContextIndex, ContextPlayerOptions}; state::{
context::{ContextType, ResetContext},
metadata::Metadata,
ConnectState, StateError,
},
};
use protobuf::MessageField; use protobuf::MessageField;
use rand::prelude::SliceRandom; use rand::Rng;
impl ConnectState { impl ConnectState {
fn add_options_if_empty(&mut self) { fn add_options_if_empty(&mut self) {
@ -39,7 +44,7 @@ impl ConnectState {
self.set_repeat_context(false); 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 if let Some(reason) = self
.player() .player()
.restrictions .restrictions
@ -55,22 +60,22 @@ impl ConnectState {
self.clear_prev_track(); self.clear_prev_track();
self.clear_next_tracks(); 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)?; self.reset_context(ResetContext::DefaultIndex);
let current_track = Self::find_index_in_context(ctx, |t| &t.uri == current_uri)?; 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 // 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(); let seed = seed
shuffle_context.tracks.shuffle(&mut rng); .unwrap_or_else(|| rand::thread_rng().gen_range(100_000_000_000..1_000_000_000_000));
shuffle_context.index = ContextIndex::new();
self.shuffle_context = Some(shuffle_context); ctx.tracks.shuffle_with_seed(seed);
self.set_active_context(ContextType::Shuffle); ctx.set_shuffle_seed(seed);
self.fill_up_context = ContextType::Shuffle;
self.set_active_context(ContextType::Default);
self.fill_up_context = ContextType::Default;
self.fill_up_next_tracks()?; self.fill_up_next_tracks()?;
Ok(()) Ok(())

View file

@ -23,7 +23,7 @@ impl<'ct> ConnectState {
..Default::default() ..Default::default()
}; };
delimiter.set_hidden(true); delimiter.set_hidden(true);
delimiter.add_iteration(iteration); delimiter.set_iteration(iteration);
delimiter delimiter
} }
@ -124,6 +124,7 @@ impl<'ct> ConnectState {
continue; continue;
} }
Some(next) if next.is_unavailable() => continue, Some(next) if next.is_unavailable() => continue,
Some(next) if self.is_skip_track(&next) => continue,
other => break other, other => break other,
}; };
}; };
@ -141,12 +142,10 @@ impl<'ct> ConnectState {
self.set_active_context(ContextType::Autoplay); self.set_active_context(ContextType::Autoplay);
None None
} else { } else {
let ctx = self.get_context(ContextType::Default)?; match new_track.get_context_index() {
let new_index = Self::find_index_in_context(ctx, |c| c.uri == new_track.uri); Some(new_index) => Some(new_index as u32),
match new_index { None => {
Ok(new_index) => Some(new_index as u32), error!("the given context track had no set context_index");
Err(why) => {
error!("didn't find the track in the current context: {why}");
None None
} }
} }
@ -323,7 +322,7 @@ impl<'ct> ConnectState {
} }
} }
None => break, None => break,
Some(ct) if ct.is_unavailable() => { Some(ct) if ct.is_unavailable() || self.is_skip_track(ct) => {
new_index += 1; new_index += 1;
continue; continue;
} }
@ -414,7 +413,7 @@ impl<'ct> ConnectState {
track.set_provider(Provider::Queue); track.set_provider(Provider::Queue);
if !track.is_from_queue() { if !track.is_from_queue() {
track.set_queued(true); track.set_from_queue(true);
} }
let next_tracks = self.next_tracks_mut(); let next_tracks = self.next_tracks_mut();

View file

@ -26,6 +26,7 @@ impl ConnectState {
track, track,
transfer.current_session.context.uri.as_deref(), transfer.current_session.context.uri.as_deref(),
None, None,
None,
transfer transfer
.queue .queue
.is_playing_queue .is_playing_queue
@ -52,10 +53,25 @@ impl ConnectState {
_ => player.playback_speed = 1., _ => player.playback_speed = 1.,
} }
let mut shuffle_seed = None;
if let Some(session) = transfer.current_session.as_mut() { if let Some(session) = transfer.current_session.as_mut() {
player.play_origin = session.play_origin.take().map(Into::into).into(); player.play_origin = session.play_origin.take().map(Into::into).into();
player.suppressions = session.suppressions.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() { if let Some(mut ctx) = session.context.take() {
player.restrictions = ctx.restrictions.take().map(Into::into).into(); player.restrictions = ctx.restrictions.take().map(Into::into).into();
for (key, value) in ctx.metadata { for (key, value) in ctx.metadata {
@ -73,6 +89,8 @@ impl ConnectState {
} }
} }
self.transfer_shuffle_seed = shuffle_seed;
self.clear_prev_track(); self.clear_prev_track();
self.clear_next_tracks(); self.clear_next_tracks();
self.update_queue_revision() self.update_queue_revision()
@ -134,6 +152,7 @@ impl ConnectState {
track, track,
Some(self.context_uri()), Some(self.context_uri()),
None, None,
None,
Some(Provider::Queue), Some(Provider::Queue),
) { ) {
self.add_to_queue(queued_track, false); self.add_to_queue(queued_track, false);
@ -143,7 +162,9 @@ impl ConnectState {
if self.shuffling_context() { if self.shuffling_context() {
self.set_current_track(current_index.unwrap_or_default())?; self.set_current_track(current_index.unwrap_or_default())?;
self.set_shuffle(true); self.set_shuffle(true);
self.shuffle()?;
let previous_seed = self.transfer_shuffle_seed.take();
self.shuffle(previous_seed)?;
} else { } else {
self.reset_playback_to_position(current_index)?; self.reset_playback_to_position(current_index)?;
} }