Change panics into Result<_, librespot_core::Error>

This commit is contained in:
Roderick van Domburg 2021-12-26 21:18:42 +01:00
parent a297c68913
commit 62461be1fc
No known key found for this signature in database
GPG key ID: A9EF5222A26F0451
69 changed files with 2041 additions and 1331 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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;

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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"

View file

@ -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)
} }

View file

@ -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);

View file

@ -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()?)
} }
} }

View file

@ -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")

View file

@ -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)
} }

View file

@ -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,
} })
} }
} }

View file

@ -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 {

View file

@ -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))
} }

View file

@ -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))?;

View file

@ -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)
} }

View file

@ -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)]

View file

@ -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())?;

View file

@ -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(),
) ))
} }

View file

@ -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?)
} }

View file

@ -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)
} }

View file

@ -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())
} }
} }
} }

View file

@ -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
View 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)
}
}

View file

@ -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)]

View file

@ -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()));

View file

@ -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;

View file

@ -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())
} }
} }

View file

@ -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();
} }

View file

@ -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)
} }
} }

View file

@ -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,

View file

@ -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);
}
} }
} }
} }

View file

@ -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;

View file

@ -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)

View file

@ -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: "....................",

View file

@ -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,

View file

@ -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.

View file

@ -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"

View file

@ -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())?;

View file

@ -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(&params), (Method::POST, Some("addUser")) => self.handle_add_user(&params)?,
_ => 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,
}) }))
} }
} }

View file

@ -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(),

View file

@ -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(),

View file

@ -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)]

View file

@ -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)),
} }
} }
} }

View file

@ -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>;

View file

@ -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)]

View file

@ -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)]

View file

@ -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()?,

View file

@ -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,
} }

View file

@ -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)]

View file

@ -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(),

View file

@ -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>;
} }

View file

@ -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, &current_user, playlist_id).await Self::request_for_user(session, &current_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> {

View file

@ -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(),

View file

@ -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()?,

View file

@ -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()?,

View file

@ -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()?,

View file

@ -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(),

View file

@ -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;

View file

@ -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)),
} }
} }
} }

View file

@ -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;

View file

@ -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)]

View file

@ -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()?,

View file

@ -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()?,

View file

@ -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?))

View file

@ -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)]

View file

@ -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 }

View file

@ -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(())
} }

View file

@ -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! {