diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 77e280b2..4ad4b406 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.40.0 # MSRV (Minimum supported rust version) + - 1.42.0 # MSRV (Minimum supported rust version) - stable - beta experimental: [false] @@ -59,6 +59,15 @@ jobs: profile: minimal toolchain: ${{ matrix.toolchain }} override: true + - name: Cache Rust dependencies + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + target + key: ${{ runner.os }}-build-${{ 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 @@ -94,6 +103,15 @@ jobs: target: ${{ matrix.target }} toolchain: ${{ matrix.toolchain }} override: true + - name: Cache Rust dependencies + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + target + key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }} - name: Install cross run: cargo install cross || true - name: Build diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6887d9f8..00000000 --- a/.travis.yml +++ /dev/null @@ -1,73 +0,0 @@ -language: rust -rust: - - 1.40.0 - - stable - - beta - - nightly - -# Need to cache the whole `.cargo` directory to keep .crates.toml for -# cargo-update to work -cache: - directories: - - /home/travis/.cargo - -# But don't cache the cargo registry -before_cache: - - rm -rf /home/travis/.cargo/registry - -matrix: - # Performance tweak - fast_finish: true - # Ignore failures in nightly, not ideal, but necessary - allow_failures: - - rust: nightly - - # Only run the formatting check for stable - include: - - name: 'Rust: format check' - rust: stable - install: - - rustup component add rustfmt - script: - - cargo fmt --verbose --all -- --check - -addons: - apt: - packages: - - gcc-arm-linux-gnueabihf - - libc6-dev-armhf-cross - - libpulse-dev - - portaudio19-dev - - libasound2-dev - - libsdl2-dev - - gstreamer1.0-dev - - libgstreamer-plugins-base1.0-dev - -before_script: - - mkdir -p ~/.cargo - - echo '[target.armv7-unknown-linux-gnueabihf]' > ~/.cargo/config - - echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config - - rustup target add armv7-unknown-linux-gnueabihf - -script: - - cargo build --locked --no-default-features - - cargo build --locked --examples - - cargo build --locked --no-default-features --features "with-tremor" - - cargo build --locked --no-default-features --features "with-vorbis" - - cargo build --locked --no-default-features --features "alsa-backend" - - cargo build --locked --no-default-features --features "portaudio-backend" - - cargo build --locked --no-default-features --features "pulseaudio-backend" - - cargo build --locked --no-default-features --features "jackaudio-backend" - - cargo build --locked --no-default-features --features "rodio-backend" - - cargo build --locked --no-default-features --features "sdl-backend" - - cargo build --locked --no-default-features --features "gstreamer-backend" - - cargo build --locked --no-default-features --target armv7-unknown-linux-gnueabihf - -notifications: - email: false - webhooks: - urls: - - https://webhooks.gitter.im/e/780b178b15811059752e - on_success: change # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: never # options: [always|never|change] default: always diff --git a/Cargo.lock b/Cargo.lock index 7606ed82..6255e76c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,38 +73,16 @@ dependencies = [ "memchr", ] -[[package]] -name = "alsa" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a0d4ebc8b23041c5de9bc9aee13b4bad844a589479701f31a5934cfe4aeb32" -dependencies = [ - "alsa-sys 0.1.2", - "bitflags 0.9.1", - "libc", - "nix 0.9.0", -] - [[package]] name = "alsa" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb213f6b3e4b1480a60931ca2035794aa67b73103d254715b1db7b70dcb3c934" dependencies = [ - "alsa-sys 0.3.1", - "bitflags 1.2.1", + "alsa-sys", + "bitflags", "libc", - "nix 0.15.0", -] - -[[package]] -name = "alsa-sys" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0edcbbf9ef68f15ae1b620f722180b82a98b6f0628d30baa6b8d2a5abc87d58" -dependencies = [ - "libc", - "pkg-config", + "nix", ] [[package]] @@ -129,27 +107,6 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" -[[package]] -name = "async-stream" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3670df70cbc01729f901f94c887814b3c68db038aad1329a418bae178bc5295c" -dependencies = [ - "async-stream-impl", - "futures-core", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3548b8efc9f8e8a5a0a2808c5bd8451a9031b9e5b879a79590304ae928b0a70" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "async-trait" version = "0.1.42" @@ -220,7 +177,7 @@ version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" dependencies = [ - "bitflags 1.2.1", + "bitflags", "cexpr", "clang-sys", "lazy_static", @@ -248,12 +205,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -[[package]] -name = "bitflags" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" - [[package]] name = "bitflags" version = "1.2.1" @@ -283,9 +234,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.4.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" [[package]] name = "byte-tools" @@ -353,9 +304,9 @@ dependencies = [ [[package]] name = "chunked_transfer" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7477065d45a8fe57167bf3cf8bcd3729b54cfcb81cca49bda2d038ea89ae82ca" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "cipher" @@ -368,9 +319,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0659001ab56b791be01d4b729c44376edc6718cf389a502e579b77b758f3296c" +checksum = "5cb92721cb37482245ed88428f72253ce422b3b4ee169c70a0642521bb5db4cc" dependencies = [ "glob", "libc", @@ -383,7 +334,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ - "bitflags 1.2.1", + "bitflags", ] [[package]] @@ -422,7 +373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" dependencies = [ "percent-encoding 2.1.0", - "time 0.2.24", + "time 0.2.25", "version_check", ] @@ -433,12 +384,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" dependencies = [ "cookie", - "idna 0.2.0", + "idna 0.2.1", "log", "publicsuffix", "serde", "serde_json", - "time 0.2.24", + "time 0.2.25", "url 2.2.0", ] @@ -454,7 +405,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f229761965dad3e9b11081668a6ea00f1def7aa46062321b5ec245b834f6e491" dependencies = [ - "bitflags 1.2.1", + "bitflags", "coreaudio-sys", ] @@ -473,7 +424,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05631e2089dfa5d3b6ea1cfbbfd092e2ee5deeb69698911bc976b28b746d3657" dependencies = [ - "alsa 0.4.3", + "alsa", "core-foundation-sys", "coreaudio-rs", "jni 0.17.0", @@ -483,7 +434,7 @@ dependencies = [ "mach", "ndk", "ndk-glue", - "nix 0.15.0", + "nix", "oboe", "parking_lot", "stdweb 0.1.3", @@ -557,9 +508,9 @@ dependencies = [ [[package]] name = "derivative" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaed5874effa6cde088c644ddcdcb4ffd1511391c5be4fdd7a5ccd02c7e4a183" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", @@ -800,7 +751,7 @@ checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.1+wasi-snapshot-preview1", + "wasi 0.10.2+wasi-snapshot-preview1", ] [[package]] @@ -815,7 +766,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" dependencies = [ - "bitflags 1.2.1", + "bitflags", "futures-channel", "futures-core", "futures-executor", @@ -877,7 +828,7 @@ version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d50f822055923f1cbede233aa5dfd4ee957cf328fb3076e330886094e11d6cf" dependencies = [ - "bitflags 1.2.1", + "bitflags", "cfg-if 1.0.0", "futures-channel", "futures-core", @@ -901,7 +852,7 @@ version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc80888271338c3ede875d8cafc452eb207476ff5539dcbe0018a8f5b827af0e" dependencies = [ - "bitflags 1.2.1", + "bitflags", "futures-core", "futures-sink", "glib", @@ -934,7 +885,7 @@ version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bafd01c56f59cb10f4b5a10f97bb4bdf8c2b2784ae5b04da7e2d400cf6e6afcf" dependencies = [ - "bitflags 1.2.1", + "bitflags", "glib", "glib-sys", "gobject-sys", @@ -1046,9 +997,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.3.4" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" [[package]] name = "httpdate" @@ -1064,9 +1015,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.2" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12219dc884514cb4a6a03737f4413c0e01c23a1b059b0156004b23f1e19dccbe" +checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" dependencies = [ "bytes", "futures-channel", @@ -1078,7 +1029,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project 1.0.4", + "pin-project 1.0.5", "socket2", "tokio", "tower-service", @@ -1105,9 +1056,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +checksum = "de910d521f7cc3135c4de8db1cb910e0b5ed1dc6f57c381cd07e8e661ce10094" dependencies = [ "matches", "unicode-bidi", @@ -1154,7 +1105,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c1871c91fa65aa328f3bedbaa54a6e5d1de009264684c153eb708ba933aa6f5" dependencies = [ - "bitflags 1.2.1", + "bitflags", "jack-sys", "lazy_static", "libc", @@ -1207,9 +1158,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" +checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65" dependencies = [ "wasm-bindgen", ] @@ -1239,9 +1190,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.82" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" +checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" [[package]] name = "libflate" @@ -1273,11 +1224,11 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.22.0" +version = "2.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce89ab17bd343b08bd4321c674ef1477d34f83be18b1ab2ee47a5e5fbee64a91" +checksum = "b2405f806801527dfb3d2b6d48a282cdebe9a1b41b0652e0d7b5bad81dbc700e" dependencies = [ - "bitflags 1.2.1", + "bitflags", "libc", "libpulse-sys", "num-derive", @@ -1287,9 +1238,9 @@ dependencies = [ [[package]] name = "libpulse-simple-binding" -version = "2.20.1" +version = "2.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e47f6cda2748fb86f15e5e65cc33be306577140f4b500622b5d98df2ca17240" +checksum = "a574975292db859087c3957b9182f7d53278553f06bddaa2099c90e4ac3a0ee0" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -1308,9 +1259,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcfb56118765adba111da47e36278b77d00aebf822e4f014a832fbfa183a13b" +checksum = "cf17e9832643c4f320c42b7d78b2c0510f45aa5e823af094413b94e45076ba82" dependencies = [ "libc", "num-derive", @@ -1359,6 +1310,7 @@ dependencies = [ "byteorder", "bytes", "env_logger", + "error-chain", "futures", "hmac", "httparse", @@ -1404,7 +1356,7 @@ dependencies = [ name = "librespot-playback" version = "0.1.3" dependencies = [ - "alsa 0.2.2", + "alsa", "byteorder", "cpal", "futures", @@ -1438,9 +1390,9 @@ dependencies = [ [[package]] name = "librespot-tremor" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b155a7dc4e4d272e01c37a1b85c1ee1bee7f04980ad4a7784c1a6e0f2de5929b" +checksum = "97f525bff915d478a76940a7b988e5ea34911ba7280c97bd3a7673f54d68b4fe" dependencies = [ "cc", "libc", @@ -1465,11 +1417,11 @@ dependencies = [ [[package]] name = "log" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", ] [[package]] @@ -1577,25 +1529,13 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" -[[package]] -name = "nix" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c5afeb0198ec7be8569d666644b574345aad2e95a53baf3a532da3e0f3fb32" -dependencies = [ - "bitflags 0.9.1", - "cfg-if 0.1.10", - "libc", - "void", -] - [[package]] name = "nix" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" dependencies = [ - "bitflags 1.2.1", + "bitflags", "cc", "cfg-if 0.1.10", "libc", @@ -1836,6 +1776,15 @@ 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 = "0.4.27" @@ -1847,11 +1796,11 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b70b68509f17aa2857863b6fa00bf21fc93674c7a8893de2f469f6aa7ca2f2" +checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63" dependencies = [ - "pin-project-internal 1.0.4", + "pin-project-internal 1.0.5", ] [[package]] @@ -1867,9 +1816,9 @@ dependencies = [ [[package]] name = "pin-project-internal" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa25a6393f22ce819b0f50e0be89287292fda8d425be38ee0ca14c4931d9e71" +checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b" dependencies = [ "proc-macro2", "quote", @@ -1900,7 +1849,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb6b5eff96ccc9bf44d34c379ab03ae944426d83d1694345bdf8159d561d562" dependencies = [ - "bitflags 1.2.1", + "bitflags", "libc", "portaudio-sys", ] @@ -2013,7 +1962,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" dependencies = [ "error-chain", - "idna 0.2.0", + "idna 0.2.1", "lazy_static", "regex", "url 2.2.0", @@ -2065,9 +2014,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18519b42a40024d661e1714153e9ad0c3de27cd495760ceb09710920f1098b1e" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" dependencies = [ "libc", "rand_chacha 0.3.0", @@ -2158,7 +2107,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" dependencies = [ - "bitflags 1.2.1", + "bitflags", ] [[package]] @@ -2190,9 +2139,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.19" +version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", "libc", @@ -2236,7 +2185,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", ] [[package]] @@ -2295,7 +2253,7 @@ version = "0.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcbb85f4211627a7291c83434d6bbfa723e28dcaa53c7606087e3c61929e4b9c" dependencies = [ - "bitflags 1.2.1", + "bitflags", "lazy_static", "libc", "sdl2-sys", @@ -2318,7 +2276,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser", + "semver-parser 0.7.0", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser 0.10.2", ] [[package]] @@ -2328,19 +2295,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] -name = "serde" -version = "1.0.120" +name = "semver-parser" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166b2349061381baf54a58e4b13c89369feb0ef2eaa57198899e2312aac30aab" +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" -version = "1.0.120" +version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca2a8cb5805ce9e3b95435e3765b7b553cecc762d938d409434338386cb5775" +checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" dependencies = [ "proc-macro2", "quote", @@ -2349,9 +2325,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", @@ -2440,9 +2416,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "standback" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a8cff4fa24853fdf6b51f75c6d7f8206d7c75cab4e467bcd7f25c2b1febe0" +checksum = "a2beb4d1860a61f571530b3f855a1b538d0200f7871c63331ecd6f17b1f014f8" dependencies = [ "version_check", ] @@ -2460,7 +2436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" dependencies = [ "discard", - "rustc_version", + "rustc_version 0.2.3", "stdweb-derive", "stdweb-internal-macros", "stdweb-internal-runtime", @@ -2534,9 +2510,9 @@ checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" [[package]] name = "syn" -version = "1.0.58" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" dependencies = [ "proc-macro2", "quote", @@ -2572,13 +2548,12 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.30" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489997b7557e9a43e192c527face4feacc78bfbe6eed67fd55c4c9e381cba290" +checksum = "0313546c01d59e29be4f09687bcb4fb6690cec931cc3607b6aec7a0e417f4cc6" dependencies = [ "filetime", "libc", - "redox_syscall 0.1.57", "xattr", ] @@ -2590,7 +2565,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if 1.0.0", "libc", - "rand 0.8.2", + "rand 0.8.3", "redox_syscall 0.2.4", "remove_dir_all", "winapi", @@ -2646,9 +2621,9 @@ dependencies = [ [[package]] name = "time" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273d3ed44dca264b0d6b3665e8d48fb515042d42466fad93d2a45b90ec4058f7" +checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" dependencies = [ "const_fn", "libc", @@ -2684,9 +2659,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" dependencies = [ "tinyvec_macros", ] @@ -2699,9 +2674,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8efab2086f17abcddb8f756117665c958feee6b2e39974c2f1600592ab3a4195" +checksum = "e8190d04c665ea9e6b6a0dc45523ade572c088d2e6566244c1122671dbf4ae3a" dependencies = [ "autocfg", "bytes", @@ -2715,40 +2690,27 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42517d2975ca3114b22a16192634e8241dc5cc1f130be194645970cc1c371494" +checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "tokio-stream" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76066865172052eb8796c686f0b441a93df8b08d40a950b062ffb9a426f00edd" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb971a26599ffd28066d387f109746df178eff14d5ea1e235015c5601967a4b" +checksum = "ebb7cb2f00c5ae8df755b252306272cd1790d39728363936e01827e11f0b017b" dependencies = [ - "async-stream", "bytes", "futures-core", "futures-sink", "log", "pin-project-lite", "tokio", - "tokio-stream", ] [[package]] @@ -2762,15 +2724,15 @@ dependencies = [ [[package]] name = "tower-service" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +checksum = "f7d40a22fd029e33300d8d89a5cc8ffce18bb7c587662f54629e94c9de5487f3" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -2808,6 +2770,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -2819,9 +2787,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", ] @@ -2890,7 +2858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" dependencies = [ "form_urlencoded", - "idna 0.2.0", + "idna 0.2.1", "matches", "percent-encoding 2.1.0", ] @@ -2906,12 +2874,13 @@ dependencies = [ [[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 1.2.1", + "bitflags", "chrono", + "rustc_version 0.3.3", ] [[package]] @@ -2998,15 +2967,15 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.1+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c6c3420963c5c64bca373b25e77acb562081b9bb4dd5bb864187742186cea9" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.69" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -3014,9 +2983,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.69" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7" dependencies = [ "bumpalo", "lazy_static", @@ -3029,9 +2998,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.69" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3039,9 +3008,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.69" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385" dependencies = [ "proc-macro2", "quote", @@ -3052,15 +3021,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.69" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" +checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64" [[package]] name = "web-sys" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" +checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/README.md b/README.md index d49aeda1..e772c510 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -[![Build Status](https://img.shields.io/github/workflow/status/librespot-org/librespot/test/dev)](https://github.com/librespot-org/librespot/actions) -[![Build Status](https://travis-ci.org/librespot-org/librespot.svg?branch=dev)](https://travis-ci.org/librespot-org/librespot) +[![Build Status](https://github.com/librespot-org/librespot/workflows/test/badge.svg)](https://github.com/librespot-org/librespot/actions) [![Gitter chat](https://badges.gitter.im/librespot-org/librespot.png)](https://gitter.im/librespot-org/spotify-connect-resources) [![Crates.io](https://img.shields.io/crates/v/librespot.svg)](https://crates.io/crates/librespot) @@ -21,7 +20,7 @@ As the origin by [plietar](https://github.com/plietar/) is no longer actively ma # Documentation Documentation is currently a work in progress, contributions are welcome! -There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder, +There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder, [COMPILING.md](https://github.com/librespot-org/librespot/blob/master/COMPILING.md) contains detailed instructions on setting up a development environment, and compiling librespot. More general usage and compilation information is available on the [wiki](https://github.com/librespot-org/librespot/wiki). [CONTRIBUTING.md](https://github.com/librespot-org/librespot/blob/master/CONTRIBUTING.md) also contains our contributing guidelines. @@ -32,7 +31,7 @@ If you wish to learn more about how librespot works overall, the best way is to If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, eg. the Spotify URI of the song that caused the crash. # Building -A quick walk through of the build process is outlined here, while a detailed compilation guide can be found [here](https://github.com/librespot-org/librespot/blob/master/COMPILING.md). +A quick walk through of the build process is outlined here, while a detailed compilation guide can be found [here](https://github.com/librespot-org/librespot/blob/master/COMPILING.md). ## Additional Dependencies We recently switched to using [Rodio](https://github.com/tomaka/rodio) for audio playback by default, hence for macOS and Windows, you should just be able to clone and build librespot (with the command below). @@ -86,6 +85,7 @@ The above command will create a receiver named ```Librespot```, with bitrate set A full list of runtime options are available [here](https://github.com/librespot-org/librespot/wiki/Options) +_Please Note: When using the cache feature, an authentication blob is stored for your account in the cache directory. For security purposes, we recommend that you set directory permissions on the cache directory to `700`._ ## Contact Come and hang out on gitter if you need help or want to offer some. https://gitter.im/librespot-org/spotify-connect-resources @@ -110,4 +110,3 @@ functionality. - [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot. - [ncspot](https://github.com/hrkfdn/ncspot) - Cross-platform ncurses Spotify client. - [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot. - diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 5e950cdc..1b5b074c 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -23,7 +23,7 @@ num-traits = "0.2" pin-project-lite = "0.2.4" tempfile = "3.1" -librespot-tremor = { version = "0.1.0", optional = true } +librespot-tremor = { version = "0.2.0", optional = true } vorbis = { version ="0.0.14", optional = true } [features] diff --git a/audio/src/fetch.rs b/audio/src/fetch.rs index 51dddc6b..286a2b88 100644 --- a/audio/src/fetch.rs +++ b/audio/src/fetch.rs @@ -312,8 +312,8 @@ impl AudioFile { let session_ = session.clone(); session.spawn(complete_rx.map_ok(move |mut file| { if let Some(cache) = session_.cache() { - cache.save_file(file_id, &mut file); debug!("File {} complete, saving to cache", file_id); + cache.save_file(file_id, &mut file); } else { debug!("File {} complete", file_id); } @@ -336,6 +336,13 @@ impl AudioFile { }, } } + + pub fn is_cached(&self) -> bool { + match self { + AudioFile::Cached { .. } => true, + _ => false, + } + } } impl AudioFileStreaming { diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index c2198251..48be2b86 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -73,10 +73,6 @@ impl fmt::Display for VorbisError { } impl error::Error for VorbisError { - fn description(&self) -> &str { - error::Error::description(&self.0) - } - fn source(&self) -> Option<&(dyn error::Error + 'static)> { error::Error::source(&self.0) } diff --git a/connect/src/discovery.rs b/connect/src/discovery.rs index 6c542e6b..f9414ee6 100644 --- a/connect/src/discovery.rs +++ b/connect/src/discovery.rs @@ -77,7 +77,7 @@ impl Discovery { "status": 101, "statusString": "ERROR-OK", "spotifyError": 0, - "version": "2.1.0", + "version": "2.7.1", "deviceID": (self.0.device_id), "remoteName": (self.0.config.name), "activeUser": "", @@ -87,6 +87,9 @@ impl Discovery { "accountReq": "PREMIUM", "brandDisplayName": "librespot", "modelDisplayName": "librespot", + "resolverVersion": "0", + "groupStatus": "NONE", + "voiceSupport": "NO", }); let body = result.to_string(); diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 5e3ba389..352a3fcf 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -21,7 +21,6 @@ use librespot_core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError}; use librespot_core::util::url_encode; use librespot_core::util::SeqGenerator; use librespot_core::version; -use librespot_core::volume::Volume; enum SpircPlayStatus { Stopped, @@ -1297,7 +1296,7 @@ impl SpircTask { self.mixer .set_volume(volume_to_mixer(volume, &self.config.volume_ctrl)); if let Some(cache) = self.session.cache() { - cache.save_volume(Volume { volume }) + cache.save_volume(volume) } self.player.emit_volume_set_event(volume); } diff --git a/core/Cargo.toml b/core/Cargo.toml index e0d79527..25c0c654 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,6 +17,7 @@ aes = "0.6" base64 = "0.13" byteorder = "1.4" bytes = "1.0" +error-chain = { version = "0.12", default-features = false } futures = { version = "0.3", features = ["bilock", "unstable"] } hmac = "0.7" httparse = "1.3" diff --git a/core/src/authentication.rs b/core/src/authentication.rs index dd39fd85..65fa33f5 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -5,12 +5,10 @@ use hmac::Hmac; use pbkdf2::pbkdf2; use protobuf::ProtobufEnum; use sha1::{Digest, Sha1}; -use std::fs::File; -use std::io::{self, Read, Write}; -use std::ops::FnOnce; -use std::path::Path; +use std::io::{self, Read}; use crate::protocol::authentication::AuthenticationType; +use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Credentials { @@ -110,27 +108,6 @@ impl Credentials { auth_data: auth_data, } } - - fn from_reader(mut reader: R) -> Credentials { - let mut contents = String::new(); - reader.read_to_string(&mut contents).unwrap(); - - serde_json::from_str(&contents).unwrap() - } - - pub(crate) fn from_file>(path: P) -> Option { - File::open(path).ok().map(Credentials::from_reader) - } - - fn save_to_writer(&self, writer: &mut W) { - let contents = serde_json::to_string(&self.clone()).unwrap(); - writer.write_all(contents.as_bytes()).unwrap(); - } - - pub(crate) fn save_to_file>(&self, path: P) { - let mut file = File::create(path).unwrap(); - self.save_to_writer(&mut file) - } } fn serialize_protobuf_enum(v: &T, ser: S) -> Result @@ -189,3 +166,37 @@ pub fn get_credentials String>( (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/cache.rs b/core/src/cache.rs index e711777c..55c9ab01 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -1,115 +1,178 @@ use std::fs; use std::fs::File; -use std::io; -use std::io::Read; -use std::path::Path; -use std::path::PathBuf; +use std::io::{self, Error, ErrorKind, Read, Write}; +use std::path::{Path, PathBuf}; use crate::authentication::Credentials; use crate::spotify_id::FileId; -use crate::volume::Volume; +/// A cache for volume, credentials and audio files. #[derive(Clone)] pub struct Cache { - audio_root: PathBuf, - system_root: PathBuf, - use_audio_cache: bool, -} - -fn mkdir_existing(path: &Path) -> io::Result<()> { - fs::create_dir(path).or_else(|err| { - if err.kind() == io::ErrorKind::AlreadyExists { - Ok(()) - } else { - Err(err) - } - }) + credentials_location: Option, + volume_location: Option, + audio_location: Option, } impl Cache { - pub fn new(audio_location: PathBuf, system_location: PathBuf, use_audio_cache: bool) -> Cache { - if use_audio_cache == true { - mkdir_existing(&audio_location).unwrap(); - mkdir_existing(&audio_location.join("files")).unwrap(); + pub fn new>( + system_location: Option

, + audio_location: Option

, + ) -> io::Result { + if let Some(location) = &system_location { + fs::create_dir_all(location)?; } - mkdir_existing(&system_location).unwrap(); - Cache { - audio_root: audio_location, - system_root: system_location, - use_audio_cache: use_audio_cache, + if let Some(location) = &audio_location { + fs::create_dir_all(location)?; } - } -} -impl Cache { - fn credentials_path(&self) -> PathBuf { - self.system_root.join("credentials.json") + let audio_location = audio_location.map(|p| p.as_ref().to_owned()); + let volume_location = system_location.as_ref().map(|p| p.as_ref().join("volume")); + let credentials_location = system_location + .as_ref() + .map(|p| p.as_ref().join("credentials.json")); + + let cache = Cache { + credentials_location, + volume_location, + audio_location, + }; + + Ok(cache) } pub fn credentials(&self) -> Option { - let path = self.credentials_path(); - Credentials::from_file(path) + let location = self.credentials_location.as_ref()?; + + // This closure is just convencience to enable the question mark operator + let read = || { + let mut file = File::open(location)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + serde_json::from_str(&contents).map_err(|e| Error::new(ErrorKind::InvalidData, e)) + }; + + match read() { + Ok(c) => Some(c), + Err(e) => { + // If the file did not exist, the file was probably not written + // before. Otherwise, log the error. + if e.kind() != ErrorKind::NotFound { + warn!("Error reading credentials from cache: {}", e); + } + None + } + } } pub fn save_credentials(&self, cred: &Credentials) { - let path = self.credentials_path(); - cred.save_to_file(&path); - } -} + if let Some(location) = &self.credentials_location { + let result = File::create(location).and_then(|mut file| { + let data = serde_json::to_string(cred)?; + write!(file, "{}", data) + }); -// cache volume to system_root/volume -impl Cache { - fn volume_path(&self) -> PathBuf { - self.system_root.join("volume") + if let Err(e) = result { + warn!("Cannot save credentials to cache: {}", e) + } + } } pub fn volume(&self) -> Option { - let path = self.volume_path(); - Volume::from_file(path) + let location = self.volume_location.as_ref()?; + + let read = || { + let mut file = File::open(location)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + contents + .parse() + .map_err(|e| Error::new(ErrorKind::InvalidData, e)) + }; + + match read() { + Ok(v) => Some(v), + Err(e) => { + if e.kind() != ErrorKind::NotFound { + warn!("Error reading volume from cache: {}", e); + } + None + } + } } - pub fn save_volume(&self, volume: Volume) { - let path = self.volume_path(); - volume.save_to_file(&path); + pub fn save_volume(&self, volume: u16) { + if let Some(ref location) = self.volume_location { + let result = File::create(location).and_then(|mut file| write!(file, "{}", volume)); + if let Err(e) = result { + warn!("Cannot save volume to cache: {}", e); + } + } } -} -impl Cache { - fn file_path(&self, file: FileId) -> PathBuf { - let name = file.to_base16(); - self.audio_root - .join("files") - .join(&name[0..2]) - .join(&name[2..]) + fn file_path(&self, file: FileId) -> Option { + self.audio_location.as_ref().map(|location| { + let name = file.to_base16(); + let mut path = location.join(&name[0..2]); + path.push(&name[2..]); + path + }) } pub fn file(&self, file: FileId) -> Option { - File::open(self.file_path(file)).ok() + File::open(self.file_path(file)?) + .map_err(|e| { + if e.kind() != ErrorKind::NotFound { + warn!("Error reading file from cache: {}", e) + } + }) + .ok() } - pub fn save_file(&self, file: FileId, contents: &mut dyn Read) { - if self.use_audio_cache { - let path = self.file_path(file); - mkdir_existing(path.parent().unwrap()).unwrap(); + pub fn save_file(&self, file: FileId, contents: &mut F) { + let path = if let Some(path) = self.file_path(file) { + path + } else { + return; + }; + let parent = path.parent().unwrap(); - let mut cache_file = File::create(path).unwrap_or_else(|_e| { - ::std::fs::remove_dir_all(&self.audio_root.join("files")).unwrap(); - mkdir_existing(&self.audio_root.join("files")).unwrap(); + let result = fs::create_dir_all(parent) + .and_then(|_| File::create(&path)) + .and_then(|mut file| io::copy(contents, &mut file)); - let path = self.file_path(file); - mkdir_existing(path.parent().unwrap()).unwrap(); - File::create(path).unwrap() - }); - ::std::io::copy(contents, &mut cache_file).unwrap_or_else(|_e| { - ::std::fs::remove_dir_all(&self.audio_root.join("files")).unwrap(); - mkdir_existing(&self.audio_root.join("files")).unwrap(); + if let Err(e) = result { + if e.kind() == ErrorKind::Other { + // Perhaps there's no space left in the cache + // TODO: try to narrow down the error (platform-dependently) + info!("An error occured while writing to cache, trying to flush the cache"); - let path = self.file_path(file); - mkdir_existing(path.parent().unwrap()).unwrap(); - let mut file = File::create(path).unwrap(); - ::std::io::copy(contents, &mut file).unwrap() - }); + if fs::remove_dir_all(self.audio_location.as_ref().unwrap()) + .and_then(|_| fs::create_dir_all(parent)) + .and_then(|_| File::create(&path)) + .and_then(|mut file| io::copy(contents, &mut file)) + .is_ok() + { + // It worked, there's no need to print a warning + return; + } + } + + warn!("Cannot save file to cache: {}", e) + } + } + + pub fn remove_file(&self, file: FileId) -> bool { + if let Some(path) = self.file_path(file) { + if let Err(err) = fs::remove_file(path) { + warn!("Unable to remove file from cache: {}", err); + false + } else { + true + } + } else { + false } } } diff --git a/core/src/config.rs b/core/src/config.rs index 12c1b2ed..60cb66e0 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -36,6 +36,16 @@ pub enum DeviceType { AVR = 6, STB = 7, AudioDongle = 8, + GameConsole = 9, + CastAudio = 10, + CastVideo = 11, + Automobile = 12, + Smartwatch = 13, + Chromebook = 14, + UnknownSpotify = 100, + CarThing = 101, + Observer = 102, + HomeThing = 103, } impl FromStr for DeviceType { @@ -51,6 +61,14 @@ impl FromStr for DeviceType { "avr" => Ok(AVR), "stb" => Ok(STB), "audiodongle" => Ok(AudioDongle), + "gameconsole" => Ok(GameConsole), + "castaudio" => Ok(CastAudio), + "castvideo" => Ok(CastVideo), + "automobile" => Ok(Automobile), + "smartwatch" => Ok(Smartwatch), + "chromebook" => Ok(Chromebook), + "carthing" => Ok(CarThing), + "homething" => Ok(HomeThing), _ => Err(()), } } @@ -69,6 +87,16 @@ impl fmt::Display for DeviceType { 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"), + CastVideo => f.write_str("CastVideo"), + Automobile => f.write_str("Automobile"), + Smartwatch => f.write_str("Smartwatch"), + Chromebook => f.write_str("Chromebook"), + UnknownSpotify => f.write_str("UnknownSpotify"), + CarThing => f.write_str("CarThing"), + Observer => f.write_str("Observer"), + HomeThing => f.write_str("HomeThing"), } } } diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index e0071408..1ca73165 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -12,7 +12,7 @@ use tokio::net::TcpStream; use tokio_util::codec::Framed; use url::Url; -use crate::authentication::Credentials; +use crate::authentication::{AuthenticationError, Credentials}; use crate::version; use crate::proxytunnel; @@ -64,7 +64,7 @@ pub async fn authenticate( transport: &mut Transport, credentials: Credentials, device_id: &str, -) -> io::Result { +) -> Result { use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; use crate::protocol::keyexchange::APLoginFailed; @@ -114,10 +114,7 @@ pub async fn authenticate( 0xad => { let error_data: APLoginFailed = protobuf::parse_from_bytes(data.as_ref()).unwrap(); - panic!( - "Authentication failed with reason: {:?}", - error_data.get_error_code() - ) + Err(error_data.into()) } _ => panic!("Unexpected packet {:?}", cmd), diff --git a/core/src/lib.rs b/core/src/lib.rs index 3e332c28..25ce5413 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -6,6 +6,8 @@ extern crate log; extern crate serde_derive; #[macro_use] extern crate pin_project_lite; +#[macro_use] +extern crate error_chain; extern crate aes; extern crate base64; extern crate byteorder; @@ -51,4 +53,3 @@ pub mod session; pub mod spotify_id; pub mod util; pub mod version; -pub mod volume; diff --git a/core/src/session.rs b/core/src/session.rs index 2def4085..fd706798 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -19,6 +19,8 @@ use crate::config::SessionConfig; use crate::connection; use crate::mercury::MercuryManager; +pub use crate::authentication::{AuthenticationError, AuthenticationErrorKind}; + struct SessionData { country: String, time_delta: i64, @@ -50,7 +52,7 @@ impl Session { config: SessionConfig, credentials: Credentials, cache: Option, - ) -> io::Result { + ) -> Result { let ap = apresolve_or_fallback(&config.proxy, &config.ap_port).await; info!("Connecting to AP \"{}\"", ap); diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 0982f9cb..17327b47 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -1,4 +1,4 @@ -use std; +use std::convert::TryInto; use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -8,6 +8,26 @@ pub enum SpotifyAudioType { NonPlayable, } +impl From<&str> for SpotifyAudioType { + fn from(v: &str) -> Self { + match v { + "track" => SpotifyAudioType::Track, + "episode" => SpotifyAudioType::Podcast, + _ => SpotifyAudioType::NonPlayable, + } + } +} + +impl Into<&str> for SpotifyAudioType { + fn into(self) -> &'static str { + match self { + SpotifyAudioType::Track => "track", + SpotifyAudioType::Podcast => "episode", + SpotifyAudioType::NonPlayable => "unknown", + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SpotifyId { pub id: u128, @@ -17,104 +37,178 @@ pub struct SpotifyId { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct SpotifyIdError; -const BASE62_DIGITS: &'static [u8] = - b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; -const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef"; +const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef"; impl SpotifyId { + const SIZE: usize = 16; + const SIZE_BASE16: usize = 32; + const SIZE_BASE62: usize = 22; + fn as_track(n: u128) -> SpotifyId { SpotifyId { - id: n.to_owned(), + id: n, audio_type: SpotifyAudioType::Track, } } - pub fn from_base16(id: &str) -> Result { - let data = id.as_bytes(); + /// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`. + /// + /// `src` is expected to be 32 bytes long and encoded using valid characters. + /// + /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids + pub fn from_base16(src: &str) -> Result { + let mut dst: u128 = 0; - let mut n = 0u128; - for c in data { - let d = match BASE16_DIGITS.iter().position(|e| e == c) { - None => return Err(SpotifyIdError), - Some(x) => x as u128, - }; - n = n * 16; - n = n + d; + for c in src.as_bytes() { + let p = match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + _ => return Err(SpotifyIdError), + } as u128; + + dst <<= 4; + dst += p; } - Ok(SpotifyId::as_track(n)) + Ok(SpotifyId::as_track(dst)) } - pub fn from_base62(id: &str) -> Result { - let data = id.as_bytes(); + /// Parses a base62 encoded [Spotify ID] into a `SpotifyId`. + /// + /// `src` is expected to be 22 bytes long and encoded using valid characters. + /// + /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids + pub fn from_base62(src: &str) -> Result { + let mut dst: u128 = 0; - let mut n = 0u128; - for c in data { - let d = match BASE62_DIGITS.iter().position(|e| e == c) { - None => return Err(SpotifyIdError), - Some(x) => x as u128, - }; - n = n * 62; - n = n + d; + for c in src.as_bytes() { + let p = match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'z' => c - b'a' + 10, + b'A'..=b'Z' => c - b'A' + 36, + _ => return Err(SpotifyIdError), + } as u128; + + dst *= 62; + dst += p; } - Ok(SpotifyId::as_track(n)) + + Ok(SpotifyId::as_track(dst)) } - pub fn from_raw(data: &[u8]) -> Result { - if data.len() != 16 { + /// Creates a `SpotifyId` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order. + /// + /// 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))), + Err(_) => Err(SpotifyIdError), + } + } + + /// Parses a [Spotify URI] into a `SpotifyId`. + /// + /// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}` + /// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID. + /// + /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids + pub fn from_uri(src: &str) -> Result { + // We expect the ID to be the last colon-delimited item in the URI. + let b = src.as_bytes(); + let id_i = b.len() - SpotifyId::SIZE_BASE62; + if b[id_i - 1] != b':' { return Err(SpotifyIdError); - }; - - let mut arr: [u8; 16] = Default::default(); - arr.copy_from_slice(&data[0..16]); - - Ok(SpotifyId::as_track(u128::from_be_bytes(arr))) - } - - pub fn from_uri(uri: &str) -> Result { - let parts = uri.split(":").collect::>(); - let gid = parts.last().unwrap(); - if uri.contains(":episode:") { - let mut spotify_id = SpotifyId::from_base62(gid).unwrap(); - let _ = std::mem::replace(&mut spotify_id.audio_type, SpotifyAudioType::Podcast); - Ok(spotify_id) - } else if uri.contains(":track:") { - SpotifyId::from_base62(gid) - } else { - // show/playlist/artist/album/?? - let mut spotify_id = SpotifyId::from_base62(gid).unwrap(); - let _ = std::mem::replace(&mut spotify_id.audio_type, SpotifyAudioType::NonPlayable); - Ok(spotify_id) } + + let mut id = SpotifyId::from_base62(&src[id_i..])?; + + // Slice offset by 8 as we are skipping the "spotify:" prefix. + id.audio_type = src[8..id_i - 1].into(); + + Ok(id) } + /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE62` (22) + /// character long `String`. pub fn to_base16(&self) -> String { - format!("{:032x}", self.id) + to_base16(&self.to_raw(), &mut [0u8; SpotifyId::SIZE_BASE16]) } + /// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22) + /// character long `String`. + /// + /// [canonically]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids pub fn to_base62(&self) -> String { - let &SpotifyId { id: mut n, .. } = self; + let mut dst = [0u8; 22]; + let mut i = 0; + let n = self.id; - let mut data = [0u8; 22]; - for i in 0..22 { - data[21 - i] = BASE62_DIGITS[(n % 62) as usize]; - n /= 62; + // The algorithm is based on: + // https://github.com/trezor/trezor-crypto/blob/c316e775a2152db255ace96b6b65ac0f20525ec0/base58.c + // + // We are not using naive division of self.id as it is an u128 and div + mod are software + // emulated at runtime (and unoptimized into mul + shift) on non-128bit platforms, + // making them very expensive. + // + // Trezor's algorithm allows us to stick to arithmetic on native registers making this + // an order of magnitude faster. Additionally, as our sizes are known, instead of + // dealing with the ID on a byte by byte basis, we decompose it into four u32s and + // use 64-bit arithmetic on them for an additional speedup. + for shift in &[96, 64, 32, 0] { + let mut carry = (n >> shift) as u32 as u64; + + for b in &mut dst[..i] { + carry += (*b as u64) << 32; + *b = (carry % 62) as u8; + carry /= 62; + } + + while carry > 0 { + dst[i] = (carry % 62) as u8; + carry /= 62; + i += 1; + } } - std::str::from_utf8(&data).unwrap().to_owned() - } + for b in &mut dst { + *b = BASE62_DIGITS[*b as usize]; + } - pub fn to_uri(&self) -> String { - match self.audio_type { - SpotifyAudioType::Track => format!("spotify:track:{}", self.to_base62()), - SpotifyAudioType::Podcast => format!("spotify:episode:{}", self.to_base62()), - SpotifyAudioType::NonPlayable => format!("spotify:unknown:{}", self.to_base62()), + dst.reverse(); + + unsafe { + // Safety: We are only dealing with ASCII characters. + String::from_utf8_unchecked(dst.to_vec()) } } - pub fn to_raw(&self) -> [u8; 16] { + /// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in + /// big-endian order. + pub fn to_raw(&self) -> [u8; SpotifyId::SIZE] { self.id.to_be_bytes() } + + /// Returns the `SpotifyId` as a [Spotify URI] in the canonical form `spotify:{type}:{id}`, + /// where `{type}` is an arbitrary string and `{id}` is a 22-character long, base62 encoded + /// Spotify ID. + /// + /// If the `SpotifyId` has an associated type unrecognized by the library, `{type}` will + /// be encoded as `unknown`. + /// + /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids + pub fn to_uri(&self) -> String { + // 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31 + // + unknown size audio_type. + let audio_type: &str = self.audio_type.into(); + let mut dst = String::with_capacity(31 + audio_type.len()); + dst.push_str("spotify:"); + dst.push_str(audio_type); + dst.push(':'); + dst.push_str(&self.to_base62()); + + dst + } } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -122,11 +216,7 @@ pub struct FileId(pub [u8; 20]); impl FileId { pub fn to_base16(&self) -> String { - self.0 - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .concat() + to_base16(&self.0, &mut [0u8; 40]) } } @@ -141,3 +231,185 @@ impl fmt::Display for FileId { f.write_str(&self.to_base16()) } } + +#[inline] +fn to_base16(src: &[u8], buf: &mut [u8]) -> String { + let mut i = 0; + for v in src { + buf[i] = BASE16_DIGITS[(v >> 4) as usize]; + buf[i + 1] = BASE16_DIGITS[(v & 0x0f) as usize]; + i += 2; + } + + unsafe { + // Safety: We are only dealing with ASCII characters. + String::from_utf8_unchecked(buf.to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct ConversionCase { + id: u128, + kind: SpotifyAudioType, + uri: &'static str, + base16: &'static str, + base62: &'static str, + raw: &'static [u8], + } + + static CONV_VALID: [ConversionCase; 4] = [ + ConversionCase { + id: 238762092608182713602505436543891614649, + kind: SpotifyAudioType::Track, + uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH", + base16: "b39fe8081e1f4c54be38e8d6f9f12bb9", + base62: "5sWHDYs0csV6RS48xBl0tH", + raw: &[ + 179, 159, 232, 8, 30, 31, 76, 84, 190, 56, 232, 214, 249, 241, 43, 185, + ], + }, + ConversionCase { + id: 204841891221366092811751085145916697048, + kind: SpotifyAudioType::Track, + uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4", + base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", + base62: "4GNcXTGWmnZ3ySrqvol3o4", + raw: &[ + 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216, + ], + }, + ConversionCase { + id: 204841891221366092811751085145916697048, + kind: SpotifyAudioType::Podcast, + uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4", + base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", + base62: "4GNcXTGWmnZ3ySrqvol3o4", + raw: &[ + 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216, + ], + }, + ConversionCase { + id: 204841891221366092811751085145916697048, + kind: SpotifyAudioType::NonPlayable, + uri: "spotify:unknown:4GNcXTGWmnZ3ySrqvol3o4", + base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", + base62: "4GNcXTGWmnZ3ySrqvol3o4", + raw: &[ + 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216, + ], + }, + ]; + + static CONV_INVALID: [ConversionCase; 2] = [ + ConversionCase { + id: 0, + kind: SpotifyAudioType::NonPlayable, + // Invalid ID in the URI. + uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH", + base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9", + base62: "!!!!!Ys0csV6RS48xBl0tH", + raw: &[ + // Invalid length. + 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 5, 3, 108, 119, 187, 233, 216, 255, + ], + }, + ConversionCase { + id: 0, + kind: SpotifyAudioType::NonPlayable, + // Missing colon between ID and type. + uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", + base16: "--------------------", + base62: "....................", + raw: &[ + // Invalid length. + 154, 27, 28, 251, + ], + }, + ]; + + #[test] + fn from_base62() { + for c in &CONV_VALID { + assert_eq!(SpotifyId::from_base62(c.base62).unwrap().id, c.id); + } + + for c in &CONV_INVALID { + assert_eq!(SpotifyId::from_base62(c.base62), Err(SpotifyIdError)); + } + } + + #[test] + fn to_base62() { + for c in &CONV_VALID { + let id = SpotifyId { + id: c.id, + audio_type: c.kind, + }; + + assert_eq!(id.to_base62(), c.base62); + } + } + + #[test] + fn from_base16() { + for c in &CONV_VALID { + assert_eq!(SpotifyId::from_base16(c.base16).unwrap().id, c.id); + } + + for c in &CONV_INVALID { + assert_eq!(SpotifyId::from_base16(c.base16), Err(SpotifyIdError)); + } + } + + #[test] + fn to_base16() { + for c in &CONV_VALID { + let id = SpotifyId { + id: c.id, + audio_type: c.kind, + }; + + assert_eq!(id.to_base16(), c.base16); + } + } + + #[test] + fn from_uri() { + for c in &CONV_VALID { + let actual = SpotifyId::from_uri(c.uri).unwrap(); + + assert_eq!(actual.id, c.id); + assert_eq!(actual.audio_type, c.kind); + } + + for c in &CONV_INVALID { + assert_eq!(SpotifyId::from_uri(c.uri), Err(SpotifyIdError)); + } + } + + #[test] + fn to_uri() { + for c in &CONV_VALID { + let id = SpotifyId { + id: c.id, + audio_type: c.kind, + }; + + assert_eq!(id.to_uri(), c.uri); + } + } + + #[test] + fn from_raw() { + for c in &CONV_VALID { + assert_eq!(SpotifyId::from_raw(c.raw).unwrap().id, c.id); + } + + for c in &CONV_INVALID { + assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError)); + } + } +} diff --git a/core/src/volume.rs b/core/src/volume.rs deleted file mode 100644 index 6b456d1f..00000000 --- a/core/src/volume.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::fs::File; -use std::io::{Read, Write}; -use std::path::Path; - -#[derive(Clone, Copy, Debug)] -pub struct Volume { - pub volume: u16, -} - -impl Volume { - // read volume from file - fn from_reader(mut reader: R) -> u16 { - let mut contents = String::new(); - reader.read_to_string(&mut contents).unwrap(); - contents.trim().parse::().unwrap() - } - - pub(crate) fn from_file>(path: P) -> Option { - File::open(path).ok().map(Volume::from_reader) - } - - // write volume to file - fn save_to_writer(&self, writer: &mut W) { - writer - .write_all(self.volume.to_string().as_bytes()) - .unwrap(); - } - - pub(crate) fn save_to_file>(&self, path: P) { - let mut file = File::create(path).unwrap(); - self.save_to_writer(&mut file) - } -} diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 10451851..95c4a12a 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -23,7 +23,7 @@ log = "0.4" byteorder = "1.4" shell-words = "1.0.0" -alsa = { version = "0.2", optional = true } +alsa = { version = "0.4", optional = true } portaudio-rs = { version = "0.3", optional = true } libpulse-binding = { version = "2.13", optional = true, default-features = false } libpulse-simple-binding = { version = "2.13", optional = true, default-features = false } diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index e0b9ad9b..6c8d7211 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -67,7 +67,11 @@ impl Sink for PulseAudioSink { fn write(&mut self, data: &[i16]) -> io::Result<()> { if let Some(s) = &self.s { - let d: &[u8] = unsafe { std::mem::transmute(data) }; + // SAFETY: An i16 consists of two bytes, so that the given slice can be interpreted + // as a byte array of double length. Each byte pointer is validly aligned, and so + // is the newly created slice. + let d: &[u8] = + unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 2) }; match s.write(d) { Ok(_) => Ok(()), diff --git a/playback/src/config.rs b/playback/src/config.rs index 9d65042c..0a9bb47d 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -25,10 +25,34 @@ impl Default for Bitrate { } } +#[derive(Clone, Debug)] +pub enum NormalisationType { + Album, + Track, +} + +impl FromStr for NormalisationType { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "album" => Ok(NormalisationType::Album), + "track" => Ok(NormalisationType::Track), + _ => Err(()), + } + } +} + +impl Default for NormalisationType { + fn default() -> NormalisationType { + NormalisationType::Album + } +} + #[derive(Clone, Debug)] pub struct PlayerConfig { pub bitrate: Bitrate, pub normalisation: bool, + pub normalisation_type: NormalisationType, pub normalisation_pregain: f32, pub gapless: bool, } @@ -38,6 +62,7 @@ impl Default for PlayerConfig { PlayerConfig { bitrate: Bitrate::default(), normalisation: false, + normalisation_type: NormalisationType::default(), normalisation_pregain: 0.0, gapless: true, } diff --git a/playback/src/player.rs b/playback/src/player.rs index ff0fba24..b5683e55 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -5,6 +5,7 @@ use crate::audio::{ READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; use crate::audio_backend::Sink; +use crate::config::NormalisationType; use crate::config::{Bitrate, PlayerConfig}; use crate::librespot_core::tokio; use crate::metadata::{AudioItem, FileFormat}; @@ -217,17 +218,20 @@ impl NormalisationData { } fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 { - let mut normalisation_factor = f32::powf( - 10.0, - (data.track_gain_db + config.normalisation_pregain) / 20.0, - ); + let [gain_db, gain_peak] = match config.normalisation_type { + NormalisationType::Album => [data.album_gain_db, data.album_peak], + NormalisationType::Track => [data.track_gain_db, data.track_peak], + }; + let mut normalisation_factor = + f32::powf(10.0, (gain_db + config.normalisation_pregain) / 20.0); - if normalisation_factor * data.track_peak > 1.0 { + if normalisation_factor * gain_peak > 1.0 { warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid."); - normalisation_factor = 1.0 / data.track_peak; + normalisation_factor = 1.0 / gain_peak; } debug!("Normalisation Data: {:?}", data); + debug!("Normalisation Type: {:?}", config.normalisation_type); debug!("Applied normalisation factor: {}", normalisation_factor); normalisation_factor @@ -652,49 +656,27 @@ impl PlayerTrackLoader { FileFormat::OGG_VORBIS_96, ], }; - let format = formats - .iter() - .find(|format| audio.files.contains_key(format)) - .unwrap(); - let file_id = match audio.files.get(&format) { - Some(&file_id) => file_id, + let entry = formats.iter().find_map(|format| { + if let Some(&file_id) = audio.files.get(format) { + Some((*format, file_id)) + } else { + None + } + }); + + let (format, file_id) = match entry { + Some(t) => t, None => { - warn!("<{}> in not available in format {:?}", audio.name, format); + warn!("<{}> is not available in any supported format", audio.name); return None; } }; - let bytes_per_second = self.stream_data_rate(*format); + 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.await { - Ok(encrypted_file) => encrypted_file, - Err(_) => { - error!("Unable to load encrypted file."); - return None; - } - }; - - let mut stream_loader_controller = encrypted_file.get_stream_loader_controller(); - - 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 key = match key.await { + let key = match self.session.audio_key().request(spotify_id, file_id).await { Ok(key) => key, Err(_) => { error!("Unable to load decryption key"); @@ -702,39 +684,90 @@ impl PlayerTrackLoader { } }; - let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); + // 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 normalisation_factor = match NormalisationData::parse_from_file(&mut decrypted_file) { - Ok(normalisation_data) => { - NormalisationData::get_factor(&self.config, normalisation_data) + 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(); + + let mut stream_loader_controller = encrypted_file.get_stream_loader_controller(); + + 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(); } - Err(_) => { - warn!("Unable to extract normalisation data, using default value."); - 1.0_f32 + + let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); + + 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 audio_file = Subfile::new(decrypted_file, 0xa7); + + let mut decoder = match VorbisDecoder::new(audio_file) { + 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; + } + + // Just try it again + continue; + } + 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); - let audio_file = Subfile::new(decrypted_file, 0xa7); - - let mut decoder = VorbisDecoder::new(audio_file).unwrap(); - - if position_ms != 0 { - match decoder.seek(position_ms as i64) { - Ok(_) => (), - Err(err) => 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, - }) } } diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 4ce66c5e..102cf780 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -24,16 +24,16 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option { - env_vars.insert("PLAYER_EVENT", "change".to_string()); + env_vars.insert("PLAYER_EVENT", "changed".to_string()); env_vars.insert("OLD_TRACK_ID", old_track_id.to_base62()); env_vars.insert("TRACK_ID", new_track_id.to_base62()); } PlayerEvent::Started { track_id, .. } => { - env_vars.insert("PLAYER_EVENT", "start".to_string()); + env_vars.insert("PLAYER_EVENT", "started".to_string()); env_vars.insert("TRACK_ID", track_id.to_base62()); } PlayerEvent::Stopped { track_id, .. } => { - env_vars.insert("PLAYER_EVENT", "stop".to_string()); + env_vars.insert("PLAYER_EVENT", "stopped".to_string()); env_vars.insert("TRACK_ID", track_id.to_base62()); } PlayerEvent::Playing {