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)
|
||||
|
||||
|
@ -21,7 +20,7 @@ As the origin by [plietar](https://github.com/plietar/) is no longer actively ma
|
|||
# Documentation
|
||||
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).
|
||||
[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.
|
||||
|
||||
# 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
|
||||
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.
|
||||
- [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