mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Update protobuf and related crates to 3.x (#1092)
This commit is contained in:
parent
8941495b80
commit
3662302196
38 changed files with 694 additions and 672 deletions
95
Cargo.lock
generated
95
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()?)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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?;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()?,
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue