1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-03 17:59:24 +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:
Felix Prillwitz 2025-05-04 20:29:54 +02:00 committed by GitHub
parent e2c3ac3146
commit 8b729540f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 213 additions and 85 deletions

View file

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

View file

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

View file

@ -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;

View file

@ -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;
}

View file

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

View file

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