mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge branch 'dev' into rodiojack-backend
This commit is contained in:
parent
52438b1cc2
commit
aad4dba8a8
12 changed files with 143 additions and 107 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -41,7 +41,7 @@ jobs:
|
|||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
toolchain:
|
||||
- 1.40.0 # MSRV (Minimum supported rust version)
|
||||
- 1.42.0 # MSRV (Minimum supported rust version)
|
||||
- stable
|
||||
- beta
|
||||
experimental: [false]
|
||||
|
|
74
.travis.yml
74
.travis.yml
|
@ -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
|
|
@ -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://travis-ci.org/librespot-org/librespot.svg?branch=dev)](https://travis-ci.org/librespot-org/librespot)
|
||||
[![Build Status](https://github.com/librespot-org/librespot/workflows/test/badge.svg)](https://github.com/librespot-org/librespot/actions)
|
||||
[![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)
|
||||
|
||||
|
@ -112,4 +111,3 @@ functionality.
|
|||
- [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot.
|
||||
- [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.
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -75,7 +75,7 @@ impl Discovery {
|
|||
"status": 101,
|
||||
"statusString": "ERROR-OK",
|
||||
"spotifyError": 0,
|
||||
"version": "2.1.0",
|
||||
"version": "2.7.1",
|
||||
"deviceID": (self.0.device_id),
|
||||
"remoteName": (self.0.config.name),
|
||||
"activeUser": "",
|
||||
|
@ -85,6 +85,9 @@ impl Discovery {
|
|||
"accountReq": "PREMIUM",
|
||||
"brandDisplayName": "librespot",
|
||||
"modelDisplayName": "librespot",
|
||||
"resolverVersion": "0",
|
||||
"groupStatus": "NONE",
|
||||
"voiceSupport": "NO",
|
||||
});
|
||||
|
||||
let body = result.to_string();
|
||||
|
|
|
@ -7,6 +7,7 @@ use sha1::{Digest, Sha1};
|
|||
use std::io::{self, Read};
|
||||
|
||||
use crate::protocol::authentication::AuthenticationType;
|
||||
use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Credentials {
|
||||
|
@ -164,3 +165,37 @@ pub fn get_credentials<F: FnOnce(&String) -> String>(
|
|||
(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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,4 +162,17 @@ impl Cache {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,16 @@ pub enum DeviceType {
|
|||
AVR = 6,
|
||||
STB = 7,
|
||||
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 {
|
||||
|
@ -51,6 +61,14 @@ impl FromStr for DeviceType {
|
|||
"avr" => Ok(AVR),
|
||||
"stb" => Ok(STB),
|
||||
"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(()),
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +87,16 @@ impl fmt::Display for DeviceType {
|
|||
AVR => f.write_str("AVR"),
|
||||
STB => f.write_str("STB"),
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use tokio_core::net::TcpStream;
|
|||
use tokio_core::reactor::Handle;
|
||||
use url::Url;
|
||||
|
||||
use crate::authentication::Credentials;
|
||||
use crate::authentication::{AuthenticationError, Credentials};
|
||||
use crate::version;
|
||||
|
||||
use crate::proxytunnel;
|
||||
|
@ -66,7 +66,7 @@ pub fn authenticate(
|
|||
transport: Transport,
|
||||
credentials: Credentials,
|
||||
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::keyexchange::APLoginFailed;
|
||||
|
||||
|
@ -101,6 +101,7 @@ pub fn authenticate(
|
|||
transport
|
||||
.send((cmd, data))
|
||||
.and_then(|transport| transport.into_future().map_err(|(err, _stream)| err))
|
||||
.map_err(|io_err| io_err.into())
|
||||
.and_then(|(packet, transport)| match packet {
|
||||
Some((0xac, data)) => {
|
||||
let welcome_data: APWelcome =
|
||||
|
@ -118,10 +119,7 @@ pub fn authenticate(
|
|||
Some((0xad, data)) => {
|
||||
let error_data: APLoginFailed =
|
||||
protobuf::parse_from_bytes(data.as_ref()).unwrap();
|
||||
panic!(
|
||||
"Authentication failed with reason: {:?}",
|
||||
error_data.get_error_code()
|
||||
)
|
||||
Err(error_data.into())
|
||||
}
|
||||
|
||||
Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd),
|
||||
|
|
|
@ -19,6 +19,8 @@ use crate::config::SessionConfig;
|
|||
use crate::connection;
|
||||
use crate::mercury::MercuryManager;
|
||||
|
||||
pub use crate::authentication::{AuthenticationError, AuthenticationErrorKind};
|
||||
|
||||
struct SessionData {
|
||||
country: String,
|
||||
time_delta: i64,
|
||||
|
@ -53,16 +55,18 @@ impl Session {
|
|||
credentials: Credentials,
|
||||
cache: Option<Cache>,
|
||||
handle: Handle,
|
||||
) -> Box<dyn Future<Item = Session, Error = io::Error>> {
|
||||
) -> Box<dyn Future<Item = Session, Error = AuthenticationError>> {
|
||||
let access_point =
|
||||
apresolve_or_fallback::<io::Error>(&handle, &config.proxy, &config.ap_port);
|
||||
|
||||
let handle_ = handle.clone();
|
||||
let proxy = config.proxy.clone();
|
||||
let connection = access_point.and_then(move |addr| {
|
||||
info!("Connecting to AP \"{}\"", addr);
|
||||
connection::connect(addr, &handle_, &proxy)
|
||||
});
|
||||
let connection = access_point
|
||||
.and_then(move |addr| {
|
||||
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 authentication = connection.and_then(move |connection| {
|
||||
|
|
|
@ -654,20 +654,24 @@ impl PlayerTrackLoader {
|
|||
FileFormat::OGG_VORBIS_96,
|
||||
],
|
||||
};
|
||||
let format = formats
|
||||
.iter()
|
||||
.find(|format| audio.files.contains_key(format))
|
||||
.unwrap();
|
||||
|
||||
let file_id = match audio.files.get(&format) {
|
||||
Some(&file_id) => file_id,
|
||||
let entry = formats.iter().find_map(|format| {
|
||||
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 => {
|
||||
warn!("<{}> in not available in format {:?}", audio.name, format);
|
||||
warn!("<{}> is not available in any supported format", audio.name);
|
||||
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 key = self.session.audio_key().request(spotify_id, file_id);
|
||||
|
@ -685,6 +689,7 @@ impl PlayerTrackLoader {
|
|||
return None;
|
||||
}
|
||||
};
|
||||
let is_cached = encrypted_file.is_cached();
|
||||
|
||||
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 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 {
|
||||
match decoder.seek(position_ms as i64) {
|
||||
Ok(_) => (),
|
||||
Err(err) => error!("Vorbis error: {:?}", err),
|
||||
if let Err(err) = decoder.seek(position_ms as i64) {
|
||||
error!("Vorbis error: {}", err);
|
||||
}
|
||||
stream_loader_controller.set_stream_mode();
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use futures::{Async, Future, Poll, Stream};
|
|||
use log::{error, info, trace, warn};
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::env;
|
||||
use std::io::{self, stderr, Write};
|
||||
use std::io::{stderr, Write};
|
||||
use std::mem;
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
|
@ -16,7 +16,7 @@ use url::Url;
|
|||
use librespot::core::authentication::{get_credentials, Credentials};
|
||||
use librespot::core::cache::Cache;
|
||||
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::connect::discovery::{discovery, DiscoveryStream};
|
||||
|
@ -436,7 +436,7 @@ struct Main {
|
|||
|
||||
spirc: Option<Spirc>,
|
||||
spirc_task: Option<SpircTask>,
|
||||
connect: Box<dyn Future<Item = Session, Error = io::Error>>,
|
||||
connect: Box<dyn Future<Item = Session, Error = AuthenticationError>>,
|
||||
|
||||
shutdown: bool,
|
||||
last_credentials: Option<Credentials>,
|
||||
|
|
Loading…
Reference in a new issue