1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-03 01:39:28 +02:00

Dealer: Rework context retrieval (#1414)

* connect: simplify `handle_command` for SpircCommand

* connect: simplify `handle_player_event`

* connect: `handle_player_event` update log entries

* connect: set `playback_speed` according to player state

* connect: reduce/group state updates by delaying them slightly

* connect: load entire context at once

* connect: use is_playing from connect_state

* connect: move `ResolveContext` in own file

* connect: handle metadata correct

* connect: resolve context rework

- resolved contexts independently, by that we don't block our main loop
- move resolve logic into own file
- polish handling for play and transfer

* connect: rework aftermath

* general logging and comment fixups

* connect: fix incorrect stopping

* connect: always handle player seek event

* connect: adjust behavior

- rename `handle_context` to `handle_next_context`
- disconnect should only pause the playback
- find_next should not exceed queue length

* fix typo and `find_next`

* connect: fixes for context and transfer

- fix context_metadata and restriction incorrect reset
- do some state updates earlier
- add more logging

* revert removal of state setup

* `clear_next_tracks` should never clear queued items

just mimics official client behavior

* connect: make `playing_track` optional and handle it correctly

* connect: adjust finish of context resolving

* connect: set track position when shuffling

* example: adjust to model change

* connect: remove duplicate track

* connect: provide all recently played tracks to autoplay request

- removes previously added workaround

* connect: apply review suggestions

- use drain instead of for with pop
- use for instead of loop
- use or_else instead of match
- use Self::Error instead of the value
- free memory for metadata and restrictions

* connect: impl trait for player context

* connect: fix incorrect playing and paused

* connect: apply options as official clients

* protocol: move trait impls into impl_trait mod
This commit is contained in:
Felix Prillwitz 2025-01-18 16:45:33 +01:00 committed by GitHub
parent c288cf7106
commit f3bb380851
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1004 additions and 734 deletions

View file

@ -0,0 +1,347 @@
use crate::{
core::{Error, Session},
protocol::{
autoplay_context_request::AutoplayContextRequest, context::Context,
transfer_state::TransferState,
},
state::{
context::{ContextType, UpdateContext},
ConnectState,
},
};
use std::cmp::PartialEq;
use std::{
collections::{HashMap, VecDeque},
fmt::{Display, Formatter},
hash::Hash,
time::Duration,
};
use thiserror::Error as ThisError;
use tokio::time::Instant;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
enum Resolve {
Uri(String),
Context(Context),
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub(super) enum ContextAction {
Append,
Replace,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub(super) struct ResolveContext {
resolve: Resolve,
fallback: Option<String>,
update: UpdateContext,
action: ContextAction,
}
impl ResolveContext {
fn append_context(uri: impl Into<String>) -> Self {
Self {
resolve: Resolve::Uri(uri.into()),
fallback: None,
update: UpdateContext::Default,
action: ContextAction::Append,
}
}
pub fn from_uri(
uri: impl Into<String>,
fallback: impl Into<String>,
update: UpdateContext,
action: ContextAction,
) -> Self {
let fallback_uri = fallback.into();
Self {
resolve: Resolve::Uri(uri.into()),
fallback: (!fallback_uri.is_empty()).then_some(fallback_uri),
update,
action,
}
}
pub fn from_context(context: Context, update: UpdateContext, action: ContextAction) -> Self {
Self {
resolve: Resolve::Context(context),
fallback: None,
update,
action,
}
}
/// the uri which should be used to resolve the context, might not be the context uri
fn resolve_uri(&self) -> Option<&str> {
// it's important to call this always, or at least for every ResolveContext
// otherwise we might not even check if we need to fallback and just use the fallback uri
match self.resolve {
Resolve::Uri(ref uri) => ConnectState::valid_resolve_uri(uri),
Resolve::Context(ref ctx) => ConnectState::get_context_uri_from_context(ctx),
}
.or(self.fallback.as_deref())
}
/// the actual context uri
fn context_uri(&self) -> &str {
match self.resolve {
Resolve::Uri(ref uri) => uri,
Resolve::Context(ref ctx) => ctx.uri.as_deref().unwrap_or_default(),
}
}
}
impl Display for ResolveContext {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"resolve_uri: <{:?}>, context_uri: <{}>, update: <{:?}>",
self.resolve_uri(),
self.context_uri(),
self.update,
)
}
}
#[derive(Debug, ThisError)]
enum ContextResolverError {
#[error("no next context to resolve")]
NoNext,
#[error("tried appending context with {0} pages")]
UnexpectedPagesSize(usize),
#[error("tried resolving not allowed context: {0:?}")]
NotAllowedContext(String),
}
impl From<ContextResolverError> for Error {
fn from(value: ContextResolverError) -> Self {
Error::failed_precondition(value)
}
}
pub struct ContextResolver {
session: Session,
queue: VecDeque<ResolveContext>,
unavailable_contexts: HashMap<ResolveContext, Instant>,
}
// time after which an unavailable context is retried
const RETRY_UNAVAILABLE: Duration = Duration::from_secs(3600);
impl ContextResolver {
pub fn new(session: Session) -> Self {
Self {
session,
queue: VecDeque::new(),
unavailable_contexts: HashMap::new(),
}
}
pub fn add(&mut self, resolve: ResolveContext) {
let last_try = self
.unavailable_contexts
.get(&resolve)
.map(|i| i.duration_since(Instant::now()));
let last_try = if matches!(last_try, Some(last_try) if last_try > RETRY_UNAVAILABLE) {
let _ = self.unavailable_contexts.remove(&resolve);
debug!(
"context was requested {}s ago, trying again to resolve the requested context",
last_try.expect("checked by condition").as_secs()
);
None
} else {
last_try
};
if last_try.is_some() {
debug!("tried loading unavailable context: {resolve}");
return;
} else if self.queue.contains(&resolve) {
debug!("update for {resolve} is already added");
return;
} else {
trace!(
"added {} to resolver queue",
resolve.resolve_uri().unwrap_or(resolve.context_uri())
)
}
self.queue.push_back(resolve)
}
pub fn add_list(&mut self, resolve: Vec<ResolveContext>) {
for resolve in resolve {
self.add(resolve)
}
}
pub fn remove_used_and_invalid(&mut self) {
if let Some((_, _, remove)) = self.find_next() {
let _ = self.queue.drain(0..remove); // remove invalid
}
self.queue.pop_front(); // remove used
}
pub fn clear(&mut self) {
self.queue = VecDeque::new()
}
fn find_next(&self) -> Option<(&ResolveContext, &str, usize)> {
for idx in 0..self.queue.len() {
let next = self.queue.get(idx)?;
match next.resolve_uri() {
None => {
warn!("skipped {idx} because of invalid resolve_uri: {next}");
continue;
}
Some(uri) => return Some((next, uri, idx)),
}
}
None
}
pub fn has_next(&self) -> bool {
self.find_next().is_some()
}
pub async fn get_next_context(
&self,
recent_track_uri: impl Fn() -> Vec<String>,
) -> Result<Context, Error> {
let (next, resolve_uri, _) = self.find_next().ok_or(ContextResolverError::NoNext)?;
match next.update {
UpdateContext::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());
ctx.url = ctx.uri.as_ref().map(|s| format!("context://{s}"));
}
ctx
}
UpdateContext::Autoplay => {
if resolve_uri.contains("spotify:show:") || resolve_uri.contains("spotify:episode:")
{
// autoplay is not supported for podcasts
Err(ContextResolverError::NotAllowedContext(
resolve_uri.to_string(),
))?
}
let request = AutoplayContextRequest {
context_uri: Some(resolve_uri.to_string()),
recent_track_uri: recent_track_uri(),
..Default::default()
};
self.session.spclient().get_autoplay_context(&request).await
}
}
}
pub fn mark_next_unavailable(&mut self) {
if let Some((next, _, _)) = self.find_next() {
self.unavailable_contexts
.insert(next.clone(), Instant::now());
}
}
pub fn apply_next_context(
&self,
state: &mut ConnectState,
mut context: Context,
) -> Result<Option<Vec<ResolveContext>>, Error> {
let (next, _, _) = self.find_next().ok_or(ContextResolverError::NoNext)?;
let remaining = match next.action {
ContextAction::Append if context.pages.len() == 1 => state
.fill_context_from_page(context.pages.remove(0))
.map(|_| None),
ContextAction::Replace => {
let remaining = state.update_context(context, next.update);
if let Resolve::Context(ref ctx) = next.resolve {
state.merge_context(Some(ctx.clone()));
}
remaining
}
ContextAction::Append => {
warn!("unexpected page size: {context:#?}");
Err(ContextResolverError::UnexpectedPagesSize(context.pages.len()).into())
}
}?;
Ok(remaining.map(|remaining| {
remaining
.into_iter()
.map(ResolveContext::append_context)
.collect::<Vec<_>>()
}))
}
pub fn try_finish(
&self,
state: &mut ConnectState,
transfer_state: &mut Option<TransferState>,
) -> bool {
let (next, _, _) = match self.find_next() {
None => return false,
Some(next) => next,
};
// when there is only one update type, we are the last of our kind, so we should update the state
if self
.queue
.iter()
.filter(|resolve| resolve.update == next.update)
.count()
!= 1
{
return false;
}
match (next.update, state.active_context) {
(UpdateContext::Default, ContextType::Default) | (UpdateContext::Autoplay, _) => {
debug!(
"last item of type <{:?}>, finishing state setup",
next.update
);
}
(UpdateContext::Default, _) => {
debug!("skipped finishing default, because it isn't the active context");
return false;
}
}
let active_ctx = state.get_context(state.active_context);
let res = if let Some(transfer_state) = transfer_state.take() {
state.finish_transfer(transfer_state)
} else if state.shuffling_context() {
state.shuffle()
} 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
let ctx = active_ctx.expect("checked by precondition");
let idx = ConnectState::find_index_in_context(ctx, |t| {
state.current_track(|c| t.uri == c.uri)
})
.ok();
state.reset_playback_to_position(idx)
} else {
state.fill_up_next_tracks()
};
if let Err(why) = res {
error!("setup of state failed: {why}, last used resolve {next:#?}")
}
state.update_restrictions();
state.update_queue_revision();
true
}
}

View file

@ -5,6 +5,7 @@ use librespot_core as core;
use librespot_playback as playback; use librespot_playback as playback;
use librespot_protocol as protocol; use librespot_protocol as protocol;
mod context_resolver;
mod model; mod model;
pub mod spirc; pub mod spirc;
pub mod state; pub mod state;

View file

@ -1,8 +1,4 @@
use crate::state::ConnectState;
use librespot_core::dealer::protocol::SkipTo; use librespot_core::dealer::protocol::SkipTo;
use librespot_protocol::context::Context;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
#[derive(Debug)] #[derive(Debug)]
pub struct SpircLoadCommand { pub struct SpircLoadCommand {
@ -13,7 +9,11 @@ pub struct SpircLoadCommand {
pub shuffle: bool, pub shuffle: bool,
pub repeat: bool, pub repeat: bool,
pub repeat_track: bool, pub repeat_track: bool,
pub playing_track: PlayingTrack, /// Decides the starting position in the given context
///
/// ## Remarks:
/// If none is provided and shuffle true, a random track is played, otherwise the first
pub playing_track: Option<PlayingTrack>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -23,19 +23,20 @@ pub enum PlayingTrack {
Uid(String), Uid(String),
} }
impl From<SkipTo> for PlayingTrack { impl TryFrom<SkipTo> for PlayingTrack {
fn from(value: SkipTo) -> Self { type Error = ();
fn try_from(value: SkipTo) -> Result<Self, Self::Error> {
// order of checks is important, as the index can be 0, but still has an uid or uri provided, // order of checks is important, as the index can be 0, but still has an uid or uri provided,
// so we only use the index as last resort // so we only use the index as last resort
if let Some(uri) = value.track_uri { if let Some(uri) = value.track_uri {
PlayingTrack::Uri(uri) Ok(PlayingTrack::Uri(uri))
} else if let Some(uid) = value.track_uid { } else if let Some(uid) = value.track_uid {
PlayingTrack::Uid(uid) Ok(PlayingTrack::Uid(uid))
} else if let Some(index) = value.track_index {
Ok(PlayingTrack::Index(index))
} else { } else {
PlayingTrack::Index(value.track_index.unwrap_or_else(|| { Err(())
warn!("SkipTo didn't provided any point to skip to, falling back to index 0");
0
}))
} }
} }
} }
@ -58,131 +59,3 @@ pub(super) enum SpircPlayStatus {
preloading_of_next_track_triggered: bool, preloading_of_next_track_triggered: bool,
}, },
} }
#[derive(Debug, Clone)]
pub(super) struct ResolveContext {
context: Context,
fallback: Option<String>,
autoplay: bool,
/// if `true` updates the entire context, otherwise only fills the context from the next
/// retrieve page, it is usually used when loading the next page of an already established context
///
/// like for example:
/// - playing an artists profile
update: bool,
}
impl ResolveContext {
pub fn from_uri(uri: impl Into<String>, fallback: impl Into<String>, autoplay: bool) -> Self {
let fallback_uri = fallback.into();
Self {
context: Context {
uri: Some(uri.into()),
..Default::default()
},
fallback: (!fallback_uri.is_empty()).then_some(fallback_uri),
autoplay,
update: true,
}
}
pub fn from_context(context: Context, autoplay: bool) -> Self {
Self {
context,
fallback: None,
autoplay,
update: true,
}
}
// expected page_url: hm://artistplaycontext/v1/page/spotify/album/5LFzwirfFwBKXJQGfwmiMY/km_artist
pub fn from_page_url(page_url: String) -> Self {
let split = if let Some(rest) = page_url.strip_prefix("hm://") {
rest.split('/')
} else {
warn!("page_url didn't started with hm://. got page_url: {page_url}");
page_url.split('/')
};
let uri = split
.skip_while(|s| s != &"spotify")
.take(3)
.collect::<Vec<&str>>()
.join(":");
trace!("created an ResolveContext from page_url <{page_url}> as uri <{uri}>");
Self {
context: Context {
uri: Some(uri),
..Default::default()
},
fallback: None,
update: false,
autoplay: false,
}
}
/// the uri which should be used to resolve the context, might not be the context uri
pub fn resolve_uri(&self) -> Option<&String> {
// it's important to call this always, or at least for every ResolveContext
// otherwise we might not even check if we need to fallback and just use the fallback uri
ConnectState::get_context_uri_from_context(&self.context)
.and_then(|s| (!s.is_empty()).then_some(s))
.or(self.fallback.as_ref())
}
/// the actual context uri
pub fn context_uri(&self) -> &str {
self.context.uri.as_deref().unwrap_or_default()
}
pub fn autoplay(&self) -> bool {
self.autoplay
}
pub fn update(&self) -> bool {
self.update
}
}
impl Display for ResolveContext {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"resolve_uri: <{:?}>, context_uri: <{:?}>, autoplay: <{}>, update: <{}>",
self.resolve_uri(),
self.context.uri,
self.autoplay,
self.update
)
}
}
impl PartialEq for ResolveContext {
fn eq(&self, other: &Self) -> bool {
let eq_context = self.context_uri() == other.context_uri();
let eq_resolve = self.resolve_uri() == other.resolve_uri();
let eq_autoplay = self.autoplay == other.autoplay;
let eq_update = self.update == other.update;
eq_context && eq_resolve && eq_autoplay && eq_update
}
}
impl Eq for ResolveContext {}
impl Hash for ResolveContext {
fn hash<H: Hasher>(&self, state: &mut H) {
self.context_uri().hash(state);
self.resolve_uri().hash(state);
self.autoplay.hash(state);
self.update.hash(state);
}
}
impl From<ResolveContext> for Context {
fn from(value: ResolveContext) -> Self {
value.context
}
}

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,6 @@ use crate::{
}, },
protocol::{ protocol::{
connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest}, connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest},
context_page::ContextPage,
player::{ player::{
ContextIndex, ContextPlayerOptions, PlayOrigin, PlayerState, ProvidedTrack, ContextIndex, ContextPlayerOptions, PlayOrigin, PlayerState, ProvidedTrack,
Suppressions, Suppressions,
@ -105,19 +104,17 @@ pub struct ConnectState {
unavailable_uri: Vec<String>, unavailable_uri: Vec<String>,
pub active_since: Option<SystemTime>, active_since: Option<SystemTime>,
queue_count: u64, queue_count: u64,
// separation is necessary because we could have already loaded // separation is necessary because we could have already loaded
// the autoplay context but are still playing from the default context // the autoplay context but are still playing from the default context
/// to update the active context use [switch_active_context](ConnectState::set_active_context) /// to update the active context use [switch_active_context](ConnectState::set_active_context)
pub active_context: ContextType, pub active_context: ContextType,
pub fill_up_context: ContextType, fill_up_context: ContextType,
/// 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
pub context: Option<StateContext>, context: Option<StateContext>,
/// upcoming contexts, directly provided by the context-resolver
next_contexts: Vec<ContextPage>,
/// a context to keep track of our shuffled context, /// a context to keep track of our shuffled context,
/// should be only available when `player.option.shuffling_context` is true /// should be only available when `player.option.shuffling_context` is true
@ -240,6 +237,22 @@ impl ConnectState {
self.request.is_active self.request.is_active
} }
/// Returns the `is_playing` value as perceived by other connect devices
///
/// see [ConnectState::set_status]
pub fn is_playing(&self) -> bool {
let player = self.player();
player.is_playing && !player.is_paused
}
/// Returns the `is_paused` state value as perceived by other connect devices
///
/// see [ConnectState::set_status]
pub fn is_pause(&self) -> bool {
let player = self.player();
player.is_playing && player.is_paused && player.is_buffering
}
pub fn set_volume(&mut self, volume: u32) { pub fn set_volume(&mut self, volume: u32) {
self.device_mut() self.device_mut()
.device_info .device_info
@ -297,6 +310,12 @@ impl ConnectState {
| SpircPlayStatus::Stopped | SpircPlayStatus::Stopped
); );
if player.is_paused {
player.playback_speed = 0.;
} else {
player.playback_speed = 1.;
}
// desktop and mobile require all 'states' set to true, when we are paused, // desktop and mobile require all 'states' set to true, when we are paused,
// otherwise the play button (desktop) is grayed out or the preview (mobile) can't be opened // otherwise the play button (desktop) is grayed out or the preview (mobile) can't be opened
player.is_buffering = player.is_paused player.is_buffering = player.is_paused
@ -349,9 +368,15 @@ impl ConnectState {
} }
pub fn reset_playback_to_position(&mut self, new_index: Option<usize>) -> Result<(), Error> { pub fn reset_playback_to_position(&mut self, new_index: Option<usize>) -> Result<(), Error> {
debug!(
"reset_playback with active ctx <{:?}> fill_up ctx <{:?}>",
self.active_context, self.fill_up_context
);
let new_index = new_index.unwrap_or(0); let new_index = new_index.unwrap_or(0);
self.update_current_index(|i| i.track = new_index as u32); self.update_current_index(|i| i.track = new_index as u32);
self.update_context_index(self.active_context, new_index + 1)?; self.update_context_index(self.active_context, new_index + 1)?;
self.fill_up_context = self.active_context;
if !self.current_track(|t| t.is_queue()) { if !self.current_track(|t| t.is_queue()) {
self.set_current_track(new_index)?; self.set_current_track(new_index)?;
@ -360,7 +385,7 @@ impl ConnectState {
self.clear_prev_track(); self.clear_prev_track();
if new_index > 0 { if new_index > 0 {
let context = self.get_context(&self.active_context)?; let context = self.get_context(self.active_context)?;
let before_new_track = context.tracks.len() - new_index; let before_new_track = context.tracks.len() - new_index;
self.player_mut().prev_tracks = context self.player_mut().prev_tracks = context
@ -375,7 +400,7 @@ impl ConnectState {
debug!("has {} prev tracks", self.prev_tracks().len()) debug!("has {} prev tracks", self.prev_tracks().len())
} }
self.clear_next_tracks(true); self.clear_next_tracks();
self.fill_up_next_tracks()?; self.fill_up_next_tracks()?;
self.update_restrictions(); self.update_restrictions();

View file

@ -7,10 +7,15 @@ use crate::{
player::{ContextIndex, ProvidedTrack}, player::{ContextIndex, ProvidedTrack},
restrictions::Restrictions, restrictions::Restrictions,
}, },
state::{metadata::Metadata, provider::Provider, ConnectState, StateError}, state::{
metadata::Metadata,
provider::{IsProvider, Provider},
ConnectState, StateError, SPOTIFY_MAX_NEXT_TRACKS_SIZE,
},
}; };
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";
@ -25,7 +30,7 @@ pub struct StateContext {
pub index: ContextIndex, pub index: ContextIndex,
} }
#[derive(Default, Debug, Copy, Clone)] #[derive(Default, Debug, Copy, Clone, PartialEq)]
pub enum ContextType { pub enum ContextType {
#[default] #[default]
Default, Default,
@ -33,57 +38,81 @@ pub enum ContextType {
Autoplay, Autoplay,
} }
pub enum LoadNext { #[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)]
Done,
PageUrl(String),
Empty,
}
#[derive(Debug)]
pub enum UpdateContext { pub enum UpdateContext {
Default, Default,
Autoplay, 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,
WhenDifferent(&'s str), WhenDifferent(&'s str),
} }
/// Extracts the spotify uri from a given page_url
///
/// Just extracts "spotify/album/5LFzwirfFwBKXJQGfwmiMY" and replaces the slash's with colon's
///
/// Expected `page_url` should look something like the following:
/// `hm://artistplaycontext/v1/page/spotify/album/5LFzwirfFwBKXJQGfwmiMY/km_artist`
fn page_url_to_uri(page_url: &str) -> String {
let split = if let Some(rest) = page_url.strip_prefix("hm://") {
rest.split('/')
} else {
warn!("page_url didn't start with hm://. got page_url: {page_url}");
page_url.split('/')
};
split
.skip_while(|s| s != &"spotify")
.take(3)
.collect::<Vec<&str>>()
.join(":")
}
impl ConnectState { impl ConnectState {
pub fn find_index_in_context<F: Fn(&ProvidedTrack) -> bool>( pub fn find_index_in_context<F: Fn(&ProvidedTrack) -> bool>(
context: Option<&StateContext>, ctx: &StateContext,
f: F, f: F,
) -> Result<usize, StateError> { ) -> Result<usize, StateError> {
let ctx = context
.as_ref()
.ok_or(StateError::NoContext(ContextType::Default))?;
ctx.tracks ctx.tracks
.iter() .iter()
.position(f) .position(f)
.ok_or(StateError::CanNotFindTrackInContext(None, ctx.tracks.len())) .ok_or(StateError::CanNotFindTrackInContext(None, ctx.tracks.len()))
} }
pub(super) 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::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 context_uri(&self) -> &String { pub fn context_uri(&self) -> &String {
&self.player().context_uri &self.player().context_uri
} }
pub fn reset_context(&mut self, mut reset_as: ResetContext) { fn different_context_uri(&self, uri: &str) -> bool {
self.set_active_context(ContextType::Default); // search identifier is always different
self.fill_up_context = ContextType::Default; self.context_uri() != uri || uri.starts_with(SEARCH_IDENTIFIER)
}
if matches!(reset_as, ResetContext::WhenDifferent(ctx) if self.context_uri() != ctx) { pub fn reset_context(&mut self, mut reset_as: ResetContext) {
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; self.shuffle_context = None;
@ -92,7 +121,6 @@ impl ConnectState {
ResetContext::Completely => { ResetContext::Completely => {
self.context = None; self.context = None;
self.autoplay_context = None; self.autoplay_context = None;
self.next_contexts.clear();
} }
ResetContext::WhenDifferent(_) => debug!("context didn't change, no reset"), ResetContext::WhenDifferent(_) => debug!("context didn't change, no reset"),
ResetContext::DefaultIndex => { ResetContext::DefaultIndex => {
@ -106,28 +134,40 @@ impl ConnectState {
} }
} }
self.fill_up_context = ContextType::Default;
self.set_active_context(ContextType::Default);
self.update_restrictions() self.update_restrictions()
} }
pub fn get_context_uri_from_context(context: &Context) -> Option<&String> { pub fn valid_resolve_uri(uri: &str) -> Option<&str> {
let context_uri = context.uri.as_ref()?; if uri.is_empty() || uri.starts_with(SEARCH_IDENTIFIER) {
None
if !context_uri.starts_with(SEARCH_IDENTIFIER) { } else {
return Some(context_uri); Some(uri)
} }
}
context pub fn get_context_uri_from_context(context: &Context) -> Option<&str> {
.pages let uri = context.uri.as_deref().unwrap_or_default();
.first() Self::valid_resolve_uri(uri).or_else(|| {
.and_then(|p| p.tracks.first().and_then(|t| t.uri.as_ref())) context
.pages
.first()
.and_then(|p| p.tracks.first().and_then(|t| t.uri.as_deref()))
})
} }
pub fn set_active_context(&mut self, new_context: ContextType) { pub fn set_active_context(&mut self, new_context: ContextType) {
self.active_context = new_context; self.active_context = new_context;
let ctx = match self.get_context(&new_context) { let player = self.player_mut();
player.context_metadata = Default::default();
player.restrictions = Some(Default::default()).into();
let ctx = match self.get_context(new_context) {
Err(why) => { Err(why) => {
debug!("couldn't load context info because: {why}"); warn!("couldn't load context info because: {why}");
return; return;
} }
Ok(ctx) => ctx, Ok(ctx) => ctx,
@ -138,9 +178,6 @@ impl ConnectState {
let player = self.player_mut(); let player = self.player_mut();
player.context_metadata.clear();
player.restrictions.clear();
if let Some(restrictions) = restrictions.take() { if let Some(restrictions) = restrictions.take() {
player.restrictions = MessageField::some(restrictions.into()); player.restrictions = MessageField::some(restrictions.into());
} }
@ -150,24 +187,25 @@ impl ConnectState {
} }
} }
pub fn update_context(&mut self, mut context: Context, ty: UpdateContext) -> Result<(), Error> { pub fn update_context(
&mut self,
mut context: Context,
ty: UpdateContext,
) -> 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:#?}");
return Err(StateError::ContextHasNoTracks.into()); Err(StateError::ContextHasNoTracks)?;
} else if matches!(context.uri, Some(ref uri) if uri.starts_with(LOCAL_FILES_IDENTIFIER)) { } else if matches!(context.uri, Some(ref uri) if uri.starts_with(LOCAL_FILES_IDENTIFIER)) {
return Err(StateError::UnsupportedLocalPlayBack.into()); Err(StateError::UnsupportedLocalPlayBack)?;
}
if matches!(ty, UpdateContext::Default) {
self.next_contexts.clear();
} }
let mut next_contexts = Vec::new();
let mut first_page = None; let mut first_page = None;
for page in context.pages { for page in context.pages {
if first_page.is_none() && !page.tracks.is_empty() { if first_page.is_none() && !page.tracks.is_empty() {
first_page = Some(page); first_page = Some(page);
} else { } else {
self.next_contexts.push(page) next_contexts.push(page)
} }
} }
@ -176,17 +214,8 @@ impl ConnectState {
Some(p) => p, Some(p) => p,
}; };
let prev_context = match ty {
UpdateContext::Default => self.context.as_ref(),
UpdateContext::Autoplay => self.autoplay_context.as_ref(),
};
debug!( debug!(
"updated context {ty:?} from <{:?}> ({} tracks) to <{:?}> ({} tracks)", "updated context {ty:?} to <{:?}> ({} tracks)",
self.context_uri(),
prev_context
.map(|c| c.tracks.len().to_string())
.unwrap_or_else(|| "-".to_string()),
context.uri, context.uri,
page.tracks.len() page.tracks.len()
); );
@ -195,32 +224,32 @@ impl ConnectState {
UpdateContext::Default => { UpdateContext::Default => {
let mut new_context = self.state_context_from_page( let mut new_context = self.state_context_from_page(
page, page,
context.metadata,
context.restrictions.take(), context.restrictions.take(),
context.uri.as_deref(), context.uri.as_deref(),
None, None,
); );
// when we update the same context, we should try to preserve the previous position // when we update the same context, we should try to preserve the previous position
// otherwise we might load the entire context twice // otherwise we might load the entire context twice, unless it's the search context
if !self.context_uri().contains(SEARCH_IDENTIFIER) if !self.context_uri().starts_with(SEARCH_IDENTIFIER)
&& matches!(context.uri, Some(ref uri) if uri == self.context_uri()) && matches!(context.uri, Some(ref uri) if uri == self.context_uri())
{ {
match Self::find_index_in_context(Some(&new_context), |t| { if let Some(new_index) = self.find_last_index_in_new_context(&new_context) {
self.current_track(|t| &t.uri) == &t.uri new_context.index.track = match new_index {
}) { Ok(i) => i,
Ok(new_pos) => { Err(i) => {
debug!("found new index of current track, updating new_context index to {new_pos}"); self.player_mut().index = MessageField::none();
new_context.index.track = (new_pos + 1) as u32; i
}
};
// enforce reloading the context
if let Some(autoplay_ctx) = self.autoplay_context.as_mut() {
autoplay_ctx.index.track = 0
} }
// the track isn't anymore in the context self.clear_next_tracks();
Err(_) if matches!(self.active_context, ContextType::Default) => {
warn!("current track was removed, setting pos to last known index");
new_context.index.track = self.player().index.track
}
Err(_) => {}
} }
// enforce reloading the context
self.clear_next_tracks(true);
} }
self.context = Some(new_context); self.context = Some(new_context);
@ -235,6 +264,7 @@ impl ConnectState {
UpdateContext::Autoplay => { UpdateContext::Autoplay => {
self.autoplay_context = Some(self.state_context_from_page( self.autoplay_context = Some(self.state_context_from_page(
page, page,
context.metadata,
context.restrictions.take(), context.restrictions.take(),
context.uri.as_deref(), context.uri.as_deref(),
Some(Provider::Autoplay), Some(Provider::Autoplay),
@ -242,12 +272,81 @@ impl ConnectState {
} }
} }
Ok(()) if next_contexts.is_empty() {
return Ok(None);
}
// load remaining contexts
let next_contexts = next_contexts
.into_iter()
.flat_map(|page| {
if !page.tracks.is_empty() {
self.fill_context_from_page(page).ok()?;
None
} else if matches!(page.page_url, Some(ref url) if !url.is_empty()) {
Some(page_url_to_uri(
&page.page_url.expect("checked by precondition"),
))
} else {
warn!("unhandled context page: {page:#?}");
None
}
})
.collect();
Ok(Some(next_contexts))
}
fn find_first_prev_track_index(&self, ctx: &StateContext) -> Option<usize> {
let prev_tracks = self.prev_tracks();
for i in (0..prev_tracks.len()).rev() {
let prev_track = prev_tracks.get(i)?;
if let Ok(idx) = Self::find_index_in_context(ctx, |t| prev_track.uri == t.uri) {
return Some(idx);
}
}
None
}
fn find_last_index_in_new_context(
&self,
new_context: &StateContext,
) -> Option<Result<u32, u32>> {
let ctx = self.context.as_ref()?;
let is_queued_item = self.current_track(|t| t.is_queue() || t.is_from_queue());
let new_index = if ctx.index.track as usize >= SPOTIFY_MAX_NEXT_TRACKS_SIZE {
Some(ctx.index.track as usize - SPOTIFY_MAX_NEXT_TRACKS_SIZE)
} else if is_queued_item {
self.find_first_prev_track_index(new_context)
} else {
Self::find_index_in_context(new_context, |current| {
self.current_track(|t| t.uri == current.uri)
})
.ok()
}
.map(|i| i as u32 + 1);
Some(new_index.ok_or_else(|| {
info!(
"couldn't distinguish index from current or previous tracks in the updated context"
);
let fallback_index = self
.player()
.index
.as_ref()
.map(|i| i.track)
.unwrap_or_default();
info!("falling back to index {fallback_index}");
fallback_index
}))
} }
fn state_context_from_page( fn state_context_from_page(
&mut self, &mut self,
page: ContextPage, page: ContextPage,
metadata: HashMap<String, String>,
restrictions: Option<Restrictions>, restrictions: Option<Restrictions>,
new_context_uri: Option<&str>, new_context_uri: Option<&str>,
provider: Option<Provider>, provider: Option<Provider>,
@ -258,8 +357,12 @@ impl ConnectState {
.tracks .tracks
.iter() .iter()
.flat_map(|track| { .flat_map(|track| {
match self.context_to_provided_track(track, Some(new_context_uri), provider.clone()) match self.context_to_provided_track(
{ track,
Some(new_context_uri),
Some(&page.metadata),
provider.clone(),
) {
Ok(t) => Some(t), Ok(t) => Some(t),
Err(why) => { Err(why) => {
error!("couldn't convert {track:#?} into ProvidedTrack: {why}"); error!("couldn't convert {track:#?} into ProvidedTrack: {why}");
@ -272,7 +375,7 @@ impl ConnectState {
StateContext { StateContext {
tracks, tracks,
restrictions, restrictions,
metadata: page.metadata, metadata,
index: ContextIndex::new(), index: ContextIndex::new(),
} }
} }
@ -293,12 +396,11 @@ impl ConnectState {
let new_track_uri = new_track.uri.unwrap_or_default(); let new_track_uri = new_track.uri.unwrap_or_default();
if let Ok(position) = if let Ok(position) =
Self::find_index_in_context(Some(current_context), |t| t.uri == new_track_uri) Self::find_index_in_context(current_context, |t| t.uri == new_track_uri)
{ {
let context_track = current_context.tracks.get_mut(position)?; let context_track = current_context.tracks.get_mut(position)?;
for (key, value) in new_track.metadata { for (key, value) in new_track.metadata {
warn!("merging metadata {key} {value}");
context_track.metadata.insert(key, value); context_track.metadata.insert(key, value);
} }
@ -334,10 +436,10 @@ impl ConnectState {
&self, &self,
ctx_track: &ContextTrack, ctx_track: &ContextTrack,
context_uri: Option<&str>, context_uri: Option<&str>,
page_metadata: Option<&HashMap<String, String>>,
provider: Option<Provider>, provider: Option<Provider>,
) -> Result<ProvidedTrack, Error> { ) -> Result<ProvidedTrack, Error> {
let id = match (ctx_track.uri.as_ref(), ctx_track.gid.as_ref()) { let id = match (ctx_track.uri.as_ref(), ctx_track.gid.as_ref()) {
(None, None) => Err(StateError::InvalidTrackUri(None))?,
(Some(uri), _) if uri.contains(['?', '%']) => { (Some(uri), _) if uri.contains(['?', '%']) => {
Err(StateError::InvalidTrackUri(Some(uri.clone())))? Err(StateError::InvalidTrackUri(Some(uri.clone())))?
} }
@ -363,7 +465,7 @@ impl ConnectState {
_ => Uuid::new_v4().as_simple().to_string(), _ => Uuid::new_v4().as_simple().to_string(),
}; };
let mut metadata = HashMap::new(); let mut metadata = page_metadata.cloned().unwrap_or_default();
for (k, v) in &ctx_track.metadata { for (k, v) in &ctx_track.metadata {
metadata.insert(k.to_string(), v.to_string()); metadata.insert(k.to_string(), v.to_string());
} }
@ -389,7 +491,7 @@ impl ConnectState {
} }
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, None, None, None); let context = self.state_context_from_page(page, HashMap::new(), None, None, None);
let ctx = self let ctx = self
.context .context
.as_mut() .as_mut()
@ -401,26 +503,4 @@ impl ConnectState {
Ok(()) Ok(())
} }
pub fn try_load_next_context(&mut self) -> Result<LoadNext, Error> {
let next = match self.next_contexts.first() {
None => return Ok(LoadNext::Empty),
Some(_) => self.next_contexts.remove(0),
};
if next.tracks.is_empty() {
let next_page_url = match next.page_url {
Some(page_url) if !page_url.is_empty() => page_url,
_ => Err(StateError::NoContext(ContextType::Default))?,
};
self.update_current_index(|i| i.page += 1);
return Ok(LoadNext::PageUrl(next_page_url));
}
self.fill_context_from_page(next)?;
self.fill_up_next_tracks()?;
Ok(LoadNext::Done)
}
} }

View file

@ -1,5 +1,10 @@
use crate::state::{context::ResetContext, ConnectState}; use crate::{
use librespot_core::{dealer::protocol::SetQueueCommand, Error}; core::{dealer::protocol::SetQueueCommand, Error},
state::{
context::{ContextType, ResetContext},
ConnectState,
},
};
use protobuf::MessageField; use protobuf::MessageField;
impl ConnectState { impl ConnectState {
@ -16,7 +21,7 @@ impl ConnectState {
return Ok(()); return Ok(());
} }
let ctx = self.context.as_ref(); let ctx = self.get_context(ContextType::Default)?;
let current_index = let current_index =
ConnectState::find_index_in_context(ctx, |c| self.current_track(|t| c.uri == t.uri))?; ConnectState::find_index_in_context(ctx, |c| self.current_track(|t| c.uri == t.uri))?;
@ -52,7 +57,7 @@ impl ConnectState {
self.set_shuffle(false); self.set_shuffle(false);
self.reset_context(ResetContext::DefaultIndex); self.reset_context(ResetContext::DefaultIndex);
let ctx = self.context.as_ref(); let ctx = self.get_context(ContextType::Default)?;
let current_track = ConnectState::find_index_in_context(ctx, |t| { let current_track = ConnectState::find_index_in_context(ctx, |t| {
self.current_track(|t| &t.uri) == &t.uri self.current_track(|t| &t.uri) == &t.uri
})?; })?;

View file

@ -33,6 +33,12 @@ impl ConnectState {
} }
} }
pub fn reset_options(&mut self) {
self.set_shuffle(false);
self.set_repeat_track(false);
self.set_repeat_context(false);
}
pub fn shuffle(&mut self) -> Result<(), Error> { pub fn shuffle(&mut self) -> Result<(), Error> {
if let Some(reason) = self if let Some(reason) = self
.player() .player()
@ -47,16 +53,12 @@ impl ConnectState {
} }
self.clear_prev_track(); self.clear_prev_track();
self.clear_next_tracks(true); self.clear_next_tracks();
let current_uri = self.current_track(|t| &t.uri); let current_uri = self.current_track(|t| &t.uri);
let ctx = self let ctx = self.get_context(ContextType::Default)?;
.context let current_track = Self::find_index_in_context(ctx, |t| &t.uri == current_uri)?;
.as_ref()
.ok_or(StateError::NoContext(ContextType::Default))?;
let current_track = Self::find_index_in_context(Some(ctx), |t| &t.uri == current_uri)?;
let mut shuffle_context = ctx.clone(); 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

View file

@ -17,14 +17,18 @@ impl ConnectState {
const ENDLESS_CONTEXT: &str = "endless_context"; const ENDLESS_CONTEXT: &str = "endless_context";
let prev_tracks_is_empty = self.prev_tracks().is_empty(); let prev_tracks_is_empty = self.prev_tracks().is_empty();
let is_paused = self.is_pause();
let is_playing = self.is_playing();
let player = self.player_mut(); let player = self.player_mut();
if let Some(restrictions) = player.restrictions.as_mut() { if let Some(restrictions) = player.restrictions.as_mut() {
if player.is_playing { if is_playing {
restrictions.disallow_pausing_reasons.clear(); restrictions.disallow_pausing_reasons.clear();
restrictions.disallow_resuming_reasons = vec!["not_paused".to_string()] restrictions.disallow_resuming_reasons = vec!["not_paused".to_string()]
} }
if player.is_paused { if is_paused {
restrictions.disallow_resuming_reasons.clear(); restrictions.disallow_resuming_reasons.clear();
restrictions.disallow_pausing_reasons = vec!["not_playing".to_string()] restrictions.disallow_pausing_reasons = vec!["not_playing".to_string()]
} }

View file

@ -1,12 +1,15 @@
use crate::state::{ use crate::{
context::ContextType, core::{Error, SpotifyId},
metadata::Metadata, protocol::player::ProvidedTrack,
provider::{IsProvider, Provider}, state::{
ConnectState, StateError, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, context::ContextType,
metadata::Metadata,
provider::{IsProvider, Provider},
ConnectState, StateError, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE,
},
}; };
use librespot_core::{Error, SpotifyId};
use librespot_protocol::player::ProvidedTrack;
use protobuf::MessageField; use protobuf::MessageField;
use rand::Rng;
// identifier used as part of the uid // identifier used as part of the uid
pub const IDENTIFIER_DELIMITER: &str = "delimiter"; pub const IDENTIFIER_DELIMITER: &str = "delimiter";
@ -64,8 +67,14 @@ impl<'ct> ConnectState {
&self.player().next_tracks &self.player().next_tracks
} }
pub fn set_current_track_random(&mut self) -> Result<(), Error> {
let max_tracks = self.get_context(self.active_context)?.tracks.len();
let rng_track = rand::thread_rng().gen_range(0..max_tracks);
self.set_current_track(rng_track)
}
pub fn set_current_track(&mut self, index: usize) -> Result<(), Error> { pub fn set_current_track(&mut self, index: usize) -> Result<(), Error> {
let context = self.get_context(&self.active_context)?; let context = self.get_context(self.active_context)?;
let new_track = context let new_track = context
.tracks .tracks
@ -77,8 +86,8 @@ impl<'ct> ConnectState {
debug!( debug!(
"set track to: {} at {} of {} tracks", "set track to: {} at {} of {} tracks",
index,
new_track.uri, new_track.uri,
index,
context.tracks.len() context.tracks.len()
); );
@ -132,7 +141,7 @@ impl<'ct> ConnectState {
self.set_active_context(ContextType::Autoplay); self.set_active_context(ContextType::Autoplay);
None None
} else { } else {
let ctx = self.context.as_ref(); let ctx = self.get_context(ContextType::Default)?;
let new_index = Self::find_index_in_context(ctx, |c| c.uri == new_track.uri); let new_index = Self::find_index_in_context(ctx, |c| c.uri == new_track.uri);
match new_index { match new_index {
Ok(new_index) => Some(new_index as u32), Ok(new_index) => Some(new_index as u32),
@ -251,12 +260,7 @@ impl<'ct> ConnectState {
self.prev_tracks_mut().clear() self.prev_tracks_mut().clear()
} }
pub fn clear_next_tracks(&mut self, keep_queued: bool) { pub fn clear_next_tracks(&mut self) {
if !keep_queued {
self.next_tracks_mut().clear();
return;
}
// respect queued track and don't throw them out of our next played tracks // respect queued track and don't throw them out of our next played tracks
let first_non_queued_track = self let first_non_queued_track = self
.next_tracks() .next_tracks()
@ -271,13 +275,13 @@ impl<'ct> ConnectState {
} }
} }
pub fn fill_up_next_tracks(&mut self) -> Result<(), StateError> { pub fn fill_up_next_tracks(&mut self) -> Result<(), Error> {
let ctx = self.get_context(&self.fill_up_context)?; let ctx = self.get_context(self.fill_up_context)?;
let mut new_index = ctx.index.track as usize; let mut new_index = ctx.index.track as usize;
let mut iteration = ctx.index.page; let mut iteration = ctx.index.page;
while self.next_tracks().len() < SPOTIFY_MAX_NEXT_TRACKS_SIZE { while self.next_tracks().len() < SPOTIFY_MAX_NEXT_TRACKS_SIZE {
let ctx = self.get_context(&self.fill_up_context)?; let ctx = self.get_context(self.fill_up_context)?;
let track = match ctx.tracks.get(new_index) { let track = match ctx.tracks.get(new_index) {
None if self.repeat_context() => { None if self.repeat_context() => {
let delimiter = Self::new_delimiter(iteration.into()); let delimiter = Self::new_delimiter(iteration.into());
@ -292,14 +296,14 @@ impl<'ct> ConnectState {
// transition to autoplay as fill up context // transition to autoplay as fill up context
self.fill_up_context = ContextType::Autoplay; self.fill_up_context = ContextType::Autoplay;
new_index = self.get_context(&ContextType::Autoplay)?.index.track as usize; new_index = self.get_context(ContextType::Autoplay)?.index.track as usize;
// add delimiter to only display the current context // add delimiter to only display the current context
Self::new_delimiter(iteration.into()) Self::new_delimiter(iteration.into())
} }
None if self.autoplay_context.is_some() => { None if self.autoplay_context.is_some() => {
match self match self
.get_context(&ContextType::Autoplay)? .get_context(ContextType::Autoplay)?
.tracks .tracks
.get(new_index) .get(new_index)
{ {
@ -324,6 +328,11 @@ impl<'ct> ConnectState {
self.next_tracks_mut().push(track); self.next_tracks_mut().push(track);
} }
debug!(
"finished filling up next_tracks ({})",
self.next_tracks().len()
);
self.update_context_index(self.fill_up_context, new_index)?; self.update_context_index(self.fill_up_context, new_index)?;
// the web-player needs a revision update, otherwise the queue isn't updated in the ui // the web-player needs a revision update, otherwise the queue isn't updated in the ui
@ -350,17 +359,14 @@ impl<'ct> ConnectState {
} }
} }
pub fn prev_autoplay_track_uris(&self) -> Vec<String> { pub fn recent_track_uris(&self) -> Vec<String> {
let mut prev = self let mut prev = self
.prev_tracks() .prev_tracks()
.iter() .iter()
.flat_map(|t| t.is_autoplay().then_some(t.uri.clone())) .map(|t| t.uri.clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if self.current_track(|t| t.is_autoplay()) { prev.push(self.current_track(|t| t.uri.clone()));
prev.push(self.current_track(|t| t.uri.clone()));
}
prev prev
} }

View file

@ -25,10 +25,12 @@ impl ConnectState {
self.context_to_provided_track( self.context_to_provided_track(
track, track,
transfer.current_session.context.uri.as_deref(), transfer.current_session.context.uri.as_deref(),
None,
transfer transfer
.queue .queue
.is_playing_queue .is_playing_queue
.and_then(|b| b.then_some(Provider::Queue)), .unwrap_or_default()
.then_some(Provider::Queue),
) )
} }
@ -72,7 +74,8 @@ impl ConnectState {
} }
self.clear_prev_track(); self.clear_prev_track();
self.clear_next_tracks(false); self.clear_next_tracks();
self.update_queue_revision()
} }
/// completes the transfer, loading the queue and updating metadata /// completes the transfer, loading the queue and updating metadata
@ -91,7 +94,7 @@ impl ConnectState {
self.set_active_context(context_ty); self.set_active_context(context_ty);
self.fill_up_context = context_ty; self.fill_up_context = context_ty;
let ctx = self.get_context(&self.active_context).ok(); let ctx = self.get_context(self.active_context)?;
let current_index = match transfer.current_session.current_uid.as_ref() { let current_index = match transfer.current_session.current_uid.as_ref() {
Some(uid) if track.is_queue() => Self::find_index_in_context(ctx, |c| &c.uid == uid) Some(uid) if track.is_queue() => Self::find_index_in_context(ctx, |c| &c.uid == uid)
@ -103,7 +106,7 @@ impl ConnectState {
"active track is <{}> with index {current_index:?} in {:?} context, has {} tracks", "active track is <{}> with index {current_index:?} in {:?} context, has {} tracks",
track.uri, track.uri,
self.active_context, self.active_context,
ctx.map(|c| c.tracks.len()).unwrap_or_default() ctx.tracks.len()
); );
if self.player().track.is_none() { if self.player().track.is_none() {
@ -130,6 +133,7 @@ impl ConnectState {
if let Ok(queued_track) = self.context_to_provided_track( if let Ok(queued_track) = self.context_to_provided_track(
track, track,
Some(self.context_uri()), Some(self.context_uri()),
None,
Some(Provider::Queue), Some(Provider::Queue),
) { ) {
self.add_to_queue(queued_track, false); self.add_to_queue(queued_track, false);

View file

@ -174,7 +174,9 @@ fn handle_transfer_encoding(
) -> Result<Vec<u8>, Error> { ) -> Result<Vec<u8>, Error> {
let encoding = headers.get("Transfer-Encoding").map(String::as_str); let encoding = headers.get("Transfer-Encoding").map(String::as_str);
if let Some(encoding) = encoding { if let Some(encoding) = encoding {
trace!("message was send with {encoding} encoding "); trace!("message was sent with {encoding} encoding ");
} else {
trace!("message was sent with no encoding ");
} }
if !matches!(encoding, Some("gzip")) { if !matches!(encoding, Some("gzip")) {

View file

@ -813,7 +813,7 @@ impl SpClient {
/// **will** contain the query /// **will** contain the query
/// - artists /// - artists
/// - returns 2 pages with tracks: 10 most popular tracks and latest/popular album /// - returns 2 pages with tracks: 10 most popular tracks and latest/popular album
/// - remaining pages are albums of the artists and are only provided as page_url /// - remaining pages are artist albums sorted by popularity (only provided as page_url)
/// - search /// - search
/// - is massively influenced by the provided query /// - is massively influenced by the provided query
/// - the query result shown by the search expects no query at all /// - the query result shown by the search expects no query at all

View file

@ -84,7 +84,7 @@ async fn main() {
repeat: false, repeat: false,
repeat_track: false, repeat_track: false,
// the index specifies which track in the context starts playing, in this case the first in the album // the index specifies which track in the context starts playing, in this case the first in the album
playing_track: PlayingTrack::Index(0), playing_track: PlayingTrack::Index(0).into(),
}) })
.unwrap(); .unwrap();
}); });

View file

@ -0,0 +1,2 @@
mod context;
mod player;

View file

@ -0,0 +1,13 @@
use crate::context::Context;
use protobuf::Message;
use std::hash::{Hash, Hasher};
impl Hash for Context {
fn hash<H: Hasher>(&self, state: &mut H) {
if let Ok(ctx) = self.write_to_bytes() {
ctx.hash(state)
}
}
}
impl Eq for Context {}

View file

@ -1,6 +1,6 @@
// This file is parsed by build.rs // This file is parsed by build.rs
// Each included module will be compiled from the matching .proto definition. // Each included module will be compiled from the matching .proto definition.
mod conversion; mod impl_trait;
include!(concat!(env!("OUT_DIR"), "/mod.rs")); include!(concat!(env!("OUT_DIR"), "/mod.rs"));