mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00: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
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1246,6 +1246,7 @@ dependencies = [
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
]
|
]
|
||||||
|
@ -1309,6 +1310,7 @@ dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use aes_ctr::cipher::generic_array::GenericArray;
|
use aes_ctr::{
|
||||||
use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek};
|
cipher::{
|
||||||
use aes_ctr::Aes128Ctr;
|
generic_array::GenericArray, NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek,
|
||||||
|
},
|
||||||
|
Aes128Ctr,
|
||||||
|
};
|
||||||
|
|
||||||
use librespot_core::audio_key::AudioKey;
|
use librespot_core::audio_key::AudioKey;
|
||||||
|
|
||||||
|
|
|
@ -1,54 +1,57 @@
|
||||||
mod receive;
|
mod receive;
|
||||||
|
|
||||||
use std::cmp::{max, min};
|
use std::{
|
||||||
use std::fs;
|
cmp::{max, min},
|
||||||
use std::io::{self, Read, Seek, SeekFrom};
|
fs,
|
||||||
use std::sync::atomic::{self, AtomicUsize};
|
io::{self, Read, Seek, SeekFrom},
|
||||||
use std::sync::{Arc, Condvar, Mutex};
|
sync::{
|
||||||
use std::time::{Duration, Instant};
|
atomic::{self, AtomicUsize},
|
||||||
|
Arc, Condvar, Mutex,
|
||||||
|
},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use futures_util::future::IntoStream;
|
use futures_util::{future::IntoStream, StreamExt, TryFutureExt};
|
||||||
use futures_util::{StreamExt, TryFutureExt};
|
use hyper::{client::ResponseFuture, header::CONTENT_RANGE, Body, Response, StatusCode};
|
||||||
use hyper::client::ResponseFuture;
|
|
||||||
use hyper::header::CONTENT_RANGE;
|
|
||||||
use hyper::Body;
|
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
use librespot_core::cdn_url::{CdnUrl, CdnUrlError};
|
use librespot_core::{cdn_url::CdnUrl, Error, FileId, Session};
|
||||||
use librespot_core::file_id::FileId;
|
|
||||||
use librespot_core::session::Session;
|
|
||||||
use librespot_core::spclient::SpClientError;
|
|
||||||
|
|
||||||
use self::receive::audio_file_fetch;
|
use self::receive::audio_file_fetch;
|
||||||
|
|
||||||
use crate::range_set::{Range, RangeSet};
|
use crate::range_set::{Range, RangeSet};
|
||||||
|
|
||||||
pub type AudioFileResult = Result<(), AudioFileError>;
|
pub type AudioFileResult = Result<(), librespot_core::Error>;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum AudioFileError {
|
pub enum AudioFileError {
|
||||||
#[error("could not complete CDN request: {0}")]
|
#[error("other end of channel disconnected")]
|
||||||
Cdn(#[from] hyper::Error),
|
|
||||||
#[error("channel was disconnected")]
|
|
||||||
Channel,
|
Channel,
|
||||||
#[error("empty response")]
|
#[error("required header not found")]
|
||||||
Empty,
|
Header,
|
||||||
#[error("I/O error: {0}")]
|
#[error("streamer received no data")]
|
||||||
Io(#[from] io::Error),
|
NoData,
|
||||||
#[error("output file unavailable")]
|
#[error("no output available")]
|
||||||
Output,
|
Output,
|
||||||
#[error("error parsing response")]
|
#[error("invalid status code {0}")]
|
||||||
Parsing,
|
StatusCode(StatusCode),
|
||||||
#[error("mutex was poisoned")]
|
#[error("wait timeout exceeded")]
|
||||||
Poisoned,
|
WaitTimeout,
|
||||||
#[error("could not complete API request: {0}")]
|
}
|
||||||
SpClient(#[from] SpClientError),
|
|
||||||
#[error("streamer did not report progress")]
|
impl From<AudioFileError> for Error {
|
||||||
Timeout,
|
fn from(err: AudioFileError) -> Self {
|
||||||
#[error("could not get CDN URL: {0}")]
|
match err {
|
||||||
Url(#[from] CdnUrlError),
|
AudioFileError::Channel => Error::aborted(err),
|
||||||
|
AudioFileError::Header => Error::unavailable(err),
|
||||||
|
AudioFileError::NoData => Error::unavailable(err),
|
||||||
|
AudioFileError::Output => Error::aborted(err),
|
||||||
|
AudioFileError::StatusCode(_) => Error::failed_precondition(err),
|
||||||
|
AudioFileError::WaitTimeout => Error::deadline_exceeded(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The minimum size of a block that is requested from the Spotify servers in one request.
|
/// The minimum size of a block that is requested from the Spotify servers in one request.
|
||||||
|
@ -124,7 +127,7 @@ pub enum AudioFile {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StreamingRequest {
|
pub struct StreamingRequest {
|
||||||
streamer: IntoStream<ResponseFuture>,
|
streamer: IntoStream<ResponseFuture>,
|
||||||
initial_body: Option<Body>,
|
initial_response: Option<Response<Body>>,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
length: usize,
|
length: usize,
|
||||||
request_time: Instant,
|
request_time: Instant,
|
||||||
|
@ -154,12 +157,9 @@ impl StreamLoaderController {
|
||||||
self.file_size == 0
|
self.file_size == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn range_available(&self, range: Range) -> Result<bool, AudioFileError> {
|
pub fn range_available(&self, range: Range) -> bool {
|
||||||
let available = if let Some(ref shared) = self.stream_shared {
|
let available = if let Some(ref shared) = self.stream_shared {
|
||||||
let download_status = shared
|
let download_status = shared.download_status.lock().unwrap();
|
||||||
.download_status
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?;
|
|
||||||
|
|
||||||
range.length
|
range.length
|
||||||
<= download_status
|
<= download_status
|
||||||
|
@ -169,16 +169,16 @@ impl StreamLoaderController {
|
||||||
range.length <= self.len() - range.start
|
range.length <= self.len() - range.start
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(available)
|
available
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn range_to_end_available(&self) -> Result<bool, AudioFileError> {
|
pub fn range_to_end_available(&self) -> bool {
|
||||||
match self.stream_shared {
|
match self.stream_shared {
|
||||||
Some(ref shared) => {
|
Some(ref shared) => {
|
||||||
let read_position = shared.read_position.load(atomic::Ordering::Relaxed);
|
let read_position = shared.read_position.load(atomic::Ordering::Relaxed);
|
||||||
self.range_available(Range::new(read_position, self.len() - read_position))
|
self.range_available(Range::new(read_position, self.len() - read_position))
|
||||||
}
|
}
|
||||||
None => Ok(true),
|
None => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +190,8 @@ impl StreamLoaderController {
|
||||||
|
|
||||||
fn send_stream_loader_command(&self, command: StreamLoaderCommand) {
|
fn send_stream_loader_command(&self, command: StreamLoaderCommand) {
|
||||||
if let Some(ref channel) = self.channel_tx {
|
if let Some(ref channel) = self.channel_tx {
|
||||||
// ignore the error in case the channel has been closed already.
|
// Ignore the error in case the channel has been closed already.
|
||||||
|
// This means that the file was completely downloaded.
|
||||||
let _ = channel.send(command);
|
let _ = channel.send(command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,10 +214,7 @@ impl StreamLoaderController {
|
||||||
self.fetch(range);
|
self.fetch(range);
|
||||||
|
|
||||||
if let Some(ref shared) = self.stream_shared {
|
if let Some(ref shared) = self.stream_shared {
|
||||||
let mut download_status = shared
|
let mut download_status = shared.download_status.lock().unwrap();
|
||||||
.download_status
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?;
|
|
||||||
|
|
||||||
while range.length
|
while range.length
|
||||||
> download_status
|
> download_status
|
||||||
|
@ -226,7 +224,7 @@ impl StreamLoaderController {
|
||||||
download_status = shared
|
download_status = shared
|
||||||
.cond
|
.cond
|
||||||
.wait_timeout(download_status, DOWNLOAD_TIMEOUT)
|
.wait_timeout(download_status, DOWNLOAD_TIMEOUT)
|
||||||
.map_err(|_| AudioFileError::Timeout)?
|
.map_err(|_| AudioFileError::WaitTimeout)?
|
||||||
.0;
|
.0;
|
||||||
if range.length
|
if range.length
|
||||||
> (download_status
|
> (download_status
|
||||||
|
@ -319,7 +317,7 @@ impl AudioFile {
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
bytes_per_second: usize,
|
bytes_per_second: usize,
|
||||||
play_from_beginning: bool,
|
play_from_beginning: bool,
|
||||||
) -> Result<AudioFile, AudioFileError> {
|
) -> Result<AudioFile, Error> {
|
||||||
if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) {
|
if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) {
|
||||||
debug!("File {} already in cache", file_id);
|
debug!("File {} already in cache", file_id);
|
||||||
return Ok(AudioFile::Cached(file));
|
return Ok(AudioFile::Cached(file));
|
||||||
|
@ -340,9 +338,14 @@ impl AudioFile {
|
||||||
let session_ = session.clone();
|
let session_ = session.clone();
|
||||||
session.spawn(complete_rx.map_ok(move |mut file| {
|
session.spawn(complete_rx.map_ok(move |mut file| {
|
||||||
if let Some(cache) = session_.cache() {
|
if let Some(cache) = session_.cache() {
|
||||||
if cache.save_file(file_id, &mut file) {
|
if let Some(cache_id) = cache.file(file_id) {
|
||||||
debug!("File {} cached to {:?}", file_id, cache.file(file_id));
|
if let Err(e) = cache.save_file(file_id, &mut file) {
|
||||||
|
error!("Error caching file {} to {:?}: {}", file_id, cache_id, e);
|
||||||
|
} else {
|
||||||
|
debug!("File {} cached to {:?}", file_id, cache_id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
debug!("Downloading file {} complete", file_id);
|
debug!("Downloading file {} complete", file_id);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
@ -350,7 +353,7 @@ impl AudioFile {
|
||||||
Ok(AudioFile::Streaming(streaming.await?))
|
Ok(AudioFile::Streaming(streaming.await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_stream_loader_controller(&self) -> Result<StreamLoaderController, AudioFileError> {
|
pub fn get_stream_loader_controller(&self) -> Result<StreamLoaderController, Error> {
|
||||||
let controller = match self {
|
let controller = match self {
|
||||||
AudioFile::Streaming(ref stream) => StreamLoaderController {
|
AudioFile::Streaming(ref stream) => StreamLoaderController {
|
||||||
channel_tx: Some(stream.stream_loader_command_tx.clone()),
|
channel_tx: Some(stream.stream_loader_command_tx.clone()),
|
||||||
|
@ -379,7 +382,7 @@ impl AudioFileStreaming {
|
||||||
complete_tx: oneshot::Sender<NamedTempFile>,
|
complete_tx: oneshot::Sender<NamedTempFile>,
|
||||||
bytes_per_second: usize,
|
bytes_per_second: usize,
|
||||||
play_from_beginning: bool,
|
play_from_beginning: bool,
|
||||||
) -> Result<AudioFileStreaming, AudioFileError> {
|
) -> Result<AudioFileStreaming, Error> {
|
||||||
let download_size = if play_from_beginning {
|
let download_size = if play_from_beginning {
|
||||||
INITIAL_DOWNLOAD_SIZE
|
INITIAL_DOWNLOAD_SIZE
|
||||||
+ max(
|
+ max(
|
||||||
|
@ -392,8 +395,8 @@ impl AudioFileStreaming {
|
||||||
INITIAL_DOWNLOAD_SIZE
|
INITIAL_DOWNLOAD_SIZE
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?;
|
let cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?;
|
||||||
let url = cdn_url.get_url()?;
|
let url = cdn_url.try_get_url()?;
|
||||||
|
|
||||||
trace!("Streaming {:?}", url);
|
trace!("Streaming {:?}", url);
|
||||||
|
|
||||||
|
@ -403,23 +406,19 @@ impl AudioFileStreaming {
|
||||||
// Get the first chunk with the headers to get the file size.
|
// Get the first chunk with the headers to get the file size.
|
||||||
// The remainder of that chunk with possibly also a response body is then
|
// The remainder of that chunk with possibly also a response body is then
|
||||||
// further processed in `audio_file_fetch`.
|
// further processed in `audio_file_fetch`.
|
||||||
let response = match streamer.next().await {
|
let response = streamer.next().await.ok_or(AudioFileError::NoData)??;
|
||||||
Some(Ok(data)) => data,
|
|
||||||
Some(Err(e)) => return Err(AudioFileError::Cdn(e)),
|
|
||||||
None => return Err(AudioFileError::Empty),
|
|
||||||
};
|
|
||||||
let header_value = response
|
let header_value = response
|
||||||
.headers()
|
.headers()
|
||||||
.get(CONTENT_RANGE)
|
.get(CONTENT_RANGE)
|
||||||
.ok_or(AudioFileError::Parsing)?;
|
.ok_or(AudioFileError::Header)?;
|
||||||
|
let str_value = header_value.to_str()?;
|
||||||
let str_value = header_value.to_str().map_err(|_| AudioFileError::Parsing)?;
|
let file_size_str = str_value.split('/').last().unwrap_or_default();
|
||||||
let file_size_str = str_value.split('/').last().ok_or(AudioFileError::Parsing)?;
|
let file_size = file_size_str.parse()?;
|
||||||
let file_size = file_size_str.parse().map_err(|_| AudioFileError::Parsing)?;
|
|
||||||
|
|
||||||
let initial_request = StreamingRequest {
|
let initial_request = StreamingRequest {
|
||||||
streamer,
|
streamer,
|
||||||
initial_body: Some(response.into_body()),
|
initial_response: Some(response),
|
||||||
offset: 0,
|
offset: 0,
|
||||||
length: download_size,
|
length: download_size,
|
||||||
request_time,
|
request_time,
|
||||||
|
@ -474,12 +473,7 @@ impl Read for AudioFileStreaming {
|
||||||
|
|
||||||
let length = min(output.len(), self.shared.file_size - offset);
|
let length = min(output.len(), self.shared.file_size - offset);
|
||||||
|
|
||||||
let length_to_request = match *(self
|
let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) {
|
||||||
.shared
|
|
||||||
.download_strategy
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?)
|
|
||||||
{
|
|
||||||
DownloadStrategy::RandomAccess() => length,
|
DownloadStrategy::RandomAccess() => length,
|
||||||
DownloadStrategy::Streaming() => {
|
DownloadStrategy::Streaming() => {
|
||||||
// Due to the read-ahead stuff, we potentially request more than the actual request demanded.
|
// Due to the read-ahead stuff, we potentially request more than the actual request demanded.
|
||||||
|
@ -503,42 +497,32 @@ impl Read for AudioFileStreaming {
|
||||||
let mut ranges_to_request = RangeSet::new();
|
let mut ranges_to_request = RangeSet::new();
|
||||||
ranges_to_request.add_range(&Range::new(offset, length_to_request));
|
ranges_to_request.add_range(&Range::new(offset, length_to_request));
|
||||||
|
|
||||||
let mut download_status = self
|
let mut download_status = self.shared.download_status.lock().unwrap();
|
||||||
.shared
|
|
||||||
.download_status
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?;
|
|
||||||
ranges_to_request.subtract_range_set(&download_status.downloaded);
|
ranges_to_request.subtract_range_set(&download_status.downloaded);
|
||||||
ranges_to_request.subtract_range_set(&download_status.requested);
|
ranges_to_request.subtract_range_set(&download_status.requested);
|
||||||
|
|
||||||
for &range in ranges_to_request.iter() {
|
for &range in ranges_to_request.iter() {
|
||||||
self.stream_loader_command_tx
|
self.stream_loader_command_tx
|
||||||
.send(StreamLoaderCommand::Fetch(range))
|
.send(StreamLoaderCommand::Fetch(range))
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "tx channel is disconnected"))?;
|
.map_err(|err| io::Error::new(io::ErrorKind::BrokenPipe, err))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut download_message_printed = false;
|
|
||||||
while !download_status.downloaded.contains(offset) {
|
while !download_status.downloaded.contains(offset) {
|
||||||
if let DownloadStrategy::Streaming() = *self
|
|
||||||
.shared
|
|
||||||
.download_strategy
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?
|
|
||||||
{
|
|
||||||
if !download_message_printed {
|
|
||||||
debug!("Stream waiting for download of file position {}. Downloaded ranges: {}. Pending ranges: {}", offset, download_status.downloaded, download_status.requested.minus(&download_status.downloaded));
|
|
||||||
download_message_printed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
download_status = self
|
download_status = self
|
||||||
.shared
|
.shared
|
||||||
.cond
|
.cond
|
||||||
.wait_timeout(download_status, DOWNLOAD_TIMEOUT)
|
.wait_timeout(download_status, DOWNLOAD_TIMEOUT)
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "timeout acquiring mutex"))?
|
.map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::TimedOut,
|
||||||
|
Error::deadline_exceeded(AudioFileError::WaitTimeout),
|
||||||
|
)
|
||||||
|
})?
|
||||||
.0;
|
.0;
|
||||||
}
|
}
|
||||||
let available_length = download_status
|
let available_length = download_status
|
||||||
|
@ -551,15 +535,6 @@ impl Read for AudioFileStreaming {
|
||||||
let read_len = min(length, available_length);
|
let read_len = min(length, available_length);
|
||||||
let read_len = self.read_file.read(&mut output[..read_len])?;
|
let read_len = self.read_file.read(&mut output[..read_len])?;
|
||||||
|
|
||||||
if download_message_printed {
|
|
||||||
debug!(
|
|
||||||
"Read at postion {} completed. {} bytes returned, {} bytes were requested.",
|
|
||||||
offset,
|
|
||||||
read_len,
|
|
||||||
output.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.position += read_len as u64;
|
self.position += read_len as u64;
|
||||||
self.shared
|
self.shared
|
||||||
.read_position
|
.read_position
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
use std::cmp::{max, min};
|
use std::{
|
||||||
use std::io::{Seek, SeekFrom, Write};
|
cmp::{max, min},
|
||||||
use std::sync::{atomic, Arc};
|
io::{Seek, SeekFrom, Write},
|
||||||
use std::time::{Duration, Instant};
|
sync::{atomic, Arc},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use atomic::Ordering;
|
use atomic::Ordering;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
|
use hyper::StatusCode;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
use librespot_core::session::Session;
|
use librespot_core::{session::Session, Error};
|
||||||
|
|
||||||
use crate::range_set::{Range, RangeSet};
|
use crate::range_set::{Range, RangeSet};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AudioFileError, AudioFileResult, AudioFileShared, DownloadStrategy, StreamLoaderCommand,
|
AudioFileError, AudioFileResult, AudioFileShared, DownloadStrategy, StreamLoaderCommand,
|
||||||
StreamingRequest,
|
StreamingRequest, FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME,
|
||||||
};
|
MAX_PREFETCH_REQUESTS, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR,
|
||||||
use super::{
|
|
||||||
FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS,
|
|
||||||
MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PartialFileData {
|
struct PartialFileData {
|
||||||
|
@ -49,19 +49,27 @@ async fn receive_data(
|
||||||
|
|
||||||
let mut measure_ping_time = old_number_of_request == 0;
|
let mut measure_ping_time = old_number_of_request == 0;
|
||||||
|
|
||||||
let result = loop {
|
let result: Result<_, Error> = loop {
|
||||||
let body = match request.initial_body.take() {
|
let response = match request.initial_response.take() {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => match request.streamer.next().await {
|
None => match request.streamer.next().await {
|
||||||
Some(Ok(response)) => response.into_body(),
|
Some(Ok(response)) => response,
|
||||||
Some(Err(e)) => break Err(e),
|
Some(Err(e)) => break Err(e.into()),
|
||||||
None => break Ok(()),
|
None => break Ok(()),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let code = response.status();
|
||||||
|
let body = response.into_body();
|
||||||
|
|
||||||
|
if code != StatusCode::PARTIAL_CONTENT {
|
||||||
|
debug!("Streamer expected partial content but got: {}", code);
|
||||||
|
break Err(AudioFileError::StatusCode(code).into());
|
||||||
|
}
|
||||||
|
|
||||||
let data = match hyper::body::to_bytes(body).await {
|
let data = match hyper::body::to_bytes(body).await {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
Err(e) => break Err(e),
|
Err(e) => break Err(e.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if measure_ping_time {
|
if measure_ping_time {
|
||||||
|
@ -69,16 +77,16 @@ async fn receive_data(
|
||||||
if duration > MAXIMUM_ASSUMED_PING_TIME {
|
if duration > MAXIMUM_ASSUMED_PING_TIME {
|
||||||
duration = MAXIMUM_ASSUMED_PING_TIME;
|
duration = MAXIMUM_ASSUMED_PING_TIME;
|
||||||
}
|
}
|
||||||
let _ = file_data_tx.send(ReceivedData::ResponseTime(duration));
|
file_data_tx.send(ReceivedData::ResponseTime(duration))?;
|
||||||
measure_ping_time = false;
|
measure_ping_time = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data_size = data.len();
|
let data_size = data.len();
|
||||||
|
|
||||||
let _ = file_data_tx.send(ReceivedData::Data(PartialFileData {
|
file_data_tx.send(ReceivedData::Data(PartialFileData {
|
||||||
offset: data_offset,
|
offset: data_offset,
|
||||||
data,
|
data,
|
||||||
}));
|
}))?;
|
||||||
data_offset += data_size;
|
data_offset += data_size;
|
||||||
if request_length < data_size {
|
if request_length < data_size {
|
||||||
warn!(
|
warn!(
|
||||||
|
@ -100,10 +108,8 @@ async fn receive_data(
|
||||||
if request_length > 0 {
|
if request_length > 0 {
|
||||||
let missing_range = Range::new(data_offset, request_length);
|
let missing_range = Range::new(data_offset, request_length);
|
||||||
|
|
||||||
let mut download_status = shared
|
let mut download_status = shared.download_status.lock().unwrap();
|
||||||
.download_status
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?;
|
|
||||||
download_status.requested.subtract_range(&missing_range);
|
download_status.requested.subtract_range(&missing_range);
|
||||||
shared.cond.notify_all();
|
shared.cond.notify_all();
|
||||||
}
|
}
|
||||||
|
@ -127,7 +133,7 @@ async fn receive_data(
|
||||||
"Error from streamer for range {} (+{}): {:?}",
|
"Error from streamer for range {} (+{}): {:?}",
|
||||||
requested_offset, requested_length, e
|
requested_offset, requested_length, e
|
||||||
);
|
);
|
||||||
Err(e.into())
|
Err(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,14 +156,8 @@ enum ControlFlow {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioFileFetch {
|
impl AudioFileFetch {
|
||||||
fn get_download_strategy(&mut self) -> Result<DownloadStrategy, AudioFileError> {
|
fn get_download_strategy(&mut self) -> DownloadStrategy {
|
||||||
let strategy = self
|
*(self.shared.download_strategy.lock().unwrap())
|
||||||
.shared
|
|
||||||
.download_strategy
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?;
|
|
||||||
|
|
||||||
Ok(*(strategy))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult {
|
fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult {
|
||||||
|
@ -172,32 +172,24 @@ impl AudioFileFetch {
|
||||||
let mut ranges_to_request = RangeSet::new();
|
let mut ranges_to_request = RangeSet::new();
|
||||||
ranges_to_request.add_range(&Range::new(offset, length));
|
ranges_to_request.add_range(&Range::new(offset, length));
|
||||||
|
|
||||||
let mut download_status = self
|
let mut download_status = self.shared.download_status.lock().unwrap();
|
||||||
.shared
|
|
||||||
.download_status
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?;
|
|
||||||
|
|
||||||
ranges_to_request.subtract_range_set(&download_status.downloaded);
|
ranges_to_request.subtract_range_set(&download_status.downloaded);
|
||||||
ranges_to_request.subtract_range_set(&download_status.requested);
|
ranges_to_request.subtract_range_set(&download_status.requested);
|
||||||
|
|
||||||
let cdn_url = &self.shared.cdn_url;
|
|
||||||
let file_id = cdn_url.file_id;
|
|
||||||
|
|
||||||
for range in ranges_to_request.iter() {
|
for range in ranges_to_request.iter() {
|
||||||
match cdn_url.urls.first() {
|
let url = self.shared.cdn_url.try_get_url()?;
|
||||||
Some(url) => {
|
|
||||||
match self
|
let streamer = self
|
||||||
.session
|
.session
|
||||||
.spclient()
|
.spclient()
|
||||||
.stream_file(&url.0, range.start, range.length)
|
.stream_file(url, range.start, range.length)?;
|
||||||
{
|
|
||||||
Ok(streamer) => {
|
|
||||||
download_status.requested.add_range(range);
|
download_status.requested.add_range(range);
|
||||||
|
|
||||||
let streaming_request = StreamingRequest {
|
let streaming_request = StreamingRequest {
|
||||||
streamer,
|
streamer,
|
||||||
initial_body: None,
|
initial_response: None,
|
||||||
offset: range.start,
|
offset: range.start,
|
||||||
length: range.length,
|
length: range.length,
|
||||||
request_time: Instant::now(),
|
request_time: Instant::now(),
|
||||||
|
@ -209,16 +201,6 @@ impl AudioFileFetch {
|
||||||
streaming_request,
|
streaming_request,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
error!("Unable to open stream for track <{}>: {:?}", file_id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
error!("Unable to get CDN URL for track <{}>", file_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -236,11 +218,8 @@ impl AudioFileFetch {
|
||||||
let mut missing_data = RangeSet::new();
|
let mut missing_data = RangeSet::new();
|
||||||
missing_data.add_range(&Range::new(0, self.shared.file_size));
|
missing_data.add_range(&Range::new(0, self.shared.file_size));
|
||||||
{
|
{
|
||||||
let download_status = self
|
let download_status = self.shared.download_status.lock().unwrap();
|
||||||
.shared
|
|
||||||
.download_status
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?;
|
|
||||||
missing_data.subtract_range_set(&download_status.downloaded);
|
missing_data.subtract_range_set(&download_status.downloaded);
|
||||||
missing_data.subtract_range_set(&download_status.requested);
|
missing_data.subtract_range_set(&download_status.requested);
|
||||||
}
|
}
|
||||||
|
@ -277,7 +256,7 @@ impl AudioFileFetch {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_file_data(&mut self, data: ReceivedData) -> Result<ControlFlow, AudioFileError> {
|
fn handle_file_data(&mut self, data: ReceivedData) -> Result<ControlFlow, Error> {
|
||||||
match data {
|
match data {
|
||||||
ReceivedData::ResponseTime(response_time) => {
|
ReceivedData::ResponseTime(response_time) => {
|
||||||
let old_ping_time_ms = self.shared.ping_time_ms.load(Ordering::Relaxed);
|
let old_ping_time_ms = self.shared.ping_time_ms.load(Ordering::Relaxed);
|
||||||
|
@ -324,14 +303,10 @@ impl AudioFileFetch {
|
||||||
output.seek(SeekFrom::Start(data.offset as u64))?;
|
output.seek(SeekFrom::Start(data.offset as u64))?;
|
||||||
output.write_all(data.data.as_ref())?;
|
output.write_all(data.data.as_ref())?;
|
||||||
}
|
}
|
||||||
None => return Err(AudioFileError::Output),
|
None => return Err(AudioFileError::Output.into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut download_status = self
|
let mut download_status = self.shared.download_status.lock().unwrap();
|
||||||
.shared
|
|
||||||
.download_status
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?;
|
|
||||||
|
|
||||||
let received_range = Range::new(data.offset, data.data.len());
|
let received_range = Range::new(data.offset, data.data.len());
|
||||||
download_status.downloaded.add_range(&received_range);
|
download_status.downloaded.add_range(&received_range);
|
||||||
|
@ -355,38 +330,38 @@ impl AudioFileFetch {
|
||||||
fn handle_stream_loader_command(
|
fn handle_stream_loader_command(
|
||||||
&mut self,
|
&mut self,
|
||||||
cmd: StreamLoaderCommand,
|
cmd: StreamLoaderCommand,
|
||||||
) -> Result<ControlFlow, AudioFileError> {
|
) -> Result<ControlFlow, Error> {
|
||||||
match cmd {
|
match cmd {
|
||||||
StreamLoaderCommand::Fetch(request) => {
|
StreamLoaderCommand::Fetch(request) => {
|
||||||
self.download_range(request.start, request.length)?;
|
self.download_range(request.start, request.length)?;
|
||||||
}
|
}
|
||||||
StreamLoaderCommand::RandomAccessMode() => {
|
StreamLoaderCommand::RandomAccessMode() => {
|
||||||
*(self
|
*(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::RandomAccess();
|
||||||
.shared
|
|
||||||
.download_strategy
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?) = DownloadStrategy::RandomAccess();
|
|
||||||
}
|
}
|
||||||
StreamLoaderCommand::StreamMode() => {
|
StreamLoaderCommand::StreamMode() => {
|
||||||
*(self
|
*(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::Streaming();
|
||||||
.shared
|
|
||||||
.download_strategy
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?) = DownloadStrategy::Streaming();
|
|
||||||
}
|
}
|
||||||
StreamLoaderCommand::Close() => return Ok(ControlFlow::Break),
|
StreamLoaderCommand::Close() => return Ok(ControlFlow::Break),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ControlFlow::Continue)
|
Ok(ControlFlow::Continue)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&mut self) -> AudioFileResult {
|
fn finish(&mut self) -> AudioFileResult {
|
||||||
let mut output = self.output.take().ok_or(AudioFileError::Output)?;
|
let output = self.output.take();
|
||||||
let complete_tx = self.complete_tx.take().ok_or(AudioFileError::Output)?;
|
|
||||||
|
|
||||||
|
let complete_tx = self.complete_tx.take();
|
||||||
|
|
||||||
|
if let Some(mut output) = output {
|
||||||
output.seek(SeekFrom::Start(0))?;
|
output.seek(SeekFrom::Start(0))?;
|
||||||
|
if let Some(complete_tx) = complete_tx {
|
||||||
complete_tx
|
complete_tx
|
||||||
.send(output)
|
.send(output)
|
||||||
.map_err(|_| AudioFileError::Channel)
|
.map_err(|_| AudioFileError::Channel)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,10 +380,8 @@ pub(super) async fn audio_file_fetch(
|
||||||
initial_request.offset,
|
initial_request.offset,
|
||||||
initial_request.offset + initial_request.length,
|
initial_request.offset + initial_request.length,
|
||||||
);
|
);
|
||||||
let mut download_status = shared
|
let mut download_status = shared.download_status.lock().unwrap();
|
||||||
.download_status
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?;
|
|
||||||
download_status.requested.add_range(&requested_range);
|
download_status.requested.add_range(&requested_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,18 +425,15 @@ pub(super) async fn audio_file_fetch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if fetch.get_download_strategy()? == DownloadStrategy::Streaming() {
|
if fetch.get_download_strategy() == DownloadStrategy::Streaming() {
|
||||||
let number_of_open_requests =
|
let number_of_open_requests =
|
||||||
fetch.shared.number_of_open_requests.load(Ordering::SeqCst);
|
fetch.shared.number_of_open_requests.load(Ordering::SeqCst);
|
||||||
if number_of_open_requests < MAX_PREFETCH_REQUESTS {
|
if number_of_open_requests < MAX_PREFETCH_REQUESTS {
|
||||||
let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests;
|
let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests;
|
||||||
|
|
||||||
let bytes_pending: usize = {
|
let bytes_pending: usize = {
|
||||||
let download_status = fetch
|
let download_status = fetch.shared.download_status.lock().unwrap();
|
||||||
.shared
|
|
||||||
.download_status
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| AudioFileError::Poisoned)?;
|
|
||||||
download_status
|
download_status
|
||||||
.requested
|
.requested
|
||||||
.minus(&download_status.downloaded)
|
.minus(&download_status.downloaded)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use std::cmp::{max, min};
|
use std::{
|
||||||
use std::fmt;
|
cmp::{max, min},
|
||||||
use std::slice::Iter;
|
fmt,
|
||||||
|
slice::Iter,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct Range {
|
pub struct Range {
|
||||||
|
|
|
@ -15,6 +15,7 @@ protobuf = "2.14.0"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
tokio = { version = "1.0", features = ["macros", "sync"] }
|
tokio = { version = "1.0", features = ["macros", "sync"] }
|
||||||
tokio-stream = "0.1.1"
|
tokio-stream = "0.1.1"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
|
// TODO : move to metadata
|
||||||
|
|
||||||
use crate::core::spotify_id::SpotifyId;
|
use crate::core::spotify_id::SpotifyId;
|
||||||
use crate::protocol::spirc::TrackRef;
|
use crate::protocol::spirc::TrackRef;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::{
|
||||||
|
de::{Error, Unexpected},
|
||||||
|
Deserialize,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct StationContext {
|
pub struct StationContext {
|
||||||
|
@ -72,17 +77,23 @@ where
|
||||||
D: serde::Deserializer<'d>,
|
D: serde::Deserializer<'d>,
|
||||||
{
|
{
|
||||||
let v: Vec<TrackContext> = serde::Deserialize::deserialize(de)?;
|
let v: Vec<TrackContext> = serde::Deserialize::deserialize(de)?;
|
||||||
let track_vec = v
|
v.iter()
|
||||||
.iter()
|
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
let mut t = TrackRef::new();
|
let mut t = TrackRef::new();
|
||||||
// This has got to be the most round about way of doing this.
|
// This has got to be the most round about way of doing this.
|
||||||
t.set_gid(SpotifyId::from_base62(&v.gid).unwrap().to_raw().to_vec());
|
t.set_gid(
|
||||||
|
SpotifyId::from_base62(&v.gid)
|
||||||
|
.map_err(|_| {
|
||||||
|
D::Error::invalid_value(
|
||||||
|
Unexpected::Str(&v.gid),
|
||||||
|
&"a Base-62 encoded Spotify ID",
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.to_raw()
|
||||||
|
.to_vec(),
|
||||||
|
);
|
||||||
t.set_uri(v.uri.to_owned());
|
t.set_uri(v.uri.to_owned());
|
||||||
|
Ok(t)
|
||||||
t
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<TrackRef>>();
|
.collect::<Result<Vec<TrackRef>, D::Error>>()
|
||||||
|
|
||||||
Ok(track_vec)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use std::io;
|
use std::{
|
||||||
use std::pin::Pin;
|
io,
|
||||||
use std::task::{Context, Poll};
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
use futures_util::Stream;
|
use futures_util::Stream;
|
||||||
use librespot_core::authentication::Credentials;
|
use librespot_core::{authentication::Credentials, config::ConnectConfig};
|
||||||
use librespot_core::config::ConnectConfig;
|
|
||||||
|
|
||||||
pub struct DiscoveryStream(librespot_discovery::Discovery);
|
pub struct DiscoveryStream(librespot_discovery::Discovery);
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,67 @@
|
||||||
use std::convert::TryFrom;
|
use std::{
|
||||||
use std::future::Future;
|
convert::TryFrom,
|
||||||
use std::pin::Pin;
|
future::Future,
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
pin::Pin,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::context::StationContext;
|
use futures_util::{
|
||||||
use crate::core::config::ConnectConfig;
|
future::{self, FusedFuture},
|
||||||
use crate::core::mercury::{MercuryError, MercurySender};
|
stream::FusedStream,
|
||||||
use crate::core::session::{Session, UserAttributes};
|
FutureExt, StreamExt, TryFutureExt,
|
||||||
use crate::core::spotify_id::SpotifyId;
|
};
|
||||||
use crate::core::util::SeqGenerator;
|
|
||||||
use crate::core::version;
|
|
||||||
use crate::playback::mixer::Mixer;
|
|
||||||
use crate::playback::player::{Player, PlayerEvent, PlayerEventChannel};
|
|
||||||
|
|
||||||
use crate::protocol;
|
|
||||||
use crate::protocol::explicit_content_pubsub::UserAttributesUpdate;
|
|
||||||
use crate::protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef};
|
|
||||||
use crate::protocol::user_attributes::UserAttributesMutation;
|
|
||||||
|
|
||||||
use futures_util::future::{self, FusedFuture};
|
|
||||||
use futures_util::stream::FusedStream;
|
|
||||||
use futures_util::{FutureExt, StreamExt};
|
|
||||||
use protobuf::{self, Message};
|
use protobuf::{self, Message};
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
|
use thiserror::Error;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
context::StationContext,
|
||||||
|
core::{
|
||||||
|
config::ConnectConfig, // TODO: move to connect?
|
||||||
|
mercury::{MercuryError, MercurySender},
|
||||||
|
session::UserAttributes,
|
||||||
|
util::SeqGenerator,
|
||||||
|
version,
|
||||||
|
Error,
|
||||||
|
Session,
|
||||||
|
SpotifyId,
|
||||||
|
},
|
||||||
|
playback::{
|
||||||
|
mixer::Mixer,
|
||||||
|
player::{Player, PlayerEvent, PlayerEventChannel},
|
||||||
|
},
|
||||||
|
protocol::{
|
||||||
|
self,
|
||||||
|
explicit_content_pubsub::UserAttributesUpdate,
|
||||||
|
spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef},
|
||||||
|
user_attributes::UserAttributesMutation,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SpircError {
|
||||||
|
#[error("response payload empty")]
|
||||||
|
NoData,
|
||||||
|
#[error("message addressed at another ident: {0}")]
|
||||||
|
Ident(String),
|
||||||
|
#[error("message pushed for another URI")]
|
||||||
|
InvalidUri(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SpircError> for Error {
|
||||||
|
fn from(err: SpircError) -> Self {
|
||||||
|
match err {
|
||||||
|
SpircError::NoData => Error::unavailable(err),
|
||||||
|
SpircError::Ident(_) => Error::aborted(err),
|
||||||
|
SpircError::InvalidUri(_) => Error::aborted(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum SpircPlayStatus {
|
enum SpircPlayStatus {
|
||||||
Stopped,
|
Stopped,
|
||||||
LoadingPlay {
|
LoadingPlay {
|
||||||
|
@ -60,18 +96,18 @@ struct SpircTask {
|
||||||
play_request_id: Option<u64>,
|
play_request_id: Option<u64>,
|
||||||
play_status: SpircPlayStatus,
|
play_status: SpircPlayStatus,
|
||||||
|
|
||||||
subscription: BoxedStream<Frame>,
|
remote_update: BoxedStream<Result<Frame, Error>>,
|
||||||
connection_id_update: BoxedStream<String>,
|
connection_id_update: BoxedStream<Result<String, Error>>,
|
||||||
user_attributes_update: BoxedStream<UserAttributesUpdate>,
|
user_attributes_update: BoxedStream<Result<UserAttributesUpdate, Error>>,
|
||||||
user_attributes_mutation: BoxedStream<UserAttributesMutation>,
|
user_attributes_mutation: BoxedStream<Result<UserAttributesMutation, Error>>,
|
||||||
sender: MercurySender,
|
sender: MercurySender,
|
||||||
commands: Option<mpsc::UnboundedReceiver<SpircCommand>>,
|
commands: Option<mpsc::UnboundedReceiver<SpircCommand>>,
|
||||||
player_events: Option<PlayerEventChannel>,
|
player_events: Option<PlayerEventChannel>,
|
||||||
|
|
||||||
shutdown: bool,
|
shutdown: bool,
|
||||||
session: Session,
|
session: Session,
|
||||||
context_fut: BoxedFuture<Result<serde_json::Value, MercuryError>>,
|
context_fut: BoxedFuture<Result<serde_json::Value, Error>>,
|
||||||
autoplay_fut: BoxedFuture<Result<String, MercuryError>>,
|
autoplay_fut: BoxedFuture<Result<String, Error>>,
|
||||||
context: Option<StationContext>,
|
context: Option<StationContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,7 +268,7 @@ impl Spirc {
|
||||||
session: Session,
|
session: Session,
|
||||||
player: Player,
|
player: Player,
|
||||||
mixer: Box<dyn Mixer>,
|
mixer: Box<dyn Mixer>,
|
||||||
) -> (Spirc, impl Future<Output = ()>) {
|
) -> Result<(Spirc, impl Future<Output = ()>), Error> {
|
||||||
debug!("new Spirc[{}]", session.session_id());
|
debug!("new Spirc[{}]", session.session_id());
|
||||||
|
|
||||||
let ident = session.device_id().to_owned();
|
let ident = session.device_id().to_owned();
|
||||||
|
@ -242,16 +278,18 @@ impl Spirc {
|
||||||
debug!("canonical_username: {}", canonical_username);
|
debug!("canonical_username: {}", canonical_username);
|
||||||
let uri = format!("hm://remote/user/{}/", url_encode(canonical_username));
|
let uri = format!("hm://remote/user/{}/", url_encode(canonical_username));
|
||||||
|
|
||||||
let subscription = Box::pin(
|
let remote_update = Box::pin(
|
||||||
session
|
session
|
||||||
.mercury()
|
.mercury()
|
||||||
.subscribe(uri.clone())
|
.subscribe(uri.clone())
|
||||||
.map(Result::unwrap)
|
.inspect_err(|x| error!("remote update error: {}", x))
|
||||||
|
.and_then(|x| async move { Ok(x) })
|
||||||
|
.map(Result::unwrap) // guaranteed to be safe by `and_then` above
|
||||||
.map(UnboundedReceiverStream::new)
|
.map(UnboundedReceiverStream::new)
|
||||||
.flatten_stream()
|
.flatten_stream()
|
||||||
.map(|response| -> Frame {
|
.map(|response| -> Result<Frame, Error> {
|
||||||
let data = response.payload.first().unwrap();
|
let data = response.payload.first().ok_or(SpircError::NoData)?;
|
||||||
Frame::parse_from_bytes(data).unwrap()
|
Ok(Frame::parse_from_bytes(data)?)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -261,12 +299,12 @@ impl Spirc {
|
||||||
.listen_for("hm://pusher/v1/connections/")
|
.listen_for("hm://pusher/v1/connections/")
|
||||||
.map(UnboundedReceiverStream::new)
|
.map(UnboundedReceiverStream::new)
|
||||||
.flatten_stream()
|
.flatten_stream()
|
||||||
.map(|response| -> String {
|
.map(|response| -> Result<String, Error> {
|
||||||
response
|
let connection_id = response
|
||||||
.uri
|
.uri
|
||||||
.strip_prefix("hm://pusher/v1/connections/")
|
.strip_prefix("hm://pusher/v1/connections/")
|
||||||
.unwrap_or("")
|
.ok_or_else(|| SpircError::InvalidUri(response.uri.clone()))?;
|
||||||
.to_owned()
|
Ok(connection_id.to_owned())
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -276,9 +314,9 @@ impl Spirc {
|
||||||
.listen_for("spotify:user:attributes:update")
|
.listen_for("spotify:user:attributes:update")
|
||||||
.map(UnboundedReceiverStream::new)
|
.map(UnboundedReceiverStream::new)
|
||||||
.flatten_stream()
|
.flatten_stream()
|
||||||
.map(|response| -> UserAttributesUpdate {
|
.map(|response| -> Result<UserAttributesUpdate, Error> {
|
||||||
let data = response.payload.first().unwrap();
|
let data = response.payload.first().ok_or(SpircError::NoData)?;
|
||||||
UserAttributesUpdate::parse_from_bytes(data).unwrap()
|
Ok(UserAttributesUpdate::parse_from_bytes(data)?)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -288,9 +326,9 @@ impl Spirc {
|
||||||
.listen_for("spotify:user:attributes:mutated")
|
.listen_for("spotify:user:attributes:mutated")
|
||||||
.map(UnboundedReceiverStream::new)
|
.map(UnboundedReceiverStream::new)
|
||||||
.flatten_stream()
|
.flatten_stream()
|
||||||
.map(|response| -> UserAttributesMutation {
|
.map(|response| -> Result<UserAttributesMutation, Error> {
|
||||||
let data = response.payload.first().unwrap();
|
let data = response.payload.first().ok_or(SpircError::NoData)?;
|
||||||
UserAttributesMutation::parse_from_bytes(data).unwrap()
|
Ok(UserAttributesMutation::parse_from_bytes(data)?)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -321,7 +359,7 @@ impl Spirc {
|
||||||
play_request_id: None,
|
play_request_id: None,
|
||||||
play_status: SpircPlayStatus::Stopped,
|
play_status: SpircPlayStatus::Stopped,
|
||||||
|
|
||||||
subscription,
|
remote_update,
|
||||||
connection_id_update,
|
connection_id_update,
|
||||||
user_attributes_update,
|
user_attributes_update,
|
||||||
user_attributes_mutation,
|
user_attributes_mutation,
|
||||||
|
@ -346,37 +384,37 @@ impl Spirc {
|
||||||
|
|
||||||
let spirc = Spirc { commands: cmd_tx };
|
let spirc = Spirc { commands: cmd_tx };
|
||||||
|
|
||||||
task.hello();
|
task.hello()?;
|
||||||
|
|
||||||
(spirc, task.run())
|
Ok((spirc, task.run()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play(&self) {
|
pub fn play(&self) -> Result<(), Error> {
|
||||||
let _ = self.commands.send(SpircCommand::Play);
|
Ok(self.commands.send(SpircCommand::Play)?)
|
||||||
}
|
}
|
||||||
pub fn play_pause(&self) {
|
pub fn play_pause(&self) -> Result<(), Error> {
|
||||||
let _ = self.commands.send(SpircCommand::PlayPause);
|
Ok(self.commands.send(SpircCommand::PlayPause)?)
|
||||||
}
|
}
|
||||||
pub fn pause(&self) {
|
pub fn pause(&self) -> Result<(), Error> {
|
||||||
let _ = self.commands.send(SpircCommand::Pause);
|
Ok(self.commands.send(SpircCommand::Pause)?)
|
||||||
}
|
}
|
||||||
pub fn prev(&self) {
|
pub fn prev(&self) -> Result<(), Error> {
|
||||||
let _ = self.commands.send(SpircCommand::Prev);
|
Ok(self.commands.send(SpircCommand::Prev)?)
|
||||||
}
|
}
|
||||||
pub fn next(&self) {
|
pub fn next(&self) -> Result<(), Error> {
|
||||||
let _ = self.commands.send(SpircCommand::Next);
|
Ok(self.commands.send(SpircCommand::Next)?)
|
||||||
}
|
}
|
||||||
pub fn volume_up(&self) {
|
pub fn volume_up(&self) -> Result<(), Error> {
|
||||||
let _ = self.commands.send(SpircCommand::VolumeUp);
|
Ok(self.commands.send(SpircCommand::VolumeUp)?)
|
||||||
}
|
}
|
||||||
pub fn volume_down(&self) {
|
pub fn volume_down(&self) -> Result<(), Error> {
|
||||||
let _ = self.commands.send(SpircCommand::VolumeDown);
|
Ok(self.commands.send(SpircCommand::VolumeDown)?)
|
||||||
}
|
}
|
||||||
pub fn shutdown(&self) {
|
pub fn shutdown(&self) -> Result<(), Error> {
|
||||||
let _ = self.commands.send(SpircCommand::Shutdown);
|
Ok(self.commands.send(SpircCommand::Shutdown)?)
|
||||||
}
|
}
|
||||||
pub fn shuffle(&self) {
|
pub fn shuffle(&self) -> Result<(), Error> {
|
||||||
let _ = self.commands.send(SpircCommand::Shuffle);
|
Ok(self.commands.send(SpircCommand::Shuffle)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,39 +424,57 @@ impl SpircTask {
|
||||||
let commands = self.commands.as_mut();
|
let commands = self.commands.as_mut();
|
||||||
let player_events = self.player_events.as_mut();
|
let player_events = self.player_events.as_mut();
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
frame = self.subscription.next() => match frame {
|
remote_update = self.remote_update.next() => match remote_update {
|
||||||
Some(frame) => self.handle_frame(frame),
|
Some(result) => match result {
|
||||||
|
Ok(update) => if let Err(e) = self.handle_remote_update(update) {
|
||||||
|
error!("could not dispatch remote update: {}", e);
|
||||||
|
}
|
||||||
|
Err(e) => error!("could not parse remote update: {}", e),
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
error!("subscription terminated");
|
error!("subscription terminated");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
user_attributes_update = self.user_attributes_update.next() => match user_attributes_update {
|
user_attributes_update = self.user_attributes_update.next() => match user_attributes_update {
|
||||||
Some(attributes) => self.handle_user_attributes_update(attributes),
|
Some(result) => match result {
|
||||||
|
Ok(attributes) => self.handle_user_attributes_update(attributes),
|
||||||
|
Err(e) => error!("could not parse user attributes update: {}", e),
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
error!("user attributes update selected, but none received");
|
error!("user attributes update selected, but none received");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
user_attributes_mutation = self.user_attributes_mutation.next() => match user_attributes_mutation {
|
user_attributes_mutation = self.user_attributes_mutation.next() => match user_attributes_mutation {
|
||||||
Some(attributes) => self.handle_user_attributes_mutation(attributes),
|
Some(result) => match result {
|
||||||
|
Ok(attributes) => self.handle_user_attributes_mutation(attributes),
|
||||||
|
Err(e) => error!("could not parse user attributes mutation: {}", e),
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
error!("user attributes mutation selected, but none received");
|
error!("user attributes mutation selected, but none received");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
connection_id_update = self.connection_id_update.next() => match connection_id_update {
|
connection_id_update = self.connection_id_update.next() => match connection_id_update {
|
||||||
Some(connection_id) => self.handle_connection_id_update(connection_id),
|
Some(result) => match result {
|
||||||
|
Ok(connection_id) => self.handle_connection_id_update(connection_id),
|
||||||
|
Err(e) => error!("could not parse connection ID update: {}", e),
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
error!("connection ID update selected, but none received");
|
error!("connection ID update selected, but none received");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cmd = async { commands.unwrap().recv().await }, if commands.is_some() => if let Some(cmd) = cmd {
|
cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = cmd {
|
||||||
self.handle_command(cmd);
|
if let Err(e) = self.handle_command(cmd) {
|
||||||
|
error!("could not dispatch command: {}", e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
event = async { player_events.unwrap().recv().await }, if player_events.is_some() => if let Some(event) = event {
|
event = async { player_events?.recv().await }, if player_events.is_some() => if let Some(event) = event {
|
||||||
self.handle_player_event(event)
|
if let Err(e) = self.handle_player_event(event) {
|
||||||
|
error!("could not dispatch player event: {}", e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
result = self.sender.flush(), if !self.sender.is_flushed() => if result.is_err() {
|
result = self.sender.flush(), if !self.sender.is_flushed() => if result.is_err() {
|
||||||
error!("Cannot flush spirc event sender.");
|
error!("Cannot flush spirc event sender.");
|
||||||
|
@ -488,79 +544,80 @@ impl SpircTask {
|
||||||
self.state.set_position_ms(position_ms);
|
self.state.set_position_ms(position_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_command(&mut self, cmd: SpircCommand) {
|
fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> {
|
||||||
let active = self.device.get_is_active();
|
let active = self.device.get_is_active();
|
||||||
match cmd {
|
match cmd {
|
||||||
SpircCommand::Play => {
|
SpircCommand::Play => {
|
||||||
if active {
|
if active {
|
||||||
self.handle_play();
|
self.handle_play();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
} else {
|
} else {
|
||||||
CommandSender::new(self, MessageType::kMessageTypePlay).send();
|
CommandSender::new(self, MessageType::kMessageTypePlay).send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpircCommand::PlayPause => {
|
SpircCommand::PlayPause => {
|
||||||
if active {
|
if active {
|
||||||
self.handle_play_pause();
|
self.handle_play_pause();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
} else {
|
} else {
|
||||||
CommandSender::new(self, MessageType::kMessageTypePlayPause).send();
|
CommandSender::new(self, MessageType::kMessageTypePlayPause).send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpircCommand::Pause => {
|
SpircCommand::Pause => {
|
||||||
if active {
|
if active {
|
||||||
self.handle_pause();
|
self.handle_pause();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
} else {
|
} else {
|
||||||
CommandSender::new(self, MessageType::kMessageTypePause).send();
|
CommandSender::new(self, MessageType::kMessageTypePause).send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpircCommand::Prev => {
|
SpircCommand::Prev => {
|
||||||
if active {
|
if active {
|
||||||
self.handle_prev();
|
self.handle_prev();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
} else {
|
} else {
|
||||||
CommandSender::new(self, MessageType::kMessageTypePrev).send();
|
CommandSender::new(self, MessageType::kMessageTypePrev).send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpircCommand::Next => {
|
SpircCommand::Next => {
|
||||||
if active {
|
if active {
|
||||||
self.handle_next();
|
self.handle_next();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
} else {
|
} else {
|
||||||
CommandSender::new(self, MessageType::kMessageTypeNext).send();
|
CommandSender::new(self, MessageType::kMessageTypeNext).send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpircCommand::VolumeUp => {
|
SpircCommand::VolumeUp => {
|
||||||
if active {
|
if active {
|
||||||
self.handle_volume_up();
|
self.handle_volume_up();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
} else {
|
} else {
|
||||||
CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send();
|
CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpircCommand::VolumeDown => {
|
SpircCommand::VolumeDown => {
|
||||||
if active {
|
if active {
|
||||||
self.handle_volume_down();
|
self.handle_volume_down();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
} else {
|
} else {
|
||||||
CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send();
|
CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpircCommand::Shutdown => {
|
SpircCommand::Shutdown => {
|
||||||
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send();
|
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?;
|
||||||
self.shutdown = true;
|
self.shutdown = true;
|
||||||
if let Some(rx) = self.commands.as_mut() {
|
if let Some(rx) = self.commands.as_mut() {
|
||||||
rx.close()
|
rx.close()
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
SpircCommand::Shuffle => {
|
SpircCommand::Shuffle => {
|
||||||
CommandSender::new(self, MessageType::kMessageTypeShuffle).send();
|
CommandSender::new(self, MessageType::kMessageTypeShuffle).send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_player_event(&mut self, event: PlayerEvent) {
|
fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> {
|
||||||
// we only process events if the play_request_id matches. If it doesn't, it is
|
// we only process events if the play_request_id matches. If it doesn't, it is
|
||||||
// an event that belongs to a previous track and only arrives now due to a race
|
// an event that belongs to a previous track and only arrives now due to a race
|
||||||
// condition. In this case we have updated the state already and don't want to
|
// condition. In this case we have updated the state already and don't want to
|
||||||
|
@ -571,6 +628,7 @@ impl SpircTask {
|
||||||
PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(),
|
PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(),
|
||||||
PlayerEvent::Loading { .. } => self.notify(None, false),
|
PlayerEvent::Loading { .. } => self.notify(None, false),
|
||||||
PlayerEvent::Playing { position_ms, .. } => {
|
PlayerEvent::Playing { position_ms, .. } => {
|
||||||
|
trace!("==> kPlayStatusPlay");
|
||||||
let new_nominal_start_time = self.now_ms() - position_ms as i64;
|
let new_nominal_start_time = self.now_ms() - position_ms as i64;
|
||||||
match self.play_status {
|
match self.play_status {
|
||||||
SpircPlayStatus::Playing {
|
SpircPlayStatus::Playing {
|
||||||
|
@ -580,27 +638,29 @@ impl SpircTask {
|
||||||
if (*nominal_start_time - new_nominal_start_time).abs() > 100 {
|
if (*nominal_start_time - new_nominal_start_time).abs() > 100 {
|
||||||
*nominal_start_time = new_nominal_start_time;
|
*nominal_start_time = new_nominal_start_time;
|
||||||
self.update_state_position(position_ms);
|
self.update_state_position(position_ms);
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpircPlayStatus::LoadingPlay { .. }
|
SpircPlayStatus::LoadingPlay { .. }
|
||||||
| SpircPlayStatus::LoadingPause { .. } => {
|
| SpircPlayStatus::LoadingPause { .. } => {
|
||||||
self.state.set_status(PlayStatus::kPlayStatusPlay);
|
self.state.set_status(PlayStatus::kPlayStatusPlay);
|
||||||
self.update_state_position(position_ms);
|
self.update_state_position(position_ms);
|
||||||
self.notify(None, true);
|
|
||||||
self.play_status = SpircPlayStatus::Playing {
|
self.play_status = SpircPlayStatus::Playing {
|
||||||
nominal_start_time: new_nominal_start_time,
|
nominal_start_time: new_nominal_start_time,
|
||||||
preloading_of_next_track_triggered: false,
|
preloading_of_next_track_triggered: false,
|
||||||
};
|
};
|
||||||
|
self.notify(None, true)
|
||||||
|
}
|
||||||
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
trace!("==> kPlayStatusPlay");
|
|
||||||
}
|
}
|
||||||
PlayerEvent::Paused {
|
PlayerEvent::Paused {
|
||||||
position_ms: new_position_ms,
|
position_ms: new_position_ms,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
trace!("==> kPlayStatusPause");
|
||||||
match self.play_status {
|
match self.play_status {
|
||||||
SpircPlayStatus::Paused {
|
SpircPlayStatus::Paused {
|
||||||
ref mut position_ms,
|
ref mut position_ms,
|
||||||
|
@ -609,37 +669,48 @@ impl SpircTask {
|
||||||
if *position_ms != new_position_ms {
|
if *position_ms != new_position_ms {
|
||||||
*position_ms = new_position_ms;
|
*position_ms = new_position_ms;
|
||||||
self.update_state_position(new_position_ms);
|
self.update_state_position(new_position_ms);
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpircPlayStatus::LoadingPlay { .. }
|
SpircPlayStatus::LoadingPlay { .. }
|
||||||
| SpircPlayStatus::LoadingPause { .. } => {
|
| SpircPlayStatus::LoadingPause { .. } => {
|
||||||
self.state.set_status(PlayStatus::kPlayStatusPause);
|
self.state.set_status(PlayStatus::kPlayStatusPause);
|
||||||
self.update_state_position(new_position_ms);
|
self.update_state_position(new_position_ms);
|
||||||
self.notify(None, true);
|
|
||||||
self.play_status = SpircPlayStatus::Paused {
|
self.play_status = SpircPlayStatus::Paused {
|
||||||
position_ms: new_position_ms,
|
position_ms: new_position_ms,
|
||||||
preloading_of_next_track_triggered: false,
|
preloading_of_next_track_triggered: false,
|
||||||
};
|
};
|
||||||
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
trace!("==> kPlayStatusPause");
|
|
||||||
}
|
}
|
||||||
PlayerEvent::Stopped { .. } => match self.play_status {
|
PlayerEvent::Stopped { .. } => match self.play_status {
|
||||||
SpircPlayStatus::Stopped => (),
|
SpircPlayStatus::Stopped => Ok(()),
|
||||||
_ => {
|
_ => {
|
||||||
warn!("The player has stopped unexpectedly.");
|
warn!("The player has stopped unexpectedly.");
|
||||||
self.state.set_status(PlayStatus::kPlayStatusStop);
|
self.state.set_status(PlayStatus::kPlayStatusStop);
|
||||||
self.notify(None, true);
|
|
||||||
self.play_status = SpircPlayStatus::Stopped;
|
self.play_status = SpircPlayStatus::Stopped;
|
||||||
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PlayerEvent::TimeToPreloadNextTrack { .. } => self.handle_preload_next_track(),
|
PlayerEvent::TimeToPreloadNextTrack { .. } => {
|
||||||
PlayerEvent::Unavailable { track_id, .. } => self.handle_unavailable(track_id),
|
self.handle_preload_next_track();
|
||||||
_ => (),
|
Ok(())
|
||||||
}
|
}
|
||||||
|
PlayerEvent::Unavailable { track_id, .. } => {
|
||||||
|
self.handle_unavailable(track_id);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -655,7 +726,7 @@ impl SpircTask {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|pair| (pair.get_key().to_owned(), pair.get_value().to_owned()))
|
.map(|pair| (pair.get_key().to_owned(), pair.get_value().to_owned()))
|
||||||
.collect();
|
.collect();
|
||||||
let _ = self.session.set_user_attributes(attributes);
|
self.session.set_user_attributes(attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) {
|
fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) {
|
||||||
|
@ -683,8 +754,8 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_frame(&mut self, frame: Frame) {
|
fn handle_remote_update(&mut self, update: Frame) -> Result<(), Error> {
|
||||||
let state_string = match frame.get_state().get_status() {
|
let state_string = match update.get_state().get_status() {
|
||||||
PlayStatus::kPlayStatusLoading => "kPlayStatusLoading",
|
PlayStatus::kPlayStatusLoading => "kPlayStatusLoading",
|
||||||
PlayStatus::kPlayStatusPause => "kPlayStatusPause",
|
PlayStatus::kPlayStatusPause => "kPlayStatusPause",
|
||||||
PlayStatus::kPlayStatusStop => "kPlayStatusStop",
|
PlayStatus::kPlayStatusStop => "kPlayStatusStop",
|
||||||
|
@ -693,24 +764,24 @@ impl SpircTask {
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"{:?} {:?} {} {} {} {}",
|
"{:?} {:?} {} {} {} {}",
|
||||||
frame.get_typ(),
|
update.get_typ(),
|
||||||
frame.get_device_state().get_name(),
|
update.get_device_state().get_name(),
|
||||||
frame.get_ident(),
|
update.get_ident(),
|
||||||
frame.get_seq_nr(),
|
update.get_seq_nr(),
|
||||||
frame.get_state_update_id(),
|
update.get_state_update_id(),
|
||||||
state_string,
|
state_string,
|
||||||
);
|
);
|
||||||
|
|
||||||
if frame.get_ident() == self.ident
|
let device_id = &self.ident;
|
||||||
|| (!frame.get_recipient().is_empty() && !frame.get_recipient().contains(&self.ident))
|
let ident = update.get_ident();
|
||||||
|
if ident == device_id
|
||||||
|
|| (!update.get_recipient().is_empty() && !update.get_recipient().contains(device_id))
|
||||||
{
|
{
|
||||||
return;
|
return Err(SpircError::Ident(ident.to_string()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
match frame.get_typ() {
|
match update.get_typ() {
|
||||||
MessageType::kMessageTypeHello => {
|
MessageType::kMessageTypeHello => self.notify(Some(ident), true),
|
||||||
self.notify(Some(frame.get_ident()), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageType::kMessageTypeLoad => {
|
MessageType::kMessageTypeLoad => {
|
||||||
if !self.device.get_is_active() {
|
if !self.device.get_is_active() {
|
||||||
|
@ -719,12 +790,12 @@ impl SpircTask {
|
||||||
self.device.set_became_active_at(now);
|
self.device.set_became_active_at(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_tracks(&frame);
|
self.update_tracks(&update);
|
||||||
|
|
||||||
if !self.state.get_track().is_empty() {
|
if !self.state.get_track().is_empty() {
|
||||||
let start_playing =
|
let start_playing =
|
||||||
frame.get_state().get_status() == PlayStatus::kPlayStatusPlay;
|
update.get_state().get_status() == PlayStatus::kPlayStatusPlay;
|
||||||
self.load_track(start_playing, frame.get_state().get_position_ms());
|
self.load_track(start_playing, update.get_state().get_position_ms());
|
||||||
} else {
|
} else {
|
||||||
info!("No more tracks left in queue");
|
info!("No more tracks left in queue");
|
||||||
self.state.set_status(PlayStatus::kPlayStatusStop);
|
self.state.set_status(PlayStatus::kPlayStatusStop);
|
||||||
|
@ -732,51 +803,51 @@ impl SpircTask {
|
||||||
self.play_status = SpircPlayStatus::Stopped;
|
self.play_status = SpircPlayStatus::Stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypePlay => {
|
MessageType::kMessageTypePlay => {
|
||||||
self.handle_play();
|
self.handle_play();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypePlayPause => {
|
MessageType::kMessageTypePlayPause => {
|
||||||
self.handle_play_pause();
|
self.handle_play_pause();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypePause => {
|
MessageType::kMessageTypePause => {
|
||||||
self.handle_pause();
|
self.handle_pause();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeNext => {
|
MessageType::kMessageTypeNext => {
|
||||||
self.handle_next();
|
self.handle_next();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypePrev => {
|
MessageType::kMessageTypePrev => {
|
||||||
self.handle_prev();
|
self.handle_prev();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeVolumeUp => {
|
MessageType::kMessageTypeVolumeUp => {
|
||||||
self.handle_volume_up();
|
self.handle_volume_up();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeVolumeDown => {
|
MessageType::kMessageTypeVolumeDown => {
|
||||||
self.handle_volume_down();
|
self.handle_volume_down();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeRepeat => {
|
MessageType::kMessageTypeRepeat => {
|
||||||
self.state.set_repeat(frame.get_state().get_repeat());
|
self.state.set_repeat(update.get_state().get_repeat());
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeShuffle => {
|
MessageType::kMessageTypeShuffle => {
|
||||||
self.state.set_shuffle(frame.get_state().get_shuffle());
|
self.state.set_shuffle(update.get_state().get_shuffle());
|
||||||
if self.state.get_shuffle() {
|
if self.state.get_shuffle() {
|
||||||
let current_index = self.state.get_playing_track_index();
|
let current_index = self.state.get_playing_track_index();
|
||||||
{
|
{
|
||||||
|
@ -792,17 +863,17 @@ impl SpircTask {
|
||||||
let context = self.state.get_context_uri();
|
let context = self.state.get_context_uri();
|
||||||
debug!("{:?}", context);
|
debug!("{:?}", context);
|
||||||
}
|
}
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeSeek => {
|
MessageType::kMessageTypeSeek => {
|
||||||
self.handle_seek(frame.get_position());
|
self.handle_seek(update.get_position());
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeReplace => {
|
MessageType::kMessageTypeReplace => {
|
||||||
self.update_tracks(&frame);
|
self.update_tracks(&update);
|
||||||
self.notify(None, true);
|
self.notify(None, true)?;
|
||||||
|
|
||||||
if let SpircPlayStatus::Playing {
|
if let SpircPlayStatus::Playing {
|
||||||
preloading_of_next_track_triggered,
|
preloading_of_next_track_triggered,
|
||||||
|
@ -820,27 +891,29 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeVolume => {
|
MessageType::kMessageTypeVolume => {
|
||||||
self.set_volume(frame.get_volume() as u16);
|
self.set_volume(update.get_volume() as u16);
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeNotify => {
|
MessageType::kMessageTypeNotify => {
|
||||||
if self.device.get_is_active()
|
if self.device.get_is_active()
|
||||||
&& frame.get_device_state().get_is_active()
|
&& update.get_device_state().get_is_active()
|
||||||
&& self.device.get_became_active_at()
|
&& self.device.get_became_active_at()
|
||||||
<= frame.get_device_state().get_became_active_at()
|
<= update.get_device_state().get_became_active_at()
|
||||||
{
|
{
|
||||||
self.device.set_is_active(false);
|
self.device.set_is_active(false);
|
||||||
self.state.set_status(PlayStatus::kPlayStatusStop);
|
self.state.set_status(PlayStatus::kPlayStatusStop);
|
||||||
self.player.stop();
|
self.player.stop();
|
||||||
self.play_status = SpircPlayStatus::Stopped;
|
self.play_status = SpircPlayStatus::Stopped;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => (),
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -850,6 +923,7 @@ impl SpircTask {
|
||||||
position_ms,
|
position_ms,
|
||||||
preloading_of_next_track_triggered,
|
preloading_of_next_track_triggered,
|
||||||
} => {
|
} => {
|
||||||
|
// TODO - also apply this to the arm below
|
||||||
// Synchronize the volume from the mixer. This is useful on
|
// Synchronize the volume from the mixer. This is useful on
|
||||||
// systems that can switch sources from and back to librespot.
|
// systems that can switch sources from and back to librespot.
|
||||||
let current_volume = self.mixer.volume();
|
let current_volume = self.mixer.volume();
|
||||||
|
@ -864,6 +938,8 @@ impl SpircTask {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
SpircPlayStatus::LoadingPause { position_ms } => {
|
SpircPlayStatus::LoadingPause { position_ms } => {
|
||||||
|
// TODO - fix "Player::play called from invalid state" when hitting play
|
||||||
|
// on initial start-up, when starting halfway a track
|
||||||
self.player.play();
|
self.player.play();
|
||||||
self.play_status = SpircPlayStatus::LoadingPlay { position_ms };
|
self.play_status = SpircPlayStatus::LoadingPlay { position_ms };
|
||||||
}
|
}
|
||||||
|
@ -1090,9 +1166,9 @@ impl SpircTask {
|
||||||
self.set_volume(volume);
|
self.set_volume(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_end_of_track(&mut self) {
|
fn handle_end_of_track(&mut self) -> Result<(), Error> {
|
||||||
self.handle_next();
|
self.handle_next();
|
||||||
self.notify(None, true);
|
self.notify(None, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn position(&mut self) -> u32 {
|
fn position(&mut self) -> u32 {
|
||||||
|
@ -1107,48 +1183,40 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_station(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, MercuryError>> {
|
fn resolve_station(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, Error>> {
|
||||||
let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri);
|
let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri);
|
||||||
|
|
||||||
self.resolve_uri(&radio_uri)
|
self.resolve_uri(&radio_uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture<Result<String, MercuryError>> {
|
fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture<Result<String, Error>> {
|
||||||
let query_uri = format!("hm://autoplay-enabled/query?uri={}", uri);
|
let query_uri = format!("hm://autoplay-enabled/query?uri={}", uri);
|
||||||
let request = self.session.mercury().get(query_uri);
|
let request = self.session.mercury().get(query_uri);
|
||||||
Box::pin(
|
Box::pin(
|
||||||
async {
|
async {
|
||||||
let response = request.await?;
|
let response = request?.await?;
|
||||||
|
|
||||||
if response.status_code == 200 {
|
if response.status_code == 200 {
|
||||||
let data = response
|
let data = response.payload.first().ok_or(SpircError::NoData)?.to_vec();
|
||||||
.payload
|
Ok(String::from_utf8(data)?)
|
||||||
.first()
|
|
||||||
.expect("Empty autoplay uri")
|
|
||||||
.to_vec();
|
|
||||||
let autoplay_uri = String::from_utf8(data).unwrap();
|
|
||||||
Ok(autoplay_uri)
|
|
||||||
} else {
|
} else {
|
||||||
warn!("No autoplay_uri found");
|
warn!("No autoplay_uri found");
|
||||||
Err(MercuryError)
|
Err(MercuryError::Response(response).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fuse(),
|
.fuse(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_uri(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, MercuryError>> {
|
fn resolve_uri(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, Error>> {
|
||||||
let request = self.session.mercury().get(uri);
|
let request = self.session.mercury().get(uri);
|
||||||
|
|
||||||
Box::pin(
|
Box::pin(
|
||||||
async move {
|
async move {
|
||||||
let response = request.await?;
|
let response = request?.await?;
|
||||||
|
|
||||||
let data = response
|
let data = response.payload.first().ok_or(SpircError::NoData)?;
|
||||||
.payload
|
let response: serde_json::Value = serde_json::from_slice(data)?;
|
||||||
.first()
|
|
||||||
.expect("Empty payload on context uri");
|
|
||||||
let response: serde_json::Value = serde_json::from_slice(data).unwrap();
|
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
@ -1315,13 +1383,17 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hello(&mut self) {
|
fn hello(&mut self) -> Result<(), Error> {
|
||||||
CommandSender::new(self, MessageType::kMessageTypeHello).send();
|
CommandSender::new(self, MessageType::kMessageTypeHello).send()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify(&mut self, recipient: Option<&str>, suppress_loading_status: bool) {
|
fn notify(
|
||||||
|
&mut self,
|
||||||
|
recipient: Option<&str>,
|
||||||
|
suppress_loading_status: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
if suppress_loading_status && (self.state.get_status() == PlayStatus::kPlayStatusLoading) {
|
if suppress_loading_status && (self.state.get_status() == PlayStatus::kPlayStatusLoading) {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
let status_string = match self.state.get_status() {
|
let status_string = match self.state.get_status() {
|
||||||
PlayStatus::kPlayStatusLoading => "kPlayStatusLoading",
|
PlayStatus::kPlayStatusLoading => "kPlayStatusLoading",
|
||||||
|
@ -1334,7 +1406,7 @@ impl SpircTask {
|
||||||
if let Some(s) = recipient {
|
if let Some(s) = recipient {
|
||||||
cs = cs.recipient(s);
|
cs = cs.recipient(s);
|
||||||
}
|
}
|
||||||
cs.send();
|
cs.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_volume(&mut self, volume: u16) {
|
fn set_volume(&mut self, volume: u16) {
|
||||||
|
@ -1382,11 +1454,11 @@ impl<'a> CommandSender<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send(mut self) {
|
fn send(mut self) -> Result<(), Error> {
|
||||||
if !self.frame.has_state() && self.spirc.device.get_is_active() {
|
if !self.frame.has_state() && self.spirc.device.get_is_active() {
|
||||||
self.frame.set_state(self.spirc.state.clone());
|
self.frame.set_state(self.spirc.state.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.spirc.sender.send(self.frame.write_to_bytes().unwrap());
|
self.spirc.sender.send(self.frame.write_to_bytes()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
use hyper::{Body, Method, Request};
|
use hyper::{Body, Method, Request};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::error::Error;
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use crate::Error;
|
||||||
|
|
||||||
pub type SocketAddress = (String, u16);
|
pub type SocketAddress = (String, u16);
|
||||||
|
|
||||||
|
@ -67,7 +69,7 @@ impl ApResolver {
|
||||||
.collect()
|
.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()
|
let req = Request::builder()
|
||||||
.method(Method::GET)
|
.method(Method::GET)
|
||||||
.uri("http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient")
|
.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 byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use std::collections::HashMap;
|
use thiserror::Error;
|
||||||
use std::io::Write;
|
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
use crate::file_id::FileId;
|
use crate::{packet::PacketType, util::SeqGenerator, Error, FileId, SpotifyId};
|
||||||
use crate::packet::PacketType;
|
|
||||||
use crate::spotify_id::SpotifyId;
|
|
||||||
use crate::util::SeqGenerator;
|
|
||||||
|
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
|
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
|
||||||
pub struct AudioKey(pub [u8; 16]);
|
pub struct AudioKey(pub [u8; 16]);
|
||||||
|
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
|
#[derive(Debug, Error)]
|
||||||
pub struct AudioKeyError;
|
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! {
|
component! {
|
||||||
AudioKeyManager : AudioKeyManagerInner {
|
AudioKeyManager : AudioKeyManagerInner {
|
||||||
sequence: SeqGenerator<u32> = SeqGenerator::new(0),
|
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 {
|
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 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 {
|
match cmd {
|
||||||
PacketType::AesKey => {
|
PacketType::AesKey => {
|
||||||
let mut key = [0u8; 16];
|
let mut key = [0u8; 16];
|
||||||
key.copy_from_slice(data.as_ref());
|
key.copy_from_slice(data.as_ref());
|
||||||
let _ = sender.send(Ok(AudioKey(key)));
|
sender
|
||||||
|
.send(Ok(AudioKey(key)))
|
||||||
|
.map_err(|_| AudioKeyError::Channel)?
|
||||||
}
|
}
|
||||||
PacketType::AesKeyError => {
|
PacketType::AesKeyError => {
|
||||||
warn!(
|
error!(
|
||||||
"error audio key {:x} {:x}",
|
"error audio key {:x} {:x}",
|
||||||
data.as_ref()[0],
|
data.as_ref()[0],
|
||||||
data.as_ref()[1]
|
data.as_ref()[1]
|
||||||
);
|
);
|
||||||
let _ = sender.send(Err(AudioKeyError));
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, AudioKeyError> {
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, Error> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
let seq = self.lock(move |inner| {
|
let seq = self.lock(move |inner| {
|
||||||
|
@ -57,16 +88,16 @@ impl AudioKeyManager {
|
||||||
seq
|
seq
|
||||||
});
|
});
|
||||||
|
|
||||||
self.send_key_request(seq, track, file);
|
self.send_key_request(seq, track, file)?;
|
||||||
rx.await.map_err(|_| AudioKeyError)?
|
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();
|
let mut data: Vec<u8> = Vec::new();
|
||||||
data.write_all(&file.0).unwrap();
|
data.write_all(&file.0)?;
|
||||||
data.write_all(&track.to_raw()).unwrap();
|
data.write_all(&track.to_raw())?;
|
||||||
data.write_u32::<BigEndian>(seq).unwrap();
|
data.write_u32::<BigEndian>(seq)?;
|
||||||
data.write_u16::<BigEndian>(0x0000).unwrap();
|
data.write_u16::<BigEndian>(0x0000)?;
|
||||||
|
|
||||||
self.session().send_packet(PacketType::RequestKey, data)
|
self.session().send_packet(PacketType::RequestKey, data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,21 @@ use pbkdf2::pbkdf2;
|
||||||
use protobuf::ProtobufEnum;
|
use protobuf::ProtobufEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha1::{Digest, Sha1};
|
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.
|
/// The credentials are used to log into the Spotify API.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
@ -46,7 +59,7 @@ impl Credentials {
|
||||||
username: impl Into<String>,
|
username: impl Into<String>,
|
||||||
encrypted_blob: impl AsRef<[u8]>,
|
encrypted_blob: impl AsRef<[u8]>,
|
||||||
device_id: impl AsRef<[u8]>,
|
device_id: impl AsRef<[u8]>,
|
||||||
) -> Credentials {
|
) -> Result<Credentials, Error> {
|
||||||
fn read_u8<R: Read>(stream: &mut R) -> io::Result<u8> {
|
fn read_u8<R: Read>(stream: &mut R) -> io::Result<u8> {
|
||||||
let mut data = [0u8];
|
let mut data = [0u8];
|
||||||
stream.read_exact(&mut data)?;
|
stream.read_exact(&mut data)?;
|
||||||
|
@ -91,7 +104,7 @@ impl Credentials {
|
||||||
use aes::cipher::generic_array::GenericArray;
|
use aes::cipher::generic_array::GenericArray;
|
||||||
use aes::cipher::{BlockCipher, NewBlockCipher};
|
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 cipher = Aes192::new(GenericArray::from_slice(&key));
|
||||||
let block_size = <Aes192 as BlockCipher>::BlockSize::to_usize();
|
let block_size = <Aes192 as BlockCipher>::BlockSize::to_usize();
|
||||||
|
|
||||||
|
@ -109,19 +122,20 @@ impl Credentials {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cursor = io::Cursor::new(blob.as_slice());
|
let mut cursor = io::Cursor::new(blob.as_slice());
|
||||||
read_u8(&mut cursor).unwrap();
|
read_u8(&mut cursor)?;
|
||||||
read_bytes(&mut cursor).unwrap();
|
read_bytes(&mut cursor)?;
|
||||||
read_u8(&mut cursor).unwrap();
|
read_u8(&mut cursor)?;
|
||||||
let auth_type = read_int(&mut cursor).unwrap();
|
let auth_type = read_int(&mut cursor)?;
|
||||||
let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap();
|
let auth_type = AuthenticationType::from_i32(auth_type as i32)
|
||||||
read_u8(&mut cursor).unwrap();
|
.ok_or(AuthenticationError::AuthType(auth_type))?;
|
||||||
let auth_data = read_bytes(&mut cursor).unwrap();
|
read_u8(&mut cursor)?;
|
||||||
|
let auth_data = read_bytes(&mut cursor)?;
|
||||||
|
|
||||||
Credentials {
|
Ok(Credentials {
|
||||||
username,
|
username,
|
||||||
auth_type,
|
auth_type,
|
||||||
auth_data,
|
auth_data,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
use std::cmp::Reverse;
|
use std::{
|
||||||
use std::collections::HashMap;
|
cmp::Reverse,
|
||||||
use std::fs::{self, File};
|
collections::HashMap,
|
||||||
use std::io::{self, Error, ErrorKind, Read, Write};
|
fs::{self, File},
|
||||||
use std::path::{Path, PathBuf};
|
io::{self, Read, Write},
|
||||||
use std::sync::{Arc, Mutex};
|
path::{Path, PathBuf},
|
||||||
use std::time::SystemTime;
|
sync::{Arc, Mutex},
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
|
|
||||||
use priority_queue::PriorityQueue;
|
use priority_queue::PriorityQueue;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::authentication::Credentials;
|
use crate::{authentication::Credentials, error::ErrorKind, Error, FileId};
|
||||||
use crate::file_id::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.
|
/// 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
|
/// 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.
|
/// to delete the file in the file system.
|
||||||
fn pop(&mut self) -> Option<PathBuf> {
|
fn pop(&mut self) -> Option<PathBuf> {
|
||||||
if self.exceeds_limit() {
|
if self.exceeds_limit() {
|
||||||
let (next, _) = self
|
if let Some((next, _)) = self.queue.pop() {
|
||||||
.queue
|
if let Some(size) = self.sizes.remove(&next) {
|
||||||
.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;
|
self.in_use -= size;
|
||||||
|
} else {
|
||||||
|
error!("`queue` and `sizes` should have the same keys.");
|
||||||
|
}
|
||||||
Some(next)
|
Some(next)
|
||||||
|
} else {
|
||||||
|
error!("in_use was > 0, so the queue should have contained an item.");
|
||||||
|
None
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -85,11 +100,11 @@ impl SizeLimiter {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let size = self
|
if let Some(size) = self.sizes.remove(file) {
|
||||||
.sizes
|
|
||||||
.remove(file)
|
|
||||||
.expect("`queue` and `sizes` should have the same keys.");
|
|
||||||
self.in_use -= size;
|
self.in_use -= size;
|
||||||
|
} else {
|
||||||
|
error!("`queue` and `sizes` should have the same keys.");
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -172,33 +187,40 @@ impl FsSizeLimiter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add(&self, file: &Path, size: u64) {
|
fn add(&self, file: &Path, size: u64) -> Result<(), Error> {
|
||||||
self.limiter
|
self.limiter
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.add(file, size, SystemTime::now());
|
.add(file, size, SystemTime::now());
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn touch(&self, file: &Path) -> bool {
|
fn touch(&self, file: &Path) -> Result<bool, Error> {
|
||||||
self.limiter.lock().unwrap().update(file, SystemTime::now())
|
Ok(self.limiter.lock().unwrap().update(file, SystemTime::now()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&self, file: &Path) {
|
fn remove(&self, file: &Path) -> Result<bool, Error> {
|
||||||
self.limiter.lock().unwrap().remove(file);
|
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 first = true;
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
let mut last_error = None;
|
||||||
|
|
||||||
while let Some(file) = pop() {
|
while let Ok(result) = pop() {
|
||||||
|
if let Some(file) = result {
|
||||||
if first {
|
if first {
|
||||||
debug!("Cache dir exceeds limit, removing least recently used files.");
|
debug!("Cache dir exceeds limit, removing least recently used files.");
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = fs::remove_file(&file) {
|
let res = fs::remove_file(&file);
|
||||||
|
if let Err(e) = res {
|
||||||
warn!("Could not remove file {:?} from cache dir: {}", file, e);
|
warn!("Could not remove file {:?} from cache dir: {}", file, e);
|
||||||
|
last_error = Some(e);
|
||||||
} else {
|
} else {
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
@ -209,19 +231,26 @@ impl FsSizeLimiter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prune(&self) {
|
if let Some(err) = last_error {
|
||||||
Self::prune_internal(|| self.limiter.lock().unwrap().pop())
|
Err(err.into())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(path: &Path, limit: u64) -> Self {
|
fn prune(&self) -> Result<(), Error> {
|
||||||
|
Self::prune_internal(|| Ok(self.limiter.lock().unwrap().pop()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(path: &Path, limit: u64) -> Result<Self, Error> {
|
||||||
let mut limiter = SizeLimiter::new(limit);
|
let mut limiter = SizeLimiter::new(limit);
|
||||||
|
|
||||||
Self::init_dir(&mut limiter, path);
|
Self::init_dir(&mut limiter, path);
|
||||||
Self::prune_internal(|| limiter.pop());
|
Self::prune_internal(|| Ok(limiter.pop()))?;
|
||||||
|
|
||||||
Self {
|
Ok(Self {
|
||||||
limiter: Mutex::new(limiter),
|
limiter: Mutex::new(limiter),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,15 +263,13 @@ pub struct Cache {
|
||||||
size_limiter: Option<Arc<FsSizeLimiter>>,
|
size_limiter: Option<Arc<FsSizeLimiter>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RemoveFileError(());
|
|
||||||
|
|
||||||
impl Cache {
|
impl Cache {
|
||||||
pub fn new<P: AsRef<Path>>(
|
pub fn new<P: AsRef<Path>>(
|
||||||
credentials_path: Option<P>,
|
credentials_path: Option<P>,
|
||||||
volume_path: Option<P>,
|
volume_path: Option<P>,
|
||||||
audio_path: Option<P>,
|
audio_path: Option<P>,
|
||||||
size_limit: Option<u64>,
|
size_limit: Option<u64>,
|
||||||
) -> io::Result<Self> {
|
) -> Result<Self, Error> {
|
||||||
let mut size_limiter = None;
|
let mut size_limiter = None;
|
||||||
|
|
||||||
if let Some(location) = &credentials_path {
|
if let Some(location) = &credentials_path {
|
||||||
|
@ -263,8 +290,7 @@ impl Cache {
|
||||||
fs::create_dir_all(location)?;
|
fs::create_dir_all(location)?;
|
||||||
|
|
||||||
if let Some(limit) = size_limit {
|
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));
|
size_limiter = Some(Arc::new(limiter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -285,11 +311,11 @@ impl Cache {
|
||||||
let location = self.credentials_location.as_ref()?;
|
let location = self.credentials_location.as_ref()?;
|
||||||
|
|
||||||
// This closure is just convencience to enable the question mark operator
|
// 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 file = File::open(location)?;
|
||||||
let mut contents = String::new();
|
let mut contents = String::new();
|
||||||
file.read_to_string(&mut contents)?;
|
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() {
|
match read() {
|
||||||
|
@ -297,7 +323,7 @@ impl Cache {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// If the file did not exist, the file was probably not written
|
// If the file did not exist, the file was probably not written
|
||||||
// before. Otherwise, log the error.
|
// before. Otherwise, log the error.
|
||||||
if e.kind() != ErrorKind::NotFound {
|
if e.kind != ErrorKind::NotFound {
|
||||||
warn!("Error reading credentials from cache: {}", e);
|
warn!("Error reading credentials from cache: {}", e);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
@ -321,19 +347,17 @@ impl Cache {
|
||||||
pub fn volume(&self) -> Option<u16> {
|
pub fn volume(&self) -> Option<u16> {
|
||||||
let location = self.volume_location.as_ref()?;
|
let location = self.volume_location.as_ref()?;
|
||||||
|
|
||||||
let read = || {
|
let read = || -> Result<u16, Error> {
|
||||||
let mut file = File::open(location)?;
|
let mut file = File::open(location)?;
|
||||||
let mut contents = String::new();
|
let mut contents = String::new();
|
||||||
file.read_to_string(&mut contents)?;
|
file.read_to_string(&mut contents)?;
|
||||||
contents
|
Ok(contents.parse()?)
|
||||||
.parse()
|
|
||||||
.map_err(|e| Error::new(ErrorKind::InvalidData, e))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match read() {
|
match read() {
|
||||||
Ok(v) => Some(v),
|
Ok(v) => Some(v),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.kind() != ErrorKind::NotFound {
|
if e.kind != ErrorKind::NotFound {
|
||||||
warn!("Error reading volume from cache: {}", e);
|
warn!("Error reading volume from cache: {}", e);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
@ -364,12 +388,14 @@ impl Cache {
|
||||||
match File::open(&path) {
|
match File::open(&path) {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
if let Some(limiter) = self.size_limiter.as_deref() {
|
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)
|
Some(file)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.kind() != ErrorKind::NotFound {
|
if e.kind() != io::ErrorKind::NotFound {
|
||||||
warn!("Error reading file from cache: {}", e)
|
warn!("Error reading file from cache: {}", e)
|
||||||
}
|
}
|
||||||
None
|
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(path) = self.file_path(file) {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
if let Ok(size) = fs::create_dir_all(parent)
|
if let Ok(size) = fs::create_dir_all(parent)
|
||||||
|
@ -385,30 +411,27 @@ impl Cache {
|
||||||
.and_then(|mut file| io::copy(contents, &mut file))
|
.and_then(|mut file| io::copy(contents, &mut file))
|
||||||
{
|
{
|
||||||
if let Some(limiter) = self.size_limiter.as_deref() {
|
if let Some(limiter) = self.size_limiter.as_deref() {
|
||||||
limiter.add(&path, size);
|
limiter.add(&path, size)?;
|
||||||
limiter.prune();
|
limiter.prune()?
|
||||||
}
|
}
|
||||||
return true;
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
Err(CacheError::Path.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_file(&self, file: FileId) -> Result<(), RemoveFileError> {
|
pub fn remove_file(&self, file: FileId) -> Result<(), Error> {
|
||||||
let path = self.file_path(file).ok_or(RemoveFileError(()))?;
|
let path = self.file_path(file).ok_or(CacheError::Path)?;
|
||||||
|
|
||||||
if let Err(err) = fs::remove_file(&path) {
|
fs::remove_file(&path)?;
|
||||||
warn!("Unable to remove file from cache: {}", err);
|
|
||||||
Err(RemoveFileError(()))
|
|
||||||
} else {
|
|
||||||
if let Some(limiter) = self.size_limiter.as_deref() {
|
if let Some(limiter) = self.size_limiter.as_deref() {
|
||||||
limiter.remove(&path);
|
limiter.remove(&path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
|
@ -1,34 +1,19 @@
|
||||||
|
use std::{
|
||||||
|
convert::{TryFrom, TryInto},
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use protobuf::{Message, ProtobufError};
|
use protobuf::Message;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use std::convert::{TryFrom, TryInto};
|
use super::{date::Date, Error, FileId, Session};
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
|
|
||||||
use super::date::Date;
|
|
||||||
use super::file_id::FileId;
|
|
||||||
use super::session::Session;
|
|
||||||
use super::spclient::SpClientError;
|
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage;
|
use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage;
|
||||||
use protocol::storage_resolve::StorageResolveResponse_Result;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MaybeExpiringUrl(pub String, pub Option<Date>);
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CdnUrl {
|
pub struct CdnUrl {
|
||||||
pub file_id: FileId,
|
pub file_id: FileId,
|
||||||
pub urls: MaybeExpiringUrls,
|
urls: MaybeExpiringUrls,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CdnUrl {
|
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 file_id = self.file_id;
|
||||||
let response = session.spclient().get_audio_urls(file_id).await?;
|
let response = session.spclient().get_audio_urls(file_id).await?;
|
||||||
let msg = CdnUrlMessage::parse_from_bytes(&response)?;
|
let msg = CdnUrlMessage::parse_from_bytes(&response)?;
|
||||||
|
@ -75,37 +77,26 @@ impl CdnUrl {
|
||||||
Ok(cdn_url)
|
Ok(cdn_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_url(&mut self) -> Result<&str, CdnUrlError> {
|
pub fn try_get_url(&self) -> Result<&str, Error> {
|
||||||
if self.urls.is_empty() {
|
|
||||||
return Err(CdnUrlError::Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
// prune expired URLs until the first one is current, or none are left
|
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
while !self.urls.is_empty() {
|
let url = self.urls.iter().find(|url| match url.1 {
|
||||||
let maybe_expiring = self.urls[0].1;
|
Some(expiry) => now < expiry.as_utc(),
|
||||||
if let Some(expiry) = maybe_expiring {
|
None => true,
|
||||||
if now < expiry.as_utc() {
|
});
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
self.urls.remove(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(cdn_url) = self.urls.first() {
|
if let Some(url) = url {
|
||||||
Ok(&cdn_url.0)
|
Ok(&url.0)
|
||||||
} else {
|
} else {
|
||||||
Err(CdnUrlError::Expired)
|
Err(CdnUrlError::Expired.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
|
impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
|
||||||
type Error = CdnUrlError;
|
type Error = crate::Error;
|
||||||
fn try_from(msg: CdnUrlMessage) -> Result<Self, Self::Error> {
|
fn try_from(msg: CdnUrlMessage) -> Result<Self, Self::Error> {
|
||||||
if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) {
|
if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) {
|
||||||
return Err(CdnUrlError::Parsing);
|
return Err(CdnUrlError::Storage.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_expiring = !msg.get_fileid().is_empty();
|
let is_expiring = !msg.get_fileid().is_empty();
|
||||||
|
@ -114,7 +105,7 @@ impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
|
||||||
.get_cdnurl()
|
.get_cdnurl()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cdn_url| {
|
.map(|cdn_url| {
|
||||||
let url = Url::parse(cdn_url).map_err(|_| CdnUrlError::Parsing)?;
|
let url = Url::parse(cdn_url)?;
|
||||||
|
|
||||||
if is_expiring {
|
if is_expiring {
|
||||||
let expiry_str = if let Some(token) = url
|
let expiry_str = if let Some(token) = url
|
||||||
|
@ -122,29 +113,47 @@ impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|(key, _value)| key == "__token__")
|
.find(|(key, _value)| key == "__token__")
|
||||||
{
|
{
|
||||||
let start = token.1.find("exp=").ok_or(CdnUrlError::Parsing)?;
|
if let Some(mut start) = token.1.find("exp=") {
|
||||||
let slice = &token.1[start + 4..];
|
start += 4;
|
||||||
let end = slice.find('~').ok_or(CdnUrlError::Parsing)?;
|
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])
|
String::from(&slice[..end])
|
||||||
|
} else {
|
||||||
|
String::from(slice)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
} else if let Some(query) = url.query() {
|
} else if let Some(query) = url.query() {
|
||||||
let mut items = query.split('_');
|
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 {
|
} else {
|
||||||
return Err(CdnUrlError::Parsing);
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut expiry: i64 = expiry_str.parse().map_err(|_| CdnUrlError::Parsing)?;
|
let mut expiry: i64 = expiry_str.parse()?;
|
||||||
|
|
||||||
expiry -= 5 * 60; // seconds
|
expiry -= 5 * 60; // seconds
|
||||||
|
|
||||||
Ok(MaybeExpiringUrl(
|
Ok(MaybeExpiringUrl(
|
||||||
cdn_url.to_owned(),
|
cdn_url.to_owned(),
|
||||||
Some(expiry.try_into().map_err(|_| CdnUrlError::Parsing)?),
|
Some(expiry.try_into()?),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(MaybeExpiringUrl(cdn_url.to_owned(), None))
|
Ok(MaybeExpiringUrl(cdn_url.to_owned(), None))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<MaybeExpiringUrl>, CdnUrlError>>()?;
|
.collect::<Result<Vec<MaybeExpiringUrl>, Error>>()?;
|
||||||
|
|
||||||
Ok(Self(result))
|
Ok(Self(result))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
use std::collections::HashMap;
|
use std::{
|
||||||
use std::pin::Pin;
|
collections::HashMap,
|
||||||
use std::task::{Context, Poll};
|
fmt,
|
||||||
use std::time::Instant;
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
use futures_util::lock::BiLock;
|
use futures_util::{lock::BiLock, ready, StreamExt};
|
||||||
use futures_util::{ready, StreamExt};
|
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
|
use thiserror::Error;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::packet::PacketType;
|
use crate::{packet::PacketType, util::SeqGenerator, Error};
|
||||||
use crate::util::SeqGenerator;
|
|
||||||
|
|
||||||
component! {
|
component! {
|
||||||
ChannelManager : ChannelManagerInner {
|
ChannelManager : ChannelManagerInner {
|
||||||
|
@ -27,9 +29,21 @@ component! {
|
||||||
|
|
||||||
const ONE_SECOND_IN_MS: usize = 1000;
|
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;
|
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 {
|
pub struct Channel {
|
||||||
receiver: mpsc::UnboundedReceiver<(u8, Bytes)>,
|
receiver: mpsc::UnboundedReceiver<(u8, Bytes)>,
|
||||||
state: ChannelState,
|
state: ChannelState,
|
||||||
|
@ -70,7 +84,7 @@ impl ChannelManager {
|
||||||
(seq, channel)
|
(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;
|
use std::collections::hash_map::Entry;
|
||||||
|
|
||||||
let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref());
|
let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref());
|
||||||
|
@ -94,9 +108,14 @@ impl ChannelManager {
|
||||||
inner.download_measurement_bytes += data.len();
|
inner.download_measurement_bytes += data.len();
|
||||||
|
|
||||||
if let Entry::Occupied(entry) = inner.channels.entry(id) {
|
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 {
|
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>> {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
loop {
|
loop {
|
||||||
match self.state.clone() {
|
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) => {
|
ChannelState::Header(mut data) => {
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
data = ready!(self.recv_packet(cx))?;
|
data = ready!(self.recv_packet(cx))?;
|
||||||
|
|
|
@ -14,7 +14,7 @@ macro_rules! component {
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn lock<F: FnOnce(&mut $inner) -> R, R>(&self, f: F) -> R {
|
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)
|
f(&mut inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use std::fmt;
|
use std::{fmt, path::PathBuf, str::FromStr};
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
use bytes::{BufMut, Bytes, BytesMut};
|
use bytes::{BufMut, Bytes, BytesMut};
|
||||||
use shannon::Shannon;
|
use shannon::Shannon;
|
||||||
use std::io;
|
use thiserror::Error;
|
||||||
use tokio_util::codec::{Decoder, Encoder};
|
use tokio_util::codec::{Decoder, Encoder};
|
||||||
|
|
||||||
const HEADER_SIZE: usize = 3;
|
const HEADER_SIZE: usize = 3;
|
||||||
const MAC_SIZE: usize = 4;
|
const MAC_SIZE: usize = 4;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ApCodecError {
|
||||||
|
#[error("payload was malformed")]
|
||||||
|
Payload,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum DecodeState {
|
enum DecodeState {
|
||||||
Header,
|
Header,
|
||||||
|
@ -87,7 +95,10 @@ impl Decoder for ApCodec {
|
||||||
|
|
||||||
let mut payload = buf.split_to(size + MAC_SIZE);
|
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);
|
let mac = payload.split_off(size);
|
||||||
self.decode_cipher.check_mac(mac.as_ref())?;
|
self.decode_cipher.check_mac(mac.as_ref())?;
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
|
use std::{env::consts::ARCH, io};
|
||||||
|
|
||||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||||
use hmac::{Hmac, Mac, NewMac};
|
use hmac::{Hmac, Mac, NewMac};
|
||||||
use protobuf::{self, Message};
|
use protobuf::{self, Message};
|
||||||
use rand::{thread_rng, RngCore};
|
use rand::{thread_rng, RngCore};
|
||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
use std::env::consts::ARCH;
|
use thiserror::Error;
|
||||||
use std::io;
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
use tokio_util::codec::{Decoder, Framed};
|
use tokio_util::codec::{Decoder, Framed};
|
||||||
|
|
||||||
use super::codec::ApCodec;
|
use super::codec::ApCodec;
|
||||||
use crate::diffie_hellman::DhLocalKeys;
|
|
||||||
|
use crate::{diffie_hellman::DhLocalKeys, version};
|
||||||
|
|
||||||
use crate::protocol;
|
use crate::protocol;
|
||||||
use crate::protocol::keyexchange::{
|
use crate::protocol::keyexchange::{
|
||||||
APResponseMessage, ClientHello, ClientResponsePlaintext, Platform, ProductFlags,
|
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>(
|
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
||||||
mut connection: T,
|
mut connection: T,
|
||||||
|
@ -31,7 +39,7 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
let shared_secret = local_keys.shared_secret(&remote_key);
|
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);
|
let codec = ApCodec::new(&send_key, &recv_key);
|
||||||
|
|
||||||
client_response(&mut connection, challenge).await?;
|
client_response(&mut connection, challenge).await?;
|
||||||
|
@ -112,8 +120,8 @@ where
|
||||||
|
|
||||||
let mut buffer = vec![0, 4];
|
let mut buffer = vec![0, 4];
|
||||||
let size = 2 + 4 + packet.compute_size();
|
let size = 2 + 4 + packet.compute_size();
|
||||||
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap();
|
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?;
|
||||||
packet.write_to_vec(&mut buffer).unwrap();
|
packet.write_to_vec(&mut buffer)?;
|
||||||
|
|
||||||
connection.write_all(&buffer[..]).await?;
|
connection.write_all(&buffer[..]).await?;
|
||||||
Ok(buffer)
|
Ok(buffer)
|
||||||
|
@ -133,8 +141,8 @@ where
|
||||||
|
|
||||||
let mut buffer = vec![];
|
let mut buffer = vec![];
|
||||||
let size = 4 + packet.compute_size();
|
let size = 4 + packet.compute_size();
|
||||||
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap();
|
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?;
|
||||||
packet.write_to_vec(&mut buffer).unwrap();
|
packet.write_to_vec(&mut buffer)?;
|
||||||
|
|
||||||
connection.write_all(&buffer[..]).await?;
|
connection.write_all(&buffer[..]).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -148,7 +156,7 @@ where
|
||||||
let header = read_into_accumulator(connection, 4, acc).await?;
|
let header = read_into_accumulator(connection, 4, acc).await?;
|
||||||
let size = BigEndian::read_u32(header) as usize;
|
let size = BigEndian::read_u32(header) as usize;
|
||||||
let data = read_into_accumulator(connection, size - 4, acc).await?;
|
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)
|
Ok(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,24 +172,26 @@ async fn read_into_accumulator<'a, 'b, T: AsyncRead + Unpin>(
|
||||||
Ok(&mut acc[offset..])
|
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>;
|
type HmacSha1 = Hmac<Sha1>;
|
||||||
|
|
||||||
let mut data = Vec::with_capacity(0x64);
|
let mut data = Vec::with_capacity(0x64);
|
||||||
for i in 1..6 {
|
for i in 1..6 {
|
||||||
let mut mac =
|
let mut mac = HmacSha1::new_from_slice(shared_secret).map_err(|_| {
|
||||||
HmacSha1::new_from_slice(shared_secret).expect("HMAC can take key of any size");
|
io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength)
|
||||||
|
})?;
|
||||||
mac.update(packets);
|
mac.update(packets);
|
||||||
mac.update(&[i]);
|
mac.update(&[i]);
|
||||||
data.extend_from_slice(&mac.finalize().into_bytes());
|
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);
|
mac.update(packets);
|
||||||
|
|
||||||
(
|
Ok((
|
||||||
mac.finalize().into_bytes().to_vec(),
|
mac.finalize().into_bytes().to_vec(),
|
||||||
data[0x14..0x34].to_vec(),
|
data[0x14..0x34].to_vec(),
|
||||||
data[0x34..0x54].to_vec(),
|
data[0x34..0x54].to_vec(),
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
mod codec;
|
mod codec;
|
||||||
mod handshake;
|
mod handshake;
|
||||||
|
|
||||||
pub use self::codec::ApCodec;
|
pub use self::{codec::ApCodec, handshake::handshake};
|
||||||
pub use self::handshake::handshake;
|
|
||||||
|
|
||||||
use std::io::{self, ErrorKind};
|
use std::io;
|
||||||
|
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use protobuf::{self, Message, ProtobufError};
|
use protobuf::{self, Message};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio_util::codec::Framed;
|
use tokio_util::codec::Framed;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::authentication::Credentials;
|
use crate::{authentication::Credentials, packet::PacketType, version, Error};
|
||||||
use crate::packet::PacketType;
|
|
||||||
use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
|
use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
|
||||||
use crate::version;
|
|
||||||
|
|
||||||
pub type Transport = Framed<TcpStream, ApCodec>;
|
pub type Transport = Framed<TcpStream, ApCodec>;
|
||||||
|
|
||||||
|
@ -42,13 +40,19 @@ fn login_error_message(code: &ErrorCode) -> &'static str {
|
||||||
pub enum AuthenticationError {
|
pub enum AuthenticationError {
|
||||||
#[error("Login failed with reason: {}", login_error_message(.0))]
|
#[error("Login failed with reason: {}", login_error_message(.0))]
|
||||||
LoginFailed(ErrorCode),
|
LoginFailed(ErrorCode),
|
||||||
#[error("Authentication failed: {0}")]
|
#[error("invalid packet {0}")]
|
||||||
IoError(#[from] io::Error),
|
Packet(u8),
|
||||||
|
#[error("transport returned no data")]
|
||||||
|
Transport,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ProtobufError> for AuthenticationError {
|
impl From<AuthenticationError> for Error {
|
||||||
fn from(e: ProtobufError) -> Self {
|
fn from(err: AuthenticationError) -> Self {
|
||||||
io::Error::new(ErrorKind::InvalidData, e).into()
|
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,
|
transport: &mut Transport,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
device_id: &str,
|
device_id: &str,
|
||||||
) -> Result<Credentials, AuthenticationError> {
|
) -> Result<Credentials, Error> {
|
||||||
use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
|
use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
|
||||||
|
|
||||||
let cpu_family = match std::env::consts::ARCH {
|
let cpu_family = match std::env::consts::ARCH {
|
||||||
|
@ -119,12 +123,15 @@ pub async fn authenticate(
|
||||||
packet.set_version_string(format!("librespot {}", version::SEMVER));
|
packet.set_version_string(format!("librespot {}", version::SEMVER));
|
||||||
|
|
||||||
let cmd = PacketType::Login;
|
let cmd = PacketType::Login;
|
||||||
let data = packet.write_to_bytes().unwrap();
|
let data = packet.write_to_bytes()?;
|
||||||
|
|
||||||
transport.send((cmd as u8, data)).await?;
|
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);
|
let packet_type = FromPrimitive::from_u8(cmd);
|
||||||
match packet_type {
|
let result = match packet_type {
|
||||||
Some(PacketType::APWelcome) => {
|
Some(PacketType::APWelcome) => {
|
||||||
let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?;
|
let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?;
|
||||||
|
|
||||||
|
@ -141,8 +148,13 @@ pub async fn authenticate(
|
||||||
Err(error_data.into())
|
Err(error_data.into())
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let msg = format!("Received invalid packet: {}", cmd);
|
trace!(
|
||||||
Err(io::Error::new(ErrorKind::InvalidData, msg).into())
|
"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::{convert::TryFrom, fmt::Debug, ops::Deref};
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
||||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
use protocol::metadata::Date as DateMessage;
|
use protocol::metadata::Date as DateMessage;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum DateError {
|
pub enum DateError {
|
||||||
#[error("item has invalid date")]
|
#[error("item has invalid timestamp {0}")]
|
||||||
InvalidTimestamp,
|
Timestamp(i64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DateError> for Error {
|
||||||
|
fn from(err: DateError) -> Self {
|
||||||
|
Error::invalid_argument(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
@ -30,11 +35,11 @@ impl Date {
|
||||||
self.0.timestamp()
|
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) {
|
if let Some(date_time) = NaiveDateTime::from_timestamp_opt(timestamp, 0) {
|
||||||
Ok(Self::from_utc(date_time))
|
Ok(Self::from_utc(date_time))
|
||||||
} else {
|
} else {
|
||||||
Err(DateError::InvalidTimestamp)
|
Err(DateError::Timestamp(timestamp).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +72,7 @@ impl From<DateTime<Utc>> for Date {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<i64> for Date {
|
impl TryFrom<i64> for Date {
|
||||||
type Error = DateError;
|
type Error = crate::Error;
|
||||||
fn try_from(timestamp: i64) -> Result<Self, Self::Error> {
|
fn try_from(timestamp: i64) -> Result<Self, Self::Error> {
|
||||||
Self::from_timestamp(timestamp)
|
Self::from_timestamp(timestamp)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug)]
|
use thiserror::Error;
|
||||||
pub struct AlreadyHandledError(());
|
|
||||||
|
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> {
|
pub enum HandlerMap<T> {
|
||||||
Leaf(T),
|
Leaf(T),
|
||||||
|
@ -19,9 +32,9 @@ impl<T> HandlerMap<T> {
|
||||||
&mut self,
|
&mut self,
|
||||||
mut path: impl Iterator<Item = &'a str>,
|
mut path: impl Iterator<Item = &'a str>,
|
||||||
handler: T,
|
handler: T,
|
||||||
) -> Result<(), AlreadyHandledError> {
|
) -> Result<(), Error> {
|
||||||
match self {
|
match self {
|
||||||
Self::Leaf(_) => Err(AlreadyHandledError(())),
|
Self::Leaf(_) => Err(HandlerMapError::AlreadyHandled.into()),
|
||||||
Self::Branch(children) => {
|
Self::Branch(children) => {
|
||||||
if let Some(component) = path.next() {
|
if let Some(component) = path.next() {
|
||||||
let node = children.entry(component.to_owned()).or_default();
|
let node = children.entry(component.to_owned()).or_default();
|
||||||
|
@ -30,7 +43,7 @@ impl<T> HandlerMap<T> {
|
||||||
*self = Self::Leaf(handler);
|
*self = Self::Leaf(handler);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(AlreadyHandledError(()))
|
Err(HandlerMapError::AlreadyHandled.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,40 @@
|
||||||
mod maps;
|
mod maps;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
|
|
||||||
use std::iter;
|
use std::{
|
||||||
use std::pin::Pin;
|
iter,
|
||||||
use std::sync::atomic::AtomicBool;
|
pin::Pin,
|
||||||
use std::sync::{atomic, Arc, Mutex};
|
sync::{
|
||||||
use std::task::Poll;
|
atomic::{self, AtomicBool},
|
||||||
use std::time::Duration;
|
Arc, Mutex,
|
||||||
|
},
|
||||||
|
task::Poll,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use futures_core::{Future, Stream};
|
use futures_core::{Future, Stream};
|
||||||
use futures_util::future::join_all;
|
use futures_util::{future::join_all, SinkExt, StreamExt};
|
||||||
use futures_util::{SinkExt, StreamExt};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::select;
|
use tokio::{
|
||||||
use tokio::sync::mpsc::{self, UnboundedReceiver};
|
select,
|
||||||
use tokio::sync::Semaphore;
|
sync::{
|
||||||
use tokio::task::JoinHandle;
|
mpsc::{self, UnboundedReceiver},
|
||||||
|
Semaphore,
|
||||||
|
},
|
||||||
|
task::JoinHandle,
|
||||||
|
};
|
||||||
use tokio_tungstenite::tungstenite;
|
use tokio_tungstenite::tungstenite;
|
||||||
use tungstenite::error::UrlError;
|
use tungstenite::error::UrlError;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use self::maps::*;
|
use self::maps::*;
|
||||||
use self::protocol::*;
|
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 WsMessage = tungstenite::Message;
|
||||||
type WsError = tungstenite::Error;
|
type WsError = tungstenite::Error;
|
||||||
|
@ -164,24 +175,38 @@ fn split_uri(s: &str) -> Option<impl Iterator<Item = &'_ str>> {
|
||||||
pub enum AddHandlerError {
|
pub enum AddHandlerError {
|
||||||
#[error("There is already a handler for the given uri")]
|
#[error("There is already a handler for the given uri")]
|
||||||
AlreadyHandled,
|
AlreadyHandled,
|
||||||
#[error("The specified uri is invalid")]
|
#[error("The specified uri {0} is invalid")]
|
||||||
InvalidUri,
|
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)]
|
#[derive(Debug, Clone, Error)]
|
||||||
pub enum SubscriptionError {
|
pub enum SubscriptionError {
|
||||||
#[error("The specified uri is invalid")]
|
#[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(
|
fn add_handler(
|
||||||
map: &mut HandlerMap<Box<dyn RequestHandler>>,
|
map: &mut HandlerMap<Box<dyn RequestHandler>>,
|
||||||
uri: &str,
|
uri: &str,
|
||||||
handler: impl RequestHandler,
|
handler: impl RequestHandler,
|
||||||
) -> Result<(), AddHandlerError> {
|
) -> Result<(), Error> {
|
||||||
let split = split_uri(uri).ok_or(AddHandlerError::InvalidUri)?;
|
let split = split_uri(uri).ok_or_else(|| AddHandlerError::InvalidUri(uri.to_string()))?;
|
||||||
map.insert(split, Box::new(handler))
|
map.insert(split, Box::new(handler))
|
||||||
.map_err(|_| AddHandlerError::AlreadyHandled)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> {
|
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(
|
fn subscribe(
|
||||||
map: &mut SubscriberMap<MessageHandler>,
|
map: &mut SubscriberMap<MessageHandler>,
|
||||||
uris: &[&str],
|
uris: &[&str],
|
||||||
) -> Result<Subscription, SubscriptionError> {
|
) -> Result<Subscription, Error> {
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
for &uri in uris {
|
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());
|
map.insert(split, tx.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,15 +262,11 @@ impl Builder {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_handler(
|
pub fn add_handler(&mut self, uri: &str, handler: impl RequestHandler) -> Result<(), Error> {
|
||||||
&mut self,
|
|
||||||
uri: &str,
|
|
||||||
handler: impl RequestHandler,
|
|
||||||
) -> Result<(), AddHandlerError> {
|
|
||||||
add_handler(&mut self.request_handlers, uri, handler)
|
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)
|
subscribe(&mut self.message_handlers, uris)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,7 +363,7 @@ pub struct Dealer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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
|
where
|
||||||
H: RequestHandler,
|
H: RequestHandler,
|
||||||
{
|
{
|
||||||
|
@ -357,7 +378,7 @@ impl Dealer {
|
||||||
remove_handler(&mut self.shared.request_handlers.lock().unwrap(), uri)
|
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)
|
subscribe(&mut self.shared.message_handlers.lock().unwrap(), uris)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,7 +388,9 @@ impl Dealer {
|
||||||
self.shared.notify_drop.close();
|
self.shared.notify_drop.close();
|
||||||
|
|
||||||
if let Some(handle) = self.handle.take() {
|
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! {
|
select! {
|
||||||
() = shared.closed() => break,
|
() = shared.closed() => break,
|
||||||
r = t0 => {
|
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();
|
tasks.0.take();
|
||||||
},
|
},
|
||||||
r = t1 => {
|
r = t1 => {
|
||||||
r.unwrap();
|
if let Err(e) = r {
|
||||||
|
error!("timeout on task 1: {}", e);
|
||||||
|
}
|
||||||
tasks.1.take();
|
tasks.1.take();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -576,7 +603,7 @@ async fn run<F, Fut>(
|
||||||
match connect(&url, proxy.as_ref(), &shared).await {
|
match connect(&url, proxy.as_ref(), &shared).await {
|
||||||
Ok((s, r)) => tasks = (init_task(s), init_task(r)),
|
Ok((s, r)) => tasks = (init_task(s), init_task(r)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Error while connecting: {}", e);
|
error!("Error while connecting: {}", e);
|
||||||
tokio::time::sleep(RECONNECT_INTERVAL).await;
|
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 std::fmt;
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use crate::spotify_id::to_base16;
|
use crate::spotify_id::to_base16;
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
|
|
@ -1,49 +1,82 @@
|
||||||
|
use std::env::consts::OS;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::future::IntoStream;
|
use futures_util::{future::IntoStream, FutureExt};
|
||||||
use futures_util::FutureExt;
|
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
use http::uri::InvalidUri;
|
use hyper::{
|
||||||
use hyper::client::{HttpConnector, ResponseFuture};
|
client::{HttpConnector, ResponseFuture},
|
||||||
use hyper::header::USER_AGENT;
|
header::USER_AGENT,
|
||||||
use hyper::{Body, Client, Request, Response, StatusCode};
|
Body, Client, Request, Response, StatusCode,
|
||||||
|
};
|
||||||
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
|
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
|
||||||
use hyper_rustls::HttpsConnector;
|
use hyper_rustls::HttpsConnector;
|
||||||
use rustls::{ClientConfig, RootCertStore};
|
use rustls::{ClientConfig, RootCertStore};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use std::env::consts::OS;
|
use crate::{
|
||||||
|
version::{FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING},
|
||||||
use crate::version::{
|
Error,
|
||||||
FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[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 {
|
pub struct HttpClient {
|
||||||
user_agent: HeaderValue,
|
user_agent: HeaderValue,
|
||||||
proxy: Option<Url>,
|
proxy: Option<Url>,
|
||||||
tls_config: ClientConfig,
|
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 {
|
impl HttpClient {
|
||||||
pub fn new(proxy: Option<&Url>) -> Self {
|
pub fn new(proxy: Option<&Url>) -> Self {
|
||||||
let spotify_version = match OS {
|
let spotify_version = match OS {
|
||||||
|
@ -53,7 +86,7 @@ impl HttpClient {
|
||||||
|
|
||||||
let spotify_platform = match OS {
|
let spotify_platform = match OS {
|
||||||
"android" => "Android/31",
|
"android" => "Android/31",
|
||||||
"ios" => "iOS/15.1.1",
|
"ios" => "iOS/15.2",
|
||||||
"macos" => "OSX/0",
|
"macos" => "OSX/0",
|
||||||
"windows" => "Win32/0",
|
"windows" => "Win32/0",
|
||||||
_ => "Linux/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());
|
debug!("Requesting {:?}", req.uri().to_string());
|
||||||
|
|
||||||
let request = self.request_fut(req)?;
|
let request = self.request_fut(req)?;
|
||||||
{
|
|
||||||
let response = request.await;
|
let response = request.await;
|
||||||
|
|
||||||
if let Ok(response) = &response {
|
if let Ok(response) = &response {
|
||||||
let status = response.status();
|
let code = response.status();
|
||||||
if status != StatusCode::OK {
|
if code != StatusCode::OK {
|
||||||
return Err(HttpClientError::NotOK(status.into()));
|
return Err(HttpClientError::StatusCode(code).into());
|
||||||
}
|
|
||||||
}
|
|
||||||
response.map_err(HttpClientError::Response)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn request_body(&self, req: Request<Body>) -> Result<Bytes, HttpClientError> {
|
Ok(response?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request_body(&self, req: Request<Body>) -> Result<Bytes, Error> {
|
||||||
let response = self.request(req).await?;
|
let response = self.request(req).await?;
|
||||||
hyper::body::to_bytes(response.into_body())
|
Ok(hyper::body::to_bytes(response.into_body()).await?)
|
||||||
.await
|
|
||||||
.map_err(HttpClientError::Response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_stream(
|
pub fn request_stream(&self, req: Request<Body>) -> Result<IntoStream<ResponseFuture>, Error> {
|
||||||
&self,
|
|
||||||
req: Request<Body>,
|
|
||||||
) -> Result<IntoStream<ResponseFuture>, HttpClientError> {
|
|
||||||
Ok(self.request_fut(req)?.into_stream())
|
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();
|
let mut http = HttpConnector::new();
|
||||||
http.enforce_http(false);
|
http.enforce_http(false);
|
||||||
let connector = HttpsConnector::from((http, self.tls_config.clone()));
|
let connector = HttpsConnector::from((http, self.tls_config.clone()));
|
||||||
|
|
|
@ -20,6 +20,7 @@ pub mod date;
|
||||||
mod dealer;
|
mod dealer;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod diffie_hellman;
|
pub mod diffie_hellman;
|
||||||
|
pub mod error;
|
||||||
pub mod file_id;
|
pub mod file_id;
|
||||||
mod http_client;
|
mod http_client;
|
||||||
pub mod mercury;
|
pub mod mercury;
|
||||||
|
@ -34,3 +35,9 @@ pub mod token;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod version;
|
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::{
|
||||||
use std::future::Future;
|
collections::HashMap,
|
||||||
use std::mem;
|
future::Future,
|
||||||
use std::pin::Pin;
|
mem,
|
||||||
use std::task::Context;
|
pin::Pin,
|
||||||
use std::task::Poll;
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
@ -11,9 +12,7 @@ use futures_util::FutureExt;
|
||||||
use protobuf::Message;
|
use protobuf::Message;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
use crate::packet::PacketType;
|
use crate::{packet::PacketType, protocol, util::SeqGenerator, Error};
|
||||||
use crate::protocol;
|
|
||||||
use crate::util::SeqGenerator;
|
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use self::types::*;
|
pub use self::types::*;
|
||||||
|
@ -33,18 +32,18 @@ component! {
|
||||||
pub struct MercuryPending {
|
pub struct MercuryPending {
|
||||||
parts: Vec<Vec<u8>>,
|
parts: Vec<Vec<u8>>,
|
||||||
partial: Option<Vec<u8>>,
|
partial: Option<Vec<u8>>,
|
||||||
callback: Option<oneshot::Sender<Result<MercuryResponse, MercuryError>>>,
|
callback: Option<oneshot::Sender<Result<MercuryResponse, Error>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MercuryFuture<T> {
|
pub struct MercuryFuture<T> {
|
||||||
receiver: oneshot::Receiver<Result<T, MercuryError>>,
|
receiver: oneshot::Receiver<Result<T, Error>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Future for MercuryFuture<T> {
|
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> {
|
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
|
seq
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request(&self, req: MercuryRequest) -> MercuryFuture<MercuryResponse> {
|
fn request(&self, req: MercuryRequest) -> Result<MercuryFuture<MercuryResponse>, Error> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
let pending = MercuryPending {
|
let pending = MercuryPending {
|
||||||
|
@ -72,13 +71,13 @@ impl MercuryManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
let cmd = req.method.command();
|
let cmd = req.method.command();
|
||||||
let data = req.encode(&seq);
|
let data = req.encode(&seq)?;
|
||||||
|
|
||||||
self.session().send_packet(cmd, data);
|
self.session().send_packet(cmd, data)?;
|
||||||
MercuryFuture { receiver: rx }
|
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 {
|
self.request(MercuryRequest {
|
||||||
method: MercuryMethod::Get,
|
method: MercuryMethod::Get,
|
||||||
uri: uri.into(),
|
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 {
|
self.request(MercuryRequest {
|
||||||
method: MercuryMethod::Send,
|
method: MercuryMethod::Send,
|
||||||
uri: uri.into(),
|
uri: uri.into(),
|
||||||
|
@ -103,7 +106,7 @@ impl MercuryManager {
|
||||||
pub fn subscribe<T: Into<String>>(
|
pub fn subscribe<T: Into<String>>(
|
||||||
&self,
|
&self,
|
||||||
uri: T,
|
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 uri = uri.into();
|
||||||
let request = self.request(MercuryRequest {
|
let request = self.request(MercuryRequest {
|
||||||
|
@ -115,7 +118,7 @@ impl MercuryManager {
|
||||||
|
|
||||||
let manager = self.clone();
|
let manager = self.clone();
|
||||||
async move {
|
async move {
|
||||||
let response = request.await?;
|
let response = request?.await?;
|
||||||
|
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
@ -125,14 +128,19 @@ impl MercuryManager {
|
||||||
if !response.payload.is_empty() {
|
if !response.payload.is_empty() {
|
||||||
// Old subscription protocol, watch the provided list of URIs
|
// Old subscription protocol, watch the provided list of URIs
|
||||||
for sub in response.payload {
|
for sub in response.payload {
|
||||||
let mut sub =
|
match protocol::pubsub::Subscription::parse_from_bytes(&sub) {
|
||||||
protocol::pubsub::Subscription::parse_from_bytes(&sub).unwrap();
|
Ok(mut sub) => {
|
||||||
let sub_uri = sub.take_uri();
|
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 {
|
} else {
|
||||||
// New subscription protocol, watch the requested URI
|
// New subscription protocol, watch the requested URI
|
||||||
inner.subscriptions.push((uri, tx));
|
inner.subscriptions.push((uri, tx));
|
||||||
|
@ -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_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
|
||||||
let seq = data.split_to(seq_len).as_ref().to_owned();
|
let seq = data.split_to(seq_len).as_ref().to_owned();
|
||||||
|
|
||||||
|
@ -185,7 +193,7 @@ impl MercuryManager {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8);
|
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 {
|
if flags == 0x1 {
|
||||||
self.complete_request(cmd, pending);
|
self.complete_request(cmd, pending)?;
|
||||||
} else {
|
} else {
|
||||||
self.lock(move |inner| inner.pending.insert(seq, pending));
|
self.lock(move |inner| inner.pending.insert(seq, pending));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_part(data: &mut Bytes) -> Vec<u8> {
|
fn parse_part(data: &mut Bytes) -> Vec<u8> {
|
||||||
|
@ -216,9 +226,9 @@ impl MercuryManager {
|
||||||
data.split_to(size).as_ref().to_owned()
|
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_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 {
|
let response = MercuryResponse {
|
||||||
uri: header.get_uri().to_string(),
|
uri: header.get_uri().to_string(),
|
||||||
|
@ -226,13 +236,17 @@ impl MercuryManager {
|
||||||
payload: pending.parts,
|
payload: pending.parts,
|
||||||
};
|
};
|
||||||
|
|
||||||
if response.status_code >= 500 {
|
let status_code = response.status_code;
|
||||||
panic!("Spotify servers returned an error. Restart librespot.");
|
if status_code >= 500 {
|
||||||
} else if response.status_code >= 400 {
|
error!("error {} for uri {}", status_code, &response.uri);
|
||||||
warn!("error {} for uri {}", response.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 {
|
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 {
|
} else if let PacketType::MercuryEvent = cmd {
|
||||||
self.lock(|inner| {
|
self.lock(|inner| {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
|
@ -242,7 +256,7 @@ impl MercuryManager {
|
||||||
// before sending while saving the subscription under its unencoded form.
|
// before sending while saving the subscription under its unencoded form.
|
||||||
let mut uri_split = response.uri.split('/');
|
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| {
|
.chain(uri_split.map(|component| {
|
||||||
form_urlencoded::byte_serialize(component.as_bytes()).collect::<String>()
|
form_urlencoded::byte_serialize(component.as_bytes()).collect::<String>()
|
||||||
}))
|
}))
|
||||||
|
@ -263,12 +277,19 @@ impl MercuryManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
debug!("unknown subscription uri={}", response.uri);
|
debug!("unknown subscription uri={}", &response.uri);
|
||||||
trace!("response pushed over Mercury: {:?}", response);
|
trace!("response pushed over Mercury: {:?}", response);
|
||||||
|
Err(MercuryError::Response(response).into())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if let Some(cb) = pending.callback {
|
} 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 std::collections::VecDeque;
|
||||||
|
|
||||||
use super::*;
|
use super::{MercuryFuture, MercuryManager, MercuryResponse};
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
pub struct MercurySender {
|
pub struct MercurySender {
|
||||||
mercury: MercuryManager,
|
mercury: MercuryManager,
|
||||||
|
@ -23,12 +25,13 @@ impl MercurySender {
|
||||||
self.buffered_future.is_none() && self.pending.is_empty()
|
self.buffered_future.is_none() && self.pending.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&mut self, item: Vec<u8>) {
|
pub fn send(&mut self, item: Vec<u8>) -> Result<(), Error> {
|
||||||
let task = self.mercury.send(self.uri.clone(), item);
|
let task = self.mercury.send(self.uri.clone(), item)?;
|
||||||
self.pending.push_back(task);
|
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() {
|
if self.buffered_future.is_none() {
|
||||||
self.buffered_future = self.pending.pop_front();
|
self.buffered_future = self.pending.pop_front();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
use byteorder::{BigEndian, WriteBytesExt};
|
use byteorder::{BigEndian, WriteBytesExt};
|
||||||
use protobuf::Message;
|
use protobuf::Message;
|
||||||
use std::fmt;
|
|
||||||
use std::io::Write;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::packet::PacketType;
|
use crate::{packet::PacketType, protocol, Error};
|
||||||
use crate::protocol;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum MercuryMethod {
|
pub enum MercuryMethod {
|
||||||
|
@ -30,12 +29,23 @@ pub struct MercuryResponse {
|
||||||
pub payload: Vec<Vec<u8>>,
|
pub payload: Vec<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)]
|
#[derive(Debug, Error)]
|
||||||
pub struct MercuryError;
|
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 {
|
impl From<MercuryError> for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn from(err: MercuryError) -> Self {
|
||||||
write!(f, "Mercury error")
|
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 {
|
impl MercuryRequest {
|
||||||
// TODO: change into Result and remove unwraps
|
pub fn encode(&self, seq: &[u8]) -> Result<Vec<u8>, Error> {
|
||||||
pub fn encode(&self, seq: &[u8]) -> Vec<u8> {
|
|
||||||
let mut packet = Vec::new();
|
let mut packet = Vec::new();
|
||||||
packet.write_u16::<BigEndian>(seq.len() as u16).unwrap();
|
packet.write_u16::<BigEndian>(seq.len() as u16)?;
|
||||||
packet.write_all(seq).unwrap();
|
packet.write_all(seq)?;
|
||||||
packet.write_u8(1).unwrap(); // Flags: FINAL
|
packet.write_u8(1)?; // Flags: FINAL
|
||||||
packet
|
packet.write_u16::<BigEndian>(1 + self.payload.len() as u16)?; // Part count
|
||||||
.write_u16::<BigEndian>(1 + self.payload.len() as u16)
|
|
||||||
.unwrap(); // Part count
|
|
||||||
|
|
||||||
let mut header = protocol::mercury::Header::new();
|
let mut header = protocol::mercury::Header::new();
|
||||||
header.set_uri(self.uri.clone());
|
header.set_uri(self.uri.clone());
|
||||||
|
@ -81,16 +88,14 @@ impl MercuryRequest {
|
||||||
header.set_content_type(content_type.clone());
|
header.set_content_type(content_type.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
packet
|
packet.write_u16::<BigEndian>(header.compute_size() as u16)?;
|
||||||
.write_u16::<BigEndian>(header.compute_size() as u16)
|
header.write_to_writer(&mut packet)?;
|
||||||
.unwrap();
|
|
||||||
header.write_to_writer(&mut packet).unwrap();
|
|
||||||
|
|
||||||
for p in &self.payload {
|
for p in &self.payload {
|
||||||
packet.write_u16::<BigEndian>(p.len() as u16).unwrap();
|
packet.write_u16::<BigEndian>(p.len() as u16)?;
|
||||||
packet.write_all(p).unwrap();
|
packet.write_all(p)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
packet
|
Ok(packet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use num_derive::{FromPrimitive, ToPrimitive};
|
use num_derive::{FromPrimitive, ToPrimitive};
|
||||||
|
|
||||||
#[derive(Debug, FromPrimitive, ToPrimitive)]
|
#[derive(Debug, Copy, Clone, FromPrimitive, ToPrimitive)]
|
||||||
pub enum PacketType {
|
pub enum PacketType {
|
||||||
SecretBlock = 0x02,
|
SecretBlock = 0x02,
|
||||||
Ping = 0x04,
|
Ping = 0x04,
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
use std::collections::HashMap;
|
use std::{
|
||||||
use std::future::Future;
|
collections::HashMap,
|
||||||
use std::io;
|
future::Future,
|
||||||
use std::pin::Pin;
|
io,
|
||||||
use std::process::exit;
|
pin::Pin,
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
process::exit,
|
||||||
use std::sync::{Arc, RwLock, Weak};
|
sync::{
|
||||||
use std::task::Context;
|
atomic::{AtomicUsize, Ordering},
|
||||||
use std::task::Poll;
|
Arc, RwLock, Weak,
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
},
|
||||||
|
task::{Context, Poll},
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
@ -20,18 +23,21 @@ use thiserror::Error;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
|
||||||
use crate::apresolve::ApResolver;
|
use crate::{
|
||||||
use crate::audio_key::AudioKeyManager;
|
apresolve::ApResolver,
|
||||||
use crate::authentication::Credentials;
|
audio_key::AudioKeyManager,
|
||||||
use crate::cache::Cache;
|
authentication::Credentials,
|
||||||
use crate::channel::ChannelManager;
|
cache::Cache,
|
||||||
use crate::config::SessionConfig;
|
channel::ChannelManager,
|
||||||
use crate::connection::{self, AuthenticationError};
|
config::SessionConfig,
|
||||||
use crate::http_client::HttpClient;
|
connection::{self, AuthenticationError},
|
||||||
use crate::mercury::MercuryManager;
|
http_client::HttpClient,
|
||||||
use crate::packet::PacketType;
|
mercury::MercuryManager,
|
||||||
use crate::spclient::SpClient;
|
packet::PacketType,
|
||||||
use crate::token::TokenProvider;
|
spclient::SpClient,
|
||||||
|
token::TokenProvider,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum SessionError {
|
pub enum SessionError {
|
||||||
|
@ -39,6 +45,18 @@ pub enum SessionError {
|
||||||
AuthenticationError(#[from] AuthenticationError),
|
AuthenticationError(#[from] AuthenticationError),
|
||||||
#[error("Cannot create session: {0}")]
|
#[error("Cannot create session: {0}")]
|
||||||
IoError(#[from] io::Error),
|
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>;
|
pub type UserAttributes = HashMap<String, String>;
|
||||||
|
@ -88,7 +106,7 @@ impl Session {
|
||||||
config: SessionConfig,
|
config: SessionConfig,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
cache: Option<Cache>,
|
cache: Option<Cache>,
|
||||||
) -> Result<Session, SessionError> {
|
) -> Result<Session, Error> {
|
||||||
let http_client = HttpClient::new(config.proxy.as_ref());
|
let http_client = HttpClient::new(config.proxy.as_ref());
|
||||||
let (sender_tx, sender_rx) = mpsc::unbounded_channel();
|
let (sender_tx, sender_rx) = mpsc::unbounded_channel();
|
||||||
let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
|
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::*;
|
use PacketType::*;
|
||||||
|
|
||||||
let packet_type = FromPrimitive::from_u8(cmd);
|
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 {
|
match packet_type {
|
||||||
Some(Ping) => {
|
Some(Ping) => {
|
||||||
let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64;
|
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.0.data.write().unwrap().time_delta = server_timestamp - timestamp;
|
||||||
|
|
||||||
self.debug_info();
|
self.debug_info();
|
||||||
self.send_packet(Pong, vec![0, 0, 0, 0]);
|
self.send_packet(Pong, vec![0, 0, 0, 0])
|
||||||
}
|
}
|
||||||
Some(CountryCode) => {
|
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);
|
info!("Country: {:?}", country);
|
||||||
self.0.data.write().unwrap().user_data.country = country;
|
self.0.data.write().unwrap().user_data.country = country;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(StreamChunkRes) | Some(ChannelError) => {
|
Some(StreamChunkRes) | Some(ChannelError) => self.channel().dispatch(cmd, data),
|
||||||
self.channel().dispatch(packet_type.unwrap(), data);
|
Some(AesKey) | Some(AesKeyError) => self.audio_key().dispatch(cmd, data),
|
||||||
}
|
|
||||||
Some(AesKey) | Some(AesKeyError) => {
|
|
||||||
self.audio_key().dispatch(packet_type.unwrap(), data);
|
|
||||||
}
|
|
||||||
Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => {
|
Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => {
|
||||||
self.mercury().dispatch(packet_type.unwrap(), data);
|
self.mercury().dispatch(cmd, data)
|
||||||
}
|
}
|
||||||
Some(ProductInfo) => {
|
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 reader = quick_xml::Reader::from_str(data);
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
@ -256,8 +280,7 @@ impl Session {
|
||||||
loop {
|
loop {
|
||||||
match reader.read_event(&mut buf) {
|
match reader.read_event(&mut buf) {
|
||||||
Ok(Event::Start(ref element)) => {
|
Ok(Event::Start(ref element)) => {
|
||||||
current_element =
|
current_element = std::str::from_utf8(element.name())?.to_owned()
|
||||||
std::str::from_utf8(element.name()).unwrap().to_owned()
|
|
||||||
}
|
}
|
||||||
Ok(Event::End(_)) => {
|
Ok(Event::End(_)) => {
|
||||||
current_element = String::new();
|
current_element = String::new();
|
||||||
|
@ -266,7 +289,7 @@ impl Session {
|
||||||
if !current_element.is_empty() {
|
if !current_element.is_empty() {
|
||||||
let _ = user_attributes.insert(
|
let _ = user_attributes.insert(
|
||||||
current_element.clone(),
|
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::check_catalogue(&user_attributes);
|
||||||
|
|
||||||
self.0.data.write().unwrap().user_data.attributes = user_attributes;
|
self.0.data.write().unwrap().user_data.attributes = user_attributes;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(PongAck)
|
Some(PongAck)
|
||||||
| Some(SecretBlock)
|
| Some(SecretBlock)
|
||||||
| Some(LegacyWelcome)
|
| Some(LegacyWelcome)
|
||||||
| Some(UnknownDataAllZeros)
|
| Some(UnknownDataAllZeros)
|
||||||
| Some(LicenseVersion) => {}
|
| Some(LicenseVersion) => Ok(()),
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(packet_type) = PacketType::from_u8(cmd) {
|
trace!("Ignoring {:?} packet with data {:#?}", cmd, data);
|
||||||
trace!("Ignoring {:?} packet with data {:#?}", packet_type, data);
|
Err(SessionError::Packet(cmd as u8).into())
|
||||||
} else {
|
|
||||||
trace!("Ignoring unknown packet {:x}", cmd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) {
|
pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) -> Result<(), Error> {
|
||||||
self.0.tx_connection.send((cmd as u8, data)).unwrap();
|
self.0.tx_connection.send((cmd as u8, data))?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cache(&self) -> Option<&Arc<Cache>> {
|
pub fn cache(&self) -> Option<&Arc<Cache>> {
|
||||||
|
@ -393,7 +415,7 @@ impl SessionWeak {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn upgrade(&self) -> Session {
|
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::{io, net::ToSocketAddrs};
|
||||||
use std::net::ToSocketAddrs;
|
|
||||||
|
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
use crate::apresolve::SocketAddress;
|
use std::time::Duration;
|
||||||
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 bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::future::IntoStream;
|
use futures_util::future::IntoStream;
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
use hyper::client::ResponseFuture;
|
use hyper::{
|
||||||
use hyper::header::{InvalidHeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE};
|
client::ResponseFuture,
|
||||||
use hyper::{Body, HeaderMap, Method, Request};
|
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE},
|
||||||
|
Body, HeaderMap, Method, Request,
|
||||||
|
};
|
||||||
use protobuf::Message;
|
use protobuf::Message;
|
||||||
use rand::Rng;
|
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! {
|
component! {
|
||||||
SpClient : SpClientInner {
|
SpClient : SpClientInner {
|
||||||
|
@ -25,23 +28,7 @@ component! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SpClientResult = Result<Bytes, SpClientError>;
|
pub type SpClientResult = Result<Bytes, Error>;
|
||||||
|
|
||||||
#[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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum RequestStrategy {
|
pub enum RequestStrategy {
|
||||||
|
@ -157,12 +144,8 @@ impl SpClient {
|
||||||
))?,
|
))?,
|
||||||
);
|
);
|
||||||
|
|
||||||
last_response = self
|
last_response = self.session().http_client().request_body(request).await;
|
||||||
.session()
|
|
||||||
.http_client()
|
|
||||||
.request_body(request)
|
|
||||||
.await
|
|
||||||
.map_err(SpClientError::Network);
|
|
||||||
if last_response.is_ok() {
|
if last_response.is_ok() {
|
||||||
return last_response;
|
return last_response;
|
||||||
}
|
}
|
||||||
|
@ -177,9 +160,9 @@ impl SpClient {
|
||||||
|
|
||||||
// Reconnection logic: drop the current access point if we are experiencing issues.
|
// 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.
|
// This will cause the next call to base_url() to resolve a new one.
|
||||||
if let Err(SpClientError::Network(ref network_error)) = last_response {
|
if let Err(ref network_error) = last_response {
|
||||||
match network_error {
|
match network_error.kind {
|
||||||
HttpClientError::Response(_) | HttpClientError::Request(_) => {
|
ErrorKind::Unavailable | ErrorKind::DeadlineExceeded => {
|
||||||
// Keep trying the current access point three times before dropping it.
|
// Keep trying the current access point three times before dropping it.
|
||||||
if tries % 3 == 0 {
|
if tries % 3 == 0 {
|
||||||
self.flush_accesspoint().await
|
self.flush_accesspoint().await
|
||||||
|
@ -244,7 +227,7 @@ impl SpClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult {
|
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)
|
self.request_as_json(&Method::GET, &endpoint, None, None)
|
||||||
.await
|
.await
|
||||||
|
@ -291,7 +274,7 @@ impl SpClient {
|
||||||
url: &str,
|
url: &str,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
length: usize,
|
length: usize,
|
||||||
) -> Result<IntoStream<ResponseFuture>, SpClientError> {
|
) -> Result<IntoStream<ResponseFuture>, Error> {
|
||||||
let req = Request::builder()
|
let req = Request::builder()
|
||||||
.method(&Method::GET)
|
.method(&Method::GET)
|
||||||
.uri(url)
|
.uri(url)
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
use librespot_protocol as protocol;
|
use std::{
|
||||||
|
convert::{TryFrom, TryInto},
|
||||||
|
fmt,
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use std::convert::{TryFrom, TryInto};
|
use crate::Error;
|
||||||
use std::fmt;
|
|
||||||
use std::ops::Deref;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
// re-export FileId for historic reasons, when it was part of this mod
|
// 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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum SpotifyItemType {
|
pub enum SpotifyItemType {
|
||||||
|
@ -64,8 +68,14 @@ pub enum SpotifyIdError {
|
||||||
InvalidRoot,
|
InvalidRoot,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SpotifyIdResult = Result<SpotifyId, SpotifyIdError>;
|
impl From<SpotifyIdError> for Error {
|
||||||
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, SpotifyIdError>;
|
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 BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
||||||
|
@ -95,7 +105,7 @@ impl SpotifyId {
|
||||||
let p = match c {
|
let p = match c {
|
||||||
b'0'..=b'9' => c - b'0',
|
b'0'..=b'9' => c - b'0',
|
||||||
b'a'..=b'f' => c - b'a' + 10,
|
b'a'..=b'f' => c - b'a' + 10,
|
||||||
_ => return Err(SpotifyIdError::InvalidId),
|
_ => return Err(SpotifyIdError::InvalidId.into()),
|
||||||
} as u128;
|
} as u128;
|
||||||
|
|
||||||
dst <<= 4;
|
dst <<= 4;
|
||||||
|
@ -121,7 +131,7 @@ impl SpotifyId {
|
||||||
b'0'..=b'9' => c - b'0',
|
b'0'..=b'9' => c - b'0',
|
||||||
b'a'..=b'z' => c - b'a' + 10,
|
b'a'..=b'z' => c - b'a' + 10,
|
||||||
b'A'..=b'Z' => c - b'A' + 36,
|
b'A'..=b'Z' => c - b'A' + 36,
|
||||||
_ => return Err(SpotifyIdError::InvalidId),
|
_ => return Err(SpotifyIdError::InvalidId.into()),
|
||||||
} as u128;
|
} as u128;
|
||||||
|
|
||||||
dst *= 62;
|
dst *= 62;
|
||||||
|
@ -143,7 +153,7 @@ impl SpotifyId {
|
||||||
id: u128::from_be_bytes(dst),
|
id: u128::from_be_bytes(dst),
|
||||||
item_type: SpotifyItemType::Unknown,
|
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}`
|
// At minimum, should be `spotify:{type}:{id}`
|
||||||
if uri_parts.len() < 3 {
|
if uri_parts.len() < 3 {
|
||||||
return Err(SpotifyIdError::InvalidFormat);
|
return Err(SpotifyIdError::InvalidFormat.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if uri_parts[0] != "spotify" {
|
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 {
|
if id.len() != Self::SIZE_BASE62 {
|
||||||
return Err(SpotifyIdError::InvalidId);
|
return Err(SpotifyIdError::InvalidId.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
item_type: uri_parts.pop().unwrap().into(),
|
item_type: uri_parts.pop().unwrap_or_default().into(),
|
||||||
..Self::from_base62(id)?
|
..Self::from_base62(id)?
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -285,15 +295,15 @@ impl NamedSpotifyId {
|
||||||
|
|
||||||
// At minimum, should be `spotify:user:{username}:{type}:{id}`
|
// At minimum, should be `spotify:user:{username}:{type}:{id}`
|
||||||
if uri_parts.len() < 5 {
|
if uri_parts.len() < 5 {
|
||||||
return Err(SpotifyIdError::InvalidFormat);
|
return Err(SpotifyIdError::InvalidFormat.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if uri_parts[0] != "spotify" {
|
if uri_parts[0] != "spotify" {
|
||||||
return Err(SpotifyIdError::InvalidRoot);
|
return Err(SpotifyIdError::InvalidRoot.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if uri_parts[1] != "user" {
|
if uri_parts[1] != "user" {
|
||||||
return Err(SpotifyIdError::InvalidFormat);
|
return Err(SpotifyIdError::InvalidFormat.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -344,35 +354,35 @@ impl fmt::Display for NamedSpotifyId {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&[u8]> for SpotifyId {
|
impl TryFrom<&[u8]> for SpotifyId {
|
||||||
type Error = SpotifyIdError;
|
type Error = crate::Error;
|
||||||
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
|
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
|
||||||
Self::from_raw(src)
|
Self::from_raw(src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for SpotifyId {
|
impl TryFrom<&str> for SpotifyId {
|
||||||
type Error = SpotifyIdError;
|
type Error = crate::Error;
|
||||||
fn try_from(src: &str) -> Result<Self, Self::Error> {
|
fn try_from(src: &str) -> Result<Self, Self::Error> {
|
||||||
Self::from_base62(src)
|
Self::from_base62(src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<String> for SpotifyId {
|
impl TryFrom<String> for SpotifyId {
|
||||||
type Error = SpotifyIdError;
|
type Error = crate::Error;
|
||||||
fn try_from(src: String) -> Result<Self, Self::Error> {
|
fn try_from(src: String) -> Result<Self, Self::Error> {
|
||||||
Self::try_from(src.as_str())
|
Self::try_from(src.as_str())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&Vec<u8>> for SpotifyId {
|
impl TryFrom<&Vec<u8>> for SpotifyId {
|
||||||
type Error = SpotifyIdError;
|
type Error = crate::Error;
|
||||||
fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
|
fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
Self::try_from(src.as_slice())
|
Self::try_from(src.as_slice())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
|
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> {
|
fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> {
|
||||||
match SpotifyId::from_raw(track.get_gid()) {
|
match SpotifyId::from_raw(track.get_gid()) {
|
||||||
Ok(mut id) => {
|
Ok(mut id) => {
|
||||||
|
@ -385,7 +395,7 @@ impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&protocol::metadata::Album> 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> {
|
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
item_type: SpotifyItemType::Album,
|
item_type: SpotifyItemType::Album,
|
||||||
|
@ -395,7 +405,7 @@ impl TryFrom<&protocol::metadata::Album> for SpotifyId {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&protocol::metadata::Artist> 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> {
|
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
item_type: SpotifyItemType::Artist,
|
item_type: SpotifyItemType::Artist,
|
||||||
|
@ -405,7 +415,7 @@ impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&protocol::metadata::Episode> 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> {
|
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
item_type: SpotifyItemType::Episode,
|
item_type: SpotifyItemType::Episode,
|
||||||
|
@ -415,7 +425,7 @@ impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&protocol::metadata::Track> 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> {
|
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
item_type: SpotifyItemType::Track,
|
item_type: SpotifyItemType::Track,
|
||||||
|
@ -425,7 +435,7 @@ impl TryFrom<&protocol::metadata::Track> for SpotifyId {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&protocol::metadata::Show> 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> {
|
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
item_type: SpotifyItemType::Show,
|
item_type: SpotifyItemType::Show,
|
||||||
|
@ -435,7 +445,7 @@ impl TryFrom<&protocol::metadata::Show> for SpotifyId {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&protocol::metadata::ArtistWithRole> 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> {
|
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
item_type: SpotifyItemType::Artist,
|
item_type: SpotifyItemType::Artist,
|
||||||
|
@ -445,7 +455,7 @@ impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&protocol::playlist4_external::Item> 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> {
|
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
item_type: SpotifyItemType::Track,
|
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,
|
// Note that this is the unique revision of an item's metadata on a playlist,
|
||||||
// not the ID of that item or playlist.
|
// not the ID of that item or playlist.
|
||||||
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
|
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> {
|
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
|
||||||
Self::try_from(item.get_revision())
|
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.
|
// Note that this is the unique revision of a playlist, not the ID of that playlist.
|
||||||
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
|
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
|
||||||
type Error = SpotifyIdError;
|
type Error = crate::Error;
|
||||||
fn try_from(
|
fn try_from(
|
||||||
playlist: &protocol::playlist4_external::SelectedListContent,
|
playlist: &protocol::playlist4_external::SelectedListContent,
|
||||||
) -> Result<Self, Self::Error> {
|
) -> 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
|
// which is why we now don't create a separate `Playlist` enum value yet and choose
|
||||||
// to discard any item type.
|
// to discard any item type.
|
||||||
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
|
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
|
||||||
type Error = SpotifyIdError;
|
type Error = crate::Error;
|
||||||
fn try_from(
|
fn try_from(
|
||||||
picture: &protocol::playlist_annotate3::TranscodedPicture,
|
picture: &protocol::playlist_annotate3::TranscodedPicture,
|
||||||
) -> Result<Self, Self::Error> {
|
) -> Result<Self, Self::Error> {
|
||||||
|
@ -565,7 +575,7 @@ mod tests {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyItemType::Unknown,
|
kind: SpotifyItemType::Unknown,
|
||||||
// Invalid ID in the URI.
|
// Invalid ID in the URI.
|
||||||
uri_error: Some(SpotifyIdError::InvalidId),
|
uri_error: SpotifyIdError::InvalidId,
|
||||||
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
|
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
|
||||||
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
|
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
|
||||||
base62: "!!!!!Ys0csV6RS48xBl0tH",
|
base62: "!!!!!Ys0csV6RS48xBl0tH",
|
||||||
|
@ -578,7 +588,7 @@ mod tests {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyItemType::Unknown,
|
kind: SpotifyItemType::Unknown,
|
||||||
// Missing colon between ID and type.
|
// Missing colon between ID and type.
|
||||||
uri_error: Some(SpotifyIdError::InvalidFormat),
|
uri_error: SpotifyIdError::InvalidFormat,
|
||||||
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
|
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
|
||||||
base16: "--------------------",
|
base16: "--------------------",
|
||||||
base62: "....................",
|
base62: "....................",
|
||||||
|
@ -591,7 +601,7 @@ mod tests {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyItemType::Unknown,
|
kind: SpotifyItemType::Unknown,
|
||||||
// Uri too short
|
// Uri too short
|
||||||
uri_error: Some(SpotifyIdError::InvalidId),
|
uri_error: SpotifyIdError::InvalidId,
|
||||||
uri: "spotify:azb:aRS48xBl0tH",
|
uri: "spotify:azb:aRS48xBl0tH",
|
||||||
base16: "--------------------",
|
base16: "--------------------",
|
||||||
base62: "....................",
|
base62: "....................",
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
// user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming,
|
// user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming,
|
||||||
// app-remote-control
|
// app-remote-control
|
||||||
|
|
||||||
use crate::mercury::MercuryError;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use std::error::Error;
|
use crate::Error;
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
component! {
|
component! {
|
||||||
TokenProvider : TokenProviderInner {
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
|
@ -54,11 +66,7 @@ impl TokenProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// scopes must be comma-separated
|
// scopes must be comma-separated
|
||||||
pub async fn get_token(&self, scopes: &str) -> Result<Token, MercuryError> {
|
pub async fn get_token(&self, scopes: &str) -> Result<Token, Error> {
|
||||||
if scopes.is_empty() {
|
|
||||||
return Err(MercuryError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(index) = self.find_token(scopes.split(',').collect()) {
|
if let Some(index) = self.find_token(scopes.split(',').collect()) {
|
||||||
let cached_token = self.lock(|inner| inner.tokens[index].clone());
|
let cached_token = self.lock(|inner| inner.tokens[index].clone());
|
||||||
if cached_token.is_expired() {
|
if cached_token.is_expired() {
|
||||||
|
@ -79,14 +87,10 @@ impl TokenProvider {
|
||||||
Self::KEYMASTER_CLIENT_ID,
|
Self::KEYMASTER_CLIENT_ID,
|
||||||
self.session().device_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 response = request.await?;
|
||||||
let data = response
|
let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec();
|
||||||
.payload
|
let token = Token::new(String::from_utf8(data)?)?;
|
||||||
.first()
|
|
||||||
.expect("No tokens received")
|
|
||||||
.to_vec();
|
|
||||||
let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?;
|
|
||||||
trace!("Got token: {:#?}", token);
|
trace!("Got token: {:#?}", token);
|
||||||
self.lock(|inner| inner.tokens.push(token.clone()));
|
self.lock(|inner| inner.tokens.push(token.clone()));
|
||||||
Ok(token)
|
Ok(token)
|
||||||
|
@ -96,7 +100,7 @@ impl TokenProvider {
|
||||||
impl Token {
|
impl Token {
|
||||||
const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10);
|
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())?;
|
let data: TokenData = serde_json::from_slice(body.as_ref())?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
access_token: data.access_token,
|
access_token: data.access_token,
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
use std::future::Future;
|
use std::{
|
||||||
use std::mem;
|
future::Future,
|
||||||
use std::pin::Pin;
|
mem,
|
||||||
use std::task::Context;
|
pin::Pin,
|
||||||
use std::task::Poll;
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use futures_util::FutureExt;
|
use futures_util::{future, FutureExt, Sink, SinkExt};
|
||||||
use futures_util::Sink;
|
use tokio::{task::JoinHandle, time::timeout};
|
||||||
use futures_util::{future, SinkExt};
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tokio::time::timeout;
|
|
||||||
|
|
||||||
/// Returns a future that will flush the sink, even if flushing is temporarily completed.
|
/// Returns a future that will flush the sink, even if flushing is temporarily completed.
|
||||||
/// Finishes only if the sink throws an error.
|
/// Finishes only if the sink throws an error.
|
||||||
|
|
|
@ -13,6 +13,7 @@ base64 = "0.13"
|
||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
form_urlencoded = "1.0"
|
form_urlencoded = "1.0"
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
|
futures-util = "0.3"
|
||||||
hmac = "0.11"
|
hmac = "0.11"
|
||||||
hyper = { version = "0.14", features = ["server", "http1", "tcp"] }
|
hyper = { version = "0.14", features = ["server", "http1", "tcp"] }
|
||||||
libmdns = "0.6"
|
libmdns = "0.6"
|
||||||
|
|
|
@ -27,6 +27,8 @@ pub use crate::core::authentication::Credentials;
|
||||||
/// Determining the icon in the list of available devices.
|
/// Determining the icon in the list of available devices.
|
||||||
pub use crate::core::config::DeviceType;
|
pub use crate::core::config::DeviceType;
|
||||||
|
|
||||||
|
pub use crate::core::Error;
|
||||||
|
|
||||||
/// Makes this device visible to Spotify clients in the local network.
|
/// Makes this device visible to Spotify clients in the local network.
|
||||||
///
|
///
|
||||||
/// `Discovery` implements the [`Stream`] trait. Every time this device
|
/// `Discovery` implements the [`Stream`] trait. Every time this device
|
||||||
|
@ -48,13 +50,28 @@ pub struct Builder {
|
||||||
|
|
||||||
/// Errors that can occur while setting up a [`Discovery`] instance.
|
/// Errors that can occur while setting up a [`Discovery`] instance.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum DiscoveryError {
|
||||||
/// Setting up service discovery via DNS-SD failed.
|
/// Setting up service discovery via DNS-SD failed.
|
||||||
#[error("Setting up dns-sd failed: {0}")]
|
#[error("Setting up dns-sd failed: {0}")]
|
||||||
DnsSdError(#[from] io::Error),
|
DnsSdError(#[from] io::Error),
|
||||||
/// Setting up the http server failed.
|
/// Setting up the http server failed.
|
||||||
|
#[error("Creating SHA1 HMAC failed for base key {0:?}")]
|
||||||
|
HmacError(Vec<u8>),
|
||||||
#[error("Setting up the http server failed: {0}")]
|
#[error("Setting up the http server failed: {0}")]
|
||||||
HttpServerError(#[from] hyper::Error),
|
HttpServerError(#[from] hyper::Error),
|
||||||
|
#[error("Missing params for key {0}")]
|
||||||
|
ParamsError(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DiscoveryError> for Error {
|
||||||
|
fn from(err: DiscoveryError) -> Self {
|
||||||
|
match err {
|
||||||
|
DiscoveryError::DnsSdError(_) => Error::unavailable(err),
|
||||||
|
DiscoveryError::HmacError(_) => Error::invalid_argument(err),
|
||||||
|
DiscoveryError::HttpServerError(_) => Error::unavailable(err),
|
||||||
|
DiscoveryError::ParamsError(_) => Error::invalid_argument(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Builder {
|
impl Builder {
|
||||||
|
@ -96,7 +113,7 @@ impl Builder {
|
||||||
pub fn launch(self) -> Result<Discovery, Error> {
|
pub fn launch(self) -> Result<Discovery, Error> {
|
||||||
let mut port = self.port;
|
let mut port = self.port;
|
||||||
let name = self.server_config.name.clone().into_owned();
|
let name = self.server_config.name.clone().into_owned();
|
||||||
let server = DiscoveryServer::new(self.server_config, &mut port)?;
|
let server = DiscoveryServer::new(self.server_config, &mut port)??;
|
||||||
|
|
||||||
let svc;
|
let svc;
|
||||||
|
|
||||||
|
@ -109,8 +126,7 @@ impl Builder {
|
||||||
None,
|
None,
|
||||||
port,
|
port,
|
||||||
&["VERSION=1.0", "CPath=/"],
|
&["VERSION=1.0", "CPath=/"],
|
||||||
)
|
)?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?;
|
let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?;
|
||||||
|
|
|
@ -1,26 +1,35 @@
|
||||||
use std::borrow::Cow;
|
use std::{
|
||||||
use std::collections::BTreeMap;
|
borrow::Cow,
|
||||||
use std::convert::Infallible;
|
collections::BTreeMap,
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
convert::Infallible,
|
||||||
use std::pin::Pin;
|
net::{Ipv4Addr, SocketAddr},
|
||||||
use std::sync::Arc;
|
pin::Pin,
|
||||||
use std::task::{Context, Poll};
|
sync::Arc,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
use aes_ctr::cipher::generic_array::GenericArray;
|
use aes_ctr::{
|
||||||
use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher};
|
cipher::generic_array::GenericArray,
|
||||||
use aes_ctr::Aes128Ctr;
|
cipher::{NewStreamCipher, SyncStreamCipher},
|
||||||
|
Aes128Ctr,
|
||||||
|
};
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
|
use futures_util::{FutureExt, TryFutureExt};
|
||||||
use hmac::{Hmac, Mac, NewMac};
|
use hmac::{Hmac, Mac, NewMac};
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::{
|
||||||
use hyper::{Body, Method, Request, Response, StatusCode};
|
service::{make_service_fn, service_fn},
|
||||||
use log::{debug, warn};
|
Body, Method, Request, Response, StatusCode,
|
||||||
|
};
|
||||||
|
use log::{debug, error, warn};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
use crate::core::authentication::Credentials;
|
use super::DiscoveryError;
|
||||||
use crate::core::config::DeviceType;
|
|
||||||
use crate::core::diffie_hellman::DhLocalKeys;
|
use crate::core::{
|
||||||
|
authentication::Credentials, config::DeviceType, diffie_hellman::DhLocalKeys, Error,
|
||||||
|
};
|
||||||
|
|
||||||
type Params<'a> = BTreeMap<Cow<'a, str>, Cow<'a, str>>;
|
type Params<'a> = BTreeMap<Cow<'a, str>, Cow<'a, str>>;
|
||||||
|
|
||||||
|
@ -76,14 +85,26 @@ impl RequestHandler {
|
||||||
Response::new(Body::from(body))
|
Response::new(Body::from(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_add_user(&self, params: &Params<'_>) -> Response<hyper::Body> {
|
fn handle_add_user(&self, params: &Params<'_>) -> Result<Response<hyper::Body>, Error> {
|
||||||
let username = params.get("userName").unwrap().as_ref();
|
let username_key = "userName";
|
||||||
let encrypted_blob = params.get("blob").unwrap();
|
let username = params
|
||||||
let client_key = params.get("clientKey").unwrap();
|
.get(username_key)
|
||||||
|
.ok_or(DiscoveryError::ParamsError(username_key))?
|
||||||
|
.as_ref();
|
||||||
|
|
||||||
let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap();
|
let blob_key = "blob";
|
||||||
|
let encrypted_blob = params
|
||||||
|
.get(blob_key)
|
||||||
|
.ok_or(DiscoveryError::ParamsError(blob_key))?;
|
||||||
|
|
||||||
let client_key = base64::decode(client_key.as_bytes()).unwrap();
|
let clientkey_key = "clientKey";
|
||||||
|
let client_key = params
|
||||||
|
.get(clientkey_key)
|
||||||
|
.ok_or(DiscoveryError::ParamsError(clientkey_key))?;
|
||||||
|
|
||||||
|
let encrypted_blob = base64::decode(encrypted_blob.as_bytes())?;
|
||||||
|
|
||||||
|
let client_key = base64::decode(client_key.as_bytes())?;
|
||||||
let shared_key = self.keys.shared_secret(&client_key);
|
let shared_key = self.keys.shared_secret(&client_key);
|
||||||
|
|
||||||
let iv = &encrypted_blob[0..16];
|
let iv = &encrypted_blob[0..16];
|
||||||
|
@ -94,21 +115,21 @@ impl RequestHandler {
|
||||||
let base_key = &base_key[..16];
|
let base_key = &base_key[..16];
|
||||||
|
|
||||||
let checksum_key = {
|
let checksum_key = {
|
||||||
let mut h =
|
let mut h = Hmac::<Sha1>::new_from_slice(base_key)
|
||||||
Hmac::<Sha1>::new_from_slice(base_key).expect("HMAC can take key of any size");
|
.map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?;
|
||||||
h.update(b"checksum");
|
h.update(b"checksum");
|
||||||
h.finalize().into_bytes()
|
h.finalize().into_bytes()
|
||||||
};
|
};
|
||||||
|
|
||||||
let encryption_key = {
|
let encryption_key = {
|
||||||
let mut h =
|
let mut h = Hmac::<Sha1>::new_from_slice(base_key)
|
||||||
Hmac::<Sha1>::new_from_slice(base_key).expect("HMAC can take key of any size");
|
.map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?;
|
||||||
h.update(b"encryption");
|
h.update(b"encryption");
|
||||||
h.finalize().into_bytes()
|
h.finalize().into_bytes()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut h =
|
let mut h = Hmac::<Sha1>::new_from_slice(&checksum_key)
|
||||||
Hmac::<Sha1>::new_from_slice(&checksum_key).expect("HMAC can take key of any size");
|
.map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?;
|
||||||
h.update(encrypted);
|
h.update(encrypted);
|
||||||
if h.verify(cksum).is_err() {
|
if h.verify(cksum).is_err() {
|
||||||
warn!("Login error for user {:?}: MAC mismatch", username);
|
warn!("Login error for user {:?}: MAC mismatch", username);
|
||||||
|
@ -119,7 +140,7 @@ impl RequestHandler {
|
||||||
});
|
});
|
||||||
|
|
||||||
let body = result.to_string();
|
let body = result.to_string();
|
||||||
return Response::new(Body::from(body));
|
return Ok(Response::new(Body::from(body)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let decrypted = {
|
let decrypted = {
|
||||||
|
@ -132,9 +153,9 @@ impl RequestHandler {
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
|
|
||||||
let credentials = Credentials::with_blob(username, &decrypted, &self.config.device_id);
|
let credentials = Credentials::with_blob(username, &decrypted, &self.config.device_id)?;
|
||||||
|
|
||||||
self.tx.send(credentials).unwrap();
|
self.tx.send(credentials)?;
|
||||||
|
|
||||||
let result = json!({
|
let result = json!({
|
||||||
"status": 101,
|
"status": 101,
|
||||||
|
@ -143,7 +164,7 @@ impl RequestHandler {
|
||||||
});
|
});
|
||||||
|
|
||||||
let body = result.to_string();
|
let body = result.to_string();
|
||||||
Response::new(Body::from(body))
|
Ok(Response::new(Body::from(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn not_found(&self) -> Response<hyper::Body> {
|
fn not_found(&self) -> Response<hyper::Body> {
|
||||||
|
@ -152,7 +173,10 @@ impl RequestHandler {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(self: Arc<Self>, request: Request<Body>) -> hyper::Result<Response<Body>> {
|
async fn handle(
|
||||||
|
self: Arc<Self>,
|
||||||
|
request: Request<Body>,
|
||||||
|
) -> Result<hyper::Result<Response<Body>>, Error> {
|
||||||
let mut params = Params::new();
|
let mut params = Params::new();
|
||||||
|
|
||||||
let (parts, body) = request.into_parts();
|
let (parts, body) = request.into_parts();
|
||||||
|
@ -172,11 +196,11 @@ impl RequestHandler {
|
||||||
|
|
||||||
let action = params.get("action").map(Cow::as_ref);
|
let action = params.get("action").map(Cow::as_ref);
|
||||||
|
|
||||||
Ok(match (parts.method, action) {
|
Ok(Ok(match (parts.method, action) {
|
||||||
(Method::GET, Some("getInfo")) => self.handle_get_info(),
|
(Method::GET, Some("getInfo")) => self.handle_get_info(),
|
||||||
(Method::POST, Some("addUser")) => self.handle_add_user(¶ms),
|
(Method::POST, Some("addUser")) => self.handle_add_user(¶ms)?,
|
||||||
_ => self.not_found(),
|
_ => self.not_found(),
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +210,7 @@ pub struct DiscoveryServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiscoveryServer {
|
impl DiscoveryServer {
|
||||||
pub fn new(config: Config, port: &mut u16) -> hyper::Result<Self> {
|
pub fn new(config: Config, port: &mut u16) -> Result<hyper::Result<Self>, Error> {
|
||||||
let (discovery, cred_rx) = RequestHandler::new(config);
|
let (discovery, cred_rx) = RequestHandler::new(config);
|
||||||
let discovery = Arc::new(discovery);
|
let discovery = Arc::new(discovery);
|
||||||
|
|
||||||
|
@ -197,7 +221,14 @@ impl DiscoveryServer {
|
||||||
let make_service = make_service_fn(move |_| {
|
let make_service = make_service_fn(move |_| {
|
||||||
let discovery = discovery.clone();
|
let discovery = discovery.clone();
|
||||||
async move {
|
async move {
|
||||||
Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().handle(request)))
|
Ok::<_, hyper::Error>(service_fn(move |request| {
|
||||||
|
discovery
|
||||||
|
.clone()
|
||||||
|
.handle(request)
|
||||||
|
.inspect_err(|e| error!("could not handle discovery request: {}", e))
|
||||||
|
.and_then(|x| async move { Ok(x) })
|
||||||
|
.map(Result::unwrap) // guaranteed by `and_then` above
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -209,8 +240,10 @@ impl DiscoveryServer {
|
||||||
tokio::spawn(async {
|
tokio::spawn(async {
|
||||||
let result = server
|
let result = server
|
||||||
.with_graceful_shutdown(async {
|
.with_graceful_shutdown(async {
|
||||||
close_rx.await.unwrap_err();
|
|
||||||
debug!("Shutting down discovery server");
|
debug!("Shutting down discovery server");
|
||||||
|
if close_rx.await.is_ok() {
|
||||||
|
debug!("unable to close discovery Rx channel completely");
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -219,10 +252,10 @@ impl DiscoveryServer {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Ok(Self {
|
||||||
cred_rx,
|
cred_rx,
|
||||||
_close_tx: close_tx,
|
_close_tx: close_tx,
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,20 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::{
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
use std::ops::Deref;
|
fmt::Debug,
|
||||||
|
ops::Deref,
|
||||||
use crate::{
|
|
||||||
artist::Artists,
|
|
||||||
availability::Availabilities,
|
|
||||||
copyright::Copyrights,
|
|
||||||
error::{MetadataError, RequestError},
|
|
||||||
external_id::ExternalIds,
|
|
||||||
image::Images,
|
|
||||||
request::RequestResult,
|
|
||||||
restriction::Restrictions,
|
|
||||||
sale_period::SalePeriods,
|
|
||||||
track::Tracks,
|
|
||||||
util::try_from_repeated_message,
|
|
||||||
Metadata,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::date::Date;
|
use crate::{
|
||||||
use librespot_core::session::Session;
|
artist::Artists, availability::Availabilities, copyright::Copyrights, external_id::ExternalIds,
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
image::Images, request::RequestResult, restriction::Restrictions, sale_period::SalePeriods,
|
||||||
|
track::Tracks, util::try_from_repeated_message, Metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
use librespot_core::{date::Date, Error, Session, SpotifyId};
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use protocol::metadata::Disc as DiscMessage;
|
|
||||||
|
|
||||||
pub use protocol::metadata::Album_Type as AlbumType;
|
pub use protocol::metadata::Album_Type as AlbumType;
|
||||||
|
use protocol::metadata::Disc as DiscMessage;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Album {
|
pub struct Album {
|
||||||
|
@ -94,20 +84,16 @@ impl Metadata for Album {
|
||||||
type Message = protocol::metadata::Album;
|
type Message = protocol::metadata::Album;
|
||||||
|
|
||||||
async fn request(session: &Session, album_id: SpotifyId) -> RequestResult {
|
async fn request(session: &Session, album_id: SpotifyId) -> RequestResult {
|
||||||
session
|
session.spclient().get_album_metadata(album_id).await
|
||||||
.spclient()
|
|
||||||
.get_album_metadata(album_id)
|
|
||||||
.await
|
|
||||||
.map_err(RequestError::Http)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
|
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
|
||||||
Self::try_from(msg)
|
Self::try_from(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&<Self as Metadata>::Message> for Album {
|
impl TryFrom<&<Self as Metadata>::Message> for Album {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(album: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
|
fn try_from(album: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: album.try_into()?,
|
id: album.try_into()?,
|
||||||
|
@ -138,7 +124,7 @@ impl TryFrom<&<Self as Metadata>::Message> for Album {
|
||||||
try_from_repeated_message!(<Album as Metadata>::Message, Albums);
|
try_from_repeated_message!(<Album as Metadata>::Message, Albums);
|
||||||
|
|
||||||
impl TryFrom<&DiscMessage> for Disc {
|
impl TryFrom<&DiscMessage> for Disc {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(disc: &DiscMessage) -> Result<Self, Self::Error> {
|
fn try_from(disc: &DiscMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
number: disc.get_number(),
|
number: disc.get_number(),
|
||||||
|
|
|
@ -1,23 +1,17 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::{
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
use std::ops::Deref;
|
fmt::Debug,
|
||||||
|
ops::Deref,
|
||||||
use crate::{
|
|
||||||
error::{MetadataError, RequestError},
|
|
||||||
request::RequestResult,
|
|
||||||
track::Tracks,
|
|
||||||
util::try_from_repeated_message,
|
|
||||||
Metadata,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::session::Session;
|
use crate::{request::RequestResult, track::Tracks, util::try_from_repeated_message, Metadata};
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
|
||||||
|
use librespot_core::{Error, Session, SpotifyId};
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use protocol::metadata::ArtistWithRole as ArtistWithRoleMessage;
|
use protocol::metadata::ArtistWithRole as ArtistWithRoleMessage;
|
||||||
use protocol::metadata::TopTracks as TopTracksMessage;
|
|
||||||
|
|
||||||
pub use protocol::metadata::ArtistWithRole_ArtistRole as ArtistRole;
|
pub use protocol::metadata::ArtistWithRole_ArtistRole as ArtistRole;
|
||||||
|
use protocol::metadata::TopTracks as TopTracksMessage;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Artist {
|
pub struct Artist {
|
||||||
|
@ -88,20 +82,16 @@ impl Metadata for Artist {
|
||||||
type Message = protocol::metadata::Artist;
|
type Message = protocol::metadata::Artist;
|
||||||
|
|
||||||
async fn request(session: &Session, artist_id: SpotifyId) -> RequestResult {
|
async fn request(session: &Session, artist_id: SpotifyId) -> RequestResult {
|
||||||
session
|
session.spclient().get_artist_metadata(artist_id).await
|
||||||
.spclient()
|
|
||||||
.get_artist_metadata(artist_id)
|
|
||||||
.await
|
|
||||||
.map_err(RequestError::Http)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
|
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
|
||||||
Self::try_from(msg)
|
Self::try_from(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&<Self as Metadata>::Message> for Artist {
|
impl TryFrom<&<Self as Metadata>::Message> for Artist {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(artist: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
|
fn try_from(artist: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: artist.try_into()?,
|
id: artist.try_into()?,
|
||||||
|
@ -114,7 +104,7 @@ impl TryFrom<&<Self as Metadata>::Message> for Artist {
|
||||||
try_from_repeated_message!(<Artist as Metadata>::Message, Artists);
|
try_from_repeated_message!(<Artist as Metadata>::Message, Artists);
|
||||||
|
|
||||||
impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole {
|
impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result<Self, Self::Error> {
|
fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: artist_with_role.try_into()?,
|
id: artist_with_role.try_into()?,
|
||||||
|
@ -127,7 +117,7 @@ impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole {
|
||||||
try_from_repeated_message!(ArtistWithRoleMessage, ArtistsWithRole);
|
try_from_repeated_message!(ArtistWithRoleMessage, ArtistsWithRole);
|
||||||
|
|
||||||
impl TryFrom<&TopTracksMessage> for TopTracks {
|
impl TryFrom<&TopTracksMessage> for TopTracks {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(top_tracks: &TopTracksMessage) -> Result<Self, Self::Error> {
|
fn try_from(top_tracks: &TopTracksMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
country: top_tracks.get_country().to_owned(),
|
country: top_tracks.get_country().to_owned(),
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, fmt::Debug, ops::Deref};
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::ops::Deref;
|
use librespot_core::FileId;
|
||||||
|
|
||||||
use librespot_core::file_id::FileId;
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use protocol::metadata::AudioFile as AudioFileMessage;
|
use protocol::metadata::AudioFile as AudioFileMessage;
|
||||||
|
|
||||||
pub use protocol::metadata::AudioFile_Format as AudioFileFormat;
|
pub use protocol::metadata::AudioFile_Format as AudioFileFormat;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -12,10 +12,9 @@ use crate::{
|
||||||
|
|
||||||
use super::file::AudioFiles;
|
use super::file::AudioFiles;
|
||||||
|
|
||||||
use librespot_core::session::{Session, UserData};
|
use librespot_core::{session::UserData, spotify_id::SpotifyItemType, Error, Session, SpotifyId};
|
||||||
use librespot_core::spotify_id::{SpotifyId, SpotifyItemType};
|
|
||||||
|
|
||||||
pub type AudioItemResult = Result<AudioItem, MetadataError>;
|
pub type AudioItemResult = Result<AudioItem, Error>;
|
||||||
|
|
||||||
// A wrapper with fields the player needs
|
// A wrapper with fields the player needs
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -34,7 +33,7 @@ impl AudioItem {
|
||||||
match id.item_type {
|
match id.item_type {
|
||||||
SpotifyItemType::Track => Track::get_audio_item(session, id).await,
|
SpotifyItemType::Track => Track::get_audio_item(session, id).await,
|
||||||
SpotifyItemType::Episode => Episode::get_audio_item(session, id).await,
|
SpotifyItemType::Episode => Episode::get_audio_item(session, id).await,
|
||||||
_ => Err(MetadataError::NonPlayable),
|
_ => Err(Error::unavailable(MetadataError::NonPlayable)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
use std::fmt::Debug;
|
use std::{fmt::Debug, ops::Deref};
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::util::from_repeated_message;
|
use crate::util::from_repeated_message;
|
||||||
|
|
||||||
use librespot_core::date::Date;
|
use librespot_core::date::Date;
|
||||||
use librespot_protocol as protocol;
|
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
use protocol::metadata::Availability as AvailabilityMessage;
|
use protocol::metadata::Availability as AvailabilityMessage;
|
||||||
|
|
||||||
pub type AudioItemAvailability = Result<(), UnavailabilityReason>;
|
pub type AudioItemAvailability = Result<(), UnavailabilityReason>;
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
use std::fmt::Debug;
|
use std::{fmt::Debug, ops::Deref};
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use crate::util::from_repeated_message;
|
use crate::util::from_repeated_message;
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use protocol::metadata::ContentRating as ContentRatingMessage;
|
use protocol::metadata::ContentRating as ContentRatingMessage;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
use std::fmt::Debug;
|
use std::{fmt::Debug, ops::Deref};
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
|
||||||
|
|
||||||
use crate::util::from_repeated_message;
|
use crate::util::from_repeated_message;
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
use protocol::metadata::Copyright as CopyrightMessage;
|
use protocol::metadata::Copyright as CopyrightMessage;
|
||||||
|
|
||||||
pub use protocol::metadata::Copyright_Type as CopyrightType;
|
pub use protocol::metadata::Copyright_Type as CopyrightType;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::{
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
use std::ops::Deref;
|
fmt::Debug,
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
audio::{
|
audio::{
|
||||||
|
@ -9,7 +11,6 @@ use crate::{
|
||||||
},
|
},
|
||||||
availability::Availabilities,
|
availability::Availabilities,
|
||||||
content_rating::ContentRatings,
|
content_rating::ContentRatings,
|
||||||
error::{MetadataError, RequestError},
|
|
||||||
image::Images,
|
image::Images,
|
||||||
request::RequestResult,
|
request::RequestResult,
|
||||||
restriction::Restrictions,
|
restriction::Restrictions,
|
||||||
|
@ -18,11 +19,9 @@ use crate::{
|
||||||
Metadata,
|
Metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::date::Date;
|
use librespot_core::{date::Date, Error, Session, SpotifyId};
|
||||||
use librespot_core::session::Session;
|
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
|
||||||
use librespot_protocol as protocol;
|
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
pub use protocol::metadata::Episode_EpisodeType as EpisodeType;
|
pub use protocol::metadata::Episode_EpisodeType as EpisodeType;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -90,20 +89,16 @@ impl Metadata for Episode {
|
||||||
type Message = protocol::metadata::Episode;
|
type Message = protocol::metadata::Episode;
|
||||||
|
|
||||||
async fn request(session: &Session, episode_id: SpotifyId) -> RequestResult {
|
async fn request(session: &Session, episode_id: SpotifyId) -> RequestResult {
|
||||||
session
|
session.spclient().get_episode_metadata(episode_id).await
|
||||||
.spclient()
|
|
||||||
.get_episode_metadata(episode_id)
|
|
||||||
.await
|
|
||||||
.map_err(RequestError::Http)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
|
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
|
||||||
Self::try_from(msg)
|
Self::try_from(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&<Self as Metadata>::Message> for Episode {
|
impl TryFrom<&<Self as Metadata>::Message> for Episode {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(episode: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
|
fn try_from(episode: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: episode.try_into()?,
|
id: episode.try_into()?,
|
||||||
|
|
|
@ -1,35 +1,10 @@
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use protobuf::ProtobufError;
|
|
||||||
|
|
||||||
use librespot_core::date::DateError;
|
|
||||||
use librespot_core::mercury::MercuryError;
|
|
||||||
use librespot_core::spclient::SpClientError;
|
|
||||||
use librespot_core::spotify_id::SpotifyIdError;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum RequestError {
|
|
||||||
#[error("could not get metadata over HTTP: {0}")]
|
|
||||||
Http(#[from] SpClientError),
|
|
||||||
#[error("could not get metadata over Mercury: {0}")]
|
|
||||||
Mercury(#[from] MercuryError),
|
|
||||||
#[error("response was empty")]
|
|
||||||
Empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum MetadataError {
|
pub enum MetadataError {
|
||||||
#[error("{0}")]
|
#[error("empty response")]
|
||||||
InvalidSpotifyId(#[from] SpotifyIdError),
|
Empty,
|
||||||
#[error("item has invalid date")]
|
#[error("audio item is non-playable when it should be")]
|
||||||
InvalidTimestamp(#[from] DateError),
|
|
||||||
#[error("audio item is non-playable")]
|
|
||||||
NonPlayable,
|
NonPlayable,
|
||||||
#[error("could not parse protobuf: {0}")]
|
|
||||||
Protobuf(#[from] ProtobufError),
|
|
||||||
#[error("error executing request: {0}")]
|
|
||||||
Request(#[from] RequestError),
|
|
||||||
#[error("could not parse repeated fields")]
|
|
||||||
InvalidRepeated,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
use std::fmt::Debug;
|
use std::{fmt::Debug, ops::Deref};
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use crate::util::from_repeated_message;
|
use crate::util::from_repeated_message;
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use protocol::metadata::ExternalId as ExternalIdMessage;
|
use protocol::metadata::ExternalId as ExternalIdMessage;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::{
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
use std::ops::Deref;
|
fmt::Debug,
|
||||||
|
ops::Deref,
|
||||||
use crate::{
|
|
||||||
error::MetadataError,
|
|
||||||
util::{from_repeated_message, try_from_repeated_message},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::file_id::FileId;
|
use crate::util::{from_repeated_message, try_from_repeated_message};
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
|
||||||
use librespot_protocol as protocol;
|
|
||||||
|
|
||||||
|
use librespot_core::{FileId, SpotifyId};
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
use protocol::metadata::Image as ImageMessage;
|
use protocol::metadata::Image as ImageMessage;
|
||||||
|
pub use protocol::metadata::Image_Size as ImageSize;
|
||||||
use protocol::playlist4_external::PictureSize as PictureSizeMessage;
|
use protocol::playlist4_external::PictureSize as PictureSizeMessage;
|
||||||
use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage;
|
use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage;
|
||||||
|
|
||||||
pub use protocol::metadata::Image_Size as ImageSize;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
pub id: FileId,
|
pub id: FileId,
|
||||||
|
@ -92,7 +89,7 @@ impl From<&PictureSizeMessage> for PictureSize {
|
||||||
from_repeated_message!(PictureSizeMessage, PictureSizes);
|
from_repeated_message!(PictureSizeMessage, PictureSizes);
|
||||||
|
|
||||||
impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture {
|
impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(picture: &TranscodedPictureMessage) -> Result<Self, Self::Error> {
|
fn try_from(picture: &TranscodedPictureMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
target_name: picture.get_target_name().to_owned(),
|
target_name: picture.get_target_name().to_owned(),
|
||||||
|
|
|
@ -6,8 +6,7 @@ extern crate async_trait;
|
||||||
|
|
||||||
use protobuf::Message;
|
use protobuf::Message;
|
||||||
|
|
||||||
use librespot_core::session::Session;
|
use librespot_core::{Error, Session, SpotifyId};
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
|
||||||
|
|
||||||
pub mod album;
|
pub mod album;
|
||||||
pub mod artist;
|
pub mod artist;
|
||||||
|
@ -46,12 +45,12 @@ pub trait Metadata: Send + Sized + 'static {
|
||||||
async fn request(session: &Session, id: SpotifyId) -> RequestResult;
|
async fn request(session: &Session, id: SpotifyId) -> RequestResult;
|
||||||
|
|
||||||
// Request a metadata struct
|
// Request a metadata struct
|
||||||
async fn get(session: &Session, id: SpotifyId) -> Result<Self, MetadataError> {
|
async fn get(session: &Session, id: SpotifyId) -> Result<Self, Error> {
|
||||||
let response = Self::request(session, id).await?;
|
let response = Self::request(session, id).await?;
|
||||||
let msg = Self::Message::parse_from_bytes(&response)?;
|
let msg = Self::Message::parse_from_bytes(&response)?;
|
||||||
trace!("Received metadata: {:#?}", msg);
|
trace!("Received metadata: {:#?}", msg);
|
||||||
Self::parse(&msg, id)
|
Self::parse(&msg, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError>;
|
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,14 @@ use std::fmt::Debug;
|
||||||
use protobuf::Message;
|
use protobuf::Message;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::MetadataError,
|
|
||||||
image::TranscodedPictures,
|
image::TranscodedPictures,
|
||||||
request::{MercuryRequest, RequestResult},
|
request::{MercuryRequest, RequestResult},
|
||||||
Metadata,
|
Metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::session::Session;
|
use librespot_core::{Error, Session, SpotifyId};
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
|
||||||
use librespot_protocol as protocol;
|
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
pub use protocol::playlist_annotate3::AbuseReportState;
|
pub use protocol::playlist_annotate3::AbuseReportState;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -34,7 +32,7 @@ impl Metadata for PlaylistAnnotation {
|
||||||
Self::request_for_user(session, ¤t_user, playlist_id).await
|
Self::request_for_user(session, ¤t_user, playlist_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
|
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
description: msg.get_description().to_owned(),
|
description: msg.get_description().to_owned(),
|
||||||
picture: msg.get_picture().to_owned(), // TODO: is this a URL or Spotify URI?
|
picture: msg.get_picture().to_owned(), // TODO: is this a URL or Spotify URI?
|
||||||
|
@ -64,7 +62,7 @@ impl PlaylistAnnotation {
|
||||||
session: &Session,
|
session: &Session,
|
||||||
username: &str,
|
username: &str,
|
||||||
playlist_id: SpotifyId,
|
playlist_id: SpotifyId,
|
||||||
) -> Result<Self, MetadataError> {
|
) -> Result<Self, Error> {
|
||||||
let response = Self::request_for_user(session, username, playlist_id).await?;
|
let response = Self::request_for_user(session, username, playlist_id).await?;
|
||||||
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
|
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
|
||||||
Self::parse(&msg, playlist_id)
|
Self::parse(&msg, playlist_id)
|
||||||
|
@ -74,7 +72,7 @@ impl PlaylistAnnotation {
|
||||||
impl MercuryRequest for PlaylistAnnotation {}
|
impl MercuryRequest for PlaylistAnnotation {}
|
||||||
|
|
||||||
impl TryFrom<&<PlaylistAnnotation as Metadata>::Message> for PlaylistAnnotation {
|
impl TryFrom<&<PlaylistAnnotation as Metadata>::Message> for PlaylistAnnotation {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(
|
fn try_from(
|
||||||
annotation: &<PlaylistAnnotation as Metadata>::Message,
|
annotation: &<PlaylistAnnotation as Metadata>::Message,
|
||||||
) -> Result<Self, Self::Error> {
|
) -> Result<Self, Self::Error> {
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
use std::collections::HashMap;
|
use std::{
|
||||||
use std::convert::{TryFrom, TryInto};
|
collections::HashMap,
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
use std::ops::Deref;
|
fmt::Debug,
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{error::MetadataError, image::PictureSizes, util::from_repeated_enum};
|
use crate::{image::PictureSizes, util::from_repeated_enum};
|
||||||
|
|
||||||
|
use librespot_core::{date::Date, SpotifyId};
|
||||||
|
|
||||||
use librespot_core::date::Date;
|
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use protocol::playlist4_external::FormatListAttribute as PlaylistFormatAttributeMessage;
|
use protocol::playlist4_external::FormatListAttribute as PlaylistFormatAttributeMessage;
|
||||||
|
pub use protocol::playlist4_external::ItemAttributeKind as PlaylistItemAttributeKind;
|
||||||
use protocol::playlist4_external::ItemAttributes as PlaylistItemAttributesMessage;
|
use protocol::playlist4_external::ItemAttributes as PlaylistItemAttributesMessage;
|
||||||
use protocol::playlist4_external::ItemAttributesPartialState as PlaylistPartialItemAttributesMessage;
|
use protocol::playlist4_external::ItemAttributesPartialState as PlaylistPartialItemAttributesMessage;
|
||||||
|
pub use protocol::playlist4_external::ListAttributeKind as PlaylistAttributeKind;
|
||||||
use protocol::playlist4_external::ListAttributes as PlaylistAttributesMessage;
|
use protocol::playlist4_external::ListAttributes as PlaylistAttributesMessage;
|
||||||
use protocol::playlist4_external::ListAttributesPartialState as PlaylistPartialAttributesMessage;
|
use protocol::playlist4_external::ListAttributesPartialState as PlaylistPartialAttributesMessage;
|
||||||
use protocol::playlist4_external::UpdateItemAttributes as PlaylistUpdateItemAttributesMessage;
|
use protocol::playlist4_external::UpdateItemAttributes as PlaylistUpdateItemAttributesMessage;
|
||||||
use protocol::playlist4_external::UpdateListAttributes as PlaylistUpdateAttributesMessage;
|
use protocol::playlist4_external::UpdateListAttributes as PlaylistUpdateAttributesMessage;
|
||||||
|
|
||||||
pub use protocol::playlist4_external::ItemAttributeKind as PlaylistItemAttributeKind;
|
|
||||||
pub use protocol::playlist4_external::ListAttributeKind as PlaylistAttributeKind;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PlaylistAttributes {
|
pub struct PlaylistAttributes {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -108,7 +108,7 @@ pub struct PlaylistUpdateItemAttributes {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&PlaylistAttributesMessage> for PlaylistAttributes {
|
impl TryFrom<&PlaylistAttributesMessage> for PlaylistAttributes {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(attributes: &PlaylistAttributesMessage) -> Result<Self, Self::Error> {
|
fn try_from(attributes: &PlaylistAttributesMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
name: attributes.get_name().to_owned(),
|
name: attributes.get_name().to_owned(),
|
||||||
|
@ -142,7 +142,7 @@ impl From<&[PlaylistFormatAttributeMessage]> for PlaylistFormatAttribute {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes {
|
impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result<Self, Self::Error> {
|
fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
added_by: attributes.get_added_by().to_owned(),
|
added_by: attributes.get_added_by().to_owned(),
|
||||||
|
@ -155,7 +155,7 @@ impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes {
|
impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result<Self, Self::Error> {
|
fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
values: attributes.get_values().try_into()?,
|
values: attributes.get_values().try_into()?,
|
||||||
|
@ -165,7 +165,7 @@ impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttributes {
|
impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttributes {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Result<Self, Self::Error> {
|
fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
values: attributes.get_values().try_into()?,
|
values: attributes.get_values().try_into()?,
|
||||||
|
@ -175,7 +175,7 @@ impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttri
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes {
|
impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result<Self, Self::Error> {
|
fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
new_attributes: update.get_new_attributes().try_into()?,
|
new_attributes: update.get_new_attributes().try_into()?,
|
||||||
|
@ -185,7 +185,7 @@ impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&PlaylistUpdateItemAttributesMessage> for PlaylistUpdateItemAttributes {
|
impl TryFrom<&PlaylistUpdateItemAttributesMessage> for PlaylistUpdateItemAttributes {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(update: &PlaylistUpdateItemAttributesMessage) -> Result<Self, Self::Error> {
|
fn try_from(update: &PlaylistUpdateItemAttributesMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
index: update.get_index(),
|
index: update.get_index(),
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::{
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
|
fmt::Debug,
|
||||||
use crate::error::MetadataError;
|
};
|
||||||
|
|
||||||
use super::operation::PlaylistOperations;
|
use super::operation::PlaylistOperations;
|
||||||
|
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
use librespot_core::SpotifyId;
|
||||||
use librespot_protocol as protocol;
|
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
use protocol::playlist4_external::Diff as DiffMessage;
|
use protocol::playlist4_external::Diff as DiffMessage;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -18,7 +18,7 @@ pub struct PlaylistDiff {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&DiffMessage> for PlaylistDiff {
|
impl TryFrom<&DiffMessage> for PlaylistDiff {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(diff: &DiffMessage) -> Result<Self, Self::Error> {
|
fn try_from(diff: &DiffMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
from_revision: diff.get_from_revision().try_into()?,
|
from_revision: diff.get_from_revision().try_into()?,
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::{
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
use std::ops::Deref;
|
fmt::Debug,
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{error::MetadataError, util::try_from_repeated_message};
|
use crate::util::try_from_repeated_message;
|
||||||
|
|
||||||
use super::attribute::{PlaylistAttributes, PlaylistItemAttributes};
|
use super::{
|
||||||
|
attribute::{PlaylistAttributes, PlaylistItemAttributes},
|
||||||
|
permission::Capabilities,
|
||||||
|
};
|
||||||
|
|
||||||
|
use librespot_core::{date::Date, SpotifyId};
|
||||||
|
|
||||||
use librespot_core::date::Date;
|
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use super::permission::Capabilities;
|
|
||||||
|
|
||||||
use protocol::playlist4_external::Item as PlaylistItemMessage;
|
use protocol::playlist4_external::Item as PlaylistItemMessage;
|
||||||
use protocol::playlist4_external::ListItems as PlaylistItemsMessage;
|
use protocol::playlist4_external::ListItems as PlaylistItemsMessage;
|
||||||
use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage;
|
use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage;
|
||||||
|
@ -62,7 +64,7 @@ impl Deref for PlaylistMetaItems {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&PlaylistItemMessage> for PlaylistItem {
|
impl TryFrom<&PlaylistItemMessage> for PlaylistItem {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(item: &PlaylistItemMessage) -> Result<Self, Self::Error> {
|
fn try_from(item: &PlaylistItemMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: item.try_into()?,
|
id: item.try_into()?,
|
||||||
|
@ -74,7 +76,7 @@ impl TryFrom<&PlaylistItemMessage> for PlaylistItem {
|
||||||
try_from_repeated_message!(PlaylistItemMessage, PlaylistItems);
|
try_from_repeated_message!(PlaylistItemMessage, PlaylistItems);
|
||||||
|
|
||||||
impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList {
|
impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(list_items: &PlaylistItemsMessage) -> Result<Self, Self::Error> {
|
fn try_from(list_items: &PlaylistItemsMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
position: list_items.get_pos(),
|
position: list_items.get_pos(),
|
||||||
|
@ -86,7 +88,7 @@ impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem {
|
impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(item: &PlaylistMetaItemMessage) -> Result<Self, Self::Error> {
|
fn try_from(item: &PlaylistMetaItemMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
revision: item.try_into()?,
|
revision: item.try_into()?,
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::{
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
use std::ops::Deref;
|
fmt::Debug,
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
use protobuf::Message;
|
use protobuf::Message;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::MetadataError,
|
|
||||||
request::{MercuryRequest, RequestResult},
|
request::{MercuryRequest, RequestResult},
|
||||||
util::{from_repeated_enum, try_from_repeated_message},
|
util::{from_repeated_enum, try_from_repeated_message},
|
||||||
Metadata,
|
Metadata,
|
||||||
|
@ -16,11 +17,13 @@ use super::{
|
||||||
permission::Capabilities,
|
permission::Capabilities,
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::date::Date;
|
use librespot_core::{
|
||||||
use librespot_core::session::Session;
|
date::Date,
|
||||||
use librespot_core::spotify_id::{NamedSpotifyId, SpotifyId};
|
spotify_id::{NamedSpotifyId, SpotifyId},
|
||||||
use librespot_protocol as protocol;
|
Error, Session,
|
||||||
|
};
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
use protocol::playlist4_external::GeoblockBlockingType as Geoblock;
|
use protocol::playlist4_external::GeoblockBlockingType as Geoblock;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -111,7 +114,7 @@ impl Playlist {
|
||||||
session: &Session,
|
session: &Session,
|
||||||
username: &str,
|
username: &str,
|
||||||
playlist_id: SpotifyId,
|
playlist_id: SpotifyId,
|
||||||
) -> Result<Self, MetadataError> {
|
) -> Result<Self, Error> {
|
||||||
let response = Self::request_for_user(session, username, playlist_id).await?;
|
let response = Self::request_for_user(session, username, playlist_id).await?;
|
||||||
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
|
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
|
||||||
Self::parse(&msg, playlist_id)
|
Self::parse(&msg, playlist_id)
|
||||||
|
@ -153,7 +156,7 @@ impl Metadata for Playlist {
|
||||||
<Self as MercuryRequest>::request(session, &uri).await
|
<Self as MercuryRequest>::request(session, &uri).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, id: SpotifyId) -> Result<Self, MetadataError> {
|
fn parse(msg: &Self::Message, id: SpotifyId) -> Result<Self, Error> {
|
||||||
// the playlist proto doesn't contain the id so we decorate it
|
// the playlist proto doesn't contain the id so we decorate it
|
||||||
let playlist = SelectedListContent::try_from(msg)?;
|
let playlist = SelectedListContent::try_from(msg)?;
|
||||||
let id = NamedSpotifyId::from_spotify_id(id, playlist.owner_username);
|
let id = NamedSpotifyId::from_spotify_id(id, playlist.owner_username);
|
||||||
|
@ -188,10 +191,7 @@ impl RootPlaylist {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub async fn get_root_for_user(
|
pub async fn get_root_for_user(session: &Session, username: &str) -> Result<Self, Error> {
|
||||||
session: &Session,
|
|
||||||
username: &str,
|
|
||||||
) -> Result<Self, MetadataError> {
|
|
||||||
let response = Self::request_for_user(session, username).await?;
|
let response = Self::request_for_user(session, username).await?;
|
||||||
let msg = protocol::playlist4_external::SelectedListContent::parse_from_bytes(&response)?;
|
let msg = protocol::playlist4_external::SelectedListContent::parse_from_bytes(&response)?;
|
||||||
Ok(Self(SelectedListContent::try_from(&msg)?))
|
Ok(Self(SelectedListContent::try_from(&msg)?))
|
||||||
|
@ -199,7 +199,7 @@ impl RootPlaylist {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&<Playlist as Metadata>::Message> for SelectedListContent {
|
impl TryFrom<&<Playlist as Metadata>::Message> for SelectedListContent {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self, Self::Error> {
|
fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
revision: playlist.get_revision().try_into()?,
|
revision: playlist.get_revision().try_into()?,
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::{
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
use std::ops::Deref;
|
fmt::Debug,
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::MetadataError,
|
|
||||||
playlist::{
|
playlist::{
|
||||||
attribute::{PlaylistUpdateAttributes, PlaylistUpdateItemAttributes},
|
attribute::{PlaylistUpdateAttributes, PlaylistUpdateItemAttributes},
|
||||||
item::PlaylistItems,
|
item::PlaylistItems,
|
||||||
|
@ -12,13 +13,11 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use protocol::playlist4_external::Add as PlaylistAddMessage;
|
use protocol::playlist4_external::Add as PlaylistAddMessage;
|
||||||
use protocol::playlist4_external::Mov as PlaylistMoveMessage;
|
use protocol::playlist4_external::Mov as PlaylistMoveMessage;
|
||||||
use protocol::playlist4_external::Op as PlaylistOperationMessage;
|
use protocol::playlist4_external::Op as PlaylistOperationMessage;
|
||||||
use protocol::playlist4_external::Rem as PlaylistRemoveMessage;
|
|
||||||
|
|
||||||
pub use protocol::playlist4_external::Op_Kind as PlaylistOperationKind;
|
pub use protocol::playlist4_external::Op_Kind as PlaylistOperationKind;
|
||||||
|
use protocol::playlist4_external::Rem as PlaylistRemoveMessage;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PlaylistOperation {
|
pub struct PlaylistOperation {
|
||||||
|
@ -64,7 +63,7 @@ pub struct PlaylistOperationRemove {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation {
|
impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(operation: &PlaylistOperationMessage) -> Result<Self, Self::Error> {
|
fn try_from(operation: &PlaylistOperationMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
kind: operation.get_kind(),
|
kind: operation.get_kind(),
|
||||||
|
@ -80,7 +79,7 @@ impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation {
|
||||||
try_from_repeated_message!(PlaylistOperationMessage, PlaylistOperations);
|
try_from_repeated_message!(PlaylistOperationMessage, PlaylistOperations);
|
||||||
|
|
||||||
impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd {
|
impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(add: &PlaylistAddMessage) -> Result<Self, Self::Error> {
|
fn try_from(add: &PlaylistAddMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
from_index: add.get_from_index(),
|
from_index: add.get_from_index(),
|
||||||
|
@ -102,7 +101,7 @@ impl From<&PlaylistMoveMessage> for PlaylistOperationMove {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&PlaylistRemoveMessage> for PlaylistOperationRemove {
|
impl TryFrom<&PlaylistRemoveMessage> for PlaylistOperationRemove {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(remove: &PlaylistRemoveMessage) -> Result<Self, Self::Error> {
|
fn try_from(remove: &PlaylistRemoveMessage) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
from_index: remove.get_from_index(),
|
from_index: remove.get_from_index(),
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
use std::fmt::Debug;
|
use std::{fmt::Debug, ops::Deref};
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use crate::util::from_repeated_enum;
|
use crate::util::from_repeated_enum;
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use protocol::playlist_permission::Capabilities as CapabilitiesMessage;
|
use protocol::playlist_permission::Capabilities as CapabilitiesMessage;
|
||||||
use protocol::playlist_permission::PermissionLevel;
|
use protocol::playlist_permission::PermissionLevel;
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
use crate::error::RequestError;
|
use crate::MetadataError;
|
||||||
|
|
||||||
use librespot_core::session::Session;
|
use librespot_core::{Error, Session};
|
||||||
|
|
||||||
pub type RequestResult = Result<bytes::Bytes, RequestError>;
|
pub type RequestResult = Result<bytes::Bytes, Error>;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait MercuryRequest {
|
pub trait MercuryRequest {
|
||||||
async fn request(session: &Session, uri: &str) -> RequestResult {
|
async fn request(session: &Session, uri: &str) -> RequestResult {
|
||||||
let response = session.mercury().get(uri).await?;
|
let request = session.mercury().get(uri)?;
|
||||||
|
let response = request.await?;
|
||||||
match response.payload.first() {
|
match response.payload.first() {
|
||||||
Some(data) => {
|
Some(data) => {
|
||||||
let data = data.to_vec().into();
|
let data = data.to_vec().into();
|
||||||
trace!("Received metadata: {:?}", data);
|
trace!("Received metadata: {:?}", data);
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
None => Err(RequestError::Empty),
|
None => Err(Error::unavailable(MetadataError::Empty)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
use std::fmt::Debug;
|
use std::{fmt::Debug, ops::Deref};
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use crate::util::{from_repeated_enum, from_repeated_message};
|
use crate::util::{from_repeated_enum, from_repeated_message};
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
|
||||||
|
|
||||||
use protocol::metadata::Restriction as RestrictionMessage;
|
use protocol::metadata::Restriction as RestrictionMessage;
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
pub use protocol::metadata::Restriction_Catalogue as RestrictionCatalogue;
|
pub use protocol::metadata::Restriction_Catalogue as RestrictionCatalogue;
|
||||||
pub use protocol::metadata::Restriction_Type as RestrictionType;
|
pub use protocol::metadata::Restriction_Type as RestrictionType;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
use std::fmt::Debug;
|
use std::{fmt::Debug, ops::Deref};
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use crate::{restriction::Restrictions, util::from_repeated_message};
|
use crate::{restriction::Restrictions, util::from_repeated_message};
|
||||||
|
|
||||||
use librespot_core::date::Date;
|
use librespot_core::date::Date;
|
||||||
use librespot_protocol as protocol;
|
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
use protocol::metadata::SalePeriod as SalePeriodMessage;
|
use protocol::metadata::SalePeriod as SalePeriodMessage;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::{
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
|
fmt::Debug,
|
||||||
use crate::{
|
|
||||||
availability::Availabilities, copyright::Copyrights, episode::Episodes, error::RequestError,
|
|
||||||
image::Images, restriction::Restrictions, Metadata, MetadataError, RequestResult,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::session::Session;
|
use crate::{
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
availability::Availabilities, copyright::Copyrights, episode::Episodes, image::Images,
|
||||||
use librespot_protocol as protocol;
|
restriction::Restrictions, Metadata, RequestResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use librespot_core::{Error, Session, SpotifyId};
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
pub use protocol::metadata::Show_ConsumptionOrder as ShowConsumptionOrder;
|
pub use protocol::metadata::Show_ConsumptionOrder as ShowConsumptionOrder;
|
||||||
pub use protocol::metadata::Show_MediaType as ShowMediaType;
|
pub use protocol::metadata::Show_MediaType as ShowMediaType;
|
||||||
|
|
||||||
|
@ -39,20 +40,16 @@ impl Metadata for Show {
|
||||||
type Message = protocol::metadata::Show;
|
type Message = protocol::metadata::Show;
|
||||||
|
|
||||||
async fn request(session: &Session, show_id: SpotifyId) -> RequestResult {
|
async fn request(session: &Session, show_id: SpotifyId) -> RequestResult {
|
||||||
session
|
session.spclient().get_show_metadata(show_id).await
|
||||||
.spclient()
|
|
||||||
.get_show_metadata(show_id)
|
|
||||||
.await
|
|
||||||
.map_err(RequestError::Http)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
|
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
|
||||||
Self::try_from(msg)
|
Self::try_from(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&<Self as Metadata>::Message> for Show {
|
impl TryFrom<&<Self as Metadata>::Message> for Show {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(show: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
|
fn try_from(show: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: show.try_into()?,
|
id: show.try_into()?,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::{
|
||||||
use std::fmt::Debug;
|
convert::{TryFrom, TryInto},
|
||||||
use std::ops::Deref;
|
fmt::Debug,
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -13,17 +15,14 @@ use crate::{
|
||||||
},
|
},
|
||||||
availability::{Availabilities, UnavailabilityReason},
|
availability::{Availabilities, UnavailabilityReason},
|
||||||
content_rating::ContentRatings,
|
content_rating::ContentRatings,
|
||||||
error::RequestError,
|
|
||||||
external_id::ExternalIds,
|
external_id::ExternalIds,
|
||||||
restriction::Restrictions,
|
restriction::Restrictions,
|
||||||
sale_period::SalePeriods,
|
sale_period::SalePeriods,
|
||||||
util::try_from_repeated_message,
|
util::try_from_repeated_message,
|
||||||
Metadata, MetadataError, RequestResult,
|
Metadata, RequestResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::date::Date;
|
use librespot_core::{date::Date, Error, Session, SpotifyId};
|
||||||
use librespot_core::session::Session;
|
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -105,20 +104,16 @@ impl Metadata for Track {
|
||||||
type Message = protocol::metadata::Track;
|
type Message = protocol::metadata::Track;
|
||||||
|
|
||||||
async fn request(session: &Session, track_id: SpotifyId) -> RequestResult {
|
async fn request(session: &Session, track_id: SpotifyId) -> RequestResult {
|
||||||
session
|
session.spclient().get_track_metadata(track_id).await
|
||||||
.spclient()
|
|
||||||
.get_track_metadata(track_id)
|
|
||||||
.await
|
|
||||||
.map_err(RequestError::Http)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
|
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
|
||||||
Self::try_from(msg)
|
Self::try_from(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&<Self as Metadata>::Message> for Track {
|
impl TryFrom<&<Self as Metadata>::Message> for Track {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(track: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
|
fn try_from(track: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: track.try_into()?,
|
id: track.try_into()?,
|
||||||
|
|
|
@ -27,7 +27,7 @@ pub(crate) use from_repeated_enum;
|
||||||
macro_rules! try_from_repeated_message {
|
macro_rules! try_from_repeated_message {
|
||||||
($src:ty, $dst:ty) => {
|
($src:ty, $dst:ty) => {
|
||||||
impl TryFrom<&[$src]> for $dst {
|
impl TryFrom<&[$src]> for $dst {
|
||||||
type Error = MetadataError;
|
type Error = librespot_core::Error;
|
||||||
fn try_from(src: &[$src]) -> Result<Self, Self::Error> {
|
fn try_from(src: &[$src]) -> Result<Self, Self::Error> {
|
||||||
let result: Result<Vec<_>, _> = src.iter().map(TryFrom::try_from).collect();
|
let result: Result<Vec<_>, _> = src.iter().map(TryFrom::try_from).collect();
|
||||||
Ok(Self(result?))
|
Ok(Self(result?))
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
use std::fmt::Debug;
|
use std::{fmt::Debug, ops::Deref};
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use crate::util::from_repeated_message;
|
use crate::util::from_repeated_message;
|
||||||
|
|
||||||
use librespot_core::file_id::FileId;
|
use librespot_core::FileId;
|
||||||
use librespot_protocol as protocol;
|
|
||||||
|
|
||||||
|
use librespot_protocol as protocol;
|
||||||
use protocol::metadata::VideoFile as VideoFileMessage;
|
use protocol::metadata::VideoFile as VideoFileMessage;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -23,9 +23,9 @@ futures-util = { version = "0.3", default_features = false, features = ["alloc"]
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
byteorder = "1.4"
|
byteorder = "1.4"
|
||||||
shell-words = "1.0.0"
|
shell-words = "1.0.0"
|
||||||
|
thiserror = "1.0"
|
||||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] }
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] }
|
||||||
zerocopy = { version = "0.3" }
|
zerocopy = { version = "0.3" }
|
||||||
thiserror = { version = "1" }
|
|
||||||
|
|
||||||
# Backends
|
# Backends
|
||||||
alsa = { version = "0.5", optional = true }
|
alsa = { version = "0.5", optional = true }
|
||||||
|
|
|
@ -1,45 +1,40 @@
|
||||||
use std::cmp::max;
|
use std::{
|
||||||
use std::future::Future;
|
cmp::max,
|
||||||
use std::io::{self, Read, Seek, SeekFrom};
|
future::Future,
|
||||||
use std::pin::Pin;
|
io::{self, Read, Seek, SeekFrom},
|
||||||
use std::process::exit;
|
mem,
|
||||||
use std::task::{Context, Poll};
|
pin::Pin,
|
||||||
use std::time::{Duration, Instant};
|
process::exit,
|
||||||
use std::{mem, thread};
|
task::{Context, Poll},
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
use futures_util::stream::futures_unordered::FuturesUnordered;
|
use futures_util::{future, stream::futures_unordered::FuturesUnordered, StreamExt, TryFutureExt};
|
||||||
use futures_util::{future, StreamExt, TryFutureExt};
|
|
||||||
use thiserror::Error;
|
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
use crate::audio::{AudioDecrypt, AudioFile, AudioFileError, StreamLoaderController};
|
use crate::{
|
||||||
use crate::audio::{
|
audio::{
|
||||||
READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK,
|
AudioDecrypt, AudioFile, StreamLoaderController, READ_AHEAD_BEFORE_PLAYBACK,
|
||||||
|
READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK,
|
||||||
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,
|
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,
|
||||||
|
},
|
||||||
|
audio_backend::Sink,
|
||||||
|
config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
|
||||||
|
convert::Converter,
|
||||||
|
core::{util::SeqGenerator, Error, Session, SpotifyId},
|
||||||
|
decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder},
|
||||||
|
metadata::audio::{AudioFileFormat, AudioItem},
|
||||||
|
mixer::AudioFilter,
|
||||||
};
|
};
|
||||||
use crate::audio_backend::Sink;
|
|
||||||
use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig};
|
|
||||||
use crate::convert::Converter;
|
|
||||||
use crate::core::session::Session;
|
|
||||||
use crate::core::spotify_id::SpotifyId;
|
|
||||||
use crate::core::util::SeqGenerator;
|
|
||||||
use crate::decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder};
|
|
||||||
use crate::metadata::audio::{AudioFileFormat, AudioItem};
|
|
||||||
use crate::mixer::AudioFilter;
|
|
||||||
|
|
||||||
use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND};
|
use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND};
|
||||||
|
|
||||||
const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
|
const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
|
||||||
pub const DB_VOLTAGE_RATIO: f64 = 20.0;
|
pub const DB_VOLTAGE_RATIO: f64 = 20.0;
|
||||||
|
|
||||||
pub type PlayerResult = Result<(), PlayerError>;
|
pub type PlayerResult = Result<(), Error>;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum PlayerError {
|
|
||||||
#[error("audio file error: {0}")]
|
|
||||||
AudioFile(#[from] AudioFileError),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
commands: Option<mpsc::UnboundedSender<PlayerCommand>>,
|
commands: Option<mpsc::UnboundedSender<PlayerCommand>>,
|
||||||
|
@ -755,7 +750,7 @@ impl PlayerTrackLoader {
|
||||||
let audio = match self.find_available_alternative(audio).await {
|
let audio = match self.find_available_alternative(audio).await {
|
||||||
Some(audio) => audio,
|
Some(audio) => audio,
|
||||||
None => {
|
None => {
|
||||||
warn!("<{}> is not available", spotify_id.to_uri());
|
error!("<{}> is not available", spotify_id.to_uri());
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -801,7 +796,7 @@ impl PlayerTrackLoader {
|
||||||
let (format, file_id) = match entry {
|
let (format, file_id) = match entry {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
warn!("<{}> is not available in any supported format", audio.name);
|
error!("<{}> is not available in any supported format", audio.name);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -973,7 +968,7 @@ impl Future for PlayerInternal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Poll::Ready(Err(e)) => {
|
Poll::Ready(Err(e)) => {
|
||||||
warn!(
|
error!(
|
||||||
"Skipping to next track, unable to load track <{:?}>: {:?}",
|
"Skipping to next track, unable to load track <{:?}>: {:?}",
|
||||||
track_id, e
|
track_id, e
|
||||||
);
|
);
|
||||||
|
@ -1077,7 +1072,7 @@ impl Future for PlayerInternal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Skipping to next track, unable to decode samples for track <{:?}>: {:?}", track_id, e);
|
error!("Skipping to next track, unable to decode samples for track <{:?}>: {:?}", track_id, e);
|
||||||
self.send_event(PlayerEvent::EndOfTrack {
|
self.send_event(PlayerEvent::EndOfTrack {
|
||||||
track_id,
|
track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
|
@ -1093,7 +1088,7 @@ impl Future for PlayerInternal {
|
||||||
self.handle_packet(packet, normalisation_factor);
|
self.handle_packet(packet, normalisation_factor);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e);
|
error!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e);
|
||||||
self.send_event(PlayerEvent::EndOfTrack {
|
self.send_event(PlayerEvent::EndOfTrack {
|
||||||
track_id,
|
track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
|
@ -1128,9 +1123,7 @@ impl Future for PlayerInternal {
|
||||||
if (!*suggested_to_preload_next_track)
|
if (!*suggested_to_preload_next_track)
|
||||||
&& ((duration_ms as i64 - Self::position_pcm_to_ms(stream_position_pcm) as i64)
|
&& ((duration_ms as i64 - Self::position_pcm_to_ms(stream_position_pcm) as i64)
|
||||||
< PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64)
|
< PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64)
|
||||||
&& stream_loader_controller
|
&& stream_loader_controller.range_to_end_available()
|
||||||
.range_to_end_available()
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
{
|
||||||
*suggested_to_preload_next_track = true;
|
*suggested_to_preload_next_track = true;
|
||||||
self.send_event(PlayerEvent::TimeToPreloadNextTrack {
|
self.send_event(PlayerEvent::TimeToPreloadNextTrack {
|
||||||
|
@ -1266,7 +1259,7 @@ impl PlayerInternal {
|
||||||
});
|
});
|
||||||
self.ensure_sink_running();
|
self.ensure_sink_running();
|
||||||
} else {
|
} else {
|
||||||
warn!("Player::play called from invalid state");
|
error!("Player::play called from invalid state");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1290,7 +1283,7 @@ impl PlayerInternal {
|
||||||
duration_ms,
|
duration_ms,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
warn!("Player::pause called from invalid state");
|
error!("Player::pause called from invalid state");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1830,7 +1823,7 @@ impl PlayerInternal {
|
||||||
Err(e) => error!("PlayerInternal handle_command_seek: {}", e),
|
Err(e) => error!("PlayerInternal handle_command_seek: {}", e),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Player::seek called from invalid state");
|
error!("Player::seek called from invalid state");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're playing, ensure, that we have enough data leaded to avoid a buffer underrun.
|
// If we're playing, ensure, that we have enough data leaded to avoid a buffer underrun.
|
||||||
|
@ -1953,7 +1946,7 @@ impl PlayerInternal {
|
||||||
result_rx.map_err(|_| ())
|
result_rx.map_err(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preload_data_before_playback(&mut self) -> Result<(), PlayerError> {
|
fn preload_data_before_playback(&mut self) -> PlayerResult {
|
||||||
if let PlayerState::Playing {
|
if let PlayerState::Playing {
|
||||||
bytes_per_second,
|
bytes_per_second,
|
||||||
ref mut stream_loader_controller,
|
ref mut stream_loader_controller,
|
||||||
|
@ -1978,7 +1971,7 @@ impl PlayerInternal {
|
||||||
);
|
);
|
||||||
stream_loader_controller
|
stream_loader_controller
|
||||||
.fetch_next_blocking(wait_for_data_length)
|
.fetch_next_blocking(wait_for_data_length)
|
||||||
.map_err(|e| e.into())
|
.map_err(Into::into)
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
66
src/main.rs
66
src/main.rs
|
@ -1,3 +1,14 @@
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
fs::create_dir_all,
|
||||||
|
ops::RangeInclusive,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
pin::Pin,
|
||||||
|
process::exit,
|
||||||
|
str::FromStr,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use futures_util::{future, FutureExt, StreamExt};
|
use futures_util::{future, FutureExt, StreamExt};
|
||||||
use librespot_playback::player::PlayerEvent;
|
use librespot_playback::player::PlayerEvent;
|
||||||
use log::{error, info, trace, warn};
|
use log::{error, info, trace, warn};
|
||||||
|
@ -6,35 +17,31 @@ use thiserror::Error;
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use librespot::connect::spirc::Spirc;
|
use librespot::{
|
||||||
use librespot::core::authentication::Credentials;
|
connect::spirc::Spirc,
|
||||||
use librespot::core::cache::Cache;
|
core::{
|
||||||
use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig};
|
authentication::Credentials,
|
||||||
use librespot::core::session::Session;
|
cache::Cache,
|
||||||
use librespot::core::version;
|
config::{ConnectConfig, DeviceType},
|
||||||
use librespot::playback::audio_backend::{self, SinkBuilder, BACKENDS};
|
version, Session, SessionConfig,
|
||||||
use librespot::playback::config::{
|
},
|
||||||
|
playback::{
|
||||||
|
audio_backend::{self, SinkBuilder, BACKENDS},
|
||||||
|
config::{
|
||||||
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
|
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
|
||||||
|
},
|
||||||
|
dither,
|
||||||
|
mixer::{self, MixerConfig, MixerFn},
|
||||||
|
player::{db_to_ratio, ratio_to_db, Player},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use librespot::playback::dither;
|
|
||||||
#[cfg(feature = "alsa-backend")]
|
#[cfg(feature = "alsa-backend")]
|
||||||
use librespot::playback::mixer::alsamixer::AlsaMixer;
|
use librespot::playback::mixer::alsamixer::AlsaMixer;
|
||||||
use librespot::playback::mixer::{self, MixerConfig, MixerFn};
|
|
||||||
use librespot::playback::player::{db_to_ratio, ratio_to_db, Player};
|
|
||||||
|
|
||||||
mod player_event_handler;
|
mod player_event_handler;
|
||||||
use player_event_handler::{emit_sink_event, run_program_on_events};
|
use player_event_handler::{emit_sink_event, run_program_on_events};
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::fs::create_dir_all;
|
|
||||||
use std::ops::RangeInclusive;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::process::exit;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
fn device_id(name: &str) -> String {
|
fn device_id(name: &str) -> String {
|
||||||
hex::encode(Sha1::digest(name.as_bytes()))
|
hex::encode(Sha1::digest(name.as_bytes()))
|
||||||
}
|
}
|
||||||
|
@ -1530,7 +1537,9 @@ async fn main() {
|
||||||
auto_connect_times.clear();
|
auto_connect_times.clear();
|
||||||
|
|
||||||
if let Some(spirc) = spirc.take() {
|
if let Some(spirc) = spirc.take() {
|
||||||
spirc.shutdown();
|
if let Err(e) = spirc.shutdown() {
|
||||||
|
error!("error sending spirc shutdown message: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(spirc_task) = spirc_task.take() {
|
if let Some(spirc_task) = spirc_task.take() {
|
||||||
// Continue shutdown in its own task
|
// Continue shutdown in its own task
|
||||||
|
@ -1585,8 +1594,13 @@ async fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (spirc_, spirc_task_) = Spirc::new(connect_config, session, player, mixer);
|
let (spirc_, spirc_task_) = match Spirc::new(connect_config, session, player, mixer) {
|
||||||
|
Ok((spirc_, spirc_task_)) => (spirc_, spirc_task_),
|
||||||
|
Err(e) => {
|
||||||
|
error!("could not initialize spirc: {}", e);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
spirc = Some(spirc_);
|
spirc = Some(spirc_);
|
||||||
spirc_task = Some(Box::pin(spirc_task_));
|
spirc_task = Some(Box::pin(spirc_task_));
|
||||||
player_event_channel = Some(event_channel);
|
player_event_channel = Some(event_channel);
|
||||||
|
@ -1663,7 +1677,9 @@ async fn main() {
|
||||||
|
|
||||||
// Shutdown spirc if necessary
|
// Shutdown spirc if necessary
|
||||||
if let Some(spirc) = spirc {
|
if let Some(spirc) = spirc {
|
||||||
spirc.shutdown();
|
if let Err(e) = spirc.shutdown() {
|
||||||
|
error!("error sending spirc shutdown message: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(mut spirc_task) = spirc_task {
|
if let Some(mut spirc_task) = spirc_task {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|
Loading…
Reference in a new issue