From 62461be1fcfcaa93bb52f32cc2f88b7fdcd6ecd7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 26 Dec 2021 21:18:42 +0100 Subject: [PATCH] Change panics into `Result<_, librespot_core::Error>` --- Cargo.lock | 2 + audio/src/decrypt.rs | 9 +- audio/src/fetch/mod.rs | 177 +++++------ audio/src/fetch/receive.rs | 188 +++++------- audio/src/range_set.rs | 8 +- connect/Cargo.toml | 1 + connect/src/context.rs | 29 +- connect/src/discovery.rs | 11 +- connect/src/spirc.rs | 414 +++++++++++++++----------- core/src/apresolve.rs | 8 +- core/src/audio_key.rs | 101 ++++--- core/src/authentication.rs | 38 ++- core/src/cache.rs | 175 ++++++----- core/src/cdn_url.rs | 119 ++++---- core/src/channel.rs | 49 +++- core/src/component.rs | 2 +- core/src/config.rs | 5 +- core/src/connection/codec.rs | 15 +- core/src/connection/handshake.rs | 42 ++- core/src/connection/mod.rs | 50 ++-- core/src/date.rs | 25 +- core/src/dealer/maps.rs | 23 +- core/src/dealer/mod.rs | 95 +++--- core/src/error.rs | 437 ++++++++++++++++++++++++++++ core/src/file_id.rs | 4 +- core/src/http_client.rs | 126 ++++---- core/src/lib.rs | 7 + core/src/mercury/mod.rs | 99 ++++--- core/src/mercury/sender.rs | 11 +- core/src/mercury/types.rs | 53 ++-- core/src/packet.rs | 2 +- core/src/session.rs | 118 +++++--- core/src/socket.rs | 3 +- core/src/spclient.rs | 65 ++--- core/src/spotify_id.rs | 82 +++--- core/src/token.rs | 36 ++- core/src/util.rs | 18 +- discovery/Cargo.toml | 1 + discovery/src/lib.rs | 24 +- discovery/src/server.rs | 115 +++++--- metadata/src/album.rs | 48 ++- metadata/src/artist.rs | 38 +-- metadata/src/audio/file.rs | 9 +- metadata/src/audio/item.rs | 7 +- metadata/src/availability.rs | 5 +- metadata/src/content_rating.rs | 4 +- metadata/src/copyright.rs | 7 +- metadata/src/episode.rs | 25 +- metadata/src/error.rs | 31 +- metadata/src/external_id.rs | 4 +- metadata/src/image.rs | 23 +- metadata/src/lib.rs | 7 +- metadata/src/playlist/annotation.rs | 12 +- metadata/src/playlist/attribute.rs | 34 +-- metadata/src/playlist/diff.rs | 14 +- metadata/src/playlist/item.rs | 28 +- metadata/src/playlist/list.rs | 30 +- metadata/src/playlist/operation.rs | 19 +- metadata/src/playlist/permission.rs | 4 +- metadata/src/request.rs | 11 +- metadata/src/restriction.rs | 6 +- metadata/src/sale_period.rs | 5 +- metadata/src/show.rs | 29 +- metadata/src/track.rs | 25 +- metadata/src/util.rs | 2 +- metadata/src/video.rs | 7 +- playback/Cargo.toml | 2 +- playback/src/player.rs | 81 +++--- src/main.rs | 68 +++-- 69 files changed, 2041 insertions(+), 1331 deletions(-) create mode 100644 core/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 3e28c806..cce06c16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1246,6 +1246,7 @@ dependencies = [ "rand", "serde", "serde_json", + "thiserror", "tokio", "tokio-stream", ] @@ -1309,6 +1310,7 @@ dependencies = [ "form_urlencoded", "futures", "futures-core", + "futures-util", "hex", "hmac", "hyper", diff --git a/audio/src/decrypt.rs b/audio/src/decrypt.rs index 17f4edba..95dc7c08 100644 --- a/audio/src/decrypt.rs +++ b/audio/src/decrypt.rs @@ -1,8 +1,11 @@ use std::io; -use aes_ctr::cipher::generic_array::GenericArray; -use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek}; -use aes_ctr::Aes128Ctr; +use aes_ctr::{ + cipher::{ + generic_array::GenericArray, NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek, + }, + Aes128Ctr, +}; use librespot_core::audio_key::AudioKey; diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 09db431f..dc5bcdf4 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -1,54 +1,57 @@ mod receive; -use std::cmp::{max, min}; -use std::fs; -use std::io::{self, Read, Seek, SeekFrom}; -use std::sync::atomic::{self, AtomicUsize}; -use std::sync::{Arc, Condvar, Mutex}; -use std::time::{Duration, Instant}; +use std::{ + cmp::{max, min}, + fs, + io::{self, Read, Seek, SeekFrom}, + sync::{ + atomic::{self, AtomicUsize}, + Arc, Condvar, Mutex, + }, + time::{Duration, Instant}, +}; -use futures_util::future::IntoStream; -use futures_util::{StreamExt, TryFutureExt}; -use hyper::client::ResponseFuture; -use hyper::header::CONTENT_RANGE; -use hyper::Body; +use futures_util::{future::IntoStream, StreamExt, TryFutureExt}; +use hyper::{client::ResponseFuture, header::CONTENT_RANGE, Body, Response, StatusCode}; use tempfile::NamedTempFile; use thiserror::Error; use tokio::sync::{mpsc, oneshot}; -use librespot_core::cdn_url::{CdnUrl, CdnUrlError}; -use librespot_core::file_id::FileId; -use librespot_core::session::Session; -use librespot_core::spclient::SpClientError; +use librespot_core::{cdn_url::CdnUrl, Error, FileId, Session}; use self::receive::audio_file_fetch; use crate::range_set::{Range, RangeSet}; -pub type AudioFileResult = Result<(), AudioFileError>; +pub type AudioFileResult = Result<(), librespot_core::Error>; #[derive(Error, Debug)] pub enum AudioFileError { - #[error("could not complete CDN request: {0}")] - Cdn(#[from] hyper::Error), - #[error("channel was disconnected")] + #[error("other end of channel disconnected")] Channel, - #[error("empty response")] - Empty, - #[error("I/O error: {0}")] - Io(#[from] io::Error), - #[error("output file unavailable")] + #[error("required header not found")] + Header, + #[error("streamer received no data")] + NoData, + #[error("no output available")] Output, - #[error("error parsing response")] - Parsing, - #[error("mutex was poisoned")] - Poisoned, - #[error("could not complete API request: {0}")] - SpClient(#[from] SpClientError), - #[error("streamer did not report progress")] - Timeout, - #[error("could not get CDN URL: {0}")] - Url(#[from] CdnUrlError), + #[error("invalid status code {0}")] + StatusCode(StatusCode), + #[error("wait timeout exceeded")] + WaitTimeout, +} + +impl From for Error { + fn from(err: AudioFileError) -> Self { + match err { + 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. @@ -124,7 +127,7 @@ pub enum AudioFile { #[derive(Debug)] pub struct StreamingRequest { streamer: IntoStream, - initial_body: Option, + initial_response: Option>, offset: usize, length: usize, request_time: Instant, @@ -154,12 +157,9 @@ impl StreamLoaderController { self.file_size == 0 } - pub fn range_available(&self, range: Range) -> Result { + pub fn range_available(&self, range: Range) -> bool { let available = if let Some(ref shared) = self.stream_shared { - let download_status = shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let download_status = shared.download_status.lock().unwrap(); range.length <= download_status @@ -169,16 +169,16 @@ impl StreamLoaderController { range.length <= self.len() - range.start }; - Ok(available) + available } - pub fn range_to_end_available(&self) -> Result { + pub fn range_to_end_available(&self) -> bool { match self.stream_shared { Some(ref shared) => { let read_position = shared.read_position.load(atomic::Ordering::Relaxed); 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) { 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); } } @@ -213,10 +214,7 @@ impl StreamLoaderController { self.fetch(range); if let Some(ref shared) = self.stream_shared { - let mut download_status = shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let mut download_status = shared.download_status.lock().unwrap(); while range.length > download_status @@ -226,7 +224,7 @@ impl StreamLoaderController { download_status = shared .cond .wait_timeout(download_status, DOWNLOAD_TIMEOUT) - .map_err(|_| AudioFileError::Timeout)? + .map_err(|_| AudioFileError::WaitTimeout)? .0; if range.length > (download_status @@ -319,7 +317,7 @@ impl AudioFile { file_id: FileId, bytes_per_second: usize, play_from_beginning: bool, - ) -> Result { + ) -> Result { if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) { debug!("File {} already in cache", file_id); return Ok(AudioFile::Cached(file)); @@ -340,9 +338,14 @@ impl AudioFile { let session_ = session.clone(); session.spawn(complete_rx.map_ok(move |mut file| { if let Some(cache) = session_.cache() { - if cache.save_file(file_id, &mut file) { - debug!("File {} cached to {:?}", file_id, cache.file(file_id)); + if let Some(cache_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); } })); @@ -350,7 +353,7 @@ impl AudioFile { Ok(AudioFile::Streaming(streaming.await?)) } - pub fn get_stream_loader_controller(&self) -> Result { + pub fn get_stream_loader_controller(&self) -> Result { let controller = match self { AudioFile::Streaming(ref stream) => StreamLoaderController { channel_tx: Some(stream.stream_loader_command_tx.clone()), @@ -379,7 +382,7 @@ impl AudioFileStreaming { complete_tx: oneshot::Sender, bytes_per_second: usize, play_from_beginning: bool, - ) -> Result { + ) -> Result { let download_size = if play_from_beginning { INITIAL_DOWNLOAD_SIZE + max( @@ -392,8 +395,8 @@ impl AudioFileStreaming { INITIAL_DOWNLOAD_SIZE }; - let mut cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?; - let url = cdn_url.get_url()?; + let cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?; + let url = cdn_url.try_get_url()?; trace!("Streaming {:?}", url); @@ -403,23 +406,19 @@ impl AudioFileStreaming { // 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 // further processed in `audio_file_fetch`. - let response = match streamer.next().await { - Some(Ok(data)) => data, - Some(Err(e)) => return Err(AudioFileError::Cdn(e)), - None => return Err(AudioFileError::Empty), - }; + let response = streamer.next().await.ok_or(AudioFileError::NoData)??; + let header_value = response .headers() .get(CONTENT_RANGE) - .ok_or(AudioFileError::Parsing)?; - - let str_value = header_value.to_str().map_err(|_| AudioFileError::Parsing)?; - let file_size_str = str_value.split('/').last().ok_or(AudioFileError::Parsing)?; - let file_size = file_size_str.parse().map_err(|_| AudioFileError::Parsing)?; + .ok_or(AudioFileError::Header)?; + let str_value = header_value.to_str()?; + let file_size_str = str_value.split('/').last().unwrap_or_default(); + let file_size = file_size_str.parse()?; let initial_request = StreamingRequest { streamer, - initial_body: Some(response.into_body()), + initial_response: Some(response), offset: 0, length: download_size, request_time, @@ -474,12 +473,7 @@ impl Read for AudioFileStreaming { let length = min(output.len(), self.shared.file_size - offset); - let length_to_request = match *(self - .shared - .download_strategy - .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?) - { + let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) { DownloadStrategy::RandomAccess() => length, DownloadStrategy::Streaming() => { // 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(); ranges_to_request.add_range(&Range::new(offset, length_to_request)); - let mut download_status = self - .shared - .download_status - .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?; + let mut download_status = self.shared.download_status.lock().unwrap(); + ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); for &range in ranges_to_request.iter() { self.stream_loader_command_tx .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 { return Ok(0); } - let mut download_message_printed = false; 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 .shared .cond .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; } let available_length = download_status @@ -551,15 +535,6 @@ impl Read for AudioFileStreaming { let read_len = min(length, available_length); 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.shared .read_position diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 716c24e1..f26c95f8 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -1,25 +1,25 @@ -use std::cmp::{max, min}; -use std::io::{Seek, SeekFrom, Write}; -use std::sync::{atomic, Arc}; -use std::time::{Duration, Instant}; +use std::{ + cmp::{max, min}, + io::{Seek, SeekFrom, Write}, + sync::{atomic, Arc}, + time::{Duration, Instant}, +}; use atomic::Ordering; use bytes::Bytes; use futures_util::StreamExt; +use hyper::StatusCode; use tempfile::NamedTempFile; use tokio::sync::{mpsc, oneshot}; -use librespot_core::session::Session; +use librespot_core::{session::Session, Error}; use crate::range_set::{Range, RangeSet}; use super::{ AudioFileError, AudioFileResult, AudioFileShared, DownloadStrategy, StreamLoaderCommand, - StreamingRequest, -}; -use super::{ - FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, - MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, + StreamingRequest, FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, + MAX_PREFETCH_REQUESTS, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, }; struct PartialFileData { @@ -49,19 +49,27 @@ async fn receive_data( let mut measure_ping_time = old_number_of_request == 0; - let result = loop { - let body = match request.initial_body.take() { + let result: Result<_, Error> = loop { + let response = match request.initial_response.take() { Some(data) => data, None => match request.streamer.next().await { - Some(Ok(response)) => response.into_body(), - Some(Err(e)) => break Err(e), + Some(Ok(response)) => response, + Some(Err(e)) => break Err(e.into()), 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 { Ok(bytes) => bytes, - Err(e) => break Err(e), + Err(e) => break Err(e.into()), }; if measure_ping_time { @@ -69,16 +77,16 @@ async fn receive_data( if 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; } let data_size = data.len(); - let _ = file_data_tx.send(ReceivedData::Data(PartialFileData { + file_data_tx.send(ReceivedData::Data(PartialFileData { offset: data_offset, data, - })); + }))?; data_offset += data_size; if request_length < data_size { warn!( @@ -100,10 +108,8 @@ async fn receive_data( if request_length > 0 { let missing_range = Range::new(data_offset, request_length); - let mut download_status = shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let mut download_status = shared.download_status.lock().unwrap(); + download_status.requested.subtract_range(&missing_range); shared.cond.notify_all(); } @@ -127,7 +133,7 @@ async fn receive_data( "Error from streamer for range {} (+{}): {:?}", requested_offset, requested_length, e ); - Err(e.into()) + Err(e) } } } @@ -150,14 +156,8 @@ enum ControlFlow { } impl AudioFileFetch { - fn get_download_strategy(&mut self) -> Result { - let strategy = self - .shared - .download_strategy - .lock() - .map_err(|_| AudioFileError::Poisoned)?; - - Ok(*(strategy)) + fn get_download_strategy(&mut self) -> DownloadStrategy { + *(self.shared.download_strategy.lock().unwrap()) } fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult { @@ -172,52 +172,34 @@ impl AudioFileFetch { let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length)); - let mut download_status = self - .shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let mut download_status = self.shared.download_status.lock().unwrap(); ranges_to_request.subtract_range_set(&download_status.downloaded); 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() { - match cdn_url.urls.first() { - Some(url) => { - match self - .session - .spclient() - .stream_file(&url.0, range.start, range.length) - { - Ok(streamer) => { - download_status.requested.add_range(range); + let url = self.shared.cdn_url.try_get_url()?; - let streaming_request = StreamingRequest { - streamer, - initial_body: None, - offset: range.start, - length: range.length, - request_time: Instant::now(), - }; + let streamer = self + .session + .spclient() + .stream_file(url, range.start, range.length)?; - self.session.spawn(receive_data( - self.shared.clone(), - self.file_data_tx.clone(), - 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); - } - } + download_status.requested.add_range(range); + + let streaming_request = StreamingRequest { + streamer, + initial_response: None, + offset: range.start, + length: range.length, + request_time: Instant::now(), + }; + + self.session.spawn(receive_data( + self.shared.clone(), + self.file_data_tx.clone(), + streaming_request, + )); } Ok(()) @@ -236,11 +218,8 @@ impl AudioFileFetch { let mut missing_data = RangeSet::new(); missing_data.add_range(&Range::new(0, self.shared.file_size)); { - let download_status = self - .shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let download_status = self.shared.download_status.lock().unwrap(); + missing_data.subtract_range_set(&download_status.downloaded); missing_data.subtract_range_set(&download_status.requested); } @@ -277,7 +256,7 @@ impl AudioFileFetch { Ok(()) } - fn handle_file_data(&mut self, data: ReceivedData) -> Result { + fn handle_file_data(&mut self, data: ReceivedData) -> Result { match data { ReceivedData::ResponseTime(response_time) => { 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.write_all(data.data.as_ref())?; } - None => return Err(AudioFileError::Output), + None => return Err(AudioFileError::Output.into()), } - let mut download_status = self - .shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let mut download_status = self.shared.download_status.lock().unwrap(); let received_range = Range::new(data.offset, data.data.len()); download_status.downloaded.add_range(&received_range); @@ -355,38 +330,38 @@ impl AudioFileFetch { fn handle_stream_loader_command( &mut self, cmd: StreamLoaderCommand, - ) -> Result { + ) -> Result { match cmd { StreamLoaderCommand::Fetch(request) => { self.download_range(request.start, request.length)?; } StreamLoaderCommand::RandomAccessMode() => { - *(self - .shared - .download_strategy - .lock() - .map_err(|_| AudioFileError::Poisoned)?) = DownloadStrategy::RandomAccess(); + *(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::RandomAccess(); } StreamLoaderCommand::StreamMode() => { - *(self - .shared - .download_strategy - .lock() - .map_err(|_| AudioFileError::Poisoned)?) = DownloadStrategy::Streaming(); + *(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::Streaming(); } StreamLoaderCommand::Close() => return Ok(ControlFlow::Break), } + Ok(ControlFlow::Continue) } fn finish(&mut self) -> AudioFileResult { - let mut output = self.output.take().ok_or(AudioFileError::Output)?; - let complete_tx = self.complete_tx.take().ok_or(AudioFileError::Output)?; + let output = self.output.take(); - output.seek(SeekFrom::Start(0))?; - complete_tx - .send(output) - .map_err(|_| AudioFileError::Channel) + let complete_tx = self.complete_tx.take(); + + if let Some(mut output) = output { + output.seek(SeekFrom::Start(0))?; + if let Some(complete_tx) = complete_tx { + complete_tx + .send(output) + .map_err(|_| AudioFileError::Channel)?; + } + } + + Ok(()) } } @@ -405,10 +380,8 @@ pub(super) async fn audio_file_fetch( initial_request.offset, initial_request.offset + initial_request.length, ); - let mut download_status = shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let mut download_status = shared.download_status.lock().unwrap(); + 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 = fetch.shared.number_of_open_requests.load(Ordering::SeqCst); if number_of_open_requests < MAX_PREFETCH_REQUESTS { let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests; let bytes_pending: usize = { - let download_status = fetch - .shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let download_status = fetch.shared.download_status.lock().unwrap(); + download_status .requested .minus(&download_status.downloaded) diff --git a/audio/src/range_set.rs b/audio/src/range_set.rs index a37b03ae..005a4cda 100644 --- a/audio/src/range_set.rs +++ b/audio/src/range_set.rs @@ -1,6 +1,8 @@ -use std::cmp::{max, min}; -use std::fmt; -use std::slice::Iter; +use std::{ + cmp::{max, min}, + fmt, + slice::Iter, +}; #[derive(Copy, Clone, Debug)] pub struct Range { diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 4daf89f4..b0878c1c 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -15,6 +15,7 @@ protobuf = "2.14.0" rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +thiserror = "1.0" tokio = { version = "1.0", features = ["macros", "sync"] } tokio-stream = "0.1.1" diff --git a/connect/src/context.rs b/connect/src/context.rs index 154d9507..928aec23 100644 --- a/connect/src/context.rs +++ b/connect/src/context.rs @@ -1,7 +1,12 @@ +// TODO : move to metadata + use crate::core::spotify_id::SpotifyId; use crate::protocol::spirc::TrackRef; -use serde::Deserialize; +use serde::{ + de::{Error, Unexpected}, + Deserialize, +}; #[derive(Deserialize, Debug)] pub struct StationContext { @@ -72,17 +77,23 @@ where D: serde::Deserializer<'d>, { let v: Vec = serde::Deserialize::deserialize(de)?; - let track_vec = v - .iter() + v.iter() .map(|v| { let mut t = TrackRef::new(); // 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 + Ok(t) }) - .collect::>(); - - Ok(track_vec) + .collect::, D::Error>>() } diff --git a/connect/src/discovery.rs b/connect/src/discovery.rs index 8ce3f4f0..8f4f9b34 100644 --- a/connect/src/discovery.rs +++ b/connect/src/discovery.rs @@ -1,10 +1,11 @@ -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; use futures_util::Stream; -use librespot_core::authentication::Credentials; -use librespot_core::config::ConnectConfig; +use librespot_core::{authentication::Credentials, config::ConnectConfig}; pub struct DiscoveryStream(librespot_discovery::Discovery); diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index b3878a42..dc631831 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1,31 +1,67 @@ -use std::convert::TryFrom; -use std::future::Future; -use std::pin::Pin; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + convert::TryFrom, + future::Future, + pin::Pin, + time::{SystemTime, UNIX_EPOCH}, +}; -use crate::context::StationContext; -use crate::core::config::ConnectConfig; -use crate::core::mercury::{MercuryError, MercurySender}; -use crate::core::session::{Session, UserAttributes}; -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 futures_util::{ + future::{self, FusedFuture}, + stream::FusedStream, + FutureExt, StreamExt, TryFutureExt, +}; -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 rand::seq::SliceRandom; +use thiserror::Error; use tokio::sync::mpsc; 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 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 { Stopped, LoadingPlay { @@ -60,18 +96,18 @@ struct SpircTask { play_request_id: Option, play_status: SpircPlayStatus, - subscription: BoxedStream, - connection_id_update: BoxedStream, - user_attributes_update: BoxedStream, - user_attributes_mutation: BoxedStream, + remote_update: BoxedStream>, + connection_id_update: BoxedStream>, + user_attributes_update: BoxedStream>, + user_attributes_mutation: BoxedStream>, sender: MercurySender, commands: Option>, player_events: Option, shutdown: bool, session: Session, - context_fut: BoxedFuture>, - autoplay_fut: BoxedFuture>, + context_fut: BoxedFuture>, + autoplay_fut: BoxedFuture>, context: Option, } @@ -232,7 +268,7 @@ impl Spirc { session: Session, player: Player, mixer: Box, - ) -> (Spirc, impl Future) { + ) -> Result<(Spirc, impl Future), Error> { debug!("new Spirc[{}]", session.session_id()); let ident = session.device_id().to_owned(); @@ -242,16 +278,18 @@ impl Spirc { debug!("canonical_username: {}", canonical_username); let uri = format!("hm://remote/user/{}/", url_encode(canonical_username)); - let subscription = Box::pin( + let remote_update = Box::pin( session .mercury() .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) .flatten_stream() - .map(|response| -> Frame { - let data = response.payload.first().unwrap(); - Frame::parse_from_bytes(data).unwrap() + .map(|response| -> Result { + let data = response.payload.first().ok_or(SpircError::NoData)?; + Ok(Frame::parse_from_bytes(data)?) }), ); @@ -261,12 +299,12 @@ impl Spirc { .listen_for("hm://pusher/v1/connections/") .map(UnboundedReceiverStream::new) .flatten_stream() - .map(|response| -> String { - response + .map(|response| -> Result { + let connection_id = response .uri .strip_prefix("hm://pusher/v1/connections/") - .unwrap_or("") - .to_owned() + .ok_or_else(|| SpircError::InvalidUri(response.uri.clone()))?; + Ok(connection_id.to_owned()) }), ); @@ -276,9 +314,9 @@ impl Spirc { .listen_for("spotify:user:attributes:update") .map(UnboundedReceiverStream::new) .flatten_stream() - .map(|response| -> UserAttributesUpdate { - let data = response.payload.first().unwrap(); - UserAttributesUpdate::parse_from_bytes(data).unwrap() + .map(|response| -> Result { + let data = response.payload.first().ok_or(SpircError::NoData)?; + Ok(UserAttributesUpdate::parse_from_bytes(data)?) }), ); @@ -288,9 +326,9 @@ impl Spirc { .listen_for("spotify:user:attributes:mutated") .map(UnboundedReceiverStream::new) .flatten_stream() - .map(|response| -> UserAttributesMutation { - let data = response.payload.first().unwrap(); - UserAttributesMutation::parse_from_bytes(data).unwrap() + .map(|response| -> Result { + let data = response.payload.first().ok_or(SpircError::NoData)?; + Ok(UserAttributesMutation::parse_from_bytes(data)?) }), ); @@ -321,7 +359,7 @@ impl Spirc { play_request_id: None, play_status: SpircPlayStatus::Stopped, - subscription, + remote_update, connection_id_update, user_attributes_update, user_attributes_mutation, @@ -346,37 +384,37 @@ impl Spirc { let spirc = Spirc { commands: cmd_tx }; - task.hello(); + task.hello()?; - (spirc, task.run()) + Ok((spirc, task.run())) } - pub fn play(&self) { - let _ = self.commands.send(SpircCommand::Play); + pub fn play(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Play)?) } - pub fn play_pause(&self) { - let _ = self.commands.send(SpircCommand::PlayPause); + pub fn play_pause(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::PlayPause)?) } - pub fn pause(&self) { - let _ = self.commands.send(SpircCommand::Pause); + pub fn pause(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Pause)?) } - pub fn prev(&self) { - let _ = self.commands.send(SpircCommand::Prev); + pub fn prev(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Prev)?) } - pub fn next(&self) { - let _ = self.commands.send(SpircCommand::Next); + pub fn next(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Next)?) } - pub fn volume_up(&self) { - let _ = self.commands.send(SpircCommand::VolumeUp); + pub fn volume_up(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::VolumeUp)?) } - pub fn volume_down(&self) { - let _ = self.commands.send(SpircCommand::VolumeDown); + pub fn volume_down(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::VolumeDown)?) } - pub fn shutdown(&self) { - let _ = self.commands.send(SpircCommand::Shutdown); + pub fn shutdown(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Shutdown)?) } - pub fn shuffle(&self) { - let _ = self.commands.send(SpircCommand::Shuffle); + pub fn shuffle(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Shuffle)?) } } @@ -386,39 +424,57 @@ impl SpircTask { let commands = self.commands.as_mut(); let player_events = self.player_events.as_mut(); tokio::select! { - frame = self.subscription.next() => match frame { - Some(frame) => self.handle_frame(frame), + remote_update = self.remote_update.next() => match remote_update { + 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 => { error!("subscription terminated"); break; } }, 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 => { error!("user attributes update selected, but none received"); break; } }, 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 => { error!("user attributes mutation selected, but none received"); break; } }, 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 => { error!("connection ID update selected, but none received"); break; } }, - cmd = async { commands.unwrap().recv().await }, if commands.is_some() => if let Some(cmd) = cmd { - self.handle_command(cmd); + cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = 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 { - self.handle_player_event(event) + event = async { player_events?.recv().await }, if player_events.is_some() => if let Some(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() { error!("Cannot flush spirc event sender."); @@ -488,79 +544,80 @@ impl SpircTask { 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(); match cmd { SpircCommand::Play => { if active { self.handle_play(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypePlay).send(); + CommandSender::new(self, MessageType::kMessageTypePlay).send() } } SpircCommand::PlayPause => { if active { self.handle_play_pause(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypePlayPause).send(); + CommandSender::new(self, MessageType::kMessageTypePlayPause).send() } } SpircCommand::Pause => { if active { self.handle_pause(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypePause).send(); + CommandSender::new(self, MessageType::kMessageTypePause).send() } } SpircCommand::Prev => { if active { self.handle_prev(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypePrev).send(); + CommandSender::new(self, MessageType::kMessageTypePrev).send() } } SpircCommand::Next => { if active { self.handle_next(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypeNext).send(); + CommandSender::new(self, MessageType::kMessageTypeNext).send() } } SpircCommand::VolumeUp => { if active { self.handle_volume_up(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send(); + CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send() } } SpircCommand::VolumeDown => { if active { self.handle_volume_down(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send(); + CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send() } } SpircCommand::Shutdown => { - CommandSender::new(self, MessageType::kMessageTypeGoodbye).send(); + CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?; self.shutdown = true; if let Some(rx) = self.commands.as_mut() { rx.close() } + Ok(()) } 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 // 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 @@ -571,6 +628,7 @@ impl SpircTask { PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(), PlayerEvent::Loading { .. } => self.notify(None, false), PlayerEvent::Playing { position_ms, .. } => { + trace!("==> kPlayStatusPlay"); let new_nominal_start_time = self.now_ms() - position_ms as i64; match self.play_status { SpircPlayStatus::Playing { @@ -580,27 +638,29 @@ impl SpircTask { if (*nominal_start_time - new_nominal_start_time).abs() > 100 { *nominal_start_time = new_nominal_start_time; self.update_state_position(position_ms); - self.notify(None, true); + self.notify(None, true) + } else { + Ok(()) } } SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => { self.state.set_status(PlayStatus::kPlayStatusPlay); self.update_state_position(position_ms); - self.notify(None, true); self.play_status = SpircPlayStatus::Playing { nominal_start_time: new_nominal_start_time, preloading_of_next_track_triggered: false, }; + self.notify(None, true) } - _ => (), - }; - trace!("==> kPlayStatusPlay"); + _ => Ok(()), + } } PlayerEvent::Paused { position_ms: new_position_ms, .. } => { + trace!("==> kPlayStatusPause"); match self.play_status { SpircPlayStatus::Paused { ref mut position_ms, @@ -609,37 +669,48 @@ impl SpircTask { if *position_ms != new_position_ms { *position_ms = new_position_ms; self.update_state_position(new_position_ms); - self.notify(None, true); + self.notify(None, true) + } else { + Ok(()) } } SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => { self.state.set_status(PlayStatus::kPlayStatusPause); self.update_state_position(new_position_ms); - self.notify(None, true); self.play_status = SpircPlayStatus::Paused { position_ms: new_position_ms, preloading_of_next_track_triggered: false, }; + self.notify(None, true) } - _ => (), + _ => Ok(()), } - trace!("==> kPlayStatusPause"); } PlayerEvent::Stopped { .. } => match self.play_status { - SpircPlayStatus::Stopped => (), + SpircPlayStatus::Stopped => Ok(()), _ => { warn!("The player has stopped unexpectedly."); self.state.set_status(PlayStatus::kPlayStatusStop); - self.notify(None, true); self.play_status = SpircPlayStatus::Stopped; + self.notify(None, true) } }, - PlayerEvent::TimeToPreloadNextTrack { .. } => self.handle_preload_next_track(), - PlayerEvent::Unavailable { track_id, .. } => self.handle_unavailable(track_id), - _ => (), + PlayerEvent::TimeToPreloadNextTrack { .. } => { + 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() .map(|pair| (pair.get_key().to_owned(), pair.get_value().to_owned())) .collect(); - let _ = self.session.set_user_attributes(attributes); + self.session.set_user_attributes(attributes) } fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) { @@ -683,8 +754,8 @@ impl SpircTask { } } - fn handle_frame(&mut self, frame: Frame) { - let state_string = match frame.get_state().get_status() { + fn handle_remote_update(&mut self, update: Frame) -> Result<(), Error> { + let state_string = match update.get_state().get_status() { PlayStatus::kPlayStatusLoading => "kPlayStatusLoading", PlayStatus::kPlayStatusPause => "kPlayStatusPause", PlayStatus::kPlayStatusStop => "kPlayStatusStop", @@ -693,24 +764,24 @@ impl SpircTask { debug!( "{:?} {:?} {} {} {} {}", - frame.get_typ(), - frame.get_device_state().get_name(), - frame.get_ident(), - frame.get_seq_nr(), - frame.get_state_update_id(), + update.get_typ(), + update.get_device_state().get_name(), + update.get_ident(), + update.get_seq_nr(), + update.get_state_update_id(), state_string, ); - if frame.get_ident() == self.ident - || (!frame.get_recipient().is_empty() && !frame.get_recipient().contains(&self.ident)) + let device_id = &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() { - MessageType::kMessageTypeHello => { - self.notify(Some(frame.get_ident()), true); - } + match update.get_typ() { + MessageType::kMessageTypeHello => self.notify(Some(ident), true), MessageType::kMessageTypeLoad => { if !self.device.get_is_active() { @@ -719,12 +790,12 @@ impl SpircTask { self.device.set_became_active_at(now); } - self.update_tracks(&frame); + self.update_tracks(&update); if !self.state.get_track().is_empty() { let start_playing = - frame.get_state().get_status() == PlayStatus::kPlayStatusPlay; - self.load_track(start_playing, frame.get_state().get_position_ms()); + update.get_state().get_status() == PlayStatus::kPlayStatusPlay; + self.load_track(start_playing, update.get_state().get_position_ms()); } else { info!("No more tracks left in queue"); self.state.set_status(PlayStatus::kPlayStatusStop); @@ -732,51 +803,51 @@ impl SpircTask { self.play_status = SpircPlayStatus::Stopped; } - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypePlay => { self.handle_play(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypePlayPause => { self.handle_play_pause(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypePause => { self.handle_pause(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypeNext => { self.handle_next(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypePrev => { self.handle_prev(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypeVolumeUp => { self.handle_volume_up(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypeVolumeDown => { self.handle_volume_down(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypeRepeat => { - self.state.set_repeat(frame.get_state().get_repeat()); - self.notify(None, true); + self.state.set_repeat(update.get_state().get_repeat()); + self.notify(None, true) } 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() { let current_index = self.state.get_playing_track_index(); { @@ -792,17 +863,17 @@ impl SpircTask { let context = self.state.get_context_uri(); debug!("{:?}", context); } - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypeSeek => { - self.handle_seek(frame.get_position()); - self.notify(None, true); + self.handle_seek(update.get_position()); + self.notify(None, true) } MessageType::kMessageTypeReplace => { - self.update_tracks(&frame); - self.notify(None, true); + self.update_tracks(&update); + self.notify(None, true)?; if let SpircPlayStatus::Playing { preloading_of_next_track_triggered, @@ -820,27 +891,29 @@ impl SpircTask { } } } + Ok(()) } MessageType::kMessageTypeVolume => { - self.set_volume(frame.get_volume() as u16); - self.notify(None, true); + self.set_volume(update.get_volume() as u16); + self.notify(None, true) } MessageType::kMessageTypeNotify => { 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() - <= frame.get_device_state().get_became_active_at() + <= update.get_device_state().get_became_active_at() { self.device.set_is_active(false); self.state.set_status(PlayStatus::kPlayStatusStop); self.player.stop(); self.play_status = SpircPlayStatus::Stopped; } + Ok(()) } - _ => (), + _ => Ok(()), } } @@ -850,6 +923,7 @@ impl SpircTask { position_ms, preloading_of_next_track_triggered, } => { + // TODO - also apply this to the arm below // Synchronize the volume from the mixer. This is useful on // systems that can switch sources from and back to librespot. let current_volume = self.mixer.volume(); @@ -864,6 +938,8 @@ impl SpircTask { }; } 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.play_status = SpircPlayStatus::LoadingPlay { position_ms }; } @@ -1090,9 +1166,9 @@ impl SpircTask { self.set_volume(volume); } - fn handle_end_of_track(&mut self) { + fn handle_end_of_track(&mut self) -> Result<(), Error> { self.handle_next(); - self.notify(None, true); + self.notify(None, true) } fn position(&mut self) -> u32 { @@ -1107,48 +1183,40 @@ impl SpircTask { } } - fn resolve_station(&self, uri: &str) -> BoxedFuture> { + fn resolve_station(&self, uri: &str) -> BoxedFuture> { let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri); self.resolve_uri(&radio_uri) } - fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture> { + fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture> { let query_uri = format!("hm://autoplay-enabled/query?uri={}", uri); let request = self.session.mercury().get(query_uri); Box::pin( async { - let response = request.await?; + let response = request?.await?; if response.status_code == 200 { - let data = response - .payload - .first() - .expect("Empty autoplay uri") - .to_vec(); - let autoplay_uri = String::from_utf8(data).unwrap(); - Ok(autoplay_uri) + let data = response.payload.first().ok_or(SpircError::NoData)?.to_vec(); + Ok(String::from_utf8(data)?) } else { warn!("No autoplay_uri found"); - Err(MercuryError) + Err(MercuryError::Response(response).into()) } } .fuse(), ) } - fn resolve_uri(&self, uri: &str) -> BoxedFuture> { + fn resolve_uri(&self, uri: &str) -> BoxedFuture> { let request = self.session.mercury().get(uri); Box::pin( async move { - let response = request.await?; + let response = request?.await?; - let data = response - .payload - .first() - .expect("Empty payload on context uri"); - let response: serde_json::Value = serde_json::from_slice(data).unwrap(); + let data = response.payload.first().ok_or(SpircError::NoData)?; + let response: serde_json::Value = serde_json::from_slice(data)?; Ok(response) } @@ -1315,13 +1383,17 @@ impl SpircTask { } } - fn hello(&mut self) { - CommandSender::new(self, MessageType::kMessageTypeHello).send(); + fn hello(&mut self) -> Result<(), Error> { + 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) { - return; + return Ok(()); }; let status_string = match self.state.get_status() { PlayStatus::kPlayStatusLoading => "kPlayStatusLoading", @@ -1334,7 +1406,7 @@ impl SpircTask { if let Some(s) = recipient { cs = cs.recipient(s); } - cs.send(); + cs.send() } fn set_volume(&mut self, volume: u16) { @@ -1382,11 +1454,11 @@ impl<'a> CommandSender<'a> { self } - fn send(mut self) { + fn send(mut self) -> Result<(), Error> { if !self.frame.has_state() && self.spirc.device.get_is_active() { 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()?) } } diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index e78a272c..69a8e15c 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,7 +1,9 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + use hyper::{Body, Method, Request}; use serde::Deserialize; -use std::error::Error; -use std::sync::atomic::{AtomicUsize, Ordering}; + +use crate::Error; pub type SocketAddress = (String, u16); @@ -67,7 +69,7 @@ impl ApResolver { .collect() } - pub async fn try_apresolve(&self) -> Result> { + pub async fn try_apresolve(&self) -> Result { let req = Request::builder() .method(Method::GET) .uri("http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient") diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index 2198819e..74be4258 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -1,54 +1,85 @@ +use std::{collections::HashMap, io::Write}; + use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use bytes::Bytes; -use std::collections::HashMap; -use std::io::Write; +use thiserror::Error; use tokio::sync::oneshot; -use crate::file_id::FileId; -use crate::packet::PacketType; -use crate::spotify_id::SpotifyId; -use crate::util::SeqGenerator; +use crate::{packet::PacketType, util::SeqGenerator, Error, FileId, SpotifyId}; #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] pub struct AudioKey(pub [u8; 16]); -#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] -pub struct AudioKeyError; +#[derive(Debug, Error)] +pub enum AudioKeyError { + #[error("audio key error")] + AesKey, + #[error("other end of channel disconnected")] + Channel, + #[error("unexpected packet type {0}")] + Packet(u8), + #[error("sequence {0} not pending")] + Sequence(u32), +} + +impl From for Error { + fn from(err: AudioKeyError) -> Self { + match err { + AudioKeyError::AesKey => Error::unavailable(err), + AudioKeyError::Channel => Error::aborted(err), + AudioKeyError::Sequence(_) => Error::aborted(err), + AudioKeyError::Packet(_) => Error::unimplemented(err), + } + } +} component! { AudioKeyManager : AudioKeyManagerInner { sequence: SeqGenerator = SeqGenerator::new(0), - pending: HashMap>> = HashMap::new(), + pending: HashMap>> = HashMap::new(), } } impl AudioKeyManager { - pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> { let seq = BigEndian::read_u32(data.split_to(4).as_ref()); - let sender = self.lock(|inner| inner.pending.remove(&seq)); + let sender = self + .lock(|inner| inner.pending.remove(&seq)) + .ok_or(AudioKeyError::Sequence(seq))?; - if let Some(sender) = sender { - match cmd { - PacketType::AesKey => { - let mut key = [0u8; 16]; - key.copy_from_slice(data.as_ref()); - let _ = sender.send(Ok(AudioKey(key))); - } - PacketType::AesKeyError => { - warn!( - "error audio key {:x} {:x}", - data.as_ref()[0], - data.as_ref()[1] - ); - let _ = sender.send(Err(AudioKeyError)); - } - _ => (), + match cmd { + PacketType::AesKey => { + let mut key = [0u8; 16]; + key.copy_from_slice(data.as_ref()); + sender + .send(Ok(AudioKey(key))) + .map_err(|_| AudioKeyError::Channel)? + } + PacketType::AesKeyError => { + error!( + "error audio key {:x} {:x}", + data.as_ref()[0], + data.as_ref()[1] + ); + sender + .send(Err(AudioKeyError::AesKey.into())) + .map_err(|_| AudioKeyError::Channel)? + } + _ => { + trace!( + "Did not expect {:?} AES key packet with data {:#?}", + cmd, + data + ); + return Err(AudioKeyError::Packet(cmd as u8).into()); } } + + Ok(()) } - pub async fn request(&self, track: SpotifyId, file: FileId) -> Result { + pub async fn request(&self, track: SpotifyId, file: FileId) -> Result { let (tx, rx) = oneshot::channel(); let seq = self.lock(move |inner| { @@ -57,16 +88,16 @@ impl AudioKeyManager { seq }); - self.send_key_request(seq, track, file); - rx.await.map_err(|_| AudioKeyError)? + self.send_key_request(seq, track, file)?; + rx.await? } - fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) { + fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) -> Result<(), Error> { let mut data: Vec = Vec::new(); - data.write_all(&file.0).unwrap(); - data.write_all(&track.to_raw()).unwrap(); - data.write_u32::(seq).unwrap(); - data.write_u16::(0x0000).unwrap(); + data.write_all(&file.0)?; + data.write_all(&track.to_raw())?; + data.write_u32::(seq)?; + data.write_u16::(0x0000)?; self.session().send_packet(PacketType::RequestKey, data) } diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 3c188ecf..ad7cf331 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -7,8 +7,21 @@ use pbkdf2::pbkdf2; use protobuf::ProtobufEnum; use serde::{Deserialize, Serialize}; use sha1::{Digest, Sha1}; +use thiserror::Error; -use crate::protocol::authentication::AuthenticationType; +use crate::{protocol::authentication::AuthenticationType, Error}; + +#[derive(Debug, Error)] +pub enum AuthenticationError { + #[error("unknown authentication type {0}")] + AuthType(u32), +} + +impl From for Error { + fn from(err: AuthenticationError) -> Self { + Error::invalid_argument(err) + } +} /// The credentials are used to log into the Spotify API. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -46,7 +59,7 @@ impl Credentials { username: impl Into, encrypted_blob: impl AsRef<[u8]>, device_id: impl AsRef<[u8]>, - ) -> Credentials { + ) -> Result { fn read_u8(stream: &mut R) -> io::Result { let mut data = [0u8]; stream.read_exact(&mut data)?; @@ -91,7 +104,7 @@ impl Credentials { use aes::cipher::generic_array::GenericArray; use aes::cipher::{BlockCipher, NewBlockCipher}; - let mut data = base64::decode(encrypted_blob).unwrap(); + let mut data = base64::decode(encrypted_blob)?; let cipher = Aes192::new(GenericArray::from_slice(&key)); let block_size = ::BlockSize::to_usize(); @@ -109,19 +122,20 @@ impl Credentials { }; let mut cursor = io::Cursor::new(blob.as_slice()); - read_u8(&mut cursor).unwrap(); - read_bytes(&mut cursor).unwrap(); - read_u8(&mut cursor).unwrap(); - let auth_type = read_int(&mut cursor).unwrap(); - let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap(); - read_u8(&mut cursor).unwrap(); - let auth_data = read_bytes(&mut cursor).unwrap(); + read_u8(&mut cursor)?; + read_bytes(&mut cursor)?; + read_u8(&mut cursor)?; + let auth_type = read_int(&mut cursor)?; + let auth_type = AuthenticationType::from_i32(auth_type as i32) + .ok_or(AuthenticationError::AuthType(auth_type))?; + read_u8(&mut cursor)?; + let auth_data = read_bytes(&mut cursor)?; - Credentials { + Ok(Credentials { username, auth_type, auth_data, - } + }) } } diff --git a/core/src/cache.rs b/core/src/cache.rs index aec00e84..ed7cf83e 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -1,15 +1,29 @@ -use std::cmp::Reverse; -use std::collections::HashMap; -use std::fs::{self, File}; -use std::io::{self, Error, ErrorKind, Read, Write}; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; -use std::time::SystemTime; +use std::{ + cmp::Reverse, + collections::HashMap, + fs::{self, File}, + io::{self, Read, Write}, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + time::SystemTime, +}; use priority_queue::PriorityQueue; +use thiserror::Error; -use crate::authentication::Credentials; -use crate::file_id::FileId; +use crate::{authentication::Credentials, error::ErrorKind, Error, FileId}; + +#[derive(Debug, Error)] +pub enum CacheError { + #[error("audio cache location is not configured")] + Path, +} + +impl From for Error { + fn from(err: CacheError) -> Self { + Error::failed_precondition(err) + } +} /// Some kind of data structure that holds some paths, the size of these files and a timestamp. /// It keeps track of the file sizes and is able to pop the path with the oldest timestamp if @@ -57,16 +71,17 @@ impl SizeLimiter { /// to delete the file in the file system. fn pop(&mut self) -> Option { if self.exceeds_limit() { - let (next, _) = self - .queue - .pop() - .expect("in_use was > 0, so the queue should have contained an item."); - let size = self - .sizes - .remove(&next) - .expect("`queue` and `sizes` should have the same keys."); - self.in_use -= size; - Some(next) + if let Some((next, _)) = self.queue.pop() { + if let Some(size) = self.sizes.remove(&next) { + self.in_use -= size; + } else { + error!("`queue` and `sizes` should have the same keys."); + } + Some(next) + } else { + error!("in_use was > 0, so the queue should have contained an item."); + None + } } else { None } @@ -85,11 +100,11 @@ impl SizeLimiter { return false; } - let size = self - .sizes - .remove(file) - .expect("`queue` and `sizes` should have the same keys."); - self.in_use -= size; + if let Some(size) = self.sizes.remove(file) { + self.in_use -= size; + } else { + error!("`queue` and `sizes` should have the same keys."); + } true } @@ -172,56 +187,70 @@ impl FsSizeLimiter { } } - fn add(&self, file: &Path, size: u64) { + fn add(&self, file: &Path, size: u64) -> Result<(), Error> { self.limiter .lock() .unwrap() .add(file, size, SystemTime::now()); + Ok(()) } - fn touch(&self, file: &Path) -> bool { - self.limiter.lock().unwrap().update(file, SystemTime::now()) + fn touch(&self, file: &Path) -> Result { + Ok(self.limiter.lock().unwrap().update(file, SystemTime::now())) } - fn remove(&self, file: &Path) { - self.limiter.lock().unwrap().remove(file); + fn remove(&self, file: &Path) -> Result { + Ok(self.limiter.lock().unwrap().remove(file)) } - fn prune_internal Option>(mut pop: F) { + fn prune_internal Result, Error>>( + mut pop: F, + ) -> Result<(), Error> { let mut first = true; let mut count = 0; + let mut last_error = None; - while let Some(file) = pop() { - if first { - debug!("Cache dir exceeds limit, removing least recently used files."); - first = false; + while let Ok(result) = pop() { + if let Some(file) = result { + if first { + debug!("Cache dir exceeds limit, removing least recently used files."); + first = false; + } + + let res = fs::remove_file(&file); + if let Err(e) = res { + warn!("Could not remove file {:?} from cache dir: {}", file, e); + last_error = Some(e); + } else { + count += 1; + } } - if let Err(e) = fs::remove_file(&file) { - warn!("Could not remove file {:?} from cache dir: {}", file, e); - } else { - count += 1; + if count > 0 { + info!("Removed {} cache files.", count); } } - if count > 0 { - info!("Removed {} cache files.", count); + if let Some(err) = last_error { + Err(err.into()) + } else { + Ok(()) } } - fn prune(&self) { - Self::prune_internal(|| self.limiter.lock().unwrap().pop()) + fn prune(&self) -> Result<(), Error> { + Self::prune_internal(|| Ok(self.limiter.lock().unwrap().pop())) } - fn new(path: &Path, limit: u64) -> Self { + fn new(path: &Path, limit: u64) -> Result { let mut limiter = SizeLimiter::new(limit); Self::init_dir(&mut limiter, path); - Self::prune_internal(|| limiter.pop()); + Self::prune_internal(|| Ok(limiter.pop()))?; - Self { + Ok(Self { limiter: Mutex::new(limiter), - } + }) } } @@ -234,15 +263,13 @@ pub struct Cache { size_limiter: Option>, } -pub struct RemoveFileError(()); - impl Cache { pub fn new>( credentials_path: Option

, volume_path: Option

, audio_path: Option

, size_limit: Option, - ) -> io::Result { + ) -> Result { let mut size_limiter = None; if let Some(location) = &credentials_path { @@ -263,8 +290,7 @@ impl Cache { fs::create_dir_all(location)?; if let Some(limit) = size_limit { - let limiter = FsSizeLimiter::new(location.as_ref(), limit); - + let limiter = FsSizeLimiter::new(location.as_ref(), limit)?; size_limiter = Some(Arc::new(limiter)); } } @@ -285,11 +311,11 @@ impl Cache { let location = self.credentials_location.as_ref()?; // This closure is just convencience to enable the question mark operator - let read = || { + let read = || -> Result { let mut file = File::open(location)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; - serde_json::from_str(&contents).map_err(|e| Error::new(ErrorKind::InvalidData, e)) + Ok(serde_json::from_str(&contents)?) }; match read() { @@ -297,7 +323,7 @@ impl Cache { Err(e) => { // If the file did not exist, the file was probably not written // before. Otherwise, log the error. - if e.kind() != ErrorKind::NotFound { + if e.kind != ErrorKind::NotFound { warn!("Error reading credentials from cache: {}", e); } None @@ -321,19 +347,17 @@ impl Cache { pub fn volume(&self) -> Option { let location = self.volume_location.as_ref()?; - let read = || { + let read = || -> Result { let mut file = File::open(location)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; - contents - .parse() - .map_err(|e| Error::new(ErrorKind::InvalidData, e)) + Ok(contents.parse()?) }; match read() { Ok(v) => Some(v), Err(e) => { - if e.kind() != ErrorKind::NotFound { + if e.kind != ErrorKind::NotFound { warn!("Error reading volume from cache: {}", e); } None @@ -364,12 +388,14 @@ impl Cache { match File::open(&path) { Ok(file) => { if let Some(limiter) = self.size_limiter.as_deref() { - limiter.touch(&path); + if let Err(e) = limiter.touch(&path) { + error!("limiter could not touch {:?}: {}", path, e); + } } Some(file) } Err(e) => { - if e.kind() != ErrorKind::NotFound { + if e.kind() != io::ErrorKind::NotFound { warn!("Error reading file from cache: {}", e) } None @@ -377,7 +403,7 @@ impl Cache { } } - pub fn save_file(&self, file: FileId, contents: &mut F) -> bool { + pub fn save_file(&self, file: FileId, contents: &mut F) -> Result<(), Error> { if let Some(path) = self.file_path(file) { if let Some(parent) = path.parent() { if let Ok(size) = fs::create_dir_all(parent) @@ -385,28 +411,25 @@ impl Cache { .and_then(|mut file| io::copy(contents, &mut file)) { if let Some(limiter) = self.size_limiter.as_deref() { - limiter.add(&path, size); - limiter.prune(); + limiter.add(&path, size)?; + limiter.prune()? } - return true; + return Ok(()); } } } - false + Err(CacheError::Path.into()) } - pub fn remove_file(&self, file: FileId) -> Result<(), RemoveFileError> { - let path = self.file_path(file).ok_or(RemoveFileError(()))?; + pub fn remove_file(&self, file: FileId) -> Result<(), Error> { + let path = self.file_path(file).ok_or(CacheError::Path)?; - if let Err(err) = fs::remove_file(&path) { - warn!("Unable to remove file from cache: {}", err); - Err(RemoveFileError(())) - } else { - if let Some(limiter) = self.size_limiter.as_deref() { - limiter.remove(&path); - } - Ok(()) + fs::remove_file(&path)?; + if let Some(limiter) = self.size_limiter.as_deref() { + limiter.remove(&path)?; } + + Ok(()) } } diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 13f23a37..409d7f25 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -1,34 +1,19 @@ +use std::{ + convert::{TryFrom, TryInto}, + ops::{Deref, DerefMut}, +}; + use chrono::Local; -use protobuf::{Message, ProtobufError}; +use protobuf::Message; use thiserror::Error; use url::Url; -use std::convert::{TryFrom, TryInto}; -use std::ops::{Deref, DerefMut}; - -use super::date::Date; -use super::file_id::FileId; -use super::session::Session; -use super::spclient::SpClientError; +use super::{date::Date, Error, FileId, Session}; use librespot_protocol as protocol; use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage; use protocol::storage_resolve::StorageResolveResponse_Result; -#[derive(Error, Debug)] -pub enum CdnUrlError { - #[error("no URLs available")] - Empty, - #[error("all tokens expired")] - Expired, - #[error("error parsing response")] - Parsing, - #[error("could not parse protobuf: {0}")] - Protobuf(#[from] ProtobufError), - #[error("could not complete API request: {0}")] - SpClient(#[from] SpClientError), -} - #[derive(Debug, Clone)] pub struct MaybeExpiringUrl(pub String, pub Option); @@ -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 for Error { + fn from(err: CdnUrlError) -> Self { + match err { + CdnUrlError::Expired => Error::deadline_exceeded(err), + CdnUrlError::Storage => Error::unavailable(err), + } + } +} + #[derive(Debug, Clone)] pub struct CdnUrl { pub file_id: FileId, - pub urls: MaybeExpiringUrls, + urls: MaybeExpiringUrls, } impl CdnUrl { @@ -62,7 +64,7 @@ impl CdnUrl { } } - pub async fn resolve_audio(&self, session: &Session) -> Result { + pub async fn resolve_audio(&self, session: &Session) -> Result { let file_id = self.file_id; let response = session.spclient().get_audio_urls(file_id).await?; let msg = CdnUrlMessage::parse_from_bytes(&response)?; @@ -75,37 +77,26 @@ impl CdnUrl { Ok(cdn_url) } - pub fn get_url(&mut self) -> Result<&str, CdnUrlError> { - if self.urls.is_empty() { - return Err(CdnUrlError::Empty); - } - - // prune expired URLs until the first one is current, or none are left + pub fn try_get_url(&self) -> Result<&str, Error> { let now = Local::now(); - while !self.urls.is_empty() { - let maybe_expiring = self.urls[0].1; - if let Some(expiry) = maybe_expiring { - if now < expiry.as_utc() { - break; - } else { - self.urls.remove(0); - } - } - } + let url = self.urls.iter().find(|url| match url.1 { + Some(expiry) => now < expiry.as_utc(), + None => true, + }); - if let Some(cdn_url) = self.urls.first() { - Ok(&cdn_url.0) + if let Some(url) = url { + Ok(&url.0) } else { - Err(CdnUrlError::Expired) + Err(CdnUrlError::Expired.into()) } } } impl TryFrom for MaybeExpiringUrls { - type Error = CdnUrlError; + type Error = crate::Error; fn try_from(msg: CdnUrlMessage) -> Result { if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) { - return Err(CdnUrlError::Parsing); + return Err(CdnUrlError::Storage.into()); } let is_expiring = !msg.get_fileid().is_empty(); @@ -114,7 +105,7 @@ impl TryFrom for MaybeExpiringUrls { .get_cdnurl() .iter() .map(|cdn_url| { - let url = Url::parse(cdn_url).map_err(|_| CdnUrlError::Parsing)?; + let url = Url::parse(cdn_url)?; if is_expiring { let expiry_str = if let Some(token) = url @@ -122,29 +113,47 @@ impl TryFrom for MaybeExpiringUrls { .into_iter() .find(|(key, _value)| key == "__token__") { - let start = token.1.find("exp=").ok_or(CdnUrlError::Parsing)?; - let slice = &token.1[start + 4..]; - let end = slice.find('~').ok_or(CdnUrlError::Parsing)?; - String::from(&slice[..end]) + if let Some(mut start) = token.1.find("exp=") { + start += 4; + if token.1.len() >= start { + let slice = &token.1[start..]; + if let Some(end) = slice.find('~') { + // this is the only valid invariant for akamaized.net + String::from(&slice[..end]) + } else { + String::from(slice) + } + } else { + String::new() + } + } else { + String::new() + } } else if let Some(query) = url.query() { let mut items = query.split('_'); - String::from(items.next().ok_or(CdnUrlError::Parsing)?) + if let Some(first) = items.next() { + // this is the only valid invariant for scdn.co + String::from(first) + } else { + String::new() + } } else { - return Err(CdnUrlError::Parsing); + String::new() }; - let mut expiry: i64 = expiry_str.parse().map_err(|_| CdnUrlError::Parsing)?; + let mut expiry: i64 = expiry_str.parse()?; + expiry -= 5 * 60; // seconds Ok(MaybeExpiringUrl( cdn_url.to_owned(), - Some(expiry.try_into().map_err(|_| CdnUrlError::Parsing)?), + Some(expiry.try_into()?), )) } else { Ok(MaybeExpiringUrl(cdn_url.to_owned(), None)) } }) - .collect::, CdnUrlError>>()?; + .collect::, Error>>()?; Ok(Self(result)) } diff --git a/core/src/channel.rs b/core/src/channel.rs index 31c01a40..607189a0 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -1,18 +1,20 @@ -use std::collections::HashMap; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::time::Instant; +use std::{ + collections::HashMap, + fmt, + pin::Pin, + task::{Context, Poll}, + time::Instant, +}; use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use futures_core::Stream; -use futures_util::lock::BiLock; -use futures_util::{ready, StreamExt}; +use futures_util::{lock::BiLock, ready, StreamExt}; use num_traits::FromPrimitive; +use thiserror::Error; use tokio::sync::mpsc; -use crate::packet::PacketType; -use crate::util::SeqGenerator; +use crate::{packet::PacketType, util::SeqGenerator, Error}; component! { ChannelManager : ChannelManagerInner { @@ -27,9 +29,21 @@ component! { const ONE_SECOND_IN_MS: usize = 1000; -#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)] pub struct ChannelError; +impl From for Error { + fn from(err: ChannelError) -> Self { + Error::aborted(err) + } +} + +impl fmt::Display for ChannelError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "channel error") + } +} + pub struct Channel { receiver: mpsc::UnboundedReceiver<(u8, Bytes)>, state: ChannelState, @@ -70,7 +84,7 @@ impl ChannelManager { (seq, channel) } - pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> { use std::collections::hash_map::Entry; let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref()); @@ -94,9 +108,14 @@ impl ChannelManager { inner.download_measurement_bytes += data.len(); if let Entry::Occupied(entry) = inner.channels.entry(id) { - let _ = entry.get().send((cmd as u8, data)); + entry + .get() + .send((cmd as u8, data)) + .map_err(|_| ChannelError)?; } - }); + + Ok(()) + }) } pub fn get_download_rate_estimate(&self) -> usize { @@ -142,7 +161,11 @@ impl Stream for Channel { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { loop { match self.state.clone() { - ChannelState::Closed => panic!("Polling already terminated channel"), + ChannelState::Closed => { + error!("Polling already terminated channel"); + return Poll::Ready(None); + } + ChannelState::Header(mut data) => { if data.is_empty() { data = ready!(self.recv_packet(cx))?; diff --git a/core/src/component.rs b/core/src/component.rs index a761c455..aa1da840 100644 --- a/core/src/component.rs +++ b/core/src/component.rs @@ -14,7 +14,7 @@ macro_rules! component { #[allow(dead_code)] fn lock 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) } diff --git a/core/src/config.rs b/core/src/config.rs index c6b3d23c..f04326ae 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -1,6 +1,5 @@ -use std::fmt; -use std::path::PathBuf; -use std::str::FromStr; +use std::{fmt, path::PathBuf, str::FromStr}; + use url::Url; #[derive(Clone, Debug)] diff --git a/core/src/connection/codec.rs b/core/src/connection/codec.rs index 86533aaf..826839c6 100644 --- a/core/src/connection/codec.rs +++ b/core/src/connection/codec.rs @@ -1,12 +1,20 @@ +use std::io; + use byteorder::{BigEndian, ByteOrder}; use bytes::{BufMut, Bytes, BytesMut}; use shannon::Shannon; -use std::io; +use thiserror::Error; use tokio_util::codec::{Decoder, Encoder}; const HEADER_SIZE: usize = 3; const MAC_SIZE: usize = 4; +#[derive(Debug, Error)] +pub enum ApCodecError { + #[error("payload was malformed")] + Payload, +} + #[derive(Debug)] enum DecodeState { Header, @@ -87,7 +95,10 @@ impl Decoder for ApCodec { let mut payload = buf.split_to(size + MAC_SIZE); - self.decode_cipher.decrypt(payload.get_mut(..size).unwrap()); + self.decode_cipher + .decrypt(payload.get_mut(..size).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, ApCodecError::Payload) + })?); let mac = payload.split_off(size); self.decode_cipher.check_mac(mac.as_ref())?; diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 8acc0d01..42d64df2 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -1,20 +1,28 @@ +use std::{env::consts::ARCH, io}; + use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use hmac::{Hmac, Mac, NewMac}; use protobuf::{self, Message}; use rand::{thread_rng, RngCore}; use sha1::Sha1; -use std::env::consts::ARCH; -use std::io; +use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio_util::codec::{Decoder, Framed}; use super::codec::ApCodec; -use crate::diffie_hellman::DhLocalKeys; + +use crate::{diffie_hellman::DhLocalKeys, version}; + use crate::protocol; use crate::protocol::keyexchange::{ APResponseMessage, ClientHello, ClientResponsePlaintext, Platform, ProductFlags, }; -use crate::version; + +#[derive(Debug, Error)] +pub enum HandshakeError { + #[error("invalid key length")] + InvalidLength, +} pub async fn handshake( mut connection: T, @@ -31,7 +39,7 @@ pub async fn handshake( .to_owned(); let shared_secret = local_keys.shared_secret(&remote_key); - let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator); + let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator)?; let codec = ApCodec::new(&send_key, &recv_key); client_response(&mut connection, challenge).await?; @@ -112,8 +120,8 @@ where let mut buffer = vec![0, 4]; let size = 2 + 4 + packet.compute_size(); - as WriteBytesExt>::write_u32::(&mut buffer, size).unwrap(); - packet.write_to_vec(&mut buffer).unwrap(); + as WriteBytesExt>::write_u32::(&mut buffer, size)?; + packet.write_to_vec(&mut buffer)?; connection.write_all(&buffer[..]).await?; Ok(buffer) @@ -133,8 +141,8 @@ where let mut buffer = vec![]; let size = 4 + packet.compute_size(); - as WriteBytesExt>::write_u32::(&mut buffer, size).unwrap(); - packet.write_to_vec(&mut buffer).unwrap(); + as WriteBytesExt>::write_u32::(&mut buffer, size)?; + packet.write_to_vec(&mut buffer)?; connection.write_all(&buffer[..]).await?; Ok(()) @@ -148,7 +156,7 @@ where let header = read_into_accumulator(connection, 4, acc).await?; let size = BigEndian::read_u32(header) as usize; let data = read_into_accumulator(connection, size - 4, acc).await?; - let message = M::parse_from_bytes(data).unwrap(); + let message = M::parse_from_bytes(data)?; Ok(message) } @@ -164,24 +172,26 @@ async fn read_into_accumulator<'a, 'b, T: AsyncRead + Unpin>( Ok(&mut acc[offset..]) } -fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec, Vec, Vec) { +fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> io::Result<(Vec, Vec, Vec)> { type HmacSha1 = Hmac; let mut data = Vec::with_capacity(0x64); for i in 1..6 { - let mut mac = - HmacSha1::new_from_slice(shared_secret).expect("HMAC can take key of any size"); + let mut mac = HmacSha1::new_from_slice(shared_secret).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength) + })?; mac.update(packets); mac.update(&[i]); data.extend_from_slice(&mac.finalize().into_bytes()); } - let mut mac = HmacSha1::new_from_slice(&data[..0x14]).expect("HMAC can take key of any size"); + let mut mac = HmacSha1::new_from_slice(&data[..0x14]) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength))?; mac.update(packets); - ( + Ok(( mac.finalize().into_bytes().to_vec(), data[0x14..0x34].to_vec(), data[0x34..0x54].to_vec(), - ) + )) } diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 29a33296..0b59de88 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -1,23 +1,21 @@ mod codec; mod handshake; -pub use self::codec::ApCodec; -pub use self::handshake::handshake; +pub use self::{codec::ApCodec, handshake::handshake}; -use std::io::{self, ErrorKind}; +use std::io; use futures_util::{SinkExt, StreamExt}; use num_traits::FromPrimitive; -use protobuf::{self, Message, ProtobufError}; +use protobuf::{self, Message}; use thiserror::Error; use tokio::net::TcpStream; use tokio_util::codec::Framed; use url::Url; -use crate::authentication::Credentials; -use crate::packet::PacketType; +use crate::{authentication::Credentials, packet::PacketType, version, Error}; + use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; -use crate::version; pub type Transport = Framed; @@ -42,13 +40,19 @@ fn login_error_message(code: &ErrorCode) -> &'static str { pub enum AuthenticationError { #[error("Login failed with reason: {}", login_error_message(.0))] LoginFailed(ErrorCode), - #[error("Authentication failed: {0}")] - IoError(#[from] io::Error), + #[error("invalid packet {0}")] + Packet(u8), + #[error("transport returned no data")] + Transport, } -impl From for AuthenticationError { - fn from(e: ProtobufError) -> Self { - io::Error::new(ErrorKind::InvalidData, e).into() +impl From for Error { + fn from(err: AuthenticationError) -> Self { + match err { + AuthenticationError::LoginFailed(_) => Error::permission_denied(err), + AuthenticationError::Packet(_) => Error::unimplemented(err), + AuthenticationError::Transport => Error::unavailable(err), + } } } @@ -68,7 +72,7 @@ pub async fn authenticate( transport: &mut Transport, credentials: Credentials, device_id: &str, -) -> Result { +) -> Result { use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; let cpu_family = match std::env::consts::ARCH { @@ -119,12 +123,15 @@ pub async fn authenticate( packet.set_version_string(format!("librespot {}", version::SEMVER)); let cmd = PacketType::Login; - let data = packet.write_to_bytes().unwrap(); + let data = packet.write_to_bytes()?; transport.send((cmd as u8, data)).await?; - let (cmd, data) = transport.next().await.expect("EOF")?; + let (cmd, data) = transport + .next() + .await + .ok_or(AuthenticationError::Transport)??; let packet_type = FromPrimitive::from_u8(cmd); - match packet_type { + let result = match packet_type { Some(PacketType::APWelcome) => { let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?; @@ -141,8 +148,13 @@ pub async fn authenticate( Err(error_data.into()) } _ => { - let msg = format!("Received invalid packet: {}", cmd); - Err(io::Error::new(ErrorKind::InvalidData, msg).into()) + trace!( + "Did not expect {:?} AES key packet with data {:#?}", + cmd, + data + ); + Err(AuthenticationError::Packet(cmd)) } - } + }; + Ok(result?) } diff --git a/core/src/date.rs b/core/src/date.rs index a84da606..fe052299 100644 --- a/core/src/date.rs +++ b/core/src/date.rs @@ -1,18 +1,23 @@ -use std::convert::TryFrom; -use std::fmt::Debug; -use std::ops::Deref; +use std::{convert::TryFrom, fmt::Debug, ops::Deref}; -use chrono::{DateTime, Utc}; -use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; +use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use thiserror::Error; +use crate::Error; + use librespot_protocol as protocol; use protocol::metadata::Date as DateMessage; #[derive(Debug, Error)] pub enum DateError { - #[error("item has invalid date")] - InvalidTimestamp, + #[error("item has invalid timestamp {0}")] + Timestamp(i64), +} + +impl From for Error { + fn from(err: DateError) -> Self { + Error::invalid_argument(err) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -30,11 +35,11 @@ impl Date { self.0.timestamp() } - pub fn from_timestamp(timestamp: i64) -> Result { + pub fn from_timestamp(timestamp: i64) -> Result { if let Some(date_time) = NaiveDateTime::from_timestamp_opt(timestamp, 0) { Ok(Self::from_utc(date_time)) } else { - Err(DateError::InvalidTimestamp) + Err(DateError::Timestamp(timestamp).into()) } } @@ -67,7 +72,7 @@ impl From> for Date { } impl TryFrom for Date { - type Error = DateError; + type Error = crate::Error; fn try_from(timestamp: i64) -> Result { Self::from_timestamp(timestamp) } diff --git a/core/src/dealer/maps.rs b/core/src/dealer/maps.rs index 38916e40..4f719de7 100644 --- a/core/src/dealer/maps.rs +++ b/core/src/dealer/maps.rs @@ -1,7 +1,20 @@ use std::collections::HashMap; -#[derive(Debug)] -pub struct AlreadyHandledError(()); +use thiserror::Error; + +use crate::Error; + +#[derive(Debug, Error)] +pub enum HandlerMapError { + #[error("request was already handled")] + AlreadyHandled, +} + +impl From for Error { + fn from(err: HandlerMapError) -> Self { + Error::aborted(err) + } +} pub enum HandlerMap { Leaf(T), @@ -19,9 +32,9 @@ impl HandlerMap { &mut self, mut path: impl Iterator, handler: T, - ) -> Result<(), AlreadyHandledError> { + ) -> Result<(), Error> { match self { - Self::Leaf(_) => Err(AlreadyHandledError(())), + Self::Leaf(_) => Err(HandlerMapError::AlreadyHandled.into()), Self::Branch(children) => { if let Some(component) = path.next() { let node = children.entry(component.to_owned()).or_default(); @@ -30,7 +43,7 @@ impl HandlerMap { *self = Self::Leaf(handler); Ok(()) } else { - Err(AlreadyHandledError(())) + Err(HandlerMapError::AlreadyHandled.into()) } } } diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index ba1e68df..ac19fd6d 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -1,29 +1,40 @@ mod maps; pub mod protocol; -use std::iter; -use std::pin::Pin; -use std::sync::atomic::AtomicBool; -use std::sync::{atomic, Arc, Mutex}; -use std::task::Poll; -use std::time::Duration; +use std::{ + iter, + pin::Pin, + sync::{ + atomic::{self, AtomicBool}, + Arc, Mutex, + }, + task::Poll, + time::Duration, +}; use futures_core::{Future, Stream}; -use futures_util::future::join_all; -use futures_util::{SinkExt, StreamExt}; +use futures_util::{future::join_all, SinkExt, StreamExt}; use thiserror::Error; -use tokio::select; -use tokio::sync::mpsc::{self, UnboundedReceiver}; -use tokio::sync::Semaphore; -use tokio::task::JoinHandle; +use tokio::{ + select, + sync::{ + mpsc::{self, UnboundedReceiver}, + Semaphore, + }, + task::JoinHandle, +}; use tokio_tungstenite::tungstenite; use tungstenite::error::UrlError; use url::Url; use self::maps::*; use self::protocol::*; -use crate::socket; -use crate::util::{keep_flushing, CancelOnDrop, TimeoutOnDrop}; + +use crate::{ + socket, + util::{keep_flushing, CancelOnDrop, TimeoutOnDrop}, + Error, +}; type WsMessage = tungstenite::Message; type WsError = tungstenite::Error; @@ -164,24 +175,38 @@ fn split_uri(s: &str) -> Option> { pub enum AddHandlerError { #[error("There is already a handler for the given uri")] AlreadyHandled, - #[error("The specified uri is invalid")] - InvalidUri, + #[error("The specified uri {0} is invalid")] + InvalidUri(String), +} + +impl From for Error { + fn from(err: AddHandlerError) -> Self { + match err { + AddHandlerError::AlreadyHandled => Error::aborted(err), + AddHandlerError::InvalidUri(_) => Error::invalid_argument(err), + } + } } #[derive(Debug, Clone, Error)] pub enum SubscriptionError { #[error("The specified uri is invalid")] - InvalidUri, + InvalidUri(String), +} + +impl From for Error { + fn from(err: SubscriptionError) -> Self { + Error::invalid_argument(err) + } } fn add_handler( map: &mut HandlerMap>, uri: &str, handler: impl RequestHandler, -) -> Result<(), AddHandlerError> { - let split = split_uri(uri).ok_or(AddHandlerError::InvalidUri)?; +) -> Result<(), Error> { + let split = split_uri(uri).ok_or_else(|| AddHandlerError::InvalidUri(uri.to_string()))?; map.insert(split, Box::new(handler)) - .map_err(|_| AddHandlerError::AlreadyHandled) } fn remove_handler(map: &mut HandlerMap, uri: &str) -> Option { @@ -191,11 +216,11 @@ fn remove_handler(map: &mut HandlerMap, uri: &str) -> Option { fn subscribe( map: &mut SubscriberMap, uris: &[&str], -) -> Result { +) -> Result { let (tx, rx) = mpsc::unbounded_channel(); for &uri in uris { - let split = split_uri(uri).ok_or(SubscriptionError::InvalidUri)?; + let split = split_uri(uri).ok_or_else(|| SubscriptionError::InvalidUri(uri.to_string()))?; map.insert(split, tx.clone()); } @@ -237,15 +262,11 @@ impl Builder { Self::default() } - pub fn add_handler( - &mut self, - uri: &str, - handler: impl RequestHandler, - ) -> Result<(), AddHandlerError> { + pub fn add_handler(&mut self, uri: &str, handler: impl RequestHandler) -> Result<(), Error> { add_handler(&mut self.request_handlers, uri, handler) } - pub fn subscribe(&mut self, uris: &[&str]) -> Result { + pub fn subscribe(&mut self, uris: &[&str]) -> Result { subscribe(&mut self.message_handlers, uris) } @@ -342,7 +363,7 @@ pub struct Dealer { } impl Dealer { - pub fn add_handler(&self, uri: &str, handler: H) -> Result<(), AddHandlerError> + pub fn add_handler(&self, uri: &str, handler: H) -> Result<(), Error> where H: RequestHandler, { @@ -357,7 +378,7 @@ impl Dealer { remove_handler(&mut self.shared.request_handlers.lock().unwrap(), uri) } - pub fn subscribe(&self, uris: &[&str]) -> Result { + pub fn subscribe(&self, uris: &[&str]) -> Result { subscribe(&mut self.shared.message_handlers.lock().unwrap(), uris) } @@ -367,7 +388,9 @@ impl Dealer { self.shared.notify_drop.close(); if let Some(handle) = self.handle.take() { - CancelOnDrop(handle).await.unwrap(); + if let Err(e) = CancelOnDrop(handle).await { + error!("error aborting dealer operations: {}", e); + } } } } @@ -556,11 +579,15 @@ async fn run( select! { () = shared.closed() => break, r = t0 => { - r.unwrap(); // Whatever has gone wrong (probably panicked), we can't handle it, so let's panic too. + if let Err(e) = r { + error!("timeout on task 0: {}", e); + } tasks.0.take(); }, r = t1 => { - r.unwrap(); + if let Err(e) = r { + error!("timeout on task 1: {}", e); + } tasks.1.take(); } } @@ -576,7 +603,7 @@ async fn run( match connect(&url, proxy.as_ref(), &shared).await { Ok((s, r)) => tasks = (init_task(s), init_task(r)), Err(e) => { - warn!("Error while connecting: {}", e); + error!("Error while connecting: {}", e); tokio::time::sleep(RECONNECT_INTERVAL).await; } } diff --git a/core/src/error.rs b/core/src/error.rs new file mode 100644 index 00000000..e3753014 --- /dev/null +++ b/core/src/error.rs @@ -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, +} + +#[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(kind: ErrorKind, error: E) -> Error + where + E: Into>, + { + Self { + kind, + error: error.into(), + } + } + + pub fn aborted(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Aborted, + error: error.into(), + } + } + + pub fn already_exists(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::AlreadyExists, + error: error.into(), + } + } + + pub fn cancelled(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Cancelled, + error: error.into(), + } + } + + pub fn data_loss(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::DataLoss, + error: error.into(), + } + } + + pub fn deadline_exceeded(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::DeadlineExceeded, + error: error.into(), + } + } + + pub fn do_not_use(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::DoNotUse, + error: error.into(), + } + } + + pub fn failed_precondition(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::FailedPrecondition, + error: error.into(), + } + } + + pub fn internal(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Internal, + error: error.into(), + } + } + + pub fn invalid_argument(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::InvalidArgument, + error: error.into(), + } + } + + pub fn not_found(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::NotFound, + error: error.into(), + } + } + + pub fn out_of_range(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::OutOfRange, + error: error.into(), + } + } + + pub fn permission_denied(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::PermissionDenied, + error: error.into(), + } + } + + pub fn resource_exhausted(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::ResourceExhausted, + error: error.into(), + } + } + + pub fn unauthenticated(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Unauthenticated, + error: error.into(), + } + } + + pub fn unavailable(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Unavailable, + error: error.into(), + } + } + + pub fn unimplemented(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Unimplemented, + error: error.into(), + } + } + + pub fn unknown(error: E) -> Error + where + E: Into>, + { + 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 for Error { + fn from(err: DecodeError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: http::Error) -> Self { + if err.is::() + || err.is::() + || err.is::() + || err.is::() + || err.is::() + { + return Self::new(ErrorKind::InvalidArgument, err); + } + + if err.is::() { + return Self::new(ErrorKind::FailedPrecondition, err); + } + + Self::new(ErrorKind::Unknown, err) + } +} + +impl From 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 for Error { + fn from(err: quick_xml::Error) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: serde_json::Error) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From 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 for Error { + fn from(err: FromUtf8Error) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: InvalidHeaderValue) -> Self { + Self::new(ErrorKind::InvalidArgument, err) + } +} + +impl From for Error { + fn from(err: InvalidUri) -> Self { + Self::new(ErrorKind::InvalidArgument, err) + } +} + +impl From for Error { + fn from(err: ParseError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: ParseIntError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: ProtobufError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: RecvError) -> Self { + Self::new(ErrorKind::Internal, err) + } +} + +impl From> for Error { + fn from(err: SendError) -> Self { + Self { + kind: ErrorKind::Internal, + error: ErrorMessage(err.to_string()).into(), + } + } +} + +impl From for Error { + fn from(err: ToStrError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: Utf8Error) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} diff --git a/core/src/file_id.rs b/core/src/file_id.rs index f6e385cd..79969848 100644 --- a/core/src/file_id.rs +++ b/core/src/file_id.rs @@ -1,7 +1,7 @@ -use librespot_protocol as protocol; - use std::fmt; +use librespot_protocol as protocol; + use crate::spotify_id::to_base16; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 52206c5c..2dc21355 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,49 +1,82 @@ +use std::env::consts::OS; + use bytes::Bytes; -use futures_util::future::IntoStream; -use futures_util::FutureExt; +use futures_util::{future::IntoStream, FutureExt}; use http::header::HeaderValue; -use http::uri::InvalidUri; -use hyper::client::{HttpConnector, ResponseFuture}; -use hyper::header::USER_AGENT; -use hyper::{Body, Client, Request, Response, StatusCode}; +use hyper::{ + client::{HttpConnector, ResponseFuture}, + header::USER_AGENT, + Body, Client, Request, Response, StatusCode, +}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::HttpsConnector; use rustls::{ClientConfig, RootCertStore}; use thiserror::Error; use url::Url; -use std::env::consts::OS; - -use crate::version::{ - FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING, +use crate::{ + version::{FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING}, + Error, }; +#[derive(Debug, Error)] +pub enum HttpClientError { + #[error("Response status code: {0}")] + StatusCode(hyper::StatusCode), +} + +impl From for Error { + fn from(err: HttpClientError) -> Self { + match err { + HttpClientError::StatusCode(code) => { + // not exhaustive, but what reasonably could be expected + match code { + StatusCode::GATEWAY_TIMEOUT | StatusCode::REQUEST_TIMEOUT => { + Error::deadline_exceeded(err) + } + StatusCode::GONE + | StatusCode::NOT_FOUND + | StatusCode::MOVED_PERMANENTLY + | StatusCode::PERMANENT_REDIRECT + | StatusCode::TEMPORARY_REDIRECT => Error::not_found(err), + StatusCode::FORBIDDEN | StatusCode::PAYMENT_REQUIRED => { + Error::permission_denied(err) + } + StatusCode::NETWORK_AUTHENTICATION_REQUIRED + | StatusCode::PROXY_AUTHENTICATION_REQUIRED + | StatusCode::UNAUTHORIZED => Error::unauthenticated(err), + StatusCode::EXPECTATION_FAILED + | StatusCode::PRECONDITION_FAILED + | StatusCode::PRECONDITION_REQUIRED => Error::failed_precondition(err), + StatusCode::RANGE_NOT_SATISFIABLE => Error::out_of_range(err), + StatusCode::INTERNAL_SERVER_ERROR + | StatusCode::MISDIRECTED_REQUEST + | StatusCode::SERVICE_UNAVAILABLE + | StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => Error::unavailable(err), + StatusCode::BAD_REQUEST + | StatusCode::HTTP_VERSION_NOT_SUPPORTED + | StatusCode::LENGTH_REQUIRED + | StatusCode::METHOD_NOT_ALLOWED + | StatusCode::NOT_ACCEPTABLE + | StatusCode::PAYLOAD_TOO_LARGE + | StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE + | StatusCode::UNSUPPORTED_MEDIA_TYPE + | StatusCode::URI_TOO_LONG => Error::invalid_argument(err), + StatusCode::TOO_MANY_REQUESTS => Error::resource_exhausted(err), + StatusCode::NOT_IMPLEMENTED => Error::unimplemented(err), + _ => Error::unknown(err), + } + } + } + } +} + pub struct HttpClient { user_agent: HeaderValue, proxy: Option, 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 for HttpClientError { - fn from(err: InvalidUri) -> Self { - Self::Parsing(err.into()) - } -} - impl HttpClient { pub fn new(proxy: Option<&Url>) -> Self { let spotify_version = match OS { @@ -53,7 +86,7 @@ impl HttpClient { let spotify_platform = match OS { "android" => "Android/31", - "ios" => "iOS/15.1.1", + "ios" => "iOS/15.2", "macos" => "OSX/0", "windows" => "Win32/0", _ => "Linux/0", @@ -95,37 +128,32 @@ impl HttpClient { } } - pub async fn request(&self, req: Request) -> Result, HttpClientError> { + pub async fn request(&self, req: Request) -> Result, Error> { debug!("Requesting {:?}", req.uri().to_string()); let request = self.request_fut(req)?; - { - let response = request.await; - if let Ok(response) = &response { - let status = response.status(); - if status != StatusCode::OK { - return Err(HttpClientError::NotOK(status.into())); - } + let response = request.await; + + if let Ok(response) = &response { + let code = response.status(); + if code != StatusCode::OK { + return Err(HttpClientError::StatusCode(code).into()); } - response.map_err(HttpClientError::Response) } + + Ok(response?) } - pub async fn request_body(&self, req: Request) -> Result { + pub async fn request_body(&self, req: Request) -> Result { let response = self.request(req).await?; - hyper::body::to_bytes(response.into_body()) - .await - .map_err(HttpClientError::Response) + Ok(hyper::body::to_bytes(response.into_body()).await?) } - pub fn request_stream( - &self, - req: Request, - ) -> Result, HttpClientError> { + pub fn request_stream(&self, req: Request) -> Result, Error> { Ok(self.request_fut(req)?.into_stream()) } - pub fn request_fut(&self, mut req: Request) -> Result { + pub fn request_fut(&self, mut req: Request) -> Result { let mut http = HttpConnector::new(); http.enforce_http(false); let connector = HttpsConnector::from((http, self.tls_config.clone())); diff --git a/core/src/lib.rs b/core/src/lib.rs index 76ddbd37..a0f180ca 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -20,6 +20,7 @@ pub mod date; mod dealer; #[doc(hidden)] pub mod diffie_hellman; +pub mod error; pub mod file_id; mod http_client; pub mod mercury; @@ -34,3 +35,9 @@ pub mod token; #[doc(hidden)] pub mod util; pub mod version; + +pub use config::SessionConfig; +pub use error::Error; +pub use file_id::FileId; +pub use session::Session; +pub use spotify_id::SpotifyId; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index ad2d5013..b693444a 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -1,9 +1,10 @@ -use std::collections::HashMap; -use std::future::Future; -use std::mem; -use std::pin::Pin; -use std::task::Context; -use std::task::Poll; +use std::{ + collections::HashMap, + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, +}; use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; @@ -11,9 +12,7 @@ use futures_util::FutureExt; use protobuf::Message; use tokio::sync::{mpsc, oneshot}; -use crate::packet::PacketType; -use crate::protocol; -use crate::util::SeqGenerator; +use crate::{packet::PacketType, protocol, util::SeqGenerator, Error}; mod types; pub use self::types::*; @@ -33,18 +32,18 @@ component! { pub struct MercuryPending { parts: Vec>, partial: Option>, - callback: Option>>, + callback: Option>>, } pub struct MercuryFuture { - receiver: oneshot::Receiver>, + receiver: oneshot::Receiver>, } impl Future for MercuryFuture { - type Output = Result; + type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - self.receiver.poll_unpin(cx).map_err(|_| MercuryError)? + self.receiver.poll_unpin(cx)? } } @@ -55,7 +54,7 @@ impl MercuryManager { seq } - fn request(&self, req: MercuryRequest) -> MercuryFuture { + fn request(&self, req: MercuryRequest) -> Result, Error> { let (tx, rx) = oneshot::channel(); let pending = MercuryPending { @@ -72,13 +71,13 @@ impl MercuryManager { }); let cmd = req.method.command(); - let data = req.encode(&seq); + let data = req.encode(&seq)?; - self.session().send_packet(cmd, data); - MercuryFuture { receiver: rx } + self.session().send_packet(cmd, data)?; + Ok(MercuryFuture { receiver: rx }) } - pub fn get>(&self, uri: T) -> MercuryFuture { + pub fn get>(&self, uri: T) -> Result, Error> { self.request(MercuryRequest { method: MercuryMethod::Get, uri: uri.into(), @@ -87,7 +86,11 @@ impl MercuryManager { }) } - pub fn send>(&self, uri: T, data: Vec) -> MercuryFuture { + pub fn send>( + &self, + uri: T, + data: Vec, + ) -> Result, Error> { self.request(MercuryRequest { method: MercuryMethod::Send, uri: uri.into(), @@ -103,7 +106,7 @@ impl MercuryManager { pub fn subscribe>( &self, uri: T, - ) -> impl Future, MercuryError>> + 'static + ) -> impl Future, Error>> + 'static { let uri = uri.into(); let request = self.request(MercuryRequest { @@ -115,7 +118,7 @@ impl MercuryManager { let manager = self.clone(); async move { - let response = request.await?; + let response = request?.await?; let (tx, rx) = mpsc::unbounded_channel(); @@ -125,13 +128,18 @@ impl MercuryManager { if !response.payload.is_empty() { // Old subscription protocol, watch the provided list of URIs for sub in response.payload { - let mut sub = - protocol::pubsub::Subscription::parse_from_bytes(&sub).unwrap(); - let sub_uri = sub.take_uri(); + match protocol::pubsub::Subscription::parse_from_bytes(&sub) { + Ok(mut sub) => { + let sub_uri = sub.take_uri(); - debug!("subscribed sub_uri={}", sub_uri); + debug!("subscribed sub_uri={}", sub_uri); - inner.subscriptions.push((sub_uri, tx.clone())); + inner.subscriptions.push((sub_uri, tx.clone())); + } + Err(e) => { + error!("could not subscribe to {}: {}", uri, e); + } + } } } else { // New subscription protocol, watch the requested URI @@ -165,7 +173,7 @@ impl MercuryManager { } } - pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> { let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize; let seq = data.split_to(seq_len).as_ref().to_owned(); @@ -185,7 +193,7 @@ impl MercuryManager { } } else { warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8); - return; + return Err(MercuryError::Command(cmd).into()); } } }; @@ -205,10 +213,12 @@ impl MercuryManager { } if flags == 0x1 { - self.complete_request(cmd, pending); + self.complete_request(cmd, pending)?; } else { self.lock(move |inner| inner.pending.insert(seq, pending)); } + + Ok(()) } fn parse_part(data: &mut Bytes) -> Vec { @@ -216,9 +226,9 @@ impl MercuryManager { data.split_to(size).as_ref().to_owned() } - fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) { + fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) -> Result<(), Error> { let header_data = pending.parts.remove(0); - let header = protocol::mercury::Header::parse_from_bytes(&header_data).unwrap(); + let header = protocol::mercury::Header::parse_from_bytes(&header_data)?; let response = MercuryResponse { uri: header.get_uri().to_string(), @@ -226,13 +236,17 @@ impl MercuryManager { payload: pending.parts, }; - if response.status_code >= 500 { - panic!("Spotify servers returned an error. Restart librespot."); - } else if response.status_code >= 400 { - warn!("error {} for uri {}", response.status_code, &response.uri); + let status_code = response.status_code; + if status_code >= 500 { + error!("error {} for uri {}", status_code, &response.uri); + Err(MercuryError::Response(response).into()) + } else if status_code >= 400 { + error!("error {} for uri {}", status_code, &response.uri); if let Some(cb) = pending.callback { - let _ = cb.send(Err(MercuryError)); + cb.send(Err(MercuryError::Response(response.clone()).into())) + .map_err(|_| MercuryError::Channel)?; } + Err(MercuryError::Response(response).into()) } else if let PacketType::MercuryEvent = cmd { self.lock(|inner| { let mut found = false; @@ -242,7 +256,7 @@ impl MercuryManager { // before sending while saving the subscription under its unencoded form. let mut uri_split = response.uri.split('/'); - let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string()) + let encoded_uri = std::iter::once(uri_split.next().unwrap_or_default().to_string()) .chain(uri_split.map(|component| { form_urlencoded::byte_serialize(component.as_bytes()).collect::() })) @@ -263,12 +277,19 @@ impl MercuryManager { }); if !found { - debug!("unknown subscription uri={}", response.uri); + debug!("unknown subscription uri={}", &response.uri); trace!("response pushed over Mercury: {:?}", response); + Err(MercuryError::Response(response).into()) + } else { + Ok(()) } }) } else if let Some(cb) = pending.callback { - let _ = cb.send(Ok(response)); + cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?; + Ok(()) + } else { + error!("can't handle Mercury response: {:?}", response); + Err(MercuryError::Response(response).into()) } } diff --git a/core/src/mercury/sender.rs b/core/src/mercury/sender.rs index 268554d9..31409e88 100644 --- a/core/src/mercury/sender.rs +++ b/core/src/mercury/sender.rs @@ -1,6 +1,8 @@ use std::collections::VecDeque; -use super::*; +use super::{MercuryFuture, MercuryManager, MercuryResponse}; + +use crate::Error; pub struct MercurySender { mercury: MercuryManager, @@ -23,12 +25,13 @@ impl MercurySender { self.buffered_future.is_none() && self.pending.is_empty() } - pub fn send(&mut self, item: Vec) { - let task = self.mercury.send(self.uri.clone(), item); + pub fn send(&mut self, item: Vec) -> Result<(), Error> { + let task = self.mercury.send(self.uri.clone(), item)?; self.pending.push_back(task); + Ok(()) } - pub async fn flush(&mut self) -> Result<(), MercuryError> { + pub async fn flush(&mut self) -> Result<(), Error> { if self.buffered_future.is_none() { self.buffered_future = self.pending.pop_front(); } diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 007ffb38..9c7593fe 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -1,11 +1,10 @@ +use std::io::Write; + use byteorder::{BigEndian, WriteBytesExt}; use protobuf::Message; -use std::fmt; -use std::io::Write; use thiserror::Error; -use crate::packet::PacketType; -use crate::protocol; +use crate::{packet::PacketType, protocol, Error}; #[derive(Debug, PartialEq, Eq)] pub enum MercuryMethod { @@ -30,12 +29,23 @@ pub struct MercuryResponse { pub payload: Vec>, } -#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)] -pub struct MercuryError; +#[derive(Debug, Error)] +pub enum MercuryError { + #[error("callback receiver was disconnected")] + Channel, + #[error("error handling packet type: {0:?}")] + Command(PacketType), + #[error("error handling Mercury response: {0:?}")] + Response(MercuryResponse), +} -impl fmt::Display for MercuryError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Mercury error") +impl From for Error { + fn from(err: MercuryError) -> Self { + match err { + MercuryError::Channel => Error::aborted(err), + MercuryError::Command(_) => Error::unimplemented(err), + MercuryError::Response(_) => Error::unavailable(err), + } } } @@ -63,15 +73,12 @@ impl MercuryMethod { } impl MercuryRequest { - // TODO: change into Result and remove unwraps - pub fn encode(&self, seq: &[u8]) -> Vec { + pub fn encode(&self, seq: &[u8]) -> Result, Error> { let mut packet = Vec::new(); - packet.write_u16::(seq.len() as u16).unwrap(); - packet.write_all(seq).unwrap(); - packet.write_u8(1).unwrap(); // Flags: FINAL - packet - .write_u16::(1 + self.payload.len() as u16) - .unwrap(); // Part count + packet.write_u16::(seq.len() as u16)?; + packet.write_all(seq)?; + packet.write_u8(1)?; // Flags: FINAL + packet.write_u16::(1 + self.payload.len() as u16)?; // Part count let mut header = protocol::mercury::Header::new(); header.set_uri(self.uri.clone()); @@ -81,16 +88,14 @@ impl MercuryRequest { header.set_content_type(content_type.clone()); } - packet - .write_u16::(header.compute_size() as u16) - .unwrap(); - header.write_to_writer(&mut packet).unwrap(); + packet.write_u16::(header.compute_size() as u16)?; + header.write_to_writer(&mut packet)?; for p in &self.payload { - packet.write_u16::(p.len() as u16).unwrap(); - packet.write_all(p).unwrap(); + packet.write_u16::(p.len() as u16)?; + packet.write_all(p)?; } - packet + Ok(packet) } } diff --git a/core/src/packet.rs b/core/src/packet.rs index de780f13..2f50d158 100644 --- a/core/src/packet.rs +++ b/core/src/packet.rs @@ -2,7 +2,7 @@ use num_derive::{FromPrimitive, ToPrimitive}; -#[derive(Debug, FromPrimitive, ToPrimitive)] +#[derive(Debug, Copy, Clone, FromPrimitive, ToPrimitive)] pub enum PacketType { SecretBlock = 0x02, Ping = 0x04, diff --git a/core/src/session.rs b/core/src/session.rs index 426480f6..72805551 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -1,13 +1,16 @@ -use std::collections::HashMap; -use std::future::Future; -use std::io; -use std::pin::Pin; -use std::process::exit; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, RwLock, Weak}; -use std::task::Context; -use std::task::Poll; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + collections::HashMap, + future::Future, + io, + pin::Pin, + process::exit, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, RwLock, Weak, + }, + task::{Context, Poll}, + time::{SystemTime, UNIX_EPOCH}, +}; use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; @@ -20,18 +23,21 @@ use thiserror::Error; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; -use crate::apresolve::ApResolver; -use crate::audio_key::AudioKeyManager; -use crate::authentication::Credentials; -use crate::cache::Cache; -use crate::channel::ChannelManager; -use crate::config::SessionConfig; -use crate::connection::{self, AuthenticationError}; -use crate::http_client::HttpClient; -use crate::mercury::MercuryManager; -use crate::packet::PacketType; -use crate::spclient::SpClient; -use crate::token::TokenProvider; +use crate::{ + apresolve::ApResolver, + audio_key::AudioKeyManager, + authentication::Credentials, + cache::Cache, + channel::ChannelManager, + config::SessionConfig, + connection::{self, AuthenticationError}, + http_client::HttpClient, + mercury::MercuryManager, + packet::PacketType, + spclient::SpClient, + token::TokenProvider, + Error, +}; #[derive(Debug, Error)] pub enum SessionError { @@ -39,6 +45,18 @@ pub enum SessionError { AuthenticationError(#[from] AuthenticationError), #[error("Cannot create session: {0}")] IoError(#[from] io::Error), + #[error("packet {0} unknown")] + Packet(u8), +} + +impl From 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; @@ -88,7 +106,7 @@ impl Session { config: SessionConfig, credentials: Credentials, cache: Option, - ) -> Result { + ) -> Result { let http_client = HttpClient::new(config.proxy.as_ref()); let (sender_tx, sender_rx) = mpsc::unbounded_channel(); let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); @@ -214,9 +232,18 @@ impl Session { } } - fn dispatch(&self, cmd: u8, data: Bytes) { + fn dispatch(&self, cmd: u8, data: Bytes) -> Result<(), Error> { use PacketType::*; + let packet_type = FromPrimitive::from_u8(cmd); + let cmd = match packet_type { + Some(cmd) => cmd, + None => { + trace!("Ignoring unknown packet {:x}", cmd); + return Err(SessionError::Packet(cmd).into()); + } + }; + match packet_type { Some(Ping) => { let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64; @@ -229,24 +256,21 @@ impl Session { self.0.data.write().unwrap().time_delta = server_timestamp - timestamp; self.debug_info(); - self.send_packet(Pong, vec![0, 0, 0, 0]); + self.send_packet(Pong, vec![0, 0, 0, 0]) } Some(CountryCode) => { - let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); + let country = String::from_utf8(data.as_ref().to_owned())?; info!("Country: {:?}", country); self.0.data.write().unwrap().user_data.country = country; + Ok(()) } - Some(StreamChunkRes) | Some(ChannelError) => { - self.channel().dispatch(packet_type.unwrap(), data); - } - Some(AesKey) | Some(AesKeyError) => { - self.audio_key().dispatch(packet_type.unwrap(), data); - } + Some(StreamChunkRes) | Some(ChannelError) => self.channel().dispatch(cmd, data), + Some(AesKey) | Some(AesKeyError) => self.audio_key().dispatch(cmd, data), Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => { - self.mercury().dispatch(packet_type.unwrap(), data); + self.mercury().dispatch(cmd, data) } Some(ProductInfo) => { - let data = std::str::from_utf8(&data).unwrap(); + let data = std::str::from_utf8(&data)?; let mut reader = quick_xml::Reader::from_str(data); let mut buf = Vec::new(); @@ -256,8 +280,7 @@ impl Session { loop { match reader.read_event(&mut buf) { Ok(Event::Start(ref element)) => { - current_element = - std::str::from_utf8(element.name()).unwrap().to_owned() + current_element = std::str::from_utf8(element.name())?.to_owned() } Ok(Event::End(_)) => { current_element = String::new(); @@ -266,7 +289,7 @@ impl Session { if !current_element.is_empty() { let _ = user_attributes.insert( current_element.clone(), - value.unescape_and_decode(&reader).unwrap(), + value.unescape_and_decode(&reader)?, ); } } @@ -284,24 +307,23 @@ impl Session { Self::check_catalogue(&user_attributes); self.0.data.write().unwrap().user_data.attributes = user_attributes; + Ok(()) } Some(PongAck) | Some(SecretBlock) | Some(LegacyWelcome) | Some(UnknownDataAllZeros) - | Some(LicenseVersion) => {} + | Some(LicenseVersion) => Ok(()), _ => { - if let Some(packet_type) = PacketType::from_u8(cmd) { - trace!("Ignoring {:?} packet with data {:#?}", packet_type, data); - } else { - trace!("Ignoring unknown packet {:x}", cmd); - } + trace!("Ignoring {:?} packet with data {:#?}", cmd, data); + Err(SessionError::Packet(cmd as u8).into()) } } } - pub fn send_packet(&self, cmd: PacketType, data: Vec) { - self.0.tx_connection.send((cmd as u8, data)).unwrap(); + pub fn send_packet(&self, cmd: PacketType, data: Vec) -> Result<(), Error> { + self.0.tx_connection.send((cmd as u8, data))?; + Ok(()) } pub fn cache(&self) -> Option<&Arc> { @@ -393,7 +415,7 @@ impl SessionWeak { } pub(crate) fn upgrade(&self) -> Session { - self.try_upgrade().expect("Session died") + self.try_upgrade().expect("Session died") // TODO } } @@ -434,7 +456,9 @@ where } }; - session.dispatch(cmd, data); + if let Err(e) = session.dispatch(cmd, data) { + error!("could not dispatch command: {}", e); + } } } } diff --git a/core/src/socket.rs b/core/src/socket.rs index 92274cc6..84ac6024 100644 --- a/core/src/socket.rs +++ b/core/src/socket.rs @@ -1,5 +1,4 @@ -use std::io; -use std::net::ToSocketAddrs; +use std::{io, net::ToSocketAddrs}; use tokio::net::TcpStream; use url::Url; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index c0336690..c4285cd4 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -1,22 +1,25 @@ -use crate::apresolve::SocketAddress; -use crate::file_id::FileId; -use crate::http_client::HttpClientError; -use crate::mercury::MercuryError; -use crate::protocol::canvaz::EntityCanvazRequest; -use crate::protocol::connect::PutStateRequest; -use crate::protocol::extended_metadata::BatchedEntityRequest; -use crate::spotify_id::SpotifyId; +use std::time::Duration; use bytes::Bytes; use futures_util::future::IntoStream; use http::header::HeaderValue; -use hyper::client::ResponseFuture; -use hyper::header::{InvalidHeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}; -use hyper::{Body, HeaderMap, Method, Request}; +use hyper::{ + client::ResponseFuture, + header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, + Body, HeaderMap, Method, Request, +}; use protobuf::Message; use rand::Rng; -use std::time::Duration; -use thiserror::Error; + +use crate::{ + apresolve::SocketAddress, + error::ErrorKind, + protocol::{ + canvaz::EntityCanvazRequest, connect::PutStateRequest, + extended_metadata::BatchedEntityRequest, + }, + Error, FileId, SpotifyId, +}; component! { SpClient : SpClientInner { @@ -25,23 +28,7 @@ component! { } } -pub type SpClientResult = Result; - -#[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 for SpClientError { - fn from(err: InvalidHeaderValue) -> Self { - Self::Parsing(err.into()) - } -} +pub type SpClientResult = Result; #[derive(Copy, Clone, Debug)] pub enum RequestStrategy { @@ -157,12 +144,8 @@ impl SpClient { ))?, ); - last_response = self - .session() - .http_client() - .request_body(request) - .await - .map_err(SpClientError::Network); + last_response = self.session().http_client().request_body(request).await; + if last_response.is_ok() { return last_response; } @@ -177,9 +160,9 @@ impl SpClient { // Reconnection logic: drop the current access point if we are experiencing issues. // This will cause the next call to base_url() to resolve a new one. - if let Err(SpClientError::Network(ref network_error)) = last_response { - match network_error { - HttpClientError::Response(_) | HttpClientError::Request(_) => { + if let Err(ref network_error) = last_response { + match network_error.kind { + ErrorKind::Unavailable | ErrorKind::DeadlineExceeded => { // Keep trying the current access point three times before dropping it. if tries % 3 == 0 { self.flush_accesspoint().await @@ -244,7 +227,7 @@ impl SpClient { } pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult { - let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62(),); + let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62()); self.request_as_json(&Method::GET, &endpoint, None, None) .await @@ -291,7 +274,7 @@ impl SpClient { url: &str, offset: usize, length: usize, - ) -> Result, SpClientError> { + ) -> Result, Error> { let req = Request::builder() .method(&Method::GET) .uri(url) diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 9f6d92ed..15b365b0 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -1,13 +1,17 @@ -use librespot_protocol as protocol; +use std::{ + convert::{TryFrom, TryInto}, + fmt, + ops::Deref, +}; use thiserror::Error; -use std::convert::{TryFrom, TryInto}; -use std::fmt; -use std::ops::Deref; +use crate::Error; + +use librespot_protocol as protocol; // re-export FileId for historic reasons, when it was part of this mod -pub use crate::file_id::FileId; +pub use crate::FileId; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SpotifyItemType { @@ -64,8 +68,14 @@ pub enum SpotifyIdError { InvalidRoot, } -pub type SpotifyIdResult = Result; -pub type NamedSpotifyIdResult = Result; +impl From for Error { + fn from(err: SpotifyIdError) -> Self { + Error::invalid_argument(err) + } +} + +pub type SpotifyIdResult = Result; +pub type NamedSpotifyIdResult = Result; const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef"; @@ -95,7 +105,7 @@ impl SpotifyId { let p = match c { b'0'..=b'9' => c - b'0', b'a'..=b'f' => c - b'a' + 10, - _ => return Err(SpotifyIdError::InvalidId), + _ => return Err(SpotifyIdError::InvalidId.into()), } as u128; dst <<= 4; @@ -121,7 +131,7 @@ impl SpotifyId { b'0'..=b'9' => c - b'0', b'a'..=b'z' => c - b'a' + 10, b'A'..=b'Z' => c - b'A' + 36, - _ => return Err(SpotifyIdError::InvalidId), + _ => return Err(SpotifyIdError::InvalidId.into()), } as u128; dst *= 62; @@ -143,7 +153,7 @@ impl SpotifyId { id: u128::from_be_bytes(dst), item_type: SpotifyItemType::Unknown, }), - Err(_) => Err(SpotifyIdError::InvalidId), + Err(_) => Err(SpotifyIdError::InvalidId.into()), } } @@ -161,20 +171,20 @@ impl SpotifyId { // At minimum, should be `spotify:{type}:{id}` if uri_parts.len() < 3 { - return Err(SpotifyIdError::InvalidFormat); + return Err(SpotifyIdError::InvalidFormat.into()); } if uri_parts[0] != "spotify" { - return Err(SpotifyIdError::InvalidRoot); + return Err(SpotifyIdError::InvalidRoot.into()); } - let id = uri_parts.pop().unwrap(); + let id = uri_parts.pop().unwrap_or_default(); if id.len() != Self::SIZE_BASE62 { - return Err(SpotifyIdError::InvalidId); + return Err(SpotifyIdError::InvalidId.into()); } Ok(Self { - item_type: uri_parts.pop().unwrap().into(), + item_type: uri_parts.pop().unwrap_or_default().into(), ..Self::from_base62(id)? }) } @@ -285,15 +295,15 @@ impl NamedSpotifyId { // At minimum, should be `spotify:user:{username}:{type}:{id}` if uri_parts.len() < 5 { - return Err(SpotifyIdError::InvalidFormat); + return Err(SpotifyIdError::InvalidFormat.into()); } if uri_parts[0] != "spotify" { - return Err(SpotifyIdError::InvalidRoot); + return Err(SpotifyIdError::InvalidRoot.into()); } if uri_parts[1] != "user" { - return Err(SpotifyIdError::InvalidFormat); + return Err(SpotifyIdError::InvalidFormat.into()); } Ok(Self { @@ -344,35 +354,35 @@ impl fmt::Display for NamedSpotifyId { } impl TryFrom<&[u8]> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(src: &[u8]) -> Result { Self::from_raw(src) } } impl TryFrom<&str> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(src: &str) -> Result { Self::from_base62(src) } } impl TryFrom for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(src: String) -> Result { Self::try_from(src.as_str()) } } impl TryFrom<&Vec> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(src: &Vec) -> Result { Self::try_from(src.as_slice()) } } impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(track: &protocol::spirc::TrackRef) -> Result { match SpotifyId::from_raw(track.get_gid()) { Ok(mut id) => { @@ -385,7 +395,7 @@ impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId { } impl TryFrom<&protocol::metadata::Album> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(album: &protocol::metadata::Album) -> Result { Ok(Self { item_type: SpotifyItemType::Album, @@ -395,7 +405,7 @@ impl TryFrom<&protocol::metadata::Album> for SpotifyId { } impl TryFrom<&protocol::metadata::Artist> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(artist: &protocol::metadata::Artist) -> Result { Ok(Self { item_type: SpotifyItemType::Artist, @@ -405,7 +415,7 @@ impl TryFrom<&protocol::metadata::Artist> for SpotifyId { } impl TryFrom<&protocol::metadata::Episode> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(episode: &protocol::metadata::Episode) -> Result { Ok(Self { item_type: SpotifyItemType::Episode, @@ -415,7 +425,7 @@ impl TryFrom<&protocol::metadata::Episode> for SpotifyId { } impl TryFrom<&protocol::metadata::Track> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(track: &protocol::metadata::Track) -> Result { Ok(Self { item_type: SpotifyItemType::Track, @@ -425,7 +435,7 @@ impl TryFrom<&protocol::metadata::Track> for SpotifyId { } impl TryFrom<&protocol::metadata::Show> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(show: &protocol::metadata::Show) -> Result { Ok(Self { item_type: SpotifyItemType::Show, @@ -435,7 +445,7 @@ impl TryFrom<&protocol::metadata::Show> for SpotifyId { } impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result { Ok(Self { item_type: SpotifyItemType::Artist, @@ -445,7 +455,7 @@ impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId { } impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(item: &protocol::playlist4_external::Item) -> Result { Ok(Self { item_type: SpotifyItemType::Track, @@ -457,7 +467,7 @@ impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId { // Note that this is the unique revision of an item's metadata on a playlist, // not the ID of that item or playlist. impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result { Self::try_from(item.get_revision()) } @@ -465,7 +475,7 @@ impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId { // Note that this is the unique revision of a playlist, not the ID of that playlist. impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from( playlist: &protocol::playlist4_external::SelectedListContent, ) -> Result { @@ -477,7 +487,7 @@ impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId { // which is why we now don't create a separate `Playlist` enum value yet and choose // to discard any item type. impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from( picture: &protocol::playlist_annotate3::TranscodedPicture, ) -> Result { @@ -565,7 +575,7 @@ mod tests { id: 0, kind: SpotifyItemType::Unknown, // Invalid ID in the URI. - uri_error: Some(SpotifyIdError::InvalidId), + uri_error: SpotifyIdError::InvalidId, uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH", base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9", base62: "!!!!!Ys0csV6RS48xBl0tH", @@ -578,7 +588,7 @@ mod tests { id: 0, kind: SpotifyItemType::Unknown, // Missing colon between ID and type. - uri_error: Some(SpotifyIdError::InvalidFormat), + uri_error: SpotifyIdError::InvalidFormat, uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", base16: "--------------------", base62: "....................", @@ -591,7 +601,7 @@ mod tests { id: 0, kind: SpotifyItemType::Unknown, // Uri too short - uri_error: Some(SpotifyIdError::InvalidId), + uri_error: SpotifyIdError::InvalidId, uri: "spotify:azb:aRS48xBl0tH", base16: "--------------------", base62: "....................", diff --git a/core/src/token.rs b/core/src/token.rs index b9afa620..0c0b7394 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -8,12 +8,12 @@ // user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming, // app-remote-control -use crate::mercury::MercuryError; +use std::time::{Duration, Instant}; use serde::Deserialize; +use thiserror::Error; -use std::error::Error; -use std::time::{Duration, Instant}; +use crate::Error; component! { TokenProvider : TokenProviderInner { @@ -21,6 +21,18 @@ component! { } } +#[derive(Debug, Error)] +pub enum TokenError { + #[error("no tokens available")] + Empty, +} + +impl From for Error { + fn from(err: TokenError) -> Self { + Error::unavailable(err) + } +} + #[derive(Clone, Debug)] pub struct Token { pub access_token: String, @@ -54,11 +66,7 @@ impl TokenProvider { } // scopes must be comma-separated - pub async fn get_token(&self, scopes: &str) -> Result { - if scopes.is_empty() { - return Err(MercuryError); - } - + pub async fn get_token(&self, scopes: &str) -> Result { if let Some(index) = self.find_token(scopes.split(',').collect()) { let cached_token = self.lock(|inner| inner.tokens[index].clone()); if cached_token.is_expired() { @@ -79,14 +87,10 @@ impl TokenProvider { Self::KEYMASTER_CLIENT_ID, self.session().device_id() ); - let request = self.session().mercury().get(query_uri); + let request = self.session().mercury().get(query_uri)?; let response = request.await?; - let data = response - .payload - .first() - .expect("No tokens received") - .to_vec(); - let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?; + let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec(); + let token = Token::new(String::from_utf8(data)?)?; trace!("Got token: {:#?}", token); self.lock(|inner| inner.tokens.push(token.clone())); Ok(token) @@ -96,7 +100,7 @@ impl TokenProvider { impl Token { const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10); - pub fn new(body: String) -> Result> { + pub fn new(body: String) -> Result { let data: TokenData = serde_json::from_slice(body.as_ref())?; Ok(Self { access_token: data.access_token, diff --git a/core/src/util.rs b/core/src/util.rs index 4f78c467..a01f8b56 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,15 +1,13 @@ -use std::future::Future; -use std::mem; -use std::pin::Pin; -use std::task::Context; -use std::task::Poll; +use std::{ + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, +}; use futures_core::ready; -use futures_util::FutureExt; -use futures_util::Sink; -use futures_util::{future, SinkExt}; -use tokio::task::JoinHandle; -use tokio::time::timeout; +use futures_util::{future, FutureExt, Sink, SinkExt}; +use tokio::{task::JoinHandle, time::timeout}; /// Returns a future that will flush the sink, even if flushing is temporarily completed. /// Finishes only if the sink throws an error. diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 368f3747..7edd934a 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -13,6 +13,7 @@ base64 = "0.13" cfg-if = "1.0" form_urlencoded = "1.0" futures-core = "0.3" +futures-util = "0.3" hmac = "0.11" hyper = { version = "0.14", features = ["server", "http1", "tcp"] } libmdns = "0.6" diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index 98f776fb..a29b3b8c 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -27,6 +27,8 @@ pub use crate::core::authentication::Credentials; /// Determining the icon in the list of available devices. pub use crate::core::config::DeviceType; +pub use crate::core::Error; + /// Makes this device visible to Spotify clients in the local network. /// /// `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. #[derive(Debug, Error)] -pub enum Error { +pub enum DiscoveryError { /// Setting up service discovery via DNS-SD failed. #[error("Setting up dns-sd failed: {0}")] DnsSdError(#[from] io::Error), /// Setting up the http server failed. + #[error("Creating SHA1 HMAC failed for base key {0:?}")] + HmacError(Vec), #[error("Setting up the http server failed: {0}")] HttpServerError(#[from] hyper::Error), + #[error("Missing params for key {0}")] + ParamsError(&'static str), +} + +impl From 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 { @@ -96,7 +113,7 @@ impl Builder { pub fn launch(self) -> Result { let mut port = self.port; 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; @@ -109,8 +126,7 @@ impl Builder { None, port, &["VERSION=1.0", "CPath=/"], - ) - .unwrap(); + )?; } else { let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; diff --git a/discovery/src/server.rs b/discovery/src/server.rs index a82f90c0..74af6fa3 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -1,26 +1,35 @@ -use std::borrow::Cow; -use std::collections::BTreeMap; -use std::convert::Infallible; -use std::net::{Ipv4Addr, SocketAddr}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; +use std::{ + borrow::Cow, + collections::BTreeMap, + convert::Infallible, + net::{Ipv4Addr, SocketAddr}, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; -use aes_ctr::cipher::generic_array::GenericArray; -use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; -use aes_ctr::Aes128Ctr; +use aes_ctr::{ + cipher::generic_array::GenericArray, + cipher::{NewStreamCipher, SyncStreamCipher}, + Aes128Ctr, +}; use futures_core::Stream; +use futures_util::{FutureExt, TryFutureExt}; use hmac::{Hmac, Mac, NewMac}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Method, Request, Response, StatusCode}; -use log::{debug, warn}; +use hyper::{ + service::{make_service_fn, service_fn}, + Body, Method, Request, Response, StatusCode, +}; +use log::{debug, error, warn}; use serde_json::json; use sha1::{Digest, Sha1}; use tokio::sync::{mpsc, oneshot}; -use crate::core::authentication::Credentials; -use crate::core::config::DeviceType; -use crate::core::diffie_hellman::DhLocalKeys; +use super::DiscoveryError; + +use crate::core::{ + authentication::Credentials, config::DeviceType, diffie_hellman::DhLocalKeys, Error, +}; type Params<'a> = BTreeMap, Cow<'a, str>>; @@ -76,14 +85,26 @@ impl RequestHandler { Response::new(Body::from(body)) } - fn handle_add_user(&self, params: &Params<'_>) -> Response { - let username = params.get("userName").unwrap().as_ref(); - let encrypted_blob = params.get("blob").unwrap(); - let client_key = params.get("clientKey").unwrap(); + fn handle_add_user(&self, params: &Params<'_>) -> Result, Error> { + let username_key = "userName"; + let username = params + .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 iv = &encrypted_blob[0..16]; @@ -94,21 +115,21 @@ impl RequestHandler { let base_key = &base_key[..16]; let checksum_key = { - let mut h = - Hmac::::new_from_slice(base_key).expect("HMAC can take key of any size"); + let mut h = Hmac::::new_from_slice(base_key) + .map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?; h.update(b"checksum"); h.finalize().into_bytes() }; let encryption_key = { - let mut h = - Hmac::::new_from_slice(base_key).expect("HMAC can take key of any size"); + let mut h = Hmac::::new_from_slice(base_key) + .map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?; h.update(b"encryption"); h.finalize().into_bytes() }; - let mut h = - Hmac::::new_from_slice(&checksum_key).expect("HMAC can take key of any size"); + let mut h = Hmac::::new_from_slice(&checksum_key) + .map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?; h.update(encrypted); if h.verify(cksum).is_err() { warn!("Login error for user {:?}: MAC mismatch", username); @@ -119,7 +140,7 @@ impl RequestHandler { }); let body = result.to_string(); - return Response::new(Body::from(body)); + return Ok(Response::new(Body::from(body))); } let decrypted = { @@ -132,9 +153,9 @@ impl RequestHandler { 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!({ "status": 101, @@ -143,7 +164,7 @@ impl RequestHandler { }); let body = result.to_string(); - Response::new(Body::from(body)) + Ok(Response::new(Body::from(body))) } fn not_found(&self) -> Response { @@ -152,7 +173,10 @@ impl RequestHandler { res } - async fn handle(self: Arc, request: Request) -> hyper::Result> { + async fn handle( + self: Arc, + request: Request, + ) -> Result>, Error> { let mut params = Params::new(); let (parts, body) = request.into_parts(); @@ -172,11 +196,11 @@ impl RequestHandler { 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::POST, Some("addUser")) => self.handle_add_user(¶ms), + (Method::POST, Some("addUser")) => self.handle_add_user(¶ms)?, _ => self.not_found(), - }) + })) } } @@ -186,7 +210,7 @@ pub struct DiscoveryServer { } impl DiscoveryServer { - pub fn new(config: Config, port: &mut u16) -> hyper::Result { + pub fn new(config: Config, port: &mut u16) -> Result, Error> { let (discovery, cred_rx) = RequestHandler::new(config); let discovery = Arc::new(discovery); @@ -197,7 +221,14 @@ impl DiscoveryServer { let make_service = make_service_fn(move |_| { let discovery = discovery.clone(); 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 { let result = server .with_graceful_shutdown(async { - close_rx.await.unwrap_err(); debug!("Shutting down discovery server"); + if close_rx.await.is_ok() { + debug!("unable to close discovery Rx channel completely"); + } }) .await; @@ -219,10 +252,10 @@ impl DiscoveryServer { } }); - Ok(Self { + Ok(Ok(Self { cred_rx, _close_tx: close_tx, - }) + })) } } diff --git a/metadata/src/album.rs b/metadata/src/album.rs index ac6fec20..6e07ed7e 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -1,30 +1,20 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::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 std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, }; -use librespot_core::date::Date; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; +use crate::{ + artist::Artists, availability::Availabilities, copyright::Copyrights, 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, Error, Session, SpotifyId}; + use librespot_protocol as protocol; - -use protocol::metadata::Disc as DiscMessage; - pub use protocol::metadata::Album_Type as AlbumType; +use protocol::metadata::Disc as DiscMessage; #[derive(Debug, Clone)] pub struct Album { @@ -94,20 +84,16 @@ impl Metadata for Album { type Message = protocol::metadata::Album; async fn request(session: &Session, album_id: SpotifyId) -> RequestResult { - session - .spclient() - .get_album_metadata(album_id) - .await - .map_err(RequestError::Http) + session.spclient().get_album_metadata(album_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Self::try_from(msg) } } impl TryFrom<&::Message> for Album { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(album: &::Message) -> Result { Ok(Self { id: album.try_into()?, @@ -138,7 +124,7 @@ impl TryFrom<&::Message> for Album { try_from_repeated_message!(::Message, Albums); impl TryFrom<&DiscMessage> for Disc { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(disc: &DiscMessage) -> Result { Ok(Self { number: disc.get_number(), diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index 517977bf..ac07d90e 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -1,23 +1,17 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; - -use crate::{ - error::{MetadataError, RequestError}, - request::RequestResult, - track::Tracks, - util::try_from_repeated_message, - Metadata, +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, }; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; +use crate::{request::RequestResult, track::Tracks, util::try_from_repeated_message, Metadata}; + +use librespot_core::{Error, Session, SpotifyId}; + use librespot_protocol as protocol; - use protocol::metadata::ArtistWithRole as ArtistWithRoleMessage; -use protocol::metadata::TopTracks as TopTracksMessage; - pub use protocol::metadata::ArtistWithRole_ArtistRole as ArtistRole; +use protocol::metadata::TopTracks as TopTracksMessage; #[derive(Debug, Clone)] pub struct Artist { @@ -88,20 +82,16 @@ impl Metadata for Artist { type Message = protocol::metadata::Artist; async fn request(session: &Session, artist_id: SpotifyId) -> RequestResult { - session - .spclient() - .get_artist_metadata(artist_id) - .await - .map_err(RequestError::Http) + session.spclient().get_artist_metadata(artist_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Self::try_from(msg) } } impl TryFrom<&::Message> for Artist { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(artist: &::Message) -> Result { Ok(Self { id: artist.try_into()?, @@ -114,7 +104,7 @@ impl TryFrom<&::Message> for Artist { try_from_repeated_message!(::Message, Artists); impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result { Ok(Self { id: artist_with_role.try_into()?, @@ -127,7 +117,7 @@ impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole { try_from_repeated_message!(ArtistWithRoleMessage, ArtistsWithRole); impl TryFrom<&TopTracksMessage> for TopTracks { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(top_tracks: &TopTracksMessage) -> Result { Ok(Self { country: top_tracks.get_country().to_owned(), diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs index fd202a40..d3ce69b7 100644 --- a/metadata/src/audio/file.rs +++ b/metadata/src/audio/file.rs @@ -1,12 +1,9 @@ -use std::collections::HashMap; -use std::fmt::Debug; -use std::ops::Deref; +use std::{collections::HashMap, fmt::Debug, ops::Deref}; + +use librespot_core::FileId; -use librespot_core::file_id::FileId; use librespot_protocol as protocol; - use protocol::metadata::AudioFile as AudioFileMessage; - pub use protocol::metadata::AudioFile_Format as AudioFileFormat; #[derive(Debug, Clone)] diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index 50aa2bf9..2b1f4eba 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -12,10 +12,9 @@ use crate::{ use super::file::AudioFiles; -use librespot_core::session::{Session, UserData}; -use librespot_core::spotify_id::{SpotifyId, SpotifyItemType}; +use librespot_core::{session::UserData, spotify_id::SpotifyItemType, Error, Session, SpotifyId}; -pub type AudioItemResult = Result; +pub type AudioItemResult = Result; // A wrapper with fields the player needs #[derive(Debug, Clone)] @@ -34,7 +33,7 @@ impl AudioItem { match id.item_type { SpotifyItemType::Track => Track::get_audio_item(session, id).await, SpotifyItemType::Episode => Episode::get_audio_item(session, id).await, - _ => Err(MetadataError::NonPlayable), + _ => Err(Error::unavailable(MetadataError::NonPlayable)), } } } diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index 27a85eed..d4681c28 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -1,13 +1,12 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use thiserror::Error; use crate::util::from_repeated_message; use librespot_core::date::Date; -use librespot_protocol as protocol; +use librespot_protocol as protocol; use protocol::metadata::Availability as AvailabilityMessage; pub type AudioItemAvailability = Result<(), UnavailabilityReason>; diff --git a/metadata/src/content_rating.rs b/metadata/src/content_rating.rs index a6f061d0..343f0e26 100644 --- a/metadata/src/content_rating.rs +++ b/metadata/src/content_rating.rs @@ -1,10 +1,8 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::util::from_repeated_message; use librespot_protocol as protocol; - use protocol::metadata::ContentRating as ContentRatingMessage; #[derive(Debug, Clone)] diff --git a/metadata/src/copyright.rs b/metadata/src/copyright.rs index 7842b7dd..b7f0e838 100644 --- a/metadata/src/copyright.rs +++ b/metadata/src/copyright.rs @@ -1,12 +1,9 @@ -use std::fmt::Debug; -use std::ops::Deref; - -use librespot_protocol as protocol; +use std::{fmt::Debug, ops::Deref}; use crate::util::from_repeated_message; +use librespot_protocol as protocol; use protocol::metadata::Copyright as CopyrightMessage; - pub use protocol::metadata::Copyright_Type as CopyrightType; #[derive(Debug, Clone)] diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 05d68aaf..30aae5a8 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -1,6 +1,8 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; use crate::{ audio::{ @@ -9,7 +11,6 @@ use crate::{ }, availability::Availabilities, content_rating::ContentRatings, - error::{MetadataError, RequestError}, image::Images, request::RequestResult, restriction::Restrictions, @@ -18,11 +19,9 @@ use crate::{ Metadata, }; -use librespot_core::date::Date; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; -use librespot_protocol as protocol; +use librespot_core::{date::Date, Error, Session, SpotifyId}; +use librespot_protocol as protocol; pub use protocol::metadata::Episode_EpisodeType as EpisodeType; #[derive(Debug, Clone)] @@ -90,20 +89,16 @@ impl Metadata for Episode { type Message = protocol::metadata::Episode; async fn request(session: &Session, episode_id: SpotifyId) -> RequestResult { - session - .spclient() - .get_episode_metadata(episode_id) - .await - .map_err(RequestError::Http) + session.spclient().get_episode_metadata(episode_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Self::try_from(msg) } } impl TryFrom<&::Message> for Episode { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(episode: &::Message) -> Result { Ok(Self { id: episode.try_into()?, diff --git a/metadata/src/error.rs b/metadata/src/error.rs index d1f6cc0b..31c600b0 100644 --- a/metadata/src/error.rs +++ b/metadata/src/error.rs @@ -1,35 +1,10 @@ use std::fmt::Debug; 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)] pub enum MetadataError { - #[error("{0}")] - InvalidSpotifyId(#[from] SpotifyIdError), - #[error("item has invalid date")] - InvalidTimestamp(#[from] DateError), - #[error("audio item is non-playable")] + #[error("empty response")] + Empty, + #[error("audio item is non-playable when it should be")] 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, } diff --git a/metadata/src/external_id.rs b/metadata/src/external_id.rs index 5da45634..b310200a 100644 --- a/metadata/src/external_id.rs +++ b/metadata/src/external_id.rs @@ -1,10 +1,8 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::util::from_repeated_message; use librespot_protocol as protocol; - use protocol::metadata::ExternalId as ExternalIdMessage; #[derive(Debug, Clone)] diff --git a/metadata/src/image.rs b/metadata/src/image.rs index 345722c9..495158d6 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -1,22 +1,19 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; - -use crate::{ - error::MetadataError, - util::{from_repeated_message, try_from_repeated_message}, +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, }; -use librespot_core::file_id::FileId; -use librespot_core::spotify_id::SpotifyId; -use librespot_protocol as protocol; +use crate::util::{from_repeated_message, try_from_repeated_message}; +use librespot_core::{FileId, SpotifyId}; + +use librespot_protocol as protocol; use protocol::metadata::Image as ImageMessage; +pub use protocol::metadata::Image_Size as ImageSize; use protocol::playlist4_external::PictureSize as PictureSizeMessage; use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage; -pub use protocol::metadata::Image_Size as ImageSize; - #[derive(Debug, Clone)] pub struct Image { pub id: FileId, @@ -92,7 +89,7 @@ impl From<&PictureSizeMessage> for PictureSize { from_repeated_message!(PictureSizeMessage, PictureSizes); impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(picture: &TranscodedPictureMessage) -> Result { Ok(Self { target_name: picture.get_target_name().to_owned(), diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index af9c80ec..577af387 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -6,8 +6,7 @@ extern crate async_trait; use protobuf::Message; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; +use librespot_core::{Error, Session, SpotifyId}; pub mod album; pub mod artist; @@ -46,12 +45,12 @@ pub trait Metadata: Send + Sized + 'static { async fn request(session: &Session, id: SpotifyId) -> RequestResult; // Request a metadata struct - async fn get(session: &Session, id: SpotifyId) -> Result { + async fn get(session: &Session, id: SpotifyId) -> Result { let response = Self::request(session, id).await?; let msg = Self::Message::parse_from_bytes(&response)?; trace!("Received metadata: {:#?}", msg); Self::parse(&msg, id) } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result; + fn parse(msg: &Self::Message, _: SpotifyId) -> Result; } diff --git a/metadata/src/playlist/annotation.rs b/metadata/src/playlist/annotation.rs index 0116d997..587f9b39 100644 --- a/metadata/src/playlist/annotation.rs +++ b/metadata/src/playlist/annotation.rs @@ -4,16 +4,14 @@ use std::fmt::Debug; use protobuf::Message; use crate::{ - error::MetadataError, image::TranscodedPictures, request::{MercuryRequest, RequestResult}, Metadata, }; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; -use librespot_protocol as protocol; +use librespot_core::{Error, Session, SpotifyId}; +use librespot_protocol as protocol; pub use protocol::playlist_annotate3::AbuseReportState; #[derive(Debug, Clone)] @@ -34,7 +32,7 @@ impl Metadata for PlaylistAnnotation { Self::request_for_user(session, ¤t_user, playlist_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Ok(Self { description: msg.get_description().to_owned(), picture: msg.get_picture().to_owned(), // TODO: is this a URL or Spotify URI? @@ -64,7 +62,7 @@ impl PlaylistAnnotation { session: &Session, username: &str, playlist_id: SpotifyId, - ) -> Result { + ) -> Result { let response = Self::request_for_user(session, username, playlist_id).await?; let msg = ::Message::parse_from_bytes(&response)?; Self::parse(&msg, playlist_id) @@ -74,7 +72,7 @@ impl PlaylistAnnotation { impl MercuryRequest for PlaylistAnnotation {} impl TryFrom<&::Message> for PlaylistAnnotation { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from( annotation: &::Message, ) -> Result { diff --git a/metadata/src/playlist/attribute.rs b/metadata/src/playlist/attribute.rs index ac2eef65..eb4fb577 100644 --- a/metadata/src/playlist/attribute.rs +++ b/metadata/src/playlist/attribute.rs @@ -1,25 +1,25 @@ -use std::collections::HashMap; -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, + 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 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::ItemAttributesPartialState as PlaylistPartialItemAttributesMessage; +pub use protocol::playlist4_external::ListAttributeKind as PlaylistAttributeKind; use protocol::playlist4_external::ListAttributes as PlaylistAttributesMessage; use protocol::playlist4_external::ListAttributesPartialState as PlaylistPartialAttributesMessage; use protocol::playlist4_external::UpdateItemAttributes as PlaylistUpdateItemAttributesMessage; 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)] pub struct PlaylistAttributes { pub name: String, @@ -108,7 +108,7 @@ pub struct PlaylistUpdateItemAttributes { } impl TryFrom<&PlaylistAttributesMessage> for PlaylistAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(attributes: &PlaylistAttributesMessage) -> Result { Ok(Self { name: attributes.get_name().to_owned(), @@ -142,7 +142,7 @@ impl From<&[PlaylistFormatAttributeMessage]> for PlaylistFormatAttribute { } impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result { Ok(Self { added_by: attributes.get_added_by().to_owned(), @@ -155,7 +155,7 @@ impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes { } } impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result { Ok(Self { values: attributes.get_values().try_into()?, @@ -165,7 +165,7 @@ impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes { } impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Result { Ok(Self { values: attributes.get_values().try_into()?, @@ -175,7 +175,7 @@ impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttri } impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result { Ok(Self { new_attributes: update.get_new_attributes().try_into()?, @@ -185,7 +185,7 @@ impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes { } impl TryFrom<&PlaylistUpdateItemAttributesMessage> for PlaylistUpdateItemAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(update: &PlaylistUpdateItemAttributesMessage) -> Result { Ok(Self { index: update.get_index(), diff --git a/metadata/src/playlist/diff.rs b/metadata/src/playlist/diff.rs index 080d72a1..4e40d2a5 100644 --- a/metadata/src/playlist/diff.rs +++ b/metadata/src/playlist/diff.rs @@ -1,13 +1,13 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; - -use crate::error::MetadataError; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, +}; use super::operation::PlaylistOperations; -use librespot_core::spotify_id::SpotifyId; -use librespot_protocol as protocol; +use librespot_core::SpotifyId; +use librespot_protocol as protocol; use protocol::playlist4_external::Diff as DiffMessage; #[derive(Debug, Clone)] @@ -18,7 +18,7 @@ pub struct PlaylistDiff { } impl TryFrom<&DiffMessage> for PlaylistDiff { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(diff: &DiffMessage) -> Result { Ok(Self { from_revision: diff.get_from_revision().try_into()?, diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index 5b97c382..dbd5fda2 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -1,17 +1,19 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + convert::{TryFrom, TryInto}, + 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 super::permission::Capabilities; - use protocol::playlist4_external::Item as PlaylistItemMessage; use protocol::playlist4_external::ListItems as PlaylistItemsMessage; use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage; @@ -62,7 +64,7 @@ impl Deref for PlaylistMetaItems { } impl TryFrom<&PlaylistItemMessage> for PlaylistItem { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(item: &PlaylistItemMessage) -> Result { Ok(Self { id: item.try_into()?, @@ -74,7 +76,7 @@ impl TryFrom<&PlaylistItemMessage> for PlaylistItem { try_from_repeated_message!(PlaylistItemMessage, PlaylistItems); impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(list_items: &PlaylistItemsMessage) -> Result { Ok(Self { position: list_items.get_pos(), @@ -86,7 +88,7 @@ impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList { } impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(item: &PlaylistMetaItemMessage) -> Result { Ok(Self { revision: item.try_into()?, diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 5df839b1..612ef857 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -1,11 +1,12 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; use protobuf::Message; use crate::{ - error::MetadataError, request::{MercuryRequest, RequestResult}, util::{from_repeated_enum, try_from_repeated_message}, Metadata, @@ -16,11 +17,13 @@ use super::{ permission::Capabilities, }; -use librespot_core::date::Date; -use librespot_core::session::Session; -use librespot_core::spotify_id::{NamedSpotifyId, SpotifyId}; -use librespot_protocol as protocol; +use librespot_core::{ + date::Date, + spotify_id::{NamedSpotifyId, SpotifyId}, + Error, Session, +}; +use librespot_protocol as protocol; use protocol::playlist4_external::GeoblockBlockingType as Geoblock; #[derive(Debug, Clone)] @@ -111,7 +114,7 @@ impl Playlist { session: &Session, username: &str, playlist_id: SpotifyId, - ) -> Result { + ) -> Result { let response = Self::request_for_user(session, username, playlist_id).await?; let msg = ::Message::parse_from_bytes(&response)?; Self::parse(&msg, playlist_id) @@ -153,7 +156,7 @@ impl Metadata for Playlist { ::request(session, &uri).await } - fn parse(msg: &Self::Message, id: SpotifyId) -> Result { + fn parse(msg: &Self::Message, id: SpotifyId) -> Result { // the playlist proto doesn't contain the id so we decorate it let playlist = SelectedListContent::try_from(msg)?; let id = NamedSpotifyId::from_spotify_id(id, playlist.owner_username); @@ -188,10 +191,7 @@ impl RootPlaylist { } #[allow(dead_code)] - pub async fn get_root_for_user( - session: &Session, - username: &str, - ) -> Result { + pub async fn get_root_for_user(session: &Session, username: &str) -> Result { let response = Self::request_for_user(session, username).await?; let msg = protocol::playlist4_external::SelectedListContent::parse_from_bytes(&response)?; Ok(Self(SelectedListContent::try_from(&msg)?)) @@ -199,7 +199,7 @@ impl RootPlaylist { } impl TryFrom<&::Message> for SelectedListContent { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(playlist: &::Message) -> Result { Ok(Self { revision: playlist.get_revision().try_into()?, diff --git a/metadata/src/playlist/operation.rs b/metadata/src/playlist/operation.rs index c6ffa785..fe33d0dc 100644 --- a/metadata/src/playlist/operation.rs +++ b/metadata/src/playlist/operation.rs @@ -1,9 +1,10 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; use crate::{ - error::MetadataError, playlist::{ attribute::{PlaylistUpdateAttributes, PlaylistUpdateItemAttributes}, item::PlaylistItems, @@ -12,13 +13,11 @@ use crate::{ }; use librespot_protocol as protocol; - use protocol::playlist4_external::Add as PlaylistAddMessage; use protocol::playlist4_external::Mov as PlaylistMoveMessage; use protocol::playlist4_external::Op as PlaylistOperationMessage; -use protocol::playlist4_external::Rem as PlaylistRemoveMessage; - pub use protocol::playlist4_external::Op_Kind as PlaylistOperationKind; +use protocol::playlist4_external::Rem as PlaylistRemoveMessage; #[derive(Debug, Clone)] pub struct PlaylistOperation { @@ -64,7 +63,7 @@ pub struct PlaylistOperationRemove { } impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(operation: &PlaylistOperationMessage) -> Result { Ok(Self { kind: operation.get_kind(), @@ -80,7 +79,7 @@ impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation { try_from_repeated_message!(PlaylistOperationMessage, PlaylistOperations); impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(add: &PlaylistAddMessage) -> Result { Ok(Self { from_index: add.get_from_index(), @@ -102,7 +101,7 @@ impl From<&PlaylistMoveMessage> for PlaylistOperationMove { } impl TryFrom<&PlaylistRemoveMessage> for PlaylistOperationRemove { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(remove: &PlaylistRemoveMessage) -> Result { Ok(Self { from_index: remove.get_from_index(), diff --git a/metadata/src/playlist/permission.rs b/metadata/src/playlist/permission.rs index 163859a1..2923a636 100644 --- a/metadata/src/playlist/permission.rs +++ b/metadata/src/playlist/permission.rs @@ -1,10 +1,8 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::util::from_repeated_enum; use librespot_protocol as protocol; - use protocol::playlist_permission::Capabilities as CapabilitiesMessage; use protocol::playlist_permission::PermissionLevel; diff --git a/metadata/src/request.rs b/metadata/src/request.rs index 4e47fc38..2ebd4037 100644 --- a/metadata/src/request.rs +++ b/metadata/src/request.rs @@ -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; +pub type RequestResult = Result; #[async_trait] pub trait MercuryRequest { 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() { Some(data) => { let data = data.to_vec().into(); trace!("Received metadata: {:?}", data); Ok(data) } - None => Err(RequestError::Empty), + None => Err(Error::unavailable(MetadataError::Empty)), } } } diff --git a/metadata/src/restriction.rs b/metadata/src/restriction.rs index 588e45e2..279da342 100644 --- a/metadata/src/restriction.rs +++ b/metadata/src/restriction.rs @@ -1,12 +1,10 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::util::{from_repeated_enum, from_repeated_message}; -use librespot_protocol as protocol; - use protocol::metadata::Restriction as RestrictionMessage; +use librespot_protocol as protocol; pub use protocol::metadata::Restriction_Catalogue as RestrictionCatalogue; pub use protocol::metadata::Restriction_Type as RestrictionType; diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs index 9040d71e..af6b58ac 100644 --- a/metadata/src/sale_period.rs +++ b/metadata/src/sale_period.rs @@ -1,11 +1,10 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::{restriction::Restrictions, util::from_repeated_message}; use librespot_core::date::Date; -use librespot_protocol as protocol; +use librespot_protocol as protocol; use protocol::metadata::SalePeriod as SalePeriodMessage; #[derive(Debug, Clone)] diff --git a/metadata/src/show.rs b/metadata/src/show.rs index f69ee021..9f84ba21 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -1,15 +1,16 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; - -use crate::{ - availability::Availabilities, copyright::Copyrights, episode::Episodes, error::RequestError, - image::Images, restriction::Restrictions, Metadata, MetadataError, RequestResult, +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, }; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; -use librespot_protocol as protocol; +use crate::{ + availability::Availabilities, copyright::Copyrights, episode::Episodes, image::Images, + 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_MediaType as ShowMediaType; @@ -39,20 +40,16 @@ impl Metadata for Show { type Message = protocol::metadata::Show; async fn request(session: &Session, show_id: SpotifyId) -> RequestResult { - session - .spclient() - .get_show_metadata(show_id) - .await - .map_err(RequestError::Http) + session.spclient().get_show_metadata(show_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Self::try_from(msg) } } impl TryFrom<&::Message> for Show { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(show: &::Message) -> Result { Ok(Self { id: show.try_into()?, diff --git a/metadata/src/track.rs b/metadata/src/track.rs index fc9c131e..06efd310 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -1,6 +1,8 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; use chrono::Local; use uuid::Uuid; @@ -13,17 +15,14 @@ use crate::{ }, availability::{Availabilities, UnavailabilityReason}, content_rating::ContentRatings, - error::RequestError, external_id::ExternalIds, restriction::Restrictions, sale_period::SalePeriods, util::try_from_repeated_message, - Metadata, MetadataError, RequestResult, + Metadata, RequestResult, }; -use librespot_core::date::Date; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; +use librespot_core::{date::Date, Error, Session, SpotifyId}; use librespot_protocol as protocol; #[derive(Debug, Clone)] @@ -105,20 +104,16 @@ impl Metadata for Track { type Message = protocol::metadata::Track; async fn request(session: &Session, track_id: SpotifyId) -> RequestResult { - session - .spclient() - .get_track_metadata(track_id) - .await - .map_err(RequestError::Http) + session.spclient().get_track_metadata(track_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Self::try_from(msg) } } impl TryFrom<&::Message> for Track { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(track: &::Message) -> Result { Ok(Self { id: track.try_into()?, diff --git a/metadata/src/util.rs b/metadata/src/util.rs index d0065221..59142847 100644 --- a/metadata/src/util.rs +++ b/metadata/src/util.rs @@ -27,7 +27,7 @@ pub(crate) use from_repeated_enum; macro_rules! try_from_repeated_message { ($src:ty, $dst:ty) => { impl TryFrom<&[$src]> for $dst { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(src: &[$src]) -> Result { let result: Result, _> = src.iter().map(TryFrom::try_from).collect(); Ok(Self(result?)) diff --git a/metadata/src/video.rs b/metadata/src/video.rs index 83f653bb..5e883339 100644 --- a/metadata/src/video.rs +++ b/metadata/src/video.rs @@ -1,11 +1,10 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::util::from_repeated_message; -use librespot_core::file_id::FileId; -use librespot_protocol as protocol; +use librespot_core::FileId; +use librespot_protocol as protocol; use protocol::metadata::VideoFile as VideoFileMessage; #[derive(Debug, Clone)] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 8946912b..1cd589a5 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -23,9 +23,9 @@ futures-util = { version = "0.3", default_features = false, features = ["alloc"] log = "0.4" byteorder = "1.4" shell-words = "1.0.0" +thiserror = "1.0" tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] } zerocopy = { version = "0.3" } -thiserror = { version = "1" } # Backends alsa = { version = "0.5", optional = true } diff --git a/playback/src/player.rs b/playback/src/player.rs index f0c4acda..c0748987 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,45 +1,40 @@ -use std::cmp::max; -use std::future::Future; -use std::io::{self, Read, Seek, SeekFrom}; -use std::pin::Pin; -use std::process::exit; -use std::task::{Context, Poll}; -use std::time::{Duration, Instant}; -use std::{mem, thread}; +use std::{ + cmp::max, + future::Future, + io::{self, Read, Seek, SeekFrom}, + mem, + pin::Pin, + process::exit, + task::{Context, Poll}, + thread, + time::{Duration, Instant}, +}; use byteorder::{LittleEndian, ReadBytesExt}; -use futures_util::stream::futures_unordered::FuturesUnordered; -use futures_util::{future, StreamExt, TryFutureExt}; -use thiserror::Error; +use futures_util::{future, stream::futures_unordered::FuturesUnordered, StreamExt, TryFutureExt}; use tokio::sync::{mpsc, oneshot}; -use crate::audio::{AudioDecrypt, AudioFile, AudioFileError, StreamLoaderController}; -use crate::audio::{ - READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, - READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, +use crate::{ + audio::{ + AudioDecrypt, AudioFile, StreamLoaderController, READ_AHEAD_BEFORE_PLAYBACK, + READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, + 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}; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; pub const DB_VOLTAGE_RATIO: f64 = 20.0; -pub type PlayerResult = Result<(), PlayerError>; - -#[derive(Debug, Error)] -pub enum PlayerError { - #[error("audio file error: {0}")] - AudioFile(#[from] AudioFileError), -} +pub type PlayerResult = Result<(), Error>; pub struct Player { commands: Option>, @@ -755,7 +750,7 @@ impl PlayerTrackLoader { let audio = match self.find_available_alternative(audio).await { Some(audio) => audio, None => { - warn!("<{}> is not available", spotify_id.to_uri()); + error!("<{}> is not available", spotify_id.to_uri()); return None; } }; @@ -801,7 +796,7 @@ impl PlayerTrackLoader { let (format, file_id) = match entry { Some(t) => t, None => { - warn!("<{}> is not available in any supported format", audio.name); + error!("<{}> is not available in any supported format", audio.name); return None; } }; @@ -973,7 +968,7 @@ impl Future for PlayerInternal { } } Poll::Ready(Err(e)) => { - warn!( + error!( "Skipping to next track, unable to load track <{:?}>: {:?}", track_id, e ); @@ -1077,7 +1072,7 @@ impl Future for PlayerInternal { } } 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 { track_id, play_request_id, @@ -1093,7 +1088,7 @@ impl Future for PlayerInternal { self.handle_packet(packet, normalisation_factor); } 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 { track_id, play_request_id, @@ -1128,9 +1123,7 @@ impl Future for PlayerInternal { if (!*suggested_to_preload_next_track) && ((duration_ms as i64 - Self::position_pcm_to_ms(stream_position_pcm) as i64) < PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64) - && stream_loader_controller - .range_to_end_available() - .unwrap_or(false) + && stream_loader_controller.range_to_end_available() { *suggested_to_preload_next_track = true; self.send_event(PlayerEvent::TimeToPreloadNextTrack { @@ -1266,7 +1259,7 @@ impl PlayerInternal { }); self.ensure_sink_running(); } else { - warn!("Player::play called from invalid state"); + error!("Player::play called from invalid state"); } } @@ -1290,7 +1283,7 @@ impl PlayerInternal { duration_ms, }); } 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), } } 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. @@ -1953,7 +1946,7 @@ impl PlayerInternal { 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 { bytes_per_second, ref mut stream_loader_controller, @@ -1978,7 +1971,7 @@ impl PlayerInternal { ); stream_loader_controller .fetch_next_blocking(wait_for_data_length) - .map_err(|e| e.into()) + .map_err(Into::into) } else { Ok(()) } diff --git a/src/main.rs b/src/main.rs index 6bfb027b..0dc25408 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,14 @@ +use std::{ + env, + fs::create_dir_all, + ops::RangeInclusive, + path::{Path, PathBuf}, + pin::Pin, + process::exit, + str::FromStr, + time::{Duration, Instant}, +}; + use futures_util::{future, FutureExt, StreamExt}; use librespot_playback::player::PlayerEvent; use log::{error, info, trace, warn}; @@ -6,35 +17,31 @@ use thiserror::Error; use tokio::sync::mpsc::UnboundedReceiver; use url::Url; -use librespot::connect::spirc::Spirc; -use librespot::core::authentication::Credentials; -use librespot::core::cache::Cache; -use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig}; -use librespot::core::session::Session; -use librespot::core::version; -use librespot::playback::audio_backend::{self, SinkBuilder, BACKENDS}; -use librespot::playback::config::{ - AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, +use librespot::{ + connect::spirc::Spirc, + core::{ + authentication::Credentials, + cache::Cache, + config::{ConnectConfig, DeviceType}, + version, Session, SessionConfig, + }, + playback::{ + audio_backend::{self, SinkBuilder, BACKENDS}, + config::{ + 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")] 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; 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 { hex::encode(Sha1::digest(name.as_bytes())) } @@ -1530,7 +1537,9 @@ async fn main() { auto_connect_times.clear(); 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() { // 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_task = Some(Box::pin(spirc_task_)); player_event_channel = Some(event_channel); @@ -1663,7 +1677,9 @@ async fn main() { // Shutdown spirc if necessary 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 { tokio::select! {