mirror of
https://github.com/librespot-org/librespot.git
synced 2025-10-03 01:39:28 +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"
|
||||
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"] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
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,
|
||||
},
|
||||
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,
|
||||
));
|
||||
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)?;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue