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

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

2
Cargo.lock generated
View file

@ -1246,6 +1246,7 @@ dependencies = [
"rand",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
]
@ -1309,6 +1310,7 @@ dependencies = [
"form_urlencoded",
"futures",
"futures-core",
"futures-util",
"hex",
"hmac",
"hyper",

View file

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

View file

@ -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<AudioFileError> 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<ResponseFuture>,
initial_body: Option<Body>,
initial_response: Option<Response<Body>>,
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<bool, AudioFileError> {
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<bool, AudioFileError> {
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<AudioFile, AudioFileError> {
) -> Result<AudioFile, Error> {
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<StreamLoaderController, AudioFileError> {
pub fn get_stream_loader_controller(&self) -> Result<StreamLoaderController, Error> {
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<NamedTempFile>,
bytes_per_second: usize,
play_from_beginning: bool,
) -> Result<AudioFileStreaming, AudioFileError> {
) -> Result<AudioFileStreaming, Error> {
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

View file

@ -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<DownloadStrategy, AudioFileError> {
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,32 +172,24 @@ 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
let url = self.shared.cdn_url.try_get_url()?;
let streamer = self
.session
.spclient()
.stream_file(&url.0, range.start, range.length)
{
Ok(streamer) => {
.stream_file(url, range.start, range.length)?;
download_status.requested.add_range(range);
let streaming_request = StreamingRequest {
streamer,
initial_body: None,
initial_response: None,
offset: range.start,
length: range.length,
request_time: Instant::now(),
@ -209,16 +201,6 @@ impl AudioFileFetch {
streaming_request,
));
}
Err(e) => {
error!("Unable to open stream for track <{}>: {:?}", file_id, e);
}
}
}
None => {
error!("Unable to get CDN URL for track <{}>", file_id);
}
}
}
Ok(())
}
@ -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<ControlFlow, AudioFileError> {
fn handle_file_data(&mut self, data: ReceivedData) -> Result<ControlFlow, Error> {
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<ControlFlow, AudioFileError> {
) -> Result<ControlFlow, Error> {
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();
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)
.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)

View file

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

View file

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

View file

@ -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<TrackContext> = 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::<Vec<TrackRef>>();
Ok(track_vec)
.collect::<Result<Vec<TrackRef>, D::Error>>()
}

View file

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

View file

@ -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<SpircError> for Error {
fn from(err: SpircError) -> Self {
match err {
SpircError::NoData => Error::unavailable(err),
SpircError::Ident(_) => Error::aborted(err),
SpircError::InvalidUri(_) => Error::aborted(err),
}
}
}
#[derive(Debug)]
enum SpircPlayStatus {
Stopped,
LoadingPlay {
@ -60,18 +96,18 @@ struct SpircTask {
play_request_id: Option<u64>,
play_status: SpircPlayStatus,
subscription: BoxedStream<Frame>,
connection_id_update: BoxedStream<String>,
user_attributes_update: BoxedStream<UserAttributesUpdate>,
user_attributes_mutation: BoxedStream<UserAttributesMutation>,
remote_update: BoxedStream<Result<Frame, Error>>,
connection_id_update: BoxedStream<Result<String, Error>>,
user_attributes_update: BoxedStream<Result<UserAttributesUpdate, Error>>,
user_attributes_mutation: BoxedStream<Result<UserAttributesMutation, Error>>,
sender: MercurySender,
commands: Option<mpsc::UnboundedReceiver<SpircCommand>>,
player_events: Option<PlayerEventChannel>,
shutdown: bool,
session: Session,
context_fut: BoxedFuture<Result<serde_json::Value, MercuryError>>,
autoplay_fut: BoxedFuture<Result<String, MercuryError>>,
context_fut: BoxedFuture<Result<serde_json::Value, Error>>,
autoplay_fut: BoxedFuture<Result<String, Error>>,
context: Option<StationContext>,
}
@ -232,7 +268,7 @@ impl Spirc {
session: Session,
player: Player,
mixer: Box<dyn Mixer>,
) -> (Spirc, impl Future<Output = ()>) {
) -> Result<(Spirc, impl Future<Output = ()>), 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<Frame, Error> {
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<String, Error> {
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<UserAttributesUpdate, Error> {
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<UserAttributesMutation, Error> {
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)
}
_ => Ok(()),
}
_ => (),
};
trace!("==> kPlayStatusPlay");
}
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<Result<serde_json::Value, MercuryError>> {
fn resolve_station(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, Error>> {
let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri);
self.resolve_uri(&radio_uri)
}
fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture<Result<String, MercuryError>> {
fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture<Result<String, Error>> {
let query_uri = format!("hm://autoplay-enabled/query?uri={}", uri);
let 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<Result<serde_json::Value, MercuryError>> {
fn resolve_uri(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, Error>> {
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()?)
}
}

View file

@ -1,7 +1,9 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use hyper::{Body, Method, Request};
use serde::Deserialize;
use std::error::Error;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::Error;
pub type SocketAddress = (String, u16);
@ -67,7 +69,7 @@ impl ApResolver {
.collect()
}
pub async fn try_apresolve(&self) -> Result<ApResolveData, Box<dyn Error>> {
pub async fn try_apresolve(&self) -> Result<ApResolveData, Error> {
let req = Request::builder()
.method(Method::GET)
.uri("http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient")

View file

@ -1,54 +1,85 @@
use std::{collections::HashMap, io::Write};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use bytes::Bytes;
use std::collections::HashMap;
use std::io::Write;
use thiserror::Error;
use tokio::sync::oneshot;
use crate::file_id::FileId;
use crate::packet::PacketType;
use crate::spotify_id::SpotifyId;
use crate::util::SeqGenerator;
use crate::{packet::PacketType, util::SeqGenerator, Error, FileId, SpotifyId};
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct AudioKey(pub [u8; 16]);
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct AudioKeyError;
#[derive(Debug, Error)]
pub enum AudioKeyError {
#[error("audio key error")]
AesKey,
#[error("other end of channel disconnected")]
Channel,
#[error("unexpected packet type {0}")]
Packet(u8),
#[error("sequence {0} not pending")]
Sequence(u32),
}
impl From<AudioKeyError> for Error {
fn from(err: AudioKeyError) -> Self {
match err {
AudioKeyError::AesKey => Error::unavailable(err),
AudioKeyError::Channel => Error::aborted(err),
AudioKeyError::Sequence(_) => Error::aborted(err),
AudioKeyError::Packet(_) => Error::unimplemented(err),
}
}
}
component! {
AudioKeyManager : AudioKeyManagerInner {
sequence: SeqGenerator<u32> = SeqGenerator::new(0),
pending: HashMap<u32, oneshot::Sender<Result<AudioKey, AudioKeyError>>> = HashMap::new(),
pending: HashMap<u32, oneshot::Sender<Result<AudioKey, Error>>> = HashMap::new(),
}
}
impl AudioKeyManager {
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) {
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
let seq = BigEndian::read_u32(data.split_to(4).as_ref());
let sender = self.lock(|inner| inner.pending.remove(&seq));
let sender = self
.lock(|inner| inner.pending.remove(&seq))
.ok_or(AudioKeyError::Sequence(seq))?;
if let Some(sender) = sender {
match cmd {
PacketType::AesKey => {
let mut key = [0u8; 16];
key.copy_from_slice(data.as_ref());
let _ = sender.send(Ok(AudioKey(key)));
sender
.send(Ok(AudioKey(key)))
.map_err(|_| AudioKeyError::Channel)?
}
PacketType::AesKeyError => {
warn!(
error!(
"error audio key {:x} {:x}",
data.as_ref()[0],
data.as_ref()[1]
);
let _ = sender.send(Err(AudioKeyError));
}
_ => (),
sender
.send(Err(AudioKeyError::AesKey.into()))
.map_err(|_| AudioKeyError::Channel)?
}
_ => {
trace!(
"Did not expect {:?} AES key packet with data {:#?}",
cmd,
data
);
return Err(AudioKeyError::Packet(cmd as u8).into());
}
}
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, AudioKeyError> {
Ok(())
}
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, Error> {
let (tx, rx) = oneshot::channel();
let seq = self.lock(move |inner| {
@ -57,16 +88,16 @@ impl AudioKeyManager {
seq
});
self.send_key_request(seq, track, file);
rx.await.map_err(|_| AudioKeyError)?
self.send_key_request(seq, track, file)?;
rx.await?
}
fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) {
fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) -> Result<(), Error> {
let mut data: Vec<u8> = Vec::new();
data.write_all(&file.0).unwrap();
data.write_all(&track.to_raw()).unwrap();
data.write_u32::<BigEndian>(seq).unwrap();
data.write_u16::<BigEndian>(0x0000).unwrap();
data.write_all(&file.0)?;
data.write_all(&track.to_raw())?;
data.write_u32::<BigEndian>(seq)?;
data.write_u16::<BigEndian>(0x0000)?;
self.session().send_packet(PacketType::RequestKey, data)
}

View file

@ -7,8 +7,21 @@ use pbkdf2::pbkdf2;
use protobuf::ProtobufEnum;
use serde::{Deserialize, Serialize};
use sha1::{Digest, Sha1};
use thiserror::Error;
use crate::protocol::authentication::AuthenticationType;
use crate::{protocol::authentication::AuthenticationType, Error};
#[derive(Debug, Error)]
pub enum AuthenticationError {
#[error("unknown authentication type {0}")]
AuthType(u32),
}
impl From<AuthenticationError> for Error {
fn from(err: AuthenticationError) -> Self {
Error::invalid_argument(err)
}
}
/// The credentials are used to log into the Spotify API.
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -46,7 +59,7 @@ impl Credentials {
username: impl Into<String>,
encrypted_blob: impl AsRef<[u8]>,
device_id: impl AsRef<[u8]>,
) -> Credentials {
) -> Result<Credentials, Error> {
fn read_u8<R: Read>(stream: &mut R) -> io::Result<u8> {
let mut data = [0u8];
stream.read_exact(&mut data)?;
@ -91,7 +104,7 @@ impl Credentials {
use aes::cipher::generic_array::GenericArray;
use aes::cipher::{BlockCipher, NewBlockCipher};
let mut data = base64::decode(encrypted_blob).unwrap();
let mut data = base64::decode(encrypted_blob)?;
let cipher = Aes192::new(GenericArray::from_slice(&key));
let block_size = <Aes192 as BlockCipher>::BlockSize::to_usize();
@ -109,19 +122,20 @@ impl Credentials {
};
let mut cursor = io::Cursor::new(blob.as_slice());
read_u8(&mut cursor).unwrap();
read_bytes(&mut cursor).unwrap();
read_u8(&mut cursor).unwrap();
let auth_type = read_int(&mut cursor).unwrap();
let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap();
read_u8(&mut cursor).unwrap();
let auth_data = read_bytes(&mut cursor).unwrap();
read_u8(&mut cursor)?;
read_bytes(&mut cursor)?;
read_u8(&mut cursor)?;
let auth_type = read_int(&mut cursor)?;
let auth_type = AuthenticationType::from_i32(auth_type as i32)
.ok_or(AuthenticationError::AuthType(auth_type))?;
read_u8(&mut cursor)?;
let auth_data = read_bytes(&mut cursor)?;
Credentials {
Ok(Credentials {
username,
auth_type,
auth_data,
}
})
}
}

View file

@ -1,15 +1,29 @@
use std::cmp::Reverse;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{self, Error, ErrorKind, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use std::{
cmp::Reverse,
collections::HashMap,
fs::{self, File},
io::{self, Read, Write},
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::SystemTime,
};
use priority_queue::PriorityQueue;
use thiserror::Error;
use crate::authentication::Credentials;
use crate::file_id::FileId;
use crate::{authentication::Credentials, error::ErrorKind, Error, FileId};
#[derive(Debug, Error)]
pub enum CacheError {
#[error("audio cache location is not configured")]
Path,
}
impl From<CacheError> for Error {
fn from(err: CacheError) -> Self {
Error::failed_precondition(err)
}
}
/// Some kind of data structure that holds some paths, the size of these files and a timestamp.
/// It keeps track of the file sizes and is able to pop the path with the oldest timestamp if
@ -57,16 +71,17 @@ impl SizeLimiter {
/// to delete the file in the file system.
fn pop(&mut self) -> Option<PathBuf> {
if self.exceeds_limit() {
let (next, _) = self
.queue
.pop()
.expect("in_use was > 0, so the queue should have contained an item.");
let size = self
.sizes
.remove(&next)
.expect("`queue` and `sizes` should have the same keys.");
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.");
if let Some(size) = self.sizes.remove(file) {
self.in_use -= size;
} else {
error!("`queue` and `sizes` should have the same keys.");
}
true
}
@ -172,33 +187,40 @@ impl FsSizeLimiter {
}
}
fn add(&self, file: &Path, size: u64) {
fn add(&self, file: &Path, size: u64) -> Result<(), Error> {
self.limiter
.lock()
.unwrap()
.add(file, size, SystemTime::now());
Ok(())
}
fn touch(&self, file: &Path) -> bool {
self.limiter.lock().unwrap().update(file, SystemTime::now())
fn touch(&self, file: &Path) -> Result<bool, Error> {
Ok(self.limiter.lock().unwrap().update(file, SystemTime::now()))
}
fn remove(&self, file: &Path) {
self.limiter.lock().unwrap().remove(file);
fn remove(&self, file: &Path) -> Result<bool, Error> {
Ok(self.limiter.lock().unwrap().remove(file))
}
fn prune_internal<F: FnMut() -> Option<PathBuf>>(mut pop: F) {
fn prune_internal<F: FnMut() -> Result<Option<PathBuf>, Error>>(
mut pop: F,
) -> Result<(), Error> {
let mut first = true;
let mut count = 0;
let mut last_error = None;
while let Some(file) = pop() {
while let Ok(result) = pop() {
if let Some(file) = result {
if first {
debug!("Cache dir exceeds limit, removing least recently used files.");
first = false;
}
if let Err(e) = fs::remove_file(&file) {
let res = fs::remove_file(&file);
if let Err(e) = res {
warn!("Could not remove file {:?} from cache dir: {}", file, e);
last_error = Some(e);
} else {
count += 1;
}
@ -209,19 +231,26 @@ impl FsSizeLimiter {
}
}
fn prune(&self) {
Self::prune_internal(|| self.limiter.lock().unwrap().pop())
if let Some(err) = last_error {
Err(err.into())
} else {
Ok(())
}
}
fn new(path: &Path, limit: u64) -> Self {
fn prune(&self) -> Result<(), Error> {
Self::prune_internal(|| Ok(self.limiter.lock().unwrap().pop()))
}
fn new(path: &Path, limit: u64) -> Result<Self, Error> {
let mut limiter = SizeLimiter::new(limit);
Self::init_dir(&mut limiter, path);
Self::prune_internal(|| limiter.pop());
Self::prune_internal(|| Ok(limiter.pop()))?;
Self {
Ok(Self {
limiter: Mutex::new(limiter),
}
})
}
}
@ -234,15 +263,13 @@ pub struct Cache {
size_limiter: Option<Arc<FsSizeLimiter>>,
}
pub struct RemoveFileError(());
impl Cache {
pub fn new<P: AsRef<Path>>(
credentials_path: Option<P>,
volume_path: Option<P>,
audio_path: Option<P>,
size_limit: Option<u64>,
) -> io::Result<Self> {
) -> Result<Self, Error> {
let mut size_limiter = None;
if let Some(location) = &credentials_path {
@ -263,8 +290,7 @@ impl Cache {
fs::create_dir_all(location)?;
if let Some(limit) = size_limit {
let limiter = FsSizeLimiter::new(location.as_ref(), limit);
let limiter = FsSizeLimiter::new(location.as_ref(), limit)?;
size_limiter = Some(Arc::new(limiter));
}
}
@ -285,11 +311,11 @@ impl Cache {
let location = self.credentials_location.as_ref()?;
// This closure is just convencience to enable the question mark operator
let read = || {
let read = || -> Result<Credentials, Error> {
let mut file = File::open(location)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
serde_json::from_str(&contents).map_err(|e| Error::new(ErrorKind::InvalidData, e))
Ok(serde_json::from_str(&contents)?)
};
match read() {
@ -297,7 +323,7 @@ impl Cache {
Err(e) => {
// If the file did not exist, the file was probably not written
// before. Otherwise, log the error.
if e.kind() != ErrorKind::NotFound {
if e.kind != ErrorKind::NotFound {
warn!("Error reading credentials from cache: {}", e);
}
None
@ -321,19 +347,17 @@ impl Cache {
pub fn volume(&self) -> Option<u16> {
let location = self.volume_location.as_ref()?;
let read = || {
let read = || -> Result<u16, Error> {
let mut file = File::open(location)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
contents
.parse()
.map_err(|e| Error::new(ErrorKind::InvalidData, e))
Ok(contents.parse()?)
};
match read() {
Ok(v) => Some(v),
Err(e) => {
if e.kind() != ErrorKind::NotFound {
if e.kind != ErrorKind::NotFound {
warn!("Error reading volume from cache: {}", e);
}
None
@ -364,12 +388,14 @@ impl Cache {
match File::open(&path) {
Ok(file) => {
if let Some(limiter) = self.size_limiter.as_deref() {
limiter.touch(&path);
if let Err(e) = limiter.touch(&path) {
error!("limiter could not touch {:?}: {}", path, e);
}
}
Some(file)
}
Err(e) => {
if e.kind() != ErrorKind::NotFound {
if e.kind() != io::ErrorKind::NotFound {
warn!("Error reading file from cache: {}", e)
}
None
@ -377,7 +403,7 @@ impl Cache {
}
}
pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) -> bool {
pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) -> Result<(), Error> {
if let Some(path) = self.file_path(file) {
if let Some(parent) = path.parent() {
if let Ok(size) = fs::create_dir_all(parent)
@ -385,29 +411,26 @@ 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 {
fs::remove_file(&path)?;
if let Some(limiter) = self.size_limiter.as_deref() {
limiter.remove(&path);
limiter.remove(&path)?;
}
Ok(())
}
}
}
#[cfg(test)]

View file

@ -1,34 +1,19 @@
use std::{
convert::{TryFrom, TryInto},
ops::{Deref, DerefMut},
};
use chrono::Local;
use protobuf::{Message, ProtobufError};
use protobuf::Message;
use thiserror::Error;
use url::Url;
use std::convert::{TryFrom, TryInto};
use std::ops::{Deref, DerefMut};
use super::date::Date;
use super::file_id::FileId;
use super::session::Session;
use super::spclient::SpClientError;
use super::{date::Date, Error, FileId, Session};
use librespot_protocol as protocol;
use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage;
use protocol::storage_resolve::StorageResolveResponse_Result;
#[derive(Error, Debug)]
pub enum CdnUrlError {
#[error("no URLs available")]
Empty,
#[error("all tokens expired")]
Expired,
#[error("error parsing response")]
Parsing,
#[error("could not parse protobuf: {0}")]
Protobuf(#[from] ProtobufError),
#[error("could not complete API request: {0}")]
SpClient(#[from] SpClientError),
}
#[derive(Debug, Clone)]
pub struct MaybeExpiringUrl(pub String, pub Option<Date>);
@ -48,10 +33,27 @@ impl DerefMut for MaybeExpiringUrls {
}
}
#[derive(Debug, Error)]
pub enum CdnUrlError {
#[error("all URLs expired")]
Expired,
#[error("resolved storage is not for CDN")]
Storage,
}
impl From<CdnUrlError> for Error {
fn from(err: CdnUrlError) -> Self {
match err {
CdnUrlError::Expired => Error::deadline_exceeded(err),
CdnUrlError::Storage => Error::unavailable(err),
}
}
}
#[derive(Debug, Clone)]
pub struct CdnUrl {
pub file_id: FileId,
pub urls: MaybeExpiringUrls,
urls: MaybeExpiringUrls,
}
impl CdnUrl {
@ -62,7 +64,7 @@ impl CdnUrl {
}
}
pub async fn resolve_audio(&self, session: &Session) -> Result<Self, CdnUrlError> {
pub async fn resolve_audio(&self, session: &Session) -> Result<Self, Error> {
let file_id = self.file_id;
let response = session.spclient().get_audio_urls(file_id).await?;
let msg = CdnUrlMessage::parse_from_bytes(&response)?;
@ -75,37 +77,26 @@ impl CdnUrl {
Ok(cdn_url)
}
pub fn get_url(&mut self) -> Result<&str, CdnUrlError> {
if self.urls.is_empty() {
return Err(CdnUrlError::Empty);
}
// prune expired URLs until the first one is current, or none are left
pub fn try_get_url(&self) -> Result<&str, Error> {
let now = Local::now();
while !self.urls.is_empty() {
let maybe_expiring = self.urls[0].1;
if let Some(expiry) = maybe_expiring {
if now < expiry.as_utc() {
break;
} else {
self.urls.remove(0);
}
}
}
let url = self.urls.iter().find(|url| match url.1 {
Some(expiry) => now < expiry.as_utc(),
None => true,
});
if let Some(cdn_url) = self.urls.first() {
Ok(&cdn_url.0)
if let Some(url) = url {
Ok(&url.0)
} else {
Err(CdnUrlError::Expired)
Err(CdnUrlError::Expired.into())
}
}
}
impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
type Error = CdnUrlError;
type Error = crate::Error;
fn try_from(msg: CdnUrlMessage) -> Result<Self, Self::Error> {
if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) {
return Err(CdnUrlError::Parsing);
return Err(CdnUrlError::Storage.into());
}
let is_expiring = !msg.get_fileid().is_empty();
@ -114,7 +105,7 @@ impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
.get_cdnurl()
.iter()
.map(|cdn_url| {
let url = Url::parse(cdn_url).map_err(|_| CdnUrlError::Parsing)?;
let url = Url::parse(cdn_url)?;
if is_expiring {
let expiry_str = if let Some(token) = url
@ -122,29 +113,47 @@ impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
.into_iter()
.find(|(key, _value)| key == "__token__")
{
let start = token.1.find("exp=").ok_or(CdnUrlError::Parsing)?;
let slice = &token.1[start + 4..];
let end = slice.find('~').ok_or(CdnUrlError::Parsing)?;
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 {
return Err(CdnUrlError::Parsing);
String::new()
}
} else {
String::new()
};
let mut expiry: i64 = expiry_str.parse().map_err(|_| CdnUrlError::Parsing)?;
let mut expiry: i64 = expiry_str.parse()?;
expiry -= 5 * 60; // seconds
Ok(MaybeExpiringUrl(
cdn_url.to_owned(),
Some(expiry.try_into().map_err(|_| CdnUrlError::Parsing)?),
Some(expiry.try_into()?),
))
} else {
Ok(MaybeExpiringUrl(cdn_url.to_owned(), None))
}
})
.collect::<Result<Vec<MaybeExpiringUrl>, CdnUrlError>>()?;
.collect::<Result<Vec<MaybeExpiringUrl>, Error>>()?;
Ok(Self(result))
}

View file

@ -1,18 +1,20 @@
use std::collections::HashMap;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Instant;
use std::{
collections::HashMap,
fmt,
pin::Pin,
task::{Context, Poll},
time::Instant,
};
use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
use futures_core::Stream;
use futures_util::lock::BiLock;
use futures_util::{ready, StreamExt};
use futures_util::{lock::BiLock, ready, StreamExt};
use num_traits::FromPrimitive;
use thiserror::Error;
use tokio::sync::mpsc;
use crate::packet::PacketType;
use crate::util::SeqGenerator;
use crate::{packet::PacketType, util::SeqGenerator, Error};
component! {
ChannelManager : ChannelManagerInner {
@ -27,9 +29,21 @@ component! {
const ONE_SECOND_IN_MS: usize = 1000;
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)]
pub struct ChannelError;
impl From<ChannelError> for Error {
fn from(err: ChannelError) -> Self {
Error::aborted(err)
}
}
impl fmt::Display for ChannelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "channel error")
}
}
pub struct Channel {
receiver: mpsc::UnboundedReceiver<(u8, Bytes)>,
state: ChannelState,
@ -70,7 +84,7 @@ impl ChannelManager {
(seq, channel)
}
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) {
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
use std::collections::hash_map::Entry;
let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref());
@ -94,9 +108,14 @@ impl ChannelManager {
inner.download_measurement_bytes += data.len();
if let Entry::Occupied(entry) = inner.channels.entry(id) {
let _ = entry.get().send((cmd as u8, data));
entry
.get()
.send((cmd as u8, data))
.map_err(|_| ChannelError)?;
}
});
Ok(())
})
}
pub fn get_download_rate_estimate(&self) -> usize {
@ -142,7 +161,11 @@ impl Stream for Channel {
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
match self.state.clone() {
ChannelState::Closed => panic!("Polling already terminated channel"),
ChannelState::Closed => {
error!("Polling already terminated channel");
return Poll::Ready(None);
}
ChannelState::Header(mut data) => {
if data.is_empty() {
data = ready!(self.recv_packet(cx))?;

View file

@ -14,7 +14,7 @@ macro_rules! component {
#[allow(dead_code)]
fn lock<F: FnOnce(&mut $inner) -> R, R>(&self, f: F) -> R {
let mut inner = (self.0).1.lock().expect("Mutex poisoned");
let mut inner = (self.0).1.lock().unwrap();
f(&mut inner)
}

View file

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

View file

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

View file

@ -1,20 +1,28 @@
use std::{env::consts::ARCH, io};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use hmac::{Hmac, Mac, NewMac};
use protobuf::{self, Message};
use rand::{thread_rng, RngCore};
use sha1::Sha1;
use std::env::consts::ARCH;
use std::io;
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio_util::codec::{Decoder, Framed};
use super::codec::ApCodec;
use crate::diffie_hellman::DhLocalKeys;
use crate::{diffie_hellman::DhLocalKeys, version};
use crate::protocol;
use crate::protocol::keyexchange::{
APResponseMessage, ClientHello, ClientResponsePlaintext, Platform, ProductFlags,
};
use crate::version;
#[derive(Debug, Error)]
pub enum HandshakeError {
#[error("invalid key length")]
InvalidLength,
}
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
mut connection: T,
@ -31,7 +39,7 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
.to_owned();
let shared_secret = local_keys.shared_secret(&remote_key);
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator);
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator)?;
let codec = ApCodec::new(&send_key, &recv_key);
client_response(&mut connection, challenge).await?;
@ -112,8 +120,8 @@ where
let mut buffer = vec![0, 4];
let size = 2 + 4 + packet.compute_size();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap();
packet.write_to_vec(&mut buffer).unwrap();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?;
packet.write_to_vec(&mut buffer)?;
connection.write_all(&buffer[..]).await?;
Ok(buffer)
@ -133,8 +141,8 @@ where
let mut buffer = vec![];
let size = 4 + packet.compute_size();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap();
packet.write_to_vec(&mut buffer).unwrap();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?;
packet.write_to_vec(&mut buffer)?;
connection.write_all(&buffer[..]).await?;
Ok(())
@ -148,7 +156,7 @@ where
let header = read_into_accumulator(connection, 4, acc).await?;
let size = BigEndian::read_u32(header) as usize;
let data = read_into_accumulator(connection, size - 4, acc).await?;
let message = M::parse_from_bytes(data).unwrap();
let message = M::parse_from_bytes(data)?;
Ok(message)
}
@ -164,24 +172,26 @@ async fn read_into_accumulator<'a, 'b, T: AsyncRead + Unpin>(
Ok(&mut acc[offset..])
}
fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> io::Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
type HmacSha1 = Hmac<Sha1>;
let mut data = Vec::with_capacity(0x64);
for i in 1..6 {
let mut mac =
HmacSha1::new_from_slice(shared_secret).expect("HMAC can take key of any size");
let mut mac = HmacSha1::new_from_slice(shared_secret).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength)
})?;
mac.update(packets);
mac.update(&[i]);
data.extend_from_slice(&mac.finalize().into_bytes());
}
let mut mac = HmacSha1::new_from_slice(&data[..0x14]).expect("HMAC can take key of any size");
let mut mac = HmacSha1::new_from_slice(&data[..0x14])
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength))?;
mac.update(packets);
(
Ok((
mac.finalize().into_bytes().to_vec(),
data[0x14..0x34].to_vec(),
data[0x34..0x54].to_vec(),
)
))
}

View file

@ -1,23 +1,21 @@
mod codec;
mod handshake;
pub use self::codec::ApCodec;
pub use self::handshake::handshake;
pub use self::{codec::ApCodec, handshake::handshake};
use std::io::{self, ErrorKind};
use std::io;
use futures_util::{SinkExt, StreamExt};
use num_traits::FromPrimitive;
use protobuf::{self, Message, ProtobufError};
use protobuf::{self, Message};
use thiserror::Error;
use tokio::net::TcpStream;
use tokio_util::codec::Framed;
use url::Url;
use crate::authentication::Credentials;
use crate::packet::PacketType;
use crate::{authentication::Credentials, packet::PacketType, version, Error};
use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
use crate::version;
pub type Transport = Framed<TcpStream, ApCodec>;
@ -42,13 +40,19 @@ fn login_error_message(code: &ErrorCode) -> &'static str {
pub enum AuthenticationError {
#[error("Login failed with reason: {}", login_error_message(.0))]
LoginFailed(ErrorCode),
#[error("Authentication failed: {0}")]
IoError(#[from] io::Error),
#[error("invalid packet {0}")]
Packet(u8),
#[error("transport returned no data")]
Transport,
}
impl From<ProtobufError> for AuthenticationError {
fn from(e: ProtobufError) -> Self {
io::Error::new(ErrorKind::InvalidData, e).into()
impl From<AuthenticationError> for Error {
fn from(err: AuthenticationError) -> Self {
match err {
AuthenticationError::LoginFailed(_) => Error::permission_denied(err),
AuthenticationError::Packet(_) => Error::unimplemented(err),
AuthenticationError::Transport => Error::unavailable(err),
}
}
}
@ -68,7 +72,7 @@ pub async fn authenticate(
transport: &mut Transport,
credentials: Credentials,
device_id: &str,
) -> Result<Credentials, AuthenticationError> {
) -> Result<Credentials, Error> {
use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
let cpu_family = match std::env::consts::ARCH {
@ -119,12 +123,15 @@ pub async fn authenticate(
packet.set_version_string(format!("librespot {}", version::SEMVER));
let cmd = PacketType::Login;
let data = packet.write_to_bytes().unwrap();
let data = packet.write_to_bytes()?;
transport.send((cmd as u8, data)).await?;
let (cmd, data) = transport.next().await.expect("EOF")?;
let (cmd, data) = transport
.next()
.await
.ok_or(AuthenticationError::Transport)??;
let packet_type = FromPrimitive::from_u8(cmd);
match packet_type {
let result = match packet_type {
Some(PacketType::APWelcome) => {
let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?;
@ -141,8 +148,13 @@ pub async fn authenticate(
Err(error_data.into())
}
_ => {
let msg = format!("Received invalid packet: {}", cmd);
Err(io::Error::new(ErrorKind::InvalidData, msg).into())
}
trace!(
"Did not expect {:?} AES key packet with data {:#?}",
cmd,
data
);
Err(AuthenticationError::Packet(cmd))
}
};
Ok(result?)
}

View file

@ -1,18 +1,23 @@
use std::convert::TryFrom;
use std::fmt::Debug;
use std::ops::Deref;
use std::{convert::TryFrom, fmt::Debug, ops::Deref};
use chrono::{DateTime, Utc};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use thiserror::Error;
use crate::Error;
use librespot_protocol as protocol;
use protocol::metadata::Date as DateMessage;
#[derive(Debug, Error)]
pub enum DateError {
#[error("item has invalid date")]
InvalidTimestamp,
#[error("item has invalid timestamp {0}")]
Timestamp(i64),
}
impl From<DateError> for Error {
fn from(err: DateError) -> Self {
Error::invalid_argument(err)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@ -30,11 +35,11 @@ impl Date {
self.0.timestamp()
}
pub fn from_timestamp(timestamp: i64) -> Result<Self, DateError> {
pub fn from_timestamp(timestamp: i64) -> Result<Self, Error> {
if let Some(date_time) = NaiveDateTime::from_timestamp_opt(timestamp, 0) {
Ok(Self::from_utc(date_time))
} else {
Err(DateError::InvalidTimestamp)
Err(DateError::Timestamp(timestamp).into())
}
}
@ -67,7 +72,7 @@ impl From<DateTime<Utc>> for Date {
}
impl TryFrom<i64> for Date {
type Error = DateError;
type Error = crate::Error;
fn try_from(timestamp: i64) -> Result<Self, Self::Error> {
Self::from_timestamp(timestamp)
}

View file

@ -1,7 +1,20 @@
use std::collections::HashMap;
#[derive(Debug)]
pub struct AlreadyHandledError(());
use thiserror::Error;
use crate::Error;
#[derive(Debug, Error)]
pub enum HandlerMapError {
#[error("request was already handled")]
AlreadyHandled,
}
impl From<HandlerMapError> for Error {
fn from(err: HandlerMapError) -> Self {
Error::aborted(err)
}
}
pub enum HandlerMap<T> {
Leaf(T),
@ -19,9 +32,9 @@ impl<T> HandlerMap<T> {
&mut self,
mut path: impl Iterator<Item = &'a str>,
handler: T,
) -> Result<(), AlreadyHandledError> {
) -> Result<(), Error> {
match self {
Self::Leaf(_) => Err(AlreadyHandledError(())),
Self::Leaf(_) => Err(HandlerMapError::AlreadyHandled.into()),
Self::Branch(children) => {
if let Some(component) = path.next() {
let node = children.entry(component.to_owned()).or_default();
@ -30,7 +43,7 @@ impl<T> HandlerMap<T> {
*self = Self::Leaf(handler);
Ok(())
} else {
Err(AlreadyHandledError(()))
Err(HandlerMapError::AlreadyHandled.into())
}
}
}

View file

@ -1,29 +1,40 @@
mod maps;
pub mod protocol;
use std::iter;
use std::pin::Pin;
use std::sync::atomic::AtomicBool;
use std::sync::{atomic, Arc, Mutex};
use std::task::Poll;
use std::time::Duration;
use std::{
iter,
pin::Pin,
sync::{
atomic::{self, AtomicBool},
Arc, Mutex,
},
task::Poll,
time::Duration,
};
use futures_core::{Future, Stream};
use futures_util::future::join_all;
use futures_util::{SinkExt, StreamExt};
use futures_util::{future::join_all, SinkExt, StreamExt};
use thiserror::Error;
use tokio::select;
use tokio::sync::mpsc::{self, UnboundedReceiver};
use tokio::sync::Semaphore;
use tokio::task::JoinHandle;
use tokio::{
select,
sync::{
mpsc::{self, UnboundedReceiver},
Semaphore,
},
task::JoinHandle,
};
use tokio_tungstenite::tungstenite;
use tungstenite::error::UrlError;
use url::Url;
use self::maps::*;
use self::protocol::*;
use crate::socket;
use crate::util::{keep_flushing, CancelOnDrop, TimeoutOnDrop};
use crate::{
socket,
util::{keep_flushing, CancelOnDrop, TimeoutOnDrop},
Error,
};
type WsMessage = tungstenite::Message;
type WsError = tungstenite::Error;
@ -164,24 +175,38 @@ fn split_uri(s: &str) -> Option<impl Iterator<Item = &'_ str>> {
pub enum AddHandlerError {
#[error("There is already a handler for the given uri")]
AlreadyHandled,
#[error("The specified uri is invalid")]
InvalidUri,
#[error("The specified uri {0} is invalid")]
InvalidUri(String),
}
impl From<AddHandlerError> for Error {
fn from(err: AddHandlerError) -> Self {
match err {
AddHandlerError::AlreadyHandled => Error::aborted(err),
AddHandlerError::InvalidUri(_) => Error::invalid_argument(err),
}
}
}
#[derive(Debug, Clone, Error)]
pub enum SubscriptionError {
#[error("The specified uri is invalid")]
InvalidUri,
InvalidUri(String),
}
impl From<SubscriptionError> for Error {
fn from(err: SubscriptionError) -> Self {
Error::invalid_argument(err)
}
}
fn add_handler(
map: &mut HandlerMap<Box<dyn RequestHandler>>,
uri: &str,
handler: impl RequestHandler,
) -> Result<(), AddHandlerError> {
let split = split_uri(uri).ok_or(AddHandlerError::InvalidUri)?;
) -> Result<(), Error> {
let split = split_uri(uri).ok_or_else(|| AddHandlerError::InvalidUri(uri.to_string()))?;
map.insert(split, Box::new(handler))
.map_err(|_| AddHandlerError::AlreadyHandled)
}
fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> {
@ -191,11 +216,11 @@ fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> {
fn subscribe(
map: &mut SubscriberMap<MessageHandler>,
uris: &[&str],
) -> Result<Subscription, SubscriptionError> {
) -> Result<Subscription, Error> {
let (tx, rx) = mpsc::unbounded_channel();
for &uri in uris {
let split = split_uri(uri).ok_or(SubscriptionError::InvalidUri)?;
let split = split_uri(uri).ok_or_else(|| SubscriptionError::InvalidUri(uri.to_string()))?;
map.insert(split, tx.clone());
}
@ -237,15 +262,11 @@ impl Builder {
Self::default()
}
pub fn add_handler(
&mut self,
uri: &str,
handler: impl RequestHandler,
) -> Result<(), AddHandlerError> {
pub fn add_handler(&mut self, uri: &str, handler: impl RequestHandler) -> Result<(), Error> {
add_handler(&mut self.request_handlers, uri, handler)
}
pub fn subscribe(&mut self, uris: &[&str]) -> Result<Subscription, SubscriptionError> {
pub fn subscribe(&mut self, uris: &[&str]) -> Result<Subscription, Error> {
subscribe(&mut self.message_handlers, uris)
}
@ -342,7 +363,7 @@ pub struct Dealer {
}
impl Dealer {
pub fn add_handler<H>(&self, uri: &str, handler: H) -> Result<(), AddHandlerError>
pub fn add_handler<H>(&self, uri: &str, handler: H) -> Result<(), Error>
where
H: RequestHandler,
{
@ -357,7 +378,7 @@ impl Dealer {
remove_handler(&mut self.shared.request_handlers.lock().unwrap(), uri)
}
pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, SubscriptionError> {
pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, Error> {
subscribe(&mut self.shared.message_handlers.lock().unwrap(), uris)
}
@ -367,7 +388,9 @@ impl Dealer {
self.shared.notify_drop.close();
if let Some(handle) = self.handle.take() {
CancelOnDrop(handle).await.unwrap();
if let Err(e) = CancelOnDrop(handle).await {
error!("error aborting dealer operations: {}", e);
}
}
}
}
@ -556,11 +579,15 @@ async fn run<F, Fut>(
select! {
() = shared.closed() => break,
r = t0 => {
r.unwrap(); // Whatever has gone wrong (probably panicked), we can't handle it, so let's panic too.
if let Err(e) = r {
error!("timeout on task 0: {}", e);
}
tasks.0.take();
},
r = t1 => {
r.unwrap();
if let Err(e) = r {
error!("timeout on task 1: {}", e);
}
tasks.1.take();
}
}
@ -576,7 +603,7 @@ async fn run<F, Fut>(
match connect(&url, proxy.as_ref(), &shared).await {
Ok((s, r)) => tasks = (init_task(s), init_task(r)),
Err(e) => {
warn!("Error while connecting: {}", e);
error!("Error while connecting: {}", e);
tokio::time::sleep(RECONNECT_INTERVAL).await;
}
}

437
core/src/error.rs Normal file
View file

@ -0,0 +1,437 @@
use std::{error, fmt, num::ParseIntError, str::Utf8Error, string::FromUtf8Error};
use base64::DecodeError;
use http::{
header::{InvalidHeaderName, InvalidHeaderValue, ToStrError},
method::InvalidMethod,
status::InvalidStatusCode,
uri::{InvalidUri, InvalidUriParts},
};
use protobuf::ProtobufError;
use thiserror::Error;
use tokio::sync::{mpsc::error::SendError, oneshot::error::RecvError};
use url::ParseError;
#[derive(Debug)]
pub struct Error {
pub kind: ErrorKind,
pub error: Box<dyn error::Error + Send + Sync>,
}
#[derive(Clone, Copy, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
pub enum ErrorKind {
#[error("The operation was cancelled by the caller")]
Cancelled = 1,
#[error("Unknown error")]
Unknown = 2,
#[error("Client specified an invalid argument")]
InvalidArgument = 3,
#[error("Deadline expired before operation could complete")]
DeadlineExceeded = 4,
#[error("Requested entity was not found")]
NotFound = 5,
#[error("Attempt to create entity that already exists")]
AlreadyExists = 6,
#[error("Permission denied")]
PermissionDenied = 7,
#[error("No valid authentication credentials")]
Unauthenticated = 16,
#[error("Resource has been exhausted")]
ResourceExhausted = 8,
#[error("Invalid state")]
FailedPrecondition = 9,
#[error("Operation aborted")]
Aborted = 10,
#[error("Operation attempted past the valid range")]
OutOfRange = 11,
#[error("Not implemented")]
Unimplemented = 12,
#[error("Internal error")]
Internal = 13,
#[error("Service unavailable")]
Unavailable = 14,
#[error("Unrecoverable data loss or corruption")]
DataLoss = 15,
#[error("Operation must not be used")]
DoNotUse = -1,
}
#[derive(Debug, Error)]
struct ErrorMessage(String);
impl fmt::Display for ErrorMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error {
pub fn new<E>(kind: ErrorKind, error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind,
error: error.into(),
}
}
pub fn aborted<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Aborted,
error: error.into(),
}
}
pub fn already_exists<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::AlreadyExists,
error: error.into(),
}
}
pub fn cancelled<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Cancelled,
error: error.into(),
}
}
pub fn data_loss<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::DataLoss,
error: error.into(),
}
}
pub fn deadline_exceeded<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::DeadlineExceeded,
error: error.into(),
}
}
pub fn do_not_use<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::DoNotUse,
error: error.into(),
}
}
pub fn failed_precondition<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::FailedPrecondition,
error: error.into(),
}
}
pub fn internal<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Internal,
error: error.into(),
}
}
pub fn invalid_argument<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::InvalidArgument,
error: error.into(),
}
}
pub fn not_found<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::NotFound,
error: error.into(),
}
}
pub fn out_of_range<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::OutOfRange,
error: error.into(),
}
}
pub fn permission_denied<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::PermissionDenied,
error: error.into(),
}
}
pub fn resource_exhausted<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::ResourceExhausted,
error: error.into(),
}
}
pub fn unauthenticated<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unauthenticated,
error: error.into(),
}
}
pub fn unavailable<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unavailable,
error: error.into(),
}
}
pub fn unimplemented<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unimplemented,
error: error.into(),
}
}
pub fn unknown<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unknown,
error: error.into(),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.error.source()
}
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "{} {{ ", self.kind)?;
self.error.fmt(fmt)?;
write!(fmt, " }}")
}
}
impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<http::Error> for Error {
fn from(err: http::Error) -> Self {
if err.is::<InvalidHeaderName>()
|| err.is::<InvalidHeaderValue>()
|| err.is::<InvalidMethod>()
|| err.is::<InvalidUri>()
|| err.is::<InvalidUriParts>()
{
return Self::new(ErrorKind::InvalidArgument, err);
}
if err.is::<InvalidStatusCode>() {
return Self::new(ErrorKind::FailedPrecondition, err);
}
Self::new(ErrorKind::Unknown, err)
}
}
impl From<hyper::Error> for Error {
fn from(err: hyper::Error) -> Self {
if err.is_parse() || err.is_parse_too_large() || err.is_parse_status() || err.is_user() {
return Self::new(ErrorKind::Internal, err);
}
if err.is_canceled() {
return Self::new(ErrorKind::Cancelled, err);
}
if err.is_connect() {
return Self::new(ErrorKind::Unavailable, err);
}
if err.is_incomplete_message() {
return Self::new(ErrorKind::DataLoss, err);
}
if err.is_body_write_aborted() || err.is_closed() {
return Self::new(ErrorKind::Aborted, err);
}
if err.is_timeout() {
return Self::new(ErrorKind::DeadlineExceeded, err);
}
Self::new(ErrorKind::Unknown, err)
}
}
impl From<quick_xml::Error> for Error {
fn from(err: quick_xml::Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
use std::io::ErrorKind as IoErrorKind;
match err.kind() {
IoErrorKind::NotFound => Self::new(ErrorKind::NotFound, err),
IoErrorKind::PermissionDenied => Self::new(ErrorKind::PermissionDenied, err),
IoErrorKind::AddrInUse | IoErrorKind::AlreadyExists => {
Self::new(ErrorKind::AlreadyExists, err)
}
IoErrorKind::AddrNotAvailable
| IoErrorKind::ConnectionRefused
| IoErrorKind::NotConnected => Self::new(ErrorKind::Unavailable, err),
IoErrorKind::BrokenPipe
| IoErrorKind::ConnectionReset
| IoErrorKind::ConnectionAborted => Self::new(ErrorKind::Aborted, err),
IoErrorKind::Interrupted | IoErrorKind::WouldBlock => {
Self::new(ErrorKind::Cancelled, err)
}
IoErrorKind::InvalidData | IoErrorKind::UnexpectedEof => {
Self::new(ErrorKind::FailedPrecondition, err)
}
IoErrorKind::TimedOut => Self::new(ErrorKind::DeadlineExceeded, err),
IoErrorKind::InvalidInput => Self::new(ErrorKind::InvalidArgument, err),
IoErrorKind::WriteZero => Self::new(ErrorKind::ResourceExhausted, err),
_ => Self::new(ErrorKind::Unknown, err),
}
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<InvalidHeaderValue> for Error {
fn from(err: InvalidHeaderValue) -> Self {
Self::new(ErrorKind::InvalidArgument, err)
}
}
impl From<InvalidUri> for Error {
fn from(err: InvalidUri) -> Self {
Self::new(ErrorKind::InvalidArgument, err)
}
}
impl From<ParseError> for Error {
fn from(err: ParseError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<ParseIntError> for Error {
fn from(err: ParseIntError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<ProtobufError> for Error {
fn from(err: ProtobufError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<RecvError> for Error {
fn from(err: RecvError) -> Self {
Self::new(ErrorKind::Internal, err)
}
}
impl<T> From<SendError<T>> for Error {
fn from(err: SendError<T>) -> Self {
Self {
kind: ErrorKind::Internal,
error: ErrorMessage(err.to_string()).into(),
}
}
}
impl From<ToStrError> for Error {
fn from(err: ToStrError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<Utf8Error> for Error {
fn from(err: Utf8Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}

View file

@ -1,7 +1,7 @@
use librespot_protocol as protocol;
use std::fmt;
use librespot_protocol as protocol;
use crate::spotify_id::to_base16;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]

View file

@ -1,49 +1,82 @@
use std::env::consts::OS;
use bytes::Bytes;
use futures_util::future::IntoStream;
use futures_util::FutureExt;
use futures_util::{future::IntoStream, FutureExt};
use http::header::HeaderValue;
use http::uri::InvalidUri;
use hyper::client::{HttpConnector, ResponseFuture};
use hyper::header::USER_AGENT;
use hyper::{Body, Client, Request, Response, StatusCode};
use hyper::{
client::{HttpConnector, ResponseFuture},
header::USER_AGENT,
Body, Client, Request, Response, StatusCode,
};
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
use hyper_rustls::HttpsConnector;
use rustls::{ClientConfig, RootCertStore};
use thiserror::Error;
use url::Url;
use std::env::consts::OS;
use crate::version::{
FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING,
use crate::{
version::{FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING},
Error,
};
#[derive(Debug, Error)]
pub enum HttpClientError {
#[error("Response status code: {0}")]
StatusCode(hyper::StatusCode),
}
impl From<HttpClientError> for Error {
fn from(err: HttpClientError) -> Self {
match err {
HttpClientError::StatusCode(code) => {
// not exhaustive, but what reasonably could be expected
match code {
StatusCode::GATEWAY_TIMEOUT | StatusCode::REQUEST_TIMEOUT => {
Error::deadline_exceeded(err)
}
StatusCode::GONE
| StatusCode::NOT_FOUND
| StatusCode::MOVED_PERMANENTLY
| StatusCode::PERMANENT_REDIRECT
| StatusCode::TEMPORARY_REDIRECT => Error::not_found(err),
StatusCode::FORBIDDEN | StatusCode::PAYMENT_REQUIRED => {
Error::permission_denied(err)
}
StatusCode::NETWORK_AUTHENTICATION_REQUIRED
| StatusCode::PROXY_AUTHENTICATION_REQUIRED
| StatusCode::UNAUTHORIZED => Error::unauthenticated(err),
StatusCode::EXPECTATION_FAILED
| StatusCode::PRECONDITION_FAILED
| StatusCode::PRECONDITION_REQUIRED => Error::failed_precondition(err),
StatusCode::RANGE_NOT_SATISFIABLE => Error::out_of_range(err),
StatusCode::INTERNAL_SERVER_ERROR
| StatusCode::MISDIRECTED_REQUEST
| StatusCode::SERVICE_UNAVAILABLE
| StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => Error::unavailable(err),
StatusCode::BAD_REQUEST
| StatusCode::HTTP_VERSION_NOT_SUPPORTED
| StatusCode::LENGTH_REQUIRED
| StatusCode::METHOD_NOT_ALLOWED
| StatusCode::NOT_ACCEPTABLE
| StatusCode::PAYLOAD_TOO_LARGE
| StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE
| StatusCode::UNSUPPORTED_MEDIA_TYPE
| StatusCode::URI_TOO_LONG => Error::invalid_argument(err),
StatusCode::TOO_MANY_REQUESTS => Error::resource_exhausted(err),
StatusCode::NOT_IMPLEMENTED => Error::unimplemented(err),
_ => Error::unknown(err),
}
}
}
}
}
pub struct HttpClient {
user_agent: HeaderValue,
proxy: Option<Url>,
tls_config: ClientConfig,
}
#[derive(Error, Debug)]
pub enum HttpClientError {
#[error("could not parse request: {0}")]
Parsing(#[from] http::Error),
#[error("could not send request: {0}")]
Request(hyper::Error),
#[error("could not read response: {0}")]
Response(hyper::Error),
#[error("status code: {0}")]
NotOK(u16),
#[error("could not build proxy connector: {0}")]
ProxyBuilder(#[from] std::io::Error),
}
impl From<InvalidUri> for HttpClientError {
fn from(err: InvalidUri) -> Self {
Self::Parsing(err.into())
}
}
impl HttpClient {
pub fn new(proxy: Option<&Url>) -> Self {
let spotify_version = match OS {
@ -53,7 +86,7 @@ impl HttpClient {
let spotify_platform = match OS {
"android" => "Android/31",
"ios" => "iOS/15.1.1",
"ios" => "iOS/15.2",
"macos" => "OSX/0",
"windows" => "Win32/0",
_ => "Linux/0",
@ -95,37 +128,32 @@ impl HttpClient {
}
}
pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, HttpClientError> {
pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, Error> {
debug!("Requesting {:?}", req.uri().to_string());
let request = self.request_fut(req)?;
{
let response = request.await;
if let Ok(response) = &response {
let status = response.status();
if status != StatusCode::OK {
return Err(HttpClientError::NotOK(status.into()));
}
}
response.map_err(HttpClientError::Response)
let code = response.status();
if code != StatusCode::OK {
return Err(HttpClientError::StatusCode(code).into());
}
}
pub async fn request_body(&self, req: Request<Body>) -> Result<Bytes, HttpClientError> {
Ok(response?)
}
pub async fn request_body(&self, req: Request<Body>) -> Result<Bytes, Error> {
let response = self.request(req).await?;
hyper::body::to_bytes(response.into_body())
.await
.map_err(HttpClientError::Response)
Ok(hyper::body::to_bytes(response.into_body()).await?)
}
pub fn request_stream(
&self,
req: Request<Body>,
) -> Result<IntoStream<ResponseFuture>, HttpClientError> {
pub fn request_stream(&self, req: Request<Body>) -> Result<IntoStream<ResponseFuture>, Error> {
Ok(self.request_fut(req)?.into_stream())
}
pub fn request_fut(&self, mut req: Request<Body>) -> Result<ResponseFuture, HttpClientError> {
pub fn request_fut(&self, mut req: Request<Body>) -> Result<ResponseFuture, Error> {
let mut http = HttpConnector::new();
http.enforce_http(false);
let connector = HttpsConnector::from((http, self.tls_config.clone()));

View file

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

View file

@ -1,9 +1,10 @@
use std::collections::HashMap;
use std::future::Future;
use std::mem;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
use std::{
collections::HashMap,
future::Future,
mem,
pin::Pin,
task::{Context, Poll},
};
use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
@ -11,9 +12,7 @@ use futures_util::FutureExt;
use protobuf::Message;
use tokio::sync::{mpsc, oneshot};
use crate::packet::PacketType;
use crate::protocol;
use crate::util::SeqGenerator;
use crate::{packet::PacketType, protocol, util::SeqGenerator, Error};
mod types;
pub use self::types::*;
@ -33,18 +32,18 @@ component! {
pub struct MercuryPending {
parts: Vec<Vec<u8>>,
partial: Option<Vec<u8>>,
callback: Option<oneshot::Sender<Result<MercuryResponse, MercuryError>>>,
callback: Option<oneshot::Sender<Result<MercuryResponse, Error>>>,
}
pub struct MercuryFuture<T> {
receiver: oneshot::Receiver<Result<T, MercuryError>>,
receiver: oneshot::Receiver<Result<T, Error>>,
}
impl<T> Future for MercuryFuture<T> {
type Output = Result<T, MercuryError>;
type Output = Result<T, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.receiver.poll_unpin(cx).map_err(|_| MercuryError)?
self.receiver.poll_unpin(cx)?
}
}
@ -55,7 +54,7 @@ impl MercuryManager {
seq
}
fn request(&self, req: MercuryRequest) -> MercuryFuture<MercuryResponse> {
fn request(&self, req: MercuryRequest) -> Result<MercuryFuture<MercuryResponse>, Error> {
let (tx, rx) = oneshot::channel();
let pending = MercuryPending {
@ -72,13 +71,13 @@ impl MercuryManager {
});
let cmd = req.method.command();
let data = req.encode(&seq);
let data = req.encode(&seq)?;
self.session().send_packet(cmd, data);
MercuryFuture { receiver: rx }
self.session().send_packet(cmd, data)?;
Ok(MercuryFuture { receiver: rx })
}
pub fn get<T: Into<String>>(&self, uri: T) -> MercuryFuture<MercuryResponse> {
pub fn get<T: Into<String>>(&self, uri: T) -> Result<MercuryFuture<MercuryResponse>, Error> {
self.request(MercuryRequest {
method: MercuryMethod::Get,
uri: uri.into(),
@ -87,7 +86,11 @@ impl MercuryManager {
})
}
pub fn send<T: Into<String>>(&self, uri: T, data: Vec<u8>) -> MercuryFuture<MercuryResponse> {
pub fn send<T: Into<String>>(
&self,
uri: T,
data: Vec<u8>,
) -> Result<MercuryFuture<MercuryResponse>, Error> {
self.request(MercuryRequest {
method: MercuryMethod::Send,
uri: uri.into(),
@ -103,7 +106,7 @@ impl MercuryManager {
pub fn subscribe<T: Into<String>>(
&self,
uri: T,
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<MercuryResponse>, MercuryError>> + 'static
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<MercuryResponse>, Error>> + 'static
{
let uri = uri.into();
let request = self.request(MercuryRequest {
@ -115,7 +118,7 @@ impl MercuryManager {
let manager = self.clone();
async move {
let response = request.await?;
let response = request?.await?;
let (tx, rx) = mpsc::unbounded_channel();
@ -125,14 +128,19 @@ 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();
match protocol::pubsub::Subscription::parse_from_bytes(&sub) {
Ok(mut sub) => {
let sub_uri = sub.take_uri();
debug!("subscribed sub_uri={}", sub_uri);
inner.subscriptions.push((sub_uri, tx.clone()));
}
Err(e) => {
error!("could not subscribe to {}: {}", uri, e);
}
}
}
} else {
// New subscription protocol, watch the requested URI
inner.subscriptions.push((uri, tx));
@ -165,7 +173,7 @@ impl MercuryManager {
}
}
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) {
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
let seq = data.split_to(seq_len).as_ref().to_owned();
@ -185,7 +193,7 @@ impl MercuryManager {
}
} else {
warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8);
return;
return Err(MercuryError::Command(cmd).into());
}
}
};
@ -205,10 +213,12 @@ impl MercuryManager {
}
if flags == 0x1 {
self.complete_request(cmd, pending);
self.complete_request(cmd, pending)?;
} else {
self.lock(move |inner| inner.pending.insert(seq, pending));
}
Ok(())
}
fn parse_part(data: &mut Bytes) -> Vec<u8> {
@ -216,9 +226,9 @@ impl MercuryManager {
data.split_to(size).as_ref().to_owned()
}
fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) {
fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) -> Result<(), Error> {
let header_data = pending.parts.remove(0);
let header = protocol::mercury::Header::parse_from_bytes(&header_data).unwrap();
let header = protocol::mercury::Header::parse_from_bytes(&header_data)?;
let response = MercuryResponse {
uri: header.get_uri().to_string(),
@ -226,13 +236,17 @@ impl MercuryManager {
payload: pending.parts,
};
if response.status_code >= 500 {
panic!("Spotify servers returned an error. Restart librespot.");
} else if response.status_code >= 400 {
warn!("error {} for uri {}", response.status_code, &response.uri);
let status_code = response.status_code;
if status_code >= 500 {
error!("error {} for uri {}", status_code, &response.uri);
Err(MercuryError::Response(response).into())
} else if status_code >= 400 {
error!("error {} for uri {}", status_code, &response.uri);
if let Some(cb) = pending.callback {
let _ = cb.send(Err(MercuryError));
cb.send(Err(MercuryError::Response(response.clone()).into()))
.map_err(|_| MercuryError::Channel)?;
}
Err(MercuryError::Response(response).into())
} else if let PacketType::MercuryEvent = cmd {
self.lock(|inner| {
let mut found = false;
@ -242,7 +256,7 @@ impl MercuryManager {
// before sending while saving the subscription under its unencoded form.
let mut uri_split = response.uri.split('/');
let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string())
let encoded_uri = std::iter::once(uri_split.next().unwrap_or_default().to_string())
.chain(uri_split.map(|component| {
form_urlencoded::byte_serialize(component.as_bytes()).collect::<String>()
}))
@ -263,12 +277,19 @@ impl MercuryManager {
});
if !found {
debug!("unknown subscription uri={}", response.uri);
debug!("unknown subscription uri={}", &response.uri);
trace!("response pushed over Mercury: {:?}", response);
Err(MercuryError::Response(response).into())
} else {
Ok(())
}
})
} else if let Some(cb) = pending.callback {
let _ = cb.send(Ok(response));
cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?;
Ok(())
} else {
error!("can't handle Mercury response: {:?}", response);
Err(MercuryError::Response(response).into())
}
}

View file

@ -1,6 +1,8 @@
use std::collections::VecDeque;
use super::*;
use super::{MercuryFuture, MercuryManager, MercuryResponse};
use crate::Error;
pub struct MercurySender {
mercury: MercuryManager,
@ -23,12 +25,13 @@ impl MercurySender {
self.buffered_future.is_none() && self.pending.is_empty()
}
pub fn send(&mut self, item: Vec<u8>) {
let task = self.mercury.send(self.uri.clone(), item);
pub fn send(&mut self, item: Vec<u8>) -> Result<(), Error> {
let task = self.mercury.send(self.uri.clone(), item)?;
self.pending.push_back(task);
Ok(())
}
pub async fn flush(&mut self) -> Result<(), MercuryError> {
pub async fn flush(&mut self) -> Result<(), Error> {
if self.buffered_future.is_none() {
self.buffered_future = self.pending.pop_front();
}

View file

@ -1,11 +1,10 @@
use std::io::Write;
use byteorder::{BigEndian, WriteBytesExt};
use protobuf::Message;
use std::fmt;
use std::io::Write;
use thiserror::Error;
use crate::packet::PacketType;
use crate::protocol;
use crate::{packet::PacketType, protocol, Error};
#[derive(Debug, PartialEq, Eq)]
pub enum MercuryMethod {
@ -30,12 +29,23 @@ pub struct MercuryResponse {
pub payload: Vec<Vec<u8>>,
}
#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)]
pub struct MercuryError;
#[derive(Debug, Error)]
pub enum MercuryError {
#[error("callback receiver was disconnected")]
Channel,
#[error("error handling packet type: {0:?}")]
Command(PacketType),
#[error("error handling Mercury response: {0:?}")]
Response(MercuryResponse),
}
impl fmt::Display for MercuryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Mercury error")
impl From<MercuryError> for Error {
fn from(err: MercuryError) -> Self {
match err {
MercuryError::Channel => Error::aborted(err),
MercuryError::Command(_) => Error::unimplemented(err),
MercuryError::Response(_) => Error::unavailable(err),
}
}
}
@ -63,15 +73,12 @@ impl MercuryMethod {
}
impl MercuryRequest {
// TODO: change into Result and remove unwraps
pub fn encode(&self, seq: &[u8]) -> Vec<u8> {
pub fn encode(&self, seq: &[u8]) -> Result<Vec<u8>, Error> {
let mut packet = Vec::new();
packet.write_u16::<BigEndian>(seq.len() as u16).unwrap();
packet.write_all(seq).unwrap();
packet.write_u8(1).unwrap(); // Flags: FINAL
packet
.write_u16::<BigEndian>(1 + self.payload.len() as u16)
.unwrap(); // Part count
packet.write_u16::<BigEndian>(seq.len() as u16)?;
packet.write_all(seq)?;
packet.write_u8(1)?; // Flags: FINAL
packet.write_u16::<BigEndian>(1 + self.payload.len() as u16)?; // Part count
let mut header = protocol::mercury::Header::new();
header.set_uri(self.uri.clone());
@ -81,16 +88,14 @@ impl MercuryRequest {
header.set_content_type(content_type.clone());
}
packet
.write_u16::<BigEndian>(header.compute_size() as u16)
.unwrap();
header.write_to_writer(&mut packet).unwrap();
packet.write_u16::<BigEndian>(header.compute_size() as u16)?;
header.write_to_writer(&mut packet)?;
for p in &self.payload {
packet.write_u16::<BigEndian>(p.len() as u16).unwrap();
packet.write_all(p).unwrap();
packet.write_u16::<BigEndian>(p.len() as u16)?;
packet.write_all(p)?;
}
packet
Ok(packet)
}
}

View file

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

View file

@ -1,13 +1,16 @@
use std::collections::HashMap;
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::process::exit;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, RwLock, Weak};
use std::task::Context;
use std::task::Poll;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{
collections::HashMap,
future::Future,
io,
pin::Pin,
process::exit,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, RwLock, Weak,
},
task::{Context, Poll},
time::{SystemTime, UNIX_EPOCH},
};
use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
@ -20,18 +23,21 @@ use thiserror::Error;
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use crate::apresolve::ApResolver;
use crate::audio_key::AudioKeyManager;
use crate::authentication::Credentials;
use crate::cache::Cache;
use crate::channel::ChannelManager;
use crate::config::SessionConfig;
use crate::connection::{self, AuthenticationError};
use crate::http_client::HttpClient;
use crate::mercury::MercuryManager;
use crate::packet::PacketType;
use crate::spclient::SpClient;
use crate::token::TokenProvider;
use crate::{
apresolve::ApResolver,
audio_key::AudioKeyManager,
authentication::Credentials,
cache::Cache,
channel::ChannelManager,
config::SessionConfig,
connection::{self, AuthenticationError},
http_client::HttpClient,
mercury::MercuryManager,
packet::PacketType,
spclient::SpClient,
token::TokenProvider,
Error,
};
#[derive(Debug, Error)]
pub enum SessionError {
@ -39,6 +45,18 @@ pub enum SessionError {
AuthenticationError(#[from] AuthenticationError),
#[error("Cannot create session: {0}")]
IoError(#[from] io::Error),
#[error("packet {0} unknown")]
Packet(u8),
}
impl From<SessionError> for Error {
fn from(err: SessionError) -> Self {
match err {
SessionError::AuthenticationError(_) => Error::unauthenticated(err),
SessionError::IoError(_) => Error::unavailable(err),
SessionError::Packet(_) => Error::unimplemented(err),
}
}
}
pub type UserAttributes = HashMap<String, String>;
@ -88,7 +106,7 @@ impl Session {
config: SessionConfig,
credentials: Credentials,
cache: Option<Cache>,
) -> Result<Session, SessionError> {
) -> Result<Session, Error> {
let http_client = HttpClient::new(config.proxy.as_ref());
let (sender_tx, sender_rx) = mpsc::unbounded_channel();
let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
@ -214,9 +232,18 @@ impl Session {
}
}
fn dispatch(&self, cmd: u8, data: Bytes) {
fn dispatch(&self, cmd: u8, data: Bytes) -> Result<(), Error> {
use PacketType::*;
let packet_type = FromPrimitive::from_u8(cmd);
let cmd = match packet_type {
Some(cmd) => cmd,
None => {
trace!("Ignoring unknown packet {:x}", cmd);
return Err(SessionError::Packet(cmd).into());
}
};
match packet_type {
Some(Ping) => {
let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64;
@ -229,24 +256,21 @@ impl Session {
self.0.data.write().unwrap().time_delta = server_timestamp - timestamp;
self.debug_info();
self.send_packet(Pong, vec![0, 0, 0, 0]);
self.send_packet(Pong, vec![0, 0, 0, 0])
}
Some(CountryCode) => {
let country = String::from_utf8(data.as_ref().to_owned()).unwrap();
let country = String::from_utf8(data.as_ref().to_owned())?;
info!("Country: {:?}", country);
self.0.data.write().unwrap().user_data.country = country;
Ok(())
}
Some(StreamChunkRes) | Some(ChannelError) => {
self.channel().dispatch(packet_type.unwrap(), data);
}
Some(AesKey) | Some(AesKeyError) => {
self.audio_key().dispatch(packet_type.unwrap(), data);
}
Some(StreamChunkRes) | Some(ChannelError) => self.channel().dispatch(cmd, data),
Some(AesKey) | Some(AesKeyError) => self.audio_key().dispatch(cmd, data),
Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => {
self.mercury().dispatch(packet_type.unwrap(), data);
self.mercury().dispatch(cmd, data)
}
Some(ProductInfo) => {
let data = std::str::from_utf8(&data).unwrap();
let data = std::str::from_utf8(&data)?;
let mut reader = quick_xml::Reader::from_str(data);
let mut buf = Vec::new();
@ -256,8 +280,7 @@ impl Session {
loop {
match reader.read_event(&mut buf) {
Ok(Event::Start(ref element)) => {
current_element =
std::str::from_utf8(element.name()).unwrap().to_owned()
current_element = std::str::from_utf8(element.name())?.to_owned()
}
Ok(Event::End(_)) => {
current_element = String::new();
@ -266,7 +289,7 @@ impl Session {
if !current_element.is_empty() {
let _ = user_attributes.insert(
current_element.clone(),
value.unescape_and_decode(&reader).unwrap(),
value.unescape_and_decode(&reader)?,
);
}
}
@ -284,24 +307,23 @@ impl Session {
Self::check_catalogue(&user_attributes);
self.0.data.write().unwrap().user_data.attributes = user_attributes;
Ok(())
}
Some(PongAck)
| Some(SecretBlock)
| Some(LegacyWelcome)
| Some(UnknownDataAllZeros)
| Some(LicenseVersion) => {}
| Some(LicenseVersion) => Ok(()),
_ => {
if let Some(packet_type) = PacketType::from_u8(cmd) {
trace!("Ignoring {:?} packet with data {:#?}", packet_type, data);
} else {
trace!("Ignoring unknown packet {:x}", cmd);
}
trace!("Ignoring {:?} packet with data {:#?}", cmd, data);
Err(SessionError::Packet(cmd as u8).into())
}
}
}
pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) {
self.0.tx_connection.send((cmd as u8, data)).unwrap();
pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) -> Result<(), Error> {
self.0.tx_connection.send((cmd as u8, data))?;
Ok(())
}
pub fn cache(&self) -> Option<&Arc<Cache>> {
@ -393,7 +415,7 @@ impl SessionWeak {
}
pub(crate) fn upgrade(&self) -> Session {
self.try_upgrade().expect("Session died")
self.try_upgrade().expect("Session died") // TODO
}
}
@ -434,7 +456,9 @@ where
}
};
session.dispatch(cmd, data);
if let Err(e) = session.dispatch(cmd, data) {
error!("could not dispatch command: {}", e);
}
}
}
}

View file

@ -1,5 +1,4 @@
use std::io;
use std::net::ToSocketAddrs;
use std::{io, net::ToSocketAddrs};
use tokio::net::TcpStream;
use url::Url;

View file

@ -1,22 +1,25 @@
use crate::apresolve::SocketAddress;
use crate::file_id::FileId;
use crate::http_client::HttpClientError;
use crate::mercury::MercuryError;
use crate::protocol::canvaz::EntityCanvazRequest;
use crate::protocol::connect::PutStateRequest;
use crate::protocol::extended_metadata::BatchedEntityRequest;
use crate::spotify_id::SpotifyId;
use std::time::Duration;
use bytes::Bytes;
use futures_util::future::IntoStream;
use http::header::HeaderValue;
use hyper::client::ResponseFuture;
use hyper::header::{InvalidHeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE};
use hyper::{Body, HeaderMap, Method, Request};
use hyper::{
client::ResponseFuture,
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE},
Body, HeaderMap, Method, Request,
};
use protobuf::Message;
use rand::Rng;
use std::time::Duration;
use thiserror::Error;
use crate::{
apresolve::SocketAddress,
error::ErrorKind,
protocol::{
canvaz::EntityCanvazRequest, connect::PutStateRequest,
extended_metadata::BatchedEntityRequest,
},
Error, FileId, SpotifyId,
};
component! {
SpClient : SpClientInner {
@ -25,23 +28,7 @@ component! {
}
}
pub type SpClientResult = Result<Bytes, SpClientError>;
#[derive(Error, Debug)]
pub enum SpClientError {
#[error("could not get authorization token")]
Token(#[from] MercuryError),
#[error("could not parse request: {0}")]
Parsing(#[from] http::Error),
#[error("could not complete request: {0}")]
Network(#[from] HttpClientError),
}
impl From<InvalidHeaderValue> for SpClientError {
fn from(err: InvalidHeaderValue) -> Self {
Self::Parsing(err.into())
}
}
pub type SpClientResult = Result<Bytes, Error>;
#[derive(Copy, Clone, Debug)]
pub enum RequestStrategy {
@ -157,12 +144,8 @@ impl SpClient {
))?,
);
last_response = self
.session()
.http_client()
.request_body(request)
.await
.map_err(SpClientError::Network);
last_response = self.session().http_client().request_body(request).await;
if last_response.is_ok() {
return last_response;
}
@ -177,9 +160,9 @@ impl SpClient {
// Reconnection logic: drop the current access point if we are experiencing issues.
// This will cause the next call to base_url() to resolve a new one.
if let Err(SpClientError::Network(ref network_error)) = last_response {
match network_error {
HttpClientError::Response(_) | HttpClientError::Request(_) => {
if let Err(ref network_error) = last_response {
match network_error.kind {
ErrorKind::Unavailable | ErrorKind::DeadlineExceeded => {
// Keep trying the current access point three times before dropping it.
if tries % 3 == 0 {
self.flush_accesspoint().await
@ -244,7 +227,7 @@ impl SpClient {
}
pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult {
let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62(),);
let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62());
self.request_as_json(&Method::GET, &endpoint, None, None)
.await
@ -291,7 +274,7 @@ impl SpClient {
url: &str,
offset: usize,
length: usize,
) -> Result<IntoStream<ResponseFuture>, SpClientError> {
) -> Result<IntoStream<ResponseFuture>, Error> {
let req = Request::builder()
.method(&Method::GET)
.uri(url)

View file

@ -1,13 +1,17 @@
use librespot_protocol as protocol;
use std::{
convert::{TryFrom, TryInto},
fmt,
ops::Deref,
};
use thiserror::Error;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::ops::Deref;
use crate::Error;
use librespot_protocol as protocol;
// re-export FileId for historic reasons, when it was part of this mod
pub use crate::file_id::FileId;
pub use crate::FileId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpotifyItemType {
@ -64,8 +68,14 @@ pub enum SpotifyIdError {
InvalidRoot,
}
pub type SpotifyIdResult = Result<SpotifyId, SpotifyIdError>;
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, SpotifyIdError>;
impl From<SpotifyIdError> for Error {
fn from(err: SpotifyIdError) -> Self {
Error::invalid_argument(err)
}
}
pub type SpotifyIdResult = Result<SpotifyId, Error>;
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, Error>;
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
@ -95,7 +105,7 @@ impl SpotifyId {
let p = match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
_ => return Err(SpotifyIdError::InvalidId),
_ => return Err(SpotifyIdError::InvalidId.into()),
} as u128;
dst <<= 4;
@ -121,7 +131,7 @@ impl SpotifyId {
b'0'..=b'9' => c - b'0',
b'a'..=b'z' => c - b'a' + 10,
b'A'..=b'Z' => c - b'A' + 36,
_ => return Err(SpotifyIdError::InvalidId),
_ => return Err(SpotifyIdError::InvalidId.into()),
} as u128;
dst *= 62;
@ -143,7 +153,7 @@ impl SpotifyId {
id: u128::from_be_bytes(dst),
item_type: SpotifyItemType::Unknown,
}),
Err(_) => Err(SpotifyIdError::InvalidId),
Err(_) => Err(SpotifyIdError::InvalidId.into()),
}
}
@ -161,20 +171,20 @@ impl SpotifyId {
// At minimum, should be `spotify:{type}:{id}`
if uri_parts.len() < 3 {
return Err(SpotifyIdError::InvalidFormat);
return Err(SpotifyIdError::InvalidFormat.into());
}
if uri_parts[0] != "spotify" {
return Err(SpotifyIdError::InvalidRoot);
return Err(SpotifyIdError::InvalidRoot.into());
}
let id = uri_parts.pop().unwrap();
let id = uri_parts.pop().unwrap_or_default();
if id.len() != Self::SIZE_BASE62 {
return Err(SpotifyIdError::InvalidId);
return Err(SpotifyIdError::InvalidId.into());
}
Ok(Self {
item_type: uri_parts.pop().unwrap().into(),
item_type: uri_parts.pop().unwrap_or_default().into(),
..Self::from_base62(id)?
})
}
@ -285,15 +295,15 @@ impl NamedSpotifyId {
// At minimum, should be `spotify:user:{username}:{type}:{id}`
if uri_parts.len() < 5 {
return Err(SpotifyIdError::InvalidFormat);
return Err(SpotifyIdError::InvalidFormat.into());
}
if uri_parts[0] != "spotify" {
return Err(SpotifyIdError::InvalidRoot);
return Err(SpotifyIdError::InvalidRoot.into());
}
if uri_parts[1] != "user" {
return Err(SpotifyIdError::InvalidFormat);
return Err(SpotifyIdError::InvalidFormat.into());
}
Ok(Self {
@ -344,35 +354,35 @@ impl fmt::Display for NamedSpotifyId {
}
impl TryFrom<&[u8]> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
Self::from_raw(src)
}
}
impl TryFrom<&str> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(src: &str) -> Result<Self, Self::Error> {
Self::from_base62(src)
}
}
impl TryFrom<String> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(src: String) -> Result<Self, Self::Error> {
Self::try_from(src.as_str())
}
}
impl TryFrom<&Vec<u8>> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(src.as_slice())
}
}
impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> {
match SpotifyId::from_raw(track.get_gid()) {
Ok(mut id) => {
@ -385,7 +395,7 @@ impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
}
impl TryFrom<&protocol::metadata::Album> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Album,
@ -395,7 +405,7 @@ impl TryFrom<&protocol::metadata::Album> for SpotifyId {
}
impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Artist,
@ -405,7 +415,7 @@ impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
}
impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Episode,
@ -415,7 +425,7 @@ impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
}
impl TryFrom<&protocol::metadata::Track> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Track,
@ -425,7 +435,7 @@ impl TryFrom<&protocol::metadata::Track> for SpotifyId {
}
impl TryFrom<&protocol::metadata::Show> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Show,
@ -435,7 +445,7 @@ impl TryFrom<&protocol::metadata::Show> for SpotifyId {
}
impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Artist,
@ -445,7 +455,7 @@ impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
}
impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Track,
@ -457,7 +467,7 @@ impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
// Note that this is the unique revision of an item's metadata on a playlist,
// not the ID of that item or playlist.
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
Self::try_from(item.get_revision())
}
@ -465,7 +475,7 @@ impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
// Note that this is the unique revision of a playlist, not the ID of that playlist.
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(
playlist: &protocol::playlist4_external::SelectedListContent,
) -> Result<Self, Self::Error> {
@ -477,7 +487,7 @@ impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
// which is why we now don't create a separate `Playlist` enum value yet and choose
// to discard any item type.
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
type Error = SpotifyIdError;
type Error = crate::Error;
fn try_from(
picture: &protocol::playlist_annotate3::TranscodedPicture,
) -> Result<Self, Self::Error> {
@ -565,7 +575,7 @@ mod tests {
id: 0,
kind: SpotifyItemType::Unknown,
// Invalid ID in the URI.
uri_error: Some(SpotifyIdError::InvalidId),
uri_error: SpotifyIdError::InvalidId,
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
base62: "!!!!!Ys0csV6RS48xBl0tH",
@ -578,7 +588,7 @@ mod tests {
id: 0,
kind: SpotifyItemType::Unknown,
// Missing colon between ID and type.
uri_error: Some(SpotifyIdError::InvalidFormat),
uri_error: SpotifyIdError::InvalidFormat,
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
base16: "--------------------",
base62: "....................",
@ -591,7 +601,7 @@ mod tests {
id: 0,
kind: SpotifyItemType::Unknown,
// Uri too short
uri_error: Some(SpotifyIdError::InvalidId),
uri_error: SpotifyIdError::InvalidId,
uri: "spotify:azb:aRS48xBl0tH",
base16: "--------------------",
base62: "....................",

View file

@ -8,12 +8,12 @@
// user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming,
// app-remote-control
use crate::mercury::MercuryError;
use std::time::{Duration, Instant};
use serde::Deserialize;
use thiserror::Error;
use std::error::Error;
use std::time::{Duration, Instant};
use crate::Error;
component! {
TokenProvider : TokenProviderInner {
@ -21,6 +21,18 @@ component! {
}
}
#[derive(Debug, Error)]
pub enum TokenError {
#[error("no tokens available")]
Empty,
}
impl From<TokenError> for Error {
fn from(err: TokenError) -> Self {
Error::unavailable(err)
}
}
#[derive(Clone, Debug)]
pub struct Token {
pub access_token: String,
@ -54,11 +66,7 @@ impl TokenProvider {
}
// scopes must be comma-separated
pub async fn get_token(&self, scopes: &str) -> Result<Token, MercuryError> {
if scopes.is_empty() {
return Err(MercuryError);
}
pub async fn get_token(&self, scopes: &str) -> Result<Token, Error> {
if let Some(index) = self.find_token(scopes.split(',').collect()) {
let cached_token = self.lock(|inner| inner.tokens[index].clone());
if cached_token.is_expired() {
@ -79,14 +87,10 @@ impl TokenProvider {
Self::KEYMASTER_CLIENT_ID,
self.session().device_id()
);
let request = self.session().mercury().get(query_uri);
let request = self.session().mercury().get(query_uri)?;
let response = request.await?;
let data = response
.payload
.first()
.expect("No tokens received")
.to_vec();
let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?;
let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec();
let token = Token::new(String::from_utf8(data)?)?;
trace!("Got token: {:#?}", token);
self.lock(|inner| inner.tokens.push(token.clone()));
Ok(token)
@ -96,7 +100,7 @@ impl TokenProvider {
impl Token {
const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10);
pub fn new(body: String) -> Result<Self, Box<dyn Error>> {
pub fn new(body: String) -> Result<Self, Error> {
let data: TokenData = serde_json::from_slice(body.as_ref())?;
Ok(Self {
access_token: data.access_token,

View file

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

View file

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

View file

@ -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<u8>),
#[error("Setting up the http server failed: {0}")]
HttpServerError(#[from] hyper::Error),
#[error("Missing params for key {0}")]
ParamsError(&'static str),
}
impl From<DiscoveryError> for Error {
fn from(err: DiscoveryError) -> Self {
match err {
DiscoveryError::DnsSdError(_) => Error::unavailable(err),
DiscoveryError::HmacError(_) => Error::invalid_argument(err),
DiscoveryError::HttpServerError(_) => Error::unavailable(err),
DiscoveryError::ParamsError(_) => Error::invalid_argument(err),
}
}
}
impl Builder {
@ -96,7 +113,7 @@ impl Builder {
pub fn launch(self) -> Result<Discovery, Error> {
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())?;

View file

@ -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>, Cow<'a, str>>;
@ -76,14 +85,26 @@ impl RequestHandler {
Response::new(Body::from(body))
}
fn handle_add_user(&self, params: &Params<'_>) -> Response<hyper::Body> {
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<Response<hyper::Body>, 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::<Sha1>::new_from_slice(base_key).expect("HMAC can take key of any size");
let mut h = Hmac::<Sha1>::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::<Sha1>::new_from_slice(base_key).expect("HMAC can take key of any size");
let mut h = Hmac::<Sha1>::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::<Sha1>::new_from_slice(&checksum_key).expect("HMAC can take key of any size");
let mut h = Hmac::<Sha1>::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<hyper::Body> {
@ -152,7 +173,10 @@ impl RequestHandler {
res
}
async fn handle(self: Arc<Self>, request: Request<Body>) -> hyper::Result<Response<Body>> {
async fn handle(
self: Arc<Self>,
request: Request<Body>,
) -> Result<hyper::Result<Response<Body>>, Error> {
let mut params = Params::new();
let (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(&params),
(Method::POST, Some("addUser")) => self.handle_add_user(&params)?,
_ => self.not_found(),
})
}))
}
}
@ -186,7 +210,7 @@ pub struct DiscoveryServer {
}
impl DiscoveryServer {
pub fn new(config: Config, port: &mut u16) -> hyper::Result<Self> {
pub fn new(config: Config, port: &mut u16) -> Result<hyper::Result<Self>, Error> {
let (discovery, cred_rx) = RequestHandler::new(config);
let discovery = 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,
})
}))
}
}

View file

@ -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<Self, MetadataError> {
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Album {
type Error = MetadataError;
type Error = librespot_core::Error;
fn try_from(album: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: album.try_into()?,
@ -138,7 +124,7 @@ impl TryFrom<&<Self as Metadata>::Message> for Album {
try_from_repeated_message!(<Album as Metadata>::Message, Albums);
impl TryFrom<&DiscMessage> for Disc {
type Error = MetadataError;
type Error = librespot_core::Error;
fn try_from(disc: &DiscMessage) -> Result<Self, Self::Error> {
Ok(Self {
number: disc.get_number(),

View file

@ -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<Self, MetadataError> {
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Artist {
type Error = MetadataError;
type Error = librespot_core::Error;
fn try_from(artist: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: artist.try_into()?,
@ -114,7 +104,7 @@ impl TryFrom<&<Self as Metadata>::Message> for Artist {
try_from_repeated_message!(<Artist as Metadata>::Message, Artists);
impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole {
type Error = MetadataError;
type Error = librespot_core::Error;
fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result<Self, Self::Error> {
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<Self, Self::Error> {
Ok(Self {
country: top_tracks.get_country().to_owned(),

View file

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

View file

@ -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<AudioItem, MetadataError>;
pub type AudioItemResult = Result<AudioItem, Error>;
// 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)),
}
}
}

View file

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

View file

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

View file

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

View file

@ -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<Self, MetadataError> {
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Episode {
type Error = MetadataError;
type Error = librespot_core::Error;
fn try_from(episode: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: episode.try_into()?,

View file

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

View file

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

View file

@ -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<Self, Self::Error> {
Ok(Self {
target_name: picture.get_target_name().to_owned(),

View file

@ -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<Self, MetadataError> {
async fn get(session: &Session, id: SpotifyId) -> Result<Self, Error> {
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<Self, MetadataError>;
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error>;
}

View file

@ -4,16 +4,14 @@ use std::fmt::Debug;
use protobuf::Message;
use 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, &current_user, playlist_id).await
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Ok(Self {
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<Self, MetadataError> {
) -> Result<Self, Error> {
let response = Self::request_for_user(session, username, playlist_id).await?;
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
Self::parse(&msg, playlist_id)
@ -74,7 +72,7 @@ impl PlaylistAnnotation {
impl MercuryRequest for PlaylistAnnotation {}
impl TryFrom<&<PlaylistAnnotation as Metadata>::Message> for PlaylistAnnotation {
type Error = MetadataError;
type Error = librespot_core::Error;
fn try_from(
annotation: &<PlaylistAnnotation as Metadata>::Message,
) -> Result<Self, Self::Error> {

View file

@ -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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
Ok(Self {
index: update.get_index(),

View file

@ -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<Self, Self::Error> {
Ok(Self {
from_revision: diff.get_from_revision().try_into()?,

View file

@ -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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
Ok(Self {
revision: item.try_into()?,

View file

@ -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<Self, MetadataError> {
) -> Result<Self, Error> {
let response = Self::request_for_user(session, username, playlist_id).await?;
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
Self::parse(&msg, playlist_id)
@ -153,7 +156,7 @@ impl Metadata for Playlist {
<Self as MercuryRequest>::request(session, &uri).await
}
fn parse(msg: &Self::Message, id: SpotifyId) -> Result<Self, MetadataError> {
fn parse(msg: &Self::Message, id: SpotifyId) -> Result<Self, Error> {
// the playlist proto doesn't contain the id so we decorate it
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<Self, MetadataError> {
pub async fn get_root_for_user(session: &Session, username: &str) -> Result<Self, Error> {
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<&<Playlist as Metadata>::Message> for SelectedListContent {
type Error = MetadataError;
type Error = librespot_core::Error;
fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
revision: playlist.get_revision().try_into()?,

View file

@ -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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
Ok(Self {
from_index: remove.get_from_index(),

View file

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

View file

@ -1,20 +1,21 @@
use crate::error::RequestError;
use crate::MetadataError;
use librespot_core::session::Session;
use librespot_core::{Error, Session};
pub type RequestResult = Result<bytes::Bytes, RequestError>;
pub type RequestResult = Result<bytes::Bytes, Error>;
#[async_trait]
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)),
}
}
}

View file

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

View file

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

View file

@ -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<Self, MetadataError> {
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Show {
type Error = MetadataError;
type Error = librespot_core::Error;
fn try_from(show: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: show.try_into()?,

View file

@ -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<Self, MetadataError> {
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Track {
type Error = MetadataError;
type Error = librespot_core::Error;
fn try_from(track: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: track.try_into()?,

View file

@ -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<Self, Self::Error> {
let result: Result<Vec<_>, _> = src.iter().map(TryFrom::try_from).collect();
Ok(Self(result?))

View file

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

View file

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

View file

@ -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,
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<mpsc::UnboundedSender<PlayerCommand>>,
@ -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(())
}

View file

@ -1,3 +1,14 @@
use std::{
env,
fs::create_dir_all,
ops::RangeInclusive,
path::{Path, PathBuf},
pin::Pin,
process::exit,
str::FromStr,
time::{Duration, Instant},
};
use futures_util::{future, FutureExt, StreamExt};
use 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::{
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! {