Merge branch 'dev' into rodiojack-backend

This commit is contained in:
Sasha Hilton 2021-02-10 01:07:02 +00:00
parent 52438b1cc2
commit aad4dba8a8
12 changed files with 143 additions and 107 deletions

View file

@ -41,7 +41,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
toolchain: toolchain:
- 1.40.0 # MSRV (Minimum supported rust version) - 1.42.0 # MSRV (Minimum supported rust version)
- stable - stable
- beta - beta
experimental: [false] experimental: [false]

View file

@ -1,74 +0,0 @@
language: rust
rust:
- 1.42.0
- stable
- beta
- nightly
# Need to cache the whole `.cargo` directory to keep .crates.toml for
# cargo-update to work
cache:
directories:
- /home/travis/.cargo
# But don't cache the cargo registry
before_cache:
- rm -rf /home/travis/.cargo/registry
matrix:
# Performance tweak
fast_finish: true
# Ignore failures in nightly, not ideal, but necessary
allow_failures:
- rust: nightly
# Only run the formatting check for stable
include:
- name: 'Rust: format check'
rust: stable
install:
- rustup component add rustfmt
script:
- cargo fmt --verbose --all -- --check
addons:
apt:
packages:
- gcc-arm-linux-gnueabihf
- libc6-dev-armhf-cross
- libpulse-dev
- portaudio19-dev
- libasound2-dev
- libsdl2-dev
- gstreamer1.0-dev
- libgstreamer-plugins-base1.0-dev
before_script:
- mkdir -p ~/.cargo
- echo '[target.armv7-unknown-linux-gnueabihf]' > ~/.cargo/config
- echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config
- rustup target add armv7-unknown-linux-gnueabihf
script:
- cargo build --locked --no-default-features
- cargo build --locked --examples
- cargo build --locked --no-default-features --features "with-tremor"
- cargo build --locked --no-default-features --features "with-vorbis"
- cargo build --locked --no-default-features --features "alsa-backend"
- cargo build --locked --no-default-features --features "portaudio-backend"
- cargo build --locked --no-default-features --features "pulseaudio-backend"
- cargo build --locked --no-default-features --features "jackaudio-backend"
- cargo build --locked --no-default-features --features "rodiojack-backend"
- cargo build --locked --no-default-features --features "rodio-backend"
- cargo build --locked --no-default-features --features "sdl-backend"
- cargo build --locked --no-default-features --features "gstreamer-backend"
- cargo build --locked --no-default-features --target armv7-unknown-linux-gnueabihf
notifications:
email: false
webhooks:
urls:
- https://webhooks.gitter.im/e/780b178b15811059752e
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: never # options: [always|never|change] default: always

View file

@ -1,5 +1,4 @@
[![Build Status](https://img.shields.io/github/workflow/status/librespot-org/librespot/test/dev)](https://github.com/librespot-org/librespot/actions) [![Build Status](https://github.com/librespot-org/librespot/workflows/test/badge.svg)](https://github.com/librespot-org/librespot/actions)
[![Build Status](https://travis-ci.org/librespot-org/librespot.svg?branch=dev)](https://travis-ci.org/librespot-org/librespot)
[![Gitter chat](https://badges.gitter.im/librespot-org/librespot.png)](https://gitter.im/librespot-org/spotify-connect-resources) [![Gitter chat](https://badges.gitter.im/librespot-org/librespot.png)](https://gitter.im/librespot-org/spotify-connect-resources)
[![Crates.io](https://img.shields.io/crates/v/librespot.svg)](https://crates.io/crates/librespot) [![Crates.io](https://img.shields.io/crates/v/librespot.svg)](https://crates.io/crates/librespot)
@ -21,7 +20,7 @@ As the origin by [plietar](https://github.com/plietar/) is no longer actively ma
# Documentation # Documentation
Documentation is currently a work in progress, contributions are welcome! Documentation is currently a work in progress, contributions are welcome!
There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder, There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder,
[COMPILING.md](https://github.com/librespot-org/librespot/blob/master/COMPILING.md) contains detailed instructions on setting up a development environment, and compiling librespot. More general usage and compilation information is available on the [wiki](https://github.com/librespot-org/librespot/wiki). [COMPILING.md](https://github.com/librespot-org/librespot/blob/master/COMPILING.md) contains detailed instructions on setting up a development environment, and compiling librespot. More general usage and compilation information is available on the [wiki](https://github.com/librespot-org/librespot/wiki).
[CONTRIBUTING.md](https://github.com/librespot-org/librespot/blob/master/CONTRIBUTING.md) also contains our contributing guidelines. [CONTRIBUTING.md](https://github.com/librespot-org/librespot/blob/master/CONTRIBUTING.md) also contains our contributing guidelines.
@ -32,7 +31,7 @@ If you wish to learn more about how librespot works overall, the best way is to
If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, eg. the Spotify URI of the song that caused the crash. If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, eg. the Spotify URI of the song that caused the crash.
# Building # Building
A quick walk through of the build process is outlined here, while a detailed compilation guide can be found [here](https://github.com/librespot-org/librespot/blob/master/COMPILING.md). A quick walk through of the build process is outlined here, while a detailed compilation guide can be found [here](https://github.com/librespot-org/librespot/blob/master/COMPILING.md).
## Additional Dependencies ## Additional Dependencies
We recently switched to using [Rodio](https://github.com/tomaka/rodio) for audio playback by default, hence for macOS and Windows, you should just be able to clone and build librespot (with the command below). We recently switched to using [Rodio](https://github.com/tomaka/rodio) for audio playback by default, hence for macOS and Windows, you should just be able to clone and build librespot (with the command below).
@ -112,4 +111,3 @@ functionality.
- [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot. - [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot.
- [ncspot](https://github.com/hrkfdn/ncspot) - Cross-platform ncurses Spotify client. - [ncspot](https://github.com/hrkfdn/ncspot) - Cross-platform ncurses Spotify client.
- [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot. - [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot.

View file

@ -459,6 +459,13 @@ impl AudioFile {
} }
} }
} }
pub fn is_cached(&self) -> bool {
match self {
AudioFile::Cached { .. } => true,
_ => false,
}
}
} }
fn request_range(session: &Session, file: FileId, offset: usize, length: usize) -> Channel { fn request_range(session: &Session, file: FileId, offset: usize, length: usize) -> Channel {

View file

@ -75,7 +75,7 @@ impl Discovery {
"status": 101, "status": 101,
"statusString": "ERROR-OK", "statusString": "ERROR-OK",
"spotifyError": 0, "spotifyError": 0,
"version": "2.1.0", "version": "2.7.1",
"deviceID": (self.0.device_id), "deviceID": (self.0.device_id),
"remoteName": (self.0.config.name), "remoteName": (self.0.config.name),
"activeUser": "", "activeUser": "",
@ -85,6 +85,9 @@ impl Discovery {
"accountReq": "PREMIUM", "accountReq": "PREMIUM",
"brandDisplayName": "librespot", "brandDisplayName": "librespot",
"modelDisplayName": "librespot", "modelDisplayName": "librespot",
"resolverVersion": "0",
"groupStatus": "NONE",
"voiceSupport": "NO",
}); });
let body = result.to_string(); let body = result.to_string();

View file

@ -7,6 +7,7 @@ use sha1::{Digest, Sha1};
use std::io::{self, Read}; use std::io::{self, Read};
use crate::protocol::authentication::AuthenticationType; use crate::protocol::authentication::AuthenticationType;
use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Credentials { pub struct Credentials {
@ -164,3 +165,37 @@ pub fn get_credentials<F: FnOnce(&String) -> String>(
(None, _, None) => None, (None, _, None) => None,
} }
} }
error_chain! {
types {
AuthenticationError, AuthenticationErrorKind, AuthenticationResultExt, AuthenticationResult;
}
foreign_links {
Io(::std::io::Error);
}
errors {
BadCredentials {
description("Bad credentials")
display("Authentication failed with error: Bad credentials")
}
PremiumAccountRequired {
description("Premium account required")
display("Authentication failed with error: Premium account required")
}
}
}
impl From<APLoginFailed> for AuthenticationError {
fn from(login_failure: APLoginFailed) -> Self {
let error_code = login_failure.get_error_code();
match error_code {
ErrorCode::BadCredentials => Self::from_kind(AuthenticationErrorKind::BadCredentials),
ErrorCode::PremiumAccountRequired => {
Self::from_kind(AuthenticationErrorKind::PremiumAccountRequired)
}
_ => format!("Authentication failed with error: {:?}", error_code).into(),
}
}
}

View file

@ -162,4 +162,17 @@ impl Cache {
warn!("Cannot save file to cache: {}", e) warn!("Cannot save file to cache: {}", e)
} }
} }
pub fn remove_file(&self, file: FileId) -> bool {
if let Some(path) = self.file_path(file) {
if let Err(err) = fs::remove_file(path) {
warn!("Unable to remove file from cache: {}", err);
false
} else {
true
}
} else {
false
}
}
} }

View file

@ -36,6 +36,16 @@ pub enum DeviceType {
AVR = 6, AVR = 6,
STB = 7, STB = 7,
AudioDongle = 8, AudioDongle = 8,
GameConsole = 9,
CastAudio = 10,
CastVideo = 11,
Automobile = 12,
Smartwatch = 13,
Chromebook = 14,
UnknownSpotify = 100,
CarThing = 101,
Observer = 102,
HomeThing = 103,
} }
impl FromStr for DeviceType { impl FromStr for DeviceType {
@ -51,6 +61,14 @@ impl FromStr for DeviceType {
"avr" => Ok(AVR), "avr" => Ok(AVR),
"stb" => Ok(STB), "stb" => Ok(STB),
"audiodongle" => Ok(AudioDongle), "audiodongle" => Ok(AudioDongle),
"gameconsole" => Ok(GameConsole),
"castaudio" => Ok(CastAudio),
"castvideo" => Ok(CastVideo),
"automobile" => Ok(Automobile),
"smartwatch" => Ok(Smartwatch),
"chromebook" => Ok(Chromebook),
"carthing" => Ok(CarThing),
"homething" => Ok(HomeThing),
_ => Err(()), _ => Err(()),
} }
} }
@ -69,6 +87,16 @@ impl fmt::Display for DeviceType {
AVR => f.write_str("AVR"), AVR => f.write_str("AVR"),
STB => f.write_str("STB"), STB => f.write_str("STB"),
AudioDongle => f.write_str("AudioDongle"), AudioDongle => f.write_str("AudioDongle"),
GameConsole => f.write_str("GameConsole"),
CastAudio => f.write_str("CastAudio"),
CastVideo => f.write_str("CastVideo"),
Automobile => f.write_str("Automobile"),
Smartwatch => f.write_str("Smartwatch"),
Chromebook => f.write_str("Chromebook"),
UnknownSpotify => f.write_str("UnknownSpotify"),
CarThing => f.write_str("CarThing"),
Observer => f.write_str("Observer"),
HomeThing => f.write_str("HomeThing"),
} }
} }
} }

View file

@ -13,7 +13,7 @@ use tokio_core::net::TcpStream;
use tokio_core::reactor::Handle; use tokio_core::reactor::Handle;
use url::Url; use url::Url;
use crate::authentication::Credentials; use crate::authentication::{AuthenticationError, Credentials};
use crate::version; use crate::version;
use crate::proxytunnel; use crate::proxytunnel;
@ -66,7 +66,7 @@ pub fn authenticate(
transport: Transport, transport: Transport,
credentials: Credentials, credentials: Credentials,
device_id: String, device_id: String,
) -> Box<dyn Future<Item = (Transport, Credentials), Error = io::Error>> { ) -> Box<dyn Future<Item = (Transport, Credentials), Error = AuthenticationError>> {
use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
use crate::protocol::keyexchange::APLoginFailed; use crate::protocol::keyexchange::APLoginFailed;
@ -101,6 +101,7 @@ pub fn authenticate(
transport transport
.send((cmd, data)) .send((cmd, data))
.and_then(|transport| transport.into_future().map_err(|(err, _stream)| err)) .and_then(|transport| transport.into_future().map_err(|(err, _stream)| err))
.map_err(|io_err| io_err.into())
.and_then(|(packet, transport)| match packet { .and_then(|(packet, transport)| match packet {
Some((0xac, data)) => { Some((0xac, data)) => {
let welcome_data: APWelcome = let welcome_data: APWelcome =
@ -118,10 +119,7 @@ pub fn authenticate(
Some((0xad, data)) => { Some((0xad, data)) => {
let error_data: APLoginFailed = let error_data: APLoginFailed =
protobuf::parse_from_bytes(data.as_ref()).unwrap(); protobuf::parse_from_bytes(data.as_ref()).unwrap();
panic!( Err(error_data.into())
"Authentication failed with reason: {:?}",
error_data.get_error_code()
)
} }
Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd), Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd),

View file

@ -19,6 +19,8 @@ use crate::config::SessionConfig;
use crate::connection; use crate::connection;
use crate::mercury::MercuryManager; use crate::mercury::MercuryManager;
pub use crate::authentication::{AuthenticationError, AuthenticationErrorKind};
struct SessionData { struct SessionData {
country: String, country: String,
time_delta: i64, time_delta: i64,
@ -53,16 +55,18 @@ impl Session {
credentials: Credentials, credentials: Credentials,
cache: Option<Cache>, cache: Option<Cache>,
handle: Handle, handle: Handle,
) -> Box<dyn Future<Item = Session, Error = io::Error>> { ) -> Box<dyn Future<Item = Session, Error = AuthenticationError>> {
let access_point = let access_point =
apresolve_or_fallback::<io::Error>(&handle, &config.proxy, &config.ap_port); apresolve_or_fallback::<io::Error>(&handle, &config.proxy, &config.ap_port);
let handle_ = handle.clone(); let handle_ = handle.clone();
let proxy = config.proxy.clone(); let proxy = config.proxy.clone();
let connection = access_point.and_then(move |addr| { let connection = access_point
info!("Connecting to AP \"{}\"", addr); .and_then(move |addr| {
connection::connect(addr, &handle_, &proxy) info!("Connecting to AP \"{}\"", addr);
}); connection::connect(addr, &handle_, &proxy)
})
.map_err(|io_err| io_err.into());
let device_id = config.device_id.clone(); let device_id = config.device_id.clone();
let authentication = connection.and_then(move |connection| { let authentication = connection.and_then(move |connection| {

View file

@ -654,20 +654,24 @@ impl PlayerTrackLoader {
FileFormat::OGG_VORBIS_96, FileFormat::OGG_VORBIS_96,
], ],
}; };
let format = formats
.iter()
.find(|format| audio.files.contains_key(format))
.unwrap();
let file_id = match audio.files.get(&format) { let entry = formats.iter().find_map(|format| {
Some(&file_id) => file_id, if let Some(&file_id) = audio.files.get(format) {
Some((*format, file_id))
} else {
None
}
});
let (format, file_id) = match entry {
Some(t) => t,
None => { None => {
warn!("<{}> in not available in format {:?}", audio.name, format); warn!("<{}> is not available in any supported format", audio.name);
return None; return None;
} }
}; };
let bytes_per_second = self.stream_data_rate(*format); let bytes_per_second = self.stream_data_rate(format);
let play_from_beginning = position_ms == 0; let play_from_beginning = position_ms == 0;
let key = self.session.audio_key().request(spotify_id, file_id); let key = self.session.audio_key().request(spotify_id, file_id);
@ -685,6 +689,7 @@ impl PlayerTrackLoader {
return None; return None;
} }
}; };
let is_cached = encrypted_file.is_cached();
let mut stream_loader_controller = encrypted_file.get_stream_loader_controller(); let mut stream_loader_controller = encrypted_file.get_stream_loader_controller();
@ -718,12 +723,31 @@ impl PlayerTrackLoader {
let audio_file = Subfile::new(decrypted_file, 0xa7); let audio_file = Subfile::new(decrypted_file, 0xa7);
let mut decoder = VorbisDecoder::new(audio_file).unwrap(); let mut decoder = match VorbisDecoder::new(audio_file) {
Ok(decoder) => decoder,
Err(e) if is_cached => {
warn!(
"Unable to read cached audio file: {}. Trying to download it.",
e
);
// unwrap safety: The file is cached, so session must have a cache
if !self.session.cache().unwrap().remove_file(file_id) {
return None;
}
// Just try it again
return self.load_track(spotify_id, position_ms);
}
Err(e) => {
error!("Unable to read audio file: {}", e);
return None;
}
};
if position_ms != 0 { if position_ms != 0 {
match decoder.seek(position_ms as i64) { if let Err(err) = decoder.seek(position_ms as i64) {
Ok(_) => (), error!("Vorbis error: {}", err);
Err(err) => error!("Vorbis error: {:?}", err),
} }
stream_loader_controller.set_stream_mode(); stream_loader_controller.set_stream_mode();
} }

View file

@ -3,7 +3,7 @@ use futures::{Async, Future, Poll, Stream};
use log::{error, info, trace, warn}; use log::{error, info, trace, warn};
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use std::env; use std::env;
use std::io::{self, stderr, Write}; use std::io::{stderr, Write};
use std::mem; use std::mem;
use std::path::Path; use std::path::Path;
use std::process::exit; use std::process::exit;
@ -16,7 +16,7 @@ use url::Url;
use librespot::core::authentication::{get_credentials, Credentials}; use librespot::core::authentication::{get_credentials, Credentials};
use librespot::core::cache::Cache; use librespot::core::cache::Cache;
use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig, VolumeCtrl}; use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig, VolumeCtrl};
use librespot::core::session::Session; use librespot::core::session::{AuthenticationError, Session};
use librespot::core::version; use librespot::core::version;
use librespot::connect::discovery::{discovery, DiscoveryStream}; use librespot::connect::discovery::{discovery, DiscoveryStream};
@ -436,7 +436,7 @@ struct Main {
spirc: Option<Spirc>, spirc: Option<Spirc>,
spirc_task: Option<SpircTask>, spirc_task: Option<SpircTask>,
connect: Box<dyn Future<Item = Session, Error = io::Error>>, connect: Box<dyn Future<Item = Session, Error = AuthenticationError>>,
shutdown: bool, shutdown: bool,
last_credentials: Option<Credentials>, last_credentials: Option<Credentials>,