diff --git a/CHANGELOG.md b/CHANGELOG.md index b2611eb0..652b4c3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/connect/src/model.rs b/connect/src/model.rs index f9165eae..8e9a5a57 100644 --- a/connect/src/model.rs +++ b/connect/src/model.rs @@ -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, diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index e3892866..6a7ad503 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -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 {}", diff --git a/connect/src/state.rs b/connect/src/state.rs index 28e57dad..02e028ce 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -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), } impl From for Error { diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index 3e9d720e..b65a4d51 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -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, - new_context_uri: Option<&str>, + new_context_uri: Option<&String>, provider: Option, ) -> 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) -> 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, ) -> Result { - 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)?; diff --git a/connect/src/state/metadata.rs b/connect/src/state/metadata.rs index d3788b22..b1effb68 100644 --- a/connect/src/state/metadata.rs +++ b/connect/src/state/metadata.rs @@ -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"; diff --git a/connect/src/state/transfer.rs b/connect/src/state/transfer.rs index c310e0b9..53d420a1 100644 --- a/connect/src/state/transfer.rs +++ b/connect/src/state/transfer.rs @@ -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 { - 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; diff --git a/core/src/dealer/protocol/request.rs b/core/src/dealer/protocol/request.rs index 67992437..86c44cf1 100644 --- a/core/src/dealer/protocol/request.rs +++ b/core/src/dealer/protocol/request.rs @@ -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; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index c818570a..42213a57 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -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) diff --git a/metadata/src/album.rs b/metadata/src/album.rs index ede9cc5b..6663b9c3 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -32,7 +32,6 @@ pub struct Album { pub label: String, pub date: Date, pub popularity: i32, - pub genres: Vec, pub covers: Images, pub external_ids: ExternalIds, pub discs: Discs, @@ -95,7 +94,6 @@ impl TryFrom<&::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()?, diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index 927846a3..6a3a63fc 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -37,7 +37,6 @@ pub struct Artist { pub singles: AlbumGroups, pub compilations: AlbumGroups, pub appears_on_albums: AlbumGroups, - pub genre: Vec, pub external_ids: ExternalIds, pub portraits: Images, pub biographies: Biographies, @@ -193,7 +192,6 @@ impl TryFrom<&::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(), diff --git a/playback/src/player.rs b/playback/src/player.rs index 6a4170f0..e9663a70 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -907,27 +907,24 @@ impl PlayerTrackLoader { fn stream_data_rate(&self, format: AudioFileFormat) -> Option { 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( diff --git a/protocol/build.rs b/protocol/build.rs index 43971bc8..a20ea22d 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -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"), diff --git a/protocol/src/conversion.rs b/protocol/src/conversion.rs new file mode 100644 index 00000000..13286e7f --- /dev/null +++ b/protocol/src/conversion.rs @@ -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, V>(map: HashMap) -> HashMap { + map.into_iter().map(|(k, v)| (k, v.into())).collect() +} + +impl From 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 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 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 for ModeRestrictions { + fn from(value: PlayerModeRestrictions) -> Self { + ModeRestrictions { + values: hashmap_into(value.values), + special_fields: value.special_fields, + } + } +} + +impl From for PlayerModeRestrictions { + fn from(value: ModeRestrictions) -> Self { + PlayerModeRestrictions { + values: hashmap_into(value.values), + special_fields: value.special_fields, + } + } +} + +impl From for RestrictionReasons { + fn from(value: PlayerRestrictionReasons) -> Self { + RestrictionReasons { + reasons: value.reasons, + special_fields: value.special_fields, + } + } +} + +impl From for PlayerRestrictionReasons { + fn from(value: RestrictionReasons) -> Self { + PlayerRestrictionReasons { + reasons: value.reasons, + special_fields: value.special_fields, + } + } +} + +impl From 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 for PlayerSuppressions { + fn from(value: Suppressions) -> Self { + PlayerSuppressions { + providers: value.providers, + special_fields: value.special_fields, + } + } +} diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 224043e7..05aef10f 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -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"));