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)
@ -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
.and_then(move |addr| {
info!("Connecting to AP \"{}\"", addr); info!("Connecting to AP \"{}\"", addr);
connection::connect(addr, &handle_, &proxy) 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>,