mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge branch 'master' into volume-normalization
This commit is contained in:
commit
89df82bd03
27 changed files with 826 additions and 583 deletions
14
.travis.yml
14
.travis.yml
|
@ -1,6 +1,6 @@
|
||||||
language: rust
|
language: rust
|
||||||
rust:
|
rust:
|
||||||
- 1.17.0
|
- 1.18.0
|
||||||
- stable
|
- stable
|
||||||
- beta
|
- beta
|
||||||
- nightly
|
- nightly
|
||||||
|
@ -24,13 +24,17 @@ before_script:
|
||||||
script:
|
script:
|
||||||
- cargo build --no-default-features
|
- cargo build --no-default-features
|
||||||
- cargo build --no-default-features --features "with-tremor"
|
- cargo build --no-default-features --features "with-tremor"
|
||||||
|
- cargo build --no-default-features --features "with-lewton";
|
||||||
- cargo build --no-default-features --features "portaudio-backend"
|
- cargo build --no-default-features --features "portaudio-backend"
|
||||||
- cargo build --no-default-features --features "pulseaudio-backend"
|
- cargo build --no-default-features --features "pulseaudio-backend"
|
||||||
- cargo build --no-default-features --features "alsa-backend"
|
- cargo build --no-default-features --features "alsa-backend"
|
||||||
- cargo build --no-default-features --target armv7-unknown-linux-gnueabihf
|
- cargo build --no-default-features --target armv7-unknown-linux-gnueabihf
|
||||||
- if [[ $TRAVIS_RUST_VERSION != *"1.17.0"* ]]; then
|
|
||||||
cargo build --no-default-features --features "with-lewton";
|
|
||||||
fi
|
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
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
|
||||||
|
|
606
Cargo.lock
generated
606
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
11
Cargo.toml
11
Cargo.toml
|
@ -46,12 +46,15 @@ serde = "0.9.6"
|
||||||
serde_derive = "0.9.6"
|
serde_derive = "0.9.6"
|
||||||
serde_json = "0.9.5"
|
serde_json = "0.9.5"
|
||||||
tokio-core = "0.1.2"
|
tokio-core = "0.1.2"
|
||||||
|
tokio-io = "0.1"
|
||||||
tokio-signal = "0.1.2"
|
tokio-signal = "0.1.2"
|
||||||
url = "1.3"
|
url = "1.3"
|
||||||
|
|
||||||
alsa = { git = "https://github.com/plietar/rust-alsa", optional = true }
|
alsa = { git = "https://github.com/plietar/rust-alsa", optional = true }
|
||||||
portaudio-rs = { version = "0.3.0", optional = true }
|
portaudio-rs = { version = "0.3.0", optional = true }
|
||||||
libpulse-sys = { version = "0.0.0", optional = true }
|
libpulse-sys = { version = "0.0.0", optional = true }
|
||||||
|
libc = { version = "0.2", optional = true }
|
||||||
|
dns-sd = { version = "0.1.3", optional = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
rand = "0.3.13"
|
rand = "0.3.13"
|
||||||
|
@ -61,16 +64,18 @@ protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros", fea
|
||||||
[features]
|
[features]
|
||||||
alsa-backend = ["alsa"]
|
alsa-backend = ["alsa"]
|
||||||
portaudio-backend = ["portaudio-rs"]
|
portaudio-backend = ["portaudio-rs"]
|
||||||
pulseaudio-backend = ["libpulse-sys"]
|
pulseaudio-backend = ["libpulse-sys", "libc"]
|
||||||
|
|
||||||
with-tremor = ["librespot-audio/with-tremor"]
|
with-tremor = ["librespot-audio/with-tremor"]
|
||||||
with-lewton = ["librespot-audio/with-lewton"]
|
with-lewton = ["librespot-audio/with-lewton"]
|
||||||
|
|
||||||
|
with-dns-sd = ["dns-sd"]
|
||||||
|
|
||||||
default = ["portaudio-backend"]
|
default = ["portaudio-backend"]
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
maintainer = "nobody"
|
maintainer = "librespot-org"
|
||||||
copyright = "2016 Paul Liétar"
|
copyright = "2018 Paul Liétar"
|
||||||
license_file = ["LICENSE", "4"]
|
license_file = ["LICENSE", "4"]
|
||||||
depends = "$auto"
|
depends = "$auto"
|
||||||
extended_description = """\
|
extended_description = """\
|
||||||
|
|
97
README.md
97
README.md
|
@ -1,21 +1,24 @@
|
||||||
|
[![Build Status](https://travis-ci.org/librespot-org/librespot.svg?branch=master)](https://travis-ci.org/librespot-org/librespot)
|
||||||
|
[![Gitter chat](https://badges.gitter.im/librespot-org/librespot.png)](https://gitter.im/sashahilton00/spotify-connect-resources)
|
||||||
|
|
||||||
# librespot
|
# librespot
|
||||||
*librespot* is an open source client library for Spotify. It enables
|
*librespot* is an open source client library for Spotify. It enables
|
||||||
applications to use Spotify's service, without using the official but
|
applications to use Spotify's service, without using the official but
|
||||||
closed-source libspotify. Additionally, it will provide extra features
|
closed-source libspotify. Additionally, it will provide extra features
|
||||||
which are not available in the official library.
|
which are not available in the official library.
|
||||||
|
|
||||||
Note: librespot only works with Spotify Premium.
|
Note: librespot only works with Spotify Premium
|
||||||
|
|
||||||
# Unmaintained
|
## This fork
|
||||||
Unfortunately I am unable to maintain librespot anymore. It should still work,
|
As the origin by [plietar](https://github.com/plietar/) is no longer actively maintained, this organisation and repository have been set up so that the project may be maintained and upgraded in the future.
|
||||||
but issues and Pull requests will be ignored. Feel free to fork it and continue
|
|
||||||
development there. If a fork gains traction I will happily point to it from the
|
|
||||||
README.
|
|
||||||
|
|
||||||
## Building
|
# Wiki
|
||||||
Rust 1.17.0 or later is required to build librespot.
|
More information can be found in the [wiki](https://github.com/librespot-org/librespot/wiki)
|
||||||
|
|
||||||
**If you are building librespot on macOS, the homebrew provided rust may fail due to the way in which homebrew installs rust. In this case, uninstall the homebrew version of rust and use [rustup](https://www.rustup.rs/), and librespot should then build.**
|
# Building
|
||||||
|
Rust 1.18.0 or later is required to build librespot.
|
||||||
|
|
||||||
|
**If you are building librespot on macOS, the homebrew provided rust may fail due to the way in which homebrew installs rust. In this case, uninstall the homebrew version of rust and use [rustup](https://www.rustup.rs/), and librespot should then build. This should have been fixed in more recent versions of Homebrew, but we're leaving this notice here as a warning.**
|
||||||
|
|
||||||
It also requires a C, with portaudio.
|
It also requires a C, with portaudio.
|
||||||
|
|
||||||
|
@ -43,69 +46,29 @@ cargo build --release
|
||||||
A sample program implementing a headless Spotify Connect receiver is provided.
|
A sample program implementing a headless Spotify Connect receiver is provided.
|
||||||
Once you've built *librespot*, run it using :
|
Once you've built *librespot*, run it using :
|
||||||
```shell
|
```shell
|
||||||
target/release/librespot --username USERNAME --cache CACHEDIR --name DEVICENAME
|
target/release/librespot --name DEVICENAME
|
||||||
```
|
```
|
||||||
|
|
||||||
## Discovery mode
|
|
||||||
*librespot* can be run in discovery mode, in which case no password is required at startup.
|
|
||||||
For that, simply omit the `--username` argument.
|
|
||||||
|
|
||||||
## Audio Backends
|
|
||||||
*librespot* supports various audio backends. Multiple backends can be enabled at compile time by enabling the
|
|
||||||
corresponding cargo feature. By default, only PortAudio is enabled.
|
|
||||||
|
|
||||||
A specific backend can selected at runtime using the `--backend` switch.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cargo build --features portaudio-backend
|
|
||||||
target/release/librespot [...] --backend portaudio
|
|
||||||
```
|
|
||||||
|
|
||||||
The following backends are currently available :
|
|
||||||
- ALSA
|
|
||||||
- PortAudio
|
|
||||||
- PulseAudio
|
|
||||||
|
|
||||||
## Cross-compiling
|
|
||||||
A cross compilation environment is provided as a docker image.
|
|
||||||
Build the image from the root of the project with the following command :
|
|
||||||
|
|
||||||
```
|
|
||||||
$ docker build -t librespot-cross -f contrib/Dockerfile .
|
|
||||||
```
|
|
||||||
|
|
||||||
The resulting image can be used to build librespot for linux x86_64, armhf (compatible e. g. with Raspberry Pi 2 or 3, but not with Raspberry Pi 1 or Zero) and armel.
|
|
||||||
The compiled binaries will be located in /tmp/librespot-build
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run -v /tmp/librespot-build:/build librespot-cross
|
|
||||||
```
|
|
||||||
|
|
||||||
If only one architecture is desired, cargo can be invoked directly with the appropriate options :
|
|
||||||
```shell
|
|
||||||
docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features alsa-backend
|
|
||||||
docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend
|
|
||||||
docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
Don't forget to set the `with-tremor` feature flag if your target device does not have floating-point capabilities.
|
|
||||||
|
|
||||||
## Development
|
|
||||||
When developing *librespot*, it is preferable to use Rust nightly, and build it using the following :
|
|
||||||
```shell
|
|
||||||
cargo build --no-default-features --features "nightly portaudio-backend"
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces better compilation error messages than with the default configuration.
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
Using this code to connect to Spotify's API is probably forbidden by them.
|
|
||||||
Use at your own risk.
|
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
Come and hang out on gitter if you need help or want to offer some.
|
Come and hang out on gitter if you need help or want to offer some.
|
||||||
https://gitter.im/sashahilton00/spotify-connect-resources
|
https://gitter.im/sashahilton00/spotify-connect-resources
|
||||||
|
|
||||||
|
## To-Do/Feature Requests
|
||||||
|
If there is a feature request that is being considered, or has been widely requested, it should be listed below. Please do not use this for bug reports or special use case feature requests.
|
||||||
|
|
||||||
|
- [ ] Add support for contexts (used by dynamic playlists, Spotify Radio, green now-playing bar, etc.) ([#57](https://github.com/librespot-org/librespot/issues/57))
|
||||||
|
- [ ] Document the Spotify Protocol and provide reference example.
|
||||||
|
- [ ] Implement API to allow wrappers to be written for librespot.
|
||||||
|
- [x] Logarithmic volume scaling ([#10](https://github.com/librespot-org/librespot/issues/10))
|
||||||
|
- [ ] Fix Shuffle & Repeat functionality
|
||||||
|
- [ ] Provide automatic release binaries for download
|
||||||
|
- [ ] Provide an adequate method for exporting metadata ([#7](https://github.com/librespot-org/librespot/issues/7))
|
||||||
|
- [ ] Provide API Documentation
|
||||||
|
- [ ] Provide Schema/Versioning
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
Using this code to connect to Spotify's API is probably forbidden by them.
|
||||||
|
Use at your own risk.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Everything in this repository is licensed under the MIT license.
|
Everything in this repository is licensed under the MIT license.
|
||||||
|
|
||||||
|
|
|
@ -340,7 +340,7 @@ impl Seek for AudioFileStreaming {
|
||||||
// Notify the fetch thread to get the correct block
|
// Notify the fetch thread to get the correct block
|
||||||
// This can fail if fetch thread has completed, in which case the
|
// This can fail if fetch thread has completed, in which case the
|
||||||
// block is ready. Just ignore the error.
|
// block is ready. Just ignore the error.
|
||||||
let _ = self.seek.send(self.position);
|
let _ = self.seek.unbounded_send(self.position);
|
||||||
Ok(self.position)
|
Ok(self.position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ path = "../protocol"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.5.0"
|
base64 = "0.5.0"
|
||||||
byteorder = "1.0"
|
byteorder = "1.0"
|
||||||
|
bytes = "0.4"
|
||||||
error-chain = { version = "0.9.0", default_features = false }
|
error-chain = { version = "0.9.0", default_features = false }
|
||||||
futures = "0.1.8"
|
futures = "0.1.8"
|
||||||
hyper = "0.11.2"
|
hyper = "0.11.2"
|
||||||
|
@ -27,6 +28,7 @@ serde_derive = "0.9.6"
|
||||||
serde_json = "0.9.5"
|
serde_json = "0.9.5"
|
||||||
shannon = "0.2.0"
|
shannon = "0.2.0"
|
||||||
tokio-core = "0.1.2"
|
tokio-core = "0.1.2"
|
||||||
|
tokio-io = "0.1"
|
||||||
uuid = { version = "0.4", features = ["v4"] }
|
uuid = { version = "0.4", features = ["v4"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|
|
@ -39,5 +39,7 @@ pub fn build_id() -> &'static str {{
|
||||||
protobuf_macros::expand("src/lib.in.rs", &out.join("lib.rs")).unwrap();
|
protobuf_macros::expand("src/lib.in.rs", &out.join("lib.rs")).unwrap();
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=src/lib.in.rs");
|
println!("cargo:rerun-if-changed=src/lib.in.rs");
|
||||||
println!("cargo:rerun-if-changed=src/connection");
|
println!("cargo:rerun-if-changed=src/connection/mod.rs");
|
||||||
|
println!("cargo:rerun-if-changed=src/connection/codec.rs");
|
||||||
|
println!("cargo:rerun-if-changed=src/connection/handshake.rs");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||||
|
use bytes::Bytes;
|
||||||
use futures::sync::oneshot;
|
use futures::sync::oneshot;
|
||||||
use futures::{Async, Future, Poll};
|
use futures::{Async, Future, Poll};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tokio_core::io::EasyBuf;
|
|
||||||
|
|
||||||
use util::SeqGenerator;
|
use util::SeqGenerator;
|
||||||
use util::{SpotifyId, FileId};
|
use util::{SpotifyId, FileId};
|
||||||
|
@ -22,8 +22,8 @@ component! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioKeyManager {
|
impl AudioKeyManager {
|
||||||
pub fn dispatch(&self, cmd: u8, mut data: EasyBuf) {
|
pub fn dispatch(&self, cmd: u8, mut data: Bytes) {
|
||||||
let seq = BigEndian::read_u32(data.drain_to(4).as_ref());
|
let seq = BigEndian::read_u32(data.split_to(4).as_ref());
|
||||||
|
|
||||||
let sender = self.lock(|inner| inner.pending.remove(&seq));
|
let sender = self.lock(|inner| inner.pending.remove(&seq));
|
||||||
|
|
||||||
|
@ -32,11 +32,11 @@ impl AudioKeyManager {
|
||||||
0xd => {
|
0xd => {
|
||||||
let mut key = [0u8; 16];
|
let mut key = [0u8; 16];
|
||||||
key.copy_from_slice(data.as_ref());
|
key.copy_from_slice(data.as_ref());
|
||||||
sender.complete(Ok(AudioKey(key)));
|
let _ = sender.send(Ok(AudioKey(key)));
|
||||||
}
|
}
|
||||||
0xe => {
|
0xe => {
|
||||||
warn!("error audio key {:x} {:x}", data.as_ref()[0], data.as_ref()[1]);
|
warn!("error audio key {:x} {:x}", data.as_ref()[0], data.as_ref()[1]);
|
||||||
sender.complete(Err(AudioKeyError));
|
let _ = sender.send(Err(AudioKeyError));
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
|
use bytes::Bytes;
|
||||||
use futures::sync::{BiLock, mpsc};
|
use futures::sync::{BiLock, mpsc};
|
||||||
use futures::{Poll, Async, Stream};
|
use futures::{Poll, Async, Stream};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tokio_core::io::EasyBuf;
|
|
||||||
|
|
||||||
use util::SeqGenerator;
|
use util::SeqGenerator;
|
||||||
|
|
||||||
component! {
|
component! {
|
||||||
ChannelManager : ChannelManagerInner {
|
ChannelManager : ChannelManagerInner {
|
||||||
sequence: SeqGenerator<u16> = SeqGenerator::new(0),
|
sequence: SeqGenerator<u16> = SeqGenerator::new(0),
|
||||||
channels: HashMap<u16, mpsc::UnboundedSender<(u8, EasyBuf)>> = HashMap::new(),
|
channels: HashMap<u16, mpsc::UnboundedSender<(u8, Bytes)>> = HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ component! {
|
||||||
pub struct ChannelError;
|
pub struct ChannelError;
|
||||||
|
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
receiver: mpsc::UnboundedReceiver<(u8, EasyBuf)>,
|
receiver: mpsc::UnboundedReceiver<(u8, Bytes)>,
|
||||||
state: ChannelState,
|
state: ChannelState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,12 +26,12 @@ pub struct ChannelData(BiLock<Channel>);
|
||||||
|
|
||||||
pub enum ChannelEvent {
|
pub enum ChannelEvent {
|
||||||
Header(u8, Vec<u8>),
|
Header(u8, Vec<u8>),
|
||||||
Data(EasyBuf),
|
Data(Bytes),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum ChannelState {
|
enum ChannelState {
|
||||||
Header(EasyBuf),
|
Header(Bytes),
|
||||||
Data,
|
Data,
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
@ -48,27 +48,27 @@ impl ChannelManager {
|
||||||
|
|
||||||
let channel = Channel {
|
let channel = Channel {
|
||||||
receiver: rx,
|
receiver: rx,
|
||||||
state: ChannelState::Header(EasyBuf::new()),
|
state: ChannelState::Header(Bytes::new()),
|
||||||
};
|
};
|
||||||
|
|
||||||
(seq, channel)
|
(seq, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch(&self, cmd: u8, mut data: EasyBuf) {
|
pub fn dispatch(&self, cmd: u8, mut data: Bytes) {
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
|
|
||||||
let id: u16 = BigEndian::read_u16(data.drain_to(2).as_ref());
|
let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref());
|
||||||
|
|
||||||
self.lock(|inner| {
|
self.lock(|inner| {
|
||||||
if let Entry::Occupied(entry) = inner.channels.entry(id) {
|
if let Entry::Occupied(entry) = inner.channels.entry(id) {
|
||||||
let _ = entry.get().send((cmd, data));
|
let _ = entry.get().unbounded_send((cmd, data));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
fn recv_packet(&mut self) -> Poll<EasyBuf, ChannelError> {
|
fn recv_packet(&mut self) -> Poll<Bytes, ChannelError> {
|
||||||
let (cmd, packet) = match self.receiver.poll() {
|
let (cmd, packet) = match self.receiver.poll() {
|
||||||
Ok(Async::Ready(t)) => t.expect("channel closed"),
|
Ok(Async::Ready(t)) => t.expect("channel closed"),
|
||||||
Ok(Async::NotReady) => return Ok(Async::NotReady),
|
Ok(Async::NotReady) => return Ok(Async::NotReady),
|
||||||
|
@ -107,13 +107,13 @@ impl Stream for Channel {
|
||||||
data = try_ready!(self.recv_packet());
|
data = try_ready!(self.recv_packet());
|
||||||
}
|
}
|
||||||
|
|
||||||
let length = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize;
|
let length = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
assert_eq!(data.len(), 0);
|
assert_eq!(data.len(), 0);
|
||||||
self.state = ChannelState::Data;
|
self.state = ChannelState::Data;
|
||||||
} else {
|
} else {
|
||||||
let header_id = data.drain_to(1).as_ref()[0];
|
let header_id = data.split_to(1).as_ref()[0];
|
||||||
let header_data = data.drain_to(length - 1).as_ref().to_owned();
|
let header_data = data.split_to(length - 1).as_ref().to_owned();
|
||||||
|
|
||||||
self.state = ChannelState::Header(data);
|
self.state = ChannelState::Header(data);
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ impl Stream for Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for ChannelData {
|
impl Stream for ChannelData {
|
||||||
type Item = EasyBuf;
|
type Item = Bytes;
|
||||||
type Error = ChannelError;
|
type Error = ChannelError;
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||||
|
|
|
@ -127,4 +127,5 @@ impl Default for PlayerConfig {
|
||||||
pub struct ConnectConfig {
|
pub struct ConnectConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub device_type: DeviceType,
|
pub device_type: DeviceType,
|
||||||
|
pub volume: i32,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
|
use bytes::{Bytes, BytesMut, BufMut};
|
||||||
use shannon::Shannon;
|
use shannon::Shannon;
|
||||||
use std::io;
|
use std::io;
|
||||||
use tokio_core::io::{Codec, EasyBuf};
|
use tokio_io::codec::{Decoder, Encoder};
|
||||||
|
|
||||||
const HEADER_SIZE: usize = 3;
|
const HEADER_SIZE: usize = 3;
|
||||||
const MAC_SIZE: usize = 4;
|
const MAC_SIZE: usize = 4;
|
||||||
|
@ -34,16 +35,17 @@ impl APCodec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Codec for APCodec {
|
impl Encoder for APCodec {
|
||||||
type Out = (u8, Vec<u8>);
|
type Item = (u8, Vec<u8>);
|
||||||
type In = (u8, EasyBuf);
|
type Error = io::Error;
|
||||||
|
|
||||||
fn encode(&mut self, item: (u8, Vec<u8>), buf: &mut Vec<u8>) -> io::Result<()> {
|
fn encode(&mut self, item: (u8, Vec<u8>), buf: &mut BytesMut) -> io::Result<()> {
|
||||||
let (cmd, payload) = item;
|
let (cmd, payload) = item;
|
||||||
let offset = buf.len();
|
let offset = buf.len();
|
||||||
|
|
||||||
buf.write_u8(cmd).unwrap();
|
buf.reserve(3 + payload.len());
|
||||||
buf.write_u16::<BigEndian>(payload.len() as u16).unwrap();
|
buf.put_u8(cmd);
|
||||||
|
buf.put_u16::<BigEndian>(payload.len() as u16);
|
||||||
buf.extend_from_slice(&payload);
|
buf.extend_from_slice(&payload);
|
||||||
|
|
||||||
self.encode_cipher.nonce_u32(self.encode_nonce);
|
self.encode_cipher.nonce_u32(self.encode_nonce);
|
||||||
|
@ -57,12 +59,17 @@ impl Codec for APCodec {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn decode(&mut self, buf: &mut EasyBuf) -> io::Result<Option<(u8, EasyBuf)>> {
|
impl Decoder for APCodec {
|
||||||
|
type Item = (u8, Bytes);
|
||||||
|
type Error = io::Error;
|
||||||
|
|
||||||
|
fn decode(&mut self, buf: &mut BytesMut) -> io::Result<Option<(u8, Bytes)>> {
|
||||||
if let DecodeState::Header = self.decode_state {
|
if let DecodeState::Header = self.decode_state {
|
||||||
if buf.len() >= HEADER_SIZE {
|
if buf.len() >= HEADER_SIZE {
|
||||||
let mut header = [0u8; HEADER_SIZE];
|
let mut header = [0u8; HEADER_SIZE];
|
||||||
header.copy_from_slice(buf.drain_to(HEADER_SIZE).as_slice());
|
header.copy_from_slice(buf.split_to(HEADER_SIZE).as_ref());
|
||||||
|
|
||||||
self.decode_cipher.nonce_u32(self.decode_nonce);
|
self.decode_cipher.nonce_u32(self.decode_nonce);
|
||||||
self.decode_nonce += 1;
|
self.decode_nonce += 1;
|
||||||
|
@ -79,13 +86,13 @@ impl Codec for APCodec {
|
||||||
if buf.len() >= size + MAC_SIZE {
|
if buf.len() >= size + MAC_SIZE {
|
||||||
self.decode_state = DecodeState::Header;
|
self.decode_state = DecodeState::Header;
|
||||||
|
|
||||||
let mut payload = buf.drain_to(size + MAC_SIZE);
|
let mut payload = buf.split_to(size + MAC_SIZE);
|
||||||
|
|
||||||
self.decode_cipher.decrypt(&mut payload.get_mut()[..size]);
|
self.decode_cipher.decrypt(&mut payload.get_mut(..size).unwrap());
|
||||||
let mac = payload.split_off(size);
|
let mac = payload.split_off(size);
|
||||||
self.decode_cipher.check_mac(mac.as_slice())?;
|
self.decode_cipher.check_mac(mac.as_ref())?;
|
||||||
|
|
||||||
return Ok(Some((cmd, payload)));
|
return Ok(Some((cmd, payload.freeze())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,11 @@ use crypto::hmac::Hmac;
|
||||||
use crypto::mac::Mac;use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
use crypto::mac::Mac;use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||||
use protobuf::{self, Message, MessageStatic};
|
use protobuf::{self, Message, MessageStatic};
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use tokio_core::io::{Io, Framed, write_all, WriteAll, read_exact, ReadExact, Window};
|
use tokio_io::{AsyncRead, AsyncWrite};
|
||||||
|
use tokio_io::codec::Framed;
|
||||||
|
use tokio_io::io::{write_all, WriteAll, read_exact, ReadExact, Window};
|
||||||
use futures::{Poll, Async, Future};
|
use futures::{Poll, Async, Future};
|
||||||
|
|
||||||
use diffie_hellman::DHLocalKeys;
|
use diffie_hellman::DHLocalKeys;
|
||||||
|
@ -25,7 +27,7 @@ enum HandshakeState<T> {
|
||||||
ClientResponse(Option<APCodec>, WriteAll<T, Vec<u8>>),
|
ClientResponse(Option<APCodec>, WriteAll<T, Vec<u8>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handshake<T: Io>(connection: T) -> Handshake<T> {
|
pub fn handshake<T: AsyncRead + AsyncWrite>(connection: T) -> Handshake<T> {
|
||||||
let local_keys = DHLocalKeys::random(&mut thread_rng());
|
let local_keys = DHLocalKeys::random(&mut thread_rng());
|
||||||
let client_hello = client_hello(connection, local_keys.public_key());
|
let client_hello = client_hello(connection, local_keys.public_key());
|
||||||
|
|
||||||
|
@ -35,7 +37,7 @@ pub fn handshake<T: Io>(connection: T) -> Handshake<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <T: Io> Future for Handshake<T> {
|
impl <T: AsyncRead + AsyncWrite> Future for Handshake<T> {
|
||||||
type Item = Framed<T, APCodec>;
|
type Item = Framed<T, APCodec>;
|
||||||
type Error = io::Error;
|
type Error = io::Error;
|
||||||
|
|
||||||
|
@ -78,7 +80,7 @@ impl <T: Io> Future for Handshake<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_hello<T: Write>(connection: T, gc: Vec<u8>) -> WriteAll<T, Vec<u8>> {
|
fn client_hello<T: AsyncWrite>(connection: T, gc: Vec<u8>) -> WriteAll<T, Vec<u8>> {
|
||||||
let packet = protobuf_init!(ClientHello::new(), {
|
let packet = protobuf_init!(ClientHello::new(), {
|
||||||
build_info => {
|
build_info => {
|
||||||
product: protocol::keyexchange::Product::PRODUCT_PARTNER,
|
product: protocol::keyexchange::Product::PRODUCT_PARTNER,
|
||||||
|
@ -104,7 +106,7 @@ fn client_hello<T: Write>(connection: T, gc: Vec<u8>) -> WriteAll<T, Vec<u8>> {
|
||||||
write_all(connection, buffer)
|
write_all(connection, buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_response<T: Write>(connection: T, challenge: Vec<u8>) -> WriteAll<T, Vec<u8>> {
|
fn client_response<T: AsyncWrite>(connection: T, challenge: Vec<u8>) -> WriteAll<T, Vec<u8>> {
|
||||||
let packet = protobuf_init!(ClientResponsePlaintext::new(), {
|
let packet = protobuf_init!(ClientResponsePlaintext::new(), {
|
||||||
login_crypto_response.diffie_hellman => {
|
login_crypto_response.diffie_hellman => {
|
||||||
hmac: challenge
|
hmac: challenge
|
||||||
|
@ -126,14 +128,14 @@ enum RecvPacket<T, M: MessageStatic> {
|
||||||
Body(ReadExact<T, Window<Vec<u8>>>, PhantomData<M>),
|
Body(ReadExact<T, Window<Vec<u8>>>, PhantomData<M>),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recv_packet<T, M>(connection: T, acc: Vec<u8>) -> RecvPacket<T, M>
|
fn recv_packet<T: AsyncRead, M>(connection: T, acc: Vec<u8>) -> RecvPacket<T, M>
|
||||||
where T: Read,
|
where T: Read,
|
||||||
M: MessageStatic
|
M: MessageStatic
|
||||||
{
|
{
|
||||||
RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData)
|
RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <T, M> Future for RecvPacket<T, M>
|
impl <T: AsyncRead, M> Future for RecvPacket<T, M>
|
||||||
where T: Read,
|
where T: Read,
|
||||||
M: MessageStatic
|
M: MessageStatic
|
||||||
{
|
{
|
||||||
|
@ -165,7 +167,7 @@ impl <T, M> Future for RecvPacket<T, M>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_into_accumulator<T: Read>(connection: T, size: usize, mut acc: Vec<u8>) -> ReadExact<T, Window<Vec<u8>>> {
|
fn read_into_accumulator<T: AsyncRead>(connection: T, size: usize, mut acc: Vec<u8>) -> ReadExact<T, Window<Vec<u8>>> {
|
||||||
let offset = acc.len();
|
let offset = acc.len();
|
||||||
acc.resize(offset + size, 0);
|
acc.resize(offset + size, 0);
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@ mod handshake;
|
||||||
pub use self::codec::APCodec;
|
pub use self::codec::APCodec;
|
||||||
pub use self::handshake::handshake;
|
pub use self::handshake::handshake;
|
||||||
|
|
||||||
use futures::{Future, Sink, Stream, BoxFuture};
|
use futures::{Future, Sink, Stream};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
use tokio_core::net::TcpStream;
|
use tokio_core::net::TcpStream;
|
||||||
use tokio_core::reactor::Handle;
|
use tokio_core::reactor::Handle;
|
||||||
use tokio_core::io::Framed;
|
use tokio_io::codec::Framed;
|
||||||
use protobuf::{self, Message};
|
use protobuf::{self, Message};
|
||||||
|
|
||||||
use authentication::Credentials;
|
use authentication::Credentials;
|
||||||
|
@ -17,18 +17,18 @@ use version;
|
||||||
|
|
||||||
pub type Transport = Framed<TcpStream, APCodec>;
|
pub type Transport = Framed<TcpStream, APCodec>;
|
||||||
|
|
||||||
pub fn connect<A: ToSocketAddrs>(addr: A, handle: &Handle) -> BoxFuture<Transport, io::Error> {
|
pub fn connect<A: ToSocketAddrs>(addr: A, handle: &Handle) -> Box<Future<Item = Transport, Error = io::Error>> {
|
||||||
let addr = addr.to_socket_addrs().unwrap().next().unwrap();
|
let addr = addr.to_socket_addrs().unwrap().next().unwrap();
|
||||||
let socket = TcpStream::connect(&addr, handle);
|
let socket = TcpStream::connect(&addr, handle);
|
||||||
let connection = socket.and_then(|socket| {
|
let connection = socket.and_then(|socket| {
|
||||||
handshake(socket)
|
handshake(socket)
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.boxed()
|
Box::new(connection)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn authenticate(transport: Transport, credentials: Credentials, device_id: String)
|
pub fn authenticate(transport: Transport, credentials: Credentials, device_id: String)
|
||||||
-> BoxFuture<(Transport, Credentials), io::Error>
|
-> Box<Future<Item = (Transport, Credentials), Error = io::Error>>
|
||||||
{
|
{
|
||||||
use protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
|
use protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ pub fn authenticate(transport: Transport, credentials: Credentials, device_id: S
|
||||||
let cmd = 0xab;
|
let cmd = 0xab;
|
||||||
let data = packet.write_to_bytes().unwrap();
|
let data = packet.write_to_bytes().unwrap();
|
||||||
|
|
||||||
transport.send((cmd, data)).and_then(|transport| {
|
Box::new(transport.send((cmd, data)).and_then(|transport| {
|
||||||
transport.into_future().map_err(|(err, _stream)| err)
|
transport.into_future().map_err(|(err, _stream)| err)
|
||||||
}).and_then(|(packet, transport)| {
|
}).and_then(|(packet, transport)| {
|
||||||
match packet {
|
match packet {
|
||||||
|
@ -71,5 +71,5 @@ pub fn authenticate(transport: Transport, credentials: Credentials, device_id: S
|
||||||
Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd),
|
Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd),
|
||||||
None => panic!("EOF"),
|
None => panic!("EOF"),
|
||||||
}
|
}
|
||||||
}).boxed()
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
#[allow(unused_mut)]
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))]
|
#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))]
|
||||||
|
|
||||||
// TODO: many items from tokio-core::io have been deprecated in favour of tokio-io
|
|
||||||
#![allow(deprecated)]
|
|
||||||
|
|
||||||
#[macro_use] extern crate error_chain;
|
#[macro_use] extern crate error_chain;
|
||||||
#[macro_use] extern crate futures;
|
#[macro_use] extern crate futures;
|
||||||
#[macro_use] extern crate lazy_static;
|
#[macro_use] extern crate lazy_static;
|
||||||
|
@ -11,6 +8,7 @@
|
||||||
|
|
||||||
extern crate base64;
|
extern crate base64;
|
||||||
extern crate byteorder;
|
extern crate byteorder;
|
||||||
|
extern crate bytes;
|
||||||
extern crate crypto;
|
extern crate crypto;
|
||||||
extern crate hyper;
|
extern crate hyper;
|
||||||
extern crate num_bigint;
|
extern crate num_bigint;
|
||||||
|
@ -23,6 +21,7 @@ extern crate serde;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
extern crate shannon;
|
extern crate shannon;
|
||||||
extern crate tokio_core;
|
extern crate tokio_core;
|
||||||
|
extern crate tokio_io;
|
||||||
extern crate uuid;
|
extern crate uuid;
|
||||||
|
|
||||||
extern crate librespot_protocol as protocol;
|
extern crate librespot_protocol as protocol;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
|
use bytes::Bytes;
|
||||||
use futures::sync::{oneshot, mpsc};
|
use futures::sync::{oneshot, mpsc};
|
||||||
use futures::{Async, Poll, BoxFuture, Future};
|
use futures::{Async, Poll, Future};
|
||||||
use protobuf;
|
use protobuf;
|
||||||
use protocol;
|
use protocol;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use tokio_core::io::EasyBuf;
|
|
||||||
|
|
||||||
use util::SeqGenerator;
|
use util::SeqGenerator;
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ impl MercuryManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe<T: Into<String>>(&self, uri: T)
|
pub fn subscribe<T: Into<String>>(&self, uri: T)
|
||||||
-> BoxFuture<mpsc::UnboundedReceiver<MercuryResponse>, MercuryError>
|
-> Box<Future<Item = mpsc::UnboundedReceiver<MercuryResponse>, Error = MercuryError>>
|
||||||
{
|
{
|
||||||
let uri = uri.into();
|
let uri = uri.into();
|
||||||
let request = self.request(MercuryRequest {
|
let request = self.request(MercuryRequest {
|
||||||
|
@ -110,7 +110,7 @@ impl MercuryManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
let manager = self.clone();
|
let manager = self.clone();
|
||||||
request.map(move |response| {
|
Box::new(request.map(move |response| {
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded();
|
||||||
|
|
||||||
manager.lock(move |inner| {
|
manager.lock(move |inner| {
|
||||||
|
@ -133,15 +133,15 @@ impl MercuryManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
rx
|
rx
|
||||||
}).boxed()
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch(&self, cmd: u8, mut data: EasyBuf) {
|
pub fn dispatch(&self, cmd: u8, mut data: Bytes) {
|
||||||
let seq_len = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize;
|
let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
|
||||||
let seq = data.drain_to(seq_len).as_ref().to_owned();
|
let seq = data.split_to(seq_len).as_ref().to_owned();
|
||||||
|
|
||||||
let flags = data.drain_to(1).as_ref()[0];
|
let flags = data.split_to(1).as_ref()[0];
|
||||||
let count = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize;
|
let count = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
|
||||||
|
|
||||||
let pending = self.lock(|inner| inner.pending.remove(&seq));
|
let pending = self.lock(|inner| inner.pending.remove(&seq));
|
||||||
|
|
||||||
|
@ -181,9 +181,9 @@ impl MercuryManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_part(data: &mut EasyBuf) -> Vec<u8> {
|
fn parse_part(data: &mut Bytes) -> Vec<u8> {
|
||||||
let size = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize;
|
let size = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
|
||||||
data.drain_to(size).as_ref().to_owned()
|
data.split_to(size).as_ref().to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete_request(&self, cmd: u8, mut pending: MercuryPending) {
|
fn complete_request(&self, cmd: u8, mut pending: MercuryPending) {
|
||||||
|
@ -199,7 +199,7 @@ impl MercuryManager {
|
||||||
if response.status_code >= 400 {
|
if response.status_code >= 400 {
|
||||||
warn!("error {} for uri {}", response.status_code, &response.uri);
|
warn!("error {} for uri {}", response.status_code, &response.uri);
|
||||||
if let Some(cb) = pending.callback {
|
if let Some(cb) = pending.callback {
|
||||||
cb.complete(Err(MercuryError));
|
let _ = cb.send(Err(MercuryError));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if cmd == 0xb5 {
|
if cmd == 0xb5 {
|
||||||
|
@ -211,7 +211,7 @@ impl MercuryManager {
|
||||||
|
|
||||||
// if send fails, remove from list of subs
|
// if send fails, remove from list of subs
|
||||||
// TODO: send unsub message
|
// TODO: send unsub message
|
||||||
sub.send(response.clone()).is_ok()
|
sub.unbounded_send(response.clone()).is_ok()
|
||||||
} else {
|
} else {
|
||||||
// URI doesn't match
|
// URI doesn't match
|
||||||
true
|
true
|
||||||
|
@ -223,7 +223,7 @@ impl MercuryManager {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if let Some(cb) = pending.callback {
|
} else if let Some(cb) = pending.callback {
|
||||||
cb.complete(Ok(response));
|
let _ = cb.send(Ok(response));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
use bytes::Bytes;
|
||||||
use crypto::digest::Digest;
|
use crypto::digest::Digest;
|
||||||
use crypto::sha1::Sha1;
|
use crypto::sha1::Sha1;
|
||||||
use futures::sync::mpsc;
|
use futures::sync::mpsc;
|
||||||
use futures::{Future, Stream, BoxFuture, IntoFuture, Poll, Async};
|
use futures::{Future, Stream, IntoFuture, Poll, Async};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::sync::{RwLock, Arc, Weak};
|
use std::sync::{RwLock, Arc, Weak};
|
||||||
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};
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ impl Session {
|
||||||
|
|
||||||
fn create(handle: &Handle, transport: connection::Transport,
|
fn create(handle: &Handle, transport: connection::Transport,
|
||||||
config: SessionConfig, cache: Option<Cache>, username: String)
|
config: SessionConfig, cache: Option<Cache>, username: String)
|
||||||
-> (Session, BoxFuture<(), io::Error>)
|
-> (Session, Box<Future<Item = (), Error = io::Error>>)
|
||||||
{
|
{
|
||||||
let (sink, stream) = transport.split();
|
let (sink, stream) = transport.split();
|
||||||
|
|
||||||
|
@ -124,8 +124,8 @@ impl Session {
|
||||||
.forward(sink).map(|_| ());
|
.forward(sink).map(|_| ());
|
||||||
let receiver_task = DispatchTask(stream, session.weak());
|
let receiver_task = DispatchTask(stream, session.weak());
|
||||||
|
|
||||||
let task = (receiver_task, sender_task).into_future()
|
let task = Box::new((receiver_task, sender_task).into_future()
|
||||||
.map(|((), ())| ()).boxed();
|
.map(|((), ())| ()));
|
||||||
|
|
||||||
(session, task)
|
(session, task)
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,7 @@ impl Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
|
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
|
||||||
fn dispatch(&self, cmd: u8, data: EasyBuf) {
|
fn dispatch(&self, cmd: u8, data: Bytes) {
|
||||||
match cmd {
|
match cmd {
|
||||||
0x4 => {
|
0x4 => {
|
||||||
self.debug_info();
|
self.debug_info();
|
||||||
|
@ -177,7 +177,7 @@ impl Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_packet(&self, cmd: u8, data: Vec<u8>) {
|
pub fn send_packet(&self, cmd: u8, data: Vec<u8>) {
|
||||||
self.0.tx_connection.send((cmd, data)).unwrap();
|
self.0.tx_connection.unbounded_send((cmd, data)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cache(&self) -> Option<&Arc<Cache>> {
|
pub fn cache(&self) -> Option<&Arc<Cache>> {
|
||||||
|
@ -229,10 +229,10 @@ impl Drop for SessionInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DispatchTask<S>(S, SessionWeak)
|
struct DispatchTask<S>(S, SessionWeak)
|
||||||
where S: Stream<Item = (u8, EasyBuf)>;
|
where S: Stream<Item = (u8, Bytes)>;
|
||||||
|
|
||||||
impl <S> Future for DispatchTask<S>
|
impl <S> Future for DispatchTask<S>
|
||||||
where S: Stream<Item = (u8, EasyBuf)>
|
where S: Stream<Item = (u8, Bytes)>
|
||||||
{
|
{
|
||||||
type Item = ();
|
type Item = ();
|
||||||
type Error = S::Error;
|
type Error = S::Error;
|
||||||
|
@ -253,7 +253,7 @@ impl <S> Future for DispatchTask<S>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <S> Drop for DispatchTask<S>
|
impl <S> Drop for DispatchTask<S>
|
||||||
where S: Stream<Item = (u8, EasyBuf)>
|
where S: Stream<Item = (u8, Bytes)>
|
||||||
{
|
{
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
debug!("drop Dispatch");
|
debug!("drop Dispatch");
|
||||||
|
|
|
@ -2,6 +2,8 @@ use std;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use util::u128;
|
use util::u128;
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
|
// Unneeded since 1.21
|
||||||
|
#[allow(unused_imports)]
|
||||||
use std::ascii::AsciiExt;
|
use std::ascii::AsciiExt;
|
||||||
|
|
||||||
#[derive(Debug,Copy,Clone,PartialEq,Eq,Hash)]
|
#[derive(Debug,Copy,Clone,PartialEq,Eq,Hash)]
|
||||||
|
|
|
@ -8,7 +8,7 @@ extern crate librespot_protocol as protocol;
|
||||||
|
|
||||||
pub mod cover;
|
pub mod cover;
|
||||||
|
|
||||||
use futures::{Future, BoxFuture};
|
use futures::Future;
|
||||||
use linear_map::LinearMap;
|
use linear_map::LinearMap;
|
||||||
|
|
||||||
use core::mercury::MercuryError;
|
use core::mercury::MercuryError;
|
||||||
|
@ -57,17 +57,17 @@ pub trait Metadata : Send + Sized + 'static {
|
||||||
fn base_url() -> &'static str;
|
fn base_url() -> &'static str;
|
||||||
fn parse(msg: &Self::Message, session: &Session) -> Self;
|
fn parse(msg: &Self::Message, session: &Session) -> Self;
|
||||||
|
|
||||||
fn get(session: &Session, id: SpotifyId) -> BoxFuture<Self, MercuryError> {
|
fn get(session: &Session, id: SpotifyId) -> Box<Future<Item = Self, Error = MercuryError>> {
|
||||||
let uri = format!("{}/{}", Self::base_url(), id.to_base16());
|
let uri = format!("{}/{}", Self::base_url(), id.to_base16());
|
||||||
let request = session.mercury().get(uri);
|
let request = session.mercury().get(uri);
|
||||||
|
|
||||||
let session = session.clone();
|
let session = session.clone();
|
||||||
request.and_then(move |response| {
|
Box::new(request.and_then(move |response| {
|
||||||
let data = response.payload.first().expect("Empty payload");
|
let data = response.payload.first().expect("Empty payload");
|
||||||
let msg: Self::Message = protobuf::parse_from_bytes(data).unwrap();
|
let msg: Self::Message = protobuf::parse_from_bytes(data).unwrap();
|
||||||
|
|
||||||
Ok(Self::parse(&msg, &session))
|
Ok(Self::parse(&msg, &session))
|
||||||
}).boxed()
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,50 @@ use super::{Open, Sink};
|
||||||
use std::io;
|
use std::io;
|
||||||
use libpulse_sys::*;
|
use libpulse_sys::*;
|
||||||
use std::ptr::{null, null_mut};
|
use std::ptr::{null, null_mut};
|
||||||
use std::mem::{transmute};
|
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::mem;
|
||||||
|
use libc;
|
||||||
|
|
||||||
pub struct PulseAudioSink(*mut pa_simple);
|
pub struct PulseAudioSink {
|
||||||
|
s : *mut pa_simple,
|
||||||
|
ss : pa_sample_spec,
|
||||||
|
name : CString,
|
||||||
|
desc : CString
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call_pulseaudio<T, F, FailCheck>(f: F, fail_check: FailCheck, kind: io::ErrorKind) -> io::Result<T> where
|
||||||
|
T: Copy,
|
||||||
|
F: Fn(*mut libc::c_int) -> T,
|
||||||
|
FailCheck: Fn(T) -> bool,
|
||||||
|
{
|
||||||
|
let mut error: libc::c_int = 0;
|
||||||
|
let ret = f(&mut error);
|
||||||
|
if fail_check(ret) {
|
||||||
|
let err_cstr = unsafe { CStr::from_ptr(pa_strerror(error)) };
|
||||||
|
let errstr = err_cstr.to_string_lossy().into_owned();
|
||||||
|
Err(io::Error::new(kind, errstr))
|
||||||
|
} else {
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PulseAudioSink {
|
||||||
|
fn free_connection(&mut self) {
|
||||||
|
if self.s != null_mut() {
|
||||||
|
unsafe {
|
||||||
|
pa_simple_free(self.s);
|
||||||
|
}
|
||||||
|
self.s = null_mut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PulseAudioSink {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.free_connection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Open for PulseAudioSink {
|
impl Open for PulseAudioSink {
|
||||||
fn open(device: Option<String>) -> PulseAudioSink {
|
fn open(device: Option<String>) -> PulseAudioSink {
|
||||||
|
@ -22,46 +62,57 @@ impl Open for PulseAudioSink {
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = CString::new("librespot").unwrap();
|
let name = CString::new("librespot").unwrap();
|
||||||
let description = CString::new("A spoty client library").unwrap();
|
let description = CString::new("Spotify endpoint").unwrap();
|
||||||
|
|
||||||
let s = unsafe {
|
PulseAudioSink {
|
||||||
pa_simple_new(null(), // Use the default server.
|
s: null_mut(),
|
||||||
name.as_ptr(), // Our application's name.
|
ss: ss,
|
||||||
PA_STREAM_PLAYBACK,
|
name: name,
|
||||||
null(), // Use the default device.
|
desc: description
|
||||||
description.as_ptr(), // Description of our stream.
|
}
|
||||||
&ss, // Our sample format.
|
|
||||||
null(), // Use default channel map
|
|
||||||
null(), // Use default buffering attributes.
|
|
||||||
null_mut(), // Ignore error code.
|
|
||||||
)
|
|
||||||
};
|
|
||||||
assert!(s != null_mut());
|
|
||||||
|
|
||||||
PulseAudioSink(s)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sink for PulseAudioSink {
|
impl Sink for PulseAudioSink {
|
||||||
fn start(&mut self) -> io::Result<()> {
|
fn start(&mut self) -> io::Result<()> {
|
||||||
|
if self.s == null_mut() {
|
||||||
|
self.s = call_pulseaudio(
|
||||||
|
|err| unsafe {
|
||||||
|
pa_simple_new(null(), // Use the default server.
|
||||||
|
self.name.as_ptr(), // Our application's name.
|
||||||
|
PA_STREAM_PLAYBACK,
|
||||||
|
null(), // Use the default device.
|
||||||
|
self.desc.as_ptr(), // desc of our stream.
|
||||||
|
&self.ss, // Our sample format.
|
||||||
|
null(), // Use default channel map
|
||||||
|
null(), // Use default buffering attributes.
|
||||||
|
err)
|
||||||
|
},
|
||||||
|
|ptr| ptr == null_mut(),
|
||||||
|
io::ErrorKind::ConnectionRefused)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> io::Result<()> {
|
fn stop(&mut self) -> io::Result<()> {
|
||||||
|
self.free_connection();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&mut self, data: &[i16]) -> io::Result<()> {
|
fn write(&mut self, data: &[i16]) -> io::Result<()> {
|
||||||
unsafe {
|
if self.s == null_mut() {
|
||||||
let ptr = transmute(data.as_ptr());
|
Err(io::Error::new(io::ErrorKind::NotConnected, "Not connected to pulseaudio"))
|
||||||
let bytes = data.len() as usize * 2;
|
}
|
||||||
pa_simple_write(self.0, ptr, bytes, null_mut());
|
else {
|
||||||
};
|
let ptr = data.as_ptr() as *const libc::c_void;
|
||||||
|
let len = data.len() as usize * mem::size_of::<i16>();
|
||||||
Ok(())
|
call_pulseaudio(
|
||||||
|
|err| unsafe {
|
||||||
|
pa_simple_write(self.s, ptr, len, err)
|
||||||
|
},
|
||||||
|
|ret| ret < 0,
|
||||||
|
io::ErrorKind::BrokenPipe)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,16 +3,21 @@ use crypto::digest::Digest;
|
||||||
use crypto::mac::Mac;
|
use crypto::mac::Mac;
|
||||||
use crypto;
|
use crypto;
|
||||||
use futures::sync::mpsc;
|
use futures::sync::mpsc;
|
||||||
use futures::{Future, Stream, BoxFuture, Poll, Async};
|
use futures::{Future, Stream, Poll};
|
||||||
use hyper::server::{Service, NewService, Request, Response, Http};
|
use hyper::server::{Service, Request, Response, Http};
|
||||||
use hyper::{self, Get, Post, StatusCode};
|
use hyper::{self, Get, Post, StatusCode};
|
||||||
|
|
||||||
|
#[cfg(feature = "with-dns-sd")]
|
||||||
|
use dns_sd::DNSService;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
use mdns;
|
use mdns;
|
||||||
|
|
||||||
use num_bigint::BigUint;
|
use num_bigint::BigUint;
|
||||||
use rand;
|
use rand;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio_core::net::TcpListener;
|
|
||||||
use tokio_core::reactor::Handle;
|
use tokio_core::reactor::Handle;
|
||||||
use url;
|
use url;
|
||||||
|
|
||||||
|
@ -32,7 +37,7 @@ struct DiscoveryInner {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Discovery {
|
impl Discovery {
|
||||||
pub fn new(config: ConnectConfig, device_id: String)
|
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();
|
||||||
|
@ -136,7 +141,7 @@ impl Discovery {
|
||||||
|
|
||||||
let credentials = Credentials::with_blob(username.to_owned(), &decrypted, &self.0.device_id);
|
let credentials = Credentials::with_blob(username.to_owned(), &decrypted, &self.0.device_id);
|
||||||
|
|
||||||
self.0.tx.send(credentials).unwrap();
|
self.0.tx.unbounded_send(credentials).unwrap();
|
||||||
|
|
||||||
let result = json!({
|
let result = json!({
|
||||||
"status": 101,
|
"status": 101,
|
||||||
|
@ -159,7 +164,7 @@ impl Service for Discovery {
|
||||||
type Request = Request;
|
type Request = Request;
|
||||||
type Response = Response;
|
type Response = Response;
|
||||||
type Error = hyper::Error;
|
type Error = hyper::Error;
|
||||||
type Future = BoxFuture<Response, hyper::Error>;
|
type Future = Box<Future<Item = Response, Error = hyper::Error>>;
|
||||||
|
|
||||||
fn call(&self, request: Request) -> Self::Future {
|
fn call(&self, request: Request) -> Self::Future {
|
||||||
let mut params = BTreeMap::new();
|
let mut params = BTreeMap::new();
|
||||||
|
@ -174,7 +179,7 @@ impl Service for Discovery {
|
||||||
}
|
}
|
||||||
|
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
body.fold(Vec::new(), |mut acc, chunk| {
|
Box::new(body.fold(Vec::new(), |mut acc, chunk| {
|
||||||
acc.extend_from_slice(chunk.as_ref());
|
acc.extend_from_slice(chunk.as_ref());
|
||||||
Ok::<_, hyper::Error>(acc)
|
Ok::<_, hyper::Error>(acc)
|
||||||
}).map(move |body| {
|
}).map(move |body| {
|
||||||
|
@ -186,43 +191,61 @@ impl Service for Discovery {
|
||||||
(Post, Some("addUser")) => this.handle_add_user(¶ms),
|
(Post, Some("addUser")) => this.handle_add_user(¶ms),
|
||||||
_ => this.not_found(),
|
_ => this.not_found(),
|
||||||
}
|
}
|
||||||
}).boxed()
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewService for Discovery {
|
#[cfg(feature = "with-dns-sd")]
|
||||||
type Request = Request;
|
pub struct DiscoveryStream {
|
||||||
type Response = Response;
|
credentials: mpsc::UnboundedReceiver<Credentials>,
|
||||||
type Error = hyper::Error;
|
_svc: DNSService,
|
||||||
type Instance = Self;
|
|
||||||
|
|
||||||
fn new_service(&self) -> io::Result<Self::Instance> {
|
|
||||||
Ok(self.clone())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
pub struct DiscoveryStream {
|
pub struct DiscoveryStream {
|
||||||
credentials: mpsc::UnboundedReceiver<Credentials>,
|
credentials: mpsc::UnboundedReceiver<Credentials>,
|
||||||
_svc: mdns::Service,
|
_svc: mdns::Service,
|
||||||
task: Box<Future<Item=(), Error=io::Error>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn discovery(handle: &Handle, config: ConnectConfig, device_id: String)
|
pub fn discovery(handle: &Handle, config: ConnectConfig, device_id: String, port: u16)
|
||||||
-> io::Result<DiscoveryStream>
|
-> io::Result<DiscoveryStream>
|
||||||
{
|
{
|
||||||
let (discovery, creds_rx) = Discovery::new(config.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 serve = {
|
||||||
let addr = listener.local_addr()?;
|
let http = Http::new();
|
||||||
|
debug!("Zeroconf server listening on 0.0.0.0:{}", port);
|
||||||
|
http.serve_addr_handle(&format!("0.0.0.0:{}", port).parse().unwrap(), &handle, move || Ok(discovery.clone())).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
let http = Http::new();
|
#[cfg(feature = "with-dns-sd")]
|
||||||
let handle_ = handle.clone();
|
let port = serve.incoming_ref().local_addr().port();
|
||||||
let task = Box::new(listener.incoming().for_each(move |(socket, addr)| {
|
|
||||||
http.bind_connection(&handle_, socket, addr, discovery.clone());
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
|
let addr = serve.incoming_ref().local_addr();
|
||||||
|
|
||||||
|
let server_future = {
|
||||||
|
let handle = handle.clone();
|
||||||
|
serve.for_each(move |connection| {
|
||||||
|
handle.spawn(connection.then(|_| Ok(())));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.then(|_| Ok(()))
|
||||||
|
};
|
||||||
|
handle.spawn(server_future);
|
||||||
|
|
||||||
|
#[cfg(feature = "with-dns-sd")]
|
||||||
|
let svc = DNSService::register(Some(&*config.name),
|
||||||
|
"_spotify-connect._tcp",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
port,
|
||||||
|
&["VERSION=1.0", "CPath=/"]).unwrap();
|
||||||
|
|
||||||
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
let responder = mdns::Responder::spawn(&handle)?;
|
let responder = mdns::Responder::spawn(&handle)?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
let svc = responder.register(
|
let svc = responder.register(
|
||||||
"_spotify-connect._tcp".to_owned(),
|
"_spotify-connect._tcp".to_owned(),
|
||||||
config.name,
|
config.name,
|
||||||
|
@ -232,20 +255,14 @@ pub fn discovery(handle: &Handle, config: ConnectConfig, device_id: String)
|
||||||
Ok(DiscoveryStream {
|
Ok(DiscoveryStream {
|
||||||
credentials: creds_rx,
|
credentials: creds_rx,
|
||||||
_svc: svc,
|
_svc: svc,
|
||||||
task: task,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for DiscoveryStream {
|
impl Stream for DiscoveryStream {
|
||||||
type Item = Credentials;
|
type Item = Credentials;
|
||||||
type Error = io::Error;
|
type Error = ();
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||||
match self.task.poll()? {
|
self.credentials.poll()
|
||||||
Async::Ready(()) => unreachable!(),
|
|
||||||
Async::NotReady => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.credentials.poll().unwrap())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use futures::{Future, BoxFuture};
|
use futures::Future;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use core::mercury::MercuryError;
|
use core::mercury::MercuryError;
|
||||||
|
@ -13,14 +13,14 @@ pub struct Token {
|
||||||
pub scope: Vec<String>,
|
pub scope: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_token(session: &Session, client_id: &str, scopes: &str) -> BoxFuture<Token, MercuryError> {
|
pub fn get_token(session: &Session, client_id: &str, scopes: &str) -> Box<Future<Item = Token, Error = MercuryError>> {
|
||||||
let url = format!("hm://keymaster/token/authenticated?client_id={}&scope={}",
|
let url = format!("hm://keymaster/token/authenticated?client_id={}&scope={}",
|
||||||
client_id, scopes);
|
client_id, scopes);
|
||||||
session.mercury().get(url).map(move |response| {
|
Box::new(session.mercury().get(url).map(move |response| {
|
||||||
let data = response.payload.first().expect("Empty payload");
|
let data = response.payload.first().expect("Empty payload");
|
||||||
let data = String::from_utf8(data.clone()).unwrap();
|
let data = String::from_utf8(data.clone()).unwrap();
|
||||||
let token : Token = serde_json::from_str(&data).unwrap();
|
let token : Token = serde_json::from_str(&data).unwrap();
|
||||||
|
|
||||||
token
|
token
|
||||||
}).boxed()
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
#[allow(unused_mut)]
|
||||||
pub mod spirc;
|
pub mod spirc;
|
||||||
|
|
13
src/lib.rs
13
src/lib.rs
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))]
|
#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))]
|
||||||
|
|
||||||
// TODO: many items from tokio-core::io have been deprecated in favour of tokio-io
|
|
||||||
#![allow(deprecated)]
|
|
||||||
|
|
||||||
#[macro_use] extern crate log;
|
#[macro_use] extern crate log;
|
||||||
#[macro_use] extern crate serde_json;
|
#[macro_use] extern crate serde_json;
|
||||||
#[macro_use] extern crate serde_derive;
|
#[macro_use] extern crate serde_derive;
|
||||||
|
@ -13,7 +10,6 @@ extern crate base64;
|
||||||
extern crate crypto;
|
extern crate crypto;
|
||||||
extern crate futures;
|
extern crate futures;
|
||||||
extern crate hyper;
|
extern crate hyper;
|
||||||
extern crate mdns;
|
|
||||||
extern crate num_bigint;
|
extern crate num_bigint;
|
||||||
extern crate protobuf;
|
extern crate protobuf;
|
||||||
extern crate rand;
|
extern crate rand;
|
||||||
|
@ -34,6 +30,15 @@ extern crate portaudio_rs;
|
||||||
#[cfg(feature = "libpulse-sys")]
|
#[cfg(feature = "libpulse-sys")]
|
||||||
extern crate libpulse_sys;
|
extern crate libpulse_sys;
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
extern crate libc;
|
||||||
|
|
||||||
|
#[cfg(feature = "with-dns-sd")]
|
||||||
|
extern crate dns_sd;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
|
extern crate mdns;
|
||||||
|
|
||||||
pub mod audio_backend;
|
pub mod audio_backend;
|
||||||
pub mod discovery;
|
pub mod discovery;
|
||||||
pub mod keymaster;
|
pub mod keymaster;
|
||||||
|
|
43
src/main.rs
43
src/main.rs
|
@ -1,12 +1,10 @@
|
||||||
// TODO: many items from tokio-core::io have been deprecated in favour of tokio-io
|
|
||||||
#![allow(deprecated)]
|
|
||||||
|
|
||||||
#[macro_use] extern crate log;
|
#[macro_use] extern crate log;
|
||||||
extern crate env_logger;
|
extern crate env_logger;
|
||||||
extern crate futures;
|
extern crate futures;
|
||||||
extern crate getopts;
|
extern crate getopts;
|
||||||
extern crate librespot;
|
extern crate librespot;
|
||||||
extern crate tokio_core;
|
extern crate tokio_core;
|
||||||
|
extern crate tokio_io;
|
||||||
extern crate tokio_signal;
|
extern crate tokio_signal;
|
||||||
|
|
||||||
use env_logger::LogBuilder;
|
use env_logger::LogBuilder;
|
||||||
|
@ -17,7 +15,7 @@ use std::path::PathBuf;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tokio_core::reactor::{Handle, Core};
|
use tokio_core::reactor::{Handle, Core};
|
||||||
use tokio_core::io::IoStream;
|
use tokio_io::IoStream;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
|
||||||
use librespot::core::authentication::{get_credentials, Credentials};
|
use librespot::core::authentication::{get_credentials, Credentials};
|
||||||
|
@ -83,6 +81,7 @@ struct Setup {
|
||||||
connect_config: ConnectConfig,
|
connect_config: ConnectConfig,
|
||||||
credentials: Option<Credentials>,
|
credentials: Option<Credentials>,
|
||||||
enable_discovery: bool,
|
enable_discovery: bool,
|
||||||
|
zeroconf_port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup(args: &[String]) -> Setup {
|
fn setup(args: &[String]) -> Setup {
|
||||||
|
@ -102,7 +101,9 @@ fn setup(args: &[String]) -> Setup {
|
||||||
.optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE")
|
.optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE")
|
||||||
.optopt("", "mixer", "Mixer to use", "MIXER")
|
.optopt("", "mixer", "Mixer to use", "MIXER")
|
||||||
.optflag("", "enable-volume-normalization", "Play all tracks at the same volume")
|
.optflag("", "enable-volume-normalization", "Play all tracks at the same volume")
|
||||||
.optopt("", "normalization-pre-gain", "Pre-gain (dB) applied by volume normalization", "PREGAIN");
|
.optopt("", "normalization-pre-gain", "Pre-gain (dB) applied by volume normalization", "PREGAIN")
|
||||||
|
.optopt("", "initial-volume", "Initial volume in %, once connected (must be from 0 to 100)", "VOLUME")
|
||||||
|
.optopt("z", "zeroconf-port", "The port the internal server advertised over zeroconf uses.", "ZEROCONF_PORT")
|
||||||
|
|
||||||
let matches = match opts.parse(&args[1..]) {
|
let matches = match opts.parse(&args[1..]) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
|
@ -136,6 +137,31 @@ fn setup(args: &[String]) -> Setup {
|
||||||
let mixer = mixer::find(mixer_name.as_ref())
|
let mixer = mixer::find(mixer_name.as_ref())
|
||||||
.expect("Invalid mixer");
|
.expect("Invalid mixer");
|
||||||
|
|
||||||
|
let initial_volume: i32;
|
||||||
|
if matches.opt_present("initial-volume") && matches.opt_str("initial-volume").unwrap().parse::<i32>().is_ok() {
|
||||||
|
let iv = matches.opt_str("initial-volume").unwrap().parse::<i32>().unwrap();
|
||||||
|
match iv {
|
||||||
|
iv if iv >= 0 && iv <= 100 => { initial_volume = iv * 0xFFFF / 100 }
|
||||||
|
_ => {
|
||||||
|
debug!("Volume needs to be a value from 0-100; set volume level to 50%");
|
||||||
|
initial_volume = 0x8000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initial_volume = 0x8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
let zeroconf_port: u16;
|
||||||
|
if matches.opt_present("zeroconf-port") && matches.opt_str("zeroconf-port").unwrap().parse::<u16>().is_ok() {
|
||||||
|
let z = matches.opt_str("zeroconf-port").unwrap().parse::<u16>().unwrap();
|
||||||
|
match z {
|
||||||
|
z if z >= 1024 => { zeroconf_port = z }
|
||||||
|
_ => { zeroconf_port = 0 }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
zeroconf_port = 0
|
||||||
|
}
|
||||||
|
|
||||||
let name = matches.opt_str("name").unwrap();
|
let name = matches.opt_str("name").unwrap();
|
||||||
let use_audio_cache = !matches.opt_present("disable-audio-cache");
|
let use_audio_cache = !matches.opt_present("disable-audio-cache");
|
||||||
|
|
||||||
|
@ -186,6 +212,7 @@ fn setup(args: &[String]) -> Setup {
|
||||||
ConnectConfig {
|
ConnectConfig {
|
||||||
name: name,
|
name: name,
|
||||||
device_type: device_type,
|
device_type: device_type,
|
||||||
|
volume: initial_volume,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -200,6 +227,7 @@ fn setup(args: &[String]) -> Setup {
|
||||||
credentials: credentials,
|
credentials: credentials,
|
||||||
device: device,
|
device: device,
|
||||||
enable_discovery: enable_discovery,
|
enable_discovery: enable_discovery,
|
||||||
|
zeroconf_port: zeroconf_port,
|
||||||
mixer: mixer,
|
mixer: mixer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,14 +269,14 @@ impl Main {
|
||||||
spirc: None,
|
spirc: None,
|
||||||
spirc_task: None,
|
spirc_task: None,
|
||||||
shutdown: false,
|
shutdown: false,
|
||||||
signal: tokio_signal::ctrl_c(&handle).flatten_stream().boxed(),
|
signal: Box::new(tokio_signal::ctrl_c(&handle).flatten_stream()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if setup.enable_discovery {
|
if setup.enable_discovery {
|
||||||
let config = task.connect_config.clone();
|
let config = task.connect_config.clone();
|
||||||
let device_id = task.session_config.device_id.clone();
|
let device_id = task.session_config.device_id.clone();
|
||||||
|
|
||||||
task.discovery = Some(discovery(&handle, config, device_id).unwrap());
|
task.discovery = Some(discovery(&handle, config, device_id, setup.zeroconf_port).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(credentials) = setup.credentials {
|
if let Some(credentials) = setup.credentials {
|
||||||
|
@ -348,4 +376,3 @@ fn main() {
|
||||||
|
|
||||||
core.run(Main::new(handle, setup(&args))).unwrap()
|
core.run(Main::new(handle, setup(&args))).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
112
src/player.rs
112
src/player.rs
|
@ -2,8 +2,9 @@ use futures::sync::oneshot;
|
||||||
use futures::{future, Future};
|
use futures::{future, Future};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::sync::mpsc::{RecvError, TryRecvError};
|
use std::sync::mpsc::{RecvError, TryRecvError, RecvTimeoutError};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
use std;
|
use std;
|
||||||
use std::io::Seek;
|
use std::io::Seek;
|
||||||
use std::io::SeekFrom;
|
use std::io::SeekFrom;
|
||||||
|
@ -19,9 +20,9 @@ use audio::{VorbisDecoder, VorbisPacket};
|
||||||
use metadata::{FileFormat, Track, Metadata};
|
use metadata::{FileFormat, Track, Metadata};
|
||||||
use mixer::AudioFilter;
|
use mixer::AudioFilter;
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
commands: std::sync::mpsc::Sender<PlayerCommand>,
|
commands: Option<std::sync::mpsc::Sender<PlayerCommand>>,
|
||||||
|
thread_handle: Option<thread::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PlayerInternal {
|
struct PlayerInternal {
|
||||||
|
@ -31,6 +32,7 @@ struct PlayerInternal {
|
||||||
|
|
||||||
state: PlayerState,
|
state: PlayerState,
|
||||||
sink: Box<Sink>,
|
sink: Box<Sink>,
|
||||||
|
sink_running: bool,
|
||||||
audio_filter: Option<Box<AudioFilter + Send>>,
|
audio_filter: Option<Box<AudioFilter + Send>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +52,7 @@ impl Player {
|
||||||
{
|
{
|
||||||
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
|
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
thread::spawn(move || {
|
let handle = thread::spawn(move || {
|
||||||
debug!("new Player[{}]", session.session_id());
|
debug!("new Player[{}]", session.session_id());
|
||||||
|
|
||||||
let internal = PlayerInternal {
|
let internal = PlayerInternal {
|
||||||
|
@ -60,6 +62,7 @@ impl Player {
|
||||||
|
|
||||||
state: PlayerState::Stopped,
|
state: PlayerState::Stopped,
|
||||||
sink: sink_builder(),
|
sink: sink_builder(),
|
||||||
|
sink_running: false,
|
||||||
audio_filter: audio_filter,
|
audio_filter: audio_filter,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,12 +70,13 @@ impl Player {
|
||||||
});
|
});
|
||||||
|
|
||||||
Player {
|
Player {
|
||||||
commands: cmd_tx,
|
commands: Some(cmd_tx),
|
||||||
|
thread_handle: Some(handle),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn command(&self, cmd: PlayerCommand) {
|
fn command(&self, cmd: PlayerCommand) {
|
||||||
self.commands.send(cmd).unwrap();
|
self.commands.as_ref().unwrap().send(cmd).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(&self, track: SpotifyId, start_playing: bool, position_ms: u32)
|
pub fn load(&self, track: SpotifyId, start_playing: bool, position_ms: u32)
|
||||||
|
@ -101,6 +105,19 @@ impl Player {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for Player {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
debug!("Shutting down player thread ...");
|
||||||
|
self.commands = None;
|
||||||
|
if let Some(handle) = self.thread_handle.take() {
|
||||||
|
match handle.join() {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => error!("Player thread panicked!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Decoder = VorbisDecoder<Subfile<AudioDecrypt<AudioFile>>>;
|
type Decoder = VorbisDecoder<Subfile<AudioDecrypt<AudioFile>>>;
|
||||||
enum PlayerState {
|
enum PlayerState {
|
||||||
Stopped,
|
Stopped,
|
||||||
|
@ -143,7 +160,7 @@ impl PlayerState {
|
||||||
match self {
|
match self {
|
||||||
Paused { end_of_track, .. } |
|
Paused { end_of_track, .. } |
|
||||||
Playing { end_of_track, .. } => {
|
Playing { end_of_track, .. } => {
|
||||||
end_of_track.complete(())
|
let _ = end_of_track.send(());
|
||||||
}
|
}
|
||||||
|
|
||||||
Stopped => warn!("signal_end_of_track from stopped state"),
|
Stopped => warn!("signal_end_of_track from stopped state"),
|
||||||
|
@ -184,10 +201,21 @@ impl PlayerInternal {
|
||||||
fn run(mut self) {
|
fn run(mut self) {
|
||||||
loop {
|
loop {
|
||||||
let cmd = if self.state.is_playing() {
|
let cmd = if self.state.is_playing() {
|
||||||
match self.commands.try_recv() {
|
if self.sink_running
|
||||||
Ok(cmd) => Some(cmd),
|
{
|
||||||
Err(TryRecvError::Empty) => None,
|
match self.commands.try_recv() {
|
||||||
Err(TryRecvError::Disconnected) => return,
|
Ok(cmd) => Some(cmd),
|
||||||
|
Err(TryRecvError::Empty) => None,
|
||||||
|
Err(TryRecvError::Disconnected) => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
match self.commands.recv_timeout(Duration::from_secs(5)) {
|
||||||
|
Ok(cmd) => Some(cmd),
|
||||||
|
Err(RecvTimeoutError::Timeout) => None,
|
||||||
|
Err(RecvTimeoutError::Disconnected) => return,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match self.commands.recv() {
|
match self.commands.recv() {
|
||||||
|
@ -200,25 +228,51 @@ impl PlayerInternal {
|
||||||
self.handle_command(cmd);
|
self.handle_command(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut current_normalization_factor: f32 = 1.0;
|
if self.state.is_playing() && ! self.sink_running {
|
||||||
let packet = if let PlayerState::Playing { ref mut decoder, normalization_factor, .. } = self.state {
|
self.start_sink();
|
||||||
current_normalization_factor = normalization_factor;
|
}
|
||||||
Some(decoder.next_packet().expect("Vorbis error"))
|
|
||||||
} else { None };
|
|
||||||
|
|
||||||
if let Some(packet) = packet {
|
if self.sink_running {
|
||||||
self.handle_packet(packet, current_normalization_factor);
|
let mut current_normalization_factor: f32 = 1.0;
|
||||||
|
let packet = if let PlayerState::Playing { ref mut decoder, normalization_factor, .. } = self.state {
|
||||||
|
current_normalization_factor = normalization_factor;
|
||||||
|
Some(decoder.next_packet().expect("Vorbis error"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(packet) = packet {
|
||||||
|
self.handle_packet(packet, current_normalization_factor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_sink(&mut self) {
|
||||||
|
match self.sink.start() {
|
||||||
|
Ok(()) => self.sink_running = true,
|
||||||
|
Err(err) => error!("Could not start audio: {}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_sink_if_running(&mut self) {
|
||||||
|
if self.sink_running {
|
||||||
|
self.stop_sink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_sink(&mut self) {
|
||||||
|
self.sink.stop().unwrap();
|
||||||
|
self.sink_running = false;
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_packet(&mut self, packet: Option<VorbisPacket>, normalization_factor: f32) {
|
fn handle_packet(&mut self, packet: Option<VorbisPacket>, normalization_factor: f32) {
|
||||||
match packet {
|
match packet {
|
||||||
Some(mut packet) => {
|
Some(mut packet) => {
|
||||||
if let Some(ref editor) = self.audio_filter {
|
if let Some(ref editor) = self.audio_filter {
|
||||||
editor.modify_stream(&mut packet.data_mut())
|
editor.modify_stream(&mut packet.data_mut())
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.config.normalization {
|
if self.config.normalization {
|
||||||
|
|
||||||
if normalization_factor != 1.0 {
|
if normalization_factor != 1.0 {
|
||||||
|
@ -228,12 +282,14 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
if let Err(err) = self.sink.write(&packet.data()) {
|
||||||
self.sink.write(&packet.data()).unwrap();
|
error!("Could not write audio: {}", err);
|
||||||
|
self.stop_sink();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
self.sink.stop().unwrap();
|
self.stop_sink();
|
||||||
self.run_onstop();
|
self.run_onstop();
|
||||||
|
|
||||||
let old_state = mem::replace(&mut self.state, PlayerState::Stopped);
|
let old_state = mem::replace(&mut self.state, PlayerState::Stopped);
|
||||||
|
@ -247,7 +303,7 @@ impl PlayerInternal {
|
||||||
match cmd {
|
match cmd {
|
||||||
PlayerCommand::Load(track_id, play, position, end_of_track) => {
|
PlayerCommand::Load(track_id, play, position, end_of_track) => {
|
||||||
if self.state.is_playing() {
|
if self.state.is_playing() {
|
||||||
self.sink.stop().unwrap();
|
self.stop_sink_if_running();
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.load_track(track_id, position as i64) {
|
match self.load_track(track_id, position as i64) {
|
||||||
|
@ -256,7 +312,7 @@ impl PlayerInternal {
|
||||||
if !self.state.is_playing() {
|
if !self.state.is_playing() {
|
||||||
self.run_onstart();
|
self.run_onstart();
|
||||||
}
|
}
|
||||||
self.sink.start().unwrap();
|
self.start_sink();
|
||||||
|
|
||||||
self.state = PlayerState::Playing {
|
self.state = PlayerState::Playing {
|
||||||
decoder: decoder,
|
decoder: decoder,
|
||||||
|
@ -277,7 +333,7 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
end_of_track.complete(());
|
let _ = end_of_track.send(());
|
||||||
if self.state.is_playing() {
|
if self.state.is_playing() {
|
||||||
self.run_onstop();
|
self.run_onstop();
|
||||||
}
|
}
|
||||||
|
@ -301,7 +357,7 @@ impl PlayerInternal {
|
||||||
self.state.paused_to_playing();
|
self.state.paused_to_playing();
|
||||||
|
|
||||||
self.run_onstart();
|
self.run_onstart();
|
||||||
self.sink.start().unwrap();
|
self.start_sink();
|
||||||
} else {
|
} else {
|
||||||
warn!("Player::play called from invalid state");
|
warn!("Player::play called from invalid state");
|
||||||
}
|
}
|
||||||
|
@ -311,7 +367,7 @@ impl PlayerInternal {
|
||||||
if let PlayerState::Playing { .. } = self.state {
|
if let PlayerState::Playing { .. } = self.state {
|
||||||
self.state.playing_to_paused();
|
self.state.playing_to_paused();
|
||||||
|
|
||||||
self.sink.stop().unwrap();
|
self.stop_sink_if_running();
|
||||||
self.run_onstop();
|
self.run_onstop();
|
||||||
} else {
|
} else {
|
||||||
warn!("Player::pause called from invalid state");
|
warn!("Player::pause called from invalid state");
|
||||||
|
@ -321,7 +377,7 @@ impl PlayerInternal {
|
||||||
PlayerCommand::Stop => {
|
PlayerCommand::Stop => {
|
||||||
match self.state {
|
match self.state {
|
||||||
PlayerState::Playing { .. } => {
|
PlayerState::Playing { .. } => {
|
||||||
self.sink.stop().unwrap();
|
self.stop_sink_if_running();
|
||||||
self.run_onstop();
|
self.run_onstop();
|
||||||
self.state = PlayerState::Stopped;
|
self.state = PlayerState::Stopped;
|
||||||
}
|
}
|
||||||
|
|
124
src/spirc.rs
124
src/spirc.rs
|
@ -1,8 +1,6 @@
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use futures::sink::BoxSink;
|
|
||||||
use futures::stream::BoxStream;
|
|
||||||
use futures::sync::{oneshot, mpsc};
|
use futures::sync::{oneshot, mpsc};
|
||||||
use futures::{Future, Stream, Sink, Async, Poll, BoxFuture};
|
use futures::{Future, Stream, Sink, Async, Poll};
|
||||||
use protobuf::{self, Message};
|
use protobuf::{self, Message};
|
||||||
|
|
||||||
use core::config::ConnectConfig;
|
use core::config::ConnectConfig;
|
||||||
|
@ -17,6 +15,10 @@ use protocol::spirc::{PlayStatus, State, MessageType, Frame, DeviceState};
|
||||||
use mixer::Mixer;
|
use mixer::Mixer;
|
||||||
use player::Player;
|
use player::Player;
|
||||||
|
|
||||||
|
use std;
|
||||||
|
use rand;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
pub struct SpircTask {
|
pub struct SpircTask {
|
||||||
player: Player,
|
player: Player,
|
||||||
mixer: Box<Mixer>,
|
mixer: Box<Mixer>,
|
||||||
|
@ -27,10 +29,10 @@ pub struct SpircTask {
|
||||||
device: DeviceState,
|
device: DeviceState,
|
||||||
state: State,
|
state: State,
|
||||||
|
|
||||||
subscription: BoxStream<Frame, MercuryError>,
|
subscription: Box<Stream<Item = Frame, Error = MercuryError>>,
|
||||||
sender: BoxSink<Frame, MercuryError>,
|
sender: Box<Sink<SinkItem = Frame, SinkError = MercuryError>>,
|
||||||
commands: mpsc::UnboundedReceiver<SpircCommand>,
|
commands: mpsc::UnboundedReceiver<SpircCommand>,
|
||||||
end_of_track: BoxFuture<(), oneshot::Canceled>,
|
end_of_track: Box<Future<Item = (), Error = oneshot::Canceled>>,
|
||||||
|
|
||||||
shutdown: bool,
|
shutdown: bool,
|
||||||
session: Session,
|
session: Session,
|
||||||
|
@ -119,6 +121,29 @@ fn initial_device_state(config: ConnectConfig, volume: u16) -> DeviceState {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn volume_to_mixer(volume: u16) -> u16 {
|
||||||
|
// Volume conversion taken from https://www.dr-lex.be/info-stuff/volumecontrols.html#ideal2
|
||||||
|
// Convert the given volume [0..0xffff] to a dB gain
|
||||||
|
// We assume a dB range of 60dB.
|
||||||
|
// Use the equatation: a * exp(b * x)
|
||||||
|
// in which a = IDEAL_FACTOR, b = 1/1000
|
||||||
|
const IDEAL_FACTOR: f64 = 6.908;
|
||||||
|
let normalized_volume = volume as f64 / std::u16::MAX as f64; // To get a value between 0 and 1
|
||||||
|
|
||||||
|
let mut val = std::u16::MAX;
|
||||||
|
// Prevent val > std::u16::MAX due to rounding errors
|
||||||
|
if normalized_volume < 0.999 {
|
||||||
|
let new_volume = (normalized_volume * IDEAL_FACTOR).exp() / 1000.0;
|
||||||
|
val = (new_volume * std::u16::MAX as f64) as u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("input volume:{} to mixer: {}", volume, val);
|
||||||
|
|
||||||
|
// return the scale factor (0..0xffff) (equivalent to a voltage multiplier).
|
||||||
|
val
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Spirc {
|
impl Spirc {
|
||||||
pub fn new(config: ConnectConfig, session: Session, player: Player, mixer: Box<Mixer>)
|
pub fn new(config: ConnectConfig, session: Session, player: Player, mixer: Box<Mixer>)
|
||||||
-> (Spirc, SpircTask)
|
-> (Spirc, SpircTask)
|
||||||
|
@ -131,10 +156,10 @@ impl Spirc {
|
||||||
|
|
||||||
let subscription = session.mercury().subscribe(&uri as &str);
|
let subscription = session.mercury().subscribe(&uri as &str);
|
||||||
let subscription = subscription.map(|stream| stream.map_err(|_| MercuryError)).flatten_stream();
|
let subscription = subscription.map(|stream| stream.map_err(|_| MercuryError)).flatten_stream();
|
||||||
let subscription = subscription.map(|response| -> Frame {
|
let subscription = Box::new(subscription.map(|response| -> Frame {
|
||||||
let data = response.payload.first().unwrap();
|
let data = response.payload.first().unwrap();
|
||||||
protobuf::parse_from_bytes(data).unwrap()
|
protobuf::parse_from_bytes(data).unwrap()
|
||||||
}).boxed();
|
}));
|
||||||
|
|
||||||
let sender = Box::new(session.mercury().sender(uri).with(|frame: Frame| {
|
let sender = Box::new(session.mercury().sender(uri).with(|frame: Frame| {
|
||||||
Ok(frame.write_to_bytes().unwrap())
|
Ok(frame.write_to_bytes().unwrap())
|
||||||
|
@ -142,9 +167,9 @@ impl Spirc {
|
||||||
|
|
||||||
let (cmd_tx, cmd_rx) = mpsc::unbounded();
|
let (cmd_tx, cmd_rx) = mpsc::unbounded();
|
||||||
|
|
||||||
let volume = 0xFFFF;
|
let volume = config.volume as u16;
|
||||||
let device = initial_device_state(config, volume);
|
let device = initial_device_state(config, volume);
|
||||||
mixer.set_volume(volume);
|
mixer.set_volume(volume_to_mixer(volume as u16));
|
||||||
|
|
||||||
let mut task = SpircTask {
|
let mut task = SpircTask {
|
||||||
player: player,
|
player: player,
|
||||||
|
@ -160,7 +185,7 @@ impl Spirc {
|
||||||
subscription: subscription,
|
subscription: subscription,
|
||||||
sender: sender,
|
sender: sender,
|
||||||
commands: cmd_rx,
|
commands: cmd_rx,
|
||||||
end_of_track: future::empty().boxed(),
|
end_of_track: Box::new(future::empty()),
|
||||||
|
|
||||||
shutdown: false,
|
shutdown: false,
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
|
@ -176,28 +201,28 @@ impl Spirc {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play(&self) {
|
pub fn play(&self) {
|
||||||
let _ = mpsc::UnboundedSender::send(&self.commands, SpircCommand::Play);
|
let _ = self.commands.unbounded_send(SpircCommand::Play);
|
||||||
}
|
}
|
||||||
pub fn play_pause(&self) {
|
pub fn play_pause(&self) {
|
||||||
let _ = mpsc::UnboundedSender::send(&self.commands, SpircCommand::PlayPause);
|
let _ = self.commands.unbounded_send(SpircCommand::PlayPause);
|
||||||
}
|
}
|
||||||
pub fn pause(&self) {
|
pub fn pause(&self) {
|
||||||
let _ = mpsc::UnboundedSender::send(&self.commands, SpircCommand::Pause);
|
let _ = self.commands.unbounded_send(SpircCommand::Pause);
|
||||||
}
|
}
|
||||||
pub fn prev(&self) {
|
pub fn prev(&self) {
|
||||||
let _ = mpsc::UnboundedSender::send(&self.commands, SpircCommand::Prev);
|
let _ = self.commands.unbounded_send(SpircCommand::Prev);
|
||||||
}
|
}
|
||||||
pub fn next(&self) {
|
pub fn next(&self) {
|
||||||
let _ = mpsc::UnboundedSender::send(&self.commands, SpircCommand::Next);
|
let _ = self.commands.unbounded_send(SpircCommand::Next);
|
||||||
}
|
}
|
||||||
pub fn volume_up(&self) {
|
pub fn volume_up(&self) {
|
||||||
let _ = mpsc::UnboundedSender::send(&self.commands, SpircCommand::VolumeUp);
|
let _ = self.commands.unbounded_send(SpircCommand::VolumeUp);
|
||||||
}
|
}
|
||||||
pub fn volume_down(&self) {
|
pub fn volume_down(&self) {
|
||||||
let _ = mpsc::UnboundedSender::send(&self.commands, SpircCommand::VolumeDown);
|
let _ = self.commands.unbounded_send(SpircCommand::VolumeDown);
|
||||||
}
|
}
|
||||||
pub fn shutdown(&self) {
|
pub fn shutdown(&self) {
|
||||||
let _ = mpsc::UnboundedSender::send(&self.commands, SpircCommand::Shutdown);
|
let _ = self.commands.unbounded_send(SpircCommand::Shutdown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +260,7 @@ impl Future for SpircTask {
|
||||||
}
|
}
|
||||||
Ok(Async::NotReady) => (),
|
Ok(Async::NotReady) => (),
|
||||||
Err(oneshot::Canceled) => {
|
Err(oneshot::Canceled) => {
|
||||||
self.end_of_track = future::empty().boxed()
|
self.end_of_track = Box::new(future::empty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -396,6 +421,31 @@ impl SpircTask {
|
||||||
self.notify(None);
|
self.notify(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageType::kMessageTypeRepeat => {
|
||||||
|
self.state.set_repeat(frame.get_state().get_repeat());
|
||||||
|
self.notify(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageType::kMessageTypeShuffle => {
|
||||||
|
self.state.set_shuffle(frame.get_state().get_shuffle());
|
||||||
|
if self.state.get_shuffle()
|
||||||
|
{
|
||||||
|
let current_index = self.state.get_playing_track_index();
|
||||||
|
{
|
||||||
|
let tracks = self.state.mut_track();
|
||||||
|
tracks.swap(0, current_index as usize);
|
||||||
|
if let Some((_, rest)) = tracks.split_first_mut() {
|
||||||
|
rand::thread_rng().shuffle(rest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.state.set_playing_track_index(0);
|
||||||
|
} else {
|
||||||
|
let context = self.state.get_context_uri();
|
||||||
|
debug!("{:?}", context);
|
||||||
|
}
|
||||||
|
self.notify(None);
|
||||||
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeSeek => {
|
MessageType::kMessageTypeSeek => {
|
||||||
let position = frame.get_position();
|
let position = frame.get_position();
|
||||||
|
|
||||||
|
@ -411,9 +461,8 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageType::kMessageTypeVolume => {
|
MessageType::kMessageTypeVolume => {
|
||||||
let volume = frame.get_volume();
|
self.device.set_volume(frame.get_volume());
|
||||||
self.device.set_volume(volume);
|
self.mixer.set_volume(volume_to_mixer(frame.get_volume() as u16));
|
||||||
self.mixer.set_volume(frame.get_volume() as u16);
|
|
||||||
self.notify(None);
|
self.notify(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -467,13 +516,19 @@ impl SpircTask {
|
||||||
|
|
||||||
fn handle_next(&mut self) {
|
fn handle_next(&mut self) {
|
||||||
let current_index = self.state.get_playing_track_index();
|
let current_index = self.state.get_playing_track_index();
|
||||||
let new_index = (current_index + 1) % (self.state.get_track().len() as u32);
|
let num_tracks = self.state.get_track().len() as u32;
|
||||||
|
let new_index = (current_index + 1) % num_tracks;
|
||||||
|
|
||||||
|
let mut was_last_track = (current_index + 1) >= num_tracks;
|
||||||
|
if self.state.get_repeat() {
|
||||||
|
was_last_track = false;
|
||||||
|
}
|
||||||
|
|
||||||
self.state.set_playing_track_index(new_index);
|
self.state.set_playing_track_index(new_index);
|
||||||
self.state.set_position_ms(0);
|
self.state.set_position_ms(0);
|
||||||
self.state.set_position_measured_at(now_ms() as u64);
|
self.state.set_position_measured_at(now_ms() as u64);
|
||||||
|
|
||||||
self.load_track(true);
|
self.load_track(!was_last_track);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_prev(&mut self) {
|
fn handle_prev(&mut self) {
|
||||||
|
@ -502,32 +557,25 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_volume_up(&mut self) {
|
fn handle_volume_up(&mut self) {
|
||||||
let mut volume: u32 = self.mixer.volume() as u32 + 4096;
|
let mut volume: u32 = self.device.get_volume() as u32 + 4096;
|
||||||
if volume > 0xFFFF {
|
if volume > 0xFFFF {
|
||||||
volume = 0xFFFF;
|
volume = 0xFFFF;
|
||||||
}
|
}
|
||||||
self.device.set_volume(volume);
|
self.device.set_volume(volume);
|
||||||
self.mixer.set_volume(volume as u16);
|
self.mixer.set_volume(volume_to_mixer(volume as u16));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_volume_down(&mut self) {
|
fn handle_volume_down(&mut self) {
|
||||||
let mut volume: i32 = self.mixer.volume() as i32 - 4096;
|
let mut volume: i32 = self.device.get_volume() as i32 - 4096;
|
||||||
if volume < 0 {
|
if volume < 0 {
|
||||||
volume = 0;
|
volume = 0;
|
||||||
}
|
}
|
||||||
self.device.set_volume(volume as u32);
|
self.device.set_volume(volume as u32);
|
||||||
self.mixer.set_volume(volume as u16);
|
self.mixer.set_volume(volume_to_mixer(volume as u16));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_end_of_track(&mut self) {
|
fn handle_end_of_track(&mut self) {
|
||||||
let current_index = self.state.get_playing_track_index();
|
self.handle_next();
|
||||||
let new_index = (current_index + 1) % (self.state.get_track().len() as u32);
|
|
||||||
|
|
||||||
self.state.set_playing_track_index(new_index);
|
|
||||||
self.state.set_position_ms(0);
|
|
||||||
self.state.set_position_measured_at(now_ms() as u64);
|
|
||||||
|
|
||||||
self.load_track(true);
|
|
||||||
self.notify(None);
|
self.notify(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,7 +608,7 @@ impl SpircTask {
|
||||||
self.state.set_status(PlayStatus::kPlayStatusPause);
|
self.state.set_status(PlayStatus::kPlayStatusPause);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.end_of_track = end_of_track.boxed();
|
self.end_of_track = Box::new(end_of_track);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hello(&mut self) {
|
fn hello(&mut self) {
|
||||||
|
|
Loading…
Reference in a new issue