1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-05 02:39:53 +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

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