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:
parent
471735aa5a
commit
34762f2274
12 changed files with 314 additions and 170 deletions
|
@ -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"] }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
117
connect/src/shuffle_vec.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_iteration(&self) -> Option<&String> {
|
macro_rules! impl_metadata {
|
||||||
self.metadata().get(ITERATION)
|
($impl_for:ident) => {
|
||||||
}
|
impl Metadata for $impl_for {
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Metadata for ContextTrack {
|
|
||||||
fn metadata(&self) -> &HashMap<String, String> {
|
fn metadata(&self) -> &HashMap<String, String> {
|
||||||
&self.metadata
|
&self.metadata
|
||||||
}
|
}
|
||||||
|
@ -72,13 +73,10 @@ impl Metadata for ContextTrack {
|
||||||
&mut self.metadata
|
&mut self.metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
impl Metadata for ProvidedTrack {
|
|
||||||
fn metadata(&self) -> &HashMap<String, String> {
|
|
||||||
&self.metadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metadata_mut(&mut self) -> &mut HashMap<String, String> {
|
impl_metadata!(ContextTrack);
|
||||||
&mut self.metadata
|
impl_metadata!(ProvidedTrack);
|
||||||
}
|
impl_metadata!(Context);
|
||||||
}
|
impl_metadata!(StateContext);
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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)?;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue