mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
parent
910974e5e2
commit
72070b6ce0
8 changed files with 244 additions and 134 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -25,7 +25,7 @@ dependencies = [
|
||||||
"protobuf_macros 0.6.0 (git+https://github.com/plietar/rust-protobuf-macros)",
|
"protobuf_macros 0.6.0 (git+https://github.com/plietar/rust-protobuf-macros)",
|
||||||
"rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rpassword 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rpassword 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rust-crypto 0.2.36 (git+https://github.com/awmath/rust-crypto.git?branch=avx2)",
|
||||||
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -528,7 +528,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-crypto"
|
name = "rust-crypto"
|
||||||
version = "0.2.36"
|
version = "0.2.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/awmath/rust-crypto.git?branch=avx2#394c247254dbe2ac5d44483232cf335d10cf0260"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)",
|
"gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -1019,7 +1019,7 @@ dependencies = [
|
||||||
"checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01"
|
"checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01"
|
||||||
"checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457"
|
"checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457"
|
||||||
"checksum rpassword 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ec4bdede957362ec6fdd550f7e79c6d14cad2bc26b2d062786234c6ee0cb27bb"
|
"checksum rpassword 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ec4bdede957362ec6fdd550f7e79c6d14cad2bc26b2d062786234c6ee0cb27bb"
|
||||||
"checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a"
|
"checksum rust-crypto 0.2.36 (git+https://github.com/awmath/rust-crypto.git?branch=avx2)" = "<none>"
|
||||||
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
||||||
"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084"
|
"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084"
|
||||||
"checksum scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f417c22df063e9450888a7561788e9bd46d3bb3c1466435b4eccb903807f147d"
|
"checksum scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f417c22df063e9450888a7561788e9bd46d3bb3c1466435b4eccb903807f147d"
|
||||||
|
|
|
@ -19,19 +19,20 @@ use url;
|
||||||
|
|
||||||
use authentication::Credentials;
|
use authentication::Credentials;
|
||||||
use util;
|
use util;
|
||||||
|
use config::ConnectConfig;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Discovery(Arc<DiscoveryInner>);
|
struct Discovery(Arc<DiscoveryInner>);
|
||||||
struct DiscoveryInner {
|
struct DiscoveryInner {
|
||||||
|
config: ConnectConfig,
|
||||||
|
device_id: String,
|
||||||
private_key: BigUint,
|
private_key: BigUint,
|
||||||
public_key: BigUint,
|
public_key: BigUint,
|
||||||
device_id: String,
|
|
||||||
device_name: String,
|
|
||||||
tx: mpsc::UnboundedSender<Credentials>,
|
tx: mpsc::UnboundedSender<Credentials>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Discovery {
|
impl Discovery {
|
||||||
pub fn new(device_name: String, device_id: String)
|
pub fn new(config: ConnectConfig, device_id: String)
|
||||||
-> (Discovery, mpsc::UnboundedReceiver<Credentials>)
|
-> (Discovery, mpsc::UnboundedReceiver<Credentials>)
|
||||||
{
|
{
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded();
|
||||||
|
@ -41,8 +42,8 @@ impl Discovery {
|
||||||
let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME);
|
let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME);
|
||||||
|
|
||||||
let discovery = Discovery(Arc::new(DiscoveryInner {
|
let discovery = Discovery(Arc::new(DiscoveryInner {
|
||||||
device_name: device_name.to_owned(),
|
config: config,
|
||||||
device_id: device_id.to_owned(),
|
device_id: device_id,
|
||||||
private_key: private_key,
|
private_key: private_key,
|
||||||
public_key: public_key,
|
public_key: public_key,
|
||||||
tx: tx,
|
tx: tx,
|
||||||
|
@ -65,10 +66,10 @@ impl Discovery {
|
||||||
"spotifyError": 0,
|
"spotifyError": 0,
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"deviceID": (self.0.device_id),
|
"deviceID": (self.0.device_id),
|
||||||
"remoteName": (self.0.device_name),
|
"remoteName": (self.0.config.name),
|
||||||
"activeUser": "",
|
"activeUser": "",
|
||||||
"publicKey": (public_key),
|
"publicKey": (public_key),
|
||||||
"deviceType": "UNKNOWN",
|
"deviceType": (self.0.config.device_type.to_string().to_uppercase()),
|
||||||
"libraryVersion": "0.1.0",
|
"libraryVersion": "0.1.0",
|
||||||
"accountReq": "PREMIUM",
|
"accountReq": "PREMIUM",
|
||||||
"brandDisplayName": "librespot",
|
"brandDisplayName": "librespot",
|
||||||
|
@ -206,10 +207,10 @@ pub struct DiscoveryStream {
|
||||||
task: Box<Future<Item=(), Error=io::Error>>,
|
task: Box<Future<Item=(), Error=io::Error>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn discovery(handle: &Handle, device_name: String, device_id: String)
|
pub fn discovery(handle: &Handle, config: ConnectConfig, device_id: String)
|
||||||
-> io::Result<DiscoveryStream>
|
-> io::Result<DiscoveryStream>
|
||||||
{
|
{
|
||||||
let (discovery, creds_rx) = Discovery::new(device_name.clone(), device_id);
|
let (discovery, creds_rx) = Discovery::new(config.clone(), device_id);
|
||||||
|
|
||||||
let listener = TcpListener::bind(&"0.0.0.0:0".parse().unwrap(), handle)?;
|
let listener = TcpListener::bind(&"0.0.0.0:0".parse().unwrap(), handle)?;
|
||||||
let addr = listener.local_addr()?;
|
let addr = listener.local_addr()?;
|
||||||
|
@ -224,7 +225,7 @@ pub fn discovery(handle: &Handle, device_name: String, device_id: String)
|
||||||
let responder = mdns::Responder::spawn(&handle)?;
|
let responder = mdns::Responder::spawn(&handle)?;
|
||||||
let svc = responder.register(
|
let svc = responder.register(
|
||||||
"_spotify-connect._tcp".to_owned(),
|
"_spotify-connect._tcp".to_owned(),
|
||||||
device_name,
|
config.name,
|
||||||
addr.port(),
|
addr.port(),
|
||||||
&["VERSION=1.0", "CPath=/"]);
|
&["VERSION=1.0", "CPath=/"]);
|
||||||
|
|
||||||
|
|
123
src/config.rs
Normal file
123
src/config.rs
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
use uuid::Uuid;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use version;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
|
pub enum Bitrate {
|
||||||
|
Bitrate96,
|
||||||
|
Bitrate160,
|
||||||
|
Bitrate320,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Bitrate {
|
||||||
|
type Err = ();
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"96" => Ok(Bitrate::Bitrate96),
|
||||||
|
"160" => Ok(Bitrate::Bitrate160),
|
||||||
|
"320" => Ok(Bitrate::Bitrate320),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Bitrate {
|
||||||
|
fn default() -> Bitrate {
|
||||||
|
Bitrate::Bitrate160
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
|
pub enum DeviceType {
|
||||||
|
Unknown = 0,
|
||||||
|
Computer = 1,
|
||||||
|
Tablet = 2,
|
||||||
|
Smartphone = 3,
|
||||||
|
Speaker = 4,
|
||||||
|
TV = 5,
|
||||||
|
AVR = 6,
|
||||||
|
STB = 7,
|
||||||
|
AudioDongle = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for DeviceType {
|
||||||
|
type Err = ();
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
use self::DeviceType::*;
|
||||||
|
match s.to_lowercase().as_ref() {
|
||||||
|
"computer" => Ok(Computer),
|
||||||
|
"tablet" => Ok(Tablet),
|
||||||
|
"smartphone" => Ok(Smartphone),
|
||||||
|
"speaker" => Ok(Speaker),
|
||||||
|
"tv" => Ok(TV),
|
||||||
|
"avr" => Ok(AVR),
|
||||||
|
"stb" => Ok(STB),
|
||||||
|
"audiodongle" => Ok(AudioDongle),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DeviceType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use self::DeviceType::*;
|
||||||
|
match *self {
|
||||||
|
Unknown => f.write_str("Unknown"),
|
||||||
|
Computer => f.write_str("Computer"),
|
||||||
|
Tablet => f.write_str("Tablet"),
|
||||||
|
Smartphone => f.write_str("Smartphone"),
|
||||||
|
Speaker => f.write_str("Speaker"),
|
||||||
|
TV => f.write_str("TV"),
|
||||||
|
AVR => f.write_str("AVR"),
|
||||||
|
STB => f.write_str("STB"),
|
||||||
|
AudioDongle => f.write_str("AudioDongle"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DeviceType {
|
||||||
|
fn default() -> DeviceType {
|
||||||
|
DeviceType::Speaker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone,Debug)]
|
||||||
|
pub struct SessionConfig {
|
||||||
|
pub user_agent: String,
|
||||||
|
pub device_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SessionConfig {
|
||||||
|
fn default() -> SessionConfig {
|
||||||
|
let device_id = Uuid::new_v4().hyphenated().to_string();
|
||||||
|
SessionConfig {
|
||||||
|
user_agent: version::version_string(),
|
||||||
|
device_id: device_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone,Debug)]
|
||||||
|
pub struct PlayerConfig {
|
||||||
|
pub bitrate: Bitrate,
|
||||||
|
pub onstart: Option<String>,
|
||||||
|
pub onstop: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PlayerConfig {
|
||||||
|
fn default() -> PlayerConfig {
|
||||||
|
PlayerConfig {
|
||||||
|
bitrate: Bitrate::default(),
|
||||||
|
onstart: None,
|
||||||
|
onstop: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone,Debug)]
|
||||||
|
pub struct ConnectConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub device_type: DeviceType,
|
||||||
|
}
|
|
@ -61,14 +61,15 @@ pub mod audio_key;
|
||||||
pub mod authentication;
|
pub mod authentication;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
|
pub mod config;
|
||||||
pub mod diffie_hellman;
|
pub mod diffie_hellman;
|
||||||
|
pub mod keymaster;
|
||||||
pub mod mercury;
|
pub mod mercury;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
|
pub mod mixer;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
pub mod mixer;
|
|
||||||
pub mod keymaster;
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/lib.rs"));
|
include!(concat!(env!("OUT_DIR"), "/lib.rs"));
|
||||||
|
|
139
src/main.rs
139
src/main.rs
|
@ -26,7 +26,8 @@ use librespot::authentication::discovery::{discovery, DiscoveryStream};
|
||||||
use librespot::audio_backend::{self, Sink, BACKENDS};
|
use librespot::audio_backend::{self, Sink, BACKENDS};
|
||||||
use librespot::cache::Cache;
|
use librespot::cache::Cache;
|
||||||
use librespot::player::Player;
|
use librespot::player::Player;
|
||||||
use librespot::session::{Bitrate, Config, Session};
|
use librespot::session::Session;
|
||||||
|
use librespot::config::{Bitrate, DeviceType, PlayerConfig, SessionConfig, ConnectConfig};
|
||||||
use librespot::mixer::{self, Mixer};
|
use librespot::mixer::{self, Mixer};
|
||||||
|
|
||||||
use librespot::version;
|
use librespot::version;
|
||||||
|
@ -76,9 +77,10 @@ struct Setup {
|
||||||
|
|
||||||
mixer: fn() -> Box<Mixer>,
|
mixer: fn() -> Box<Mixer>,
|
||||||
|
|
||||||
name: String,
|
|
||||||
cache: Option<Cache>,
|
cache: Option<Cache>,
|
||||||
config: Config,
|
player_config: PlayerConfig,
|
||||||
|
session_config: SessionConfig,
|
||||||
|
connect_config: ConnectConfig,
|
||||||
credentials: Option<Credentials>,
|
credentials: Option<Credentials>,
|
||||||
enable_discovery: bool,
|
enable_discovery: bool,
|
||||||
}
|
}
|
||||||
|
@ -88,6 +90,7 @@ fn setup(args: &[String]) -> Setup {
|
||||||
opts.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE")
|
opts.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE")
|
||||||
.optflag("", "disable-audio-cache", "Disable caching of the audio data.")
|
.optflag("", "disable-audio-cache", "Disable caching of the audio data.")
|
||||||
.reqopt("n", "name", "Device name", "NAME")
|
.reqopt("n", "name", "Device name", "NAME")
|
||||||
|
.optopt("", "device-type", "Displayed device type", "DEVICE_TYPE")
|
||||||
.optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE")
|
.optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE")
|
||||||
.optopt("", "onstart", "Run PROGRAM when playback is about to begin.", "PROGRAM")
|
.optopt("", "onstart", "Run PROGRAM when playback is about to begin.", "PROGRAM")
|
||||||
.optopt("", "onstop", "Run PROGRAM when playback has ended.", "PROGRAM")
|
.optopt("", "onstop", "Run PROGRAM when playback has ended.", "PROGRAM")
|
||||||
|
@ -125,45 +128,69 @@ fn setup(args: &[String]) -> Setup {
|
||||||
let backend = audio_backend::find(backend_name)
|
let backend = audio_backend::find(backend_name)
|
||||||
.expect("Invalid backend");
|
.expect("Invalid backend");
|
||||||
|
|
||||||
|
let device = matches.opt_str("device");
|
||||||
|
|
||||||
let mixer_name = matches.opt_str("mixer");
|
let mixer_name = matches.opt_str("mixer");
|
||||||
let mixer = mixer::find(mixer_name.as_ref())
|
let mixer = mixer::find(mixer_name.as_ref())
|
||||||
.expect("Invalid mixer");
|
.expect("Invalid mixer");
|
||||||
|
|
||||||
let bitrate = matches.opt_str("b").as_ref()
|
|
||||||
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
|
|
||||||
.unwrap_or(Bitrate::Bitrate160);
|
|
||||||
|
|
||||||
let name = matches.opt_str("name").unwrap();
|
let name = matches.opt_str("name").unwrap();
|
||||||
let device_id = librespot::session::device_id(&name);
|
|
||||||
let use_audio_cache = !matches.opt_present("disable-audio-cache");
|
let use_audio_cache = !matches.opt_present("disable-audio-cache");
|
||||||
|
|
||||||
let cache = matches.opt_str("c").map(|cache_location| {
|
let cache = matches.opt_str("c").map(|cache_location| {
|
||||||
Cache::new(PathBuf::from(cache_location), use_audio_cache)
|
Cache::new(PathBuf::from(cache_location), use_audio_cache)
|
||||||
});
|
});
|
||||||
|
|
||||||
let cached_credentials = cache.as_ref().and_then(Cache::credentials);
|
let credentials = {
|
||||||
|
let cached_credentials = cache.as_ref().and_then(Cache::credentials);
|
||||||
|
|
||||||
let credentials = get_credentials(matches.opt_str("username"),
|
get_credentials(
|
||||||
matches.opt_str("password"),
|
matches.opt_str("username"),
|
||||||
cached_credentials);
|
matches.opt_str("password"),
|
||||||
|
cached_credentials
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let session_config = {
|
||||||
|
let device_id = librespot::session::device_id(&name);
|
||||||
|
|
||||||
|
SessionConfig {
|
||||||
|
user_agent: version::version_string(),
|
||||||
|
device_id: device_id,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let player_config = {
|
||||||
|
let bitrate = matches.opt_str("b").as_ref()
|
||||||
|
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
|
||||||
|
.unwrap_or(Bitrate::default());
|
||||||
|
|
||||||
|
PlayerConfig {
|
||||||
|
bitrate: bitrate,
|
||||||
|
onstart: matches.opt_str("onstart"),
|
||||||
|
onstop: matches.opt_str("onstop"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let connect_config = {
|
||||||
|
let device_type = matches.opt_str("device-type").as_ref()
|
||||||
|
.map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type"))
|
||||||
|
.unwrap_or(DeviceType::default());
|
||||||
|
|
||||||
|
ConnectConfig {
|
||||||
|
name: name,
|
||||||
|
device_type: device_type,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let enable_discovery = !matches.opt_present("disable-discovery");
|
let enable_discovery = !matches.opt_present("disable-discovery");
|
||||||
|
|
||||||
let config = Config {
|
|
||||||
user_agent: version::version_string(),
|
|
||||||
device_id: device_id,
|
|
||||||
bitrate: bitrate,
|
|
||||||
onstart: matches.opt_str("onstart"),
|
|
||||||
onstop: matches.opt_str("onstop"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let device = matches.opt_str("device");
|
|
||||||
|
|
||||||
Setup {
|
Setup {
|
||||||
name: name,
|
|
||||||
backend: backend,
|
backend: backend,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
config: config,
|
session_config: session_config,
|
||||||
|
player_config: player_config,
|
||||||
|
connect_config: connect_config,
|
||||||
credentials: credentials,
|
credentials: credentials,
|
||||||
device: device,
|
device: device,
|
||||||
enable_discovery: enable_discovery,
|
enable_discovery: enable_discovery,
|
||||||
|
@ -172,9 +199,10 @@ fn setup(args: &[String]) -> Setup {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Main {
|
struct Main {
|
||||||
name: String,
|
|
||||||
cache: Option<Cache>,
|
cache: Option<Cache>,
|
||||||
config: Config,
|
player_config: PlayerConfig,
|
||||||
|
session_config: SessionConfig,
|
||||||
|
connect_config: ConnectConfig,
|
||||||
backend: fn(Option<String>) -> Box<Sink>,
|
backend: fn(Option<String>) -> Box<Sink>,
|
||||||
device: Option<String>,
|
device: Option<String>,
|
||||||
mixer: fn() -> Box<Mixer>,
|
mixer: fn() -> Box<Mixer>,
|
||||||
|
@ -191,22 +219,16 @@ struct Main {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Main {
|
impl Main {
|
||||||
fn new(handle: Handle,
|
fn new(handle: Handle, setup: Setup) -> Main {
|
||||||
name: String,
|
let mut task = Main {
|
||||||
config: Config,
|
|
||||||
cache: Option<Cache>,
|
|
||||||
backend: fn(Option<String>) -> Box<Sink>,
|
|
||||||
device: Option<String>,
|
|
||||||
mixer: fn() -> Box<Mixer>) -> Main
|
|
||||||
{
|
|
||||||
Main {
|
|
||||||
handle: handle.clone(),
|
handle: handle.clone(),
|
||||||
name: name,
|
cache: setup.cache,
|
||||||
cache: cache,
|
session_config: setup.session_config,
|
||||||
config: config,
|
player_config: setup.player_config,
|
||||||
backend: backend,
|
connect_config: setup.connect_config,
|
||||||
device: device,
|
backend: setup.backend,
|
||||||
mixer: mixer,
|
device: setup.device,
|
||||||
|
mixer: setup.mixer,
|
||||||
|
|
||||||
connect: Box::new(futures::future::empty()),
|
connect: Box::new(futures::future::empty()),
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
@ -214,18 +236,24 @@ impl Main {
|
||||||
spirc_task: None,
|
spirc_task: None,
|
||||||
shutdown: false,
|
shutdown: false,
|
||||||
signal: tokio_signal::ctrl_c(&handle).flatten_stream().boxed(),
|
signal: tokio_signal::ctrl_c(&handle).flatten_stream().boxed(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if setup.enable_discovery {
|
||||||
|
let config = task.connect_config.clone();
|
||||||
|
let device_id = task.session_config.device_id.clone();
|
||||||
|
|
||||||
|
task.discovery = Some(discovery(&handle, config, device_id).unwrap());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn discovery(&mut self) {
|
if let Some(credentials) = setup.credentials {
|
||||||
let device_id = self.config.device_id.clone();
|
task.credentials(credentials);
|
||||||
let name = self.name.clone();
|
}
|
||||||
|
|
||||||
self.discovery = Some(discovery(&self.handle, name, device_id).unwrap());
|
task
|
||||||
}
|
}
|
||||||
|
|
||||||
fn credentials(&mut self, credentials: Credentials) {
|
fn credentials(&mut self, credentials: Credentials) {
|
||||||
let config = self.config.clone();
|
let config = self.session_config.clone();
|
||||||
let handle = self.handle.clone();
|
let handle = self.handle.clone();
|
||||||
|
|
||||||
let connection = Session::connect(config, credentials, self.cache.clone(), handle);
|
let connection = Session::connect(config, credentials, self.cache.clone(), handle);
|
||||||
|
@ -260,14 +288,16 @@ impl Future for Main {
|
||||||
self.connect = Box::new(futures::future::empty());
|
self.connect = Box::new(futures::future::empty());
|
||||||
let device = self.device.clone();
|
let device = self.device.clone();
|
||||||
let mixer = (self.mixer)();
|
let mixer = (self.mixer)();
|
||||||
|
let player_config = self.player_config.clone();
|
||||||
|
let connect_config = self.connect_config.clone();
|
||||||
|
|
||||||
let audio_filter = mixer.get_audio_filter();
|
let audio_filter = mixer.get_audio_filter();
|
||||||
let backend = self.backend;
|
let backend = self.backend;
|
||||||
let player = Player::new(session.clone(), audio_filter, move || {
|
let player = Player::new(player_config, session.clone(), audio_filter, move || {
|
||||||
(backend)(device)
|
(backend)(device)
|
||||||
});
|
});
|
||||||
|
|
||||||
let (spirc, spirc_task) = Spirc::new(self.name.clone(), session, player, mixer);
|
let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer);
|
||||||
self.spirc = Some(spirc);
|
self.spirc = Some(spirc);
|
||||||
self.spirc_task = Some(spirc_task);
|
self.spirc_task = Some(spirc_task);
|
||||||
|
|
||||||
|
@ -309,16 +339,7 @@ fn main() {
|
||||||
let handle = core.handle();
|
let handle = core.handle();
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let Setup { name, backend, config, device, cache, enable_discovery, credentials, mixer } = setup(&args);
|
|
||||||
|
|
||||||
let mut task = Main::new(handle, name, config, cache, backend, device, mixer);
|
core.run(Main::new(handle, setup(&args))).unwrap()
|
||||||
if enable_discovery {
|
|
||||||
task.discovery();
|
|
||||||
}
|
|
||||||
if let Some(credentials) = credentials {
|
|
||||||
task.credentials(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
core.run(task).unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,10 @@ use audio_backend::Sink;
|
||||||
use audio_decrypt::AudioDecrypt;
|
use audio_decrypt::AudioDecrypt;
|
||||||
use audio_file::AudioFile;
|
use audio_file::AudioFile;
|
||||||
use metadata::{FileFormat, Track};
|
use metadata::{FileFormat, Track};
|
||||||
use session::{Bitrate, Session};
|
use session::Session;
|
||||||
use mixer::AudioFilter;
|
use mixer::AudioFilter;
|
||||||
use util::{self, SpotifyId, Subfile};
|
use util::{self, SpotifyId, Subfile};
|
||||||
|
use config::{Bitrate, PlayerConfig};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
|
@ -23,6 +24,7 @@ pub struct Player {
|
||||||
|
|
||||||
struct PlayerInternal {
|
struct PlayerInternal {
|
||||||
session: Session,
|
session: Session,
|
||||||
|
config: PlayerConfig,
|
||||||
commands: std::sync::mpsc::Receiver<PlayerCommand>,
|
commands: std::sync::mpsc::Receiver<PlayerCommand>,
|
||||||
|
|
||||||
state: PlayerState,
|
state: PlayerState,
|
||||||
|
@ -39,8 +41,11 @@ enum PlayerCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn new<F>(session: Session, audio_filter: Option<Box<AudioFilter + Send>>, sink_builder: F) -> Player
|
pub fn new<F>(config: PlayerConfig, session: Session,
|
||||||
where F: FnOnce() -> Box<Sink> + Send + 'static {
|
audio_filter: Option<Box<AudioFilter + Send>>,
|
||||||
|
sink_builder: F) -> Player
|
||||||
|
where F: FnOnce() -> Box<Sink> + Send + 'static
|
||||||
|
{
|
||||||
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
|
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
|
@ -48,6 +53,7 @@ impl Player {
|
||||||
|
|
||||||
let internal = PlayerInternal {
|
let internal = PlayerInternal {
|
||||||
session: session,
|
session: session,
|
||||||
|
config: config,
|
||||||
commands: cmd_rx,
|
commands: cmd_rx,
|
||||||
|
|
||||||
state: PlayerState::Stopped,
|
state: PlayerState::Stopped,
|
||||||
|
@ -314,13 +320,13 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_onstart(&self) {
|
fn run_onstart(&self) {
|
||||||
if let Some(ref program) = self.session.config().onstart {
|
if let Some(ref program) = self.config.onstart {
|
||||||
util::run_program(program)
|
util::run_program(program)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_onstop(&self) {
|
fn run_onstop(&self) {
|
||||||
if let Some(ref program) = self.session.config().onstop {
|
if let Some(ref program) = self.config.onstop {
|
||||||
util::run_program(program)
|
util::run_program(program)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,7 +359,7 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let format = match self.session.config().bitrate {
|
let format = match self.config.bitrate {
|
||||||
Bitrate::Bitrate96 => FileFormat::OGG_VORBIS_96,
|
Bitrate::Bitrate96 => FileFormat::OGG_VORBIS_96,
|
||||||
Bitrate::Bitrate160 => FileFormat::OGG_VORBIS_160,
|
Bitrate::Bitrate160 => FileFormat::OGG_VORBIS_160,
|
||||||
Bitrate::Bitrate320 => FileFormat::OGG_VORBIS_320,
|
Bitrate::Bitrate320 => FileFormat::OGG_VORBIS_320,
|
||||||
|
|
|
@ -3,20 +3,17 @@ use crypto::sha1::Sha1;
|
||||||
use futures::sync::mpsc;
|
use futures::sync::mpsc;
|
||||||
use futures::{Future, Stream, BoxFuture, IntoFuture, Poll, Async};
|
use futures::{Future, Stream, BoxFuture, IntoFuture, Poll, Async};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::result::Result;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::{RwLock, Arc, Weak};
|
use std::sync::{RwLock, Arc, Weak};
|
||||||
use tokio_core::io::EasyBuf;
|
use tokio_core::io::EasyBuf;
|
||||||
use tokio_core::reactor::{Handle, Remote};
|
use tokio_core::reactor::{Handle, Remote};
|
||||||
use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
|
use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use apresolve::apresolve_or_fallback;
|
use apresolve::apresolve_or_fallback;
|
||||||
use authentication::Credentials;
|
use authentication::Credentials;
|
||||||
use cache::Cache;
|
use cache::Cache;
|
||||||
use component::Lazy;
|
use component::Lazy;
|
||||||
use connection;
|
use connection;
|
||||||
use version;
|
use config::SessionConfig;
|
||||||
|
|
||||||
use audio_key::AudioKeyManager;
|
use audio_key::AudioKeyManager;
|
||||||
use channel::ChannelManager;
|
use channel::ChannelManager;
|
||||||
|
@ -24,53 +21,13 @@ use mercury::MercuryManager;
|
||||||
use metadata::MetadataManager;
|
use metadata::MetadataManager;
|
||||||
use audio_file::AudioFileManager;
|
use audio_file::AudioFileManager;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
|
|
||||||
pub enum Bitrate {
|
|
||||||
Bitrate96,
|
|
||||||
Bitrate160,
|
|
||||||
Bitrate320,
|
|
||||||
}
|
|
||||||
impl FromStr for Bitrate {
|
|
||||||
type Err = String;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"96" => Ok(Bitrate::Bitrate96),
|
|
||||||
"160" => Ok(Bitrate::Bitrate160),
|
|
||||||
"320" => Ok(Bitrate::Bitrate320),
|
|
||||||
_ => Err(s.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Config {
|
|
||||||
pub user_agent: String,
|
|
||||||
pub device_id: String,
|
|
||||||
pub bitrate: Bitrate,
|
|
||||||
pub onstart: Option<String>,
|
|
||||||
pub onstop: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Config {
|
|
||||||
let device_id = Uuid::new_v4().hyphenated().to_string();
|
|
||||||
Config {
|
|
||||||
user_agent: version::version_string(),
|
|
||||||
device_id: device_id,
|
|
||||||
bitrate: Bitrate::Bitrate160,
|
|
||||||
onstart: None,
|
|
||||||
onstop: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SessionData {
|
pub struct SessionData {
|
||||||
country: String,
|
country: String,
|
||||||
canonical_username: String,
|
canonical_username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SessionInternal {
|
pub struct SessionInternal {
|
||||||
config: Config,
|
config: SessionConfig,
|
||||||
data: RwLock<SessionData>,
|
data: RwLock<SessionData>,
|
||||||
|
|
||||||
tx_connection: mpsc::UnboundedSender<(u8, Vec<u8>)>,
|
tx_connection: mpsc::UnboundedSender<(u8, Vec<u8>)>,
|
||||||
|
@ -99,7 +56,7 @@ pub fn device_id(name: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn connect(config: Config, credentials: Credentials,
|
pub fn connect(config: SessionConfig, credentials: Credentials,
|
||||||
cache: Option<Cache>, handle: Handle)
|
cache: Option<Cache>, handle: Handle)
|
||||||
-> Box<Future<Item=Session, Error=io::Error>>
|
-> Box<Future<Item=Session, Error=io::Error>>
|
||||||
{
|
{
|
||||||
|
@ -135,7 +92,7 @@ impl Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create(handle: &Handle, transport: connection::Transport,
|
fn create(handle: &Handle, transport: connection::Transport,
|
||||||
config: Config, cache: Option<Cache>, username: String)
|
config: SessionConfig, cache: Option<Cache>, username: String)
|
||||||
-> (Session, BoxFuture<(), io::Error>)
|
-> (Session, BoxFuture<(), io::Error>)
|
||||||
{
|
{
|
||||||
let (sink, stream) = transport.split();
|
let (sink, stream) = transport.split();
|
||||||
|
@ -240,7 +197,7 @@ impl Session {
|
||||||
self.0.cache.as_ref()
|
self.0.cache.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config(&self) -> &Config {
|
pub fn config(&self) -> &SessionConfig {
|
||||||
&self.0.config
|
&self.0.config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
13
src/spirc.rs
13
src/spirc.rs
|
@ -5,9 +5,10 @@ use futures::sync::{oneshot, mpsc};
|
||||||
use futures::{Future, Stream, Sink, Async, Poll, BoxFuture};
|
use futures::{Future, Stream, Sink, Async, Poll, BoxFuture};
|
||||||
use protobuf::{self, Message};
|
use protobuf::{self, Message};
|
||||||
|
|
||||||
|
use config::ConnectConfig;
|
||||||
use mercury::MercuryError;
|
use mercury::MercuryError;
|
||||||
use player::Player;
|
|
||||||
use mixer::Mixer;
|
use mixer::Mixer;
|
||||||
|
use player::Player;
|
||||||
use session::Session;
|
use session::Session;
|
||||||
use util::{now_ms, SpotifyId, SeqGenerator};
|
use util::{now_ms, SpotifyId, SeqGenerator};
|
||||||
use version;
|
use version;
|
||||||
|
@ -59,13 +60,13 @@ fn initial_state() -> State {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initial_device_state(name: String, volume: u16) -> DeviceState {
|
fn initial_device_state(config: ConnectConfig, volume: u16) -> DeviceState {
|
||||||
protobuf_init!(DeviceState::new(), {
|
protobuf_init!(DeviceState::new(), {
|
||||||
sw_version: version::version_string(),
|
sw_version: version::version_string(),
|
||||||
is_active: false,
|
is_active: false,
|
||||||
can_play: true,
|
can_play: true,
|
||||||
volume: volume as u32,
|
volume: volume as u32,
|
||||||
name: name,
|
name: config.name,
|
||||||
capabilities => [
|
capabilities => [
|
||||||
@{
|
@{
|
||||||
typ: protocol::spirc::CapabilityType::kCanBePlayer,
|
typ: protocol::spirc::CapabilityType::kCanBePlayer,
|
||||||
|
@ -73,7 +74,7 @@ fn initial_device_state(name: String, volume: u16) -> DeviceState {
|
||||||
},
|
},
|
||||||
@{
|
@{
|
||||||
typ: protocol::spirc::CapabilityType::kDeviceType,
|
typ: protocol::spirc::CapabilityType::kDeviceType,
|
||||||
intValue => [5]
|
intValue => [config.device_type as i64]
|
||||||
},
|
},
|
||||||
@{
|
@{
|
||||||
typ: protocol::spirc::CapabilityType::kGaiaEqConnectId,
|
typ: protocol::spirc::CapabilityType::kGaiaEqConnectId,
|
||||||
|
@ -118,7 +119,7 @@ fn initial_device_state(name: String, volume: u16) -> DeviceState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Spirc {
|
impl Spirc {
|
||||||
pub fn new(name: String, session: Session, player: Player, mixer: Box<Mixer>)
|
pub fn new(config: ConnectConfig, session: Session, player: Player, mixer: Box<Mixer>)
|
||||||
-> (Spirc, SpircTask)
|
-> (Spirc, SpircTask)
|
||||||
{
|
{
|
||||||
debug!("new Spirc[{}]", session.session_id());
|
debug!("new Spirc[{}]", session.session_id());
|
||||||
|
@ -141,7 +142,7 @@ impl Spirc {
|
||||||
let (cmd_tx, cmd_rx) = mpsc::unbounded();
|
let (cmd_tx, cmd_rx) = mpsc::unbounded();
|
||||||
|
|
||||||
let volume = 0xFFFF;
|
let volume = 0xFFFF;
|
||||||
let device = initial_device_state(name, volume);
|
let device = initial_device_state(config, volume);
|
||||||
mixer.set_volume(volume);
|
mixer.set_volume(volume);
|
||||||
|
|
||||||
let mut task = SpircTask {
|
let mut task = SpircTask {
|
||||||
|
|
Loading…
Reference in a new issue