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 .cargo
spotify_appkey.key spotify_appkey.key
.vagrant/ .vagrant/
.project
.history

View file

@ -1,6 +1,6 @@
language: rust language: rust
rust: rust:
- 1.18.0 - 1.20.0
- stable - stable
- beta - beta
- nightly - nightly
@ -20,15 +20,19 @@ before_script:
- echo '[target.armv7-unknown-linux-gnueabihf]' > ~/.cargo/config - echo '[target.armv7-unknown-linux-gnueabihf]' > ~/.cargo/config
- echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config - echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config
- rustup target add armv7-unknown-linux-gnueabihf - 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: script:
- cargo build --no-default-features - cargo build --locked --no-default-features
- cargo build --no-default-features --features "with-tremor" - cargo build --locked --no-default-features --features "with-tremor"
- cargo build --no-default-features --features "with-lewton"; - cargo build --locked --no-default-features --features "with-vorbis"
- cargo build --no-default-features --features "portaudio-backend" - cargo build --locked --no-default-features --features "portaudio-backend"
- cargo build --no-default-features --features "pulseaudio-backend" - cargo build --locked --no-default-features --features "pulseaudio-backend"
- cargo build --no-default-features --features "alsa-backend" - cargo build --locked --no-default-features --features "alsa-backend"
- cargo build --no-default-features --target armv7-unknown-linux-gnueabihf - cargo build --locked --no-default-features --target armv7-unknown-linux-gnueabihf
notifications: notifications:
email: false 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)", "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]] [[package]]
name = "dtoa" name = "dtoa"
version = "0.4.2" version = "0.4.2"
@ -215,6 +224,27 @@ name = "itoa"
version = "0.3.4" version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "kernel32-sys" name = "kernel32-sys"
version = "0.2.2" version = "0.2.2"
@ -246,7 +276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "lewton" name = "lewton"
version = "0.6.2" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -258,6 +288,16 @@ name = "libc"
version = "0.2.36" version = "0.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "libpulse-sys" name = "libpulse-sys"
version = "0.0.0" version = "0.0.0"
@ -270,27 +310,24 @@ dependencies = [
name = "librespot" name = "librespot"
version = "0.1.0" version = "0.1.0"
dependencies = [ 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)", "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)", "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)", "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)", "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)", "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-audio 0.1.0",
"librespot-connect 0.1.0",
"librespot-core 0.1.0", "librespot-core 0.1.0",
"librespot-metadata 0.1.0", "librespot-metadata 0.1.0",
"librespot-playback 0.1.0",
"librespot-protocol 0.1.0", "librespot-protocol 0.1.0",
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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 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)", "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)", "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)", "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 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
@ -308,17 +345,42 @@ dependencies = [
"bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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", "librespot-core 0.1.0",
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "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-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)", "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)", "tempfile 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tremor 0.1.0 (git+https://github.com/plietar/rust-tremor)", "tremor 0.1.0 (git+https://github.com/plietar/rust-tremor)",
"vorbis 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "librespot-core" name = "librespot-core"
version = "0.1.0" version = "0.1.0"
@ -336,10 +398,9 @@ dependencies = [
"num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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 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)", "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)", "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 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
@ -362,6 +423,22 @@ dependencies = [
"protobuf 1.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "librespot-protocol" name = "librespot-protocol"
version = "0.1.0" version = "0.1.0"
@ -660,6 +737,12 @@ dependencies = [
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "rustc-serialize" name = "rustc-serialize"
version = "0.3.24" 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 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 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-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 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 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" "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 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 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 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 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 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 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 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 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 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 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 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" "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 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 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 (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 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 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" "checksum scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f417c22df063e9450888a7561788e9bd46d3bb3c1466435b4eccb903807f147d"

View file

@ -2,7 +2,6 @@
name = "librespot" name = "librespot"
version = "0.1.0" version = "0.1.0"
authors = ["Paul Liétar <paul@lietar.net>"] authors = ["Paul Liétar <paul@lietar.net>"]
build = "build.rs"
license = "MIT" license = "MIT"
description = "Open Source Spotify client library" description = "Open Source Spotify client library"
keywords = ["spotify"] keywords = ["spotify"]
@ -22,10 +21,14 @@ doc = false
[dependencies.librespot-audio] [dependencies.librespot-audio]
path = "audio" path = "audio"
[dependencies.librespot-connect]
path = "connect"
[dependencies.librespot-core] [dependencies.librespot-core]
path = "core" path = "core"
[dependencies.librespot-metadata] [dependencies.librespot-metadata]
path = "metadata" path = "metadata"
[dependencies.librespot-playback]
path = "playback"
[dependencies.librespot-protocol] [dependencies.librespot-protocol]
path = "protocol" path = "protocol"
@ -36,12 +39,11 @@ futures = "0.1.8"
getopts = "0.2.14" getopts = "0.2.14"
hyper = "0.11.2" hyper = "0.11.2"
log = "0.3.5" log = "0.3.5"
mdns = { git = "https://github.com/plietar/rust-mdns" }
num-bigint = "0.1.35" num-bigint = "0.1.35"
protobuf = "1.1" protobuf = "1.1"
rand = "0.3.13" rand = "0.3.13"
rpassword = "0.3.0" 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 = "0.9.6"
serde_derive = "0.9.6" serde_derive = "0.9.6"
serde_json = "0.9.5" serde_json = "0.9.5"
@ -50,29 +52,30 @@ 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 }
portaudio-rs = { version = "0.3.0", optional = true }
libpulse-sys = { version = "0.0.0", optional = true }
libc = { version = "0.2", optional = true }
[build-dependencies] [build-dependencies]
rand = "0.3.13" rand = "0.3.13"
vergen = "0.1.0" vergen = "0.1.0"
protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros", features = ["with-syntex"] } 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] [features]
alsa-backend = ["alsa"] alsa-backend = ["librespot-playback/alsa-backend"]
portaudio-backend = ["portaudio-rs"] portaudio-backend = ["librespot-playback/portaudio-backend"]
pulseaudio-backend = ["libpulse-sys", "libc"] pulseaudio-backend = ["librespot-playback/pulseaudio-backend"]
jackaudio-backend = ["librespot-playback/jackaudio-backend"]
with-tremor = ["librespot-audio/with-tremor"] 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] [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 = """\

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) More information can be found in the [wiki](https://github.com/librespot-org/librespot/wiki)
# Building # 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.** **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 ## 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. 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. - [ ] Document the Spotify Protocol and provide reference example.
- [ ] Implement API to allow wrappers to be written for librespot. - [ ] 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 automatic release binaries for download
- [ ] Provide an adequate method for exporting metadata ([#7](https://github.com/librespot-org/librespot/issues/7)) - [ ] Provide an adequate method for exporting metadata ([#7](https://github.com/librespot-org/librespot/issues/7))
- [ ] Provide API Documentation - [ ] Provide API Documentation

View file

@ -10,16 +10,16 @@ path = "../core"
bit-set = "0.4.0" bit-set = "0.4.0"
byteorder = "1.0" byteorder = "1.0"
futures = "0.1.8" futures = "0.1.8"
lewton = "0.8.0"
log = "0.3.5" log = "0.3.5"
num-bigint = "0.1.35" num-bigint = "0.1.35"
num-traits = "0.1.36" 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" tempfile = "2.1"
vorbis = "0.1.0"
tremor = { git = "https://github.com/plietar/rust-tremor", optional = true } 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] [features]
with-tremor = ["tremor"] 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::channel::{Channel, ChannelData, ChannelError, ChannelHeaders};
use core::session::Session; use core::session::Session;
use core::util::FileId; use core::spotify_id::FileId;
const CHUNK_SIZE: usize = 0x20000; const CHUNK_SIZE: usize = 0x20000;
@ -115,7 +115,7 @@ impl Future for AudioFileOpenStreaming {
if id == 0x3 { if id == 0x3 {
let size = BigEndian::read_u32(&data) as usize * 4; let size = BigEndian::read_u32(&data) as usize * 4;
let file = self.finish(size); let file = self.finish(size);
return Ok(Async::Ready(file)); return Ok(Async::Ready(file));
} }
} }

View file

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

View file

@ -13,15 +13,15 @@ extern crate librespot_core as core;
mod fetch; mod fetch;
mod decrypt; mod decrypt;
#[cfg(not(feature = "with-lewton"))] #[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))]
mod libvorbis_decoder;
#[cfg(feature = "with-lewton")]
mod lewton_decoder; mod lewton_decoder;
#[cfg(any(feature = "with-tremor", feature = "with-vorbis"))]
mod libvorbis_decoder;
pub use fetch::{AudioFile, AudioFileOpen}; pub use fetch::{AudioFile, AudioFileOpen};
pub use decrypt::AudioDecrypt; pub use decrypt::AudioDecrypt;
#[cfg(not(feature = "with-lewton"))] #[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))]
pub use libvorbis_decoder::{VorbisDecoder, VorbisPacket, VorbisError};
#[cfg(feature = "with-lewton")]
pub use lewton_decoder::{VorbisDecoder, VorbisPacket, VorbisError}; 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/lib.in.rs");
println!("cargo:rerun-if-changed=src/spirc.rs"); println!("cargo:rerun-if-changed=src/spirc.rs");
} }

View file

@ -1,12 +1,18 @@
use base64; use base64;
use crypto;
use crypto::digest::Digest; use crypto::digest::Digest;
use crypto::mac::Mac; use crypto::mac::Mac;
use crypto; use futures::{Future, Poll, Stream};
use futures::sync::mpsc; use futures::sync::mpsc;
use futures::{Future, Stream, Poll};
use hyper::server::{Service, Request, Response, Http};
use hyper::{self, Get, Post, StatusCode}; 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 mdns;
use num_bigint::BigUint; use num_bigint::BigUint;
use rand; use rand;
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -15,10 +21,10 @@ use std::sync::Arc;
use tokio_core::reactor::Handle; use tokio_core::reactor::Handle;
use url; use url;
use core::diffie_hellman::{DH_GENERATOR, DH_PRIME};
use core::authentication::Credentials; use core::authentication::Credentials;
use core::util;
use core::config::ConnectConfig; use core::config::ConnectConfig;
use core::diffie_hellman::{DH_GENERATOR, DH_PRIME};
use core::util;
#[derive(Clone)] #[derive(Clone)]
struct Discovery(Arc<DiscoveryInner>); struct Discovery(Arc<DiscoveryInner>);
@ -31,9 +37,10 @@ struct DiscoveryInner {
} }
impl Discovery { impl Discovery {
fn new(config: ConnectConfig, device_id: String) fn new(
-> (Discovery, mpsc::UnboundedReceiver<Credentials>) config: ConnectConfig,
{ device_id: String,
) -> (Discovery, mpsc::UnboundedReceiver<Credentials>) {
let (tx, rx) = mpsc::unbounded(); let (tx, rx) = mpsc::unbounded();
let key_data = util::rand_vec(&mut rand::thread_rng(), 95); let key_data = util::rand_vec(&mut rand::thread_rng(), 95);
@ -53,9 +60,10 @@ impl Discovery {
} }
impl Discovery { impl Discovery {
fn handle_get_info(&self, _params: &BTreeMap<String, String>) fn handle_get_info(
-> ::futures::Finished<Response, hyper::Error> &self,
{ _params: &BTreeMap<String, String>,
) -> ::futures::Finished<Response, hyper::Error> {
let public_key = self.0.public_key.to_bytes_be(); let public_key = self.0.public_key.to_bytes_be();
let public_key = base64::encode(&public_key); let public_key = base64::encode(&public_key);
@ -79,9 +87,10 @@ impl Discovery {
::futures::finished(Response::new().with_body(body)) ::futures::finished(Response::new().with_body(body))
} }
fn handle_add_user(&self, params: &BTreeMap<String, String>) fn handle_add_user(
-> ::futures::Finished<Response, hyper::Error> &self,
{ params: &BTreeMap<String, String>,
) -> ::futures::Finished<Response, hyper::Error> {
let username = params.get("userName").unwrap(); let username = params.get("userName").unwrap();
let encrypted_blob = params.get("blob").unwrap(); let encrypted_blob = params.get("blob").unwrap();
let client_key = params.get("clientKey").unwrap(); let client_key = params.get("clientKey").unwrap();
@ -127,8 +136,8 @@ impl Discovery {
let decrypted = { let decrypted = {
let mut data = vec![0u8; encrypted.len()]; let mut data = vec![0u8; encrypted.len()];
let mut cipher = crypto::aes::ctr(crypto::aes::KeySize::KeySize128, let mut cipher =
&encryption_key[0..16], iv); crypto::aes::ctr(crypto::aes::KeySize::KeySize128, &encryption_key[0..16], iv);
cipher.process(encrypted, &mut data); cipher.process(encrypted, &mut data);
String::from_utf8(data).unwrap() String::from_utf8(data).unwrap()
}; };
@ -147,9 +156,7 @@ impl Discovery {
::futures::finished(Response::new().with_body(body)) ::futures::finished(Response::new().with_body(body))
} }
fn not_found(&self) fn not_found(&self) -> ::futures::Finished<Response, hyper::Error> {
-> ::futures::Finished<Response, hyper::Error>
{
::futures::finished(Response::new().with_status(StatusCode::NotFound)) ::futures::finished(Response::new().with_status(StatusCode::NotFound))
} }
} }
@ -173,40 +180,61 @@ impl Service for Discovery {
} }
let this = self.clone(); let this = self.clone();
Box::new(body.fold(Vec::new(), |mut acc, chunk| { Box::new(
acc.extend_from_slice(chunk.as_ref()); body.fold(Vec::new(), |mut acc, chunk| {
Ok::<_, hyper::Error>(acc) acc.extend_from_slice(chunk.as_ref());
}).map(move |body| { Ok::<_, hyper::Error>(acc)
params.extend(url::form_urlencoded::parse(&body).into_owned()); }).map(move |body| {
params params.extend(url::form_urlencoded::parse(&body).into_owned());
}).and_then(move |params| { params
match (method, params.get("action").map(AsRef::as_ref)) { })
(Get, Some("getInfo")) => this.handle_get_info(&params), .and_then(
(Post, Some("addUser")) => this.handle_add_user(&params), move |params| match (method, params.get("action").map(AsRef::as_ref)) {
_ => this.not_found(), (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 { pub struct DiscoveryStream {
credentials: mpsc::UnboundedReceiver<Credentials>, credentials: mpsc::UnboundedReceiver<Credentials>,
_svc: mdns::Service, _svc: mdns::Service,
} }
pub fn discovery(handle: &Handle, config: ConnectConfig, device_id: String) pub fn discovery(
-> io::Result<DiscoveryStream> handle: &Handle,
{ config: ConnectConfig,
device_id: String,
port: u16,
) -> io::Result<DiscoveryStream> {
let (discovery, creds_rx) = Discovery::new(config.clone(), device_id); let (discovery, creds_rx) = Discovery::new(config.clone(), device_id);
let serve = { let serve = {
let http = Http::new(); 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 server_future = {
let handle = handle.clone(); let handle = handle.clone();
serve.for_each(move |connection| { serve
.for_each(move |connection| {
handle.spawn(connection.then(|_| Ok(()))); handle.spawn(connection.then(|_| Ok(())));
Ok(()) Ok(())
}) })
@ -214,12 +242,26 @@ pub fn discovery(handle: &Handle, config: ConnectConfig, device_id: String)
}; };
handle.spawn(server_future); 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)?; 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,
addr.port(), s_port,
&["VERSION=1.0", "CPath=/"]); &["VERSION=1.0", "CPath=/"],
);
Ok(DiscoveryStream { Ok(DiscoveryStream {
credentials: creds_rx, 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::future;
use futures::sync::{oneshot, mpsc}; use futures::sync::{mpsc, oneshot};
use futures::{Future, Stream, Sink, Async, Poll};
use protobuf::{self, Message}; use protobuf::{self, Message};
use core::config::ConnectConfig; use core::config::ConnectConfig;
use core::mercury::MercuryError; use core::mercury::MercuryError;
use core::session::Session; use core::session::Session;
use core::util::{now_ms, SpotifyId, SeqGenerator}; use core::spotify_id::SpotifyId;
use core::util::SeqGenerator;
use core::version; use core::version;
use protocol; use protocol;
use protocol::spirc::{PlayStatus, State, MessageType, Frame, DeviceState}; use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State};
use mixer::Mixer; use playback::mixer::Mixer;
use player::Player; use playback::player::Player;
use rand; use rand;
use rand::Rng; use rand::Rng;
use std;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct SpircTask { pub struct SpircTask {
player: Player, player: Player,
@ -45,13 +48,21 @@ pub enum SpircCommand {
Next, Next,
VolumeUp, VolumeUp,
VolumeDown, VolumeDown,
Shutdown Shutdown,
} }
pub struct Spirc { pub struct Spirc {
commands: mpsc::UnboundedSender<SpircCommand>, 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 { fn initial_state() -> State {
protobuf_init!(protocol::spirc::State::new(), { protobuf_init!(protocol::spirc::State::new(), {
repeat: false, 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 { impl Spirc {
pub fn new(config: ConnectConfig, session: Session, player: Player, mixer: Box<Mixer>) pub fn new(
-> (Spirc, SpircTask) config: ConnectConfig,
{ session: Session,
player: Player,
mixer: Box<Mixer>,
) -> (Spirc, SpircTask) {
debug!("new Spirc[{}]", session.session_id()); debug!("new Spirc[{}]", session.session_id());
let ident = session.device_id().to_owned(); let ident = session.device_id().to_owned();
@ -131,21 +167,26 @@ impl Spirc {
let uri = format!("hm://remote/3/user/{}/", session.username()); let uri = format!("hm://remote/3/user/{}/", session.username());
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 = Box::new(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()
})); }));
let sender = Box::new(session.mercury().sender(uri).with(|frame: Frame| { let sender = Box::new(
Ok(frame.write_to_bytes().unwrap()) session
})); .mercury()
.sender(uri)
.with(|frame: Frame| Ok(frame.write_to_bytes().unwrap())),
);
let (cmd_tx, cmd_rx) = mpsc::unbounded(); let (cmd_tx, cmd_rx) = mpsc::unbounded();
let volume = config.volume as u16; let volume = config.volume as u16;
let device = initial_device_state(config, volume); 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 { let mut task = SpircTask {
player: player, player: player,
@ -167,9 +208,7 @@ impl Spirc {
session: session.clone(), session: session.clone(),
}; };
let spirc = Spirc { let spirc = Spirc { commands: cmd_tx };
commands: cmd_tx,
};
task.hello(); task.hello();
@ -235,9 +274,7 @@ impl Future for SpircTask {
self.handle_end_of_track(); self.handle_end_of_track();
} }
Ok(Async::NotReady) => (), Ok(Async::NotReady) => (),
Err(oneshot::Canceled) => { Err(oneshot::Canceled) => self.end_of_track = Box::new(future::empty()),
self.end_of_track = Box::new(future::empty())
}
} }
} }
@ -324,15 +361,18 @@ impl SpircTask {
} }
fn handle_frame(&mut self, frame: Frame) { fn handle_frame(&mut self, frame: Frame) {
debug!("{:?} {:?} {} {} {}", debug!(
frame.get_typ(), "{:?} {:?} {} {} {}",
frame.get_device_state().get_name(), frame.get_typ(),
frame.get_ident(), frame.get_device_state().get_name(),
frame.get_seq_nr(), frame.get_ident(),
frame.get_state_update_id()); frame.get_seq_nr(),
frame.get_state_update_id()
);
if frame.get_ident() == self.ident || if frame.get_ident() == self.ident
(frame.get_recipient().len() > 0 && !frame.get_recipient().contains(&self.ident)) { || (frame.get_recipient().len() > 0 && !frame.get_recipient().contains(&self.ident))
{
return; return;
} }
@ -350,7 +390,8 @@ impl SpircTask {
self.update_tracks(&frame); self.update_tracks(&frame);
if self.state.get_track().len() > 0 { 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); self.state.set_position_measured_at(now_ms() as u64);
let play = frame.get_state().get_status() == PlayStatus::kPlayStatusPlay; let play = frame.get_state().get_status() == PlayStatus::kPlayStatusPlay;
@ -404,8 +445,7 @@ impl SpircTask {
MessageType::kMessageTypeShuffle => { MessageType::kMessageTypeShuffle => {
self.state.set_shuffle(frame.get_state().get_shuffle()); 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 current_index = self.state.get_playing_track_index();
{ {
let tracks = self.state.mut_track(); let tracks = self.state.mut_track();
@ -437,16 +477,14 @@ 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
self.mixer.set_volume(frame.get_volume() as u16); .set_volume(volume_to_mixer(frame.get_volume() as u16));
self.notify(None); self.notify(None);
} }
MessageType::kMessageTypeNotify => { MessageType::kMessageTypeNotify => {
if self.device.get_is_active() && if self.device.get_is_active() && frame.get_device_state().get_is_active() {
frame.get_device_state().get_is_active()
{
self.device.set_is_active(false); self.device.set_is_active(false);
self.state.set_status(PlayStatus::kPlayStatusStop); self.state.set_status(PlayStatus::kPlayStatusStop);
self.player.stop(); self.player.stop();
@ -491,35 +529,61 @@ impl SpircTask {
} }
} }
fn handle_next(&mut self) { fn consume_queued_track(&mut self) -> usize {
let current_index = self.state.get_playing_track_index(); // Removes current track if it is queued
let num_tracks = self.state.get_track().len() as u32; // Returns the index of the next track
let new_index = (current_index + 1) % num_tracks; let current_index = self.state.get_playing_track_index() as usize;
if self.state.get_track()[current_index].get_queued() {
let mut was_last_track = (current_index + 1) >= num_tracks; self.state.mut_track().remove(current_index);
if self.state.get_repeat() { return current_index;
was_last_track = false;
} }
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_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(!was_last_track); self.load_track(continue_playing);
} }
fn handle_prev(&mut self) { fn handle_prev(&mut self) {
// Previous behaves differently based on the position // Previous behaves differently based on the position
// Under 3s it goes to the previous song // Under 3s it goes to the previous song (starts playing)
// Over 3s it seeks to zero // Over 3s it seeks to zero (retains previous play status)
if self.position() < 3000 { 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 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 self.state.get_track().len() as u32 - 1
} else { } 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_playing_track_index(new_index);
self.state.set_position_ms(0); self.state.set_position_ms(0);
@ -534,25 +598,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) {
self.handle_next(); self.handle_next();
self.notify(None); self.notify(None);
} }

View file

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

View file

@ -1,45 +1,36 @@
extern crate vergen;
extern crate protobuf_macros;
extern crate rand; extern crate rand;
extern crate vergen;
use rand::Rng; use rand::Rng;
use std::env; use std::env;
use std::path::PathBuf;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Write; use std::io::Write;
use std::path::PathBuf;
fn main() { fn main() {
let out = PathBuf::from(env::var("OUT_DIR").unwrap()); let out = PathBuf::from(env::var("OUT_DIR").unwrap());
vergen::vergen(vergen::OutputFns::all()).unwrap(); vergen::vergen(vergen::OutputFns::all()).unwrap();
let build_id: String = rand::thread_rng() let build_id: String = rand::thread_rng().gen_ascii_chars().take(8).collect();
.gen_ascii_chars()
.take(8)
.collect();
let mut version_file = let mut version_file = OpenOptions::new()
OpenOptions::new()
.write(true) .write(true)
.append(true) .append(true)
.open(&out.join("version.rs")) .open(&out.join("version.rs"))
.unwrap(); .unwrap();
let build_id_fn = format!(" let build_id_fn = format!(
"
/// Generate a random build id. /// Generate a random build id.
pub fn build_id() -> &'static str {{ pub fn build_id() -> &'static str {{
\"{}\" \"{}\"
}} }}
", build_id); ",
build_id
);
if let Err(e) = version_file.write_all(build_id_fn.as_bytes()) { if let Err(e) = version_file.write_all(build_id_fn.as_bytes()) {
println!("{}", e); 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 AP_FALLBACK: &'static str = "ap.spotify.com:80";
const APRESOLVE_ENDPOINT : &'static str = "http://apresolve.spotify.com/"; const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com/";
use std::str::FromStr;
use futures::{Future, Stream}; use futures::{Future, Stream};
use hyper::{self, Uri, Client}; use hyper::{self, Client, Uri};
use serde_json; use serde_json;
use std::str::FromStr;
use tokio_core::reactor::Handle; use tokio_core::reactor::Handle;
error_chain! { } error_chain!{}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct APResolveData { 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 url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL");
let client = Client::new(handle); 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.then(|result| result.chain_err(|| "HTTP error"));
let body = body.and_then(|body| { let body = body.and_then(|body| String::from_utf8(body).chain_err(|| "invalid UTF8 in response"));
String::from_utf8(body).chain_err(|| "invalid UTF8 in response")
});
let data = body.and_then(|body| { let data =
serde_json::from_str::<APResolveData>(&body) body.and_then(|body| serde_json::from_str::<APResolveData>(&body).chain_err(|| "invalid JSON"));
.chain_err(|| "invalid JSON")
});
let ap = data.and_then(|data| { let ap = data.and_then(|data| {
let ap = data.ap_list.first().ok_or("empty AP List")?; 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) Box::new(ap)
} }
pub fn apresolve_or_fallback<E>(handle: &Handle) pub(crate) fn apresolve_or_fallback<E>(handle: &Handle) -> Box<Future<Item = String, Error = E>>
-> Box<Future<Item=String, Error=E>> where
where E: 'static E: 'static,
{ {
let ap = apresolve(handle).or_else(|e| { let ap = apresolve(handle).or_else(|e| {
warn!("Failed to resolve Access Point: {}", e.description()); warn!("Failed to resolve Access Point: {}", e.description());

View file

@ -1,17 +1,17 @@
use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use bytes::Bytes; use bytes::Bytes;
use futures::sync::oneshot;
use futures::{Async, Future, Poll}; use futures::{Async, Future, Poll};
use futures::sync::oneshot;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Write; use std::io::Write;
use spotify_id::{FileId, SpotifyId};
use util::SeqGenerator; use util::SeqGenerator;
use util::{SpotifyId, FileId};
#[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)] #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct AudioKey(pub [u8; 16]); pub struct AudioKey(pub [u8; 16]);
#[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)] #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct AudioKeyError; pub struct AudioKeyError;
component! { component! {
@ -22,7 +22,7 @@ component! {
} }
impl AudioKeyManager { 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 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));
@ -35,7 +35,11 @@ impl AudioKeyManager {
let _ = sender.send(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]
);
let _ = sender.send(Err(AudioKeyError)); let _ = sender.send(Err(AudioKeyError));
} }
_ => (), _ => (),
@ -68,7 +72,7 @@ impl AudioKeyManager {
} }
pub struct AudioKeyFuture<T>(oneshot::Receiver<Result<T, AudioKeyError>>); pub struct AudioKeyFuture<T>(oneshot::Receiver<Result<T, AudioKeyError>>);
impl <T> Future for AudioKeyFuture<T> { impl<T> Future for AudioKeyFuture<T> {
type Item = T; type Item = T;
type Error = AudioKeyError; type Error = AudioKeyError;
@ -81,4 +85,3 @@ impl <T> Future for AudioKeyFuture<T> {
} }
} }
} }

View file

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

View file

@ -1,9 +1,12 @@
use std::path::PathBuf; use std::fs;
use std::io::Read;
use std::fs::File; 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 authentication::Credentials;
use spotify_id::FileId;
#[derive(Clone)] #[derive(Clone)]
pub struct Cache { pub struct Cache {
@ -11,6 +14,16 @@ pub struct Cache {
use_audio_cache: bool, 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 { impl Cache {
pub fn new(location: PathBuf, use_audio_cache: bool) -> Cache { pub fn new(location: PathBuf, use_audio_cache: bool) -> Cache {
mkdir_existing(&location).unwrap(); mkdir_existing(&location).unwrap();
@ -18,7 +31,7 @@ impl Cache {
Cache { Cache {
root: location, 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 byteorder::{BigEndian, ByteOrder};
use bytes::Bytes; use bytes::Bytes;
use futures::sync::{BiLock, mpsc}; use futures::{Async, Poll, Stream};
use futures::{Poll, Async, Stream}; use futures::sync::{mpsc, BiLock};
use std::collections::HashMap; use std::collections::HashMap;
use util::SeqGenerator; use util::SeqGenerator;
@ -13,7 +13,7 @@ component! {
} }
} }
#[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)] #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct ChannelError; pub struct ChannelError;
pub struct Channel { pub struct Channel {
@ -54,7 +54,7 @@ impl ChannelManager {
(seq, channel) (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; use std::collections::hash_map::Entry;
let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref()); 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>)>); pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::std::sync::Mutex<$inner>)>);
impl $name { impl $name {
#[allow(dead_code)] #[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)); debug!(target:"librespot::component", "new {}", stringify!($name));
$name(::std::sync::Arc::new((session, ::std::sync::Mutex::new($inner { $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::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: Sync> Sync for Lazy<T> {}
unsafe impl <T: Send> Send for Lazy<T> {} unsafe impl<T: Send> Send for Lazy<T> {}
#[cfg_attr(feature = "cargo-clippy", allow(mutex_atomic))] #[cfg_attr(feature = "cargo-clippy", allow(mutex_atomic))]
impl <T> Lazy<T> { impl<T> Lazy<T> {
pub fn new() -> Lazy<T> { pub(crate) fn new() -> Lazy<T> {
Lazy(Mutex::new(false), UnsafeCell::new(None)) 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(); let mut inner = self.0.lock().unwrap();
if !*inner { if !*inner {
unsafe { unsafe {

View file

@ -1,10 +1,10 @@
use uuid::Uuid;
use std::str::FromStr;
use std::fmt; use std::fmt;
use std::str::FromStr;
use uuid::Uuid;
use version; use version;
#[derive(Clone,Debug)] #[derive(Clone, Debug)]
pub struct SessionConfig { pub struct SessionConfig {
pub user_agent: String, pub user_agent: String,
pub device_id: String, pub device_id: String,
@ -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)] #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub enum DeviceType { pub enum DeviceType {
Unknown = 0, Unknown = 0,
@ -100,24 +74,7 @@ impl Default for DeviceType {
} }
} }
#[derive(Clone,Debug)] #[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 struct ConnectConfig {
pub name: String, pub name: String,
pub device_type: DeviceType, pub device_type: DeviceType,

View file

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

View file

@ -1,20 +1,21 @@
use crypto::sha1::Sha1; use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use crypto::hmac::Hmac; 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 protobuf::{self, Message, MessageStatic};
use rand::thread_rng; use rand::thread_rng;
use std::io::{self, Read}; use std::io::{self, Read};
use std::marker::PhantomData; use std::marker::PhantomData;
use tokio_io::{AsyncRead, AsyncWrite}; use tokio_io::{AsyncRead, AsyncWrite};
use tokio_io::codec::Framed; use tokio_io::codec::Framed;
use tokio_io::io::{write_all, WriteAll, read_exact, ReadExact, Window}; use tokio_io::io::{read_exact, write_all, ReadExact, Window, WriteAll};
use futures::{Poll, Async, Future};
use super::codec::APCodec;
use diffie_hellman::DHLocalKeys; use diffie_hellman::DHLocalKeys;
use protocol; use protocol;
use protocol::keyexchange::{ClientHello, APResponseMessage, ClientResponsePlaintext}; use protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext};
use util; use util;
use super::codec::APCodec;
pub struct Handshake<T> { pub struct Handshake<T> {
keys: DHLocalKeys, keys: DHLocalKeys,
@ -37,7 +38,7 @@ pub fn handshake<T: AsyncRead + AsyncWrite>(connection: T) -> Handshake<T> {
} }
} }
impl <T: AsyncRead + AsyncWrite> 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;
@ -47,22 +48,22 @@ impl <T: AsyncRead + AsyncWrite> Future for Handshake<T> {
self.state = match self.state { self.state = match self.state {
ClientHello(ref mut write) => { ClientHello(ref mut write) => {
let (connection, accumulator) = try_ready!(write.poll()); let (connection, accumulator) = try_ready!(write.poll());
let read = recv_packet(connection, accumulator); let read = recv_packet(connection, accumulator);
APResponse(read) APResponse(read)
} }
APResponse(ref mut read) => { APResponse(ref mut read) => {
let (connection, message, accumulator) = try_ready!(read.poll()); 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_login_crypto_challenge()
.get_diffie_hellman() .get_diffie_hellman()
.get_gs() .get_gs()
.to_owned(); .to_owned();
let shared_secret = self.keys.shared_secret(&remote_key); let shared_secret = self.keys.shared_secret(&remote_key);
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator);
&accumulator);
let codec = APCodec::new(&send_key, &recv_key); let codec = APCodec::new(&send_key, &recv_key);
let write = client_response(connection, challenge); 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>> { fn client_hello<T: AsyncWrite>(connection: T, gc: Vec<u8>) -> WriteAll<T, Vec<u8>> {
let packet = protobuf_init!(ClientHello::new(), { let mut packet = ClientHello::new();
build_info => { packet
product: protocol::keyexchange::Product::PRODUCT_PARTNER, .mut_build_info()
platform: protocol::keyexchange::Platform::PLATFORM_LINUX_X86, .set_product(protocol::keyexchange::Product::PRODUCT_PARTNER);
version: 0x10800000000, packet
}, .mut_build_info()
cryptosuites_supported => [ .set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86);
protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON, packet.mut_build_info().set_version(0x10800000000);
], packet
login_crypto_hello.diffie_hellman => { .mut_cryptosuites_supported()
gc: gc, .push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON);
server_keys_known: 1, packet
}, .mut_login_crypto_hello()
client_nonce: util::rand_vec(&mut thread_rng(), 0x10), .mut_diffie_hellman()
padding: vec![0x1e], .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 mut buffer = vec![0, 4];
let size = 2 + 4 + packet.compute_size(); 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>> { fn client_response<T: AsyncWrite>(connection: T, challenge: Vec<u8>) -> WriteAll<T, Vec<u8>> {
let packet = protobuf_init!(ClientResponsePlaintext::new(), { let mut packet = ClientResponsePlaintext::new();
login_crypto_response.diffie_hellman => { packet
hmac: challenge .mut_login_crypto_response()
}, .mut_diffie_hellman()
pow_response => {}, .set_hmac(challenge);
crypto_response => {}, packet.mut_pow_response();
}); packet.mut_crypto_response();
let mut buffer = vec![]; let mut buffer = vec![];
let size = 4 + packet.compute_size(); 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> fn recv_packet<T: AsyncRead, M>(connection: T, acc: Vec<u8>) -> RecvPacket<T, M>
where T: Read, where
M: MessageStatic T: Read,
M: MessageStatic,
{ {
RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData) RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData)
} }
impl <T: AsyncRead, M> Future for RecvPacket<T, M> impl<T: AsyncRead, M> Future for RecvPacket<T, M>
where T: Read, where
M: MessageStatic T: Read,
M: MessageStatic,
{ {
type Item = (T, M, Vec<u8>); type Item = (T, M, Vec<u8>);
type Error = io::Error; 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(); let offset = acc.len();
acc.resize(offset + size, 0); 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 = Hmac::new(Sha1::new(), &data[..0x14]);
mac.input(packets); 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,71 +5,88 @@ pub use self::codec::APCodec;
pub use self::handshake::handshake; pub use self::handshake::handshake;
use futures::{Future, Sink, Stream}; use futures::{Future, Sink, Stream};
use protobuf::{self, Message};
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_io::codec::Framed; use tokio_io::codec::Framed;
use protobuf::{self, Message};
use authentication::Credentials; use authentication::Credentials;
use version; use version;
pub type Transport = Framed<TcpStream, APCodec>; 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 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)
});
Box::new(connection) Box::new(connection)
} }
pub fn authenticate(transport: Transport, credentials: Credentials, device_id: String) pub fn authenticate(
-> Box<Future<Item = (Transport, Credentials), Error = io::Error>> transport: Transport,
{ credentials: Credentials,
device_id: String,
) -> Box<Future<Item = (Transport, Credentials), Error = io::Error>> {
use protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; use protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
use protocol::keyexchange::APLoginFailed;
let packet = protobuf_init!(ClientResponseEncrypted::new(), { let mut packet = ClientResponseEncrypted::new();
login_credentials => { packet
username: credentials.username, .mut_login_credentials()
typ: credentials.auth_type, .set_username(credentials.username);
auth_data: credentials.auth_data, packet
}, .mut_login_credentials()
system_info => { .set_typ(credentials.auth_type);
cpu_family: CpuFamily::CPU_UNKNOWN, packet
os: Os::OS_UNKNOWN, .mut_login_credentials()
system_information_string: format!("librespot_{}_{}", version::short_sha(), version::build_id()), .set_auth_data(credentials.auth_data);
device_id: device_id, packet
}, .mut_system_info()
version_string: version::version_string(), .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 cmd = 0xab;
let data = packet.write_to_bytes().unwrap(); let data = packet.write_to_bytes().unwrap();
Box::new(transport.send((cmd, data)).and_then(|transport| { Box::new(
transport.into_future().map_err(|(err, _stream)| err) transport
}).and_then(|(packet, transport)| { .send((cmd, data))
match packet { .and_then(|transport| transport.into_future().map_err(|(err, _stream)| err))
Some((0xac, data)) => { .and_then(|(packet, transport)| match packet {
let welcome_data: APWelcome = Some((0xac, data)) => {
protobuf::parse_from_bytes(data.as_ref()).unwrap(); let welcome_data: APWelcome = protobuf::parse_from_bytes(data.as_ref()).unwrap();
let reusable_credentials = Credentials { let reusable_credentials = Credentials {
username: welcome_data.get_canonical_username().to_owned(), username: welcome_data.get_canonical_username().to_owned(),
auth_type: welcome_data.get_reusable_auth_credentials_type(), auth_type: welcome_data.get_reusable_auth_credentials_type(),
auth_data: welcome_data.get_reusable_auth_credentials().to_owned(), auth_data: welcome_data.get_reusable_auth_credentials().to_owned(),
}; };
Ok((transport, reusable_credentials)) Ok((transport, reusable_credentials))
} }
Some((0xad, _)) => panic!("Authentication failed"), Some((0xad, data)) => {
Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd), let error_data: APLoginFailed = protobuf::parse_from_bytes(data.as_ref()).unwrap();
None => panic!("EOF"), 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> { 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(
&self.private_key, &BigUint::from_bytes_be(remote_key),
&DH_PRIME); &self.private_key,
&DH_PRIME,
);
shared_key.to_bytes_be() shared_key.to_bytes_be()
} }
} }

View file

@ -1,8 +1,8 @@
use futures::Future; use futures::Future;
use serde_json; use serde_json;
use core::mercury::MercuryError; use mercury::MercuryError;
use core::session::Session; use session::Session;
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -13,13 +13,19 @@ pub struct Token {
pub scope: Vec<String>, pub scope: Vec<String>,
} }
pub fn get_token(session: &Session, client_id: &str, scopes: &str) -> Box<Future<Item = Token, Error = MercuryError>> { pub fn get_token(
let url = format!("hm://keymaster/token/authenticated?client_id={}&scope={}", session: &Session,
client_id, scopes); 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| { 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
})) }))

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

View file

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

View file

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

View file

@ -27,18 +27,17 @@ pub struct MercuryResponse {
pub payload: Vec<Vec<u8>>, pub payload: Vec<Vec<u8>>,
} }
#[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)] #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct MercuryError; pub struct MercuryError;
impl ToString for MercuryMethod { impl ToString for MercuryMethod {
fn to_string(&self) -> String { fn to_string(&self) -> String {
match *self { match *self {
MercuryMethod::GET => "GET", MercuryMethod::GET => "GET",
MercuryMethod::SUB => "SUB", MercuryMethod::SUB => "SUB",
MercuryMethod::UNSUB => "UNSUB", MercuryMethod::UNSUB => "UNSUB",
MercuryMethod::SEND => "SEND", MercuryMethod::SEND => "SEND",
} }.to_owned()
.to_owned()
} }
} }
@ -58,7 +57,9 @@ impl MercuryRequest {
packet.write_u16::<BigEndian>(seq.len() as u16).unwrap(); packet.write_u16::<BigEndian>(seq.len() as u16).unwrap();
packet.write_all(seq).unwrap(); packet.write_all(seq).unwrap();
packet.write_u8(1).unwrap(); // Flags: FINAL 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(); let mut header = protocol::mercury::Header::new();
header.set_uri(self.uri.clone()); header.set_uri(self.uri.clone());
@ -68,7 +69,9 @@ impl MercuryRequest {
header.set_content_type(content_type.clone()); 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(); header.write_to_writer(&mut packet).unwrap();
for p in &self.payload { for p in &self.payload {

View file

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

View file

@ -1,16 +1,15 @@
use byteorder::{BigEndian, ByteOrder};
use std; use std;
use std::fmt; use std::fmt;
use util::u128; use util::u128;
use byteorder::{BigEndian, ByteOrder};
// Unneeded since 1.21 // Unneeded since 1.21
#[allow(unused_imports)] #[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)]
pub struct SpotifyId(u128); pub struct SpotifyId(u128);
const BASE62_DIGITS: &'static [u8] = const BASE62_DIGITS: &'static [u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef"; const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef";
impl SpotifyId { impl SpotifyId {
@ -79,7 +78,7 @@ impl SpotifyId {
} }
} }
#[derive(Copy,Clone,PartialEq,Eq,PartialOrd,Ord,Hash)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileId(pub [u8; 20]); pub struct FileId(pub [u8; 20]);
impl FileId { impl FileId {

View file

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

View file

@ -1,55 +1,18 @@
use num_bigint::BigUint; use num_bigint::BigUint;
use num_traits::{Zero, One};
use num_integer::Integer; use num_integer::Integer;
use rand::{Rng, Rand}; use num_traits::{One, Zero};
use std::io; use rand::{Rand, Rng};
use std::mem; use std::mem;
use std::ops::{Mul, Rem, Shr}; 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 int128;
mod spotify_id;
mod subfile;
pub use util::int128::u128; 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> { pub fn rand_vec<G: Rng, R: Rand>(rng: &mut G, size: usize) -> Vec<R> {
rng.gen_iter().take(size).collect() 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 { pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
let mut base = base.clone(); let mut base = base.clone();
let mut exp = exp.clone(); let mut exp = exp.clone();
@ -66,34 +29,8 @@ pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
result result
} }
pub struct StrChunks<'s>(&'s str, usize); pub trait ReadSeek: ::std::io::Read + ::std::io::Seek {}
impl<T: ::std::io::Read + ::std::io::Seek> ReadSeek for T {}
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 { }
pub trait Seq { pub trait Seq {
fn next(&self) -> Self; fn next(&self) -> Self;
@ -112,7 +49,7 @@ impl_seq!(u8 u16 u32 u64 usize);
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct SeqGenerator<T: Seq>(T); pub struct SeqGenerator<T: Seq>(T);
impl <T: Seq> SeqGenerator<T> { impl<T: Seq> SeqGenerator<T> {
pub fn new(value: T) -> Self { pub fn new(value: T) -> Self {
SeqGenerator(value) SeqGenerator(value)
} }

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 tokio_core::reactor::Core;
use librespot::core::authentication::Credentials; 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::session::Session;
use librespot::core::util::SpotifyId; use librespot::core::spotify_id::SpotifyId;
use librespot::audio_backend; use librespot::audio_backend;
use librespot::player::Player; use librespot::player::Player;

View file

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

View file

@ -13,7 +13,7 @@ use linear_map::LinearMap;
use core::mercury::MercuryError; use core::mercury::MercuryError;
use core::session::Session; use core::session::Session;
use core::util::{SpotifyId, FileId, StrChunksExt}; use core::spotify_id::{FileId, SpotifyId};
pub use protocol::metadata::AudioFile_Format as FileFormat; 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 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 forbidden = "".to_string();
let mut has_forbidden = false; 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 allowed = "".to_string();
let mut has_allowed = false; let mut has_allowed = false;
let rs = restrictions.into_iter().filter(|r| let rs = restrictions
r.get_catalogue_str().contains(&catalogue.to_owned()) .into_iter()
); .filter(|r| r.get_catalogue_str().contains(&catalogue.to_owned()));
for r in rs { for r in rs {
if r.has_countries_forbidden() { if r.has_countries_forbidden() {
@ -46,12 +47,12 @@ fn parse_restrictions<'s, I>(restrictions: I, country: &str, catalogue: &str) ->
} }
} }
(has_forbidden || has_allowed) && (has_forbidden || has_allowed)
(!has_forbidden || !countrylist_contains(forbidden.as_str(), country)) && && (!has_forbidden || !countrylist_contains(forbidden.as_str(), country))
(!has_allowed || countrylist_contains(allowed.as_str(), country)) && (!has_allowed || countrylist_contains(allowed.as_str(), country))
} }
pub trait Metadata : Send + Sized + 'static { pub trait Metadata: Send + Sized + 'static {
type Message: protobuf::MessageStatic; type Message: protobuf::MessageStatic;
fn base_url() -> &'static str; fn base_url() -> &'static str;
@ -75,6 +76,7 @@ pub trait Metadata : Send + Sized + 'static {
pub struct Track { pub struct Track {
pub id: SpotifyId, pub id: SpotifyId,
pub name: String, pub name: String,
pub duration: i32,
pub album: SpotifyId, pub album: SpotifyId,
pub artists: Vec<SpotifyId>, pub artists: Vec<SpotifyId>,
pub files: LinearMap<FileFormat, FileId>, pub files: LinearMap<FileFormat, FileId>,
@ -109,34 +111,33 @@ impl Metadata for Track {
let country = session.country(); let country = session.country();
let artists = msg.get_artist() let artists = msg.get_artist()
.iter() .iter()
.filter(|artist| artist.has_gid()) .filter(|artist| artist.has_gid())
.map(|artist| SpotifyId::from_raw(artist.get_gid())) .map(|artist| SpotifyId::from_raw(artist.get_gid()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let files = msg.get_file() let files = msg.get_file()
.iter() .iter()
.filter(|file| file.has_file_id()) .filter(|file| file.has_file_id())
.map(|file| { .map(|file| {
let mut dst = [0u8; 20]; let mut dst = [0u8; 20];
dst.clone_from_slice(file.get_file_id()); dst.clone_from_slice(file.get_file_id());
(file.get_format(), FileId(dst)) (file.get_format(), FileId(dst))
}) })
.collect(); .collect();
Track { Track {
id: SpotifyId::from_raw(msg.get_gid()), id: SpotifyId::from_raw(msg.get_gid()),
name: msg.get_name().to_owned(), name: msg.get_name().to_owned(),
duration: msg.get_duration(),
album: SpotifyId::from_raw(msg.get_album().get_gid()), album: SpotifyId::from_raw(msg.get_album().get_gid()),
artists: artists, artists: artists,
files: files, files: files,
alternatives: msg.get_alternative() alternatives: msg.get_alternative()
.iter() .iter()
.map(|alt| SpotifyId::from_raw(alt.get_gid())) .map(|alt| SpotifyId::from_raw(alt.get_gid()))
.collect(), .collect(),
available: parse_restrictions(msg.get_restriction(), available: parse_restrictions(msg.get_restriction(), &country, "premium"),
&country,
"premium"),
} }
} }
} }
@ -150,28 +151,28 @@ impl Metadata for Album {
fn parse(msg: &Self::Message, _: &Session) -> Self { fn parse(msg: &Self::Message, _: &Session) -> Self {
let artists = msg.get_artist() let artists = msg.get_artist()
.iter() .iter()
.filter(|artist| artist.has_gid()) .filter(|artist| artist.has_gid())
.map(|artist| SpotifyId::from_raw(artist.get_gid())) .map(|artist| SpotifyId::from_raw(artist.get_gid()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let tracks = msg.get_disc() let tracks = msg.get_disc()
.iter() .iter()
.flat_map(|disc| disc.get_track()) .flat_map(|disc| disc.get_track())
.filter(|track| track.has_gid()) .filter(|track| track.has_gid())
.map(|track| SpotifyId::from_raw(track.get_gid())) .map(|track| SpotifyId::from_raw(track.get_gid()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let covers = msg.get_cover_group() let covers = msg.get_cover_group()
.get_image() .get_image()
.iter() .iter()
.filter(|image| image.has_file_id()) .filter(|image| image.has_file_id())
.map(|image| { .map(|image| {
let mut dst = [0u8; 20]; let mut dst = [0u8; 20];
dst.clone_from_slice(image.get_file_id()); dst.clone_from_slice(image.get_file_id());
FileId(dst) FileId(dst)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Album { Album {
id: SpotifyId::from_raw(msg.get_gid()), id: SpotifyId::from_raw(msg.get_gid()),
@ -183,7 +184,6 @@ impl Metadata for Album {
} }
} }
impl Metadata for Artist { impl Metadata for Artist {
type Message = protocol::metadata::Artist; type Message = protocol::metadata::Artist;
@ -195,24 +195,48 @@ impl Metadata for Artist {
let country = session.country(); let country = session.country();
let top_tracks: Vec<SpotifyId> = match msg.get_top_track() let top_tracks: Vec<SpotifyId> = match msg.get_top_track()
.iter() .iter()
.find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country)) { .find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country))
Some(tracks) => { {
tracks.get_track() Some(tracks) => tracks
.iter() .get_track()
.filter(|track| track.has_gid()) .iter()
.map(|track| SpotifyId::from_raw(track.get_gid())) .filter(|track| track.has_gid())
.collect::<Vec<_>>() .map(|track| SpotifyId::from_raw(track.get_gid()))
}, .collect::<Vec<_>>(),
None => Vec::new() None => Vec::new(),
}; };
Artist { Artist {
id: SpotifyId::from_raw(msg.get_gid()), id: SpotifyId::from_raw(msg.get_gid()),
name: msg.get_name().to_owned(), 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 { impl Sink for AlsaSink {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> io::Result<()> {
if self.0.is_some() { if self.0.is_none() {
} else { match PCM::open(&*self.1,
self.0 = Some(PCM::open(&*self.1,
Stream::Playback, Mode::Blocking, Stream::Playback, Mode::Blocking,
Format::Signed16, Access::Interleaved, 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(()) 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")] #[cfg(feature = "pulseaudio-backend")]
use self::pulseaudio::PulseAudioSink; use self::pulseaudio::PulseAudioSink;
#[cfg(feature = "jackaudio-backend")]
mod jackaudio;
#[cfg(feature = "jackaudio-backend")]
use self::jackaudio::JackSink;
mod pipe; mod pipe;
use self::pipe::StdoutSink; use self::pipe::StdoutSink;
@ -41,6 +46,8 @@ pub const BACKENDS : &'static [
("portaudio", mk_sink::<PortAudioSink>), ("portaudio", mk_sink::<PortAudioSink>),
#[cfg(feature = "pulseaudio-backend")] #[cfg(feature = "pulseaudio-backend")]
("pulseaudio", mk_sink::<PulseAudioSink>), ("pulseaudio", mk_sink::<PulseAudioSink>),
#[cfg(feature = "jackaudio-backend")]
("jackaudio", mk_sink::<JackSink>),
("pipe", mk_sink::<StdoutSink>), ("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::sync::oneshot;
use futures::{future, Future}; use futures::{future, Future};
use std;
use std::borrow::Cow; use std::borrow::Cow;
use std::io::{Read, Seek, SeekFrom, Result};
use std::mem; use std::mem;
use std::process::Command;
use std::sync::mpsc::{RecvError, TryRecvError, RecvTimeoutError}; use std::sync::mpsc::{RecvError, TryRecvError, RecvTimeoutError};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use std;
use core::config::{Bitrate, PlayerConfig}; use config::{Bitrate, PlayerConfig};
use core::session::Session; use core::session::Session;
use core::util::{self, SpotifyId, Subfile}; use core::spotify_id::SpotifyId;
use audio_backend::Sink; use audio_backend::Sink;
use audio::{AudioFile, AudioDecrypt}; use audio::{AudioFile, AudioDecrypt};
@ -375,13 +377,13 @@ impl PlayerInternal {
fn run_onstart(&self) { fn run_onstart(&self) {
if let Some(ref program) = self.config.onstart { if let Some(ref program) = self.config.onstart {
util::run_program(program) run_program(program)
} }
} }
fn run_onstop(&self) { fn run_onstop(&self) {
if let Some(ref program) = self.config.onstop { 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))] #![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 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;
@ -18,26 +13,8 @@ extern crate tokio_core;
extern crate url; extern crate url;
pub extern crate librespot_audio as audio; 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_core as core;
pub extern crate librespot_playback as playback;
pub extern crate librespot_protocol as protocol; pub extern crate librespot_protocol as protocol;
pub extern crate librespot_metadata as metadata; 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::authentication::{get_credentials, Credentials};
use librespot::core::cache::Cache; 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::session::Session;
use librespot::core::version; use librespot::core::version;
use librespot::audio_backend::{self, Sink, BACKENDS}; use librespot::playback::audio_backend::{self, Sink, BACKENDS};
use librespot::discovery::{discovery, DiscoveryStream}; use librespot::playback::config::{Bitrate, PlayerConfig};
use librespot::mixer::{self, Mixer}; use librespot::connect::discovery::{discovery, DiscoveryStream};
use librespot::player::Player; use librespot::playback::mixer::{self, Mixer};
use librespot::spirc::{Spirc, SpircTask}; use librespot::playback::player::Player;
use librespot::connect::spirc::{Spirc, SpircTask};
fn usage(program: &str, opts: &getopts::Options) -> String { fn usage(program: &str, opts: &getopts::Options) -> String {
let brief = format!("Usage: {} [options]", program); let brief = format!("Usage: {} [options]", program);
@ -81,6 +82,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 {
@ -97,9 +99,10 @@ fn setup(args: &[String]) -> Setup {
.optopt("p", "password", "Password", "PASSWORD") .optopt("p", "password", "Password", "PASSWORD")
.optflag("", "disable-discovery", "Disable discovery mode") .optflag("", "disable-discovery", "Disable discovery mode")
.optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND") .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("", "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..]) { let matches = match opts.parse(&args[1..]) {
Ok(m) => m, Ok(m) => m,
@ -132,33 +135,22 @@ fn setup(args: &[String]) -> Setup {
let mixer_name = matches.opt_str("mixer"); let mixer_name = matches.opt_str("mixer");
let mixer = mixer::find(mixer_name.as_ref()) let mixer = mixer::find(mixer_name.as_ref())
.expect("Invalid mixer"); .expect("Invalid mixer");
let initial_volume;
// check if initial-volume argument is present let initial_volume = matches
if matches.opt_present("initial-volume"){ .opt_str("initial-volume")
// check if value is a number .map(|volume| {
if matches.opt_str("initial-volume").unwrap().parse::<i32>().is_ok(){ let volume = volume.parse::<i32>().unwrap();
// check if value is in [0-100] range, otherwise put the bound values if volume < 0 || volume > 100 {
if matches.opt_str("initial-volume").unwrap().parse::<i32>().unwrap() < 0 { panic!("Initial volume must be in the range 0-100");
initial_volume = 0 as i32;
} }
else if matches.opt_str("initial-volume").unwrap().parse::<i32>().unwrap() > 100{ volume * 0xFFFF / 100
initial_volume = 0xFFFF as i32; })
} .unwrap_or(0x8000);
// checks ok
else{ let zeroconf_port =
initial_volume = matches.opt_str("initial-volume").unwrap().parse::<i32>().unwrap()* 0xFFFF as i32 / 100 ; matches.opt_str("zeroconf-port")
} .map(|port| port.parse::<u16>().unwrap())
} .unwrap_or(0);
// 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);
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");
@ -221,6 +213,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,
} }
} }
@ -269,7 +262,7 @@ impl Main {
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 {
@ -362,6 +355,9 @@ impl Future for Main {
} }
fn main() { fn main() {
if env::var("RUST_BACKTRACE").is_err() {
env::set_var("RUST_BACKTRACE", "full")
}
let mut core = Core::new().unwrap(); let mut core = Core::new().unwrap();
let handle = core.handle(); let handle = core.handle();