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:
|
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]
|
||||||
|
|
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://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.
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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| {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
Loading…
Reference in a new issue