Merge pull request #3 from librespot-org/master

Updating personal fork
This commit is contained in:
Colm 2018-02-14 04:47:07 +00:00 committed by GitHub
commit 63cc63b4b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1169 additions and 744 deletions

2
.gitignore vendored
View file

@ -2,3 +2,5 @@ target
.cargo
spotify_appkey.key
.vagrant/
.project
.history

View file

@ -1,6 +1,6 @@
language: rust
rust:
- 1.18.0
- 1.20.0
- stable
- beta
- nightly
@ -20,15 +20,19 @@ before_script:
- echo '[target.armv7-unknown-linux-gnueabihf]' > ~/.cargo/config
- echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config
- rustup target add armv7-unknown-linux-gnueabihf
- if [[ $TRAVIS_RUST_VERSION == *"nightly"* ]]; then
rustup component add rustfmt-preview;
cargo fmt --package=librespot-core -- --write-mode=diff;
fi
script:
- cargo build --no-default-features
- 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 "pulseaudio-backend"
- cargo build --no-default-features --features "alsa-backend"
- cargo build --no-default-features --target armv7-unknown-linux-gnueabihf
- cargo build --locked --no-default-features
- cargo build --locked --no-default-features --features "with-tremor"
- cargo build --locked --no-default-features --features "with-vorbis"
- cargo build --locked --no-default-features --features "portaudio-backend"
- cargo build --locked --no-default-features --features "pulseaudio-backend"
- cargo build --locked --no-default-features --features "alsa-backend"
- cargo build --locked --no-default-features --target armv7-unknown-linux-gnueabihf
notifications:
email: false

112
Cargo.lock generated
View file

@ -106,6 +106,15 @@ dependencies = [
"quick-error 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dns-sd"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dtoa"
version = "0.4.2"
@ -215,6 +224,27 @@ name = "itoa"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "jack"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"jack-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "jack-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"libloading 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@ -246,7 +276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lewton"
version = "0.6.2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -258,6 +288,16 @@ name = "libc"
version = "0.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libloading"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "libpulse-sys"
version = "0.0.0"
@ -270,27 +310,24 @@ dependencies = [
name = "librespot"
version = "0.1.0"
dependencies = [
"alsa 0.0.1 (git+https://github.com/plietar/rust-alsa)",
"base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"getopts 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.14 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"libpulse-sys 0.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"librespot-audio 0.1.0",
"librespot-connect 0.1.0",
"librespot-core 0.1.0",
"librespot-metadata 0.1.0",
"librespot-playback 0.1.0",
"librespot-protocol 0.1.0",
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"mdns 0.2.0 (git+https://github.com/plietar/rust-mdns)",
"num-bigint 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
"portaudio-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"protobuf 1.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"protobuf_macros 0.6.0 (git+https://github.com/plietar/rust-protobuf-macros)",
"rand 0.3.20 (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 (git+https://github.com/awmath/rust-crypto.git?branch=avx2)",
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
@ -308,17 +345,42 @@ dependencies = [
"bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"lewton 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lewton 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"librespot-core 0.1.0",
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"num-bigint 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
"rust-crypto 0.2.36 (git+https://github.com/awmath/rust-crypto.git?branch=avx2)",
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tremor 0.1.0 (git+https://github.com/plietar/rust-tremor)",
"vorbis 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "librespot-connect"
version = "0.1.0"
dependencies = [
"base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"dns-sd 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.14 (registry+https://github.com/rust-lang/crates.io-index)",
"librespot-core 0.1.0",
"librespot-playback 0.1.0",
"librespot-protocol 0.1.0",
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"mdns 0.2.0 (git+https://github.com/plietar/rust-mdns)",
"num-bigint 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
"protobuf 1.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"protobuf_macros 0.6.0 (git+https://github.com/plietar/rust-protobuf-macros)",
"rand 0.3.20 (registry+https://github.com/rust-lang/crates.io-index)",
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "librespot-core"
version = "0.1.0"
@ -336,10 +398,9 @@ dependencies = [
"num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
"protobuf 1.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"protobuf_macros 0.6.0 (git+https://github.com/plietar/rust-protobuf-macros)",
"rand 0.3.20 (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 (git+https://github.com/awmath/rust-crypto.git?branch=avx2)",
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
@ -362,6 +423,22 @@ dependencies = [
"protobuf 1.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "librespot-playback"
version = "0.1.0"
dependencies = [
"alsa 0.0.1 (git+https://github.com/plietar/rust-alsa)",
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"jack 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"libpulse-sys 0.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"librespot-audio 0.1.0",
"librespot-core 0.1.0",
"librespot-metadata 0.1.0",
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"portaudio-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "librespot-protocol"
version = "0.1.0"
@ -660,6 +737,12 @@ dependencies = [
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rust-crypto"
version = "0.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
replace = "rust-crypto 0.2.36 (git+https://github.com/awmath/rust-crypto.git?branch=avx2)"
[[package]]
name = "rustc-serialize"
version = "0.3.24"
@ -1108,6 +1191,7 @@ dependencies = [
"checksum bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1b7db437d718977f6dc9b2e3fd6fc343c02ac6b899b73fdd2179163447bd9ce9"
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
"checksum dns-parser 0.3.2 (git+https://github.com/plietar/dns-parser)" = "<none>"
"checksum dns-sd 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d748509dea20228f63ba519bf142ce2593396386125b01f5b0d6412dab972087"
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
"checksum env_logger 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3ddf21e73e016298f5cb37d6ef8e8da8e39f91f9ec8b0df44b7deb16a9f8cd5b"
"checksum error-chain 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e92ecf0a508c8e074c0e6fa8fe0fa38414848ad4dfc4db6f74c5e9753330b248"
@ -1122,13 +1206,16 @@ dependencies = [
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
"checksum iovec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b6e8b9c2247fcf6c6a1151f1156932be5606c9fd6f55a2d7f9fc1cb29386b2f7"
"checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c"
"checksum jack 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1e15fc592e2e5a74a105ff507083c04db1aa20ba1b90d425362ba000e57422df"
"checksum jack-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d4ca501477fd3cd93a36df581046e5d6338ed826cf7e9b8d302603521e6cc3"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
"checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73"
"checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d"
"checksum lazycell 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a6f08839bc70ef4a3fe1d566d5350f519c5912ea86be0df1740a7d247c7fc0ef"
"checksum lewton 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0de1cca3399919efa65ae7e01ed6696c961bd0fc9e844c05c80f90b638300bbf"
"checksum lewton 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d170da25c0b3541e3260f84aa8f9d323468083bd1ed6c4c15aec7ff33e2a1c4"
"checksum libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "1e5d97d6708edaa407429faa671b942dc0f2727222fb6b6539bf1db936e4b121"
"checksum libloading 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fd38073de8f7965d0c17d30546d4bb6da311ab428d1c7a3fc71dff7f9d4979b9"
"checksum libpulse-sys 0.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9bb11b06faf883500c1b625cf4453e6c7737e9df9c7ba01df3f84b22b083e4ac"
"checksum linear-map 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee"
"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
@ -1164,6 +1251,7 @@ dependencies = [
"checksum relay 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f301bafeb60867c85170031bdb2fcf24c8041f33aee09e7b116a58d4e9f781c5"
"checksum rpassword 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ec4bdede957362ec6fdd550f7e79c6d14cad2bc26b2d062786234c6ee0cb27bb"
"checksum rust-crypto 0.2.36 (git+https://github.com/awmath/rust-crypto.git?branch=avx2)" = "<none>"
"checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a"
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
"checksum scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f417c22df063e9450888a7561788e9bd46d3bb3c1466435b4eccb903807f147d"

View file

@ -2,7 +2,6 @@
name = "librespot"
version = "0.1.0"
authors = ["Paul Liétar <paul@lietar.net>"]
build = "build.rs"
license = "MIT"
description = "Open Source Spotify client library"
keywords = ["spotify"]
@ -22,10 +21,14 @@ doc = false
[dependencies.librespot-audio]
path = "audio"
[dependencies.librespot-connect]
path = "connect"
[dependencies.librespot-core]
path = "core"
[dependencies.librespot-metadata]
path = "metadata"
[dependencies.librespot-playback]
path = "playback"
[dependencies.librespot-protocol]
path = "protocol"
@ -36,12 +39,11 @@ futures = "0.1.8"
getopts = "0.2.14"
hyper = "0.11.2"
log = "0.3.5"
mdns = { git = "https://github.com/plietar/rust-mdns" }
num-bigint = "0.1.35"
protobuf = "1.1"
rand = "0.3.13"
rpassword = "0.3.0"
rust-crypto = { git = "https://github.com/awmath/rust-crypto.git", branch = "avx2" }
rust-crypto = "0.2.36"
serde = "0.9.6"
serde_derive = "0.9.6"
serde_json = "0.9.5"
@ -50,29 +52,30 @@ tokio-io = "0.1"
tokio-signal = "0.1.2"
url = "1.3"
alsa = { git = "https://github.com/plietar/rust-alsa", optional = true }
portaudio-rs = { version = "0.3.0", optional = true }
libpulse-sys = { version = "0.0.0", optional = true }
libc = { version = "0.2", optional = true }
[build-dependencies]
rand = "0.3.13"
vergen = "0.1.0"
protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros", features = ["with-syntex"] }
[replace]
"rust-crypto:0.2.36" = { git = "https://github.com/awmath/rust-crypto.git", branch = "avx2" }
[features]
alsa-backend = ["alsa"]
portaudio-backend = ["portaudio-rs"]
pulseaudio-backend = ["libpulse-sys", "libc"]
alsa-backend = ["librespot-playback/alsa-backend"]
portaudio-backend = ["librespot-playback/portaudio-backend"]
pulseaudio-backend = ["librespot-playback/pulseaudio-backend"]
jackaudio-backend = ["librespot-playback/jackaudio-backend"]
with-tremor = ["librespot-audio/with-tremor"]
with-lewton = ["librespot-audio/with-lewton"]
with-vorbis = ["librespot-audio/with-vorbis"]
default = ["portaudio-backend"]
with-dns-sd = ["librespot-connect/with-dns-sd"]
default = ["librespot-playback/portaudio-backend"]
[package.metadata.deb]
maintainer = "nobody"
copyright = "2016 Paul Liétar"
maintainer = "librespot-org"
copyright = "2018 Paul Liétar"
license_file = ["LICENSE", "4"]
depends = "$auto"
extended_description = """\

View file

@ -16,7 +16,7 @@ As the origin by [plietar](https://github.com/plietar/) is no longer actively ma
More information can be found in the [wiki](https://github.com/librespot-org/librespot/wiki)
# Building
Rust 1.18.0 or later is required to build librespot.
Rust 1.20.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.**
@ -56,9 +56,11 @@ 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.
- [ ] Logarithmic volume scaling ([#10](https://github.com/librespot-org/librespot/issues/10))
- [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

View file

@ -10,16 +10,16 @@ path = "../core"
bit-set = "0.4.0"
byteorder = "1.0"
futures = "0.1.8"
lewton = "0.8.0"
log = "0.3.5"
num-bigint = "0.1.35"
num-traits = "0.1.36"
rust-crypto = { git = "https://github.com/awmath/rust-crypto.git", branch = "avx2" }
rust-crypto = "0.2.36"
tempfile = "2.1"
vorbis = "0.1.0"
tremor = { git = "https://github.com/plietar/rust-tremor", optional = true }
lewton = { version = "0.6.2", optional = true }
vorbis = { version ="0.1.0", optional = true }
[features]
with-tremor = ["tremor"]
with-lewton = ["lewton"]
with-vorbis = ["vorbis"]

View file

@ -11,7 +11,7 @@ use tempfile::NamedTempFile;
use core::channel::{Channel, ChannelData, ChannelError, ChannelHeaders};
use core::session::Session;
use core::util::FileId;
use core::spotify_id::FileId;
const CHUNK_SIZE: usize = 0x20000;

View file

@ -24,7 +24,9 @@ impl <R> VorbisDecoder<R>
}
pub fn next_packet(&mut self) -> Result<Option<VorbisPacket>, VorbisError> {
use self::lewton::OggReadError::NoCapturePatternFound;
use self::lewton::VorbisError::BadAudio;
use self::lewton::VorbisError::OggError;
use self::lewton::audio::AudioReadError::AudioIsHeader;
loop {
match self.0.read_dec_packet_itl() {
@ -32,6 +34,7 @@ impl <R> VorbisDecoder<R>
Ok(None) => return Ok(None),
Err(BadAudio(AudioIsHeader)) => (),
Err(OggError(NoCapturePatternFound)) => (),
Err(err) => return Err(err.into()),
}
}

View file

@ -13,15 +13,15 @@ extern crate librespot_core as core;
mod fetch;
mod decrypt;
#[cfg(not(feature = "with-lewton"))]
mod libvorbis_decoder;
#[cfg(feature = "with-lewton")]
#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))]
mod lewton_decoder;
#[cfg(any(feature = "with-tremor", feature = "with-vorbis"))]
mod libvorbis_decoder;
pub use fetch::{AudioFile, AudioFileOpen};
pub use decrypt::AudioDecrypt;
#[cfg(not(feature = "with-lewton"))]
pub use libvorbis_decoder::{VorbisDecoder, VorbisPacket, VorbisError};
#[cfg(feature = "with-lewton")]
#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))]
pub use lewton_decoder::{VorbisDecoder, VorbisPacket, VorbisError};
#[cfg(any(feature = "with-tremor", feature = "with-vorbis"))]
pub use libvorbis_decoder::{VorbisDecoder, VorbisPacket, VorbisError};

37
connect/Cargo.toml Normal file
View file

@ -0,0 +1,37 @@
[package]
name = "librespot-connect"
version = "0.1.0"
authors = ["Paul Lietar <paul@lietar.net>"]
build = "build.rs"
[dependencies.librespot-core]
path = "../core"
[dependencies.librespot-playback]
path = "../playback"
[dependencies.librespot-protocol]
path = "../protocol"
[dependencies]
base64 = "0.5.0"
futures = "0.1.8"
hyper = "0.11.2"
log = "0.3.5"
num-bigint = "0.1.35"
protobuf = "1.1"
rand = "0.3.13"
rust-crypto = "0.2.36"
serde = "0.9.6"
serde_derive = "0.9.6"
serde_json = "0.9.5"
tokio-core = "0.1.2"
url = "1.3"
dns-sd = { version = "0.1.3", optional = true }
mdns = { git = "https://github.com/plietar/rust-mdns", optional = true }
[build-dependencies]
protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros", features = ["with-syntex"] }
[features]
default = ["mdns"]
with-dns-sd = ["dns-sd"]

View file

@ -10,5 +10,4 @@ fn main() {
println!("cargo:rerun-if-changed=src/lib.in.rs");
println!("cargo:rerun-if-changed=src/spirc.rs");
}

View file

@ -1,12 +1,18 @@
use base64;
use crypto;
use crypto::digest::Digest;
use crypto::mac::Mac;
use crypto;
use futures::{Future, Poll, Stream};
use futures::sync::mpsc;
use futures::{Future, Stream, Poll};
use hyper::server::{Service, Request, Response, Http};
use hyper::{self, Get, Post, StatusCode};
use hyper::server::{Http, Request, Response, Service};
#[cfg(feature = "with-dns-sd")]
use dns_sd::DNSService;
#[cfg(not(feature = "with-dns-sd"))]
use mdns;
use num_bigint::BigUint;
use rand;
use std::collections::BTreeMap;
@ -15,10 +21,10 @@ use std::sync::Arc;
use tokio_core::reactor::Handle;
use url;
use core::diffie_hellman::{DH_GENERATOR, DH_PRIME};
use core::authentication::Credentials;
use core::util;
use core::config::ConnectConfig;
use core::diffie_hellman::{DH_GENERATOR, DH_PRIME};
use core::util;
#[derive(Clone)]
struct Discovery(Arc<DiscoveryInner>);
@ -31,9 +37,10 @@ struct DiscoveryInner {
}
impl Discovery {
fn new(config: ConnectConfig, device_id: String)
-> (Discovery, mpsc::UnboundedReceiver<Credentials>)
{
fn new(
config: ConnectConfig,
device_id: String,
) -> (Discovery, mpsc::UnboundedReceiver<Credentials>) {
let (tx, rx) = mpsc::unbounded();
let key_data = util::rand_vec(&mut rand::thread_rng(), 95);
@ -53,9 +60,10 @@ impl Discovery {
}
impl Discovery {
fn handle_get_info(&self, _params: &BTreeMap<String, String>)
-> ::futures::Finished<Response, hyper::Error>
{
fn handle_get_info(
&self,
_params: &BTreeMap<String, String>,
) -> ::futures::Finished<Response, hyper::Error> {
let public_key = self.0.public_key.to_bytes_be();
let public_key = base64::encode(&public_key);
@ -79,9 +87,10 @@ impl Discovery {
::futures::finished(Response::new().with_body(body))
}
fn handle_add_user(&self, params: &BTreeMap<String, String>)
-> ::futures::Finished<Response, hyper::Error>
{
fn handle_add_user(
&self,
params: &BTreeMap<String, String>,
) -> ::futures::Finished<Response, hyper::Error> {
let username = params.get("userName").unwrap();
let encrypted_blob = params.get("blob").unwrap();
let client_key = params.get("clientKey").unwrap();
@ -127,8 +136,8 @@ impl Discovery {
let decrypted = {
let mut data = vec![0u8; encrypted.len()];
let mut cipher = crypto::aes::ctr(crypto::aes::KeySize::KeySize128,
&encryption_key[0..16], iv);
let mut cipher =
crypto::aes::ctr(crypto::aes::KeySize::KeySize128, &encryption_key[0..16], iv);
cipher.process(encrypted, &mut data);
String::from_utf8(data).unwrap()
};
@ -147,9 +156,7 @@ impl Discovery {
::futures::finished(Response::new().with_body(body))
}
fn not_found(&self)
-> ::futures::Finished<Response, hyper::Error>
{
fn not_found(&self) -> ::futures::Finished<Response, hyper::Error> {
::futures::finished(Response::new().with_status(StatusCode::NotFound))
}
}
@ -173,40 +180,61 @@ impl Service for Discovery {
}
let this = self.clone();
Box::new(body.fold(Vec::new(), |mut acc, chunk| {
Box::new(
body.fold(Vec::new(), |mut acc, chunk| {
acc.extend_from_slice(chunk.as_ref());
Ok::<_, hyper::Error>(acc)
}).map(move |body| {
params.extend(url::form_urlencoded::parse(&body).into_owned());
params
}).and_then(move |params| {
match (method, params.get("action").map(AsRef::as_ref)) {
})
.and_then(
move |params| match (method, params.get("action").map(AsRef::as_ref)) {
(Get, Some("getInfo")) => this.handle_get_info(&params),
(Post, Some("addUser")) => this.handle_add_user(&params),
_ => this.not_found(),
}
}))
},
),
)
}
}
#[cfg(feature = "with-dns-sd")]
pub struct DiscoveryStream {
credentials: mpsc::UnboundedReceiver<Credentials>,
_svc: DNSService,
}
#[cfg(not(feature = "with-dns-sd"))]
pub struct DiscoveryStream {
credentials: mpsc::UnboundedReceiver<Credentials>,
_svc: mdns::Service,
}
pub fn discovery(handle: &Handle, config: ConnectConfig, device_id: String)
-> io::Result<DiscoveryStream>
{
pub fn discovery(
handle: &Handle,
config: ConnectConfig,
device_id: String,
port: u16,
) -> io::Result<DiscoveryStream> {
let (discovery, creds_rx) = Discovery::new(config.clone(), device_id);
let serve = {
let http = Http::new();
http.serve_addr_handle(&"0.0.0.0:0".parse().unwrap(), &handle, move || Ok(discovery.clone())).unwrap()
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 addr = serve.incoming_ref().local_addr();
let s_port = serve.incoming_ref().local_addr().port();
let server_future = {
let handle = handle.clone();
serve.for_each(move |connection| {
serve
.for_each(move |connection| {
handle.spawn(connection.then(|_| Ok(())));
Ok(())
})
@ -214,12 +242,26 @@ pub fn discovery(handle: &Handle, config: ConnectConfig, device_id: String)
};
handle.spawn(server_future);
#[cfg(feature = "with-dns-sd")]
let svc = DNSService::register(
Some(&*config.name),
"_spotify-connect._tcp",
None,
None,
s_port,
&["VERSION=1.0", "CPath=/"],
).unwrap();
#[cfg(not(feature = "with-dns-sd"))]
let responder = mdns::Responder::spawn(&handle)?;
#[cfg(not(feature = "with-dns-sd"))]
let svc = responder.register(
"_spotify-connect._tcp".to_owned(),
config.name,
addr.port(),
&["VERSION=1.0", "CPath=/"]);
s_port,
&["VERSION=1.0", "CPath=/"],
);
Ok(DiscoveryStream {
credentials: creds_rx,

28
connect/src/lib.rs Normal file
View file

@ -0,0 +1,28 @@
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_json;
extern crate base64;
extern crate crypto;
extern crate futures;
extern crate hyper;
extern crate num_bigint;
extern crate protobuf;
extern crate rand;
extern crate tokio_core;
extern crate url;
#[cfg(feature = "with-dns-sd")]
extern crate dns_sd;
#[cfg(not(feature = "with-dns-sd"))]
extern crate mdns;
extern crate librespot_core as core;
extern crate librespot_playback as playback;
extern crate librespot_protocol as protocol;
pub mod discovery;
include!(concat!(env!("OUT_DIR"), "/lib.rs"));

View file

@ -1,22 +1,25 @@
use futures::{Async, Future, Poll, Sink, Stream};
use futures::future;
use futures::sync::{oneshot, mpsc};
use futures::{Future, Stream, Sink, Async, Poll};
use futures::sync::{mpsc, oneshot};
use protobuf::{self, Message};
use core::config::ConnectConfig;
use core::mercury::MercuryError;
use core::session::Session;
use core::util::{now_ms, SpotifyId, SeqGenerator};
use core::spotify_id::SpotifyId;
use core::util::SeqGenerator;
use core::version;
use protocol;
use protocol::spirc::{PlayStatus, State, MessageType, Frame, DeviceState};
use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State};
use mixer::Mixer;
use player::Player;
use playback::mixer::Mixer;
use playback::player::Player;
use rand;
use rand::Rng;
use std;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct SpircTask {
player: Player,
@ -45,13 +48,21 @@ pub enum SpircCommand {
Next,
VolumeUp,
VolumeDown,
Shutdown
Shutdown,
}
pub struct Spirc {
commands: mpsc::UnboundedSender<SpircCommand>,
}
fn now_ms() -> i64 {
let dur = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(dur) => dur,
Err(err) => err.duration(),
};
(dur.as_secs() * 1000 + (dur.subsec_nanos() / 1000_000) as u64) as i64
}
fn initial_state() -> State {
protobuf_init!(protocol::spirc::State::new(), {
repeat: false,
@ -120,10 +131,35 @@ 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 {
pub fn new(config: ConnectConfig, session: Session, player: Player, mixer: Box<Mixer>)
-> (Spirc, SpircTask)
{
pub fn new(
config: ConnectConfig,
session: Session,
player: Player,
mixer: Box<Mixer>,
) -> (Spirc, SpircTask) {
debug!("new Spirc[{}]", session.session_id());
let ident = session.device_id().to_owned();
@ -131,21 +167,26 @@ impl Spirc {
let uri = format!("hm://remote/3/user/{}/", session.username());
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 = Box::new(subscription.map(|response| -> Frame {
let data = response.payload.first().unwrap();
protobuf::parse_from_bytes(data).unwrap()
}));
let sender = Box::new(session.mercury().sender(uri).with(|frame: Frame| {
Ok(frame.write_to_bytes().unwrap())
}));
let sender = Box::new(
session
.mercury()
.sender(uri)
.with(|frame: Frame| Ok(frame.write_to_bytes().unwrap())),
);
let (cmd_tx, cmd_rx) = mpsc::unbounded();
let volume = config.volume as u16;
let device = initial_device_state(config, volume);
mixer.set_volume(volume as u16);
mixer.set_volume(volume_to_mixer(volume as u16));
let mut task = SpircTask {
player: player,
@ -167,9 +208,7 @@ impl Spirc {
session: session.clone(),
};
let spirc = Spirc {
commands: cmd_tx,
};
let spirc = Spirc { commands: cmd_tx };
task.hello();
@ -235,9 +274,7 @@ impl Future for SpircTask {
self.handle_end_of_track();
}
Ok(Async::NotReady) => (),
Err(oneshot::Canceled) => {
self.end_of_track = Box::new(future::empty())
}
Err(oneshot::Canceled) => self.end_of_track = Box::new(future::empty()),
}
}
@ -324,15 +361,18 @@ impl SpircTask {
}
fn handle_frame(&mut self, frame: Frame) {
debug!("{:?} {:?} {} {} {}",
debug!(
"{:?} {:?} {} {} {}",
frame.get_typ(),
frame.get_device_state().get_name(),
frame.get_ident(),
frame.get_seq_nr(),
frame.get_state_update_id());
frame.get_state_update_id()
);
if frame.get_ident() == self.ident ||
(frame.get_recipient().len() > 0 && !frame.get_recipient().contains(&self.ident)) {
if frame.get_ident() == self.ident
|| (frame.get_recipient().len() > 0 && !frame.get_recipient().contains(&self.ident))
{
return;
}
@ -350,7 +390,8 @@ impl SpircTask {
self.update_tracks(&frame);
if self.state.get_track().len() > 0 {
self.state.set_position_ms(frame.get_state().get_position_ms());
self.state
.set_position_ms(frame.get_state().get_position_ms());
self.state.set_position_measured_at(now_ms() as u64);
let play = frame.get_state().get_status() == PlayStatus::kPlayStatusPlay;
@ -404,8 +445,7 @@ impl SpircTask {
MessageType::kMessageTypeShuffle => {
self.state.set_shuffle(frame.get_state().get_shuffle());
if self.state.get_shuffle()
{
if self.state.get_shuffle() {
let current_index = self.state.get_playing_track_index();
{
let tracks = self.state.mut_track();
@ -437,16 +477,14 @@ impl SpircTask {
}
MessageType::kMessageTypeVolume => {
let volume = frame.get_volume();
self.device.set_volume(volume);
self.mixer.set_volume(frame.get_volume() as u16);
self.device.set_volume(frame.get_volume());
self.mixer
.set_volume(volume_to_mixer(frame.get_volume() as u16));
self.notify(None);
}
MessageType::kMessageTypeNotify => {
if self.device.get_is_active() &&
frame.get_device_state().get_is_active()
{
if self.device.get_is_active() && frame.get_device_state().get_is_active() {
self.device.set_is_active(false);
self.state.set_status(PlayStatus::kPlayStatusStop);
self.player.stop();
@ -491,35 +529,61 @@ impl SpircTask {
}
}
fn handle_next(&mut self) {
let current_index = self.state.get_playing_track_index();
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;
fn consume_queued_track(&mut self) -> usize {
// Removes current track if it is queued
// Returns the index of the next track
let current_index = self.state.get_playing_track_index() as usize;
if self.state.get_track()[current_index].get_queued() {
self.state.mut_track().remove(current_index);
return current_index;
}
current_index + 1
}
fn handle_next(&mut self) {
let mut new_index = self.consume_queued_track() as u32;
let mut continue_playing = true;
if new_index >= self.state.get_track().len() as u32 {
new_index = 0; // Loop around back to start
continue_playing = self.state.get_repeat();
}
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(!was_last_track);
self.load_track(continue_playing);
}
fn handle_prev(&mut self) {
// Previous behaves differently based on the position
// Under 3s it goes to the previous song
// Over 3s it seeks to zero
// Under 3s it goes to the previous song (starts playing)
// Over 3s it seeks to zero (retains previous play status)
if self.position() < 3000 {
// Queued tracks always follow the currently playing track.
// They should not be considered when calculating the previous
// track so extract them beforehand and reinsert them after it.
let mut queue_tracks = Vec::new();
{
let queue_index = self.consume_queued_track();
let tracks = self.state.mut_track();
while queue_index < tracks.len() && tracks[queue_index].get_queued() {
queue_tracks.push(tracks.remove(queue_index));
}
}
let current_index = self.state.get_playing_track_index();
let new_index = if current_index == 0 {
let new_index = if current_index > 0 {
current_index - 1
} else if self.state.get_repeat() {
self.state.get_track().len() as u32 - 1
} else {
current_index - 1
0
};
// Reinsert queued tracks after the new playing track.
let mut pos = (new_index + 1) as usize;
for track in queue_tracks.into_iter() {
self.state.mut_track().insert(pos, track);
pos += 1;
}
self.state.set_playing_track_index(new_index);
self.state.set_position_ms(0);
@ -534,21 +598,21 @@ impl SpircTask {
}
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 {
volume = 0xFFFF;
}
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) {
let mut volume: i32 = self.mixer.volume() as i32 - 4096;
let mut volume: i32 = self.device.get_volume() as i32 - 4096;
if volume < 0 {
volume = 0;
}
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) {

View file

@ -22,7 +22,7 @@ num-traits = "0.1.36"
protobuf = "1.1"
rand = "0.3.13"
rpassword = "0.3.0"
rust-crypto = { git = "https://github.com/awmath/rust-crypto.git", branch = "avx2" }
rust-crypto = "0.2.36"
serde = "0.9.6"
serde_derive = "0.9.6"
serde_json = "0.9.5"
@ -32,6 +32,5 @@ tokio-io = "0.1"
uuid = { version = "0.4", features = ["v4"] }
[build-dependencies]
protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros", features = ["with-syntex"] }
rand = "0.3.13"
vergen = "0.1.0"

View file

@ -1,45 +1,36 @@
extern crate vergen;
extern crate protobuf_macros;
extern crate rand;
extern crate vergen;
use rand::Rng;
use std::env;
use std::path::PathBuf;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
fn main() {
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
vergen::vergen(vergen::OutputFns::all()).unwrap();
let build_id: String = rand::thread_rng()
.gen_ascii_chars()
.take(8)
.collect();
let build_id: String = rand::thread_rng().gen_ascii_chars().take(8).collect();
let mut version_file =
OpenOptions::new()
let mut version_file = OpenOptions::new()
.write(true)
.append(true)
.open(&out.join("version.rs"))
.unwrap();
let build_id_fn = format!("
let build_id_fn = format!(
"
/// Generate a random build id.
pub fn build_id() -> &'static str {{
\"{}\"
}}
", build_id);
",
build_id
);
if let Err(e) = version_file.write_all(build_id_fn.as_bytes()) {
println!("{}", e);
}
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/connection/mod.rs");
println!("cargo:rerun-if-changed=src/connection/codec.rs");
println!("cargo:rerun-if-changed=src/connection/handshake.rs");
}

View file

@ -1,20 +1,20 @@
const AP_FALLBACK: &'static str = "ap.spotify.com:80";
const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com/";
use std::str::FromStr;
use futures::{Future, Stream};
use hyper::{self, Uri, Client};
use hyper::{self, Client, Uri};
use serde_json;
use std::str::FromStr;
use tokio_core::reactor::Handle;
error_chain!{}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct APResolveData {
ap_list: Vec<String>
ap_list: Vec<String>,
}
pub fn apresolve(handle: &Handle) -> Box<Future<Item=String, Error=Error>> {
fn apresolve(handle: &Handle) -> Box<Future<Item = String, Error = Error>> {
let url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL");
let client = Client::new(handle);
@ -27,14 +27,10 @@ pub fn apresolve(handle: &Handle) -> Box<Future<Item=String, Error=Error>> {
})
});
let body = body.then(|result| result.chain_err(|| "HTTP error"));
let body = body.and_then(|body| {
String::from_utf8(body).chain_err(|| "invalid UTF8 in response")
});
let body = body.and_then(|body| String::from_utf8(body).chain_err(|| "invalid UTF8 in response"));
let data = body.and_then(|body| {
serde_json::from_str::<APResolveData>(&body)
.chain_err(|| "invalid JSON")
});
let data =
body.and_then(|body| serde_json::from_str::<APResolveData>(&body).chain_err(|| "invalid JSON"));
let ap = data.and_then(|data| {
let ap = data.ap_list.first().ok_or("empty AP List")?;
@ -44,9 +40,9 @@ pub fn apresolve(handle: &Handle) -> Box<Future<Item=String, Error=Error>> {
Box::new(ap)
}
pub fn apresolve_or_fallback<E>(handle: &Handle)
-> Box<Future<Item=String, Error=E>>
where E: 'static
pub(crate) fn apresolve_or_fallback<E>(handle: &Handle) -> Box<Future<Item = String, Error = E>>
where
E: 'static,
{
let ap = apresolve(handle).or_else(|e| {
warn!("Failed to resolve Access Point: {}", e.description());

View file

@ -1,12 +1,12 @@
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use bytes::Bytes;
use futures::sync::oneshot;
use futures::{Async, Future, Poll};
use futures::sync::oneshot;
use std::collections::HashMap;
use std::io::Write;
use spotify_id::{FileId, SpotifyId};
use util::SeqGenerator;
use util::{SpotifyId, FileId};
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct AudioKey(pub [u8; 16]);
@ -22,7 +22,7 @@ component! {
}
impl AudioKeyManager {
pub fn dispatch(&self, cmd: u8, mut data: Bytes) {
pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) {
let seq = BigEndian::read_u32(data.split_to(4).as_ref());
let sender = self.lock(|inner| inner.pending.remove(&seq));
@ -35,7 +35,11 @@ impl AudioKeyManager {
let _ = sender.send(Ok(AudioKey(key)));
}
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]
);
let _ = sender.send(Err(AudioKeyError));
}
_ => (),
@ -81,4 +85,3 @@ impl <T> Future for AudioKeyFuture<T> {
}
}
}

View file

@ -10,14 +10,13 @@ use protobuf::ProtobufEnum;
use rpassword;
use serde;
use serde_json;
use std::io::{self, stderr, Read, Write};
use std::fs::File;
use std::io::{self, stderr, Read, Write};
use std::path::Path;
use protocol::authentication::AuthenticationType;
#[derive(Debug, Clone)]
#[derive(Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Credentials {
pub username: String,
@ -89,12 +88,17 @@ impl Credentials {
let blob = {
// Anyone know what this block mode is ?
let mut data = vec![0u8; encrypted_blob.len()];
let mut cipher = aes::ecb_decryptor(aes::KeySize::KeySize192,
let mut cipher = aes::ecb_decryptor(
aes::KeySize::KeySize192,
&key,
crypto::blockmodes::NoPadding);
cipher.decrypt(&mut crypto::buffer::RefReadBuffer::new(&encrypted_blob),
crypto::blockmodes::NoPadding,
);
cipher
.decrypt(
&mut crypto::buffer::RefReadBuffer::new(&encrypted_blob),
&mut crypto::buffer::RefWriteBuffer::new(&mut data),
true)
true,
)
.unwrap();
let l = encrypted_blob.len();
@ -112,7 +116,7 @@ impl Credentials {
let auth_type = read_int(&mut cursor).unwrap();
let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap();
read_u8(&mut cursor).unwrap();
let auth_data = read_bytes(&mut cursor).unwrap();;
let auth_data = read_bytes(&mut cursor).unwrap();
Credentials {
username: username,
@ -121,65 +125,72 @@ impl Credentials {
}
}
pub fn from_reader<R: Read>(mut reader: R) -> Credentials {
fn from_reader<R: Read>(mut reader: R) -> Credentials {
let mut contents = String::new();
reader.read_to_string(&mut contents).unwrap();
serde_json::from_str(&contents).unwrap()
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Option<Credentials> {
pub(crate) fn from_file<P: AsRef<Path>>(path: P) -> Option<Credentials> {
File::open(path).ok().map(Credentials::from_reader)
}
pub fn save_to_writer<W: Write>(&self, writer: &mut W) {
fn save_to_writer<W: Write>(&self, writer: &mut W) {
let contents = serde_json::to_string(&self.clone()).unwrap();
writer.write_all(contents.as_bytes()).unwrap();
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) {
pub(crate) fn save_to_file<P: AsRef<Path>>(&self, path: P) {
let mut file = File::create(path).unwrap();
self.save_to_writer(&mut file)
}
}
fn serialize_protobuf_enum<T, S>(v: &T, ser: S) -> Result<S::Ok, S::Error>
where T: ProtobufEnum, S: serde::Serializer {
where
T: ProtobufEnum,
S: serde::Serializer,
{
serde::Serialize::serialize(&v.value(), ser)
}
fn deserialize_protobuf_enum<T, D>(de: D) -> Result<T, D::Error>
where T: ProtobufEnum, D: serde::Deserializer {
where
T: ProtobufEnum,
D: serde::Deserializer,
{
let v: i32 = try!(serde::Deserialize::deserialize(de));
T::from_i32(v).ok_or_else(|| serde::de::Error::custom("Invalid enum value"))
}
fn serialize_base64<T, S>(v: &T, ser: S) -> Result<S::Ok, S::Error>
where T: AsRef<[u8]>, S: serde::Serializer {
where
T: AsRef<[u8]>,
S: serde::Serializer,
{
serde::Serialize::serialize(&base64::encode(v.as_ref()), ser)
}
fn deserialize_base64<D>(de: D) -> Result<Vec<u8>, D::Error>
where D: serde::Deserializer {
where
D: serde::Deserializer,
{
let v: String = try!(serde::Deserialize::deserialize(de));
base64::decode(&v).map_err(|e| serde::de::Error::custom(e.to_string()))
}
pub fn get_credentials(username: Option<String>, password: Option<String>,
cached_credentials: Option<Credentials>)
-> Option<Credentials>
{
pub fn get_credentials(
username: Option<String>,
password: Option<String>,
cached_credentials: Option<Credentials>,
) -> Option<Credentials> {
match (username, password, cached_credentials) {
(Some(username), Some(password), _) => Some(Credentials::with_password(username, password)),
(Some(username), Some(password), _)
=> Some(Credentials::with_password(username, password)),
(Some(ref username), _, Some(ref credentials))
if *username == credentials.username => Some(credentials.clone()),
(Some(ref username), _, Some(ref credentials)) if *username == credentials.username => {
Some(credentials.clone())
}
(Some(username), None, _) => {
write!(stderr(), "Password for {}: ", username).unwrap();
@ -188,8 +199,7 @@ pub fn get_credentials(username: Option<String>, password: Option<String>,
Some(Credentials::with_password(username.clone(), password))
}
(None, _, Some(credentials))
=> Some(credentials),
(None, _, Some(credentials)) => Some(credentials),
(None, _, None) => None,
}

View file

@ -1,9 +1,12 @@
use std::path::PathBuf;
use std::io::Read;
use std::fs;
use std::fs::File;
use std::io;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use util::{FileId, mkdir_existing};
use authentication::Credentials;
use spotify_id::FileId;
#[derive(Clone)]
pub struct Cache {
@ -11,6 +14,16 @@ pub struct Cache {
use_audio_cache: bool,
}
fn mkdir_existing(path: &Path) -> io::Result<()> {
fs::create_dir(path).or_else(|err| {
if err.kind() == io::ErrorKind::AlreadyExists {
Ok(())
} else {
Err(err)
}
})
}
impl Cache {
pub fn new(location: PathBuf, use_audio_cache: bool) -> Cache {
mkdir_existing(&location).unwrap();
@ -18,7 +31,7 @@ impl Cache {
Cache {
root: location,
use_audio_cache: use_audio_cache
use_audio_cache: use_audio_cache,
}
}
}

View file

@ -1,7 +1,7 @@
use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
use futures::sync::{BiLock, mpsc};
use futures::{Poll, Async, Stream};
use futures::{Async, Poll, Stream};
use futures::sync::{mpsc, BiLock};
use std::collections::HashMap;
use util::SeqGenerator;
@ -54,7 +54,7 @@ impl ChannelManager {
(seq, channel)
}
pub fn dispatch(&self, cmd: u8, mut data: Bytes) {
pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) {
use std::collections::hash_map::Entry;
let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref());

View file

@ -4,7 +4,7 @@ macro_rules! component {
pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::std::sync::Mutex<$inner>)>);
impl $name {
#[allow(dead_code)]
pub fn new(session: $crate::session::SessionWeak) -> $name {
pub(crate) fn new(session: $crate::session::SessionWeak) -> $name {
debug!(target:"librespot::component", "new {}", stringify!($name));
$name(::std::sync::Arc::new((session, ::std::sync::Mutex::new($inner {
@ -36,20 +36,20 @@ macro_rules! component {
}
}
use std::sync::Mutex;
use std::cell::UnsafeCell;
use std::sync::Mutex;
pub struct Lazy<T>(Mutex<bool>, UnsafeCell<Option<T>>);
pub(crate) struct Lazy<T>(Mutex<bool>, UnsafeCell<Option<T>>);
unsafe impl<T: Sync> Sync for Lazy<T> {}
unsafe impl<T: Send> Send for Lazy<T> {}
#[cfg_attr(feature = "cargo-clippy", allow(mutex_atomic))]
impl<T> Lazy<T> {
pub fn new() -> Lazy<T> {
pub(crate) fn new() -> Lazy<T> {
Lazy(Mutex::new(false), UnsafeCell::new(None))
}
pub fn get<F: FnOnce() -> T>(&self, f: F) -> &T {
pub(crate) fn get<F: FnOnce() -> T>(&self, f: F) -> &T {
let mut inner = self.0.lock().unwrap();
if !*inner {
unsafe {

View file

@ -1,6 +1,6 @@
use uuid::Uuid;
use std::str::FromStr;
use std::fmt;
use std::str::FromStr;
use uuid::Uuid;
use version;
@ -20,32 +20,6 @@ impl Default for SessionConfig {
}
}
#[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,
@ -100,23 +74,6 @@ impl Default for DeviceType {
}
}
#[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,

View file

@ -1,5 +1,5 @@
use byteorder::{BigEndian, ByteOrder};
use bytes::{Bytes, BytesMut, BufMut};
use bytes::{BufMut, Bytes, BytesMut};
use shannon::Shannon;
use std::io;
use tokio_io::codec::{Decoder, Encoder};
@ -88,7 +88,8 @@ impl Decoder for APCodec {
let mut payload = buf.split_to(size + MAC_SIZE);
self.decode_cipher.decrypt(&mut payload.get_mut(..size).unwrap());
self.decode_cipher
.decrypt(&mut payload.get_mut(..size).unwrap());
let mac = payload.split_off(size);
self.decode_cipher.check_mac(mac.as_ref())?;
@ -96,7 +97,6 @@ impl Decoder for APCodec {
}
}
Ok(None)
}
}

View file

@ -1,20 +1,21 @@
use crypto::sha1::Sha1;
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use crypto::hmac::Hmac;
use crypto::mac::Mac;use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use crypto::mac::Mac;
use crypto::sha1::Sha1;
use futures::{Async, Future, Poll};
use protobuf::{self, Message, MessageStatic};
use rand::thread_rng;
use std::io::{self, Read};
use std::marker::PhantomData;
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 tokio_io::io::{read_exact, write_all, ReadExact, Window, WriteAll};
use super::codec::APCodec;
use diffie_hellman::DHLocalKeys;
use protocol;
use protocol::keyexchange::{ClientHello, APResponseMessage, ClientResponsePlaintext};
use protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext};
use util;
use super::codec::APCodec;
pub struct Handshake<T> {
keys: DHLocalKeys,
@ -54,15 +55,15 @@ impl <T: AsyncRead + AsyncWrite> Future for Handshake<T> {
APResponse(ref mut read) => {
let (connection, message, accumulator) = try_ready!(read.poll());
let remote_key = message.get_challenge()
let remote_key = message
.get_challenge()
.get_login_crypto_challenge()
.get_diffie_hellman()
.get_gs()
.to_owned();
let shared_secret = self.keys.shared_secret(&remote_key);
let (challenge, send_key, recv_key) = compute_keys(&shared_secret,
&accumulator);
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator);
let codec = APCodec::new(&send_key, &recv_key);
let write = client_response(connection, challenge);
@ -81,22 +82,27 @@ impl <T: AsyncRead + AsyncWrite> Future for Handshake<T> {
}
fn client_hello<T: AsyncWrite>(connection: T, gc: Vec<u8>) -> WriteAll<T, Vec<u8>> {
let packet = protobuf_init!(ClientHello::new(), {
build_info => {
product: protocol::keyexchange::Product::PRODUCT_PARTNER,
platform: protocol::keyexchange::Platform::PLATFORM_LINUX_X86,
version: 0x10800000000,
},
cryptosuites_supported => [
protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON,
],
login_crypto_hello.diffie_hellman => {
gc: gc,
server_keys_known: 1,
},
client_nonce: util::rand_vec(&mut thread_rng(), 0x10),
padding: vec![0x1e],
});
let mut packet = ClientHello::new();
packet
.mut_build_info()
.set_product(protocol::keyexchange::Product::PRODUCT_PARTNER);
packet
.mut_build_info()
.set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86);
packet.mut_build_info().set_version(0x10800000000);
packet
.mut_cryptosuites_supported()
.push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON);
packet
.mut_login_crypto_hello()
.mut_diffie_hellman()
.set_gc(gc);
packet
.mut_login_crypto_hello()
.mut_diffie_hellman()
.set_server_keys_known(1);
packet.set_client_nonce(util::rand_vec(&mut thread_rng(), 0x10));
packet.set_padding(vec![0x1e]);
let mut buffer = vec![0, 4];
let size = 2 + 4 + packet.compute_size();
@ -107,13 +113,13 @@ fn client_hello<T: AsyncWrite>(connection: T, gc: 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(), {
login_crypto_response.diffie_hellman => {
hmac: challenge
},
pow_response => {},
crypto_response => {},
});
let mut packet = ClientResponsePlaintext::new();
packet
.mut_login_crypto_response()
.mut_diffie_hellman()
.set_hmac(challenge);
packet.mut_pow_response();
packet.mut_crypto_response();
let mut buffer = vec![];
let size = 4 + packet.compute_size();
@ -129,15 +135,17 @@ enum RecvPacket<T, M: MessageStatic> {
}
fn recv_packet<T: AsyncRead, M>(connection: T, acc: Vec<u8>) -> RecvPacket<T, M>
where T: Read,
M: MessageStatic
where
T: Read,
M: MessageStatic,
{
RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData)
}
impl<T: AsyncRead, M> Future for RecvPacket<T, M>
where T: Read,
M: MessageStatic
where
T: Read,
M: MessageStatic,
{
type Item = (T, M, Vec<u8>);
type Error = io::Error;
@ -167,7 +175,11 @@ impl <T: AsyncRead, M> Future for RecvPacket<T, M>
}
}
fn read_into_accumulator<T: AsyncRead>(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();
acc.resize(offset + size, 0);
@ -191,5 +203,9 @@ fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<
mac = Hmac::new(Sha1::new(), &data[..0x14]);
mac.input(packets);
(mac.result().code().to_vec(), data[0x14..0x34].to_vec(), data[0x34..0x54].to_vec())
(
mac.result().code().to_vec(),
data[0x14..0x34].to_vec(),
data[0x34..0x54].to_vec(),
)
}

View file

@ -5,58 +5,71 @@ pub use self::codec::APCodec;
pub use self::handshake::handshake;
use futures::{Future, Sink, Stream};
use protobuf::{self, Message};
use std::io;
use std::net::ToSocketAddrs;
use tokio_core::net::TcpStream;
use tokio_core::reactor::Handle;
use tokio_io::codec::Framed;
use protobuf::{self, Message};
use authentication::Credentials;
use version;
pub type Transport = Framed<TcpStream, APCodec>;
pub fn connect<A: ToSocketAddrs>(addr: A, handle: &Handle) -> Box<Future<Item = Transport, Error = 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 socket = TcpStream::connect(&addr, handle);
let connection = socket.and_then(|socket| {
handshake(socket)
});
let connection = socket.and_then(|socket| handshake(socket));
Box::new(connection)
}
pub fn authenticate(transport: Transport, credentials: Credentials, device_id: String)
-> Box<Future<Item = (Transport, Credentials), Error = io::Error>>
{
pub fn authenticate(
transport: Transport,
credentials: Credentials,
device_id: String,
) -> Box<Future<Item = (Transport, Credentials), Error = io::Error>> {
use protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
use protocol::keyexchange::APLoginFailed;
let packet = protobuf_init!(ClientResponseEncrypted::new(), {
login_credentials => {
username: credentials.username,
typ: credentials.auth_type,
auth_data: credentials.auth_data,
},
system_info => {
cpu_family: CpuFamily::CPU_UNKNOWN,
os: Os::OS_UNKNOWN,
system_information_string: format!("librespot_{}_{}", version::short_sha(), version::build_id()),
device_id: device_id,
},
version_string: version::version_string(),
});
let mut packet = ClientResponseEncrypted::new();
packet
.mut_login_credentials()
.set_username(credentials.username);
packet
.mut_login_credentials()
.set_typ(credentials.auth_type);
packet
.mut_login_credentials()
.set_auth_data(credentials.auth_data);
packet
.mut_system_info()
.set_cpu_family(CpuFamily::CPU_UNKNOWN);
packet.mut_system_info().set_os(Os::OS_UNKNOWN);
packet
.mut_system_info()
.set_system_information_string(format!(
"librespot_{}_{}",
version::short_sha(),
version::build_id()
));
packet.mut_system_info().set_device_id(device_id);
packet.set_version_string(version::version_string());
let cmd = 0xab;
let data = packet.write_to_bytes().unwrap();
Box::new(transport.send((cmd, data)).and_then(|transport| {
transport.into_future().map_err(|(err, _stream)| err)
}).and_then(|(packet, transport)| {
match packet {
Box::new(
transport
.send((cmd, data))
.and_then(|transport| transport.into_future().map_err(|(err, _stream)| err))
.and_then(|(packet, transport)| match packet {
Some((0xac, data)) => {
let welcome_data: APWelcome =
protobuf::parse_from_bytes(data.as_ref()).unwrap();
let welcome_data: APWelcome = protobuf::parse_from_bytes(data.as_ref()).unwrap();
let reusable_credentials = Credentials {
username: welcome_data.get_canonical_username().to_owned(),
@ -67,9 +80,13 @@ pub fn authenticate(transport: Transport, credentials: Credentials, device_id: S
Ok((transport, reusable_credentials))
}
Some((0xad, _)) => panic!("Authentication failed"),
Some((0xad, data)) => {
let error_data: APLoginFailed = protobuf::parse_from_bytes(data.as_ref()).unwrap();
panic!("Authentication failed with reason: {:?}", error_data.get_error_code())
}
Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd),
None => panic!("EOF"),
}
}))
}),
)
}

View file

@ -43,9 +43,11 @@ impl DHLocalKeys {
}
pub fn shared_secret(&self, remote_key: &[u8]) -> Vec<u8> {
let shared_key = util::powm(&BigUint::from_bytes_be(remote_key),
let shared_key = util::powm(
&BigUint::from_bytes_be(remote_key),
&self.private_key,
&DH_PRIME);
&DH_PRIME,
);
shared_key.to_bytes_be()
}
}

View file

@ -1,8 +1,8 @@
use futures::Future;
use serde_json;
use core::mercury::MercuryError;
use core::session::Session;
use mercury::MercuryError;
use session::Session;
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
@ -13,9 +13,15 @@ pub struct Token {
pub scope: Vec<String>,
}
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={}",
client_id, scopes);
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={}",
client_id, scopes
);
Box::new(session.mercury().get(url).map(move |response| {
let data = response.payload.first().expect("Empty payload");
let data = String::from_utf8(data.clone()).unwrap();

View file

@ -1,2 +0,0 @@
#[allow(unused_mut)]
pub mod connection;

View file

@ -1,10 +1,15 @@
#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))]
#[macro_use] extern crate error_chain;
#[macro_use] extern crate futures;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate log;
#[macro_use] extern crate serde_derive;
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate futures;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
extern crate base64;
extern crate byteorder;
@ -26,17 +31,19 @@ extern crate uuid;
extern crate librespot_protocol as protocol;
#[macro_use] mod component;
pub mod apresolve;
#[macro_use]
mod component;
mod apresolve;
pub mod audio_key;
pub mod authentication;
pub mod cache;
pub mod channel;
pub mod config;
mod connection;
pub mod diffie_hellman;
pub mod keymaster;
pub mod mercury;
pub mod session;
pub mod spotify_id;
pub mod util;
pub mod version;
include!(concat!(env!("OUT_DIR"), "/lib.rs"));

View file

@ -1,7 +1,7 @@
use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
use futures::sync::{oneshot, mpsc};
use futures::{Async, Poll, Future};
use futures::{Async, Future, Poll};
use futures::sync::{mpsc, oneshot};
use protobuf;
use protocol;
use std::collections::HashMap;
@ -51,9 +51,7 @@ impl MercuryManager {
seq
}
pub fn request(&self, req: MercuryRequest)
-> MercuryFuture<MercuryResponse>
{
fn request(&self, req: MercuryRequest) -> MercuryFuture<MercuryResponse> {
let (tx, rx) = oneshot::channel();
let pending = MercuryPending {
@ -72,9 +70,7 @@ impl MercuryManager {
MercuryFuture(rx)
}
pub fn get<T: Into<String>>(&self, uri: T)
-> MercuryFuture<MercuryResponse>
{
pub fn get<T: Into<String>>(&self, uri: T) -> MercuryFuture<MercuryResponse> {
self.request(MercuryRequest {
method: MercuryMethod::GET,
uri: uri.into(),
@ -83,9 +79,7 @@ impl MercuryManager {
})
}
pub fn send<T: Into<String>>(&self, uri: T, data: Vec<u8>)
-> MercuryFuture<MercuryResponse>
{
pub fn send<T: Into<String>>(&self, uri: T, data: Vec<u8>) -> MercuryFuture<MercuryResponse> {
self.request(MercuryRequest {
method: MercuryMethod::SEND,
uri: uri.into(),
@ -98,9 +92,10 @@ impl MercuryManager {
MercurySender::new(self.clone(), uri.into())
}
pub fn subscribe<T: Into<String>>(&self, uri: T)
-> Box<Future<Item = mpsc::UnboundedReceiver<MercuryResponse>, Error = MercuryError>>
{
pub fn subscribe<T: Into<String>>(
&self,
uri: T,
) -> Box<Future<Item = mpsc::UnboundedReceiver<MercuryResponse>, Error = MercuryError>> {
let uri = uri.into();
let request = self.request(MercuryRequest {
method: MercuryMethod::SUB,
@ -118,8 +113,8 @@ impl MercuryManager {
if response.payload.len() > 0 {
// Old subscription protocol, watch the provided list of URIs
for sub in response.payload {
let mut sub : protocol::pubsub::Subscription
= protobuf::parse_from_bytes(&sub).unwrap();
let mut sub: protocol::pubsub::Subscription =
protobuf::parse_from_bytes(&sub).unwrap();
let sub_uri = sub.take_uri();
debug!("subscribed sub_uri={}", sub_uri);
@ -136,7 +131,7 @@ impl MercuryManager {
}))
}
pub fn dispatch(&self, cmd: u8, mut data: Bytes) {
pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) {
let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
let seq = data.split_to(seq_len).as_ref().to_owned();
@ -147,13 +142,11 @@ impl MercuryManager {
let mut pending = match pending {
Some(pending) => pending,
None if cmd == 0xb5 => {
MercuryPending {
None if cmd == 0xb5 => MercuryPending {
parts: Vec::new(),
partial: None,
callback: None,
}
}
},
None => {
warn!("Ignore seq {:?} cmd {:x}", seq, cmd);
return;

View file

@ -1,5 +1,5 @@
use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend};
use std::collections::VecDeque;
use futures::{Async, Poll, Future, Sink, StartSend, AsyncSink};
use super::*;
@ -11,7 +11,7 @@ pub struct MercurySender {
impl MercurySender {
// TODO: pub(super) when stable
pub fn new(mercury: MercuryManager, uri: String) -> MercurySender {
pub(crate) fn new(mercury: MercuryManager, uri: String) -> MercurySender {
MercurySender {
mercury: mercury,
uri: uri,

View file

@ -37,8 +37,7 @@ impl ToString for MercuryMethod {
MercuryMethod::SUB => "SUB",
MercuryMethod::UNSUB => "UNSUB",
MercuryMethod::SEND => "SEND",
}
.to_owned()
}.to_owned()
}
}
@ -58,7 +57,9 @@ impl MercuryRequest {
packet.write_u16::<BigEndian>(seq.len() as u16).unwrap();
packet.write_all(seq).unwrap();
packet.write_u8(1).unwrap(); // Flags: FINAL
packet.write_u16::<BigEndian>(1 + self.payload.len() as u16).unwrap(); // Part count
packet
.write_u16::<BigEndian>(1 + self.payload.len() as u16)
.unwrap(); // Part count
let mut header = protocol::mercury::Header::new();
header.set_uri(self.uri.clone());
@ -68,7 +69,9 @@ impl MercuryRequest {
header.set_content_type(content_type.clone());
}
packet.write_u16::<BigEndian>(header.compute_size() as u16).unwrap();
packet
.write_u16::<BigEndian>(header.compute_size() as u16)
.unwrap();
header.write_to_writer(&mut packet).unwrap();
for p in &self.payload {

View file

@ -1,30 +1,30 @@
use bytes::Bytes;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use futures::{Async, Future, IntoFuture, Poll, Stream};
use futures::sync::mpsc;
use futures::{Future, Stream, IntoFuture, Poll, Async};
use std::io;
use std::sync::{RwLock, Arc, Weak};
use std::sync::{Arc, RwLock, Weak};
use std::sync::atomic::{AtomicUsize, Ordering, ATOMIC_USIZE_INIT};
use tokio_core::reactor::{Handle, Remote};
use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
use apresolve::apresolve_or_fallback;
use authentication::Credentials;
use cache::Cache;
use component::Lazy;
use connection;
use config::SessionConfig;
use connection;
use audio_key::AudioKeyManager;
use channel::ChannelManager;
use mercury::MercuryManager;
pub struct SessionData {
struct SessionData {
country: String,
canonical_username: String,
}
pub struct SessionInternal {
struct SessionInternal {
config: SessionConfig,
data: RwLock<SessionData>,
@ -43,7 +43,7 @@ pub struct SessionInternal {
static SESSION_COUNTER: AtomicUsize = ATOMIC_USIZE_INIT;
#[derive(Clone)]
pub struct Session(pub Arc<SessionInternal>);
pub struct Session(Arc<SessionInternal>);
pub fn device_id(name: &str) -> String {
let mut h = Sha1::new();
@ -52,13 +52,14 @@ pub fn device_id(name: &str) -> String {
}
impl Session {
pub fn connect(config: SessionConfig, credentials: Credentials,
cache: Option<Cache>, handle: Handle)
-> Box<Future<Item=Session, Error=io::Error>>
{
pub fn connect(
config: SessionConfig,
credentials: Credentials,
cache: Option<Cache>,
handle: Handle,
) -> Box<Future<Item = Session, Error = io::Error>> {
let access_point = apresolve_or_fallback::<io::Error>(&handle);
let handle_ = handle.clone();
let connection = access_point.and_then(move |addr| {
info!("Connecting to AP \"{}\"", addr);
@ -66,9 +67,8 @@ impl Session {
});
let device_id = config.device_id.clone();
let authentication = connection.and_then(move |connection| {
connection::authenticate(connection, credentials, device_id)
});
let authentication = connection
.and_then(move |connection| connection::authenticate(connection, credentials, device_id));
let result = authentication.map(move |(transport, reusable_credentials)| {
info!("Authenticated as \"{}\" !", reusable_credentials.username);
@ -77,7 +77,11 @@ impl Session {
}
let (session, task) = Session::create(
&handle, transport, config, cache, reusable_credentials.username.clone()
&handle,
transport,
config,
cache,
reusable_credentials.username.clone(),
);
handle.spawn(task.map_err(|e| panic!(e)));
@ -88,10 +92,13 @@ impl Session {
Box::new(result)
}
fn create(handle: &Handle, transport: connection::Transport,
config: SessionConfig, cache: Option<Cache>, username: String)
-> (Session, Box<Future<Item = (), Error = io::Error>>)
{
fn create(
handle: &Handle,
transport: connection::Transport,
config: SessionConfig,
cache: Option<Cache>,
username: String,
) -> (Session, Box<Future<Item = (), Error = io::Error>>) {
let (sink, stream) = transport.split();
let (sender_tx, sender_rx) = mpsc::unbounded();
@ -121,11 +128,15 @@ impl Session {
let sender_task = sender_rx
.map_err(|e| -> io::Error { panic!(e) })
.forward(sink).map(|_| ());
.forward(sink)
.map(|_| ());
let receiver_task = DispatchTask(stream, session.weak());
let task = Box::new((receiver_task, sender_task).into_future()
.map(|((), ())| ()));
let task = Box::new(
(receiver_task, sender_task)
.into_future()
.map(|((), ())| ()),
);
(session, task)
}
@ -143,16 +154,21 @@ impl Session {
}
pub fn spawn<F, R>(&self, f: F)
where F: FnOnce(&Handle) -> R + Send + 'static,
where
F: FnOnce(&Handle) -> R + Send + 'static,
R: IntoFuture<Item = (), Error = ()>,
R::Future: 'static
R::Future: 'static,
{
self.0.handle.spawn(f)
}
fn debug_info(&self) {
debug!("Session[{}] strong={} weak={}",
self.0.session_id, Arc::strong_count(&self.0), Arc::weak_count(&self.0));
debug!(
"Session[{}] strong={} weak={}",
self.0.session_id,
Arc::strong_count(&self.0),
Arc::weak_count(&self.0)
);
}
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
@ -161,7 +177,7 @@ impl Session {
0x4 => {
self.debug_info();
self.send_packet(0x49, data.as_ref().to_owned());
},
}
0x4a => (),
0x1b => {
let country = String::from_utf8(data.as_ref().to_owned()).unwrap();
@ -184,7 +200,7 @@ impl Session {
self.0.cache.as_ref()
}
pub fn config(&self) -> &SessionConfig {
fn config(&self) -> &SessionConfig {
&self.0.config
}
@ -200,7 +216,7 @@ impl Session {
&self.config().device_id
}
pub fn weak(&self) -> SessionWeak {
fn weak(&self) -> SessionWeak {
SessionWeak(Arc::downgrade(&self.0))
}
@ -210,14 +226,14 @@ impl Session {
}
#[derive(Clone)]
pub struct SessionWeak(pub Weak<SessionInternal>);
pub struct SessionWeak(Weak<SessionInternal>);
impl SessionWeak {
pub fn try_upgrade(&self) -> Option<Session> {
fn try_upgrade(&self) -> Option<Session> {
self.0.upgrade().map(Session)
}
pub fn upgrade(&self) -> Session {
pub(crate) fn upgrade(&self) -> Session {
self.try_upgrade().expect("Session died")
}
}
@ -229,10 +245,12 @@ impl Drop for SessionInternal {
}
struct DispatchTask<S>(S, SessionWeak)
where S: Stream<Item = (u8, Bytes)>;
where
S: Stream<Item = (u8, Bytes)>;
impl<S> Future for DispatchTask<S>
where S: Stream<Item = (u8, Bytes)>
where
S: Stream<Item = (u8, Bytes)>,
{
type Item = ();
type Error = S::Error;
@ -240,9 +258,7 @@ impl <S> Future for DispatchTask<S>
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let session = match self.1.try_upgrade() {
Some(session) => session,
None => {
return Ok(Async::Ready(()))
},
None => return Ok(Async::Ready(())),
};
loop {
@ -253,7 +269,8 @@ impl <S> Future for DispatchTask<S>
}
impl<S> Drop for DispatchTask<S>
where S: Stream<Item = (u8, Bytes)>
where
S: Stream<Item = (u8, Bytes)>,
{
fn drop(&mut self) {
debug!("drop Dispatch");

View file

@ -1,7 +1,7 @@
use byteorder::{BigEndian, ByteOrder};
use std;
use std::fmt;
use util::u128;
use byteorder::{BigEndian, ByteOrder};
// Unneeded since 1.21
#[allow(unused_imports)]
use std::ascii::AsciiExt;
@ -9,8 +9,7 @@ use std::ascii::AsciiExt;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct SpotifyId(u128);
const BASE62_DIGITS: &'static [u8] =
b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const BASE62_DIGITS: &'static [u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef";
impl SpotifyId {

View file

@ -28,12 +28,7 @@ impl std::ops::Add<u128> for u128 {
type Output = u128;
fn add(self, rhs: u128) -> u128 {
let low = self.low + rhs.low;
let high = self.high + rhs.high +
if low < self.low {
1
} else {
0
};
let high = self.high + rhs.high + if low < self.low { 1 } else { 0 };
u128::from_parts(high, low)
}
@ -43,12 +38,7 @@ impl<'a> std::ops::Add<&'a u128> for u128 {
type Output = u128;
fn add(self, rhs: &'a u128) -> u128 {
let low = self.low + rhs.low;
let high = self.high + rhs.high +
if low < self.low {
1
} else {
0
};
let high = self.high + rhs.high + if low < self.low { 1 } else { 0 };
u128::from_parts(high, low)
}
@ -60,20 +50,23 @@ impl std::convert::From<u8> for u128 {
}
}
impl std::ops::Mul<u128> for u128 {
type Output = u128;
fn mul(self, rhs: u128) -> u128 {
let top: [u64; 4] = [self.high >> 32,
let top: [u64; 4] = [
self.high >> 32,
self.high & 0xFFFFFFFF,
self.low >> 32,
self.low & 0xFFFFFFFF];
self.low & 0xFFFFFFFF,
];
let bottom: [u64; 4] = [rhs.high >> 32,
let bottom: [u64; 4] = [
rhs.high >> 32,
rhs.high & 0xFFFFFFFF,
rhs.low >> 32,
rhs.low & 0xFFFFFFFF];
rhs.low & 0xFFFFFFFF,
];
let mut rows = [u128::zero(); 16];
for i in 0..4 {

View file

@ -1,55 +1,18 @@
use num_bigint::BigUint;
use num_traits::{Zero, One};
use num_integer::Integer;
use rand::{Rng, Rand};
use std::io;
use num_traits::{One, Zero};
use rand::{Rand, Rng};
use std::mem;
use std::ops::{Mul, Rem, Shr};
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::{UNIX_EPOCH, SystemTime};
mod int128;
mod spotify_id;
mod subfile;
pub use util::int128::u128;
pub use util::spotify_id::{SpotifyId, FileId};
pub use util::subfile::Subfile;
pub fn rand_vec<G: Rng, R: Rand>(rng: &mut G, size: usize) -> Vec<R> {
rng.gen_iter().take(size).collect()
}
pub fn now_ms() -> i64 {
let dur = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(dur) => dur,
Err(err) => err.duration(),
};
(dur.as_secs() * 1000 + (dur.subsec_nanos() / 1000_000) as u64) as i64
}
pub fn mkdir_existing(path: &Path) -> io::Result<()> {
fs::create_dir(path).or_else(|err| {
if err.kind() == io::ErrorKind::AlreadyExists {
Ok(())
} else {
Err(err)
}
})
}
pub fn run_program(program: &str) {
info!("Running {}", program);
let mut v: Vec<&str> = program.split_whitespace().collect();
let status = Command::new(&v.remove(0))
.args(&v)
.status()
.expect("program failed to start");
info!("Exit status: {}", status);
}
pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
let mut base = base.clone();
let mut exp = exp.clone();
@ -66,32 +29,6 @@ pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
result
}
pub struct StrChunks<'s>(&'s str, usize);
pub trait StrChunksExt {
fn chunks(&self, size: usize) -> StrChunks;
}
impl StrChunksExt for str {
fn chunks(&self, size: usize) -> StrChunks {
StrChunks(self, size)
}
}
impl<'s> Iterator for StrChunks<'s> {
type Item = &'s str;
fn next(&mut self) -> Option<&'s str> {
let &mut StrChunks(data, size) = self;
if data.is_empty() {
None
} else {
let ret = Some(&data[..size]);
self.0 = &data[size..];
ret
}
}
}
pub trait ReadSeek: ::std::io::Read + ::std::io::Seek {}
impl<T: ::std::io::Read + ::std::io::Seek> ReadSeek for T {}

View file

@ -1,38 +0,0 @@
use std::io::{Read, Seek, SeekFrom, Result};
pub struct Subfile<T: Read + Seek> {
stream: T,
offset: u64,
}
impl<T: Read + Seek> Subfile<T> {
pub fn new(mut stream: T, offset: u64) -> Subfile<T> {
stream.seek(SeekFrom::Start(offset)).unwrap();
Subfile {
stream: stream,
offset: offset,
}
}
}
impl<T: Read + Seek> Read for Subfile<T> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
self.stream.read(buf)
}
}
impl<T: Read + Seek> Seek for Subfile<T> {
fn seek(&mut self, mut pos: SeekFrom) -> Result<u64> {
pos = match pos {
SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset),
x => x,
};
let newpos = try!(self.stream.seek(pos));
if newpos > self.offset {
Ok(newpos - self.offset)
} else {
Ok(0)
}
}
}

View file

@ -5,9 +5,9 @@ use std::env;
use tokio_core::reactor::Core;
use librespot::core::authentication::Credentials;
use librespot::core::config::{PlayerConfig, SessionConfig};
use librespot::playback::config::{PlayerConfig, SessionConfig};
use librespot::core::session::Session;
use librespot::core::util::SpotifyId;
use librespot::core::spotify_id::SpotifyId;
use librespot::audio_backend;
use librespot::player::Player;

View file

@ -3,7 +3,7 @@ use std::io::Write;
use core::channel::ChannelData;
use core::session::Session;
use core::util::FileId;
use core::spotify_id::FileId;
pub fn get(session: &Session, file: FileId) -> ChannelData {
let (channel_id, channel) = session.channel().allocate();

View file

@ -13,7 +13,7 @@ use linear_map::LinearMap;
use core::mercury::MercuryError;
use core::session::Session;
use core::util::{SpotifyId, FileId, StrChunksExt};
use core::spotify_id::{FileId, SpotifyId};
pub use protocol::metadata::AudioFile_Format as FileFormat;
@ -22,7 +22,8 @@ fn countrylist_contains(list: &str, country: &str) -> bool {
}
fn parse_restrictions<'s, I>(restrictions: I, country: &str, catalogue: &str) -> bool
where I: IntoIterator<Item = &'s protocol::metadata::Restriction>
where
I: IntoIterator<Item = &'s protocol::metadata::Restriction>,
{
let mut forbidden = "".to_string();
let mut has_forbidden = false;
@ -30,9 +31,9 @@ fn parse_restrictions<'s, I>(restrictions: I, country: &str, catalogue: &str) ->
let mut allowed = "".to_string();
let mut has_allowed = false;
let rs = restrictions.into_iter().filter(|r|
r.get_catalogue_str().contains(&catalogue.to_owned())
);
let rs = restrictions
.into_iter()
.filter(|r| r.get_catalogue_str().contains(&catalogue.to_owned()));
for r in rs {
if r.has_countries_forbidden() {
@ -46,9 +47,9 @@ fn parse_restrictions<'s, I>(restrictions: I, country: &str, catalogue: &str) ->
}
}
(has_forbidden || has_allowed) &&
(!has_forbidden || !countrylist_contains(forbidden.as_str(), country)) &&
(!has_allowed || countrylist_contains(allowed.as_str(), country))
(has_forbidden || has_allowed)
&& (!has_forbidden || !countrylist_contains(forbidden.as_str(), country))
&& (!has_allowed || countrylist_contains(allowed.as_str(), country))
}
pub trait Metadata: Send + Sized + 'static {
@ -75,6 +76,7 @@ pub trait Metadata : Send + Sized + 'static {
pub struct Track {
pub id: SpotifyId,
pub name: String,
pub duration: i32,
pub album: SpotifyId,
pub artists: Vec<SpotifyId>,
pub files: LinearMap<FileFormat, FileId>,
@ -127,6 +129,7 @@ impl Metadata for Track {
Track {
id: SpotifyId::from_raw(msg.get_gid()),
name: msg.get_name().to_owned(),
duration: msg.get_duration(),
album: SpotifyId::from_raw(msg.get_album().get_gid()),
artists: artists,
files: files,
@ -134,9 +137,7 @@ impl Metadata for Track {
.iter()
.map(|alt| SpotifyId::from_raw(alt.get_gid()))
.collect(),
available: parse_restrictions(msg.get_restriction(),
&country,
"premium"),
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
}
}
}
@ -183,7 +184,6 @@ impl Metadata for Album {
}
}
impl Metadata for Artist {
type Message = protocol::metadata::Artist;
@ -196,23 +196,47 @@ impl Metadata for Artist {
let top_tracks: Vec<SpotifyId> = match msg.get_top_track()
.iter()
.find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country)) {
Some(tracks) => {
tracks.get_track()
.find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country))
{
Some(tracks) => tracks
.get_track()
.iter()
.filter(|track| track.has_gid())
.map(|track| SpotifyId::from_raw(track.get_gid()))
.collect::<Vec<_>>()
},
None => Vec::new()
.collect::<Vec<_>>(),
None => Vec::new(),
};
Artist {
id: SpotifyId::from_raw(msg.get_gid()),
name: msg.get_name().to_owned(),
top_tracks: top_tracks
top_tracks: top_tracks,
}
}
}
struct StrChunks<'s>(&'s str, usize);
trait StrChunksExt {
fn chunks(&self, size: usize) -> StrChunks;
}
impl StrChunksExt for str {
fn chunks(&self, size: usize) -> StrChunks {
StrChunks(self, size)
}
}
impl<'s> Iterator for StrChunks<'s> {
type Item = &'s str;
fn next(&mut self) -> Option<&'s str> {
let &mut StrChunks(data, size) = self;
if data.is_empty() {
None
} else {
let ret = Some(&data[..size]);
self.0 = &data[size..];
ret
}
}
}

27
playback/Cargo.toml Normal file
View file

@ -0,0 +1,27 @@
[package]
name = "librespot-playback"
version = "0.1.0"
authors = ["Sasha Hilton <sashahilton00@gmail.com>"]
[dependencies.librespot-audio]
path = "../audio"
[dependencies.librespot-core]
path = "../core"
[dependencies.librespot-metadata]
path = "../metadata"
[dependencies]
futures = "0.1.8"
log = "0.3.5"
alsa = { git = "https://github.com/plietar/rust-alsa", optional = true }
portaudio-rs = { version = "0.3.0", optional = true }
libpulse-sys = { version = "0.0.0", optional = true }
jack = { version = "0.5.3", optional = true }
libc = { version = "0.2", optional = true }
[features]
alsa-backend = ["alsa"]
portaudio-backend = ["portaudio-rs"]
pulseaudio-backend = ["libpulse-sys", "libc"]
jackaudio-backend = ["jack"]

View file

@ -16,12 +16,17 @@ impl Open for AlsaSink {
impl Sink for AlsaSink {
fn start(&mut self) -> io::Result<()> {
if self.0.is_some() {
} else {
self.0 = Some(PCM::open(&*self.1,
if self.0.is_none() {
match PCM::open(&*self.1,
Stream::Playback, Mode::Blocking,
Format::Signed16, Access::Interleaved,
2, 44100).ok().unwrap());
2, 44100) {
Ok(f) => self.0 = Some(f),
Err(e) => {
error!("Alsa error PCM open {}", e);
return Err(io::Error::new(io::ErrorKind::Other, "Alsa error: PCM open failed"));
}
}
}
Ok(())
}

View file

@ -0,0 +1,75 @@
use std::io;
use super::{Open, Sink};
use jack::prelude::{AudioOutPort, AudioOutSpec, Client, JackControl, ProcessScope, AsyncClient, client_options, ProcessHandler, Port };
use std::sync::mpsc::{sync_channel, SyncSender, Receiver};
pub struct JackSink {
send: SyncSender<i16>,
active_client: AsyncClient<(),JackData>,
}
pub struct JackData {
rec: Receiver<i16>,
port_l: Port<AudioOutSpec>,
port_r: Port<AudioOutSpec>,
}
fn pcm_to_f32(sample: i16) -> f32 {
sample as f32 / 32768.0;
}
impl ProcessHandler for JackData {
fn process(&mut self, _: &Client, ps: &ProcessScope) -> JackControl {
// get output port buffers
let mut out_r = AudioOutPort::new(&mut self.port_r, ps);
let mut out_l = AudioOutPort::new(&mut self.port_l, ps);
let buf_r: &mut [f32] = &mut out_r;
let buf_l: &mut [f32] = &mut out_l;
// get queue iterator
let mut queue_iter = self.rec.try_iter();
let buf_size = buf_r.len();
for i in 0..buf_size {
buf_r[i] = pcm_to_f32(queue_iter.next().unwrap_or(0));
buf_l[i] = pcm_to_f32(queue_iter.next().unwrap_or(0));
}
JackControl::Continue
}
}
impl Open for JackSink {
fn open(client_name: Option<String>) -> JackSink {
info!("Using jack sink!");
let client_name = client_name.unwrap_or("librespot".to_string());
let (client, _status) = Client::new(&client_name[..], client_options::NO_START_SERVER).unwrap();
let ch_r = client.register_port("out_0", AudioOutSpec::default()).unwrap();
let ch_l = client.register_port("out_1", AudioOutSpec::default()).unwrap();
// buffer for samples from librespot (~10ms)
let (tx, rx) = sync_channel(2*1024*4);
let jack_data = JackData { rec: rx, port_l: ch_l, port_r: ch_r };
let active_client = AsyncClient::new(client, (), jack_data).unwrap();
JackSink { send: tx, active_client: active_client }
}
}
impl Sink for JackSink {
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
fn write(&mut self, data: &[i16]) -> io::Result<()> {
for s in data.iter() {
let res = self.send.send(*s);
if res.is_err() {
error!("jackaudio: cannot write to channel");
}
}
Ok(())
}
}

View file

@ -29,6 +29,11 @@ mod pulseaudio;
#[cfg(feature = "pulseaudio-backend")]
use self::pulseaudio::PulseAudioSink;
#[cfg(feature = "jackaudio-backend")]
mod jackaudio;
#[cfg(feature = "jackaudio-backend")]
use self::jackaudio::JackSink;
mod pipe;
use self::pipe::StdoutSink;
@ -41,6 +46,8 @@ pub const BACKENDS : &'static [
("portaudio", mk_sink::<PortAudioSink>),
#[cfg(feature = "pulseaudio-backend")]
("pulseaudio", mk_sink::<PulseAudioSink>),
#[cfg(feature = "jackaudio-backend")]
("jackaudio", mk_sink::<JackSink>),
("pipe", mk_sink::<StdoutSink>),
];

43
playback/src/config.rs Normal file
View file

@ -0,0 +1,43 @@
use std::str::FromStr;
#[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, 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,
}
}
}

27
playback/src/lib.rs Normal file
View file

@ -0,0 +1,27 @@
#[macro_use] extern crate log;
extern crate futures;
#[cfg(feature = "alsa-backend")]
extern crate alsa;
#[cfg(feature = "portaudio-rs")]
extern crate portaudio_rs;
#[cfg(feature = "libpulse-sys")]
extern crate libpulse_sys;
#[cfg(feature = "jackaudio-backend")]
extern crate jack;
#[cfg(feature = "libc")]
extern crate libc;
extern crate librespot_audio as audio;
extern crate librespot_core as core;
extern crate librespot_metadata as metadata;
pub mod audio_backend;
pub mod config;
pub mod mixer;
pub mod player;

View file

@ -1,15 +1,17 @@
use futures::sync::oneshot;
use futures::{future, Future};
use std;
use std::borrow::Cow;
use std::io::{Read, Seek, SeekFrom, Result};
use std::mem;
use std::process::Command;
use std::sync::mpsc::{RecvError, TryRecvError, RecvTimeoutError};
use std::thread;
use std::time::Duration;
use std;
use core::config::{Bitrate, PlayerConfig};
use config::{Bitrate, PlayerConfig};
use core::session::Session;
use core::util::{self, SpotifyId, Subfile};
use core::spotify_id::SpotifyId;
use audio_backend::Sink;
use audio::{AudioFile, AudioDecrypt};
@ -375,13 +377,13 @@ impl PlayerInternal {
fn run_onstart(&self) {
if let Some(ref program) = self.config.onstart {
util::run_program(program)
run_program(program)
}
}
fn run_onstop(&self) {
if let Some(ref program) = self.config.onstop {
util::run_program(program)
run_program(program)
}
}
@ -478,3 +480,50 @@ impl ::std::fmt::Debug for PlayerCommand {
}
}
}
struct Subfile<T: Read + Seek> {
stream: T,
offset: u64,
}
impl<T: Read + Seek> Subfile<T> {
pub fn new(mut stream: T, offset: u64) -> Subfile<T> {
stream.seek(SeekFrom::Start(offset)).unwrap();
Subfile {
stream: stream,
offset: offset,
}
}
}
impl<T: Read + Seek> Read for Subfile<T> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
self.stream.read(buf)
}
}
impl<T: Read + Seek> Seek for Subfile<T> {
fn seek(&mut self, mut pos: SeekFrom) -> Result<u64> {
pos = match pos {
SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset),
x => x,
};
let newpos = try!(self.stream.seek(pos));
if newpos > self.offset {
Ok(newpos - self.offset)
} else {
Ok(0)
}
}
}
fn run_program(program: &str) {
info!("Running {}", program);
let mut v: Vec<&str> = program.split_whitespace().collect();
let status = Command::new(&v.remove(0))
.args(&v)
.status()
.expect("program failed to start");
info!("Exit status: {}", status);
}

4
rustfmt.toml Normal file
View file

@ -0,0 +1,4 @@
max_width = 105
reorder_imports = true
reorder_imports_in_group = true
reorder_modules = true

View file

@ -2,15 +2,10 @@
#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))]
#[macro_use] extern crate log;
#[macro_use] extern crate serde_json;
#[macro_use] extern crate serde_derive;
extern crate base64;
extern crate crypto;
extern crate futures;
extern crate hyper;
extern crate mdns;
extern crate num_bigint;
extern crate protobuf;
extern crate rand;
@ -18,26 +13,8 @@ extern crate tokio_core;
extern crate url;
pub extern crate librespot_audio as audio;
pub extern crate librespot_connect as connect;
pub extern crate librespot_core as core;
pub extern crate librespot_playback as playback;
pub extern crate librespot_protocol as protocol;
pub extern crate librespot_metadata as metadata;
#[cfg(feature = "alsa-backend")]
extern crate alsa;
#[cfg(feature = "portaudio-rs")]
extern crate portaudio_rs;
#[cfg(feature = "libpulse-sys")]
extern crate libpulse_sys;
#[cfg(feature = "libc")]
extern crate libc;
pub mod audio_backend;
pub mod discovery;
pub mod keymaster;
pub mod mixer;
pub mod player;
include!(concat!(env!("OUT_DIR"), "/lib.rs"));

View file

@ -20,15 +20,16 @@ use std::mem;
use librespot::core::authentication::{get_credentials, Credentials};
use librespot::core::cache::Cache;
use librespot::core::config::{Bitrate, DeviceType, PlayerConfig, SessionConfig, ConnectConfig};
use librespot::core::config::{DeviceType, SessionConfig, ConnectConfig};
use librespot::core::session::Session;
use librespot::core::version;
use librespot::audio_backend::{self, Sink, BACKENDS};
use librespot::discovery::{discovery, DiscoveryStream};
use librespot::mixer::{self, Mixer};
use librespot::player::Player;
use librespot::spirc::{Spirc, SpircTask};
use librespot::playback::audio_backend::{self, Sink, BACKENDS};
use librespot::playback::config::{Bitrate, PlayerConfig};
use librespot::connect::discovery::{discovery, DiscoveryStream};
use librespot::playback::mixer::{self, Mixer};
use librespot::playback::player::Player;
use librespot::connect::spirc::{Spirc, SpircTask};
fn usage(program: &str, opts: &getopts::Options) -> String {
let brief = format!("Usage: {} [options]", program);
@ -81,6 +82,7 @@ struct Setup {
connect_config: ConnectConfig,
credentials: Option<Credentials>,
enable_discovery: bool,
zeroconf_port: u16,
}
fn setup(args: &[String]) -> Setup {
@ -97,9 +99,10 @@ fn setup(args: &[String]) -> Setup {
.optopt("p", "password", "Password", "PASSWORD")
.optflag("", "disable-discovery", "Disable discovery mode")
.optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND")
.optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE")
.optopt("", "device", "Audio device to use. Use '?' to list options if using portaudio", "DEVICE")
.optopt("", "mixer", "Mixer to use", "MIXER")
.optopt("", "initial-volume", "Initial volume in %, once connected (must be from 0 to 100)", "VOLUME");
.optopt("", "initial-volume", "Initial volume in %, once connected (must be from 0 to 100)", "VOLUME")
.optopt("", "zeroconf-port", "The port the internal server advertised over zeroconf uses.", "ZEROCONF_PORT");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
@ -132,33 +135,22 @@ fn setup(args: &[String]) -> Setup {
let mixer_name = matches.opt_str("mixer");
let mixer = mixer::find(mixer_name.as_ref())
.expect("Invalid mixer");
let initial_volume;
// check if initial-volume argument is present
if matches.opt_present("initial-volume"){
// check if value is a number
if matches.opt_str("initial-volume").unwrap().parse::<i32>().is_ok(){
// check if value is in [0-100] range, otherwise put the bound values
if matches.opt_str("initial-volume").unwrap().parse::<i32>().unwrap() < 0 {
initial_volume = 0 as i32;
let initial_volume = matches
.opt_str("initial-volume")
.map(|volume| {
let volume = volume.parse::<i32>().unwrap();
if volume < 0 || volume > 100 {
panic!("Initial volume must be in the range 0-100");
}
else if matches.opt_str("initial-volume").unwrap().parse::<i32>().unwrap() > 100{
initial_volume = 0xFFFF as i32;
}
// checks ok
else{
initial_volume = matches.opt_str("initial-volume").unwrap().parse::<i32>().unwrap()* 0xFFFF as i32 / 100 ;
}
}
// if value is not a number use default value (50%)
else {
initial_volume = 0x8000 as i32;
}
}
// if argument not present use default values (50%)
else{
initial_volume = 0x8000 as i32;
}
debug!("Volume \"{}\" !", initial_volume);
volume * 0xFFFF / 100
})
.unwrap_or(0x8000);
let zeroconf_port =
matches.opt_str("zeroconf-port")
.map(|port| port.parse::<u16>().unwrap())
.unwrap_or(0);
let name = matches.opt_str("name").unwrap();
let use_audio_cache = !matches.opt_present("disable-audio-cache");
@ -221,6 +213,7 @@ fn setup(args: &[String]) -> Setup {
credentials: credentials,
device: device,
enable_discovery: enable_discovery,
zeroconf_port: zeroconf_port,
mixer: mixer,
}
}
@ -269,7 +262,7 @@ impl Main {
let config = task.connect_config.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 {
@ -362,6 +355,9 @@ impl Future for Main {
}
fn main() {
if env::var("RUST_BACKTRACE").is_err() {
env::set_var("RUST_BACKTRACE", "full")
}
let mut core = Core::new().unwrap();
let handle = core.handle();