mirror of
https://github.com/librespot-org/librespot.git
synced 2025-10-04 18:29:45 +02:00
Change panics into Result<_, librespot_core::Error>
This commit is contained in:
parent
a297c68913
commit
62461be1fc
69 changed files with 2041 additions and 1331 deletions
|
@ -1,7 +1,9 @@
|
|||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use hyper::{Body, Method, Request};
|
||||
use serde::Deserialize;
|
||||
use std::error::Error;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
pub type SocketAddress = (String, u16);
|
||||
|
||||
|
@ -67,7 +69,7 @@ impl ApResolver {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub async fn try_apresolve(&self) -> Result<ApResolveData, Box<dyn Error>> {
|
||||
pub async fn try_apresolve(&self) -> Result<ApResolveData, Error> {
|
||||
let req = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient")
|
||||
|
|
|
@ -1,54 +1,85 @@
|
|||
use std::{collections::HashMap, io::Write};
|
||||
|
||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||
use bytes::Bytes;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::file_id::FileId;
|
||||
use crate::packet::PacketType;
|
||||
use crate::spotify_id::SpotifyId;
|
||||
use crate::util::SeqGenerator;
|
||||
use crate::{packet::PacketType, util::SeqGenerator, Error, FileId, SpotifyId};
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
|
||||
pub struct AudioKey(pub [u8; 16]);
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
|
||||
pub struct AudioKeyError;
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AudioKeyError {
|
||||
#[error("audio key error")]
|
||||
AesKey,
|
||||
#[error("other end of channel disconnected")]
|
||||
Channel,
|
||||
#[error("unexpected packet type {0}")]
|
||||
Packet(u8),
|
||||
#[error("sequence {0} not pending")]
|
||||
Sequence(u32),
|
||||
}
|
||||
|
||||
impl From<AudioKeyError> for Error {
|
||||
fn from(err: AudioKeyError) -> Self {
|
||||
match err {
|
||||
AudioKeyError::AesKey => Error::unavailable(err),
|
||||
AudioKeyError::Channel => Error::aborted(err),
|
||||
AudioKeyError::Sequence(_) => Error::aborted(err),
|
||||
AudioKeyError::Packet(_) => Error::unimplemented(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component! {
|
||||
AudioKeyManager : AudioKeyManagerInner {
|
||||
sequence: SeqGenerator<u32> = SeqGenerator::new(0),
|
||||
pending: HashMap<u32, oneshot::Sender<Result<AudioKey, AudioKeyError>>> = HashMap::new(),
|
||||
pending: HashMap<u32, oneshot::Sender<Result<AudioKey, Error>>> = HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioKeyManager {
|
||||
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) {
|
||||
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
|
||||
let seq = BigEndian::read_u32(data.split_to(4).as_ref());
|
||||
|
||||
let sender = self.lock(|inner| inner.pending.remove(&seq));
|
||||
let sender = self
|
||||
.lock(|inner| inner.pending.remove(&seq))
|
||||
.ok_or(AudioKeyError::Sequence(seq))?;
|
||||
|
||||
if let Some(sender) = sender {
|
||||
match cmd {
|
||||
PacketType::AesKey => {
|
||||
let mut key = [0u8; 16];
|
||||
key.copy_from_slice(data.as_ref());
|
||||
let _ = sender.send(Ok(AudioKey(key)));
|
||||
}
|
||||
PacketType::AesKeyError => {
|
||||
warn!(
|
||||
"error audio key {:x} {:x}",
|
||||
data.as_ref()[0],
|
||||
data.as_ref()[1]
|
||||
);
|
||||
let _ = sender.send(Err(AudioKeyError));
|
||||
}
|
||||
_ => (),
|
||||
match cmd {
|
||||
PacketType::AesKey => {
|
||||
let mut key = [0u8; 16];
|
||||
key.copy_from_slice(data.as_ref());
|
||||
sender
|
||||
.send(Ok(AudioKey(key)))
|
||||
.map_err(|_| AudioKeyError::Channel)?
|
||||
}
|
||||
PacketType::AesKeyError => {
|
||||
error!(
|
||||
"error audio key {:x} {:x}",
|
||||
data.as_ref()[0],
|
||||
data.as_ref()[1]
|
||||
);
|
||||
sender
|
||||
.send(Err(AudioKeyError::AesKey.into()))
|
||||
.map_err(|_| AudioKeyError::Channel)?
|
||||
}
|
||||
_ => {
|
||||
trace!(
|
||||
"Did not expect {:?} AES key packet with data {:#?}",
|
||||
cmd,
|
||||
data
|
||||
);
|
||||
return Err(AudioKeyError::Packet(cmd as u8).into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, AudioKeyError> {
|
||||
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, Error> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let seq = self.lock(move |inner| {
|
||||
|
@ -57,16 +88,16 @@ impl AudioKeyManager {
|
|||
seq
|
||||
});
|
||||
|
||||
self.send_key_request(seq, track, file);
|
||||
rx.await.map_err(|_| AudioKeyError)?
|
||||
self.send_key_request(seq, track, file)?;
|
||||
rx.await?
|
||||
}
|
||||
|
||||
fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) {
|
||||
fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) -> Result<(), Error> {
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
data.write_all(&file.0).unwrap();
|
||||
data.write_all(&track.to_raw()).unwrap();
|
||||
data.write_u32::<BigEndian>(seq).unwrap();
|
||||
data.write_u16::<BigEndian>(0x0000).unwrap();
|
||||
data.write_all(&file.0)?;
|
||||
data.write_all(&track.to_raw())?;
|
||||
data.write_u32::<BigEndian>(seq)?;
|
||||
data.write_u16::<BigEndian>(0x0000)?;
|
||||
|
||||
self.session().send_packet(PacketType::RequestKey, data)
|
||||
}
|
||||
|
|
|
@ -7,8 +7,21 @@ use pbkdf2::pbkdf2;
|
|||
use protobuf::ProtobufEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha1::{Digest, Sha1};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::protocol::authentication::AuthenticationType;
|
||||
use crate::{protocol::authentication::AuthenticationType, Error};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthenticationError {
|
||||
#[error("unknown authentication type {0}")]
|
||||
AuthType(u32),
|
||||
}
|
||||
|
||||
impl From<AuthenticationError> for Error {
|
||||
fn from(err: AuthenticationError) -> Self {
|
||||
Error::invalid_argument(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// The credentials are used to log into the Spotify API.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
@ -46,7 +59,7 @@ impl Credentials {
|
|||
username: impl Into<String>,
|
||||
encrypted_blob: impl AsRef<[u8]>,
|
||||
device_id: impl AsRef<[u8]>,
|
||||
) -> Credentials {
|
||||
) -> Result<Credentials, Error> {
|
||||
fn read_u8<R: Read>(stream: &mut R) -> io::Result<u8> {
|
||||
let mut data = [0u8];
|
||||
stream.read_exact(&mut data)?;
|
||||
|
@ -91,7 +104,7 @@ impl Credentials {
|
|||
use aes::cipher::generic_array::GenericArray;
|
||||
use aes::cipher::{BlockCipher, NewBlockCipher};
|
||||
|
||||
let mut data = base64::decode(encrypted_blob).unwrap();
|
||||
let mut data = base64::decode(encrypted_blob)?;
|
||||
let cipher = Aes192::new(GenericArray::from_slice(&key));
|
||||
let block_size = <Aes192 as BlockCipher>::BlockSize::to_usize();
|
||||
|
||||
|
@ -109,19 +122,20 @@ impl Credentials {
|
|||
};
|
||||
|
||||
let mut cursor = io::Cursor::new(blob.as_slice());
|
||||
read_u8(&mut cursor).unwrap();
|
||||
read_bytes(&mut cursor).unwrap();
|
||||
read_u8(&mut cursor).unwrap();
|
||||
let auth_type = read_int(&mut cursor).unwrap();
|
||||
let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap();
|
||||
read_u8(&mut cursor).unwrap();
|
||||
let auth_data = read_bytes(&mut cursor).unwrap();
|
||||
read_u8(&mut cursor)?;
|
||||
read_bytes(&mut cursor)?;
|
||||
read_u8(&mut cursor)?;
|
||||
let auth_type = read_int(&mut cursor)?;
|
||||
let auth_type = AuthenticationType::from_i32(auth_type as i32)
|
||||
.ok_or(AuthenticationError::AuthType(auth_type))?;
|
||||
read_u8(&mut cursor)?;
|
||||
let auth_data = read_bytes(&mut cursor)?;
|
||||
|
||||
Credentials {
|
||||
Ok(Credentials {
|
||||
username,
|
||||
auth_type,
|
||||
auth_data,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
use std::cmp::Reverse;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Error, ErrorKind, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::HashMap,
|
||||
fs::{self, File},
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use priority_queue::PriorityQueue;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::authentication::Credentials;
|
||||
use crate::file_id::FileId;
|
||||
use crate::{authentication::Credentials, error::ErrorKind, Error, FileId};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CacheError {
|
||||
#[error("audio cache location is not configured")]
|
||||
Path,
|
||||
}
|
||||
|
||||
impl From<CacheError> for Error {
|
||||
fn from(err: CacheError) -> Self {
|
||||
Error::failed_precondition(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Some kind of data structure that holds some paths, the size of these files and a timestamp.
|
||||
/// It keeps track of the file sizes and is able to pop the path with the oldest timestamp if
|
||||
|
@ -57,16 +71,17 @@ impl SizeLimiter {
|
|||
/// to delete the file in the file system.
|
||||
fn pop(&mut self) -> Option<PathBuf> {
|
||||
if self.exceeds_limit() {
|
||||
let (next, _) = self
|
||||
.queue
|
||||
.pop()
|
||||
.expect("in_use was > 0, so the queue should have contained an item.");
|
||||
let size = self
|
||||
.sizes
|
||||
.remove(&next)
|
||||
.expect("`queue` and `sizes` should have the same keys.");
|
||||
self.in_use -= size;
|
||||
Some(next)
|
||||
if let Some((next, _)) = self.queue.pop() {
|
||||
if let Some(size) = self.sizes.remove(&next) {
|
||||
self.in_use -= size;
|
||||
} else {
|
||||
error!("`queue` and `sizes` should have the same keys.");
|
||||
}
|
||||
Some(next)
|
||||
} else {
|
||||
error!("in_use was > 0, so the queue should have contained an item.");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -85,11 +100,11 @@ impl SizeLimiter {
|
|||
return false;
|
||||
}
|
||||
|
||||
let size = self
|
||||
.sizes
|
||||
.remove(file)
|
||||
.expect("`queue` and `sizes` should have the same keys.");
|
||||
self.in_use -= size;
|
||||
if let Some(size) = self.sizes.remove(file) {
|
||||
self.in_use -= size;
|
||||
} else {
|
||||
error!("`queue` and `sizes` should have the same keys.");
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
@ -172,56 +187,70 @@ impl FsSizeLimiter {
|
|||
}
|
||||
}
|
||||
|
||||
fn add(&self, file: &Path, size: u64) {
|
||||
fn add(&self, file: &Path, size: u64) -> Result<(), Error> {
|
||||
self.limiter
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add(file, size, SystemTime::now());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn touch(&self, file: &Path) -> bool {
|
||||
self.limiter.lock().unwrap().update(file, SystemTime::now())
|
||||
fn touch(&self, file: &Path) -> Result<bool, Error> {
|
||||
Ok(self.limiter.lock().unwrap().update(file, SystemTime::now()))
|
||||
}
|
||||
|
||||
fn remove(&self, file: &Path) {
|
||||
self.limiter.lock().unwrap().remove(file);
|
||||
fn remove(&self, file: &Path) -> Result<bool, Error> {
|
||||
Ok(self.limiter.lock().unwrap().remove(file))
|
||||
}
|
||||
|
||||
fn prune_internal<F: FnMut() -> Option<PathBuf>>(mut pop: F) {
|
||||
fn prune_internal<F: FnMut() -> Result<Option<PathBuf>, Error>>(
|
||||
mut pop: F,
|
||||
) -> Result<(), Error> {
|
||||
let mut first = true;
|
||||
let mut count = 0;
|
||||
let mut last_error = None;
|
||||
|
||||
while let Some(file) = pop() {
|
||||
if first {
|
||||
debug!("Cache dir exceeds limit, removing least recently used files.");
|
||||
first = false;
|
||||
while let Ok(result) = pop() {
|
||||
if let Some(file) = result {
|
||||
if first {
|
||||
debug!("Cache dir exceeds limit, removing least recently used files.");
|
||||
first = false;
|
||||
}
|
||||
|
||||
let res = fs::remove_file(&file);
|
||||
if let Err(e) = res {
|
||||
warn!("Could not remove file {:?} from cache dir: {}", file, e);
|
||||
last_error = Some(e);
|
||||
} else {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = fs::remove_file(&file) {
|
||||
warn!("Could not remove file {:?} from cache dir: {}", file, e);
|
||||
} else {
|
||||
count += 1;
|
||||
if count > 0 {
|
||||
info!("Removed {} cache files.", count);
|
||||
}
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
info!("Removed {} cache files.", count);
|
||||
if let Some(err) = last_error {
|
||||
Err(err.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn prune(&self) {
|
||||
Self::prune_internal(|| self.limiter.lock().unwrap().pop())
|
||||
fn prune(&self) -> Result<(), Error> {
|
||||
Self::prune_internal(|| Ok(self.limiter.lock().unwrap().pop()))
|
||||
}
|
||||
|
||||
fn new(path: &Path, limit: u64) -> Self {
|
||||
fn new(path: &Path, limit: u64) -> Result<Self, Error> {
|
||||
let mut limiter = SizeLimiter::new(limit);
|
||||
|
||||
Self::init_dir(&mut limiter, path);
|
||||
Self::prune_internal(|| limiter.pop());
|
||||
Self::prune_internal(|| Ok(limiter.pop()))?;
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
limiter: Mutex::new(limiter),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,15 +263,13 @@ pub struct Cache {
|
|||
size_limiter: Option<Arc<FsSizeLimiter>>,
|
||||
}
|
||||
|
||||
pub struct RemoveFileError(());
|
||||
|
||||
impl Cache {
|
||||
pub fn new<P: AsRef<Path>>(
|
||||
credentials_path: Option<P>,
|
||||
volume_path: Option<P>,
|
||||
audio_path: Option<P>,
|
||||
size_limit: Option<u64>,
|
||||
) -> io::Result<Self> {
|
||||
) -> Result<Self, Error> {
|
||||
let mut size_limiter = None;
|
||||
|
||||
if let Some(location) = &credentials_path {
|
||||
|
@ -263,8 +290,7 @@ impl Cache {
|
|||
fs::create_dir_all(location)?;
|
||||
|
||||
if let Some(limit) = size_limit {
|
||||
let limiter = FsSizeLimiter::new(location.as_ref(), limit);
|
||||
|
||||
let limiter = FsSizeLimiter::new(location.as_ref(), limit)?;
|
||||
size_limiter = Some(Arc::new(limiter));
|
||||
}
|
||||
}
|
||||
|
@ -285,11 +311,11 @@ impl Cache {
|
|||
let location = self.credentials_location.as_ref()?;
|
||||
|
||||
// This closure is just convencience to enable the question mark operator
|
||||
let read = || {
|
||||
let read = || -> Result<Credentials, Error> {
|
||||
let mut file = File::open(location)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
serde_json::from_str(&contents).map_err(|e| Error::new(ErrorKind::InvalidData, e))
|
||||
Ok(serde_json::from_str(&contents)?)
|
||||
};
|
||||
|
||||
match read() {
|
||||
|
@ -297,7 +323,7 @@ impl Cache {
|
|||
Err(e) => {
|
||||
// If the file did not exist, the file was probably not written
|
||||
// before. Otherwise, log the error.
|
||||
if e.kind() != ErrorKind::NotFound {
|
||||
if e.kind != ErrorKind::NotFound {
|
||||
warn!("Error reading credentials from cache: {}", e);
|
||||
}
|
||||
None
|
||||
|
@ -321,19 +347,17 @@ impl Cache {
|
|||
pub fn volume(&self) -> Option<u16> {
|
||||
let location = self.volume_location.as_ref()?;
|
||||
|
||||
let read = || {
|
||||
let read = || -> Result<u16, Error> {
|
||||
let mut file = File::open(location)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
contents
|
||||
.parse()
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidData, e))
|
||||
Ok(contents.parse()?)
|
||||
};
|
||||
|
||||
match read() {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
if e.kind() != ErrorKind::NotFound {
|
||||
if e.kind != ErrorKind::NotFound {
|
||||
warn!("Error reading volume from cache: {}", e);
|
||||
}
|
||||
None
|
||||
|
@ -364,12 +388,14 @@ impl Cache {
|
|||
match File::open(&path) {
|
||||
Ok(file) => {
|
||||
if let Some(limiter) = self.size_limiter.as_deref() {
|
||||
limiter.touch(&path);
|
||||
if let Err(e) = limiter.touch(&path) {
|
||||
error!("limiter could not touch {:?}: {}", path, e);
|
||||
}
|
||||
}
|
||||
Some(file)
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() != ErrorKind::NotFound {
|
||||
if e.kind() != io::ErrorKind::NotFound {
|
||||
warn!("Error reading file from cache: {}", e)
|
||||
}
|
||||
None
|
||||
|
@ -377,7 +403,7 @@ impl Cache {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) -> bool {
|
||||
pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) -> Result<(), Error> {
|
||||
if let Some(path) = self.file_path(file) {
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Ok(size) = fs::create_dir_all(parent)
|
||||
|
@ -385,28 +411,25 @@ impl Cache {
|
|||
.and_then(|mut file| io::copy(contents, &mut file))
|
||||
{
|
||||
if let Some(limiter) = self.size_limiter.as_deref() {
|
||||
limiter.add(&path, size);
|
||||
limiter.prune();
|
||||
limiter.add(&path, size)?;
|
||||
limiter.prune()?
|
||||
}
|
||||
return true;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
Err(CacheError::Path.into())
|
||||
}
|
||||
|
||||
pub fn remove_file(&self, file: FileId) -> Result<(), RemoveFileError> {
|
||||
let path = self.file_path(file).ok_or(RemoveFileError(()))?;
|
||||
pub fn remove_file(&self, file: FileId) -> Result<(), Error> {
|
||||
let path = self.file_path(file).ok_or(CacheError::Path)?;
|
||||
|
||||
if let Err(err) = fs::remove_file(&path) {
|
||||
warn!("Unable to remove file from cache: {}", err);
|
||||
Err(RemoveFileError(()))
|
||||
} else {
|
||||
if let Some(limiter) = self.size_limiter.as_deref() {
|
||||
limiter.remove(&path);
|
||||
}
|
||||
Ok(())
|
||||
fs::remove_file(&path)?;
|
||||
if let Some(limiter) = self.size_limiter.as_deref() {
|
||||
limiter.remove(&path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +1,19 @@
|
|||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use protobuf::{Message, ProtobufError};
|
||||
use protobuf::Message;
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use super::date::Date;
|
||||
use super::file_id::FileId;
|
||||
use super::session::Session;
|
||||
use super::spclient::SpClientError;
|
||||
use super::{date::Date, Error, FileId, Session};
|
||||
|
||||
use librespot_protocol as protocol;
|
||||
use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage;
|
||||
use protocol::storage_resolve::StorageResolveResponse_Result;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CdnUrlError {
|
||||
#[error("no URLs available")]
|
||||
Empty,
|
||||
#[error("all tokens expired")]
|
||||
Expired,
|
||||
#[error("error parsing response")]
|
||||
Parsing,
|
||||
#[error("could not parse protobuf: {0}")]
|
||||
Protobuf(#[from] ProtobufError),
|
||||
#[error("could not complete API request: {0}")]
|
||||
SpClient(#[from] SpClientError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MaybeExpiringUrl(pub String, pub Option<Date>);
|
||||
|
||||
|
@ -48,10 +33,27 @@ impl DerefMut for MaybeExpiringUrls {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CdnUrlError {
|
||||
#[error("all URLs expired")]
|
||||
Expired,
|
||||
#[error("resolved storage is not for CDN")]
|
||||
Storage,
|
||||
}
|
||||
|
||||
impl From<CdnUrlError> for Error {
|
||||
fn from(err: CdnUrlError) -> Self {
|
||||
match err {
|
||||
CdnUrlError::Expired => Error::deadline_exceeded(err),
|
||||
CdnUrlError::Storage => Error::unavailable(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CdnUrl {
|
||||
pub file_id: FileId,
|
||||
pub urls: MaybeExpiringUrls,
|
||||
urls: MaybeExpiringUrls,
|
||||
}
|
||||
|
||||
impl CdnUrl {
|
||||
|
@ -62,7 +64,7 @@ impl CdnUrl {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_audio(&self, session: &Session) -> Result<Self, CdnUrlError> {
|
||||
pub async fn resolve_audio(&self, session: &Session) -> Result<Self, Error> {
|
||||
let file_id = self.file_id;
|
||||
let response = session.spclient().get_audio_urls(file_id).await?;
|
||||
let msg = CdnUrlMessage::parse_from_bytes(&response)?;
|
||||
|
@ -75,37 +77,26 @@ impl CdnUrl {
|
|||
Ok(cdn_url)
|
||||
}
|
||||
|
||||
pub fn get_url(&mut self) -> Result<&str, CdnUrlError> {
|
||||
if self.urls.is_empty() {
|
||||
return Err(CdnUrlError::Empty);
|
||||
}
|
||||
|
||||
// prune expired URLs until the first one is current, or none are left
|
||||
pub fn try_get_url(&self) -> Result<&str, Error> {
|
||||
let now = Local::now();
|
||||
while !self.urls.is_empty() {
|
||||
let maybe_expiring = self.urls[0].1;
|
||||
if let Some(expiry) = maybe_expiring {
|
||||
if now < expiry.as_utc() {
|
||||
break;
|
||||
} else {
|
||||
self.urls.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
let url = self.urls.iter().find(|url| match url.1 {
|
||||
Some(expiry) => now < expiry.as_utc(),
|
||||
None => true,
|
||||
});
|
||||
|
||||
if let Some(cdn_url) = self.urls.first() {
|
||||
Ok(&cdn_url.0)
|
||||
if let Some(url) = url {
|
||||
Ok(&url.0)
|
||||
} else {
|
||||
Err(CdnUrlError::Expired)
|
||||
Err(CdnUrlError::Expired.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
|
||||
type Error = CdnUrlError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(msg: CdnUrlMessage) -> Result<Self, Self::Error> {
|
||||
if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) {
|
||||
return Err(CdnUrlError::Parsing);
|
||||
return Err(CdnUrlError::Storage.into());
|
||||
}
|
||||
|
||||
let is_expiring = !msg.get_fileid().is_empty();
|
||||
|
@ -114,7 +105,7 @@ impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
|
|||
.get_cdnurl()
|
||||
.iter()
|
||||
.map(|cdn_url| {
|
||||
let url = Url::parse(cdn_url).map_err(|_| CdnUrlError::Parsing)?;
|
||||
let url = Url::parse(cdn_url)?;
|
||||
|
||||
if is_expiring {
|
||||
let expiry_str = if let Some(token) = url
|
||||
|
@ -122,29 +113,47 @@ impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
|
|||
.into_iter()
|
||||
.find(|(key, _value)| key == "__token__")
|
||||
{
|
||||
let start = token.1.find("exp=").ok_or(CdnUrlError::Parsing)?;
|
||||
let slice = &token.1[start + 4..];
|
||||
let end = slice.find('~').ok_or(CdnUrlError::Parsing)?;
|
||||
String::from(&slice[..end])
|
||||
if let Some(mut start) = token.1.find("exp=") {
|
||||
start += 4;
|
||||
if token.1.len() >= start {
|
||||
let slice = &token.1[start..];
|
||||
if let Some(end) = slice.find('~') {
|
||||
// this is the only valid invariant for akamaized.net
|
||||
String::from(&slice[..end])
|
||||
} else {
|
||||
String::from(slice)
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else if let Some(query) = url.query() {
|
||||
let mut items = query.split('_');
|
||||
String::from(items.next().ok_or(CdnUrlError::Parsing)?)
|
||||
if let Some(first) = items.next() {
|
||||
// this is the only valid invariant for scdn.co
|
||||
String::from(first)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
return Err(CdnUrlError::Parsing);
|
||||
String::new()
|
||||
};
|
||||
|
||||
let mut expiry: i64 = expiry_str.parse().map_err(|_| CdnUrlError::Parsing)?;
|
||||
let mut expiry: i64 = expiry_str.parse()?;
|
||||
|
||||
expiry -= 5 * 60; // seconds
|
||||
|
||||
Ok(MaybeExpiringUrl(
|
||||
cdn_url.to_owned(),
|
||||
Some(expiry.try_into().map_err(|_| CdnUrlError::Parsing)?),
|
||||
Some(expiry.try_into()?),
|
||||
))
|
||||
} else {
|
||||
Ok(MaybeExpiringUrl(cdn_url.to_owned(), None))
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<MaybeExpiringUrl>, CdnUrlError>>()?;
|
||||
.collect::<Result<Vec<MaybeExpiringUrl>, Error>>()?;
|
||||
|
||||
Ok(Self(result))
|
||||
}
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use futures_util::lock::BiLock;
|
||||
use futures_util::{ready, StreamExt};
|
||||
use futures_util::{lock::BiLock, ready, StreamExt};
|
||||
use num_traits::FromPrimitive;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::packet::PacketType;
|
||||
use crate::util::SeqGenerator;
|
||||
use crate::{packet::PacketType, util::SeqGenerator, Error};
|
||||
|
||||
component! {
|
||||
ChannelManager : ChannelManagerInner {
|
||||
|
@ -27,9 +29,21 @@ component! {
|
|||
|
||||
const ONE_SECOND_IN_MS: usize = 1000;
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
|
||||
#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)]
|
||||
pub struct ChannelError;
|
||||
|
||||
impl From<ChannelError> for Error {
|
||||
fn from(err: ChannelError) -> Self {
|
||||
Error::aborted(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ChannelError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "channel error")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Channel {
|
||||
receiver: mpsc::UnboundedReceiver<(u8, Bytes)>,
|
||||
state: ChannelState,
|
||||
|
@ -70,7 +84,7 @@ impl ChannelManager {
|
|||
(seq, channel)
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) {
|
||||
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref());
|
||||
|
@ -94,9 +108,14 @@ impl ChannelManager {
|
|||
inner.download_measurement_bytes += data.len();
|
||||
|
||||
if let Entry::Occupied(entry) = inner.channels.entry(id) {
|
||||
let _ = entry.get().send((cmd as u8, data));
|
||||
entry
|
||||
.get()
|
||||
.send((cmd as u8, data))
|
||||
.map_err(|_| ChannelError)?;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_download_rate_estimate(&self) -> usize {
|
||||
|
@ -142,7 +161,11 @@ impl Stream for Channel {
|
|||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
match self.state.clone() {
|
||||
ChannelState::Closed => panic!("Polling already terminated channel"),
|
||||
ChannelState::Closed => {
|
||||
error!("Polling already terminated channel");
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
ChannelState::Header(mut data) => {
|
||||
if data.is_empty() {
|
||||
data = ready!(self.recv_packet(cx))?;
|
||||
|
|
|
@ -14,7 +14,7 @@ macro_rules! component {
|
|||
|
||||
#[allow(dead_code)]
|
||||
fn lock<F: FnOnce(&mut $inner) -> R, R>(&self, f: F) -> R {
|
||||
let mut inner = (self.0).1.lock().expect("Mutex poisoned");
|
||||
let mut inner = (self.0).1.lock().unwrap();
|
||||
f(&mut inner)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, path::PathBuf, str::FromStr};
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
use std::io;
|
||||
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use shannon::Shannon;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
const HEADER_SIZE: usize = 3;
|
||||
const MAC_SIZE: usize = 4;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ApCodecError {
|
||||
#[error("payload was malformed")]
|
||||
Payload,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DecodeState {
|
||||
Header,
|
||||
|
@ -87,7 +95,10 @@ impl Decoder for ApCodec {
|
|||
|
||||
let mut payload = buf.split_to(size + MAC_SIZE);
|
||||
|
||||
self.decode_cipher.decrypt(payload.get_mut(..size).unwrap());
|
||||
self.decode_cipher
|
||||
.decrypt(payload.get_mut(..size).ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, ApCodecError::Payload)
|
||||
})?);
|
||||
let mac = payload.split_off(size);
|
||||
self.decode_cipher.check_mac(mac.as_ref())?;
|
||||
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
use std::{env::consts::ARCH, io};
|
||||
|
||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||
use hmac::{Hmac, Mac, NewMac};
|
||||
use protobuf::{self, Message};
|
||||
use rand::{thread_rng, RngCore};
|
||||
use sha1::Sha1;
|
||||
use std::env::consts::ARCH;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio_util::codec::{Decoder, Framed};
|
||||
|
||||
use super::codec::ApCodec;
|
||||
use crate::diffie_hellman::DhLocalKeys;
|
||||
|
||||
use crate::{diffie_hellman::DhLocalKeys, version};
|
||||
|
||||
use crate::protocol;
|
||||
use crate::protocol::keyexchange::{
|
||||
APResponseMessage, ClientHello, ClientResponsePlaintext, Platform, ProductFlags,
|
||||
};
|
||||
use crate::version;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HandshakeError {
|
||||
#[error("invalid key length")]
|
||||
InvalidLength,
|
||||
}
|
||||
|
||||
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
||||
mut connection: T,
|
||||
|
@ -31,7 +39,7 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
|||
.to_owned();
|
||||
|
||||
let shared_secret = local_keys.shared_secret(&remote_key);
|
||||
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator);
|
||||
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator)?;
|
||||
let codec = ApCodec::new(&send_key, &recv_key);
|
||||
|
||||
client_response(&mut connection, challenge).await?;
|
||||
|
@ -112,8 +120,8 @@ where
|
|||
|
||||
let mut buffer = vec![0, 4];
|
||||
let size = 2 + 4 + packet.compute_size();
|
||||
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap();
|
||||
packet.write_to_vec(&mut buffer).unwrap();
|
||||
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?;
|
||||
packet.write_to_vec(&mut buffer)?;
|
||||
|
||||
connection.write_all(&buffer[..]).await?;
|
||||
Ok(buffer)
|
||||
|
@ -133,8 +141,8 @@ where
|
|||
|
||||
let mut buffer = vec![];
|
||||
let size = 4 + packet.compute_size();
|
||||
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap();
|
||||
packet.write_to_vec(&mut buffer).unwrap();
|
||||
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?;
|
||||
packet.write_to_vec(&mut buffer)?;
|
||||
|
||||
connection.write_all(&buffer[..]).await?;
|
||||
Ok(())
|
||||
|
@ -148,7 +156,7 @@ where
|
|||
let header = read_into_accumulator(connection, 4, acc).await?;
|
||||
let size = BigEndian::read_u32(header) as usize;
|
||||
let data = read_into_accumulator(connection, size - 4, acc).await?;
|
||||
let message = M::parse_from_bytes(data).unwrap();
|
||||
let message = M::parse_from_bytes(data)?;
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
|
@ -164,24 +172,26 @@ async fn read_into_accumulator<'a, 'b, T: AsyncRead + Unpin>(
|
|||
Ok(&mut acc[offset..])
|
||||
}
|
||||
|
||||
fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
|
||||
fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> io::Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
|
||||
type HmacSha1 = Hmac<Sha1>;
|
||||
|
||||
let mut data = Vec::with_capacity(0x64);
|
||||
for i in 1..6 {
|
||||
let mut mac =
|
||||
HmacSha1::new_from_slice(shared_secret).expect("HMAC can take key of any size");
|
||||
let mut mac = HmacSha1::new_from_slice(shared_secret).map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength)
|
||||
})?;
|
||||
mac.update(packets);
|
||||
mac.update(&[i]);
|
||||
data.extend_from_slice(&mac.finalize().into_bytes());
|
||||
}
|
||||
|
||||
let mut mac = HmacSha1::new_from_slice(&data[..0x14]).expect("HMAC can take key of any size");
|
||||
let mut mac = HmacSha1::new_from_slice(&data[..0x14])
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength))?;
|
||||
mac.update(packets);
|
||||
|
||||
(
|
||||
Ok((
|
||||
mac.finalize().into_bytes().to_vec(),
|
||||
data[0x14..0x34].to_vec(),
|
||||
data[0x34..0x54].to_vec(),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
mod codec;
|
||||
mod handshake;
|
||||
|
||||
pub use self::codec::ApCodec;
|
||||
pub use self::handshake::handshake;
|
||||
pub use self::{codec::ApCodec, handshake::handshake};
|
||||
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::io;
|
||||
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use num_traits::FromPrimitive;
|
||||
use protobuf::{self, Message, ProtobufError};
|
||||
use protobuf::{self, Message};
|
||||
use thiserror::Error;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_util::codec::Framed;
|
||||
use url::Url;
|
||||
|
||||
use crate::authentication::Credentials;
|
||||
use crate::packet::PacketType;
|
||||
use crate::{authentication::Credentials, packet::PacketType, version, Error};
|
||||
|
||||
use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
|
||||
use crate::version;
|
||||
|
||||
pub type Transport = Framed<TcpStream, ApCodec>;
|
||||
|
||||
|
@ -42,13 +40,19 @@ fn login_error_message(code: &ErrorCode) -> &'static str {
|
|||
pub enum AuthenticationError {
|
||||
#[error("Login failed with reason: {}", login_error_message(.0))]
|
||||
LoginFailed(ErrorCode),
|
||||
#[error("Authentication failed: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("invalid packet {0}")]
|
||||
Packet(u8),
|
||||
#[error("transport returned no data")]
|
||||
Transport,
|
||||
}
|
||||
|
||||
impl From<ProtobufError> for AuthenticationError {
|
||||
fn from(e: ProtobufError) -> Self {
|
||||
io::Error::new(ErrorKind::InvalidData, e).into()
|
||||
impl From<AuthenticationError> for Error {
|
||||
fn from(err: AuthenticationError) -> Self {
|
||||
match err {
|
||||
AuthenticationError::LoginFailed(_) => Error::permission_denied(err),
|
||||
AuthenticationError::Packet(_) => Error::unimplemented(err),
|
||||
AuthenticationError::Transport => Error::unavailable(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +72,7 @@ pub async fn authenticate(
|
|||
transport: &mut Transport,
|
||||
credentials: Credentials,
|
||||
device_id: &str,
|
||||
) -> Result<Credentials, AuthenticationError> {
|
||||
) -> Result<Credentials, Error> {
|
||||
use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
|
||||
|
||||
let cpu_family = match std::env::consts::ARCH {
|
||||
|
@ -119,12 +123,15 @@ pub async fn authenticate(
|
|||
packet.set_version_string(format!("librespot {}", version::SEMVER));
|
||||
|
||||
let cmd = PacketType::Login;
|
||||
let data = packet.write_to_bytes().unwrap();
|
||||
let data = packet.write_to_bytes()?;
|
||||
|
||||
transport.send((cmd as u8, data)).await?;
|
||||
let (cmd, data) = transport.next().await.expect("EOF")?;
|
||||
let (cmd, data) = transport
|
||||
.next()
|
||||
.await
|
||||
.ok_or(AuthenticationError::Transport)??;
|
||||
let packet_type = FromPrimitive::from_u8(cmd);
|
||||
match packet_type {
|
||||
let result = match packet_type {
|
||||
Some(PacketType::APWelcome) => {
|
||||
let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?;
|
||||
|
||||
|
@ -141,8 +148,13 @@ pub async fn authenticate(
|
|||
Err(error_data.into())
|
||||
}
|
||||
_ => {
|
||||
let msg = format!("Received invalid packet: {}", cmd);
|
||||
Err(io::Error::new(ErrorKind::InvalidData, msg).into())
|
||||
trace!(
|
||||
"Did not expect {:?} AES key packet with data {:#?}",
|
||||
cmd,
|
||||
data
|
||||
);
|
||||
Err(AuthenticationError::Packet(cmd))
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(result?)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use std::{convert::TryFrom, fmt::Debug, ops::Deref};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
use librespot_protocol as protocol;
|
||||
use protocol::metadata::Date as DateMessage;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DateError {
|
||||
#[error("item has invalid date")]
|
||||
InvalidTimestamp,
|
||||
#[error("item has invalid timestamp {0}")]
|
||||
Timestamp(i64),
|
||||
}
|
||||
|
||||
impl From<DateError> for Error {
|
||||
fn from(err: DateError) -> Self {
|
||||
Error::invalid_argument(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
@ -30,11 +35,11 @@ impl Date {
|
|||
self.0.timestamp()
|
||||
}
|
||||
|
||||
pub fn from_timestamp(timestamp: i64) -> Result<Self, DateError> {
|
||||
pub fn from_timestamp(timestamp: i64) -> Result<Self, Error> {
|
||||
if let Some(date_time) = NaiveDateTime::from_timestamp_opt(timestamp, 0) {
|
||||
Ok(Self::from_utc(date_time))
|
||||
} else {
|
||||
Err(DateError::InvalidTimestamp)
|
||||
Err(DateError::Timestamp(timestamp).into())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,7 +72,7 @@ impl From<DateTime<Utc>> for Date {
|
|||
}
|
||||
|
||||
impl TryFrom<i64> for Date {
|
||||
type Error = DateError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(timestamp: i64) -> Result<Self, Self::Error> {
|
||||
Self::from_timestamp(timestamp)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AlreadyHandledError(());
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HandlerMapError {
|
||||
#[error("request was already handled")]
|
||||
AlreadyHandled,
|
||||
}
|
||||
|
||||
impl From<HandlerMapError> for Error {
|
||||
fn from(err: HandlerMapError) -> Self {
|
||||
Error::aborted(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum HandlerMap<T> {
|
||||
Leaf(T),
|
||||
|
@ -19,9 +32,9 @@ impl<T> HandlerMap<T> {
|
|||
&mut self,
|
||||
mut path: impl Iterator<Item = &'a str>,
|
||||
handler: T,
|
||||
) -> Result<(), AlreadyHandledError> {
|
||||
) -> Result<(), Error> {
|
||||
match self {
|
||||
Self::Leaf(_) => Err(AlreadyHandledError(())),
|
||||
Self::Leaf(_) => Err(HandlerMapError::AlreadyHandled.into()),
|
||||
Self::Branch(children) => {
|
||||
if let Some(component) = path.next() {
|
||||
let node = children.entry(component.to_owned()).or_default();
|
||||
|
@ -30,7 +43,7 @@ impl<T> HandlerMap<T> {
|
|||
*self = Self::Leaf(handler);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AlreadyHandledError(()))
|
||||
Err(HandlerMapError::AlreadyHandled.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,40 @@
|
|||
mod maps;
|
||||
pub mod protocol;
|
||||
|
||||
use std::iter;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{atomic, Arc, Mutex};
|
||||
use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
iter,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Arc, Mutex,
|
||||
},
|
||||
task::Poll,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures_core::{Future, Stream};
|
||||
use futures_util::future::join_all;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use futures_util::{future::join_all, SinkExt, StreamExt};
|
||||
use thiserror::Error;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::{self, UnboundedReceiver};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{
|
||||
mpsc::{self, UnboundedReceiver},
|
||||
Semaphore,
|
||||
},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_tungstenite::tungstenite;
|
||||
use tungstenite::error::UrlError;
|
||||
use url::Url;
|
||||
|
||||
use self::maps::*;
|
||||
use self::protocol::*;
|
||||
use crate::socket;
|
||||
use crate::util::{keep_flushing, CancelOnDrop, TimeoutOnDrop};
|
||||
|
||||
use crate::{
|
||||
socket,
|
||||
util::{keep_flushing, CancelOnDrop, TimeoutOnDrop},
|
||||
Error,
|
||||
};
|
||||
|
||||
type WsMessage = tungstenite::Message;
|
||||
type WsError = tungstenite::Error;
|
||||
|
@ -164,24 +175,38 @@ fn split_uri(s: &str) -> Option<impl Iterator<Item = &'_ str>> {
|
|||
pub enum AddHandlerError {
|
||||
#[error("There is already a handler for the given uri")]
|
||||
AlreadyHandled,
|
||||
#[error("The specified uri is invalid")]
|
||||
InvalidUri,
|
||||
#[error("The specified uri {0} is invalid")]
|
||||
InvalidUri(String),
|
||||
}
|
||||
|
||||
impl From<AddHandlerError> for Error {
|
||||
fn from(err: AddHandlerError) -> Self {
|
||||
match err {
|
||||
AddHandlerError::AlreadyHandled => Error::aborted(err),
|
||||
AddHandlerError::InvalidUri(_) => Error::invalid_argument(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum SubscriptionError {
|
||||
#[error("The specified uri is invalid")]
|
||||
InvalidUri,
|
||||
InvalidUri(String),
|
||||
}
|
||||
|
||||
impl From<SubscriptionError> for Error {
|
||||
fn from(err: SubscriptionError) -> Self {
|
||||
Error::invalid_argument(err)
|
||||
}
|
||||
}
|
||||
|
||||
fn add_handler(
|
||||
map: &mut HandlerMap<Box<dyn RequestHandler>>,
|
||||
uri: &str,
|
||||
handler: impl RequestHandler,
|
||||
) -> Result<(), AddHandlerError> {
|
||||
let split = split_uri(uri).ok_or(AddHandlerError::InvalidUri)?;
|
||||
) -> Result<(), Error> {
|
||||
let split = split_uri(uri).ok_or_else(|| AddHandlerError::InvalidUri(uri.to_string()))?;
|
||||
map.insert(split, Box::new(handler))
|
||||
.map_err(|_| AddHandlerError::AlreadyHandled)
|
||||
}
|
||||
|
||||
fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> {
|
||||
|
@ -191,11 +216,11 @@ fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> {
|
|||
fn subscribe(
|
||||
map: &mut SubscriberMap<MessageHandler>,
|
||||
uris: &[&str],
|
||||
) -> Result<Subscription, SubscriptionError> {
|
||||
) -> Result<Subscription, Error> {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
for &uri in uris {
|
||||
let split = split_uri(uri).ok_or(SubscriptionError::InvalidUri)?;
|
||||
let split = split_uri(uri).ok_or_else(|| SubscriptionError::InvalidUri(uri.to_string()))?;
|
||||
map.insert(split, tx.clone());
|
||||
}
|
||||
|
||||
|
@ -237,15 +262,11 @@ impl Builder {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add_handler(
|
||||
&mut self,
|
||||
uri: &str,
|
||||
handler: impl RequestHandler,
|
||||
) -> Result<(), AddHandlerError> {
|
||||
pub fn add_handler(&mut self, uri: &str, handler: impl RequestHandler) -> Result<(), Error> {
|
||||
add_handler(&mut self.request_handlers, uri, handler)
|
||||
}
|
||||
|
||||
pub fn subscribe(&mut self, uris: &[&str]) -> Result<Subscription, SubscriptionError> {
|
||||
pub fn subscribe(&mut self, uris: &[&str]) -> Result<Subscription, Error> {
|
||||
subscribe(&mut self.message_handlers, uris)
|
||||
}
|
||||
|
||||
|
@ -342,7 +363,7 @@ pub struct Dealer {
|
|||
}
|
||||
|
||||
impl Dealer {
|
||||
pub fn add_handler<H>(&self, uri: &str, handler: H) -> Result<(), AddHandlerError>
|
||||
pub fn add_handler<H>(&self, uri: &str, handler: H) -> Result<(), Error>
|
||||
where
|
||||
H: RequestHandler,
|
||||
{
|
||||
|
@ -357,7 +378,7 @@ impl Dealer {
|
|||
remove_handler(&mut self.shared.request_handlers.lock().unwrap(), uri)
|
||||
}
|
||||
|
||||
pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, SubscriptionError> {
|
||||
pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, Error> {
|
||||
subscribe(&mut self.shared.message_handlers.lock().unwrap(), uris)
|
||||
}
|
||||
|
||||
|
@ -367,7 +388,9 @@ impl Dealer {
|
|||
self.shared.notify_drop.close();
|
||||
|
||||
if let Some(handle) = self.handle.take() {
|
||||
CancelOnDrop(handle).await.unwrap();
|
||||
if let Err(e) = CancelOnDrop(handle).await {
|
||||
error!("error aborting dealer operations: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -556,11 +579,15 @@ async fn run<F, Fut>(
|
|||
select! {
|
||||
() = shared.closed() => break,
|
||||
r = t0 => {
|
||||
r.unwrap(); // Whatever has gone wrong (probably panicked), we can't handle it, so let's panic too.
|
||||
if let Err(e) = r {
|
||||
error!("timeout on task 0: {}", e);
|
||||
}
|
||||
tasks.0.take();
|
||||
},
|
||||
r = t1 => {
|
||||
r.unwrap();
|
||||
if let Err(e) = r {
|
||||
error!("timeout on task 1: {}", e);
|
||||
}
|
||||
tasks.1.take();
|
||||
}
|
||||
}
|
||||
|
@ -576,7 +603,7 @@ async fn run<F, Fut>(
|
|||
match connect(&url, proxy.as_ref(), &shared).await {
|
||||
Ok((s, r)) => tasks = (init_task(s), init_task(r)),
|
||||
Err(e) => {
|
||||
warn!("Error while connecting: {}", e);
|
||||
error!("Error while connecting: {}", e);
|
||||
tokio::time::sleep(RECONNECT_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
|
|
437
core/src/error.rs
Normal file
437
core/src/error.rs
Normal file
|
@ -0,0 +1,437 @@
|
|||
use std::{error, fmt, num::ParseIntError, str::Utf8Error, string::FromUtf8Error};
|
||||
|
||||
use base64::DecodeError;
|
||||
use http::{
|
||||
header::{InvalidHeaderName, InvalidHeaderValue, ToStrError},
|
||||
method::InvalidMethod,
|
||||
status::InvalidStatusCode,
|
||||
uri::{InvalidUri, InvalidUriParts},
|
||||
};
|
||||
use protobuf::ProtobufError;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{mpsc::error::SendError, oneshot::error::RecvError};
|
||||
use url::ParseError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
pub kind: ErrorKind,
|
||||
pub error: Box<dyn error::Error + Send + Sync>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum ErrorKind {
|
||||
#[error("The operation was cancelled by the caller")]
|
||||
Cancelled = 1,
|
||||
|
||||
#[error("Unknown error")]
|
||||
Unknown = 2,
|
||||
|
||||
#[error("Client specified an invalid argument")]
|
||||
InvalidArgument = 3,
|
||||
|
||||
#[error("Deadline expired before operation could complete")]
|
||||
DeadlineExceeded = 4,
|
||||
|
||||
#[error("Requested entity was not found")]
|
||||
NotFound = 5,
|
||||
|
||||
#[error("Attempt to create entity that already exists")]
|
||||
AlreadyExists = 6,
|
||||
|
||||
#[error("Permission denied")]
|
||||
PermissionDenied = 7,
|
||||
|
||||
#[error("No valid authentication credentials")]
|
||||
Unauthenticated = 16,
|
||||
|
||||
#[error("Resource has been exhausted")]
|
||||
ResourceExhausted = 8,
|
||||
|
||||
#[error("Invalid state")]
|
||||
FailedPrecondition = 9,
|
||||
|
||||
#[error("Operation aborted")]
|
||||
Aborted = 10,
|
||||
|
||||
#[error("Operation attempted past the valid range")]
|
||||
OutOfRange = 11,
|
||||
|
||||
#[error("Not implemented")]
|
||||
Unimplemented = 12,
|
||||
|
||||
#[error("Internal error")]
|
||||
Internal = 13,
|
||||
|
||||
#[error("Service unavailable")]
|
||||
Unavailable = 14,
|
||||
|
||||
#[error("Unrecoverable data loss or corruption")]
|
||||
DataLoss = 15,
|
||||
|
||||
#[error("Operation must not be used")]
|
||||
DoNotUse = -1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
struct ErrorMessage(String);
|
||||
|
||||
impl fmt::Display for ErrorMessage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new<E>(kind: ErrorKind, error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aborted<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::Aborted,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn already_exists<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::AlreadyExists,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancelled<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::Cancelled,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data_loss<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::DataLoss,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deadline_exceeded<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::DeadlineExceeded,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_not_use<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::DoNotUse,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn failed_precondition<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::FailedPrecondition,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::Internal,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalid_argument<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::InvalidArgument,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_found<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::NotFound,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn out_of_range<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::OutOfRange,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn permission_denied<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::PermissionDenied,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resource_exhausted<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::ResourceExhausted,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unauthenticated<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::Unauthenticated,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unavailable<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::Unavailable,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unimplemented<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::Unimplemented,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unknown<E>(error: E) -> Error
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
kind: ErrorKind::Unknown,
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.error.source()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(fmt, "{} {{ ", self.kind)?;
|
||||
self.error.fmt(fmt)?;
|
||||
write!(fmt, " }}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for Error {
|
||||
fn from(err: DecodeError) -> Self {
|
||||
Self::new(ErrorKind::FailedPrecondition, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<http::Error> for Error {
|
||||
fn from(err: http::Error) -> Self {
|
||||
if err.is::<InvalidHeaderName>()
|
||||
|| err.is::<InvalidHeaderValue>()
|
||||
|| err.is::<InvalidMethod>()
|
||||
|| err.is::<InvalidUri>()
|
||||
|| err.is::<InvalidUriParts>()
|
||||
{
|
||||
return Self::new(ErrorKind::InvalidArgument, err);
|
||||
}
|
||||
|
||||
if err.is::<InvalidStatusCode>() {
|
||||
return Self::new(ErrorKind::FailedPrecondition, err);
|
||||
}
|
||||
|
||||
Self::new(ErrorKind::Unknown, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hyper::Error> for Error {
|
||||
fn from(err: hyper::Error) -> Self {
|
||||
if err.is_parse() || err.is_parse_too_large() || err.is_parse_status() || err.is_user() {
|
||||
return Self::new(ErrorKind::Internal, err);
|
||||
}
|
||||
|
||||
if err.is_canceled() {
|
||||
return Self::new(ErrorKind::Cancelled, err);
|
||||
}
|
||||
|
||||
if err.is_connect() {
|
||||
return Self::new(ErrorKind::Unavailable, err);
|
||||
}
|
||||
|
||||
if err.is_incomplete_message() {
|
||||
return Self::new(ErrorKind::DataLoss, err);
|
||||
}
|
||||
|
||||
if err.is_body_write_aborted() || err.is_closed() {
|
||||
return Self::new(ErrorKind::Aborted, err);
|
||||
}
|
||||
|
||||
if err.is_timeout() {
|
||||
return Self::new(ErrorKind::DeadlineExceeded, err);
|
||||
}
|
||||
|
||||
Self::new(ErrorKind::Unknown, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<quick_xml::Error> for Error {
|
||||
fn from(err: quick_xml::Error) -> Self {
|
||||
Self::new(ErrorKind::FailedPrecondition, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
Self::new(ErrorKind::FailedPrecondition, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
use std::io::ErrorKind as IoErrorKind;
|
||||
match err.kind() {
|
||||
IoErrorKind::NotFound => Self::new(ErrorKind::NotFound, err),
|
||||
IoErrorKind::PermissionDenied => Self::new(ErrorKind::PermissionDenied, err),
|
||||
IoErrorKind::AddrInUse | IoErrorKind::AlreadyExists => {
|
||||
Self::new(ErrorKind::AlreadyExists, err)
|
||||
}
|
||||
IoErrorKind::AddrNotAvailable
|
||||
| IoErrorKind::ConnectionRefused
|
||||
| IoErrorKind::NotConnected => Self::new(ErrorKind::Unavailable, err),
|
||||
IoErrorKind::BrokenPipe
|
||||
| IoErrorKind::ConnectionReset
|
||||
| IoErrorKind::ConnectionAborted => Self::new(ErrorKind::Aborted, err),
|
||||
IoErrorKind::Interrupted | IoErrorKind::WouldBlock => {
|
||||
Self::new(ErrorKind::Cancelled, err)
|
||||
}
|
||||
IoErrorKind::InvalidData | IoErrorKind::UnexpectedEof => {
|
||||
Self::new(ErrorKind::FailedPrecondition, err)
|
||||
}
|
||||
IoErrorKind::TimedOut => Self::new(ErrorKind::DeadlineExceeded, err),
|
||||
IoErrorKind::InvalidInput => Self::new(ErrorKind::InvalidArgument, err),
|
||||
IoErrorKind::WriteZero => Self::new(ErrorKind::ResourceExhausted, err),
|
||||
_ => Self::new(ErrorKind::Unknown, err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(err: FromUtf8Error) -> Self {
|
||||
Self::new(ErrorKind::FailedPrecondition, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvalidHeaderValue> for Error {
|
||||
fn from(err: InvalidHeaderValue) -> Self {
|
||||
Self::new(ErrorKind::InvalidArgument, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvalidUri> for Error {
|
||||
fn from(err: InvalidUri) -> Self {
|
||||
Self::new(ErrorKind::InvalidArgument, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseError> for Error {
|
||||
fn from(err: ParseError) -> Self {
|
||||
Self::new(ErrorKind::FailedPrecondition, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for Error {
|
||||
fn from(err: ParseIntError) -> Self {
|
||||
Self::new(ErrorKind::FailedPrecondition, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProtobufError> for Error {
|
||||
fn from(err: ProtobufError) -> Self {
|
||||
Self::new(ErrorKind::FailedPrecondition, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RecvError> for Error {
|
||||
fn from(err: RecvError) -> Self {
|
||||
Self::new(ErrorKind::Internal, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<SendError<T>> for Error {
|
||||
fn from(err: SendError<T>) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::Internal,
|
||||
error: ErrorMessage(err.to_string()).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToStrError> for Error {
|
||||
fn from(err: ToStrError) -> Self {
|
||||
Self::new(ErrorKind::FailedPrecondition, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Utf8Error> for Error {
|
||||
fn from(err: Utf8Error) -> Self {
|
||||
Self::new(ErrorKind::FailedPrecondition, err)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
use librespot_protocol as protocol;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use librespot_protocol as protocol;
|
||||
|
||||
use crate::spotify_id::to_base16;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
|
|
|
@ -1,49 +1,82 @@
|
|||
use std::env::consts::OS;
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::IntoStream;
|
||||
use futures_util::FutureExt;
|
||||
use futures_util::{future::IntoStream, FutureExt};
|
||||
use http::header::HeaderValue;
|
||||
use http::uri::InvalidUri;
|
||||
use hyper::client::{HttpConnector, ResponseFuture};
|
||||
use hyper::header::USER_AGENT;
|
||||
use hyper::{Body, Client, Request, Response, StatusCode};
|
||||
use hyper::{
|
||||
client::{HttpConnector, ResponseFuture},
|
||||
header::USER_AGENT,
|
||||
Body, Client, Request, Response, StatusCode,
|
||||
};
|
||||
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
|
||||
use hyper_rustls::HttpsConnector;
|
||||
use rustls::{ClientConfig, RootCertStore};
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
use std::env::consts::OS;
|
||||
|
||||
use crate::version::{
|
||||
FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING,
|
||||
use crate::{
|
||||
version::{FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING},
|
||||
Error,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HttpClientError {
|
||||
#[error("Response status code: {0}")]
|
||||
StatusCode(hyper::StatusCode),
|
||||
}
|
||||
|
||||
impl From<HttpClientError> for Error {
|
||||
fn from(err: HttpClientError) -> Self {
|
||||
match err {
|
||||
HttpClientError::StatusCode(code) => {
|
||||
// not exhaustive, but what reasonably could be expected
|
||||
match code {
|
||||
StatusCode::GATEWAY_TIMEOUT | StatusCode::REQUEST_TIMEOUT => {
|
||||
Error::deadline_exceeded(err)
|
||||
}
|
||||
StatusCode::GONE
|
||||
| StatusCode::NOT_FOUND
|
||||
| StatusCode::MOVED_PERMANENTLY
|
||||
| StatusCode::PERMANENT_REDIRECT
|
||||
| StatusCode::TEMPORARY_REDIRECT => Error::not_found(err),
|
||||
StatusCode::FORBIDDEN | StatusCode::PAYMENT_REQUIRED => {
|
||||
Error::permission_denied(err)
|
||||
}
|
||||
StatusCode::NETWORK_AUTHENTICATION_REQUIRED
|
||||
| StatusCode::PROXY_AUTHENTICATION_REQUIRED
|
||||
| StatusCode::UNAUTHORIZED => Error::unauthenticated(err),
|
||||
StatusCode::EXPECTATION_FAILED
|
||||
| StatusCode::PRECONDITION_FAILED
|
||||
| StatusCode::PRECONDITION_REQUIRED => Error::failed_precondition(err),
|
||||
StatusCode::RANGE_NOT_SATISFIABLE => Error::out_of_range(err),
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
| StatusCode::MISDIRECTED_REQUEST
|
||||
| StatusCode::SERVICE_UNAVAILABLE
|
||||
| StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => Error::unavailable(err),
|
||||
StatusCode::BAD_REQUEST
|
||||
| StatusCode::HTTP_VERSION_NOT_SUPPORTED
|
||||
| StatusCode::LENGTH_REQUIRED
|
||||
| StatusCode::METHOD_NOT_ALLOWED
|
||||
| StatusCode::NOT_ACCEPTABLE
|
||||
| StatusCode::PAYLOAD_TOO_LARGE
|
||||
| StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE
|
||||
| StatusCode::UNSUPPORTED_MEDIA_TYPE
|
||||
| StatusCode::URI_TOO_LONG => Error::invalid_argument(err),
|
||||
StatusCode::TOO_MANY_REQUESTS => Error::resource_exhausted(err),
|
||||
StatusCode::NOT_IMPLEMENTED => Error::unimplemented(err),
|
||||
_ => Error::unknown(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HttpClient {
|
||||
user_agent: HeaderValue,
|
||||
proxy: Option<Url>,
|
||||
tls_config: ClientConfig,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum HttpClientError {
|
||||
#[error("could not parse request: {0}")]
|
||||
Parsing(#[from] http::Error),
|
||||
#[error("could not send request: {0}")]
|
||||
Request(hyper::Error),
|
||||
#[error("could not read response: {0}")]
|
||||
Response(hyper::Error),
|
||||
#[error("status code: {0}")]
|
||||
NotOK(u16),
|
||||
#[error("could not build proxy connector: {0}")]
|
||||
ProxyBuilder(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl From<InvalidUri> for HttpClientError {
|
||||
fn from(err: InvalidUri) -> Self {
|
||||
Self::Parsing(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
pub fn new(proxy: Option<&Url>) -> Self {
|
||||
let spotify_version = match OS {
|
||||
|
@ -53,7 +86,7 @@ impl HttpClient {
|
|||
|
||||
let spotify_platform = match OS {
|
||||
"android" => "Android/31",
|
||||
"ios" => "iOS/15.1.1",
|
||||
"ios" => "iOS/15.2",
|
||||
"macos" => "OSX/0",
|
||||
"windows" => "Win32/0",
|
||||
_ => "Linux/0",
|
||||
|
@ -95,37 +128,32 @@ impl HttpClient {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, HttpClientError> {
|
||||
pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
debug!("Requesting {:?}", req.uri().to_string());
|
||||
|
||||
let request = self.request_fut(req)?;
|
||||
{
|
||||
let response = request.await;
|
||||
if let Ok(response) = &response {
|
||||
let status = response.status();
|
||||
if status != StatusCode::OK {
|
||||
return Err(HttpClientError::NotOK(status.into()));
|
||||
}
|
||||
let response = request.await;
|
||||
|
||||
if let Ok(response) = &response {
|
||||
let code = response.status();
|
||||
if code != StatusCode::OK {
|
||||
return Err(HttpClientError::StatusCode(code).into());
|
||||
}
|
||||
response.map_err(HttpClientError::Response)
|
||||
}
|
||||
|
||||
Ok(response?)
|
||||
}
|
||||
|
||||
pub async fn request_body(&self, req: Request<Body>) -> Result<Bytes, HttpClientError> {
|
||||
pub async fn request_body(&self, req: Request<Body>) -> Result<Bytes, Error> {
|
||||
let response = self.request(req).await?;
|
||||
hyper::body::to_bytes(response.into_body())
|
||||
.await
|
||||
.map_err(HttpClientError::Response)
|
||||
Ok(hyper::body::to_bytes(response.into_body()).await?)
|
||||
}
|
||||
|
||||
pub fn request_stream(
|
||||
&self,
|
||||
req: Request<Body>,
|
||||
) -> Result<IntoStream<ResponseFuture>, HttpClientError> {
|
||||
pub fn request_stream(&self, req: Request<Body>) -> Result<IntoStream<ResponseFuture>, Error> {
|
||||
Ok(self.request_fut(req)?.into_stream())
|
||||
}
|
||||
|
||||
pub fn request_fut(&self, mut req: Request<Body>) -> Result<ResponseFuture, HttpClientError> {
|
||||
pub fn request_fut(&self, mut req: Request<Body>) -> Result<ResponseFuture, Error> {
|
||||
let mut http = HttpConnector::new();
|
||||
http.enforce_http(false);
|
||||
let connector = HttpsConnector::from((http, self.tls_config.clone()));
|
||||
|
|
|
@ -20,6 +20,7 @@ pub mod date;
|
|||
mod dealer;
|
||||
#[doc(hidden)]
|
||||
pub mod diffie_hellman;
|
||||
pub mod error;
|
||||
pub mod file_id;
|
||||
mod http_client;
|
||||
pub mod mercury;
|
||||
|
@ -34,3 +35,9 @@ pub mod token;
|
|||
#[doc(hidden)]
|
||||
pub mod util;
|
||||
pub mod version;
|
||||
|
||||
pub use config::SessionConfig;
|
||||
pub use error::Error;
|
||||
pub use file_id::FileId;
|
||||
pub use session::Session;
|
||||
pub use spotify_id::SpotifyId;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::mem;
|
||||
use std::pin::Pin;
|
||||
use std::task::Context;
|
||||
use std::task::Poll;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use bytes::Bytes;
|
||||
|
@ -11,9 +12,7 @@ use futures_util::FutureExt;
|
|||
use protobuf::Message;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use crate::packet::PacketType;
|
||||
use crate::protocol;
|
||||
use crate::util::SeqGenerator;
|
||||
use crate::{packet::PacketType, protocol, util::SeqGenerator, Error};
|
||||
|
||||
mod types;
|
||||
pub use self::types::*;
|
||||
|
@ -33,18 +32,18 @@ component! {
|
|||
pub struct MercuryPending {
|
||||
parts: Vec<Vec<u8>>,
|
||||
partial: Option<Vec<u8>>,
|
||||
callback: Option<oneshot::Sender<Result<MercuryResponse, MercuryError>>>,
|
||||
callback: Option<oneshot::Sender<Result<MercuryResponse, Error>>>,
|
||||
}
|
||||
|
||||
pub struct MercuryFuture<T> {
|
||||
receiver: oneshot::Receiver<Result<T, MercuryError>>,
|
||||
receiver: oneshot::Receiver<Result<T, Error>>,
|
||||
}
|
||||
|
||||
impl<T> Future for MercuryFuture<T> {
|
||||
type Output = Result<T, MercuryError>;
|
||||
type Output = Result<T, Error>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
self.receiver.poll_unpin(cx).map_err(|_| MercuryError)?
|
||||
self.receiver.poll_unpin(cx)?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,7 +54,7 @@ impl MercuryManager {
|
|||
seq
|
||||
}
|
||||
|
||||
fn request(&self, req: MercuryRequest) -> MercuryFuture<MercuryResponse> {
|
||||
fn request(&self, req: MercuryRequest) -> Result<MercuryFuture<MercuryResponse>, Error> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let pending = MercuryPending {
|
||||
|
@ -72,13 +71,13 @@ impl MercuryManager {
|
|||
});
|
||||
|
||||
let cmd = req.method.command();
|
||||
let data = req.encode(&seq);
|
||||
let data = req.encode(&seq)?;
|
||||
|
||||
self.session().send_packet(cmd, data);
|
||||
MercuryFuture { receiver: rx }
|
||||
self.session().send_packet(cmd, data)?;
|
||||
Ok(MercuryFuture { receiver: rx })
|
||||
}
|
||||
|
||||
pub fn get<T: Into<String>>(&self, uri: T) -> MercuryFuture<MercuryResponse> {
|
||||
pub fn get<T: Into<String>>(&self, uri: T) -> Result<MercuryFuture<MercuryResponse>, Error> {
|
||||
self.request(MercuryRequest {
|
||||
method: MercuryMethod::Get,
|
||||
uri: uri.into(),
|
||||
|
@ -87,7 +86,11 @@ impl MercuryManager {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn send<T: Into<String>>(&self, uri: T, data: Vec<u8>) -> MercuryFuture<MercuryResponse> {
|
||||
pub fn send<T: Into<String>>(
|
||||
&self,
|
||||
uri: T,
|
||||
data: Vec<u8>,
|
||||
) -> Result<MercuryFuture<MercuryResponse>, Error> {
|
||||
self.request(MercuryRequest {
|
||||
method: MercuryMethod::Send,
|
||||
uri: uri.into(),
|
||||
|
@ -103,7 +106,7 @@ impl MercuryManager {
|
|||
pub fn subscribe<T: Into<String>>(
|
||||
&self,
|
||||
uri: T,
|
||||
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<MercuryResponse>, MercuryError>> + 'static
|
||||
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<MercuryResponse>, Error>> + 'static
|
||||
{
|
||||
let uri = uri.into();
|
||||
let request = self.request(MercuryRequest {
|
||||
|
@ -115,7 +118,7 @@ impl MercuryManager {
|
|||
|
||||
let manager = self.clone();
|
||||
async move {
|
||||
let response = request.await?;
|
||||
let response = request?.await?;
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
|
@ -125,13 +128,18 @@ impl MercuryManager {
|
|||
if !response.payload.is_empty() {
|
||||
// Old subscription protocol, watch the provided list of URIs
|
||||
for sub in response.payload {
|
||||
let mut sub =
|
||||
protocol::pubsub::Subscription::parse_from_bytes(&sub).unwrap();
|
||||
let sub_uri = sub.take_uri();
|
||||
match protocol::pubsub::Subscription::parse_from_bytes(&sub) {
|
||||
Ok(mut sub) => {
|
||||
let sub_uri = sub.take_uri();
|
||||
|
||||
debug!("subscribed sub_uri={}", sub_uri);
|
||||
debug!("subscribed sub_uri={}", sub_uri);
|
||||
|
||||
inner.subscriptions.push((sub_uri, tx.clone()));
|
||||
inner.subscriptions.push((sub_uri, tx.clone()));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("could not subscribe to {}: {}", uri, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// New subscription protocol, watch the requested URI
|
||||
|
@ -165,7 +173,7 @@ impl MercuryManager {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) {
|
||||
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
|
||||
let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
|
||||
let seq = data.split_to(seq_len).as_ref().to_owned();
|
||||
|
||||
|
@ -185,7 +193,7 @@ impl MercuryManager {
|
|||
}
|
||||
} else {
|
||||
warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8);
|
||||
return;
|
||||
return Err(MercuryError::Command(cmd).into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -205,10 +213,12 @@ impl MercuryManager {
|
|||
}
|
||||
|
||||
if flags == 0x1 {
|
||||
self.complete_request(cmd, pending);
|
||||
self.complete_request(cmd, pending)?;
|
||||
} else {
|
||||
self.lock(move |inner| inner.pending.insert(seq, pending));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_part(data: &mut Bytes) -> Vec<u8> {
|
||||
|
@ -216,9 +226,9 @@ impl MercuryManager {
|
|||
data.split_to(size).as_ref().to_owned()
|
||||
}
|
||||
|
||||
fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) {
|
||||
fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) -> Result<(), Error> {
|
||||
let header_data = pending.parts.remove(0);
|
||||
let header = protocol::mercury::Header::parse_from_bytes(&header_data).unwrap();
|
||||
let header = protocol::mercury::Header::parse_from_bytes(&header_data)?;
|
||||
|
||||
let response = MercuryResponse {
|
||||
uri: header.get_uri().to_string(),
|
||||
|
@ -226,13 +236,17 @@ impl MercuryManager {
|
|||
payload: pending.parts,
|
||||
};
|
||||
|
||||
if response.status_code >= 500 {
|
||||
panic!("Spotify servers returned an error. Restart librespot.");
|
||||
} else if response.status_code >= 400 {
|
||||
warn!("error {} for uri {}", response.status_code, &response.uri);
|
||||
let status_code = response.status_code;
|
||||
if status_code >= 500 {
|
||||
error!("error {} for uri {}", status_code, &response.uri);
|
||||
Err(MercuryError::Response(response).into())
|
||||
} else if status_code >= 400 {
|
||||
error!("error {} for uri {}", status_code, &response.uri);
|
||||
if let Some(cb) = pending.callback {
|
||||
let _ = cb.send(Err(MercuryError));
|
||||
cb.send(Err(MercuryError::Response(response.clone()).into()))
|
||||
.map_err(|_| MercuryError::Channel)?;
|
||||
}
|
||||
Err(MercuryError::Response(response).into())
|
||||
} else if let PacketType::MercuryEvent = cmd {
|
||||
self.lock(|inner| {
|
||||
let mut found = false;
|
||||
|
@ -242,7 +256,7 @@ impl MercuryManager {
|
|||
// before sending while saving the subscription under its unencoded form.
|
||||
let mut uri_split = response.uri.split('/');
|
||||
|
||||
let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string())
|
||||
let encoded_uri = std::iter::once(uri_split.next().unwrap_or_default().to_string())
|
||||
.chain(uri_split.map(|component| {
|
||||
form_urlencoded::byte_serialize(component.as_bytes()).collect::<String>()
|
||||
}))
|
||||
|
@ -263,12 +277,19 @@ impl MercuryManager {
|
|||
});
|
||||
|
||||
if !found {
|
||||
debug!("unknown subscription uri={}", response.uri);
|
||||
debug!("unknown subscription uri={}", &response.uri);
|
||||
trace!("response pushed over Mercury: {:?}", response);
|
||||
Err(MercuryError::Response(response).into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
} else if let Some(cb) = pending.callback {
|
||||
let _ = cb.send(Ok(response));
|
||||
cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?;
|
||||
Ok(())
|
||||
} else {
|
||||
error!("can't handle Mercury response: {:?}", response);
|
||||
Err(MercuryError::Response(response).into())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use super::*;
|
||||
use super::{MercuryFuture, MercuryManager, MercuryResponse};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
pub struct MercurySender {
|
||||
mercury: MercuryManager,
|
||||
|
@ -23,12 +25,13 @@ impl MercurySender {
|
|||
self.buffered_future.is_none() && self.pending.is_empty()
|
||||
}
|
||||
|
||||
pub fn send(&mut self, item: Vec<u8>) {
|
||||
let task = self.mercury.send(self.uri.clone(), item);
|
||||
pub fn send(&mut self, item: Vec<u8>) -> Result<(), Error> {
|
||||
let task = self.mercury.send(self.uri.clone(), item)?;
|
||||
self.pending.push_back(task);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn flush(&mut self) -> Result<(), MercuryError> {
|
||||
pub async fn flush(&mut self) -> Result<(), Error> {
|
||||
if self.buffered_future.is_none() {
|
||||
self.buffered_future = self.pending.pop_front();
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
use std::io::Write;
|
||||
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
use protobuf::Message;
|
||||
use std::fmt;
|
||||
use std::io::Write;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::packet::PacketType;
|
||||
use crate::protocol;
|
||||
use crate::{packet::PacketType, protocol, Error};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum MercuryMethod {
|
||||
|
@ -30,12 +29,23 @@ pub struct MercuryResponse {
|
|||
pub payload: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)]
|
||||
pub struct MercuryError;
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MercuryError {
|
||||
#[error("callback receiver was disconnected")]
|
||||
Channel,
|
||||
#[error("error handling packet type: {0:?}")]
|
||||
Command(PacketType),
|
||||
#[error("error handling Mercury response: {0:?}")]
|
||||
Response(MercuryResponse),
|
||||
}
|
||||
|
||||
impl fmt::Display for MercuryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Mercury error")
|
||||
impl From<MercuryError> for Error {
|
||||
fn from(err: MercuryError) -> Self {
|
||||
match err {
|
||||
MercuryError::Channel => Error::aborted(err),
|
||||
MercuryError::Command(_) => Error::unimplemented(err),
|
||||
MercuryError::Response(_) => Error::unavailable(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,15 +73,12 @@ impl MercuryMethod {
|
|||
}
|
||||
|
||||
impl MercuryRequest {
|
||||
// TODO: change into Result and remove unwraps
|
||||
pub fn encode(&self, seq: &[u8]) -> Vec<u8> {
|
||||
pub fn encode(&self, seq: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let mut packet = Vec::new();
|
||||
packet.write_u16::<BigEndian>(seq.len() as u16).unwrap();
|
||||
packet.write_all(seq).unwrap();
|
||||
packet.write_u8(1).unwrap(); // Flags: FINAL
|
||||
packet
|
||||
.write_u16::<BigEndian>(1 + self.payload.len() as u16)
|
||||
.unwrap(); // Part count
|
||||
packet.write_u16::<BigEndian>(seq.len() as u16)?;
|
||||
packet.write_all(seq)?;
|
||||
packet.write_u8(1)?; // Flags: FINAL
|
||||
packet.write_u16::<BigEndian>(1 + self.payload.len() as u16)?; // Part count
|
||||
|
||||
let mut header = protocol::mercury::Header::new();
|
||||
header.set_uri(self.uri.clone());
|
||||
|
@ -81,16 +88,14 @@ impl MercuryRequest {
|
|||
header.set_content_type(content_type.clone());
|
||||
}
|
||||
|
||||
packet
|
||||
.write_u16::<BigEndian>(header.compute_size() as u16)
|
||||
.unwrap();
|
||||
header.write_to_writer(&mut packet).unwrap();
|
||||
packet.write_u16::<BigEndian>(header.compute_size() as u16)?;
|
||||
header.write_to_writer(&mut packet)?;
|
||||
|
||||
for p in &self.payload {
|
||||
packet.write_u16::<BigEndian>(p.len() as u16).unwrap();
|
||||
packet.write_all(p).unwrap();
|
||||
packet.write_u16::<BigEndian>(p.len() as u16)?;
|
||||
packet.write_all(p)?;
|
||||
}
|
||||
|
||||
packet
|
||||
Ok(packet)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use num_derive::{FromPrimitive, ToPrimitive};
|
||||
|
||||
#[derive(Debug, FromPrimitive, ToPrimitive)]
|
||||
#[derive(Debug, Copy, Clone, FromPrimitive, ToPrimitive)]
|
||||
pub enum PacketType {
|
||||
SecretBlock = 0x02,
|
||||
Ping = 0x04,
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::process::exit;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, RwLock, Weak};
|
||||
use std::task::Context;
|
||||
use std::task::Poll;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
io,
|
||||
pin::Pin,
|
||||
process::exit,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, RwLock, Weak,
|
||||
},
|
||||
task::{Context, Poll},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use bytes::Bytes;
|
||||
|
@ -20,18 +23,21 @@ use thiserror::Error;
|
|||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
use crate::apresolve::ApResolver;
|
||||
use crate::audio_key::AudioKeyManager;
|
||||
use crate::authentication::Credentials;
|
||||
use crate::cache::Cache;
|
||||
use crate::channel::ChannelManager;
|
||||
use crate::config::SessionConfig;
|
||||
use crate::connection::{self, AuthenticationError};
|
||||
use crate::http_client::HttpClient;
|
||||
use crate::mercury::MercuryManager;
|
||||
use crate::packet::PacketType;
|
||||
use crate::spclient::SpClient;
|
||||
use crate::token::TokenProvider;
|
||||
use crate::{
|
||||
apresolve::ApResolver,
|
||||
audio_key::AudioKeyManager,
|
||||
authentication::Credentials,
|
||||
cache::Cache,
|
||||
channel::ChannelManager,
|
||||
config::SessionConfig,
|
||||
connection::{self, AuthenticationError},
|
||||
http_client::HttpClient,
|
||||
mercury::MercuryManager,
|
||||
packet::PacketType,
|
||||
spclient::SpClient,
|
||||
token::TokenProvider,
|
||||
Error,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SessionError {
|
||||
|
@ -39,6 +45,18 @@ pub enum SessionError {
|
|||
AuthenticationError(#[from] AuthenticationError),
|
||||
#[error("Cannot create session: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("packet {0} unknown")]
|
||||
Packet(u8),
|
||||
}
|
||||
|
||||
impl From<SessionError> for Error {
|
||||
fn from(err: SessionError) -> Self {
|
||||
match err {
|
||||
SessionError::AuthenticationError(_) => Error::unauthenticated(err),
|
||||
SessionError::IoError(_) => Error::unavailable(err),
|
||||
SessionError::Packet(_) => Error::unimplemented(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type UserAttributes = HashMap<String, String>;
|
||||
|
@ -88,7 +106,7 @@ impl Session {
|
|||
config: SessionConfig,
|
||||
credentials: Credentials,
|
||||
cache: Option<Cache>,
|
||||
) -> Result<Session, SessionError> {
|
||||
) -> Result<Session, Error> {
|
||||
let http_client = HttpClient::new(config.proxy.as_ref());
|
||||
let (sender_tx, sender_rx) = mpsc::unbounded_channel();
|
||||
let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
|
@ -214,9 +232,18 @@ impl Session {
|
|||
}
|
||||
}
|
||||
|
||||
fn dispatch(&self, cmd: u8, data: Bytes) {
|
||||
fn dispatch(&self, cmd: u8, data: Bytes) -> Result<(), Error> {
|
||||
use PacketType::*;
|
||||
|
||||
let packet_type = FromPrimitive::from_u8(cmd);
|
||||
let cmd = match packet_type {
|
||||
Some(cmd) => cmd,
|
||||
None => {
|
||||
trace!("Ignoring unknown packet {:x}", cmd);
|
||||
return Err(SessionError::Packet(cmd).into());
|
||||
}
|
||||
};
|
||||
|
||||
match packet_type {
|
||||
Some(Ping) => {
|
||||
let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64;
|
||||
|
@ -229,24 +256,21 @@ impl Session {
|
|||
self.0.data.write().unwrap().time_delta = server_timestamp - timestamp;
|
||||
|
||||
self.debug_info();
|
||||
self.send_packet(Pong, vec![0, 0, 0, 0]);
|
||||
self.send_packet(Pong, vec![0, 0, 0, 0])
|
||||
}
|
||||
Some(CountryCode) => {
|
||||
let country = String::from_utf8(data.as_ref().to_owned()).unwrap();
|
||||
let country = String::from_utf8(data.as_ref().to_owned())?;
|
||||
info!("Country: {:?}", country);
|
||||
self.0.data.write().unwrap().user_data.country = country;
|
||||
Ok(())
|
||||
}
|
||||
Some(StreamChunkRes) | Some(ChannelError) => {
|
||||
self.channel().dispatch(packet_type.unwrap(), data);
|
||||
}
|
||||
Some(AesKey) | Some(AesKeyError) => {
|
||||
self.audio_key().dispatch(packet_type.unwrap(), data);
|
||||
}
|
||||
Some(StreamChunkRes) | Some(ChannelError) => self.channel().dispatch(cmd, data),
|
||||
Some(AesKey) | Some(AesKeyError) => self.audio_key().dispatch(cmd, data),
|
||||
Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => {
|
||||
self.mercury().dispatch(packet_type.unwrap(), data);
|
||||
self.mercury().dispatch(cmd, data)
|
||||
}
|
||||
Some(ProductInfo) => {
|
||||
let data = std::str::from_utf8(&data).unwrap();
|
||||
let data = std::str::from_utf8(&data)?;
|
||||
let mut reader = quick_xml::Reader::from_str(data);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
|
@ -256,8 +280,7 @@ impl Session {
|
|||
loop {
|
||||
match reader.read_event(&mut buf) {
|
||||
Ok(Event::Start(ref element)) => {
|
||||
current_element =
|
||||
std::str::from_utf8(element.name()).unwrap().to_owned()
|
||||
current_element = std::str::from_utf8(element.name())?.to_owned()
|
||||
}
|
||||
Ok(Event::End(_)) => {
|
||||
current_element = String::new();
|
||||
|
@ -266,7 +289,7 @@ impl Session {
|
|||
if !current_element.is_empty() {
|
||||
let _ = user_attributes.insert(
|
||||
current_element.clone(),
|
||||
value.unescape_and_decode(&reader).unwrap(),
|
||||
value.unescape_and_decode(&reader)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -284,24 +307,23 @@ impl Session {
|
|||
Self::check_catalogue(&user_attributes);
|
||||
|
||||
self.0.data.write().unwrap().user_data.attributes = user_attributes;
|
||||
Ok(())
|
||||
}
|
||||
Some(PongAck)
|
||||
| Some(SecretBlock)
|
||||
| Some(LegacyWelcome)
|
||||
| Some(UnknownDataAllZeros)
|
||||
| Some(LicenseVersion) => {}
|
||||
| Some(LicenseVersion) => Ok(()),
|
||||
_ => {
|
||||
if let Some(packet_type) = PacketType::from_u8(cmd) {
|
||||
trace!("Ignoring {:?} packet with data {:#?}", packet_type, data);
|
||||
} else {
|
||||
trace!("Ignoring unknown packet {:x}", cmd);
|
||||
}
|
||||
trace!("Ignoring {:?} packet with data {:#?}", cmd, data);
|
||||
Err(SessionError::Packet(cmd as u8).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) {
|
||||
self.0.tx_connection.send((cmd as u8, data)).unwrap();
|
||||
pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) -> Result<(), Error> {
|
||||
self.0.tx_connection.send((cmd as u8, data))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cache(&self) -> Option<&Arc<Cache>> {
|
||||
|
@ -393,7 +415,7 @@ impl SessionWeak {
|
|||
}
|
||||
|
||||
pub(crate) fn upgrade(&self) -> Session {
|
||||
self.try_upgrade().expect("Session died")
|
||||
self.try_upgrade().expect("Session died") // TODO
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -434,7 +456,9 @@ where
|
|||
}
|
||||
};
|
||||
|
||||
session.dispatch(cmd, data);
|
||||
if let Err(e) = session.dispatch(cmd, data) {
|
||||
error!("could not dispatch command: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use std::io;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::{io, net::ToSocketAddrs};
|
||||
|
||||
use tokio::net::TcpStream;
|
||||
use url::Url;
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
use crate::apresolve::SocketAddress;
|
||||
use crate::file_id::FileId;
|
||||
use crate::http_client::HttpClientError;
|
||||
use crate::mercury::MercuryError;
|
||||
use crate::protocol::canvaz::EntityCanvazRequest;
|
||||
use crate::protocol::connect::PutStateRequest;
|
||||
use crate::protocol::extended_metadata::BatchedEntityRequest;
|
||||
use crate::spotify_id::SpotifyId;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::IntoStream;
|
||||
use http::header::HeaderValue;
|
||||
use hyper::client::ResponseFuture;
|
||||
use hyper::header::{InvalidHeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE};
|
||||
use hyper::{Body, HeaderMap, Method, Request};
|
||||
use hyper::{
|
||||
client::ResponseFuture,
|
||||
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE},
|
||||
Body, HeaderMap, Method, Request,
|
||||
};
|
||||
use protobuf::Message;
|
||||
use rand::Rng;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
apresolve::SocketAddress,
|
||||
error::ErrorKind,
|
||||
protocol::{
|
||||
canvaz::EntityCanvazRequest, connect::PutStateRequest,
|
||||
extended_metadata::BatchedEntityRequest,
|
||||
},
|
||||
Error, FileId, SpotifyId,
|
||||
};
|
||||
|
||||
component! {
|
||||
SpClient : SpClientInner {
|
||||
|
@ -25,23 +28,7 @@ component! {
|
|||
}
|
||||
}
|
||||
|
||||
pub type SpClientResult = Result<Bytes, SpClientError>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SpClientError {
|
||||
#[error("could not get authorization token")]
|
||||
Token(#[from] MercuryError),
|
||||
#[error("could not parse request: {0}")]
|
||||
Parsing(#[from] http::Error),
|
||||
#[error("could not complete request: {0}")]
|
||||
Network(#[from] HttpClientError),
|
||||
}
|
||||
|
||||
impl From<InvalidHeaderValue> for SpClientError {
|
||||
fn from(err: InvalidHeaderValue) -> Self {
|
||||
Self::Parsing(err.into())
|
||||
}
|
||||
}
|
||||
pub type SpClientResult = Result<Bytes, Error>;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum RequestStrategy {
|
||||
|
@ -157,12 +144,8 @@ impl SpClient {
|
|||
))?,
|
||||
);
|
||||
|
||||
last_response = self
|
||||
.session()
|
||||
.http_client()
|
||||
.request_body(request)
|
||||
.await
|
||||
.map_err(SpClientError::Network);
|
||||
last_response = self.session().http_client().request_body(request).await;
|
||||
|
||||
if last_response.is_ok() {
|
||||
return last_response;
|
||||
}
|
||||
|
@ -177,9 +160,9 @@ impl SpClient {
|
|||
|
||||
// Reconnection logic: drop the current access point if we are experiencing issues.
|
||||
// This will cause the next call to base_url() to resolve a new one.
|
||||
if let Err(SpClientError::Network(ref network_error)) = last_response {
|
||||
match network_error {
|
||||
HttpClientError::Response(_) | HttpClientError::Request(_) => {
|
||||
if let Err(ref network_error) = last_response {
|
||||
match network_error.kind {
|
||||
ErrorKind::Unavailable | ErrorKind::DeadlineExceeded => {
|
||||
// Keep trying the current access point three times before dropping it.
|
||||
if tries % 3 == 0 {
|
||||
self.flush_accesspoint().await
|
||||
|
@ -244,7 +227,7 @@ impl SpClient {
|
|||
}
|
||||
|
||||
pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult {
|
||||
let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62(),);
|
||||
let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62());
|
||||
|
||||
self.request_as_json(&Method::GET, &endpoint, None, None)
|
||||
.await
|
||||
|
@ -291,7 +274,7 @@ impl SpClient {
|
|||
url: &str,
|
||||
offset: usize,
|
||||
length: usize,
|
||||
) -> Result<IntoStream<ResponseFuture>, SpClientError> {
|
||||
) -> Result<IntoStream<ResponseFuture>, Error> {
|
||||
let req = Request::builder()
|
||||
.method(&Method::GET)
|
||||
.uri(url)
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
use librespot_protocol as protocol;
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use crate::Error;
|
||||
|
||||
use librespot_protocol as protocol;
|
||||
|
||||
// re-export FileId for historic reasons, when it was part of this mod
|
||||
pub use crate::file_id::FileId;
|
||||
pub use crate::FileId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SpotifyItemType {
|
||||
|
@ -64,8 +68,14 @@ pub enum SpotifyIdError {
|
|||
InvalidRoot,
|
||||
}
|
||||
|
||||
pub type SpotifyIdResult = Result<SpotifyId, SpotifyIdError>;
|
||||
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, SpotifyIdError>;
|
||||
impl From<SpotifyIdError> for Error {
|
||||
fn from(err: SpotifyIdError) -> Self {
|
||||
Error::invalid_argument(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub type SpotifyIdResult = Result<SpotifyId, Error>;
|
||||
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, Error>;
|
||||
|
||||
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
||||
|
@ -95,7 +105,7 @@ impl SpotifyId {
|
|||
let p = match c {
|
||||
b'0'..=b'9' => c - b'0',
|
||||
b'a'..=b'f' => c - b'a' + 10,
|
||||
_ => return Err(SpotifyIdError::InvalidId),
|
||||
_ => return Err(SpotifyIdError::InvalidId.into()),
|
||||
} as u128;
|
||||
|
||||
dst <<= 4;
|
||||
|
@ -121,7 +131,7 @@ impl SpotifyId {
|
|||
b'0'..=b'9' => c - b'0',
|
||||
b'a'..=b'z' => c - b'a' + 10,
|
||||
b'A'..=b'Z' => c - b'A' + 36,
|
||||
_ => return Err(SpotifyIdError::InvalidId),
|
||||
_ => return Err(SpotifyIdError::InvalidId.into()),
|
||||
} as u128;
|
||||
|
||||
dst *= 62;
|
||||
|
@ -143,7 +153,7 @@ impl SpotifyId {
|
|||
id: u128::from_be_bytes(dst),
|
||||
item_type: SpotifyItemType::Unknown,
|
||||
}),
|
||||
Err(_) => Err(SpotifyIdError::InvalidId),
|
||||
Err(_) => Err(SpotifyIdError::InvalidId.into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,20 +171,20 @@ impl SpotifyId {
|
|||
|
||||
// At minimum, should be `spotify:{type}:{id}`
|
||||
if uri_parts.len() < 3 {
|
||||
return Err(SpotifyIdError::InvalidFormat);
|
||||
return Err(SpotifyIdError::InvalidFormat.into());
|
||||
}
|
||||
|
||||
if uri_parts[0] != "spotify" {
|
||||
return Err(SpotifyIdError::InvalidRoot);
|
||||
return Err(SpotifyIdError::InvalidRoot.into());
|
||||
}
|
||||
|
||||
let id = uri_parts.pop().unwrap();
|
||||
let id = uri_parts.pop().unwrap_or_default();
|
||||
if id.len() != Self::SIZE_BASE62 {
|
||||
return Err(SpotifyIdError::InvalidId);
|
||||
return Err(SpotifyIdError::InvalidId.into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
item_type: uri_parts.pop().unwrap().into(),
|
||||
item_type: uri_parts.pop().unwrap_or_default().into(),
|
||||
..Self::from_base62(id)?
|
||||
})
|
||||
}
|
||||
|
@ -285,15 +295,15 @@ impl NamedSpotifyId {
|
|||
|
||||
// At minimum, should be `spotify:user:{username}:{type}:{id}`
|
||||
if uri_parts.len() < 5 {
|
||||
return Err(SpotifyIdError::InvalidFormat);
|
||||
return Err(SpotifyIdError::InvalidFormat.into());
|
||||
}
|
||||
|
||||
if uri_parts[0] != "spotify" {
|
||||
return Err(SpotifyIdError::InvalidRoot);
|
||||
return Err(SpotifyIdError::InvalidRoot.into());
|
||||
}
|
||||
|
||||
if uri_parts[1] != "user" {
|
||||
return Err(SpotifyIdError::InvalidFormat);
|
||||
return Err(SpotifyIdError::InvalidFormat.into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
|
@ -344,35 +354,35 @@ impl fmt::Display for NamedSpotifyId {
|
|||
}
|
||||
|
||||
impl TryFrom<&[u8]> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
|
||||
Self::from_raw(src)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(src: &str) -> Result<Self, Self::Error> {
|
||||
Self::from_base62(src)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(src: String) -> Result<Self, Self::Error> {
|
||||
Self::try_from(src.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Vec<u8>> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
|
||||
Self::try_from(src.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> {
|
||||
match SpotifyId::from_raw(track.get_gid()) {
|
||||
Ok(mut id) => {
|
||||
|
@ -385,7 +395,7 @@ impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
|
|||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Album> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Album,
|
||||
|
@ -395,7 +405,7 @@ impl TryFrom<&protocol::metadata::Album> for SpotifyId {
|
|||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Artist,
|
||||
|
@ -405,7 +415,7 @@ impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
|
|||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Episode,
|
||||
|
@ -415,7 +425,7 @@ impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
|
|||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Track> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Track,
|
||||
|
@ -425,7 +435,7 @@ impl TryFrom<&protocol::metadata::Track> for SpotifyId {
|
|||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Show> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Show,
|
||||
|
@ -435,7 +445,7 @@ impl TryFrom<&protocol::metadata::Show> for SpotifyId {
|
|||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Artist,
|
||||
|
@ -445,7 +455,7 @@ impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
|
|||
}
|
||||
|
||||
impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Track,
|
||||
|
@ -457,7 +467,7 @@ impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
|
|||
// Note that this is the unique revision of an item's metadata on a playlist,
|
||||
// not the ID of that item or playlist.
|
||||
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
|
||||
Self::try_from(item.get_revision())
|
||||
}
|
||||
|
@ -465,7 +475,7 @@ impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
|
|||
|
||||
// Note that this is the unique revision of a playlist, not the ID of that playlist.
|
||||
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(
|
||||
playlist: &protocol::playlist4_external::SelectedListContent,
|
||||
) -> Result<Self, Self::Error> {
|
||||
|
@ -477,7 +487,7 @@ impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
|
|||
// which is why we now don't create a separate `Playlist` enum value yet and choose
|
||||
// to discard any item type.
|
||||
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
type Error = crate::Error;
|
||||
fn try_from(
|
||||
picture: &protocol::playlist_annotate3::TranscodedPicture,
|
||||
) -> Result<Self, Self::Error> {
|
||||
|
@ -565,7 +575,7 @@ mod tests {
|
|||
id: 0,
|
||||
kind: SpotifyItemType::Unknown,
|
||||
// Invalid ID in the URI.
|
||||
uri_error: Some(SpotifyIdError::InvalidId),
|
||||
uri_error: SpotifyIdError::InvalidId,
|
||||
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
|
||||
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
|
||||
base62: "!!!!!Ys0csV6RS48xBl0tH",
|
||||
|
@ -578,7 +588,7 @@ mod tests {
|
|||
id: 0,
|
||||
kind: SpotifyItemType::Unknown,
|
||||
// Missing colon between ID and type.
|
||||
uri_error: Some(SpotifyIdError::InvalidFormat),
|
||||
uri_error: SpotifyIdError::InvalidFormat,
|
||||
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
|
||||
base16: "--------------------",
|
||||
base62: "....................",
|
||||
|
@ -591,7 +601,7 @@ mod tests {
|
|||
id: 0,
|
||||
kind: SpotifyItemType::Unknown,
|
||||
// Uri too short
|
||||
uri_error: Some(SpotifyIdError::InvalidId),
|
||||
uri_error: SpotifyIdError::InvalidId,
|
||||
uri: "spotify:azb:aRS48xBl0tH",
|
||||
base16: "--------------------",
|
||||
base62: "....................",
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
// user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming,
|
||||
// app-remote-control
|
||||
|
||||
use crate::mercury::MercuryError;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use std::error::Error;
|
||||
use std::time::{Duration, Instant};
|
||||
use crate::Error;
|
||||
|
||||
component! {
|
||||
TokenProvider : TokenProviderInner {
|
||||
|
@ -21,6 +21,18 @@ component! {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TokenError {
|
||||
#[error("no tokens available")]
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl From<TokenError> for Error {
|
||||
fn from(err: TokenError) -> Self {
|
||||
Error::unavailable(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Token {
|
||||
pub access_token: String,
|
||||
|
@ -54,11 +66,7 @@ impl TokenProvider {
|
|||
}
|
||||
|
||||
// scopes must be comma-separated
|
||||
pub async fn get_token(&self, scopes: &str) -> Result<Token, MercuryError> {
|
||||
if scopes.is_empty() {
|
||||
return Err(MercuryError);
|
||||
}
|
||||
|
||||
pub async fn get_token(&self, scopes: &str) -> Result<Token, Error> {
|
||||
if let Some(index) = self.find_token(scopes.split(',').collect()) {
|
||||
let cached_token = self.lock(|inner| inner.tokens[index].clone());
|
||||
if cached_token.is_expired() {
|
||||
|
@ -79,14 +87,10 @@ impl TokenProvider {
|
|||
Self::KEYMASTER_CLIENT_ID,
|
||||
self.session().device_id()
|
||||
);
|
||||
let request = self.session().mercury().get(query_uri);
|
||||
let request = self.session().mercury().get(query_uri)?;
|
||||
let response = request.await?;
|
||||
let data = response
|
||||
.payload
|
||||
.first()
|
||||
.expect("No tokens received")
|
||||
.to_vec();
|
||||
let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?;
|
||||
let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec();
|
||||
let token = Token::new(String::from_utf8(data)?)?;
|
||||
trace!("Got token: {:#?}", token);
|
||||
self.lock(|inner| inner.tokens.push(token.clone()));
|
||||
Ok(token)
|
||||
|
@ -96,7 +100,7 @@ impl TokenProvider {
|
|||
impl Token {
|
||||
const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10);
|
||||
|
||||
pub fn new(body: String) -> Result<Self, Box<dyn Error>> {
|
||||
pub fn new(body: String) -> Result<Self, Error> {
|
||||
let data: TokenData = serde_json::from_slice(body.as_ref())?;
|
||||
Ok(Self {
|
||||
access_token: data.access_token,
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
use std::future::Future;
|
||||
use std::mem;
|
||||
use std::pin::Pin;
|
||||
use std::task::Context;
|
||||
use std::task::Poll;
|
||||
use std::{
|
||||
future::Future,
|
||||
mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures_core::ready;
|
||||
use futures_util::FutureExt;
|
||||
use futures_util::Sink;
|
||||
use futures_util::{future, SinkExt};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::timeout;
|
||||
use futures_util::{future, FutureExt, Sink, SinkExt};
|
||||
use tokio::{task::JoinHandle, time::timeout};
|
||||
|
||||
/// Returns a future that will flush the sink, even if flushing is temporarily completed.
|
||||
/// Finishes only if the sink throws an error.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue