Remove unwraps from librespot-audio

This commit is contained in:
Roderick van Domburg 2021-12-18 23:44:13 +01:00
parent d18a0d1803
commit 0d51fd43dc
No known key found for this signature in database
GPG key ID: A9EF5222A26F0451
9 changed files with 301 additions and 165 deletions

View file

@ -25,16 +25,28 @@ use self::receive::audio_file_fetch;
use crate::range_set::{Range, RangeSet}; use crate::range_set::{Range, RangeSet};
pub type AudioFileResult = Result<(), AudioFileError>;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum AudioFileError { pub enum AudioFileError {
#[error("could not complete CDN request: {0}")] #[error("could not complete CDN request: {0}")]
Cdn(hyper::Error), Cdn(#[from] hyper::Error),
#[error("channel was disconnected")]
Channel,
#[error("empty response")] #[error("empty response")]
Empty, Empty,
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("output file unavailable")]
Output,
#[error("error parsing response")] #[error("error parsing response")]
Parsing, Parsing,
#[error("mutex was poisoned")]
Poisoned,
#[error("could not complete API request: {0}")] #[error("could not complete API request: {0}")]
SpClient(#[from] SpClientError), SpClient(#[from] SpClientError),
#[error("streamer did not report progress")]
Timeout,
#[error("could not get CDN URL: {0}")] #[error("could not get CDN URL: {0}")]
Url(#[from] CdnUrlError), Url(#[from] CdnUrlError),
} }
@ -42,7 +54,7 @@ pub enum AudioFileError {
/// The minimum size of a block that is requested from the Spotify servers in one request. /// The minimum size of a block that is requested from the Spotify servers in one request.
/// This is the block size that is typically requested while doing a `seek()` on a file. /// This is the block size that is typically requested while doing a `seek()` on a file.
/// Note: smaller requests can happen if part of the block is downloaded already. /// Note: smaller requests can happen if part of the block is downloaded already.
pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 256; pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128;
/// The amount of data that is requested when initially opening a file. /// The amount of data that is requested when initially opening a file.
/// Note: if the file is opened to play from the beginning, the amount of data to /// Note: if the file is opened to play from the beginning, the amount of data to
@ -142,23 +154,32 @@ impl StreamLoaderController {
self.file_size == 0 self.file_size == 0
} }
pub fn range_available(&self, range: Range) -> bool { pub fn range_available(&self, range: Range) -> Result<bool, AudioFileError> {
if let Some(ref shared) = self.stream_shared { let available = if let Some(ref shared) = self.stream_shared {
let download_status = shared.download_status.lock().unwrap(); let download_status = shared
.download_status
.lock()
.map_err(|_| AudioFileError::Poisoned)?;
range.length range.length
<= download_status <= download_status
.downloaded .downloaded
.contained_length_from_value(range.start) .contained_length_from_value(range.start)
} else { } else {
range.length <= self.len() - range.start range.length <= self.len() - range.start
} };
Ok(available)
} }
pub fn range_to_end_available(&self) -> bool { pub fn range_to_end_available(&self) -> Result<bool, AudioFileError> {
self.stream_shared.as_ref().map_or(true, |shared| { match self.stream_shared {
let read_position = shared.read_position.load(atomic::Ordering::Relaxed); Some(ref shared) => {
self.range_available(Range::new(read_position, self.len() - read_position)) let read_position = shared.read_position.load(atomic::Ordering::Relaxed);
}) self.range_available(Range::new(read_position, self.len() - read_position))
}
None => Ok(true),
}
} }
pub fn ping_time(&self) -> Duration { pub fn ping_time(&self) -> Duration {
@ -179,7 +200,7 @@ impl StreamLoaderController {
self.send_stream_loader_command(StreamLoaderCommand::Fetch(range)); self.send_stream_loader_command(StreamLoaderCommand::Fetch(range));
} }
pub fn fetch_blocking(&self, mut range: Range) { pub fn fetch_blocking(&self, mut range: Range) -> AudioFileResult {
// signal the stream loader to tech a range of the file and block until it is loaded. // signal the stream loader to tech a range of the file and block until it is loaded.
// ensure the range is within the file's bounds. // ensure the range is within the file's bounds.
@ -192,7 +213,11 @@ impl StreamLoaderController {
self.fetch(range); self.fetch(range);
if let Some(ref shared) = self.stream_shared { if let Some(ref shared) = self.stream_shared {
let mut download_status = shared.download_status.lock().unwrap(); let mut download_status = shared
.download_status
.lock()
.map_err(|_| AudioFileError::Poisoned)?;
while range.length while range.length
> download_status > download_status
.downloaded .downloaded
@ -201,7 +226,7 @@ impl StreamLoaderController {
download_status = shared download_status = shared
.cond .cond
.wait_timeout(download_status, DOWNLOAD_TIMEOUT) .wait_timeout(download_status, DOWNLOAD_TIMEOUT)
.unwrap() .map_err(|_| AudioFileError::Timeout)?
.0; .0;
if range.length if range.length
> (download_status > (download_status
@ -215,6 +240,8 @@ impl StreamLoaderController {
} }
} }
} }
Ok(())
} }
pub fn fetch_next(&self, length: usize) { pub fn fetch_next(&self, length: usize) {
@ -223,17 +250,20 @@ impl StreamLoaderController {
start: shared.read_position.load(atomic::Ordering::Relaxed), start: shared.read_position.load(atomic::Ordering::Relaxed),
length, length,
}; };
self.fetch(range) self.fetch(range);
} }
} }
pub fn fetch_next_blocking(&self, length: usize) { pub fn fetch_next_blocking(&self, length: usize) -> AudioFileResult {
if let Some(ref shared) = self.stream_shared { match self.stream_shared {
let range = Range { Some(ref shared) => {
start: shared.read_position.load(atomic::Ordering::Relaxed), let range = Range {
length, start: shared.read_position.load(atomic::Ordering::Relaxed),
}; length,
self.fetch_blocking(range); };
self.fetch_blocking(range)
}
None => Ok(()),
} }
} }
@ -310,11 +340,9 @@ impl AudioFile {
let session_ = session.clone(); let session_ = session.clone();
session.spawn(complete_rx.map_ok(move |mut file| { session.spawn(complete_rx.map_ok(move |mut file| {
if let Some(cache) = session_.cache() { if let Some(cache) = session_.cache() {
if cache.file_path(file_id).is_some() { if cache.save_file(file_id, &mut file) {
cache.save_file(file_id, &mut file);
debug!("File {} cached to {:?}", file_id, cache.file(file_id)); debug!("File {} cached to {:?}", file_id, cache.file(file_id));
} }
debug!("Downloading file {} complete", file_id); debug!("Downloading file {} complete", file_id);
} }
})); }));
@ -322,8 +350,8 @@ impl AudioFile {
Ok(AudioFile::Streaming(streaming.await?)) Ok(AudioFile::Streaming(streaming.await?))
} }
pub fn get_stream_loader_controller(&self) -> StreamLoaderController { pub fn get_stream_loader_controller(&self) -> Result<StreamLoaderController, AudioFileError> {
match self { let controller = match self {
AudioFile::Streaming(ref stream) => StreamLoaderController { AudioFile::Streaming(ref stream) => StreamLoaderController {
channel_tx: Some(stream.stream_loader_command_tx.clone()), channel_tx: Some(stream.stream_loader_command_tx.clone()),
stream_shared: Some(stream.shared.clone()), stream_shared: Some(stream.shared.clone()),
@ -332,9 +360,11 @@ impl AudioFile {
AudioFile::Cached(ref file) => StreamLoaderController { AudioFile::Cached(ref file) => StreamLoaderController {
channel_tx: None, channel_tx: None,
stream_shared: None, stream_shared: None,
file_size: file.metadata().unwrap().len() as usize, file_size: file.metadata()?.len() as usize,
}, },
} };
Ok(controller)
} }
pub fn is_cached(&self) -> bool { pub fn is_cached(&self) -> bool {
@ -410,8 +440,8 @@ impl AudioFileStreaming {
read_position: AtomicUsize::new(0), read_position: AtomicUsize::new(0),
}); });
let write_file = NamedTempFile::new_in(session.config().tmp_dir.clone()).unwrap(); let write_file = NamedTempFile::new_in(session.config().tmp_dir.clone())?;
let read_file = write_file.reopen().unwrap(); let read_file = write_file.reopen()?;
let (stream_loader_command_tx, stream_loader_command_rx) = let (stream_loader_command_tx, stream_loader_command_rx) =
mpsc::unbounded_channel::<StreamLoaderCommand>(); mpsc::unbounded_channel::<StreamLoaderCommand>();
@ -444,7 +474,12 @@ impl Read for AudioFileStreaming {
let length = min(output.len(), self.shared.file_size - offset); let length = min(output.len(), self.shared.file_size - offset);
let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) { let length_to_request = match *(self
.shared
.download_strategy
.lock()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?)
{
DownloadStrategy::RandomAccess() => length, DownloadStrategy::RandomAccess() => length,
DownloadStrategy::Streaming() => { DownloadStrategy::Streaming() => {
// Due to the read-ahead stuff, we potentially request more than the actual request demanded. // Due to the read-ahead stuff, we potentially request more than the actual request demanded.
@ -468,14 +503,18 @@ impl Read for AudioFileStreaming {
let mut ranges_to_request = RangeSet::new(); let mut ranges_to_request = RangeSet::new();
ranges_to_request.add_range(&Range::new(offset, length_to_request)); ranges_to_request.add_range(&Range::new(offset, length_to_request));
let mut download_status = self.shared.download_status.lock().unwrap(); let mut download_status = self
.shared
.download_status
.lock()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?;
ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.downloaded);
ranges_to_request.subtract_range_set(&download_status.requested); ranges_to_request.subtract_range_set(&download_status.requested);
for &range in ranges_to_request.iter() { for &range in ranges_to_request.iter() {
self.stream_loader_command_tx self.stream_loader_command_tx
.send(StreamLoaderCommand::Fetch(range)) .send(StreamLoaderCommand::Fetch(range))
.unwrap(); .map_err(|_| io::Error::new(io::ErrorKind::Other, "tx channel is disconnected"))?;
} }
if length == 0 { if length == 0 {
@ -484,7 +523,12 @@ impl Read for AudioFileStreaming {
let mut download_message_printed = false; let mut download_message_printed = false;
while !download_status.downloaded.contains(offset) { while !download_status.downloaded.contains(offset) {
if let DownloadStrategy::Streaming() = *self.shared.download_strategy.lock().unwrap() { if let DownloadStrategy::Streaming() = *self
.shared
.download_strategy
.lock()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?
{
if !download_message_printed { 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)); 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_message_printed = true;
@ -494,7 +538,7 @@ impl Read for AudioFileStreaming {
.shared .shared
.cond .cond
.wait_timeout(download_status, DOWNLOAD_TIMEOUT) .wait_timeout(download_status, DOWNLOAD_TIMEOUT)
.unwrap() .map_err(|_| io::Error::new(io::ErrorKind::Other, "timeout acquiring mutex"))?
.0; .0;
} }
let available_length = download_status let available_length = download_status
@ -503,7 +547,7 @@ impl Read for AudioFileStreaming {
assert!(available_length > 0); assert!(available_length > 0);
drop(download_status); drop(download_status);
self.position = self.read_file.seek(SeekFrom::Start(offset as u64)).unwrap(); self.position = self.read_file.seek(SeekFrom::Start(offset as u64))?;
let read_len = min(length, available_length); let read_len = min(length, available_length);
let read_len = self.read_file.read(&mut output[..read_len])?; let read_len = self.read_file.read(&mut output[..read_len])?;

View file

@ -13,7 +13,10 @@ use librespot_core::session::Session;
use crate::range_set::{Range, RangeSet}; use crate::range_set::{Range, RangeSet};
use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand, StreamingRequest}; use super::{
AudioFileError, AudioFileResult, AudioFileShared, DownloadStrategy, StreamLoaderCommand,
StreamingRequest,
};
use super::{ use super::{
FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS,
MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR,
@ -33,7 +36,7 @@ async fn receive_data(
shared: Arc<AudioFileShared>, shared: Arc<AudioFileShared>,
file_data_tx: mpsc::UnboundedSender<ReceivedData>, file_data_tx: mpsc::UnboundedSender<ReceivedData>,
mut request: StreamingRequest, mut request: StreamingRequest,
) { ) -> AudioFileResult {
let requested_offset = request.offset; let requested_offset = request.offset;
let requested_length = request.length; let requested_length = request.length;
@ -97,7 +100,10 @@ async fn receive_data(
if request_length > 0 { if request_length > 0 {
let missing_range = Range::new(data_offset, request_length); let missing_range = Range::new(data_offset, request_length);
let mut download_status = shared.download_status.lock().unwrap(); let mut download_status = shared
.download_status
.lock()
.map_err(|_| AudioFileError::Poisoned)?;
download_status.requested.subtract_range(&missing_range); download_status.requested.subtract_range(&missing_range);
shared.cond.notify_all(); shared.cond.notify_all();
} }
@ -106,16 +112,23 @@ async fn receive_data(
.number_of_open_requests .number_of_open_requests
.fetch_sub(1, Ordering::SeqCst); .fetch_sub(1, Ordering::SeqCst);
if let Err(e) = result { match result {
error!( Ok(()) => {
"Error from streamer for range {} (+{}): {:?}", if request_length > 0 {
requested_offset, requested_length, e warn!(
); "Streamer for range {} (+{}) received less data from server than requested.",
} else if request_length > 0 { requested_offset, requested_length
warn!( );
"Streamer for range {} (+{}) received less data from server than requested.", }
requested_offset, requested_length Ok(())
); }
Err(e) => {
error!(
"Error from streamer for range {} (+{}): {:?}",
requested_offset, requested_length, e
);
Err(e.into())
}
} }
} }
@ -137,24 +150,21 @@ enum ControlFlow {
} }
impl AudioFileFetch { impl AudioFileFetch {
fn get_download_strategy(&mut self) -> DownloadStrategy { fn get_download_strategy(&mut self) -> Result<DownloadStrategy, AudioFileError> {
*(self.shared.download_strategy.lock().unwrap()) let strategy = self
.shared
.download_strategy
.lock()
.map_err(|_| AudioFileError::Poisoned)?;
Ok(*(strategy))
} }
fn download_range(&mut self, offset: usize, mut length: usize) { fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult {
if length < MINIMUM_DOWNLOAD_SIZE { if length < MINIMUM_DOWNLOAD_SIZE {
length = MINIMUM_DOWNLOAD_SIZE; length = MINIMUM_DOWNLOAD_SIZE;
} }
// ensure the values are within the bounds
if offset >= self.shared.file_size {
return;
}
if length == 0 {
return;
}
if offset + length > self.shared.file_size { if offset + length > self.shared.file_size {
length = self.shared.file_size - offset; length = self.shared.file_size - offset;
} }
@ -162,7 +172,11 @@ impl AudioFileFetch {
let mut ranges_to_request = RangeSet::new(); let mut ranges_to_request = RangeSet::new();
ranges_to_request.add_range(&Range::new(offset, length)); ranges_to_request.add_range(&Range::new(offset, length));
let mut download_status = self.shared.download_status.lock().unwrap(); let mut download_status = self
.shared
.download_status
.lock()
.map_err(|_| AudioFileError::Poisoned)?;
ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.downloaded);
ranges_to_request.subtract_range_set(&download_status.requested); ranges_to_request.subtract_range_set(&download_status.requested);
@ -205,9 +219,15 @@ impl AudioFileFetch {
} }
} }
} }
Ok(())
} }
fn pre_fetch_more_data(&mut self, bytes: usize, max_requests_to_send: usize) { fn pre_fetch_more_data(
&mut self,
bytes: usize,
max_requests_to_send: usize,
) -> AudioFileResult {
let mut bytes_to_go = bytes; let mut bytes_to_go = bytes;
let mut requests_to_go = max_requests_to_send; let mut requests_to_go = max_requests_to_send;
@ -216,7 +236,11 @@ impl AudioFileFetch {
let mut missing_data = RangeSet::new(); let mut missing_data = RangeSet::new();
missing_data.add_range(&Range::new(0, self.shared.file_size)); missing_data.add_range(&Range::new(0, self.shared.file_size));
{ {
let download_status = self.shared.download_status.lock().unwrap(); let download_status = self
.shared
.download_status
.lock()
.map_err(|_| AudioFileError::Poisoned)?;
missing_data.subtract_range_set(&download_status.downloaded); missing_data.subtract_range_set(&download_status.downloaded);
missing_data.subtract_range_set(&download_status.requested); missing_data.subtract_range_set(&download_status.requested);
} }
@ -234,7 +258,7 @@ impl AudioFileFetch {
let range = tail_end.get_range(0); let range = tail_end.get_range(0);
let offset = range.start; let offset = range.start;
let length = min(range.length, bytes_to_go); let length = min(range.length, bytes_to_go);
self.download_range(offset, length); self.download_range(offset, length)?;
requests_to_go -= 1; requests_to_go -= 1;
bytes_to_go -= length; bytes_to_go -= length;
} else if !missing_data.is_empty() { } else if !missing_data.is_empty() {
@ -242,20 +266,20 @@ impl AudioFileFetch {
let range = missing_data.get_range(0); let range = missing_data.get_range(0);
let offset = range.start; let offset = range.start;
let length = min(range.length, bytes_to_go); let length = min(range.length, bytes_to_go);
self.download_range(offset, length); self.download_range(offset, length)?;
requests_to_go -= 1; requests_to_go -= 1;
bytes_to_go -= length; bytes_to_go -= length;
} else { } else {
return; break;
} }
} }
Ok(())
} }
fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow { fn handle_file_data(&mut self, data: ReceivedData) -> Result<ControlFlow, AudioFileError> {
match data { match data {
ReceivedData::ResponseTime(response_time) => { ReceivedData::ResponseTime(response_time) => {
trace!("Ping time estimated as: {} ms", response_time.as_millis());
// prune old response times. Keep at most two so we can push a third. // prune old response times. Keep at most two so we can push a third.
while self.network_response_times.len() >= 3 { while self.network_response_times.len() >= 3 {
self.network_response_times.remove(0); self.network_response_times.remove(0);
@ -276,24 +300,27 @@ impl AudioFileFetch {
_ => unreachable!(), _ => unreachable!(),
}; };
trace!("Ping time estimated as: {} ms", ping_time.as_millis());
// store our new estimate for everyone to see // store our new estimate for everyone to see
self.shared self.shared
.ping_time_ms .ping_time_ms
.store(ping_time.as_millis() as usize, Ordering::Relaxed); .store(ping_time.as_millis() as usize, Ordering::Relaxed);
} }
ReceivedData::Data(data) => { ReceivedData::Data(data) => {
self.output match self.output.as_mut() {
.as_mut() Some(output) => {
.unwrap() output.seek(SeekFrom::Start(data.offset as u64))?;
.seek(SeekFrom::Start(data.offset as u64)) output.write_all(data.data.as_ref())?;
.unwrap(); }
self.output None => return Err(AudioFileError::Output),
.as_mut() }
.unwrap()
.write_all(data.data.as_ref())
.unwrap();
let mut download_status = self.shared.download_status.lock().unwrap(); let mut download_status = self
.shared
.download_status
.lock()
.map_err(|_| AudioFileError::Poisoned)?;
let received_range = Range::new(data.offset, data.data.len()); let received_range = Range::new(data.offset, data.data.len());
download_status.downloaded.add_range(&received_range); download_status.downloaded.add_range(&received_range);
@ -305,36 +332,50 @@ impl AudioFileFetch {
drop(download_status); drop(download_status);
if full { if full {
self.finish(); self.finish()?;
return ControlFlow::Break; return Ok(ControlFlow::Break);
} }
} }
} }
ControlFlow::Continue
Ok(ControlFlow::Continue)
} }
fn handle_stream_loader_command(&mut self, cmd: StreamLoaderCommand) -> ControlFlow { fn handle_stream_loader_command(
&mut self,
cmd: StreamLoaderCommand,
) -> Result<ControlFlow, AudioFileError> {
match cmd { match cmd {
StreamLoaderCommand::Fetch(request) => { StreamLoaderCommand::Fetch(request) => {
self.download_range(request.start, request.length); self.download_range(request.start, request.length)?;
} }
StreamLoaderCommand::RandomAccessMode() => { StreamLoaderCommand::RandomAccessMode() => {
*(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::RandomAccess(); *(self
.shared
.download_strategy
.lock()
.map_err(|_| AudioFileError::Poisoned)?) = DownloadStrategy::RandomAccess();
} }
StreamLoaderCommand::StreamMode() => { StreamLoaderCommand::StreamMode() => {
*(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::Streaming(); *(self
.shared
.download_strategy
.lock()
.map_err(|_| AudioFileError::Poisoned)?) = DownloadStrategy::Streaming();
} }
StreamLoaderCommand::Close() => return ControlFlow::Break, StreamLoaderCommand::Close() => return Ok(ControlFlow::Break),
} }
ControlFlow::Continue Ok(ControlFlow::Continue)
} }
fn finish(&mut self) { fn finish(&mut self) -> AudioFileResult {
let mut output = self.output.take().unwrap(); let mut output = self.output.take().ok_or(AudioFileError::Output)?;
let complete_tx = self.complete_tx.take().unwrap(); let complete_tx = self.complete_tx.take().ok_or(AudioFileError::Output)?;
output.seek(SeekFrom::Start(0)).unwrap(); output.seek(SeekFrom::Start(0))?;
let _ = complete_tx.send(output); complete_tx
.send(output)
.map_err(|_| AudioFileError::Channel)
} }
} }
@ -345,7 +386,7 @@ pub(super) async fn audio_file_fetch(
output: NamedTempFile, output: NamedTempFile,
mut stream_loader_command_rx: mpsc::UnboundedReceiver<StreamLoaderCommand>, mut stream_loader_command_rx: mpsc::UnboundedReceiver<StreamLoaderCommand>,
complete_tx: oneshot::Sender<NamedTempFile>, complete_tx: oneshot::Sender<NamedTempFile>,
) { ) -> AudioFileResult {
let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel(); let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel();
{ {
@ -353,7 +394,10 @@ pub(super) async fn audio_file_fetch(
initial_request.offset, initial_request.offset,
initial_request.offset + initial_request.length, initial_request.offset + initial_request.length,
); );
let mut download_status = shared.download_status.lock().unwrap(); let mut download_status = shared
.download_status
.lock()
.map_err(|_| AudioFileError::Poisoned)?;
download_status.requested.add_range(&requested_range); download_status.requested.add_range(&requested_range);
} }
@ -376,25 +420,39 @@ pub(super) async fn audio_file_fetch(
loop { loop {
tokio::select! { tokio::select! {
cmd = stream_loader_command_rx.recv() => { cmd = stream_loader_command_rx.recv() => {
if cmd.map_or(true, |cmd| fetch.handle_stream_loader_command(cmd) == ControlFlow::Break) { match cmd {
break; Some(cmd) => {
if fetch.handle_stream_loader_command(cmd)? == ControlFlow::Break {
break;
}
}
None => break,
}
} }
}, data = file_data_rx.recv() => {
data = file_data_rx.recv() => { match data {
if data.map_or(true, |data| fetch.handle_file_data(data) == ControlFlow::Break) { Some(data) => {
break; if fetch.handle_file_data(data)? == ControlFlow::Break {
break;
}
}
None => break,
} }
} }
} }
if fetch.get_download_strategy() == DownloadStrategy::Streaming() { if fetch.get_download_strategy()? == DownloadStrategy::Streaming() {
let number_of_open_requests = let number_of_open_requests =
fetch.shared.number_of_open_requests.load(Ordering::SeqCst); fetch.shared.number_of_open_requests.load(Ordering::SeqCst);
if number_of_open_requests < MAX_PREFETCH_REQUESTS { if number_of_open_requests < MAX_PREFETCH_REQUESTS {
let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests; let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests;
let bytes_pending: usize = { let bytes_pending: usize = {
let download_status = fetch.shared.download_status.lock().unwrap(); let download_status = fetch
.shared
.download_status
.lock()
.map_err(|_| AudioFileError::Poisoned)?;
download_status download_status
.requested .requested
.minus(&download_status.downloaded) .minus(&download_status.downloaded)
@ -418,9 +476,11 @@ pub(super) async fn audio_file_fetch(
fetch.pre_fetch_more_data( fetch.pre_fetch_more_data(
desired_pending_bytes - bytes_pending, desired_pending_bytes - bytes_pending,
max_requests_to_send, max_requests_to_send,
); )?;
} }
} }
} }
} }
Ok(())
} }

View file

@ -7,7 +7,7 @@ mod fetch;
mod range_set; mod range_set;
pub use decrypt::AudioDecrypt; pub use decrypt::AudioDecrypt;
pub use fetch::{AudioFile, StreamLoaderController}; pub use fetch::{AudioFile, AudioFileError, StreamLoaderController};
pub use fetch::{ pub use fetch::{
READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK,
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,

View file

@ -10,7 +10,7 @@ pub struct Range {
impl fmt::Display for Range { impl fmt::Display for Range {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
return write!(f, "[{}, {}]", self.start, self.start + self.length - 1); write!(f, "[{}, {}]", self.start, self.start + self.length - 1)
} }
} }
@ -24,16 +24,16 @@ impl Range {
} }
} }
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct RangeSet { pub struct RangeSet {
ranges: Vec<Range>, ranges: Vec<Range>,
} }
impl fmt::Display for RangeSet { impl fmt::Display for RangeSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "(").unwrap(); write!(f, "(")?;
for range in self.ranges.iter() { for range in self.ranges.iter() {
write!(f, "{}", range).unwrap(); write!(f, "{}", range)?;
} }
write!(f, ")") write!(f, ")")
} }

View file

@ -350,7 +350,7 @@ impl Cache {
} }
} }
pub fn file_path(&self, file: FileId) -> Option<PathBuf> { fn file_path(&self, file: FileId) -> Option<PathBuf> {
self.audio_location.as_ref().map(|location| { self.audio_location.as_ref().map(|location| {
let name = file.to_base16(); let name = file.to_base16();
let mut path = location.join(&name[0..2]); let mut path = location.join(&name[0..2]);
@ -377,24 +377,22 @@ impl Cache {
} }
} }
pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) { pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) -> bool {
let path = if let Some(path) = self.file_path(file) { if let Some(path) = self.file_path(file) {
path if let Some(parent) = path.parent() {
} else { if let Ok(size) = fs::create_dir_all(parent)
return; .and_then(|_| File::create(&path))
}; .and_then(|mut file| io::copy(contents, &mut file))
let parent = path.parent().unwrap(); {
if let Some(limiter) = self.size_limiter.as_deref() {
let result = fs::create_dir_all(parent) limiter.add(&path, size);
.and_then(|_| File::create(&path)) limiter.prune();
.and_then(|mut file| io::copy(contents, &mut file)); }
return true;
if let Ok(size) = result { }
if let Some(limiter) = self.size_limiter.as_deref() {
limiter.add(&path, size);
limiter.prune();
} }
} }
false
} }
pub fn remove_file(&self, file: FileId) -> Result<(), RemoveFileError> { pub fn remove_file(&self, file: FileId) -> Result<(), RemoveFileError> {

View file

@ -80,7 +80,7 @@ impl CdnUrl {
return Err(CdnUrlError::Empty); return Err(CdnUrlError::Empty);
} }
// remove expired URLs until the first one is current, or none are left // prune expired URLs until the first one is current, or none are left
let now = Local::now(); let now = Local::now();
while !self.urls.is_empty() { while !self.urls.is_empty() {
let maybe_expiring = self.urls[0].1; let maybe_expiring = self.urls[0].1;

View file

@ -14,7 +14,9 @@ use url::Url;
use std::env::consts::OS; use std::env::consts::OS;
use crate::version::{SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING}; use crate::version::{
FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING,
};
pub struct HttpClient { pub struct HttpClient {
user_agent: HeaderValue, user_agent: HeaderValue,
@ -64,8 +66,8 @@ impl HttpClient {
let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| { let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| {
error!("Invalid user agent <{}>: {}", user_agent_str, err); error!("Invalid user agent <{}>: {}", user_agent_str, err);
error!("Parts of the API will probably not work. Please report this as a bug."); error!("Please report this as a bug.");
HeaderValue::from_static("") HeaderValue::from_static(FALLBACK_USER_AGENT)
}); });
// configuring TLS is expensive and should be done once per process // configuring TLS is expensive and should be done once per process

View file

@ -21,3 +21,6 @@ pub const SPOTIFY_VERSION: u64 = 117300517;
/// The protocol version of the Spotify mobile app. /// The protocol version of the Spotify mobile app.
pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84"; pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84";
/// The user agent to fall back to, if one could not be determined dynamically.
pub const FALLBACK_USER_AGENT: &str = "Spotify/117300517 Linux/0 (librespot)";

View file

@ -10,9 +10,10 @@ use std::{mem, thread};
use byteorder::{LittleEndian, ReadBytesExt}; use byteorder::{LittleEndian, ReadBytesExt};
use futures_util::stream::futures_unordered::FuturesUnordered; use futures_util::stream::futures_unordered::FuturesUnordered;
use futures_util::{future, StreamExt, TryFutureExt}; use futures_util::{future, StreamExt, TryFutureExt};
use thiserror::Error;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController}; use crate::audio::{AudioDecrypt, AudioFile, AudioFileError, StreamLoaderController};
use crate::audio::{ use crate::audio::{
READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK,
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,
@ -32,6 +33,14 @@ use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND};
const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
pub const DB_VOLTAGE_RATIO: f64 = 20.0; pub const DB_VOLTAGE_RATIO: f64 = 20.0;
pub type PlayerResult = Result<(), PlayerError>;
#[derive(Debug, Error)]
pub enum PlayerError {
#[error("audio file error: {0}")]
AudioFile(#[from] AudioFileError),
}
pub struct Player { pub struct Player {
commands: Option<mpsc::UnboundedSender<PlayerCommand>>, commands: Option<mpsc::UnboundedSender<PlayerCommand>>,
thread_handle: Option<thread::JoinHandle<()>>, thread_handle: Option<thread::JoinHandle<()>>,
@ -216,6 +225,17 @@ pub struct NormalisationData {
album_peak: f32, album_peak: f32,
} }
impl Default for NormalisationData {
fn default() -> Self {
Self {
track_gain_db: 0.0,
track_peak: 1.0,
album_gain_db: 0.0,
album_peak: 1.0,
}
}
}
impl NormalisationData { impl NormalisationData {
fn parse_from_file<T: Read + Seek>(mut file: T) -> io::Result<NormalisationData> { fn parse_from_file<T: Read + Seek>(mut file: T) -> io::Result<NormalisationData> {
const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144;
@ -698,19 +718,20 @@ impl PlayerTrackLoader {
} }
fn stream_data_rate(&self, format: AudioFileFormat) -> usize { fn stream_data_rate(&self, format: AudioFileFormat) -> usize {
match format { let kbps = match format {
AudioFileFormat::OGG_VORBIS_96 => 12 * 1024, AudioFileFormat::OGG_VORBIS_96 => 12,
AudioFileFormat::OGG_VORBIS_160 => 20 * 1024, AudioFileFormat::OGG_VORBIS_160 => 20,
AudioFileFormat::OGG_VORBIS_320 => 40 * 1024, AudioFileFormat::OGG_VORBIS_320 => 40,
AudioFileFormat::MP3_256 => 32 * 1024, AudioFileFormat::MP3_256 => 32,
AudioFileFormat::MP3_320 => 40 * 1024, AudioFileFormat::MP3_320 => 40,
AudioFileFormat::MP3_160 => 20 * 1024, AudioFileFormat::MP3_160 => 20,
AudioFileFormat::MP3_96 => 12 * 1024, AudioFileFormat::MP3_96 => 12,
AudioFileFormat::MP3_160_ENC => 20 * 1024, AudioFileFormat::MP3_160_ENC => 20,
AudioFileFormat::AAC_24 => 3 * 1024, AudioFileFormat::AAC_24 => 3,
AudioFileFormat::AAC_48 => 6 * 1024, AudioFileFormat::AAC_48 => 6,
AudioFileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average AudioFileFormat::FLAC_FLAC => 112, // assume 900 kbit/s on average
} };
kbps * 1024
} }
async fn load_track( async fn load_track(
@ -805,9 +826,10 @@ impl PlayerTrackLoader {
return None; return None;
} }
}; };
let is_cached = encrypted_file.is_cached(); let is_cached = encrypted_file.is_cached();
let stream_loader_controller = encrypted_file.get_stream_loader_controller(); let stream_loader_controller = encrypted_file.get_stream_loader_controller().ok()?;
if play_from_beginning { if play_from_beginning {
// No need to seek -> we stream from the beginning // No need to seek -> we stream from the beginning
@ -830,13 +852,8 @@ impl PlayerTrackLoader {
let normalisation_data = match NormalisationData::parse_from_file(&mut decrypted_file) { let normalisation_data = match NormalisationData::parse_from_file(&mut decrypted_file) {
Ok(data) => data, Ok(data) => data,
Err(_) => { Err(_) => {
warn!("Unable to extract normalisation data, using default value."); warn!("Unable to extract normalisation data, using default values.");
NormalisationData { NormalisationData::default()
track_gain_db: 0.0,
track_peak: 1.0,
album_gain_db: 0.0,
album_peak: 1.0,
}
} }
}; };
@ -929,7 +946,9 @@ impl Future for PlayerInternal {
}; };
if let Some(cmd) = cmd { if let Some(cmd) = cmd {
self.handle_command(cmd); if let Err(e) = self.handle_command(cmd) {
error!("Error handling command: {}", e);
}
} }
// Handle loading of a new track to play // Handle loading of a new track to play
@ -1109,7 +1128,9 @@ impl Future for PlayerInternal {
if (!*suggested_to_preload_next_track) if (!*suggested_to_preload_next_track)
&& ((duration_ms as i64 - Self::position_pcm_to_ms(stream_position_pcm) as i64) && ((duration_ms as i64 - Self::position_pcm_to_ms(stream_position_pcm) as i64)
< PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64) < PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64)
&& stream_loader_controller.range_to_end_available() && stream_loader_controller
.range_to_end_available()
.unwrap_or(false)
{ {
*suggested_to_preload_next_track = true; *suggested_to_preload_next_track = true;
self.send_event(PlayerEvent::TimeToPreloadNextTrack { self.send_event(PlayerEvent::TimeToPreloadNextTrack {
@ -1785,7 +1806,7 @@ impl PlayerInternal {
} }
} }
fn handle_command_seek(&mut self, position_ms: u32) { fn handle_command_seek(&mut self, position_ms: u32) -> PlayerResult {
if let Some(stream_loader_controller) = self.state.stream_loader_controller() { if let Some(stream_loader_controller) = self.state.stream_loader_controller() {
stream_loader_controller.set_random_access_mode(); stream_loader_controller.set_random_access_mode();
} }
@ -1818,7 +1839,7 @@ impl PlayerInternal {
} }
// ensure we have a bit of a buffer of downloaded data // ensure we have a bit of a buffer of downloaded data
self.preload_data_before_playback(); self.preload_data_before_playback()?;
if let PlayerState::Playing { if let PlayerState::Playing {
track_id, track_id,
@ -1851,11 +1872,13 @@ impl PlayerInternal {
duration_ms, duration_ms,
}); });
} }
Ok(())
} }
fn handle_command(&mut self, cmd: PlayerCommand) { fn handle_command(&mut self, cmd: PlayerCommand) -> PlayerResult {
debug!("command={:?}", cmd); debug!("command={:?}", cmd);
match cmd { let result = match cmd {
PlayerCommand::Load { PlayerCommand::Load {
track_id, track_id,
play_request_id, play_request_id,
@ -1865,7 +1888,7 @@ impl PlayerInternal {
PlayerCommand::Preload { track_id } => self.handle_command_preload(track_id), PlayerCommand::Preload { track_id } => self.handle_command_preload(track_id),
PlayerCommand::Seek(position_ms) => self.handle_command_seek(position_ms), PlayerCommand::Seek(position_ms) => self.handle_command_seek(position_ms)?,
PlayerCommand::Play => self.handle_play(), PlayerCommand::Play => self.handle_play(),
@ -1884,7 +1907,9 @@ impl PlayerInternal {
PlayerCommand::SetAutoNormaliseAsAlbum(setting) => { PlayerCommand::SetAutoNormaliseAsAlbum(setting) => {
self.auto_normalise_as_album = setting self.auto_normalise_as_album = setting
} }
} };
Ok(result)
} }
fn send_event(&mut self, event: PlayerEvent) { fn send_event(&mut self, event: PlayerEvent) {
@ -1928,7 +1953,7 @@ impl PlayerInternal {
result_rx.map_err(|_| ()) result_rx.map_err(|_| ())
} }
fn preload_data_before_playback(&mut self) { fn preload_data_before_playback(&mut self) -> Result<(), PlayerError> {
if let PlayerState::Playing { if let PlayerState::Playing {
bytes_per_second, bytes_per_second,
ref mut stream_loader_controller, ref mut stream_loader_controller,
@ -1951,7 +1976,11 @@ impl PlayerInternal {
* bytes_per_second as f32) as usize, * bytes_per_second as f32) as usize,
(READ_AHEAD_BEFORE_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, (READ_AHEAD_BEFORE_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize,
); );
stream_loader_controller.fetch_next_blocking(wait_for_data_length); stream_loader_controller
.fetch_next_blocking(wait_for_data_length)
.map_err(|e| e.into())
} else {
Ok(())
} }
} }
} }