diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index adbd10c49..f06445e60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,36 +4,49 @@ name: test on: push: branches: [master, dev] - paths: ['**.rs', '**.toml', '**.lock', '**.yml'] + paths: + [ + "**.rs", + "Cargo.toml", + "/Cargo.lock", + "/rustfmt.toml", + "/.github/workflows", + ] pull_request: - branches: [master, dev] - paths: ['**.rs', '**.toml', '**.lock', '**.yml'] + paths: + [ + "**.rs", + "Cargo.toml", + "/Cargo.lock", + "/rustfmt.toml", + "/.github/workflows", + ] + schedule: + # Run CI every week + - cron: "00 01 * * 0" + +env: + RUST_BACKTRACE: 1 jobs: fmt: - name: 'Rust: format check' - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - # Only run the formatting check for stable - include: - - os: ubuntu-latest - toolchain: stable + name: rustfmt + runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Install toolchain uses: actions-rs/toolchain@v1 with: - # Use default profile to get rustfmt - profile: default - toolchain: ${{ matrix.toolchain }} + profile: minimal + toolchain: stable override: true - - run: cargo fmt --verbose --all -- --check + components: rustfmt + - run: cargo fmt --all -- --check - test: + test-linux: needs: fmt + name: cargo +${{ matrix.toolchain }} build (${{ matrix.os }}) runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} strategy: @@ -41,11 +54,11 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.41.1 # MSRV (Minimum supported rust version) + - 1.45 # MSRV (Minimum supported rust version) - stable - beta experimental: [false] - # Ignore failures in nightly, not ideal, but necessary + # Ignore failures in nightly include: - os: ubuntu-latest toolchain: nightly @@ -53,12 +66,19 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + - name: Install toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.toolchain }} override: true + + - name: Get Rustc version + id: get-rustc-version + run: echo "::set-output name=version::$(rustc -V)" + shell: bash + - name: Cache Rust dependencies uses: actions/cache@v2 with: @@ -67,20 +87,65 @@ jobs: ~/.cargo/registry/cache ~/.cargo/git target - key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }} + - name: Install developer package dependencies - run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev - - run: cargo build --locked --no-default-features - - run: cargo build --locked --examples - - run: cargo build --locked --no-default-features --features "with-tremor" - - run: cargo build --locked --no-default-features --features "with-vorbis" - - run: cargo build --locked --no-default-features --features "alsa-backend" - - 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 "rodio-backend" - - run: cargo build --locked --no-default-features --features "sdl-backend" - - run: cargo build --locked --no-default-features --features "gstreamer-backend" + run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev libavahi-compat-libdnssd-dev + + - run: cargo build --workspace --examples + - run: cargo test --workspace + + - run: cargo install cargo-hack + - run: cargo hack --workspace --remove-dev-deps + - run: cargo build -p librespot-core --no-default-features + - run: cargo build -p librespot-core + - run: cargo hack build --each-feature -p librespot-audio + - run: cargo build -p librespot-connect + - run: cargo build -p librespot-connect --no-default-features --features with-dns-sd + - run: cargo hack build --locked --each-feature + + test-windows: + needs: fmt + name: cargo build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest] + toolchain: [stable] + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + profile: minimal + override: true + + - name: Get Rustc version + id: get-rustc-version + run: echo "::set-output name=version::$(rustc -V)" + shell: bash + + - name: Cache Rust dependencies + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + target + key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }} + + - run: cargo build --workspace --examples + - run: cargo test --workspace + + - run: cargo install cargo-hack + - run: cargo hack --workspace --remove-dev-deps + - run: cargo build --no-default-features + - run: cargo build test-cross-arm: needs: fmt @@ -96,6 +161,7 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + - name: Install toolchain uses: actions-rs/toolchain@v1 with: @@ -103,6 +169,12 @@ jobs: target: ${{ matrix.target }} toolchain: ${{ matrix.toolchain }} override: true + + - name: Get Rustc version + id: get-rustc-version + run: echo "::set-output name=version::$(rustc -V)" + shell: bash + - name: Cache Rust dependencies uses: actions/cache@v2 with: @@ -111,7 +183,7 @@ jobs: ~/.cargo/registry/cache ~/.cargo/git target - key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-${{ matrix.target }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }} - name: Install cross run: cargo install cross || true - name: Build diff --git a/COMPILING.md b/COMPILING.md index 4320cdbbc..539dc698a 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -13,7 +13,7 @@ curl https://sh.rustup.rs -sSf | sh Follow any prompts it gives you to install Rust. Once that’s done, Rust's standard tools should be setup and ready to use. -*Note: The current minimum required Rust version at the time of writing is 1.41, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* +*Note: The current minimum required Rust version at the time of writing is 1.45, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* #### Additional Rust tools - `rustfmt` To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt), which is installed by default with `rustup` these days, else it can be installed manually with: diff --git a/Cargo.lock b/Cargo.lock index 837df2fec..507785989 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aes" version = "0.6.0" @@ -30,7 +32,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" dependencies = [ "cipher", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -40,7 +42,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" dependencies = [ "cipher", - "opaque-debug 0.3.0", + "opaque-debug", +] + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", ] [[package]] @@ -71,6 +82,17 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" +[[package]] +name = "async-trait" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -79,7 +101,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -88,16 +110,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[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" @@ -123,46 +135,19 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding 0.1.5", - "byte-tools", - "byteorder", - "generic-array 0.12.3", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.4", + "generic-array", ] [[package]] @@ -171,19 +156,10 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57a0e8073e8baa88212fb5823574c02ebccb395136ba9a164ab89379ec6072f0" dependencies = [ - "block-padding 0.2.1", + "block-padding", "cipher", ] -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - [[package]] name = "block-padding" version = "0.2.1" @@ -192,15 +168,9 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "bumpalo" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" - -[[package]] -name = "byte-tools" -version = "0.3.1" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" [[package]] name = "byteorder" @@ -208,16 +178,6 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] - [[package]] name = "bytes" version = "1.0.1" @@ -226,9 +186,9 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" dependencies = [ "jobserver", ] @@ -270,7 +230,7 @@ dependencies = [ "num-integer", "num-traits", "time", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -279,27 +239,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.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0659001ab56b791be01d4b729c44376edc6718cf389a502e579b77b758f3296c" +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]] @@ -308,7 +259,7 @@ version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc4369b5e4c0cddf64ad8981c0111e7df4f7078f4d6ba98fb31f2e17c4c57b7e" dependencies = [ - "bytes 1.0.1", + "bytes", "memchr", ] @@ -356,11 +307,11 @@ dependencies = [ "ndk-glue", "nix", "oboe", - "parking_lot 0.11.1", + "parking_lot", "stdweb", "thiserror", "web-sys", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -369,80 +320,13 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" -[[package]] -name = "crossbeam-deque" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils 0.7.2", - "maybe-uninit", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" -dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "crossbeam-utils 0.7.2", - "lazy_static", - "maybe-uninit", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" -dependencies = [ - "crossbeam-utils 0.6.6", -] - -[[package]] -name = "crossbeam-queue" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" -dependencies = [ - "cfg-if 0.1.10", - "crossbeam-utils 0.7.2", - "maybe-uninit", -] - -[[package]] -name = "crossbeam-utils" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" -dependencies = [ - "cfg-if 0.1.10", - "lazy_static", -] - -[[package]] -name = "crossbeam-utils" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "lazy_static", -] - [[package]] name = "crypto-mac" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" dependencies = [ - "generic-array 0.14.4", + "generic-array", "subtle", ] @@ -501,22 +385,13 @@ dependencies = [ "syn", ] -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.3", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.4", + "generic-array", ] [[package]] @@ -537,31 +412,17 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "env_logger" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e" +checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" dependencies = [ "atty", "humantime", - "log 0.4.14", + "log", + "regex", "termcolor", ] -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -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 = "fnv" version = "1.0.7" @@ -569,74 +430,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" +name = "form_urlencoded" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ - "bitflags", - "fuchsia-zircon-sys", + "matches", + "percent-encoding", ] -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" - [[package]] name = "futures" -version = "0.1.30" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7e4c2612746b0df8fed4ce0c69156021b704c9aefa360311c04e6e9e002eed" +checksum = "7f55667319111d593ba876406af7c409c0ebb44dc4be6132a783ccf163ea14c1" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] [[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" - -[[package]] -name = "futures-cpupool" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" -dependencies = [ - "futures", - "num_cpus", -] +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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" + [[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", @@ -646,33 +501,34 @@ 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", "proc-macro-nested", - "slab 0.4.2", + "slab", ] [[package]] @@ -681,15 +537,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" @@ -709,17 +556,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" @@ -728,7 +564,7 @@ checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -891,6 +727,31 @@ dependencies = [ "system-deps", ] +[[package]] +name = "headers" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" +dependencies = [ + "base64", + "bitflags", + "bytes", + "headers-core", + "http", + "mime", + "sha-1", + "time", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.3.2" @@ -922,7 +783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ "crypto-mac", - "digest 0.9.0", + "digest", ] [[package]] @@ -933,7 +794,28 @@ checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" dependencies = [ "libc", "match_cfg", - "winapi 0.3.9", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" +dependencies = [ + "bytes", + "http", ] [[package]] @@ -942,6 +824,12 @@ version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + [[package]] name = "humantime" version = "2.1.0" @@ -950,42 +838,40 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.11.27" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a590ca09d341e94cddf8e5af0bbccde205d5fbc2fa3c09dd67c7f85cea59d7" +checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" dependencies = [ - "base64 0.9.3", - "bytes 0.4.12", - "futures", - "futures-cpupool", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", "httparse", - "iovec", - "language-tags", - "log 0.4.14", - "mime", - "net2", - "percent-encoding", - "relay", - "time", - "tokio-core", - "tokio-io", - "tokio-proto", - "tokio-service", - "unicase", + "httpdate", + "itoa", + "pin-project", + "socket2", + "tokio", + "tower-service", + "tracing", "want", ] [[package]] name = "hyper-proxy" -version = "0.4.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f0925de2747e481e6e477dd212c25e8f745567f02f6182e04d27b97c3fbece" +checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc" dependencies = [ - "bytes 0.4.12", + "bytes", "futures", + "headers", + "http", "hyper", - "tokio-core", - "tokio-io", + "tokio", + "tower-service", ] [[package]] @@ -996,9 +882,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.1.5" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" dependencies = [ "matches", "unicode-bidi", @@ -1013,7 +899,7 @@ checksum = "28538916eb3f3976311f5dfbe67b5362d0add1293d0a9cad17debf86f8e3aa48" dependencies = [ "if-addrs-sys", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1035,15 +921,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - [[package]] name = "itertools" version = "0.9.0" @@ -1079,7 +956,7 @@ checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41" dependencies = [ "lazy_static", "libc", - "libloading", + "libloading 0.6.7", ] [[package]] @@ -1091,7 +968,7 @@ dependencies = [ "cesu8", "combine", "jni-sys", - "log 0.4.14", + "log", "thiserror", "walkdir", ] @@ -1120,22 +997,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - -[[package]] -name = "language-tags" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" - [[package]] name = "lazy_static" version = "1.4.0" @@ -1172,25 +1033,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" dependencies = [ "cfg-if 1.0.0", - "winapi 0.3.9", + "winapi", ] [[package]] -name = "libmdns" -version = "0.2.7" +name = "libloading" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8582c174736c53633bc482ac709b24527c018356c3dc6d8e25a788b06b394e" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" dependencies = [ - "byteorder", - "futures", - "hostname", - "if-addrs", - "log 0.4.14", + "cfg-if 1.0.0", + "winapi", +] + +[[package]] +name = "libmdns" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b276920bfc6c9285e16ffd30ed410487f0185f383483f45a3446afc0554fded" +dependencies = [ + "byteorder", + "futures-util", + "hostname", + "if-addrs", + "log", "multimap", - "net2", "quick-error", - "rand 0.7.3", - "tokio-core", + "rand", + "socket2", + "tokio", ] [[package]] @@ -1204,7 +1075,7 @@ dependencies = [ "libpulse-sys", "num-derive", "num-traits", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1238,16 +1109,16 @@ dependencies = [ "num-derive", "num-traits", "pkg-config", - "winapi 0.3.9", + "winapi", ] [[package]] name = "librespot" version = "0.1.6" dependencies = [ - "base64 0.13.0", + "base64", "env_logger", - "futures", + "futures-util", "getopts", "hex", "hyper", @@ -1257,16 +1128,10 @@ dependencies = [ "librespot-metadata", "librespot-playback", "librespot-protocol", - "log 0.4.14", - "num-bigint", - "protobuf", - "rand 0.7.3", + "log", "rpassword", - "sha-1 0.8.2", - "tokio-core", - "tokio-io", - "tokio-process", - "tokio-signal", + "sha-1", + "tokio", "url", ] @@ -1275,18 +1140,17 @@ name = "librespot-audio" version = "0.1.6" dependencies = [ "aes-ctr", - "bit-set", "byteorder", - "bytes 0.4.12", - "futures", + "bytes", + "cfg-if 1.0.0", + "futures-util", "lewton", "librespot-core", "librespot-tremor", - "log 0.4.14", - "num-bigint", - "num-traits", + "log", "ogg", "tempfile", + "tokio", "vorbis", "zerocopy", ] @@ -1296,25 +1160,26 @@ name = "librespot-connect" version = "0.1.6" dependencies = [ "aes-ctr", - "base64 0.13.0", + "base64", "block-modes", "dns-sd", - "futures", + "form_urlencoded", + "futures-core", + "futures-util", "hmac", "hyper", "libmdns", "librespot-core", "librespot-playback", "librespot-protocol", - "log 0.4.14", - "num-bigint", + "log", "protobuf", - "rand 0.7.3", + "rand", "serde", - "serde_derive", "serde_json", - "sha-1 0.9.3", - "tokio-core", + "sha-1", + "tokio", + "tokio-stream", "url", ] @@ -1323,32 +1188,35 @@ name = "librespot-core" version = "0.1.6" dependencies = [ "aes", - "base64 0.13.0", + "base64", "byteorder", - "bytes 0.4.12", - "error-chain", - "futures", + "bytes", + "env_logger", + "form_urlencoded", + "futures-core", + "futures-util", "hmac", + "http", "httparse", "hyper", "hyper-proxy", - "lazy_static", "librespot-protocol", - "log 0.4.14", + "log", "num-bigint", "num-integer", "num-traits", + "once_cell", "pbkdf2", "protobuf", - "rand 0.7.3", + "rand", "serde", - "serde_derive", "serde_json", - "sha-1 0.9.3", + "sha-1", "shannon", - "tokio-codec", - "tokio-core", - "tokio-io", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", "url", "uuid", "vergen", @@ -1358,12 +1226,11 @@ dependencies = [ name = "librespot-metadata" version = "0.1.6" dependencies = [ + "async-trait", "byteorder", - "futures", "librespot-core", "librespot-protocol", - "linear-map", - "log 0.4.14", + "log", "protobuf", ] @@ -1374,7 +1241,8 @@ dependencies = [ "alsa", "byteorder", "cpal", - "futures", + "futures-executor", + "futures-util", "glib", "gstreamer", "gstreamer-app", @@ -1385,11 +1253,13 @@ dependencies = [ "librespot-audio", "librespot-core", "librespot-metadata", - "log 0.4.14", + "log", "portaudio-rs", "rodio", "sdl2", "shell-words", + "thiserror", + "tokio", "zerocopy", ] @@ -1415,21 +1285,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.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" -dependencies = [ - "scopeguard", -] - [[package]] name = "lock_api" version = "0.4.2" @@ -1439,15 +1294,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "log" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" -dependencies = [ - "log 0.4.14", -] - [[package]] name = "log" version = "0.4.14" @@ -1478,27 +1324,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - [[package]] name = "memchr" version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" -[[package]] -name = "memoffset" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.16" @@ -1507,56 +1338,15 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" -version = "0.6.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" -dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log 0.4.14", - "miow 0.2.2", - "net2", - "slab 0.4.2", - "winapi 0.2.8", -] - -[[package]] -name = "mio-named-pipes" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" -dependencies = [ - "log 0.4.14", - "mio", - "miow 0.3.6", - "winapi 0.3.9", -] - -[[package]] -name = "mio-uds" -version = "0.6.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +checksum = "a5dede4e2065b3842b8b0af444119f3aa331cc7cc2dd20388bfb0f5d5a38823a" dependencies = [ - "iovec", "libc", - "mio", -] - -[[package]] -name = "miow" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" -dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", + "log", + "miow", + "ntapi", + "winapi", ] [[package]] @@ -1566,7 +1356,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" dependencies = [ "socket2", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1604,7 +1394,7 @@ checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" dependencies = [ "lazy_static", "libc", - "log 0.4.14", + "log", "ndk", "ndk-macro", "ndk-sys", @@ -1629,17 +1419,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" -[[package]] -name = "net2" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", -] - [[package]] name = "nix" version = "0.20.0" @@ -1662,15 +1441,25 @@ dependencies = [ "version_check", ] +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + [[package]] name = "num-bigint" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf" +checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" dependencies = [ "autocfg", "num-integer", "num-traits", + "rand", ] [[package]] @@ -1791,15 +1580,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" - -[[package]] -name = "opaque-debug" -version = "0.2.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +checksum = "4ad167a2f54e832b82dbe003a046280dceffe5227b5f79e08e363a29638cfddd" [[package]] name = "opaque-debug" @@ -1807,17 +1590,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "parking_lot" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" -dependencies = [ - "lock_api 0.3.4", - "parking_lot_core 0.6.2", - "rustc_version", -] - [[package]] name = "parking_lot" version = "0.11.1" @@ -1825,37 +1597,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ "instant", - "lock_api 0.4.2", - "parking_lot_core 0.8.2", -] - -[[package]] -name = "parking_lot_core" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" -dependencies = [ - "cfg-if 0.1.10", - "cloudabi", - "libc", - "redox_syscall 0.1.57", - "rustc_version", - "smallvec 0.6.14", - "winapi 0.3.9", + "lock_api", + "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.1.57", - "smallvec 1.6.1", - "winapi 0.3.9", + "redox_syscall", + "smallvec", + "winapi", ] [[package]] @@ -1882,9 +1639,38 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "1.0.1" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pin-project" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" +checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "pin-project-lite" @@ -2024,49 +1810,13 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" -dependencies = [ - "libc", - "rand 0.4.6", -] - -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi 0.3.9", -] - -[[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" @@ -2074,19 +1824,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" dependencies = [ "libc", - "rand_chacha 0.3.0", - "rand_core 0.6.1", - "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]] @@ -2096,49 +1836,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" dependencies = [ "ppv-lite86", - "rand_core 0.6.1", -] - -[[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]] name = "rand_core" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5" -dependencies = [ - "getrandom 0.2.2", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" dependencies = [ - "rand_core 0.5.1", + "getrandom", ] [[package]] @@ -2147,29 +1854,14 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" dependencies = [ - "rand_core 0.6.1", -] - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", + "rand_core", ] [[package]] name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - -[[package]] -name = "redox_syscall" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" dependencies = [ "bitflags", ] @@ -2180,7 +1872,10 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", + "thread_local", ] [[package]] @@ -2189,22 +1884,13 @@ version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" -[[package]] -name = "relay" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a" -dependencies = [ - "futures", -] - [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2223,7 +1909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2234,9 +1920,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" -version = "0.2.3" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" dependencies = [ "semver", ] @@ -2247,12 +1933,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" @@ -2262,12 +1942,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scoped-tls" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" - [[package]] name = "scopeguard" version = "1.1.0" @@ -2299,24 +1973,30 @@ dependencies = [ [[package]] name = "semver" -version = "0.9.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" dependencies = [ "semver-parser", ] [[package]] name = "semver-parser" -version = "0.7.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] [[package]] name = "serde" version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" @@ -2331,9 +2011,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" dependencies = [ "itoa", "ryu", @@ -2342,27 +2022,15 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - -[[package]] -name = "sha-1" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4b312c3731e3fe78a185e6b9b911a7aa715b8e31cce117975219aab2acf285d" +checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" dependencies = [ - "block-buffer 0.9.0", + "block-buffer", "cfg-if 1.0.0", "cpuid-bool", - "digest 0.9.0", - "opaque-debug 0.3.0", + "digest", + "opaque-debug", ] [[package]] @@ -2395,33 +2063,12 @@ dependencies = [ "libc", ] -[[package]] -name = "slab" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23" - [[package]] name = "slab" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" -[[package]] -name = "smallvec" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013" - -[[package]] -name = "smallvec" -version = "0.6.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" -dependencies = [ - "maybe-uninit", -] - [[package]] name = "smallvec" version = "1.6.1" @@ -2436,7 +2083,7 @@ checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" dependencies = [ "cfg-if 1.0.0", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2513,12 +2160,6 @@ dependencies = [ "version-compare", ] -[[package]] -name = "take" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5" - [[package]] name = "tempfile" version = "3.2.0" @@ -2527,10 +2168,10 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if 1.0.0", "libc", - "rand 0.8.3", - "redox_syscall 0.2.4", + "rand", + "redox_syscall", "remove_dir_all", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2544,24 +2185,33 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + [[package]] name = "time" version = "0.1.43" @@ -2569,7 +2219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2589,282 +2239,99 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" -dependencies = [ - "bytes 0.4.12", - "futures", - "mio", - "num_cpus", - "tokio-codec", - "tokio-current-thread", - "tokio-executor", - "tokio-fs", - "tokio-io", - "tokio-reactor", - "tokio-sync", - "tokio-tcp", - "tokio-threadpool", - "tokio-timer", - "tokio-udp", - "tokio-uds", -] - -[[package]] -name = "tokio-codec" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b" -dependencies = [ - "bytes 0.4.12", - "futures", - "tokio-io", -] - -[[package]] -name = "tokio-core" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b1395334443abca552f63d4f61d0486f12377c2ba8b368e523f89e828cffd4" -dependencies = [ - "bytes 0.4.12", - "futures", - "iovec", - "log 0.4.14", - "mio", - "scoped-tls", - "tokio", - "tokio-executor", - "tokio-io", - "tokio-reactor", - "tokio-timer", -] - -[[package]] -name = "tokio-current-thread" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" -dependencies = [ - "futures", - "tokio-executor", -] - -[[package]] -name = "tokio-executor" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" -dependencies = [ - "crossbeam-utils 0.7.2", - "futures", -] - -[[package]] -name = "tokio-fs" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4" -dependencies = [ - "futures", - "tokio-io", - "tokio-threadpool", -] - -[[package]] -name = "tokio-io" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" -dependencies = [ - "bytes 0.4.12", - "futures", - "log 0.4.14", -] - -[[package]] -name = "tokio-process" -version = "0.2.5" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382d90f43fa31caebe5d3bc6cfd854963394fff3b8cb59d5146607aaae7e7e43" +checksum = "e8190d04c665ea9e6b6a0dc45523ade572c088d2e6566244c1122671dbf4ae3a" dependencies = [ - "crossbeam-queue 0.1.2", - "futures", - "lazy_static", + "autocfg", + "bytes", "libc", - "log 0.4.14", - "mio", - "mio-named-pipes", - "tokio-io", - "tokio-reactor", - "tokio-signal", - "winapi 0.3.9", -] - -[[package]] -name = "tokio-proto" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fbb47ae81353c63c487030659494b295f6cb6576242f907f203473b191b0389" -dependencies = [ - "futures", - "log 0.3.9", - "net2", - "rand 0.3.23", - "slab 0.3.0", - "smallvec 0.2.1", - "take", - "tokio-core", - "tokio-io", - "tokio-service", -] - -[[package]] -name = "tokio-reactor" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" -dependencies = [ - "crossbeam-utils 0.7.2", - "futures", - "lazy_static", - "log 0.4.14", + "memchr", "mio", "num_cpus", - "parking_lot 0.9.0", - "slab 0.4.2", - "tokio-executor", - "tokio-io", - "tokio-sync", -] - -[[package]] -name = "tokio-service" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24da22d077e0f15f55162bdbdc661228c1581892f52074fb242678d015b45162" -dependencies = [ - "futures", -] - -[[package]] -name = "tokio-signal" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c34c6e548f101053321cba3da7cbb87a610b85555884c41b07da2eb91aff12" -dependencies = [ - "futures", - "libc", - "mio", - "mio-uds", + "once_cell", + "pin-project-lite", "signal-hook-registry", - "tokio-executor", - "tokio-io", - "tokio-reactor", - "winapi 0.3.9", + "tokio-macros", + "winapi", ] [[package]] -name = "tokio-sync" -version = "0.1.8" +name = "tokio-macros" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" +checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" dependencies = [ - "fnv", - "futures", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "tokio-tcp" -version = "0.1.4" +name = "tokio-stream" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" +checksum = "1981ad97df782ab506a1f43bf82c967326960d278acf3bf8279809648c3ff3ea" dependencies = [ - "bytes 0.4.12", - "futures", - "iovec", - "mio", - "tokio-io", - "tokio-reactor", + "futures-core", + "pin-project-lite", + "tokio", ] [[package]] -name = "tokio-threadpool" -version = "0.1.18" +name = "tokio-util" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" +checksum = "ebb7cb2f00c5ae8df755b252306272cd1790d39728363936e01827e11f0b017b" dependencies = [ - "crossbeam-deque", - "crossbeam-queue 0.2.3", - "crossbeam-utils 0.7.2", - "futures", - "lazy_static", - "log 0.4.14", - "num_cpus", - "slab 0.4.2", - "tokio-executor", + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", ] [[package]] -name = "tokio-timer" -version = "0.2.13" +name = "toml" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ - "crossbeam-utils 0.7.2", - "futures", - "slab 0.4.2", - "tokio-executor", + "serde", ] [[package]] -name = "tokio-udp" -version = "0.1.6" +name = "tower-service" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82" -dependencies = [ - "bytes 0.4.12", - "futures", - "log 0.4.14", - "mio", - "tokio-codec", - "tokio-io", - "tokio-reactor", -] +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] -name = "tokio-uds" -version = "0.2.7" +name = "tracing" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab57a4ac4111c8c9dbcf70779f6fc8bc35ae4b2454809febac840ad19bd7e4e0" +checksum = "f77d3842f76ca899ff2dbcf231c5c65813dea431301d6eb686279c15c4464f12" dependencies = [ - "bytes 0.4.12", - "futures", - "iovec", - "libc", - "log 0.4.14", - "mio", - "mio-uds", - "tokio-codec", - "tokio-io", - "tokio-reactor", + "cfg-if 1.0.0", + "pin-project-lite", + "tracing-core", ] [[package]] -name = "toml" -version = "0.5.8" +name = "tracing-core" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" dependencies = [ - "serde", + "lazy_static", ] [[package]] name = "try-lock" -version = "0.1.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2aa4715743892880f70885373966c83d73ef1b0838a664ef0c76fffd35e7c2" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" @@ -2873,13 +2340,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" [[package]] -name = "unicase" -version = "2.6.0" +name = "ucd-trie" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "unicode-bidi" @@ -2892,9 +2356,9 @@ dependencies = [ [[package]] name = "unicode-normalization" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" dependencies = [ "tinyvec", ] @@ -2919,10 +2383,11 @@ checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "url" -version = "1.7.2" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" dependencies = [ + "form_urlencoded", "idna", "matches", "percent-encoding", @@ -2934,17 +2399,18 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.2", + "getrandom", ] [[package]] name = "vergen" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce50d8996df1f85af15f2cd8d33daae6e479575123ef4314a51a70a230739cb" +checksum = "e7141e445af09c8919f1d5f8a20dae0b20c3b57a45dee0d5823c6ed5d237f15a" dependencies = [ "bitflags", "chrono", + "rustc_version", ] [[package]] @@ -3003,27 +2469,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" dependencies = [ "same-file", - "winapi 0.3.9", + "winapi", "winapi-util", ] [[package]] name = "want" -version = "0.0.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a05d9d966753fa4b5c8db73fcab5eed4549cfe0e1e4e66911e5564a0085c35d1" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" dependencies = [ - "futures", - "log 0.4.14", + "log", "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" @@ -3048,7 +2507,7 @@ checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7" dependencies = [ "bumpalo", "lazy_static", - "log 0.4.14", + "log", "proc-macro2", "quote", "syn", @@ -3094,12 +2553,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" - [[package]] name = "winapi" version = "0.3.9" @@ -3110,12 +2563,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -3128,7 +2575,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -3137,16 +2584,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "ws2_32-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - [[package]] name = "zerocopy" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 0b2545922..14e33a83f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,18 +23,24 @@ doc = false [dependencies.librespot-audio] path = "audio" version = "0.1.6" + [dependencies.librespot-connect] path = "connect" version = "0.1.6" + [dependencies.librespot-core] path = "core" version = "0.1.6" +features = ["apresolve"] + [dependencies.librespot-metadata] path = "metadata" version = "0.1.6" + [dependencies.librespot-playback] path = "playback" version = "0.1.6" + [dependencies.librespot-protocol] path = "protocol" version = "0.1.6" @@ -42,29 +48,23 @@ version = "0.1.6" [dependencies] base64 = "0.13" env_logger = {version = "0.8", default-features = false, features = ["termcolor","humantime","atty"]} -futures = "0.1" +futures-util = { version = "0.3", default_features = false } getopts = "0.2" -hyper = "0.11" +hex = "0.4" +hyper = "0.14" log = "0.4" -num-bigint = "0.3" -protobuf = "~2.14.0" -rand = "0.7" rpassword = "5.0" -tokio-core = "0.1" -tokio-io = "0.1" -tokio-process = "0.2" -tokio-signal = "0.2" -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] alsa-backend = ["librespot-playback/alsa-backend"] portaudio-backend = ["librespot-playback/portaudio-backend"] pulseaudio-backend = ["librespot-playback/pulseaudio-backend"] jackaudio-backend = ["librespot-playback/jackaudio-backend"] -rodiojack-backend = ["librespot-playback/rodiojack-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"] @@ -73,7 +73,7 @@ with-vorbis = ["librespot-audio/with-vorbis"] with-dns-sd = ["librespot-connect/with-dns-sd"] -default = ["librespot-playback/rodio-backend"] +default = ["rodio-backend"] [package.metadata.deb] maintainer = "librespot-org" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 2d67f4cec..a3dfbe7bb 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -12,16 +12,15 @@ version = "0.1.6" [dependencies] aes-ctr = "0.6" -bit-set = "0.5" -byteorder = "1.3" -bytes = "0.4" -futures = "0.1" +byteorder = "1.4" +bytes = "1.0" +cfg-if = "1" lewton = "0.10" -ogg = "0.8" log = "0.4" -num-bigint = "0.3" -num-traits = "0.2" +futures-util = { version = "0.3", default_features = false } +ogg = "0.8" tempfile = "3.1" +tokio = { version = "1", features = ["sync", "macros"] } zerocopy = "0.3" librespot-tremor = { version = "0.2", optional = true } diff --git a/audio/src/convert.rs b/audio/src/convert.rs index e291c804e..450910b0b 100644 --- a/audio/src/convert.rs +++ b/audio/src/convert.rs @@ -36,24 +36,21 @@ macro_rules! convert_samples_to { }; } -pub struct SamplesConverter {} -impl SamplesConverter { - pub fn to_s32(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples) - } +pub fn to_s32(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples) +} - pub fn to_s24(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples, 8) - } +pub fn to_s24(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples, 8) +} - pub fn to_s24_3(samples: &[f32]) -> Vec { - Self::to_s32(samples) - .iter() - .map(|sample| i24::pcm_from_i32(*sample)) - .collect() - } +pub fn to_s24_3(samples: &[f32]) -> Vec { + to_s32(samples) + .iter() + .map(|sample| i24::pcm_from_i32(*sample)) + .collect() +} - pub fn to_s16(samples: &[f32]) -> Vec { - convert_samples_to!(i16, samples) - } +pub fn to_s16(samples: &[f32]) -> Vec { + convert_samples_to!(i16, samples) } diff --git a/audio/src/fetch.rs b/audio/src/fetch.rs deleted file mode 100644 index 11745a259..000000000 --- a/audio/src/fetch.rs +++ /dev/null @@ -1,1099 +0,0 @@ -use crate::range_set::{Range, RangeSet}; -use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; -use bytes::Bytes; -use futures::sync::{mpsc, oneshot}; -use futures::Stream; -use futures::{Async, Future, Poll}; -use std::cmp::{max, min}; -use std::fs; -use std::io::{self, Read, Seek, SeekFrom, Write}; -use std::sync::{Arc, Condvar, Mutex}; -use std::time::{Duration, Instant}; -use tempfile::NamedTempFile; - -use futures::sync::mpsc::unbounded; -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; - -const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 16; -// The minimum size of a block that is requested from the Spotify servers in one request. -// This is the block size that is typically requested while doing a seek() on a file. -// Note: smaller requests can happen if part of the block is downloaded already. - -const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 16; -// The amount of data that is requested when initially opening a file. -// Note: if the file is opened to play from the beginning, the amount of data to -// read ahead is requested in addition to this amount. If the file is opened to seek to -// another position, then only this amount is requested on the first request. - -const INITIAL_PING_TIME_ESTIMATE_SECONDS: f64 = 0.5; -// The pig time that is used for calculations before a ping time was actually measured. - -const MAXIMUM_ASSUMED_PING_TIME_SECONDS: f64 = 1.5; -// If the measured ping time to the Spotify server is larger than this value, it is capped -// to avoid run-away block sizes and pre-fetching. - -pub const READ_AHEAD_BEFORE_PLAYBACK_SECONDS: f64 = 1.0; -// Before playback starts, this many seconds of data must be present. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. - -pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f64 = 2.0; -// Same as READ_AHEAD_BEFORE_PLAYBACK_SECONDS, but the time is taken as a factor of the ping -// time to the Spotify server. -// Both, READ_AHEAD_BEFORE_PLAYBACK_SECONDS and READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS are -// obeyed. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. - -pub const READ_AHEAD_DURING_PLAYBACK_SECONDS: f64 = 5.0; -// While playing back, this many seconds of data ahead of the current read position are -// requested. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. - -pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f64 = 10.0; -// Same as READ_AHEAD_DURING_PLAYBACK_SECONDS, but the time is taken as a factor of the ping -// time to the Spotify server. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. - -const PREFETCH_THRESHOLD_FACTOR: f64 = 4.0; -// If the amount of data that is pending (requested but not received) is less than a certain amount, -// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more -// data is calculated as -// < PREFETCH_THRESHOLD_FACTOR * * - -const FAST_PREFETCH_THRESHOLD_FACTOR: f64 = 1.5; -// Similar to PREFETCH_THRESHOLD_FACTOR, but it also takes the current download rate into account. -// The formula used is -// < FAST_PREFETCH_THRESHOLD_FACTOR * * -// This mechanism allows for fast downloading of the remainder of the file. The number should be larger -// than 1 so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster -// the download rate ramps up. However, this comes at the cost that it might hurt ping-time if a seek is -// performed while downloading. Values smaller than 1 cause the download rate to collapse and effectively -// only PREFETCH_THRESHOLD_FACTOR is in effect. Thus, set to zero if bandwidth saturation is not wanted. - -const MAX_PREFETCH_REQUESTS: usize = 4; -// Limit the number of requests that are pending simultaneously before pre-fetching data. Pending -// requests share bandwidth. Thus, havint too many requests can lead to the one that is needed next -// for playback to be delayed leading to a buffer underrun. This limit has the effect that a new -// pre-fetch request is only sent if less than MAX_PREFETCH_REQUESTS are pending. - -pub enum AudioFile { - Cached(fs::File), - Streaming(AudioFileStreaming), -} - -pub enum AudioFileOpen { - Cached(Option), - Streaming(AudioFileOpenStreaming), -} - -pub struct AudioFileOpenStreaming { - session: Session, - initial_data_rx: Option, - initial_data_length: Option, - initial_request_sent_time: Instant, - headers: ChannelHeaders, - file_id: FileId, - complete_tx: Option>, - streaming_data_rate: usize, -} - -enum StreamLoaderCommand { - Fetch(Range), // signal the stream loader to fetch a range of the file - RandomAccessMode(), // optimise download strategy for random access - StreamMode(), // optimise download strategy for streaming - Close(), // terminate and don't load any more data -} - -#[derive(Clone)] -pub struct StreamLoaderController { - channel_tx: Option>, - stream_shared: Option>, - file_size: usize, -} - -impl StreamLoaderController { - pub fn len(&self) -> usize { - return self.file_size; - } - - pub fn range_available(&self, range: Range) -> bool { - if let Some(ref shared) = self.stream_shared { - let download_status = shared.download_status.lock().unwrap(); - if range.length - <= download_status - .downloaded - .contained_length_from_value(range.start) - { - return true; - } else { - return false; - } - } else { - if range.length <= self.len() - range.start { - return true; - } else { - return false; - } - } - } - - pub fn range_to_end_available(&self) -> bool { - if let Some(ref shared) = self.stream_shared { - let read_position = shared.read_position.load(atomic::Ordering::Relaxed); - self.range_available(Range::new(read_position, self.len() - read_position)) - } else { - true - } - } - - pub fn ping_time_ms(&self) -> usize { - if let Some(ref shared) = self.stream_shared { - return shared.ping_time_ms.load(atomic::Ordering::Relaxed); - } else { - return 0; - } - } - - 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); - } - } - - pub fn fetch(&mut self, range: Range) { - // signal the stream loader to fetch a range of the file - self.send_stream_loader_command(StreamLoaderCommand::Fetch(range)); - } - - pub fn fetch_blocking(&mut self, mut range: Range) { - // signal the stream loader to tech a range of the file and block until it is loaded. - - // ensure the range is within the file's bounds. - if range.start >= self.len() { - range.length = 0; - } else if range.end() > self.len() { - range.length = self.len() - range.start; - } - - self.fetch(range); - - if let Some(ref shared) = self.stream_shared { - let mut download_status = shared.download_status.lock().unwrap(); - while range.length - > download_status - .downloaded - .contained_length_from_value(range.start) - { - download_status = shared - .cond - .wait_timeout(download_status, Duration::from_millis(1000)) - .unwrap() - .0; - if range.length - > (download_status - .downloaded - .union(&download_status.requested) - .contained_length_from_value(range.start)) - { - // For some reason, the requested range is neither downloaded nor requested. - // This could be due to a network error. Request it again. - // 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)); - } - } - } - } - } - - pub fn fetch_next(&mut self, length: usize) { - let range: Range = if let Some(ref shared) = self.stream_shared { - Range { - start: shared.read_position.load(atomic::Ordering::Relaxed), - length: length, - } - } else { - return; - }; - self.fetch(range); - } - - pub fn fetch_next_blocking(&mut self, length: usize) { - let range: Range = if let Some(ref shared) = self.stream_shared { - Range { - start: shared.read_position.load(atomic::Ordering::Relaxed), - length: length, - } - } else { - return; - }; - self.fetch_blocking(range); - } - - pub fn set_random_access_mode(&mut self) { - // optimise download strategy for random access - self.send_stream_loader_command(StreamLoaderCommand::RandomAccessMode()); - } - - pub fn set_stream_mode(&mut self) { - // optimise download strategy for streaming - self.send_stream_loader_command(StreamLoaderCommand::StreamMode()); - } - - pub fn close(&mut self) { - // terminate stream loading and don't load any more data for this file. - self.send_stream_loader_command(StreamLoaderCommand::Close()); - } -} - -pub struct AudioFileStreaming { - read_file: fs::File, - - position: u64, - - stream_loader_command_tx: mpsc::UnboundedSender, - - shared: Arc, -} - -struct AudioFileDownloadStatus { - requested: RangeSet, - downloaded: RangeSet, -} - -#[derive(Copy, Clone)] -enum DownloadStrategy { - RandomAccess(), - Streaming(), -} - -struct AudioFileShared { - file_id: FileId, - file_size: usize, - stream_data_rate: usize, - cond: Condvar, - download_status: Mutex, - download_strategy: Mutex, - number_of_open_requests: AtomicUsize, - ping_time_ms: AtomicUsize, - read_position: AtomicUsize, -} - -impl AudioFileOpenStreaming { - fn finish(&mut self, size: usize) -> AudioFileStreaming { - let shared = Arc::new(AudioFileShared { - file_id: self.file_id, - file_size: size, - stream_data_rate: self.streaming_data_rate, - cond: Condvar::new(), - download_status: Mutex::new(AudioFileDownloadStatus { - requested: RangeSet::new(), - downloaded: RangeSet::new(), - }), - download_strategy: Mutex::new(DownloadStrategy::RandomAccess()), // start with random access mode until someone tells us otherwise - number_of_open_requests: AtomicUsize::new(0), - ping_time_ms: AtomicUsize::new(0), - read_position: AtomicUsize::new(0), - }); - - let mut write_file = NamedTempFile::new().unwrap(); - write_file.as_file().set_len(size as u64).unwrap(); - write_file.seek(SeekFrom::Start(0)).unwrap(); - - let read_file = write_file.reopen().unwrap(); - - let initial_data_rx = self.initial_data_rx.take().unwrap(); - let initial_data_length = self.initial_data_length.take().unwrap(); - let complete_tx = self.complete_tx.take().unwrap(); - //let (seek_tx, seek_rx) = mpsc::unbounded(); - let (stream_loader_command_tx, stream_loader_command_rx) = - mpsc::unbounded::(); - - let fetcher = AudioFileFetch::new( - self.session.clone(), - shared.clone(), - initial_data_rx, - self.initial_request_sent_time, - initial_data_length, - write_file, - stream_loader_command_rx, - complete_tx, - ); - self.session.spawn(move |_| fetcher); - - AudioFileStreaming { - read_file: read_file, - - position: 0, - //seek: seek_tx, - stream_loader_command_tx: stream_loader_command_tx, - - shared: shared, - } - } -} - -impl Future for AudioFileOpen { - type Item = AudioFile; - type Error = ChannelError; - - fn poll(&mut self) -> Poll { - match *self { - AudioFileOpen::Streaming(ref mut open) => { - let file = try_ready!(open.poll()); - Ok(Async::Ready(AudioFile::Streaming(file))) - } - AudioFileOpen::Cached(ref mut file) => { - let file = file.take().unwrap(); - Ok(Async::Ready(AudioFile::Cached(file))) - } - } - } -} - -impl Future for AudioFileOpenStreaming { - type Item = AudioFileStreaming; - type Error = ChannelError; - - fn poll(&mut self) -> Poll { - loop { - let (id, data) = try_ready!(self.headers.poll()).unwrap(); - - if id == 0x3 { - let size = BigEndian::read_u32(&data) as usize * 4; - let file = self.finish(size); - - return Ok(Async::Ready(file)); - } - } - } -} - -impl AudioFile { - pub fn open( - session: &Session, - file_id: FileId, - bytes_per_second: usize, - play_from_beginning: bool, - ) -> AudioFileOpen { - let cache = session.cache().cloned(); - - if let Some(file) = cache.as_ref().and_then(|cache| cache.file(file_id)) { - debug!("File {} already in cache", file_id); - return AudioFileOpen::Cached(Some(file)); - } - - debug!("Downloading file {}", file_id); - - let (complete_tx, complete_rx) = oneshot::channel(); - let mut initial_data_length = if play_from_beginning { - INITIAL_DOWNLOAD_SIZE - + max( - (READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, - (INITIAL_PING_TIME_ESTIMATE_SECONDS - * READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS - * bytes_per_second as f64) as usize, - ) - } else { - INITIAL_DOWNLOAD_SIZE - }; - if initial_data_length % 4 != 0 { - initial_data_length += 4 - (initial_data_length % 4); - } - let (headers, data) = request_range(session, file_id, 0, initial_data_length).split(); - - let open = AudioFileOpenStreaming { - session: session.clone(), - file_id: file_id, - - headers: headers, - initial_data_rx: Some(data), - initial_data_length: Some(initial_data_length), - initial_request_sent_time: Instant::now(), - - complete_tx: Some(complete_tx), - streaming_data_rate: bytes_per_second, - }; - - let session_ = session.clone(); - session.spawn(move |_| { - complete_rx - .map(move |mut file| { - if let Some(cache) = session_.cache() { - debug!("File {} complete, saving to cache", file_id); - cache.save_file(file_id, &mut file); - } else { - debug!("File {} complete", file_id); - } - }) - .or_else(|oneshot::Canceled| Ok(())) - }); - - return AudioFileOpen::Streaming(open); - } - - pub fn get_stream_loader_controller(&self) -> StreamLoaderController { - match self { - AudioFile::Streaming(ref stream) => { - return StreamLoaderController { - channel_tx: Some(stream.stream_loader_command_tx.clone()), - stream_shared: Some(stream.shared.clone()), - file_size: stream.shared.file_size, - }; - } - AudioFile::Cached(ref file) => { - return StreamLoaderController { - channel_tx: None, - stream_shared: None, - file_size: file.metadata().unwrap().len() as usize, - }; - } - } - } - - pub fn is_cached(&self) -> bool { - match self { - AudioFile::Cached { .. } => true, - _ => false, - } - } -} - -fn request_range(session: &Session, file: FileId, offset: usize, length: usize) -> Channel { - assert!( - offset % 4 == 0, - "Range request start positions must be aligned by 4 bytes." - ); - assert!( - length % 4 == 0, - "Range request range lengths must be aligned by 4 bytes." - ); - let start = offset / 4; - let end = (offset + length) / 4; - - let (id, channel) = session.channel().allocate(); - - let mut data: Vec = Vec::new(); - data.write_u16::(id).unwrap(); - data.write_u8(0).unwrap(); - data.write_u8(1).unwrap(); - data.write_u16::(0x0000).unwrap(); - data.write_u32::(0x00000000).unwrap(); - data.write_u32::(0x00009C40).unwrap(); - data.write_u32::(0x00020000).unwrap(); - data.write(&file.0).unwrap(); - data.write_u32::(start as u32).unwrap(); - data.write_u32::(end as u32).unwrap(); - - session.send_packet(0x8, data); - - channel -} - -struct PartialFileData { - offset: usize, - data: Bytes, -} - -enum ReceivedData { - ResponseTimeMs(usize), - Data(PartialFileData), -} - -struct AudioFileFetchDataReceiver { - shared: Arc, - file_data_tx: mpsc::UnboundedSender, - data_rx: ChannelData, - initial_data_offset: usize, - initial_request_length: usize, - data_offset: usize, - request_length: usize, - request_sent_time: Option, - measure_ping_time: bool, -} - -impl AudioFileFetchDataReceiver { - fn new( - shared: Arc, - file_data_tx: mpsc::UnboundedSender, - data_rx: ChannelData, - data_offset: usize, - request_length: usize, - request_sent_time: Instant, - ) -> AudioFileFetchDataReceiver { - let measure_ping_time = shared - .number_of_open_requests - .load(atomic::Ordering::SeqCst) - == 0; - - shared - .number_of_open_requests - .fetch_add(1, atomic::Ordering::SeqCst); - - AudioFileFetchDataReceiver { - shared: shared, - data_rx: data_rx, - file_data_tx: file_data_tx, - initial_data_offset: data_offset, - initial_request_length: request_length, - data_offset: data_offset, - request_length: request_length, - request_sent_time: Some(request_sent_time), - measure_ping_time: measure_ping_time, - } - } -} - -impl AudioFileFetchDataReceiver { - fn finish(&mut self) { - if self.request_length > 0 { - let missing_range = Range::new(self.data_offset, self.request_length); - - let mut download_status = self.shared.download_status.lock().unwrap(); - download_status.requested.subtract_range(&missing_range); - self.shared.cond.notify_all(); - } - - self.shared - .number_of_open_requests - .fetch_sub(1, atomic::Ordering::SeqCst); - } -} - -impl Future for AudioFileFetchDataReceiver { - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll<(), ()> { - loop { - match self.data_rx.poll() { - Ok(Async::Ready(Some(data))) => { - if self.measure_ping_time { - if let Some(request_sent_time) = self.request_sent_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 _ = self - .file_data_tx - .unbounded_send(ReceivedData::ResponseTimeMs(duration_ms as usize)); - self.measure_ping_time = false; - } - } - let data_size = data.len(); - let _ = self - .file_data_tx - .unbounded_send(ReceivedData::Data(PartialFileData { - offset: self.data_offset, - data: data, - })); - self.data_offset += data_size; - if self.request_length < data_size { - warn!("Data receiver for range {} (+{}) received more data from server than requested.", self.initial_data_offset, self.initial_request_length); - self.request_length = 0; - } else { - self.request_length -= data_size; - } - if self.request_length == 0 { - self.finish(); - return Ok(Async::Ready(())); - } - } - Ok(Async::Ready(None)) => { - if self.request_length > 0 { - warn!("Data receiver for range {} (+{}) received less data from server than requested.", self.initial_data_offset, self.initial_request_length); - } - self.finish(); - return Ok(Async::Ready(())); - } - Ok(Async::NotReady) => { - return Ok(Async::NotReady); - } - Err(ChannelError) => { - warn!( - "Error from channel for data receiver for range {} (+{}).", - self.initial_data_offset, self.initial_request_length - ); - self.finish(); - return Ok(Async::Ready(())); - } - } - } - } -} - -struct AudioFileFetch { - session: Session, - shared: Arc, - output: Option, - - file_data_tx: mpsc::UnboundedSender, - file_data_rx: mpsc::UnboundedReceiver, - - stream_loader_command_rx: mpsc::UnboundedReceiver, - complete_tx: Option>, - network_response_times_ms: Vec, -} - -impl AudioFileFetch { - fn new( - session: Session, - shared: Arc, - initial_data_rx: ChannelData, - initial_request_sent_time: Instant, - initial_data_length: usize, - - output: NamedTempFile, - stream_loader_command_rx: mpsc::UnboundedReceiver, - complete_tx: oneshot::Sender, - ) -> AudioFileFetch { - let (file_data_tx, file_data_rx) = unbounded::(); - - { - let requested_range = Range::new(0, initial_data_length); - let mut download_status = shared.download_status.lock().unwrap(); - download_status.requested.add_range(&requested_range); - } - - let initial_data_receiver = AudioFileFetchDataReceiver::new( - shared.clone(), - file_data_tx.clone(), - initial_data_rx, - 0, - initial_data_length, - initial_request_sent_time, - ); - - session.spawn(move |_| initial_data_receiver); - - AudioFileFetch { - session: session, - shared: shared, - output: Some(output), - - file_data_tx: file_data_tx, - file_data_rx: file_data_rx, - - stream_loader_command_rx: stream_loader_command_rx, - complete_tx: Some(complete_tx), - network_response_times_ms: Vec::new(), - } - } - - fn get_download_strategy(&mut self) -> DownloadStrategy { - *(self.shared.download_strategy.lock().unwrap()) - } - - fn download_range(&mut self, mut offset: usize, mut length: usize) { - if length < MINIMUM_DOWNLOAD_SIZE { - length = MINIMUM_DOWNLOAD_SIZE; - } - - // ensure the values are within the bounds and align them by 4 for the spotify protocol. - if offset >= self.shared.file_size { - return; - } - - if length <= 0 { - return; - } - - if offset + length > self.shared.file_size { - length = self.shared.file_size - offset; - } - - if offset % 4 != 0 { - length += offset % 4; - offset -= offset % 4; - } - - if length % 4 != 0 { - length += 4 - (length % 4); - } - - let mut ranges_to_request = RangeSet::new(); - ranges_to_request.add_range(&Range::new(offset, length)); - - let mut download_status = self.shared.download_status.lock().unwrap(); - - ranges_to_request.subtract_range_set(&download_status.downloaded); - ranges_to_request.subtract_range_set(&download_status.requested); - - for range in ranges_to_request.iter() { - let (_headers, data) = request_range( - &self.session, - self.shared.file_id, - range.start, - range.length, - ) - .split(); - - download_status.requested.add_range(range); - - let receiver = AudioFileFetchDataReceiver::new( - self.shared.clone(), - self.file_data_tx.clone(), - data, - range.start, - range.length, - Instant::now(), - ); - - self.session.spawn(move |_| receiver); - } - } - - fn pre_fetch_more_data(&mut self, bytes: usize, max_requests_to_send: usize) { - let mut bytes_to_go = bytes; - let mut requests_to_go = max_requests_to_send; - - while bytes_to_go > 0 && requests_to_go > 0 { - // determine what is still missing - let mut missing_data = RangeSet::new(); - missing_data.add_range(&Range::new(0, self.shared.file_size)); - { - let download_status = self.shared.download_status.lock().unwrap(); - missing_data.subtract_range_set(&download_status.downloaded); - missing_data.subtract_range_set(&download_status.requested); - } - - // download data from after the current read position first - let mut tail_end = RangeSet::new(); - let read_position = self.shared.read_position.load(atomic::Ordering::Relaxed); - tail_end.add_range(&Range::new( - read_position, - self.shared.file_size - read_position, - )); - let tail_end = tail_end.intersection(&missing_data); - - if !tail_end.is_empty() { - let range = tail_end.get_range(0); - let offset = range.start; - let length = min(range.length, bytes_to_go); - self.download_range(offset, length); - requests_to_go -= 1; - bytes_to_go -= length; - } else if !missing_data.is_empty() { - // ok, the tail is downloaded, download something fom the beginning. - let range = missing_data.get_range(0); - let offset = range.start; - let length = min(range.length, bytes_to_go); - self.download_range(offset, length); - requests_to_go -= 1; - bytes_to_go -= length; - } else { - return; - } - } - } - - fn poll_file_data_rx(&mut self) -> Poll<(), ()> { - loop { - match self.file_data_rx.poll() { - Ok(Async::Ready(None)) => { - return Ok(Async::Ready(())); - } - Ok(Async::Ready(Some(ReceivedData::ResponseTimeMs(response_time_ms)))) => { - trace!("Ping time estimated as: {} ms.", response_time_ms); - - // record the response time - self.network_response_times_ms.push(response_time_ms); - - // prune old response times. Keep at most three. - while self.network_response_times_ms.len() > 3 { - self.network_response_times_ms.remove(0); - } - - // stats::median is experimental. So we calculate the median of up to three ourselves. - let ping_time_ms: usize = match self.network_response_times_ms.len() { - 1 => self.network_response_times_ms[0] as usize, - 2 => { - ((self.network_response_times_ms[0] - + self.network_response_times_ms[1]) - / 2) as usize - } - 3 => { - let mut times = self.network_response_times_ms.clone(); - times.sort(); - times[1] - } - _ => unreachable!(), - }; - - // store our new estimate for everyone to see - self.shared - .ping_time_ms - .store(ping_time_ms, atomic::Ordering::Relaxed); - } - Ok(Async::Ready(Some(ReceivedData::Data(data)))) => { - self.output - .as_mut() - .unwrap() - .seek(SeekFrom::Start(data.offset as u64)) - .unwrap(); - self.output - .as_mut() - .unwrap() - .write_all(data.data.as_ref()) - .unwrap(); - - let mut full = false; - - { - let mut download_status = self.shared.download_status.lock().unwrap(); - - let received_range = Range::new(data.offset, data.data.len()); - download_status.downloaded.add_range(&received_range); - self.shared.cond.notify_all(); - - if download_status.downloaded.contained_length_from_value(0) - >= self.shared.file_size - { - full = true; - } - - drop(download_status); - } - - if full { - self.finish(); - return Ok(Async::Ready(())); - } - } - Ok(Async::NotReady) => { - return Ok(Async::NotReady); - } - Err(()) => unreachable!(), - } - } - } - - fn poll_stream_loader_command_rx(&mut self) -> Poll<(), ()> { - loop { - match self.stream_loader_command_rx.poll() { - Ok(Async::Ready(None)) => { - return Ok(Async::Ready(())); - } - Ok(Async::Ready(Some(StreamLoaderCommand::Fetch(request)))) => { - self.download_range(request.start, request.length); - } - Ok(Async::Ready(Some(StreamLoaderCommand::RandomAccessMode()))) => { - *(self.shared.download_strategy.lock().unwrap()) = - DownloadStrategy::RandomAccess(); - } - Ok(Async::Ready(Some(StreamLoaderCommand::StreamMode()))) => { - *(self.shared.download_strategy.lock().unwrap()) = - DownloadStrategy::Streaming(); - } - Ok(Async::Ready(Some(StreamLoaderCommand::Close()))) => { - return Ok(Async::Ready(())); - } - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(()) => unreachable!(), - } - } - } - - fn finish(&mut self) { - let mut output = self.output.take().unwrap(); - let complete_tx = self.complete_tx.take().unwrap(); - - output.seek(SeekFrom::Start(0)).unwrap(); - let _ = complete_tx.send(output); - } -} - -impl Future for AudioFileFetch { - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll<(), ()> { - match self.poll_stream_loader_command_rx() { - Ok(Async::NotReady) => (), - Ok(Async::Ready(_)) => { - return Ok(Async::Ready(())); - } - Err(()) => unreachable!(), - } - - match self.poll_file_data_rx() { - Ok(Async::NotReady) => (), - Ok(Async::Ready(_)) => { - return Ok(Async::Ready(())); - } - Err(()) => unreachable!(), - } - - if let DownloadStrategy::Streaming() = self.get_download_strategy() { - let number_of_open_requests = self - .shared - .number_of_open_requests - .load(atomic::Ordering::SeqCst); - let max_requests_to_send = - MAX_PREFETCH_REQUESTS - min(MAX_PREFETCH_REQUESTS, number_of_open_requests); - - if max_requests_to_send > 0 { - let bytes_pending: usize = { - let download_status = self.shared.download_status.lock().unwrap(); - download_status - .requested - .minus(&download_status.downloaded) - .len() - }; - - let ping_time_seconds = - 0.001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; - let download_rate = self.session.channel().get_download_rate_estimate(); - - let desired_pending_bytes = max( - (PREFETCH_THRESHOLD_FACTOR - * ping_time_seconds - * self.shared.stream_data_rate as f64) as usize, - (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f64) - as usize, - ); - - if bytes_pending < desired_pending_bytes { - self.pre_fetch_more_data( - desired_pending_bytes - bytes_pending, - max_requests_to_send, - ); - } - } - } - - return Ok(Async::NotReady); - } -} - -impl Read for AudioFileStreaming { - fn read(&mut self, output: &mut [u8]) -> io::Result { - let offset = self.position as usize; - - if offset >= self.shared.file_size { - return Ok(0); - } - - let length = min(output.len(), self.shared.file_size - offset); - - let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) { - DownloadStrategy::RandomAccess() => length, - DownloadStrategy::Streaming() => { - // Due to the read-ahead stuff, we potentially request more than the actual reqeust demanded. - let ping_time_seconds = - 0.0001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; - - let length_to_request = length - + max( - (READ_AHEAD_DURING_PLAYBACK_SECONDS * self.shared.stream_data_rate as f64) - as usize, - (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS - * ping_time_seconds - * self.shared.stream_data_rate as f64) as usize, - ); - min(length_to_request, self.shared.file_size - offset) - } - }; - - let mut ranges_to_request = RangeSet::new(); - ranges_to_request.add_range(&Range::new(offset, length_to_request)); - - let mut download_status = self.shared.download_status.lock().unwrap(); - ranges_to_request.subtract_range_set(&download_status.downloaded); - ranges_to_request.subtract_range_set(&download_status.requested); - - for range in ranges_to_request.iter() { - self.stream_loader_command_tx - .unbounded_send(StreamLoaderCommand::Fetch(range.clone())) - .unwrap(); - } - - if length == 0 { - return Ok(0); - } - - let mut download_message_printed = false; - while !download_status.downloaded.contains(offset) { - if let DownloadStrategy::Streaming() = *self.shared.download_strategy.lock().unwrap() { - if !download_message_printed { - debug!("Stream waiting for download of file position {}. Downloaded ranges: {}. Pending ranges: {}", offset, download_status.downloaded, download_status.requested.minus(&download_status.downloaded)); - download_message_printed = true; - } - } - download_status = self - .shared - .cond - .wait_timeout(download_status, Duration::from_millis(1000)) - .unwrap() - .0; - } - let available_length = download_status - .downloaded - .contained_length_from_value(offset); - assert!(available_length > 0); - drop(download_status); - - self.position = self.read_file.seek(SeekFrom::Start(offset as u64)).unwrap(); - let read_len = min(length, available_length); - let read_len = self.read_file.read(&mut output[..read_len])?; - - if download_message_printed { - debug!( - "Read at postion {} completed. {} bytes returned, {} bytes were requested.", - offset, - read_len, - output.len() - ); - } - - self.position += read_len as u64; - self.shared - .read_position - .store(self.position as usize, atomic::Ordering::Relaxed); - - return Ok(read_len); - } -} - -impl Seek for AudioFileStreaming { - fn seek(&mut self, pos: SeekFrom) -> io::Result { - self.position = self.read_file.seek(pos)?; - // Do not seek past EOF - self.shared - .read_position - .store(self.position as usize, atomic::Ordering::Relaxed); - Ok(self.position) - } -} - -impl Read for AudioFile { - fn read(&mut self, output: &mut [u8]) -> io::Result { - match *self { - AudioFile::Cached(ref mut file) => file.read(output), - AudioFile::Streaming(ref mut file) => file.read(output), - } - } -} - -impl Seek for AudioFile { - fn seek(&mut self, pos: SeekFrom) -> io::Result { - match *self { - AudioFile::Cached(ref mut file) => file.seek(pos), - AudioFile::Streaming(ref mut file) => file.seek(pos), - } - } -} diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs new file mode 100644 index 000000000..c19fac2ec --- /dev/null +++ b/audio/src/fetch/mod.rs @@ -0,0 +1,509 @@ +mod receive; + +use std::cmp::{max, min}; +use std::fs; +use std::io::{self, Read, Seek, SeekFrom}; +use std::sync::atomic::{self, AtomicUsize}; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::{Duration, Instant}; + +use byteorder::{BigEndian, ByteOrder}; +use futures_util::{future, StreamExt, TryFutureExt, TryStreamExt}; +use librespot_core::channel::{ChannelData, ChannelError, ChannelHeaders}; +use librespot_core::session::Session; +use librespot_core::spotify_id::FileId; +use tempfile::NamedTempFile; +use tokio::sync::{mpsc, oneshot}; + +use self::receive::{audio_file_fetch, request_range}; +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. +// This is the block size that is typically requested while doing a seek() on a file. +// Note: smaller requests can happen if part of the block is downloaded already. + +const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 16; +// The amount of data that is requested when initially opening a file. +// Note: if the file is opened to play from the beginning, the amount of data to +// read ahead is requested in addition to this amount. If the file is opened to seek to +// another position, then only this amount is requested on the first request. + +const INITIAL_PING_TIME_ESTIMATE_SECONDS: f64 = 0.5; +// The pig time that is used for calculations before a ping time was actually measured. + +const MAXIMUM_ASSUMED_PING_TIME_SECONDS: f64 = 1.5; +// If the measured ping time to the Spotify server is larger than this value, it is capped +// to avoid run-away block sizes and pre-fetching. + +pub const READ_AHEAD_BEFORE_PLAYBACK_SECONDS: f64 = 1.0; +// Before playback starts, this many seconds of data must be present. +// Note: the calculations are done using the nominal bitrate of the file. The actual amount +// of audio data may be larger or smaller. + +pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f64 = 2.0; +// Same as READ_AHEAD_BEFORE_PLAYBACK_SECONDS, but the time is taken as a factor of the ping +// time to the Spotify server. +// Both, READ_AHEAD_BEFORE_PLAYBACK_SECONDS and READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS are +// obeyed. +// Note: the calculations are done using the nominal bitrate of the file. The actual amount +// of audio data may be larger or smaller. + +pub const READ_AHEAD_DURING_PLAYBACK_SECONDS: f64 = 5.0; +// While playing back, this many seconds of data ahead of the current read position are +// requested. +// Note: the calculations are done using the nominal bitrate of the file. The actual amount +// of audio data may be larger or smaller. + +pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f64 = 10.0; +// Same as READ_AHEAD_DURING_PLAYBACK_SECONDS, but the time is taken as a factor of the ping +// time to the Spotify server. +// Note: the calculations are done using the nominal bitrate of the file. The actual amount +// of audio data may be larger or smaller. + +const PREFETCH_THRESHOLD_FACTOR: f64 = 4.0; +// If the amount of data that is pending (requested but not received) is less than a certain amount, +// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more +// data is calculated as +// < PREFETCH_THRESHOLD_FACTOR * * + +const FAST_PREFETCH_THRESHOLD_FACTOR: f64 = 1.5; +// Similar to PREFETCH_THRESHOLD_FACTOR, but it also takes the current download rate into account. +// The formula used is +// < FAST_PREFETCH_THRESHOLD_FACTOR * * +// This mechanism allows for fast downloading of the remainder of the file. The number should be larger +// than 1 so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster +// the download rate ramps up. However, this comes at the cost that it might hurt ping-time if a seek is +// performed while downloading. Values smaller than 1 cause the download rate to collapse and effectively +// only PREFETCH_THRESHOLD_FACTOR is in effect. Thus, set to zero if bandwidth saturation is not wanted. + +const MAX_PREFETCH_REQUESTS: usize = 4; +// Limit the number of requests that are pending simultaneously before pre-fetching data. Pending +// requests share bandwidth. Thus, havint too many requests can lead to the one that is needed next +// for playback to be delayed leading to a buffer underrun. This limit has the effect that a new +// pre-fetch request is only sent if less than MAX_PREFETCH_REQUESTS are pending. + +pub enum AudioFile { + Cached(fs::File), + 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 + StreamMode(), // optimise download strategy for streaming + Close(), // terminate and don't load any more data +} + +#[derive(Clone)] +pub struct StreamLoaderController { + channel_tx: Option>, + stream_shared: Option>, + file_size: usize, +} + +impl StreamLoaderController { + pub fn len(&self) -> usize { + self.file_size + } + + pub fn is_empty(&self) -> bool { + self.file_size == 0 + } + + pub fn range_available(&self, range: Range) -> bool { + if let Some(ref shared) = self.stream_shared { + let download_status = shared.download_status.lock().unwrap(); + range.length + <= download_status + .downloaded + .contained_length_from_value(range.start) + } else { + range.length <= self.len() - range.start + } + } + + pub fn range_to_end_available(&self) -> bool { + self.stream_shared.as_ref().map_or(true, |shared| { + let read_position = shared.read_position.load(atomic::Ordering::Relaxed); + self.range_available(Range::new(read_position, self.len() - read_position)) + }) + } + + pub fn ping_time_ms(&self) -> usize { + self.stream_shared.as_ref().map_or(0, |shared| { + shared.ping_time_ms.load(atomic::Ordering::Relaxed) + }) + } + + fn send_stream_loader_command(&self, command: StreamLoaderCommand) { + if let Some(ref channel) = self.channel_tx { + // ignore the error in case the channel has been closed already. + let _ = channel.send(command); + } + } + + pub fn fetch(&self, range: Range) { + // signal the stream loader to fetch a range of the file + self.send_stream_loader_command(StreamLoaderCommand::Fetch(range)); + } + + pub fn fetch_blocking(&self, mut range: Range) { + // signal the stream loader to tech a range of the file and block until it is loaded. + + // ensure the range is within the file's bounds. + if range.start >= self.len() { + range.length = 0; + } else if range.end() > self.len() { + range.length = self.len() - range.start; + } + + self.fetch(range); + + if let Some(ref shared) = self.stream_shared { + let mut download_status = shared.download_status.lock().unwrap(); + while range.length + > download_status + .downloaded + .contained_length_from_value(range.start) + { + download_status = shared + .cond + .wait_timeout(download_status, Duration::from_millis(1000)) + .unwrap() + .0; + if range.length + > (download_status + .downloaded + .union(&download_status.requested) + .contained_length_from_value(range.start)) + { + // For some reason, the requested range is neither downloaded nor requested. + // This could be due to a network error. Request it again. + self.fetch(range); + } + } + } + } + + pub fn fetch_next(&self, length: usize) { + if let Some(ref shared) = self.stream_shared { + let range = Range { + start: shared.read_position.load(atomic::Ordering::Relaxed), + length, + }; + self.fetch(range) + } + } + + pub fn fetch_next_blocking(&self, length: usize) { + if let Some(ref shared) = self.stream_shared { + let range = Range { + start: shared.read_position.load(atomic::Ordering::Relaxed), + length, + }; + self.fetch_blocking(range); + } + } + + pub fn set_random_access_mode(&self) { + // optimise download strategy for random access + self.send_stream_loader_command(StreamLoaderCommand::RandomAccessMode()); + } + + pub fn set_stream_mode(&self) { + // optimise download strategy for streaming + self.send_stream_loader_command(StreamLoaderCommand::StreamMode()); + } + + pub fn close(&self) { + // terminate stream loading and don't load any more data for this file. + self.send_stream_loader_command(StreamLoaderCommand::Close()); + } +} + +pub struct AudioFileStreaming { + read_file: fs::File, + position: u64, + stream_loader_command_tx: mpsc::UnboundedSender, + shared: Arc, +} + +struct AudioFileDownloadStatus { + requested: RangeSet, + downloaded: RangeSet, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum DownloadStrategy { + RandomAccess(), + Streaming(), +} + +struct AudioFileShared { + file_id: FileId, + file_size: usize, + stream_data_rate: usize, + cond: Condvar, + download_status: Mutex, + download_strategy: Mutex, + ping_time_ms: AtomicUsize, + read_position: AtomicUsize, +} + +impl AudioFile { + pub async fn open( + session: &Session, + file_id: FileId, + bytes_per_second: usize, + play_from_beginning: bool, + ) -> Result { + if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) { + debug!("File {} already in cache", file_id); + return Ok(AudioFile::Cached(file)); + } + + debug!("Downloading file {}", file_id); + + let (complete_tx, complete_rx) = oneshot::channel(); + let mut initial_data_length = if play_from_beginning { + INITIAL_DOWNLOAD_SIZE + + max( + (READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, + (INITIAL_PING_TIME_ESTIMATE_SECONDS + * READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS + * bytes_per_second as f64) as usize, + ) + } else { + INITIAL_DOWNLOAD_SIZE + }; + if initial_data_length % 4 != 0 { + initial_data_length += 4 - (initial_data_length % 4); + } + let (headers, data) = request_range(session, file_id, 0, initial_data_length).split(); + + let streaming = AudioFileStreaming::open( + session.clone(), + data, + initial_data_length, + Instant::now(), + headers, + file_id, + complete_tx, + bytes_per_second, + ); + + let session_ = session.clone(); + session.spawn(complete_rx.map_ok(move |mut file| { + if let Some(cache) = session_.cache() { + debug!("File {} complete, saving to cache", file_id); + cache.save_file(file_id, &mut file); + } else { + debug!("File {} complete", file_id); + } + })); + + Ok(AudioFile::Streaming(streaming.await?)) + } + + pub fn get_stream_loader_controller(&self) -> StreamLoaderController { + match self { + AudioFile::Streaming(ref stream) => StreamLoaderController { + channel_tx: Some(stream.stream_loader_command_tx.clone()), + stream_shared: Some(stream.shared.clone()), + file_size: stream.shared.file_size, + }, + AudioFile::Cached(ref file) => StreamLoaderController { + channel_tx: None, + stream_shared: None, + file_size: file.metadata().unwrap().len() as usize, + }, + } + } + + pub fn is_cached(&self) -> bool { + matches!(self, AudioFile::Cached { .. }) + } +} + +impl AudioFileStreaming { + pub async fn open( + session: Session, + initial_data_rx: ChannelData, + initial_data_length: usize, + initial_request_sent_time: Instant, + headers: ChannelHeaders, + file_id: FileId, + complete_tx: oneshot::Sender, + streaming_data_rate: usize, + ) -> Result { + let (_, data) = headers + .try_filter(|(id, _)| future::ready(*id == 0x3)) + .next() + .await + .unwrap()?; + + let size = BigEndian::read_u32(&data) as usize * 4; + + let shared = Arc::new(AudioFileShared { + file_id, + file_size: size, + stream_data_rate: streaming_data_rate, + cond: Condvar::new(), + download_status: Mutex::new(AudioFileDownloadStatus { + requested: RangeSet::new(), + downloaded: RangeSet::new(), + }), + download_strategy: Mutex::new(DownloadStrategy::RandomAccess()), // start with random access mode until someone tells us otherwise + ping_time_ms: AtomicUsize::new(0), + read_position: AtomicUsize::new(0), + }); + + let mut write_file = NamedTempFile::new().unwrap(); + write_file.as_file().set_len(size as u64).unwrap(); + write_file.seek(SeekFrom::Start(0)).unwrap(); + + let read_file = write_file.reopen().unwrap(); + + //let (seek_tx, seek_rx) = mpsc::unbounded(); + let (stream_loader_command_tx, stream_loader_command_rx) = + mpsc::unbounded_channel::(); + + session.spawn(audio_file_fetch( + session.clone(), + shared.clone(), + initial_data_rx, + initial_request_sent_time, + initial_data_length, + write_file, + stream_loader_command_rx, + complete_tx, + )); + + Ok(AudioFileStreaming { + read_file, + position: 0, + stream_loader_command_tx, + shared, + }) + } +} + +impl Read for AudioFileStreaming { + fn read(&mut self, output: &mut [u8]) -> io::Result { + let offset = self.position as usize; + + if offset >= self.shared.file_size { + return Ok(0); + } + + let length = min(output.len(), self.shared.file_size - offset); + + let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) { + DownloadStrategy::RandomAccess() => length, + DownloadStrategy::Streaming() => { + // Due to the read-ahead stuff, we potentially request more than the actual reqeust demanded. + let ping_time_seconds = + 0.0001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; + + let length_to_request = length + + max( + (READ_AHEAD_DURING_PLAYBACK_SECONDS * self.shared.stream_data_rate as f64) + as usize, + (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS + * ping_time_seconds + * self.shared.stream_data_rate as f64) as usize, + ); + min(length_to_request, self.shared.file_size - offset) + } + }; + + let mut ranges_to_request = RangeSet::new(); + ranges_to_request.add_range(&Range::new(offset, length_to_request)); + + let mut download_status = self.shared.download_status.lock().unwrap(); + ranges_to_request.subtract_range_set(&download_status.downloaded); + ranges_to_request.subtract_range_set(&download_status.requested); + + for &range in ranges_to_request.iter() { + self.stream_loader_command_tx + .send(StreamLoaderCommand::Fetch(range)) + .unwrap(); + } + + if length == 0 { + return Ok(0); + } + + let mut download_message_printed = false; + while !download_status.downloaded.contains(offset) { + if let DownloadStrategy::Streaming() = *self.shared.download_strategy.lock().unwrap() { + if !download_message_printed { + debug!("Stream waiting for download of file position {}. Downloaded ranges: {}. Pending ranges: {}", offset, download_status.downloaded, download_status.requested.minus(&download_status.downloaded)); + download_message_printed = true; + } + } + download_status = self + .shared + .cond + .wait_timeout(download_status, Duration::from_millis(1000)) + .unwrap() + .0; + } + let available_length = download_status + .downloaded + .contained_length_from_value(offset); + assert!(available_length > 0); + drop(download_status); + + self.position = self.read_file.seek(SeekFrom::Start(offset as u64)).unwrap(); + let read_len = min(length, available_length); + let read_len = self.read_file.read(&mut output[..read_len])?; + + if download_message_printed { + debug!( + "Read at postion {} completed. {} bytes returned, {} bytes were requested.", + offset, + read_len, + output.len() + ); + } + + self.position += read_len as u64; + self.shared + .read_position + .store(self.position as usize, atomic::Ordering::Relaxed); + + Ok(read_len) + } +} + +impl Seek for AudioFileStreaming { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + self.position = self.read_file.seek(pos)?; + // Do not seek past EOF + self.shared + .read_position + .store(self.position as usize, atomic::Ordering::Relaxed); + Ok(self.position) + } +} + +impl Read for AudioFile { + fn read(&mut self, output: &mut [u8]) -> io::Result { + match *self { + AudioFile::Cached(ref mut file) => file.read(output), + AudioFile::Streaming(ref mut file) => file.read(output), + } + } +} + +impl Seek for AudioFile { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + match *self { + AudioFile::Cached(ref mut file) => file.seek(pos), + AudioFile::Streaming(ref mut file) => file.seek(pos), + } + } +} diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs new file mode 100644 index 000000000..17f884f5e --- /dev/null +++ b/audio/src/fetch/receive.rs @@ -0,0 +1,455 @@ +use std::cmp::{max, min}; +use std::io::{Seek, SeekFrom, Write}; +use std::sync::{atomic, Arc}; +use std::time::Instant; + +use byteorder::{BigEndian, WriteBytesExt}; +use bytes::Bytes; +use futures_util::StreamExt; +use librespot_core::channel::{Channel, ChannelData}; +use librespot_core::session::Session; +use librespot_core::spotify_id::FileId; +use tempfile::NamedTempFile; +use tokio::sync::{mpsc, oneshot}; + +use crate::range_set::{Range, RangeSet}; + +use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand}; +use super::{ + FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME_SECONDS, MAX_PREFETCH_REQUESTS, + MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, +}; + +pub fn request_range(session: &Session, file: FileId, offset: usize, length: usize) -> Channel { + assert!( + offset % 4 == 0, + "Range request start positions must be aligned by 4 bytes." + ); + assert!( + length % 4 == 0, + "Range request range lengths must be aligned by 4 bytes." + ); + let start = offset / 4; + let end = (offset + length) / 4; + + let (id, channel) = session.channel().allocate(); + + let mut data: Vec = Vec::new(); + data.write_u16::(id).unwrap(); + data.write_u8(0).unwrap(); + data.write_u8(1).unwrap(); + data.write_u16::(0x0000).unwrap(); + data.write_u32::(0x00000000).unwrap(); + data.write_u32::(0x00009C40).unwrap(); + data.write_u32::(0x00020000).unwrap(); + data.write(&file.0).unwrap(); + data.write_u32::(start as u32).unwrap(); + data.write_u32::(end as u32).unwrap(); + + session.send_packet(0x8, data); + + channel +} + +struct PartialFileData { + offset: usize, + data: Bytes, +} + +enum ReceivedData { + ResponseTimeMs(usize), + Data(PartialFileData), +} + +async fn receive_data( + shared: Arc, + file_data_tx: mpsc::UnboundedSender, + mut data_rx: ChannelData, + initial_data_offset: usize, + initial_request_length: usize, + request_sent_time: Instant, + mut measure_ping_time: bool, + finish_tx: mpsc::UnboundedSender<()>, +) { + let mut data_offset = initial_data_offset; + let mut request_length = initial_request_length; + + let result = loop { + let data = match data_rx.next().await { + Some(Ok(data)) => data, + Some(Err(e)) => break Err(e), + None => break Ok(()), + }; + + 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.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_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; + } + + if request_length == 0 { + break Ok(()); + } + }; + + if request_length > 0 { + let missing_range = Range::new(data_offset, request_length); + + let mut download_status = shared.download_status.lock().unwrap(); + download_status.requested.subtract_range(&missing_range); + shared.cond.notify_all(); + } + + let _ = finish_tx.send(()); + + if result.is_err() { + warn!( + "Error from channel for data receiver for range {} (+{}).", + initial_data_offset, initial_request_length + ); + } else if request_length > 0 { + warn!( + "Data receiver for range {} (+{}) received less data from server than requested.", + initial_data_offset, initial_request_length + ); + } +} + +struct AudioFileFetch { + session: Session, + shared: Arc, + output: Option, + + file_data_tx: mpsc::UnboundedSender, + complete_tx: Option>, + network_response_times_ms: Vec, + number_of_open_requests: usize, + + download_finish_tx: mpsc::UnboundedSender<()>, +} + +// Might be replaced by enum from std once stable +#[derive(PartialEq, Eq)] +enum ControlFlow { + Break, + Continue, +} + +impl AudioFileFetch { + fn get_download_strategy(&mut self) -> DownloadStrategy { + *(self.shared.download_strategy.lock().unwrap()) + } + + fn download_range(&mut self, mut offset: usize, mut length: usize) { + if length < MINIMUM_DOWNLOAD_SIZE { + length = MINIMUM_DOWNLOAD_SIZE; + } + + // ensure the values are within the bounds and align them by 4 for the spotify protocol. + if offset >= self.shared.file_size { + return; + } + + if length == 0 { + return; + } + + if offset + length > self.shared.file_size { + length = self.shared.file_size - offset; + } + + if offset % 4 != 0 { + length += offset % 4; + offset -= offset % 4; + } + + if length % 4 != 0 { + length += 4 - (length % 4); + } + + let mut ranges_to_request = RangeSet::new(); + ranges_to_request.add_range(&Range::new(offset, length)); + + let mut download_status = self.shared.download_status.lock().unwrap(); + + ranges_to_request.subtract_range_set(&download_status.downloaded); + ranges_to_request.subtract_range_set(&download_status.requested); + + for range in ranges_to_request.iter() { + let (_headers, data) = request_range( + &self.session, + self.shared.file_id, + range.start, + range.length, + ) + .split(); + + download_status.requested.add_range(range); + + self.session.spawn(receive_data( + self.shared.clone(), + self.file_data_tx.clone(), + data, + range.start, + range.length, + Instant::now(), + self.number_of_open_requests == 0, + self.download_finish_tx.clone(), + )); + + self.number_of_open_requests += 1; + } + } + + fn pre_fetch_more_data(&mut self, bytes: usize, max_requests_to_send: usize) { + let mut bytes_to_go = bytes; + let mut requests_to_go = max_requests_to_send; + + while bytes_to_go > 0 && requests_to_go > 0 { + // determine what is still missing + let mut missing_data = RangeSet::new(); + missing_data.add_range(&Range::new(0, self.shared.file_size)); + { + let download_status = self.shared.download_status.lock().unwrap(); + missing_data.subtract_range_set(&download_status.downloaded); + missing_data.subtract_range_set(&download_status.requested); + } + + // download data from after the current read position first + let mut tail_end = RangeSet::new(); + let read_position = self.shared.read_position.load(atomic::Ordering::Relaxed); + tail_end.add_range(&Range::new( + read_position, + self.shared.file_size - read_position, + )); + let tail_end = tail_end.intersection(&missing_data); + + if !tail_end.is_empty() { + let range = tail_end.get_range(0); + let offset = range.start; + let length = min(range.length, bytes_to_go); + self.download_range(offset, length); + requests_to_go -= 1; + bytes_to_go -= length; + } else if !missing_data.is_empty() { + // ok, the tail is downloaded, download something fom the beginning. + let range = missing_data.get_range(0); + let offset = range.start; + let length = min(range.length, bytes_to_go); + self.download_range(offset, length); + requests_to_go -= 1; + bytes_to_go -= length; + } else { + return; + } + } + } + + fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow { + match data { + ReceivedData::ResponseTimeMs(response_time_ms) => { + trace!("Ping time estimated as: {} ms.", response_time_ms); + + // record the response time + self.network_response_times_ms.push(response_time_ms); + + // prune old response times. Keep at most three. + while self.network_response_times_ms.len() > 3 { + self.network_response_times_ms.remove(0); + } + + // stats::median is experimental. So we calculate the median of up to three ourselves. + let ping_time_ms: usize = match self.network_response_times_ms.len() { + 1 => self.network_response_times_ms[0] as usize, + 2 => { + ((self.network_response_times_ms[0] + self.network_response_times_ms[1]) + / 2) as usize + } + 3 => { + let mut times = self.network_response_times_ms.clone(); + times.sort_unstable(); + times[1] + } + _ => unreachable!(), + }; + + // store our new estimate for everyone to see + self.shared + .ping_time_ms + .store(ping_time_ms, atomic::Ordering::Relaxed); + } + ReceivedData::Data(data) => { + self.output + .as_mut() + .unwrap() + .seek(SeekFrom::Start(data.offset as u64)) + .unwrap(); + self.output + .as_mut() + .unwrap() + .write_all(data.data.as_ref()) + .unwrap(); + + let mut download_status = self.shared.download_status.lock().unwrap(); + + let received_range = Range::new(data.offset, data.data.len()); + download_status.downloaded.add_range(&received_range); + self.shared.cond.notify_all(); + + let full = download_status.downloaded.contained_length_from_value(0) + >= self.shared.file_size; + + drop(download_status); + + if full { + self.finish(); + return ControlFlow::Break; + } + } + } + ControlFlow::Continue + } + + fn handle_stream_loader_command(&mut self, cmd: StreamLoaderCommand) -> ControlFlow { + match cmd { + StreamLoaderCommand::Fetch(request) => { + self.download_range(request.start, request.length); + } + StreamLoaderCommand::RandomAccessMode() => { + *(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::RandomAccess(); + } + StreamLoaderCommand::StreamMode() => { + *(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::Streaming(); + self.trigger_preload(); + } + StreamLoaderCommand::Close() => return ControlFlow::Break, + } + ControlFlow::Continue + } + + fn finish(&mut self) { + let mut output = self.output.take().unwrap(); + let complete_tx = self.complete_tx.take().unwrap(); + + output.seek(SeekFrom::Start(0)).unwrap(); + let _ = complete_tx.send(output); + } + + fn trigger_preload(&mut self) { + if self.number_of_open_requests >= MAX_PREFETCH_REQUESTS { + return; + } + + let max_requests_to_send = MAX_PREFETCH_REQUESTS - self.number_of_open_requests; + + let bytes_pending: usize = { + let download_status = self.shared.download_status.lock().unwrap(); + download_status + .requested + .minus(&download_status.downloaded) + .len() + }; + + let ping_time_seconds = + 0.001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; + let download_rate = self.session.channel().get_download_rate_estimate(); + + let desired_pending_bytes = max( + (PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * self.shared.stream_data_rate as f64) + as usize, + (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f64) as usize, + ); + + if bytes_pending < desired_pending_bytes { + self.pre_fetch_more_data(desired_pending_bytes - bytes_pending, max_requests_to_send); + } + } +} + +pub(super) async fn audio_file_fetch( + session: Session, + shared: Arc, + initial_data_rx: ChannelData, + initial_request_sent_time: Instant, + initial_data_length: usize, + + output: NamedTempFile, + mut stream_loader_command_rx: mpsc::UnboundedReceiver, + complete_tx: oneshot::Sender, +) { + let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel(); + let (download_finish_tx, mut download_finish_rx) = mpsc::unbounded_channel(); + + { + let requested_range = Range::new(0, initial_data_length); + let mut download_status = shared.download_status.lock().unwrap(); + download_status.requested.add_range(&requested_range); + } + + session.spawn(receive_data( + shared.clone(), + file_data_tx.clone(), + initial_data_rx, + 0, + initial_data_length, + initial_request_sent_time, + true, + download_finish_tx.clone(), + )); + + let mut fetch = AudioFileFetch { + session, + shared, + output: Some(output), + + file_data_tx, + complete_tx: Some(complete_tx), + network_response_times_ms: Vec::new(), + number_of_open_requests: 1, + + download_finish_tx, + }; + + loop { + tokio::select! { + cmd = stream_loader_command_rx.recv() => { + if cmd.map_or(true, |cmd| fetch.handle_stream_loader_command(cmd) == ControlFlow::Break) { + break; + } + }, + data = file_data_rx.recv() => { + if data.map_or(true, |data| fetch.handle_file_data(data) == ControlFlow::Break) { + break; + } + }, + _ = download_finish_rx.recv() => { + fetch.number_of_open_requests -= 1; + + if fetch.get_download_strategy() == DownloadStrategy::Streaming() { + fetch.trigger_preload(); + } + } + } + } +} diff --git a/audio/src/lewton_decoder.rs b/audio/src/lewton_decoder.rs index 8e7d089ea..528d9344e 100644 --- a/audio/src/lewton_decoder.rs +++ b/audio/src/lewton_decoder.rs @@ -1,8 +1,7 @@ -extern crate lewton; +use super::{AudioDecoder, AudioError, AudioPacket}; -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}; @@ -26,16 +25,15 @@ where fn seek(&mut self, ms: i64) -> Result<(), AudioError> { let absgp = ms * 44100 / 1000; match self.0.seek_absgp_pg(absgp as u64) { - Ok(_) => return Ok(()), - Err(err) => return Err(AudioError::VorbisError(err.into())), + Ok(_) => Ok(()), + Err(err) => Err(AudioError::VorbisError(err.into())), } } fn next_packet(&mut self) -> Result, AudioError> { - use self::lewton::audio::AudioReadError::AudioIsHeader; - use self::lewton::OggReadError::NoCapturePatternFound; - use self::lewton::VorbisError::BadAudio; - use self::lewton::VorbisError::OggError; + use lewton::audio::AudioReadError::AudioIsHeader; + use lewton::OggReadError::NoCapturePatternFound; + use lewton::VorbisError::{BadAudio, OggError}; loop { match self .0 diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 44f732b33..b587f038c 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -1,33 +1,31 @@ -#[macro_use] -extern crate futures; +#![allow(clippy::unused_io_amount, clippy::too_many_arguments)] + #[macro_use] extern crate log; -extern crate aes_ctr; -extern crate bit_set; -extern crate byteorder; -extern crate bytes; -extern crate num_bigint; -extern crate num_traits; -extern crate tempfile; - -extern crate librespot_core; - -mod convert; +pub mod convert; 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_if! { + if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] { + mod libvorbis_decoder; + pub use crate::libvorbis_decoder::{VorbisDecoder, VorbisError}; + } else { + mod lewton_decoder; + pub use lewton_decoder::{VorbisDecoder, VorbisError}; + } +} + mod passthrough_decoder; +pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; mod range_set; -pub use convert::{i24, SamplesConverter}; pub use decrypt::AudioDecrypt; -pub use fetch::{AudioFile, AudioFileOpen, StreamLoaderController}; +pub use fetch::{AudioFile, StreamLoaderController}; pub use fetch::{ READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, @@ -62,12 +60,6 @@ impl AudioPacket { } } -#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] -pub use crate::lewton_decoder::{VorbisDecoder, VorbisError}; -#[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] -pub use libvorbis_decoder::{VorbisDecoder, VorbisError}; -pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; - #[derive(Debug)] pub enum AudioError { PassthroughError(PassthroughError), @@ -85,13 +77,13 @@ impl fmt::Display for AudioError { impl From for AudioError { fn from(err: VorbisError) -> AudioError { - AudioError::VorbisError(VorbisError::from(err)) + AudioError::VorbisError(err) } } impl From for AudioError { fn from(err: PassthroughError) -> AudioError { - AudioError::PassthroughError(PassthroughError::from(err)) + AudioError::PassthroughError(err) } } diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index 5ad48f0d1..6f9a68a3a 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -1,7 +1,5 @@ #[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; diff --git a/audio/src/passthrough_decoder.rs b/audio/src/passthrough_decoder.rs index 082f6915a..e064cba38 100644 --- a/audio/src/passthrough_decoder.rs +++ b/audio/src/passthrough_decoder.rs @@ -18,7 +18,7 @@ where return Err(PassthroughError(OggReadError::InvalidData)); } - return Ok(pck.data.into_boxed_slice()); + Ok(pck.data.into_boxed_slice()) } pub struct PassthroughDecoder { @@ -54,7 +54,7 @@ impl PassthroughDecoder { // remove un-needed packets rdr.delete_unread_packets(); - return Ok(PassthroughDecoder { + Ok(PassthroughDecoder { rdr, wtr: PacketWriter::new(Vec::new()), ofsgp_page: 0, @@ -64,7 +64,7 @@ impl PassthroughDecoder { setup, eos: false, bos: false, - }); + }) } } @@ -102,15 +102,15 @@ impl AudioDecoder for PassthroughDecoder { let pck = self.rdr.read_packet().unwrap().unwrap(); self.ofsgp_page = pck.absgp_page(); debug!("Seek to offset page {}", self.ofsgp_page); - return Ok(()); + Ok(()) } - Err(err) => return Err(AudioError::PassthroughError(err.into())), + Err(err) => Err(AudioError::PassthroughError(err.into())), } } fn next_packet(&mut self) -> Result, AudioError> { // write headers if we are (re)starting - if self.bos == false { + if !self.bos { self.wtr .write_packet( self.ident.clone(), @@ -177,7 +177,7 @@ impl AudioDecoder for PassthroughDecoder { let data = self.wtr.inner_mut(); - if data.len() > 0 { + if !data.is_empty() { let result = AudioPacket::OggData(std::mem::take(data)); return Ok(Some(result)); } diff --git a/audio/src/range_set.rs b/audio/src/range_set.rs index 449553885..f74058a31 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, @@ -16,14 +16,11 @@ impl fmt::Display for Range { impl Range { pub fn new(start: usize, length: usize) -> Range { - return Range { - start: start, - length: length, - }; + Range { start, length } } pub fn end(&self) -> usize { - return self.start + self.length; + self.start + self.length } } @@ -50,23 +47,19 @@ impl RangeSet { } pub fn is_empty(&self) -> bool { - return self.ranges.is_empty(); + self.ranges.is_empty() } pub fn len(&self) -> usize { - let mut result = 0; - for range in self.ranges.iter() { - result += range.length; - } - return result; + self.ranges.iter().map(|r| r.length).sum() } pub fn get_range(&self, index: usize) -> Range { - return self.ranges[index].clone(); + self.ranges[index] } pub fn iter(&self) -> Iter { - return self.ranges.iter(); + self.ranges.iter() } pub fn contains(&self, value: usize) -> bool { @@ -77,7 +70,7 @@ impl RangeSet { return true; } } - return false; + false } pub fn contained_length_from_value(&self, value: usize) -> usize { @@ -88,7 +81,7 @@ impl RangeSet { return range.end() - value; } } - return 0; + 0 } #[allow(dead_code)] @@ -98,12 +91,12 @@ impl RangeSet { return false; } } - return true; + true } pub fn add_range(&mut self, range: &Range) { - if range.length <= 0 { - // the interval is empty or invalid -> nothing to do. + if range.length == 0 { + // the interval is empty -> nothing to do. return; } @@ -111,7 +104,7 @@ impl RangeSet { // the new range is clear of any ranges we already iterated over. if range.end() < self.ranges[index].start { // the new range starts after anything we already passed and ends before the next range starts (they don't touch) -> insert it. - self.ranges.insert(index, range.clone()); + self.ranges.insert(index, *range); return; } else if range.start <= self.ranges[index].end() && self.ranges[index].start <= range.end() @@ -119,7 +112,7 @@ impl RangeSet { // the new range overlaps (or touches) the first range. They are to be merged. // In addition we might have to merge further ranges in as well. - let mut new_range = range.clone(); + let mut new_range = *range; while index < self.ranges.len() && self.ranges[index].start <= new_range.end() { let new_end = max(new_range.end(), self.ranges[index].end()); @@ -134,7 +127,7 @@ impl RangeSet { } // the new range is after everything else -> just add it - self.ranges.push(range.clone()); + self.ranges.push(*range); } #[allow(dead_code)] @@ -148,11 +141,11 @@ impl RangeSet { pub fn union(&self, other: &RangeSet) -> RangeSet { let mut result = self.clone(); result.add_range_set(other); - return result; + result } pub fn subtract_range(&mut self, range: &Range) { - if range.length <= 0 { + if range.length == 0 { return; } @@ -208,7 +201,7 @@ impl RangeSet { pub fn minus(&self, other: &RangeSet) -> RangeSet { let mut result = self.clone(); result.subtract_range_set(other); - return result; + result } pub fn intersection(&self, other: &RangeSet) -> RangeSet { @@ -244,6 +237,6 @@ impl RangeSet { } } - return result; + result } } diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 124337f88..689efd3c2 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -7,37 +7,40 @@ license = "MIT" repository = "https://github.com/librespot-org/librespot" edition = "2018" -[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" - [dependencies] aes-ctr = "0.6" base64 = "0.13" block-modes = "0.7" -futures = "0.1" +form_urlencoded = "1.0" +futures-core = "0.3" +futures-util = { version = "0.3", default_features = false } hmac = "0.10" -hyper = "0.11" +hyper = { version = "0.14", features = ["server", "http1", "tcp"] } +libmdns = "0.6" 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" sha-1 = "0.9" -tokio-core = "0.1" -url = "1.7" +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.2.7", 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-dns-sd = ["dns-sd"] + diff --git a/connect/src/context.rs b/connect/src/context.rs index 5a94f6cb7..63a2aebb4 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 d3e3f709f..383035cc5 100644 --- a/connect/src/discovery.rs +++ b/connect/src/discovery.rs @@ -1,32 +1,29 @@ use aes_ctr::cipher::generic_array::GenericArray; use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; use aes_ctr::Aes128Ctr; -use base64; -use futures::sync::mpsc; -use futures::{Future, Poll, Stream}; +use futures_core::Stream; use hmac::{Hmac, Mac, NewMac}; -use hyper::server::{Http, Request, Response, Service}; -use hyper::{self, Get, Post, StatusCode}; +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}; #[cfg(feature = "with-dns-sd")] use dns_sd::DNSService; -#[cfg(not(feature = "with-dns-sd"))] -use libmdns; +use librespot_core::authentication::Credentials; +use librespot_core::config::ConnectConfig; +use librespot_core::diffie_hellman::DhLocalKeys; -use num_bigint::BigUint; -use rand; +use std::borrow::Cow; use std::collections::BTreeMap; +use std::convert::Infallible; use std::io; +use std::net::{Ipv4Addr, SocketAddr}; +use std::pin::Pin; use std::sync::Arc; -use tokio_core::reactor::Handle; -use url; - -use librespot_core::authentication::Credentials; -use librespot_core::config::ConnectConfig; -use librespot_core::diffie_hellman::{DH_GENERATOR, DH_PRIME}; -use librespot_core::util; +use std::task::{Context, Poll}; type HmacSha1 = Hmac; @@ -35,8 +32,7 @@ struct Discovery(Arc); struct DiscoveryInner { config: ConnectConfig, device_id: String, - private_key: BigUint, - public_key: BigUint, + keys: DhLocalKeys, tx: mpsc::UnboundedSender, } @@ -45,31 +41,20 @@ impl Discovery { config: ConnectConfig, device_id: String, ) -> (Discovery, mpsc::UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded(); - - let key_data = util::rand_vec(&mut rand::thread_rng(), 95); - let private_key = BigUint::from_bytes_be(&key_data); - let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); + let (tx, rx) = mpsc::unbounded_channel(); let discovery = Discovery(Arc::new(DiscoveryInner { - config: config, - device_id: device_id, - private_key: private_key, - public_key: public_key, - tx: tx, + config, + device_id, + keys: DhLocalKeys::random(&mut rand::thread_rng()), + tx, })); (discovery, rx) } -} -impl Discovery { - fn handle_get_info( - &self, - _params: &BTreeMap, - ) -> ::futures::Finished { - let public_key = self.0.public_key.to_bytes_be(); - let public_key = base64::encode(&public_key); + fn handle_get_info(&self, _: BTreeMap, Cow<'_, str>>) -> Response { + let public_key = base64::encode(&self.0.keys.public_key()); let result = json!({ "status": 101, @@ -91,29 +76,29 @@ impl Discovery { }); let body = result.to_string(); - ::futures::finished(Response::new().with_body(body)) + Response::new(Body::from(body)) } fn handle_add_user( &self, - params: &BTreeMap, - ) -> ::futures::Finished { - let username = params.get("userName").unwrap(); + params: BTreeMap, Cow<'_, str>>, + ) -> Response { + let username = params.get("userName").unwrap().as_ref(); let encrypted_blob = params.get("blob").unwrap(); let client_key = params.get("clientKey").unwrap(); - let encrypted_blob = base64::decode(encrypted_blob).unwrap(); + let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); - let client_key = base64::decode(client_key).unwrap(); - let client_key = BigUint::from_bytes_be(&client_key); - - let shared_key = util::powm(&client_key, &self.0.private_key, &DH_PRIME); + let shared_key = self + .0 + .keys + .shared_secret(&base64::decode(client_key.as_bytes()).unwrap()); let iv = &encrypted_blob[0..16]; let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; - let base_key = Sha1::digest(&shared_key.to_bytes_be()); + let base_key = Sha1::digest(&shared_key); let base_key = &base_key[..16]; let checksum_key = { @@ -130,7 +115,7 @@ impl Discovery { let mut h = HmacSha1::new_varkey(&checksum_key).expect("HMAC can take key of any size"); h.update(encrypted); - if let Err(_) = h.verify(cksum) { + if h.verify(cksum).is_err() { warn!("Login error for user {:?}: MAC mismatch", username); let result = json!({ "status": 102, @@ -139,7 +124,7 @@ impl Discovery { }); let body = result.to_string(); - return ::futures::finished(Response::new().with_body(body)); + return Response::new(Body::from(body)); } let decrypted = { @@ -153,9 +138,9 @@ impl Discovery { }; let credentials = - Credentials::with_blob(username.to_owned(), &decrypted, &self.0.device_id); + 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, @@ -164,49 +149,39 @@ impl Discovery { }); let body = result.to_string(); - ::futures::finished(Response::new().with_body(body)) + Response::new(Body::from(body)) } - fn not_found(&self) -> ::futures::Finished { - ::futures::finished(Response::new().with_status(StatusCode::NotFound)) + fn not_found(&self) -> Response { + let mut res = Response::default(); + *res.status_mut() = StatusCode::NOT_FOUND; + res } -} - -impl Service for Discovery { - type Request = Request; - type Response = Response; - type Error = hyper::Error; - type Future = Box>; - fn call(&self, request: Request) -> Self::Future { + async fn call(self, request: Request) -> hyper::Result> { let mut params = BTreeMap::new(); - let (method, uri, _, _, body) = request.deconstruct(); - if let Some(query) = uri.query() { - params.extend(url::form_urlencoded::parse(query.as_bytes()).into_owned()); + let (parts, body) = request.into_parts(); + + if let Some(query) = parts.uri.query() { + let query_params = url::form_urlencoded::parse(query.as_bytes()); + params.extend(query_params); } - if method != Get { - debug!("{:?} {:?} {:?}", method, uri.path(), params); + if parts.method != Method::GET { + debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params); } - let this = self.clone(); - Box::new( - body.fold(Vec::new(), |mut acc, chunk| { - acc.extend_from_slice(chunk.as_ref()); - Ok::<_, hyper::Error>(acc) - }) - .map(move |body| { - params.extend(url::form_urlencoded::parse(&body).into_owned()); - params - }) - .and_then(move |params| { - match (method, params.get("action").map(AsRef::as_ref)) { - (Get, Some("getInfo")) => this.handle_get_info(¶ms), - (Post, Some("addUser")) => this.handle_add_user(¶ms), - _ => this.not_found(), - } - }), + let body = hyper::body::to_bytes(body).await?; + + params.extend(url::form_urlencoded::parse(&body)); + + Ok( + match (parts.method, params.get("action").map(AsRef::as_ref)) { + (Method::GET, Some("getInfo")) => self.handle_get_info(params), + (Method::POST, Some("addUser")) => self.handle_add_user(params), + _ => self.not_found(), + }, ) } } @@ -215,45 +190,40 @@ impl Service for Discovery { pub struct DiscoveryStream { credentials: mpsc::UnboundedReceiver, _svc: DNSService, + _close_tx: oneshot::Sender, } #[cfg(not(feature = "with-dns-sd"))] pub struct DiscoveryStream { credentials: mpsc::UnboundedReceiver, _svc: libmdns::Service, + _close_tx: oneshot::Sender, } pub fn discovery( - handle: &Handle, config: ConnectConfig, device_id: String, port: u16, ) -> io::Result { let (discovery, creds_rx) = Discovery::new(config.clone(), device_id); + let (close_tx, close_rx) = oneshot::channel(); - let serve = { - let http = Http::new(); - http.serve_addr_handle( - &format!("0.0.0.0:{}", port).parse().unwrap(), - &handle, - move || Ok(discovery.clone()), - ) - .unwrap() - }; + let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port); + + let make_service = make_service_fn(move |_| { + let discovery = discovery.clone(); + async move { Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().call(request))) } + }); + + let server = hyper::Server::bind(&address).serve(make_service); - let s_port = serve.incoming_ref().local_addr().port(); + let s_port = server.local_addr().port(); debug!("Zeroconf server listening on 0.0.0.0:{}", s_port); - let server_future = { - let handle = handle.clone(); - serve - .for_each(move |connection| { - handle.spawn(connection.then(|_| Ok(()))); - Ok(()) - }) - .then(|_| Ok(())) - }; - handle.spawn(server_future); + tokio::spawn(server.with_graceful_shutdown(async { + close_rx.await.unwrap_err(); + debug!("Shutting down discovery server"); + })); #[cfg(feature = "with-dns-sd")] let svc = DNSService::register( @@ -267,7 +237,7 @@ pub fn discovery( .unwrap(); #[cfg(not(feature = "with-dns-sd"))] - let responder = libmdns::Responder::spawn(&handle)?; + let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; #[cfg(not(feature = "with-dns-sd"))] let svc = responder.register( @@ -280,14 +250,14 @@ pub fn discovery( Ok(DiscoveryStream { credentials: creds_rx, _svc: svc, + _close_tx: close_tx, }) } impl Stream for DiscoveryStream { type Item = Credentials; - type Error = (); - fn poll(&mut self) -> Poll, Self::Error> { - self.credentials.poll() + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.credentials.poll_recv(cx) } } diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 118c85dfa..600dd0335 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_core; -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 3153d829d..4fcb025a5 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1,26 +1,26 @@ -use std; +use std::future::Future; +use std::pin::Pin; use std::time::{SystemTime, UNIX_EPOCH}; -use futures::future; -use futures::sync::mpsc; -use futures::{Async, Future, Poll, Sink, Stream}; -use protobuf::{self, Message}; -use rand; -use rand::seq::SliceRandom; -use serde_json; - 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::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 librespot_core::config::{ConnectConfig, VolumeCtrl}; -use librespot_core::mercury::MercuryError; -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::seq::SliceRandom; +use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; enum SpircPlayStatus { Stopped, @@ -40,7 +40,10 @@ enum SpircPlayStatus { }, } -pub struct SpircTask { +type BoxedFuture = Pin + Send>>; +type BoxedStream = Pin + Send>>; + +struct SpircTask { player: Player, mixer: Box, config: SpircTaskConfig, @@ -54,15 +57,15 @@ pub struct SpircTask { mixer_started: bool, play_status: SpircPlayStatus, - subscription: Box>, - sender: Box>, - commands: mpsc::UnboundedReceiver, - player_events: PlayerEventChannel, + subscription: BoxedStream, + sender: MercurySender, + commands: Option>, + player_events: Option, shutdown: bool, session: Session, - context_fut: Box>, - autoplay_fut: Box>, + context_fut: BoxedFuture>, + autoplay_fut: BoxedFuture>, context: Option, } @@ -240,38 +243,41 @@ fn volume_to_mixer(volume: u16, volume_ctrl: &VolumeCtrl) -> u16 { } } +fn url_encode(bytes: impl AsRef<[u8]>) -> String { + form_urlencoded::byte_serialize(bytes.as_ref()).collect() +} + impl Spirc { pub fn new( config: ConnectConfig, session: Session, player: Player, mixer: Box, - ) -> (Spirc, SpircTask) { + ) -> (Spirc, impl Future) { debug!("new Spirc[{}]", session.session_id()); let ident = session.device_id().to_owned(); // Uri updated in response to issue #288 - debug!("canonical_username: {}", url_encode(&session.username())); + debug!("canonical_username: {}", &session.username()); let uri = format!("hm://remote/user/{}/", url_encode(&session.username())); - let subscription = session.mercury().subscribe(&uri as &str); - let subscription = subscription - .map(|stream| stream.map_err(|_| MercuryError)) - .flatten_stream(); - let subscription = Box::new(subscription.map(|response| -> Frame { - let data = response.payload.first().unwrap(); - protobuf::parse_from_bytes(data).unwrap() - })); - - let sender = Box::new( + let subscription = Box::pin( session .mercury() - .sender(uri) - .with(|frame: Frame| Ok(frame.write_to_bytes().unwrap())), + .subscribe(uri.clone()) + .map(Result::unwrap) + .map(UnboundedReceiverStream::new) + .flatten_stream() + .map(|response| -> Frame { + let data = response.payload.first().unwrap(); + protobuf::parse_from_bytes(data).unwrap() + }), ); - let (cmd_tx, cmd_rx) = mpsc::unbounded(); + let sender = session.mercury().sender(uri); + + let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); let volume = config.volume; let task_config = SpircTaskConfig { @@ -284,30 +290,30 @@ impl Spirc { let player_events = player.get_player_event_channel(); let mut task = SpircTask { - player: player, - mixer: mixer, + player, + mixer, config: task_config, sequence: SeqGenerator::new(1), - ident: ident, + ident, - device: device, + device, state: initial_state(), play_request_id: None, mixer_started: false, play_status: SpircPlayStatus::Stopped, - subscription: subscription, - sender: sender, - commands: cmd_rx, - player_events: player_events, + subscription, + sender, + commands: Some(cmd_rx), + player_events: Some(player_events), shutdown: false, - session: session.clone(), + session, - context_fut: Box::new(future::empty()), - autoplay_fut: Box::new(future::empty()), + context_fut: Box::pin(future::pending()), + autoplay_fut: Box::pin(future::pending()), context: None, }; @@ -317,150 +323,114 @@ impl Spirc { task.hello(); - (spirc, task) + (spirc, task.run()) } 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 Future for SpircTask { - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll<(), ()> { - loop { - let mut progress = false; - - if self.session.is_invalid() { - return Ok(Async::Ready(())); - } - - if !self.shutdown { - match self.subscription.poll().unwrap() { - Async::Ready(Some(frame)) => { - progress = true; - self.handle_frame(frame); - } - Async::Ready(None) => { +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), + None => { error!("subscription terminated"); - self.shutdown = true; - self.commands.close(); - } - Async::NotReady => (), - } - - match self.commands.poll().unwrap() { - Async::Ready(Some(command)) => { - progress = true; - self.handle_command(command); - } - Async::Ready(None) => (), - Async::NotReady => (), - } - - match self.player_events.poll() { - Ok(Async::NotReady) => (), - Ok(Async::Ready(None)) => (), - Err(_) => (), - Ok(Async::Ready(Some(event))) => { - progress = true; - self.handle_player_event(event); + break; } - } - // TODO: Refactor - match self.context_fut.poll() { - Ok(Async::Ready(value)) => { - let r_context = serde_json::from_value::(value.clone()); - self.context = match r_context { - Ok(context) => { - info!( - "Resolved {:?} tracks from <{:?}>", - context.tracks.len(), - self.state.get_context_uri(), - ); - Some(context) - } - Err(e) => { - error!("Unable to parse JSONContext {:?}\n{:?}", e, value); - None - } - }; - // It needn't be so verbose - can be as simple as - // if let Some(ref context) = r_context { - // info!("Got {:?} tracks from <{}>", context.tracks.len(), context.uri); - // } - // self.context = r_context; - - progress = true; - self.context_fut = Box::new(future::empty()); - } - Ok(Async::NotReady) => (), - Err(err) => { - self.context_fut = Box::new(future::empty()); - error!("ContextError: {:?}", err) - } - } - - match self.autoplay_fut.poll() { - Ok(Async::Ready(autoplay_station_uri)) => { - info!("Autoplay uri resolved to <{:?}>", autoplay_station_uri); - self.context_fut = self.resolve_station(&autoplay_station_uri); - progress = true; - self.autoplay_fut = Box::new(future::empty()); + }, + cmd = async { commands.unwrap().recv().await }, if commands.is_some() => if let Some(cmd) = cmd { + self.handle_command(cmd); + }, + 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() { + error!("Cannot flush spirc event sender."); + break; + }, + context = &mut self.context_fut, if !self.context_fut.is_terminated() => { + match context { + Ok(value) => { + let r_context = serde_json::from_value::(value); + self.context = match r_context { + Ok(context) => { + info!( + "Resolved {:?} tracks from <{:?}>", + context.tracks.len(), + self.state.get_context_uri(), + ); + Some(context) + } + Err(e) => { + error!("Unable to parse JSONContext {:?}", e); + None + } + }; + // It needn't be so verbose - can be as simple as + // if let Some(ref context) = r_context { + // info!("Got {:?} tracks from <{}>", context.tracks.len(), context.uri); + // } + // self.context = r_context; + }, + Err(err) => { + error!("ContextError: {:?}", err) + } } - Ok(Async::NotReady) => (), - Err(err) => { - self.autoplay_fut = Box::new(future::empty()); - error!("AutoplayError: {:?}", err) + }, + autoplay = &mut self.autoplay_fut, if !self.autoplay_fut.is_terminated() => { + match autoplay { + Ok(autoplay_station_uri) => { + info!("Autoplay uri resolved to <{:?}>", autoplay_station_uri); + self.context_fut = self.resolve_station(&autoplay_station_uri); + }, + Err(err) => { + error!("AutoplayError: {:?}", err) + } } - } - } - - let poll_sender = self.sender.poll_complete().unwrap(); - - // Only shutdown once we've flushed out all our messages - if self.shutdown && poll_sender.is_ready() { - return Ok(Async::Ready(())); + }, + else => break } + } - if !progress { - return Ok(Async::NotReady); - } + if self.sender.flush().await.is_err() { + warn!("Cannot flush spirc event sender."); } } -} -impl SpircTask { fn now_ms(&mut self) -> i64 { let dur = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(dur) => dur, Err(err) => err.duration(), }; - (dur.as_secs() as i64 + self.session.time_delta()) * 1000 - + (dur.subsec_nanos() / 1000_000) as i64 + + dur.as_millis() as i64 + 1000 * self.session.time_delta() } fn ensure_mixer_started(&mut self) { @@ -545,7 +515,9 @@ impl SpircTask { SpircCommand::Shutdown => { CommandSender::new(self, MessageType::kMessageTypeGoodbye).send(); self.shutdown = true; - self.commands.close(); + if let Some(rx) = self.commands.as_mut() { + rx.close() + } } } } @@ -653,7 +625,7 @@ impl SpircTask { ); if frame.get_ident() == self.ident - || (frame.get_recipient().len() > 0 && !frame.get_recipient().contains(&self.ident)) + || (!frame.get_recipient().is_empty() && !frame.get_recipient().contains(&self.ident)) { return; } @@ -672,7 +644,7 @@ impl SpircTask { self.update_tracks(&frame); - if self.state.get_track().len() > 0 { + if !self.state.get_track().is_empty() { let start_playing = frame.get_state().get_status() == PlayStatus::kPlayStatusPlay; self.load_track(start_playing, frame.get_state().get_position_ms()); @@ -895,7 +867,7 @@ impl SpircTask { fn preview_next_track(&mut self) -> Option { self.get_track_id_to_play_from_playlist(self.state.get_playing_track_index() + 1) - .and_then(|(track_id, _)| Some(track_id)) + .map(|(track_id, _)| track_id) } fn handle_preload_next_track(&mut self) { @@ -1014,7 +986,7 @@ impl SpircTask { }; // Reinsert queued tracks after the new playing track. let mut pos = (new_index + 1) as usize; - for track in queue_tracks.into_iter() { + for track in queue_tracks { self.state.mut_track().insert(pos, track); pos += 1; } @@ -1060,52 +1032,53 @@ impl SpircTask { } } - fn resolve_station( - &self, - uri: &str, - ) -> Box> { + fn resolve_station(&self, uri: &str) -> BoxedFuture> { let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri); self.resolve_uri(&radio_uri) } - fn resolve_autoplay_uri( - &self, - uri: &str, - ) -> Box> { + fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture> { let query_uri = format!("hm://autoplay-enabled/query?uri={}", uri); let request = self.session.mercury().get(query_uri); - Box::new(request.and_then(move |response| { - if response.status_code == 200 { - let data = response - .payload - .first() - .expect("Empty autoplay uri") - .to_vec(); - let autoplay_uri = String::from_utf8(data).unwrap(); - Ok(autoplay_uri) - } else { - warn!("No autoplay_uri found"); - Err(MercuryError) + Box::pin( + async { + let response = request.await?; + + if response.status_code == 200 { + let data = response + .payload + .first() + .expect("Empty autoplay uri") + .to_vec(); + let autoplay_uri = String::from_utf8(data).unwrap(); + Ok(autoplay_uri) + } else { + warn!("No autoplay_uri found"); + Err(MercuryError) + } } - })) + .fuse(), + ) } - fn resolve_uri( - &self, - uri: &str, - ) -> Box> { + fn resolve_uri(&self, uri: &str) -> BoxedFuture> { let request = self.session.mercury().get(uri); - Box::new(request.and_then(move |response| { - let data = response - .payload - .first() - .expect("Empty payload on context uri"); - let response: serde_json::Value = serde_json::from_slice(&data).unwrap(); + Box::pin( + async move { + let response = request.await?; - Ok(response) - })) + let data = response + .payload + .first() + .expect("Empty payload on context uri"); + let response: serde_json::Value = serde_json::from_slice(&data).unwrap(); + + Ok(response) + } + .fuse(), + ) } fn update_tracks_from_context(&mut self) { @@ -1152,7 +1125,7 @@ impl SpircTask { } self.state.set_playing_track_index(index); - self.state.set_track(tracks.into_iter().cloned().collect()); + self.state.set_track(tracks.iter().cloned().collect()); self.state.set_context_uri(context_uri); // has_shuffle/repeat seem to always be true in these replace msgs, // but to replicate the behaviour of the Android client we have to @@ -1323,10 +1296,7 @@ impl<'a> CommandSender<'a> { frame.set_typ(cmd); frame.set_device_state(spirc.device.clone()); frame.set_state_update_id(spirc.now_ms()); - CommandSender { - spirc: spirc, - frame: frame, - } + CommandSender { spirc, frame } } fn recipient(mut self, recipient: &'a str) -> CommandSender { @@ -1345,7 +1315,6 @@ impl<'a> CommandSender<'a> { self.frame.set_state(self.spirc.state.clone()); } - let send = self.spirc.sender.start_send(self.frame).unwrap(); - assert!(send.is_ready()); + self.spirc.sender.send(self.frame.write_to_bytes().unwrap()); } } diff --git a/core/Cargo.toml b/core/Cargo.toml index 89bc6bfd8..29f4f3329 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -15,33 +15,42 @@ version = "0.1.6" [dependencies] aes = "0.6" base64 = "0.13" -byteorder = "1.3" -bytes = "0.4" -error-chain = { version = "0.12", default_features = false } -futures = "0.1" +byteorder = "1.4" +bytes = "1.0" +form_urlencoded = "1.0" +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 = "0.11" -hyper-proxy = { version = "0.4", default_features = false } -lazy_static = "1.3" +http = "0.2" +hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] } +hyper-proxy = { version = "0.9.1", optional = true, default-features = false } log = "0.4" -num-bigint = "0.3" +num-bigint = { version = "0.4", features = ["rand"] } num-integer = "0.1" num-traits = "0.2" -pbkdf2 = { version = "0.7", default_features = false, features = ["hmac"] } +once_cell = "1.5.2" +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.9" shannon = "0.2.0" -tokio-codec = "0.1" -tokio-core = "0.1" -tokio-io = "0.1" -url = "1.7" -uuid = { version = "0.8", features = ["v4"] } +thiserror = "1" +tokio = { version = "1.0", features = ["io-util", "net", "rt", "sync"] } +tokio-stream = "0.1" +tokio-util = { version = "0.6", features = ["codec"] } +url = "2.1" +uuid = { version = "0.8", default-features = false, features = ["v4"] } [build-dependencies] -rand = "0.7" +rand = "0.8" vergen = "3.0.4" + +[dev-dependencies] +env_logger = "*" +tokio = {version = "1.0", features = ["macros"] } + +[features] +apresolve = ["hyper", "hyper-proxy"] diff --git a/core/build.rs b/core/build.rs index 4877ff463..8e61c9125 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=LIBRESPOT_BUILD_ID={}", build_id); diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 94d942440..b11e275f2 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,101 +1,91 @@ -const AP_FALLBACK: &'static str = "ap.spotify.com:443"; -const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com/"; +use std::error::Error; -use futures::{Future, Stream}; use hyper::client::HttpConnector; -use hyper::{self, Client, Method, Request, Uri}; +use hyper::{Body, Client, Method, Request, Uri}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; -use serde_json; -use std::str::FromStr; -use tokio_core::reactor::Handle; +use serde::Deserialize; use url::Url; -error_chain! {} +use super::AP_FALLBACK; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct APResolveData { +const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80"; + +#[derive(Clone, Debug, Deserialize)] +struct ApResolveData { ap_list: Vec, } -fn apresolve( - handle: &Handle, - proxy: &Option, - ap_port: &Option, -) -> Box> { - let url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL"); - let use_proxy = proxy.is_some(); - - let mut req = Request::new(Method::Get, url.clone()); - let response = match *proxy { - Some(ref val) => { - let proxy_url = Uri::from_str(val.as_str()).expect("invalid http proxy"); - let proxy = Proxy::new(Intercept::All, proxy_url); - let connector = HttpConnector::new(4, handle); - let proxy_connector = ProxyConnector::from_proxy_unsecured(connector, proxy); - if let Some(headers) = proxy_connector.http_headers(&url) { - req.headers_mut().extend(headers.iter()); - req.set_proxy(true); - } - let client = Client::configure().connector(proxy_connector).build(handle); - client.request(req) - } - _ => { - let client = Client::new(handle); - client.request(req) - } +async fn try_apresolve( + proxy: Option<&Url>, + ap_port: Option, +) -> Result> { + let port = ap_port.unwrap_or(443); + + let mut req = Request::new(Body::empty()); + *req.method_mut() = Method::GET; + // panic safety: APRESOLVE_ENDPOINT above is valid url. + *req.uri_mut() = APRESOLVE_ENDPOINT.parse().expect("invalid AP resolve URL"); + + let response = if let Some(url) = proxy { + // Panic safety: all URLs are valid URIs + let uri = url.to_string().parse().unwrap(); + let proxy = Proxy::new(Intercept::All, uri); + let connector = HttpConnector::new(); + let proxy_connector = ProxyConnector::from_proxy_unsecured(connector, proxy); + Client::builder() + .build(proxy_connector) + .request(req) + .await? + } else { + Client::new().request(req).await? }; - let body = response.and_then(|response| { - response.body().fold(Vec::new(), |mut acc, chunk| { - acc.extend_from_slice(chunk.as_ref()); - Ok::<_, hyper::Error>(acc) - }) - }); - let body = body.then(|result| result.chain_err(|| "HTTP error")); - let body = - body.and_then(|body| String::from_utf8(body).chain_err(|| "invalid UTF8 in response")); - - let data = body - .and_then(|body| serde_json::from_str::(&body).chain_err(|| "invalid JSON")); - - let p = ap_port.clone(); - - let ap = data.and_then(move |data| { - let mut aps = data.ap_list.iter().filter(|ap| { - if p.is_some() { - Uri::from_str(ap).ok().map_or(false, |uri| { - uri.port().map_or(false, |port| port == p.unwrap()) - }) - } else if use_proxy { - // It is unlikely that the proxy will accept CONNECT on anything other than 443. - Uri::from_str(ap) - .ok() - .map_or(false, |uri| uri.port().map_or(false, |port| port == 443)) + 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 { - true + None } - }); - - let ap = aps.next().ok_or("empty AP List")?; - Ok(ap.clone()) - }); + }) + } else { + data.ap_list.into_iter().next() + } + .ok_or("empty AP List")?; - Box::new(ap) + Ok(ap) } -pub(crate) fn apresolve_or_fallback( - handle: &Handle, - proxy: &Option, - ap_port: &Option, -) -> Box> -where - E: 'static, -{ - let ap = apresolve(handle, proxy, ap_port).or_else(|e| { - warn!("Failed to resolve Access Point: {}", e.description()); +pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> String { + try_apresolve(proxy, ap_port).await.unwrap_or_else(|e| { + warn!("Failed to resolve Access Point: {}", e); warn!("Using fallback \"{}\"", AP_FALLBACK); - Ok(AP_FALLBACK.into()) - }); + AP_FALLBACK.into() + }) +} + +#[cfg(test)] +mod test { + use std::net::ToSocketAddrs; + + use super::try_apresolve; + + #[tokio::test] + async fn test_apresolve() { + let ap = try_apresolve(None, None).await.unwrap(); + + // Assert that the result contains a valid host and port + ap.to_socket_addrs().unwrap().next().unwrap(); + } + + #[tokio::test] + async fn test_apresolve_port_443() { + let ap = try_apresolve(None, Some(443)).await.unwrap(); - Box::new(ap) + let port = ap.to_socket_addrs().unwrap().next().unwrap().port(); + assert_eq!(port, 443); + } } diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index 1e5310c21..3bce1c73e 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -1,9 +1,8 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use bytes::Bytes; -use futures::sync::oneshot; -use futures::{Async, Future, Poll}; use std::collections::HashMap; use std::io::Write; +use tokio::sync::oneshot; use crate::spotify_id::{FileId, SpotifyId}; use crate::util::SeqGenerator; @@ -47,7 +46,7 @@ impl AudioKeyManager { } } - pub fn request(&self, track: SpotifyId, file: FileId) -> AudioKeyFuture { + pub async fn request(&self, track: SpotifyId, file: FileId) -> Result { let (tx, rx) = oneshot::channel(); let seq = self.lock(move |inner| { @@ -57,7 +56,7 @@ impl AudioKeyManager { }); self.send_key_request(seq, track, file); - AudioKeyFuture(rx) + rx.await.map_err(|_| AudioKeyError)? } fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) { @@ -70,18 +69,3 @@ impl AudioKeyManager { self.session().send_packet(0xc, data) } } - -pub struct AudioKeyFuture(oneshot::Receiver>); -impl Future for AudioKeyFuture { - type Item = T; - type Error = AudioKeyError; - - fn poll(&mut self) -> Poll { - match self.0.poll() { - Ok(Async::Ready(Ok(value))) => Ok(Async::Ready(value)), - Ok(Async::Ready(Err(err))) => Err(err), - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(oneshot::Canceled) => Err(AudioKeyError), - } - } -} diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 5394ff351..db787bbe2 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -1,14 +1,16 @@ +use std::io::{self, Read}; + use aes::Aes192; 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}; +/// The credentials are used to log into the Spotify API. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Credentials { pub username: String, @@ -24,11 +26,19 @@ pub struct Credentials { } impl Credentials { - pub fn with_password(username: String, password: String) -> Credentials { + /// Intialize these credentials from a username and a password. + /// + /// ### Example + /// ```rust + /// use librespot_core::authentication::Credentials; + /// + /// let creds = Credentials::with_password("my account", "my password"); + /// ``` + pub fn with_password(username: impl Into, password: impl Into) -> Credentials { Credentials { - username: username, + username: username.into(), auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, - auth_data: password.into_bytes(), + auth_data: password.into().into_bytes(), } } @@ -102,9 +112,9 @@ impl Credentials { let auth_data = read_bytes(&mut cursor).unwrap(); Credentials { - username: username, - auth_type: auth_type, - auth_data: auth_data, + username, + auth_type, + auth_data, } } } @@ -141,61 +151,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 b614fac49..387b3966f 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -1,10 +1,15 @@ -use byteorder::{BigEndian, ByteOrder}; -use bytes::Bytes; -use futures::sync::{mpsc, BiLock}; -use futures::{Async, Poll, Stream}; 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_core::Stream; +use futures_util::lock::BiLock; +use futures_util::StreamExt; +use tokio::sync::mpsc; + use crate::util::SeqGenerator; component! { @@ -43,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(); @@ -82,13 +87,13 @@ 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)); } }); } pub fn get_download_rate_estimate(&self) -> usize { - return self.lock(|inner| inner.download_rate_estimate); + self.lock(|inner| inner.download_rate_estimate) } pub(crate) fn shutdown(&self) { @@ -101,12 +106,10 @@ impl ChannelManager { } impl Channel { - fn recv_packet(&mut self) -> Poll { - let (cmd, packet) = match self.receiver.poll() { - Ok(Async::Ready(Some(t))) => t, - Ok(Async::Ready(None)) => return Err(ChannelError), // The channel has been closed. - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(()) => unreachable!(), + fn recv_packet(&mut self, cx: &mut Context<'_>) -> Poll> { + let (cmd, packet) = match self.receiver.poll_recv(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(o) => o.ok_or(ChannelError)?, }; if cmd == 0xa { @@ -115,9 +118,9 @@ impl Channel { self.state = ChannelState::Closed; - Err(ChannelError) + Poll::Ready(Err(ChannelError)) } else { - Ok(Async::Ready(packet)) + Poll::Ready(Ok(packet)) } } @@ -129,16 +132,19 @@ impl Channel { } impl Stream for Channel { - type Item = ChannelEvent; - type Error = ChannelError; + type Item = Result; - fn poll(&mut self) -> Poll, Self::Error> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { loop { match self.state.clone() { ChannelState::Closed => panic!("Polling already terminated channel"), ChannelState::Header(mut data) => { - if data.len() == 0 { - data = try_ready!(self.recv_packet()); + if data.is_empty() { + data = match self.recv_packet(cx) { + Poll::Ready(Ok(x)) => x, + Poll::Ready(Err(x)) => return Poll::Ready(Some(Err(x))), + Poll::Pending => return Poll::Pending, + }; } let length = BigEndian::read_u16(data.split_to(2).as_ref()) as usize; @@ -152,19 +158,23 @@ impl Stream for Channel { self.state = ChannelState::Header(data); let event = ChannelEvent::Header(header_id, header_data); - return Ok(Async::Ready(Some(event))); + return Poll::Ready(Some(Ok(event))); } } ChannelState::Data => { - let data = try_ready!(self.recv_packet()); - if data.len() == 0 { + let data = match self.recv_packet(cx) { + Poll::Ready(Ok(x)) => x, + Poll::Ready(Err(x)) => return Poll::Ready(Some(Err(x))), + Poll::Pending => return Poll::Pending, + }; + if data.is_empty() { self.receiver.close(); self.state = ChannelState::Closed; - return Ok(Async::Ready(None)); + return Poll::Ready(None); } else { let event = ChannelEvent::Data(data); - return Ok(Async::Ready(Some(event))); + return Poll::Ready(Some(Ok(event))); } } } @@ -173,38 +183,46 @@ impl Stream for Channel { } impl Stream for ChannelData { - type Item = Bytes; - type Error = ChannelError; + type Item = Result; - fn poll(&mut self) -> Poll, Self::Error> { - let mut channel = match self.0.poll_lock() { - Async::Ready(c) => c, - Async::NotReady => return Ok(Async::NotReady), + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut channel = match self.0.poll_lock(cx) { + Poll::Ready(c) => c, + Poll::Pending => return Poll::Pending, }; loop { - match try_ready!(channel.poll()) { + let event = match channel.poll_next_unpin(cx) { + Poll::Ready(x) => x.transpose()?, + Poll::Pending => return Poll::Pending, + }; + + match event { Some(ChannelEvent::Header(..)) => (), - Some(ChannelEvent::Data(data)) => return Ok(Async::Ready(Some(data))), - None => return Ok(Async::Ready(None)), + Some(ChannelEvent::Data(data)) => return Poll::Ready(Some(Ok(data))), + None => return Poll::Ready(None), } } } } impl Stream for ChannelHeaders { - type Item = (u8, Vec); - type Error = ChannelError; + type Item = Result<(u8, Vec), ChannelError>; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut channel = match self.0.poll_lock(cx) { + Poll::Ready(c) => c, + Poll::Pending => return Poll::Pending, + }; - fn poll(&mut self) -> Poll, Self::Error> { - let mut channel = match self.0.poll_lock() { - Async::Ready(c) => c, - Async::NotReady => return Ok(Async::NotReady), + let event = match channel.poll_next_unpin(cx) { + Poll::Ready(x) => x.transpose()?, + Poll::Pending => return Poll::Pending, }; - match try_ready!(channel.poll()) { - Some(ChannelEvent::Header(id, data)) => Ok(Async::Ready(Some((id, data)))), - Some(ChannelEvent::Data(..)) | None => Ok(Async::Ready(None)), + match event { + Some(ChannelEvent::Header(id, data)) => Poll::Ready(Some(Ok((id, data)))), + Some(ChannelEvent::Data(..)) | None => Poll::Ready(None), } } } diff --git a/core/src/component.rs b/core/src/component.rs index 50ab7b37e..a761c4551 100644 --- a/core/src/component.rs +++ b/core/src/component.rs @@ -35,29 +35,3 @@ macro_rules! component { } } } - -use std::cell::UnsafeCell; -use std::sync::Mutex; - -pub(crate) struct Lazy(Mutex, UnsafeCell>); -unsafe impl Sync for Lazy {} -unsafe impl Send for Lazy {} - -#[cfg_attr(feature = "cargo-clippy", allow(mutex_atomic))] -impl Lazy { - pub(crate) fn new() -> Lazy { - Lazy(Mutex::new(false), UnsafeCell::new(None)) - } - - pub(crate) fn get T>(&self, f: F) -> &T { - let mut inner = self.0.lock().unwrap(); - if !*inner { - unsafe { - *self.1.get() = Some(f()); - } - *inner = true; - } - - unsafe { &*self.1.get() }.as_ref().unwrap() - } -} diff --git a/core/src/config.rs b/core/src/config.rs index ae47fc3d8..9c70c25b5 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 { @@ -15,10 +12,10 @@ pub struct SessionConfig { impl Default for SessionConfig { fn default() -> SessionConfig { - let device_id = Uuid::new_v4().to_hyphenated().to_string(); + let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string(); SessionConfig { - user_agent: version::VERSION_STRING.to_string(), - device_id: device_id, + user_agent: crate::version::VERSION_STRING.to_string(), + device_id, proxy: None, ap_port: None, } @@ -32,9 +29,9 @@ pub enum DeviceType { Tablet = 2, Smartphone = 3, Speaker = 4, - TV = 5, - AVR = 6, - STB = 7, + Tv = 5, + Avr = 6, + Stb = 7, AudioDongle = 8, GameConsole = 9, CastAudio = 10, @@ -57,9 +54,9 @@ impl FromStr for DeviceType { "tablet" => Ok(Tablet), "smartphone" => Ok(Smartphone), "speaker" => Ok(Speaker), - "tv" => Ok(TV), - "avr" => Ok(AVR), - "stb" => Ok(STB), + "tv" => Ok(Tv), + "avr" => Ok(Avr), + "stb" => Ok(Stb), "audiodongle" => Ok(AudioDongle), "gameconsole" => Ok(GameConsole), "castaudio" => Ok(CastAudio), @@ -83,9 +80,9 @@ impl fmt::Display for DeviceType { Tablet => f.write_str("Tablet"), Smartphone => f.write_str("Smartphone"), Speaker => f.write_str("Speaker"), - TV => f.write_str("TV"), - AVR => f.write_str("AVR"), - STB => f.write_str("STB"), + Tv => f.write_str("TV"), + Avr => f.write_str("AVR"), + Stb => f.write_str("STB"), AudioDongle => f.write_str("AudioDongle"), GameConsole => f.write_str("GameConsole"), CastAudio => f.write_str("CastAudio"), diff --git a/core/src/connection/codec.rs b/core/src/connection/codec.rs index fa4cd9d99..299220f64 100644 --- a/core/src/connection/codec.rs +++ b/core/src/connection/codec.rs @@ -2,7 +2,7 @@ use byteorder::{BigEndian, ByteOrder}; use bytes::{BufMut, Bytes, BytesMut}; use shannon::Shannon; use std::io; -use tokio_io::codec::{Decoder, Encoder}; +use tokio_util::codec::{Decoder, Encoder}; const HEADER_SIZE: usize = 3; const MAC_SIZE: usize = 4; @@ -13,7 +13,7 @@ enum DecodeState { Payload(u8, usize), } -pub struct APCodec { +pub struct ApCodec { encode_nonce: u32, encode_cipher: Shannon, @@ -22,9 +22,9 @@ pub struct APCodec { decode_state: DecodeState, } -impl APCodec { - pub fn new(send_key: &[u8], recv_key: &[u8]) -> APCodec { - APCodec { +impl ApCodec { + pub fn new(send_key: &[u8], recv_key: &[u8]) -> ApCodec { + ApCodec { encode_nonce: 0, encode_cipher: Shannon::new(send_key), @@ -35,8 +35,7 @@ impl APCodec { } } -impl Encoder for APCodec { - type Item = (u8, Vec); +impl Encoder<(u8, Vec)> for ApCodec { type Error = io::Error; fn encode(&mut self, item: (u8, Vec), buf: &mut BytesMut) -> io::Result<()> { @@ -45,7 +44,7 @@ impl Encoder for APCodec { buf.reserve(3 + payload.len()); buf.put_u8(cmd); - buf.put_u16_be(payload.len() as u16); + buf.put_u16(payload.len() as u16); buf.extend_from_slice(&payload); self.encode_cipher.nonce_u32(self.encode_nonce); @@ -61,7 +60,7 @@ impl Encoder for APCodec { } } -impl Decoder for APCodec { +impl Decoder for ApCodec { type Item = (u8, Bytes); type Error = io::Error; diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index abd6a1699..6f802ab55 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -1,87 +1,47 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; -use futures::{Async, Future, Poll}; use hmac::{Hmac, Mac, NewMac}; use protobuf::{self, Message}; -use rand::thread_rng; +use rand::{thread_rng, RngCore}; use sha1::Sha1; -use std::io::{self, Read}; -use std::marker::PhantomData; -use tokio_codec::{Decoder, Framed}; -use tokio_io::io::{read_exact, write_all, ReadExact, Window, WriteAll}; -use tokio_io::{AsyncRead, AsyncWrite}; - -use super::codec::APCodec; -use crate::diffie_hellman::DHLocalKeys; +use std::io; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio_util::codec::{Decoder, Framed}; + +use super::codec::ApCodec; +use crate::diffie_hellman::DhLocalKeys; use crate::protocol; use crate::protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext}; -use crate::util; - -pub struct Handshake { - keys: DHLocalKeys, - state: HandshakeState, -} -enum HandshakeState { - ClientHello(WriteAll>), - APResponse(RecvPacket), - ClientResponse(Option, WriteAll>), +pub async fn handshake( + mut connection: T, +) -> io::Result> { + let local_keys = DhLocalKeys::random(&mut thread_rng()); + let gc = local_keys.public_key(); + let mut accumulator = client_hello(&mut connection, gc).await?; + let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?; + let remote_key = message + .get_challenge() + .get_login_crypto_challenge() + .get_diffie_hellman() + .get_gs() + .to_owned(); + + let shared_secret = local_keys.shared_secret(&remote_key); + let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator); + let codec = ApCodec::new(&send_key, &recv_key); + + client_response(&mut connection, challenge).await?; + + Ok(codec.framed(connection)) } -pub fn handshake(connection: T) -> Handshake { - let local_keys = DHLocalKeys::random(&mut thread_rng()); - let client_hello = client_hello(connection, local_keys.public_key()); - - Handshake { - keys: local_keys, - state: HandshakeState::ClientHello(client_hello), - } -} - -impl Future for Handshake { - type Item = Framed; - type Error = io::Error; - - fn poll(&mut self) -> Poll { - use self::HandshakeState::*; - loop { - self.state = match self.state { - ClientHello(ref mut write) => { - let (connection, accumulator) = try_ready!(write.poll()); - - let read = recv_packet(connection, accumulator); - APResponse(read) - } - - APResponse(ref mut read) => { - let (connection, message, accumulator) = try_ready!(read.poll()); - let remote_key = message - .get_challenge() - .get_login_crypto_challenge() - .get_diffie_hellman() - .get_gs() - .to_owned(); - - let shared_secret = self.keys.shared_secret(&remote_key); - let (challenge, send_key, recv_key) = - compute_keys(&shared_secret, &accumulator); - let codec = APCodec::new(&send_key, &recv_key); - - let write = client_response(connection, challenge); - ClientResponse(Some(codec), write) - } - - ClientResponse(ref mut codec, ref mut write) => { - let (connection, _) = try_ready!(write.poll()); - let codec = codec.take().unwrap(); - let framed = codec.framed(connection); - return Ok(Async::Ready(framed)); - } - } - } - } -} +async fn client_hello(connection: &mut T, gc: Vec) -> io::Result> +where + T: AsyncWrite + Unpin, +{ + let mut client_nonce = vec![0; 0x10]; + thread_rng().fill_bytes(&mut client_nonce); -fn client_hello(connection: T, gc: Vec) -> WriteAll> { let mut packet = ClientHello::new(); packet .mut_build_info() @@ -101,18 +61,22 @@ fn client_hello(connection: T, gc: Vec) -> WriteAll(size).unwrap(); + as WriteBytesExt>::write_u32::(&mut buffer, size).unwrap(); packet.write_to_vec(&mut buffer).unwrap(); - write_all(connection, buffer) + connection.write_all(&buffer[..]).await?; + Ok(buffer) } -fn client_response(connection: T, challenge: Vec) -> WriteAll> { +async fn client_response(connection: &mut T, challenge: Vec) -> io::Result<()> +where + T: AsyncWrite + Unpin, +{ let mut packet = ClientResponsePlaintext::new(); packet .mut_login_crypto_response() @@ -123,70 +87,35 @@ fn client_response(connection: T, challenge: Vec) -> WriteAll let mut buffer = vec![]; let size = 4 + packet.compute_size(); - buffer.write_u32::(size).unwrap(); + as WriteBytesExt>::write_u32::(&mut buffer, size).unwrap(); packet.write_to_vec(&mut buffer).unwrap(); - write_all(connection, buffer) -} - -enum RecvPacket { - Header(ReadExact>>, PhantomData), - Body(ReadExact>>, PhantomData), + connection.write_all(&buffer[..]).await?; + Ok(()) } -fn recv_packet(connection: T, acc: Vec) -> RecvPacket +async fn recv_packet(connection: &mut T, acc: &mut Vec) -> io::Result where - T: Read, + T: AsyncRead + Unpin, M: Message, { - RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData) + let header = read_into_accumulator(connection, 4, acc).await?; + let size = BigEndian::read_u32(header) as usize; + let data = read_into_accumulator(connection, size - 4, acc).await?; + let message = protobuf::parse_from_bytes(data).unwrap(); + Ok(message) } -impl Future for RecvPacket -where - T: Read, - M: Message, -{ - type Item = (T, M, Vec); - type Error = io::Error; - - fn poll(&mut self) -> Poll { - use self::RecvPacket::*; - loop { - *self = match *self { - Header(ref mut read, _) => { - let (connection, header) = try_ready!(read.poll()); - let size = BigEndian::read_u32(header.as_ref()) as usize; - - let acc = header.into_inner(); - let read = read_into_accumulator(connection, size - 4, acc); - RecvPacket::Body(read, PhantomData) - } - - Body(ref mut read, _) => { - let (connection, data) = try_ready!(read.poll()); - let message = protobuf::parse_from_bytes(data.as_ref()).unwrap(); - - let acc = data.into_inner(); - return Ok(Async::Ready((connection, message, acc))); - } - } - } - } -} - -fn read_into_accumulator( - connection: T, +async fn read_into_accumulator<'a, 'b, T: AsyncRead + Unpin>( + connection: &'a mut T, size: usize, - mut acc: Vec, -) -> ReadExact>> { + acc: &'b mut Vec, +) -> io::Result<&'b mut [u8]> { let offset = acc.len(); acc.resize(offset + size, 0); - let mut window = Window::new(acc); - window.set_start(offset); - - read_exact(connection, window) + connection.read_exact(&mut acc[offset..]).await?; + Ok(&mut acc[offset..]) } fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec, Vec, Vec) { diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index eccd5355b..d8a40129b 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -1,74 +1,117 @@ mod codec; mod handshake; -pub use self::codec::APCodec; +pub use self::codec::ApCodec; pub use self::handshake::handshake; -use futures::{Future, Sink, Stream}; -use protobuf::{self, Message}; -use std::io; +use std::io::{self, ErrorKind}; use std::net::ToSocketAddrs; -use tokio_codec::Framed; -use tokio_core::net::TcpStream; -use tokio_core::reactor::Handle; + +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), +} -pub type Transport = Framed; - -pub fn connect( - addr: String, - handle: &Handle, - proxy: &Option, -) -> Box> { - let (addr, connect_url) = match *proxy { - Some(ref url) => { - info!("Using proxy \"{}\"", url); - match url.to_socket_addrs().and_then(|mut iter| { - iter.next().ok_or(io::Error::new( +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<&Url>) -> io::Result { + let socket = if let Some(proxy_url) = proxy { + info!("Using proxy \"{}\"", proxy_url); + + let socket_addr = proxy_url.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", - )) - }) { - Ok(socket_addr) => (socket_addr, Some(addr)), - Err(error) => return Box::new(futures::future::err(error)), - } - } - None => { - match addr.to_socket_addrs().and_then(|mut iter| { - iter.next().ok_or(io::Error::new( - io::ErrorKind::NotFound, - "Can't resolve server address", - )) - }) { - Ok(socket_addr) => (socket_addr, None), - Err(error) => return Box::new(futures::future::err(error)), - } - } - }; + ) + }) + })?; + let socket = TcpStream::connect(&socket_addr).await?; + + let uri = addr.parse::().map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "Can't parse access point address", + ) + })?; + let host = uri.host().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "The access point address contains no hostname", + ) + })?; + let port = uri.port().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "The access point address contains no port", + ) + })?; - let socket = TcpStream::connect(&addr, handle); - if let Some(connect_url) = connect_url { - let connection = socket - .and_then(move |socket| proxytunnel::connect(socket, &connect_url).and_then(handshake)); - Box::new(connection) + proxytunnel::proxy_connect(socket, host, port.as_str()).await? } else { - let connection = socket.and_then(handshake); - Box::new(connection) - } + let socket_addr = addr.to_socket_addrs()?.next().ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "Can't resolve access point address", + ) + })?; + + TcpStream::connect(&socket_addr).await? + }; + + handshake(socket).await } -pub fn authenticate( - transport: Transport, +pub async fn authenticate( + transport: &mut Transport, credentials: Credentials, - device_id: String, -) -> Box> { + device_id: &str, +) -> Result { use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; - use crate::protocol::keyexchange::APLoginFailed; let mut packet = ClientResponseEncrypted::new(); packet @@ -91,39 +134,35 @@ pub fn authenticate( version::SHA_SHORT, version::BUILD_ID )); - packet.mut_system_info().set_device_id(device_id); + packet + .mut_system_info() + .set_device_id(device_id.to_string()); packet.set_version_string(version::VERSION_STRING.to_string()); let cmd = 0xab; let data = packet.write_to_bytes().unwrap(); - Box::new( - transport - .send((cmd, data)) - .and_then(|transport| transport.into_future().map_err(|(err, _stream)| err)) - .map_err(|io_err| io_err.into()) - .and_then(|(packet, transport)| match packet { - Some((0xac, data)) => { - let welcome_data: APWelcome = - protobuf::parse_from_bytes(data.as_ref()).unwrap(); - - let reusable_credentials = Credentials { - username: welcome_data.get_canonical_username().to_owned(), - auth_type: welcome_data.get_reusable_auth_credentials_type(), - auth_data: welcome_data.get_reusable_auth_credentials().to_owned(), - }; - - Ok((transport, reusable_credentials)) - } - - Some((0xad, data)) => { - let error_data: APLoginFailed = - protobuf::parse_from_bytes(data.as_ref()).unwrap(); - Err(error_data.into()) - } - - Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd), - None => panic!("EOF"), - }), - ) + transport.send((cmd, data)).await?; + let (cmd, data) = transport.next().await.expect("EOF")?; + match cmd { + 0xac => { + let welcome_data: APWelcome = protobuf::parse_from_bytes(data.as_ref())?; + + let reusable_credentials = Credentials { + username: welcome_data.get_canonical_username().to_owned(), + auth_type: welcome_data.get_reusable_auth_credentials_type(), + auth_data: welcome_data.get_reusable_auth_credentials().to_owned(), + }; + + Ok(reusable_credentials) + } + 0xad => { + let error_data: APLoginFailed = protobuf::parse_from_bytes(data.as_ref())?; + Err(error_data.into()) + } + _ => { + let msg = format!("Received invalid packet: {}", cmd); + Err(io::Error::new(ErrorKind::InvalidData, msg).into()) + } + } } diff --git a/core/src/diffie_hellman.rs b/core/src/diffie_hellman.rs index dec34a3b8..57caa029c 100644 --- a/core/src/diffie_hellman.rs +++ b/core/src/diffie_hellman.rs @@ -1,12 +1,12 @@ -use num_bigint::BigUint; -use num_traits::FromPrimitive; -use rand::Rng; +use num_bigint::{BigUint, RandBigInt}; +use num_integer::Integer; +use num_traits::{One, Zero}; +use once_cell::sync::Lazy; +use rand::{CryptoRng, Rng}; -use crate::util; - -lazy_static! { - pub static ref DH_GENERATOR: BigUint = BigUint::from_u64(0x2).unwrap(); - pub static ref DH_PRIME: BigUint = BigUint::from_bytes_be(&[ +static DH_GENERATOR: Lazy = Lazy::new(|| BigUint::from_bytes_be(&[0x02])); +static DH_PRIME: Lazy = Lazy::new(|| { + BigUint::from_bytes_be(&[ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, @@ -14,24 +14,38 @@ lazy_static! { 0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - ]); + ]) +}); + +fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { + let mut base = base.clone(); + let mut exp = exp.clone(); + let mut result: BigUint = One::one(); + + while !exp.is_zero() { + if exp.is_odd() { + result = (result * &base) % modulus; + } + exp >>= 1; + base = (&base * &base) % modulus; + } + + result } -pub struct DHLocalKeys { +pub struct DhLocalKeys { private_key: BigUint, public_key: BigUint, } -impl DHLocalKeys { - pub fn random(rng: &mut R) -> DHLocalKeys { - let key_data = util::rand_vec(rng, 95); - - let private_key = BigUint::from_bytes_be(&key_data); - let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); +impl DhLocalKeys { + pub fn random(rng: &mut R) -> DhLocalKeys { + let private_key = rng.gen_biguint(95 * 8); + let public_key = powm(&DH_GENERATOR, &private_key, &DH_PRIME); - DHLocalKeys { - private_key: private_key, - public_key: public_key, + DhLocalKeys { + private_key, + public_key, } } @@ -40,7 +54,7 @@ impl DHLocalKeys { } pub fn shared_secret(&self, remote_key: &[u8]) -> Vec { - let shared_key = util::powm( + let shared_key = powm( &BigUint::from_bytes_be(remote_key), &self.private_key, &DH_PRIME, diff --git a/core/src/keymaster.rs b/core/src/keymaster.rs index f2d7b772c..8c3c00a21 100644 --- a/core/src/keymaster.rs +++ b/core/src/keymaster.rs @@ -1,8 +1,6 @@ -use futures::Future; -use serde_json; +use serde::Deserialize; -use crate::mercury::MercuryError; -use crate::session::Session; +use crate::{mercury::MercuryError, session::Session}; #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] @@ -13,20 +11,16 @@ pub struct Token { pub scope: Vec, } -pub fn get_token( +pub async fn get_token( session: &Session, client_id: &str, scopes: &str, -) -> Box> { +) -> Result { let url = format!( "hm://keymaster/token/authenticated?client_id={}&scope={}", client_id, scopes ); - Box::new(session.mercury().get(url).map(move |response| { - let data = response.payload.first().expect("Empty payload"); - let data = String::from_utf8(data.clone()).unwrap(); - let token: Token = serde_json::from_str(&data).unwrap(); - - token - })) + let response = session.mercury().get(url).await?; + let data = response.payload.first().expect("Empty payload"); + serde_json::from_slice(data.as_ref()).map_err(|_| MercuryError) } diff --git a/core/src/lib.rs b/core/src/lib.rs index a00d30bf4..bb3e21d59 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,56 +1,38 @@ -#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))] +#![allow(clippy::unused_io_amount)] -#[macro_use] -extern crate error_chain; -#[macro_use] -extern crate futures; -#[macro_use] -extern crate lazy_static; #[macro_use] extern crate log; -#[macro_use] -extern crate serde_derive; - -extern crate aes; -extern crate base64; -extern crate byteorder; -extern crate bytes; -extern crate hmac; -extern crate httparse; -extern crate hyper; -extern crate hyper_proxy; -extern crate num_bigint; -extern crate num_integer; -extern crate num_traits; -extern crate pbkdf2; -extern crate protobuf; -extern crate rand; -extern crate serde; -extern crate serde_json; -extern crate sha1; -extern crate shannon; -extern crate tokio_codec; -extern crate tokio_core; -extern crate tokio_io; -extern crate url; -extern crate uuid; -extern crate librespot_protocol as protocol; +use librespot_protocol as protocol; #[macro_use] mod component; -mod apresolve; + pub mod audio_key; pub mod authentication; pub mod cache; pub mod channel; pub mod config; mod connection; +#[doc(hidden)] pub mod diffie_hellman; pub mod keymaster; pub mod mercury; mod proxytunnel; pub mod session; pub mod spotify_id; +#[doc(hidden)] pub mod util; pub mod version; + +const AP_FALLBACK: &str = "ap.spotify.com:443"; + +#[cfg(feature = "apresolve")] +mod apresolve; + +#[cfg(not(feature = "apresolve"))] +mod apresolve { + pub async fn apresolve(_: Option<&url::Url>, _: Option) -> String { + return super::AP_FALLBACK.into(); + } +} diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 20e3f0db1..ef04e9854 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -1,13 +1,15 @@ -use crate::protocol; -use crate::util::url_encode; -use byteorder::{BigEndian, ByteOrder}; -use bytes::Bytes; -use futures::sync::{mpsc, oneshot}; -use futures::{Async, Future, Poll}; -use protobuf; 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::SeqGenerator; mod types; @@ -31,17 +33,18 @@ pub struct MercuryPending { callback: Option>>, } -pub struct MercuryFuture(oneshot::Receiver>); +pub struct MercuryFuture { + receiver: oneshot::Receiver>, +} + impl Future for MercuryFuture { - type Item = T; - type Error = MercuryError; - - fn poll(&mut self) -> Poll { - match self.0.poll() { - Ok(Async::Ready(Ok(value))) => Ok(Async::Ready(value)), - Ok(Async::Ready(Err(err))) => Err(err), - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(oneshot::Canceled) => Err(MercuryError), + type Output = Result; + + 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, } } } @@ -73,12 +76,12 @@ impl MercuryManager { let data = req.encode(&seq); self.session().send_packet(cmd, data); - MercuryFuture(rx) + MercuryFuture { receiver: rx } } pub fn get>(&self, uri: T) -> MercuryFuture { self.request(MercuryRequest { - method: MercuryMethod::GET, + method: MercuryMethod::Get, uri: uri.into(), content_type: None, payload: Vec::new(), @@ -87,7 +90,7 @@ impl MercuryManager { pub fn send>(&self, uri: T, data: Vec) -> MercuryFuture { self.request(MercuryRequest { - method: MercuryMethod::SEND, + method: MercuryMethod::Send, uri: uri.into(), content_type: None, payload: vec![data], @@ -101,24 +104,26 @@ impl MercuryManager { pub fn subscribe>( &self, uri: T, - ) -> Box, Error = MercuryError>> + ) -> impl Future, MercuryError>> + 'static { let uri = uri.into(); let request = self.request(MercuryRequest { - method: MercuryMethod::SUB, + method: MercuryMethod::Sub, uri: uri.clone(), content_type: None, payload: Vec::new(), }); let manager = self.clone(); - Box::new(request.map(move |response| { - let (tx, rx) = mpsc::unbounded(); + async move { + let response = request.await?; + + let (tx, rx) = mpsc::unbounded_channel(); manager.lock(move |inner| { if !inner.invalid { debug!("subscribed uri={} count={}", uri, response.payload.len()); - if response.payload.len() > 0 { + if !response.payload.is_empty() { // Old subscription protocol, watch the provided list of URIs for sub in response.payload { let mut sub: protocol::pubsub::Subscription = @@ -136,8 +141,8 @@ impl MercuryManager { } }); - rx - })) + Ok(rx) + } } pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { @@ -193,7 +198,7 @@ impl MercuryManager { let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap(); let response = MercuryResponse { - uri: url_encode(header.get_uri()).to_owned(), + uri: header.get_uri().to_string(), status_code: header.get_status_code(), payload: pending.parts, }; @@ -205,30 +210,41 @@ impl MercuryManager { if let Some(cb) = pending.callback { let _ = cb.send(Err(MercuryError)); } - } else { - if cmd == 0xb5 { - self.lock(|inner| { - let mut found = false; - inner.subscriptions.retain(|&(ref prefix, ref sub)| { - if response.uri.starts_with(prefix) { - found = true; - - // if send fails, remove from list of subs - // TODO: send unsub message - sub.unbounded_send(response.clone()).is_ok() - } else { - // URI doesn't match - true - } - }); - - if !found { - debug!("unknown subscription uri={}", response.uri); + } else if cmd == 0xb5 { + self.lock(|inner| { + let mut found = false; + + // TODO: This is just a workaround to make utf-8 encoded usernames work. + // A better solution would be to use an uri struct and urlencode it directly + // before sending while saving the subscription under its unencoded form. + let mut uri_split = response.uri.split('/'); + + let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string()) + .chain(uri_split.map(|component| { + form_urlencoded::byte_serialize(component.as_bytes()).collect::() + })) + .collect::>() + .join("/"); + + inner.subscriptions.retain(|&(ref prefix, ref sub)| { + if encoded_uri.starts_with(prefix) { + found = true; + + // if send fails, remove from list of subs + // TODO: send unsub message + sub.send(response.clone()).is_ok() + } else { + // URI doesn't match + true } - }) - } else if let Some(cb) = pending.callback { - let _ = cb.send(Ok(response)); - } + }); + + if !found { + debug!("unknown subscription uri={}", response.uri); + } + }) + } else if let Some(cb) = pending.callback { + let _ = cb.send(Ok(response)); } } diff --git a/core/src/mercury/sender.rs b/core/src/mercury/sender.rs index f00235ef9..383d449d2 100644 --- a/core/src/mercury/sender.rs +++ b/core/src/mercury/sender.rs @@ -1,4 +1,3 @@ -use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend}; use std::collections::VecDeque; use super::*; @@ -13,11 +12,27 @@ impl MercurySender { // TODO: pub(super) when stable pub(crate) fn new(mercury: MercuryManager, uri: String) -> MercurySender { MercurySender { - mercury: mercury, - uri: uri, + mercury, + uri, pending: VecDeque::new(), } } + + pub fn is_flushed(&self) -> bool { + self.pending.is_empty() + } + + pub fn send(&mut self, item: Vec) { + let task = self.mercury.send(self.uri.clone(), item); + self.pending.push_back(task); + } + + pub async fn flush(&mut self) -> Result<(), MercuryError> { + for fut in self.pending.drain(..) { + fut.await?; + } + Ok(()) + } } impl Clone for MercurySender { @@ -29,28 +44,3 @@ impl Clone for MercurySender { } } } - -impl Sink for MercurySender { - type SinkItem = Vec; - type SinkError = MercuryError; - - fn start_send(&mut self, item: Self::SinkItem) -> StartSend { - let task = self.mercury.send(self.uri.clone(), item); - self.pending.push_back(task); - Ok(AsyncSink::Ready) - } - - fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { - loop { - match self.pending.front_mut() { - Some(task) => { - try_ready!(task.poll()); - } - None => { - return Ok(Async::Ready(())); - } - } - self.pending.pop_front(); - } - } -} diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 57cedce51..402a954c5 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -6,10 +6,10 @@ use crate::protocol; #[derive(Debug, PartialEq, Eq)] pub enum MercuryMethod { - GET, - SUB, - UNSUB, - SEND, + Get, + Sub, + Unsub, + Send, } #[derive(Debug)] @@ -33,10 +33,10 @@ pub struct MercuryError; impl ToString for MercuryMethod { fn to_string(&self) -> String { match *self { - MercuryMethod::GET => "GET", - MercuryMethod::SUB => "SUB", - MercuryMethod::UNSUB => "UNSUB", - MercuryMethod::SEND => "SEND", + MercuryMethod::Get => "GET", + MercuryMethod::Sub => "SUB", + MercuryMethod::Unsub => "UNSUB", + MercuryMethod::Send => "SEND", } .to_owned() } @@ -45,9 +45,9 @@ impl ToString for MercuryMethod { impl MercuryMethod { pub fn command(&self) -> u8 { match *self { - MercuryMethod::GET | MercuryMethod::SEND => 0xb2, - MercuryMethod::SUB => 0xb3, - MercuryMethod::UNSUB => 0xb4, + MercuryMethod::Get | MercuryMethod::Send => 0xb2, + MercuryMethod::Sub => 0xb3, + MercuryMethod::Unsub => 0xb4, } } } diff --git a/core/src/proxytunnel.rs b/core/src/proxytunnel.rs index b13638469..6f1587f06 100644 --- a/core/src/proxytunnel.rs +++ b/core/src/proxytunnel.rs @@ -1,110 +1,55 @@ use std::io; -use std::str::FromStr; -use futures::{Async, Future, Poll}; -use httparse; -use hyper::Uri; -use tokio_io::io::{read, write_all, Read, Window, WriteAll}; -use tokio_io::{AsyncRead, AsyncWrite}; - -pub struct ProxyTunnel { - state: ProxyState, -} - -enum ProxyState { - ProxyConnect(WriteAll>), - ProxyResponse(Read>>), -} - -pub fn connect(connection: T, connect_url: &str) -> ProxyTunnel { - let proxy = proxy_connect(connection, connect_url); - ProxyTunnel { - state: ProxyState::ProxyConnect(proxy), - } -} - -impl Future for ProxyTunnel { - type Item = T; - type Error = io::Error; - - fn poll(&mut self) -> Poll { - use self::ProxyState::*; - loop { - self.state = match self.state { - ProxyConnect(ref mut write) => { - let (connection, mut accumulator) = try_ready!(write.poll()); - - let capacity = accumulator.capacity(); - accumulator.resize(capacity, 0); - let window = Window::new(accumulator); - - let read = read(connection, window); - ProxyResponse(read) +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +pub async fn proxy_connect( + mut proxy_connection: T, + connect_host: &str, + connect_port: &str, +) -> io::Result { + let mut buffer = Vec::new(); + buffer.extend_from_slice(b"CONNECT "); + buffer.extend_from_slice(connect_host.as_bytes()); + buffer.push(b':'); + buffer.extend_from_slice(connect_port.as_bytes()); + buffer.extend_from_slice(b" HTTP/1.1\r\n\r\n"); + + proxy_connection.write_all(buffer.as_ref()).await?; + + buffer.resize(buffer.capacity(), 0); + + let mut offset = 0; + loop { + let bytes_read = proxy_connection.read(&mut buffer[offset..]).await?; + if bytes_read == 0 { + return Err(io::Error::new(io::ErrorKind::Other, "Early EOF from proxy")); + } + offset += bytes_read; + + let mut headers = [httparse::EMPTY_HEADER; 16]; + let mut response = httparse::Response::new(&mut headers); + + let status = response + .parse(&buffer[..offset]) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + + if status.is_complete() { + return match response.code { + Some(200) => Ok(proxy_connection), // Proxy says all is well + Some(code) => { + let reason = response.reason.unwrap_or("no reason"); + let msg = format!("Proxy responded with {}: {}", code, reason); + Err(io::Error::new(io::ErrorKind::Other, msg)) } + None => Err(io::Error::new( + io::ErrorKind::Other, + "Malformed response from proxy", + )), + }; + } - ProxyResponse(ref mut read_f) => { - let (connection, mut window, bytes_read) = try_ready!(read_f.poll()); - - if bytes_read == 0 { - return Err(io::Error::new(io::ErrorKind::Other, "Early EOF from proxy")); - } - - let data_end = window.start() + bytes_read; - - let buf = window.get_ref()[0..data_end].to_vec(); - let mut headers = [httparse::EMPTY_HEADER; 16]; - let mut response = httparse::Response::new(&mut headers); - let status = match response.parse(&buf) { - Ok(status) => status, - Err(err) => { - return Err(io::Error::new(io::ErrorKind::Other, err.to_string())); - } - }; - - if status.is_complete() { - if let Some(code) = response.code { - if code == 200 { - // Proxy says all is well - return Ok(Async::Ready(connection)); - } else { - let reason = response.reason.unwrap_or("no reason"); - let msg = format!("Proxy responded with {}: {}", code, reason); - - return Err(io::Error::new(io::ErrorKind::Other, msg)); - } - } else { - return Err(io::Error::new( - io::ErrorKind::Other, - "Malformed response from proxy", - )); - } - } else { - if data_end >= window.end() { - // Allocate some more buffer space - let newsize = data_end + 100; - window.get_mut().resize(newsize, 0); - window.set_end(newsize); - } - // We did not get a full header - window.set_start(data_end); - let read = read(connection, window); - ProxyResponse(read) - } - } - } + if offset >= buffer.len() { + buffer.resize(buffer.len() + 100, 0); } } } - -fn proxy_connect(connection: T, connect_url: &str) -> WriteAll> { - let uri = Uri::from_str(connect_url).unwrap(); - let buffer = format!( - "CONNECT {0}:{1} HTTP/1.1\r\n\ - \r\n", - uri.host().expect(&format!("No host in {}", uri)), - uri.port().expect(&format!("No port in {}", uri)) - ) - .into_bytes(); - - write_all(connection, buffer) -} diff --git a/core/src/session.rs b/core/src/session.rs index 3754a000a..d7e478fa9 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -1,25 +1,37 @@ +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 byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; -use futures::sync::mpsc; -use futures::{Async, Future, IntoFuture, Poll, Stream}; -use tokio_core::reactor::{Handle, Remote}; - -use crate::apresolve::apresolve_or_fallback; +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; use crate::audio_key::AudioKeyManager; use crate::authentication::Credentials; use crate::cache::Cache; use crate::channel::ChannelManager; -use crate::component::Lazy; 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, @@ -34,12 +46,12 @@ struct SessionInternal { tx_connection: mpsc::UnboundedSender<(u8, Vec)>, - audio_key: Lazy, - channel: Lazy, - mercury: Lazy, + audio_key: OnceCell, + channel: OnceCell, + mercury: OnceCell, cache: Option>, - handle: Remote, + handle: tokio::runtime::Handle, session_id: usize, } @@ -50,127 +62,104 @@ static SESSION_COUNTER: AtomicUsize = AtomicUsize::new(0); pub struct Session(Arc); impl Session { - pub fn connect( + pub async fn connect( config: SessionConfig, credentials: Credentials, cache: Option, - handle: Handle, - ) -> Box> { - let access_point = - apresolve_or_fallback::(&handle, &config.proxy, &config.ap_port); - - let handle_ = handle.clone(); - let proxy = config.proxy.clone(); - let connection = access_point - .and_then(move |addr| { - info!("Connecting to AP \"{}\"", addr); - connection::connect(addr, &handle_, &proxy) - }) - .map_err(|io_err| io_err.into()); - - let device_id = config.device_id.clone(); - let authentication = connection.and_then(move |connection| { - connection::authenticate(connection, credentials, device_id) - }); - - let result = authentication.map(move |(transport, reusable_credentials)| { - info!("Authenticated as \"{}\" !", reusable_credentials.username); - if let Some(ref cache) = cache { - cache.save_credentials(&reusable_credentials); - } + ) -> Result { + let ap = apresolve(config.proxy.as_ref(), config.ap_port).await; - let (session, task) = Session::create( - &handle, - transport, - config, - cache, - reusable_credentials.username.clone(), - ); + info!("Connecting to AP \"{}\"", ap); + let mut conn = connection::connect(ap, config.proxy.as_ref()).await?; - handle.spawn(task.map_err(|e| { - error!("{:?}", e); - })); + let reusable_credentials = + connection::authenticate(&mut conn, credentials, &config.device_id).await?; + info!("Authenticated as \"{}\" !", reusable_credentials.username); + if let Some(cache) = &cache { + cache.save_credentials(&reusable_credentials); + } - session - }); + let session = Session::create( + conn, + config, + cache, + reusable_credentials.username, + tokio::runtime::Handle::current(), + ); - Box::new(result) + Ok(session) } fn create( - handle: &Handle, transport: connection::Transport, config: SessionConfig, cache: Option, username: String, - ) -> (Session, Box>) { + handle: tokio::runtime::Handle, + ) -> 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); let session = Session(Arc::new(SessionInternal { - config: config, + config, data: RwLock::new(SessionData { country: String::new(), canonical_username: username, invalid: false, time_delta: 0, }), - tx_connection: sender_tx, - cache: cache.map(Arc::new), - - audio_key: Lazy::new(), - channel: Lazy::new(), - mercury: Lazy::new(), - - handle: handle.remote().clone(), - - session_id: session_id, + audio_key: OnceCell::new(), + channel: OnceCell::new(), + mercury: OnceCell::new(), + handle, + session_id, })); - let sender_task = sender_rx - .map_err(|e| -> io::Error { panic!(e) }) - .forward(sink) - .map(|_| ()); + let sender_task = UnboundedReceiverStream::new(sender_rx) + .map(Ok) + .forward(sink); let receiver_task = DispatchTask(stream, session.weak()); - let task = Box::new( - (receiver_task, sender_task) - .into_future() - .map(|((), ())| ()), - ); - - (session, task) + let task = + futures_util::future::join(sender_task, receiver_task).map(|_| io::Result::<_>::Ok(())); + tokio::spawn(task); + session } pub fn audio_key(&self) -> &AudioKeyManager { - self.0.audio_key.get(|| AudioKeyManager::new(self.weak())) + self.0 + .audio_key + .get_or_init(|| AudioKeyManager::new(self.weak())) } pub fn channel(&self) -> &ChannelManager { - self.0.channel.get(|| ChannelManager::new(self.weak())) + self.0 + .channel + .get_or_init(|| ChannelManager::new(self.weak())) } pub fn mercury(&self) -> &MercuryManager { - self.0.mercury.get(|| MercuryManager::new(self.weak())) + self.0 + .mercury + .get_or_init(|| MercuryManager::new(self.weak())) } pub fn time_delta(&self) -> i64 { self.0.data.read().unwrap().time_delta } - pub fn spawn(&self, f: F) + pub fn spawn(&self, task: T) where - F: FnOnce(&Handle) -> R + Send + 'static, - R: IntoFuture, - R::Future: 'static, + T: Future + Send + 'static, + T::Output: Send + 'static, { - self.0.handle.spawn(f) + self.0.handle.spawn(task); } fn debug_info(&self) { @@ -182,7 +171,7 @@ impl Session { ); } - #[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))] + #[allow(clippy::match_same_arms)] fn dispatch(&self, cmd: u8, data: Bytes) { match cmd { 0x4 => { @@ -213,7 +202,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> { @@ -277,35 +266,34 @@ impl Drop for SessionInternal { struct DispatchTask(S, SessionWeak) where - S: Stream; + S: TryStream + Unpin; impl Future for DispatchTask where - S: Stream, - ::Error: ::std::fmt::Debug, + S: TryStream + Unpin, + ::Ok: std::fmt::Debug, { - type Item = (); - type Error = S::Error; + type Output = Result<(), S::Error>; - fn poll(&mut self) -> Poll { + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let session = match self.1.try_upgrade() { Some(session) => session, - None => return Ok(Async::Ready(())), + None => return Poll::Ready(Ok(())), }; loop { - let (cmd, data) = match self.0.poll() { - Ok(Async::Ready(Some(t))) => t, - Ok(Async::Ready(None)) => { + let (cmd, data) = match self.0.try_poll_next_unpin(cx) { + Poll::Ready(Some(Ok(t))) => t, + Poll::Ready(None) => { warn!("Connection to server closed."); session.shutdown(); - return Ok(Async::Ready(())); + return Poll::Ready(Ok(())); } - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(e) => { + Poll::Ready(Some(Err(e))) => { session.shutdown(); - return Err(From::from(e)); + return Poll::Ready(Err(e)); } + Poll::Pending => return Poll::Pending, }; session.dispatch(cmd, data); @@ -315,7 +303,7 @@ where impl Drop for DispatchTask where - S: Stream, + S: TryStream + Unpin, { fn drop(&mut self) { debug!("drop Dispatch"); diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 17327b478..801c6ac95 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -18,9 +18,9 @@ impl From<&str> for SpotifyAudioType { } } -impl Into<&str> for SpotifyAudioType { - fn into(self) -> &'static str { - match self { +impl From for &str { + fn from(audio_type: SpotifyAudioType) -> &'static str { + match audio_type { SpotifyAudioType::Track => "track", SpotifyAudioType::Podcast => "episode", SpotifyAudioType::NonPlayable => "unknown", @@ -45,7 +45,7 @@ impl SpotifyId { const SIZE_BASE16: usize = 32; const SIZE_BASE62: usize = 22; - fn as_track(n: u128) -> SpotifyId { + fn track(n: u128) -> SpotifyId { SpotifyId { id: n, audio_type: SpotifyAudioType::Track, @@ -71,7 +71,7 @@ impl SpotifyId { dst += p; } - Ok(SpotifyId::as_track(dst)) + Ok(SpotifyId::track(dst)) } /// Parses a base62 encoded [Spotify ID] into a `SpotifyId`. @@ -94,7 +94,7 @@ impl SpotifyId { dst += p; } - Ok(SpotifyId::as_track(dst)) + Ok(SpotifyId::track(dst)) } /// Creates a `SpotifyId` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order. @@ -102,7 +102,7 @@ impl SpotifyId { /// The resulting `SpotifyId` will default to a `SpotifyAudioType::TRACK`. pub fn from_raw(src: &[u8]) -> Result { match src.try_into() { - Ok(dst) => Ok(SpotifyId::as_track(u128::from_be_bytes(dst))), + Ok(dst) => Ok(SpotifyId::track(u128::from_be_bytes(dst))), Err(_) => Err(SpotifyIdError), } } diff --git a/core/src/util.rs b/core/src/util.rs new file mode 100644 index 000000000..df9ea714f --- /dev/null +++ b/core/src/util.rs @@ -0,0 +1,29 @@ +use std::mem; + +pub trait Seq { + fn next(&self) -> Self; +} + +macro_rules! impl_seq { + ($($ty:ty)*) => { $( + impl Seq for $ty { + fn next(&self) -> Self { (*self).wrapping_add(1) } + } + )* } +} + +impl_seq!(u8 u16 u32 u64 usize); + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] +pub struct SeqGenerator(T); + +impl SeqGenerator { + pub fn new(value: T) -> Self { + SeqGenerator(value) + } + + pub fn get(&mut self) -> T { + let value = self.0.next(); + mem::replace(&mut self.0, value) + } +} diff --git a/core/src/util/mod.rs b/core/src/util/mod.rs deleted file mode 100644 index c55d96016..000000000 --- a/core/src/util/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -use num_bigint::BigUint; -use num_integer::Integer; -use num_traits::{One, Zero}; -use rand::Rng; -use std::mem; -use std::ops::{Mul, Rem, Shr}; - -pub fn rand_vec(rng: &mut G, size: usize) -> Vec { - ::std::iter::repeat(()) - .map(|()| rng.gen()) - .take(size) - .collect() -} - -pub fn url_encode(inp: &str) -> String { - let mut encoded = String::new(); - - for c in inp.as_bytes().iter() { - match *c as char { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' | ':' | '/' => { - encoded.push(*c as char) - } - c => encoded.push_str(format!("%{:02X}", c as u32).as_str()), - }; - } - - encoded -} - -pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { - let mut base = base.clone(); - let mut exp = exp.clone(); - let mut result: BigUint = One::one(); - - while !exp.is_zero() { - if exp.is_odd() { - result = result.mul(&base).rem(modulus); - } - exp = exp.shr(1); - base = (&base).mul(&base).rem(modulus); - } - - result -} - -pub trait ReadSeek: ::std::io::Read + ::std::io::Seek {} -impl ReadSeek for T {} - -pub trait Seq { - fn next(&self) -> Self; -} - -macro_rules! impl_seq { - ($($ty:ty)*) => { $( - impl Seq for $ty { - fn next(&self) -> Self { (*self).wrapping_add(1) } - } - )* } -} - -impl_seq!(u8 u16 u32 u64 usize); - -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] -pub struct SeqGenerator(T); - -impl SeqGenerator { - pub fn new(value: T) -> Self { - SeqGenerator(value) - } - - pub fn get(&mut self) -> T { - let value = self.0.next(); - mem::replace(&mut self.0, value) - } -} diff --git a/core/tests/connect.rs b/core/tests/connect.rs new file mode 100644 index 000000000..4f1dbe6b8 --- /dev/null +++ b/core/tests/connect.rs @@ -0,0 +1,18 @@ +use librespot_core::authentication::Credentials; +use librespot_core::config::SessionConfig; +use librespot_core::session::Session; + +#[tokio::test] +async fn test_connection() { + let result = Session::connect( + SessionConfig::default(), + Credentials::with_password("test", "test"), + None, + ) + .await; + + match result { + Ok(_) => panic!("Authentication succeeded despite of bad credentials."), + Err(e) => assert_eq!(e.to_string(), "Login failed with reason: Bad credentials"), + }; +} diff --git a/examples/get_token.rs b/examples/get_token.rs index d722e994a..636155e04 100644 --- a/examples/get_token.rs +++ b/examples/get_token.rs @@ -1,5 +1,4 @@ use std::env; -use tokio_core::reactor::Core; use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; @@ -9,29 +8,26 @@ use librespot::core::session::Session; const SCOPES: &str = "streaming,user-read-playback-state,user-modify-playback-state,user-read-currently-playing"; -fn main() { - let mut core = Core::new().unwrap(); - let handle = core.handle(); - +#[tokio::main] +async fn main() { let session_config = SessionConfig::default(); let args: Vec<_> = env::args().collect(); if args.len() != 4 { - println!("Usage: {} USERNAME PASSWORD CLIENT_ID", args[0]); + eprintln!("Usage: {} USERNAME PASSWORD CLIENT_ID", args[0]); + return; } - let username = args[1].to_owned(); - let password = args[2].to_owned(); - let client_id = &args[3]; println!("Connecting.."); - let credentials = Credentials::with_password(username, password); - let session = core - .run(Session::connect(session_config, credentials, None, handle)) + let credentials = Credentials::with_password(&args[1], &args[2]); + let session = Session::connect(session_config, credentials, None) + .await .unwrap(); println!( "Token: {:#?}", - core.run(keymaster::get_token(&session, &client_id, SCOPES)) + keymaster::get_token(&session, &args[3], SCOPES) + .await .unwrap() ); } diff --git a/examples/play.rs b/examples/play.rs index 2c239ef32..d6c7196df 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -1,48 +1,44 @@ use std::env; -use tokio_core::reactor::Core; use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; use librespot::core::session::Session; use librespot::core::spotify_id::SpotifyId; -use librespot::playback::config::{AudioFormat, PlayerConfig}; - use librespot::playback::audio_backend; +use librespot::playback::config::{AudioFormat, PlayerConfig}; use librespot::playback::player::Player; -fn main() { - let mut core = Core::new().unwrap(); - let handle = core.handle(); - +#[tokio::main] +async fn main() { let session_config = SessionConfig::default(); let player_config = PlayerConfig::default(); let audio_format = AudioFormat::default(); let args: Vec<_> = env::args().collect(); if args.len() != 4 { - println!("Usage: {} USERNAME PASSWORD TRACK", args[0]); + eprintln!("Usage: {} USERNAME PASSWORD TRACK", args[0]); + return; } - let username = args[1].to_owned(); - let password = args[2].to_owned(); - let credentials = Credentials::with_password(username, password); + let credentials = Credentials::with_password(&args[1], &args[2]); let track = SpotifyId::from_base62(&args[3]).unwrap(); let backend = audio_backend::find(None).unwrap(); println!("Connecting .."); - let session = core - .run(Session::connect(session_config, credentials, None, handle)) + let session = Session::connect(session_config, credentials, None) + .await .unwrap(); - let (mut player, _) = Player::new(player_config, session.clone(), None, move || { - (backend)(None, audio_format) + let (mut player, _) = Player::new(player_config, session, None, move || { + backend(None, audio_format) }); player.load(track, true, 0); println!("Playing..."); - core.run(player.get_end_of_track_future()).unwrap(); + + player.await_end_of_track().await; println!("Done"); } diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index fc288d188..e96938cbc 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -1,6 +1,5 @@ use env_logger; use std::env; -use tokio_core::reactor::Core; use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; @@ -8,35 +7,32 @@ use librespot::core::session::Session; use librespot::core::spotify_id::SpotifyId; use librespot::metadata::{Metadata, Playlist, Track}; -fn main() { +#[tokio::main] +async fn main() { env_logger::init(); - let mut core = Core::new().unwrap(); - let handle = core.handle(); - let session_config = SessionConfig::default(); let args: Vec<_> = env::args().collect(); if args.len() != 4 { - println!("Usage: {} USERNAME PASSWORD PLAYLIST", args[0]); + eprintln!("Usage: {} USERNAME PASSWORD PLAYLIST", args[0]); + return; } - let username = args[1].to_owned(); - let password = args[2].to_owned(); - let credentials = Credentials::with_password(username, password); + let credentials = Credentials::with_password(&args[1], &args[2]); - let uri_split = args[3].split(":"); + let uri_split = args[3].split(':'); let uri_parts: Vec<&str> = uri_split.collect(); println!("{}, {}, {}", uri_parts[0], uri_parts[1], uri_parts[2]); let plist_uri = SpotifyId::from_base62(uri_parts[2]).unwrap(); - let session = core - .run(Session::connect(session_config, credentials, None, handle)) + let session = Session::connect(session_config, credentials, None) + .await .unwrap(); - let plist = core.run(Playlist::get(&session, plist_uri)).unwrap(); + let plist = Playlist::get(&session, plist_uri).await.unwrap(); println!("{:?}", plist); for track_id in plist.tracks { - let plist_track = core.run(Track::get(&session, track_id)).unwrap(); + let plist_track = Track::get(&session, track_id).await.unwrap(); println!("track: {} ", plist_track.name); } } diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index f90a9c539..f3087b8a9 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -8,9 +8,8 @@ repository = "https://github.com/librespot-org/librespot" edition = "2018" [dependencies] +async-trait = "0.1" byteorder = "1.3" -futures = "0.1" -linear-map = "1.2" protobuf = "~2.14.0" log = "0.4" diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 8bd422ce5..2c982ec7d 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -1,23 +1,19 @@ +#![allow(clippy::unused_io_amount)] + #[macro_use] extern crate log; -extern crate byteorder; -extern crate futures; -extern crate linear_map; -extern crate protobuf; - -extern crate librespot_core; -extern crate librespot_protocol as protocol; +#[macro_use] +extern crate async_trait; pub mod cover; -use futures::future; -use futures::Future; -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; @@ -61,7 +57,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, @@ -69,81 +65,67 @@ pub struct AudioItem { } impl AudioItem { - pub fn get_audio_item( - session: &Session, - id: SpotifyId, - ) -> Box> { + pub async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { match id.audio_type { - SpotifyAudioType::Track => Track::get_audio_item(session, id), - SpotifyAudioType::Podcast => Episode::get_audio_item(session, id), - SpotifyAudioType::NonPlayable => { - Box::new(future::err::(MercuryError)) - } + SpotifyAudioType::Track => Track::get_audio_item(session, id).await, + SpotifyAudioType::Podcast => Episode::get_audio_item(session, id).await, + SpotifyAudioType::NonPlayable => Err(MercuryError), } } } +#[async_trait] trait AudioFiles { - fn get_audio_item( - session: &Session, - id: SpotifyId, - ) -> Box>; + async fn get_audio_item(session: &Session, id: SpotifyId) -> Result; } +#[async_trait] impl AudioFiles for Track { - fn get_audio_item( - session: &Session, - id: SpotifyId, - ) -> Box> { - Box::new(Self::get(session, id).and_then(move |item| { - Ok(AudioItem { - id: id, - uri: format!("spotify:track:{}", id.to_base62()), - files: item.files, - name: item.name, - duration: item.duration, - available: item.available, - alternatives: Some(item.alternatives), - }) - })) + async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { + let item = Self::get(session, id).await?; + Ok(AudioItem { + id, + uri: format!("spotify:track:{}", id.to_base62()), + files: item.files, + name: item.name, + duration: item.duration, + available: item.available, + alternatives: Some(item.alternatives), + }) } } +#[async_trait] impl AudioFiles for Episode { - fn get_audio_item( - session: &Session, - id: SpotifyId, - ) -> Box> { - Box::new(Self::get(session, id).and_then(move |item| { - Ok(AudioItem { - id: id, - uri: format!("spotify:episode:{}", id.to_base62()), - files: item.files, - name: item.name, - duration: item.duration, - available: item.available, - alternatives: None, - }) - })) + async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { + let item = Self::get(session, id).await?; + + Ok(AudioItem { + id, + uri: format!("spotify:episode:{}", id.to_base62()), + files: item.files, + name: item.name, + duration: item.duration, + available: item.available, + alternatives: None, + }) } } + +#[async_trait] pub trait Metadata: Send + Sized + 'static { type Message: protobuf::Message; fn request_url(id: SpotifyId) -> String; fn parse(msg: &Self::Message, session: &Session) -> Self; - fn get(session: &Session, id: SpotifyId) -> Box> { + async fn get(session: &Session, id: SpotifyId) -> Result { let uri = Self::request_url(id); - let request = session.mercury().get(uri); - - let session = session.clone(); - Box::new(request.and_then(move |response| { - let data = response.payload.first().expect("Empty payload"); - let msg: Self::Message = protobuf::parse_from_bytes(data).unwrap(); + let response = session.mercury().get(uri).await?; + let data = response.payload.first().expect("Empty payload"); + let msg: Self::Message = protobuf::parse_from_bytes(data).unwrap(); - Ok(Self::parse(&msg, &session)) - })) + Ok(Self::parse(&msg, &session)) } } @@ -154,7 +136,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, } @@ -176,7 +158,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, @@ -239,8 +221,8 @@ impl Metadata for Track { name: msg.get_name().to_owned(), duration: msg.get_duration(), album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(), - artists: artists, - files: files, + artists, + files, alternatives: msg .get_alternative() .iter() @@ -289,9 +271,9 @@ impl Metadata for Album { Album { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), - artists: artists, - tracks: tracks, - covers: covers, + artists, + tracks, + covers, } } } @@ -309,7 +291,7 @@ impl Metadata for Playlist { .get_items() .iter() .map(|item| { - let uri_split = item.get_uri().split(":"); + let uri_split = item.get_uri().split(':'); let uri_parts: Vec<&str> = uri_split.collect(); SpotifyId::from_base62(uri_parts[2]).unwrap() }) @@ -326,7 +308,7 @@ impl Metadata for Playlist { Playlist { revision: msg.get_revision().to_vec(), name: msg.get_attributes().get_name().to_owned(), - tracks: tracks, + tracks, user: msg.get_owner_username().to_string(), } } @@ -359,7 +341,7 @@ impl Metadata for Artist { Artist { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), - top_tracks: top_tracks, + top_tracks, } } } @@ -405,8 +387,8 @@ impl Metadata for Episode { duration: msg.get_duration().to_owned(), language: msg.get_language().to_owned(), show: SpotifyId::from_raw(msg.get_show().get_gid()).unwrap(), - covers: covers, - files: files, + covers, + files, available: parse_restrictions(msg.get_restriction(), &country, "premium"), explicit: msg.get_explicit().to_owned(), } @@ -444,8 +426,8 @@ impl Metadata for Show { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), publisher: msg.get_publisher().to_owned(), - episodes: episodes, - covers: covers, + episodes, + covers, } } } diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 0666259d0..b8736db04 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -18,10 +18,12 @@ path = "../metadata" version = "0.1.6" [dependencies] -futures = "0.1" +futures-executor = "0.3" +futures-util = { version = "0.3", default_features = false, features = ["alloc"] } log = "0.4" -byteorder = "1.3" +byteorder = "1.4" shell-words = "1.0.0" +tokio = { version = "1", features = ["sync"] } alsa = { version = "0.5", optional = true } portaudio-rs = { version = "0.3", optional = true } @@ -29,20 +31,23 @@ libpulse-binding = { version = "2", optional = true, default-features = f libpulse-simple-binding = { version = "2", optional = true, default-features = false } jack = { version = "0.6", optional = true } libc = { version = "0.2", optional = true } -rodio = { version = "0.13", optional = true, default-features = false } -cpal = { version = "0.13", optional = true } sdl2 = { version = "0.34.3", optional = true } gstreamer = { version = "0.16", optional = true } gstreamer-app = { version = "0.16", optional = true } glib = { version = "0.10", optional = true } zerocopy = { version = "0.3" } +# Rodio dependencies +rodio = { version = "0.13", optional = true, default-features = false } +cpal = { version = "0.13", optional = true } +thiserror = { version = "1", optional = true } + [features] alsa-backend = ["alsa"] portaudio-backend = ["portaudio-rs"] pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] jackaudio-backend = ["jack"] -rodiojack-backend = ["rodio", "cpal/jack"] -rodio-backend = ["rodio", "cpal"] +rodio-backend = ["rodio", "cpal", "thiserror"] +rodiojack-backend = ["rodio", "cpal/jack", "thiserror"] sdl-backend = ["sdl2"] gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 1d5518785..54fed319d 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -87,7 +87,7 @@ impl Open for AlsaSink { Self { pcm: None, - format: format, + format, device: name, buffer: vec![], } @@ -146,7 +146,7 @@ impl SinkAsBytes for AlsaSink { .extend_from_slice(&data[processed_data..processed_data + data_to_buffer]); processed_data += data_to_buffer; if self.buffer.len() == self.buffer.capacity() { - self.write_buf().expect("could not append to buffer"); + self.write_buf(); self.buffer.clear(); } } @@ -156,14 +156,12 @@ impl SinkAsBytes for AlsaSink { } impl AlsaSink { - fn write_buf(&mut self) -> io::Result<()> { + fn write_buf(&mut self) { let pcm = self.pcm.as_mut().unwrap(); let io = pcm.io_bytes(); match io.writei(&self.buffer) { Ok(_) => (), Err(err) => pcm.try_recover(err, false).unwrap(), }; - - Ok(()) } } diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index d59677ac4..93b718dcd 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -2,11 +2,15 @@ use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; + +use gstreamer as gst; +use gstreamer_app as gst_app; + use gst::prelude::*; -use gst::*; +use zerocopy::AsBytes; + use std::sync::mpsc::{sync_channel, SyncSender}; use std::{io, thread}; -use zerocopy::AsBytes; #[allow(dead_code)] pub struct GstreamerSink { @@ -68,14 +72,13 @@ impl Open for GstreamerSink { thread::spawn(move || { for data in rx { let buffer = bufferpool.acquire_buffer(None); - if !buffer.is_err() { - let mut okbuffer = buffer.unwrap(); - let mutbuf = okbuffer.make_mut(); + if let Ok(mut buffer) = buffer { + let mutbuf = buffer.make_mut(); mutbuf.set_size(data.len()); mutbuf .copy_from_slice(0, data.as_bytes()) - .expect("failed to copy from slice"); - let _eat = appsrc.push_buffer(okbuffer); + .expect("Failed to copy from slice"); + let _eat = appsrc.push_buffer(buffer); } } }); @@ -85,8 +88,8 @@ impl Open for GstreamerSink { let watch_mainloop = thread_mainloop.clone(); bus.add_watch(move |_, msg| { match msg.view() { - MessageView::Eos(..) => watch_mainloop.quit(), - MessageView::Error(err) => { + gst::MessageView::Eos(..) => watch_mainloop.quit(), + gst::MessageView::Error(err) => { println!( "Error from {:?}: {} ({:?})", err.get_src().map(|s| s.get_path_string()), @@ -109,9 +112,9 @@ impl Open for GstreamerSink { .expect("unable to set the pipeline to the `Playing` state"); Self { - tx: tx, - pipeline: pipeline, - format: format, + tx, + pipeline, + format, } } } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 24a94a3e5..aca2edd2b 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -47,7 +47,7 @@ impl Open for JackSink { } info!("Using JACK sink with format {:?}", AudioFormat::F32); - let client_name = client_name.unwrap_or("librespot".to_string()); + let client_name = client_name.unwrap_or_else(|| "librespot".to_string()); let (client, _status) = Client::new(&client_name[..], ClientOptions::NO_START_SERVER).unwrap(); let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); @@ -63,7 +63,7 @@ impl Open for JackSink { Self { send: tx, - active_client: active_client, + active_client, } } } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 94b6a5296..72659f196 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -12,6 +12,8 @@ pub trait Sink { fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; } +pub type SinkBuilder = fn(Option, AudioFormat) -> Box; + pub trait SinkAsBytes { fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; } @@ -24,25 +26,25 @@ fn mk_sink(device: Option, format: AudioFormat macro_rules! sink_as_bytes { () => { fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - use crate::audio::{i24, SamplesConverter}; + use crate::audio::convert::{self, i24}; use zerocopy::AsBytes; match packet { AudioPacket::Samples(samples) => match self.format { AudioFormat::F32 => self.write_bytes(samples.as_bytes()), AudioFormat::S32 => { - let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); + let samples_s32: &[i32] = &convert::to_s32(samples); self.write_bytes(samples_s32.as_bytes()) } AudioFormat::S24 => { - let samples_s24: &[i32] = &SamplesConverter::to_s24(samples); + let samples_s24: &[i32] = &convert::to_s24(samples); self.write_bytes(samples_s24.as_bytes()) } AudioFormat::S24_3 => { - let samples_s24_3: &[i24] = &SamplesConverter::to_s24_3(samples); + let samples_s24_3: &[i24] = &convert::to_s24_3(samples); self.write_bytes(samples_s24_3.as_bytes()) } AudioFormat::S16 => { - let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); self.write_bytes(samples_s16.as_bytes()) } }, @@ -83,18 +85,6 @@ mod jackaudio; #[cfg(feature = "jackaudio-backend")] use self::jackaudio::JackSink; -#[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(all( - feature = "rodiojack-backend", - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") -))] -use self::rodio::JackRodioSink; - #[cfg(feature = "gstreamer-backend")] mod gstreamer; #[cfg(feature = "gstreamer-backend")] @@ -102,8 +92,6 @@ use self::gstreamer::GstreamerSink; #[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] mod rodio; -#[cfg(feature = "rodio-backend")] -use self::rodio::RodioSink; #[cfg(feature = "sdl-backend")] mod sdl; @@ -116,10 +104,7 @@ use self::pipe::StdoutSink; mod subprocess; use self::subprocess::SubprocessSink; -pub const BACKENDS: &'static [( - &'static str, - fn(Option, AudioFormat) -> Box, -)] = &[ +pub const BACKENDS: &[(&str, SinkBuilder)] = &[ #[cfg(feature = "alsa-backend")] ("alsa", mk_sink::), #[cfg(feature = "portaudio-backend")] @@ -128,22 +113,19 @@ pub const BACKENDS: &'static [( ("pulseaudio", mk_sink::), #[cfg(feature = "jackaudio-backend")] ("jackaudio", mk_sink::), - #[cfg(all( - feature = "rodiojack-backend", - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") - ))] - ("rodiojack", mk_sink::), #[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::), ("subprocess", mk_sink::), ]; -pub fn find(name: Option) -> Option, AudioFormat) -> Box> { +pub fn find(name: Option) -> Option { if let Some(name) = name { BACKENDS .iter() diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 6948db940..4c6f27c11 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -18,10 +18,7 @@ impl Open for StdoutSink { _ => Box::new(io::stdout()), }; - Self { - output: output, - format: format, - } + Self { output, format } } } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 5faff6ca4..234a9af6a 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,8 +1,7 @@ use super::{Open, Sink}; -use crate::audio::{AudioPacket, SamplesConverter}; +use crate::audio::{convert, AudioPacket}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; -use portaudio_rs; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; use std::io; @@ -157,11 +156,11 @@ impl<'a> Sink for PortAudioSink<'a> { write_sink!(ref mut stream, samples) } Self::S32(stream, _parameters) => { - let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); + let samples_s32: &[i32] = &convert::to_s32(samples); write_sink!(ref mut stream, samples_s32) } Self::S16(stream, _parameters) => { - let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); write_sink!(ref mut stream, samples_s16) } }; diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 507e453c1..b165c0b2e 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -38,9 +38,9 @@ impl Open for PulseAudioSink { Self { s: None, - ss: ss, - device: device, - format: format, + ss, + device, + format, } } } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 908099f1b..65436a326 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,197 +1,212 @@ -use super::{Open, Sink}; -extern crate cpal; -extern crate rodio; -use crate::audio::{AudioPacket, SamplesConverter}; -use crate::config::AudioFormat; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; -use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; -// most code is shared between RodioSink and JackRodioSink -macro_rules! rodio_sink { - ($name: ident) => { - pub struct $name { - rodio_sink: rodio::Sink, - // We have to keep hold of this object, or the Sink can't play... - #[allow(dead_code)] - stream: rodio::OutputStream, - format: AudioFormat, - } - - impl Sink for $name { - start_stop_noop!(); - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let samples = packet.samples(); - match self.format { - AudioFormat::F32 => { - let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); - self.rodio_sink.append(source); - }, - AudioFormat::S16 => { - let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); - let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples_s16); - self.rodio_sink.append(source); - }, - _ => unreachable!(), - }; - - // Chunk sizes seem to be about 256 to 3000 ish items long. - // Assuming they're on average 1628 then a half second buffer is: - // 44100 elements --> about 27 chunks - while self.rodio_sink.len() > 26 { - // sleep and wait for rodio to drain a bit - thread::sleep(time::Duration::from_millis(10)); - } - Ok(()) - } - } - - impl $name { - fn open_sink(host: &cpal::Host, device: Option, format: AudioFormat) -> $name { - match format { - AudioFormat::F32 => { - #[cfg(target_os = "linux")] - { - warn!("Rodio output to Alsa is known to cause garbled sound, consider using `--backend alsa`"); - } - }, - AudioFormat::S16 => {}, - _ => unimplemented!("Rodio currently only supports F32 and S16 formats"), - } +use cpal::traits::{DeviceTrait, HostTrait}; +use thiserror::Error; - let rodio_device = match_device(&host, device); - debug!("Using cpal device"); - let (stream, stream_handle) = rodio::OutputStream::try_from_device(&rodio_device) - .expect("couldn't open output stream."); - debug!("Using Rodio stream"); - let sink = rodio::Sink::try_new(&stream_handle).expect("couldn't create output sink."); - debug!("Using Rodio sink"); - - Self { - rodio_sink: sink, - stream: stream, - format: format, - } - } - } - }; -} -rodio_sink!(RodioSink); +use super::Sink; +use crate::audio::{convert, AudioPacket}; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; #[cfg(all( feature = "rodiojack-backend", - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") + not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")) ))] -rodio_sink!(JackRodioSink); +compile_error!("Rodio JACK backend is currently only supported on linux."); -fn list_formats(ref device: &rodio::Device) { - let default_fmt = match device.default_output_config() { - Ok(fmt) => cpal::SupportedStreamConfig::from(fmt), - Err(e) => { - warn!("Error getting default Rodio output config: {}", e); - return; - } - }; - debug!(" Default config:"); - debug!(" {:?}", default_fmt); +#[cfg(feature = "rodio-backend")] +pub fn mk_rodio(device: Option, format: AudioFormat) -> Box { + Box::new(open(cpal::default_host(), device, format)) +} + +#[cfg(feature = "rodiojack-backend")] +pub fn mk_rodiojack(device: Option, format: AudioFormat) -> Box { + Box::new(open( + cpal::host_from_id(cpal::HostId::Jack).unwrap(), + device, + format, + )) +} - let mut output_configs = match device.supported_output_configs() { - Ok(f) => f.peekable(), +#[derive(Debug, Error)] +pub enum RodioError { + #[error("Rodio: no device available")] + NoDeviceAvailable, + #[error("Rodio: device \"{0}\" is not available")] + DeviceNotAvailable(String), + #[error("Rodio play error: {0}")] + PlayError(#[from] rodio::PlayError), + #[error("Rodio stream error: {0}")] + StreamError(#[from] rodio::StreamError), + #[error("Cannot get audio devices: {0}")] + DevicesError(#[from] cpal::DevicesError), +} + +pub struct RodioSink { + rodio_sink: rodio::Sink, + format: AudioFormat, + _stream: rodio::OutputStream, +} + +fn list_formats(device: &rodio::Device) { + match device.default_output_config() { + Ok(cfg) => { + debug!(" Default config:"); + debug!(" {:?}", cfg); + } Err(e) => { - warn!("Error getting supported Rodio output configs: {}", e); - return; + // Use loglevel debug, since even the output is only debug + debug!("Error getting default rodio::Sink config: {}", e); } }; - if output_configs.peek().is_some() { - debug!(" Available output configs:"); - for format in output_configs { - debug!(" {:?}", format); + match device.supported_output_configs() { + Ok(mut cfgs) => { + if let Some(first) = cfgs.next() { + debug!(" Available configs:"); + debug!(" {:?}", first); + } else { + return; + } + + for cfg in cfgs { + debug!(" {:?}", cfg); + } + } + Err(e) => { + debug!("Error getting supported rodio::Sink configs: {}", e); } } } -fn list_outputs(ref host: &cpal::Host) { - let default_device = get_default_device(host); - let default_device_name = default_device.name().expect("cannot get output name"); - println!("Default audio device:\n {}", default_device_name); - list_formats(&default_device); - - println!("Other available audio devices:"); - - let found_devices = host.output_devices().expect(&format!( - "Cannot get list of output devices of host: {:?}", - host.id() - )); - for device in found_devices { - let device_name = device.name().expect("cannot get output name"); - if device_name != default_device_name { - println!(" {}", device_name); - list_formats(&device); +fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> { + let mut default_device_name = None; + + if let Some(default_device) = host.default_output_device() { + default_device_name = default_device.name().ok(); + println!( + "Default Audio Device:\n {}", + default_device_name.as_deref().unwrap_or("[unknown name]") + ); + + list_formats(&default_device); + + println!("Other Available Audio Devices:"); + } else { + warn!("No default device was found"); + } + + for device in host.output_devices()? { + match device.name() { + Ok(name) if Some(&name) == default_device_name.as_ref() => (), + Ok(name) => { + println!(" {}", name); + list_formats(&device); + } + Err(e) => { + warn!("Cannot get device name: {}", e); + println!(" [unknown name]"); + list_formats(&device); + } } } -} -fn get_default_device(ref host: &cpal::Host) -> rodio::Device { - host.default_output_device() - .expect("no default output device available") + Ok(()) } -fn match_device(ref host: &cpal::Host, device: Option) -> rodio::Device { - match device { +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(host) { + Ok(()) => 0, + Err(e) => { + error!("{}", e); + 1 + } + }; + exit(exit_code) + } Some(device_name) => { - if device_name == "?".to_string() { - list_outputs(host); - exit(0) - } + 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 => host + .default_output_device() + .ok_or(RodioError::NoDeviceAvailable)?, + }; - let found_devices = host.output_devices().expect(&format!( - "cannot get list of output devices of host: {:?}", - host.id() - )); - for d in found_devices { - if d.name().expect("cannot get output name") == device_name { - return d; - } - } - println!("No output sink matching '{}' found.", device_name); - exit(0) + let name = rodio_device.name().ok(); + info!( + "Using audio device: {}", + name.as_deref().unwrap_or("[unknown name]") + ); + + let (stream, handle) = rodio::OutputStream::try_from_device(&rodio_device)?; + let sink = rodio::Sink::try_new(&handle)?; + Ok((sink, stream)) +} + +pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> RodioSink { + debug!( + "Using rodio sink with format {:?} and cpal host: {}", + format, + host.id().name() + ); + + match format { + AudioFormat::F32 => { + #[cfg(target_os = "linux")] + warn!("Rodio output to Alsa is known to cause garbled sound, consider using `--backend alsa`") } - None => return get_default_device(host), + AudioFormat::S16 => (), + _ => unimplemented!("Rodio currently only supports F32 and S16 formats"), } -} -impl Open for RodioSink { - fn open(device: Option, format: AudioFormat) -> RodioSink { - let host = cpal::default_host(); - info!( - "Using Rodio sink with format {:?} and cpal host: {:?}", - format, - host.id() - ); - Self::open_sink(&host, device, format) + let (sink, stream) = create_sink(&host, device).unwrap(); + + debug!("Rodio sink was created"); + RodioSink { + rodio_sink: sink, + format, + _stream: stream, } } -#[cfg(all( - feature = "rodiojack-backend", - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") -))] -impl Open for JackRodioSink { - fn open(device: Option, format: AudioFormat) -> JackRodioSink { - let host = cpal::host_from_id( - cpal::available_hosts() - .into_iter() - .find(|id| *id == cpal::HostId::Jack) - .expect("JACK host not found"), - ) - .expect("JACK host not found"); - info!( - "Using JACK Rodio sink with format {:?} and cpal JACK host", - format - ); - Self::open_sink(&host, device, format) +impl Sink for RodioSink { + start_stop_noop!(); + + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + let samples = packet.samples(); + match self.format { + AudioFormat::F32 => { + let source = + rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); + self.rodio_sink.append(source); + } + AudioFormat::S16 => { + let samples_s16: &[i16] = &convert::to_s16(samples); + let source = rodio::buffer::SamplesBuffer::new( + NUM_CHANNELS as u16, + SAMPLE_RATE, + samples_s16, + ); + self.rodio_sink.append(source); + } + _ => unreachable!(), + }; + + // Chunk sizes seem to be about 256 to 3000 ish items long. + // Assuming they're on average 1628 then a half second buffer is: + // 44100 elements --> about 27 chunks + while self.rodio_sink.len() > 26 { + // sleep and wait for rodio to drain a bit + thread::sleep(time::Duration::from_millis(10)); + } + Ok(()) } } diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 0a3fd433b..29566533b 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,5 +1,5 @@ use super::{Open, Sink}; -use crate::audio::{AudioPacket, SamplesConverter}; +use crate::audio::{convert, AudioPacket}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; @@ -97,12 +97,12 @@ impl Sink for SdlSink { queue.queue(samples) } Self::S32(queue) => { - let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); + let samples_s32: &[i32] = &convert::to_s32(samples); drain_sink!(queue, AudioFormat::S32.size()); queue.queue(samples_s32) } Self::S16(queue) => { - let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); drain_sink!(queue, AudioFormat::S16.size()); queue.queue(samples_s16) } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index bebb6ea01..b4af1b40f 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -2,6 +2,7 @@ use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; use crate::config::AudioFormat; use shell_words::split; + use std::io::{self, Write}; use std::process::{Child, Command, Stdio}; @@ -17,9 +18,9 @@ impl Open for SubprocessSink { if let Some(shell_command) = shell_command { SubprocessSink { - shell_command: shell_command, + shell_command, child: None, - format: format, + format, } } else { panic!("subprocess sink requires specifying a shell command"); diff --git a/playback/src/config.rs b/playback/src/config.rs index 95c970923..f8f028933 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,4 +1,4 @@ -use crate::audio::i24; +use crate::audio::convert::i24; use std::convert::TryFrom; use std::mem; use std::str::FromStr; diff --git a/playback/src/lib.rs b/playback/src/lib.rs index f46064305..9613f2e52 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/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 1e00cc846..5e0a963f4 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -1,10 +1,7 @@ use super::AudioFilter; use super::{Mixer, MixerConfig}; -use std; use std::error::Error; -use alsa; - const SND_CTL_TLV_DB_GAIN_MUTE: i64 = -9999999; #[derive(Clone)] @@ -36,13 +33,12 @@ impl AlsaMixer { let mixer = alsa::mixer::Mixer::new(&config.card, false)?; let sid = alsa::mixer::SelemId::new(&config.mixer, config.index); - let selem = mixer.find_selem(&sid).expect( - format!( + let selem = mixer.find_selem(&sid).unwrap_or_else(|| { + panic!( "Couldn't find simple mixer control for {},{}", &config.mixer, &config.index, ) - .as_str(), - ); + }); let (min, max) = selem.get_playback_volume_range(); let (min_db, max_db) = selem.get_playback_db_range(); let hw_mix = selem @@ -72,14 +68,14 @@ impl AlsaMixer { } Ok(AlsaMixer { - config: config, + config, params: AlsaMixerVolumeParams { - min: min, - max: max, + min, + max, range: (max - min) as f64, - min_db: min_db, - max_db: max_db, - has_switch: has_switch, + min_db, + max_db, + has_switch, }, }) } diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 3424526ae..af41c6f43 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -42,11 +42,13 @@ impl Default for MixerConfig { pub mod softmixer; use self::softmixer::SoftMixer; +type MixerFn = fn(Option) -> Box; + fn mk_sink(device: Option) -> Box { Box::new(M::open(device)) } -pub fn find>(name: Option) -> Option) -> Box> { +pub fn find>(name: Option) -> Option { match name.as_ref().map(AsRef::as_ref) { None | Some("softvol") => Some(mk_sink::), #[cfg(feature = "alsa-backend")] diff --git a/playback/src/player.rs b/playback/src/player.rs index 0a573c932..3f0778f9a 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,19 +1,15 @@ -use byteorder::{LittleEndian, ReadBytesExt}; -use futures; -use futures::{future, Async, Future, Poll, Stream}; -use std; -use std::borrow::Cow; use std::cmp::max; -use std::io::{Read, Result, Seek, SeekFrom}; -use std::mem; -use std::thread; +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 crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; - -use librespot_core::util::SeqGenerator; +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}; @@ -22,6 +18,10 @@ use crate::audio::{ READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; use crate::audio_backend::Sink; +use crate::config::{Bitrate, NormalisationMethod, NormalisationType, 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; @@ -33,7 +33,7 @@ const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; const DB_VOLTAGE_RATIO: f32 = 20.0; pub struct Player { - commands: Option>, + commands: Option>, thread_handle: Option>, play_request_id_generator: SeqGenerator, } @@ -50,7 +50,7 @@ pub type SinkEventCallback = Box; struct PlayerInternal { session: Session, config: PlayerConfig, - commands: futures::sync::mpsc::UnboundedReceiver, + commands: mpsc::UnboundedReceiver, state: PlayerState, preload: PlayerPreload, @@ -58,7 +58,7 @@ struct PlayerInternal { sink_status: SinkStatus, sink_event_callback: Option, audio_filter: Option>, - event_senders: Vec>, + event_senders: Vec>, limiter_active: bool, limiter_attack_counter: u32, @@ -82,7 +82,7 @@ enum PlayerCommand { Pause, Stop, Seek(u32), - AddEventSender(futures::sync::mpsc::UnboundedSender), + AddEventSender(mpsc::UnboundedSender), SetSinkEventCallback(Option), EmitVolumeSetEvent(u16), } @@ -194,7 +194,7 @@ impl PlayerEvent { } } -pub type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver; +pub type PlayerEventChannel = mpsc::UnboundedReceiver; #[derive(Clone, Copy, Debug)] pub struct NormalisationData { @@ -206,28 +206,27 @@ pub struct NormalisationData { impl NormalisationData { pub fn db_to_ratio(db: f32) -> f32 { - return f32::powf(10.0, db / DB_VOLTAGE_RATIO); + f32::powf(10.0, db / DB_VOLTAGE_RATIO) } pub fn ratio_to_db(ratio: f32) -> f32 { - return ratio.log10() * DB_VOLTAGE_RATIO; + ratio.log10() * DB_VOLTAGE_RATIO } - fn parse_from_file(mut file: T) -> Result { + fn parse_from_file(mut file: T) -> io::Result { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; - file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET)) - .unwrap(); + file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?; - let track_gain_db = file.read_f32::().unwrap(); - let track_peak = file.read_f32::().unwrap(); - let album_gain_db = file.read_f32::().unwrap(); - let album_peak = file.read_f32::().unwrap(); + let track_gain_db = file.read_f32::()?; + let track_peak = file.read_f32::()?; + let album_gain_db = file.read_f32::()?; + let album_peak = file.read_f32::()?; let r = NormalisationData { - track_gain_db: track_gain_db, - track_peak: track_peak, - album_gain_db: album_gain_db, - album_peak: album_peak, + track_gain_db, + track_peak, + album_gain_db, + album_peak, }; Ok(r) @@ -288,15 +287,15 @@ impl Player { where F: FnOnce() -> Box + Send + 'static, { - let (cmd_tx, cmd_rx) = futures::sync::mpsc::unbounded(); - let (event_sender, event_receiver) = futures::sync::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()); let internal = PlayerInternal { - session: session, - config: config, + session, + config, commands: cmd_rx, state: PlayerState::Stopped, @@ -304,7 +303,7 @@ impl Player { sink: sink_builder(), sink_status: SinkStatus::Closed, sink_event_callback: None, - audio_filter: audio_filter, + audio_filter, event_senders: [event_sender].to_vec(), limiter_active: false, @@ -316,8 +315,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. - let _ = internal.wait(); + // It must be run by using block_on() in a dedicated thread. + futures_executor::block_on(internal); debug!("PlayerInternal thread finished."); }); @@ -332,7 +331,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 { @@ -368,22 +367,21 @@ impl Player { } pub fn get_player_event_channel(&self) -> PlayerEventChannel { - let (event_sender, event_receiver) = futures::sync::mpsc::unbounded(); + let (event_sender, event_receiver) = mpsc::unbounded_channel(); self.command(PlayerCommand::AddEventSender(event_sender)); event_receiver } - pub fn get_end_of_track_future(&self) -> Box> { - let result = self - .get_player_event_channel() - .filter(|event| match event { - PlayerEvent::EndOfTrack { .. } | PlayerEvent::Stopped { .. } => true, - _ => false, - }) - .into_future() - .map_err(|_| ()) - .map(|_| ()); - Box::new(result) + pub async fn await_end_of_track(&self) { + let mut channel = self.get_player_event_channel(); + while let Some(event) = channel.recv().await { + if matches!( + event, + PlayerEvent::EndOfTrack { .. } | PlayerEvent::Stopped { .. } + ) { + return; + } + } } pub fn set_sink_event_callback(&self, callback: Option) { @@ -421,11 +419,11 @@ enum PlayerPreload { None, Loading { track_id: SpotifyId, - loader: Box>, + loader: Pin> + Send>>, }, Ready { track_id: SpotifyId, - loaded_track: PlayerLoadedTrackData, + loaded_track: Box, }, } @@ -437,7 +435,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, start_playback: bool, - loader: Box>, + loader: Pin> + Send>>, }, Paused { track_id: SpotifyId, @@ -483,18 +481,12 @@ impl PlayerState { #[allow(dead_code)] fn is_stopped(&self) -> bool { use self::PlayerState::*; - match *self { - Stopped => true, - _ => false, - } + matches!(self, Stopped) } fn is_loading(&self) -> bool { use self::PlayerState::*; - match *self { - Loading { .. } => true, - _ => false, - } + matches!(self, Loading { .. }) } fn decoder(&mut self) -> Option<&mut Decoder> { @@ -627,22 +619,22 @@ struct PlayerTrackLoader { } impl PlayerTrackLoader { - fn find_available_alternative<'a>(&self, audio: &'a 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: FuturesUnordered<_> = alternatives + .iter() + .map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id)) + .collect(); + + alternatives + .filter_map(|x| future::ready(x.ok())) + .filter(|x| future::ready(x.available)) + .next() + .await } else { - if let Some(alternatives) = &audio.alternatives { - let alternatives = alternatives - .iter() - .map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id)); - let alternatives = future::join_all(alternatives).wait().unwrap(); - alternatives - .into_iter() - .find(|alt| alt.available) - .map(Cow::Owned) - } else { - None - } + None } } @@ -665,8 +657,12 @@ impl PlayerTrackLoader { } } - fn load_track(&self, spotify_id: SpotifyId, position_ms: u32) -> Option { - let audio = match AudioItem::get_audio_item(&self.session, spotify_id).wait() { + async fn load_track( + &self, + spotify_id: SpotifyId, + position_ms: u32, + ) -> Option { + let audio = match AudioItem::get_audio_item(&self.session, spotify_id).await { Ok(audio) => audio, Err(_) => { error!("Unable to load audio item."); @@ -676,10 +672,10 @@ impl PlayerTrackLoader { info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri); - let audio = match self.find_available_alternative(&audio) { + 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; } }; @@ -725,128 +721,132 @@ impl PlayerTrackLoader { let bytes_per_second = self.stream_data_rate(format); let play_from_beginning = position_ms == 0; - let key = self.session.audio_key().request(spotify_id, file_id); - let encrypted_file = AudioFile::open( - &self.session, - file_id, - bytes_per_second, - play_from_beginning, - ); - - let encrypted_file = match encrypted_file.wait() { - Ok(encrypted_file) => encrypted_file, - Err(_) => { - error!("Unable to load encrypted file."); - return None; - } - }; - let is_cached = encrypted_file.is_cached(); + // This is only a loop to be able to reload the file if an error occured + // while opening a cached file. + loop { + let encrypted_file = AudioFile::open( + &self.session, + file_id, + bytes_per_second, + play_from_beginning, + ); - let mut stream_loader_controller = encrypted_file.get_stream_loader_controller(); + let encrypted_file = match encrypted_file.await { + Ok(encrypted_file) => encrypted_file, + Err(_) => { + error!("Unable to load encrypted file."); + return None; + } + }; + let is_cached = encrypted_file.is_cached(); - if play_from_beginning { - // No need to seek -> we stream from the beginning - stream_loader_controller.set_stream_mode(); - } else { - // we need to seek -> we set stream mode after the initial seek. - stream_loader_controller.set_random_access_mode(); - } + let stream_loader_controller = encrypted_file.get_stream_loader_controller(); - let key = match key.wait() { - Ok(key) => key, - Err(_) => { - error!("Unable to load decryption key"); - return None; + if play_from_beginning { + // No need to seek -> we stream from the beginning + stream_loader_controller.set_stream_mode(); + } else { + // we need to seek -> we set stream mode after the initial seek. + stream_loader_controller.set_random_access_mode(); } - }; - let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); + let key = match self.session.audio_key().request(spotify_id, file_id).await { + Ok(key) => key, + Err(_) => { + error!("Unable to load decryption key"); + return None; + } + }; - let normalisation_factor = match NormalisationData::parse_from_file(&mut decrypted_file) { - Ok(normalisation_data) => { - NormalisationData::get_factor(&self.config, normalisation_data) - } - Err(_) => { - warn!("Unable to extract normalisation data, using default value."); - 1.0 as f32 - } - }; + let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); - let audio_file = Subfile::new(decrypted_file, 0xa7); + let normalisation_factor = match NormalisationData::parse_from_file(&mut decrypted_file) + { + Ok(normalisation_data) => { + NormalisationData::get_factor(&self.config, normalisation_data) + } + Err(_) => { + warn!("Unable to extract normalisation data, using default value."); + 1.0_f32 + } + }; - 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 audio_file = Subfile::new(decrypted_file, 0xa7); - let mut decoder = match result { - Ok(decoder) => decoder, - Err(e) if is_cached => { - warn!( - "Unable to read cached audio file: {}. Trying to download it.", - e - ); + 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!( + "Unable to read cached audio file: {}. Trying to download it.", + e + ); + + // unwrap safety: The file is cached, so session must have a cache + if !self.session.cache().unwrap().remove_file(file_id) { + return None; + } - // unwrap safety: The file is cached, so session must have a cache - if !self.session.cache().unwrap().remove_file(file_id) { + // Just try it again + continue; + } + Err(e) => { + error!("Unable to read audio file: {}", e); return None; } + }; - // Just try it again - return self.load_track(spotify_id, position_ms); - } - Err(e) => { - error!("Unable to read audio file: {}", e); - return None; + if position_ms != 0 { + if let Err(err) = decoder.seek(position_ms as i64) { + error!("Vorbis error: {}", err); + } + stream_loader_controller.set_stream_mode(); } - }; + let stream_position_pcm = PlayerInternal::position_ms_to_pcm(position_ms); + info!("<{}> ({} ms) loaded", audio.name, audio.duration); - if position_ms != 0 { - if let Err(err) = decoder.seek(position_ms as i64) { - error!("Vorbis error: {}", err); - } - stream_loader_controller.set_stream_mode(); + return Some(PlayerLoadedTrackData { + decoder, + normalisation_factor, + stream_loader_controller, + bytes_per_second, + duration_ms, + stream_position_pcm, + }); } - let stream_position_pcm = PlayerInternal::position_ms_to_pcm(position_ms); - info!("<{}> ({} ms) loaded", audio.name, audio.duration); - Some(PlayerLoadedTrackData { - decoder, - normalisation_factor, - stream_loader_controller, - bytes_per_second, - duration_ms, - stream_position_pcm, - }) } } impl Future for PlayerInternal { - type Item = (); - type Error = (); + type Output = (); - fn poll(&mut self) -> Poll<(), ()> { + 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 self.commands.poll() { - Ok(Async::Ready(None)) => return Ok(Async::Ready(())), // client has disconnected - shut down. - Ok(Async::Ready(Some(cmd))) => { + 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; Some(cmd) } - Ok(Async::NotReady) => None, - Err(_) => None, + _ => None, }; if let Some(cmd) = cmd { @@ -861,8 +861,8 @@ impl Future for PlayerInternal { play_request_id, } = self.state { - match loader.poll() { - Ok(Async::Ready(loaded_track)) => { + match loader.as_mut().poll(cx) { + Poll::Ready(Ok(loaded_track)) => { self.start_playback( track_id, play_request_id, @@ -873,8 +873,7 @@ impl Future for PlayerInternal { panic!("The state wasn't changed by start_playback()"); } } - Ok(Async::NotReady) => (), - Err(_) => { + Poll::Ready(Err(_)) => { warn!("Unable to load <{:?}>\nSkipping to next track", track_id); assert!(self.state.is_loading()); self.send_event(PlayerEvent::EndOfTrack { @@ -882,6 +881,7 @@ impl Future for PlayerInternal { play_request_id, }) } + Poll::Pending => (), } } @@ -891,16 +891,15 @@ impl Future for PlayerInternal { track_id, } = self.preload { - match loader.poll() { - Ok(Async::Ready(loaded_track)) => { + match loader.as_mut().poll(cx) { + Poll::Ready(Ok(loaded_track)) => { self.send_event(PlayerEvent::Preloading { track_id }); self.preload = PlayerPreload::Ready { track_id, - loaded_track, + loaded_track: Box::new(loaded_track), }; } - Ok(Async::NotReady) => (), - Err(_) => { + Poll::Ready(Err(_)) => { debug!("Unable to preload {:?}", track_id); self.preload = PlayerPreload::None; // Let Spirc know that the track was unavailable. @@ -917,6 +916,7 @@ impl Future for PlayerInternal { }); } } + Poll::Pending => (), } } @@ -936,10 +936,10 @@ impl Future for PlayerInternal { { let packet = decoder.next_packet().expect("Vorbis error"); - if !self.config.passthrough { + if !passthrough { if let Some(ref packet) = packet { - *stream_position_pcm = *stream_position_pcm - + (packet.samples().len() / NUM_CHANNELS as usize) as u64; + *stream_position_pcm += + (packet.samples().len() / NUM_CHANNELS as usize) as u64; let stream_position_millis = Self::position_pcm_to_ms(*stream_position_pcm); @@ -951,11 +951,7 @@ impl Future for PlayerInternal { .as_millis() as i64 - stream_position_millis as i64; - if lag > 1000 { - true - } else { - false - } + lag > 1000 } }; if notify_about_position { @@ -1015,11 +1011,11 @@ impl Future for PlayerInternal { } if self.session.is_invalid() { - return Ok(Async::Ready(())); + return Poll::Ready(()); } if (!self.state.is_playing()) && all_futures_completed_or_not_ready { - return Ok(Async::NotReady); + return Poll::Pending; } } } @@ -1165,7 +1161,7 @@ impl PlayerInternal { } if self.config.normalisation - && (normalisation_factor != 1.0 + && (f32::abs(normalisation_factor - 1.0) < f32::EPSILON || self.config.normalisation_method != NormalisationMethod::Basic) { for sample in data.iter_mut() { @@ -1333,8 +1329,8 @@ impl PlayerInternal { }); self.state = PlayerState::Playing { - track_id: track_id, - play_request_id: play_request_id, + track_id, + play_request_id, decoder: loaded_track.decoder, normalisation_factor: loaded_track.normalisation_factor, stream_loader_controller: loaded_track.stream_loader_controller, @@ -1350,8 +1346,8 @@ impl PlayerInternal { self.ensure_sink_stopped(false); self.state = PlayerState::Paused { - track_id: track_id, - play_request_id: play_request_id, + track_id, + play_request_id, decoder: loaded_track.decoder, normalisation_factor: loaded_track.normalisation_factor, stream_loader_controller: loaded_track.stream_loader_controller, @@ -1398,7 +1394,7 @@ impl PlayerInternal { track_id: old_track_id, .. } => self.send_event(PlayerEvent::Changed { - old_track_id: old_track_id, + old_track_id, new_track_id: track_id, }), PlayerState::Stopped => self.send_event(PlayerEvent::Started { @@ -1536,7 +1532,7 @@ impl PlayerInternal { let _ = loaded_track.decoder.seek(position_ms as i64); // This may be blocking loaded_track.stream_loader_controller.set_stream_mode(); } - self.start_playback(track_id, play_request_id, loaded_track, play); + self.start_playback(track_id, play_request_id, *loaded_track, play); return; } else { unreachable!(); @@ -1578,9 +1574,7 @@ impl PlayerInternal { self.preload = PlayerPreload::None; // If we don't have a loader yet, create one from scratch. - let loader = loader - .or_else(|| Some(self.load_track(track_id, position_ms))) - .unwrap(); + let loader = loader.unwrap_or_else(|| Box::pin(self.load_track(track_id, position_ms))); // Set ourselves to a loading state. self.state = PlayerState::Loading { @@ -1635,7 +1629,10 @@ impl PlayerInternal { // schedule the preload of the current track if desired. if preload_track { let loader = self.load_track(track_id, 0); - self.preload = PlayerPreload::Loading { track_id, loader } + self.preload = PlayerPreload::Loading { + track_id, + loader: Box::pin(loader), + } } } @@ -1738,7 +1735,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); @@ -1751,7 +1748,7 @@ impl PlayerInternal { &self, spotify_id: SpotifyId, position_ms: u32, - ) -> Box> { + ) -> impl Future> + Send + 'static { // This method creates a future that returns the loaded stream and associated info. // Ideally all work should be done using asynchronous code. However, seek() on the // audio stream is implemented in a blocking fashion. Thus, we can't turn it into future @@ -1763,18 +1760,16 @@ impl PlayerInternal { config: self.config.clone(), }; - let (result_tx, result_rx) = futures::sync::oneshot::channel(); + let (result_tx, result_rx) = oneshot::channel(); std::thread::spawn(move || { - loader - .load_track(spotify_id, position_ms) - .and_then(move |data| { - let _ = result_tx.send(data); - Some(()) - }); + let data = futures_executor::block_on(loader.load_track(spotify_id, position_ms)); + if let Some(data) = data { + let _ = result_tx.send(data); + } }); - Box::new(result_rx.map_err(|_| ())) + result_rx.map_err(|_| ()) } fn preload_data_before_playback(&mut self) { @@ -1896,21 +1891,18 @@ struct Subfile { impl Subfile { pub fn new(mut stream: T, offset: u64) -> Subfile { stream.seek(SeekFrom::Start(offset)).unwrap(); - Subfile { - stream: stream, - offset: offset, - } + Subfile { stream, offset } } } impl Read for Subfile { - fn read(&mut self, buf: &mut [u8]) -> Result { + fn read(&mut self, buf: &mut [u8]) -> io::Result { self.stream.read(buf) } } impl Seek for Subfile { - fn seek(&mut self, mut pos: SeekFrom) -> Result { + fn seek(&mut self, mut pos: SeekFrom) -> io::Result { pos = match pos { SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset), x => x, diff --git a/rustfmt.toml b/rustfmt.toml index 25c1fc1ec..aefd6aa83 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,3 +1,4 @@ # max_width = 105 reorder_imports = true reorder_modules = true +edition = "2018" diff --git a/src/lib.rs b/src/lib.rs index 610062e28..7722e93e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,8 @@ #![crate_name = "librespot"] -#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))] -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 7426e2c42..78e7e2f96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,36 +1,35 @@ -use futures::sync::mpsc::UnboundedReceiver; -use futures::{Async, Future, Poll, Stream}; -use log::{error, info, trace, warn}; +use futures_util::{future, FutureExt, StreamExt}; +use librespot_playback::player::PlayerEvent; +use log::{error, info, warn}; use sha1::{Digest, Sha1}; -use std::convert::TryFrom; -use std::env; -use std::io::{stderr, Write}; -use std::mem; -use std::path::Path; -use std::process::exit; -use std::str::FromStr; -use std::time::Instant; -use tokio_core::reactor::{Core, Handle}; -use tokio_io::IoStream; +use tokio::sync::mpsc::UnboundedReceiver; use url::Url; -use librespot::core::authentication::{get_credentials, Credentials}; +use librespot::connect::spirc::Spirc; +use librespot::core::authentication::Credentials; use librespot::core::cache::Cache; use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig, VolumeCtrl}; -use librespot::core::session::{AuthenticationError, Session}; +use librespot::core::session::Session; use librespot::core::version; - -use librespot::connect::discovery::{discovery, DiscoveryStream}; -use librespot::connect::spirc::{Spirc, SpircTask}; use librespot::playback::audio_backend::{self, Sink, BACKENDS}; use librespot::playback::config::{ AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, }; use librespot::playback::mixer::{self, Mixer, MixerConfig}; -use librespot::playback::player::{NormalisationData, Player, PlayerEvent}; +use librespot::playback::player::{NormalisationData, Player}; mod player_event_handler; -use crate::player_event_handler::{emit_sink_event, run_program_on_events}; +use player_event_handler::{emit_sink_event, run_program_on_events}; + +use std::convert::TryFrom; +use std::path::Path; +use std::process::exit; +use std::str::FromStr; +use std::{env, time::Instant}; +use std::{ + io::{stderr, Write}, + pin::Pin, +}; const MILLIS: f32 = 1000.0; @@ -76,6 +75,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 + } +} + fn print_version() { println!( "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", @@ -89,7 +111,7 @@ fn print_version() { #[derive(Clone)] struct Setup { format: AudioFormat, - backend: fn(Option, AudioFormat) -> Box, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, @@ -106,7 +128,7 @@ struct Setup { emit_sink_events: bool, } -fn setup(args: &[String]) -> Setup { +fn get_setup(args: &[String]) -> Setup { let mut opts = getopts::Options::new(); opts.optopt( "c", @@ -267,13 +289,7 @@ fn setup(args: &[String]) -> Setup { let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => { - writeln!( - stderr(), - "error: {}\n{}", - f.to_string(), - usage(&args[0], &opts) - ) - .unwrap(); + eprintln!("error: {}\n{}", f.to_string(), usage(&args[0], &opts)); exit(1); } }; @@ -306,7 +322,7 @@ fn setup(args: &[String]) -> Setup { .opt_str("format") .as_ref() .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) - .unwrap_or(AudioFormat::default()); + .unwrap_or_default(); let device = matches.opt_str("device"); if device == Some("?".into()) { @@ -320,8 +336,10 @@ fn setup(args: &[String]) -> Setup { let mixer_config = MixerConfig { card: matches .opt_str("mixer-card") - .unwrap_or(String::from("default")), - mixer: matches.opt_str("mixer-name").unwrap_or(String::from("PCM")), + .unwrap_or_else(|| String::from("default")), + mixer: matches + .opt_str("mixer-name") + .unwrap_or_else(|| String::from("PCM")), index: matches .opt_str("mixer-index") .map(|index| index.parse::().unwrap()) @@ -345,7 +363,7 @@ fn setup(args: &[String]) -> Setup { .map(|p| AsRef::::as_ref(p).join("files")); system_dir = matches .opt_str("system-cache") - .or_else(|| cache_dir) + .or(cache_dir) .map(|p| p.into()); } @@ -375,15 +393,17 @@ fn setup(args: &[String]) -> Setup { .map(|port| port.parse::().unwrap()) .unwrap_or(0); - let name = matches.opt_str("name").unwrap_or("Librespot".to_string()); + let name = matches + .opt_str("name") + .unwrap_or_else(|| "Librespot".to_string()); 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( @@ -399,8 +419,8 @@ fn setup(args: &[String]) -> Setup { SessionConfig { user_agent: version::VERSION_STRING.to_string(), - device_id: device_id, - proxy: matches.opt_str("proxy").or(std::env::var("http_proxy").ok()).map( + device_id, + proxy: matches.opt_str("proxy").or_else(|| std::env::var("http_proxy").ok()).map( |s| { match Url::parse(&s) { Ok(url) => { @@ -430,26 +450,27 @@ fn setup(args: &[String]) -> Setup { .opt_str("b") .as_ref() .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) - .unwrap_or(Bitrate::default()); + .unwrap_or_default(); let gain_type = matches .opt_str("normalisation-gain-type") .as_ref() .map(|gain_type| { NormalisationType::from_str(gain_type).expect("Invalid normalisation type") }) - .unwrap_or(NormalisationType::default()); + .unwrap_or_default(); let normalisation_method = matches .opt_str("normalisation-method") .as_ref() .map(|gain_type| { NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method") }) - .unwrap_or(NormalisationMethod::default()); + .unwrap_or_default(); + PlayerConfig { - bitrate: bitrate, + bitrate, gapless: !matches.opt_present("disable-gapless"), normalisation: matches.opt_present("enable-volume-normalisation"), - normalisation_method: normalisation_method, + normalisation_method, normalisation_type: gain_type, normalisation_pregain: matches .opt_str("normalisation-pregain") @@ -488,19 +509,19 @@ fn setup(args: &[String]) -> Setup { .opt_str("device-type") .as_ref() .map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type")) - .unwrap_or(DeviceType::default()); + .unwrap_or_default(); let volume_ctrl = matches .opt_str("volume-ctrl") .as_ref() .map(|volume_ctrl| VolumeCtrl::from_str(volume_ctrl).expect("Invalid volume ctrl type")) - .unwrap_or(VolumeCtrl::default()); + .unwrap_or_default(); ConnectConfig { - name: name, - device_type: device_type, + name, + device_type, volume: initial_volume, - volume_ctrl: volume_ctrl, + volume_ctrl, autoplay: matches.opt_present("autoplay"), } }; @@ -508,251 +529,197 @@ fn setup(args: &[String]) -> Setup { let enable_discovery = !matches.opt_present("disable-discovery"); Setup { - format: format, - backend: backend, - cache: cache, - session_config: session_config, - player_config: player_config, - connect_config: connect_config, - credentials: credentials, - device: device, - enable_discovery: enable_discovery, - zeroconf_port: zeroconf_port, - mixer: mixer, - mixer_config: mixer_config, + format, + backend, + cache, + session_config, + player_config, + connect_config, + credentials, + device, + enable_discovery, + zeroconf_port, + mixer, + mixer_config, player_event_program: matches.opt_str("onevent"), emit_sink_events: matches.opt_present("emit-sink-events"), } } -struct Main { - cache: Option, - player_config: PlayerConfig, - session_config: SessionConfig, - connect_config: ConnectConfig, - format: AudioFormat, - backend: fn(Option, AudioFormat) -> Box, - device: Option, - mixer: fn(Option) -> Box, - mixer_config: MixerConfig, - handle: Handle, - - discovery: Option, - signal: IoStream<()>, - - spirc: Option, - spirc_task: Option, - connect: Box>, - - shutdown: bool, - last_credentials: Option, - auto_connect_times: Vec, - - player_event_channel: Option>, - player_event_program: Option, - emit_sink_events: bool, -} - -impl Main { - fn new(handle: Handle, setup: Setup) -> Main { - let mut task = Main { - handle: handle.clone(), - cache: setup.cache, - session_config: setup.session_config, - player_config: setup.player_config, - connect_config: setup.connect_config, - format: setup.format, - backend: setup.backend, - device: setup.device, - mixer: setup.mixer, - mixer_config: setup.mixer_config, - - connect: Box::new(futures::future::empty()), - discovery: None, - spirc: None, - spirc_task: None, - shutdown: false, - last_credentials: None, - auto_connect_times: Vec::new(), - signal: Box::new(tokio_signal::ctrl_c().flatten_stream()), - - player_event_channel: None, - player_event_program: setup.player_event_program, - emit_sink_events: setup.emit_sink_events, - }; - - if setup.enable_discovery { - let config = task.connect_config.clone(); - let device_id = task.session_config.device_id.clone(); - - task.discovery = - Some(discovery(&handle, config, device_id, setup.zeroconf_port).unwrap()); - } - - if let Some(credentials) = setup.credentials { - task.credentials(credentials); - } - - task +#[tokio::main(flavor = "current_thread")] +async fn main() { + if env::var("RUST_BACKTRACE").is_err() { + env::set_var("RUST_BACKTRACE", "full") } - fn credentials(&mut self, credentials: Credentials) { - self.last_credentials = Some(credentials.clone()); - let config = self.session_config.clone(); - let handle = self.handle.clone(); - - let connection = Session::connect(config, credentials, self.cache.clone(), handle); + let args: Vec = std::env::args().collect(); + let setup = get_setup(&args); + + let mut last_credentials = None; + let mut spirc: Option = None; + let mut spirc_task: Option> = None; + let mut player_event_channel: Option> = None; + let mut auto_connect_times: Vec = vec![]; + let mut discovery = None; + let mut connecting: Pin>> = Box::pin(future::pending()); + + if setup.enable_discovery { + let config = setup.connect_config.clone(); + let device_id = setup.session_config.device_id.clone(); + + discovery = Some( + librespot_connect::discovery::discovery(config, device_id, setup.zeroconf_port) + .unwrap(), + ); + } - self.connect = connection; - self.spirc = None; - let task = mem::replace(&mut self.spirc_task, None); - if let Some(task) = task { - self.handle.spawn(task); - } + if let Some(credentials) = setup.credentials { + last_credentials = Some(credentials.clone()); + connecting = Box::pin( + Session::connect( + setup.session_config.clone(), + credentials, + setup.cache.clone(), + ) + .fuse(), + ); } -} -impl Future for Main { - type Item = (); - type Error = (); + loop { + tokio::select! { + credentials = async { discovery.as_mut().unwrap().next().await }, if discovery.is_some() => { + match credentials { + Some(credentials) => { + last_credentials = Some(credentials.clone()); + auto_connect_times.clear(); - fn poll(&mut self) -> Poll<(), ()> { - loop { - let mut progress = false; + if let Some(spirc) = spirc.take() { + spirc.shutdown(); + } + if let Some(spirc_task) = spirc_task.take() { + // Continue shutdown in its own task + tokio::spawn(spirc_task); + } - if let Some(Async::Ready(Some(creds))) = - self.discovery.as_mut().map(|d| d.poll().unwrap()) - { - if let Some(ref spirc) = self.spirc { - spirc.shutdown(); + connecting = Box::pin(Session::connect( + setup.session_config.clone(), + credentials, + setup.cache.clone(), + ).fuse()); + }, + None => { + warn!("Discovery stopped!"); + discovery = None; + } } - self.auto_connect_times.clear(); - self.credentials(creds); - - progress = true; - } - - match self.connect.poll() { - Ok(Async::Ready(session)) => { - self.connect = Box::new(futures::future::empty()); - let mixer_config = self.mixer_config.clone(); - let mixer = (self.mixer)(Some(mixer_config)); - let player_config = self.player_config.clone(); - let connect_config = self.connect_config.clone(); + }, + session = &mut connecting, if !connecting.is_terminated() => match session { + Ok(session) => { + let mixer_config = setup.mixer_config.clone(); + let mixer = (setup.mixer)(Some(mixer_config)); + let player_config = setup.player_config.clone(); + let connect_config = setup.connect_config.clone(); let audio_filter = mixer.get_audio_filter(); - let format = self.format; - let backend = self.backend; - let device = self.device.clone(); + let format = setup.format; + let backend = setup.backend; + let device = setup.device.clone(); let (player, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { (backend)(device, format) }); - if self.emit_sink_events { - if let Some(player_event_program) = &self.player_event_program { - let player_event_program = player_event_program.clone(); + if setup.emit_sink_events { + if let Some(player_event_program) = setup.player_event_program.clone() { player.set_sink_event_callback(Some(Box::new(move |sink_status| { - emit_sink_event(sink_status, &player_event_program) + match emit_sink_event(sink_status, &player_event_program) { + Ok(e) if e.success() => (), + Ok(e) => { + if let Some(code) = e.code() { + warn!("Sink event prog returned exit code {}", code); + } else { + warn!("Sink event prog returned failure"); + } + } + Err(e) => { + warn!("Emitting sink event failed: {}", e); + } + } }))); } - } + }; - let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer); - self.spirc = Some(spirc); - self.spirc_task = Some(spirc_task); - self.player_event_channel = Some(event_channel); + let (spirc_, spirc_task_) = Spirc::new(connect_config, session, player, mixer); - progress = true; - } - Ok(Async::NotReady) => (), - Err(error) => { - error!("Could not connect to server: {}", error); - self.connect = Box::new(futures::future::empty()); - } - } - - if let Async::Ready(Some(())) = self.signal.poll().unwrap() { - trace!("Ctrl-C received"); - if !self.shutdown { - if let Some(ref spirc) = self.spirc { - spirc.shutdown(); - } else { - return Ok(Async::Ready(())); - } - self.shutdown = true; - } else { - return Ok(Async::Ready(())); + spirc = Some(spirc_); + spirc_task = Some(Box::pin(spirc_task_)); + player_event_channel = Some(event_channel); + }, + Err(e) => { + warn!("Connection failed: {}", e); } + }, + _ = async { spirc_task.as_mut().unwrap().await }, if spirc_task.is_some() => { + spirc_task = None; - progress = true; - } - - let mut drop_spirc_and_try_to_reconnect = false; - if let Some(ref mut spirc_task) = self.spirc_task { - if let Async::Ready(()) = spirc_task.poll().unwrap() { - if self.shutdown { - return Ok(Async::Ready(())); - } else { - warn!("Spirc shut down unexpectedly"); - drop_spirc_and_try_to_reconnect = true; - } - progress = true; - } - } - if drop_spirc_and_try_to_reconnect { - self.spirc_task = None; - while (!self.auto_connect_times.is_empty()) - && ((Instant::now() - self.auto_connect_times[0]).as_secs() > 600) + warn!("Spirc shut down unexpectedly"); + while !auto_connect_times.is_empty() + && ((Instant::now() - auto_connect_times[0]).as_secs() > 600) { - let _ = self.auto_connect_times.remove(0); + let _ = auto_connect_times.remove(0); } - if let Some(credentials) = self.last_credentials.clone() { - if self.auto_connect_times.len() >= 5 { + if let Some(credentials) = last_credentials.clone() { + if auto_connect_times.len() >= 5 { warn!("Spirc shut down too often. Not reconnecting automatically."); } else { - self.auto_connect_times.push(Instant::now()); - self.credentials(credentials); + auto_connect_times.push(Instant::now()); + + connecting = Box::pin(Session::connect( + setup.session_config.clone(), + credentials, + setup.cache.clone(), + ).fuse()); } } - } - - if let Some(ref mut player_event_channel) = self.player_event_channel { - if let Async::Ready(Some(event)) = player_event_channel.poll().unwrap() { - progress = true; - if let Some(ref program) = self.player_event_program { + }, + event = async { player_event_channel.as_mut().unwrap().recv().await }, if player_event_channel.is_some() => match event { + Some(event) => { + if let Some(program) = &setup.player_event_program { if let Some(child) = run_program_on_events(event, program) { - let child = child - .expect("program failed to start") - .map(|status| { - if !status.success() { - error!("child exited with status {:?}", status.code()); - } - }) - .map_err(|e| error!("failed to wait on child process: {}", e)); - - self.handle.spawn(child); + let mut child = child.expect("program failed to start"); + + tokio::spawn(async move { + match child.wait().await { + Ok(status) if !status.success() => error!("child exited with status {:?}", status.code()), + Err(e) => error!("failed to wait on child process: {}", e), + _ => {} + } + }); } } + }, + None => { + player_event_channel = None; } - } - - if !progress { - return Ok(Async::NotReady); + }, + _ = tokio::signal::ctrl_c() => { + break; } } } -} -fn main() { - if env::var("RUST_BACKTRACE").is_err() { - env::set_var("RUST_BACKTRACE", "full") - } - let mut core = Core::new().unwrap(); - let handle = core.handle(); + info!("Gracefully shutting down"); - let args: Vec = std::env::args().collect(); + // Shutdown spirc if necessary + if let Some(spirc) = spirc { + spirc.shutdown(); - core.run(Main::new(handle, setup(&args))).unwrap() + if let Some(mut spirc_task) = spirc_task { + tokio::select! { + _ = tokio::signal::ctrl_c() => (), + _ = spirc_task.as_mut() => () + } + } + } } diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 102cf7809..4c75128cc 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -1,23 +1,13 @@ use librespot::playback::player::PlayerEvent; +use librespot::playback::player::SinkStatus; use log::info; +use tokio::process::{Child as AsyncChild, Command as AsyncCommand}; + use std::collections::HashMap; use std::io; -use std::process::Command; -use tokio_process::{Child, CommandExt}; - -use futures::Future; -use librespot::playback::player::SinkStatus; - -fn run_program(program: &str, env_vars: HashMap<&str, String>) -> io::Result { - let mut v: Vec<&str> = program.split_whitespace().collect(); - info!("Running {:?} with environment variables {:?}", v, env_vars); - Command::new(&v.remove(0)) - .args(&v) - .envs(env_vars.iter()) - .spawn_async() -} +use std::process::{Command, ExitStatus}; -pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option> { +pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option> { let mut env_vars = HashMap::new(); match event { PlayerEvent::Changed { @@ -68,10 +58,18 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option return None, } - Some(run_program(onevent, env_vars)) + + let mut v: Vec<&str> = onevent.split_whitespace().collect(); + info!("Running {:?} with environment variables {:?}", v, env_vars); + Some( + AsyncCommand::new(&v.remove(0)) + .args(&v) + .envs(env_vars.iter()) + .spawn(), + ) } -pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) { +pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) -> io::Result { let mut env_vars = HashMap::new(); env_vars.insert("PLAYER_EVENT", "sink".to_string()); let sink_status = match sink_status { @@ -80,6 +78,12 @@ pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) { SinkStatus::Closed => "closed", }; env_vars.insert("SINK_STATUS", sink_status.to_string()); + let mut v: Vec<&str> = onevent.split_whitespace().collect(); + info!("Running {:?} with environment variables {:?}", v, env_vars); - let _ = run_program(onevent, env_vars).and_then(|child| child.wait()); + Command::new(&v.remove(0)) + .args(&v) + .envs(env_vars.iter()) + .spawn()? + .wait() }