Update protobuf and related crates to 3.x (#1092)

This commit is contained in:
Lukáš Tyrychtr 2023-01-17 21:46:14 +01:00 committed by GitHub
parent 8941495b80
commit 3662302196
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 694 additions and 672 deletions

95
Cargo.lock generated
View file

@ -441,6 +441,12 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "either"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.31" version = "0.8.31"
@ -1545,7 +1551,7 @@ version = "0.5.0-dev"
dependencies = [ dependencies = [
"glob", "glob",
"protobuf", "protobuf",
"protobuf-codegen-pure", "protobuf-codegen",
] ]
[[package]] [[package]]
@ -2183,36 +2189,62 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.44" version = "1.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "2.28.0" version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e"
[[package]]
name = "protobuf-codegen"
version = "2.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6"
dependencies = [ dependencies = [
"protobuf", "once_cell",
"protobuf-support",
"thiserror",
] ]
[[package]] [[package]]
name = "protobuf-codegen-pure" name = "protobuf-codegen"
version = "2.28.0" version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a29399fc94bcd3eeaa951c715f7bea69409b2445356b00519740bcd6ddd865" checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901"
dependencies = [ dependencies = [
"anyhow",
"once_cell",
"protobuf", "protobuf",
"protobuf-codegen", "protobuf-parse",
"regex",
"tempfile",
"thiserror",
]
[[package]]
name = "protobuf-parse"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49"
dependencies = [
"anyhow",
"indexmap",
"log",
"protobuf",
"protobuf-support",
"tempfile",
"thiserror",
"which",
]
[[package]]
name = "protobuf-support"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372"
dependencies = [
"thiserror",
] ]
[[package]] [[package]]
@ -2227,9 +2259,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.21" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -2765,9 +2797,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.101" version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2838,18 +2870,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.37" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.37" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3063,9 +3095,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.4" version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
@ -3266,6 +3298,17 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "which"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b"
dependencies = [
"either",
"libc",
"once_cell",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View file

@ -12,7 +12,7 @@ edition = "2021"
form_urlencoded = "1.0" form_urlencoded = "1.0"
futures-util = "0.3" futures-util = "0.3"
log = "0.4" log = "0.4"
protobuf = "2" protobuf = "3"
rand = "0.8" rand = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View file

@ -9,7 +9,7 @@ use std::{
use futures_util::{stream::FusedStream, FutureExt, StreamExt}; use futures_util::{stream::FusedStream, FutureExt, StreamExt};
use protobuf::{self, Message}; use protobuf::{self, Message};
use rand::seq::SliceRandom; use rand::prelude::SliceRandom;
use thiserror::Error; use thiserror::Error;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
@ -149,7 +149,7 @@ impl From<SpircLoadCommand> for State {
state.set_shuffle(command.shuffle); state.set_shuffle(command.shuffle);
state.set_repeat(command.repeat); state.set_repeat(command.repeat);
state.set_playing_track_index(command.playing_track_index); state.set_playing_track_index(command.playing_track_index);
state.set_track(command.tracks.into()); state.track = command.tracks;
state state
} }
} }
@ -174,154 +174,93 @@ fn initial_state() -> State {
frame frame
} }
fn int_capability(typ: protocol::spirc::CapabilityType, val: i64) -> protocol::spirc::Capability {
let mut cap = protocol::spirc::Capability::new();
cap.set_typ(typ);
cap.intValue.push(val);
cap
}
fn initial_device_state(config: ConnectConfig) -> DeviceState { fn initial_device_state(config: ConnectConfig) -> DeviceState {
{ let mut msg = DeviceState::new();
let mut msg = DeviceState::new(); msg.set_sw_version(version::SEMVER.to_string());
msg.set_sw_version(version::SEMVER.to_string()); msg.set_is_active(false);
msg.set_is_active(false); msg.set_can_play(true);
msg.set_can_play(true); msg.set_volume(0);
msg.set_volume(0); msg.set_name(config.name);
msg.set_name(config.name); msg.capabilities.push(int_capability(
{ protocol::spirc::CapabilityType::kCanBePlayer,
let repeated = msg.mut_capabilities(); 1,
{ ));
let msg = repeated.push_default(); msg.capabilities.push(int_capability(
msg.set_typ(protocol::spirc::CapabilityType::kCanBePlayer); protocol::spirc::CapabilityType::kDeviceType,
{ config.device_type as i64,
let repeated = msg.mut_intValue(); ));
repeated.push(1) msg.capabilities.push(int_capability(
}; protocol::spirc::CapabilityType::kGaiaEqConnectId,
msg 1,
}; ));
{ // TODO: implement logout
let msg = repeated.push_default(); msg.capabilities.push(int_capability(
msg.set_typ(protocol::spirc::CapabilityType::kDeviceType); protocol::spirc::CapabilityType::kSupportsLogout,
{ 0,
let repeated = msg.mut_intValue(); ));
repeated.push(config.device_type as i64) msg.capabilities.push(int_capability(
}; protocol::spirc::CapabilityType::kIsObservable,
msg 1,
}; ));
{ msg.capabilities.push(int_capability(
let msg = repeated.push_default(); protocol::spirc::CapabilityType::kVolumeSteps,
msg.set_typ(protocol::spirc::CapabilityType::kGaiaEqConnectId); if config.has_volume_ctrl {
{ VOLUME_STEPS
let repeated = msg.mut_intValue(); } else {
repeated.push(1) 0
}; },
msg ));
}; msg.capabilities.push(int_capability(
{ protocol::spirc::CapabilityType::kSupportsPlaylistV2,
let msg = repeated.push_default(); 1,
// TODO: implement logout ));
msg.set_typ(protocol::spirc::CapabilityType::kSupportsLogout); msg.capabilities.push(int_capability(
{ protocol::spirc::CapabilityType::kSupportsExternalEpisodes,
let repeated = msg.mut_intValue(); 1,
repeated.push(0) ));
}; // TODO: how would such a rename command be triggered? Handle it.
msg msg.capabilities.push(int_capability(
}; protocol::spirc::CapabilityType::kSupportsRename,
{ 1,
let msg = repeated.push_default(); ));
msg.set_typ(protocol::spirc::CapabilityType::kIsObservable); msg.capabilities.push(int_capability(
{ protocol::spirc::CapabilityType::kCommandAcks,
let repeated = msg.mut_intValue(); 0,
repeated.push(1) ));
}; // TODO: does this mean local files or the local network?
msg // LAN may be an interesting privacy toggle.
}; msg.capabilities.push(int_capability(
{ protocol::spirc::CapabilityType::kRestrictToLocal,
let msg = repeated.push_default(); 0,
msg.set_typ(protocol::spirc::CapabilityType::kVolumeSteps); ));
{ // TODO: what does this hide, or who do we hide from?
let repeated = msg.mut_intValue(); // May be an interesting privacy toggle.
if config.has_volume_ctrl { msg.capabilities
repeated.push(VOLUME_STEPS) .push(int_capability(protocol::spirc::CapabilityType::kHidden, 0));
} else { let mut supported_types = protocol::spirc::Capability::new();
repeated.push(0) supported_types.set_typ(protocol::spirc::CapabilityType::kSupportedTypes);
} supported_types
}; .stringValue
msg .push("audio/episode".to_string());
}; supported_types
{ .stringValue
let msg = repeated.push_default(); .push("audio/episode+track".to_string());
msg.set_typ(protocol::spirc::CapabilityType::kSupportsPlaylistV2); supported_types.stringValue.push("audio/track".to_string());
{ // other known types:
let repeated = msg.mut_intValue(); // - "audio/ad"
repeated.push(1) // - "audio/interruption"
}; // - "audio/local"
msg // - "video/ad"
}; // - "video/episode"
{ msg.capabilities.push(supported_types);
let msg = repeated.push_default(); msg
msg.set_typ(protocol::spirc::CapabilityType::kSupportsExternalEpisodes);
{
let repeated = msg.mut_intValue();
repeated.push(1)
};
msg
};
{
let msg = repeated.push_default();
// TODO: how would such a rename command be triggered? Handle it.
msg.set_typ(protocol::spirc::CapabilityType::kSupportsRename);
{
let repeated = msg.mut_intValue();
repeated.push(1)
};
msg
};
{
let msg = repeated.push_default();
msg.set_typ(protocol::spirc::CapabilityType::kCommandAcks);
{
let repeated = msg.mut_intValue();
repeated.push(0)
};
msg
};
{
let msg = repeated.push_default();
// TODO: does this mean local files or the local network?
// LAN may be an interesting privacy toggle.
msg.set_typ(protocol::spirc::CapabilityType::kRestrictToLocal);
{
let repeated = msg.mut_intValue();
repeated.push(0)
};
msg
};
{
let msg = repeated.push_default();
// TODO: what does this hide, or who do we hide from?
// May be an interesting privacy toggle.
msg.set_typ(protocol::spirc::CapabilityType::kHidden);
{
let repeated = msg.mut_intValue();
repeated.push(0)
};
msg
};
{
let msg = repeated.push_default();
msg.set_typ(protocol::spirc::CapabilityType::kSupportedTypes);
{
let repeated = msg.mut_stringValue();
repeated.push("audio/episode".to_string());
repeated.push("audio/episode+track".to_string());
repeated.push("audio/track".to_string());
// other known types:
// - "audio/ad"
// - "audio/interruption"
// - "audio/local"
// - "video/ad"
// - "video/episode"
};
msg
};
};
msg
}
} }
fn url_encode(bytes: impl AsRef<[u8]>) -> String { fn url_encode(bytes: impl AsRef<[u8]>) -> String {
@ -583,8 +522,8 @@ impl SpircTask {
self.session.spclient().get_next_page(&context_uri).await self.session.spclient().get_next_page(&context_uri).await
} else { } else {
// only send previous tracks that were before the current playback position // only send previous tracks that were before the current playback position
let current_position = self.state.get_playing_track_index() as usize; let current_position = self.state.playing_track_index() as usize;
let previous_tracks = self.state.get_track()[..current_position].iter().filter_map(|t| SpotifyId::try_from(t).ok()).collect(); let previous_tracks = self.state.track[..current_position].iter().filter_map(|t| SpotifyId::try_from(t).ok()).collect();
let scope = if self.autoplay_context { let scope = if self.autoplay_context {
"stations" // this returns a `StationContext` but we deserialize it into a `PageContext` "stations" // this returns a `StationContext` but we deserialize it into a `PageContext`
@ -602,7 +541,7 @@ impl SpircTask {
info!( info!(
"Resolved {:?} tracks from <{:?}>", "Resolved {:?} tracks from <{:?}>",
context.tracks.len(), context.tracks.len(),
self.state.get_context_uri(), self.state.context_uri(),
); );
Some(context) Some(context)
} }
@ -651,7 +590,7 @@ impl SpircTask {
rx.close() rx.close()
} }
Ok(()) Ok(())
} else if self.device.get_is_active() { } else if self.device.is_active() {
trace!("Received SpircCommand::{:?}", cmd); trace!("Received SpircCommand::{:?}", cmd);
match cmd { match cmd {
SpircCommand::Play => { SpircCommand::Play => {
@ -848,16 +787,16 @@ impl SpircTask {
fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) { fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) {
trace!("Received attributes update: {:#?}", update); trace!("Received attributes update: {:#?}", update);
let attributes: UserAttributes = update let attributes: UserAttributes = update
.get_pairs() .pairs
.iter() .iter()
.map(|pair| (pair.get_key().to_owned(), pair.get_value().to_owned())) .map(|pair| (pair.key().to_owned(), pair.value().to_owned()))
.collect(); .collect();
self.session.set_user_attributes(attributes) self.session.set_user_attributes(attributes)
} }
fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) { fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) {
for attribute in mutation.get_fields().iter() { for attribute in mutation.fields.iter() {
let key = attribute.get_name(); let key = &attribute.name;
if key == "autoplay" && self.session.config().autoplay.is_some() { if key == "autoplay" && self.session.config().autoplay.is_some() {
trace!("Autoplay override active. Ignoring mutation."); trace!("Autoplay override active. Ignoring mutation.");
@ -902,30 +841,29 @@ impl SpircTask {
// First see if this update was intended for us. // First see if this update was intended for us.
let device_id = &self.ident; let device_id = &self.ident;
let ident = update.get_ident(); let ident = update.ident();
if ident == device_id if ident == device_id
|| (!update.get_recipient().is_empty() && !update.get_recipient().contains(device_id)) || (!update.recipient.is_empty() && !update.recipient.contains(device_id))
{ {
return Err(SpircError::Ident(ident.to_string()).into()); return Err(SpircError::Ident(ident.to_string()).into());
} }
let old_client_id = self.session.client_id(); let old_client_id = self.session.client_id();
for entry in update.get_device_state().get_metadata().iter() { for entry in update.device_state.metadata.iter() {
match entry.get_field_type() { match entry.type_() {
"client_id" => self.session.set_client_id(entry.get_metadata()), "client_id" => self.session.set_client_id(entry.metadata()),
"brand_display_name" => self.session.set_client_brand_name(entry.get_metadata()), "brand_display_name" => self.session.set_client_brand_name(entry.metadata()),
"model_display_name" => self.session.set_client_model_name(entry.get_metadata()), "model_display_name" => self.session.set_client_model_name(entry.metadata()),
_ => (), _ => (),
} }
} }
self.session self.session.set_client_name(update.device_state.name());
.set_client_name(update.get_device_state().get_name());
let new_client_id = self.session.client_id(); let new_client_id = self.session.client_id();
if self.device.get_is_active() && new_client_id != old_client_id { if self.device.is_active() && new_client_id != old_client_id {
self.player.emit_session_client_changed_event( self.player.emit_session_client_changed_event(
new_client_id, new_client_id,
self.session.client_name(), self.session.client_name(),
@ -934,11 +872,11 @@ impl SpircTask {
); );
} }
match update.get_typ() { match update.typ() {
MessageType::kMessageTypeHello => self.notify(Some(ident)), MessageType::kMessageTypeHello => self.notify(Some(ident)),
MessageType::kMessageTypeLoad => { MessageType::kMessageTypeLoad => {
self.handle_load(update.get_state())?; self.handle_load(update.state.get_or_default())?;
self.notify(None) self.notify(None)
} }
@ -978,7 +916,7 @@ impl SpircTask {
} }
MessageType::kMessageTypeRepeat => { MessageType::kMessageTypeRepeat => {
let repeat = update.get_state().get_repeat(); let repeat = update.state.repeat();
self.state.set_repeat(repeat); self.state.set_repeat(repeat);
self.player.emit_repeat_changed_event(repeat); self.player.emit_repeat_changed_event(repeat);
@ -987,11 +925,11 @@ impl SpircTask {
} }
MessageType::kMessageTypeShuffle => { MessageType::kMessageTypeShuffle => {
let shuffle = update.get_state().get_shuffle(); let shuffle = update.state.shuffle();
self.state.set_shuffle(shuffle); self.state.set_shuffle(shuffle);
if shuffle { if shuffle {
let current_index = self.state.get_playing_track_index(); let current_index = self.state.playing_track_index();
let tracks = self.state.mut_track(); let tracks = &mut self.state.track;
if !tracks.is_empty() { if !tracks.is_empty() {
tracks.swap(0, current_index as usize); tracks.swap(0, current_index as usize);
if let Some((_, rest)) = tracks.split_first_mut() { if let Some((_, rest)) = tracks.split_first_mut() {
@ -1007,12 +945,12 @@ impl SpircTask {
} }
MessageType::kMessageTypeSeek => { MessageType::kMessageTypeSeek => {
self.handle_seek(update.get_position()); self.handle_seek(update.position());
self.notify(None) self.notify(None)
} }
MessageType::kMessageTypeReplace => { MessageType::kMessageTypeReplace => {
let context_uri = update.get_state().get_context_uri().to_owned(); let context_uri = update.state.context_uri().to_owned();
// completely ignore local playback. // completely ignore local playback.
if context_uri.starts_with("spotify:local-files") { if context_uri.starts_with("spotify:local-files") {
@ -1020,7 +958,7 @@ impl SpircTask {
return Err(SpircError::UnsupportedLocalPlayBack.into()); return Err(SpircError::UnsupportedLocalPlayBack.into());
} }
self.update_tracks(update.get_state()); self.update_tracks(update.state.get_or_default());
if let SpircPlayStatus::Playing { if let SpircPlayStatus::Playing {
preloading_of_next_track_triggered, preloading_of_next_track_triggered,
@ -1043,15 +981,14 @@ impl SpircTask {
} }
MessageType::kMessageTypeVolume => { MessageType::kMessageTypeVolume => {
self.set_volume(update.get_volume() as u16); self.set_volume(update.volume() as u16);
self.notify(None) self.notify(None)
} }
MessageType::kMessageTypeNotify => { MessageType::kMessageTypeNotify => {
if self.device.get_is_active() if self.device.is_active()
&& update.get_device_state().get_is_active() && update.device_state.is_active()
&& self.device.get_became_active_at() && self.device.became_active_at() <= update.device_state.became_active_at()
<= update.get_device_state().get_became_active_at()
{ {
self.handle_disconnect(); self.handle_disconnect();
} }
@ -1088,7 +1025,7 @@ impl SpircTask {
); );
self.player self.player
.emit_volume_changed_event(self.device.get_volume() as u16); .emit_volume_changed_event(self.device.volume() as u16);
self.player self.player
.emit_auto_play_changed_event(self.session.autoplay()); .emit_auto_play_changed_event(self.session.autoplay());
@ -1096,19 +1033,17 @@ impl SpircTask {
self.player self.player
.emit_filter_explicit_content_changed_event(self.session.filter_explicit_content()); .emit_filter_explicit_content_changed_event(self.session.filter_explicit_content());
self.player self.player.emit_shuffle_changed_event(self.state.shuffle());
.emit_shuffle_changed_event(self.state.get_shuffle());
self.player self.player.emit_repeat_changed_event(self.state.repeat());
.emit_repeat_changed_event(self.state.get_repeat());
} }
fn handle_load(&mut self, state: &State) -> Result<(), Error> { fn handle_load(&mut self, state: &State) -> Result<(), Error> {
if !self.device.get_is_active() { if !self.device.is_active() {
self.handle_activate(); self.handle_activate();
} }
let context_uri = state.get_context_uri().to_owned(); let context_uri = state.context_uri().to_owned();
// completely ignore local playback. // completely ignore local playback.
if context_uri.starts_with("spotify:local-files") { if context_uri.starts_with("spotify:local-files") {
@ -1118,9 +1053,9 @@ impl SpircTask {
self.update_tracks(state); self.update_tracks(state);
if !self.state.get_track().is_empty() { if !self.state.track.is_empty() {
let start_playing = state.get_status() == PlayStatus::kPlayStatusPlay; let start_playing = state.status() == PlayStatus::kPlayStatusPlay;
self.load_track(start_playing, state.get_position_ms()); self.load_track(start_playing, state.position_ms());
} else { } else {
info!("No more tracks left in queue"); info!("No more tracks left in queue");
self.handle_stop(); self.handle_stop();
@ -1216,11 +1151,9 @@ impl SpircTask {
fn consume_queued_track(&mut self) -> usize { fn consume_queued_track(&mut self) -> usize {
// Removes current track if it is queued // Removes current track if it is queued
// Returns the index of the next track // Returns the index of the next track
let current_index = self.state.get_playing_track_index() as usize; let current_index = self.state.playing_track_index() as usize;
if (current_index < self.state.get_track().len()) if (current_index < self.state.track.len()) && self.state.track[current_index].queued() {
&& self.state.get_track()[current_index].get_queued() self.state.track.remove(current_index);
{
self.state.mut_track().remove(current_index);
current_index current_index
} else { } else {
current_index + 1 current_index + 1
@ -1228,7 +1161,7 @@ impl SpircTask {
} }
fn preview_next_track(&mut self) -> Option<SpotifyId> { fn preview_next_track(&mut self) -> Option<SpotifyId> {
self.get_track_id_to_play_from_playlist(self.state.get_playing_track_index() + 1) self.get_track_id_to_play_from_playlist(self.state.playing_track_index() + 1)
.map(|(track_id, _)| track_id) .map(|(track_id, _)| track_id)
} }
@ -1260,27 +1193,23 @@ impl SpircTask {
let unavailables = self.get_track_index_for_spotify_id(&track_id, 0); let unavailables = self.get_track_index_for_spotify_id(&track_id, 0);
for &index in unavailables.iter() { for &index in unavailables.iter() {
let mut unplayable_track_ref = TrackRef::new(); let mut unplayable_track_ref = TrackRef::new();
unplayable_track_ref.set_gid(self.state.get_track()[index].get_gid().to_vec()); unplayable_track_ref.set_gid(self.state.track[index].gid().to_vec());
// Misuse context field to flag the track // Misuse context field to flag the track
unplayable_track_ref.set_context(String::from("NonPlayable")); unplayable_track_ref.set_context(String::from("NonPlayable"));
std::mem::swap( std::mem::swap(&mut self.state.track[index], &mut unplayable_track_ref);
&mut self.state.mut_track()[index],
&mut unplayable_track_ref,
);
debug!( debug!(
"Marked <{:?}> at {:?} as NonPlayable", "Marked <{:?}> at {:?} as NonPlayable",
self.state.get_track()[index], self.state.track[index], index,
index,
); );
} }
self.handle_preload_next_track(); self.handle_preload_next_track();
} }
fn handle_next(&mut self) { fn handle_next(&mut self) {
let context_uri = self.state.get_context_uri().to_owned(); let context_uri = self.state.context_uri().to_owned();
let mut tracks_len = self.state.get_track().len() as u32; let mut tracks_len = self.state.track.len() as u32;
let mut new_index = self.consume_queued_track() as u32; let mut new_index = self.consume_queued_track() as u32;
let mut continue_playing = self.state.get_status() == PlayStatus::kPlayStatusPlay; let mut continue_playing = self.state.status() == PlayStatus::kPlayStatusPlay;
let update_tracks = let update_tracks =
self.autoplay_context && tracks_len - new_index < CONTEXT_FETCH_THRESHOLD; self.autoplay_context && tracks_len - new_index < CONTEXT_FETCH_THRESHOLD;
@ -1298,7 +1227,7 @@ impl SpircTask {
if let Some(ref context) = self.context { if let Some(ref context) = self.context {
self.resolve_context = Some(context.next_page_url.to_owned()); self.resolve_context = Some(context.next_page_url.to_owned());
self.update_tracks_from_context(); self.update_tracks_from_context();
tracks_len = self.state.get_track().len() as u32; tracks_len = self.state.track.len() as u32;
} }
} }
@ -1311,12 +1240,12 @@ impl SpircTask {
debug!("Starting autoplay for <{}>", context_uri); debug!("Starting autoplay for <{}>", context_uri);
// force reloading the current context with an autoplay context // force reloading the current context with an autoplay context
self.autoplay_context = true; self.autoplay_context = true;
self.resolve_context = Some(self.state.get_context_uri().to_owned()); self.resolve_context = Some(self.state.context_uri().to_owned());
self.update_tracks_from_context(); self.update_tracks_from_context();
self.player.set_auto_normalise_as_album(false); self.player.set_auto_normalise_as_album(false);
} else { } else {
new_index = 0; new_index = 0;
continue_playing &= self.state.get_repeat(); continue_playing &= self.state.repeat();
debug!("Looping back to start, repeat is {}", continue_playing); debug!("Looping back to start, repeat is {}", continue_playing);
} }
} }
@ -1342,29 +1271,29 @@ impl SpircTask {
let mut queue_tracks = Vec::new(); let mut queue_tracks = Vec::new();
{ {
let queue_index = self.consume_queued_track(); let queue_index = self.consume_queued_track();
let tracks = self.state.mut_track(); let tracks = &mut self.state.track;
while queue_index < tracks.len() && tracks[queue_index].get_queued() { while queue_index < tracks.len() && tracks[queue_index].queued() {
queue_tracks.push(tracks.remove(queue_index)); queue_tracks.push(tracks.remove(queue_index));
} }
} }
let current_index = self.state.get_playing_track_index(); let current_index = self.state.playing_track_index();
let new_index = if current_index > 0 { let new_index = if current_index > 0 {
current_index - 1 current_index - 1
} else if self.state.get_repeat() { } else if self.state.repeat() {
self.state.get_track().len() as u32 - 1 self.state.track.len() as u32 - 1
} else { } else {
0 0
}; };
// Reinsert queued tracks after the new playing track. // Reinsert queued tracks after the new playing track.
let mut pos = (new_index + 1) as usize; let mut pos = (new_index + 1) as usize;
for track in queue_tracks { for track in queue_tracks {
self.state.mut_track().insert(pos, track); self.state.track.insert(pos, track);
pos += 1; pos += 1;
} }
self.state.set_playing_track_index(new_index); self.state.set_playing_track_index(new_index);
let start_playing = self.state.get_status() == PlayStatus::kPlayStatusPlay; let start_playing = self.state.status() == PlayStatus::kPlayStatusPlay;
self.load_track(start_playing, 0); self.load_track(start_playing, 0);
} else { } else {
self.handle_seek(0); self.handle_seek(0);
@ -1372,12 +1301,12 @@ impl SpircTask {
} }
fn handle_volume_up(&mut self) { fn handle_volume_up(&mut self) {
let volume = (self.device.get_volume() as u16).saturating_add(VOLUME_STEP_SIZE); let volume = (self.device.volume() as u16).saturating_add(VOLUME_STEP_SIZE);
self.set_volume(volume); self.set_volume(volume);
} }
fn handle_volume_down(&mut self) { fn handle_volume_down(&mut self) {
let volume = (self.device.get_volume() as u16).saturating_sub(VOLUME_STEP_SIZE); let volume = (self.device.volume() as u16).saturating_sub(VOLUME_STEP_SIZE);
self.set_volume(volume); self.set_volume(volume);
} }
@ -1404,18 +1333,17 @@ impl SpircTask {
debug!("Adding {:?} tracks from context to frame", new_tracks.len()); debug!("Adding {:?} tracks from context to frame", new_tracks.len());
let mut track_vec = self.state.take_track().into_vec(); let mut track_vec = self.state.track.clone();
if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) {
track_vec.drain(0..head); track_vec.drain(0..head);
} }
track_vec.extend_from_slice(new_tracks); track_vec.extend_from_slice(new_tracks);
self.state self.state.track = track_vec;
.set_track(protobuf::RepeatedField::from_vec(track_vec));
// Update playing index // Update playing index
if let Some(new_index) = self if let Some(new_index) = self
.state .state
.get_playing_track_index() .playing_track_index()
.checked_sub(CONTEXT_TRACKS_HISTORY as u32) .checked_sub(CONTEXT_TRACKS_HISTORY as u32)
{ {
self.state.set_playing_track_index(new_index); self.state.set_playing_track_index(new_index);
@ -1428,9 +1356,9 @@ impl SpircTask {
fn update_tracks(&mut self, state: &State) { fn update_tracks(&mut self, state: &State) {
trace!("State: {:#?}", state); trace!("State: {:#?}", state);
let index = state.get_playing_track_index(); let index = state.playing_track_index();
let context_uri = state.get_context_uri(); let context_uri = state.context_uri();
let tracks = state.get_track(); let tracks = &state.track;
trace!("Frame has {:?} tracks", tracks.len()); trace!("Frame has {:?} tracks", tracks.len());
@ -1443,16 +1371,16 @@ impl SpircTask {
.set_auto_normalise_as_album(context_uri.starts_with("spotify:album:")); .set_auto_normalise_as_album(context_uri.starts_with("spotify:album:"));
self.state.set_playing_track_index(index); self.state.set_playing_track_index(index);
self.state.set_track(tracks.iter().cloned().collect()); self.state.track = tracks.to_vec();
self.state.set_context_uri(context_uri.to_owned()); self.state.set_context_uri(context_uri.to_owned());
// has_shuffle/repeat seem to always be true in these replace msgs, // has_shuffle/repeat seem to always be true in these replace msgs,
// but to replicate the behaviour of the Android client we have to // but to replicate the behaviour of the Android client we have to
// ignore false values. // ignore false values.
let state = state; let state = state;
if state.get_repeat() { if state.repeat() {
self.state.set_repeat(true); self.state.set_repeat(true);
} }
if state.get_shuffle() { if state.shuffle() {
self.state.set_shuffle(true); self.state.set_shuffle(true);
} }
} }
@ -1463,10 +1391,10 @@ impl SpircTask {
track_id: &SpotifyId, track_id: &SpotifyId,
start_index: usize, start_index: usize,
) -> Vec<usize> { ) -> Vec<usize> {
let index: Vec<usize> = self.state.get_track()[start_index..] let index: Vec<usize> = self.state.track[start_index..]
.iter() .iter()
.enumerate() .enumerate()
.filter(|&(_, track_ref)| track_ref.get_gid() == track_id.to_raw()) .filter(|&(_, track_ref)| track_ref.gid() == track_id.to_raw())
.map(|(idx, _)| start_index + idx) .map(|(idx, _)| start_index + idx)
.collect(); .collect();
index index
@ -1474,11 +1402,11 @@ impl SpircTask {
// Broken out here so we can refactor this later when we move to SpotifyObjectID or similar // Broken out here so we can refactor this later when we move to SpotifyObjectID or similar
fn track_ref_is_unavailable(&self, track_ref: &TrackRef) -> bool { fn track_ref_is_unavailable(&self, track_ref: &TrackRef) -> bool {
track_ref.get_context() == "NonPlayable" track_ref.context() == "NonPlayable"
} }
fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> { fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> {
let tracks_len = self.state.get_track().len(); let tracks_len = self.state.track.len();
// Guard against tracks_len being zero to prevent // Guard against tracks_len being zero to prevent
// 'index out of bounds: the len is 0 but the index is 0' // 'index out of bounds: the len is 0 but the index is 0'
@ -1500,7 +1428,7 @@ impl SpircTask {
// tracks in each frame either have a gid or uri (that may or may not be a valid track) // tracks in each frame either have a gid or uri (that may or may not be a valid track)
// E.g - context based frames sometimes contain tracks with <spotify:meta:page:> // E.g - context based frames sometimes contain tracks with <spotify:meta:page:>
let mut track_ref = self.state.get_track()[new_playlist_index].clone(); let mut track_ref = self.state.track[new_playlist_index].clone();
let mut track_id = SpotifyId::try_from(&track_ref); let mut track_id = SpotifyId::try_from(&track_ref);
while self.track_ref_is_unavailable(&track_ref) || track_id.is_err() { while self.track_ref_is_unavailable(&track_ref) || track_id.is_err() {
warn!( warn!(
@ -1517,7 +1445,7 @@ impl SpircTask {
warn!("No playable track found in state: {:?}", self.state); warn!("No playable track found in state: {:?}", self.state);
return None; return None;
} }
track_ref = self.state.get_track()[new_playlist_index].clone(); track_ref = self.state.track[new_playlist_index].clone();
track_id = SpotifyId::try_from(&track_ref); track_id = SpotifyId::try_from(&track_ref);
} }
@ -1528,7 +1456,7 @@ impl SpircTask {
} }
fn load_track(&mut self, start_playing: bool, position_ms: u32) { fn load_track(&mut self, start_playing: bool, position_ms: u32) {
let index = self.state.get_playing_track_index(); let index = self.state.playing_track_index();
match self.get_track_id_to_play_from_playlist(index) { match self.get_track_id_to_play_from_playlist(index) {
Some((track, index)) => { Some((track, index)) => {
@ -1556,7 +1484,7 @@ impl SpircTask {
} }
fn notify(&mut self, recipient: Option<&str>) -> Result<(), Error> { fn notify(&mut self, recipient: Option<&str>) -> Result<(), Error> {
let status = self.state.get_status(); let status = self.state.status();
// When in loading state, the Spotify UI is disabled for interaction. // When in loading state, the Spotify UI is disabled for interaction.
// On desktop this isn't so bad but on mobile it means that the bottom // On desktop this isn't so bad but on mobile it means that the bottom
@ -1575,7 +1503,7 @@ impl SpircTask {
} }
fn set_volume(&mut self, volume: u16) { fn set_volume(&mut self, volume: u16) {
let old_volume = self.device.get_volume(); let old_volume = self.device.volume();
let new_volume = volume as u32; let new_volume = volume as u32;
if old_volume != new_volume { if old_volume != new_volume {
self.device.set_volume(new_volume); self.device.set_volume(new_volume);
@ -1583,7 +1511,7 @@ impl SpircTask {
if let Some(cache) = self.session.cache() { if let Some(cache) = self.session.cache() {
cache.save_volume(volume) cache.save_volume(volume)
} }
if self.device.get_is_active() { if self.device.is_active() {
self.player.emit_volume_changed_event(volume); self.player.emit_volume_changed_event(volume);
} }
} }
@ -1612,25 +1540,25 @@ impl<'a> CommandSender<'a> {
frame.set_ident(spirc.ident.clone()); frame.set_ident(spirc.ident.clone());
frame.set_seq_nr(spirc.sequence.get()); frame.set_seq_nr(spirc.sequence.get());
frame.set_typ(cmd); frame.set_typ(cmd);
frame.set_device_state(spirc.device.clone()); *frame.device_state.mut_or_insert_default() = spirc.device.clone();
frame.set_state_update_id(spirc.now_ms()); frame.set_state_update_id(spirc.now_ms());
CommandSender { spirc, frame } CommandSender { spirc, frame }
} }
fn recipient(mut self, recipient: &'a str) -> CommandSender<'_> { fn recipient(mut self, recipient: &'a str) -> CommandSender<'_> {
self.frame.mut_recipient().push(recipient.to_owned()); self.frame.recipient.push(recipient.to_owned());
self self
} }
#[allow(dead_code)] #[allow(dead_code)]
fn state(mut self, state: protocol::spirc::State) -> CommandSender<'a> { fn state(mut self, state: protocol::spirc::State) -> CommandSender<'a> {
self.frame.set_state(state); *self.frame.state.mut_or_insert_default() = state;
self self
} }
fn send(mut self) -> Result<(), Error> { fn send(mut self) -> Result<(), Error> {
if !self.frame.has_state() && self.spirc.device.get_is_active() { if self.frame.state.is_none() && self.spirc.device.is_active() {
self.frame.set_state(self.spirc.state.clone()); *self.frame.state.mut_or_insert_default() = self.spirc.state.clone();
} }
self.spirc.sender.send(self.frame.write_to_bytes()?) self.spirc.sender.send(self.frame.write_to_bytes()?)

View file

@ -41,7 +41,7 @@ once_cell = "1"
parking_lot = { version = "0.12", features = ["deadlock_detection"] } parking_lot = { version = "0.12", features = ["deadlock_detection"] }
pbkdf2 = { version = "0.11", default-features = false, features = ["hmac"] } pbkdf2 = { version = "0.11", default-features = false, features = ["hmac"] }
priority-queue = "1.2" priority-queue = "1.2"
protobuf = "2" protobuf = "3"
quick-xml = { version = "0.23", features = ["serialize"] } quick-xml = { version = "0.23", features = ["serialize"] }
rand = "0.8" rand = "0.8"
rsa = "0.6" rsa = "0.6"

View file

@ -4,7 +4,7 @@ use aes::Aes192;
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use hmac::Hmac; use hmac::Hmac;
use pbkdf2::pbkdf2; use pbkdf2::pbkdf2;
use protobuf::ProtobufEnum; use protobuf::Enum;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use thiserror::Error; use thiserror::Error;
@ -145,7 +145,7 @@ impl Credentials {
fn serialize_protobuf_enum<T, S>(v: &T, ser: S) -> Result<S::Ok, S::Error> fn serialize_protobuf_enum<T, S>(v: &T, ser: S) -> Result<S::Ok, S::Error>
where where
T: ProtobufEnum, T: Enum,
S: serde::Serializer, S: serde::Serializer,
{ {
serde::Serialize::serialize(&v.value(), ser) serde::Serialize::serialize(&v.value(), ser)
@ -153,7 +153,7 @@ where
fn deserialize_protobuf_enum<'de, T, D>(de: D) -> Result<T, D::Error> fn deserialize_protobuf_enum<'de, T, D>(de: D) -> Result<T, D::Error>
where where
T: ProtobufEnum, T: Enum,
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
let v: i32 = serde::Deserialize::deserialize(de)?; let v: i32 = serde::Deserialize::deserialize(de)?;

View file

@ -10,8 +10,8 @@ use url::Url;
use super::{date::Date, Error, FileId, Session}; use super::{date::Date, Error, FileId, Session};
use librespot_protocol as protocol; use librespot_protocol as protocol;
use protocol::storage_resolve::storage_resolve_response::Result as StorageResolveResponse_Result;
use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage; use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage;
use protocol::storage_resolve::StorageResolveResponse_Result;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MaybeExpiringUrl(pub String, pub Option<Date>); pub struct MaybeExpiringUrl(pub String, pub Option<Date>);
@ -100,14 +100,17 @@ impl CdnUrl {
impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls { impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
type Error = crate::Error; type Error = crate::Error;
fn try_from(msg: CdnUrlMessage) -> Result<Self, Self::Error> { fn try_from(msg: CdnUrlMessage) -> Result<Self, Self::Error> {
if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) { if !matches!(
msg.result.enum_value_or_default(),
StorageResolveResponse_Result::CDN
) {
return Err(CdnUrlError::Storage.into()); return Err(CdnUrlError::Storage.into());
} }
let is_expiring = !msg.get_fileid().is_empty(); let is_expiring = !msg.fileid.is_empty();
let result = msg let result = msg
.get_cdnurl() .cdnurl
.iter() .iter()
.map(|cdn_url| { .map(|cdn_url| {
let url = Url::parse(cdn_url)?; let url = Url::parse(cdn_url)?;

View file

@ -54,16 +54,22 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
let mut accumulator = client_hello(&mut connection, gc).await?; let mut accumulator = client_hello(&mut connection, gc).await?;
let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?; let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?;
let remote_key = message let remote_key = message
.get_challenge() .challenge
.get_login_crypto_challenge() .get_or_default()
.get_diffie_hellman() .login_crypto_challenge
.get_gs() .get_or_default()
.diffie_hellman
.get_or_default()
.gs()
.to_owned(); .to_owned();
let remote_signature = message let remote_signature = message
.get_challenge() .challenge
.get_login_crypto_challenge() .get_or_default()
.get_diffie_hellman() .login_crypto_challenge
.get_gs_signature() .get_or_default()
.diffie_hellman
.get_or_default()
.gs_signature()
.to_owned(); .to_owned();
// Prevent man-in-the-middle attacks: check server signature // Prevent man-in-the-middle attacks: check server signature
@ -151,35 +157,45 @@ where
let mut packet = ClientHello::new(); let mut packet = ClientHello::new();
packet packet
.mut_build_info() .build_info
.mut_or_insert_default()
// ProductInfo won't push autoplay and perhaps other settings // ProductInfo won't push autoplay and perhaps other settings
// when set to anything else than PRODUCT_CLIENT // when set to anything else than PRODUCT_CLIENT
.set_product(protocol::keyexchange::Product::PRODUCT_CLIENT); .set_product(protocol::keyexchange::Product::PRODUCT_CLIENT);
packet packet
.mut_build_info() .build_info
.mut_product_flags() .mut_or_insert_default()
.push(PRODUCT_FLAGS); .product_flags
packet.mut_build_info().set_platform(platform); .push(PRODUCT_FLAGS.into());
packet packet
.mut_build_info() .build_info
.mut_or_insert_default()
.set_platform(platform);
packet
.build_info
.mut_or_insert_default()
.set_version(version::SPOTIFY_VERSION); .set_version(version::SPOTIFY_VERSION);
packet packet
.mut_cryptosuites_supported() .cryptosuites_supported
.push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON); .push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON.into());
packet packet
.mut_login_crypto_hello() .login_crypto_hello
.mut_diffie_hellman() .mut_or_insert_default()
.diffie_hellman
.mut_or_insert_default()
.set_gc(gc); .set_gc(gc);
packet packet
.mut_login_crypto_hello() .login_crypto_hello
.mut_diffie_hellman() .mut_or_insert_default()
.diffie_hellman
.mut_or_insert_default()
.set_server_keys_known(1); .set_server_keys_known(1);
packet.set_client_nonce(client_nonce); packet.set_client_nonce(client_nonce);
packet.set_padding(vec![0x1e]); packet.set_padding(vec![0x1e]);
let mut buffer = vec![0, 4]; let mut buffer = vec![0, 4];
let size = 2 + 4 + packet.compute_size(); let size = 2 + 4 + packet.compute_size();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?; <Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size.try_into().unwrap())?;
packet.write_to_vec(&mut buffer)?; packet.write_to_vec(&mut buffer)?;
connection.write_all(&buffer[..]).await?; connection.write_all(&buffer[..]).await?;
@ -192,15 +208,15 @@ where
{ {
let mut packet = ClientResponsePlaintext::new(); let mut packet = ClientResponsePlaintext::new();
packet packet
.mut_login_crypto_response() .login_crypto_response
.mut_diffie_hellman() .mut_or_insert_default()
.diffie_hellman
.mut_or_insert_default()
.set_hmac(challenge); .set_hmac(challenge);
packet.mut_pow_response();
packet.mut_crypto_response();
let mut buffer = vec![]; let mut buffer = vec![];
let size = 4 + packet.compute_size(); let size = 4 + packet.compute_size();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?; <Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size.try_into().unwrap())?;
packet.write_to_vec(&mut buffer)?; packet.write_to_vec(&mut buffer)?;
connection.write_all(&buffer[..]).await?; connection.write_all(&buffer[..]).await?;

View file

@ -58,7 +58,7 @@ impl From<AuthenticationError> for Error {
impl From<APLoginFailed> for AuthenticationError { impl From<APLoginFailed> for AuthenticationError {
fn from(login_failure: APLoginFailed) -> Self { fn from(login_failure: APLoginFailed) -> Self {
Self::LoginFailed(login_failure.get_error_code()) Self::LoginFailed(login_failure.error_code())
} }
} }
@ -100,25 +100,33 @@ pub async fn authenticate(
let mut packet = ClientResponseEncrypted::new(); let mut packet = ClientResponseEncrypted::new();
packet packet
.mut_login_credentials() .login_credentials
.mut_or_insert_default()
.set_username(credentials.username); .set_username(credentials.username);
packet packet
.mut_login_credentials() .login_credentials
.mut_or_insert_default()
.set_typ(credentials.auth_type); .set_typ(credentials.auth_type);
packet packet
.mut_login_credentials() .login_credentials
.mut_or_insert_default()
.set_auth_data(credentials.auth_data); .set_auth_data(credentials.auth_data);
packet.mut_system_info().set_cpu_family(cpu_family);
packet.mut_system_info().set_os(os);
packet packet
.mut_system_info() .system_info
.mut_or_insert_default()
.set_cpu_family(cpu_family);
packet.system_info.mut_or_insert_default().set_os(os);
packet
.system_info
.mut_or_insert_default()
.set_system_information_string(format!( .set_system_information_string(format!(
"librespot-{}-{}", "librespot-{}-{}",
version::SHA_SHORT, version::SHA_SHORT,
version::BUILD_ID version::BUILD_ID
)); ));
packet packet
.mut_system_info() .system_info
.mut_or_insert_default()
.set_device_id(device_id.to_string()); .set_device_id(device_id.to_string());
packet.set_version_string(format!("librespot {}", version::SEMVER)); packet.set_version_string(format!("librespot {}", version::SEMVER));
@ -136,9 +144,9 @@ pub async fn authenticate(
let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?; let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?;
let reusable_credentials = Credentials { let reusable_credentials = Credentials {
username: welcome_data.get_canonical_username().to_owned(), username: welcome_data.canonical_username().to_owned(),
auth_type: welcome_data.get_reusable_auth_credentials_type(), auth_type: welcome_data.reusable_auth_credentials_type(),
auth_data: welcome_data.get_reusable_auth_credentials().to_owned(), auth_data: welcome_data.reusable_auth_credentials().to_owned(),
}; };
Ok(reusable_credentials) Ok(reusable_credentials)

View file

@ -1,8 +1,4 @@
use std::{ use std::{convert::TryFrom, fmt::Debug, ops::Deref};
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Deref,
};
use time::{ use time::{
error::ComponentRange, format_description::well_known::Iso8601, Date as _Date, OffsetDateTime, error::ComponentRange, format_description::well_known::Iso8601, Date as _Date, OffsetDateTime,
@ -63,21 +59,17 @@ impl TryFrom<&DateMessage> for Date {
fn try_from(msg: &DateMessage) -> Result<Self, Self::Error> { fn try_from(msg: &DateMessage) -> Result<Self, Self::Error> {
// Some metadata contains a year, but no month. In that case just set January. // Some metadata contains a year, but no month. In that case just set January.
let month = if msg.has_month() { let month = if msg.has_month() {
msg.get_month() as u8 msg.month() as u8
} else { } else {
1 1
}; };
// Having no day will work, but may be unexpected: it will imply the last day // Having no day will work, but may be unexpected: it will imply the last day
// of the month before. So prevent that, and just set day 1. // of the month before. So prevent that, and just set day 1.
let day = if msg.has_day() { let day = if msg.has_day() { msg.day() as u8 } else { 1 };
msg.get_day() as u8
} else {
1
};
let date = _Date::from_calendar_date(msg.get_year(), month.try_into()?, day)?; let date = _Date::from_calendar_date(msg.year(), month.try_into()?, day)?;
let time = Time::from_hms(msg.get_hour() as u8, msg.get_minute() as u8, 0)?; let time = Time::from_hms(msg.hour() as u8, msg.minute() as u8, 0)?;
Ok(Self::from_utc(PrimitiveDateTime::new(date, time))) Ok(Self::from_utc(PrimitiveDateTime::new(date, time)))
} }
} }

View file

@ -12,7 +12,7 @@ use http::{
status::InvalidStatusCode, status::InvalidStatusCode,
uri::{InvalidUri, InvalidUriParts}, uri::{InvalidUri, InvalidUriParts},
}; };
use protobuf::ProtobufError; use protobuf::Error as ProtobufError;
use thiserror::Error; use thiserror::Error;
use tokio::sync::{ use tokio::sync::{
mpsc::error::SendError, oneshot::error::RecvError, AcquireError, TryAcquireError, mpsc::error::SendError, oneshot::error::RecvError, AcquireError, TryAcquireError,

View file

@ -39,18 +39,18 @@ impl From<&[u8]> for FileId {
} }
impl From<&protocol::metadata::Image> for FileId { impl From<&protocol::metadata::Image> for FileId {
fn from(image: &protocol::metadata::Image) -> Self { fn from(image: &protocol::metadata::Image) -> Self {
Self::from(image.get_file_id()) Self::from(image.file_id())
} }
} }
impl From<&protocol::metadata::AudioFile> for FileId { impl From<&protocol::metadata::AudioFile> for FileId {
fn from(file: &protocol::metadata::AudioFile) -> Self { fn from(file: &protocol::metadata::AudioFile) -> Self {
Self::from(file.get_file_id()) Self::from(file.file_id())
} }
} }
impl From<&protocol::metadata::VideoFile> for FileId { impl From<&protocol::metadata::VideoFile> for FileId {
fn from(video: &protocol::metadata::VideoFile) -> Self { fn from(video: &protocol::metadata::VideoFile) -> Self {
Self::from(video.get_file_id()) Self::from(video.file_id())
} }
} }

View file

@ -231,8 +231,8 @@ impl MercuryManager {
let header = protocol::mercury::Header::parse_from_bytes(&header_data)?; let header = protocol::mercury::Header::parse_from_bytes(&header_data)?;
let response = MercuryResponse { let response = MercuryResponse {
uri: header.get_uri().to_string(), uri: header.uri().to_string(),
status_code: header.get_status_code(), status_code: header.status_code(),
payload: pending.parts, payload: pending.parts,
}; };

View file

@ -1,5 +1,4 @@
use std::{ use std::{
convert::TryInto,
env::consts::OS, env::consts::OS,
fmt::Write, fmt::Write,
time::{Duration, Instant}, time::{Duration, Instant},
@ -14,7 +13,7 @@ use hyper::{
header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE},
Body, HeaderMap, Method, Request, Body, HeaderMap, Method, Request,
}; };
use protobuf::{Message, ProtobufEnum}; use protobuf::{Enum, Message, MessageFull};
use rand::RngCore; use rand::RngCore;
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use sysinfo::{System, SystemExt}; use sysinfo::{System, SystemExt};
@ -150,7 +149,7 @@ impl SpClient {
Ok(()) Ok(())
} }
async fn client_token_request(&self, message: &dyn Message) -> Result<Bytes, Error> { async fn client_token_request<M: Message>(&self, message: &M) -> Result<Bytes, Error> {
let body = message.write_to_bytes()?; let body = message.write_to_bytes()?;
let request = Request::builder() let request = Request::builder()
@ -179,10 +178,11 @@ impl SpClient {
debug!("Client token unavailable or expired, requesting new token."); debug!("Client token unavailable or expired, requesting new token.");
let mut request = ClientTokenRequest::new(); let mut request = ClientTokenRequest::new();
request.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); request.request_type = ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST.into();
let client_data = request.mut_client_data(); let client_data = request.mut_client_data();
client_data.set_client_version(spotify_version());
client_data.client_version = spotify_version();
// Current state of affairs: keymaster ID works on all tested platforms, but may be phased out, // Current state of affairs: keymaster ID works on all tested platforms, but may be phased out,
// so it seems a good idea to mimick the real clients. `self.session().client_id()` returns the // so it seems a good idea to mimick the real clients. `self.session().client_id()` returns the
@ -194,12 +194,14 @@ impl SpClient {
"macos" | "windows" => self.session().client_id(), "macos" | "windows" => self.session().client_id(),
_ => SessionConfig::default().client_id, _ => SessionConfig::default().client_id,
}; };
client_data.set_client_id(client_id); client_data.client_id = client_id;
let connectivity_data = client_data.mut_connectivity_sdk_data(); let connectivity_data = client_data.mut_connectivity_sdk_data();
connectivity_data.set_device_id(self.session().device_id().to_string()); connectivity_data.device_id = self.session().device_id().to_string();
let platform_data = connectivity_data.mut_platform_specific_data(); let platform_data = connectivity_data
.platform_specific_data
.mut_or_insert_default();
let sys = System::new(); let sys = System::new();
let os_version = sys.os_version().unwrap_or_else(|| String::from("0")); let os_version = sys.os_version().unwrap_or_else(|| String::from("0"));
@ -218,41 +220,41 @@ impl SpClient {
}; };
let windows_data = platform_data.mut_desktop_windows(); let windows_data = platform_data.mut_desktop_windows();
windows_data.set_os_version(os_version); windows_data.os_version = os_version;
windows_data.set_os_build(kernel_version); windows_data.os_build = kernel_version;
windows_data.set_platform_id(2); windows_data.platform_id = 2;
windows_data.set_unknown_value_6(9); windows_data.unknown_value_6 = 9;
windows_data.set_image_file_machine(image_file); windows_data.image_file_machine = image_file;
windows_data.set_pe_machine(pe); windows_data.pe_machine = pe;
windows_data.set_unknown_value_10(true); windows_data.unknown_value_10 = true;
} }
"ios" => { "ios" => {
let ios_data = platform_data.mut_ios(); let ios_data = platform_data.mut_ios();
ios_data.set_user_interface_idiom(0); ios_data.user_interface_idiom = 0;
ios_data.set_target_iphone_simulator(false); ios_data.target_iphone_simulator = false;
ios_data.set_hw_machine("iPhone14,5".to_string()); ios_data.hw_machine = "iPhone14,5".to_string();
ios_data.set_system_version(os_version); ios_data.system_version = os_version;
} }
"android" => { "android" => {
let android_data = platform_data.mut_android(); let android_data = platform_data.mut_android();
android_data.set_android_version(os_version); android_data.android_version = os_version;
android_data.set_api_version(31); android_data.api_version = 31;
android_data.set_device_name("Pixel".to_owned()); android_data.device_name = "Pixel".to_owned();
android_data.set_model_str("GF5KQ".to_owned()); android_data.model_str = "GF5KQ".to_owned();
android_data.set_vendor("Google".to_owned()); android_data.vendor = "Google".to_owned();
} }
"macos" => { "macos" => {
let macos_data = platform_data.mut_desktop_macos(); let macos_data = platform_data.mut_desktop_macos();
macos_data.set_system_version(os_version); macos_data.system_version = os_version;
macos_data.set_hw_model("iMac21,1".to_string()); macos_data.hw_model = "iMac21,1".to_string();
macos_data.set_compiled_cpu_type(std::env::consts::ARCH.to_string()); macos_data.compiled_cpu_type = std::env::consts::ARCH.to_string();
} }
_ => { _ => {
let linux_data = platform_data.mut_desktop_linux(); let linux_data = platform_data.mut_desktop_linux();
linux_data.set_system_name("Linux".to_string()); linux_data.system_name = "Linux".to_string();
linux_data.set_system_release(kernel_version); linux_data.system_release = kernel_version;
linux_data.set_system_version(os_version); linux_data.system_version = os_version;
linux_data.set_hardware(std::env::consts::ARCH.to_string()); linux_data.hardware = std::env::consts::ARCH.to_string();
} }
} }
@ -272,10 +274,10 @@ impl SpClient {
Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => { Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => {
debug!("Received a hash cash challenge, solving..."); debug!("Received a hash cash challenge, solving...");
let challenges = message.get_challenges().clone(); let challenges = message.challenges().clone();
let state = challenges.get_state(); let state = challenges.state;
if let Some(challenge) = challenges.challenges.first() { if let Some(challenge) = challenges.challenges.first() {
let hash_cash_challenge = challenge.get_evaluate_hashcash_parameters(); let hash_cash_challenge = challenge.evaluate_hashcash_parameters();
let ctx = vec![]; let ctx = vec![];
let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| { let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| {
@ -295,15 +297,16 @@ impl SpClient {
let suffix = hex::encode(suffix).to_uppercase(); let suffix = hex::encode(suffix).to_uppercase();
let mut answer_message = ClientTokenRequest::new(); let mut answer_message = ClientTokenRequest::new();
answer_message.set_request_type( answer_message.request_type =
ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST, ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST
); .into();
let challenge_answers = answer_message.mut_challenge_answers(); let challenge_answers = answer_message.mut_challenge_answers();
let mut challenge_answer = ChallengeAnswer::new(); let mut challenge_answer = ChallengeAnswer::new();
challenge_answer.mut_hash_cash().suffix = suffix.to_string(); challenge_answer.mut_hash_cash().suffix = suffix.to_string();
challenge_answer.ChallengeType = ChallengeType::CHALLENGE_HASH_CASH; challenge_answer.ChallengeType =
ChallengeType::CHALLENGE_HASH_CASH.into();
challenge_answers.state = state.to_string(); challenge_answers.state = state.to_string();
challenge_answers.answers.push(challenge_answer); challenge_answers.answers.push(challenge_answer);
@ -355,21 +358,21 @@ impl SpClient {
} }
}; };
let granted_token = token_response.get_granted_token(); let granted_token = token_response.granted_token();
let access_token = granted_token.get_token().to_owned(); let access_token = granted_token.token.to_owned();
self.lock(|inner| { self.lock(|inner| {
let client_token = Token { let client_token = Token {
access_token: access_token.clone(), access_token: access_token.clone(),
expires_in: Duration::from_secs( expires_in: Duration::from_secs(
granted_token granted_token
.get_refresh_after_seconds() .refresh_after_seconds
.try_into() .try_into()
.unwrap_or(7200), .unwrap_or(7200),
), ),
token_type: "client-token".to_string(), token_type: "client-token".to_string(),
scopes: granted_token scopes: granted_token
.get_domains() .domains
.iter() .iter()
.map(|d| d.domain.clone()) .map(|d| d.domain.clone())
.collect(), .collect(),
@ -384,12 +387,12 @@ impl SpClient {
Ok(access_token) Ok(access_token)
} }
pub async fn request_with_protobuf( pub async fn request_with_protobuf<M: Message + MessageFull>(
&self, &self,
method: &Method, method: &Method,
endpoint: &str, endpoint: &str,
headers: Option<HeaderMap>, headers: Option<HeaderMap>,
message: &dyn Message, message: &M,
) -> SpClientResult { ) -> SpClientResult {
let body = protobuf::text_format::print_to_string(message); let body = protobuf::text_format::print_to_string(message);

View file

@ -423,12 +423,12 @@ impl TryFrom<&Vec<u8>> for SpotifyId {
impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId { impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
type Error = crate::Error; type Error = crate::Error;
fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> { fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> {
match SpotifyId::from_raw(track.get_gid()) { match SpotifyId::from_raw(track.gid()) {
Ok(mut id) => { Ok(mut id) => {
id.item_type = SpotifyItemType::Track; id.item_type = SpotifyItemType::Track;
Ok(id) Ok(id)
} }
Err(_) => SpotifyId::from_uri(track.get_uri()), Err(_) => SpotifyId::from_uri(track.uri()),
} }
} }
} }
@ -438,7 +438,7 @@ impl TryFrom<&protocol::metadata::Album> for SpotifyId {
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> { fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
item_type: SpotifyItemType::Album, item_type: SpotifyItemType::Album,
..Self::from_raw(album.get_gid())? ..Self::from_raw(album.gid())?
}) })
} }
} }
@ -448,7 +448,7 @@ impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> { fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
item_type: SpotifyItemType::Artist, item_type: SpotifyItemType::Artist,
..Self::from_raw(artist.get_gid())? ..Self::from_raw(artist.gid())?
}) })
} }
} }
@ -458,7 +458,7 @@ impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> { fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
item_type: SpotifyItemType::Episode, item_type: SpotifyItemType::Episode,
..Self::from_raw(episode.get_gid())? ..Self::from_raw(episode.gid())?
}) })
} }
} }
@ -468,7 +468,7 @@ impl TryFrom<&protocol::metadata::Track> for SpotifyId {
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> { fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
item_type: SpotifyItemType::Track, item_type: SpotifyItemType::Track,
..Self::from_raw(track.get_gid())? ..Self::from_raw(track.gid())?
}) })
} }
} }
@ -478,7 +478,7 @@ impl TryFrom<&protocol::metadata::Show> for SpotifyId {
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> { fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
item_type: SpotifyItemType::Show, item_type: SpotifyItemType::Show,
..Self::from_raw(show.get_gid())? ..Self::from_raw(show.gid())?
}) })
} }
} }
@ -488,7 +488,7 @@ impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> { fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
item_type: SpotifyItemType::Artist, item_type: SpotifyItemType::Artist,
..Self::from_raw(artist.get_artist_gid())? ..Self::from_raw(artist.artist_gid())?
}) })
} }
} }
@ -498,7 +498,7 @@ impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> { fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
item_type: SpotifyItemType::Track, item_type: SpotifyItemType::Track,
..Self::from_uri(item.get_uri())? ..Self::from_uri(item.uri())?
}) })
} }
} }
@ -508,7 +508,7 @@ impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId { impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
type Error = crate::Error; type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> { fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
Self::try_from(item.get_revision()) Self::try_from(item.revision())
} }
} }
@ -518,7 +518,7 @@ impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
fn try_from( fn try_from(
playlist: &protocol::playlist4_external::SelectedListContent, playlist: &protocol::playlist4_external::SelectedListContent,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
Self::try_from(playlist.get_revision()) Self::try_from(playlist.revision())
} }
} }
@ -530,7 +530,7 @@ impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
fn try_from( fn try_from(
picture: &protocol::playlist_annotate3::TranscodedPicture, picture: &protocol::playlist_annotate3::TranscodedPicture,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
Self::from_base62(picture.get_uri()) Self::from_base62(picture.uri())
} }
} }

View file

@ -13,7 +13,7 @@ async-trait = "0.1"
byteorder = "1" byteorder = "1"
bytes = "1" bytes = "1"
log = "0.4" log = "0.4"
protobuf = "2" protobuf = "3"
thiserror = "1" thiserror = "1"
uuid = { version = "1", default-features = false } uuid = { version = "1", default-features = false }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -21,7 +21,7 @@ use crate::{
use librespot_core::{date::Date, Error, Session, SpotifyId}; use librespot_core::{date::Date, Error, Session, SpotifyId};
use librespot_protocol as protocol; use librespot_protocol as protocol;
pub use protocol::metadata::Album_Type as AlbumType; pub use protocol::metadata::album::Type as AlbumType;
use protocol::metadata::Disc as DiscMessage; use protocol::metadata::Disc as DiscMessage;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -90,26 +90,26 @@ impl TryFrom<&<Self as Metadata>::Message> for Album {
fn try_from(album: &<Self as Metadata>::Message) -> Result<Self, Self::Error> { fn try_from(album: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
id: album.try_into()?, id: album.try_into()?,
name: album.get_name().to_owned(), name: album.name().to_owned(),
artists: album.get_artist().try_into()?, artists: album.artist.as_slice().try_into()?,
album_type: album.get_field_type(), album_type: album.type_(),
label: album.get_label().to_owned(), label: album.label().to_owned(),
date: album.get_date().try_into()?, date: album.date.get_or_default().try_into()?,
popularity: album.get_popularity(), popularity: album.popularity(),
genres: album.get_genre().to_vec(), genres: album.genre.to_vec(),
covers: album.get_cover_group().into(), covers: album.cover_group.get_or_default().into(),
external_ids: album.get_external_id().into(), external_ids: album.external_id.as_slice().into(),
discs: album.get_disc().try_into()?, discs: album.disc.as_slice().try_into()?,
reviews: album.get_review().to_vec(), reviews: album.review.to_vec(),
copyrights: album.get_copyright().into(), copyrights: album.copyright.as_slice().into(),
restrictions: album.get_restriction().into(), restrictions: album.restriction.as_slice().into(),
related: album.get_related().try_into()?, related: album.related.as_slice().try_into()?,
sale_periods: album.get_sale_period().try_into()?, sale_periods: album.sale_period.as_slice().try_into()?,
cover_group: album.get_cover_group().get_image().into(), cover_group: album.cover_group.image.as_slice().into(),
original_title: album.get_original_title().to_owned(), original_title: album.original_title().to_owned(),
version_title: album.get_version_title().to_owned(), version_title: album.version_title().to_owned(),
type_str: album.get_type_str().to_owned(), type_str: album.type_str().to_owned(),
availability: album.get_availability().try_into()?, availability: album.availability.as_slice().try_into()?,
}) })
} }
} }
@ -120,9 +120,9 @@ impl TryFrom<&DiscMessage> for Disc {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(disc: &DiscMessage) -> Result<Self, Self::Error> { fn try_from(disc: &DiscMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
number: disc.get_number(), number: disc.number(),
name: disc.get_name().to_owned(), name: disc.name().to_owned(),
tracks: disc.get_track().try_into()?, tracks: disc.track.as_slice().try_into()?,
}) })
} }
} }

View file

@ -20,7 +20,7 @@ use crate::{
use librespot_core::{Error, Session, SpotifyId}; use librespot_core::{Error, Session, SpotifyId};
use librespot_protocol as protocol; use librespot_protocol as protocol;
pub use protocol::metadata::ArtistWithRole_ArtistRole as ArtistRole; pub use protocol::metadata::artist_with_role::ArtistRole;
use protocol::metadata::ActivityPeriod as ActivityPeriodMessage; use protocol::metadata::ActivityPeriod as ActivityPeriodMessage;
use protocol::metadata::AlbumGroup as AlbumGroupMessage; use protocol::metadata::AlbumGroup as AlbumGroupMessage;
@ -187,24 +187,29 @@ impl TryFrom<&<Self as Metadata>::Message> for Artist {
fn try_from(artist: &<Self as Metadata>::Message) -> Result<Self, Self::Error> { fn try_from(artist: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
id: artist.try_into()?, id: artist.try_into()?,
name: artist.get_name().to_owned(), name: artist.name().to_owned(),
popularity: artist.get_popularity(), popularity: artist.popularity(),
top_tracks: artist.get_top_track().try_into()?, top_tracks: artist.top_track.as_slice().try_into()?,
albums: artist.get_album_group().try_into()?, albums: artist.album_group.as_slice().try_into()?,
singles: artist.get_single_group().try_into()?, singles: artist.single_group.as_slice().try_into()?,
compilations: artist.get_compilation_group().try_into()?, compilations: artist.compilation_group.as_slice().try_into()?,
appears_on_albums: artist.get_appears_on_group().try_into()?, appears_on_albums: artist.appears_on_group.as_slice().try_into()?,
genre: artist.get_genre().to_vec(), genre: artist.genre.to_vec(),
external_ids: artist.get_external_id().into(), external_ids: artist.external_id.as_slice().into(),
portraits: artist.get_portrait().into(), portraits: artist.portrait.as_slice().into(),
biographies: artist.get_biography().into(), biographies: artist.biography.as_slice().into(),
activity_periods: artist.get_activity_period().try_into()?, activity_periods: artist.activity_period.as_slice().try_into()?,
restrictions: artist.get_restriction().into(), restrictions: artist.restriction.as_slice().into(),
related: artist.get_related().try_into()?, related: artist.related.as_slice().try_into()?,
is_portrait_album_cover: artist.get_is_portrait_album_cover(), is_portrait_album_cover: artist.is_portrait_album_cover(),
portrait_group: artist.get_portrait_group().get_image().into(), portrait_group: artist
sales_periods: artist.get_sale_period().try_into()?, .portrait_group
availabilities: artist.get_availability().try_into()?, .get_or_default()
.image
.as_slice()
.into(),
sales_periods: artist.sale_period.as_slice().try_into()?,
availabilities: artist.availability.as_slice().try_into()?,
}) })
} }
} }
@ -216,8 +221,8 @@ impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole {
fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result<Self, Self::Error> { fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
id: artist_with_role.try_into()?, id: artist_with_role.try_into()?,
name: artist_with_role.get_artist_name().to_owned(), name: artist_with_role.artist_name().to_owned(),
role: artist_with_role.get_role(), role: artist_with_role.role(),
}) })
} }
} }
@ -228,8 +233,8 @@ impl TryFrom<&TopTracksMessage> for TopTracks {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(top_tracks: &TopTracksMessage) -> Result<Self, Self::Error> { fn try_from(top_tracks: &TopTracksMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
country: top_tracks.get_country().to_owned(), country: top_tracks.country().to_owned(),
tracks: top_tracks.get_track().try_into()?, tracks: top_tracks.track.as_slice().try_into()?,
}) })
} }
} }
@ -239,7 +244,7 @@ impl_try_from_repeated!(TopTracksMessage, CountryTopTracks);
impl TryFrom<&AlbumGroupMessage> for AlbumGroup { impl TryFrom<&AlbumGroupMessage> for AlbumGroup {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(album_groups: &AlbumGroupMessage) -> Result<Self, Self::Error> { fn try_from(album_groups: &AlbumGroupMessage) -> Result<Self, Self::Error> {
Ok(Self(album_groups.get_album().try_into()?)) Ok(Self(album_groups.album.as_slice().try_into()?))
} }
} }
@ -257,14 +262,14 @@ impl_try_from_repeated!(AlbumGroupMessage, AlbumGroups);
impl From<&BiographyMessage> for Biography { impl From<&BiographyMessage> for Biography {
fn from(biography: &BiographyMessage) -> Self { fn from(biography: &BiographyMessage) -> Self {
let portrait_group = biography let portrait_group = biography
.get_portrait_group() .portrait_group
.iter() .iter()
.map(|it| it.get_image().into()) .map(|it| it.image.as_slice().into())
.collect(); .collect();
Self { Self {
text: biography.get_text().to_owned(), text: biography.text().to_owned(),
portraits: biography.get_portrait().into(), portraits: biography.portrait.as_slice().into(),
portrait_group, portrait_group,
} }
} }
@ -282,11 +287,11 @@ impl TryFrom<&ActivityPeriodMessage> for ActivityPeriod {
period.has_end_year(), period.has_end_year(),
) { ) {
// (decade, start_year, end_year) // (decade, start_year, end_year)
(true, false, false) => Self::Decade(period.get_decade().try_into()?), (true, false, false) => Self::Decade(period.decade().try_into()?),
(false, true, closed_period) => Self::Timespan { (false, true, closed_period) => Self::Timespan {
start_year: period.get_start_year().try_into()?, start_year: period.start_year().try_into()?,
end_year: closed_period end_year: closed_period
.then(|| period.get_end_year().try_into()) .then(|| period.end_year().try_into())
.transpose()?, .transpose()?,
}, },
_ => { _ => {

View file

@ -7,8 +7,8 @@ use std::{
use librespot_core::FileId; use librespot_core::FileId;
use librespot_protocol as protocol; use librespot_protocol as protocol;
pub use protocol::metadata::audio_file::Format as AudioFileFormat;
use protocol::metadata::AudioFile as AudioFileMessage; use protocol::metadata::AudioFile as AudioFileMessage;
pub use protocol::metadata::AudioFile_Format as AudioFileFormat;
use crate::util::impl_deref_wrapped; use crate::util::impl_deref_wrapped;
@ -45,12 +45,12 @@ impl AudioFiles {
impl From<&[AudioFileMessage]> for AudioFiles { impl From<&[AudioFileMessage]> for AudioFiles {
fn from(files: &[AudioFileMessage]) -> Self { fn from(files: &[AudioFileMessage]) -> Self {
let audio_files = files let audio_files: HashMap<AudioFileFormat, FileId> = files
.iter() .iter()
.filter_map(|file| { .filter_map(|file| {
let file_id = FileId::from(file.get_file_id()); let file_id = FileId::from(file.file_id());
if file.has_format() { if file.has_format() {
Some((file.get_format(), file_id)) Some((file.format(), file_id))
} else { } else {
trace!("Ignoring file <{}> with unspecified format", file_id); trace!("Ignoring file <{}> with unspecified format", file_id);
None None

View file

@ -1,5 +1,5 @@
use std::{ use std::{
convert::{TryFrom, TryInto}, convert::TryFrom,
fmt::Debug, fmt::Debug,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
@ -42,8 +42,8 @@ impl TryFrom<&AvailabilityMessage> for Availability {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(availability: &AvailabilityMessage) -> Result<Self, Self::Error> { fn try_from(availability: &AvailabilityMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
catalogue_strs: availability.get_catalogue_str().to_vec(), catalogue_strs: availability.catalogue_str.to_vec(),
start: availability.get_start().try_into()?, start: availability.start.get_or_default().try_into()?,
}) })
} }
} }

View file

@ -22,8 +22,8 @@ impl_deref_wrapped!(ContentRatings, Vec<ContentRating>);
impl From<&ContentRatingMessage> for ContentRating { impl From<&ContentRatingMessage> for ContentRating {
fn from(content_rating: &ContentRatingMessage) -> Self { fn from(content_rating: &ContentRatingMessage) -> Self {
Self { Self {
country: content_rating.get_country().to_owned(), country: content_rating.country().to_owned(),
tags: content_rating.get_tag().to_vec(), tags: content_rating.tag.to_vec(),
} }
} }
} }

View file

@ -6,8 +6,8 @@ use std::{
use crate::util::{impl_deref_wrapped, impl_from_repeated}; use crate::util::{impl_deref_wrapped, impl_from_repeated};
use librespot_protocol as protocol; use librespot_protocol as protocol;
pub use protocol::metadata::copyright::Type as CopyrightType;
use protocol::metadata::Copyright as CopyrightMessage; use protocol::metadata::Copyright as CopyrightMessage;
pub use protocol::metadata::Copyright_Type as CopyrightType;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Copyright { pub struct Copyright {
@ -23,8 +23,8 @@ impl_deref_wrapped!(Copyrights, Vec<Copyright>);
impl From<&CopyrightMessage> for Copyright { impl From<&CopyrightMessage> for Copyright {
fn from(copyright: &CopyrightMessage) -> Self { fn from(copyright: &CopyrightMessage) -> Self {
Self { Self {
copyright_type: copyright.get_field_type(), copyright_type: copyright.type_(),
text: copyright.get_text().to_owned(), text: copyright.text().to_owned(),
} }
} }
} }

View file

@ -19,7 +19,7 @@ use crate::{
use librespot_core::{date::Date, Error, Session, SpotifyId}; use librespot_core::{date::Date, Error, Session, SpotifyId};
use librespot_protocol as protocol; use librespot_protocol as protocol;
pub use protocol::metadata::Episode_EpisodeType as EpisodeType; pub use protocol::metadata::episode::EpisodeType;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Episode { pub struct Episode {
@ -72,29 +72,29 @@ impl TryFrom<&<Self as Metadata>::Message> for Episode {
fn try_from(episode: &<Self as Metadata>::Message) -> Result<Self, Self::Error> { fn try_from(episode: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
id: episode.try_into()?, id: episode.try_into()?,
name: episode.get_name().to_owned(), name: episode.name().to_owned(),
duration: episode.get_duration().to_owned(), duration: episode.duration().to_owned(),
audio: episode.get_audio().into(), audio: episode.audio.as_slice().into(),
description: episode.get_description().to_owned(), description: episode.description().to_owned(),
number: episode.get_number(), number: episode.number(),
publish_time: episode.get_publish_time().try_into()?, publish_time: episode.publish_time.get_or_default().try_into()?,
covers: episode.get_cover_image().get_image().into(), covers: episode.cover_image.image.as_slice().into(),
language: episode.get_language().to_owned(), language: episode.language().to_owned(),
is_explicit: episode.get_explicit().to_owned(), is_explicit: episode.explicit().to_owned(),
show_name: episode.get_show().get_name().to_owned(), show_name: episode.show.name().to_owned(),
videos: episode.get_video().into(), videos: episode.video.as_slice().into(),
video_previews: episode.get_video_preview().into(), video_previews: episode.video_preview.as_slice().into(),
audio_previews: episode.get_audio_preview().into(), audio_previews: episode.audio_preview.as_slice().into(),
restrictions: episode.get_restriction().into(), restrictions: episode.restriction.as_slice().into(),
freeze_frames: episode.get_freeze_frame().get_image().into(), freeze_frames: episode.freeze_frame.image.as_slice().into(),
keywords: episode.get_keyword().to_vec(), keywords: episode.keyword.to_vec(),
allow_background_playback: episode.get_allow_background_playback(), allow_background_playback: episode.allow_background_playback(),
availability: episode.get_availability().try_into()?, availability: episode.availability.as_slice().try_into()?,
external_url: episode.get_external_url().to_owned(), external_url: episode.external_url().to_owned(),
episode_type: episode.get_field_type(), episode_type: episode.type_(),
has_music_and_talk: episode.get_music_and_talk(), has_music_and_talk: episode.music_and_talk(),
content_rating: episode.get_content_rating().into(), content_rating: episode.content_rating.as_slice().into(),
is_audiobook_chapter: episode.get_is_audiobook_chapter(), is_audiobook_chapter: episode.is_audiobook_chapter(),
}) })
} }
} }

View file

@ -22,8 +22,8 @@ impl_deref_wrapped!(ExternalIds, Vec<ExternalId>);
impl From<&ExternalIdMessage> for ExternalId { impl From<&ExternalIdMessage> for ExternalId {
fn from(external_id: &ExternalIdMessage) -> Self { fn from(external_id: &ExternalIdMessage) -> Self {
Self { Self {
external_type: external_id.get_field_type().to_owned(), external_type: external_id.type_().to_owned(),
id: external_id.get_id().to_owned(), id: external_id.id().to_owned(),
} }
} }
} }

View file

@ -9,9 +9,9 @@ use crate::util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated
use librespot_core::{FileId, SpotifyId}; use librespot_core::{FileId, SpotifyId};
use librespot_protocol as protocol; use librespot_protocol as protocol;
pub use protocol::metadata::image::Size as ImageSize;
use protocol::metadata::Image as ImageMessage; use protocol::metadata::Image as ImageMessage;
use protocol::metadata::ImageGroup; use protocol::metadata::ImageGroup;
pub use protocol::metadata::Image_Size as ImageSize;
use protocol::playlist4_external::PictureSize as PictureSizeMessage; use protocol::playlist4_external::PictureSize as PictureSizeMessage;
use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage; use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage;
@ -60,9 +60,9 @@ impl From<&ImageMessage> for Image {
fn from(image: &ImageMessage) -> Self { fn from(image: &ImageMessage) -> Self {
Self { Self {
id: image.into(), id: image.into(),
size: image.get_size(), size: image.size(),
width: image.get_width(), width: image.width(),
height: image.get_height(), height: image.height(),
} }
} }
} }
@ -72,8 +72,8 @@ impl_from_repeated!(ImageMessage, Images);
impl From<&PictureSizeMessage> for PictureSize { impl From<&PictureSizeMessage> for PictureSize {
fn from(size: &PictureSizeMessage) -> Self { fn from(size: &PictureSizeMessage) -> Self {
Self { Self {
target_name: size.get_target_name().to_owned(), target_name: size.target_name().to_owned(),
url: size.get_url().to_owned(), url: size.url().to_owned(),
} }
} }
} }
@ -84,7 +84,7 @@ impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(picture: &TranscodedPictureMessage) -> Result<Self, Self::Error> { fn try_from(picture: &TranscodedPictureMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
target_name: picture.get_target_name().to_owned(), target_name: picture.target_name().to_owned(),
uri: picture.try_into()?, uri: picture.try_into()?,
}) })
} }

View file

@ -41,7 +41,7 @@ pub use track::Track;
#[async_trait] #[async_trait]
pub trait Metadata: Send + Sized + 'static { pub trait Metadata: Send + Sized + 'static {
type Message: protobuf::Message; type Message: protobuf::Message + std::fmt::Debug;
// Request a protobuf // Request a protobuf
async fn request(session: &Session, id: &SpotifyId) -> RequestResult; async fn request(session: &Session, id: &SpotifyId) -> RequestResult;

View file

@ -1,4 +1,4 @@
use std::convert::{TryFrom, TryInto}; use std::convert::TryFrom;
use std::fmt::Debug; use std::fmt::Debug;
use protobuf::Message; use protobuf::Message;
@ -34,11 +34,11 @@ impl Metadata for PlaylistAnnotation {
fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error> { fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error> {
Ok(Self { Ok(Self {
description: msg.get_description().to_owned(), description: msg.description().to_owned(),
picture: msg.get_picture().to_owned(), // TODO: is this a URL or Spotify URI? picture: msg.picture().to_owned(), // TODO: is this a URL or Spotify URI?
transcoded_pictures: msg.get_transcoded_picture().try_into()?, transcoded_pictures: msg.transcoded_picture.as_slice().try_into()?,
has_abuse_reporting: msg.get_is_abuse_reporting_enabled(), has_abuse_reporting: msg.is_abuse_reporting_enabled(),
abuse_report_state: msg.get_abuse_report_state(), abuse_report_state: msg.abuse_report_state(),
}) })
} }
} }
@ -77,11 +77,11 @@ impl TryFrom<&<PlaylistAnnotation as Metadata>::Message> for PlaylistAnnotation
annotation: &<PlaylistAnnotation as Metadata>::Message, annotation: &<PlaylistAnnotation as Metadata>::Message,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
description: annotation.get_description().to_owned(), description: annotation.description().to_owned(),
picture: annotation.get_picture().to_owned(), picture: annotation.picture().to_owned(),
transcoded_pictures: annotation.get_transcoded_picture().try_into()?, transcoded_pictures: annotation.transcoded_picture.as_slice().try_into()?,
has_abuse_reporting: annotation.get_is_abuse_reporting_enabled(), has_abuse_reporting: annotation.is_abuse_reporting_enabled(),
abuse_report_state: annotation.get_abuse_report_state(), abuse_report_state: annotation.abuse_report_state(),
}) })
} }
} }

View file

@ -1,6 +1,6 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
convert::{TryFrom, TryInto}, convert::TryFrom,
fmt::Debug, fmt::Debug,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
@ -99,16 +99,16 @@ impl TryFrom<&PlaylistAttributesMessage> for PlaylistAttributes {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(attributes: &PlaylistAttributesMessage) -> Result<Self, Self::Error> { fn try_from(attributes: &PlaylistAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
name: attributes.get_name().to_owned(), name: attributes.name().to_owned(),
description: attributes.get_description().to_owned(), description: attributes.description().to_owned(),
picture: attributes.get_picture().to_owned(), picture: attributes.picture().to_owned(),
is_collaborative: attributes.get_collaborative(), is_collaborative: attributes.collaborative(),
pl3_version: attributes.get_pl3_version().to_owned(), pl3_version: attributes.pl3_version().to_owned(),
is_deleted_by_owner: attributes.get_deleted_by_owner(), is_deleted_by_owner: attributes.deleted_by_owner(),
client_id: attributes.get_client_id().to_owned(), client_id: attributes.client_id().to_owned(),
format: attributes.get_format().to_owned(), format: attributes.format().to_owned(),
format_attributes: attributes.get_format_attributes().into(), format_attributes: attributes.format_attributes.as_slice().into(),
picture_sizes: attributes.get_picture_size().into(), picture_sizes: attributes.picture_size.as_slice().into(),
}) })
} }
} }
@ -117,12 +117,7 @@ impl From<&[PlaylistFormatAttributeMessage]> for PlaylistFormatAttribute {
fn from(attributes: &[PlaylistFormatAttributeMessage]) -> Self { fn from(attributes: &[PlaylistFormatAttributeMessage]) -> Self {
let format_attributes = attributes let format_attributes = attributes
.iter() .iter()
.map(|attribute| { .map(|attribute| (attribute.key().to_owned(), attribute.value().to_owned()))
(
attribute.get_key().to_owned(),
attribute.get_value().to_owned(),
)
})
.collect(); .collect();
PlaylistFormatAttribute(format_attributes) PlaylistFormatAttribute(format_attributes)
@ -133,12 +128,12 @@ impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result<Self, Self::Error> { fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
added_by: attributes.get_added_by().to_owned(), added_by: attributes.added_by().to_owned(),
timestamp: Date::from_timestamp_ms(attributes.get_timestamp())?, timestamp: Date::from_timestamp_ms(attributes.timestamp())?,
seen_at: Date::from_timestamp_ms(attributes.get_seen_at())?, seen_at: Date::from_timestamp_ms(attributes.seen_at())?,
is_public: attributes.get_public(), is_public: attributes.public(),
format_attributes: attributes.get_format_attributes().into(), format_attributes: attributes.format_attributes.as_slice().into(),
item_id: attributes.get_item_id().to_owned(), item_id: attributes.item_id().to_owned(),
}) })
} }
} }
@ -146,8 +141,14 @@ impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result<Self, Self::Error> { fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
values: attributes.get_values().try_into()?, values: attributes.values.get_or_default().try_into()?,
no_value: attributes.get_no_value().into(), no_value: attributes
.no_value
.iter()
.map(|v| v.enum_value_or_default())
.collect::<Vec<PlaylistAttributeKind>>()
.as_slice()
.into(),
}) })
} }
} }
@ -156,8 +157,14 @@ impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttri
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Result<Self, Self::Error> { fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
values: attributes.get_values().try_into()?, values: attributes.values.get_or_default().try_into()?,
no_value: attributes.get_no_value().into(), no_value: attributes
.no_value
.iter()
.map(|v| v.enum_value_or_default())
.collect::<Vec<PlaylistItemAttributeKind>>()
.as_slice()
.into(),
}) })
} }
} }
@ -166,8 +173,8 @@ impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result<Self, Self::Error> { fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
new_attributes: update.get_new_attributes().try_into()?, new_attributes: update.new_attributes.get_or_default().try_into()?,
old_attributes: update.get_old_attributes().try_into()?, old_attributes: update.old_attributes.get_or_default().try_into()?,
}) })
} }
} }
@ -176,9 +183,9 @@ impl TryFrom<&PlaylistUpdateItemAttributesMessage> for PlaylistUpdateItemAttribu
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(update: &PlaylistUpdateItemAttributesMessage) -> Result<Self, Self::Error> { fn try_from(update: &PlaylistUpdateItemAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
index: update.get_index(), index: update.index(),
new_attributes: update.get_new_attributes().try_into()?, new_attributes: update.new_attributes.get_or_default().try_into()?,
old_attributes: update.get_old_attributes().try_into()?, old_attributes: update.old_attributes.get_or_default().try_into()?,
}) })
} }
} }

View file

@ -1,7 +1,4 @@
use std::{ use std::{convert::TryFrom, fmt::Debug};
convert::{TryFrom, TryInto},
fmt::Debug,
};
use super::operation::PlaylistOperations; use super::operation::PlaylistOperations;
@ -21,9 +18,19 @@ impl TryFrom<&DiffMessage> for PlaylistDiff {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(diff: &DiffMessage) -> Result<Self, Self::Error> { fn try_from(diff: &DiffMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
from_revision: diff.get_from_revision().try_into()?, from_revision: diff
operations: diff.get_ops().try_into()?, .from_revision
to_revision: diff.get_to_revision().try_into()?, .clone()
.unwrap_or_default()
.as_slice()
.try_into()?,
operations: diff.ops.as_slice().try_into()?,
to_revision: diff
.to_revision
.clone()
.unwrap_or_default()
.as_slice()
.try_into()?,
}) })
} }
} }

View file

@ -58,7 +58,7 @@ impl TryFrom<&PlaylistItemMessage> for PlaylistItem {
fn try_from(item: &PlaylistItemMessage) -> Result<Self, Self::Error> { fn try_from(item: &PlaylistItemMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
id: item.try_into()?, id: item.try_into()?,
attributes: item.get_attributes().try_into()?, attributes: item.attributes.get_or_default().try_into()?,
}) })
} }
} }
@ -69,10 +69,10 @@ impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(list_items: &PlaylistItemsMessage) -> Result<Self, Self::Error> { fn try_from(list_items: &PlaylistItemsMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
position: list_items.get_pos(), position: list_items.pos(),
is_truncated: list_items.get_truncated(), is_truncated: list_items.truncated(),
items: list_items.get_items().try_into()?, items: list_items.items.as_slice().try_into()?,
meta_items: list_items.get_meta_items().try_into()?, meta_items: list_items.meta_items.as_slice().try_into()?,
}) })
} }
} }
@ -82,12 +82,12 @@ impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem {
fn try_from(item: &PlaylistMetaItemMessage) -> Result<Self, Self::Error> { fn try_from(item: &PlaylistMetaItemMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
revision: item.try_into()?, revision: item.try_into()?,
attributes: item.get_attributes().try_into()?, attributes: item.attributes.get_or_default().try_into()?,
length: item.get_length(), length: item.length(),
timestamp: Date::from_timestamp_ms(item.get_timestamp())?, timestamp: Date::from_timestamp_ms(item.timestamp())?,
owner_username: item.get_owner_username().to_owned(), owner_username: item.owner_username().to_owned(),
has_abuse_reporting: item.get_abuse_reporting_enabled(), has_abuse_reporting: item.abuse_reporting_enabled(),
capabilities: item.get_capabilities().into(), capabilities: item.capabilities.get_or_default().into(),
}) })
} }
} }

View file

@ -129,7 +129,7 @@ impl Metadata for Playlist {
impl TryFrom<&<Playlist as Metadata>::Message> for SelectedListContent { impl TryFrom<&<Playlist as Metadata>::Message> for SelectedListContent {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self, Self::Error> { fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self, Self::Error> {
let timestamp = playlist.get_timestamp(); let timestamp = playlist.timestamp();
let timestamp = if timestamp > 9295169800000 { let timestamp = if timestamp > 9295169800000 {
// timestamp is way out of range for milliseconds. Some seem to be in microseconds? // timestamp is way out of range for milliseconds. Some seem to be in microseconds?
// Observed on playlists where: // Observed on playlists where:
@ -146,25 +146,37 @@ impl TryFrom<&<Playlist as Metadata>::Message> for SelectedListContent {
let timestamp = Date::from_timestamp_ms(timestamp)?; let timestamp = Date::from_timestamp_ms(timestamp)?;
Ok(Self { Ok(Self {
revision: playlist.get_revision().to_owned(), revision: playlist.revision().to_owned(),
length: playlist.get_length(), length: playlist.length(),
attributes: playlist.get_attributes().try_into()?, attributes: playlist.attributes.get_or_default().try_into()?,
contents: playlist.get_contents().try_into()?, contents: playlist.contents.get_or_default().try_into()?,
diff: playlist.diff.as_ref().map(TryInto::try_into).transpose()?, diff: playlist.diff.as_ref().map(TryInto::try_into).transpose()?,
sync_result: playlist sync_result: playlist
.sync_result .sync_result
.as_ref() .as_ref()
.map(TryInto::try_into) .map(TryInto::try_into)
.transpose()?, .transpose()?,
resulting_revisions: playlist.get_resulting_revisions().try_into()?, resulting_revisions: Playlists(
has_multiple_heads: playlist.get_multiple_heads(), playlist
is_up_to_date: playlist.get_up_to_date(), .resulting_revisions
nonces: playlist.get_nonces().into(), .iter()
.map(|p| p.try_into())
.collect::<Result<Vec<SpotifyId>, Error>>()?,
),
has_multiple_heads: playlist.multiple_heads(),
is_up_to_date: playlist.up_to_date(),
nonces: playlist.nonces.clone(),
timestamp, timestamp,
owner_username: playlist.get_owner_username().to_owned(), owner_username: playlist.owner_username().to_owned(),
has_abuse_reporting: playlist.get_abuse_reporting_enabled(), has_abuse_reporting: playlist.abuse_reporting_enabled(),
capabilities: playlist.get_capabilities().into(), capabilities: playlist.capabilities.get_or_default().into(),
geoblocks: playlist.get_geoblock().into(), geoblocks: Geoblocks(
playlist
.geoblock
.iter()
.map(|b| b.enum_value_or_default())
.collect(),
),
}) })
} }
} }

View file

@ -1,5 +1,5 @@
use std::{ use std::{
convert::{TryFrom, TryInto}, convert::TryFrom,
fmt::Debug, fmt::Debug,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
@ -13,10 +13,10 @@ use crate::{
}; };
use librespot_protocol as protocol; use librespot_protocol as protocol;
pub use protocol::playlist4_external::op::Kind as PlaylistOperationKind;
use protocol::playlist4_external::Add as PlaylistAddMessage; use protocol::playlist4_external::Add as PlaylistAddMessage;
use protocol::playlist4_external::Mov as PlaylistMoveMessage; use protocol::playlist4_external::Mov as PlaylistMoveMessage;
use protocol::playlist4_external::Op as PlaylistOperationMessage; use protocol::playlist4_external::Op as PlaylistOperationMessage;
pub use protocol::playlist4_external::Op_Kind as PlaylistOperationKind;
use protocol::playlist4_external::Rem as PlaylistRemoveMessage; use protocol::playlist4_external::Rem as PlaylistRemoveMessage;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -61,12 +61,18 @@ impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(operation: &PlaylistOperationMessage) -> Result<Self, Self::Error> { fn try_from(operation: &PlaylistOperationMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
kind: operation.get_kind(), kind: operation.kind(),
add: operation.get_add().try_into()?, add: operation.add.get_or_default().try_into()?,
rem: operation.get_rem().try_into()?, rem: operation.rem.get_or_default().try_into()?,
mov: operation.get_mov().into(), mov: operation.mov.get_or_default().into(),
update_item_attributes: operation.get_update_item_attributes().try_into()?, update_item_attributes: operation
update_list_attributes: operation.get_update_list_attributes().try_into()?, .update_item_attributes
.get_or_default()
.try_into()?,
update_list_attributes: operation
.update_list_attributes
.get_or_default()
.try_into()?,
}) })
} }
} }
@ -77,10 +83,10 @@ impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(add: &PlaylistAddMessage) -> Result<Self, Self::Error> { fn try_from(add: &PlaylistAddMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
from_index: add.get_from_index(), from_index: add.from_index(),
items: add.get_items().try_into()?, items: add.items.as_slice().try_into()?,
add_last: add.get_add_last(), add_last: add.add_last(),
add_first: add.get_add_first(), add_first: add.add_first(),
}) })
} }
} }
@ -88,9 +94,9 @@ impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd {
impl From<&PlaylistMoveMessage> for PlaylistOperationMove { impl From<&PlaylistMoveMessage> for PlaylistOperationMove {
fn from(mov: &PlaylistMoveMessage) -> Self { fn from(mov: &PlaylistMoveMessage) -> Self {
Self { Self {
from_index: mov.get_from_index(), from_index: mov.from_index(),
length: mov.get_length(), length: mov.length(),
to_index: mov.get_to_index(), to_index: mov.to_index(),
} }
} }
} }
@ -99,10 +105,10 @@ impl TryFrom<&PlaylistRemoveMessage> for PlaylistOperationRemove {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(remove: &PlaylistRemoveMessage) -> Result<Self, Self::Error> { fn try_from(remove: &PlaylistRemoveMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
from_index: remove.get_from_index(), from_index: remove.from_index(),
length: remove.get_length(), length: remove.length(),
items: remove.get_items().try_into()?, items: remove.items.as_slice().try_into()?,
has_items_as_key: remove.get_items_as_key(), has_items_as_key: remove.items_as_key(),
}) })
} }
} }

View file

@ -27,12 +27,18 @@ impl_deref_wrapped!(PermissionLevels, Vec<PermissionLevel>);
impl From<&CapabilitiesMessage> for Capabilities { impl From<&CapabilitiesMessage> for Capabilities {
fn from(playlist: &CapabilitiesMessage) -> Self { fn from(playlist: &CapabilitiesMessage) -> Self {
Self { Self {
can_view: playlist.get_can_view(), can_view: playlist.can_view(),
can_administrate_permissions: playlist.get_can_administrate_permissions(), can_administrate_permissions: playlist.can_administrate_permissions(),
grantable_levels: playlist.get_grantable_level().into(), grantable_levels: PermissionLevels(
can_edit_metadata: playlist.get_can_edit_metadata(), playlist
can_edit_items: playlist.get_can_edit_items(), .grantable_level
can_cancel_membership: playlist.get_can_cancel_membership(), .iter()
.map(|l| l.enum_value_or_default())
.collect(),
),
can_edit_metadata: playlist.can_edit_metadata(),
can_edit_items: playlist.can_edit_items(),
can_cancel_membership: playlist.can_cancel_membership(),
} }
} }
} }

View file

@ -9,8 +9,8 @@ use crate::util::{impl_from_repeated, impl_from_repeated_copy};
use protocol::metadata::Restriction as RestrictionMessage; use protocol::metadata::Restriction as RestrictionMessage;
use librespot_protocol as protocol; use librespot_protocol as protocol;
pub use protocol::metadata::Restriction_Catalogue as RestrictionCatalogue; pub use protocol::metadata::restriction::Catalogue as RestrictionCatalogue;
pub use protocol::metadata::Restriction_Type as RestrictionType; pub use protocol::metadata::restriction::Type as RestrictionType;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Restriction { pub struct Restriction {
@ -43,25 +43,30 @@ impl Restriction {
impl From<&RestrictionMessage> for Restriction { impl From<&RestrictionMessage> for Restriction {
fn from(restriction: &RestrictionMessage) -> Self { fn from(restriction: &RestrictionMessage) -> Self {
let countries_allowed = if restriction.has_countries_allowed() { let countries_allowed = if restriction.has_countries_allowed() {
Some(Self::parse_country_codes( Some(Self::parse_country_codes(restriction.countries_allowed()))
restriction.get_countries_allowed(),
))
} else { } else {
None None
}; };
let countries_forbidden = if restriction.has_countries_forbidden() { let countries_forbidden = if restriction.has_countries_forbidden() {
Some(Self::parse_country_codes( Some(Self::parse_country_codes(restriction.countries_forbidden()))
restriction.get_countries_forbidden(),
))
} else { } else {
None None
}; };
Self { Self {
catalogues: restriction.get_catalogue().into(), catalogues: restriction
restriction_type: restriction.get_field_type(), .catalogue
catalogue_strs: restriction.get_catalogue_str().to_vec(), .iter()
.map(|c| c.enum_value_or_default())
.collect::<Vec<RestrictionCatalogue>>()
.as_slice()
.into(),
restriction_type: restriction
.type_
.unwrap_or_default()
.enum_value_or_default(),
catalogue_strs: restriction.catalogue_str.to_vec(),
countries_allowed, countries_allowed,
countries_forbidden, countries_forbidden,
} }

View file

@ -1,5 +1,5 @@
use std::{ use std::{
convert::{TryFrom, TryInto}, convert::TryFrom,
fmt::Debug, fmt::Debug,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
@ -30,9 +30,9 @@ impl TryFrom<&SalePeriodMessage> for SalePeriod {
type Error = librespot_core::Error; type Error = librespot_core::Error;
fn try_from(sale_period: &SalePeriodMessage) -> Result<Self, Self::Error> { fn try_from(sale_period: &SalePeriodMessage) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
restrictions: sale_period.get_restriction().into(), restrictions: sale_period.restriction.as_slice().into(),
start: sale_period.get_start().try_into()?, start: sale_period.start.get_or_default().try_into()?,
end: sale_period.get_end().try_into()?, end: sale_period.end.get_or_default().try_into()?,
}) })
} }
} }

View file

@ -11,8 +11,8 @@ use crate::{
use librespot_core::{Error, Session, SpotifyId}; use librespot_core::{Error, Session, SpotifyId};
use librespot_protocol as protocol; use librespot_protocol as protocol;
pub use protocol::metadata::Show_ConsumptionOrder as ShowConsumptionOrder; pub use protocol::metadata::show::ConsumptionOrder as ShowConsumptionOrder;
pub use protocol::metadata::Show_MediaType as ShowMediaType; pub use protocol::metadata::show::MediaType as ShowMediaType;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Show { pub struct Show {
@ -53,22 +53,22 @@ impl TryFrom<&<Self as Metadata>::Message> for Show {
fn try_from(show: &<Self as Metadata>::Message) -> Result<Self, Self::Error> { fn try_from(show: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
id: show.try_into()?, id: show.try_into()?,
name: show.get_name().to_owned(), name: show.name().to_owned(),
description: show.get_description().to_owned(), description: show.description().to_owned(),
publisher: show.get_publisher().to_owned(), publisher: show.publisher().to_owned(),
language: show.get_language().to_owned(), language: show.language().to_owned(),
is_explicit: show.get_explicit(), is_explicit: show.explicit(),
covers: show.get_cover_image().get_image().into(), covers: show.cover_image.image.as_slice().into(),
episodes: show.get_episode().try_into()?, episodes: show.episode.as_slice().try_into()?,
copyrights: show.get_copyright().into(), copyrights: show.copyright.as_slice().into(),
restrictions: show.get_restriction().into(), restrictions: show.restriction.as_slice().into(),
keywords: show.get_keyword().to_vec(), keywords: show.keyword.to_vec(),
media_type: show.get_media_type(), media_type: show.media_type(),
consumption_order: show.get_consumption_order(), consumption_order: show.consumption_order(),
availability: show.get_availability().try_into()?, availability: show.availability.as_slice().try_into()?,
trailer_uri: SpotifyId::from_uri(show.get_trailer_uri())?, trailer_uri: SpotifyId::from_uri(show.trailer_uri())?,
has_music_and_talk: show.get_music_and_talk(), has_music_and_talk: show.music_and_talk(),
is_audiobook: show.get_is_audiobook(), is_audiobook: show.is_audiobook(),
}) })
} }
} }

View file

@ -73,31 +73,30 @@ impl TryFrom<&<Self as Metadata>::Message> for Track {
fn try_from(track: &<Self as Metadata>::Message) -> Result<Self, Self::Error> { fn try_from(track: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
id: track.try_into()?, id: track.try_into()?,
name: track.get_name().to_owned(), name: track.name().to_owned(),
album: track.get_album().try_into()?, album: track.album.get_or_default().try_into()?,
artists: track.get_artist().try_into()?, artists: track.artist.as_slice().try_into()?,
number: track.get_number(), number: track.number(),
disc_number: track.get_disc_number(), disc_number: track.disc_number(),
duration: track.get_duration(), duration: track.duration(),
popularity: track.get_popularity(), popularity: track.popularity(),
is_explicit: track.get_explicit(), is_explicit: track.explicit(),
external_ids: track.get_external_id().into(), external_ids: track.external_id.as_slice().into(),
restrictions: track.get_restriction().into(), restrictions: track.restriction.as_slice().into(),
files: track.get_file().into(), files: track.file.as_slice().into(),
alternatives: track.get_alternative().try_into()?, alternatives: track.alternative.as_slice().try_into()?,
sale_periods: track.get_sale_period().try_into()?, sale_periods: track.sale_period.as_slice().try_into()?,
previews: track.get_preview().into(), previews: track.preview.as_slice().into(),
tags: track.get_tags().to_vec(), tags: track.tags.to_vec(),
earliest_live_timestamp: Date::from_timestamp_ms(track.get_earliest_live_timestamp())?, earliest_live_timestamp: Date::from_timestamp_ms(track.earliest_live_timestamp())?,
has_lyrics: track.get_has_lyrics(), has_lyrics: track.has_lyrics(),
availability: track.get_availability().try_into()?, availability: track.availability.as_slice().try_into()?,
licensor: Uuid::from_slice(track.get_licensor().get_uuid()) licensor: Uuid::from_slice(track.licensor.uuid()).unwrap_or_else(|_| Uuid::nil()),
.unwrap_or_else(|_| Uuid::nil()), language_of_performance: track.language_of_performance.to_vec(),
language_of_performance: track.get_language_of_performance().to_vec(), content_ratings: track.content_rating.as_slice().into(),
content_ratings: track.get_content_rating().into(), original_title: track.original_title().to_owned(),
original_title: track.get_original_title().to_owned(), version_title: track.version_title().to_owned(),
version_title: track.get_version_title().to_owned(), artists_with_role: track.artist_with_role.as_slice().try_into()?,
artists_with_role: track.get_artist_with_role().try_into()?,
}) })
} }
} }

View file

@ -10,8 +10,8 @@ repository = "https://github.com/librespot-org/librespot"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
protobuf = "2" protobuf = "3"
[build-dependencies] [build-dependencies]
glob = "0.3" glob = "0.3"
protobuf-codegen-pure = "2" protobuf-codegen = "3"

View file

@ -46,7 +46,8 @@ fn compile() {
let out_dir = out_dir(); let out_dir = out_dir();
fs::create_dir(&out_dir).expect("create_dir"); fs::create_dir(&out_dir).expect("create_dir");
protobuf_codegen_pure::Codegen::new() protobuf_codegen::Codegen::new()
.pure()
.out_dir(&out_dir) .out_dir(&out_dir)
.inputs(&slices) .inputs(&slices)
.include(&proto_dir) .include(&proto_dir)
@ -54,26 +55,7 @@ fn compile() {
.expect("Codegen failed."); .expect("Codegen failed.");
} }
fn generate_mod_rs() {
let out_dir = out_dir();
let mods = glob::glob(&out_dir.join("*.rs").to_string_lossy())
.expect("glob")
.filter_map(|p| {
p.ok()
.map(|p| format!("pub mod {};", p.file_stem().unwrap().to_string_lossy()))
})
.collect::<Vec<_>>()
.join("\n");
let mod_rs = out_dir.join("mod.rs");
fs::write(&mod_rs, format!("// @generated\n{}\n", mods)).expect("write");
println!("cargo:rustc-env=PROTO_MOD_RS={}", mod_rs.to_string_lossy());
}
fn main() { fn main() {
cleanup(); cleanup();
compile(); compile();
generate_mod_rs();
} }