Allow device type to be chosen.

Fix #187
This commit is contained in:
Paul Lietar 2017-08-03 19:31:15 +01:00
parent 910974e5e2
commit 72070b6ce0
8 changed files with 244 additions and 134 deletions

6
Cargo.lock generated
View file

@ -25,7 +25,7 @@ dependencies = [
"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)",
"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_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)",
@ -528,7 +528,7 @@ dependencies = [
[[package]]
name = "rust-crypto"
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 = [
"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)",
@ -1019,7 +1019,7 @@ dependencies = [
"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 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_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"

View file

@ -19,19 +19,20 @@ use url;
use authentication::Credentials;
use util;
use config::ConnectConfig;
#[derive(Clone)]
struct Discovery(Arc<DiscoveryInner>);
struct DiscoveryInner {
config: ConnectConfig,
device_id: String,
private_key: BigUint,
public_key: BigUint,
device_id: String,
device_name: String,
tx: mpsc::UnboundedSender<Credentials>,
}
impl Discovery {
pub fn new(device_name: String, device_id: String)
pub fn new(config: ConnectConfig, device_id: String)
-> (Discovery, mpsc::UnboundedReceiver<Credentials>)
{
let (tx, rx) = mpsc::unbounded();
@ -41,8 +42,8 @@ impl Discovery {
let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME);
let discovery = Discovery(Arc::new(DiscoveryInner {
device_name: device_name.to_owned(),
device_id: device_id.to_owned(),
config: config,
device_id: device_id,
private_key: private_key,
public_key: public_key,
tx: tx,
@ -65,10 +66,10 @@ impl Discovery {
"spotifyError": 0,
"version": "2.1.0",
"deviceID": (self.0.device_id),
"remoteName": (self.0.device_name),
"remoteName": (self.0.config.name),
"activeUser": "",
"publicKey": (public_key),
"deviceType": "UNKNOWN",
"deviceType": (self.0.config.device_type.to_string().to_uppercase()),
"libraryVersion": "0.1.0",
"accountReq": "PREMIUM",
"brandDisplayName": "librespot",
@ -206,10 +207,10 @@ pub struct DiscoveryStream {
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>
{
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 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 svc = responder.register(
"_spotify-connect._tcp".to_owned(),
device_name,
config.name,
addr.port(),
&["VERSION=1.0", "CPath=/"]);

123
src/config.rs Normal file
View 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,
}

View file

@ -61,14 +61,15 @@ pub mod audio_key;
pub mod authentication;
pub mod cache;
pub mod channel;
pub mod config;
pub mod diffie_hellman;
pub mod keymaster;
pub mod mercury;
pub mod metadata;
pub mod mixer;
pub mod player;
pub mod session;
pub mod util;
pub mod version;
pub mod mixer;
pub mod keymaster;
include!(concat!(env!("OUT_DIR"), "/lib.rs"));

View file

@ -26,7 +26,8 @@ use librespot::authentication::discovery::{discovery, DiscoveryStream};
use librespot::audio_backend::{self, Sink, BACKENDS};
use librespot::cache::Cache;
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::version;
@ -76,9 +77,10 @@ struct Setup {
mixer: fn() -> Box<Mixer>,
name: String,
cache: Option<Cache>,
config: Config,
player_config: PlayerConfig,
session_config: SessionConfig,
connect_config: ConnectConfig,
credentials: Option<Credentials>,
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")
.optflag("", "disable-audio-cache", "Disable caching of the audio data.")
.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("", "onstart", "Run PROGRAM when playback is about to begin.", "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)
.expect("Invalid backend");
let device = matches.opt_str("device");
let mixer_name = matches.opt_str("mixer");
let mixer = mixer::find(mixer_name.as_ref())
.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 device_id = librespot::session::device_id(&name);
let use_audio_cache = !matches.opt_present("disable-audio-cache");
let cache = matches.opt_str("c").map(|cache_location| {
Cache::new(PathBuf::from(cache_location), use_audio_cache)
});
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("username"),
matches.opt_str("password"),
cached_credentials);
cached_credentials
)
};
let enable_discovery = !matches.opt_present("disable-discovery");
let session_config = {
let device_id = librespot::session::device_id(&name);
let config = Config {
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 device = matches.opt_str("device");
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");
Setup {
name: name,
backend: backend,
cache: cache,
config: config,
session_config: session_config,
player_config: player_config,
connect_config: connect_config,
credentials: credentials,
device: device,
enable_discovery: enable_discovery,
@ -172,9 +199,10 @@ fn setup(args: &[String]) -> Setup {
}
struct Main {
name: String,
cache: Option<Cache>,
config: Config,
player_config: PlayerConfig,
session_config: SessionConfig,
connect_config: ConnectConfig,
backend: fn(Option<String>) -> Box<Sink>,
device: Option<String>,
mixer: fn() -> Box<Mixer>,
@ -191,22 +219,16 @@ struct Main {
}
impl Main {
fn new(handle: Handle,
name: String,
config: Config,
cache: Option<Cache>,
backend: fn(Option<String>) -> Box<Sink>,
device: Option<String>,
mixer: fn() -> Box<Mixer>) -> Main
{
Main {
fn new(handle: Handle, setup: Setup) -> Main {
let mut task = Main {
handle: handle.clone(),
name: name,
cache: cache,
config: config,
backend: backend,
device: device,
mixer: mixer,
cache: setup.cache,
session_config: setup.session_config,
player_config: setup.player_config,
connect_config: setup.connect_config,
backend: setup.backend,
device: setup.device,
mixer: setup.mixer,
connect: Box::new(futures::future::empty()),
discovery: None,
@ -214,18 +236,24 @@ impl Main {
spirc_task: None,
shutdown: false,
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) {
let device_id = self.config.device_id.clone();
let name = self.name.clone();
if let Some(credentials) = setup.credentials {
task.credentials(credentials);
}
self.discovery = Some(discovery(&self.handle, name, device_id).unwrap());
task
}
fn credentials(&mut self, credentials: Credentials) {
let config = self.config.clone();
let config = self.session_config.clone();
let handle = self.handle.clone();
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());
let device = self.device.clone();
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 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)
});
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_task = Some(spirc_task);
@ -309,16 +339,7 @@ fn main() {
let handle = core.handle();
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);
if enable_discovery {
task.discovery();
}
if let Some(credentials) = credentials {
task.credentials(credentials);
}
core.run(task).unwrap()
core.run(Main::new(handle, setup(&args))).unwrap()
}

View file

@ -12,9 +12,10 @@ use audio_backend::Sink;
use audio_decrypt::AudioDecrypt;
use audio_file::AudioFile;
use metadata::{FileFormat, Track};
use session::{Bitrate, Session};
use session::Session;
use mixer::AudioFilter;
use util::{self, SpotifyId, Subfile};
use config::{Bitrate, PlayerConfig};
#[derive(Clone)]
pub struct Player {
@ -23,6 +24,7 @@ pub struct Player {
struct PlayerInternal {
session: Session,
config: PlayerConfig,
commands: std::sync::mpsc::Receiver<PlayerCommand>,
state: PlayerState,
@ -39,8 +41,11 @@ enum PlayerCommand {
}
impl Player {
pub fn new<F>(session: Session, audio_filter: Option<Box<AudioFilter + Send>>, sink_builder: F) -> Player
where F: FnOnce() -> Box<Sink> + Send + 'static {
pub fn new<F>(config: PlayerConfig, session: Session,
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();
thread::spawn(move || {
@ -48,6 +53,7 @@ impl Player {
let internal = PlayerInternal {
session: session,
config: config,
commands: cmd_rx,
state: PlayerState::Stopped,
@ -314,13 +320,13 @@ impl PlayerInternal {
}
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)
}
}
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)
}
}
@ -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::Bitrate160 => FileFormat::OGG_VORBIS_160,
Bitrate::Bitrate320 => FileFormat::OGG_VORBIS_320,

View file

@ -3,20 +3,17 @@ use crypto::sha1::Sha1;
use futures::sync::mpsc;
use futures::{Future, Stream, BoxFuture, IntoFuture, Poll, Async};
use std::io;
use std::result::Result;
use std::str::FromStr;
use std::sync::{RwLock, Arc, Weak};
use tokio_core::io::EasyBuf;
use tokio_core::reactor::{Handle, Remote};
use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
use uuid::Uuid;
use apresolve::apresolve_or_fallback;
use authentication::Credentials;
use cache::Cache;
use component::Lazy;
use connection;
use version;
use config::SessionConfig;
use audio_key::AudioKeyManager;
use channel::ChannelManager;
@ -24,53 +21,13 @@ use mercury::MercuryManager;
use metadata::MetadataManager;
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 {
country: String,
canonical_username: String,
}
pub struct SessionInternal {
config: Config,
config: SessionConfig,
data: RwLock<SessionData>,
tx_connection: mpsc::UnboundedSender<(u8, Vec<u8>)>,
@ -99,7 +56,7 @@ pub fn device_id(name: &str) -> String {
}
impl Session {
pub fn connect(config: Config, credentials: Credentials,
pub fn connect(config: SessionConfig, credentials: Credentials,
cache: Option<Cache>, handle: Handle)
-> Box<Future<Item=Session, Error=io::Error>>
{
@ -135,7 +92,7 @@ impl Session {
}
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>)
{
let (sink, stream) = transport.split();
@ -240,7 +197,7 @@ impl Session {
self.0.cache.as_ref()
}
pub fn config(&self) -> &Config {
pub fn config(&self) -> &SessionConfig {
&self.0.config
}

View file

@ -5,9 +5,10 @@ use futures::sync::{oneshot, mpsc};
use futures::{Future, Stream, Sink, Async, Poll, BoxFuture};
use protobuf::{self, Message};
use config::ConnectConfig;
use mercury::MercuryError;
use player::Player;
use mixer::Mixer;
use player::Player;
use session::Session;
use util::{now_ms, SpotifyId, SeqGenerator};
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(), {
sw_version: version::version_string(),
is_active: false,
can_play: true,
volume: volume as u32,
name: name,
name: config.name,
capabilities => [
@{
typ: protocol::spirc::CapabilityType::kCanBePlayer,
@ -73,7 +74,7 @@ fn initial_device_state(name: String, volume: u16) -> DeviceState {
},
@{
typ: protocol::spirc::CapabilityType::kDeviceType,
intValue => [5]
intValue => [config.device_type as i64]
},
@{
typ: protocol::spirc::CapabilityType::kGaiaEqConnectId,
@ -118,7 +119,7 @@ fn initial_device_state(name: String, volume: u16) -> DeviceState {
}
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)
{
debug!("new Spirc[{}]", session.session_id());
@ -141,7 +142,7 @@ impl Spirc {
let (cmd_tx, cmd_rx) = mpsc::unbounded();
let volume = 0xFFFF;
let device = initial_device_state(name, volume);
let device = initial_device_state(config, volume);
mixer.set_volume(volume);
let mut task = SpircTask {