mirror of
https://github.com/librespot-org/librespot.git
synced 2025-10-03 09:49:31 +02:00
Re-Add ability to handle/play tracks (#1468)
* re-add support to play a set of tracks * connect: reduce some cloning * connect: derive clone for LoadRequest * apply review, improve function naming * clippy fix
This commit is contained in:
parent
e2c3ac3146
commit
8b729540f4
6 changed files with 213 additions and 85 deletions
|
@ -76,7 +76,9 @@ impl 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),
|
||||
Resolve::Context(ref ctx) => {
|
||||
ConnectState::find_valid_uri(ctx.uri.as_deref(), ctx.pages.first())
|
||||
}
|
||||
}
|
||||
.or(self.fallback.as_deref())
|
||||
}
|
||||
|
@ -260,7 +262,7 @@ impl ContextResolver {
|
|||
ContextAction::Replace => {
|
||||
let remaining = state.update_context(context, next.update);
|
||||
if let Resolve::Context(ref ctx) = next.resolve {
|
||||
state.merge_context(Some(ctx.clone()));
|
||||
state.merge_context(ctx.pages.clone().pop());
|
||||
}
|
||||
|
||||
remaining
|
||||
|
|
|
@ -5,9 +5,9 @@ use crate::{
|
|||
use std::ops::Deref;
|
||||
|
||||
/// Request for loading playback
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoadRequest {
|
||||
pub(super) context_uri: String,
|
||||
pub(super) context: PlayContext,
|
||||
pub(super) options: LoadRequestOptions,
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,14 @@ impl Deref for LoadRequest {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) enum PlayContext {
|
||||
Uri(String),
|
||||
Tracks(Vec<String>),
|
||||
}
|
||||
|
||||
/// The parameters for creating a load request
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct LoadRequestOptions {
|
||||
/// Whether the given tracks should immediately start playing, or just be initially loaded.
|
||||
pub start_playing: bool,
|
||||
|
@ -44,7 +50,7 @@ pub struct LoadRequestOptions {
|
|||
///
|
||||
/// Separated into an `enum` to exclude the other variants from being used
|
||||
/// simultaneously, as they are not compatible.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LoadContextOptions {
|
||||
/// Starts the context with options
|
||||
Options(Options),
|
||||
|
@ -56,7 +62,7 @@ pub enum LoadContextOptions {
|
|||
}
|
||||
|
||||
/// The available options that indicate how to start the context
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Options {
|
||||
/// Start the context in shuffle mode
|
||||
pub shuffle: bool,
|
||||
|
@ -80,16 +86,30 @@ impl LoadRequest {
|
|||
/// Create a load request from a `context_uri`
|
||||
///
|
||||
/// For supported `context_uri` see [`SpClient::get_context`](librespot_core::spclient::SpClient::get_context)
|
||||
///
|
||||
/// Equivalent to using [`/me/player/play`](https://developer.spotify.com/documentation/web-api/reference/start-a-users-playback)
|
||||
/// and providing `context_uri`
|
||||
pub fn from_context_uri(context_uri: String, options: LoadRequestOptions) -> Self {
|
||||
Self {
|
||||
context_uri,
|
||||
context: PlayContext::Uri(context_uri),
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a load request from a set of `tracks`
|
||||
///
|
||||
/// Equivalent to using [`/me/player/play`](https://developer.spotify.com/documentation/web-api/reference/start-a-users-playback)
|
||||
/// and providing `uris`
|
||||
pub fn from_tracks(tracks: Vec<String>, options: LoadRequestOptions) -> Self {
|
||||
Self {
|
||||
context: PlayContext::Tracks(tracks),
|
||||
options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An item that represent a track to play
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PlayingTrack {
|
||||
/// Represent the track at a given index.
|
||||
Index(u32),
|
||||
|
|
|
@ -28,9 +28,10 @@ use crate::{
|
|||
provider::IsProvider,
|
||||
{ConnectConfig, ConnectState},
|
||||
},
|
||||
LoadContextOptions, LoadRequestOptions,
|
||||
LoadContextOptions, LoadRequestOptions, PlayContext,
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
use librespot_protocol::context_page::ContextPage;
|
||||
use protobuf::MessageField;
|
||||
use std::{
|
||||
future::Future,
|
||||
|
@ -975,12 +976,21 @@ impl SpircTask {
|
|||
self.handle_transfer(transfer.data.expect("by condition checked"))?;
|
||||
return self.notify().await;
|
||||
}
|
||||
Play(play) => {
|
||||
let context_uri = play
|
||||
.context
|
||||
.uri
|
||||
.clone()
|
||||
.ok_or(SpircError::NoUri("context"))?;
|
||||
Play(mut play) => {
|
||||
let first_page = play.context.pages.pop();
|
||||
let context = match play.context.uri {
|
||||
Some(s) => PlayContext::Uri(s),
|
||||
None if !play.context.pages.is_empty() => PlayContext::Tracks(
|
||||
play.context
|
||||
.pages
|
||||
.iter()
|
||||
.cloned()
|
||||
.flat_map(|p| p.tracks)
|
||||
.flat_map(|t| t.uri)
|
||||
.collect(),
|
||||
),
|
||||
None => Err(SpircError::NoUri("context"))?,
|
||||
};
|
||||
|
||||
let context_options = play
|
||||
.options
|
||||
|
@ -989,16 +999,16 @@ impl SpircTask {
|
|||
.map(LoadContextOptions::Options);
|
||||
|
||||
self.handle_load(
|
||||
LoadRequest::from_context_uri(
|
||||
context_uri,
|
||||
LoadRequestOptions {
|
||||
LoadRequest {
|
||||
context,
|
||||
options: LoadRequestOptions {
|
||||
start_playing: true,
|
||||
seek_to: play.options.seek_to.unwrap_or_default(),
|
||||
playing_track: play.options.skip_to.and_then(|s| s.try_into().ok()),
|
||||
context_options,
|
||||
},
|
||||
),
|
||||
Some(play.context),
|
||||
},
|
||||
first_page,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -1046,6 +1056,8 @@ impl SpircTask {
|
|||
fn handle_transfer(&mut self, mut transfer: TransferState) -> Result<(), Error> {
|
||||
let mut ctx_uri = match transfer.current_session.context.uri {
|
||||
None => Err(SpircError::NoUri("transfer context"))?,
|
||||
// can apparently happen when a state is transferred stared with "uris" via the api
|
||||
Some(ref uri) if uri == "-" => String::new(),
|
||||
Some(ref uri) => uri.clone(),
|
||||
};
|
||||
|
||||
|
@ -1066,6 +1078,27 @@ impl SpircTask {
|
|||
}
|
||||
|
||||
let fallback = self.connect_state.current_track(|t| &t.uri).clone();
|
||||
let load_from_context_uri = !ctx_uri.is_empty();
|
||||
|
||||
if load_from_context_uri {
|
||||
self.context_resolver.add(ResolveContext::from_uri(
|
||||
ctx_uri.clone(),
|
||||
&fallback,
|
||||
ContextType::Default,
|
||||
ContextAction::Replace,
|
||||
));
|
||||
} else {
|
||||
self.load_context_from_tracks(
|
||||
transfer
|
||||
.current_session
|
||||
.context
|
||||
.pages
|
||||
.iter()
|
||||
.cloned()
|
||||
.flat_map(|p| p.tracks)
|
||||
.collect::<Vec<_>>(),
|
||||
)?
|
||||
}
|
||||
|
||||
self.context_resolver.add(ResolveContext::from_uri(
|
||||
ctx_uri.clone(),
|
||||
|
@ -1112,7 +1145,15 @@ impl SpircTask {
|
|||
))
|
||||
}
|
||||
|
||||
self.transfer_state = Some(transfer);
|
||||
if load_from_context_uri {
|
||||
self.transfer_state = Some(transfer);
|
||||
} else {
|
||||
let ctx = self.connect_state.get_context(ContextType::Default)?;
|
||||
let idx = ConnectState::find_index_in_context(ctx, |pt| {
|
||||
self.connect_state.current_track(|t| pt.uri == t.uri)
|
||||
})?;
|
||||
self.connect_state.reset_playback_to_position(Some(idx))?;
|
||||
}
|
||||
|
||||
self.load_track(is_playing, position.try_into()?)
|
||||
}
|
||||
|
@ -1181,61 +1222,41 @@ impl SpircTask {
|
|||
async fn handle_load(
|
||||
&mut self,
|
||||
cmd: LoadRequest,
|
||||
context: Option<Context>,
|
||||
page: Option<ContextPage>,
|
||||
) -> Result<(), Error> {
|
||||
self.connect_state
|
||||
.reset_context(ResetContext::WhenDifferent(&cmd.context_uri));
|
||||
.reset_context(if let PlayContext::Uri(ref uri) = cmd.context {
|
||||
ResetContext::WhenDifferent(uri)
|
||||
} else {
|
||||
ResetContext::Completely
|
||||
});
|
||||
|
||||
self.connect_state.reset_options();
|
||||
|
||||
if !self.connect_state.is_active() {
|
||||
self.handle_activate();
|
||||
}
|
||||
|
||||
let fallback = if let Some(ref ctx) = context {
|
||||
match ConnectState::get_context_uri_from_context(ctx) {
|
||||
Some(ctx_uri) => ctx_uri,
|
||||
None => Err(SpircError::InvalidUri(cmd.context_uri.clone()))?,
|
||||
let autoplay = matches!(cmd.context_options, Some(LoadContextOptions::Autoplay));
|
||||
match cmd.context {
|
||||
PlayContext::Uri(uri) => {
|
||||
self.load_context_from_uri(uri, page.as_ref(), autoplay)
|
||||
.await?
|
||||
}
|
||||
} else {
|
||||
&cmd.context_uri
|
||||
};
|
||||
|
||||
let update_context = if matches!(cmd.context_options, Some(LoadContextOptions::Autoplay)) {
|
||||
ContextType::Autoplay
|
||||
} else {
|
||||
ContextType::Default
|
||||
};
|
||||
|
||||
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 {
|
||||
debug!("context <{current_context_uri}> didn't change, no resolving required")
|
||||
} else {
|
||||
debug!("resolving context for load command");
|
||||
self.context_resolver.clear();
|
||||
self.context_resolver.add(ResolveContext::from_uri(
|
||||
&cmd.context_uri,
|
||||
fallback,
|
||||
update_context,
|
||||
ContextAction::Replace,
|
||||
));
|
||||
let context = self.context_resolver.get_next_context(Vec::new).await;
|
||||
self.handle_next_context(context);
|
||||
PlayContext::Tracks(tracks) => self.load_context_from_tracks(tracks)?,
|
||||
}
|
||||
|
||||
let cmd_options = cmd.options;
|
||||
|
||||
self.connect_state.set_active_context(ContextType::Default);
|
||||
|
||||
// for play commands with skip by uid, the context of the command contains
|
||||
// tracks with uri and uid, so we merge the new context with the resolved/existing context
|
||||
self.connect_state.merge_context(context);
|
||||
self.connect_state.merge_context(page);
|
||||
|
||||
// load here, so that we clear the queue only after we definitely retrieved a new context
|
||||
self.connect_state.clear_next_tracks();
|
||||
self.connect_state.clear_restrictions();
|
||||
|
||||
debug!("play track <{:?}>", cmd.playing_track);
|
||||
debug!("play track <{:?}>", cmd_options.playing_track);
|
||||
|
||||
let index = match cmd.playing_track {
|
||||
let index = match cmd_options.playing_track {
|
||||
None => None,
|
||||
Some(ref playing_track) => Some(match playing_track {
|
||||
PlayingTrack::Index(i) => *i as usize,
|
||||
|
@ -1250,7 +1271,7 @@ impl SpircTask {
|
|||
}),
|
||||
};
|
||||
|
||||
if let Some(LoadContextOptions::Options(ref options)) = cmd.context_options {
|
||||
if let Some(LoadContextOptions::Options(ref options)) = cmd_options.context_options {
|
||||
debug!(
|
||||
"loading with shuffle: <{}>, repeat track: <{}> context: <{}>",
|
||||
options.shuffle, options.repeat, options.repeat_track
|
||||
|
@ -1261,7 +1282,8 @@ impl SpircTask {
|
|||
self.connect_state.set_repeat_track(options.repeat_track);
|
||||
}
|
||||
|
||||
if matches!(cmd.context_options, Some(LoadContextOptions::Options(ref o)) if o.shuffle) {
|
||||
if matches!(cmd_options.context_options, Some(LoadContextOptions::Options(ref o)) if o.shuffle)
|
||||
{
|
||||
if let Some(index) = index {
|
||||
self.connect_state.set_current_track(index)?;
|
||||
} else {
|
||||
|
@ -1282,7 +1304,7 @@ impl SpircTask {
|
|||
}
|
||||
|
||||
if self.connect_state.current_track(MessageField::is_some) {
|
||||
self.load_track(cmd.start_playing, cmd.seek_to)?;
|
||||
self.load_track(cmd_options.start_playing, cmd_options.seek_to)?;
|
||||
} else {
|
||||
info!("No active track, stopping");
|
||||
self.handle_stop()
|
||||
|
@ -1291,6 +1313,67 @@ impl SpircTask {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_context_from_uri(
|
||||
&mut self,
|
||||
context_uri: String,
|
||||
page: Option<&ContextPage>,
|
||||
autoplay: bool,
|
||||
) -> Result<(), Error> {
|
||||
if !self.connect_state.is_active() {
|
||||
self.handle_activate();
|
||||
}
|
||||
|
||||
let update_context = if autoplay {
|
||||
ContextType::Autoplay
|
||||
} else {
|
||||
ContextType::Default
|
||||
};
|
||||
|
||||
self.connect_state.set_active_context(update_context);
|
||||
|
||||
let fallback = match page {
|
||||
// check that the uri is valid or the page has a valid uri that can be used
|
||||
Some(page) => match ConnectState::find_valid_uri(Some(&context_uri), Some(page)) {
|
||||
Some(ctx_uri) => ctx_uri,
|
||||
None => return Err(SpircError::InvalidUri(context_uri).into()),
|
||||
},
|
||||
// when there is no page, the uri should be valid
|
||||
None => &context_uri,
|
||||
};
|
||||
|
||||
let current_context_uri = self.connect_state.context_uri();
|
||||
|
||||
if current_context_uri == &context_uri && fallback == context_uri {
|
||||
debug!("context <{current_context_uri}> didn't change, no resolving required")
|
||||
} else {
|
||||
debug!("resolving context for load command");
|
||||
self.context_resolver.clear();
|
||||
self.context_resolver.add(ResolveContext::from_uri(
|
||||
&context_uri,
|
||||
fallback,
|
||||
update_context,
|
||||
ContextAction::Replace,
|
||||
));
|
||||
let context = self.context_resolver.get_next_context(Vec::new).await;
|
||||
self.handle_next_context(context);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_context_from_tracks(&mut self, tracks: impl Into<ContextPage>) -> Result<(), Error> {
|
||||
let ctx = Context {
|
||||
pages: vec![tracks.into()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _ = self
|
||||
.connect_state
|
||||
.update_context(ctx, ContextType::Default)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_play(&mut self) {
|
||||
match self.play_status {
|
||||
SpircPlayStatus::Paused {
|
||||
|
@ -1433,7 +1516,8 @@ impl SpircTask {
|
|||
let require_load_new = !self
|
||||
.connect_state
|
||||
.has_next_tracks(Some(CONTEXT_FETCH_THRESHOLD))
|
||||
&& self.session.autoplay();
|
||||
&& self.session.autoplay()
|
||||
&& !self.connect_state.context_uri().is_empty();
|
||||
|
||||
if !require_load_new {
|
||||
return;
|
||||
|
|
|
@ -116,6 +116,10 @@ impl ConnectState {
|
|||
ResetContext::Completely => {
|
||||
self.context = None;
|
||||
self.autoplay_context = None;
|
||||
|
||||
let player = self.player_mut();
|
||||
player.context_uri.clear();
|
||||
player.context_url.clear();
|
||||
}
|
||||
ResetContext::DefaultIndex => {
|
||||
for ctx in [self.context.as_mut(), self.autoplay_context.as_mut()]
|
||||
|
@ -141,14 +145,13 @@ impl ConnectState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_context_uri_from_context(context: &Context) -> Option<&str> {
|
||||
let uri = context.uri.as_deref().unwrap_or_default();
|
||||
Self::valid_resolve_uri(uri).or_else(|| {
|
||||
context
|
||||
.pages
|
||||
.first()
|
||||
.and_then(|p| p.tracks.first().and_then(|t| t.uri.as_deref()))
|
||||
})
|
||||
pub fn find_valid_uri<'s>(
|
||||
context_uri: Option<&'s str>,
|
||||
first_page: Option<&'s ContextPage>,
|
||||
) -> Option<&'s str> {
|
||||
context_uri
|
||||
.and_then(Self::valid_resolve_uri)
|
||||
.or_else(|| first_page.and_then(|p| p.tracks.first().and_then(|t| t.uri.as_deref())))
|
||||
}
|
||||
|
||||
pub fn set_active_context(&mut self, new_context: ContextType) {
|
||||
|
@ -157,7 +160,8 @@ impl ConnectState {
|
|||
let player = self.player_mut();
|
||||
|
||||
player.context_metadata = Default::default();
|
||||
player.restrictions = Some(Default::default()).into();
|
||||
player.context_restrictions = MessageField::some(Default::default());
|
||||
player.restrictions = MessageField::some(Default::default());
|
||||
|
||||
let ctx = match self.get_context(new_context) {
|
||||
Err(why) => {
|
||||
|
@ -387,16 +391,10 @@ impl ConnectState {
|
|||
.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;
|
||||
}
|
||||
|
||||
pub fn merge_context(&mut self, new_page: Option<ContextPage>) -> Option<()> {
|
||||
let current_context = self.get_context_mut(ContextType::Default).ok()?;
|
||||
let new_page = context.pages.pop()?;
|
||||
|
||||
for new_track in new_page.tracks {
|
||||
for new_track in new_page?.tracks {
|
||||
if new_track.uri.is_none() || matches!(new_track.uri, Some(ref uri) if uri.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ impl ConnectState {
|
|||
pub fn clear_restrictions(&mut self) {
|
||||
let player = self.player_mut();
|
||||
|
||||
player.restrictions.clear();
|
||||
player.context_restrictions.clear();
|
||||
player.context_restrictions = Some(Default::default()).into();
|
||||
player.restrictions = Some(Default::default()).into();
|
||||
}
|
||||
|
||||
pub fn update_restrictions(&mut self) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::context::Context;
|
||||
use crate::{context::Context, context_page::ContextPage, context_track::ContextTrack};
|
||||
use protobuf::Message;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
|
@ -11,3 +11,27 @@ impl Hash for Context {
|
|||
}
|
||||
|
||||
impl Eq for Context {}
|
||||
|
||||
impl From<Vec<String>> for ContextPage {
|
||||
fn from(value: Vec<String>) -> Self {
|
||||
ContextPage {
|
||||
tracks: value
|
||||
.into_iter()
|
||||
.map(|uri| ContextTrack {
|
||||
uri: Some(uri),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ContextTrack>> for ContextPage {
|
||||
fn from(tracks: Vec<ContextTrack>) -> Self {
|
||||
ContextPage {
|
||||
tracks,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue