diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ad4b406..c20fe1c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,6 +78,7 @@ jobs: - run: cargo build --locked --no-default-features --features "portaudio-backend" - run: cargo build --locked --no-default-features --features "pulseaudio-backend" - run: cargo build --locked --no-default-features --features "jackaudio-backend" + - run: cargo build --locked --no-default-features --features "rodiojack-backend" - run: cargo build --locked --no-default-features --features "rodio-backend" - run: cargo build --locked --no-default-features --features "sdl-backend" - run: cargo build --locked --no-default-features --features "gstreamer-backend" diff --git a/.gitignore b/.gitignore index 1ca8ef72..1fa44327 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ target spotify_appkey.key .vagrant/ .project -.history \ No newline at end of file +.history +*.save + + diff --git a/COMPILING.md b/COMPILING.md index 7b3467ee..40eefb39 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -46,6 +46,7 @@ Depending on the chosen backend, specific development libraries are required. |PortAudio | `portaudio19-dev` | `portaudio-devel` | `portaudio` | |PulseAudio | `libpulse-dev` | `pulseaudio-libs-devel` | | |JACK | `libjack-dev` | `jack-audio-connection-kit-devel` | | +|JACK over Rodio | `libjack-dev` | `jack-audio-connection-kit-devel` | - | |SDL | `libsdl2-dev` | `SDL2-devel` | | |Pipe | - | - | - | diff --git a/Cargo.lock b/Cargo.lock index 50234e98..65493a27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,44 +27,21 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" dependencies = [ - "aes-soft 0.6.4", - "aesni 0.10.0", + "aes-soft", + "aesni", "cipher", ] -[[package]] -name = "aes-ctr" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e5b0458ea3beae0d1d8c0f3946564f8e10f90646cf78c06b4351052058d1ee" -dependencies = [ - "aes-soft 0.3.3", - "aesni 0.6.0", - "ctr 0.3.2", - "stream-cipher", -] - [[package]] name = "aes-ctr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7729c3cde54d67063be556aeac75a81330d802f0259500ca40cb52967f975763" dependencies = [ - "aes-soft 0.6.4", - "aesni 0.10.0", + "aes-soft", + "aesni", "cipher", - "ctr 0.6.0", -] - -[[package]] -name = "aes-soft" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd7e7ae3f9a1fb5c03b389fc6bb9a51400d0c13053f0dca698c832bfd893a0d" -dependencies = [ - "block-cipher-trait", - "byteorder", - "opaque-debug 0.2.3", + "ctr", ] [[package]] @@ -74,18 +51,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" dependencies = [ "cipher", - "opaque-debug 0.3.0", -] - -[[package]] -name = "aesni" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100" -dependencies = [ - "block-cipher-trait", - "opaque-debug 0.2.3", - "stream-cipher", + "opaque-debug", ] [[package]] @@ -95,7 +61,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" dependencies = [ "cipher", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -189,16 +155,6 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" -[[package]] -name = "base64" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" -dependencies = [ - "byteorder", - "safemem", -] - [[package]] name = "base64" version = "0.13.0" @@ -224,21 +180,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "bit-set" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bitflags" version = "1.2.1" @@ -247,43 +188,28 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "block-buffer" -version = "0.7.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.3", -] - -[[package]] -name = "block-cipher-trait" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" -dependencies = [ - "generic-array 0.12.3", + "generic-array", ] [[package]] name = "block-modes" -version = "0.3.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31aa8410095e39fdb732909fb5730a48d5bd7c2e3cd76bd1b07b3dbea130c529" +checksum = "57a0e8073e8baa88212fb5823574c02ebccb395136ba9a164ab89379ec6072f0" dependencies = [ - "block-cipher-trait", "block-padding", + "cipher", ] [[package]] name = "block-padding" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "bumpalo" @@ -291,12 +217,6 @@ version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "byteorder" version = "1.4.2" @@ -367,27 +287,18 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" dependencies = [ - "generic-array 0.14.4", + "generic-array", ] [[package]] name = "clang-sys" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cb92721cb37482245ed88428f72253ce422b3b4ee169c70a0642521bb5db4cc" +checksum = "f54d78e30b388d4815220c8dd03fea5656b6c6d32adb59e89061552a102f8da1" dependencies = [ "glob", "libc", - "libloading", -] - -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags", + "libloading 0.7.0", ] [[package]] @@ -425,7 +336,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" dependencies = [ - "percent-encoding 2.1.0", + "percent-encoding", "time 0.2.25", "version_check", ] @@ -437,13 +348,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" dependencies = [ "cookie", - "idna 0.2.2", + "idna", "log", "publicsuffix", "serde", "serde_json", "time 0.2.25", - "url 2.2.1", + "url", ] [[package]] @@ -480,6 +391,7 @@ dependencies = [ "alsa", "core-foundation-sys", "coreaudio-rs", + "jack", "jni 0.17.0", "js-sys", "lazy_static", @@ -496,6 +408,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + [[package]] name = "crc32fast" version = "1.2.1" @@ -507,24 +425,14 @@ dependencies = [ [[package]] name = "crypto-mac" -version = "0.7.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" dependencies = [ - "generic-array 0.12.3", + "generic-array", "subtle", ] -[[package]] -name = "ctr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022cd691704491df67d25d006fe8eca083098253c4d43516c2206479c58c6736" -dependencies = [ - "block-cipher-trait", - "stream-cipher", -] - [[package]] name = "ctr" version = "0.6.0" @@ -582,11 +490,11 @@ dependencies = [ [[package]] name = "digest" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.12.3", + "generic-array", ] [[package]] @@ -634,12 +542,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fetch_unroll" version = "0.2.2" @@ -676,68 +578,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", - "percent-encoding 2.1.0", -] - -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - -[[package]] -name = "futures" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "percent-encoding", ] [[package]] name = "futures-channel" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" +checksum = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939" dependencies = [ "futures-core", - "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" +checksum = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94" [[package]] name = "futures-executor" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +checksum = "891a4b7b96d84d5940084b2a37632dd65deeae662c114ceaa2c879629c9c0ad1" dependencies = [ "futures-core", "futures-task", "futures-util", ] -[[package]] -name = "futures-io" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" - [[package]] name = "futures-macro" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" +checksum = "ea405816a5139fb39af82c2beb921d52143f556038378d6db21183a5c37fbfb7" dependencies = [ "proc-macro-hack", "proc-macro2", @@ -747,32 +621,26 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" +checksum = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3" [[package]] name = "futures-task" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" -dependencies = [ - "once_cell", -] +checksum = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80" [[package]] name = "futures-util" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +checksum = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1" dependencies = [ - "futures-channel", "futures-core", - "futures-io", "futures-macro", "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "proc-macro-hack", @@ -786,15 +654,6 @@ version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" -[[package]] -name = "generic-array" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.4" @@ -814,17 +673,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.2" @@ -833,7 +681,7 @@ checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -1054,9 +902,9 @@ checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" [[package]] name = "hmac" -version = "0.7.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ "crypto-mac", "digest", @@ -1142,17 +990,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "0.2.2" @@ -1239,7 +1076,7 @@ checksum = "8e1d6ab7ada402b6a27912a2b86504be62a48c58313c886fe72a059127acb4d7" dependencies = [ "lazy_static", "libc", - "libloading", + "libloading 0.6.7", ] [[package]] @@ -1342,6 +1179,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + [[package]] name = "libmdns" version = "0.6.0" @@ -1355,7 +1202,7 @@ dependencies = [ "log", "multimap", "quick-error", - "rand 0.8.3", + "rand", "socket2", "tokio", ] @@ -1410,11 +1257,11 @@ dependencies = [ [[package]] name = "librespot" -version = "0.1.3" +version = "0.1.6" dependencies = [ - "base64 0.13.0", + "base64", "env_logger", - "futures", + "futures-util", "getopts", "hex", "hyper", @@ -1425,44 +1272,41 @@ dependencies = [ "librespot-playback", "librespot-protocol", "log", - "num-bigint", - "protobuf", - "rand 0.7.3", "rpassword", "sha-1", "tokio", - "url 1.7.2", + "url", ] [[package]] name = "librespot-audio" -version = "0.1.3" +version = "0.1.6" dependencies = [ - "aes-ctr 0.6.0", - "bit-set", + "aes-ctr", "byteorder", "bytes", - "futures", + "cfg-if 1.0.0", + "futures-util", "lewton", "librespot-core", "librespot-tremor", "log", - "num-bigint", - "num-traits", - "pin-project-lite", + "ogg", "tempfile", + "tokio", "vorbis", ] [[package]] name = "librespot-connect" -version = "0.1.3" +version = "0.1.6" dependencies = [ - "aes-ctr 0.3.0", - "base64 0.13.0", + "aes-ctr", + "base64", "block-modes", "dns-sd", - "futures", + "futures-core", + "futures-util", "hmac", "hyper", "libmdns", @@ -1472,26 +1316,27 @@ dependencies = [ "log", "num-bigint", "protobuf", - "rand 0.7.3", + "rand", "serde", - "serde_derive", "serde_json", "sha-1", "tokio", - "url 1.7.2", + "tokio-stream", + "url", ] [[package]] name = "librespot-core" -version = "0.1.3" +version = "0.1.6" dependencies = [ "aes", - "base64 0.13.0", + "base64", "byteorder", "bytes", + "cfg-if 1.0.0", "env_logger", - "error-chain", - "futures", + "futures-core", + "futures-util", "hmac", "httparse", "hyper", @@ -1502,44 +1347,41 @@ dependencies = [ "num-traits", "once_cell", "pbkdf2", - "pin-project-lite", "protobuf", - "rand 0.7.3", + "rand", "serde", - "serde_derive", "serde_json", "sha-1", "shannon", + "thiserror", "tokio", + "tokio-stream", "tokio-util", - "tower-service", - "url 1.7.2", - "uuid", + "url", "vergen", ] [[package]] name = "librespot-metadata" -version = "0.1.3" +version = "0.1.6" dependencies = [ "async-trait", "byteorder", - "futures", "librespot-core", "librespot-protocol", - "linear-map", "log", "protobuf", ] [[package]] name = "librespot-playback" -version = "0.1.3" +version = "0.1.6" dependencies = [ "alsa", "byteorder", "cpal", - "futures", + "futures-executor", + "futures-util", "glib", "gstreamer", "gstreamer-app", @@ -1556,12 +1398,13 @@ dependencies = [ "sdl2", "shell-words", "thiserror", + "tokio", "zerocopy", ] [[package]] name = "librespot-protocol" -version = "0.1.3" +version = "0.1.6" dependencies = [ "glob", "protobuf", @@ -1581,12 +1424,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "linear-map" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" - [[package]] name = "lock_api" version = "0.4.2" @@ -1644,9 +1481,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc250d6848c90d719ea2ce34546fb5df7af1d3fd189d10bf7bad80bfcebecd95" +checksum = "a5dede4e2065b3842b8b0af444119f3aa331cc7cc2dd20388bfb0f5d5a38823a" dependencies = [ "libc", "log", @@ -1892,15 +1729,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" - -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +checksum = "4ad167a2f54e832b82dbe003a046280dceffe5227b5f79e08e363a29638cfddd" [[package]] name = "opaque-debug" @@ -1941,17 +1772,12 @@ checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1" [[package]] name = "pbkdf2" -version = "0.3.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006c038a43a45995a9670da19e67600114740e8511d4333bf97a56e66a7542d9" +checksum = "309c95c5f738c85920eb7062a2de29f3840d4f96974453fc9ac1ba078da9c627" dependencies = [ - "base64 0.9.3", - "byteorder", "crypto-mac", "hmac", - "rand 0.5.6", - "sha2", - "subtle", ] [[package]] @@ -1960,12 +1786,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" -[[package]] -name = "percent-encoding" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" - [[package]] name = "percent-encoding" version = "2.1.0" @@ -2138,10 +1958,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" dependencies = [ "error-chain", - "idna 0.2.2", + "idna", "lazy_static", "regex", - "url 2.2.1", + "url", ] [[package]] @@ -2150,7 +1970,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" dependencies = [ - "percent-encoding 2.1.0", + "percent-encoding", ] [[package]] @@ -2168,32 +1988,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "winapi", -] - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc 0.2.0", -] - [[package]] name = "rand" version = "0.8.3" @@ -2201,19 +1995,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" dependencies = [ "libc", - "rand_chacha 0.3.0", - "rand_core 0.6.2", - "rand_hc 0.3.0", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha", + "rand_core", + "rand_hc", ] [[package]] @@ -2223,31 +2007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" dependencies = [ "ppv-lite86", - "rand_core 0.6.2", -] - -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -2256,16 +2016,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" dependencies = [ - "getrandom 0.2.2", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", + "getrandom", ] [[package]] @@ -2274,7 +2025,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" dependencies = [ - "rand_core 0.6.2", + "rand_core", ] [[package]] @@ -2389,7 +2140,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" dependencies = [ - "base64 0.13.0", + "base64", "log", "ring", "sct", @@ -2402,12 +2153,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - [[package]] name = "same-file" version = "1.0.6" @@ -2522,14 +2267,15 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.8.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" dependencies = [ "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool", "digest", - "fake-simd", - "opaque-debug 0.2.3", + "opaque-debug", ] [[package]] @@ -2538,18 +2284,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" -[[package]] -name = "sha2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" -dependencies = [ - "block-buffer", - "digest", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "shannon" version = "0.2.0" @@ -2673,15 +2407,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" -[[package]] -name = "stream-cipher" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c" -dependencies = [ - "generic-array 0.12.3", -] - [[package]] name = "strsim" version = "0.9.3" @@ -2708,9 +2433,9 @@ dependencies = [ [[package]] name = "subtle" -version = "1.0.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" [[package]] name = "syn" @@ -2769,7 +2494,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if 1.0.0", "libc", - "rand 0.8.3", + "rand", "redox_syscall", "remove_dir_all", "winapi", @@ -2906,6 +2631,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1981ad97df782ab506a1f43bf82c967326960d278acf3bf8279809648c3ff3ea" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.6.3" @@ -3040,7 +2776,7 @@ version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "294b85ef5dbc3670a72e82a89971608a1fcc4ed5c7c5a2895230d31a95f0569b" dependencies = [ - "base64 0.13.0", + "base64", "chunked_transfer", "cookie", "cookie_store", @@ -3048,22 +2784,11 @@ dependencies = [ "once_cell", "qstring", "rustls", - "url 2.2.1", + "url", "webpki", "webpki-roots", ] -[[package]] -name = "url" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" -dependencies = [ - "idna 0.1.5", - "matches", - "percent-encoding 1.0.1", -] - [[package]] name = "url" version = "2.2.1" @@ -3071,18 +2796,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" dependencies = [ "form_urlencoded", - "idna 0.2.2", + "idna", "matches", - "percent-encoding 2.1.0", -] - -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.2", + "percent-encoding", ] [[package]] @@ -3172,12 +2888,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index e4f9c51e..6b8cbee5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.1.3" +version = "0.1.6" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -22,59 +22,61 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.1.3" +version = "0.1.6" [dependencies.librespot-connect] path = "connect" -version = "0.1.3" +version = "0.1.6" [dependencies.librespot-core] path = "core" -version = "0.1.3" +version = "0.1.6" [dependencies.librespot-metadata] path = "metadata" -version = "0.1.3" +version = "0.1.6" [dependencies.librespot-playback] path = "playback" -version = "0.1.3" +version = "0.1.6" [dependencies.librespot-protocol] path = "protocol" -version = "0.1.3" +version = "0.1.6" [dependencies] base64 = "0.13" env_logger = {version = "0.8", default-features = false, features = ["termcolor","humantime","atty"]} -futures = "0.3" +futures-util = { version = "0.3", default_features = false } getopts = "0.2" +hex = "0.4" hyper = "0.14" log = "0.4" -num-bigint = "0.3" -protobuf = "~2.14.0" -rand = "0.7" rpassword = "5.0" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "signal", "process"] } -url = "1.7" -sha-1 = "0.8" -hex = "0.4" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "signal", "sync", "process"] } +url = "2.1" +sha-1 = "0.9" [features] +apresolve = ["librespot-core/apresolve"] +apresolve-http2 = ["librespot-core/apresolve-http2"] + alsa-backend = ["librespot-playback/alsa-backend"] portaudio-backend = ["librespot-playback/portaudio-backend"] pulseaudio-backend = ["librespot-playback/pulseaudio-backend"] jackaudio-backend = ["librespot-playback/jackaudio-backend"] rodio-backend = ["librespot-playback/rodio-backend"] +rodiojack-backend = ["librespot-playback/rodiojack-backend"] sdl-backend = ["librespot-playback/sdl-backend"] gstreamer-backend = ["librespot-playback/gstreamer-backend"] with-tremor = ["librespot-audio/with-tremor"] with-vorbis = ["librespot-audio/with-vorbis"] +with-lewton = ["librespot-audio/with-lewton"] # with-dns-sd = ["librespot-connect/with-dns-sd"] -default = ["librespot-playback/rodio-backend"] +default = ["rodio-backend", "apresolve", "with-lewton"] [package.metadata.deb] maintainer = "librespot-org" diff --git a/README.md b/README.md index e772c510..7102c28a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ There is some brief documentation on how the protocol works in the [docs](https: If you wish to learn more about how librespot works overall, the best way is to simply read the code, and ask any questions you have in our [Gitter Room](https://gitter.im/librespot-org/spotify-connect-resources). -# Issues +# Issues & Discussions +**We have recently started using Github discussions for general questions and feature requests, as they are a more natural medium for such cases, and allow for upvoting to prioritize feature development. Check them out [here](https://github.com/librespot-org/librespot/discussions). Bugs and issues with the underlying library should still be reported as issues.** + If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, eg. the Spotify URI of the song that caused the crash. # Building @@ -54,6 +56,7 @@ ALSA PortAudio PulseAudio JACK +JACK over Rodio SDL Pipe ``` diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 1b5b074c..d8c0eea2 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.1.3" +version = "0.1.6" authors = ["Paul Lietar "] description="The audio fetching and processing logic for librespot" license="MIT" @@ -8,24 +8,24 @@ edition = "2018" [dependencies.librespot-core] path = "../core" -version = "0.1.3" +version = "0.1.6" [dependencies] aes-ctr = "0.6" -bit-set = "0.5" byteorder = "1.4" bytes = "1.0" -futures = "0.3" -lewton = "0.10" +cfg-if = "1" log = "0.4" -num-bigint = "0.3" -num-traits = "0.2" -pin-project-lite = "0.2.4" +futures-util = { version = "0.3", default_features = false } +ogg = "0.8" tempfile = "3.1" +tokio = { version = "1", features = ["sync"] } +lewton = { version = "0.10", optional = true } librespot-tremor = { version = "0.2.0", optional = true } vorbis = { version ="0.0.14", optional = true } [features] +with-lewton = ["lewton"] with-tremor = ["librespot-tremor"] with-vorbis = ["vorbis"] diff --git a/audio/src/fetch.rs b/audio/src/fetch.rs index 286a2b88..ccbb8989 100644 --- a/audio/src/fetch.rs +++ b/audio/src/fetch.rs @@ -1,30 +1,23 @@ -use crate::range_set::{Range, RangeSet}; +use std::cmp::{max, min}; +use std::fs; +use std::future::Future; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::pin::Pin; +use std::sync::atomic::{self, AtomicUsize}; +use std::sync::{Arc, Condvar, Mutex}; +use std::task::{Context, Poll}; +use std::time::{Duration, Instant}; + use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use bytes::Bytes; -use futures::{ - channel::{mpsc, oneshot}, - future, -}; -use futures::{Future, Stream, StreamExt, TryFutureExt, TryStreamExt}; - -use std::fs; -use std::io::{self, Read, Seek, SeekFrom, Write}; -use std::sync::{Arc, Condvar, Mutex}; -use std::task::Poll; -use std::time::{Duration, Instant}; -use std::{ - cmp::{max, min}, - pin::Pin, - task::Context, -}; -use tempfile::NamedTempFile; - -use futures::channel::mpsc::unbounded; +use futures_util::{future, StreamExt, TryFutureExt, TryStreamExt}; use librespot_core::channel::{Channel, ChannelData, ChannelError, ChannelHeaders}; use librespot_core::session::Session; use librespot_core::spotify_id::FileId; -use std::sync::atomic; -use std::sync::atomic::AtomicUsize; +use tempfile::NamedTempFile; +use tokio::sync::{mpsc, oneshot}; + +use crate::range_set::{Range, RangeSet}; const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 16; // The minimum size of a block that is requested from the Spotify servers in one request. @@ -96,6 +89,7 @@ pub enum AudioFile { Streaming(AudioFileStreaming), } +#[derive(Debug)] enum StreamLoaderCommand { Fetch(Range), // signal the stream loader to fetch a range of the file RandomAccessMode(), // optimise download strategy for random access @@ -147,7 +141,7 @@ impl StreamLoaderController { fn send_stream_loader_command(&mut self, command: StreamLoaderCommand) { if let Some(ref mut channel) = self.channel_tx { // ignore the error in case the channel has been closed already. - let _ = channel.unbounded_send(command); + let _ = channel.send(command); } } @@ -191,7 +185,7 @@ impl StreamLoaderController { // We can't use self.fetch here because self can't be borrowed mutably, so we access the channel directly. if let Some(ref mut channel) = self.channel_tx { // ignore the error in case the channel has been closed already. - let _ = channel.unbounded_send(StreamLoaderCommand::Fetch(range)); + let _ = channel.send(StreamLoaderCommand::Fetch(range)); } } } @@ -387,7 +381,7 @@ impl AudioFileStreaming { //let (seek_tx, seek_rx) = mpsc::unbounded(); let (stream_loader_command_tx, stream_loader_command_rx) = - mpsc::unbounded::(); + mpsc::unbounded_channel::(); let fetcher = AudioFileFetch::new( session.clone(), @@ -455,7 +449,7 @@ enum ReceivedData { async fn audio_file_fetch_receive_data( shared: Arc, file_data_tx: mpsc::UnboundedSender, - data_rx: ChannelData, + mut data_rx: ChannelData, initial_data_offset: usize, initial_request_length: usize, request_sent_time: Instant, @@ -471,49 +465,44 @@ async fn audio_file_fetch_receive_data( .number_of_open_requests .fetch_add(1, atomic::Ordering::SeqCst); - enum TryFoldErr { - ChannelError, - FinishEarly, - } + let result = loop { + let data = match data_rx.next().await { + Some(Ok(data)) => data, + Some(Err(e)) => break Err(e), + None => break Ok(()), + }; - let result = data_rx - .map_err(|_| TryFoldErr::ChannelError) - .try_for_each(|data| { - if measure_ping_time { - let duration = Instant::now() - request_sent_time; - let duration_ms: u64; - if 0.001 * (duration.as_millis() as f64) - > MAXIMUM_ASSUMED_PING_TIME_SECONDS - { - duration_ms = (MAXIMUM_ASSUMED_PING_TIME_SECONDS * 1000.0) as u64; - } else { - duration_ms = duration.as_millis() as u64; - } - let _ = file_data_tx - .unbounded_send(ReceivedData::ResponseTimeMs(duration_ms as usize)); - measure_ping_time = false; - } - let data_size = data.len(); - let _ = file_data_tx - .unbounded_send(ReceivedData::Data(PartialFileData { - offset: data_offset, - data: data, - })); - data_offset += data_size; - if request_length < data_size { - warn!("Data receiver for range {} (+{}) received more data from server than requested.", initial_data_offset, initial_request_length); - request_length = 0; + if measure_ping_time { + let duration = Instant::now() - request_sent_time; + let duration_ms: u64; + if 0.001 * (duration.as_millis() as f64) > MAXIMUM_ASSUMED_PING_TIME_SECONDS { + duration_ms = (MAXIMUM_ASSUMED_PING_TIME_SECONDS * 1000.0) as u64; } else { - request_length -= data_size; + duration_ms = duration.as_millis() as u64; } + let _ = file_data_tx.send(ReceivedData::ResponseTimeMs(duration_ms as usize)); + measure_ping_time = false; + } + let data_size = data.len(); + let _ = file_data_tx.send(ReceivedData::Data(PartialFileData { + offset: data_offset, + data: data, + })); + data_offset += data_size; + if request_length < data_size { + warn!( + "Data receiver for range {} (+{}) received more data from server than requested.", + initial_data_offset, initial_request_length + ); + request_length = 0; + } else { + request_length -= data_size; + } - future::ready(if request_length == 0 { - Err(TryFoldErr::FinishEarly) - } else { - Ok(()) - }) - }) - .await; + if request_length == 0 { + break Ok(()); + } + }; if request_length > 0 { let missing_range = Range::new(data_offset, request_length); @@ -527,7 +516,7 @@ async fn audio_file_fetch_receive_data( .number_of_open_requests .fetch_sub(1, atomic::Ordering::SeqCst); - if let Err(TryFoldErr::ChannelError) = result { + if result.is_err() { warn!( "Error from channel for data receiver for range {} (+{}).", initial_data_offset, initial_request_length @@ -696,21 +685,17 @@ async fn audio_file_fetch( future::select_all(vec![f1, f2, f3]).await }*/ -pin_project! { - struct AudioFileFetch { - session: Session, - shared: Arc, - output: Option, +struct AudioFileFetch { + session: Session, + shared: Arc, + output: Option, - file_data_tx: mpsc::UnboundedSender, - #[pin] - file_data_rx: mpsc::UnboundedReceiver, + file_data_tx: mpsc::UnboundedSender, + file_data_rx: mpsc::UnboundedReceiver, - #[pin] - stream_loader_command_rx: mpsc::UnboundedReceiver, - complete_tx: Option>, - network_response_times_ms: Vec, - } + stream_loader_command_rx: mpsc::UnboundedReceiver, + complete_tx: Option>, + network_response_times_ms: Vec, } impl AudioFileFetch { @@ -725,7 +710,7 @@ impl AudioFileFetch { stream_loader_command_rx: mpsc::UnboundedReceiver, complete_tx: oneshot::Sender, ) -> AudioFileFetch { - let (file_data_tx, file_data_rx) = unbounded::(); + let (file_data_tx, file_data_rx) = mpsc::unbounded_channel::(); { let requested_range = Range::new(0, initial_data_length); @@ -863,7 +848,7 @@ impl AudioFileFetch { fn poll_file_data_rx(&mut self, cx: &mut Context<'_>) -> Poll<()> { loop { - match Pin::new(&mut self.file_data_rx).poll_next(cx) { + match self.file_data_rx.poll_recv(cx) { Poll::Ready(None) => return Poll::Ready(()), Poll::Ready(Some(ReceivedData::ResponseTimeMs(response_time_ms))) => { trace!("Ping time estimated as: {} ms.", response_time_ms); @@ -939,7 +924,7 @@ impl AudioFileFetch { fn poll_stream_loader_command_rx(&mut self, cx: &mut Context<'_>) -> Poll<()> { loop { - match Pin::new(&mut self.stream_loader_command_rx).poll_next(cx) { + match self.stream_loader_command_rx.poll_recv(cx) { Poll::Ready(None) => return Poll::Ready(()), Poll::Ready(Some(cmd)) => match cmd { StreamLoaderCommand::Fetch(request) => { @@ -1059,7 +1044,7 @@ impl Read for AudioFileStreaming { for &range in ranges_to_request.iter() { self.stream_loader_command_tx - .unbounded_send(StreamLoaderCommand::Fetch(range)) + .send(StreamLoaderCommand::Fetch(range)) .unwrap(); } diff --git a/audio/src/lewton_decoder.rs b/audio/src/lewton_decoder.rs index b9f05d4c..086ea57e 100644 --- a/audio/src/lewton_decoder.rs +++ b/audio/src/lewton_decoder.rs @@ -1,13 +1,11 @@ -extern crate lewton; - -use self::lewton::inside_ogg::OggStreamReader; +use lewton::inside_ogg::OggStreamReader; +use super::{AudioDecoder, AudioError, AudioPacket}; use std::error; use std::fmt; use std::io::{Read, Seek}; pub struct VorbisDecoder(OggStreamReader); -pub struct VorbisPacket(Vec); pub struct VorbisError(lewton::VorbisError); impl VorbisDecoder @@ -17,41 +15,38 @@ where pub fn new(input: R) -> Result, VorbisError> { Ok(VorbisDecoder(OggStreamReader::new(input)?)) } +} - pub fn seek(&mut self, ms: i64) -> Result<(), VorbisError> { +impl AudioDecoder for VorbisDecoder +where + R: Read + Seek, +{ + fn seek(&mut self, ms: i64) -> Result<(), AudioError> { let absgp = ms * 44100 / 1000; - self.0.seek_absgp_pg(absgp as u64)?; - Ok(()) + match self.0.seek_absgp_pg(absgp as u64) { + Ok(_) => return Ok(()), + Err(err) => return Err(AudioError::VorbisError(err.into())), + } } - pub fn next_packet(&mut self) -> Result, VorbisError> { - use self::lewton::audio::AudioReadError::AudioIsHeader; - use self::lewton::OggReadError::NoCapturePatternFound; - use self::lewton::VorbisError::BadAudio; - use self::lewton::VorbisError::OggError; + fn next_packet(&mut self) -> Result, AudioError> { + use lewton::audio::AudioReadError::AudioIsHeader; + use lewton::OggReadError::NoCapturePatternFound; + use lewton::VorbisError::BadAudio; + use lewton::VorbisError::OggError; loop { match self.0.read_dec_packet_itl() { - Ok(Some(packet)) => return Ok(Some(VorbisPacket(packet))), + Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet))), Ok(None) => return Ok(None), Err(BadAudio(AudioIsHeader)) => (), Err(OggError(NoCapturePatternFound)) => (), - Err(err) => return Err(err.into()), + Err(err) => return Err(AudioError::VorbisError(err.into())), } } } } -impl VorbisPacket { - pub fn data(&self) -> &[i16] { - &self.0 - } - - pub fn data_mut(&mut self) -> &mut [i16] { - &mut self.0 - } -} - impl From for VorbisError { fn from(err: lewton::VorbisError) -> VorbisError { VorbisError(err) diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 1be1ba88..099fb4a8 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -2,27 +2,33 @@ #[macro_use] extern crate log; -#[macro_use] -extern crate pin_project_lite; - -extern crate aes_ctr; -extern crate bit_set; -extern crate byteorder; -extern crate bytes; -extern crate futures; -extern crate num_bigint; -extern crate num_traits; -extern crate tempfile; - -extern crate librespot_core; mod decrypt; mod fetch; -#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] -mod lewton_decoder; -#[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] -mod libvorbis_decoder; +use cfg_if::cfg_if; + +#[cfg(any( + all(feature = "with-lewton", feature = "with-tremor"), + all(feature = "with-vorbis", feature = "with-tremor"), + all(feature = "with-lewton", feature = "with-vorbis") +))] +compile_error!("Cannot use two decoders at the same time."); + +cfg_if! { + if #[cfg(feature = "with-lewton")] { + mod lewton_decoder; + pub use lewton_decoder::{VorbisDecoder, VorbisError}; + } else if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] { + mod libvorbis_decoder; + pub use crate::libvorbis_decoder::{VorbisDecoder, VorbisError}; + } else { + compile_error!("Must choose a vorbis decoder."); + } +} + +mod passthrough_decoder; +pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; mod range_set; @@ -32,8 +38,64 @@ pub use fetch::{ READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; +use std::fmt; -#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] -pub use crate::lewton_decoder::{VorbisDecoder, VorbisError, VorbisPacket}; -#[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] -pub use libvorbis_decoder::{VorbisDecoder, VorbisError, VorbisPacket}; +pub enum AudioPacket { + Samples(Vec), + OggData(Vec), +} + +impl AudioPacket { + pub fn samples(&self) -> &[i16] { + match self { + AudioPacket::Samples(s) => s, + AudioPacket::OggData(_) => panic!("can't return OggData on samples"), + } + } + + pub fn oggdata(&self) -> &[u8] { + match self { + AudioPacket::Samples(_) => panic!("can't return samples on OggData"), + AudioPacket::OggData(d) => d, + } + } + + pub fn is_empty(&self) -> bool { + match self { + AudioPacket::Samples(s) => s.is_empty(), + AudioPacket::OggData(d) => d.is_empty(), + } + } +} + +#[derive(Debug)] +pub enum AudioError { + PassthroughError(PassthroughError), + VorbisError(VorbisError), +} + +impl fmt::Display for AudioError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AudioError::PassthroughError(err) => write!(f, "PassthroughError({})", err), + AudioError::VorbisError(err) => write!(f, "VorbisError({})", err), + } + } +} + +impl From for AudioError { + fn from(err: VorbisError) -> AudioError { + AudioError::VorbisError(VorbisError::from(err)) + } +} + +impl From for AudioError { + fn from(err: PassthroughError) -> AudioError { + AudioError::PassthroughError(PassthroughError::from(err)) + } +} + +pub trait AudioDecoder { + fn seek(&mut self, ms: i64) -> Result<(), AudioError>; + fn next_packet(&mut self) -> Result, AudioError>; +} diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index 48be2b86..eeef8ab9 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -1,14 +1,12 @@ #[cfg(feature = "with-tremor")] -extern crate librespot_tremor as vorbis; -#[cfg(not(feature = "with-tremor"))] -extern crate vorbis; +use librespot_tremor as vorbis; +use super::{AudioDecoder, AudioError, AudioPacket}; use std::error; use std::fmt; use std::io::{Read, Seek}; pub struct VorbisDecoder(vorbis::Decoder); -pub struct VorbisPacket(vorbis::Packet); pub struct VorbisError(vorbis::VorbisError); impl VorbisDecoder @@ -18,23 +16,28 @@ where pub fn new(input: R) -> Result, VorbisError> { Ok(VorbisDecoder(vorbis::Decoder::new(input)?)) } +} +impl AudioDecoder for VorbisDecoder +where + R: Read + Seek, +{ #[cfg(not(feature = "with-tremor"))] - pub fn seek(&mut self, ms: i64) -> Result<(), VorbisError> { + fn seek(&mut self, ms: i64) -> Result<(), AudioError> { self.0.time_seek(ms as f64 / 1000f64)?; Ok(()) } #[cfg(feature = "with-tremor")] - pub fn seek(&mut self, ms: i64) -> Result<(), VorbisError> { + fn seek(&mut self, ms: i64) -> Result<(), AudioError> { self.0.time_seek(ms)?; Ok(()) } - pub fn next_packet(&mut self) -> Result, VorbisError> { + fn next_packet(&mut self) -> Result, AudioError> { loop { match self.0.packets().next() { - Some(Ok(packet)) => return Ok(Some(VorbisPacket(packet))), + Some(Ok(packet)) => return Ok(Some(AudioPacket::Samples(packet.data))), None => return Ok(None), Some(Err(vorbis::VorbisError::Hole)) => (), @@ -44,16 +47,6 @@ where } } -impl VorbisPacket { - pub fn data(&self) -> &[i16] { - &self.0.data - } - - pub fn data_mut(&mut self) -> &mut [i16] { - &mut self.0.data - } -} - impl From for VorbisError { fn from(err: vorbis::VorbisError) -> VorbisError { VorbisError(err) @@ -77,3 +70,9 @@ impl error::Error for VorbisError { error::Error::source(&self.0) } } + +impl From for AudioError { + fn from(err: vorbis::VorbisError) -> AudioError { + AudioError::VorbisError(VorbisError(err)) + } +} diff --git a/audio/src/passthrough_decoder.rs b/audio/src/passthrough_decoder.rs new file mode 100644 index 00000000..3a011011 --- /dev/null +++ b/audio/src/passthrough_decoder.rs @@ -0,0 +1,191 @@ +// Passthrough decoder for librespot +use super::{AudioDecoder, AudioError, AudioPacket}; +use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; +use std::fmt; +use std::io::{Read, Seek}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn write_headers( + rdr: &mut PacketReader, + wtr: &mut PacketWriter>, +) -> Result { + let mut stream_serial: u32 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u32; + + // search for ident, comment, setup + get_header(1, rdr, wtr, &mut stream_serial, PacketWriteEndInfo::EndPage)?; + get_header( + 3, + rdr, + wtr, + &mut stream_serial, + PacketWriteEndInfo::NormalPacket, + )?; + get_header(5, rdr, wtr, &mut stream_serial, PacketWriteEndInfo::EndPage)?; + + // remove un-needed packets + rdr.delete_unread_packets(); + return Ok(stream_serial); +} + +fn get_header( + code: u8, + rdr: &mut PacketReader, + wtr: &mut PacketWriter>, + stream_serial: &mut u32, + info: PacketWriteEndInfo, +) -> Result +where + T: Read + Seek, +{ + let pck: Packet = rdr.read_packet_expected()?; + + // set a unique serial number + if pck.stream_serial() != 0 { + *stream_serial = pck.stream_serial(); + } + + let pkt_type = pck.data[0]; + debug!("Vorbis header type{}", &pkt_type); + + // all headers are mandatory + if pkt_type != code { + return Err(PassthroughError(OggReadError::InvalidData)); + } + + // headers keep original granule number + let absgp_page = pck.absgp_page(); + wtr.write_packet( + pck.data.into_boxed_slice(), + *stream_serial, + info, + absgp_page, + ) + .unwrap(); + + return Ok(*stream_serial); +} + +pub struct PassthroughDecoder { + rdr: PacketReader, + wtr: PacketWriter>, + lastgp_page: Option, + absgp_page: u64, + stream_serial: u32, +} + +pub struct PassthroughError(ogg::OggReadError); + +impl PassthroughDecoder { + /// Constructs a new Decoder from a given implementation of `Read + Seek`. + pub fn new(rdr: R) -> Result { + let mut rdr = PacketReader::new(rdr); + let mut wtr = PacketWriter::new(Vec::new()); + + let stream_serial = write_headers(&mut rdr, &mut wtr)?; + info!("Starting passthrough track with serial {}", stream_serial); + + return Ok(PassthroughDecoder { + rdr, + wtr, + lastgp_page: Some(0), + absgp_page: 0, + stream_serial, + }); + } +} + +impl AudioDecoder for PassthroughDecoder { + fn seek(&mut self, ms: i64) -> Result<(), AudioError> { + info!("Seeking to {}", ms); + self.lastgp_page = match ms { + 0 => Some(0), + _ => None, + }; + + // hard-coded to 44.1 kHz + match self.rdr.seek_absgp(None, (ms * 44100 / 1000) as u64) { + Ok(_) => return Ok(()), + Err(err) => return Err(AudioError::PassthroughError(err.into())), + } + } + + fn next_packet(&mut self) -> Result, AudioError> { + let mut skip = self.lastgp_page.is_none(); + loop { + let pck = match self.rdr.read_packet() { + Ok(Some(pck)) => pck, + + Ok(None) | Err(OggReadError::NoCapturePatternFound) => { + info!("end of streaming"); + return Ok(None); + } + + Err(err) => return Err(AudioError::PassthroughError(err.into())), + }; + + let pckgp_page = pck.absgp_page(); + let lastgp_page = self.lastgp_page.get_or_insert(pckgp_page); + + // consume packets till next page to get a granule reference + if skip { + if *lastgp_page == pckgp_page { + debug!("skipping packet"); + continue; + } + skip = false; + info!("skipped at {}", pckgp_page); + } + + // now we can calculate absolute granule + self.absgp_page += pckgp_page - *lastgp_page; + self.lastgp_page = Some(pckgp_page); + + // set packet type + let inf = if pck.last_in_stream() { + self.lastgp_page = Some(0); + PacketWriteEndInfo::EndStream + } else if pck.last_in_page() { + PacketWriteEndInfo::EndPage + } else { + PacketWriteEndInfo::NormalPacket + }; + + self.wtr + .write_packet( + pck.data.into_boxed_slice(), + self.stream_serial, + inf, + self.absgp_page, + ) + .unwrap(); + + let data = self.wtr.inner_mut(); + + if data.len() > 0 { + let result = AudioPacket::OggData(std::mem::take(data)); + return Ok(Some(result)); + } + } + } +} + +impl fmt::Debug for PassthroughError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl From for PassthroughError { + fn from(err: OggReadError) -> PassthroughError { + PassthroughError(err) + } +} + +impl fmt::Display for PassthroughError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} diff --git a/audio/src/range_set.rs b/audio/src/range_set.rs index d01d888e..31ce6500 100644 --- a/audio/src/range_set.rs +++ b/audio/src/range_set.rs @@ -2,7 +2,7 @@ use std::cmp::{max, min}; use std::fmt; use std::slice::Iter; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct Range { pub start: usize, pub length: usize, diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 1f73f01b..b03de885 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,44 +1,48 @@ [package] name = "librespot-connect" -version = "0.1.3" +version = "0.1.6" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" repository = "https://github.com/librespot-org/librespot" edition = "2018" -[dependencies.librespot-core] -path = "../core" -version = "0.1.3" -[dependencies.librespot-playback] -path = "../playback" -version = "0.1.3" -[dependencies.librespot-protocol] -path = "../protocol" -version = "0.1.3" - [dependencies] +aes-ctr = "0.6" base64 = "0.13" -futures = "0.3" -hyper = { version = "0.14", features = ["server", "http1"] } +block-modes = "0.7" +futures-core = "0.3" +futures-util = { version = "0.3", default_features = false } +hmac = "0.10" +hyper = { version = "0.14", features = ["server", "http1", "tcp"] } log = "0.4" num-bigint = "0.3" protobuf = "~2.14.0" -rand = "0.7" -serde = "1.0" -serde_derive = "1.0" +rand = "0.8" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tokio = { version = "1.0", features = ["macros"] } -url = "1.7" -sha-1 = "0.8" -hmac = "0.7" -aes-ctr = "0.3" -block-modes = "0.3" +sha-1 = "0.9" +tokio = { version = "1.0", features = ["macros", "rt", "sync"] } +tokio-stream = { version = "0.1" } +url = "2.1" dns-sd = { version = "0.1.3", optional = true } libmdns = { version = "0.6", optional = true } +[dependencies.librespot-core] +path = "../core" +version = "0.1.6" + +[dependencies.librespot-playback] +path = "../playback" +version = "0.1.6" + +[dependencies.librespot-protocol] +path = "../protocol" +version = "0.1.6" [features] -default = ["libmdns"] +with-libmdns = ["libmdns"] with-dns-sd = ["dns-sd"] + +default = ["with-libmdns"] diff --git a/connect/src/context.rs b/connect/src/context.rs index 5a94f6cb..63a2aebb 100644 --- a/connect/src/context.rs +++ b/connect/src/context.rs @@ -1,7 +1,7 @@ +use crate::core::spotify_id::SpotifyId; use crate::protocol::spirc::TrackRef; -use librespot_core::spotify_id::SpotifyId; -use serde; +use serde::Deserialize; #[derive(Deserialize, Debug)] pub struct StationContext { diff --git a/connect/src/discovery.rs b/connect/src/discovery.rs index 62310b2f..1c94ecc8 100644 --- a/connect/src/discovery.rs +++ b/connect/src/discovery.rs @@ -1,13 +1,13 @@ -use aes_ctr::stream_cipher::generic_array::GenericArray; -use aes_ctr::stream_cipher::{NewStreamCipher, SyncStreamCipher}; +use aes_ctr::cipher::generic_array::GenericArray; +use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; use aes_ctr::Aes128Ctr; -use base64; -use futures::channel::{mpsc, oneshot}; -use futures::{Stream, StreamExt}; -use hmac::{Hmac, Mac}; +use futures_core::Stream; +use hmac::{Hmac, Mac, NewMac}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Request, Response, StatusCode}; +use serde_json::json; use sha1::{Digest, Sha1}; +use tokio::sync::{mpsc, oneshot}; use std::borrow::Cow; use std::convert::Infallible; @@ -50,7 +50,7 @@ impl Discovery { config: ConnectConfig, device_id: String, ) -> (Discovery, mpsc::UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded(); + let (tx, rx) = mpsc::unbounded_channel(); let key_data = util::rand_vec(&mut rand::thread_rng(), 95); let private_key = BigUint::from_bytes_be(&key_data); @@ -118,18 +118,18 @@ impl Discovery { let checksum_key = { let mut h = HmacSha1::new_varkey(base_key).expect("HMAC can take key of any size"); - h.input(b"checksum"); - h.result().code() + h.update(b"checksum"); + h.finalize().into_bytes() }; let encryption_key = { let mut h = HmacSha1::new_varkey(&base_key).expect("HMAC can take key of any size"); - h.input(b"encryption"); - h.result().code() + h.update(b"encryption"); + h.finalize().into_bytes() }; let mut h = HmacSha1::new_varkey(&checksum_key).expect("HMAC can take key of any size"); - h.input(encrypted); + h.update(encrypted); if let Err(_) = h.verify(cksum) { warn!("Login error for user {:?}: MAC mismatch", username); let result = json!({ @@ -155,7 +155,7 @@ impl Discovery { let credentials = Credentials::with_blob(username.to_string(), &decrypted, &self.0.device_id); - self.0.tx.unbounded_send(credentials).unwrap(); + self.0.tx.send(credentials).unwrap(); let result = json!({ "status": 101, @@ -273,6 +273,6 @@ impl Stream for DiscoveryStream { type Item = Credentials; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.credentials.poll_next_unpin(cx) + self.credentials.poll_recv(cx) } } diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 47777606..600dd033 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -1,34 +1,9 @@ #[macro_use] extern crate log; -#[macro_use] -extern crate serde_json; -#[macro_use] -extern crate serde_derive; -extern crate serde; -extern crate base64; -extern crate futures; -extern crate hyper; -extern crate num_bigint; -extern crate protobuf; -extern crate rand; -extern crate tokio; -extern crate url; - -extern crate aes_ctr; -extern crate block_modes; -extern crate hmac; -extern crate sha1; - -#[cfg(feature = "with-dns-sd")] -extern crate dns_sd; - -#[cfg(not(feature = "with-dns-sd"))] -extern crate libmdns; - -extern crate librespot_core; -extern crate librespot_playback as playback; -extern crate librespot_protocol as protocol; +use librespot_core as core; +use librespot_playback as playback; +use librespot_protocol as protocol; pub mod context; pub mod discovery; diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 2e3694e4..5afefe7f 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1,26 +1,27 @@ +use std::future::Future; use std::pin::Pin; use std::time::{SystemTime, UNIX_EPOCH}; use crate::context::StationContext; +use crate::core::config::{ConnectConfig, VolumeCtrl}; +use crate::core::mercury::{MercuryError, MercurySender}; +use crate::core::session::Session; +use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError}; +use crate::core::util::url_encode; +use crate::core::util::SeqGenerator; +use crate::core::version; use crate::playback::mixer::Mixer; use crate::playback::player::{Player, PlayerEvent, PlayerEventChannel}; use crate::protocol; use crate::protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef}; -use futures::channel::mpsc; -use futures::future::{self, FusedFuture}; -use futures::stream::FusedStream; -use futures::{Future, FutureExt, StreamExt}; -use librespot_core::config::{ConnectConfig, VolumeCtrl}; -use librespot_core::mercury::{MercuryError, MercurySender}; -use librespot_core::session::Session; -use librespot_core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError}; -use librespot_core::util::url_encode; -use librespot_core::util::SeqGenerator; -use librespot_core::version; + +use futures_util::future::{self, FusedFuture}; +use futures_util::stream::FusedStream; +use futures_util::{FutureExt, StreamExt}; use protobuf::{self, Message}; -use rand; use rand::seq::SliceRandom; -use serde_json; +use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; enum SpircPlayStatus { Stopped, @@ -59,8 +60,8 @@ struct SpircTask { subscription: BoxedStream, sender: MercurySender, - commands: mpsc::UnboundedReceiver, - player_events: PlayerEventChannel, + commands: Option>, + player_events: Option, shutdown: bool, session: Session, @@ -263,6 +264,7 @@ impl Spirc { .mercury() .subscribe(uri.clone()) .map(Result::unwrap) + .map(UnboundedReceiverStream::new) .flatten_stream() .map(|response| -> Frame { let data = response.payload.first().unwrap(); @@ -272,7 +274,7 @@ impl Spirc { let sender = session.mercury().sender(uri); - let (cmd_tx, cmd_rx) = mpsc::unbounded(); + let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); let volume = config.volume; let task_config = SpircTaskConfig { @@ -301,8 +303,8 @@ impl Spirc { subscription: subscription, sender: sender, - commands: cmd_rx, - player_events: player_events, + commands: Some(cmd_rx), + player_events: Some(player_events), shutdown: false, session: session, @@ -322,34 +324,36 @@ impl Spirc { } pub fn play(&self) { - let _ = self.commands.unbounded_send(SpircCommand::Play); + let _ = self.commands.send(SpircCommand::Play); } pub fn play_pause(&self) { - let _ = self.commands.unbounded_send(SpircCommand::PlayPause); + let _ = self.commands.send(SpircCommand::PlayPause); } pub fn pause(&self) { - let _ = self.commands.unbounded_send(SpircCommand::Pause); + let _ = self.commands.send(SpircCommand::Pause); } pub fn prev(&self) { - let _ = self.commands.unbounded_send(SpircCommand::Prev); + let _ = self.commands.send(SpircCommand::Prev); } pub fn next(&self) { - let _ = self.commands.unbounded_send(SpircCommand::Next); + let _ = self.commands.send(SpircCommand::Next); } pub fn volume_up(&self) { - let _ = self.commands.unbounded_send(SpircCommand::VolumeUp); + let _ = self.commands.send(SpircCommand::VolumeUp); } pub fn volume_down(&self) { - let _ = self.commands.unbounded_send(SpircCommand::VolumeDown); + let _ = self.commands.send(SpircCommand::VolumeDown); } pub fn shutdown(&self) { - let _ = self.commands.unbounded_send(SpircCommand::Shutdown); + let _ = self.commands.send(SpircCommand::Shutdown); } } impl SpircTask { async fn run(mut self) { while !self.session.is_invalid() && !self.shutdown { + let commands = self.commands.as_mut(); + let player_events = self.player_events.as_mut(); tokio::select! { frame = self.subscription.next() => match frame { Some(frame) => self.handle_frame(frame), @@ -358,10 +362,10 @@ impl SpircTask { break; } }, - cmd = self.commands.next(), if !self.commands.is_terminated() => if let Some(cmd) = cmd { + cmd = async { commands.unwrap().recv().await }, if commands.is_some() => if let Some(cmd) = cmd { self.handle_command(cmd); }, - event = self.player_events.next(), if !self.player_events.is_terminated() => if let Some(event) = event { + event = async { player_events.unwrap().recv().await }, if player_events.is_some() => if let Some(event) = event { self.handle_player_event(event) }, result = self.sender.flush(), if !self.sender.is_flushed() => if result.is_err() { @@ -508,7 +512,7 @@ impl SpircTask { SpircCommand::Shutdown => { CommandSender::new(self, MessageType::kMessageTypeGoodbye).send(); self.shutdown = true; - self.commands.close(); + self.commands.as_mut().map(|rx| rx.close()); } } } @@ -790,7 +794,7 @@ impl SpircTask { self.handle_play() } SpircPlayStatus::Playing { .. } | SpircPlayStatus::LoadingPlay { .. } => { - self.handle_play() + self.handle_pause() } _ => (), } diff --git a/core/Cargo.toml b/core/Cargo.toml index 25c0c654..373e3088 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.1.3" +version = "0.1.6" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" @@ -10,42 +10,45 @@ edition = "2018" [dependencies.librespot-protocol] path = "../protocol" -version = "0.1.3" +version = "0.1.6" [dependencies] aes = "0.6" base64 = "0.13" byteorder = "1.4" bytes = "1.0" -error-chain = { version = "0.12", default-features = false } -futures = { version = "0.3", features = ["bilock", "unstable"] } -hmac = "0.7" +cfg-if = "1" +futures-core = { version = "0.3", default-features = false } +futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "unstable", "sink"] } +hmac = "0.10" httparse = "1.3" -hyper = { version = "0.14", features = ["client", "tcp", "http1", "http2"] } +hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] } log = "0.4" num-bigint = "0.3" num-integer = "0.1" num-traits = "0.2" once_cell = "1.5.2" -pbkdf2 = "0.3" -pin-project-lite = "0.2.4" +pbkdf2 = { version = "0.7", default-features = false, features = ["hmac"] } protobuf = "~2.14.0" -rand = "0.7" -serde = "1.0" -serde_derive = "1.0" +rand = "0.8" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -sha-1 = "~0.8" +sha-1 = "0.9" shannon = "0.2.0" -tokio = { version = "1.0", features = ["io-util", "rt-multi-thread"] } +thiserror = "1" +tokio = { version = "1.0", features = ["io-util", "net", "rt", "sync"] } +tokio-stream = "0.1" tokio-util = { version = "0.6", features = ["codec"] } -tower-service = "0.3" -url = "1.7" -uuid = { version = "0.8", features = ["v4"] } +url = "2.1" [build-dependencies] -rand = "0.7" +rand = "0.8" vergen = "3.0.4" [dev-dependencies] env_logger = "*" -tokio = {version = "1.0", features = ["macros"] } \ No newline at end of file +tokio = {version = "1.0", features = ["macros"] } + +[features] +apresolve = ["hyper"] +apresolve-http2 = ["apresolve", "hyper/http2"] diff --git a/core/build.rs b/core/build.rs index e8c71e4a..0fc29335 100644 --- a/core/build.rs +++ b/core/build.rs @@ -1,6 +1,3 @@ -extern crate rand; -extern crate vergen; - use rand::distributions::Alphanumeric; use rand::Rng; use vergen::{generate_cargo_keys, ConstantsFlags}; @@ -10,10 +7,10 @@ fn main() { flags.toggle(ConstantsFlags::REBUILD_ON_HEAD_CHANGE); generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!"); - let mut rng = rand::thread_rng(); - let build_id: String = ::std::iter::repeat(()) - .map(|()| rng.sample(Alphanumeric)) + let build_id: String = rand::thread_rng() + .sample_iter(Alphanumeric) .take(8) + .map(char::from) .collect(); println!("cargo:rustc-env=VERGEN_BUILD_ID={}", build_id); } diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 81340c9d..2086d3b1 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,61 +1,73 @@ const AP_FALLBACK: &'static str = "ap.spotify.com:443"; -const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com:80"; -use hyper::{Body, Client, Method, Request, Uri}; -use std::error::Error; use url::Url; -use crate::proxytunnel::ProxyTunnel; +cfg_if! { + if #[cfg(feature = "apresolve")] { + const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com:80"; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct APResolveData { - ap_list: Vec, -} + use std::error::Error; -async fn apresolve(proxy: &Option, ap_port: &Option) -> Result> { - let port = ap_port.unwrap_or(443); + use hyper::{Body, Client, Method, Request, Uri}; + use serde::{Serialize, Deserialize}; - let req = Request::builder() - .method(Method::GET) - .uri( - APRESOLVE_ENDPOINT - .parse::() - .expect("invalid AP resolve URL"), - ) - .body(Body::empty())?; + use crate::proxytunnel::ProxyTunnel; - let response = if let Some(url) = proxy { - Client::builder() - .build(ProxyTunnel::new(url)?) - .request(req) - .await? - } else { - Client::new().request(req).await? - }; + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct APResolveData { + ap_list: Vec, + } - let body = hyper::body::to_bytes(response.into_body()).await?; - let data: APResolveData = serde_json::from_slice(body.as_ref())?; + async fn apresolve(proxy: &Option, ap_port: &Option) -> Result> { + let port = ap_port.unwrap_or(443); - let ap = if ap_port.is_some() || proxy.is_some() { - data.ap_list.into_iter().find_map(|ap| { - if ap.parse::().ok()?.port()? == port { - Some(ap) + let req = Request::builder() + .method(Method::GET) + .uri( + APRESOLVE_ENDPOINT + .parse::() + .expect("invalid AP resolve URL"), + ) + .body(Body::empty())?; + + let response = if let Some(url) = proxy { + Client::builder() + .build(ProxyTunnel::new(&url.socket_addrs(|| None)?[..])?) + .request(req) + .await? } else { - None + Client::new().request(req).await? + }; + + let body = hyper::body::to_bytes(response.into_body()).await?; + let data: APResolveData = serde_json::from_slice(body.as_ref())?; + + let ap = if ap_port.is_some() || proxy.is_some() { + data.ap_list.into_iter().find_map(|ap| { + if ap.parse::().ok()?.port()? == port { + Some(ap) + } else { + None + } + }) + } else { + data.ap_list.into_iter().next() } - }) + .ok_or("empty AP List")?; + + Ok(ap) + } + + pub async fn apresolve_or_fallback(proxy: &Option, ap_port: &Option) -> String { + apresolve(proxy, ap_port).await.unwrap_or_else(|e| { + warn!("Failed to resolve Access Point: {}", e); + warn!("Using fallback \"{}\"", AP_FALLBACK); + AP_FALLBACK.into() + }) + } } else { - data.ap_list.into_iter().next() + pub async fn apresolve_or_fallback(_: &Option, _: &Option) -> String { + AP_FALLBACK.to_string() + } } - .ok_or("empty AP List")?; - - Ok(ap) -} - -pub async fn apresolve_or_fallback(proxy: &Option, ap_port: &Option) -> String { - apresolve(proxy, ap_port).await.unwrap_or_else(|e| { - warn!("Failed to resolve Access Point: {}", e); - warn!("Using fallback \"{}\"", AP_FALLBACK); - AP_FALLBACK.into() - }) } diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index b9f0c232..3bce1c73 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -1,8 +1,8 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use bytes::Bytes; -use futures::channel::oneshot; use std::collections::HashMap; use std::io::Write; +use tokio::sync::oneshot; use crate::spotify_id::{FileId, SpotifyId}; use crate::util::SeqGenerator; diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 65fa33f5..544dda4c 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -1,14 +1,14 @@ +use std::io::{self, Read}; + use aes::Aes192; -use aes::NewBlockCipher; use byteorder::{BigEndian, ByteOrder}; use hmac::Hmac; use pbkdf2::pbkdf2; use protobuf::ProtobufEnum; +use serde::{Deserialize, Serialize}; use sha1::{Digest, Sha1}; -use std::io::{self, Read}; use crate::protocol::authentication::AuthenticationType; -use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Credentials { @@ -74,7 +74,7 @@ impl Credentials { let blob = { use aes::cipher::generic_array::typenum::Unsigned; use aes::cipher::generic_array::GenericArray; - use aes::cipher::BlockCipher; + use aes::cipher::{BlockCipher, NewBlockCipher}; let mut data = base64::decode(encrypted_blob).unwrap(); let cipher = Aes192::new(GenericArray::from_slice(&key)); @@ -142,61 +142,3 @@ where let v: String = serde::Deserialize::deserialize(de)?; base64::decode(&v).map_err(|e| serde::de::Error::custom(e.to_string())) } - -pub fn get_credentials String>( - username: Option, - password: Option, - cached_credentials: Option, - prompt: F, -) -> Option { - match (username, password, cached_credentials) { - (Some(username), Some(password), _) => Some(Credentials::with_password(username, password)), - - (Some(ref username), _, Some(ref credentials)) if *username == credentials.username => { - Some(credentials.clone()) - } - - (Some(username), None, _) => Some(Credentials::with_password( - username.clone(), - prompt(&username), - )), - - (None, _, Some(credentials)) => Some(credentials), - - (None, _, None) => None, - } -} - -error_chain! { - types { - AuthenticationError, AuthenticationErrorKind, AuthenticationResultExt, AuthenticationResult; - } - - foreign_links { - Io(::std::io::Error); - } - - errors { - BadCredentials { - description("Bad credentials") - display("Authentication failed with error: Bad credentials") - } - PremiumAccountRequired { - description("Premium account required") - display("Authentication failed with error: Premium account required") - } - } -} - -impl From for AuthenticationError { - fn from(login_failure: APLoginFailed) -> Self { - let error_code = login_failure.get_error_code(); - match error_code { - ErrorCode::BadCredentials => Self::from_kind(AuthenticationErrorKind::BadCredentials), - ErrorCode::PremiumAccountRequired => { - Self::from_kind(AuthenticationErrorKind::PremiumAccountRequired) - } - _ => format!("Authentication failed with error: {:?}", error_code).into(), - } - } -} diff --git a/core/src/channel.rs b/core/src/channel.rs index 7ada05d5..54eee184 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -1,12 +1,14 @@ +use std::collections::HashMap; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Instant; + use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; -use futures::{channel::mpsc, lock::BiLock, Stream, StreamExt}; -use std::{ - collections::HashMap, - pin::Pin, - task::{Context, Poll}, - time::Instant, -}; +use futures_core::Stream; +use futures_util::lock::BiLock; +use futures_util::StreamExt; +use tokio::sync::mpsc; use crate::util::SeqGenerator; @@ -46,7 +48,7 @@ enum ChannelState { impl ChannelManager { pub fn allocate(&self) -> (u16, Channel) { - let (tx, rx) = mpsc::unbounded(); + let (tx, rx) = mpsc::unbounded_channel(); let seq = self.lock(|inner| { let seq = inner.sequence.get(); @@ -85,7 +87,7 @@ impl ChannelManager { inner.download_measurement_bytes += data.len(); if let Entry::Occupied(entry) = inner.channels.entry(id) { - let _ = entry.get().unbounded_send((cmd, data)); + let _ = entry.get().send((cmd, data)); } }); } @@ -105,7 +107,7 @@ impl ChannelManager { impl Channel { fn recv_packet(&mut self, cx: &mut Context<'_>) -> Poll> { - let (cmd, packet) = match self.receiver.poll_next_unpin(cx) { + let (cmd, packet) = match self.receiver.poll_recv(cx) { Poll::Pending => return Poll::Pending, Poll::Ready(o) => o.ok_or(ChannelError)?, }; diff --git a/core/src/config.rs b/core/src/config.rs index 60cb66e0..469b935a 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -1,9 +1,6 @@ use std::fmt; use std::str::FromStr; use url::Url; -use uuid::Uuid; - -use crate::version; #[derive(Clone, Debug)] pub struct SessionConfig { @@ -13,18 +10,6 @@ pub struct SessionConfig { pub ap_port: Option, } -impl Default for SessionConfig { - fn default() -> SessionConfig { - let device_id = Uuid::new_v4().to_hyphenated().to_string(); - SessionConfig { - user_agent: version::version_string(), - device_id: device_id, - proxy: None, - ap_port: None, - } - } -} - #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum DeviceType { Unknown = 0, diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 3810fc96..02d77134 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -1,5 +1,5 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; -use hmac::{Hmac, Mac}; +use hmac::{Hmac, Mac, NewMac}; use protobuf::{self, Message}; use rand::thread_rng; use sha1::Sha1; @@ -122,16 +122,16 @@ fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec, Vec, Vec< let mut data = Vec::with_capacity(0x64); for i in 1..6 { let mut mac = HmacSha1::new_varkey(&shared_secret).expect("HMAC can take key of any size"); - mac.input(packets); - mac.input(&[i]); - data.extend_from_slice(&mac.result().code()); + mac.update(packets); + mac.update(&[i]); + data.extend_from_slice(&mac.finalize().into_bytes()); } let mut mac = HmacSha1::new_varkey(&data[..0x14]).expect("HMAC can take key of any size"); - mac.input(packets); + mac.update(packets); ( - mac.result().code().to_vec(), + mac.finalize().into_bytes().to_vec(), data[0x14..0x34].to_vec(), data[0x34..0x54].to_vec(), ) diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 68e2e7a5..b715d357 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -4,21 +4,60 @@ mod handshake; pub use self::codec::APCodec; pub use self::handshake::handshake; -use futures::{SinkExt, StreamExt}; -use protobuf::{self, Message}; -use std::io; +use std::io::{self, ErrorKind}; use std::net::ToSocketAddrs; + +use futures_util::{SinkExt, StreamExt}; +use protobuf::{self, Message, ProtobufError}; +use thiserror::Error; use tokio::net::TcpStream; use tokio_util::codec::Framed; use url::Url; -use crate::authentication::{AuthenticationError, Credentials}; +use crate::authentication::Credentials; +use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; +use crate::proxytunnel; use crate::version; -use crate::proxytunnel; - pub type Transport = Framed; +fn login_error_message(code: &ErrorCode) -> &'static str { + pub use ErrorCode::*; + match code { + ProtocolError => "Protocol error", + TryAnotherAP => "Try another AP", + BadConnectionId => "Bad connection id", + TravelRestriction => "Travel restriction", + PremiumAccountRequired => "Premium account required", + BadCredentials => "Bad credentials", + CouldNotValidateCredentials => "Could not validate credentials", + AccountExists => "Account exists", + ExtraVerificationRequired => "Extra verification required", + InvalidAppKey => "Invalid app key", + ApplicationBanned => "Application banned", + } +} + +#[derive(Debug, Error)] +pub enum AuthenticationError { + #[error("Login failed with reason: {}", login_error_message(.0))] + LoginFailed(ErrorCode), + #[error("Authentication failed: {0}")] + IoError(#[from] io::Error), +} + +impl From for AuthenticationError { + fn from(e: ProtobufError) -> Self { + io::Error::new(ErrorKind::InvalidData, e).into() + } +} + +impl From for AuthenticationError { + fn from(login_failure: APLoginFailed) -> Self { + Self::LoginFailed(login_failure.get_error_code()) + } +} + pub async fn connect(addr: String, proxy: &Option) -> io::Result { let socket = if let Some(proxy) = proxy { info!("Using proxy \"{}\"", proxy); @@ -37,8 +76,8 @@ pub async fn connect(addr: String, proxy: &Option) -> io::Result .next() .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Missing port"))?; - let socket_addr = proxy.to_socket_addrs().and_then(|mut iter| { - iter.next().ok_or_else(|| { + let socket_addr = proxy.socket_addrs(|| None).and_then(|addrs| { + addrs.into_iter().next().ok_or_else(|| { io::Error::new( io::ErrorKind::NotFound, "Can't resolve proxy server address", @@ -66,7 +105,6 @@ pub async fn authenticate( device_id: &str, ) -> Result { use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; - use crate::protocol::keyexchange::APLoginFailed; let mut packet = ClientResponseEncrypted::new(); packet @@ -101,7 +139,7 @@ pub async fn authenticate( let (cmd, data) = transport.next().await.expect("EOF")?; match cmd { 0xac => { - let welcome_data: APWelcome = protobuf::parse_from_bytes(data.as_ref()).unwrap(); + let welcome_data: APWelcome = protobuf::parse_from_bytes(data.as_ref())?; let reusable_credentials = Credentials { username: welcome_data.get_canonical_username().to_owned(), @@ -111,12 +149,13 @@ pub async fn authenticate( Ok(reusable_credentials) } - 0xad => { - let error_data: APLoginFailed = protobuf::parse_from_bytes(data.as_ref()).unwrap(); + let error_data: APLoginFailed = protobuf::parse_from_bytes(data.as_ref())?; Err(error_data.into()) } - - _ => panic!("Unexpected packet {:?}", cmd), + _ => { + let msg = format!("Received invalid packet: {}", cmd); + Err(io::Error::new(ErrorKind::InvalidData, msg).into()) + } } } diff --git a/core/src/keymaster.rs b/core/src/keymaster.rs index 87b3f1e3..8c3c00a2 100644 --- a/core/src/keymaster.rs +++ b/core/src/keymaster.rs @@ -1,3 +1,5 @@ +use serde::Deserialize; + use crate::{mercury::MercuryError, session::Session}; #[derive(Deserialize, Debug, Clone)] diff --git a/core/src/lib.rs b/core/src/lib.rs index 25ce5413..4ebe8581 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -3,48 +3,20 @@ #[macro_use] extern crate log; #[macro_use] -extern crate serde_derive; -#[macro_use] -extern crate pin_project_lite; -#[macro_use] -extern crate error_chain; -extern crate aes; -extern crate base64; -extern crate byteorder; -extern crate bytes; -extern crate futures; -extern crate hmac; -extern crate httparse; -extern crate hyper; -extern crate num_bigint; -extern crate num_integer; -extern crate num_traits; -extern crate once_cell; -extern crate pbkdf2; -extern crate protobuf; -extern crate rand; -extern crate serde; -extern crate serde_json; -extern crate sha1; -extern crate shannon; -pub extern crate tokio; -extern crate tokio_util; -extern crate tower_service; -extern crate url; -extern crate uuid; +extern crate cfg_if; -extern crate librespot_protocol as protocol; +use librespot_protocol as protocol; #[macro_use] mod component; -pub mod apresolve; +mod apresolve; pub mod audio_key; pub mod authentication; pub mod cache; pub mod channel; pub mod config; -pub mod connection; +mod connection; pub mod diffie_hellman; pub mod keymaster; pub mod mercury; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 4baa674f..537ff2cb 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -1,14 +1,17 @@ +use std::collections::HashMap; +use std::future::Future; +use std::mem; +use std::pin::Pin; +use std::task::Context; +use std::task::Poll; + +use byteorder::{BigEndian, ByteOrder}; +use bytes::Bytes; +use tokio::sync::{mpsc, oneshot}; + use crate::protocol; use crate::util::url_encode; use crate::util::SeqGenerator; -use byteorder::{BigEndian, ByteOrder}; -use bytes::Bytes; -use futures::{ - channel::{mpsc, oneshot}, - Future, -}; -use std::{collections::HashMap, task::Poll}; -use std::{mem, pin::Pin, task::Context}; mod types; pub use self::types::*; @@ -31,18 +34,15 @@ pub struct MercuryPending { callback: Option>>, } -pin_project! { - pub struct MercuryFuture { - #[pin] - receiver: oneshot::Receiver> - } +pub struct MercuryFuture { + receiver: oneshot::Receiver>, } impl Future for MercuryFuture { type Output = Result; - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match self.project().receiver.poll(cx) { + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match Pin::new(&mut self.receiver).poll(cx) { Poll::Ready(Ok(x)) => Poll::Ready(x), Poll::Ready(Err(_)) => Poll::Ready(Err(MercuryError)), Poll::Pending => Poll::Pending, @@ -119,7 +119,7 @@ impl MercuryManager { async move { let response = request.await?; - let (tx, rx) = mpsc::unbounded(); + let (tx, rx) = mpsc::unbounded_channel(); manager.lock(move |inner| { if !inner.invalid { @@ -221,7 +221,7 @@ impl MercuryManager { // if send fails, remove from list of subs // TODO: send unsub message - sub.unbounded_send(response.clone()).is_ok() + sub.send(response.clone()).is_ok() } else { // URI doesn't match true diff --git a/core/src/proxytunnel.rs b/core/src/proxytunnel.rs index c2033c85..158d314f 100644 --- a/core/src/proxytunnel.rs +++ b/core/src/proxytunnel.rs @@ -1,16 +1,6 @@ -use futures::Future; -use hyper::Uri; -use std::{ - io, - net::{SocketAddr, ToSocketAddrs}, - pin::Pin, - task::Poll, -}; -use tokio::{ - io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, - net::TcpStream, -}; -use tower_service::Service; +use std::io; + +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; pub async fn connect( mut proxy_connection: T, @@ -64,43 +54,56 @@ pub async fn connect( } } -#[derive(Clone)] -pub struct ProxyTunnel { - proxy_addr: SocketAddr, -} +cfg_if! { + if #[cfg(feature = "apresolve")] { + use std::future::Future; + use std::net::{SocketAddr, ToSocketAddrs}; + use std::pin::Pin; + use std::task::Poll; -impl ProxyTunnel { - pub fn new(addr: T) -> io::Result { - let addr = addr.to_socket_addrs()?.next().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, "No socket address given") - })?; - Ok(Self { proxy_addr: addr }) - } -} - -impl Service for ProxyTunnel { - type Response = TcpStream; - type Error = io::Error; - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, _: &mut std::task::Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, url: Uri) -> Self::Future { - let proxy_addr = self.proxy_addr; - let fut = async move { - let host = url - .host() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Host is missing"))?; - let port = url - .port() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Port is missing"))?; - - let conn = TcpStream::connect(proxy_addr).await?; - connect(conn, host, port.as_u16()).await - }; - - Box::pin(fut) + use hyper::service::Service; + use hyper::Uri; + use tokio::net::TcpStream; + + #[derive(Clone)] + pub struct ProxyTunnel { + proxy_addr: SocketAddr, + } + + impl ProxyTunnel { + pub fn new(addr: T) -> io::Result { + let addr = addr.to_socket_addrs()?.next().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "No socket address given") + })?; + Ok(Self { proxy_addr: addr }) + } + } + + impl Service for ProxyTunnel { + type Response = TcpStream; + type Error = io::Error; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, _: &mut std::task::Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, url: Uri) -> Self::Future { + let proxy_addr = self.proxy_addr; + let fut = async move { + let host = url + .host() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Host is missing"))?; + let port = url + .port() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Port is missing"))?; + + let conn = TcpStream::connect(proxy_addr).await?; + connect(conn, host, port.as_u16()).await + }; + + Box::pin(fut) + } + } } } diff --git a/core/src/session.rs b/core/src/session.rs index b0eca0c0..9eaff3ed 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -1,14 +1,20 @@ +use std::future::Future; +use std::io; +use std::pin::Pin; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, RwLock, Weak}; +use std::task::Context; use std::task::Poll; use std::time::{SystemTime, UNIX_EPOCH}; -use std::{io, pin::Pin, task::Context}; - -use once_cell::sync::OnceCell; use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; -use futures::{channel::mpsc, Future, FutureExt, StreamExt, TryStream, TryStreamExt}; +use futures_core::TryStream; +use futures_util::{FutureExt, StreamExt, TryStreamExt}; +use once_cell::sync::OnceCell; +use thiserror::Error; +use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; use crate::apresolve::apresolve_or_fallback; use crate::audio_key::AudioKeyManager; @@ -16,10 +22,16 @@ use crate::authentication::Credentials; use crate::cache::Cache; use crate::channel::ChannelManager; use crate::config::SessionConfig; -use crate::connection; +use crate::connection::{self, AuthenticationError}; use crate::mercury::MercuryManager; -pub use crate::authentication::{AuthenticationError, AuthenticationErrorKind}; +#[derive(Debug, Error)] +pub enum SessionError { + #[error(transparent)] + AuthenticationError(#[from] AuthenticationError), + #[error("Cannot create session: {0}")] + IoError(#[from] io::Error), +} struct SessionData { country: String, @@ -54,7 +66,7 @@ impl Session { config: SessionConfig, credentials: Credentials, cache: Option, - ) -> Result { + ) -> Result { let ap = apresolve_or_fallback(&config.proxy, &config.ap_port).await; info!("Connecting to AP \"{}\"", ap); @@ -87,7 +99,7 @@ impl Session { ) -> Session { let (sink, stream) = transport.split(); - let (sender_tx, sender_rx) = mpsc::unbounded(); + let (sender_tx, sender_rx) = mpsc::unbounded_channel(); let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); debug!("new Session[{}]", session_id); @@ -114,11 +126,13 @@ impl Session { session_id: session_id, })); - let sender_task = sender_rx.map(Ok::<_, io::Error>).forward(sink); + let sender_task = UnboundedReceiverStream::new(sender_rx) + .map(Ok) + .forward(sink); let receiver_task = DispatchTask(stream, session.weak()); let task = - futures::future::join(sender_task, receiver_task).map(|_| io::Result::<_>::Ok(())); + futures_util::future::join(sender_task, receiver_task).map(|_| io::Result::<_>::Ok(())); tokio::spawn(task); session } @@ -193,7 +207,7 @@ impl Session { } pub fn send_packet(&self, cmd: u8, data: Vec) { - self.0.tx_connection.unbounded_send((cmd, data)).unwrap(); + self.0.tx_connection.send((cmd, data)).unwrap(); } pub fn cache(&self) -> Option<&Arc> { diff --git a/core/src/util/mod.rs b/core/src/util.rs similarity index 100% rename from core/src/util/mod.rs rename to core/src/util.rs diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index f5e61237..f3087b8a 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.1.3" +version = "0.1.6" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" @@ -10,14 +10,12 @@ edition = "2018" [dependencies] async-trait = "0.1" byteorder = "1.3" -futures = "0.3" -linear-map = "1.2" protobuf = "~2.14.0" log = "0.4" [dependencies.librespot-core] path = "../core" -version = "0.1.3" +version = "0.1.6" [dependencies.librespot-protocol] path = "../protocol" -version = "0.1.3" +version = "0.1.6" diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index f71bae95..8faa027e 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -1,26 +1,20 @@ #![allow(clippy::unused_io_amount)] #![allow(clippy::redundant_field_names)] + #[macro_use] extern crate log; #[macro_use] extern crate async_trait; -extern crate byteorder; -extern crate futures; -extern crate linear_map; -extern crate protobuf; - -extern crate librespot_core; -extern crate librespot_protocol as protocol; - pub mod cover; -use linear_map::LinearMap; +use std::collections::HashMap; use librespot_core::mercury::MercuryError; use librespot_core::session::Session; use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId}; +use librespot_protocol as protocol; pub use crate::protocol::metadata::AudioFile_Format as FileFormat; @@ -64,7 +58,7 @@ where pub struct AudioItem { pub id: SpotifyId, pub uri: String, - pub files: LinearMap, + pub files: HashMap, pub name: String, pub duration: i32, pub available: bool, @@ -143,7 +137,7 @@ pub struct Track { pub duration: i32, pub album: SpotifyId, pub artists: Vec, - pub files: LinearMap, + pub files: HashMap, pub alternatives: Vec, pub available: bool, } @@ -165,7 +159,7 @@ pub struct Episode { pub duration: i32, pub language: String, pub show: SpotifyId, - pub files: LinearMap, + pub files: HashMap, pub covers: Vec, pub available: bool, pub explicit: bool, diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 15622198..03675de8 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.1.3" +version = "0.1.6" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" @@ -9,19 +9,21 @@ edition = "2018" [dependencies.librespot-audio] path = "../audio" -version = "0.1.3" +version = "0.1.6" [dependencies.librespot-core] path = "../core" -version = "0.1.3" +version = "0.1.6" [dependencies.librespot-metadata] path = "../metadata" -version = "0.1.3" +version = "0.1.6" [dependencies] -futures = "0.3" +futures-executor = "0.3" +futures-util = { version = "0.3", default_features = false, features = ["alloc"] } log = "0.4" byteorder = "1.4" shell-words = "1.0.0" +tokio = { version = "1", features = ["sync"] } alsa = { version = "0.4", optional = true } portaudio-rs = { version = "0.3", optional = true } @@ -46,5 +48,6 @@ portaudio-backend = ["portaudio-rs"] pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] jackaudio-backend = ["jack"] rodio-backend = ["rodio", "cpal", "thiserror"] +rodiojack-backend = ["rodio", "cpal/jack", "thiserror"] sdl-backend = ["sdl2"] gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"] diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index ae76f057..bf7b1376 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,4 +1,5 @@ use super::{Open, Sink}; +use crate::audio::AudioPacket; use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, Error, ValueOr}; @@ -124,8 +125,9 @@ impl Sink for AlsaSink { Ok(()) } - fn write(&mut self, data: &[i16]) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { let mut processed_data = 0; + let data = packet.samples(); while processed_data < data.len() { let data_to_buffer = min( self.buffer.capacity() - self.buffer.len(), diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index d902cd3e..6be6dd72 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,4 +1,5 @@ use super::{Open, Sink}; +use crate::audio::AudioPacket; use gst::prelude::*; use gst::*; use std::sync::mpsc::{sync_channel, SyncSender}; @@ -104,9 +105,9 @@ impl Sink for GstreamerSink { fn stop(&mut self) -> io::Result<()> { Ok(()) } - fn write(&mut self, data: &[i16]) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { // Copy expensively (in to_vec()) to avoid thread synchronization - let deighta: &[u8] = data.as_bytes(); + let deighta: &[u8] = packet.samples().as_bytes(); self.tx .send(deighta.to_vec()) .expect("tx send failed in write function"); diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 792e7e3b..4699c182 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,4 +1,5 @@ use super::{Open, Sink}; +use crate::audio::AudioPacket; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; @@ -73,8 +74,8 @@ impl Sink for JackSink { Ok(()) } - fn write(&mut self, data: &[i16]) -> io::Result<()> { - for s in data.iter() { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + for s in packet.samples().iter() { let res = self.send.send(*s); if res.is_err() { error!("jackaudio: cannot write to channel"); diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 21ee3c05..214ede8c 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,3 +1,4 @@ +use crate::audio::AudioPacket; use std::io; pub trait Open { @@ -7,7 +8,7 @@ pub trait Open { pub trait Sink { fn start(&mut self) -> io::Result<()>; fn stop(&mut self) -> io::Result<()>; - fn write(&mut self, data: &[i16]) -> io::Result<()>; + fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; } pub type SinkBuilder = fn(Option) -> Box; @@ -41,10 +42,9 @@ mod gstreamer; #[cfg(feature = "gstreamer-backend")] use self::gstreamer::GstreamerSink; -#[cfg(feature = "rodio-backend")] +#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] mod rodio; -#[cfg(feature = "rodio-backend")] -use self::rodio::RodioSink; + #[cfg(feature = "sdl-backend")] mod sdl; #[cfg(feature = "sdl-backend")] @@ -68,7 +68,9 @@ pub const BACKENDS: &'static [(&'static str, SinkBuilder)] = &[ #[cfg(feature = "gstreamer-backend")] ("gstreamer", mk_sink::), #[cfg(feature = "rodio-backend")] - ("rodio", mk_sink::), + ("rodio", rodio::mk_rodio), + #[cfg(feature = "rodiojack-backend")] + ("rodiojack", rodio::mk_rodiojack), #[cfg(feature = "sdl-backend")] ("sdl", mk_sink::), ("pipe", mk_sink::), diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 02b8faf5..9fcd09ff 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,4 +1,5 @@ use super::{Open, Sink}; +use crate::audio::AudioPacket; use std::fs::OpenOptions; use std::io::{self, Write}; use std::mem; @@ -26,12 +27,15 @@ impl Sink for StdoutSink { Ok(()) } - fn write(&mut self, data: &[i16]) -> io::Result<()> { - let data: &[u8] = unsafe { - slice::from_raw_parts( - data.as_ptr() as *const u8, - data.len() * mem::size_of::(), - ) + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + let data: &[u8] = match packet { + AudioPacket::Samples(data) => unsafe { + slice::from_raw_parts( + data.as_ptr() as *const u8, + data.len() * mem::size_of::(), + ) + }, + AudioPacket::OggData(data) => data, }; self.0.write_all(data)?; diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 31397bfb..0e25021e 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,4 +1,5 @@ use super::{Open, Sink}; +use crate::audio::AudioPacket; use portaudio_rs; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; @@ -95,8 +96,8 @@ impl<'a> Sink for PortAudioSink<'a> { self.0 = None; Ok(()) } - fn write(&mut self, data: &[i16]) -> io::Result<()> { - match self.0.as_mut().unwrap().write(data) { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + match self.0.as_mut().unwrap().write(packet.samples()) { Ok(_) => (), Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"), Err(e) => panic!("PA Error {}", e), diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 6c8d7211..11ea026a 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,4 +1,5 @@ use super::{Open, Sink}; +use crate::audio::AudioPacket; use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_simple_binding::Simple; use std::io; @@ -65,13 +66,17 @@ impl Sink for PulseAudioSink { Ok(()) } - fn write(&mut self, data: &[i16]) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { if let Some(s) = &self.s { // SAFETY: An i16 consists of two bytes, so that the given slice can be interpreted // as a byte array of double length. Each byte pointer is validly aligned, and so // is the newly created slice. - let d: &[u8] = - unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 2) }; + let d: &[u8] = unsafe { + std::slice::from_raw_parts( + packet.samples().as_ptr() as *const u8, + packet.samples().len() * 2, + ) + }; match s.write(d) { Ok(_) => Ok(()), diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 034bd086..56e19b61 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -5,7 +5,27 @@ use std::{io, thread, time}; use cpal::traits::{DeviceTrait, HostTrait}; use thiserror::Error; -use super::{Open, Sink}; +use super::Sink; +use crate::audio::AudioPacket; + +#[cfg(all( + feature = "rodiojack-backend", + not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")) +))] +compile_error!("Rodio JACK backend is currently only supported on linux."); + +#[cfg(feature = "rodio-backend")] +pub fn mk_rodio(device: Option) -> Box { + Box::new(open(cpal::default_host(), device)) +} + +#[cfg(feature = "rodiojack-backend")] +pub fn mk_rodiojack(device: Option) -> Box { + Box::new(open( + cpal::host_from_id(cpal::HostId::Jack).unwrap(), + device, + )) +} #[derive(Debug, Error)] pub enum RodioError { @@ -59,10 +79,10 @@ fn list_formats(device: &rodio::Device) { } } -fn list_outputs() -> Result<(), cpal::DevicesError> { +fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> { let mut default_device_name = None; - if let Some(default_device) = get_default_device() { + if let Some(default_device) = host.default_output_device() { default_device_name = default_device.name().ok(); println!( "Default Audio Device:\n {}", @@ -76,7 +96,7 @@ fn list_outputs() -> Result<(), cpal::DevicesError> { warn!("No default device was found"); } - for device in cpal::default_host().output_devices()? { + for device in host.output_devices()? { match device.name() { Ok(name) if Some(&name) == default_device_name.as_ref() => (), Ok(name) => { @@ -94,14 +114,13 @@ fn list_outputs() -> Result<(), cpal::DevicesError> { Ok(()) } -fn get_default_device() -> Option { - cpal::default_host().default_output_device() -} - -fn create_sink(device: Option) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> { +fn create_sink( + host: &cpal::Host, + device: Option, +) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> { let rodio_device = match device { Some(ask) if &ask == "?" => { - let exit_code = match list_outputs() { + let exit_code = match list_outputs(host) { Ok(()) => 0, Err(e) => { error!("{}", e); @@ -111,12 +130,13 @@ fn create_sink(device: Option) -> Result<(rodio::Sink, rodio::OutputStre exit(exit_code) } Some(device_name) => { - cpal::default_host() - .output_devices()? + host.output_devices()? .find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails .ok_or(RodioError::DeviceNotAvailable(device_name))? } - None => get_default_device().ok_or(RodioError::NoDeviceAvailable)?, + None => host + .default_output_device() + .ok_or(RodioError::NoDeviceAvailable)?, }; let name = rodio_device.name().ok(); @@ -130,37 +150,32 @@ fn create_sink(device: Option) -> Result<(rodio::Sink, rodio::OutputStre Ok((sink, stream)) } -impl Open for RodioSink { - fn open(device: Option) -> RodioSink { - debug!( - "Using rodio sink with cpal host: {:?}", - cpal::default_host().id().name() - ); +pub fn open(host: cpal::Host, device: Option) -> RodioSink { + debug!("Using rodio sink with cpal host: {}", host.id().name()); - let (sink_tx, sink_rx) = mpsc::sync_channel(1); - let (close_tx, close_rx) = mpsc::sync_channel(1); + let (sink_tx, sink_rx) = mpsc::sync_channel(1); + let (close_tx, close_rx) = mpsc::sync_channel(1); - std::thread::spawn(move || match create_sink(device) { - Ok((sink, stream)) => { - sink_tx.send(Ok(sink)).unwrap(); + std::thread::spawn(move || match create_sink(&host, device) { + Ok((sink, stream)) => { + sink_tx.send(Ok(sink)).unwrap(); - close_rx.recv().unwrap_err(); // This will fail as soon as the sender is dropped - debug!("drop rodio::OutputStream"); - drop(stream); - } - Err(e) => { - sink_tx.send(Err(e)).unwrap(); - } - }); - - // Instead of the second `unwrap`, better error handling could be introduced - let sink = sink_rx.recv().unwrap().unwrap(); - - debug!("Rodio sink was created"); - RodioSink { - rodio_sink: sink, - _close_tx: close_tx, + close_rx.recv().unwrap_err(); // This will fail as soon as the sender is dropped + debug!("drop rodio::OutputStream"); + drop(stream); } + Err(e) => { + sink_tx.send(Err(e)).unwrap(); + } + }); + + // Instead of the second `unwrap`, better error handling could be introduced + let sink = sink_rx.recv().unwrap().unwrap(); + + debug!("Rodio sink was created"); + RodioSink { + rodio_sink: sink, + _close_tx: close_tx, } } @@ -178,8 +193,8 @@ impl Sink for RodioSink { Ok(()) } - fn write(&mut self, data: &[i16]) -> io::Result<()> { - let source = rodio::buffer::SamplesBuffer::new(2, 44100, data); + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + let source = rodio::buffer::SamplesBuffer::new(2, 44100, packet.samples()); self.rodio_sink.append(source); // Chunk sizes seem to be about 256 to 3000 ish items long. diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 71d19e50..27d650f9 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,4 +1,5 @@ use super::{Open, Sink}; +use crate::audio::AudioPacket; use sdl2::audio::{AudioQueue, AudioSpecDesired}; use std::{io, thread, time}; @@ -45,12 +46,12 @@ impl Sink for SdlSink { Ok(()) } - fn write(&mut self, data: &[i16]) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { while self.queue.size() > (2 * 2 * 44_100) { // sleep and wait for sdl thread to drain the queue a bit thread::sleep(time::Duration::from_millis(10)); } - self.queue.queue(data); + self.queue.queue(packet.samples()); Ok(()) } } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 2af88360..0dd25638 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,4 +1,5 @@ use super::{Open, Sink}; +use crate::audio::AudioPacket; use shell_words::split; use std::io::{self, Write}; use std::mem; @@ -43,11 +44,11 @@ impl Sink for SubprocessSink { Ok(()) } - fn write(&mut self, data: &[i16]) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { let data: &[u8] = unsafe { slice::from_raw_parts( - data.as_ptr() as *const u8, - data.len() * mem::size_of::(), + packet.samples().as_ptr() as *const u8, + packet.samples().len() * mem::size_of::(), ) }; if let Some(child) = &mut self.child { diff --git a/playback/src/config.rs b/playback/src/config.rs index 0a9bb47d..31f63626 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -55,6 +55,7 @@ pub struct PlayerConfig { pub normalisation_type: NormalisationType, pub normalisation_pregain: f32, pub gapless: bool, + pub passthrough: bool, } impl Default for PlayerConfig { @@ -65,6 +66,7 @@ impl Default for PlayerConfig { normalisation_type: NormalisationType::default(), normalisation_pregain: 0.0, gapless: true, + passthrough: false, } } } diff --git a/playback/src/lib.rs b/playback/src/lib.rs index f4606430..9613f2e5 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -1,39 +1,9 @@ #[macro_use] extern crate log; -extern crate byteorder; -extern crate futures; -extern crate shell_words; - -#[cfg(feature = "alsa-backend")] -extern crate alsa; - -#[cfg(feature = "portaudio-backend")] -extern crate portaudio_rs; - -#[cfg(feature = "pulseaudio-backend")] -extern crate libpulse_binding; -#[cfg(feature = "pulseaudio-backend")] -extern crate libpulse_simple_binding; - -#[cfg(feature = "jackaudio-backend")] -extern crate jack; - -#[cfg(feature = "gstreamer-backend")] -extern crate glib; -#[cfg(feature = "gstreamer-backend")] -extern crate gstreamer as gst; -#[cfg(feature = "gstreamer-backend")] -extern crate gstreamer_app as gst_app; -#[cfg(feature = "gstreamer-backend")] -extern crate zerocopy; - -#[cfg(feature = "sdl-backend")] -extern crate sdl2; - -extern crate librespot_audio as audio; -extern crate librespot_core; -extern crate librespot_metadata as metadata; +use librespot_audio as audio; +use librespot_core as core; +use librespot_metadata as metadata; pub mod audio_backend; pub mod config; diff --git a/playback/src/player.rs b/playback/src/player.rs index 3ee5c989..9b2c7125 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,5 +1,18 @@ +use std::cmp::max; +use std::future::Future; +use std::io::{self, Read, Seek, SeekFrom}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::{Duration, Instant}; +use std::{mem, thread}; + +use byteorder::{LittleEndian, ReadBytesExt}; +use futures_util::stream::futures_unordered::FuturesUnordered; +use futures_util::{future, StreamExt, TryFutureExt}; +use tokio::sync::{mpsc, oneshot}; + +use crate::audio::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder}; use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController}; -use crate::audio::{VorbisDecoder, VorbisPacket}; use crate::audio::{ READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, @@ -7,23 +20,11 @@ use crate::audio::{ use crate::audio_backend::Sink; use crate::config::NormalisationType; use crate::config::{Bitrate, PlayerConfig}; +use crate::core::session::Session; +use crate::core::spotify_id::SpotifyId; +use crate::core::util::SeqGenerator; use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; -use librespot_core::util::SeqGenerator; - -use byteorder::{LittleEndian, ReadBytesExt}; -use futures::channel::{mpsc, oneshot}; -use futures::{future, Future, Stream, StreamExt, TryFutureExt}; -use std::borrow::Cow; - -use std::cmp::max; -use std::io::{self, Read, Seek, SeekFrom}; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::time::{Duration, Instant}; -use std::{mem, thread}; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; @@ -244,8 +245,8 @@ impl Player { where F: FnOnce() -> Box + Send + 'static, { - let (cmd_tx, cmd_rx) = mpsc::unbounded(); - let (event_sender, event_receiver) = mpsc::unbounded(); + let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); + let (event_sender, event_receiver) = mpsc::unbounded_channel(); let handle = thread::spawn(move || { debug!("new Player[{}]", session.session_id()); @@ -265,8 +266,8 @@ impl Player { }; // While PlayerInternal is written as a future, it still contains blocking code. - // It must be run by using wait() in a dedicated thread. - futures::executor::block_on(internal); + // It must be run by using block_on() in a dedicated thread. + futures_executor::block_on(internal); debug!("PlayerInternal thread finished."); }); @@ -281,7 +282,7 @@ impl Player { } fn command(&self, cmd: PlayerCommand) { - self.commands.as_ref().unwrap().unbounded_send(cmd).unwrap(); + self.commands.as_ref().unwrap().send(cmd).unwrap(); } pub fn load(&mut self, track_id: SpotifyId, start_playing: bool, position_ms: u32) -> u64 { @@ -317,14 +318,14 @@ impl Player { } pub fn get_player_event_channel(&self) -> PlayerEventChannel { - let (event_sender, event_receiver) = mpsc::unbounded(); + let (event_sender, event_receiver) = mpsc::unbounded_channel(); self.command(PlayerCommand::AddEventSender(event_sender)); event_receiver } - pub async fn get_end_of_track_future(&self) { + pub async fn await_end_of_track(&self) { let mut channel = self.get_player_event_channel(); - while let Some(event) = channel.next().await { + while let Some(event) = channel.recv().await { if matches!( event, PlayerEvent::EndOfTrack { .. } | PlayerEvent::Stopped { .. } @@ -377,7 +378,7 @@ enum PlayerPreload { }, } -type Decoder = VorbisDecoder>>; +type Decoder = Box; enum PlayerState { Stopped, @@ -575,21 +576,20 @@ struct PlayerTrackLoader { } impl PlayerTrackLoader { - async fn find_available_alternative<'a, 'b>( - &'a self, - audio: &'b AudioItem, - ) -> Option> { + async fn find_available_alternative(&self, audio: AudioItem) -> Option { if audio.available { - Some(Cow::Borrowed(audio)) + Some(audio) } else if let Some(alternatives) = &audio.alternatives { - let alternatives = alternatives + let alternatives: FuturesUnordered<_> = alternatives .iter() - .map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id)); - let alternatives = future::try_join_all(alternatives).await.unwrap(); + .map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id)) + .collect(); + alternatives - .into_iter() - .find(|alt| alt.available) - .map(Cow::Owned) + .filter_map(|x| future::ready(x.ok())) + .filter(|x| future::ready(x.available)) + .next() + .await } else { None } @@ -629,10 +629,10 @@ impl PlayerTrackLoader { info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri); - let audio = match self.find_available_alternative(&audio).await { + let audio = match self.find_available_alternative(audio).await { Some(audio) => audio, None => { - warn!("<{}> is not available", audio.uri); + warn!("<{}> is not available", spotify_id.to_uri()); return None; } }; @@ -730,7 +730,19 @@ impl PlayerTrackLoader { let audio_file = Subfile::new(decrypted_file, 0xa7); - let mut decoder = match VorbisDecoder::new(audio_file) { + let result = if self.config.passthrough { + match PassthroughDecoder::new(audio_file) { + Ok(result) => Ok(Box::new(result) as Decoder), + Err(e) => Err(AudioError::PassthroughError(e)), + } + } else { + match VorbisDecoder::new(audio_file) { + Ok(result) => Ok(Box::new(result) as Decoder), + Err(e) => Err(AudioError::VorbisError(e)), + } + }; + + let mut decoder = match result { Ok(decoder) => decoder, Err(e) if is_cached => { warn!( @@ -779,12 +791,13 @@ impl Future for PlayerInternal { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { // While this is written as a future, it still contains blocking code. // It must be run on its own thread. + let passthrough = self.config.passthrough; loop { let mut all_futures_completed_or_not_ready = true; // process commands that were sent to us - let cmd = match Pin::new(&mut self.commands).poll_next(cx) { + let cmd = match self.commands.poll_recv(cx) { Poll::Ready(None) => return Poll::Ready(()), // client has disconnected - shut down. Poll::Ready(Some(cmd)) => { all_futures_completed_or_not_ready = false; @@ -880,32 +893,44 @@ impl Future for PlayerInternal { { let packet = decoder.next_packet().expect("Vorbis error"); - if let Some(ref packet) = packet { - *stream_position_pcm += (packet.data().len() / 2) as u64; - let stream_position_millis = Self::position_pcm_to_ms(*stream_position_pcm); + if !passthrough { + if let Some(ref packet) = packet { + *stream_position_pcm = + *stream_position_pcm + (packet.samples().len() / 2) as u64; + let stream_position_millis = + Self::position_pcm_to_ms(*stream_position_pcm); - let notify_about_position = match *reported_nominal_start_time { - None => true, - Some(reported_nominal_start_time) => { - // only notify if we're behind. If we're ahead it's probably due to a buffer of the backend and we;re actually in time. - let lag = (Instant::now() - reported_nominal_start_time).as_millis() - as i64 - - stream_position_millis as i64; - lag > 1000 + let notify_about_position = match *reported_nominal_start_time { + None => true, + Some(reported_nominal_start_time) => { + // only notify if we're behind. If we're ahead it's probably due to a buffer of the backend and we;re actually in time. + let lag = (Instant::now() - reported_nominal_start_time) + .as_millis() + as i64 + - stream_position_millis as i64; + if lag > 1000 { + true + } else { + false + } + } + }; + if notify_about_position { + *reported_nominal_start_time = Some( + Instant::now() + - Duration::from_millis(stream_position_millis as u64), + ); + self.send_event(PlayerEvent::Playing { + track_id, + play_request_id, + position_ms: stream_position_millis as u32, + duration_ms, + }); } - }; - if notify_about_position { - *reported_nominal_start_time = Some( - Instant::now() - - Duration::from_millis(stream_position_millis as u64), - ); - self.send_event(PlayerEvent::Playing { - track_id, - play_request_id, - position_ms: stream_position_millis as u32, - duration_ms, - }); } + } else { + // position, even if irrelevant, must be set so that seek() is called + *stream_position_pcm = duration_ms.into(); } self.handle_packet(packet, normalisation_factor); @@ -1087,23 +1112,23 @@ impl PlayerInternal { } } - fn handle_packet(&mut self, packet: Option, normalisation_factor: f32) { + fn handle_packet(&mut self, packet: Option, normalisation_factor: f32) { match packet { Some(mut packet) => { - if !packet.data().is_empty() { - if let Some(ref editor) = self.audio_filter { - editor.modify_stream(&mut packet.data_mut()) - }; + if !packet.is_empty() { + if let AudioPacket::Samples(ref mut data) = packet { + if let Some(ref editor) = self.audio_filter { + editor.modify_stream(data) + } - if self.config.normalisation - && (normalisation_factor - 1.0).abs() < f32::EPSILON - { - for x in packet.data_mut().iter_mut() { - *x = (*x as f32 * normalisation_factor) as i16; + if self.config.normalisation && normalisation_factor != 1.0 { + for x in data.iter_mut() { + *x = (*x as f32 * normalisation_factor) as i16; + } } } - if let Err(err) = self.sink.write(&packet.data()) { + if let Err(err) = self.sink.write(&packet) { error!("Could not write audio: {}", err); self.ensure_sink_stopped(false); } @@ -1555,7 +1580,7 @@ impl PlayerInternal { fn send_event(&mut self, event: PlayerEvent) { let mut index = 0; while index < self.event_senders.len() { - match self.event_senders[index].unbounded_send(event.clone()) { + match self.event_senders[index].send(event.clone()) { Ok(_) => index += 1, Err(_) => { self.event_senders.remove(index); @@ -1583,7 +1608,7 @@ impl PlayerInternal { let (result_tx, result_rx) = oneshot::channel(); std::thread::spawn(move || { - futures::executor::block_on(loader.load_track(spotify_id, position_ms)).and_then( + futures_executor::block_on(loader.load_track(spotify_id, position_ms)).and_then( move |data| { let _ = result_tx.send(data); Some(()) diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 35d65934..3d8040fa 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.1.3" +version = "0.1.6" authors = ["Paul LiƩtar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" diff --git a/publish.sh b/publish.sh index fb70af3e..478741a5 100755 --- a/publish.sh +++ b/publish.sh @@ -1,16 +1,25 @@ #!/bin/bash +SKIP_MERGE='false' +DRY_RUN='false' + WORKINGDIR="$( cd "$(dirname "$0")" ; pwd -P )" cd $WORKINGDIR crates=( "protocol" "core" "audio" "metadata" "playback" "connect" "librespot" ) function switchBranch { - # You are expected to have committed/stashed your changes before running this. - echo "Switching to master branch and merging development." - git checkout master - git pull - git merge dev + if [ "$SKIP_MERGE" = 'false' ] ; then + # You are expected to have committed/stashed your changes before running this. + echo "Switching to master branch and merging development." + git checkout master + git pull + if [ "$DRY_RUN" = 'true' ] ; then + git merge --no-commit --no-ff dev + else + git merge dev + fi + fi } function updateVersion { @@ -26,15 +35,25 @@ function updateVersion { echo "Path is $crate_path" if [ "$CRATE" = "librespot" ] then - cargo update - git add . && git commit -a -m "Update Cargo.lock" + if [ "$DRY_RUN" = 'true' ] ; then + cargo update --dry-run + git add . && git commit --dry-run -a -m "Update Cargo.lock" + else + cargo update + git add . && git commit -a -m "Update Cargo.lock" + fi fi done } function commitAndTag { - git commit -a -m "Update version numbers to $1" - git tag "v$1" -a -m "Update to version $1" + if [ "$DRY_RUN" = 'true' ] ; then + # Skip tagging on dry run. + git commit --dry-run -a -m "Update version numbers to $1" + else + git commit -a -m "Update version numbers to $1" + git tag "v$1" -a -m "Update to version $1" + fi } function get_crate_name { @@ -72,9 +91,17 @@ function publishCrates { if [ "$CRATE" == "protocol" ] then # Protocol crate needs --no-verify option due to build.rs modification. - cargo publish --no-verify + if [ "$DRY_RUN" = 'true' ] ; then + cargo publish --no-verify --dry-run + else + cargo publish --no-verify + fi else - cargo publish + if [ "$DRY_RUN" = 'true' ] ; then + cargo publish --dry-run + else + cargo publish + fi fi echo "Successfully published $crate_name to crates.io" remoteWait 30 $crate_name @@ -83,10 +110,32 @@ function publishCrates { function updateRepo { cd $WORKINGDIR - echo "Pushing to master branch of repo." - git push origin master - echo "Pushing v$1 tag to master branch of repo." - git push origin v$1 + if [ "$DRY_RUN" = 'true' ] ; then + echo "Pushing to master branch of repo. [DRY RUN]" + git push --dry-run origin master + echo "Pushing v$1 tag to master branch of repo. [DRY RUN]" + git push --dry-run origin v$1 + + # Cancels any merges in progress + git merge --abort + + git checkout dev + git merge --no-commit --no-ff master + + # Cancels above merge + git merge --abort + + git push --dry-run + else + echo "Pushing to master branch of repo." + git push origin master + echo "Pushing v$1 tag to master branch of repo." + git push origin v$1 + # Update the dev repo with latest version commit + git checkout dev + git merge master + git push + fi } function rebaseDev { @@ -105,5 +154,47 @@ function run { echo "Successfully published v$1 to crates.io and uploaded changes to repo." } +#Set Script Name variable +SCRIPT=`basename ${BASH_SOURCE[0]}` + +print_usage () { + local l_MSG=$1 + if [ ! -z "${l_MSG}" ]; then + echo "Usage Error: $l_MSG" + fi + echo "Usage: $SCRIPT " + echo " where specifies the version number in semver format, eg. 1.0.1" + echo "Recognized optional command line arguments" + echo "--dry-run -- Test the script before making live changes" + echo "--skip-merge -- Skip merging dev into master before publishing" + exit 1 +} + +### check number of command line arguments +NUMARGS=$# +if [ $NUMARGS -eq 0 ]; then + print_usage 'No command line arguments specified' +fi + +while test $# -gt 0; do + case "$1" in + -h|--help) + print_usage + exit 0 + ;; + --dry-run) + DRY_RUN='true' + shift + ;; + --skip-merge) + SKIP_MERGE='true' + shift + ;; + *) + break + ;; + esac +done + # First argument is new version number. run $1 diff --git a/src/lib.rs b/src/lib.rs index 7cdd3178..7722e93e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ #![crate_name = "librespot"] -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_metadata as metadata; -pub extern crate librespot_playback as playback; -pub extern crate librespot_protocol as protocol; +pub use librespot_audio as audio; +pub use librespot_connect as connect; +pub use librespot_core as core; +pub use librespot_metadata as metadata; +pub use librespot_playback as playback; +pub use librespot_protocol as protocol; diff --git a/src/main.rs b/src/main.rs index 1392c201..5de83de1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use futures::{channel::mpsc::UnboundedReceiver, future::FusedFuture, FutureExt, StreamExt}; +use futures_util::{future, FutureExt, StreamExt}; use librespot_playback::player::PlayerEvent; use log::{error, info, warn}; use sha1::{Digest, Sha1}; @@ -10,9 +10,10 @@ use std::{ io::{stderr, Write}, pin::Pin, }; +use tokio::sync::mpsc::UnboundedReceiver; use url::Url; -use librespot::core::authentication::{get_credentials, Credentials}; +use librespot::core::authentication::Credentials; use librespot::core::cache::Cache; use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig, VolumeCtrl}; use librespot::core::session::Session; @@ -70,6 +71,29 @@ fn list_backends() { } } +pub fn get_credentials Option>( + username: Option, + password: Option, + cached_credentials: Option, + prompt: F, +) -> Option { + if let Some(username) = username { + if let Some(password) = password { + return Some(Credentials::with_password(username, password)); + } + + match cached_credentials { + Some(credentials) if username == credentials.username => Some(credentials), + _ => { + let password = prompt(&username)?; + Some(Credentials::with_password(username, password)) + } + } + } else { + cached_credentials + } +} + #[derive(Clone)] struct Setup { backend: fn(Option) -> Box, @@ -203,6 +227,11 @@ fn setup(args: &[String]) -> Setup { "", "disable-gapless", "disable gapless playback.", + ) + .optflag( + "", + "passthrough", + "Pass raw stream to output, only works for \"pipe\"." ); let matches = match opts.parse(&args[1..]) { @@ -312,10 +341,10 @@ fn setup(args: &[String]) -> Setup { let credentials = { let cached_credentials = cache.as_ref().and_then(Cache::credentials); - let password = |username: &String| -> String { - write!(stderr(), "Password for {}: ", username).unwrap(); - stderr().flush().unwrap(); - rpassword::read_password().unwrap() + let password = |username: &String| -> Option { + write!(stderr(), "Password for {}: ", username).ok()?; + stderr().flush().ok()?; + rpassword::read_password().ok() }; get_credentials( @@ -355,6 +384,8 @@ fn setup(args: &[String]) -> Setup { } }; + let passthrough = matches.opt_present("passthrough"); + let player_config = { let bitrate = matches .opt_str("b") @@ -377,6 +408,7 @@ fn setup(args: &[String]) -> Setup { .opt_str("normalisation-pregain") .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) .unwrap_or(PlayerConfig::default().normalisation_pregain), + passthrough, } }; @@ -436,8 +468,7 @@ async fn main() { let mut player_event_channel: Option> = None; let mut auto_connect_times: Vec = vec![]; let mut discovery = None; - let mut connecting: Pin>> = - Box::pin(futures::future::pending()); + let mut connecting: Pin>> = Box::pin(future::pending()); if setupp.enable_discovery { let config = setupp.connect_config.clone(); @@ -558,7 +589,7 @@ async fn main() { } } }, - event = async { player_event_channel.as_mut().unwrap().next().await }, if player_event_channel.is_some() => match event { + event = async { player_event_channel.as_mut().unwrap().recv().await }, if player_event_channel.is_some() => match event { Some(event) => { if let Some(program) = &setupp.player_event_program { if let Some(child) = run_program_on_events(event, program) {