mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
adjust code to protobuf changes
This commit is contained in:
parent
0ca58750cd
commit
ff4545dadf
15 changed files with 367 additions and 160 deletions
|
@ -28,10 +28,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
on Android platform.
|
||||
- [core] Fix "Invalid Credentials" when using a Keymaster access token and
|
||||
client ID on Android platform.
|
||||
= [connect] Fix "play" command not handled if missing "offset" property
|
||||
- [connect] Fix "play" command not handled if missing "offset" property
|
||||
|
||||
### Removed
|
||||
|
||||
- [core] Removed `get_canvases` from SpClient (breaking)
|
||||
- [metadata] Removed `genres` from Album (breaking)
|
||||
- [metadata] Removed `genre` from Artists (breaking)
|
||||
|
||||
## [0.6.0] - 2024-10-30
|
||||
|
||||
This version takes another step into the direction of the HTTP API, fixes a
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::state::ConnectState;
|
||||
use librespot_core::dealer::protocol::SkipTo;
|
||||
use librespot_protocol::player::Context;
|
||||
use librespot_protocol::context::Context;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
|
@ -77,7 +77,7 @@ impl ResolveContext {
|
|||
let fallback_uri = fallback.into();
|
||||
Self {
|
||||
context: Context {
|
||||
uri: uri.into(),
|
||||
uri: Some(uri.into()),
|
||||
..Default::default()
|
||||
},
|
||||
fallback: (!fallback_uri.is_empty()).then_some(fallback_uri),
|
||||
|
@ -114,7 +114,7 @@ impl ResolveContext {
|
|||
|
||||
Self {
|
||||
context: Context {
|
||||
uri,
|
||||
uri: Some(uri),
|
||||
..Default::default()
|
||||
},
|
||||
fallback: None,
|
||||
|
@ -134,7 +134,9 @@ impl ResolveContext {
|
|||
|
||||
/// the actual context uri
|
||||
pub fn context_uri(&self) -> &str {
|
||||
&self.context.uri
|
||||
self.context
|
||||
.uri.as_deref()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn autoplay(&self) -> bool {
|
||||
|
@ -150,7 +152,7 @@ impl Display for ResolveContext {
|
|||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"resolve_uri: <{:?}>, context_uri: <{}>, autoplay: <{}>, update: <{}>",
|
||||
"resolve_uri: <{:?}>, context_uri: <{:?}>, autoplay: <{}>, update: <{}>",
|
||||
self.resolve_uri(),
|
||||
self.context.uri,
|
||||
self.autoplay,
|
||||
|
|
|
@ -17,10 +17,11 @@ use crate::{
|
|||
protocol::{
|
||||
autoplay_context_request::AutoplayContextRequest,
|
||||
connect::{Cluster, ClusterUpdate, LogoutCommand, SetVolumeCommand},
|
||||
context::Context,
|
||||
explicit_content_pubsub::UserAttributesUpdate,
|
||||
player::{Context, TransferState},
|
||||
playlist4_external::PlaylistModificationInfo,
|
||||
social_connect_v2::{session::_host_active_device_id, SessionUpdate},
|
||||
social_connect_v2::SessionUpdate,
|
||||
transfer_state::TransferState,
|
||||
user_attributes::UserAttributesMutation,
|
||||
},
|
||||
};
|
||||
|
@ -525,14 +526,14 @@ impl SpircTask {
|
|||
let mut ctx = self.session.spclient().get_context(resolve_uri).await?;
|
||||
|
||||
if update {
|
||||
ctx.uri = context_uri.to_string();
|
||||
ctx.url = format!("context://{context_uri}");
|
||||
ctx.uri = Some(context_uri.to_string());
|
||||
ctx.url = Some(format!("context://{context_uri}"));
|
||||
|
||||
self.connect_state
|
||||
.update_context(ctx, UpdateContext::Default)?
|
||||
} else if matches!(ctx.pages.first(), Some(p) if !p.tracks.is_empty()) {
|
||||
debug!(
|
||||
"update context from single page, context {} had {} pages",
|
||||
"update context from single page, context {:?} had {} pages",
|
||||
ctx.uri,
|
||||
ctx.pages.len()
|
||||
);
|
||||
|
@ -883,7 +884,7 @@ impl SpircTask {
|
|||
let attributes: UserAttributes = update
|
||||
.pairs
|
||||
.iter()
|
||||
.map(|pair| (pair.key().to_owned(), pair.value().to_owned()))
|
||||
.map(|(key, value)| (key.to_owned(), value.to_owned()))
|
||||
.collect();
|
||||
self.session.set_user_attributes(attributes)
|
||||
}
|
||||
|
@ -998,9 +999,10 @@ impl SpircTask {
|
|||
Unknown(unknown) => Err(SpircError::UnknownEndpoint(unknown))?,
|
||||
// implicit update of the connect_state
|
||||
UpdateContext(update_context) => {
|
||||
if &update_context.context.uri != self.connect_state.context_uri() {
|
||||
if matches!(update_context.context.uri, Some(ref uri) if uri != self.connect_state.context_uri())
|
||||
{
|
||||
debug!(
|
||||
"ignoring context update for <{}>, because it isn't the current context <{}>",
|
||||
"ignoring context update for <{:?}>, because it isn't the current context <{}>",
|
||||
update_context.context.uri, self.connect_state.context_uri()
|
||||
)
|
||||
} else {
|
||||
|
@ -1020,24 +1022,26 @@ impl SpircTask {
|
|||
.options
|
||||
.player_options_override
|
||||
.as_ref()
|
||||
.map(|o| o.shuffling_context)
|
||||
.map(|o| o.shuffling_context.unwrap_or_default())
|
||||
.unwrap_or_else(|| self.connect_state.shuffling_context());
|
||||
let repeat = play
|
||||
.options
|
||||
.player_options_override
|
||||
.as_ref()
|
||||
.map(|o| o.repeating_context)
|
||||
.map(|o| o.repeating_context.unwrap_or_default())
|
||||
.unwrap_or_else(|| self.connect_state.repeat_context());
|
||||
let repeat_track = play
|
||||
.options
|
||||
.player_options_override
|
||||
.as_ref()
|
||||
.map(|o| o.repeating_track)
|
||||
.map(|o| o.repeating_track.unwrap_or_default())
|
||||
.unwrap_or_else(|| self.connect_state.repeat_track());
|
||||
|
||||
let context_uri = play.context.uri.as_ref().ok_or(SpircError::NoData)?.clone();
|
||||
|
||||
self.handle_load(
|
||||
SpircLoadCommand {
|
||||
context_uri: play.context.uri.clone(),
|
||||
context_uri,
|
||||
start_playing: true,
|
||||
seek_to: play.options.seek_to.unwrap_or_default(),
|
||||
playing_track: play.options.skip_to.unwrap_or_default().into(),
|
||||
|
@ -1088,12 +1092,13 @@ impl SpircTask {
|
|||
}
|
||||
|
||||
fn handle_transfer(&mut self, mut transfer: TransferState) -> Result<(), Error> {
|
||||
self.connect_state
|
||||
.reset_context(ResetContext::WhenDifferent(
|
||||
&transfer.current_session.context.uri,
|
||||
));
|
||||
let mut ctx_uri = match transfer.current_session.context.uri {
|
||||
None => Err(SpircError::NoData)?,
|
||||
Some(ref uri) => uri.clone(),
|
||||
};
|
||||
|
||||
let mut ctx_uri = transfer.current_session.context.uri.clone();
|
||||
self.connect_state
|
||||
.reset_context(ResetContext::WhenDifferent(&ctx_uri));
|
||||
|
||||
match self.connect_state.current_track_from_transfer(&transfer) {
|
||||
Err(why) => warn!("didn't find initial track: {why}"),
|
||||
|
@ -1118,17 +1123,18 @@ impl SpircTask {
|
|||
state.set_active(true);
|
||||
state.handle_initial_transfer(&mut transfer);
|
||||
|
||||
// update position if the track continued playing
|
||||
let position = if transfer.playback.is_paused {
|
||||
transfer.playback.position_as_of_timestamp.into()
|
||||
} else if transfer.playback.position_as_of_timestamp > 0 {
|
||||
let time_since_position_update = timestamp - transfer.playback.timestamp;
|
||||
i64::from(transfer.playback.position_as_of_timestamp) + time_since_position_update
|
||||
} else {
|
||||
0
|
||||
let transfer_timestamp = transfer.playback.timestamp.unwrap_or_default();
|
||||
let position = match transfer.playback.position_as_of_timestamp {
|
||||
Some(position) if transfer.playback.is_paused.unwrap_or_default() => position.into(),
|
||||
// update position if the track continued playing
|
||||
Some(position) if position > 0 => {
|
||||
let time_since_position_update = timestamp - transfer_timestamp;
|
||||
i64::from(position) + time_since_position_update
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let is_playing = !transfer.playback.is_paused;
|
||||
let is_playing = matches!(transfer.playback.is_paused, Some(is_playing) if is_playing);
|
||||
|
||||
if self.connect_state.current_track(|t| t.is_autoplay()) || autoplay {
|
||||
debug!("currently in autoplay context, async resolving autoplay for {ctx_uri}");
|
||||
|
@ -1537,14 +1543,7 @@ impl SpircTask {
|
|||
Some(session) => session,
|
||||
};
|
||||
|
||||
let active_device = session._host_active_device_id.take().map(|id| match id {
|
||||
_host_active_device_id::HostActiveDeviceId(id) => id,
|
||||
other => {
|
||||
warn!("unexpected active device id {other:?}");
|
||||
String::new()
|
||||
}
|
||||
});
|
||||
|
||||
let active_device = session.host_active_device_id.take();
|
||||
if matches!(active_device, Some(ref device) if device == self.session.device_id()) {
|
||||
info!(
|
||||
"session update: <{:?}> for self, current session_id {}, new session_id {}",
|
||||
|
|
|
@ -8,21 +8,25 @@ mod tracks;
|
|||
mod transfer;
|
||||
|
||||
use crate::model::SpircPlayStatus;
|
||||
use crate::state::{
|
||||
context::{ContextType, ResetContext, StateContext},
|
||||
provider::{IsProvider, Provider},
|
||||
};
|
||||
use librespot_core::{
|
||||
config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult, version,
|
||||
Error, Session,
|
||||
};
|
||||
use librespot_protocol::connect::{
|
||||
Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest,
|
||||
};
|
||||
use librespot_protocol::player::{
|
||||
ContextIndex, ContextPage, ContextPlayerOptions, PlayOrigin, PlayerState, ProvidedTrack,
|
||||
Suppressions,
|
||||
use crate::{
|
||||
core::{
|
||||
config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult,
|
||||
version, Error, Session,
|
||||
},
|
||||
protocol::{
|
||||
connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest},
|
||||
context_page::ContextPage,
|
||||
player::{
|
||||
ContextIndex, ContextPlayerOptions, PlayOrigin, PlayerState, ProvidedTrack,
|
||||
Suppressions,
|
||||
},
|
||||
},
|
||||
state::{
|
||||
context::{ContextType, ResetContext, StateContext},
|
||||
provider::{IsProvider, Provider},
|
||||
},
|
||||
};
|
||||
|
||||
use log::LevelFilter;
|
||||
use protobuf::{EnumOrUnknown, MessageField};
|
||||
use std::{
|
||||
|
@ -52,8 +56,8 @@ pub enum StateError {
|
|||
ContextHasNoTracks,
|
||||
#[error("playback of local files is not supported")]
|
||||
UnsupportedLocalPlayBack,
|
||||
#[error("track uri <{0}> contains invalid characters")]
|
||||
InvalidTrackUri(String),
|
||||
#[error("track uri <{0:?}> contains invalid characters")]
|
||||
InvalidTrackUri(Option<String>),
|
||||
}
|
||||
|
||||
impl From<StateError> for Error {
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
use crate::state::{metadata::Metadata, provider::Provider, ConnectState, StateError};
|
||||
use librespot_core::{Error, SpotifyId};
|
||||
use librespot_protocol::player::{
|
||||
Context, ContextIndex, ContextPage, ContextTrack, ProvidedTrack, Restrictions,
|
||||
use crate::{
|
||||
core::{Error, SpotifyId},
|
||||
protocol::{
|
||||
context::Context,
|
||||
context_page::ContextPage,
|
||||
context_track::ContextTrack,
|
||||
player::{ContextIndex, ProvidedTrack},
|
||||
restrictions::Restrictions,
|
||||
},
|
||||
state::{metadata::Metadata, provider::Provider, ConnectState, StateError},
|
||||
};
|
||||
use protobuf::MessageField;
|
||||
use std::collections::HashMap;
|
||||
|
@ -104,14 +110,16 @@ impl ConnectState {
|
|||
}
|
||||
|
||||
pub fn get_context_uri_from_context(context: &Context) -> Option<&String> {
|
||||
if !context.uri.starts_with(SEARCH_IDENTIFIER) {
|
||||
return Some(&context.uri);
|
||||
let context_uri = context.uri.as_ref()?;
|
||||
|
||||
if !context_uri.starts_with(SEARCH_IDENTIFIER) {
|
||||
return Some(context_uri);
|
||||
}
|
||||
|
||||
context
|
||||
.pages
|
||||
.first()
|
||||
.and_then(|p| p.tracks.first().map(|t| &t.uri))
|
||||
.and_then(|p| p.tracks.first().and_then(|t| t.uri.as_ref()))
|
||||
}
|
||||
|
||||
pub fn set_active_context(&mut self, new_context: ContextType) {
|
||||
|
@ -134,7 +142,7 @@ impl ConnectState {
|
|||
player.restrictions.clear();
|
||||
|
||||
if let Some(restrictions) = restrictions.take() {
|
||||
player.restrictions = MessageField::some(restrictions);
|
||||
player.restrictions = MessageField::some(restrictions.into());
|
||||
}
|
||||
|
||||
for (key, value) in metadata {
|
||||
|
@ -146,7 +154,7 @@ impl ConnectState {
|
|||
if context.pages.iter().all(|p| p.tracks.is_empty()) {
|
||||
error!("context didn't have any tracks: {context:#?}");
|
||||
return Err(StateError::ContextHasNoTracks.into());
|
||||
} else if context.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());
|
||||
}
|
||||
|
||||
|
@ -174,7 +182,7 @@ impl ConnectState {
|
|||
};
|
||||
|
||||
debug!(
|
||||
"updated context {ty:?} from <{}> ({} tracks) to <{}> ({} tracks)",
|
||||
"updated context {ty:?} from <{:?}> ({} tracks) to <{:?}> ({} tracks)",
|
||||
self.context_uri(),
|
||||
prev_context
|
||||
.map(|c| c.tracks.len().to_string())
|
||||
|
@ -188,14 +196,14 @@ impl ConnectState {
|
|||
let mut new_context = self.state_context_from_page(
|
||||
page,
|
||||
context.restrictions.take(),
|
||||
Some(&context.uri),
|
||||
context.uri.as_ref(),
|
||||
None,
|
||||
);
|
||||
|
||||
// when we update the same context, we should try to preserve the previous position
|
||||
// otherwise we might load the entire context twice
|
||||
if !self.context_uri().contains(SEARCH_IDENTIFIER)
|
||||
&& self.context_uri() == &context.uri
|
||||
&& matches!(context.uri, Some(ref uri) if uri == self.context_uri())
|
||||
{
|
||||
match Self::find_index_in_context(Some(&new_context), |t| {
|
||||
self.current_track(|t| &t.uri) == &t.uri
|
||||
|
@ -217,18 +225,18 @@ impl ConnectState {
|
|||
|
||||
self.context = Some(new_context);
|
||||
|
||||
if !context.url.contains(SEARCH_IDENTIFIER) {
|
||||
self.player_mut().context_url = context.url;
|
||||
if !matches!(context.url, Some(ref url) if url.contains(SEARCH_IDENTIFIER)) {
|
||||
self.player_mut().context_url = context.url.take().unwrap_or_default();
|
||||
} else {
|
||||
self.player_mut().context_url.clear()
|
||||
}
|
||||
self.player_mut().context_uri = context.uri;
|
||||
self.player_mut().context_uri = context.uri.take().unwrap_or_default();
|
||||
}
|
||||
UpdateContext::Autoplay => {
|
||||
self.autoplay_context = Some(self.state_context_from_page(
|
||||
page,
|
||||
context.restrictions.take(),
|
||||
Some(&context.uri),
|
||||
context.uri.as_ref(),
|
||||
Some(Provider::Autoplay),
|
||||
))
|
||||
}
|
||||
|
@ -241,7 +249,7 @@ impl ConnectState {
|
|||
&mut self,
|
||||
page: ContextPage,
|
||||
restrictions: Option<Restrictions>,
|
||||
new_context_uri: Option<&str>,
|
||||
new_context_uri: Option<&String>,
|
||||
provider: Option<Provider>,
|
||||
) -> StateContext {
|
||||
let new_context_uri = new_context_uri.unwrap_or(self.context_uri());
|
||||
|
@ -271,7 +279,7 @@ impl ConnectState {
|
|||
|
||||
pub fn merge_context(&mut self, context: Option<Context>) -> Option<()> {
|
||||
let mut context = context?;
|
||||
if self.context_uri() != &context.uri {
|
||||
if matches!(context.uri, Some(ref uri) if uri != self.context_uri()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
@ -279,12 +287,13 @@ impl ConnectState {
|
|||
let new_page = context.pages.pop()?;
|
||||
|
||||
for new_track in new_page.tracks {
|
||||
if new_track.uri.is_empty() {
|
||||
if new_track.uri.is_none() || matches!(new_track.uri, Some(ref uri) if uri.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_track_uri = new_track.uri.unwrap_or_default();
|
||||
if let Ok(position) =
|
||||
Self::find_index_in_context(Some(current_context), |t| t.uri == new_track.uri)
|
||||
Self::find_index_in_context(Some(current_context), |t| t.uri == new_track_uri)
|
||||
{
|
||||
let context_track = current_context.tracks.get_mut(position)?;
|
||||
|
||||
|
@ -294,8 +303,10 @@ impl ConnectState {
|
|||
}
|
||||
|
||||
// the uid provided from another context might be actual uid of an item
|
||||
if !new_track.uid.is_empty() {
|
||||
context_track.uid = new_track.uid;
|
||||
if new_track.uid.is_some()
|
||||
|| matches!(new_track.uid, Some(ref uid) if uid.is_empty())
|
||||
{
|
||||
context_track.uid = new_track.uid.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -325,19 +336,19 @@ impl ConnectState {
|
|||
context_uri: Option<&str>,
|
||||
provider: Option<Provider>,
|
||||
) -> Result<ProvidedTrack, Error> {
|
||||
let id = if !ctx_track.uri.is_empty() {
|
||||
if ctx_track.uri.contains(['?', '%']) {
|
||||
Err(StateError::InvalidTrackUri(ctx_track.uri.clone()))?
|
||||
let id = match (ctx_track.uri.as_ref(), ctx_track.gid.as_ref()) {
|
||||
(None, None) => Err(StateError::InvalidTrackUri(None))?,
|
||||
(Some(uri), _) if uri.contains(['?', '%']) => {
|
||||
Err(StateError::InvalidTrackUri(Some(uri.clone())))?
|
||||
}
|
||||
|
||||
SpotifyId::from_uri(&ctx_track.uri)?
|
||||
} else if !ctx_track.gid.is_empty() {
|
||||
SpotifyId::from_raw(&ctx_track.gid)?
|
||||
} else {
|
||||
Err(StateError::InvalidTrackUri(String::new()))?
|
||||
(Some(uri), _) if !uri.is_empty() => SpotifyId::from_uri(uri)?,
|
||||
(None, Some(gid)) if !gid.is_empty() => SpotifyId::from_raw(gid)?,
|
||||
_ => Err(StateError::InvalidTrackUri(None))?,
|
||||
};
|
||||
|
||||
let provider = if self.unavailable_uri.contains(&ctx_track.uri) {
|
||||
let uri = id.to_uri()?.replace("unknown", "track");
|
||||
|
||||
let provider = if self.unavailable_uri.contains(&uri) {
|
||||
Provider::Unavailable
|
||||
} else {
|
||||
provider.unwrap_or(Provider::Context)
|
||||
|
@ -346,11 +357,10 @@ impl ConnectState {
|
|||
// assumption: the uid is used as unique-id of any item
|
||||
// - queue resorting is done by each client and orients itself by the given uid
|
||||
// - if no uid is present, resorting doesn't work or behaves not as intended
|
||||
let uid = if ctx_track.uid.is_empty() {
|
||||
// so setting providing a unique id should allow to resort the queue
|
||||
Uuid::new_v4().as_simple().to_string()
|
||||
} else {
|
||||
ctx_track.uid.to_string()
|
||||
let uid = match ctx_track.uid.as_ref() {
|
||||
Some(uid) if !uid.is_empty() => uid.to_string(),
|
||||
// so providing a unique id should allow to resort the queue
|
||||
_ => Uuid::new_v4().as_simple().to_string(),
|
||||
};
|
||||
|
||||
let mut metadata = HashMap::new();
|
||||
|
@ -359,7 +369,7 @@ impl ConnectState {
|
|||
}
|
||||
|
||||
let mut track = ProvidedTrack {
|
||||
uri: id.to_uri()?.replace("unknown", "track"),
|
||||
uri,
|
||||
uid,
|
||||
metadata,
|
||||
provider: provider.to_string(),
|
||||
|
@ -399,12 +409,13 @@ impl ConnectState {
|
|||
};
|
||||
|
||||
if next.tracks.is_empty() {
|
||||
if next.page_url.is_empty() {
|
||||
Err(StateError::NoContext(ContextType::Default))?
|
||||
}
|
||||
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));
|
||||
return Ok(LoadNext::PageUrl(next_page_url));
|
||||
}
|
||||
|
||||
self.fill_context_from_page(next)?;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use librespot_protocol::player::{ContextTrack, ProvidedTrack};
|
||||
use librespot_protocol::{context_track::ContextTrack, player::ProvidedTrack};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const CONTEXT_URI: &str = "context_uri";
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
use crate::state::context::ContextType;
|
||||
use crate::state::metadata::Metadata;
|
||||
use crate::state::provider::{IsProvider, Provider};
|
||||
use crate::state::{ConnectState, StateError};
|
||||
use librespot_core::Error;
|
||||
use librespot_protocol::player::{ProvidedTrack, TransferState};
|
||||
use crate::{
|
||||
core::Error,
|
||||
protocol::{player::ProvidedTrack, transfer_state::TransferState},
|
||||
state::{
|
||||
context::ContextType,
|
||||
metadata::Metadata,
|
||||
provider::{IsProvider, Provider},
|
||||
{ConnectState, StateError},
|
||||
},
|
||||
};
|
||||
use protobuf::MessageField;
|
||||
|
||||
impl ConnectState {
|
||||
|
@ -11,7 +15,7 @@ impl ConnectState {
|
|||
&self,
|
||||
transfer: &TransferState,
|
||||
) -> Result<ProvidedTrack, Error> {
|
||||
let track = if transfer.queue.is_playing_queue {
|
||||
let track = if transfer.queue.is_playing_queue.unwrap_or_default() {
|
||||
transfer.queue.tracks.first()
|
||||
} else {
|
||||
transfer.playback.current_track.as_ref()
|
||||
|
@ -20,8 +24,11 @@ impl ConnectState {
|
|||
|
||||
self.context_to_provided_track(
|
||||
track,
|
||||
Some(&transfer.current_session.context.uri),
|
||||
transfer.queue.is_playing_queue.then_some(Provider::Queue),
|
||||
transfer.current_session.context.uri.as_deref(),
|
||||
transfer
|
||||
.queue
|
||||
.is_playing_queue
|
||||
.and_then(|b| b.then_some(Provider::Queue)),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -33,23 +40,22 @@ impl ConnectState {
|
|||
player.is_buffering = false;
|
||||
|
||||
if let Some(options) = transfer.options.take() {
|
||||
player.options = MessageField::some(options);
|
||||
player.options = MessageField::some(options.into());
|
||||
}
|
||||
player.is_paused = transfer.playback.is_paused;
|
||||
player.is_playing = !transfer.playback.is_paused;
|
||||
player.is_paused = transfer.playback.is_paused.unwrap_or_default();
|
||||
player.is_playing = !player.is_paused;
|
||||
|
||||
if transfer.playback.playback_speed != 0. {
|
||||
player.playback_speed = transfer.playback.playback_speed
|
||||
} else {
|
||||
player.playback_speed = 1.;
|
||||
match transfer.playback.playback_speed {
|
||||
Some(speed) if speed != 0. => player.playback_speed = speed,
|
||||
_ => player.playback_speed = 1.,
|
||||
}
|
||||
|
||||
if let Some(session) = transfer.current_session.as_mut() {
|
||||
player.play_origin = session.play_origin.take().into();
|
||||
player.suppressions = session.suppressions.take().into();
|
||||
player.play_origin = session.play_origin.take().map(Into::into).into();
|
||||
player.suppressions = session.suppressions.take().map(Into::into).into();
|
||||
|
||||
if let Some(mut ctx) = session.context.take() {
|
||||
player.restrictions = ctx.restrictions.take().into();
|
||||
player.restrictions = ctx.restrictions.take().map(Into::into).into();
|
||||
for (key, value) in ctx.metadata {
|
||||
player.context_metadata.insert(key, value);
|
||||
}
|
||||
|
@ -87,11 +93,10 @@ impl ConnectState {
|
|||
|
||||
let ctx = self.get_context(&self.active_context).ok();
|
||||
|
||||
let current_index = if track.is_queue() {
|
||||
Self::find_index_in_context(ctx, |c| c.uid == transfer.current_session.current_uid)
|
||||
.map(|i| if i > 0 { i - 1 } else { i })
|
||||
} else {
|
||||
Self::find_index_in_context(ctx, |c| c.uri == track.uri || c.uid == track.uid)
|
||||
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)
|
||||
.map(|i| if i > 0 { i - 1 } else { i }),
|
||||
_ => Self::find_index_in_context(ctx, |c| c.uri == track.uri || c.uid == track.uid),
|
||||
};
|
||||
|
||||
debug!(
|
||||
|
@ -116,7 +121,7 @@ impl ConnectState {
|
|||
);
|
||||
|
||||
for (i, track) in transfer.queue.tracks.iter().enumerate() {
|
||||
if transfer.queue.is_playing_queue && i == 0 {
|
||||
if transfer.queue.is_playing_queue.unwrap_or_default() && i == 0 {
|
||||
// if we are currently playing from the queue,
|
||||
// don't add the first queued item, because we are currently playing that item
|
||||
continue;
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
use crate::deserialize_with::*;
|
||||
use librespot_protocol::player::{
|
||||
Context, ContextPlayerOptionOverrides, PlayOrigin, ProvidedTrack, TransferState,
|
||||
use crate::{
|
||||
deserialize_with::*,
|
||||
protocol::{
|
||||
context::Context,
|
||||
context_player_options::ContextPlayerOptionOverrides,
|
||||
player::{PlayOrigin, ProvidedTrack},
|
||||
transfer_state::TransferState,
|
||||
},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
|
|
@ -10,12 +10,13 @@ use crate::{
|
|||
config::SessionConfig,
|
||||
error::ErrorKind,
|
||||
protocol::{
|
||||
canvaz::EntityCanvazRequest,
|
||||
autoplay_context_request::AutoplayContextRequest,
|
||||
clienttoken_http::{
|
||||
ChallengeAnswer, ChallengeType, ClientTokenRequest, ClientTokenRequestType,
|
||||
ClientTokenResponse, ClientTokenResponseType,
|
||||
},
|
||||
connect::PutStateRequest,
|
||||
context::Context,
|
||||
extended_metadata::BatchedEntityRequest,
|
||||
},
|
||||
token::Token,
|
||||
|
@ -32,7 +33,6 @@ use hyper::{
|
|||
HeaderMap, Method, Request,
|
||||
};
|
||||
use hyper_util::client::legacy::ResponseFuture;
|
||||
use librespot_protocol::{autoplay_context_request::AutoplayContextRequest, player::Context};
|
||||
use protobuf::{Enum, Message, MessageFull};
|
||||
use rand::RngCore;
|
||||
use sysinfo::System;
|
||||
|
@ -716,13 +716,6 @@ impl SpClient {
|
|||
// TODO: Seen-in-the-wild but unimplemented endpoints
|
||||
// - /presence-view/v1/buddylist
|
||||
|
||||
// TODO: Find endpoint for newer canvas.proto and upgrade to that.
|
||||
pub async fn get_canvases(&self, request: EntityCanvazRequest) -> SpClientResult {
|
||||
let endpoint = "/canvaz-cache/v0/canvases";
|
||||
self.request_with_protobuf(&Method::POST, endpoint, None, &request)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_extended_metadata(&self, request: BatchedEntityRequest) -> SpClientResult {
|
||||
let endpoint = "/extended-metadata/v0/extended-metadata";
|
||||
self.request_with_protobuf(&Method::POST, endpoint, None, &request)
|
||||
|
|
|
@ -32,7 +32,6 @@ pub struct Album {
|
|||
pub label: String,
|
||||
pub date: Date,
|
||||
pub popularity: i32,
|
||||
pub genres: Vec<String>,
|
||||
pub covers: Images,
|
||||
pub external_ids: ExternalIds,
|
||||
pub discs: Discs,
|
||||
|
@ -95,7 +94,6 @@ impl TryFrom<&<Self as Metadata>::Message> for Album {
|
|||
label: album.label().to_owned(),
|
||||
date: album.date.get_or_default().try_into()?,
|
||||
popularity: album.popularity(),
|
||||
genres: album.genre.to_vec(),
|
||||
covers: album.cover_group.get_or_default().into(),
|
||||
external_ids: album.external_id.as_slice().into(),
|
||||
discs: album.disc.as_slice().try_into()?,
|
||||
|
|
|
@ -37,7 +37,6 @@ pub struct Artist {
|
|||
pub singles: AlbumGroups,
|
||||
pub compilations: AlbumGroups,
|
||||
pub appears_on_albums: AlbumGroups,
|
||||
pub genre: Vec<String>,
|
||||
pub external_ids: ExternalIds,
|
||||
pub portraits: Images,
|
||||
pub biographies: Biographies,
|
||||
|
@ -193,7 +192,6 @@ impl TryFrom<&<Self as Metadata>::Message> for Artist {
|
|||
singles: artist.single_group.as_slice().try_into()?,
|
||||
compilations: artist.compilation_group.as_slice().try_into()?,
|
||||
appears_on_albums: artist.appears_on_group.as_slice().try_into()?,
|
||||
genre: artist.genre.to_vec(),
|
||||
external_ids: artist.external_id.as_slice().into(),
|
||||
portraits: artist.portrait.as_slice().into(),
|
||||
biographies: artist.biography.as_slice().into(),
|
||||
|
|
|
@ -907,27 +907,24 @@ impl PlayerTrackLoader {
|
|||
|
||||
fn stream_data_rate(&self, format: AudioFileFormat) -> Option<usize> {
|
||||
let kbps = match format {
|
||||
AudioFileFormat::OGG_VORBIS_96 => 12,
|
||||
AudioFileFormat::OGG_VORBIS_160 => 20,
|
||||
AudioFileFormat::OGG_VORBIS_320 => 40,
|
||||
AudioFileFormat::MP3_256 => 32,
|
||||
AudioFileFormat::MP3_320 => 40,
|
||||
AudioFileFormat::MP3_160 => 20,
|
||||
AudioFileFormat::MP3_96 => 12,
|
||||
AudioFileFormat::MP3_160_ENC => 20,
|
||||
AudioFileFormat::AAC_24 => 3,
|
||||
AudioFileFormat::AAC_48 => 6,
|
||||
AudioFileFormat::AAC_160 => 20,
|
||||
AudioFileFormat::AAC_320 => 40,
|
||||
AudioFileFormat::MP4_128 => 16,
|
||||
AudioFileFormat::OTHER5 => 40,
|
||||
AudioFileFormat::FLAC_FLAC => 112, // assume 900 kbit/s on average
|
||||
AudioFileFormat::UNKNOWN_FORMAT => {
|
||||
error!("Unknown stream data rate");
|
||||
return None;
|
||||
}
|
||||
AudioFileFormat::OGG_VORBIS_96 => 12.,
|
||||
AudioFileFormat::OGG_VORBIS_160 => 20.,
|
||||
AudioFileFormat::OGG_VORBIS_320 => 40.,
|
||||
AudioFileFormat::MP3_256 => 32.,
|
||||
AudioFileFormat::MP3_320 => 40.,
|
||||
AudioFileFormat::MP3_160 => 20.,
|
||||
AudioFileFormat::MP3_96 => 12.,
|
||||
AudioFileFormat::MP3_160_ENC => 20.,
|
||||
AudioFileFormat::AAC_24 => 3.,
|
||||
AudioFileFormat::AAC_48 => 6.,
|
||||
AudioFileFormat::FLAC_FLAC => 112., // assume 900 kbit/s on average
|
||||
AudioFileFormat::XHE_AAC_12 => 1.5,
|
||||
AudioFileFormat::XHE_AAC_16 => 2.,
|
||||
AudioFileFormat::XHE_AAC_24 => 3.,
|
||||
AudioFileFormat::FLAC_FLAC_24BIT => 3.,
|
||||
};
|
||||
Some(kbps * 1024)
|
||||
let data_rate: f32 = kbps * 1024.;
|
||||
Some(data_rate.ceil() as usize)
|
||||
}
|
||||
|
||||
async fn load_track(
|
||||
|
|
|
@ -17,6 +17,7 @@ fn compile() {
|
|||
|
||||
let files = &[
|
||||
proto_dir.join("connect.proto"),
|
||||
proto_dir.join("media.proto"),
|
||||
proto_dir.join("connectivity.proto"),
|
||||
proto_dir.join("devices.proto"),
|
||||
proto_dir.join("entity_extension_data.proto"),
|
||||
|
@ -27,6 +28,8 @@ fn compile() {
|
|||
proto_dir.join("playlist_annotate3.proto"),
|
||||
proto_dir.join("playlist_permission.proto"),
|
||||
proto_dir.join("playlist4_external.proto"),
|
||||
proto_dir.join("lens-model.proto"),
|
||||
proto_dir.join("signal-model.proto"),
|
||||
proto_dir.join("spotify/clienttoken/v0/clienttoken_http.proto"),
|
||||
proto_dir.join("spotify/login5/v3/challenges/code.proto"),
|
||||
proto_dir.join("spotify/login5/v3/challenges/hashcash.proto"),
|
||||
|
@ -39,6 +42,19 @@ fn compile() {
|
|||
proto_dir.join("user_attributes.proto"),
|
||||
proto_dir.join("autoplay_context_request.proto"),
|
||||
proto_dir.join("social_connect_v2.proto"),
|
||||
proto_dir.join("transfer_state.proto"),
|
||||
proto_dir.join("context_player_options.proto"),
|
||||
proto_dir.join("playback.proto"),
|
||||
proto_dir.join("play_history.proto"),
|
||||
proto_dir.join("session.proto"),
|
||||
proto_dir.join("queue.proto"),
|
||||
proto_dir.join("context_track.proto"),
|
||||
proto_dir.join("context.proto"),
|
||||
proto_dir.join("restrictions.proto"),
|
||||
proto_dir.join("context_page.proto"),
|
||||
proto_dir.join("play_origin.proto"),
|
||||
proto_dir.join("suppressions.proto"),
|
||||
proto_dir.join("instrumentation_params.proto"),
|
||||
// TODO: remove these legacy protobufs when we are on the new API completely
|
||||
proto_dir.join("authentication.proto"),
|
||||
proto_dir.join("canvaz.proto"),
|
||||
|
|
173
protocol/src/conversion.rs
Normal file
173
protocol/src/conversion.rs
Normal file
|
@ -0,0 +1,173 @@
|
|||
use crate::{
|
||||
context_player_options::ContextPlayerOptions,
|
||||
play_origin::PlayOrigin,
|
||||
player::{
|
||||
ContextPlayerOptions as PlayerContextPlayerOptions,
|
||||
ModeRestrictions as PlayerModeRestrictions, PlayOrigin as PlayerPlayOrigin,
|
||||
RestrictionReasons as PlayerRestrictionReasons, Restrictions as PlayerRestrictions,
|
||||
Suppressions as PlayerSuppressions,
|
||||
},
|
||||
restrictions::{ModeRestrictions, RestrictionReasons, Restrictions},
|
||||
suppressions::Suppressions,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn hashmap_into<T: Into<V>, V>(map: HashMap<String, T>) -> HashMap<String, V> {
|
||||
map.into_iter().map(|(k, v)| (k, v.into())).collect()
|
||||
}
|
||||
|
||||
impl From<ContextPlayerOptions> for PlayerContextPlayerOptions {
|
||||
fn from(value: ContextPlayerOptions) -> Self {
|
||||
PlayerContextPlayerOptions {
|
||||
shuffling_context: value.shuffling_context.unwrap_or_default(),
|
||||
repeating_context: value.repeating_context.unwrap_or_default(),
|
||||
repeating_track: value.repeating_track.unwrap_or_default(),
|
||||
modes: value.modes,
|
||||
playback_speed: value.playback_speed,
|
||||
special_fields: value.special_fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlayerRestrictions> for Restrictions {
|
||||
fn from(value: PlayerRestrictions) -> Self {
|
||||
Restrictions {
|
||||
disallow_pausing_reasons: value.disallow_pausing_reasons,
|
||||
disallow_resuming_reasons: value.disallow_resuming_reasons,
|
||||
disallow_seeking_reasons: value.disallow_seeking_reasons,
|
||||
disallow_peeking_prev_reasons: value.disallow_peeking_prev_reasons,
|
||||
disallow_peeking_next_reasons: value.disallow_peeking_next_reasons,
|
||||
disallow_skipping_prev_reasons: value.disallow_skipping_prev_reasons,
|
||||
disallow_skipping_next_reasons: value.disallow_skipping_next_reasons,
|
||||
disallow_toggling_repeat_context_reasons: value
|
||||
.disallow_toggling_repeat_context_reasons,
|
||||
disallow_toggling_repeat_track_reasons: value.disallow_toggling_repeat_track_reasons,
|
||||
disallow_toggling_shuffle_reasons: value.disallow_toggling_shuffle_reasons,
|
||||
disallow_set_queue_reasons: value.disallow_set_queue_reasons,
|
||||
disallow_interrupting_playback_reasons: value.disallow_interrupting_playback_reasons,
|
||||
disallow_transferring_playback_reasons: value.disallow_transferring_playback_reasons,
|
||||
disallow_remote_control_reasons: value.disallow_remote_control_reasons,
|
||||
disallow_inserting_into_next_tracks_reasons: value
|
||||
.disallow_inserting_into_next_tracks_reasons,
|
||||
disallow_inserting_into_context_tracks_reasons: value
|
||||
.disallow_inserting_into_context_tracks_reasons,
|
||||
disallow_reordering_in_next_tracks_reasons: value
|
||||
.disallow_reordering_in_next_tracks_reasons,
|
||||
disallow_reordering_in_context_tracks_reasons: value
|
||||
.disallow_reordering_in_context_tracks_reasons,
|
||||
disallow_removing_from_next_tracks_reasons: value
|
||||
.disallow_removing_from_next_tracks_reasons,
|
||||
disallow_removing_from_context_tracks_reasons: value
|
||||
.disallow_removing_from_context_tracks_reasons,
|
||||
disallow_updating_context_reasons: value.disallow_updating_context_reasons,
|
||||
disallow_add_to_queue_reasons: value.disallow_add_to_queue_reasons,
|
||||
disallow_setting_playback_speed: value.disallow_setting_playback_speed_reasons,
|
||||
disallow_setting_modes: hashmap_into(value.disallow_setting_modes),
|
||||
disallow_signals: hashmap_into(value.disallow_signals),
|
||||
special_fields: value.special_fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Restrictions> for PlayerRestrictions {
|
||||
fn from(value: Restrictions) -> Self {
|
||||
PlayerRestrictions {
|
||||
disallow_pausing_reasons: value.disallow_pausing_reasons,
|
||||
disallow_resuming_reasons: value.disallow_resuming_reasons,
|
||||
disallow_seeking_reasons: value.disallow_seeking_reasons,
|
||||
disallow_peeking_prev_reasons: value.disallow_peeking_prev_reasons,
|
||||
disallow_peeking_next_reasons: value.disallow_peeking_next_reasons,
|
||||
disallow_skipping_prev_reasons: value.disallow_skipping_prev_reasons,
|
||||
disallow_skipping_next_reasons: value.disallow_skipping_next_reasons,
|
||||
disallow_toggling_repeat_context_reasons: value
|
||||
.disallow_toggling_repeat_context_reasons,
|
||||
disallow_toggling_repeat_track_reasons: value.disallow_toggling_repeat_track_reasons,
|
||||
disallow_toggling_shuffle_reasons: value.disallow_toggling_shuffle_reasons,
|
||||
disallow_set_queue_reasons: value.disallow_set_queue_reasons,
|
||||
disallow_interrupting_playback_reasons: value.disallow_interrupting_playback_reasons,
|
||||
disallow_transferring_playback_reasons: value.disallow_transferring_playback_reasons,
|
||||
disallow_remote_control_reasons: value.disallow_remote_control_reasons,
|
||||
disallow_inserting_into_next_tracks_reasons: value
|
||||
.disallow_inserting_into_next_tracks_reasons,
|
||||
disallow_inserting_into_context_tracks_reasons: value
|
||||
.disallow_inserting_into_context_tracks_reasons,
|
||||
disallow_reordering_in_next_tracks_reasons: value
|
||||
.disallow_reordering_in_next_tracks_reasons,
|
||||
disallow_reordering_in_context_tracks_reasons: value
|
||||
.disallow_reordering_in_context_tracks_reasons,
|
||||
disallow_removing_from_next_tracks_reasons: value
|
||||
.disallow_removing_from_next_tracks_reasons,
|
||||
disallow_removing_from_context_tracks_reasons: value
|
||||
.disallow_removing_from_context_tracks_reasons,
|
||||
disallow_updating_context_reasons: value.disallow_updating_context_reasons,
|
||||
disallow_add_to_queue_reasons: value.disallow_add_to_queue_reasons,
|
||||
disallow_setting_playback_speed_reasons: value.disallow_setting_playback_speed,
|
||||
disallow_setting_modes: hashmap_into(value.disallow_setting_modes),
|
||||
disallow_signals: hashmap_into(value.disallow_signals),
|
||||
disallow_playing_reasons: vec![],
|
||||
disallow_stopping_reasons: vec![],
|
||||
special_fields: value.special_fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlayerModeRestrictions> for ModeRestrictions {
|
||||
fn from(value: PlayerModeRestrictions) -> Self {
|
||||
ModeRestrictions {
|
||||
values: hashmap_into(value.values),
|
||||
special_fields: value.special_fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ModeRestrictions> for PlayerModeRestrictions {
|
||||
fn from(value: ModeRestrictions) -> Self {
|
||||
PlayerModeRestrictions {
|
||||
values: hashmap_into(value.values),
|
||||
special_fields: value.special_fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlayerRestrictionReasons> for RestrictionReasons {
|
||||
fn from(value: PlayerRestrictionReasons) -> Self {
|
||||
RestrictionReasons {
|
||||
reasons: value.reasons,
|
||||
special_fields: value.special_fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RestrictionReasons> for PlayerRestrictionReasons {
|
||||
fn from(value: RestrictionReasons) -> Self {
|
||||
PlayerRestrictionReasons {
|
||||
reasons: value.reasons,
|
||||
special_fields: value.special_fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlayOrigin> for PlayerPlayOrigin {
|
||||
fn from(value: PlayOrigin) -> Self {
|
||||
PlayerPlayOrigin {
|
||||
feature_identifier: value.feature_identifier.unwrap_or_default(),
|
||||
feature_version: value.feature_version.unwrap_or_default(),
|
||||
view_uri: value.view_uri.unwrap_or_default(),
|
||||
external_referrer: value.external_referrer.unwrap_or_default(),
|
||||
referrer_identifier: value.referrer_identifier.unwrap_or_default(),
|
||||
device_identifier: value.device_identifier.unwrap_or_default(),
|
||||
feature_classes: value.feature_classes,
|
||||
restriction_identifier: value.restriction_identifier.unwrap_or_default(),
|
||||
special_fields: value.special_fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Suppressions> for PlayerSuppressions {
|
||||
fn from(value: Suppressions) -> Self {
|
||||
PlayerSuppressions {
|
||||
providers: value.providers,
|
||||
special_fields: value.special_fields,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
// This file is parsed by build.rs
|
||||
// Each included module will be compiled from the matching .proto definition.
|
||||
|
||||
mod conversion;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/mod.rs"));
|
||||
|
|
Loading…
Reference in a new issue