Merge pull request #891 from roderickvd/new-api-wip

WIP: new Spotify API
This commit is contained in:
Roderick van Domburg 2021-12-29 07:42:57 +01:00 committed by GitHub
commit b4d3c8363f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
313 changed files with 11649 additions and 4400 deletions

View file

@ -31,32 +31,20 @@ on:
"!LICENSE", "!LICENSE",
"!*.sh", "!*.sh",
] ]
schedule:
# Run CI every week
- cron: "00 01 * * 0"
env: env:
RUST_BACKTRACE: 1 RUST_BACKTRACE: 1
RUSTFLAGS: -D warnings
# The layering here is as follows, checking in priority from highest to lowest:
# 1. absence of errors and warnings on Linux/x86
# 2. cross compilation on Windows and Linux/ARM
# 3. absence of lints
# 4. code formatting
jobs: jobs:
fmt:
name: rustfmt
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt
- run: cargo fmt --all -- --check
test-linux: test-linux:
needs: fmt name: cargo +${{ matrix.toolchain }} check (${{ matrix.os }})
name: cargo +${{ matrix.toolchain }} build (${{ matrix.os }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }} continue-on-error: ${{ matrix.experimental }}
strategy: strategy:
@ -66,12 +54,11 @@ jobs:
toolchain: toolchain:
- 1.48 # MSRV (Minimum supported rust version) - 1.48 # MSRV (Minimum supported rust version)
- stable - stable
- beta
experimental: [false] experimental: [false]
# Ignore failures in nightly # Ignore failures in beta
include: include:
- os: ubuntu-latest - os: ubuntu-latest
toolchain: nightly toolchain: beta
experimental: true experimental: true
steps: steps:
- name: Checkout code - name: Checkout code
@ -107,21 +94,24 @@ jobs:
- run: cargo install cargo-hack - run: cargo install cargo-hack
- run: cargo hack --workspace --remove-dev-deps - run: cargo hack --workspace --remove-dev-deps
- run: cargo build -p librespot-core --no-default-features - run: cargo check -p librespot-core --no-default-features
- run: cargo build -p librespot-core - run: cargo check -p librespot-core
- run: cargo hack build --each-feature -p librespot-discovery - run: cargo hack check --each-feature -p librespot-discovery
- run: cargo hack build --each-feature -p librespot-playback - run: cargo hack check --each-feature -p librespot-playback
- run: cargo hack build --each-feature - run: cargo hack check --each-feature
test-windows: test-windows:
needs: fmt needs: test-linux
name: cargo build (${{ matrix.os }}) name: cargo +${{ matrix.toolchain }} check (${{ matrix.os }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
continue-on-error: false
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [windows-latest] os: [windows-latest]
toolchain: [stable] toolchain:
- 1.48 # MSRV (Minimum supported rust version)
- stable
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -153,20 +143,22 @@ jobs:
- run: cargo install cargo-hack - run: cargo install cargo-hack
- run: cargo hack --workspace --remove-dev-deps - run: cargo hack --workspace --remove-dev-deps
- run: cargo build --no-default-features - run: cargo check --no-default-features
- run: cargo build - run: cargo check
test-cross-arm: test-cross-arm:
needs: fmt name: cross +${{ matrix.toolchain }} build ${{ matrix.target }}
needs: test-linux
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
continue-on-error: false continue-on-error: false
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: os: [ubuntu-latest]
- os: ubuntu-latest target: [armv7-unknown-linux-gnueabihf]
target: armv7-unknown-linux-gnueabihf toolchain:
toolchain: stable - 1.48 # MSRV (Minimum supported rust version)
- stable
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -197,3 +189,67 @@ jobs:
run: cargo install cross || true run: cargo install cross || true
- name: Build - name: Build
run: cross build --locked --target ${{ matrix.target }} --no-default-features run: cross build --locked --target ${{ matrix.target }} --no-default-features
clippy:
needs: [test-cross-arm, test-windows]
name: cargo +${{ matrix.toolchain }} clippy (${{ matrix.os }})
runs-on: ${{ matrix.os }}
continue-on-error: false
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
toolchain: [stable]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
override: true
components: clippy
- 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') }}
- 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 libavahi-compat-libdnssd-dev
- run: cargo install cargo-hack
- run: cargo hack --workspace --remove-dev-deps
- run: cargo clippy -p librespot-core --no-default-features
- run: cargo clippy -p librespot-core
- run: cargo hack clippy --each-feature -p librespot-discovery
- run: cargo hack clippy --each-feature -p librespot-playback
- run: cargo hack clippy --each-feature
fmt:
needs: clippy
name: cargo +${{ matrix.toolchain }} fmt
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt
- run: cargo fmt --all -- --check

View file

@ -6,15 +6,51 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0.
## [Unreleased] ## [Unreleased]
### Changed
- [main] Enforce reasonable ranges for option values (breaking).
- [main] Don't evaluate options that would otherwise have no effect.
- [playback] `alsa`: Improve `--device ?` functionality for the alsa backend.
- [contrib] Hardened security of the systemd service units
- [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted).
### Added
- [cache] Add `disable-credential-cache` flag (breaking).
- [main] Use different option descriptions and error messages based on what backends are enabled at build time.
- [main] Add a `-q`, `--quiet` option that changes the logging level to warn.
- [main] Add a short name for every flag and option.
- [main] Add the ability to parse environment variables.
### Fixed
- [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given.
- [main] Don't panic when parsing options. Instead list valid values and exit.
### Removed
- [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed.
- [playback] `alsamixer`: previously deprecated option `mixer-name` has been removed.
- [playback] `alsamixer`: previously deprecated option `mixer-index` has been removed.
## [0.3.1] - 2021-10-24
### Changed
- Include build profile in the displayed version information
- [playback] Improve dithering CPU usage by about 33%
### Fixed
- [connect] Partly fix behavior after last track of an album/playlist
## [0.3.0] - 2021-10-13
### Added ### Added
- [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`. - [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`.
- [playback] Add support for dithering with `--dither` for lower requantization error (breaking) - [playback] Add support for dithering with `--dither` for lower requantization error (breaking)
- [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves - [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves
- [playback] `alsamixer`: support for querying dB range from Alsa softvol - [playback] `alsamixer`: support for querying dB range from Alsa softvol
- [playback] Add `--format F64` (supported by Alsa and GStreamer only) - [playback] Add `--format F64` (supported by Alsa and GStreamer only)
- [playback] Add `--normalisation-gain-type auto` that switches between album and track automatically
### Changed ### Changed
- [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) - [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `DecoderError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking)
- [audio, playback] Use `Duration` for time constants and functions (breaking) - [audio, playback] Use `Duration` for time constants and functions (breaking)
- [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate - [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate
- [connect] Synchronize player volume with mixer volume on playback - [connect] Synchronize player volume with mixer volume on playback
@ -22,20 +58,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [playback] Make cubic volume control available to all mixers with `--volume-ctrl cubic` - [playback] Make cubic volume control available to all mixers with `--volume-ctrl cubic`
- [playback] Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking) - [playback] Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking)
- [playback] `alsamixer`: complete rewrite (breaking) - [playback] `alsamixer`: complete rewrite (breaking)
- [playback] `alsamixer`: query card dB range for the `log` volume control unless specified otherwise - [playback] `alsamixer`: query card dB range for the volume control unless specified otherwise
- [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise - [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise
- [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking) - [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking)
- [playback] `player`: make `convert` and `decoder` public so you can implement your own `Sink`
- [playback] `player`: update default normalisation threshold to -2 dBFS
- [playback] `player`: default normalisation type is now `auto`
### Deprecated ### Deprecated
- [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate - [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate
- [playback] `alsamixer`: renamed `mixer-card` to `alsa-mixer-device`
- [playback] `alsamixer`: renamed `mixer-name` to `alsa-mixer-control`
- [playback] `alsamixer`: renamed `mixer-index` to `alsa-mixer-index`
### Removed ### Removed
- [connect] Removed no-op mixer started/stopped logic (breaking) - [connect] Removed no-op mixer started/stopped logic (breaking)
- [playback] Removed `with-vorbis` and `with-tremor` features - [playback] Removed `with-vorbis` and `with-tremor` features
- [playback] `alsamixer`: removed `--mixer-linear-volume` option; use `--volume-ctrl linear` instead - [playback] `alsamixer`: removed `--mixer-linear-volume` option, now that `--volume-ctrl {linear|log}` work as expected on Alsa
### Fixed ### Fixed
- [connect] Fix step size on volume up/down events - [connect] Fix step size on volume up/down events
- [connect] Fix looping back to the first track after the last track of an album or playlist
- [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream - [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream
- [playback] Fix `log` and `cubic` volume controls to be mute at zero volume - [playback] Fix `log` and `cubic` volume controls to be mute at zero volume
- [playback] Fix `S24_3` format on big-endian systems - [playback] Fix `S24_3` format on big-endian systems
@ -43,7 +86,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [playback] `alsamixer`: make `--volume-ctrl {linear|log}` work as expected - [playback] `alsamixer`: make `--volume-ctrl {linear|log}` work as expected
- [playback] `alsa`, `gstreamer`, `pulseaudio`: always output in native endianness - [playback] `alsa`, `gstreamer`, `pulseaudio`: always output in native endianness
- [playback] `alsa`: revert buffer size to ~500 ms - [playback] `alsa`: revert buffer size to ~500 ms
- [playback] `alsa`, `pipe`: better error handling - [playback] `alsa`, `pipe`, `pulseaudio`: better error handling
- [metadata] Skip tracks whose Spotify ID's can't be found (e.g. local files, which aren't supported)
## [0.2.0] - 2021-05-04 ## [0.2.0] - 2021-05-04
@ -59,7 +103,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.1.0] - 2019-11-06 ## [0.1.0] - 2019-11-06
[unreleased]: https://github.com/librespot-org/librespot/compare/v0.2.0..HEAD [unreleased]: https://github.com/librespot-org/librespot/compare/v0.3.1..HEAD
[0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0..v0.3.1
[0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0
[0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6..v0.2.0 [0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6..v0.2.0
[0.1.6]: https://github.com/librespot-org/librespot/compare/v0.1.5..v0.1.6 [0.1.6]: https://github.com/librespot-org/librespot/compare/v0.1.5..v0.1.6
[0.1.5]: https://github.com/librespot-org/librespot/compare/v0.1.3..v0.1.5 [0.1.5]: https://github.com/librespot-org/librespot/compare/v0.1.3..v0.1.5

View file

@ -5,20 +5,15 @@
In order to compile librespot, you will first need to set up a suitable Rust build environment, with the necessary dependencies installed. You will need to have a C compiler, Rust, and the development libraries for the audio backend(s) you want installed. These instructions will walk you through setting up a simple build environment. In order to compile librespot, you will first need to set up a suitable Rust build environment, with the necessary dependencies installed. You will need to have a C compiler, Rust, and the development libraries for the audio backend(s) you want installed. These instructions will walk you through setting up a simple build environment.
### Install Rust ### Install Rust
The easiest, and recommended way to get Rust is to use [rustup](https://rustup.rs). On Unix/MacOS You can install `rustup` with this command: The easiest, and recommended way to get Rust is to use [rustup](https://rustup.rs). Once thats installed, Rust's standard tools should be set up and ready to use.
```bash
curl https://sh.rustup.rs -sSf | sh
```
Follow any prompts it gives you to install Rust. Once thats 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.48, 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.48, you can find the current minimum version specified in the `.github/workflow/test.yml` file.*
#### Additional Rust tools - `rustfmt` #### 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: To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt) and [`clippy`](https://github.com/rust-lang/rust-clippy), which are installed by default with `rustup` these days, else they can be installed manually with:
```bash ```bash
rustup component add rustfmt rustup component add rustfmt
rustup component add clippy
``` ```
Using `rustfmt` is not optional, as our CI checks against this repo's rules. Using `rustfmt` is not optional, as our CI checks against this repo's rules.
@ -43,12 +38,13 @@ Depending on the chosen backend, specific development libraries are required.
|--------------------|------------------------------|-----------------------------------|-------------| |--------------------|------------------------------|-----------------------------------|-------------|
|Rodio (default) | `libasound2-dev` | `alsa-lib-devel` | | |Rodio (default) | `libasound2-dev` | `alsa-lib-devel` | |
|ALSA | `libasound2-dev, pkg-config` | `alsa-lib-devel` | | |ALSA | `libasound2-dev, pkg-config` | `alsa-lib-devel` | |
|GStreamer | `gstreamer1.0-plugins-base libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good libgstreamer-plugins-good1.0-dev` | `gstreamer1 gstreamer1-devel gstreamer1-plugins-base-devel gstreamer1-plugins-good` | `gstreamer gst-devtools gst-plugins-base gst-plugins-good` |
|PortAudio | `portaudio19-dev` | `portaudio-devel` | `portaudio` | |PortAudio | `portaudio19-dev` | `portaudio-devel` | `portaudio` |
|PulseAudio | `libpulse-dev` | `pulseaudio-libs-devel` | | |PulseAudio | `libpulse-dev` | `pulseaudio-libs-devel` | |
|JACK | `libjack-dev` | `jack-audio-connection-kit-devel` | | |JACK | `libjack-dev` | `jack-audio-connection-kit-devel` | `jack` |
|JACK over Rodio | `libjack-dev` | `jack-audio-connection-kit-devel` | - | |JACK over Rodio | `libjack-dev` | `jack-audio-connection-kit-devel` | `jack` |
|SDL | `libsdl2-dev` | `SDL2-devel` | | |SDL | `libsdl2-dev` | `SDL2-devel` | `sdl2` |
|Pipe | - | - | - | |Pipe & subprocess | - | - | - |
###### For example, to build an ALSA based backend, you would need to run the following to install the required dependencies: ###### For example, to build an ALSA based backend, you would need to run the following to install the required dependencies:
@ -68,7 +64,6 @@ The recommended method is to first fork the repo, so that you have a copy that y
```bash ```bash
git clone git@github.com:YOURUSERNAME/librespot.git git clone git@github.com:YOURUSERNAME/librespot.git
cd librespot
``` ```
## Compiling & Running ## Compiling & Running
@ -109,7 +104,9 @@ cargo build --no-default-features --features "alsa-backend"
Assuming you just compiled a ```debug``` build, you can run librespot with the following command: Assuming you just compiled a ```debug``` build, you can run librespot with the following command:
```bash ```bash
./target/debug/librespot -n Librespot ./target/debug/librespot
``` ```
There are various runtime options, documented in the wiki, and visible by running librespot with the ```-h``` argument. There are various runtime options, documented in the wiki, and visible by running librespot with the ```-h``` argument.
Note that debug builds may cause buffer underruns and choppy audio when dithering is enabled (which it is by default). You can disable dithering with ```--dither none```.

View file

@ -8,10 +8,12 @@ If you have encountered a bug, please report it, as we rely on user reports to f
Please also make sure that your issues are helpful. To ensure that your issue is helpful, please read over this brief checklist to avoid the more common pitfalls: Please also make sure that your issues are helpful. To ensure that your issue is helpful, please read over this brief checklist to avoid the more common pitfalls:
- Please take a moment to search/read previous similar issues to ensure you arent posting a duplicate. Duplicates will be closed immediately. - Please take a moment to search/read previous similar issues to ensure you arent posting a duplicate. Duplicates will be closed immediately.
- Please include a clear description of what the issue is. Issues with descriptions such as It hangs after 40 minutes will be closed immediately. - Please include a clear description of what the issue is. Issues with descriptions such as It hangs after 40 minutes will be closed immediately.
- Please include, where possible, steps to reproduce the bug, along with any other material that is related to the bug. For example, if librespot consistently crashes when you try to play a song, please include the Spotify URI of that song. This can be immensely helpful in quickly pinpointing and resolving issues. - Please include, where possible, steps to reproduce the bug, along with any other material that is related to the bug. For example, if librespot consistently crashes when you try to play a song, please include the Spotify URI of that song. This can be immensely helpful in quickly pinpointing and resolving issues.
- Lastly, and perhaps most importantly, please include a backtrace where possible. Recent versions of librespot should produce these automatically when it crashes, and print them to the console, but in some cases, you may need to run export RUST_BACKTRACE=full before running librespot to enable backtraces. - Please be alert and respond to questions asked by any project members. Stale issues will be closed.
- When your issue concerns audio playback, please first make sure that your audio system is set up correctly and can play audio from other applications. This project aims to provide correct audio backends, not to provide Linux support to end users.
- Lastly, and perhaps most importantly, please include a backtrace where possible. Recent versions of librespot should produce these automatically when it crashes, and print them to the console, but in some cases, you may need to run export RUST_BACKTRACE=full before running librespot to enable backtraces.
## Contributing Code ## Contributing Code
@ -29,20 +31,25 @@ In order to prepare for a PR, you will need to do a couple of things first:
Make any changes that you are going to make to the code, but do not commit yet. Make any changes that you are going to make to the code, but do not commit yet.
Unless your changes are negligible, please add an entry in the "Unreleased" section of `CHANGELOG.md`. Refer to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) for instructions on how this entry should look like. Unless your changes are negligible, please add an entry in the "Unreleased" section of `CHANGELOG.md`. Refer to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) for instructions on how this entry should look like. If your changes break the API such that downstream packages that depend on librespot need to update their source to still compile, you should mark your changes as `(breaking)`.
Make sure that the code is correctly formatted by running: Make sure that the code is correctly formatted by running:
```bash ```bash
cargo +stable fmt --all cargo fmt --all
``` ```
This command runs the previously installed stable version of ```rustfmt```, a code formatting tool that will automatically correct any formatting that you have used that does not conform with the librespot code style. Once that command has run, you will need to rebuild the project: This command runs ```rustfmt```, a code formatting tool that will automatically correct any formatting that you have used that does not conform with the librespot code style. Once that command has run, you will need to rebuild the project:
```bash ```bash
cargo build cargo build
``` ```
Once it has built, and you have confirmed there are no warnings or errors, you should commit your changes. Once it has built, check for common code mistakes by running:
```bash
cargo clippy
```
Once you have confirmed there are no warnings or errors, you should commit your changes.
```bash ```bash
git commit -a -m "My fancy fix" git commit -a -m "My fancy fix"

816
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "librespot" name = "librespot"
version = "0.2.0" version = "0.3.1"
authors = ["Librespot Org"] authors = ["Librespot Org"]
license = "MIT" license = "MIT"
description = "An open source client library for Spotify, with support for Spotify Connect" description = "An open source client library for Spotify, with support for Spotify Connect"
@ -22,35 +22,35 @@ doc = false
[dependencies.librespot-audio] [dependencies.librespot-audio]
path = "audio" path = "audio"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-connect] [dependencies.librespot-connect]
path = "connect" path = "connect"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-core] [dependencies.librespot-core]
path = "core" path = "core"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-discovery] [dependencies.librespot-discovery]
path = "discovery" path = "discovery"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-metadata] [dependencies.librespot-metadata]
path = "metadata" path = "metadata"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-playback] [dependencies.librespot-playback]
path = "playback" path = "playback"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-protocol] [dependencies.librespot-protocol]
path = "protocol" path = "protocol"
version = "0.2.0" version = "0.3.1"
[dependencies] [dependencies]
base64 = "0.13" base64 = "0.13"
env_logger = {version = "0.8", default-features = false, features = ["termcolor","humantime","atty"]} env_logger = { version = "0.8", default-features = false, features = ["termcolor", "humantime", "atty"] }
futures-util = { version = "0.3", default_features = false } futures-util = { version = "0.3", default_features = false }
getopts = "0.2.21" getopts = "0.2.21"
hex = "0.4" hex = "0.4"
@ -58,7 +58,7 @@ hyper = "0.14"
log = "0.4" log = "0.4"
rpassword = "5.0" rpassword = "5.0"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "signal", "sync", "process"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "signal", "sync", "parking_lot", "process"] }
url = "2.2" url = "2.2"
sha-1 = "0.9" sha-1 = "0.9"
@ -72,7 +72,7 @@ rodiojack-backend = ["librespot-playback/rodiojack-backend"]
sdl-backend = ["librespot-playback/sdl-backend"] sdl-backend = ["librespot-playback/sdl-backend"]
gstreamer-backend = ["librespot-playback/gstreamer-backend"] gstreamer-backend = ["librespot-playback/gstreamer-backend"]
with-dns-sd = ["librespot-discovery/with-dns-sd"] with-dns-sd = ["librespot-core/with-dns-sd", "librespot-discovery/with-dns-sd"]
default = ["rodio-backend"] default = ["rodio-backend"]

View file

@ -2,17 +2,17 @@
[![Gitter chat](https://badges.gitter.im/librespot-org/librespot.png)](https://gitter.im/librespot-org/spotify-connect-resources) [![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) [![Crates.io](https://img.shields.io/crates/v/librespot.svg)](https://crates.io/crates/librespot)
Current maintainer is [@awiouy](https://github.com/awiouy) folks. Current maintainers are [listed on GitHub](https://github.com/orgs/librespot-org/people).
# librespot # librespot
*librespot* is an open source client library for Spotify. It enables applications to use Spotify's service to control and play music via various backends, and to act as a Spotify Connect receiver. It is an alternative to the official and [now deprecated](https://pyspotify.mopidy.com/en/latest/#libspotify-s-deprecation) closed-source `libspotify`. Additionally, it will provide extra features which are not available in the official library. *librespot* is an open source client library for Spotify. It enables applications to use Spotify's service to control and play music via various backends, and to act as a Spotify Connect receiver. It is an alternative to the official and [now deprecated](https://pyspotify.mopidy.com/en/latest/#libspotify-s-deprecation) closed-source `libspotify`. Additionally, it will provide extra features which are not available in the official library.
_Note: librespot only works with Spotify Premium. This will remain the case for the foreseeable future, as we are unlikely to work on implementing the features such as limited skips and adverts that would be required to make librespot compliant with free accounts._ _Note: librespot only works with Spotify Premium. This will remain the case. We will not support any features to make librespot compatible with free accounts, such as limited skips and adverts._
## Quick start ## Quick start
We're available on [crates.io](https://crates.io/crates/librespot) as the _librespot_ package. Simply run `cargo install librespot` to install librespot on your system. Check the wiki for more info and possible [usage options](https://github.com/librespot-org/librespot/wiki/Options). We're available on [crates.io](https://crates.io/crates/librespot) as the _librespot_ package. Simply run `cargo install librespot` to install librespot on your system. Check the wiki for more info and possible [usage options](https://github.com/librespot-org/librespot/wiki/Options).
After installation, you can run librespot from the CLI using a command such as `librespot -n "Librespot Speaker" -b 160` to create a speaker called _Librespot Speaker_ serving 160kbps audio. After installation, you can run librespot from the CLI using a command such as `librespot -n "Librespot Speaker" -b 160` to create a speaker called _Librespot Speaker_ serving 160 kbps audio.
## This fork ## This fork
As the origin by [plietar](https://github.com/plietar/) is no longer actively maintained, this organisation and repository have been set up so that the project may be maintained and upgraded in the future. As the origin by [plietar](https://github.com/plietar/) is no longer actively maintained, this organisation and repository have been set up so that the project may be maintained and upgraded in the future.
@ -20,7 +20,7 @@ As the origin by [plietar](https://github.com/plietar/) is no longer actively ma
# Documentation # Documentation
Documentation is currently a work in progress, contributions are welcome! 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). [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. [CONTRIBUTING.md](https://github.com/librespot-org/librespot/blob/master/CONTRIBUTING.md) also contains our contributing guidelines.
@ -30,37 +30,39 @@ If you wish to learn more about how librespot works overall, the best way is to
# Issues & Discussions # Issues & Discussions
**We have recently started using Github discussions for general questions and feature requests, as they are a more natural medium for such cases, and allow for upvoting to prioritize feature development. Check them out [here](https://github.com/librespot-org/librespot/discussions). Bugs and issues with the underlying library should still be reported as issues.** **We have recently started using Github discussions for general questions and feature requests, as they are a more natural medium for such cases, and allow for upvoting to prioritize feature development. Check them out [here](https://github.com/librespot-org/librespot/discussions). Bugs and issues with the underlying library should still be reported as issues.**
If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, eg. the Spotify URI of the song that caused the crash. 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, e.g. the Spotify URI of the song that caused the crash.
# Building # 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 walkthrough of the build process is outlined below, while a detailed compilation guide can be found [here](https://github.com/librespot-org/librespot/blob/master/COMPILING.md).
## Additional Dependencies ## 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). 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).
For Linux, you will need to run the additional commands below, depending on your distro. For Linux, you will need to run the additional commands below, depending on your distro.
On Debian/Ubuntu, the following command will install these dependencies : On Debian/Ubuntu, the following command will install these dependencies:
```shell ```shell
sudo apt-get install build-essential libasound2-dev sudo apt-get install build-essential libasound2-dev
``` ```
On Fedora systems, the following command will install these dependencies : On Fedora systems, the following command will install these dependencies:
```shell ```shell
sudo dnf install alsa-lib-devel make gcc sudo dnf install alsa-lib-devel make gcc
``` ```
librespot currently offers the following selection of [audio backends](https://github.com/librespot-org/librespot/wiki/Audio-Backends). librespot currently offers the following selection of [audio backends](https://github.com/librespot-org/librespot/wiki/Audio-Backends):
``` ```
Rodio (default) Rodio (default)
ALSA ALSA
GStreamer
PortAudio PortAudio
PulseAudio PulseAudio
JACK JACK
JACK over Rodio JACK over Rodio
SDL SDL
Pipe Pipe
Subprocess
``` ```
Please check the corresponding [compiling entry](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) for backend specific dependencies. Please check the corresponding [Compiling](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) entry on the wiki for backend specific dependencies.
Once you've installed the dependencies and cloned this repository you can build *librespot* with the default backend using Cargo. Once you've installed the dependencies and cloned this repository you can build *librespot* with the default backend using Cargo.
```shell ```shell
@ -84,14 +86,14 @@ The above is a minimal example. Here is a more fully fledged one:
```shell ```shell
target/release/librespot -n "Librespot" -b 320 -c ./cache --enable-volume-normalisation --initial-volume 75 --device-type avr target/release/librespot -n "Librespot" -b 320 -c ./cache --enable-volume-normalisation --initial-volume 75 --device-type avr
``` ```
The above command will create a receiver named ```Librespot```, with bitrate set to 320kbps, initial volume at 75%, with volume normalisation enabled, and the device displayed in the app as an Audio/Video Receiver. A folder named ```cache``` will be created/used in the current directory, and be used to cache audio data and credentials. The above command will create a receiver named ```Librespot```, with bitrate set to 320 kbps, initial volume at 75%, with volume normalisation enabled, and the device displayed in the app as an Audio/Video Receiver. A folder named ```cache``` will be created/used in the current directory, and be used to cache audio data and credentials.
A full list of runtime options are available [here](https://github.com/librespot-org/librespot/wiki/Options) A full list of runtime options is 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`._ _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 ## Contact
Come and hang out on gitter if you need help or want to offer some. Come and hang out on gitter if you need help or want to offer some:
https://gitter.im/librespot-org/spotify-connect-resources https://gitter.im/librespot-org/spotify-connect-resources
## Disclaimer ## Disclaimer
@ -114,3 +116,4 @@ functionality.
- [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot. - [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot.
- [ncspot](https://github.com/hrkfdn/ncspot) - Cross-platform ncurses Spotify client. - [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. - [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot.
- [Spot](https://github.com/xou816/spot) - Gtk/Rust native Spotify client for the GNOME desktop.

View file

@ -1,20 +1,25 @@
[package] [package]
name = "librespot-audio" name = "librespot-audio"
version = "0.2.0" version = "0.3.1"
authors = ["Paul Lietar <paul@lietar.net>"] authors = ["Paul Lietar <paul@lietar.net>"]
description="The audio fetching and processing logic for librespot" description="The audio fetching and processing logic for librespot"
license="MIT" license = "MIT"
edition = "2018" edition = "2018"
[dependencies.librespot-core] [dependencies.librespot-core]
path = "../core" path = "../core"
version = "0.2.0" version = "0.3.1"
[dependencies] [dependencies]
aes-ctr = "0.6" aes-ctr = "0.6"
byteorder = "1.4" byteorder = "1.4"
bytes = "1.0" bytes = "1.0"
log = "0.4" futures-core = { version = "0.3", default-features = false }
futures-executor = "0.3"
futures-util = { version = "0.3", default_features = false } futures-util = { version = "0.3", default_features = false }
hyper = { version = "0.14", features = ["client"] }
log = "0.4"
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
tempfile = "3.1" tempfile = "3.1"
tokio = { version = "1", features = ["sync", "macros"] } thiserror = "1.0"
tokio = { version = "1", features = ["macros", "parking_lot", "sync"] }

View file

@ -1,8 +1,11 @@
use std::io; use std::io;
use aes_ctr::cipher::generic_array::GenericArray; use aes_ctr::{
use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek}; cipher::{
use aes_ctr::Aes128Ctr; generic_array::GenericArray, NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek,
},
Aes128Ctr,
};
use librespot_core::audio_key::AudioKey; use librespot_core::audio_key::AudioKey;
@ -18,8 +21,8 @@ pub struct AudioDecrypt<T: io::Read> {
impl<T: io::Read> AudioDecrypt<T> { impl<T: io::Read> AudioDecrypt<T> {
pub fn new(key: AudioKey, reader: T) -> AudioDecrypt<T> { pub fn new(key: AudioKey, reader: T) -> AudioDecrypt<T> {
let cipher = Aes128Ctr::new( let cipher = Aes128Ctr::new(
&GenericArray::from_slice(&key.0), GenericArray::from_slice(&key.0),
&GenericArray::from_slice(&AUDIO_AESIV), GenericArray::from_slice(&AUDIO_AESIV),
); );
AudioDecrypt { cipher, reader } AudioDecrypt { cipher, reader }
} }

View file

@ -1,40 +1,77 @@
mod receive; mod receive;
use std::cmp::{max, min}; use std::{
use std::fs; cmp::{max, min},
use std::io::{self, Read, Seek, SeekFrom}; fs,
use std::sync::atomic::{self, AtomicUsize}; io::{self, Read, Seek, SeekFrom},
use std::sync::{Arc, Condvar, Mutex}; sync::{
use std::time::{Duration, Instant}; atomic::{self, AtomicUsize},
Arc,
},
time::{Duration, Instant},
};
use byteorder::{BigEndian, ByteOrder}; use futures_util::{future::IntoStream, StreamExt, TryFutureExt};
use futures_util::{future, StreamExt, TryFutureExt, TryStreamExt}; use hyper::{client::ResponseFuture, header::CONTENT_RANGE, Body, Response, StatusCode};
use librespot_core::channel::{ChannelData, ChannelError, ChannelHeaders}; use parking_lot::{Condvar, Mutex};
use librespot_core::session::Session;
use librespot_core::spotify_id::FileId;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use thiserror::Error;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use self::receive::{audio_file_fetch, request_range}; use librespot_core::{cdn_url::CdnUrl, Error, FileId, Session};
use self::receive::audio_file_fetch;
use crate::range_set::{Range, RangeSet}; use crate::range_set::{Range, RangeSet};
pub type AudioFileResult = Result<(), librespot_core::Error>;
#[derive(Error, Debug)]
pub enum AudioFileError {
#[error("other end of channel disconnected")]
Channel,
#[error("required header not found")]
Header,
#[error("streamer received no data")]
NoData,
#[error("no output available")]
Output,
#[error("invalid status code {0}")]
StatusCode(StatusCode),
#[error("wait timeout exceeded")]
WaitTimeout,
}
impl From<AudioFileError> for Error {
fn from(err: AudioFileError) -> Self {
match err {
AudioFileError::Channel => Error::aborted(err),
AudioFileError::Header => Error::unavailable(err),
AudioFileError::NoData => Error::unavailable(err),
AudioFileError::Output => Error::aborted(err),
AudioFileError::StatusCode(_) => Error::failed_precondition(err),
AudioFileError::WaitTimeout => Error::deadline_exceeded(err),
}
}
}
/// The minimum size of a block that is requested from the Spotify servers in one request. /// 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. /// 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. /// Note: smaller requests can happen if part of the block is downloaded already.
const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 16; pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128;
/// The amount of data that is requested when initially opening a file. /// 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 /// 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 /// 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. /// another position, then only this amount is requested on the first request.
const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 16; pub const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 128;
/// The ping time that is used for calculations before a ping time was actually measured. /// The ping time that is used for calculations before a ping time was actually measured.
const INITIAL_PING_TIME_ESTIMATE: Duration = Duration::from_millis(500); pub const INITIAL_PING_TIME_ESTIMATE: Duration = Duration::from_millis(500);
/// If the measured ping time to the Spotify server is larger than this value, it is capped /// 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. /// to avoid run-away block sizes and pre-fetching.
const MAXIMUM_ASSUMED_PING_TIME: Duration = Duration::from_millis(1500); pub const MAXIMUM_ASSUMED_PING_TIME: Duration = Duration::from_millis(1500);
/// Before playback starts, this many seconds of data must be present. /// 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 /// Note: the calculations are done using the nominal bitrate of the file. The actual amount
@ -63,7 +100,7 @@ pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f32 = 10.0;
/// If the amount of data that is pending (requested but not received) is less than a certain amount, /// 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 pre-fetched in addition to the read ahead settings above. The threshold for requesting more
/// data is calculated as `<pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>` /// data is calculated as `<pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>`
const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0; pub const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0;
/// Similar to `PREFETCH_THRESHOLD_FACTOR`, but it also takes the current download rate into account. /// Similar to `PREFETCH_THRESHOLD_FACTOR`, but it also takes the current download rate into account.
/// The formula used is `<pending bytes> < FAST_PREFETCH_THRESHOLD_FACTOR * <ping time> * <measured download rate>` /// The formula used is `<pending bytes> < FAST_PREFETCH_THRESHOLD_FACTOR * <ping time> * <measured download rate>`
@ -72,16 +109,16 @@ const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0;
/// the download rate ramps up. However, this comes at the cost that it might hurt ping time if a seek is /// 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.0` cause the download rate to collapse and effectively /// performed while downloading. Values smaller than `1.0` cause the download rate to collapse and effectively
/// only `PREFETCH_THRESHOLD_FACTOR` is in effect. Thus, set to `0.0` if bandwidth saturation is not wanted. /// only `PREFETCH_THRESHOLD_FACTOR` is in effect. Thus, set to `0.0` if bandwidth saturation is not wanted.
const FAST_PREFETCH_THRESHOLD_FACTOR: f32 = 1.5; pub const FAST_PREFETCH_THRESHOLD_FACTOR: f32 = 1.5;
/// Limit the number of requests that are pending simultaneously before pre-fetching data. Pending /// 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 /// requests share bandwidth. Thus, having 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 /// 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. /// pre-fetch request is only sent if less than `MAX_PREFETCH_REQUESTS` are pending.
const MAX_PREFETCH_REQUESTS: usize = 4; pub const MAX_PREFETCH_REQUESTS: usize = 4;
/// The time we will wait to obtain status updates on downloading. /// The time we will wait to obtain status updates on downloading.
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(1); pub const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(1);
pub enum AudioFile { pub enum AudioFile {
Cached(fs::File), Cached(fs::File),
@ -89,7 +126,16 @@ pub enum AudioFile {
} }
#[derive(Debug)] #[derive(Debug)]
enum StreamLoaderCommand { pub struct StreamingRequest {
streamer: IntoStream<ResponseFuture>,
initial_response: Option<Response<Body>>,
offset: usize,
length: usize,
request_time: Instant,
}
#[derive(Debug)]
pub enum StreamLoaderCommand {
Fetch(Range), // signal the stream loader to fetch a range of the file Fetch(Range), // signal the stream loader to fetch a range of the file
RandomAccessMode(), // optimise download strategy for random access RandomAccessMode(), // optimise download strategy for random access
StreamMode(), // optimise download strategy for streaming StreamMode(), // optimise download strategy for streaming
@ -113,22 +159,28 @@ impl StreamLoaderController {
} }
pub fn range_available(&self, range: Range) -> bool { pub fn range_available(&self, range: Range) -> bool {
if let Some(ref shared) = self.stream_shared { let available = if let Some(ref shared) = self.stream_shared {
let download_status = shared.download_status.lock().unwrap(); let download_status = shared.download_status.lock();
range.length range.length
<= download_status <= download_status
.downloaded .downloaded
.contained_length_from_value(range.start) .contained_length_from_value(range.start)
} else { } else {
range.length <= self.len() - range.start range.length <= self.len() - range.start
} };
available
} }
pub fn range_to_end_available(&self) -> bool { pub fn range_to_end_available(&self) -> bool {
self.stream_shared.as_ref().map_or(true, |shared| { match self.stream_shared {
Some(ref shared) => {
let read_position = shared.read_position.load(atomic::Ordering::Relaxed); let read_position = shared.read_position.load(atomic::Ordering::Relaxed);
self.range_available(Range::new(read_position, self.len() - read_position)) self.range_available(Range::new(read_position, self.len() - read_position))
}) }
None => true,
}
} }
pub fn ping_time(&self) -> Duration { pub fn ping_time(&self) -> Duration {
@ -139,7 +191,8 @@ impl StreamLoaderController {
fn send_stream_loader_command(&self, command: StreamLoaderCommand) { fn send_stream_loader_command(&self, command: StreamLoaderCommand) {
if let Some(ref channel) = self.channel_tx { if let Some(ref channel) = self.channel_tx {
// ignore the error in case the channel has been closed already. // Ignore the error in case the channel has been closed already.
// This means that the file was completely downloaded.
let _ = channel.send(command); let _ = channel.send(command);
} }
} }
@ -149,7 +202,7 @@ impl StreamLoaderController {
self.send_stream_loader_command(StreamLoaderCommand::Fetch(range)); self.send_stream_loader_command(StreamLoaderCommand::Fetch(range));
} }
pub fn fetch_blocking(&self, mut range: Range) { pub fn fetch_blocking(&self, mut range: Range) -> AudioFileResult {
// signal the stream loader to tech a range of the file and block until it is loaded. // 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. // ensure the range is within the file's bounds.
@ -162,17 +215,21 @@ impl StreamLoaderController {
self.fetch(range); self.fetch(range);
if let Some(ref shared) = self.stream_shared { if let Some(ref shared) = self.stream_shared {
let mut download_status = shared.download_status.lock().unwrap(); let mut download_status = shared.download_status.lock();
while range.length while range.length
> download_status > download_status
.downloaded .downloaded
.contained_length_from_value(range.start) .contained_length_from_value(range.start)
{ {
download_status = shared if shared
.cond .cond
.wait_timeout(download_status, DOWNLOAD_TIMEOUT) .wait_for(&mut download_status, DOWNLOAD_TIMEOUT)
.unwrap() .timed_out()
.0; {
return Err(AudioFileError::WaitTimeout.into());
}
if range.length if range.length
> (download_status > (download_status
.downloaded .downloaded
@ -185,6 +242,8 @@ impl StreamLoaderController {
} }
} }
} }
Ok(())
} }
pub fn fetch_next(&self, length: usize) { pub fn fetch_next(&self, length: usize) {
@ -193,17 +252,20 @@ impl StreamLoaderController {
start: shared.read_position.load(atomic::Ordering::Relaxed), start: shared.read_position.load(atomic::Ordering::Relaxed),
length, length,
}; };
self.fetch(range) self.fetch(range);
} }
} }
pub fn fetch_next_blocking(&self, length: usize) { pub fn fetch_next_blocking(&self, length: usize) -> AudioFileResult {
if let Some(ref shared) = self.stream_shared { match self.stream_shared {
Some(ref shared) => {
let range = Range { let range = Range {
start: shared.read_position.load(atomic::Ordering::Relaxed), start: shared.read_position.load(atomic::Ordering::Relaxed),
length, length,
}; };
self.fetch_blocking(range); self.fetch_blocking(range)
}
None => Ok(()),
} }
} }
@ -242,9 +304,9 @@ enum DownloadStrategy {
} }
struct AudioFileShared { struct AudioFileShared {
file_id: FileId, cdn_url: CdnUrl,
file_size: usize, file_size: usize,
stream_data_rate: usize, bytes_per_second: usize,
cond: Condvar, cond: Condvar,
download_status: Mutex<AudioFileDownloadStatus>, download_status: Mutex<AudioFileDownloadStatus>,
download_strategy: Mutex<DownloadStrategy>, download_strategy: Mutex<DownloadStrategy>,
@ -259,7 +321,7 @@ impl AudioFile {
file_id: FileId, file_id: FileId,
bytes_per_second: usize, bytes_per_second: usize,
play_from_beginning: bool, play_from_beginning: bool,
) -> Result<AudioFile, ChannelError> { ) -> Result<AudioFile, Error> {
if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) { if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) {
debug!("File {} already in cache", file_id); debug!("File {} already in cache", file_id);
return Ok(AudioFile::Cached(file)); return Ok(AudioFile::Cached(file));
@ -268,48 +330,35 @@ impl AudioFile {
debug!("Downloading file {}", file_id); debug!("Downloading file {}", file_id);
let (complete_tx, complete_rx) = oneshot::channel(); let (complete_tx, complete_rx) = oneshot::channel();
let mut initial_data_length = if play_from_beginning {
INITIAL_DOWNLOAD_SIZE
+ max(
(READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize,
(INITIAL_PING_TIME_ESTIMATE.as_secs_f32()
* READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
* bytes_per_second as f32) 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( let streaming = AudioFileStreaming::open(
session.clone(), session.clone(),
data,
initial_data_length,
Instant::now(),
headers,
file_id, file_id,
complete_tx, complete_tx,
bytes_per_second, bytes_per_second,
play_from_beginning,
); );
let session_ = session.clone(); let session_ = session.clone();
session.spawn(complete_rx.map_ok(move |mut file| { session.spawn(complete_rx.map_ok(move |mut file| {
debug!("Downloading file {} complete", file_id);
if let Some(cache) = session_.cache() { if let Some(cache) = session_.cache() {
debug!("File {} complete, saving to cache", file_id); if let Some(cache_id) = cache.file(file_id) {
cache.save_file(file_id, &mut file); if let Err(e) = cache.save_file(file_id, &mut file) {
error!("Error caching file {} to {:?}: {}", file_id, cache_id, e);
} else { } else {
debug!("File {} complete", file_id); debug!("File {} cached to {:?}", file_id, cache_id);
}
}
} }
})); }));
Ok(AudioFile::Streaming(streaming.await?)) Ok(AudioFile::Streaming(streaming.await?))
} }
pub fn get_stream_loader_controller(&self) -> StreamLoaderController { pub fn get_stream_loader_controller(&self) -> Result<StreamLoaderController, Error> {
match self { let controller = match self {
AudioFile::Streaming(ref stream) => StreamLoaderController { AudioFile::Streaming(ref stream) => StreamLoaderController {
channel_tx: Some(stream.stream_loader_command_tx.clone()), channel_tx: Some(stream.stream_loader_command_tx.clone()),
stream_shared: Some(stream.shared.clone()), stream_shared: Some(stream.shared.clone()),
@ -318,9 +367,11 @@ impl AudioFile {
AudioFile::Cached(ref file) => StreamLoaderController { AudioFile::Cached(ref file) => StreamLoaderController {
channel_tx: None, channel_tx: None,
stream_shared: None, stream_shared: None,
file_size: file.metadata().unwrap().len() as usize, file_size: file.metadata()?.len() as usize,
}, },
} };
Ok(controller)
} }
pub fn is_cached(&self) -> bool { pub fn is_cached(&self) -> bool {
@ -331,53 +382,80 @@ impl AudioFile {
impl AudioFileStreaming { impl AudioFileStreaming {
pub async fn open( pub async fn open(
session: Session, session: Session,
initial_data_rx: ChannelData,
initial_data_length: usize,
initial_request_sent_time: Instant,
headers: ChannelHeaders,
file_id: FileId, file_id: FileId,
complete_tx: oneshot::Sender<NamedTempFile>, complete_tx: oneshot::Sender<NamedTempFile>,
streaming_data_rate: usize, bytes_per_second: usize,
) -> Result<AudioFileStreaming, ChannelError> { play_from_beginning: bool,
let (_, data) = headers ) -> Result<AudioFileStreaming, Error> {
.try_filter(|(id, _)| future::ready(*id == 0x3)) let download_size = if play_from_beginning {
.next() INITIAL_DOWNLOAD_SIZE
.await + max(
.unwrap()?; (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize,
(INITIAL_PING_TIME_ESTIMATE.as_secs_f32()
* READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
* bytes_per_second as f32) as usize,
)
} else {
INITIAL_DOWNLOAD_SIZE
};
let size = BigEndian::read_u32(&data) as usize * 4; let cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?;
if let Ok(url) = cdn_url.try_get_url() {
trace!("Streaming from {}", url);
}
let mut streamer = session
.spclient()
.stream_from_cdn(&cdn_url, 0, download_size)?;
let request_time = Instant::now();
// Get the first chunk with the headers to get the file size.
// The remainder of that chunk with possibly also a response body is then
// further processed in `audio_file_fetch`.
let response = streamer.next().await.ok_or(AudioFileError::NoData)??;
let header_value = response
.headers()
.get(CONTENT_RANGE)
.ok_or(AudioFileError::Header)?;
let str_value = header_value.to_str()?;
let file_size_str = str_value.split('/').last().unwrap_or_default();
let file_size = file_size_str.parse()?;
let initial_request = StreamingRequest {
streamer,
initial_response: Some(response),
offset: 0,
length: download_size,
request_time,
};
let shared = Arc::new(AudioFileShared { let shared = Arc::new(AudioFileShared {
file_id, cdn_url,
file_size: size, file_size,
stream_data_rate: streaming_data_rate, bytes_per_second,
cond: Condvar::new(), cond: Condvar::new(),
download_status: Mutex::new(AudioFileDownloadStatus { download_status: Mutex::new(AudioFileDownloadStatus {
requested: RangeSet::new(), requested: RangeSet::new(),
downloaded: RangeSet::new(), downloaded: RangeSet::new(),
}), }),
download_strategy: Mutex::new(DownloadStrategy::RandomAccess()), // start with random access mode until someone tells us otherwise download_strategy: Mutex::new(DownloadStrategy::Streaming()),
number_of_open_requests: AtomicUsize::new(0), number_of_open_requests: AtomicUsize::new(0),
ping_time_ms: AtomicUsize::new(0), ping_time_ms: AtomicUsize::new(0),
read_position: AtomicUsize::new(0), read_position: AtomicUsize::new(0),
}); });
let mut write_file = NamedTempFile::new().unwrap(); let write_file = NamedTempFile::new_in(session.config().tmp_dir.clone())?;
write_file.as_file().set_len(size as u64).unwrap(); let read_file = write_file.reopen()?;
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) = let (stream_loader_command_tx, stream_loader_command_rx) =
mpsc::unbounded_channel::<StreamLoaderCommand>(); mpsc::unbounded_channel::<StreamLoaderCommand>();
session.spawn(audio_file_fetch( session.spawn(audio_file_fetch(
session.clone(), session.clone(),
shared.clone(), shared.clone(),
initial_data_rx, initial_request,
initial_request_sent_time,
initial_data_length,
write_file, write_file,
stream_loader_command_rx, stream_loader_command_rx,
complete_tx, complete_tx,
@ -402,7 +480,7 @@ impl Read for AudioFileStreaming {
let length = min(output.len(), self.shared.file_size - offset); let length = min(output.len(), self.shared.file_size - offset);
let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) { let length_to_request = match *(self.shared.download_strategy.lock()) {
DownloadStrategy::RandomAccess() => length, DownloadStrategy::RandomAccess() => length,
DownloadStrategy::Streaming() => { DownloadStrategy::Streaming() => {
// Due to the read-ahead stuff, we potentially request more than the actual request demanded. // Due to the read-ahead stuff, we potentially request more than the actual request demanded.
@ -414,10 +492,10 @@ impl Read for AudioFileStreaming {
let length_to_request = length let length_to_request = length
+ max( + max(
(READ_AHEAD_DURING_PLAYBACK.as_secs_f32() (READ_AHEAD_DURING_PLAYBACK.as_secs_f32()
* self.shared.stream_data_rate as f32) as usize, * self.shared.bytes_per_second as f32) as usize,
(READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
* ping_time_seconds * ping_time_seconds
* self.shared.stream_data_rate as f32) as usize, * self.shared.bytes_per_second as f32) as usize,
); );
min(length_to_request, self.shared.file_size - offset) min(length_to_request, self.shared.file_size - offset)
} }
@ -426,34 +504,33 @@ impl Read for AudioFileStreaming {
let mut ranges_to_request = RangeSet::new(); let mut ranges_to_request = RangeSet::new();
ranges_to_request.add_range(&Range::new(offset, length_to_request)); ranges_to_request.add_range(&Range::new(offset, length_to_request));
let mut download_status = self.shared.download_status.lock().unwrap(); let mut download_status = self.shared.download_status.lock();
ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.downloaded);
ranges_to_request.subtract_range_set(&download_status.requested); ranges_to_request.subtract_range_set(&download_status.requested);
for &range in ranges_to_request.iter() { for &range in ranges_to_request.iter() {
self.stream_loader_command_tx self.stream_loader_command_tx
.send(StreamLoaderCommand::Fetch(range)) .send(StreamLoaderCommand::Fetch(range))
.unwrap(); .map_err(|err| io::Error::new(io::ErrorKind::BrokenPipe, err))?;
} }
if length == 0 { if length == 0 {
return Ok(0); return Ok(0);
} }
let mut download_message_printed = false;
while !download_status.downloaded.contains(offset) { while !download_status.downloaded.contains(offset) {
if let DownloadStrategy::Streaming() = *self.shared.download_strategy.lock().unwrap() { if self
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 .shared
.cond .cond
.wait_timeout(download_status, DOWNLOAD_TIMEOUT) .wait_for(&mut download_status, DOWNLOAD_TIMEOUT)
.unwrap() .timed_out()
.0; {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
Error::deadline_exceeded(AudioFileError::WaitTimeout),
));
}
} }
let available_length = download_status let available_length = download_status
.downloaded .downloaded
@ -461,19 +538,10 @@ impl Read for AudioFileStreaming {
assert!(available_length > 0); assert!(available_length > 0);
drop(download_status); drop(download_status);
self.position = self.read_file.seek(SeekFrom::Start(offset as u64)).unwrap(); self.position = self.read_file.seek(SeekFrom::Start(offset as u64))?;
let read_len = min(length, available_length); let read_len = min(length, available_length);
let read_len = self.read_file.read(&mut output[..read_len])?; 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.position += read_len as u64;
self.shared self.shared
.read_position .read_position

View file

@ -1,58 +1,27 @@
use std::cmp::{max, min}; use std::{
use std::io::{Seek, SeekFrom, Write}; cmp::{max, min},
use std::sync::{atomic, Arc}; io::{Seek, SeekFrom, Write},
use std::time::{Duration, Instant}; sync::{atomic, Arc},
time::{Duration, Instant},
};
use atomic::Ordering; use atomic::Ordering;
use byteorder::{BigEndian, WriteBytesExt};
use bytes::Bytes; use bytes::Bytes;
use futures_util::StreamExt; use futures_util::StreamExt;
use librespot_core::channel::{Channel, ChannelData}; use hyper::StatusCode;
use librespot_core::packet::PacketType;
use librespot_core::session::Session;
use librespot_core::spotify_id::FileId;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use librespot_core::{session::Session, Error};
use crate::range_set::{Range, RangeSet}; use crate::range_set::{Range, RangeSet};
use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand};
use super::{ use super::{
FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, AudioFileError, AudioFileResult, AudioFileShared, DownloadStrategy, StreamLoaderCommand,
MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, StreamingRequest, FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME,
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<u8> = Vec::new();
data.write_u16::<BigEndian>(id).unwrap();
data.write_u8(0).unwrap();
data.write_u8(1).unwrap();
data.write_u16::<BigEndian>(0x0000).unwrap();
data.write_u32::<BigEndian>(0x00000000).unwrap();
data.write_u32::<BigEndian>(0x00009C40).unwrap();
data.write_u32::<BigEndian>(0x00020000).unwrap();
data.write(&file.0).unwrap();
data.write_u32::<BigEndian>(start as u32).unwrap();
data.write_u32::<BigEndian>(end as u32).unwrap();
session.send_packet(PacketType::StreamChunk, data);
channel
}
struct PartialFileData { struct PartialFileData {
offset: usize, offset: usize,
data: Bytes, data: Bytes,
@ -66,13 +35,15 @@ enum ReceivedData {
async fn receive_data( async fn receive_data(
shared: Arc<AudioFileShared>, shared: Arc<AudioFileShared>,
file_data_tx: mpsc::UnboundedSender<ReceivedData>, file_data_tx: mpsc::UnboundedSender<ReceivedData>,
mut data_rx: ChannelData, mut request: StreamingRequest,
initial_data_offset: usize, ) -> AudioFileResult {
initial_request_length: usize, let requested_offset = request.offset;
request_sent_time: Instant, let requested_length = request.length;
) {
let mut data_offset = initial_data_offset; let mut data_offset = requested_offset;
let mut request_length = initial_request_length; let mut request_length = requested_length;
// TODO : check Content-Length and Content-Range headers
let old_number_of_request = shared let old_number_of_request = shared
.number_of_open_requests .number_of_open_requests
@ -80,31 +51,49 @@ async fn receive_data(
let mut measure_ping_time = old_number_of_request == 0; let mut measure_ping_time = old_number_of_request == 0;
let result = loop { let result: Result<_, Error> = loop {
let data = match data_rx.next().await { let response = match request.initial_response.take() {
Some(Ok(data)) => data, Some(data) => data,
Some(Err(e)) => break Err(e), None => match request.streamer.next().await {
Some(Ok(response)) => response,
Some(Err(e)) => break Err(e.into()),
None => break Ok(()), None => break Ok(()),
},
};
let code = response.status();
let body = response.into_body();
if code != StatusCode::PARTIAL_CONTENT {
debug!("Streamer expected partial content but got: {}", code);
break Err(AudioFileError::StatusCode(code).into());
}
let data = match hyper::body::to_bytes(body).await {
Ok(bytes) => bytes,
Err(e) => break Err(e.into()),
}; };
if measure_ping_time { if measure_ping_time {
let mut duration = Instant::now() - request_sent_time; let mut duration = Instant::now() - request.request_time;
if duration > MAXIMUM_ASSUMED_PING_TIME { if duration > MAXIMUM_ASSUMED_PING_TIME {
duration = MAXIMUM_ASSUMED_PING_TIME; duration = MAXIMUM_ASSUMED_PING_TIME;
} }
let _ = file_data_tx.send(ReceivedData::ResponseTime(duration)); file_data_tx.send(ReceivedData::ResponseTime(duration))?;
measure_ping_time = false; measure_ping_time = false;
} }
let data_size = data.len(); let data_size = data.len();
let _ = file_data_tx.send(ReceivedData::Data(PartialFileData {
file_data_tx.send(ReceivedData::Data(PartialFileData {
offset: data_offset, offset: data_offset,
data, data,
})); }))?;
data_offset += data_size; data_offset += data_size;
if request_length < data_size { if request_length < data_size {
warn!( warn!(
"Data receiver for range {} (+{}) received more data from server than requested.", "Data receiver for range {} (+{}) received more data from server than requested ({} instead of {}).",
initial_data_offset, initial_request_length requested_offset, requested_length, data_size, request_length
); );
request_length = 0; request_length = 0;
} else { } else {
@ -116,28 +105,38 @@ async fn receive_data(
} }
}; };
if request_length > 0 { drop(request.streamer);
let missing_range = Range::new(data_offset, request_length);
let mut download_status = shared.download_status.lock().unwrap(); if request_length > 0 {
{
let missing_range = Range::new(data_offset, request_length);
let mut download_status = shared.download_status.lock();
download_status.requested.subtract_range(&missing_range); download_status.requested.subtract_range(&missing_range);
shared.cond.notify_all(); shared.cond.notify_all();
} }
}
shared shared
.number_of_open_requests .number_of_open_requests
.fetch_sub(1, Ordering::SeqCst); .fetch_sub(1, Ordering::SeqCst);
if result.is_err() { match result {
Ok(()) => {
if request_length > 0 {
warn!( warn!(
"Error from channel for data receiver for range {} (+{}).", "Streamer for range {} (+{}) received less data from server than requested.",
initial_data_offset, initial_request_length requested_offset, requested_length
); );
} else if request_length > 0 { }
warn!( Ok(())
"Data receiver for range {} (+{}) received less data from server than requested.", }
initial_data_offset, initial_request_length Err(e) => {
error!(
"Error from streamer for range {} (+{}): {:?}",
requested_offset, requested_length, e
); );
Err(e)
}
} }
} }
@ -160,67 +159,63 @@ enum ControlFlow {
impl AudioFileFetch { impl AudioFileFetch {
fn get_download_strategy(&mut self) -> DownloadStrategy { fn get_download_strategy(&mut self) -> DownloadStrategy {
*(self.shared.download_strategy.lock().unwrap()) *(self.shared.download_strategy.lock())
} }
fn download_range(&mut self, mut offset: usize, mut length: usize) { fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult {
if length < MINIMUM_DOWNLOAD_SIZE { if length < MINIMUM_DOWNLOAD_SIZE {
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 { if offset + length > self.shared.file_size {
length = self.shared.file_size - offset; 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(); let mut ranges_to_request = RangeSet::new();
ranges_to_request.add_range(&Range::new(offset, length)); ranges_to_request.add_range(&Range::new(offset, length));
let mut download_status = self.shared.download_status.lock().unwrap(); // The iteration that follows spawns streamers fast, without awaiting them,
// so holding the lock for the entire scope of this function should be faster
// then locking and unlocking multiple times.
let mut download_status = self.shared.download_status.lock();
ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.downloaded);
ranges_to_request.subtract_range_set(&download_status.requested); ranges_to_request.subtract_range_set(&download_status.requested);
// TODO : refresh cdn_url when the token expired
for range in ranges_to_request.iter() { for range in ranges_to_request.iter() {
let (_headers, data) = request_range( let streamer = self.session.spclient().stream_from_cdn(
&self.session, &self.shared.cdn_url,
self.shared.file_id,
range.start, range.start,
range.length, range.length,
) )?;
.split();
download_status.requested.add_range(range); download_status.requested.add_range(range);
let streaming_request = StreamingRequest {
streamer,
initial_response: None,
offset: range.start,
length: range.length,
request_time: Instant::now(),
};
self.session.spawn(receive_data( self.session.spawn(receive_data(
self.shared.clone(), self.shared.clone(),
self.file_data_tx.clone(), self.file_data_tx.clone(),
data, streaming_request,
range.start,
range.length,
Instant::now(),
)); ));
} }
Ok(())
} }
fn pre_fetch_more_data(&mut self, bytes: usize, max_requests_to_send: usize) { fn pre_fetch_more_data(
&mut self,
bytes: usize,
max_requests_to_send: usize,
) -> AudioFileResult {
let mut bytes_to_go = bytes; let mut bytes_to_go = bytes;
let mut requests_to_go = max_requests_to_send; let mut requests_to_go = max_requests_to_send;
@ -229,7 +224,7 @@ impl AudioFileFetch {
let mut missing_data = RangeSet::new(); let mut missing_data = RangeSet::new();
missing_data.add_range(&Range::new(0, self.shared.file_size)); missing_data.add_range(&Range::new(0, self.shared.file_size));
{ {
let download_status = self.shared.download_status.lock().unwrap(); let download_status = self.shared.download_status.lock();
missing_data.subtract_range_set(&download_status.downloaded); missing_data.subtract_range_set(&download_status.downloaded);
missing_data.subtract_range_set(&download_status.requested); missing_data.subtract_range_set(&download_status.requested);
} }
@ -247,7 +242,7 @@ impl AudioFileFetch {
let range = tail_end.get_range(0); let range = tail_end.get_range(0);
let offset = range.start; let offset = range.start;
let length = min(range.length, bytes_to_go); let length = min(range.length, bytes_to_go);
self.download_range(offset, length); self.download_range(offset, length)?;
requests_to_go -= 1; requests_to_go -= 1;
bytes_to_go -= length; bytes_to_go -= length;
} else if !missing_data.is_empty() { } else if !missing_data.is_empty() {
@ -255,19 +250,21 @@ impl AudioFileFetch {
let range = missing_data.get_range(0); let range = missing_data.get_range(0);
let offset = range.start; let offset = range.start;
let length = min(range.length, bytes_to_go); let length = min(range.length, bytes_to_go);
self.download_range(offset, length); self.download_range(offset, length)?;
requests_to_go -= 1; requests_to_go -= 1;
bytes_to_go -= length; bytes_to_go -= length;
} else { } else {
return; break;
}
} }
} }
fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow { Ok(())
}
fn handle_file_data(&mut self, data: ReceivedData) -> Result<ControlFlow, Error> {
match data { match data {
ReceivedData::ResponseTime(response_time) => { ReceivedData::ResponseTime(response_time) => {
trace!("Ping time estimated as: {}ms", response_time.as_millis()); let old_ping_time_ms = self.shared.ping_time_ms.load(Ordering::Relaxed);
// prune old response times. Keep at most two so we can push a third. // prune old response times. Keep at most two so we can push a third.
while self.network_response_times.len() >= 3 { while self.network_response_times.len() >= 3 {
@ -278,7 +275,8 @@ impl AudioFileFetch {
self.network_response_times.push(response_time); self.network_response_times.push(response_time);
// stats::median is experimental. So we calculate the median of up to three ourselves. // stats::median is experimental. So we calculate the median of up to three ourselves.
let ping_time = match self.network_response_times.len() { let ping_time_ms = {
let response_time = match self.network_response_times.len() {
1 => self.network_response_times[0], 1 => self.network_response_times[0],
2 => (self.network_response_times[0] + self.network_response_times[1]) / 2, 2 => (self.network_response_times[0] + self.network_response_times[1]) / 2,
3 => { 3 => {
@ -288,99 +286,118 @@ impl AudioFileFetch {
} }
_ => unreachable!(), _ => unreachable!(),
}; };
response_time.as_millis() as usize
};
// print when the new estimate deviates by more than 10% from the last
if f32::abs(
(ping_time_ms as f32 - old_ping_time_ms as f32) / old_ping_time_ms as f32,
) > 0.1
{
debug!("Ping time now estimated as: {} ms", ping_time_ms);
}
// store our new estimate for everyone to see // store our new estimate for everyone to see
self.shared self.shared
.ping_time_ms .ping_time_ms
.store(ping_time.as_millis() as usize, Ordering::Relaxed); .store(ping_time_ms, Ordering::Relaxed);
} }
ReceivedData::Data(data) => { ReceivedData::Data(data) => {
self.output match self.output.as_mut() {
.as_mut() Some(output) => {
.unwrap() output.seek(SeekFrom::Start(data.offset as u64))?;
.seek(SeekFrom::Start(data.offset as u64)) output.write_all(data.data.as_ref())?;
.unwrap(); }
self.output None => return Err(AudioFileError::Output.into()),
.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()); let received_range = Range::new(data.offset, data.data.len());
let full = {
let mut download_status = self.shared.download_status.lock();
download_status.downloaded.add_range(&received_range); download_status.downloaded.add_range(&received_range);
self.shared.cond.notify_all(); self.shared.cond.notify_all();
let full = download_status.downloaded.contained_length_from_value(0) download_status.downloaded.contained_length_from_value(0)
>= self.shared.file_size; >= self.shared.file_size
};
drop(download_status);
if full { if full {
self.finish(); self.finish()?;
return ControlFlow::Break; return Ok(ControlFlow::Break);
} }
} }
} }
ControlFlow::Continue
}
fn handle_stream_loader_command(&mut self, cmd: StreamLoaderCommand) -> ControlFlow { Ok(ControlFlow::Continue)
}
fn handle_stream_loader_command(
&mut self,
cmd: StreamLoaderCommand,
) -> Result<ControlFlow, Error> {
match cmd { match cmd {
StreamLoaderCommand::Fetch(request) => { StreamLoaderCommand::Fetch(request) => {
self.download_range(request.start, request.length); self.download_range(request.start, request.length)?;
} }
StreamLoaderCommand::RandomAccessMode() => { StreamLoaderCommand::RandomAccessMode() => {
*(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::RandomAccess(); *(self.shared.download_strategy.lock()) = DownloadStrategy::RandomAccess();
} }
StreamLoaderCommand::StreamMode() => { StreamLoaderCommand::StreamMode() => {
*(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::Streaming(); *(self.shared.download_strategy.lock()) = DownloadStrategy::Streaming();
} }
StreamLoaderCommand::Close() => return ControlFlow::Break, StreamLoaderCommand::Close() => return Ok(ControlFlow::Break),
}
ControlFlow::Continue
} }
fn finish(&mut self) { Ok(ControlFlow::Continue)
let mut output = self.output.take().unwrap(); }
let complete_tx = self.complete_tx.take().unwrap();
output.seek(SeekFrom::Start(0)).unwrap(); fn finish(&mut self) -> AudioFileResult {
let _ = complete_tx.send(output); let output = self.output.take();
let complete_tx = self.complete_tx.take();
if let Some(mut output) = output {
output.seek(SeekFrom::Start(0))?;
if let Some(complete_tx) = complete_tx {
complete_tx
.send(output)
.map_err(|_| AudioFileError::Channel)?;
}
}
Ok(())
} }
} }
pub(super) async fn audio_file_fetch( pub(super) async fn audio_file_fetch(
session: Session, session: Session,
shared: Arc<AudioFileShared>, shared: Arc<AudioFileShared>,
initial_data_rx: ChannelData, initial_request: StreamingRequest,
initial_request_sent_time: Instant,
initial_data_length: usize,
output: NamedTempFile, output: NamedTempFile,
mut stream_loader_command_rx: mpsc::UnboundedReceiver<StreamLoaderCommand>, mut stream_loader_command_rx: mpsc::UnboundedReceiver<StreamLoaderCommand>,
complete_tx: oneshot::Sender<NamedTempFile>, complete_tx: oneshot::Sender<NamedTempFile>,
) { ) -> AudioFileResult {
let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel(); let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel();
{ {
let requested_range = Range::new(0, initial_data_length); let requested_range = Range::new(
let mut download_status = shared.download_status.lock().unwrap(); initial_request.offset,
initial_request.offset + initial_request.length,
);
let mut download_status = shared.download_status.lock();
download_status.requested.add_range(&requested_range); download_status.requested.add_range(&requested_range);
} }
session.spawn(receive_data( session.spawn(receive_data(
shared.clone(), shared.clone(),
file_data_tx.clone(), file_data_tx.clone(),
initial_data_rx, initial_request,
0,
initial_data_length,
initial_request_sent_time,
)); ));
let mut fetch = AudioFileFetch { let mut fetch = AudioFileFetch {
session, session: session.clone(),
shared, shared,
output: Some(output), output: Some(output),
@ -392,15 +409,25 @@ pub(super) async fn audio_file_fetch(
loop { loop {
tokio::select! { tokio::select! {
cmd = stream_loader_command_rx.recv() => { cmd = stream_loader_command_rx.recv() => {
if cmd.map_or(true, |cmd| fetch.handle_stream_loader_command(cmd) == ControlFlow::Break) { match cmd {
Some(cmd) => {
if fetch.handle_stream_loader_command(cmd)? == ControlFlow::Break {
break; break;
} }
}, }
None => break,
}
}
data = file_data_rx.recv() => { data = file_data_rx.recv() => {
if data.map_or(true, |data| fetch.handle_file_data(data) == ControlFlow::Break) { match data {
Some(data) => {
if fetch.handle_file_data(data)? == ControlFlow::Break {
break; break;
} }
} }
None => break,
}
}
} }
if fetch.get_download_strategy() == DownloadStrategy::Streaming() { if fetch.get_download_strategy() == DownloadStrategy::Streaming() {
@ -410,7 +437,8 @@ pub(super) async fn audio_file_fetch(
let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests; let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests;
let bytes_pending: usize = { let bytes_pending: usize = {
let download_status = fetch.shared.download_status.lock().unwrap(); let download_status = fetch.shared.download_status.lock();
download_status download_status
.requested .requested
.minus(&download_status.downloaded) .minus(&download_status.downloaded)
@ -425,7 +453,7 @@ pub(super) async fn audio_file_fetch(
let desired_pending_bytes = max( let desired_pending_bytes = max(
(PREFETCH_THRESHOLD_FACTOR (PREFETCH_THRESHOLD_FACTOR
* ping_time_seconds * ping_time_seconds
* fetch.shared.stream_data_rate as f32) as usize, * fetch.shared.bytes_per_second as f32) as usize,
(FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f32) (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f32)
as usize, as usize,
); );
@ -434,9 +462,11 @@ pub(super) async fn audio_file_fetch(
fetch.pre_fetch_more_data( fetch.pre_fetch_more_data(
desired_pending_bytes - bytes_pending, desired_pending_bytes - bytes_pending,
max_requests_to_send, max_requests_to_send,
); )?;
} }
} }
} }
} }
Ok(())
} }

View file

@ -1,5 +1,3 @@
#![allow(clippy::unused_io_amount, clippy::too_many_arguments)]
#[macro_use] #[macro_use]
extern crate log; extern crate log;
@ -9,7 +7,7 @@ mod fetch;
mod range_set; mod range_set;
pub use decrypt::AudioDecrypt; pub use decrypt::AudioDecrypt;
pub use fetch::{AudioFile, StreamLoaderController}; pub use fetch::{AudioFile, AudioFileError, StreamLoaderController};
pub use fetch::{ pub use fetch::{
READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK,
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,

View file

@ -1,6 +1,8 @@
use std::cmp::{max, min}; use std::{
use std::fmt; cmp::{max, min},
use std::slice::Iter; fmt,
slice::Iter,
};
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct Range { pub struct Range {
@ -10,7 +12,7 @@ pub struct Range {
impl fmt::Display for Range { impl fmt::Display for Range {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
return write!(f, "[{}, {}]", self.start, self.start + self.length - 1); write!(f, "[{}, {}]", self.start, self.start + self.length - 1)
} }
} }
@ -24,16 +26,16 @@ impl Range {
} }
} }
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct RangeSet { pub struct RangeSet {
ranges: Vec<Range>, ranges: Vec<Range>,
} }
impl fmt::Display for RangeSet { impl fmt::Display for RangeSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "(").unwrap(); write!(f, "(")?;
for range in self.ranges.iter() { for range in self.ranges.iter() {
write!(f, "{}", range).unwrap(); write!(f, "{}", range)?;
} }
write!(f, ")") write!(f, ")")
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "librespot-connect" name = "librespot-connect"
version = "0.2.0" version = "0.3.1"
authors = ["Paul Lietar <paul@lietar.net>"] authors = ["Paul Lietar <paul@lietar.net>"]
description = "The discovery and Spotify Connect logic for librespot" description = "The discovery and Spotify Connect logic for librespot"
license = "MIT" license = "MIT"
@ -15,24 +15,25 @@ protobuf = "2.14.0"
rand = "0.8" rand = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tokio = { version = "1.0", features = ["macros", "sync"] } thiserror = "1.0"
tokio = { version = "1.0", features = ["macros", "parking_lot", "sync"] }
tokio-stream = "0.1.1" tokio-stream = "0.1.1"
[dependencies.librespot-core] [dependencies.librespot-core]
path = "../core" path = "../core"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-playback] [dependencies.librespot-playback]
path = "../playback" path = "../playback"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-protocol] [dependencies.librespot-protocol]
path = "../protocol" path = "../protocol"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-discovery] [dependencies.librespot-discovery]
path = "../discovery" path = "../discovery"
version = "0.2.0" version = "0.3.1"
[features] [features]
with-dns-sd = ["librespot-discovery/with-dns-sd"] with-dns-sd = ["librespot-discovery/with-dns-sd"]

View file

@ -1,7 +1,12 @@
// TODO : move to metadata
use crate::core::spotify_id::SpotifyId; use crate::core::spotify_id::SpotifyId;
use crate::protocol::spirc::TrackRef; use crate::protocol::spirc::TrackRef;
use serde::Deserialize; use serde::{
de::{Error, Unexpected},
Deserialize,
};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct StationContext { pub struct StationContext {
@ -46,6 +51,7 @@ pub struct TrackContext {
// pub metadata: MetadataContext, // pub metadata: MetadataContext,
} }
#[allow(dead_code)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ArtistContext { pub struct ArtistContext {
@ -54,6 +60,7 @@ pub struct ArtistContext {
image_uri: String, image_uri: String,
} }
#[allow(dead_code)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct MetadataContext { pub struct MetadataContext {
album_title: String, album_title: String,
@ -70,17 +77,23 @@ where
D: serde::Deserializer<'d>, D: serde::Deserializer<'d>,
{ {
let v: Vec<TrackContext> = serde::Deserialize::deserialize(de)?; let v: Vec<TrackContext> = serde::Deserialize::deserialize(de)?;
let track_vec = v v.iter()
.iter()
.map(|v| { .map(|v| {
let mut t = TrackRef::new(); let mut t = TrackRef::new();
// This has got to be the most round about way of doing this. // This has got to be the most round about way of doing this.
t.set_gid(SpotifyId::from_base62(&v.gid).unwrap().to_raw().to_vec()); t.set_gid(
SpotifyId::from_base62(&v.gid)
.map_err(|_| {
D::Error::invalid_value(
Unexpected::Str(&v.gid),
&"a Base-62 encoded Spotify ID",
)
})?
.to_raw()
.to_vec(),
);
t.set_uri(v.uri.to_owned()); t.set_uri(v.uri.to_owned());
Ok(t)
t
}) })
.collect::<Vec<TrackRef>>(); .collect::<Result<Vec<TrackRef>, D::Error>>()
Ok(track_vec)
} }

View file

@ -1,10 +1,11 @@
use std::io; use std::{
use std::pin::Pin; io,
use std::task::{Context, Poll}; pin::Pin,
task::{Context, Poll},
};
use futures_util::Stream; use futures_util::Stream;
use librespot_core::authentication::Credentials; use librespot_core::{authentication::Credentials, config::ConnectConfig};
use librespot_core::config::ConnectConfig;
pub struct DiscoveryStream(librespot_discovery::Discovery); pub struct DiscoveryStream(librespot_discovery::Discovery);

View file

@ -1,27 +1,67 @@
use std::future::Future; use std::{
use std::pin::Pin; convert::TryFrom,
use std::time::{SystemTime, UNIX_EPOCH}; future::Future,
pin::Pin,
time::{SystemTime, UNIX_EPOCH},
};
use crate::context::StationContext; use futures_util::{
use crate::core::config::ConnectConfig; future::{self, FusedFuture},
use crate::core::mercury::{MercuryError, MercurySender}; stream::FusedStream,
use crate::core::session::Session; FutureExt, StreamExt, TryFutureExt,
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 futures_util::future::{self, FusedFuture};
use futures_util::stream::FusedStream;
use futures_util::{FutureExt, StreamExt};
use protobuf::{self, Message}; use protobuf::{self, Message};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use thiserror::Error;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
use crate::{
context::StationContext,
core::{
config::ConnectConfig, // TODO: move to connect?
mercury::{MercuryError, MercurySender},
session::UserAttributes,
util::SeqGenerator,
version,
Error,
Session,
SpotifyId,
},
playback::{
mixer::Mixer,
player::{Player, PlayerEvent, PlayerEventChannel},
},
protocol::{
self,
explicit_content_pubsub::UserAttributesUpdate,
spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef},
user_attributes::UserAttributesMutation,
},
};
#[derive(Debug, Error)]
pub enum SpircError {
#[error("response payload empty")]
NoData,
#[error("message addressed at another ident: {0}")]
Ident(String),
#[error("message pushed for another URI")]
InvalidUri(String),
}
impl From<SpircError> for Error {
fn from(err: SpircError) -> Self {
match err {
SpircError::NoData => Error::unavailable(err),
SpircError::Ident(_) => Error::aborted(err),
SpircError::InvalidUri(_) => Error::aborted(err),
}
}
}
#[derive(Debug)]
enum SpircPlayStatus { enum SpircPlayStatus {
Stopped, Stopped,
LoadingPlay { LoadingPlay {
@ -56,15 +96,18 @@ struct SpircTask {
play_request_id: Option<u64>, play_request_id: Option<u64>,
play_status: SpircPlayStatus, play_status: SpircPlayStatus,
subscription: BoxedStream<Frame>, remote_update: BoxedStream<Result<Frame, Error>>,
connection_id_update: BoxedStream<Result<String, Error>>,
user_attributes_update: BoxedStream<Result<UserAttributesUpdate, Error>>,
user_attributes_mutation: BoxedStream<Result<UserAttributesMutation, Error>>,
sender: MercurySender, sender: MercurySender,
commands: Option<mpsc::UnboundedReceiver<SpircCommand>>, commands: Option<mpsc::UnboundedReceiver<SpircCommand>>,
player_events: Option<PlayerEventChannel>, player_events: Option<PlayerEventChannel>,
shutdown: bool, shutdown: bool,
session: Session, session: Session,
context_fut: BoxedFuture<Result<serde_json::Value, MercuryError>>, context_fut: BoxedFuture<Result<serde_json::Value, Error>>,
autoplay_fut: BoxedFuture<Result<String, MercuryError>>, autoplay_fut: BoxedFuture<Result<String, Error>>,
context: Option<StationContext>, context: Option<StationContext>,
} }
@ -107,7 +150,7 @@ fn initial_state() -> State {
fn initial_device_state(config: ConnectConfig) -> DeviceState { fn initial_device_state(config: ConnectConfig) -> DeviceState {
{ {
let mut msg = DeviceState::new(); let mut msg = DeviceState::new();
msg.set_sw_version(version::VERSION_STRING.to_string()); msg.set_sw_version(version::SEMVER.to_string());
msg.set_is_active(false); msg.set_is_active(false);
msg.set_can_play(true); msg.set_can_play(true);
msg.set_volume(0); msg.set_volume(0);
@ -225,25 +268,67 @@ impl Spirc {
session: Session, session: Session,
player: Player, player: Player,
mixer: Box<dyn Mixer>, mixer: Box<dyn Mixer>,
) -> (Spirc, impl Future<Output = ()>) { ) -> Result<(Spirc, impl Future<Output = ()>), Error> {
debug!("new Spirc[{}]", session.session_id()); debug!("new Spirc[{}]", session.session_id());
let ident = session.device_id().to_owned(); let ident = session.device_id().to_owned();
// Uri updated in response to issue #288 // Uri updated in response to issue #288
debug!("canonical_username: {}", &session.username()); let canonical_username = &session.username();
let uri = format!("hm://remote/user/{}/", url_encode(&session.username())); debug!("canonical_username: {}", canonical_username);
let uri = format!("hm://remote/user/{}/", url_encode(canonical_username));
let subscription = Box::pin( let remote_update = Box::pin(
session session
.mercury() .mercury()
.subscribe(uri.clone()) .subscribe(uri.clone())
.map(Result::unwrap) .inspect_err(|x| error!("remote update error: {}", x))
.and_then(|x| async move { Ok(x) })
.map(Result::unwrap) // guaranteed to be safe by `and_then` above
.map(UnboundedReceiverStream::new) .map(UnboundedReceiverStream::new)
.flatten_stream() .flatten_stream()
.map(|response| -> Frame { .map(|response| -> Result<Frame, Error> {
let data = response.payload.first().unwrap(); let data = response.payload.first().ok_or(SpircError::NoData)?;
Frame::parse_from_bytes(data).unwrap() Ok(Frame::parse_from_bytes(data)?)
}),
);
let connection_id_update = Box::pin(
session
.mercury()
.listen_for("hm://pusher/v1/connections/")
.map(UnboundedReceiverStream::new)
.flatten_stream()
.map(|response| -> Result<String, Error> {
let connection_id = response
.uri
.strip_prefix("hm://pusher/v1/connections/")
.ok_or_else(|| SpircError::InvalidUri(response.uri.clone()))?;
Ok(connection_id.to_owned())
}),
);
let user_attributes_update = Box::pin(
session
.mercury()
.listen_for("spotify:user:attributes:update")
.map(UnboundedReceiverStream::new)
.flatten_stream()
.map(|response| -> Result<UserAttributesUpdate, Error> {
let data = response.payload.first().ok_or(SpircError::NoData)?;
Ok(UserAttributesUpdate::parse_from_bytes(data)?)
}),
);
let user_attributes_mutation = Box::pin(
session
.mercury()
.listen_for("spotify:user:attributes:mutated")
.map(UnboundedReceiverStream::new)
.flatten_stream()
.map(|response| -> Result<UserAttributesMutation, Error> {
let data = response.payload.first().ok_or(SpircError::NoData)?;
Ok(UserAttributesMutation::parse_from_bytes(data)?)
}), }),
); );
@ -274,7 +359,10 @@ impl Spirc {
play_request_id: None, play_request_id: None,
play_status: SpircPlayStatus::Stopped, play_status: SpircPlayStatus::Stopped,
subscription, remote_update,
connection_id_update,
user_attributes_update,
user_attributes_mutation,
sender, sender,
commands: Some(cmd_rx), commands: Some(cmd_rx),
player_events: Some(player_events), player_events: Some(player_events),
@ -296,37 +384,37 @@ impl Spirc {
let spirc = Spirc { commands: cmd_tx }; let spirc = Spirc { commands: cmd_tx };
task.hello(); task.hello()?;
(spirc, task.run()) Ok((spirc, task.run()))
} }
pub fn play(&self) { pub fn play(&self) -> Result<(), Error> {
let _ = self.commands.send(SpircCommand::Play); Ok(self.commands.send(SpircCommand::Play)?)
} }
pub fn play_pause(&self) { pub fn play_pause(&self) -> Result<(), Error> {
let _ = self.commands.send(SpircCommand::PlayPause); Ok(self.commands.send(SpircCommand::PlayPause)?)
} }
pub fn pause(&self) { pub fn pause(&self) -> Result<(), Error> {
let _ = self.commands.send(SpircCommand::Pause); Ok(self.commands.send(SpircCommand::Pause)?)
} }
pub fn prev(&self) { pub fn prev(&self) -> Result<(), Error> {
let _ = self.commands.send(SpircCommand::Prev); Ok(self.commands.send(SpircCommand::Prev)?)
} }
pub fn next(&self) { pub fn next(&self) -> Result<(), Error> {
let _ = self.commands.send(SpircCommand::Next); Ok(self.commands.send(SpircCommand::Next)?)
} }
pub fn volume_up(&self) { pub fn volume_up(&self) -> Result<(), Error> {
let _ = self.commands.send(SpircCommand::VolumeUp); Ok(self.commands.send(SpircCommand::VolumeUp)?)
} }
pub fn volume_down(&self) { pub fn volume_down(&self) -> Result<(), Error> {
let _ = self.commands.send(SpircCommand::VolumeDown); Ok(self.commands.send(SpircCommand::VolumeDown)?)
} }
pub fn shutdown(&self) { pub fn shutdown(&self) -> Result<(), Error> {
let _ = self.commands.send(SpircCommand::Shutdown); Ok(self.commands.send(SpircCommand::Shutdown)?)
} }
pub fn shuffle(&self) { pub fn shuffle(&self) -> Result<(), Error> {
let _ = self.commands.send(SpircCommand::Shuffle); Ok(self.commands.send(SpircCommand::Shuffle)?)
} }
} }
@ -336,18 +424,57 @@ impl SpircTask {
let commands = self.commands.as_mut(); let commands = self.commands.as_mut();
let player_events = self.player_events.as_mut(); let player_events = self.player_events.as_mut();
tokio::select! { tokio::select! {
frame = self.subscription.next() => match frame { remote_update = self.remote_update.next() => match remote_update {
Some(frame) => self.handle_frame(frame), Some(result) => match result {
Ok(update) => if let Err(e) = self.handle_remote_update(update) {
error!("could not dispatch remote update: {}", e);
}
Err(e) => error!("could not parse remote update: {}", e),
}
None => { None => {
error!("subscription terminated"); error!("subscription terminated");
break; break;
} }
}, },
cmd = async { commands.unwrap().recv().await }, if commands.is_some() => if let Some(cmd) = cmd { user_attributes_update = self.user_attributes_update.next() => match user_attributes_update {
self.handle_command(cmd); Some(result) => match result {
Ok(attributes) => self.handle_user_attributes_update(attributes),
Err(e) => error!("could not parse user attributes update: {}", e),
}
None => {
error!("user attributes update selected, but none received");
break;
}
}, },
event = async { player_events.unwrap().recv().await }, if player_events.is_some() => if let Some(event) = event { user_attributes_mutation = self.user_attributes_mutation.next() => match user_attributes_mutation {
self.handle_player_event(event) Some(result) => match result {
Ok(attributes) => self.handle_user_attributes_mutation(attributes),
Err(e) => error!("could not parse user attributes mutation: {}", e),
}
None => {
error!("user attributes mutation selected, but none received");
break;
}
},
connection_id_update = self.connection_id_update.next() => match connection_id_update {
Some(result) => match result {
Ok(connection_id) => self.handle_connection_id_update(connection_id),
Err(e) => error!("could not parse connection ID update: {}", e),
}
None => {
error!("connection ID update selected, but none received");
break;
}
},
cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = cmd {
if let Err(e) = self.handle_command(cmd) {
error!("could not dispatch command: {}", e);
}
},
event = async { player_events?.recv().await }, if player_events.is_some() => if let Some(event) = event {
if let Err(e) = self.handle_player_event(event) {
error!("could not dispatch player event: {}", e);
}
}, },
result = self.sender.flush(), if !self.sender.is_flushed() => if result.is_err() { result = self.sender.flush(), if !self.sender.is_flushed() => if result.is_err() {
error!("Cannot flush spirc event sender."); error!("Cannot flush spirc event sender.");
@ -417,79 +544,80 @@ impl SpircTask {
self.state.set_position_ms(position_ms); self.state.set_position_ms(position_ms);
} }
fn handle_command(&mut self, cmd: SpircCommand) { fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> {
let active = self.device.get_is_active(); let active = self.device.get_is_active();
match cmd { match cmd {
SpircCommand::Play => { SpircCommand::Play => {
if active { if active {
self.handle_play(); self.handle_play();
self.notify(None, true); self.notify(None, true)
} else { } else {
CommandSender::new(self, MessageType::kMessageTypePlay).send(); CommandSender::new(self, MessageType::kMessageTypePlay).send()
} }
} }
SpircCommand::PlayPause => { SpircCommand::PlayPause => {
if active { if active {
self.handle_play_pause(); self.handle_play_pause();
self.notify(None, true); self.notify(None, true)
} else { } else {
CommandSender::new(self, MessageType::kMessageTypePlayPause).send(); CommandSender::new(self, MessageType::kMessageTypePlayPause).send()
} }
} }
SpircCommand::Pause => { SpircCommand::Pause => {
if active { if active {
self.handle_pause(); self.handle_pause();
self.notify(None, true); self.notify(None, true)
} else { } else {
CommandSender::new(self, MessageType::kMessageTypePause).send(); CommandSender::new(self, MessageType::kMessageTypePause).send()
} }
} }
SpircCommand::Prev => { SpircCommand::Prev => {
if active { if active {
self.handle_prev(); self.handle_prev();
self.notify(None, true); self.notify(None, true)
} else { } else {
CommandSender::new(self, MessageType::kMessageTypePrev).send(); CommandSender::new(self, MessageType::kMessageTypePrev).send()
} }
} }
SpircCommand::Next => { SpircCommand::Next => {
if active { if active {
self.handle_next(); self.handle_next();
self.notify(None, true); self.notify(None, true)
} else { } else {
CommandSender::new(self, MessageType::kMessageTypeNext).send(); CommandSender::new(self, MessageType::kMessageTypeNext).send()
} }
} }
SpircCommand::VolumeUp => { SpircCommand::VolumeUp => {
if active { if active {
self.handle_volume_up(); self.handle_volume_up();
self.notify(None, true); self.notify(None, true)
} else { } else {
CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send(); CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send()
} }
} }
SpircCommand::VolumeDown => { SpircCommand::VolumeDown => {
if active { if active {
self.handle_volume_down(); self.handle_volume_down();
self.notify(None, true); self.notify(None, true)
} else { } else {
CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send(); CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send()
} }
} }
SpircCommand::Shutdown => { SpircCommand::Shutdown => {
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send(); CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?;
self.shutdown = true; self.shutdown = true;
if let Some(rx) = self.commands.as_mut() { if let Some(rx) = self.commands.as_mut() {
rx.close() rx.close()
} }
Ok(())
} }
SpircCommand::Shuffle => { SpircCommand::Shuffle => {
CommandSender::new(self, MessageType::kMessageTypeShuffle).send(); CommandSender::new(self, MessageType::kMessageTypeShuffle).send()
} }
} }
} }
fn handle_player_event(&mut self, event: PlayerEvent) { fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> {
// we only process events if the play_request_id matches. If it doesn't, it is // we only process events if the play_request_id matches. If it doesn't, it is
// an event that belongs to a previous track and only arrives now due to a race // an event that belongs to a previous track and only arrives now due to a race
// condition. In this case we have updated the state already and don't want to // condition. In this case we have updated the state already and don't want to
@ -498,8 +626,13 @@ impl SpircTask {
if Some(play_request_id) == self.play_request_id { if Some(play_request_id) == self.play_request_id {
match event { match event {
PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(), PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(),
PlayerEvent::Loading { .. } => self.notify(None, false), PlayerEvent::Loading { .. } => {
trace!("==> kPlayStatusLoading");
self.state.set_status(PlayStatus::kPlayStatusLoading);
self.notify(None, false)
}
PlayerEvent::Playing { position_ms, .. } => { PlayerEvent::Playing { position_ms, .. } => {
trace!("==> kPlayStatusPlay");
let new_nominal_start_time = self.now_ms() - position_ms as i64; let new_nominal_start_time = self.now_ms() - position_ms as i64;
match self.play_status { match self.play_status {
SpircPlayStatus::Playing { SpircPlayStatus::Playing {
@ -509,27 +642,29 @@ impl SpircTask {
if (*nominal_start_time - new_nominal_start_time).abs() > 100 { if (*nominal_start_time - new_nominal_start_time).abs() > 100 {
*nominal_start_time = new_nominal_start_time; *nominal_start_time = new_nominal_start_time;
self.update_state_position(position_ms); self.update_state_position(position_ms);
self.notify(None, true); self.notify(None, true)
} else {
Ok(())
} }
} }
SpircPlayStatus::LoadingPlay { .. } SpircPlayStatus::LoadingPlay { .. }
| SpircPlayStatus::LoadingPause { .. } => { | SpircPlayStatus::LoadingPause { .. } => {
self.state.set_status(PlayStatus::kPlayStatusPlay); self.state.set_status(PlayStatus::kPlayStatusPlay);
self.update_state_position(position_ms); self.update_state_position(position_ms);
self.notify(None, true);
self.play_status = SpircPlayStatus::Playing { self.play_status = SpircPlayStatus::Playing {
nominal_start_time: new_nominal_start_time, nominal_start_time: new_nominal_start_time,
preloading_of_next_track_triggered: false, preloading_of_next_track_triggered: false,
}; };
self.notify(None, true)
}
_ => Ok(()),
} }
_ => (),
};
trace!("==> kPlayStatusPlay");
} }
PlayerEvent::Paused { PlayerEvent::Paused {
position_ms: new_position_ms, position_ms: new_position_ms,
.. ..
} => { } => {
trace!("==> kPlayStatusPause");
match self.play_status { match self.play_status {
SpircPlayStatus::Paused { SpircPlayStatus::Paused {
ref mut position_ms, ref mut position_ms,
@ -538,42 +673,96 @@ impl SpircTask {
if *position_ms != new_position_ms { if *position_ms != new_position_ms {
*position_ms = new_position_ms; *position_ms = new_position_ms;
self.update_state_position(new_position_ms); self.update_state_position(new_position_ms);
self.notify(None, true); self.notify(None, true)
} else {
Ok(())
} }
} }
SpircPlayStatus::LoadingPlay { .. } SpircPlayStatus::LoadingPlay { .. }
| SpircPlayStatus::LoadingPause { .. } => { | SpircPlayStatus::LoadingPause { .. } => {
self.state.set_status(PlayStatus::kPlayStatusPause); self.state.set_status(PlayStatus::kPlayStatusPause);
self.update_state_position(new_position_ms); self.update_state_position(new_position_ms);
self.notify(None, true);
self.play_status = SpircPlayStatus::Paused { self.play_status = SpircPlayStatus::Paused {
position_ms: new_position_ms, position_ms: new_position_ms,
preloading_of_next_track_triggered: false, preloading_of_next_track_triggered: false,
}; };
self.notify(None, true)
} }
_ => (), _ => Ok(()),
} }
trace!("==> kPlayStatusPause");
} }
PlayerEvent::Stopped { .. } => match self.play_status { PlayerEvent::Stopped { .. } => {
SpircPlayStatus::Stopped => (), trace!("==> kPlayStatusStop");
match self.play_status {
SpircPlayStatus::Stopped => Ok(()),
_ => { _ => {
warn!("The player has stopped unexpectedly."); warn!("The player has stopped unexpectedly.");
self.state.set_status(PlayStatus::kPlayStatusStop); self.state.set_status(PlayStatus::kPlayStatusStop);
self.notify(None, true);
self.play_status = SpircPlayStatus::Stopped; self.play_status = SpircPlayStatus::Stopped;
self.notify(None, true)
} }
},
PlayerEvent::TimeToPreloadNextTrack { .. } => self.handle_preload_next_track(),
PlayerEvent::Unavailable { track_id, .. } => self.handle_unavailable(track_id),
_ => (),
} }
} }
PlayerEvent::TimeToPreloadNextTrack { .. } => {
self.handle_preload_next_track();
Ok(())
}
PlayerEvent::Unavailable { track_id, .. } => {
self.handle_unavailable(track_id);
Ok(())
}
_ => Ok(()),
}
} else {
Ok(())
}
} else {
Ok(())
}
}
fn handle_connection_id_update(&mut self, connection_id: String) {
trace!("Received connection ID update: {:?}", connection_id);
self.session.set_connection_id(connection_id);
}
fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) {
trace!("Received attributes update: {:#?}", update);
let attributes: UserAttributes = update
.get_pairs()
.iter()
.map(|pair| (pair.get_key().to_owned(), pair.get_value().to_owned()))
.collect();
self.session.set_user_attributes(attributes)
}
fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) {
for attribute in mutation.get_fields().iter() {
let key = attribute.get_name();
if let Some(old_value) = self.session.user_data().attributes.get(key) {
let new_value = match old_value.as_ref() {
"0" => "1",
"1" => "0",
_ => old_value,
};
self.session.set_user_attribute(key, new_value);
trace!(
"Received attribute mutation, {} was {} is now {}",
key,
old_value,
new_value
);
} else {
trace!(
"Received attribute mutation for {} but key was not found!",
key
);
}
} }
} }
fn handle_frame(&mut self, frame: Frame) { fn handle_remote_update(&mut self, update: Frame) -> Result<(), Error> {
let state_string = match frame.get_state().get_status() { let state_string = match update.get_state().get_status() {
PlayStatus::kPlayStatusLoading => "kPlayStatusLoading", PlayStatus::kPlayStatusLoading => "kPlayStatusLoading",
PlayStatus::kPlayStatusPause => "kPlayStatusPause", PlayStatus::kPlayStatusPause => "kPlayStatusPause",
PlayStatus::kPlayStatusStop => "kPlayStatusStop", PlayStatus::kPlayStatusStop => "kPlayStatusStop",
@ -582,24 +771,24 @@ impl SpircTask {
debug!( debug!(
"{:?} {:?} {} {} {} {}", "{:?} {:?} {} {} {} {}",
frame.get_typ(), update.get_typ(),
frame.get_device_state().get_name(), update.get_device_state().get_name(),
frame.get_ident(), update.get_ident(),
frame.get_seq_nr(), update.get_seq_nr(),
frame.get_state_update_id(), update.get_state_update_id(),
state_string, state_string,
); );
if frame.get_ident() == self.ident let device_id = &self.ident;
|| (!frame.get_recipient().is_empty() && !frame.get_recipient().contains(&self.ident)) let ident = update.get_ident();
if ident == device_id
|| (!update.get_recipient().is_empty() && !update.get_recipient().contains(device_id))
{ {
return; return Err(SpircError::Ident(ident.to_string()).into());
} }
match frame.get_typ() { match update.get_typ() {
MessageType::kMessageTypeHello => { MessageType::kMessageTypeHello => self.notify(Some(ident), true),
self.notify(Some(frame.get_ident()), true);
}
MessageType::kMessageTypeLoad => { MessageType::kMessageTypeLoad => {
if !self.device.get_is_active() { if !self.device.get_is_active() {
@ -608,12 +797,12 @@ impl SpircTask {
self.device.set_became_active_at(now); self.device.set_became_active_at(now);
} }
self.update_tracks(&frame); self.update_tracks(&update);
if !self.state.get_track().is_empty() { if !self.state.get_track().is_empty() {
let start_playing = let start_playing =
frame.get_state().get_status() == PlayStatus::kPlayStatusPlay; update.get_state().get_status() == PlayStatus::kPlayStatusPlay;
self.load_track(start_playing, frame.get_state().get_position_ms()); self.load_track(start_playing, update.get_state().get_position_ms());
} else { } else {
info!("No more tracks left in queue"); info!("No more tracks left in queue");
self.state.set_status(PlayStatus::kPlayStatusStop); self.state.set_status(PlayStatus::kPlayStatusStop);
@ -621,51 +810,51 @@ impl SpircTask {
self.play_status = SpircPlayStatus::Stopped; self.play_status = SpircPlayStatus::Stopped;
} }
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypePlay => { MessageType::kMessageTypePlay => {
self.handle_play(); self.handle_play();
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypePlayPause => { MessageType::kMessageTypePlayPause => {
self.handle_play_pause(); self.handle_play_pause();
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypePause => { MessageType::kMessageTypePause => {
self.handle_pause(); self.handle_pause();
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypeNext => { MessageType::kMessageTypeNext => {
self.handle_next(); self.handle_next();
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypePrev => { MessageType::kMessageTypePrev => {
self.handle_prev(); self.handle_prev();
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypeVolumeUp => { MessageType::kMessageTypeVolumeUp => {
self.handle_volume_up(); self.handle_volume_up();
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypeVolumeDown => { MessageType::kMessageTypeVolumeDown => {
self.handle_volume_down(); self.handle_volume_down();
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypeRepeat => { MessageType::kMessageTypeRepeat => {
self.state.set_repeat(frame.get_state().get_repeat()); self.state.set_repeat(update.get_state().get_repeat());
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypeShuffle => { MessageType::kMessageTypeShuffle => {
self.state.set_shuffle(frame.get_state().get_shuffle()); self.state.set_shuffle(update.get_state().get_shuffle());
if self.state.get_shuffle() { if self.state.get_shuffle() {
let current_index = self.state.get_playing_track_index(); let current_index = self.state.get_playing_track_index();
{ {
@ -681,17 +870,17 @@ impl SpircTask {
let context = self.state.get_context_uri(); let context = self.state.get_context_uri();
debug!("{:?}", context); debug!("{:?}", context);
} }
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypeSeek => { MessageType::kMessageTypeSeek => {
self.handle_seek(frame.get_position()); self.handle_seek(update.get_position());
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypeReplace => { MessageType::kMessageTypeReplace => {
self.update_tracks(&frame); self.update_tracks(&update);
self.notify(None, true); self.notify(None, true)?;
if let SpircPlayStatus::Playing { if let SpircPlayStatus::Playing {
preloading_of_next_track_triggered, preloading_of_next_track_triggered,
@ -709,27 +898,29 @@ impl SpircTask {
} }
} }
} }
Ok(())
} }
MessageType::kMessageTypeVolume => { MessageType::kMessageTypeVolume => {
self.set_volume(frame.get_volume() as u16); self.set_volume(update.get_volume() as u16);
self.notify(None, true); self.notify(None, true)
} }
MessageType::kMessageTypeNotify => { MessageType::kMessageTypeNotify => {
if self.device.get_is_active() if self.device.get_is_active()
&& frame.get_device_state().get_is_active() && update.get_device_state().get_is_active()
&& self.device.get_became_active_at() && self.device.get_became_active_at()
<= frame.get_device_state().get_became_active_at() <= update.get_device_state().get_became_active_at()
{ {
self.device.set_is_active(false); self.device.set_is_active(false);
self.state.set_status(PlayStatus::kPlayStatusStop); self.state.set_status(PlayStatus::kPlayStatusStop);
self.player.stop(); self.player.stop();
self.play_status = SpircPlayStatus::Stopped; self.play_status = SpircPlayStatus::Stopped;
} }
Ok(())
} }
_ => (), _ => Ok(()),
} }
} }
@ -739,11 +930,6 @@ impl SpircTask {
position_ms, position_ms,
preloading_of_next_track_triggered, preloading_of_next_track_triggered,
} => { } => {
// Synchronize the volume from the mixer. This is useful on
// systems that can switch sources from and back to librespot.
let current_volume = self.mixer.volume();
self.set_volume(current_volume);
self.player.play(); self.player.play();
self.state.set_status(PlayStatus::kPlayStatusPlay); self.state.set_status(PlayStatus::kPlayStatusPlay);
self.update_state_position(position_ms); self.update_state_position(position_ms);
@ -756,8 +942,13 @@ impl SpircTask {
self.player.play(); self.player.play();
self.play_status = SpircPlayStatus::LoadingPlay { position_ms }; self.play_status = SpircPlayStatus::LoadingPlay { position_ms };
} }
_ => (), _ => return,
} }
// Synchronize the volume from the mixer. This is useful on
// systems that can switch sources from and back to librespot.
let current_volume = self.mixer.volume();
self.set_volume(current_volume);
} }
fn handle_play_pause(&mut self) { fn handle_play_pause(&mut self) {
@ -887,8 +1078,8 @@ impl SpircTask {
let tracks_len = self.state.get_track().len() as u32; let tracks_len = self.state.get_track().len() as u32;
debug!( debug!(
"At track {:?} of {:?} <{:?}> update [{}]", "At track {:?} of {:?} <{:?}> update [{}]",
new_index, new_index + 1,
self.state.get_track().len(), tracks_len,
self.state.get_context_uri(), self.state.get_context_uri(),
tracks_len - new_index < CONTEXT_FETCH_THRESHOLD tracks_len - new_index < CONTEXT_FETCH_THRESHOLD
); );
@ -902,16 +1093,20 @@ impl SpircTask {
self.context_fut = self.resolve_station(&context_uri); self.context_fut = self.resolve_station(&context_uri);
self.update_tracks_from_context(); self.update_tracks_from_context();
} }
if self.config.autoplay && new_index == tracks_len - 1 { if new_index >= tracks_len {
if self.config.autoplay {
// Extend the playlist // Extend the playlist
// Note: This doesn't seem to reflect in the UI
// the additional tracks in the frame don't show up as with station view
debug!("Extending playlist <{}>", context_uri); debug!("Extending playlist <{}>", context_uri);
self.update_tracks_from_context(); self.update_tracks_from_context();
} self.player.set_auto_normalise_as_album(false);
if new_index >= tracks_len { } else {
new_index = 0; // Loop around back to start new_index = 0;
continue_playing = self.state.get_repeat(); continue_playing = self.state.get_repeat();
debug!(
"Looping around back to start, repeat is {}",
continue_playing
);
}
} }
if tracks_len > 0 { if tracks_len > 0 {
@ -975,9 +1170,9 @@ impl SpircTask {
self.set_volume(volume); self.set_volume(volume);
} }
fn handle_end_of_track(&mut self) { fn handle_end_of_track(&mut self) -> Result<(), Error> {
self.handle_next(); self.handle_next();
self.notify(None, true); self.notify(None, true)
} }
fn position(&mut self) -> u32 { fn position(&mut self) -> u32 {
@ -992,48 +1187,40 @@ impl SpircTask {
} }
} }
fn resolve_station(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, MercuryError>> { fn resolve_station(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, Error>> {
let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri); let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri);
self.resolve_uri(&radio_uri) self.resolve_uri(&radio_uri)
} }
fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture<Result<String, MercuryError>> { fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture<Result<String, Error>> {
let query_uri = format!("hm://autoplay-enabled/query?uri={}", uri); let query_uri = format!("hm://autoplay-enabled/query?uri={}", uri);
let request = self.session.mercury().get(query_uri); let request = self.session.mercury().get(query_uri);
Box::pin( Box::pin(
async { async {
let response = request.await?; let response = request?.await?;
if response.status_code == 200 { if response.status_code == 200 {
let data = response let data = response.payload.first().ok_or(SpircError::NoData)?.to_vec();
.payload Ok(String::from_utf8(data)?)
.first()
.expect("Empty autoplay uri")
.to_vec();
let autoplay_uri = String::from_utf8(data).unwrap();
Ok(autoplay_uri)
} else { } else {
warn!("No autoplay_uri found"); warn!("No autoplay_uri found");
Err(MercuryError) Err(MercuryError::Response(response).into())
} }
} }
.fuse(), .fuse(),
) )
} }
fn resolve_uri(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, MercuryError>> { fn resolve_uri(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, Error>> {
let request = self.session.mercury().get(uri); let request = self.session.mercury().get(uri);
Box::pin( Box::pin(
async move { async move {
let response = request.await?; let response = request?.await?;
let data = response let data = response.payload.first().ok_or(SpircError::NoData)?;
.payload let response: serde_json::Value = serde_json::from_slice(data)?;
.first()
.expect("Empty payload on context uri");
let response: serde_json::Value = serde_json::from_slice(&data).unwrap();
Ok(response) Ok(response)
} }
@ -1051,7 +1238,7 @@ impl SpircTask {
if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) {
track_vec.drain(0..head); track_vec.drain(0..head);
} }
track_vec.extend_from_slice(&new_tracks); track_vec.extend_from_slice(new_tracks);
self.state self.state
.set_track(protobuf::RepeatedField::from_vec(track_vec)); .set_track(protobuf::RepeatedField::from_vec(track_vec));
@ -1069,11 +1256,11 @@ impl SpircTask {
} }
fn update_tracks(&mut self, frame: &protocol::spirc::Frame) { fn update_tracks(&mut self, frame: &protocol::spirc::Frame) {
debug!("State: {:?}", frame.get_state()); trace!("State: {:#?}", frame.get_state());
let index = frame.get_state().get_playing_track_index(); let index = frame.get_state().get_playing_track_index();
let context_uri = frame.get_state().get_context_uri().to_owned(); let context_uri = frame.get_state().get_context_uri().to_owned();
let tracks = frame.get_state().get_track(); let tracks = frame.get_state().get_track();
debug!("Frame has {:?} tracks", tracks.len()); trace!("Frame has {:?} tracks", tracks.len());
if context_uri.starts_with("spotify:station:") if context_uri.starts_with("spotify:station:")
|| context_uri.starts_with("spotify:dailymix:") || context_uri.starts_with("spotify:dailymix:")
{ {
@ -1084,6 +1271,9 @@ impl SpircTask {
self.autoplay_fut = self.resolve_autoplay_uri(&context_uri); self.autoplay_fut = self.resolve_autoplay_uri(&context_uri);
} }
self.player
.set_auto_normalise_as_album(context_uri.starts_with("spotify:album:"));
self.state.set_playing_track_index(index); self.state.set_playing_track_index(index);
self.state.set_track(tracks.iter().cloned().collect()); self.state.set_track(tracks.iter().cloned().collect());
self.state.set_context_uri(context_uri); self.state.set_context_uri(context_uri);
@ -1099,15 +1289,6 @@ impl SpircTask {
} }
} }
// should this be a method of SpotifyId directly?
fn get_spotify_id_for_track(&self, track_ref: &TrackRef) -> Result<SpotifyId, SpotifyIdError> {
SpotifyId::from_raw(track_ref.get_gid()).or_else(|_| {
let uri = track_ref.get_uri();
debug!("Malformed or no gid, attempting to parse URI <{}>", uri);
SpotifyId::from_uri(uri)
})
}
// Helper to find corresponding index(s) for track_id // Helper to find corresponding index(s) for track_id
fn get_track_index_for_spotify_id( fn get_track_index_for_spotify_id(
&self, &self,
@ -1133,6 +1314,14 @@ impl SpircTask {
fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> { fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> {
let tracks_len = self.state.get_track().len(); let tracks_len = self.state.get_track().len();
// Guard against tracks_len being zero to prevent
// 'index out of bounds: the len is 0 but the index is 0'
// https://github.com/librespot-org/librespot/issues/226#issuecomment-971642037
if tracks_len == 0 {
warn!("No playable track found in state: {:?}", self.state);
return None;
}
let mut new_playlist_index = index as usize; let mut new_playlist_index = index as usize;
if new_playlist_index >= tracks_len { if new_playlist_index >= tracks_len {
@ -1146,11 +1335,8 @@ impl SpircTask {
// E.g - context based frames sometimes contain tracks with <spotify:meta:page:> // E.g - context based frames sometimes contain tracks with <spotify:meta:page:>
let mut track_ref = self.state.get_track()[new_playlist_index].clone(); let mut track_ref = self.state.get_track()[new_playlist_index].clone();
let mut track_id = self.get_spotify_id_for_track(&track_ref); let mut track_id = SpotifyId::try_from(&track_ref);
while self.track_ref_is_unavailable(&track_ref) while self.track_ref_is_unavailable(&track_ref) || track_id.is_err() {
|| track_id.is_err()
|| track_id.unwrap().audio_type == SpotifyAudioType::NonPlayable
{
warn!( warn!(
"Skipping track <{:?}> at position [{}] of {}", "Skipping track <{:?}> at position [{}] of {}",
track_ref, new_playlist_index, tracks_len track_ref, new_playlist_index, tracks_len
@ -1166,7 +1352,7 @@ impl SpircTask {
return None; return None;
} }
track_ref = self.state.get_track()[new_playlist_index].clone(); track_ref = self.state.get_track()[new_playlist_index].clone();
track_id = self.get_spotify_id_for_track(&track_ref); track_id = SpotifyId::try_from(&track_ref);
} }
match track_id { match track_id {
@ -1201,13 +1387,17 @@ impl SpircTask {
} }
} }
fn hello(&mut self) { fn hello(&mut self) -> Result<(), Error> {
CommandSender::new(self, MessageType::kMessageTypeHello).send(); CommandSender::new(self, MessageType::kMessageTypeHello).send()
} }
fn notify(&mut self, recipient: Option<&str>, suppress_loading_status: bool) { fn notify(
&mut self,
recipient: Option<&str>,
suppress_loading_status: bool,
) -> Result<(), Error> {
if suppress_loading_status && (self.state.get_status() == PlayStatus::kPlayStatusLoading) { if suppress_loading_status && (self.state.get_status() == PlayStatus::kPlayStatusLoading) {
return; return Ok(());
}; };
let status_string = match self.state.get_status() { let status_string = match self.state.get_status() {
PlayStatus::kPlayStatusLoading => "kPlayStatusLoading", PlayStatus::kPlayStatusLoading => "kPlayStatusLoading",
@ -1218,9 +1408,9 @@ impl SpircTask {
trace!("Sending status to server: [{}]", status_string); trace!("Sending status to server: [{}]", status_string);
let mut cs = CommandSender::new(self, MessageType::kMessageTypeNotify); let mut cs = CommandSender::new(self, MessageType::kMessageTypeNotify);
if let Some(s) = recipient { if let Some(s) = recipient {
cs = cs.recipient(&s); cs = cs.recipient(s);
} }
cs.send(); cs.send()
} }
fn set_volume(&mut self, volume: u16) { fn set_volume(&mut self, volume: u16) {
@ -1268,11 +1458,11 @@ impl<'a> CommandSender<'a> {
self self
} }
fn send(mut self) { fn send(mut self) -> Result<(), Error> {
if !self.frame.has_state() && self.spirc.device.get_is_active() { if !self.frame.has_state() && self.spirc.device.get_is_active() {
self.frame.set_state(self.spirc.state.clone()); self.frame.set_state(self.spirc.state.clone());
} }
self.spirc.sender.send(self.frame.write_to_bytes().unwrap()); self.spirc.sender.send(self.frame.write_to_bytes()?)
} }
} }

View file

@ -9,8 +9,10 @@
# #
# If only one architecture is desired, cargo can be invoked directly with the appropriate options : # If only one architecture is desired, cargo can be invoked directly with the appropriate options :
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend" # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend"
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend" # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend
# $ docker run -v /tmp/librespot-build:/build librespot-cross contrib/docker-build-pi-armv6hf.sh # $ docker run -v /tmp/librespot-build:/build librespot-cross contrib/docker-build-pi-armv6hf.sh
FROM debian:stretch FROM debian:stretch

View file

@ -2,12 +2,12 @@
Description=Librespot (an open source Spotify client) Description=Librespot (an open source Spotify client)
Documentation=https://github.com/librespot-org/librespot Documentation=https://github.com/librespot-org/librespot
Documentation=https://github.com/librespot-org/librespot/wiki/Options Documentation=https://github.com/librespot-org/librespot/wiki/Options
Requires=network-online.target Wants=network.target sound.target
After=network-online.target After=network.target sound.target
[Service] [Service]
User=nobody DynamicUser=yes
Group=audio SupplementaryGroups=audio
Restart=always Restart=always
RestartSec=10 RestartSec=10
ExecStart=/usr/bin/librespot --name "%p@%H" ExecStart=/usr/bin/librespot --name "%p@%H"

View file

@ -2,6 +2,8 @@
Description=Librespot (an open source Spotify client) Description=Librespot (an open source Spotify client)
Documentation=https://github.com/librespot-org/librespot Documentation=https://github.com/librespot-org/librespot
Documentation=https://github.com/librespot-org/librespot/wiki/Options Documentation=https://github.com/librespot-org/librespot/wiki/Options
Wants=network.target sound.target
After=network.target sound.target
[Service] [Service]
Restart=always Restart=always

View file

@ -1,6 +1,6 @@
[package] [package]
name = "librespot-core" name = "librespot-core"
version = "0.2.0" version = "0.3.1"
authors = ["Paul Lietar <paul@lietar.net>"] authors = ["Paul Lietar <paul@lietar.net>"]
build = "build.rs" build = "build.rs"
description = "The core functionality provided by librespot" description = "The core functionality provided by librespot"
@ -10,21 +10,24 @@ edition = "2018"
[dependencies.librespot-protocol] [dependencies.librespot-protocol]
path = "../protocol" path = "../protocol"
version = "0.2.0" version = "0.3.1"
[dependencies] [dependencies]
aes = "0.6" aes = "0.6"
base64 = "0.13" base64 = "0.13"
byteorder = "1.4" byteorder = "1.4"
bytes = "1.0" bytes = "1"
chrono = "0.4"
dns-sd = { version = "0.1.3", optional = true }
form_urlencoded = "1.0" form_urlencoded = "1.0"
futures-core = { version = "0.3", default-features = false } futures-core = { version = "0.3", default-features = false }
futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "unstable", "sink"] } futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "sink", "unstable"] }
hmac = "0.11" hmac = "0.11"
httparse = "1.3" httparse = "1.3"
http = "0.2" http = "0.2"
hyper = { version = "0.14", features = ["client", "tcp", "http1"] } hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] }
hyper-proxy = { version = "0.9.1", default-features = false } hyper-proxy = { version = "0.9.1", default-features = false, features = ["rustls"] }
hyper-rustls = { version = "0.22", default-features = false, features = ["native-tokio"] }
log = "0.4" log = "0.4"
num = "0.4" num = "0.4"
num-bigint = { version = "0.4", features = ["rand"] } num-bigint = { version = "0.4", features = ["rand"] }
@ -32,16 +35,20 @@ num-derive = "0.3"
num-integer = "0.1" num-integer = "0.1"
num-traits = "0.2" num-traits = "0.2"
once_cell = "1.5.2" once_cell = "1.5.2"
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
pbkdf2 = { version = "0.8", default-features = false, features = ["hmac"] } pbkdf2 = { version = "0.8", default-features = false, features = ["hmac"] }
priority-queue = "1.1" priority-queue = "1.1"
protobuf = "2.14.0" protobuf = "2.14.0"
quick-xml = { version = "0.22", features = ["serialize"] }
rand = "0.8" rand = "0.8"
rustls = "0.19"
rustls-native-certs = "0.5"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
sha-1 = "0.9" sha-1 = "0.9"
shannon = "0.2.0" shannon = "0.2.0"
thiserror = "1.0.7" thiserror = "1.0"
tokio = { version = "1.5", features = ["io-util", "macros", "net", "rt", "time", "sync"] } tokio = { version = "1.5", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] }
tokio-stream = "0.1.1" tokio-stream = "0.1.1"
tokio-tungstenite = { version = "0.14", default-features = false, features = ["rustls-tls"] } tokio-tungstenite = { version = "0.14", default-features = false, features = ["rustls-tls"] }
tokio-util = { version = "0.6", features = ["codec"] } tokio-util = { version = "0.6", features = ["codec"] }
@ -54,4 +61,7 @@ vergen = "3.0.4"
[dev-dependencies] [dev-dependencies]
env_logger = "0.8" env_logger = "0.8"
tokio = {version = "1.0", features = ["macros"] } tokio = { version = "1.0", features = ["macros", "parking_lot"] }
[features]
with-dns-sd = ["dns-sd"]

View file

@ -1,19 +1,21 @@
use hyper::{Body, Request};
use serde::Deserialize;
use std::error::Error;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use hyper::{Body, Method, Request};
use serde::Deserialize;
use crate::Error;
pub type SocketAddress = (String, u16); pub type SocketAddress = (String, u16);
#[derive(Default)] #[derive(Default)]
struct AccessPoints { pub struct AccessPoints {
accesspoint: Vec<SocketAddress>, accesspoint: Vec<SocketAddress>,
dealer: Vec<SocketAddress>, dealer: Vec<SocketAddress>,
spclient: Vec<SocketAddress>, spclient: Vec<SocketAddress>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct ApResolveData { pub struct ApResolveData {
accesspoint: Vec<String>, accesspoint: Vec<String>,
dealer: Vec<String>, dealer: Vec<String>,
spclient: Vec<String>, spclient: Vec<String>,
@ -42,7 +44,7 @@ component! {
impl ApResolver { impl ApResolver {
// return a port if a proxy URL and/or a proxy port was specified. This is useful even when // return a port if a proxy URL and/or a proxy port was specified. This is useful even when
// there is no proxy, but firewalls only allow certain ports (e.g. 443 and not 4070). // there is no proxy, but firewalls only allow certain ports (e.g. 443 and not 4070).
fn port_config(&self) -> Option<u16> { pub fn port_config(&self) -> Option<u16> {
if self.session().config().proxy.is_some() || self.session().config().ap_port.is_some() { if self.session().config().proxy.is_some() || self.session().config().ap_port.is_some() {
Some(self.session().config().ap_port.unwrap_or(443)) Some(self.session().config().ap_port.unwrap_or(443))
} else { } else {
@ -54,9 +56,7 @@ impl ApResolver {
data.into_iter() data.into_iter()
.filter_map(|ap| { .filter_map(|ap| {
let mut split = ap.rsplitn(2, ':'); let mut split = ap.rsplitn(2, ':');
let port = split let port = split.next()?;
.next()
.expect("rsplitn should not return empty iterator");
let host = split.next()?.to_owned(); let host = split.next()?.to_owned();
let port: u16 = port.parse().ok()?; let port: u16 = port.parse().ok()?;
if let Some(p) = self.port_config() { if let Some(p) = self.port_config() {
@ -69,12 +69,11 @@ impl ApResolver {
.collect() .collect()
} }
async fn try_apresolve(&self) -> Result<ApResolveData, Box<dyn Error>> { pub async fn try_apresolve(&self) -> Result<ApResolveData, Error> {
let req = Request::builder() let req = Request::builder()
.method("GET") .method(Method::GET)
.uri("http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient") .uri("http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient")
.body(Body::empty()) .body(Body::empty())?;
.unwrap();
let body = self.session().http_client().request_body(req).await?; let body = self.session().http_client().request_body(req).await?;
let data: ApResolveData = serde_json::from_slice(body.as_ref())?; let data: ApResolveData = serde_json::from_slice(body.as_ref())?;

View file

@ -1,53 +1,85 @@
use std::{collections::HashMap, io::Write};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use bytes::Bytes; use bytes::Bytes;
use std::collections::HashMap; use thiserror::Error;
use std::io::Write;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use crate::packet::PacketType; use crate::{packet::PacketType, util::SeqGenerator, Error, FileId, SpotifyId};
use crate::spotify_id::{FileId, SpotifyId};
use crate::util::SeqGenerator;
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct AudioKey(pub [u8; 16]); pub struct AudioKey(pub [u8; 16]);
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] #[derive(Debug, Error)]
pub struct AudioKeyError; pub enum AudioKeyError {
#[error("audio key error")]
AesKey,
#[error("other end of channel disconnected")]
Channel,
#[error("unexpected packet type {0}")]
Packet(u8),
#[error("sequence {0} not pending")]
Sequence(u32),
}
impl From<AudioKeyError> for Error {
fn from(err: AudioKeyError) -> Self {
match err {
AudioKeyError::AesKey => Error::unavailable(err),
AudioKeyError::Channel => Error::aborted(err),
AudioKeyError::Sequence(_) => Error::aborted(err),
AudioKeyError::Packet(_) => Error::unimplemented(err),
}
}
}
component! { component! {
AudioKeyManager : AudioKeyManagerInner { AudioKeyManager : AudioKeyManagerInner {
sequence: SeqGenerator<u32> = SeqGenerator::new(0), sequence: SeqGenerator<u32> = SeqGenerator::new(0),
pending: HashMap<u32, oneshot::Sender<Result<AudioKey, AudioKeyError>>> = HashMap::new(), pending: HashMap<u32, oneshot::Sender<Result<AudioKey, Error>>> = HashMap::new(),
} }
} }
impl AudioKeyManager { impl AudioKeyManager {
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
let seq = BigEndian::read_u32(data.split_to(4).as_ref()); let seq = BigEndian::read_u32(data.split_to(4).as_ref());
let sender = self.lock(|inner| inner.pending.remove(&seq)); let sender = self
.lock(|inner| inner.pending.remove(&seq))
.ok_or(AudioKeyError::Sequence(seq))?;
if let Some(sender) = sender {
match cmd { match cmd {
PacketType::AesKey => { PacketType::AesKey => {
let mut key = [0u8; 16]; let mut key = [0u8; 16];
key.copy_from_slice(data.as_ref()); key.copy_from_slice(data.as_ref());
let _ = sender.send(Ok(AudioKey(key))); sender
.send(Ok(AudioKey(key)))
.map_err(|_| AudioKeyError::Channel)?
} }
PacketType::AesKeyError => { PacketType::AesKeyError => {
warn!( error!(
"error audio key {:x} {:x}", "error audio key {:x} {:x}",
data.as_ref()[0], data.as_ref()[0],
data.as_ref()[1] data.as_ref()[1]
); );
let _ = sender.send(Err(AudioKeyError)); sender
} .send(Err(AudioKeyError::AesKey.into()))
_ => (), .map_err(|_| AudioKeyError::Channel)?
} }
_ => {
trace!(
"Did not expect {:?} AES key packet with data {:#?}",
cmd,
data
);
return Err(AudioKeyError::Packet(cmd as u8).into());
} }
} }
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, AudioKeyError> { Ok(())
}
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, Error> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let seq = self.lock(move |inner| { let seq = self.lock(move |inner| {
@ -56,16 +88,16 @@ impl AudioKeyManager {
seq seq
}); });
self.send_key_request(seq, track, file); self.send_key_request(seq, track, file)?;
rx.await.map_err(|_| AudioKeyError)? rx.await?
} }
fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) { fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) -> Result<(), Error> {
let mut data: Vec<u8> = Vec::new(); let mut data: Vec<u8> = Vec::new();
data.write_all(&file.0).unwrap(); data.write_all(&file.0)?;
data.write_all(&track.to_raw()).unwrap(); data.write_all(&track.to_raw())?;
data.write_u32::<BigEndian>(seq).unwrap(); data.write_u32::<BigEndian>(seq)?;
data.write_u16::<BigEndian>(0x0000).unwrap(); data.write_u16::<BigEndian>(0x0000)?;
self.session().send_packet(PacketType::RequestKey, data) self.session().send_packet(PacketType::RequestKey, data)
} }

View file

@ -7,8 +7,21 @@ use pbkdf2::pbkdf2;
use protobuf::ProtobufEnum; use protobuf::ProtobufEnum;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use thiserror::Error;
use crate::protocol::authentication::AuthenticationType; use crate::{protocol::authentication::AuthenticationType, Error};
#[derive(Debug, Error)]
pub enum AuthenticationError {
#[error("unknown authentication type {0}")]
AuthType(u32),
}
impl From<AuthenticationError> for Error {
fn from(err: AuthenticationError) -> Self {
Error::invalid_argument(err)
}
}
/// The credentials are used to log into the Spotify API. /// The credentials are used to log into the Spotify API.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -42,7 +55,11 @@ impl Credentials {
} }
} }
pub fn with_blob(username: String, encrypted_blob: &str, device_id: &str) -> Credentials { pub fn with_blob(
username: impl Into<String>,
encrypted_blob: impl AsRef<[u8]>,
device_id: impl AsRef<[u8]>,
) -> Result<Credentials, Error> {
fn read_u8<R: Read>(stream: &mut R) -> io::Result<u8> { fn read_u8<R: Read>(stream: &mut R) -> io::Result<u8> {
let mut data = [0u8]; let mut data = [0u8];
stream.read_exact(&mut data)?; stream.read_exact(&mut data)?;
@ -67,7 +84,9 @@ impl Credentials {
Ok(data) Ok(data)
} }
let secret = Sha1::digest(device_id.as_bytes()); let username = username.into();
let secret = Sha1::digest(device_id.as_ref());
let key = { let key = {
let mut key = [0u8; 24]; let mut key = [0u8; 24];
@ -85,12 +104,12 @@ impl Credentials {
use aes::cipher::generic_array::GenericArray; use aes::cipher::generic_array::GenericArray;
use aes::cipher::{BlockCipher, NewBlockCipher}; use aes::cipher::{BlockCipher, NewBlockCipher};
let mut data = base64::decode(encrypted_blob).unwrap(); let mut data = base64::decode(encrypted_blob)?;
let cipher = Aes192::new(GenericArray::from_slice(&key)); let cipher = Aes192::new(GenericArray::from_slice(&key));
let block_size = <Aes192 as BlockCipher>::BlockSize::to_usize(); let block_size = <Aes192 as BlockCipher>::BlockSize::to_usize();
assert_eq!(data.len() % block_size, 0); assert_eq!(data.len() % block_size, 0);
// replace to chunks_exact_mut with MSRV bump to 1.31 for chunk in data.chunks_exact_mut(block_size) {
for chunk in data.chunks_mut(block_size) {
cipher.decrypt_block(GenericArray::from_mut_slice(chunk)); cipher.decrypt_block(GenericArray::from_mut_slice(chunk));
} }
@ -102,20 +121,21 @@ impl Credentials {
data data
}; };
let mut cursor = io::Cursor::new(&blob); let mut cursor = io::Cursor::new(blob.as_slice());
read_u8(&mut cursor).unwrap(); read_u8(&mut cursor)?;
read_bytes(&mut cursor).unwrap(); read_bytes(&mut cursor)?;
read_u8(&mut cursor).unwrap(); read_u8(&mut cursor)?;
let auth_type = read_int(&mut cursor).unwrap(); let auth_type = read_int(&mut cursor)?;
let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap(); let auth_type = AuthenticationType::from_i32(auth_type as i32)
read_u8(&mut cursor).unwrap(); .ok_or(AuthenticationError::AuthType(auth_type))?;
let auth_data = read_bytes(&mut cursor).unwrap(); read_u8(&mut cursor)?;
let auth_data = read_bytes(&mut cursor)?;
Credentials { Ok(Credentials {
username, username,
auth_type, auth_type,
auth_data, auth_data,
} })
} }
} }

View file

@ -1,15 +1,30 @@
use std::cmp::Reverse; use std::{
use std::collections::HashMap; cmp::Reverse,
use std::fs::{self, File}; collections::HashMap,
use std::io::{self, Error, ErrorKind, Read, Write}; fs::{self, File},
use std::path::{Path, PathBuf}; io::{self, Read, Write},
use std::sync::{Arc, Mutex}; path::{Path, PathBuf},
use std::time::SystemTime; sync::Arc,
time::SystemTime,
};
use parking_lot::Mutex;
use priority_queue::PriorityQueue; use priority_queue::PriorityQueue;
use thiserror::Error;
use crate::authentication::Credentials; use crate::{authentication::Credentials, error::ErrorKind, Error, FileId};
use crate::spotify_id::FileId;
#[derive(Debug, Error)]
pub enum CacheError {
#[error("audio cache location is not configured")]
Path,
}
impl From<CacheError> for Error {
fn from(err: CacheError) -> Self {
Error::failed_precondition(err)
}
}
/// Some kind of data structure that holds some paths, the size of these files and a timestamp. /// Some kind of data structure that holds some paths, the size of these files and a timestamp.
/// It keeps track of the file sizes and is able to pop the path with the oldest timestamp if /// It keeps track of the file sizes and is able to pop the path with the oldest timestamp if
@ -57,16 +72,17 @@ impl SizeLimiter {
/// to delete the file in the file system. /// to delete the file in the file system.
fn pop(&mut self) -> Option<PathBuf> { fn pop(&mut self) -> Option<PathBuf> {
if self.exceeds_limit() { if self.exceeds_limit() {
let (next, _) = self if let Some((next, _)) = self.queue.pop() {
.queue if let Some(size) = self.sizes.remove(&next) {
.pop()
.expect("in_use was > 0, so the queue should have contained an item.");
let size = self
.sizes
.remove(&next)
.expect("`queue` and `sizes` should have the same keys.");
self.in_use -= size; self.in_use -= size;
} else {
error!("`queue` and `sizes` should have the same keys.");
}
Some(next) Some(next)
} else {
error!("in_use was > 0, so the queue should have contained an item.");
None
}
} else { } else {
None None
} }
@ -85,11 +101,11 @@ impl SizeLimiter {
return false; return false;
} }
let size = self if let Some(size) = self.sizes.remove(file) {
.sizes
.remove(file)
.expect("`queue` and `sizes` should have the same keys.");
self.in_use -= size; self.in_use -= size;
} else {
error!("`queue` and `sizes` should have the same keys.");
}
true true
} }
@ -173,23 +189,21 @@ impl FsSizeLimiter {
} }
fn add(&self, file: &Path, size: u64) { fn add(&self, file: &Path, size: u64) {
self.limiter self.limiter.lock().add(file, size, SystemTime::now())
.lock()
.unwrap()
.add(file, size, SystemTime::now());
} }
fn touch(&self, file: &Path) -> bool { fn touch(&self, file: &Path) -> bool {
self.limiter.lock().unwrap().update(file, SystemTime::now()) self.limiter.lock().update(file, SystemTime::now())
} }
fn remove(&self, file: &Path) { fn remove(&self, file: &Path) -> bool {
self.limiter.lock().unwrap().remove(file); self.limiter.lock().remove(file)
} }
fn prune_internal<F: FnMut() -> Option<PathBuf>>(mut pop: F) { fn prune_internal<F: FnMut() -> Option<PathBuf>>(mut pop: F) -> Result<(), Error> {
let mut first = true; let mut first = true;
let mut count = 0; let mut count = 0;
let mut last_error = None;
while let Some(file) = pop() { while let Some(file) = pop() {
if first { if first {
@ -197,8 +211,10 @@ impl FsSizeLimiter {
first = false; first = false;
} }
if let Err(e) = fs::remove_file(&file) { let res = fs::remove_file(&file);
if let Err(e) = res {
warn!("Could not remove file {:?} from cache dir: {}", file, e); warn!("Could not remove file {:?} from cache dir: {}", file, e);
last_error = Some(e);
} else { } else {
count += 1; count += 1;
} }
@ -207,21 +223,27 @@ impl FsSizeLimiter {
if count > 0 { if count > 0 {
info!("Removed {} cache files.", count); info!("Removed {} cache files.", count);
} }
if let Some(err) = last_error {
Err(err.into())
} else {
Ok(())
}
} }
fn prune(&self) { fn prune(&self) -> Result<(), Error> {
Self::prune_internal(|| self.limiter.lock().unwrap().pop()) Self::prune_internal(|| self.limiter.lock().pop())
} }
fn new(path: &Path, limit: u64) -> Self { fn new(path: &Path, limit: u64) -> Result<Self, Error> {
let mut limiter = SizeLimiter::new(limit); let mut limiter = SizeLimiter::new(limit);
Self::init_dir(&mut limiter, path); Self::init_dir(&mut limiter, path);
Self::prune_internal(|| limiter.pop()); Self::prune_internal(|| limiter.pop())?;
Self { Ok(Self {
limiter: Mutex::new(limiter), limiter: Mutex::new(limiter),
} })
} }
} }
@ -234,33 +256,39 @@ pub struct Cache {
size_limiter: Option<Arc<FsSizeLimiter>>, size_limiter: Option<Arc<FsSizeLimiter>>,
} }
pub struct RemoveFileError(());
impl Cache { impl Cache {
pub fn new<P: AsRef<Path>>( pub fn new<P: AsRef<Path>>(
system_location: Option<P>, credentials_path: Option<P>,
audio_location: Option<P>, volume_path: Option<P>,
audio_path: Option<P>,
size_limit: Option<u64>, size_limit: Option<u64>,
) -> io::Result<Self> { ) -> Result<Self, Error> {
if let Some(location) = &system_location { let mut size_limiter = None;
if let Some(location) = &credentials_path {
fs::create_dir_all(location)?; fs::create_dir_all(location)?;
} }
let mut size_limiter = None; let credentials_location = credentials_path
.as_ref()
.map(|p| p.as_ref().join("credentials.json"));
if let Some(location) = &audio_location { if let Some(location) = &volume_path {
fs::create_dir_all(location)?; fs::create_dir_all(location)?;
}
let volume_location = volume_path.as_ref().map(|p| p.as_ref().join("volume"));
if let Some(location) = &audio_path {
fs::create_dir_all(location)?;
if let Some(limit) = size_limit { if let Some(limit) = size_limit {
let limiter = FsSizeLimiter::new(location.as_ref(), limit); let limiter = FsSizeLimiter::new(location.as_ref(), limit)?;
size_limiter = Some(Arc::new(limiter)); size_limiter = Some(Arc::new(limiter));
} }
} }
let audio_location = audio_location.map(|p| p.as_ref().to_owned()); let audio_location = audio_path.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 { let cache = Cache {
credentials_location, credentials_location,
@ -276,11 +304,11 @@ impl Cache {
let location = self.credentials_location.as_ref()?; let location = self.credentials_location.as_ref()?;
// This closure is just convencience to enable the question mark operator // This closure is just convencience to enable the question mark operator
let read = || { let read = || -> Result<Credentials, Error> {
let mut file = File::open(location)?; let mut file = File::open(location)?;
let mut contents = String::new(); let mut contents = String::new();
file.read_to_string(&mut contents)?; file.read_to_string(&mut contents)?;
serde_json::from_str(&contents).map_err(|e| Error::new(ErrorKind::InvalidData, e)) Ok(serde_json::from_str(&contents)?)
}; };
match read() { match read() {
@ -288,7 +316,7 @@ impl Cache {
Err(e) => { Err(e) => {
// If the file did not exist, the file was probably not written // If the file did not exist, the file was probably not written
// before. Otherwise, log the error. // before. Otherwise, log the error.
if e.kind() != ErrorKind::NotFound { if e.kind != ErrorKind::NotFound {
warn!("Error reading credentials from cache: {}", e); warn!("Error reading credentials from cache: {}", e);
} }
None None
@ -312,19 +340,17 @@ impl Cache {
pub fn volume(&self) -> Option<u16> { pub fn volume(&self) -> Option<u16> {
let location = self.volume_location.as_ref()?; let location = self.volume_location.as_ref()?;
let read = || { let read = || -> Result<u16, Error> {
let mut file = File::open(location)?; let mut file = File::open(location)?;
let mut contents = String::new(); let mut contents = String::new();
file.read_to_string(&mut contents)?; file.read_to_string(&mut contents)?;
contents Ok(contents.parse()?)
.parse()
.map_err(|e| Error::new(ErrorKind::InvalidData, e))
}; };
match read() { match read() {
Ok(v) => Some(v), Ok(v) => Some(v),
Err(e) => { Err(e) => {
if e.kind() != ErrorKind::NotFound { if e.kind != ErrorKind::NotFound {
warn!("Error reading volume from cache: {}", e); warn!("Error reading volume from cache: {}", e);
} }
None None
@ -355,12 +381,14 @@ impl Cache {
match File::open(&path) { match File::open(&path) {
Ok(file) => { Ok(file) => {
if let Some(limiter) = self.size_limiter.as_deref() { if let Some(limiter) = self.size_limiter.as_deref() {
limiter.touch(&path); if !limiter.touch(&path) {
error!("limiter could not touch {:?}", path);
}
} }
Some(file) Some(file)
} }
Err(e) => { Err(e) => {
if e.kind() != ErrorKind::NotFound { if e.kind() != io::ErrorKind::NotFound {
warn!("Error reading file from cache: {}", e) warn!("Error reading file from cache: {}", e)
} }
None None
@ -368,39 +396,34 @@ impl Cache {
} }
} }
pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) { pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) -> Result<(), Error> {
let path = if let Some(path) = self.file_path(file) { if let Some(path) = self.file_path(file) {
path if let Some(parent) = path.parent() {
} else { if let Ok(size) = fs::create_dir_all(parent)
return;
};
let parent = path.parent().unwrap();
let result = fs::create_dir_all(parent)
.and_then(|_| File::create(&path)) .and_then(|_| File::create(&path))
.and_then(|mut file| io::copy(contents, &mut file)); .and_then(|mut file| io::copy(contents, &mut file))
{
if let Ok(size) = result {
if let Some(limiter) = self.size_limiter.as_deref() { if let Some(limiter) = self.size_limiter.as_deref() {
limiter.add(&path, size); limiter.add(&path, size);
limiter.prune(); limiter.prune()?;
}
return Ok(());
} }
} }
} }
Err(CacheError::Path.into())
}
pub fn remove_file(&self, file: FileId) -> Result<(), RemoveFileError> { pub fn remove_file(&self, file: FileId) -> Result<(), Error> {
let path = self.file_path(file).ok_or(RemoveFileError(()))?; let path = self.file_path(file).ok_or(CacheError::Path)?;
if let Err(err) = fs::remove_file(&path) { fs::remove_file(&path)?;
warn!("Unable to remove file from cache: {}", err);
Err(RemoveFileError(()))
} else {
if let Some(limiter) = self.size_limiter.as_deref() { if let Some(limiter) = self.size_limiter.as_deref() {
limiter.remove(&path); limiter.remove(&path);
} }
Ok(()) Ok(())
} }
}
} }
#[cfg(test)] #[cfg(test)]

166
core/src/cdn_url.rs Normal file
View file

@ -0,0 +1,166 @@
use std::{
convert::{TryFrom, TryInto},
ops::{Deref, DerefMut},
};
use chrono::Local;
use protobuf::Message;
use thiserror::Error;
use url::Url;
use super::{date::Date, Error, FileId, Session};
use librespot_protocol as protocol;
use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage;
use protocol::storage_resolve::StorageResolveResponse_Result;
#[derive(Debug, Clone)]
pub struct MaybeExpiringUrl(pub String, pub Option<Date>);
#[derive(Debug, Clone)]
pub struct MaybeExpiringUrls(pub Vec<MaybeExpiringUrl>);
impl Deref for MaybeExpiringUrls {
type Target = Vec<MaybeExpiringUrl>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for MaybeExpiringUrls {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug, Error)]
pub enum CdnUrlError {
#[error("all URLs expired")]
Expired,
#[error("resolved storage is not for CDN")]
Storage,
#[error("no URLs resolved")]
Unresolved,
}
impl From<CdnUrlError> for Error {
fn from(err: CdnUrlError) -> Self {
match err {
CdnUrlError::Expired => Error::deadline_exceeded(err),
CdnUrlError::Storage | CdnUrlError::Unresolved => Error::unavailable(err),
}
}
}
#[derive(Debug, Clone)]
pub struct CdnUrl {
pub file_id: FileId,
urls: MaybeExpiringUrls,
}
impl CdnUrl {
pub fn new(file_id: FileId) -> Self {
Self {
file_id,
urls: MaybeExpiringUrls(Vec::new()),
}
}
pub async fn resolve_audio(&self, session: &Session) -> Result<Self, Error> {
let file_id = self.file_id;
let response = session.spclient().get_audio_storage(file_id).await?;
let msg = CdnUrlMessage::parse_from_bytes(&response)?;
let urls = MaybeExpiringUrls::try_from(msg)?;
let cdn_url = Self { file_id, urls };
trace!("Resolved CDN storage: {:#?}", cdn_url);
Ok(cdn_url)
}
pub fn try_get_url(&self) -> Result<&str, Error> {
if self.urls.is_empty() {
return Err(CdnUrlError::Unresolved.into());
}
let now = Local::now();
let url = self.urls.iter().find(|url| match url.1 {
Some(expiry) => now < expiry.as_utc(),
None => true,
});
if let Some(url) = url {
Ok(&url.0)
} else {
Err(CdnUrlError::Expired.into())
}
}
}
impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
type Error = crate::Error;
fn try_from(msg: CdnUrlMessage) -> Result<Self, Self::Error> {
if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) {
return Err(CdnUrlError::Storage.into());
}
let is_expiring = !msg.get_fileid().is_empty();
let result = msg
.get_cdnurl()
.iter()
.map(|cdn_url| {
let url = Url::parse(cdn_url)?;
if is_expiring {
let expiry_str = if let Some(token) = url
.query_pairs()
.into_iter()
.find(|(key, _value)| key == "__token__")
{
if let Some(mut start) = token.1.find("exp=") {
start += 4;
if token.1.len() >= start {
let slice = &token.1[start..];
if let Some(end) = slice.find('~') {
// this is the only valid invariant for akamaized.net
String::from(&slice[..end])
} else {
String::from(slice)
}
} else {
String::new()
}
} else {
String::new()
}
} else if let Some(query) = url.query() {
let mut items = query.split('_');
if let Some(first) = items.next() {
// this is the only valid invariant for scdn.co
String::from(first)
} else {
String::new()
}
} else {
String::new()
};
let mut expiry: i64 = expiry_str.parse()?;
expiry -= 5 * 60; // seconds
Ok(MaybeExpiringUrl(
cdn_url.to_owned(),
Some(expiry.try_into()?),
))
} else {
Ok(MaybeExpiringUrl(cdn_url.to_owned(), None))
}
})
.collect::<Result<Vec<MaybeExpiringUrl>, Error>>()?;
Ok(Self(result))
}
}

View file

@ -1,18 +1,20 @@
use std::collections::HashMap; use std::{
use std::pin::Pin; collections::HashMap,
use std::task::{Context, Poll}; fmt,
use std::time::Instant; pin::Pin,
task::{Context, Poll},
time::Instant,
};
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes; use bytes::Bytes;
use futures_core::Stream; use futures_core::Stream;
use futures_util::lock::BiLock; use futures_util::{lock::BiLock, ready, StreamExt};
use futures_util::{ready, StreamExt};
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use thiserror::Error;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::packet::PacketType; use crate::{packet::PacketType, util::SeqGenerator, Error};
use crate::util::SeqGenerator;
component! { component! {
ChannelManager : ChannelManagerInner { ChannelManager : ChannelManagerInner {
@ -27,9 +29,21 @@ component! {
const ONE_SECOND_IN_MS: usize = 1000; const ONE_SECOND_IN_MS: usize = 1000;
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] #[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)]
pub struct ChannelError; pub struct ChannelError;
impl From<ChannelError> for Error {
fn from(err: ChannelError) -> Self {
Error::aborted(err)
}
}
impl fmt::Display for ChannelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "channel error")
}
}
pub struct Channel { pub struct Channel {
receiver: mpsc::UnboundedReceiver<(u8, Bytes)>, receiver: mpsc::UnboundedReceiver<(u8, Bytes)>,
state: ChannelState, state: ChannelState,
@ -70,7 +84,7 @@ impl ChannelManager {
(seq, channel) (seq, channel)
} }
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref()); let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref());
@ -94,9 +108,14 @@ impl ChannelManager {
inner.download_measurement_bytes += data.len(); inner.download_measurement_bytes += data.len();
if let Entry::Occupied(entry) = inner.channels.entry(id) { if let Entry::Occupied(entry) = inner.channels.entry(id) {
let _ = entry.get().send((cmd as u8, data)); entry
.get()
.send((cmd as u8, data))
.map_err(|_| ChannelError)?;
} }
});
Ok(())
})
} }
pub fn get_download_rate_estimate(&self) -> usize { pub fn get_download_rate_estimate(&self) -> usize {
@ -142,7 +161,11 @@ impl Stream for Channel {
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop { loop {
match self.state.clone() { match self.state.clone() {
ChannelState::Closed => panic!("Polling already terminated channel"), ChannelState::Closed => {
error!("Polling already terminated channel");
return Poll::Ready(None);
}
ChannelState::Header(mut data) => { ChannelState::Header(mut data) => {
if data.is_empty() { if data.is_empty() {
data = ready!(self.recv_packet(cx))?; data = ready!(self.recv_packet(cx))?;

View file

@ -1,20 +1,20 @@
macro_rules! component { macro_rules! component {
($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => { ($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => {
#[derive(Clone)] #[derive(Clone)]
pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::std::sync::Mutex<$inner>)>); pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::parking_lot::Mutex<$inner>)>);
impl $name { impl $name {
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) fn new(session: $crate::session::SessionWeak) -> $name { pub(crate) fn new(session: $crate::session::SessionWeak) -> $name {
debug!(target:"librespot::component", "new {}", stringify!($name)); debug!(target:"librespot::component", "new {}", stringify!($name));
$name(::std::sync::Arc::new((session, ::std::sync::Mutex::new($inner { $name(::std::sync::Arc::new((session, ::parking_lot::Mutex::new($inner {
$($key : $value,)* $($key : $value,)*
})))) }))))
} }
#[allow(dead_code)] #[allow(dead_code)]
fn lock<F: FnOnce(&mut $inner) -> R, R>(&self, f: F) -> R { fn lock<F: FnOnce(&mut $inner) -> R, R>(&self, f: F) -> R {
let mut inner = (self.0).1.lock().expect("Mutex poisoned"); let mut inner = (self.0).1.lock();
f(&mut inner) f(&mut inner)
} }

View file

@ -1,23 +1,23 @@
use std::fmt; use std::{fmt, path::PathBuf, str::FromStr};
use std::str::FromStr;
use url::Url; use url::Url;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct SessionConfig { pub struct SessionConfig {
pub user_agent: String,
pub device_id: String, pub device_id: String,
pub proxy: Option<Url>, pub proxy: Option<Url>,
pub ap_port: Option<u16>, pub ap_port: Option<u16>,
pub tmp_dir: PathBuf,
} }
impl Default for SessionConfig { impl Default for SessionConfig {
fn default() -> SessionConfig { fn default() -> SessionConfig {
let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string(); let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string();
SessionConfig { SessionConfig {
user_agent: crate::version::VERSION_STRING.to_string(),
device_id, device_id,
proxy: None, proxy: None,
ap_port: None, ap_port: None,
tmp_dir: std::env::temp_dir(),
} }
} }
} }
@ -125,3 +125,15 @@ pub struct ConnectConfig {
pub has_volume_ctrl: bool, pub has_volume_ctrl: bool,
pub autoplay: bool, pub autoplay: bool,
} }
impl Default for ConnectConfig {
fn default() -> ConnectConfig {
ConnectConfig {
name: "Librespot".to_string(),
device_type: DeviceType::default(),
initial_volume: Some(50),
has_volume_ctrl: true,
autoplay: false,
}
}
}

View file

@ -1,12 +1,20 @@
use std::io;
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use bytes::{BufMut, Bytes, BytesMut}; use bytes::{BufMut, Bytes, BytesMut};
use shannon::Shannon; use shannon::Shannon;
use std::io; use thiserror::Error;
use tokio_util::codec::{Decoder, Encoder}; use tokio_util::codec::{Decoder, Encoder};
const HEADER_SIZE: usize = 3; const HEADER_SIZE: usize = 3;
const MAC_SIZE: usize = 4; const MAC_SIZE: usize = 4;
#[derive(Debug, Error)]
pub enum ApCodecError {
#[error("payload was malformed")]
Payload,
}
#[derive(Debug)] #[derive(Debug)]
enum DecodeState { enum DecodeState {
Header, Header,
@ -88,7 +96,9 @@ impl Decoder for ApCodec {
let mut payload = buf.split_to(size + MAC_SIZE); let mut payload = buf.split_to(size + MAC_SIZE);
self.decode_cipher self.decode_cipher
.decrypt(&mut payload.get_mut(..size).unwrap()); .decrypt(payload.get_mut(..size).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, ApCodecError::Payload)
})?);
let mac = payload.split_off(size); let mac = payload.split_off(size);
self.decode_cipher.check_mac(mac.as_ref())?; self.decode_cipher.check_mac(mac.as_ref())?;

View file

@ -1,16 +1,28 @@
use std::{env::consts::ARCH, io};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use hmac::{Hmac, Mac, NewMac}; use hmac::{Hmac, Mac, NewMac};
use protobuf::{self, Message}; use protobuf::{self, Message};
use rand::{thread_rng, RngCore}; use rand::{thread_rng, RngCore};
use sha1::Sha1; use sha1::Sha1;
use std::io; use thiserror::Error;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio_util::codec::{Decoder, Framed}; use tokio_util::codec::{Decoder, Framed};
use super::codec::ApCodec; use super::codec::ApCodec;
use crate::diffie_hellman::DhLocalKeys;
use crate::{diffie_hellman::DhLocalKeys, version};
use crate::protocol; use crate::protocol;
use crate::protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext}; use crate::protocol::keyexchange::{
APResponseMessage, ClientHello, ClientResponsePlaintext, Platform, ProductFlags,
};
#[derive(Debug, Error)]
pub enum HandshakeError {
#[error("invalid key length")]
InvalidLength,
}
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>( pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
mut connection: T, mut connection: T,
@ -27,7 +39,7 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
.to_owned(); .to_owned();
let shared_secret = local_keys.shared_secret(&remote_key); let shared_secret = local_keys.shared_secret(&remote_key);
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator); let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator)?;
let codec = ApCodec::new(&send_key, &recv_key); let codec = ApCodec::new(&send_key, &recv_key);
client_response(&mut connection, challenge).await?; client_response(&mut connection, challenge).await?;
@ -42,14 +54,56 @@ where
let mut client_nonce = vec![0; 0x10]; let mut client_nonce = vec![0; 0x10];
thread_rng().fill_bytes(&mut client_nonce); thread_rng().fill_bytes(&mut client_nonce);
let platform = match std::env::consts::OS {
"android" => Platform::PLATFORM_ANDROID_ARM,
"freebsd" | "netbsd" | "openbsd" => match ARCH {
"x86_64" => Platform::PLATFORM_FREEBSD_X86_64,
_ => Platform::PLATFORM_FREEBSD_X86,
},
"ios" => match ARCH {
"arm64" => Platform::PLATFORM_IPHONE_ARM64,
_ => Platform::PLATFORM_IPHONE_ARM,
},
"linux" => match ARCH {
"arm" | "arm64" => Platform::PLATFORM_LINUX_ARM,
"blackfin" => Platform::PLATFORM_LINUX_BLACKFIN,
"mips" => Platform::PLATFORM_LINUX_MIPS,
"sh" => Platform::PLATFORM_LINUX_SH,
"x86_64" => Platform::PLATFORM_LINUX_X86_64,
_ => Platform::PLATFORM_LINUX_X86,
},
"macos" => match ARCH {
"ppc" | "ppc64" => Platform::PLATFORM_OSX_PPC,
"x86_64" => Platform::PLATFORM_OSX_X86_64,
_ => Platform::PLATFORM_OSX_X86,
},
"windows" => match ARCH {
"arm" => Platform::PLATFORM_WINDOWS_CE_ARM,
"x86_64" => Platform::PLATFORM_WIN32_X86_64,
_ => Platform::PLATFORM_WIN32_X86,
},
_ => Platform::PLATFORM_LINUX_X86,
};
#[cfg(debug_assertions)]
const PRODUCT_FLAGS: ProductFlags = ProductFlags::PRODUCT_FLAG_DEV_BUILD;
#[cfg(not(debug_assertions))]
const PRODUCT_FLAGS: ProductFlags = ProductFlags::PRODUCT_FLAG_NONE;
let mut packet = ClientHello::new(); let mut packet = ClientHello::new();
packet packet
.mut_build_info() .mut_build_info()
.set_product(protocol::keyexchange::Product::PRODUCT_PARTNER); // ProductInfo won't push autoplay and perhaps other settings
// when set to anything else than PRODUCT_CLIENT
.set_product(protocol::keyexchange::Product::PRODUCT_CLIENT);
packet packet
.mut_build_info() .mut_build_info()
.set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86); .mut_product_flags()
packet.mut_build_info().set_version(109800078); .push(PRODUCT_FLAGS);
packet.mut_build_info().set_platform(platform);
packet
.mut_build_info()
.set_version(version::SPOTIFY_VERSION);
packet packet
.mut_cryptosuites_supported() .mut_cryptosuites_supported()
.push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON); .push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON);
@ -66,8 +120,8 @@ where
let mut buffer = vec![0, 4]; let mut buffer = vec![0, 4];
let size = 2 + 4 + packet.compute_size(); let size = 2 + 4 + packet.compute_size();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap(); <Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?;
packet.write_to_vec(&mut buffer).unwrap(); packet.write_to_vec(&mut buffer)?;
connection.write_all(&buffer[..]).await?; connection.write_all(&buffer[..]).await?;
Ok(buffer) Ok(buffer)
@ -87,8 +141,8 @@ where
let mut buffer = vec![]; let mut buffer = vec![];
let size = 4 + packet.compute_size(); let size = 4 + packet.compute_size();
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap(); <Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size)?;
packet.write_to_vec(&mut buffer).unwrap(); packet.write_to_vec(&mut buffer)?;
connection.write_all(&buffer[..]).await?; connection.write_all(&buffer[..]).await?;
Ok(()) Ok(())
@ -102,7 +156,7 @@ where
let header = read_into_accumulator(connection, 4, acc).await?; let header = read_into_accumulator(connection, 4, acc).await?;
let size = BigEndian::read_u32(header) as usize; let size = BigEndian::read_u32(header) as usize;
let data = read_into_accumulator(connection, size - 4, acc).await?; let data = read_into_accumulator(connection, size - 4, acc).await?;
let message = M::parse_from_bytes(data).unwrap(); let message = M::parse_from_bytes(data)?;
Ok(message) Ok(message)
} }
@ -118,24 +172,26 @@ async fn read_into_accumulator<'a, 'b, T: AsyncRead + Unpin>(
Ok(&mut acc[offset..]) Ok(&mut acc[offset..])
} }
fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) { fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> io::Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
type HmacSha1 = Hmac<Sha1>; type HmacSha1 = Hmac<Sha1>;
let mut data = Vec::with_capacity(0x64); let mut data = Vec::with_capacity(0x64);
for i in 1..6 { for i in 1..6 {
let mut mac = let mut mac = HmacSha1::new_from_slice(shared_secret).map_err(|_| {
HmacSha1::new_from_slice(&shared_secret).expect("HMAC can take key of any size"); io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength)
})?;
mac.update(packets); mac.update(packets);
mac.update(&[i]); mac.update(&[i]);
data.extend_from_slice(&mac.finalize().into_bytes()); data.extend_from_slice(&mac.finalize().into_bytes());
} }
let mut mac = HmacSha1::new_from_slice(&data[..0x14]).expect("HMAC can take key of any size"); let mut mac = HmacSha1::new_from_slice(&data[..0x14])
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength))?;
mac.update(packets); mac.update(packets);
( Ok((
mac.finalize().into_bytes().to_vec(), mac.finalize().into_bytes().to_vec(),
data[0x14..0x34].to_vec(), data[0x14..0x34].to_vec(),
data[0x34..0x54].to_vec(), data[0x34..0x54].to_vec(),
) ))
} }

View file

@ -1,23 +1,21 @@
mod codec; mod codec;
mod handshake; mod handshake;
pub use self::codec::ApCodec; pub use self::{codec::ApCodec, handshake::handshake};
pub use self::handshake::handshake;
use std::io::{self, ErrorKind}; use std::io;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use protobuf::{self, Message, ProtobufError}; use protobuf::{self, Message};
use thiserror::Error; use thiserror::Error;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio_util::codec::Framed; use tokio_util::codec::Framed;
use url::Url; use url::Url;
use crate::authentication::Credentials; use crate::{authentication::Credentials, packet::PacketType, version, Error};
use crate::packet::PacketType;
use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
use crate::version;
pub type Transport = Framed<TcpStream, ApCodec>; pub type Transport = Framed<TcpStream, ApCodec>;
@ -42,13 +40,19 @@ fn login_error_message(code: &ErrorCode) -> &'static str {
pub enum AuthenticationError { pub enum AuthenticationError {
#[error("Login failed with reason: {}", login_error_message(.0))] #[error("Login failed with reason: {}", login_error_message(.0))]
LoginFailed(ErrorCode), LoginFailed(ErrorCode),
#[error("Authentication failed: {0}")] #[error("invalid packet {0}")]
IoError(#[from] io::Error), Packet(u8),
#[error("transport returned no data")]
Transport,
} }
impl From<ProtobufError> for AuthenticationError { impl From<AuthenticationError> for Error {
fn from(e: ProtobufError) -> Self { fn from(err: AuthenticationError) -> Self {
io::Error::new(ErrorKind::InvalidData, e).into() match err {
AuthenticationError::LoginFailed(_) => Error::permission_denied(err),
AuthenticationError::Packet(_) => Error::unimplemented(err),
AuthenticationError::Transport => Error::unavailable(err),
}
} }
} }
@ -68,9 +72,32 @@ pub async fn authenticate(
transport: &mut Transport, transport: &mut Transport,
credentials: Credentials, credentials: Credentials,
device_id: &str, device_id: &str,
) -> Result<Credentials, AuthenticationError> { ) -> Result<Credentials, Error> {
use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
let cpu_family = match std::env::consts::ARCH {
"blackfin" => CpuFamily::CPU_BLACKFIN,
"arm" | "arm64" => CpuFamily::CPU_ARM,
"ia64" => CpuFamily::CPU_IA64,
"mips" => CpuFamily::CPU_MIPS,
"ppc" => CpuFamily::CPU_PPC,
"ppc64" => CpuFamily::CPU_PPC_64,
"sh" => CpuFamily::CPU_SH,
"x86" => CpuFamily::CPU_X86,
"x86_64" => CpuFamily::CPU_X86_64,
_ => CpuFamily::CPU_UNKNOWN,
};
let os = match std::env::consts::OS {
"android" => Os::OS_ANDROID,
"freebsd" | "netbsd" | "openbsd" => Os::OS_FREEBSD,
"ios" => Os::OS_IPHONE,
"linux" => Os::OS_LINUX,
"macos" => Os::OS_OSX,
"windows" => Os::OS_WINDOWS,
_ => Os::OS_UNKNOWN,
};
let mut packet = ClientResponseEncrypted::new(); let mut packet = ClientResponseEncrypted::new();
packet packet
.mut_login_credentials() .mut_login_credentials()
@ -81,29 +108,30 @@ pub async fn authenticate(
packet packet
.mut_login_credentials() .mut_login_credentials()
.set_auth_data(credentials.auth_data); .set_auth_data(credentials.auth_data);
packet packet.mut_system_info().set_cpu_family(cpu_family);
.mut_system_info() packet.mut_system_info().set_os(os);
.set_cpu_family(CpuFamily::CPU_UNKNOWN);
packet.mut_system_info().set_os(Os::OS_UNKNOWN);
packet packet
.mut_system_info() .mut_system_info()
.set_system_information_string(format!( .set_system_information_string(format!(
"librespot_{}_{}", "librespot-{}-{}",
version::SHA_SHORT, version::SHA_SHORT,
version::BUILD_ID version::BUILD_ID
)); ));
packet packet
.mut_system_info() .mut_system_info()
.set_device_id(device_id.to_string()); .set_device_id(device_id.to_string());
packet.set_version_string(version::VERSION_STRING.to_string()); packet.set_version_string(format!("librespot {}", version::SEMVER));
let cmd = PacketType::Login; let cmd = PacketType::Login;
let data = packet.write_to_bytes().unwrap(); let data = packet.write_to_bytes()?;
transport.send((cmd as u8, data)).await?; transport.send((cmd as u8, data)).await?;
let (cmd, data) = transport.next().await.expect("EOF")?; let (cmd, data) = transport
.next()
.await
.ok_or(AuthenticationError::Transport)??;
let packet_type = FromPrimitive::from_u8(cmd); let packet_type = FromPrimitive::from_u8(cmd);
match packet_type { let result = match packet_type {
Some(PacketType::APWelcome) => { Some(PacketType::APWelcome) => {
let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?; let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?;
@ -120,8 +148,13 @@ pub async fn authenticate(
Err(error_data.into()) Err(error_data.into())
} }
_ => { _ => {
let msg = format!("Received invalid packet: {}", cmd); trace!(
Err(io::Error::new(ErrorKind::InvalidData, msg).into()) "Did not expect {:?} AES key packet with data {:#?}",
} cmd,
data
);
Err(AuthenticationError::Packet(cmd))
} }
};
Ok(result?)
} }

79
core/src/date.rs Normal file
View file

@ -0,0 +1,79 @@
use std::{convert::TryFrom, fmt::Debug, ops::Deref};
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use thiserror::Error;
use crate::Error;
use librespot_protocol as protocol;
use protocol::metadata::Date as DateMessage;
#[derive(Debug, Error)]
pub enum DateError {
#[error("item has invalid timestamp {0}")]
Timestamp(i64),
}
impl From<DateError> for Error {
fn from(err: DateError) -> Self {
Error::invalid_argument(err)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date(pub DateTime<Utc>);
impl Deref for Date {
type Target = DateTime<Utc>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Date {
pub fn as_timestamp(&self) -> i64 {
self.0.timestamp()
}
pub fn from_timestamp(timestamp: i64) -> Result<Self, Error> {
if let Some(date_time) = NaiveDateTime::from_timestamp_opt(timestamp, 0) {
Ok(Self::from_utc(date_time))
} else {
Err(DateError::Timestamp(timestamp).into())
}
}
pub fn as_utc(&self) -> DateTime<Utc> {
self.0
}
pub fn from_utc(date_time: NaiveDateTime) -> Self {
Self(DateTime::<Utc>::from_utc(date_time, Utc))
}
}
impl From<&DateMessage> for Date {
fn from(date: &DateMessage) -> Self {
let naive_date = NaiveDate::from_ymd(
date.get_year() as i32,
date.get_month() as u32,
date.get_day() as u32,
);
let naive_time = NaiveTime::from_hms(date.get_hour() as u32, date.get_minute() as u32, 0);
let naive_datetime = NaiveDateTime::new(naive_date, naive_time);
Self(DateTime::<Utc>::from_utc(naive_datetime, Utc))
}
}
impl From<DateTime<Utc>> for Date {
fn from(date: DateTime<Utc>) -> Self {
Self(date)
}
}
impl TryFrom<i64> for Date {
type Error = crate::Error;
fn try_from(timestamp: i64) -> Result<Self, Self::Error> {
Self::from_timestamp(timestamp)
}
}

View file

@ -1,7 +1,20 @@
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Debug)] use thiserror::Error;
pub struct AlreadyHandledError(());
use crate::Error;
#[derive(Debug, Error)]
pub enum HandlerMapError {
#[error("request was already handled")]
AlreadyHandled,
}
impl From<HandlerMapError> for Error {
fn from(err: HandlerMapError) -> Self {
Error::aborted(err)
}
}
pub enum HandlerMap<T> { pub enum HandlerMap<T> {
Leaf(T), Leaf(T),
@ -19,9 +32,9 @@ impl<T> HandlerMap<T> {
&mut self, &mut self,
mut path: impl Iterator<Item = &'a str>, mut path: impl Iterator<Item = &'a str>,
handler: T, handler: T,
) -> Result<(), AlreadyHandledError> { ) -> Result<(), Error> {
match self { match self {
Self::Leaf(_) => Err(AlreadyHandledError(())), Self::Leaf(_) => Err(HandlerMapError::AlreadyHandled.into()),
Self::Branch(children) => { Self::Branch(children) => {
if let Some(component) = path.next() { if let Some(component) = path.next() {
let node = children.entry(component.to_owned()).or_default(); let node = children.entry(component.to_owned()).or_default();
@ -30,7 +43,7 @@ impl<T> HandlerMap<T> {
*self = Self::Leaf(handler); *self = Self::Leaf(handler);
Ok(()) Ok(())
} else { } else {
Err(AlreadyHandledError(())) Err(HandlerMapError::AlreadyHandled.into())
} }
} }
} }

View file

@ -1,29 +1,41 @@
mod maps; mod maps;
pub mod protocol; pub mod protocol;
use std::iter; use std::{
use std::pin::Pin; iter,
use std::sync::atomic::AtomicBool; pin::Pin,
use std::sync::{atomic, Arc, Mutex}; sync::{
use std::task::Poll; atomic::{self, AtomicBool},
use std::time::Duration; Arc,
},
task::Poll,
time::Duration,
};
use futures_core::{Future, Stream}; use futures_core::{Future, Stream};
use futures_util::future::join_all; use futures_util::{future::join_all, SinkExt, StreamExt};
use futures_util::{SinkExt, StreamExt}; use parking_lot::Mutex;
use thiserror::Error; use thiserror::Error;
use tokio::select; use tokio::{
use tokio::sync::mpsc::{self, UnboundedReceiver}; select,
use tokio::sync::Semaphore; sync::{
use tokio::task::JoinHandle; mpsc::{self, UnboundedReceiver},
Semaphore,
},
task::JoinHandle,
};
use tokio_tungstenite::tungstenite; use tokio_tungstenite::tungstenite;
use tungstenite::error::UrlError; use tungstenite::error::UrlError;
use url::Url; use url::Url;
use self::maps::*; use self::maps::*;
use self::protocol::*; use self::protocol::*;
use crate::socket;
use crate::util::{keep_flushing, CancelOnDrop, TimeoutOnDrop}; use crate::{
socket,
util::{keep_flushing, CancelOnDrop, TimeoutOnDrop},
Error,
};
type WsMessage = tungstenite::Message; type WsMessage = tungstenite::Message;
type WsError = tungstenite::Error; type WsError = tungstenite::Error;
@ -164,24 +176,38 @@ fn split_uri(s: &str) -> Option<impl Iterator<Item = &'_ str>> {
pub enum AddHandlerError { pub enum AddHandlerError {
#[error("There is already a handler for the given uri")] #[error("There is already a handler for the given uri")]
AlreadyHandled, AlreadyHandled,
#[error("The specified uri is invalid")] #[error("The specified uri {0} is invalid")]
InvalidUri, InvalidUri(String),
}
impl From<AddHandlerError> for Error {
fn from(err: AddHandlerError) -> Self {
match err {
AddHandlerError::AlreadyHandled => Error::aborted(err),
AddHandlerError::InvalidUri(_) => Error::invalid_argument(err),
}
}
} }
#[derive(Debug, Clone, Error)] #[derive(Debug, Clone, Error)]
pub enum SubscriptionError { pub enum SubscriptionError {
#[error("The specified uri is invalid")] #[error("The specified uri is invalid")]
InvalidUri, InvalidUri(String),
}
impl From<SubscriptionError> for Error {
fn from(err: SubscriptionError) -> Self {
Error::invalid_argument(err)
}
} }
fn add_handler( fn add_handler(
map: &mut HandlerMap<Box<dyn RequestHandler>>, map: &mut HandlerMap<Box<dyn RequestHandler>>,
uri: &str, uri: &str,
handler: impl RequestHandler, handler: impl RequestHandler,
) -> Result<(), AddHandlerError> { ) -> Result<(), Error> {
let split = split_uri(uri).ok_or(AddHandlerError::InvalidUri)?; let split = split_uri(uri).ok_or_else(|| AddHandlerError::InvalidUri(uri.to_string()))?;
map.insert(split, Box::new(handler)) map.insert(split, Box::new(handler))
.map_err(|_| AddHandlerError::AlreadyHandled)
} }
fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> { fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> {
@ -191,11 +217,11 @@ fn remove_handler<T>(map: &mut HandlerMap<T>, uri: &str) -> Option<T> {
fn subscribe( fn subscribe(
map: &mut SubscriberMap<MessageHandler>, map: &mut SubscriberMap<MessageHandler>,
uris: &[&str], uris: &[&str],
) -> Result<Subscription, SubscriptionError> { ) -> Result<Subscription, Error> {
let (tx, rx) = mpsc::unbounded_channel(); let (tx, rx) = mpsc::unbounded_channel();
for &uri in uris { for &uri in uris {
let split = split_uri(uri).ok_or(SubscriptionError::InvalidUri)?; let split = split_uri(uri).ok_or_else(|| SubscriptionError::InvalidUri(uri.to_string()))?;
map.insert(split, tx.clone()); map.insert(split, tx.clone());
} }
@ -237,15 +263,11 @@ impl Builder {
Self::default() Self::default()
} }
pub fn add_handler( pub fn add_handler(&mut self, uri: &str, handler: impl RequestHandler) -> Result<(), Error> {
&mut self,
uri: &str,
handler: impl RequestHandler,
) -> Result<(), AddHandlerError> {
add_handler(&mut self.request_handlers, uri, handler) add_handler(&mut self.request_handlers, uri, handler)
} }
pub fn subscribe(&mut self, uris: &[&str]) -> Result<Subscription, SubscriptionError> { pub fn subscribe(&mut self, uris: &[&str]) -> Result<Subscription, Error> {
subscribe(&mut self.message_handlers, uris) subscribe(&mut self.message_handlers, uris)
} }
@ -289,7 +311,6 @@ impl DealerShared {
if let Some(split) = split_uri(&msg.uri) { if let Some(split) = split_uri(&msg.uri) {
self.message_handlers self.message_handlers
.lock() .lock()
.unwrap()
.retain(split, &mut |tx| tx.send(msg.clone()).is_ok()); .retain(split, &mut |tx| tx.send(msg.clone()).is_ok());
} }
} }
@ -309,7 +330,7 @@ impl DealerShared {
}; };
{ {
let handler_map = self.request_handlers.lock().unwrap(); let handler_map = self.request_handlers.lock();
if let Some(handler) = handler_map.get(split) { if let Some(handler) = handler_map.get(split) {
handler.handle_request(request, responder); handler.handle_request(request, responder);
@ -328,7 +349,9 @@ impl DealerShared {
} }
async fn closed(&self) { async fn closed(&self) {
self.notify_drop.acquire().await.unwrap_err(); if self.notify_drop.acquire().await.is_ok() {
error!("should never have gotten a permit");
}
} }
fn is_closed(&self) -> bool { fn is_closed(&self) -> bool {
@ -342,23 +365,19 @@ pub struct Dealer {
} }
impl Dealer { impl Dealer {
pub fn add_handler<H>(&self, uri: &str, handler: H) -> Result<(), AddHandlerError> pub fn add_handler<H>(&self, uri: &str, handler: H) -> Result<(), Error>
where where
H: RequestHandler, H: RequestHandler,
{ {
add_handler( add_handler(&mut self.shared.request_handlers.lock(), uri, handler)
&mut self.shared.request_handlers.lock().unwrap(),
uri,
handler,
)
} }
pub fn remove_handler(&self, uri: &str) -> Option<Box<dyn RequestHandler>> { pub fn remove_handler(&self, uri: &str) -> Option<Box<dyn RequestHandler>> {
remove_handler(&mut self.shared.request_handlers.lock().unwrap(), uri) remove_handler(&mut self.shared.request_handlers.lock(), uri)
} }
pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, SubscriptionError> { pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, Error> {
subscribe(&mut self.shared.message_handlers.lock().unwrap(), uris) subscribe(&mut self.shared.message_handlers.lock(), uris)
} }
pub async fn close(mut self) { pub async fn close(mut self) {
@ -367,7 +386,9 @@ impl Dealer {
self.shared.notify_drop.close(); self.shared.notify_drop.close();
if let Some(handle) = self.handle.take() { if let Some(handle) = self.handle.take() {
CancelOnDrop(handle).await.unwrap(); if let Err(e) = CancelOnDrop(handle).await {
error!("error aborting dealer operations: {}", e);
}
} }
} }
} }
@ -401,7 +422,7 @@ async fn connect(
// Spawn a task that will forward messages from the channel to the websocket. // Spawn a task that will forward messages from the channel to the websocket.
let send_task = { let send_task = {
let shared = Arc::clone(&shared); let shared = Arc::clone(shared);
tokio::spawn(async move { tokio::spawn(async move {
let result = loop { let result = loop {
@ -450,7 +471,7 @@ async fn connect(
}) })
}; };
let shared = Arc::clone(&shared); let shared = Arc::clone(shared);
// A task that receives messages from the web socket. // A task that receives messages from the web socket.
let receive_task = tokio::spawn(async { let receive_task = tokio::spawn(async {
@ -556,11 +577,15 @@ async fn run<F, Fut>(
select! { select! {
() = shared.closed() => break, () = shared.closed() => break,
r = t0 => { r = t0 => {
r.unwrap(); // Whatever has gone wrong (probably panicked), we can't handle it, so let's panic too. if let Err(e) = r {
error!("timeout on task 0: {}", e);
}
tasks.0.take(); tasks.0.take();
}, },
r = t1 => { r = t1 => {
r.unwrap(); if let Err(e) = r {
error!("timeout on task 1: {}", e);
}
tasks.1.take(); tasks.1.take();
} }
} }
@ -576,7 +601,7 @@ async fn run<F, Fut>(
match connect(&url, proxy.as_ref(), &shared).await { match connect(&url, proxy.as_ref(), &shared).await {
Ok((s, r)) => tasks = (init_task(s), init_task(r)), Ok((s, r)) => tasks = (init_task(s), init_task(r)),
Err(e) => { Err(e) => {
warn!("Error while connecting: {}", e); error!("Error while connecting: {}", e);
tokio::time::sleep(RECONNECT_INTERVAL).await; tokio::time::sleep(RECONNECT_INTERVAL).await;
} }
} }

447
core/src/error.rs Normal file
View file

@ -0,0 +1,447 @@
use std::{error, fmt, num::ParseIntError, str::Utf8Error, string::FromUtf8Error};
use base64::DecodeError;
use http::{
header::{InvalidHeaderName, InvalidHeaderValue, ToStrError},
method::InvalidMethod,
status::InvalidStatusCode,
uri::{InvalidUri, InvalidUriParts},
};
use protobuf::ProtobufError;
use thiserror::Error;
use tokio::sync::{mpsc::error::SendError, oneshot::error::RecvError};
use url::ParseError;
#[cfg(feature = "with-dns-sd")]
use dns_sd::DNSError;
#[derive(Debug)]
pub struct Error {
pub kind: ErrorKind,
pub error: Box<dyn error::Error + Send + Sync>,
}
#[derive(Clone, Copy, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
pub enum ErrorKind {
#[error("The operation was cancelled by the caller")]
Cancelled = 1,
#[error("Unknown error")]
Unknown = 2,
#[error("Client specified an invalid argument")]
InvalidArgument = 3,
#[error("Deadline expired before operation could complete")]
DeadlineExceeded = 4,
#[error("Requested entity was not found")]
NotFound = 5,
#[error("Attempt to create entity that already exists")]
AlreadyExists = 6,
#[error("Permission denied")]
PermissionDenied = 7,
#[error("No valid authentication credentials")]
Unauthenticated = 16,
#[error("Resource has been exhausted")]
ResourceExhausted = 8,
#[error("Invalid state")]
FailedPrecondition = 9,
#[error("Operation aborted")]
Aborted = 10,
#[error("Operation attempted past the valid range")]
OutOfRange = 11,
#[error("Not implemented")]
Unimplemented = 12,
#[error("Internal error")]
Internal = 13,
#[error("Service unavailable")]
Unavailable = 14,
#[error("Unrecoverable data loss or corruption")]
DataLoss = 15,
#[error("Operation must not be used")]
DoNotUse = -1,
}
#[derive(Debug, Error)]
struct ErrorMessage(String);
impl fmt::Display for ErrorMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error {
pub fn new<E>(kind: ErrorKind, error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind,
error: error.into(),
}
}
pub fn aborted<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Aborted,
error: error.into(),
}
}
pub fn already_exists<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::AlreadyExists,
error: error.into(),
}
}
pub fn cancelled<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Cancelled,
error: error.into(),
}
}
pub fn data_loss<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::DataLoss,
error: error.into(),
}
}
pub fn deadline_exceeded<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::DeadlineExceeded,
error: error.into(),
}
}
pub fn do_not_use<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::DoNotUse,
error: error.into(),
}
}
pub fn failed_precondition<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::FailedPrecondition,
error: error.into(),
}
}
pub fn internal<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Internal,
error: error.into(),
}
}
pub fn invalid_argument<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::InvalidArgument,
error: error.into(),
}
}
pub fn not_found<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::NotFound,
error: error.into(),
}
}
pub fn out_of_range<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::OutOfRange,
error: error.into(),
}
}
pub fn permission_denied<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::PermissionDenied,
error: error.into(),
}
}
pub fn resource_exhausted<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::ResourceExhausted,
error: error.into(),
}
}
pub fn unauthenticated<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unauthenticated,
error: error.into(),
}
}
pub fn unavailable<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unavailable,
error: error.into(),
}
}
pub fn unimplemented<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unimplemented,
error: error.into(),
}
}
pub fn unknown<E>(error: E) -> Error
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Self {
kind: ErrorKind::Unknown,
error: error.into(),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.error.source()
}
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "{} {{ ", self.kind)?;
self.error.fmt(fmt)?;
write!(fmt, " }}")
}
}
impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
#[cfg(feature = "with-dns-sd")]
impl From<DNSError> for Error {
fn from(err: DNSError) -> Self {
Self::new(ErrorKind::Unavailable, err)
}
}
impl From<http::Error> for Error {
fn from(err: http::Error) -> Self {
if err.is::<InvalidHeaderName>()
|| err.is::<InvalidHeaderValue>()
|| err.is::<InvalidMethod>()
|| err.is::<InvalidUri>()
|| err.is::<InvalidUriParts>()
{
return Self::new(ErrorKind::InvalidArgument, err);
}
if err.is::<InvalidStatusCode>() {
return Self::new(ErrorKind::FailedPrecondition, err);
}
Self::new(ErrorKind::Unknown, err)
}
}
impl From<hyper::Error> for Error {
fn from(err: hyper::Error) -> Self {
if err.is_parse() || err.is_parse_too_large() || err.is_parse_status() || err.is_user() {
return Self::new(ErrorKind::Internal, err);
}
if err.is_canceled() {
return Self::new(ErrorKind::Cancelled, err);
}
if err.is_connect() {
return Self::new(ErrorKind::Unavailable, err);
}
if err.is_incomplete_message() {
return Self::new(ErrorKind::DataLoss, err);
}
if err.is_body_write_aborted() || err.is_closed() {
return Self::new(ErrorKind::Aborted, err);
}
if err.is_timeout() {
return Self::new(ErrorKind::DeadlineExceeded, err);
}
Self::new(ErrorKind::Unknown, err)
}
}
impl From<quick_xml::Error> for Error {
fn from(err: quick_xml::Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
use std::io::ErrorKind as IoErrorKind;
match err.kind() {
IoErrorKind::NotFound => Self::new(ErrorKind::NotFound, err),
IoErrorKind::PermissionDenied => Self::new(ErrorKind::PermissionDenied, err),
IoErrorKind::AddrInUse | IoErrorKind::AlreadyExists => {
Self::new(ErrorKind::AlreadyExists, err)
}
IoErrorKind::AddrNotAvailable
| IoErrorKind::ConnectionRefused
| IoErrorKind::NotConnected => Self::new(ErrorKind::Unavailable, err),
IoErrorKind::BrokenPipe
| IoErrorKind::ConnectionReset
| IoErrorKind::ConnectionAborted => Self::new(ErrorKind::Aborted, err),
IoErrorKind::Interrupted | IoErrorKind::WouldBlock => {
Self::new(ErrorKind::Cancelled, err)
}
IoErrorKind::InvalidData | IoErrorKind::UnexpectedEof => {
Self::new(ErrorKind::FailedPrecondition, err)
}
IoErrorKind::TimedOut => Self::new(ErrorKind::DeadlineExceeded, err),
IoErrorKind::InvalidInput => Self::new(ErrorKind::InvalidArgument, err),
IoErrorKind::WriteZero => Self::new(ErrorKind::ResourceExhausted, err),
_ => Self::new(ErrorKind::Unknown, err),
}
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<InvalidHeaderValue> for Error {
fn from(err: InvalidHeaderValue) -> Self {
Self::new(ErrorKind::InvalidArgument, err)
}
}
impl From<InvalidUri> for Error {
fn from(err: InvalidUri) -> Self {
Self::new(ErrorKind::InvalidArgument, err)
}
}
impl From<ParseError> for Error {
fn from(err: ParseError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<ParseIntError> for Error {
fn from(err: ParseIntError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<ProtobufError> for Error {
fn from(err: ProtobufError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<RecvError> for Error {
fn from(err: RecvError) -> Self {
Self::new(ErrorKind::Internal, err)
}
}
impl<T> From<SendError<T>> for Error {
fn from(err: SendError<T>) -> Self {
Self {
kind: ErrorKind::Internal,
error: ErrorMessage(err.to_string()).into(),
}
}
}
impl From<ToStrError> for Error {
fn from(err: ToStrError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}
impl From<Utf8Error> for Error {
fn from(err: Utf8Error) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
}
}

55
core/src/file_id.rs Normal file
View file

@ -0,0 +1,55 @@
use std::fmt;
use librespot_protocol as protocol;
use crate::spotify_id::to_base16;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileId(pub [u8; 20]);
impl FileId {
pub fn from_raw(src: &[u8]) -> FileId {
let mut dst = [0u8; 20];
dst.clone_from_slice(src);
FileId(dst)
}
pub fn to_base16(&self) -> String {
to_base16(&self.0, &mut [0u8; 40])
}
}
impl fmt::Debug for FileId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("FileId").field(&self.to_base16()).finish()
}
}
impl fmt::Display for FileId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.to_base16())
}
}
impl From<&[u8]> for FileId {
fn from(src: &[u8]) -> Self {
Self::from_raw(src)
}
}
impl From<&protocol::metadata::Image> for FileId {
fn from(image: &protocol::metadata::Image) -> Self {
Self::from(image.get_file_id())
}
}
impl From<&protocol::metadata::AudioFile> for FileId {
fn from(file: &protocol::metadata::AudioFile) -> Self {
Self::from(file.get_file_id())
}
}
impl From<&protocol::metadata::VideoFile> for FileId {
fn from(video: &protocol::metadata::VideoFile) -> Self {
Self::from(video.get_file_id())
}
}

View file

@ -1,34 +1,180 @@
use hyper::client::HttpConnector; use std::env::consts::OS;
use hyper::{Body, Client, Request, Response};
use bytes::Bytes;
use futures_util::{future::IntoStream, FutureExt};
use http::header::HeaderValue;
use hyper::{
client::{HttpConnector, ResponseFuture},
header::USER_AGENT,
Body, Client, Request, Response, StatusCode,
};
use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_proxy::{Intercept, Proxy, ProxyConnector};
use hyper_rustls::HttpsConnector;
use rustls::{ClientConfig, RootCertStore};
use thiserror::Error;
use url::Url; use url::Url;
use crate::{
version::{FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING},
Error,
};
#[derive(Debug, Error)]
pub enum HttpClientError {
#[error("Response status code: {0}")]
StatusCode(hyper::StatusCode),
}
impl From<HttpClientError> for Error {
fn from(err: HttpClientError) -> Self {
match err {
HttpClientError::StatusCode(code) => {
// not exhaustive, but what reasonably could be expected
match code {
StatusCode::GATEWAY_TIMEOUT | StatusCode::REQUEST_TIMEOUT => {
Error::deadline_exceeded(err)
}
StatusCode::GONE
| StatusCode::NOT_FOUND
| StatusCode::MOVED_PERMANENTLY
| StatusCode::PERMANENT_REDIRECT
| StatusCode::TEMPORARY_REDIRECT => Error::not_found(err),
StatusCode::FORBIDDEN | StatusCode::PAYMENT_REQUIRED => {
Error::permission_denied(err)
}
StatusCode::NETWORK_AUTHENTICATION_REQUIRED
| StatusCode::PROXY_AUTHENTICATION_REQUIRED
| StatusCode::UNAUTHORIZED => Error::unauthenticated(err),
StatusCode::EXPECTATION_FAILED
| StatusCode::PRECONDITION_FAILED
| StatusCode::PRECONDITION_REQUIRED => Error::failed_precondition(err),
StatusCode::RANGE_NOT_SATISFIABLE => Error::out_of_range(err),
StatusCode::INTERNAL_SERVER_ERROR
| StatusCode::MISDIRECTED_REQUEST
| StatusCode::SERVICE_UNAVAILABLE
| StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => Error::unavailable(err),
StatusCode::BAD_REQUEST
| StatusCode::HTTP_VERSION_NOT_SUPPORTED
| StatusCode::LENGTH_REQUIRED
| StatusCode::METHOD_NOT_ALLOWED
| StatusCode::NOT_ACCEPTABLE
| StatusCode::PAYLOAD_TOO_LARGE
| StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE
| StatusCode::UNSUPPORTED_MEDIA_TYPE
| StatusCode::URI_TOO_LONG => Error::invalid_argument(err),
StatusCode::TOO_MANY_REQUESTS => Error::resource_exhausted(err),
StatusCode::NOT_IMPLEMENTED => Error::unimplemented(err),
_ => Error::unknown(err),
}
}
}
}
}
pub struct HttpClient { pub struct HttpClient {
user_agent: HeaderValue,
proxy: Option<Url>, proxy: Option<Url>,
tls_config: ClientConfig,
} }
impl HttpClient { impl HttpClient {
pub fn new(proxy: Option<&Url>) -> Self { pub fn new(proxy: Option<&Url>) -> Self {
let spotify_version = match OS {
"android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(),
_ => SPOTIFY_VERSION.to_string(),
};
let spotify_platform = match OS {
"android" => "Android/31",
"ios" => "iOS/15.2",
"macos" => "OSX/0",
"windows" => "Win32/0",
_ => "Linux/0",
};
let user_agent_str = &format!(
"Spotify/{} {} ({})",
spotify_version, spotify_platform, VERSION_STRING
);
let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| {
error!("Invalid user agent <{}>: {}", user_agent_str, err);
error!("Please report this as a bug.");
HeaderValue::from_static(FALLBACK_USER_AGENT)
});
// configuring TLS is expensive and should be done once per process
let root_store = match rustls_native_certs::load_native_certs() {
Ok(store) => store,
Err((Some(store), err)) => {
warn!("Could not load all certificates: {:?}", err);
store
}
Err((None, err)) => {
error!("Cannot access native certificate store: {}", err);
error!("Continuing, but most requests will probably fail until you fix your system certificate store.");
RootCertStore::empty()
}
};
let mut tls_config = ClientConfig::new();
tls_config.root_store = root_store;
tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
Self { Self {
user_agent,
proxy: proxy.cloned(), proxy: proxy.cloned(),
tls_config,
} }
} }
pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, hyper::Error> { pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, Error> {
if let Some(url) = &self.proxy { debug!("Requesting {:?}", req.uri().to_string());
// Panic safety: all URLs are valid URIs
let uri = url.to_string().parse().unwrap(); let request = self.request_fut(req)?;
let proxy = Proxy::new(Intercept::All, uri); let response = request.await;
let connector = HttpConnector::new();
let proxy_connector = ProxyConnector::from_proxy_unsecured(connector, proxy); if let Ok(response) = &response {
Client::builder().build(proxy_connector).request(req).await let code = response.status();
} else { if code != StatusCode::OK {
Client::new().request(req).await return Err(HttpClientError::StatusCode(code).into());
} }
} }
pub async fn request_body(&self, req: Request<Body>) -> Result<bytes::Bytes, hyper::Error> { Ok(response?)
}
pub async fn request_body(&self, req: Request<Body>) -> Result<Bytes, Error> {
let response = self.request(req).await?; let response = self.request(req).await?;
hyper::body::to_bytes(response.into_body()).await Ok(hyper::body::to_bytes(response.into_body()).await?)
}
pub fn request_stream(&self, req: Request<Body>) -> Result<IntoStream<ResponseFuture>, Error> {
Ok(self.request_fut(req)?.into_stream())
}
pub fn request_fut(&self, mut req: Request<Body>) -> Result<ResponseFuture, Error> {
let mut http = HttpConnector::new();
http.enforce_http(false);
let https_connector = HttpsConnector::from((http, self.tls_config.clone()));
let headers_mut = req.headers_mut();
headers_mut.insert(USER_AGENT, self.user_agent.clone());
let request = if let Some(url) = &self.proxy {
let proxy_uri = url.to_string().parse()?;
let proxy = Proxy::new(Intercept::All, proxy_uri);
let proxy_connector = ProxyConnector::from_proxy(https_connector, proxy)?;
Client::builder().build(proxy_connector).request(req)
} else {
Client::builder()
.http2_adaptive_window(true)
.build(https_connector)
.request(req)
};
Ok(request)
} }
} }

View file

@ -7,26 +7,37 @@ use librespot_protocol as protocol;
#[macro_use] #[macro_use]
mod component; mod component;
mod apresolve; pub mod apresolve;
pub mod audio_key; pub mod audio_key;
pub mod authentication; pub mod authentication;
pub mod cache; pub mod cache;
pub mod cdn_url;
pub mod channel; pub mod channel;
pub mod config; pub mod config;
mod connection; mod connection;
pub mod date;
#[allow(dead_code)] #[allow(dead_code)]
mod dealer; mod dealer;
#[doc(hidden)] #[doc(hidden)]
pub mod diffie_hellman; pub mod diffie_hellman;
pub mod error;
pub mod file_id;
mod http_client; mod http_client;
pub mod mercury; pub mod mercury;
pub mod packet; pub mod packet;
mod proxytunnel; mod proxytunnel;
pub mod session; pub mod session;
mod socket; mod socket;
mod spclient; #[allow(dead_code)]
pub mod spclient;
pub mod spotify_id; pub mod spotify_id;
mod token; pub mod token;
#[doc(hidden)] #[doc(hidden)]
pub mod util; pub mod util;
pub mod version; pub mod version;
pub use config::SessionConfig;
pub use error::Error;
pub use file_id::FileId;
pub use session::Session;
pub use spotify_id::SpotifyId;

View file

@ -1,9 +1,10 @@
use std::collections::HashMap; use std::{
use std::future::Future; collections::HashMap,
use std::mem; future::Future,
use std::pin::Pin; mem,
use std::task::Context; pin::Pin,
use std::task::Poll; task::{Context, Poll},
};
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes; use bytes::Bytes;
@ -11,9 +12,7 @@ use futures_util::FutureExt;
use protobuf::Message; use protobuf::Message;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use crate::packet::PacketType; use crate::{packet::PacketType, protocol, util::SeqGenerator, Error};
use crate::protocol;
use crate::util::SeqGenerator;
mod types; mod types;
pub use self::types::*; pub use self::types::*;
@ -33,18 +32,18 @@ component! {
pub struct MercuryPending { pub struct MercuryPending {
parts: Vec<Vec<u8>>, parts: Vec<Vec<u8>>,
partial: Option<Vec<u8>>, partial: Option<Vec<u8>>,
callback: Option<oneshot::Sender<Result<MercuryResponse, MercuryError>>>, callback: Option<oneshot::Sender<Result<MercuryResponse, Error>>>,
} }
pub struct MercuryFuture<T> { pub struct MercuryFuture<T> {
receiver: oneshot::Receiver<Result<T, MercuryError>>, receiver: oneshot::Receiver<Result<T, Error>>,
} }
impl<T> Future for MercuryFuture<T> { impl<T> Future for MercuryFuture<T> {
type Output = Result<T, MercuryError>; type Output = Result<T, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.receiver.poll_unpin(cx).map_err(|_| MercuryError)? self.receiver.poll_unpin(cx)?
} }
} }
@ -55,7 +54,7 @@ impl MercuryManager {
seq seq
} }
fn request(&self, req: MercuryRequest) -> MercuryFuture<MercuryResponse> { fn request(&self, req: MercuryRequest) -> Result<MercuryFuture<MercuryResponse>, Error> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let pending = MercuryPending { let pending = MercuryPending {
@ -72,13 +71,13 @@ impl MercuryManager {
}); });
let cmd = req.method.command(); let cmd = req.method.command();
let data = req.encode(&seq); let data = req.encode(&seq)?;
self.session().send_packet(cmd, data); self.session().send_packet(cmd, data)?;
MercuryFuture { receiver: rx } Ok(MercuryFuture { receiver: rx })
} }
pub fn get<T: Into<String>>(&self, uri: T) -> MercuryFuture<MercuryResponse> { pub fn get<T: Into<String>>(&self, uri: T) -> Result<MercuryFuture<MercuryResponse>, Error> {
self.request(MercuryRequest { self.request(MercuryRequest {
method: MercuryMethod::Get, method: MercuryMethod::Get,
uri: uri.into(), uri: uri.into(),
@ -87,7 +86,11 @@ impl MercuryManager {
}) })
} }
pub fn send<T: Into<String>>(&self, uri: T, data: Vec<u8>) -> MercuryFuture<MercuryResponse> { pub fn send<T: Into<String>>(
&self,
uri: T,
data: Vec<u8>,
) -> Result<MercuryFuture<MercuryResponse>, Error> {
self.request(MercuryRequest { self.request(MercuryRequest {
method: MercuryMethod::Send, method: MercuryMethod::Send,
uri: uri.into(), uri: uri.into(),
@ -103,7 +106,7 @@ impl MercuryManager {
pub fn subscribe<T: Into<String>>( pub fn subscribe<T: Into<String>>(
&self, &self,
uri: T, uri: T,
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<MercuryResponse>, MercuryError>> + 'static ) -> impl Future<Output = Result<mpsc::UnboundedReceiver<MercuryResponse>, Error>> + 'static
{ {
let uri = uri.into(); let uri = uri.into();
let request = self.request(MercuryRequest { let request = self.request(MercuryRequest {
@ -115,7 +118,7 @@ impl MercuryManager {
let manager = self.clone(); let manager = self.clone();
async move { async move {
let response = request.await?; let response = request?.await?;
let (tx, rx) = mpsc::unbounded_channel(); let (tx, rx) = mpsc::unbounded_channel();
@ -125,14 +128,19 @@ impl MercuryManager {
if !response.payload.is_empty() { if !response.payload.is_empty() {
// Old subscription protocol, watch the provided list of URIs // Old subscription protocol, watch the provided list of URIs
for sub in response.payload { for sub in response.payload {
let mut sub = match protocol::pubsub::Subscription::parse_from_bytes(&sub) {
protocol::pubsub::Subscription::parse_from_bytes(&sub).unwrap(); Ok(mut sub) => {
let sub_uri = sub.take_uri(); let sub_uri = sub.take_uri();
debug!("subscribed sub_uri={}", sub_uri); debug!("subscribed sub_uri={}", sub_uri);
inner.subscriptions.push((sub_uri, tx.clone())); inner.subscriptions.push((sub_uri, tx.clone()));
} }
Err(e) => {
error!("could not subscribe to {}: {}", uri, e);
}
}
}
} else { } else {
// New subscription protocol, watch the requested URI // New subscription protocol, watch the requested URI
inner.subscriptions.push((uri, tx)); inner.subscriptions.push((uri, tx));
@ -144,7 +152,28 @@ impl MercuryManager {
} }
} }
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { pub fn listen_for<T: Into<String>>(
&self,
uri: T,
) -> impl Future<Output = mpsc::UnboundedReceiver<MercuryResponse>> + 'static {
let uri = uri.into();
let manager = self.clone();
async move {
let (tx, rx) = mpsc::unbounded_channel();
manager.lock(move |inner| {
if !inner.invalid {
debug!("listening to uri={}", uri);
inner.subscriptions.push((uri, tx));
}
});
rx
}
}
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize; let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
let seq = data.split_to(seq_len).as_ref().to_owned(); let seq = data.split_to(seq_len).as_ref().to_owned();
@ -164,7 +193,7 @@ impl MercuryManager {
} }
} else { } else {
warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8); warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8);
return; return Err(MercuryError::Command(cmd).into());
} }
} }
}; };
@ -184,10 +213,12 @@ impl MercuryManager {
} }
if flags == 0x1 { if flags == 0x1 {
self.complete_request(cmd, pending); self.complete_request(cmd, pending)?;
} else { } else {
self.lock(move |inner| inner.pending.insert(seq, pending)); self.lock(move |inner| inner.pending.insert(seq, pending));
} }
Ok(())
} }
fn parse_part(data: &mut Bytes) -> Vec<u8> { fn parse_part(data: &mut Bytes) -> Vec<u8> {
@ -195,9 +226,9 @@ impl MercuryManager {
data.split_to(size).as_ref().to_owned() data.split_to(size).as_ref().to_owned()
} }
fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) { fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) -> Result<(), Error> {
let header_data = pending.parts.remove(0); let header_data = pending.parts.remove(0);
let header = protocol::mercury::Header::parse_from_bytes(&header_data).unwrap(); let header = protocol::mercury::Header::parse_from_bytes(&header_data)?;
let response = MercuryResponse { let response = MercuryResponse {
uri: header.get_uri().to_string(), uri: header.get_uri().to_string(),
@ -205,13 +236,17 @@ impl MercuryManager {
payload: pending.parts, payload: pending.parts,
}; };
if response.status_code >= 500 { let status_code = response.status_code;
panic!("Spotify servers returned an error. Restart librespot."); if status_code >= 500 {
} else if response.status_code >= 400 { error!("error {} for uri {}", status_code, &response.uri);
warn!("error {} for uri {}", response.status_code, &response.uri); Err(MercuryError::Response(response).into())
} else if status_code >= 400 {
error!("error {} for uri {}", status_code, &response.uri);
if let Some(cb) = pending.callback { if let Some(cb) = pending.callback {
let _ = cb.send(Err(MercuryError)); cb.send(Err(MercuryError::Response(response.clone()).into()))
.map_err(|_| MercuryError::Channel)?;
} }
Err(MercuryError::Response(response).into())
} else if let PacketType::MercuryEvent = cmd { } else if let PacketType::MercuryEvent = cmd {
self.lock(|inner| { self.lock(|inner| {
let mut found = false; let mut found = false;
@ -221,7 +256,7 @@ impl MercuryManager {
// before sending while saving the subscription under its unencoded form. // before sending while saving the subscription under its unencoded form.
let mut uri_split = response.uri.split('/'); let mut uri_split = response.uri.split('/');
let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string()) let encoded_uri = std::iter::once(uri_split.next().unwrap_or_default().to_string())
.chain(uri_split.map(|component| { .chain(uri_split.map(|component| {
form_urlencoded::byte_serialize(component.as_bytes()).collect::<String>() form_urlencoded::byte_serialize(component.as_bytes()).collect::<String>()
})) }))
@ -242,11 +277,19 @@ impl MercuryManager {
}); });
if !found { if !found {
debug!("unknown subscription uri={}", response.uri); debug!("unknown subscription uri={}", &response.uri);
trace!("response pushed over Mercury: {:?}", response);
Err(MercuryError::Response(response).into())
} else {
Ok(())
} }
}) })
} else if let Some(cb) = pending.callback { } else if let Some(cb) = pending.callback {
let _ = cb.send(Ok(response)); cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?;
Ok(())
} else {
error!("can't handle Mercury response: {:?}", response);
Err(MercuryError::Response(response).into())
} }
} }

View file

@ -1,6 +1,8 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use super::*; use super::{MercuryFuture, MercuryManager, MercuryResponse};
use crate::Error;
pub struct MercurySender { pub struct MercurySender {
mercury: MercuryManager, mercury: MercuryManager,
@ -23,12 +25,13 @@ impl MercurySender {
self.buffered_future.is_none() && self.pending.is_empty() self.buffered_future.is_none() && self.pending.is_empty()
} }
pub fn send(&mut self, item: Vec<u8>) { pub fn send(&mut self, item: Vec<u8>) -> Result<(), Error> {
let task = self.mercury.send(self.uri.clone(), item); let task = self.mercury.send(self.uri.clone(), item)?;
self.pending.push_back(task); self.pending.push_back(task);
Ok(())
} }
pub async fn flush(&mut self) -> Result<(), MercuryError> { pub async fn flush(&mut self) -> Result<(), Error> {
if self.buffered_future.is_none() { if self.buffered_future.is_none() {
self.buffered_future = self.pending.pop_front(); self.buffered_future = self.pending.pop_front();
} }

View file

@ -1,9 +1,10 @@
use byteorder::{BigEndian, WriteBytesExt};
use protobuf::Message;
use std::io::Write; use std::io::Write;
use crate::packet::PacketType; use byteorder::{BigEndian, WriteBytesExt};
use crate::protocol; use protobuf::Message;
use thiserror::Error;
use crate::{packet::PacketType, protocol, Error};
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum MercuryMethod { pub enum MercuryMethod {
@ -28,8 +29,25 @@ pub struct MercuryResponse {
pub payload: Vec<Vec<u8>>, pub payload: Vec<Vec<u8>>,
} }
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] #[derive(Debug, Error)]
pub struct MercuryError; pub enum MercuryError {
#[error("callback receiver was disconnected")]
Channel,
#[error("error handling packet type: {0:?}")]
Command(PacketType),
#[error("error handling Mercury response: {0:?}")]
Response(MercuryResponse),
}
impl From<MercuryError> for Error {
fn from(err: MercuryError) -> Self {
match err {
MercuryError::Channel => Error::aborted(err),
MercuryError::Command(_) => Error::unimplemented(err),
MercuryError::Response(_) => Error::unavailable(err),
}
}
}
impl ToString for MercuryMethod { impl ToString for MercuryMethod {
fn to_string(&self) -> String { fn to_string(&self) -> String {
@ -55,14 +73,12 @@ impl MercuryMethod {
} }
impl MercuryRequest { impl MercuryRequest {
pub fn encode(&self, seq: &[u8]) -> Vec<u8> { pub fn encode(&self, seq: &[u8]) -> Result<Vec<u8>, Error> {
let mut packet = Vec::new(); let mut packet = Vec::new();
packet.write_u16::<BigEndian>(seq.len() as u16).unwrap(); packet.write_u16::<BigEndian>(seq.len() as u16)?;
packet.write_all(seq).unwrap(); packet.write_all(seq)?;
packet.write_u8(1).unwrap(); // Flags: FINAL packet.write_u8(1)?; // Flags: FINAL
packet packet.write_u16::<BigEndian>(1 + self.payload.len() as u16)?; // Part count
.write_u16::<BigEndian>(1 + self.payload.len() as u16)
.unwrap(); // Part count
let mut header = protocol::mercury::Header::new(); let mut header = protocol::mercury::Header::new();
header.set_uri(self.uri.clone()); header.set_uri(self.uri.clone());
@ -72,16 +88,14 @@ impl MercuryRequest {
header.set_content_type(content_type.clone()); header.set_content_type(content_type.clone());
} }
packet packet.write_u16::<BigEndian>(header.compute_size() as u16)?;
.write_u16::<BigEndian>(header.compute_size() as u16) header.write_to_writer(&mut packet)?;
.unwrap();
header.write_to_writer(&mut packet).unwrap();
for p in &self.payload { for p in &self.payload {
packet.write_u16::<BigEndian>(p.len() as u16).unwrap(); packet.write_u16::<BigEndian>(p.len() as u16)?;
packet.write_all(p).unwrap(); packet.write_all(p)?;
} }
packet Ok(packet)
} }
} }

View file

@ -2,7 +2,7 @@
use num_derive::{FromPrimitive, ToPrimitive}; use num_derive::{FromPrimitive, ToPrimitive};
#[derive(Debug, FromPrimitive, ToPrimitive)] #[derive(Debug, Copy, Clone, FromPrimitive, ToPrimitive)]
pub enum PacketType { pub enum PacketType {
SecretBlock = 0x02, SecretBlock = 0x02,
Ping = 0x04, Ping = 0x04,

View file

@ -1,11 +1,16 @@
use std::future::Future; use std::{
use std::io; collections::HashMap,
use std::pin::Pin; future::Future,
use std::sync::atomic::{AtomicUsize, Ordering}; io,
use std::sync::{Arc, RwLock, Weak}; pin::Pin,
use std::task::Context; process::exit,
use std::task::Poll; sync::{
use std::time::{SystemTime, UNIX_EPOCH}; atomic::{AtomicUsize, Ordering},
Arc, Weak,
},
task::{Context, Poll},
time::{SystemTime, UNIX_EPOCH},
};
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes; use bytes::Bytes;
@ -13,21 +18,27 @@ use futures_core::TryStream;
use futures_util::{future, ready, StreamExt, TryStreamExt}; use futures_util::{future, ready, StreamExt, TryStreamExt};
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use quick_xml::events::Event;
use thiserror::Error; use thiserror::Error;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
use crate::apresolve::ApResolver; use crate::{
use crate::audio_key::AudioKeyManager; apresolve::ApResolver,
use crate::authentication::Credentials; audio_key::AudioKeyManager,
use crate::cache::Cache; authentication::Credentials,
use crate::channel::ChannelManager; cache::Cache,
use crate::config::SessionConfig; channel::ChannelManager,
use crate::connection::{self, AuthenticationError}; config::SessionConfig,
use crate::http_client::HttpClient; connection::{self, AuthenticationError},
use crate::mercury::MercuryManager; http_client::HttpClient,
use crate::packet::PacketType; mercury::MercuryManager,
use crate::token::TokenProvider; packet::PacketType,
spclient::SpClient,
token::TokenProvider,
Error,
};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum SessionError { pub enum SessionError {
@ -35,13 +46,35 @@ pub enum SessionError {
AuthenticationError(#[from] AuthenticationError), AuthenticationError(#[from] AuthenticationError),
#[error("Cannot create session: {0}")] #[error("Cannot create session: {0}")]
IoError(#[from] io::Error), IoError(#[from] io::Error),
#[error("packet {0} unknown")]
Packet(u8),
} }
impl From<SessionError> for Error {
fn from(err: SessionError) -> Self {
match err {
SessionError::AuthenticationError(_) => Error::unauthenticated(err),
SessionError::IoError(_) => Error::unavailable(err),
SessionError::Packet(_) => Error::unimplemented(err),
}
}
}
pub type UserAttributes = HashMap<String, String>;
#[derive(Debug, Clone, Default)]
pub struct UserData {
pub country: String,
pub canonical_username: String,
pub attributes: UserAttributes,
}
#[derive(Debug, Clone, Default)]
struct SessionData { struct SessionData {
country: String, connection_id: String,
time_delta: i64, time_delta: i64,
canonical_username: String,
invalid: bool, invalid: bool,
user_data: UserData,
} }
struct SessionInternal { struct SessionInternal {
@ -55,6 +88,7 @@ struct SessionInternal {
audio_key: OnceCell<AudioKeyManager>, audio_key: OnceCell<AudioKeyManager>,
channel: OnceCell<ChannelManager>, channel: OnceCell<ChannelManager>,
mercury: OnceCell<MercuryManager>, mercury: OnceCell<MercuryManager>,
spclient: OnceCell<SpClient>,
token_provider: OnceCell<TokenProvider>, token_provider: OnceCell<TokenProvider>,
cache: Option<Arc<Cache>>, cache: Option<Arc<Cache>>,
@ -73,7 +107,7 @@ impl Session {
config: SessionConfig, config: SessionConfig,
credentials: Credentials, credentials: Credentials,
cache: Option<Cache>, cache: Option<Cache>,
) -> Result<Session, SessionError> { ) -> Result<Session, Error> {
let http_client = HttpClient::new(config.proxy.as_ref()); let http_client = HttpClient::new(config.proxy.as_ref());
let (sender_tx, sender_rx) = mpsc::unbounded_channel(); let (sender_tx, sender_rx) = mpsc::unbounded_channel();
let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
@ -82,12 +116,7 @@ impl Session {
let session = Session(Arc::new(SessionInternal { let session = Session(Arc::new(SessionInternal {
config, config,
data: RwLock::new(SessionData { data: RwLock::new(SessionData::default()),
country: String::new(),
canonical_username: String::new(),
invalid: false,
time_delta: 0,
}),
http_client, http_client,
tx_connection: sender_tx, tx_connection: sender_tx,
cache: cache.map(Arc::new), cache: cache.map(Arc::new),
@ -95,6 +124,7 @@ impl Session {
audio_key: OnceCell::new(), audio_key: OnceCell::new(),
channel: OnceCell::new(), channel: OnceCell::new(),
mercury: OnceCell::new(), mercury: OnceCell::new(),
spclient: OnceCell::new(),
token_provider: OnceCell::new(), token_provider: OnceCell::new(),
handle: tokio::runtime::Handle::current(), handle: tokio::runtime::Handle::current(),
session_id, session_id,
@ -109,7 +139,7 @@ impl Session {
connection::authenticate(&mut transport, credentials, &session.config().device_id) connection::authenticate(&mut transport, credentials, &session.config().device_id)
.await?; .await?;
info!("Authenticated as \"{}\" !", reusable_credentials.username); info!("Authenticated as \"{}\" !", reusable_credentials.username);
session.0.data.write().unwrap().canonical_username = reusable_credentials.username.clone(); session.0.data.write().user_data.canonical_username = reusable_credentials.username.clone();
if let Some(cache) = session.cache() { if let Some(cache) = session.cache() {
cache.save_credentials(&reusable_credentials); cache.save_credentials(&reusable_credentials);
} }
@ -159,6 +189,10 @@ impl Session {
.get_or_init(|| MercuryManager::new(self.weak())) .get_or_init(|| MercuryManager::new(self.weak()))
} }
pub fn spclient(&self) -> &SpClient {
self.0.spclient.get_or_init(|| SpClient::new(self.weak()))
}
pub fn token_provider(&self) -> &TokenProvider { pub fn token_provider(&self) -> &TokenProvider {
self.0 self.0
.token_provider .token_provider
@ -166,7 +200,7 @@ impl Session {
} }
pub fn time_delta(&self) -> i64 { pub fn time_delta(&self) -> i64 {
self.0.data.read().unwrap().time_delta self.0.data.read().time_delta
} }
pub fn spawn<T>(&self, task: T) pub fn spawn<T>(&self, task: T)
@ -186,9 +220,30 @@ impl Session {
); );
} }
fn dispatch(&self, cmd: u8, data: Bytes) { fn check_catalogue(attributes: &UserAttributes) {
if let Some(account_type) = attributes.get("type") {
if account_type != "premium" {
error!("librespot does not support {:?} accounts.", account_type);
info!("Please support Spotify and your artists and sign up for a premium account.");
// TODO: logout instead of exiting
exit(1);
}
}
}
fn dispatch(&self, cmd: u8, data: Bytes) -> Result<(), Error> {
use PacketType::*; use PacketType::*;
let packet_type = FromPrimitive::from_u8(cmd); let packet_type = FromPrimitive::from_u8(cmd);
let cmd = match packet_type {
Some(cmd) => cmd,
None => {
trace!("Ignoring unknown packet {:x}", cmd);
return Err(SessionError::Packet(cmd).into());
}
};
match packet_type { match packet_type {
Some(Ping) => { Some(Ping) => {
let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64; let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64;
@ -198,43 +253,77 @@ impl Session {
} }
.as_secs() as i64; .as_secs() as i64;
self.0.data.write().unwrap().time_delta = server_timestamp - timestamp; self.0.data.write().time_delta = server_timestamp - timestamp;
self.debug_info(); self.debug_info();
self.send_packet(Pong, vec![0, 0, 0, 0]); self.send_packet(Pong, vec![0, 0, 0, 0])
} }
Some(CountryCode) => { Some(CountryCode) => {
let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); let country = String::from_utf8(data.as_ref().to_owned())?;
info!("Country: {:?}", country); info!("Country: {:?}", country);
self.0.data.write().unwrap().country = country; self.0.data.write().user_data.country = country;
} Ok(())
Some(StreamChunkRes) | Some(ChannelError) => {
self.channel().dispatch(packet_type.unwrap(), data);
}
Some(AesKey) | Some(AesKeyError) => {
self.audio_key().dispatch(packet_type.unwrap(), data);
} }
Some(StreamChunkRes) | Some(ChannelError) => self.channel().dispatch(cmd, data),
Some(AesKey) | Some(AesKeyError) => self.audio_key().dispatch(cmd, data),
Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => { Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => {
self.mercury().dispatch(packet_type.unwrap(), data); self.mercury().dispatch(cmd, data)
}
Some(ProductInfo) => {
let data = std::str::from_utf8(&data)?;
let mut reader = quick_xml::Reader::from_str(data);
let mut buf = Vec::new();
let mut current_element = String::new();
let mut user_attributes: UserAttributes = HashMap::new();
loop {
match reader.read_event(&mut buf) {
Ok(Event::Start(ref element)) => {
current_element = std::str::from_utf8(element.name())?.to_owned()
}
Ok(Event::End(_)) => {
current_element = String::new();
}
Ok(Event::Text(ref value)) => {
if !current_element.is_empty() {
let _ = user_attributes.insert(
current_element.clone(),
value.unescape_and_decode(&reader)?,
);
}
}
Ok(Event::Eof) => break,
Ok(_) => (),
Err(e) => error!(
"Error parsing XML at position {}: {:?}",
reader.buffer_position(),
e
),
}
}
trace!("Received product info: {:#?}", user_attributes);
Self::check_catalogue(&user_attributes);
self.0.data.write().user_data.attributes = user_attributes;
Ok(())
} }
Some(PongAck) Some(PongAck)
| Some(SecretBlock) | Some(SecretBlock)
| Some(LegacyWelcome) | Some(LegacyWelcome)
| Some(UnknownDataAllZeros) | Some(UnknownDataAllZeros)
| Some(ProductInfo) | Some(LicenseVersion) => Ok(()),
| Some(LicenseVersion) => {}
_ => { _ => {
if let Some(packet_type) = PacketType::from_u8(cmd) { trace!("Ignoring {:?} packet with data {:#?}", cmd, data);
trace!("Ignoring {:?} packet with data {:?}", packet_type, data); Err(SessionError::Packet(cmd as u8).into())
} else {
trace!("Ignoring unknown packet {:x}", cmd);
}
} }
} }
} }
pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) { pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) -> Result<(), Error> {
self.0.tx_connection.send((cmd as u8, data)).unwrap(); self.0.tx_connection.send((cmd as u8, data))?;
Ok(())
} }
pub fn cache(&self) -> Option<&Arc<Cache>> { pub fn cache(&self) -> Option<&Arc<Cache>> {
@ -245,18 +334,45 @@ impl Session {
&self.0.config &self.0.config
} }
pub fn username(&self) -> String { pub fn user_data(&self) -> UserData {
self.0.data.read().unwrap().canonical_username.clone() self.0.data.read().user_data.clone()
}
pub fn country(&self) -> String {
self.0.data.read().unwrap().country.clone()
} }
pub fn device_id(&self) -> &str { pub fn device_id(&self) -> &str {
&self.config().device_id &self.config().device_id
} }
pub fn connection_id(&self) -> String {
self.0.data.read().connection_id.clone()
}
pub fn set_connection_id(&self, connection_id: String) {
self.0.data.write().connection_id = connection_id;
}
pub fn username(&self) -> String {
self.0.data.read().user_data.canonical_username.clone()
}
pub fn set_user_attribute(&self, key: &str, value: &str) -> Option<String> {
let mut dummy_attributes = UserAttributes::new();
dummy_attributes.insert(key.to_owned(), value.to_owned());
Self::check_catalogue(&dummy_attributes);
self.0
.data
.write()
.user_data
.attributes
.insert(key.to_owned(), value.to_owned())
}
pub fn set_user_attributes(&self, attributes: UserAttributes) {
Self::check_catalogue(&attributes);
self.0.data.write().user_data.attributes.extend(attributes)
}
fn weak(&self) -> SessionWeak { fn weak(&self) -> SessionWeak {
SessionWeak(Arc::downgrade(&self.0)) SessionWeak(Arc::downgrade(&self.0))
} }
@ -266,14 +382,14 @@ impl Session {
} }
pub fn shutdown(&self) { pub fn shutdown(&self) {
debug!("Invalidating session[{}]", self.0.session_id); debug!("Invalidating session [{}]", self.0.session_id);
self.0.data.write().unwrap().invalid = true; self.0.data.write().invalid = true;
self.mercury().shutdown(); self.mercury().shutdown();
self.channel().shutdown(); self.channel().shutdown();
} }
pub fn is_invalid(&self) -> bool { pub fn is_invalid(&self) -> bool {
self.0.data.read().unwrap().invalid self.0.data.read().invalid
} }
} }
@ -286,7 +402,8 @@ impl SessionWeak {
} }
pub(crate) fn upgrade(&self) -> Session { pub(crate) fn upgrade(&self) -> Session {
self.try_upgrade().expect("Session died") self.try_upgrade()
.expect("session was dropped and so should have this component")
} }
} }
@ -327,7 +444,9 @@ where
} }
}; };
session.dispatch(cmd, data); if let Err(e) = session.dispatch(cmd, data) {
error!("could not dispatch command: {}", e);
}
} }
} }
} }

View file

@ -1,5 +1,4 @@
use std::io; use std::{io, net::ToSocketAddrs};
use std::net::ToSocketAddrs;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use url::Url; use url::Url;

View file

@ -1 +1,293 @@
// https://github.com/librespot-org/librespot-java/blob/27783e06f456f95228c5ac37acf2bff8c1a8a0c4/lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java use std::time::Duration;
use bytes::Bytes;
use futures_util::future::IntoStream;
use http::header::HeaderValue;
use hyper::{
client::ResponseFuture,
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE},
Body, HeaderMap, Method, Request,
};
use protobuf::Message;
use rand::Rng;
use crate::{
apresolve::SocketAddress,
cdn_url::CdnUrl,
error::ErrorKind,
protocol::{
canvaz::EntityCanvazRequest, connect::PutStateRequest,
extended_metadata::BatchedEntityRequest,
},
Error, FileId, SpotifyId,
};
component! {
SpClient : SpClientInner {
accesspoint: Option<SocketAddress> = None,
strategy: RequestStrategy = RequestStrategy::default(),
}
}
pub type SpClientResult = Result<Bytes, Error>;
#[derive(Copy, Clone, Debug)]
pub enum RequestStrategy {
TryTimes(usize),
Infinitely,
}
impl Default for RequestStrategy {
fn default() -> Self {
RequestStrategy::TryTimes(10)
}
}
impl SpClient {
pub fn set_strategy(&self, strategy: RequestStrategy) {
self.lock(|inner| inner.strategy = strategy)
}
pub async fn flush_accesspoint(&self) {
self.lock(|inner| inner.accesspoint = None)
}
pub async fn get_accesspoint(&self) -> SocketAddress {
// Memoize the current access point.
let ap = self.lock(|inner| inner.accesspoint.clone());
match ap {
Some(tuple) => tuple,
None => {
let tuple = self.session().apresolver().resolve("spclient").await;
self.lock(|inner| inner.accesspoint = Some(tuple.clone()));
info!(
"Resolved \"{}:{}\" as spclient access point",
tuple.0, tuple.1
);
tuple
}
}
}
pub async fn base_url(&self) -> String {
let ap = self.get_accesspoint().await;
format!("https://{}:{}", ap.0, ap.1)
}
pub async fn request_with_protobuf(
&self,
method: &Method,
endpoint: &str,
headers: Option<HeaderMap>,
message: &dyn Message,
) -> SpClientResult {
let body = protobuf::text_format::print_to_string(message);
let mut headers = headers.unwrap_or_else(HeaderMap::new);
headers.insert(CONTENT_TYPE, "application/protobuf".parse()?);
self.request(method, endpoint, Some(headers), Some(body))
.await
}
pub async fn request_as_json(
&self,
method: &Method,
endpoint: &str,
headers: Option<HeaderMap>,
body: Option<String>,
) -> SpClientResult {
let mut headers = headers.unwrap_or_else(HeaderMap::new);
headers.insert(ACCEPT, "application/json".parse()?);
self.request(method, endpoint, Some(headers), body).await
}
pub async fn request(
&self,
method: &Method,
endpoint: &str,
headers: Option<HeaderMap>,
body: Option<String>,
) -> SpClientResult {
let mut tries: usize = 0;
let mut last_response;
let body = body.unwrap_or_else(String::new);
loop {
tries += 1;
// Reconnection logic: retrieve the endpoint every iteration, so we can try
// another access point when we are experiencing network issues (see below).
let mut url = self.base_url().await;
url.push_str(endpoint);
let mut request = Request::builder()
.method(method)
.uri(url)
.body(Body::from(body.clone()))?;
// Reconnection logic: keep getting (cached) tokens because they might have expired.
let headers_mut = request.headers_mut();
if let Some(ref hdrs) = headers {
*headers_mut = hdrs.clone();
}
headers_mut.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!(
"Bearer {}",
self.session()
.token_provider()
.get_token("playlist-read")
.await?
.access_token
))?,
);
last_response = self.session().http_client().request_body(request).await;
if last_response.is_ok() {
return last_response;
}
// Break before the reconnection logic below, so that the current access point
// is retained when max_tries == 1. Leave it up to the caller when to flush.
if let RequestStrategy::TryTimes(max_tries) = self.lock(|inner| inner.strategy) {
if tries >= max_tries {
break;
}
}
// Reconnection logic: drop the current access point if we are experiencing issues.
// This will cause the next call to base_url() to resolve a new one.
if let Err(ref network_error) = last_response {
match network_error.kind {
ErrorKind::Unavailable | ErrorKind::DeadlineExceeded => {
// Keep trying the current access point three times before dropping it.
if tries % 3 == 0 {
self.flush_accesspoint().await
}
}
_ => break, // if we can't build the request now, then we won't ever
}
}
// When retrying, avoid hammering the Spotify infrastructure by sleeping a while.
// The backoff time is chosen randomly from an ever-increasing range.
let max_seconds = u64::pow(tries as u64, 2) * 3;
let backoff = Duration::from_secs(rand::thread_rng().gen_range(1..=max_seconds));
warn!(
"Unable to complete API request, waiting {} seconds before retrying...",
backoff.as_secs(),
);
debug!("Error was: {:?}", last_response);
tokio::time::sleep(backoff).await;
}
last_response
}
pub async fn put_connect_state(
&self,
connection_id: String,
state: PutStateRequest,
) -> SpClientResult {
let endpoint = format!("/connect-state/v1/devices/{}", self.session().device_id());
let mut headers = HeaderMap::new();
headers.insert("X-Spotify-Connection-Id", connection_id.parse()?);
self.request_with_protobuf(&Method::PUT, &endpoint, Some(headers), &state)
.await
}
pub async fn get_metadata(&self, scope: &str, id: SpotifyId) -> SpClientResult {
let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16());
self.request(&Method::GET, &endpoint, None, None).await
}
pub async fn get_track_metadata(&self, track_id: SpotifyId) -> SpClientResult {
self.get_metadata("track", track_id).await
}
pub async fn get_episode_metadata(&self, episode_id: SpotifyId) -> SpClientResult {
self.get_metadata("episode", episode_id).await
}
pub async fn get_album_metadata(&self, album_id: SpotifyId) -> SpClientResult {
self.get_metadata("album", album_id).await
}
pub async fn get_artist_metadata(&self, artist_id: SpotifyId) -> SpClientResult {
self.get_metadata("artist", artist_id).await
}
pub async fn get_show_metadata(&self, show_id: SpotifyId) -> SpClientResult {
self.get_metadata("show", show_id).await
}
pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult {
let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62());
self.request_as_json(&Method::GET, &endpoint, None, None)
.await
}
pub async fn get_lyrics_for_image(
&self,
track_id: SpotifyId,
image_id: FileId,
) -> SpClientResult {
let endpoint = format!(
"/color-lyrics/v2/track/{}/image/spotify:image:{}",
track_id.to_base62(),
image_id
);
self.request_as_json(&Method::GET, &endpoint, None, None)
.await
}
// TODO: Find endpoint for newer canvas.proto and upgrade to that.
pub async fn get_canvases(&self, request: EntityCanvazRequest) -> SpClientResult {
let endpoint = "/canvaz-cache/v0/canvases";
self.request_with_protobuf(&Method::POST, endpoint, None, &request)
.await
}
pub async fn get_extended_metadata(&self, request: BatchedEntityRequest) -> SpClientResult {
let endpoint = "/extended-metadata/v0/extended-metadata";
self.request_with_protobuf(&Method::POST, endpoint, None, &request)
.await
}
pub async fn get_audio_storage(&self, file_id: FileId) -> SpClientResult {
let endpoint = format!(
"/storage-resolve/files/audio/interactive/{}",
file_id.to_base16()
);
self.request(&Method::GET, &endpoint, None, None).await
}
pub fn stream_from_cdn(
&self,
cdn_url: &CdnUrl,
offset: usize,
length: usize,
) -> Result<IntoStream<ResponseFuture>, Error> {
let url = cdn_url.try_get_url()?;
let req = Request::builder()
.method(&Method::GET)
.uri(url)
.header(
RANGE,
HeaderValue::from_str(&format!("bytes={}-{}", offset, offset + length - 1))?,
)
.body(Body::empty())?;
let stream = self.session().http_client().request_stream(req)?;
Ok(stream)
}
}

View file

@ -1,43 +1,81 @@
#![allow(clippy::wrong_self_convention)] use std::{
convert::{TryFrom, TryInto},
fmt,
ops::Deref,
};
use std::convert::TryInto; use thiserror::Error;
use std::fmt;
use crate::Error;
use librespot_protocol as protocol;
// re-export FileId for historic reasons, when it was part of this mod
pub use crate::FileId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpotifyAudioType { pub enum SpotifyItemType {
Album,
Artist,
Episode,
Playlist,
Show,
Track, Track,
Podcast, Unknown,
NonPlayable,
} }
impl From<&str> for SpotifyAudioType { impl From<&str> for SpotifyItemType {
fn from(v: &str) -> Self { fn from(v: &str) -> Self {
match v { match v {
"track" => SpotifyAudioType::Track, "album" => Self::Album,
"episode" => SpotifyAudioType::Podcast, "artist" => Self::Artist,
_ => SpotifyAudioType::NonPlayable, "episode" => Self::Episode,
"playlist" => Self::Playlist,
"show" => Self::Show,
"track" => Self::Track,
_ => Self::Unknown,
} }
} }
} }
impl From<SpotifyAudioType> for &str { impl From<SpotifyItemType> for &str {
fn from(audio_type: SpotifyAudioType) -> &'static str { fn from(item_type: SpotifyItemType) -> &'static str {
match audio_type { match item_type {
SpotifyAudioType::Track => "track", SpotifyItemType::Album => "album",
SpotifyAudioType::Podcast => "episode", SpotifyItemType::Artist => "artist",
SpotifyAudioType::NonPlayable => "unknown", SpotifyItemType::Episode => "episode",
SpotifyItemType::Playlist => "playlist",
SpotifyItemType::Show => "show",
SpotifyItemType::Track => "track",
_ => "unknown",
} }
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpotifyId { pub struct SpotifyId {
pub id: u128, pub id: u128,
pub audio_type: SpotifyAudioType, pub item_type: SpotifyItemType,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
pub struct SpotifyIdError; pub enum SpotifyIdError {
#[error("ID cannot be parsed")]
InvalidId,
#[error("not a valid Spotify URI")]
InvalidFormat,
#[error("URI does not belong to Spotify")]
InvalidRoot,
}
impl From<SpotifyIdError> for Error {
fn from(err: SpotifyIdError) -> Self {
Error::invalid_argument(err)
}
}
pub type SpotifyIdResult = Result<SpotifyId, Error>;
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, Error>;
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef"; const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
@ -47,11 +85,12 @@ impl SpotifyId {
const SIZE_BASE16: usize = 32; const SIZE_BASE16: usize = 32;
const SIZE_BASE62: usize = 22; const SIZE_BASE62: usize = 22;
fn track(n: u128) -> SpotifyId { /// Returns whether this `SpotifyId` is for a playable audio item, if known.
SpotifyId { pub fn is_playable(&self) -> bool {
id: n, return matches!(
audio_type: SpotifyAudioType::Track, self.item_type,
} SpotifyItemType::Episode | SpotifyItemType::Track
);
} }
/// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`. /// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`.
@ -59,29 +98,32 @@ impl SpotifyId {
/// `src` is expected to be 32 bytes long and encoded using valid characters. /// `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 /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
pub fn from_base16(src: &str) -> Result<SpotifyId, SpotifyIdError> { pub fn from_base16(src: &str) -> SpotifyIdResult {
let mut dst: u128 = 0; let mut dst: u128 = 0;
for c in src.as_bytes() { for c in src.as_bytes() {
let p = match c { let p = match c {
b'0'..=b'9' => c - b'0', b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10, b'a'..=b'f' => c - b'a' + 10,
_ => return Err(SpotifyIdError), _ => return Err(SpotifyIdError::InvalidId.into()),
} as u128; } as u128;
dst <<= 4; dst <<= 4;
dst += p; dst += p;
} }
Ok(SpotifyId::track(dst)) Ok(Self {
id: dst,
item_type: SpotifyItemType::Unknown,
})
} }
/// Parses a base62 encoded [Spotify ID] into a `SpotifyId`. /// Parses a base62 encoded [Spotify ID] into a `u128`.
/// ///
/// `src` is expected to be 22 bytes long and encoded using valid characters. /// `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 /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
pub fn from_base62(src: &str) -> Result<SpotifyId, SpotifyIdError> { pub fn from_base62(src: &str) -> SpotifyIdResult {
let mut dst: u128 = 0; let mut dst: u128 = 0;
for c in src.as_bytes() { for c in src.as_bytes() {
@ -89,23 +131,29 @@ impl SpotifyId {
b'0'..=b'9' => c - b'0', b'0'..=b'9' => c - b'0',
b'a'..=b'z' => c - b'a' + 10, b'a'..=b'z' => c - b'a' + 10,
b'A'..=b'Z' => c - b'A' + 36, b'A'..=b'Z' => c - b'A' + 36,
_ => return Err(SpotifyIdError), _ => return Err(SpotifyIdError::InvalidId.into()),
} as u128; } as u128;
dst *= 62; dst *= 62;
dst += p; dst += p;
} }
Ok(SpotifyId::track(dst)) Ok(Self {
id: dst,
item_type: SpotifyItemType::Unknown,
})
} }
/// Creates a `SpotifyId` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order. /// Creates a `u128` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
/// ///
/// The resulting `SpotifyId` will default to a `SpotifyAudioType::TRACK`. /// The resulting `SpotifyId` will default to a `SpotifyItemType::Unknown`.
pub fn from_raw(src: &[u8]) -> Result<SpotifyId, SpotifyIdError> { pub fn from_raw(src: &[u8]) -> SpotifyIdResult {
match src.try_into() { match src.try_into() {
Ok(dst) => Ok(SpotifyId::track(u128::from_be_bytes(dst))), Ok(dst) => Ok(Self {
Err(_) => Err(SpotifyIdError), id: u128::from_be_bytes(dst),
item_type: SpotifyItemType::Unknown,
}),
Err(_) => Err(SpotifyIdError::InvalidId.into()),
} }
} }
@ -114,30 +162,37 @@ impl SpotifyId {
/// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}` /// `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. /// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID.
/// ///
/// Note that this should not be used for playlists, which have the form of
/// `spotify:user:{owner_username}:playlist:{id}`.
///
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
pub fn from_uri(src: &str) -> Result<SpotifyId, SpotifyIdError> { pub fn from_uri(src: &str) -> SpotifyIdResult {
let src = src.strip_prefix("spotify:").ok_or(SpotifyIdError)?; let mut uri_parts: Vec<&str> = src.split(':').collect();
if src.len() <= SpotifyId::SIZE_BASE62 { // At minimum, should be `spotify:{type}:{id}`
return Err(SpotifyIdError); if uri_parts.len() < 3 {
return Err(SpotifyIdError::InvalidFormat.into());
} }
let colon_index = src.len() - SpotifyId::SIZE_BASE62 - 1; if uri_parts[0] != "spotify" {
return Err(SpotifyIdError::InvalidRoot.into());
if src.as_bytes()[colon_index] != b':' {
return Err(SpotifyIdError);
} }
let mut id = SpotifyId::from_base62(&src[colon_index + 1..])?; let id = uri_parts.pop().unwrap_or_default();
id.audio_type = src[..colon_index].into(); if id.len() != Self::SIZE_BASE62 {
return Err(SpotifyIdError::InvalidId.into());
}
Ok(id) Ok(Self {
item_type: uri_parts.pop().unwrap_or_default().into(),
..Self::from_base62(id)?
})
} }
/// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32) /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32)
/// character long `String`. /// character long `String`.
pub fn to_base16(&self) -> String { pub fn to_base16(&self) -> String {
to_base16(&self.to_raw(), &mut [0u8; SpotifyId::SIZE_BASE16]) to_base16(&self.to_raw(), &mut [0u8; Self::SIZE_BASE16])
} }
/// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22) /// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22)
@ -190,7 +245,7 @@ impl SpotifyId {
/// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in /// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in
/// big-endian order. /// big-endian order.
pub fn to_raw(&self) -> [u8; SpotifyId::SIZE] { pub fn to_raw(&self) -> [u8; Self::SIZE] {
self.id.to_be_bytes() self.id.to_be_bytes()
} }
@ -204,11 +259,11 @@ impl SpotifyId {
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
pub fn to_uri(&self) -> String { pub fn to_uri(&self) -> String {
// 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31 // 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
// + unknown size audio_type. // + unknown size item_type.
let audio_type: &str = self.audio_type.into(); let item_type: &str = self.item_type.into();
let mut dst = String::with_capacity(31 + audio_type.len()); let mut dst = String::with_capacity(31 + item_type.len());
dst.push_str("spotify:"); dst.push_str("spotify:");
dst.push_str(audio_type); dst.push_str(item_type);
dst.push(':'); dst.push(':');
dst.push_str(&self.to_base62()); dst.push_str(&self.to_base62());
@ -216,29 +271,231 @@ impl SpotifyId {
} }
} }
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] impl fmt::Debug for SpotifyId {
pub struct FileId(pub [u8; 20]);
impl FileId {
pub fn to_base16(&self) -> String {
to_base16(&self.0, &mut [0u8; 40])
}
}
impl fmt::Debug for FileId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("FileId").field(&self.to_base16()).finish() f.debug_tuple("SpotifyId").field(&self.to_uri()).finish()
} }
} }
impl fmt::Display for FileId { impl fmt::Display for SpotifyId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.to_base16()) f.write_str(&self.to_uri())
} }
} }
#[inline] #[derive(Clone, PartialEq, Eq, Hash)]
fn to_base16(src: &[u8], buf: &mut [u8]) -> String { pub struct NamedSpotifyId {
pub inner_id: SpotifyId,
pub username: String,
}
impl NamedSpotifyId {
pub fn from_uri(src: &str) -> NamedSpotifyIdResult {
let uri_parts: Vec<&str> = src.split(':').collect();
// At minimum, should be `spotify:user:{username}:{type}:{id}`
if uri_parts.len() < 5 {
return Err(SpotifyIdError::InvalidFormat.into());
}
if uri_parts[0] != "spotify" {
return Err(SpotifyIdError::InvalidRoot.into());
}
if uri_parts[1] != "user" {
return Err(SpotifyIdError::InvalidFormat.into());
}
Ok(Self {
inner_id: SpotifyId::from_uri(src)?,
username: uri_parts[2].to_owned(),
})
}
pub fn to_uri(&self) -> String {
let item_type: &str = self.inner_id.item_type.into();
let mut dst = String::with_capacity(37 + self.username.len() + item_type.len());
dst.push_str("spotify:user:");
dst.push_str(&self.username);
dst.push_str(item_type);
dst.push(':');
dst.push_str(&self.to_base62());
dst
}
pub fn from_spotify_id(id: SpotifyId, username: String) -> Self {
Self {
inner_id: id,
username,
}
}
}
impl Deref for NamedSpotifyId {
type Target = SpotifyId;
fn deref(&self) -> &Self::Target {
&self.inner_id
}
}
impl fmt::Debug for NamedSpotifyId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("NamedSpotifyId")
.field(&self.inner_id.to_uri())
.finish()
}
}
impl fmt::Display for NamedSpotifyId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.inner_id.to_uri())
}
}
impl TryFrom<&[u8]> for SpotifyId {
type Error = crate::Error;
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
Self::from_raw(src)
}
}
impl TryFrom<&str> for SpotifyId {
type Error = crate::Error;
fn try_from(src: &str) -> Result<Self, Self::Error> {
Self::from_base62(src)
}
}
impl TryFrom<String> for SpotifyId {
type Error = crate::Error;
fn try_from(src: String) -> Result<Self, Self::Error> {
Self::try_from(src.as_str())
}
}
impl TryFrom<&Vec<u8>> for SpotifyId {
type Error = crate::Error;
fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(src.as_slice())
}
}
impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
type Error = crate::Error;
fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> {
match SpotifyId::from_raw(track.get_gid()) {
Ok(mut id) => {
id.item_type = SpotifyItemType::Track;
Ok(id)
}
Err(_) => SpotifyId::from_uri(track.get_uri()),
}
}
}
impl TryFrom<&protocol::metadata::Album> for SpotifyId {
type Error = crate::Error;
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Album,
..Self::from_raw(album.get_gid())?
})
}
}
impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
type Error = crate::Error;
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Artist,
..Self::from_raw(artist.get_gid())?
})
}
}
impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
type Error = crate::Error;
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Episode,
..Self::from_raw(episode.get_gid())?
})
}
}
impl TryFrom<&protocol::metadata::Track> for SpotifyId {
type Error = crate::Error;
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Track,
..Self::from_raw(track.get_gid())?
})
}
}
impl TryFrom<&protocol::metadata::Show> for SpotifyId {
type Error = crate::Error;
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Show,
..Self::from_raw(show.get_gid())?
})
}
}
impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
type Error = crate::Error;
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Artist,
..Self::from_raw(artist.get_artist_gid())?
})
}
}
impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Track,
..Self::from_uri(item.get_uri())?
})
}
}
// Note that this is the unique revision of an item's metadata on a playlist,
// not the ID of that item or playlist.
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
Self::try_from(item.get_revision())
}
}
// Note that this is the unique revision of a playlist, not the ID of that playlist.
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
type Error = crate::Error;
fn try_from(
playlist: &protocol::playlist4_external::SelectedListContent,
) -> Result<Self, Self::Error> {
Self::try_from(playlist.get_revision())
}
}
// TODO: check meaning and format of this field in the wild. This might be a FileId,
// which is why we now don't create a separate `Playlist` enum value yet and choose
// to discard any item type.
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
type Error = crate::Error;
fn try_from(
picture: &protocol::playlist_annotate3::TranscodedPicture,
) -> Result<Self, Self::Error> {
Self::from_base62(picture.get_uri())
}
}
pub fn to_base16(src: &[u8], buf: &mut [u8]) -> String {
let mut i = 0; let mut i = 0;
for v in src { for v in src {
buf[i] = BASE16_DIGITS[(v >> 4) as usize]; buf[i] = BASE16_DIGITS[(v >> 4) as usize];
@ -258,7 +515,7 @@ mod tests {
struct ConversionCase { struct ConversionCase {
id: u128, id: u128,
kind: SpotifyAudioType, kind: SpotifyItemType,
uri: &'static str, uri: &'static str,
base16: &'static str, base16: &'static str,
base62: &'static str, base62: &'static str,
@ -268,7 +525,7 @@ mod tests {
static CONV_VALID: [ConversionCase; 4] = [ static CONV_VALID: [ConversionCase; 4] = [
ConversionCase { ConversionCase {
id: 238762092608182713602505436543891614649, id: 238762092608182713602505436543891614649,
kind: SpotifyAudioType::Track, kind: SpotifyItemType::Track,
uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH", uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
base16: "b39fe8081e1f4c54be38e8d6f9f12bb9", base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
base62: "5sWHDYs0csV6RS48xBl0tH", base62: "5sWHDYs0csV6RS48xBl0tH",
@ -278,7 +535,7 @@ mod tests {
}, },
ConversionCase { ConversionCase {
id: 204841891221366092811751085145916697048, id: 204841891221366092811751085145916697048,
kind: SpotifyAudioType::Track, kind: SpotifyItemType::Track,
uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4", uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
base62: "4GNcXTGWmnZ3ySrqvol3o4", base62: "4GNcXTGWmnZ3ySrqvol3o4",
@ -288,7 +545,7 @@ mod tests {
}, },
ConversionCase { ConversionCase {
id: 204841891221366092811751085145916697048, id: 204841891221366092811751085145916697048,
kind: SpotifyAudioType::Podcast, kind: SpotifyItemType::Episode,
uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4", uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
base62: "4GNcXTGWmnZ3ySrqvol3o4", base62: "4GNcXTGWmnZ3ySrqvol3o4",
@ -298,8 +555,8 @@ mod tests {
}, },
ConversionCase { ConversionCase {
id: 204841891221366092811751085145916697048, id: 204841891221366092811751085145916697048,
kind: SpotifyAudioType::NonPlayable, kind: SpotifyItemType::Show,
uri: "spotify:unknown:4GNcXTGWmnZ3ySrqvol3o4", uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4",
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
base62: "4GNcXTGWmnZ3ySrqvol3o4", base62: "4GNcXTGWmnZ3ySrqvol3o4",
raw: &[ raw: &[
@ -311,7 +568,7 @@ mod tests {
static CONV_INVALID: [ConversionCase; 3] = [ static CONV_INVALID: [ConversionCase; 3] = [
ConversionCase { ConversionCase {
id: 0, id: 0,
kind: SpotifyAudioType::NonPlayable, kind: SpotifyItemType::Unknown,
// Invalid ID in the URI. // Invalid ID in the URI.
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH", uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9", base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
@ -323,7 +580,7 @@ mod tests {
}, },
ConversionCase { ConversionCase {
id: 0, id: 0,
kind: SpotifyAudioType::NonPlayable, kind: SpotifyItemType::Unknown,
// Missing colon between ID and type. // Missing colon between ID and type.
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
base16: "--------------------", base16: "--------------------",
@ -335,7 +592,7 @@ mod tests {
}, },
ConversionCase { ConversionCase {
id: 0, id: 0,
kind: SpotifyAudioType::NonPlayable, kind: SpotifyItemType::Unknown,
// Uri too short // Uri too short
uri: "spotify:azb:aRS48xBl0tH", uri: "spotify:azb:aRS48xBl0tH",
base16: "--------------------", base16: "--------------------",
@ -354,7 +611,7 @@ mod tests {
} }
for c in &CONV_INVALID { for c in &CONV_INVALID {
assert_eq!(SpotifyId::from_base62(c.base62), Err(SpotifyIdError)); assert!(SpotifyId::from_base62(c.base62).is_err(),);
} }
} }
@ -363,7 +620,7 @@ mod tests {
for c in &CONV_VALID { for c in &CONV_VALID {
let id = SpotifyId { let id = SpotifyId {
id: c.id, id: c.id,
audio_type: c.kind, item_type: c.kind,
}; };
assert_eq!(id.to_base62(), c.base62); assert_eq!(id.to_base62(), c.base62);
@ -377,7 +634,7 @@ mod tests {
} }
for c in &CONV_INVALID { for c in &CONV_INVALID {
assert_eq!(SpotifyId::from_base16(c.base16), Err(SpotifyIdError)); assert!(SpotifyId::from_base16(c.base16).is_err(),);
} }
} }
@ -386,7 +643,7 @@ mod tests {
for c in &CONV_VALID { for c in &CONV_VALID {
let id = SpotifyId { let id = SpotifyId {
id: c.id, id: c.id,
audio_type: c.kind, item_type: c.kind,
}; };
assert_eq!(id.to_base16(), c.base16); assert_eq!(id.to_base16(), c.base16);
@ -399,11 +656,11 @@ mod tests {
let actual = SpotifyId::from_uri(c.uri).unwrap(); let actual = SpotifyId::from_uri(c.uri).unwrap();
assert_eq!(actual.id, c.id); assert_eq!(actual.id, c.id);
assert_eq!(actual.audio_type, c.kind); assert_eq!(actual.item_type, c.kind);
} }
for c in &CONV_INVALID { for c in &CONV_INVALID {
assert_eq!(SpotifyId::from_uri(c.uri), Err(SpotifyIdError)); assert!(SpotifyId::from_uri(c.uri).is_err());
} }
} }
@ -412,7 +669,7 @@ mod tests {
for c in &CONV_VALID { for c in &CONV_VALID {
let id = SpotifyId { let id = SpotifyId {
id: c.id, id: c.id,
audio_type: c.kind, item_type: c.kind,
}; };
assert_eq!(id.to_uri(), c.uri); assert_eq!(id.to_uri(), c.uri);
@ -426,7 +683,7 @@ mod tests {
} }
for c in &CONV_INVALID { for c in &CONV_INVALID {
assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError)); assert!(SpotifyId::from_raw(c.raw).is_err());
} }
} }
} }

View file

@ -8,12 +8,12 @@
// user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming, // user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming,
// app-remote-control // app-remote-control
use crate::mercury::MercuryError; use std::time::{Duration, Instant};
use serde::Deserialize; use serde::Deserialize;
use thiserror::Error;
use std::error::Error; use crate::Error;
use std::time::{Duration, Instant};
component! { component! {
TokenProvider : TokenProviderInner { TokenProvider : TokenProviderInner {
@ -21,13 +21,25 @@ component! {
} }
} }
#[derive(Debug, Error)]
pub enum TokenError {
#[error("no tokens available")]
Empty,
}
impl From<TokenError> for Error {
fn from(err: TokenError) -> Self {
Error::unavailable(err)
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Token { pub struct Token {
access_token: String, pub access_token: String,
expires_in: Duration, pub expires_in: Duration,
token_type: String, pub token_type: String,
scopes: Vec<String>, pub scopes: Vec<String>,
timestamp: Instant, pub timestamp: Instant,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -54,11 +66,7 @@ impl TokenProvider {
} }
// scopes must be comma-separated // scopes must be comma-separated
pub async fn get_token(&self, scopes: &str) -> Result<Token, MercuryError> { pub async fn get_token(&self, scopes: &str) -> Result<Token, Error> {
if scopes.is_empty() {
return Err(MercuryError);
}
if let Some(index) = self.find_token(scopes.split(',').collect()) { if let Some(index) = self.find_token(scopes.split(',').collect()) {
let cached_token = self.lock(|inner| inner.tokens[index].clone()); let cached_token = self.lock(|inner| inner.tokens[index].clone());
if cached_token.is_expired() { if cached_token.is_expired() {
@ -79,15 +87,11 @@ impl TokenProvider {
Self::KEYMASTER_CLIENT_ID, Self::KEYMASTER_CLIENT_ID,
self.session().device_id() self.session().device_id()
); );
let request = self.session().mercury().get(query_uri); let request = self.session().mercury().get(query_uri)?;
let response = request.await?; let response = request.await?;
let data = response let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec();
.payload let token = Token::new(String::from_utf8(data)?)?;
.first() trace!("Got token: {:#?}", token);
.expect("No tokens received")
.to_vec();
let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?;
trace!("Got token: {:?}", token);
self.lock(|inner| inner.tokens.push(token.clone())); self.lock(|inner| inner.tokens.push(token.clone()));
Ok(token) Ok(token)
} }
@ -96,7 +100,7 @@ impl TokenProvider {
impl Token { impl Token {
const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10); const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10);
pub fn new(body: String) -> Result<Self, Box<dyn Error>> { pub fn new(body: String) -> Result<Self, Error> {
let data: TokenData = serde_json::from_slice(body.as_ref())?; let data: TokenData = serde_json::from_slice(body.as_ref())?;
Ok(Self { Ok(Self {
access_token: data.access_token, access_token: data.access_token,

View file

@ -1,15 +1,13 @@
use std::future::Future; use std::{
use std::mem; future::Future,
use std::pin::Pin; mem,
use std::task::Context; pin::Pin,
use std::task::Poll; task::{Context, Poll},
};
use futures_core::ready; use futures_core::ready;
use futures_util::FutureExt; use futures_util::{future, FutureExt, Sink, SinkExt};
use futures_util::Sink; use tokio::{task::JoinHandle, time::timeout};
use futures_util::{future, SinkExt};
use tokio::task::JoinHandle;
use tokio::time::timeout;
/// Returns a future that will flush the sink, even if flushing is temporarily completed. /// Returns a future that will flush the sink, even if flushing is temporarily completed.
/// Finishes only if the sink throws an error. /// Finishes only if the sink throws an error.

View file

@ -15,3 +15,12 @@ pub const SEMVER: &str = env!("CARGO_PKG_VERSION");
/// A random build id. /// A random build id.
pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID"); pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID");
/// The protocol version of the Spotify desktop client.
pub const SPOTIFY_VERSION: u64 = 117300517;
/// The protocol version of the Spotify mobile app.
pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84";
/// The user agent to fall back to, if one could not be determined dynamically.
pub const FALLBACK_USER_AGENT: &str = "Spotify/117300517 Linux/0 (librespot)";

View file

@ -18,7 +18,7 @@ async fn test_connection() {
match result { match result {
Ok(_) => panic!("Authentication succeeded despite of bad credentials."), Ok(_) => panic!("Authentication succeeded despite of bad credentials."),
Err(e) => assert_eq!(e.to_string(), "Login failed with reason: Bad credentials"), Err(e) => assert!(!e.to_string().is_empty()), // there should be some error message
} }
}) })
.await .await

View file

@ -1,6 +1,6 @@
[package] [package]
name = "librespot-discovery" name = "librespot-discovery"
version = "0.2.0" version = "0.3.1"
authors = ["Paul Lietar <paul@lietar.net>"] authors = ["Paul Lietar <paul@lietar.net>"]
description = "The discovery logic for librespot" description = "The discovery logic for librespot"
license = "MIT" license = "MIT"
@ -11,30 +11,28 @@ edition = "2018"
aes-ctr = "0.6" aes-ctr = "0.6"
base64 = "0.13" base64 = "0.13"
cfg-if = "1.0" cfg-if = "1.0"
dns-sd = { version = "0.1.3", optional = true }
form_urlencoded = "1.0" form_urlencoded = "1.0"
futures-core = "0.3" futures-core = "0.3"
futures-util = "0.3"
hmac = "0.11" hmac = "0.11"
hyper = { version = "0.14", features = ["server", "http1", "tcp"] } hyper = { version = "0.14", features = ["http1", "server", "tcp"] }
libmdns = "0.6" libmdns = "0.6"
log = "0.4" log = "0.4"
rand = "0.8" rand = "0.8"
serde_json = "1.0.25" serde_json = "1.0.25"
sha-1 = "0.9" sha-1 = "0.9"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1.0", features = ["sync", "rt"] } tokio = { version = "1.0", features = ["parking_lot", "sync", "rt"] }
dns-sd = { version = "0.1.3", optional = true }
[dependencies.librespot-core] [dependencies.librespot-core]
path = "../core" path = "../core"
default_features = false version = "0.3.1"
version = "0.2.0"
[dev-dependencies] [dev-dependencies]
futures = "0.3" futures = "0.3"
hex = "0.4" hex = "0.4"
simple_logger = "1.11" tokio = { version = "1.0", features = ["macros", "parking_lot", "rt"] }
tokio = { version = "1.0", features = ["macros", "rt"] }
[features] [features]
with-dns-sd = ["dns-sd"] with-dns-sd = ["dns-sd", "librespot-core/with-dns-sd"]

View file

@ -1,15 +1,9 @@
use futures::StreamExt; use futures::StreamExt;
use librespot_discovery::DeviceType; use librespot_discovery::DeviceType;
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use simple_logger::SimpleLogger;
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() {
SimpleLogger::new()
.with_level(log::LevelFilter::Debug)
.init()
.unwrap();
let name = "Librespot"; let name = "Librespot";
let device_id = hex::encode(Sha1::digest(name.as_bytes())); let device_id = hex::encode(Sha1::digest(name.as_bytes()));

View file

@ -7,8 +7,6 @@
//! This library uses mDNS and DNS-SD so that other devices can find it, //! This library uses mDNS and DNS-SD so that other devices can find it,
//! and spawns an http server to answer requests of Spotify clients. //! and spawns an http server to answer requests of Spotify clients.
#![warn(clippy::all, missing_docs, rust_2018_idioms)]
mod server; mod server;
use std::borrow::Cow; use std::borrow::Cow;
@ -29,6 +27,8 @@ pub use crate::core::authentication::Credentials;
/// Determining the icon in the list of available devices. /// Determining the icon in the list of available devices.
pub use crate::core::config::DeviceType; pub use crate::core::config::DeviceType;
pub use crate::core::Error;
/// Makes this device visible to Spotify clients in the local network. /// Makes this device visible to Spotify clients in the local network.
/// ///
/// `Discovery` implements the [`Stream`] trait. Every time this device /// `Discovery` implements the [`Stream`] trait. Every time this device
@ -50,13 +50,28 @@ pub struct Builder {
/// Errors that can occur while setting up a [`Discovery`] instance. /// Errors that can occur while setting up a [`Discovery`] instance.
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum DiscoveryError {
/// Setting up service discovery via DNS-SD failed. /// Setting up service discovery via DNS-SD failed.
#[error("Setting up dns-sd failed: {0}")] #[error("Setting up dns-sd failed: {0}")]
DnsSdError(#[from] io::Error), DnsSdError(#[from] io::Error),
/// Setting up the http server failed. /// Setting up the http server failed.
#[error("Creating SHA1 HMAC failed for base key {0:?}")]
HmacError(Vec<u8>),
#[error("Setting up the http server failed: {0}")] #[error("Setting up the http server failed: {0}")]
HttpServerError(#[from] hyper::Error), HttpServerError(#[from] hyper::Error),
#[error("Missing params for key {0}")]
ParamsError(&'static str),
}
impl From<DiscoveryError> for Error {
fn from(err: DiscoveryError) -> Self {
match err {
DiscoveryError::DnsSdError(_) => Error::unavailable(err),
DiscoveryError::HmacError(_) => Error::invalid_argument(err),
DiscoveryError::HttpServerError(_) => Error::unavailable(err),
DiscoveryError::ParamsError(_) => Error::invalid_argument(err),
}
}
} }
impl Builder { impl Builder {
@ -98,7 +113,7 @@ impl Builder {
pub fn launch(self) -> Result<Discovery, Error> { pub fn launch(self) -> Result<Discovery, Error> {
let mut port = self.port; let mut port = self.port;
let name = self.server_config.name.clone().into_owned(); let name = self.server_config.name.clone().into_owned();
let server = DiscoveryServer::new(self.server_config, &mut port)?; let server = DiscoveryServer::new(self.server_config, &mut port)??;
let svc; let svc;
@ -111,8 +126,7 @@ impl Builder {
None, None,
port, port,
&["VERSION=1.0", "CPath=/"], &["VERSION=1.0", "CPath=/"],
) )?;
.unwrap();
} else { } else {
let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?;

View file

@ -1,26 +1,35 @@
use std::borrow::Cow; use std::{
use std::collections::BTreeMap; borrow::Cow,
use std::convert::Infallible; collections::BTreeMap,
use std::net::{Ipv4Addr, SocketAddr}; convert::Infallible,
use std::pin::Pin; net::{Ipv4Addr, SocketAddr},
use std::sync::Arc; pin::Pin,
use std::task::{Context, Poll}; sync::Arc,
task::{Context, Poll},
};
use aes_ctr::cipher::generic_array::GenericArray; use aes_ctr::{
use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; cipher::generic_array::GenericArray,
use aes_ctr::Aes128Ctr; cipher::{NewStreamCipher, SyncStreamCipher},
Aes128Ctr,
};
use futures_core::Stream; use futures_core::Stream;
use futures_util::{FutureExt, TryFutureExt};
use hmac::{Hmac, Mac, NewMac}; use hmac::{Hmac, Mac, NewMac};
use hyper::service::{make_service_fn, service_fn}; use hyper::{
use hyper::{Body, Method, Request, Response, StatusCode}; service::{make_service_fn, service_fn},
use log::{debug, warn}; Body, Method, Request, Response, StatusCode,
};
use log::{debug, error, warn};
use serde_json::json; use serde_json::json;
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use crate::core::authentication::Credentials; use super::DiscoveryError;
use crate::core::config::DeviceType;
use crate::core::diffie_hellman::DhLocalKeys; use crate::core::{
authentication::Credentials, config::DeviceType, diffie_hellman::DhLocalKeys, Error,
};
type Params<'a> = BTreeMap<Cow<'a, str>, Cow<'a, str>>; type Params<'a> = BTreeMap<Cow<'a, str>, Cow<'a, str>>;
@ -57,7 +66,7 @@ impl RequestHandler {
"status": 101, "status": 101,
"statusString": "ERROR-OK", "statusString": "ERROR-OK",
"spotifyError": 0, "spotifyError": 0,
"version": "2.7.1", "version": crate::core::version::SEMVER,
"deviceID": (self.config.device_id), "deviceID": (self.config.device_id),
"remoteName": (self.config.name), "remoteName": (self.config.name),
"activeUser": "", "activeUser": "",
@ -76,14 +85,26 @@ impl RequestHandler {
Response::new(Body::from(body)) Response::new(Body::from(body))
} }
fn handle_add_user(&self, params: &Params<'_>) -> Response<hyper::Body> { fn handle_add_user(&self, params: &Params<'_>) -> Result<Response<hyper::Body>, Error> {
let username = params.get("userName").unwrap().as_ref(); let username_key = "userName";
let encrypted_blob = params.get("blob").unwrap(); let username = params
let client_key = params.get("clientKey").unwrap(); .get(username_key)
.ok_or(DiscoveryError::ParamsError(username_key))?
.as_ref();
let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); let blob_key = "blob";
let encrypted_blob = params
.get(blob_key)
.ok_or(DiscoveryError::ParamsError(blob_key))?;
let client_key = base64::decode(client_key.as_bytes()).unwrap(); let clientkey_key = "clientKey";
let client_key = params
.get(clientkey_key)
.ok_or(DiscoveryError::ParamsError(clientkey_key))?;
let encrypted_blob = base64::decode(encrypted_blob.as_bytes())?;
let client_key = base64::decode(client_key.as_bytes())?;
let shared_key = self.keys.shared_secret(&client_key); let shared_key = self.keys.shared_secret(&client_key);
let iv = &encrypted_blob[0..16]; let iv = &encrypted_blob[0..16];
@ -94,21 +115,21 @@ impl RequestHandler {
let base_key = &base_key[..16]; let base_key = &base_key[..16];
let checksum_key = { let checksum_key = {
let mut h = let mut h = Hmac::<Sha1>::new_from_slice(base_key)
Hmac::<Sha1>::new_from_slice(base_key).expect("HMAC can take key of any size"); .map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?;
h.update(b"checksum"); h.update(b"checksum");
h.finalize().into_bytes() h.finalize().into_bytes()
}; };
let encryption_key = { let encryption_key = {
let mut h = let mut h = Hmac::<Sha1>::new_from_slice(base_key)
Hmac::<Sha1>::new_from_slice(base_key).expect("HMAC can take key of any size"); .map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?;
h.update(b"encryption"); h.update(b"encryption");
h.finalize().into_bytes() h.finalize().into_bytes()
}; };
let mut h = let mut h = Hmac::<Sha1>::new_from_slice(&checksum_key)
Hmac::<Sha1>::new_from_slice(&checksum_key).expect("HMAC can take key of any size"); .map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?;
h.update(encrypted); h.update(encrypted);
if h.verify(cksum).is_err() { if h.verify(cksum).is_err() {
warn!("Login error for user {:?}: MAC mismatch", username); warn!("Login error for user {:?}: MAC mismatch", username);
@ -119,7 +140,7 @@ impl RequestHandler {
}); });
let body = result.to_string(); let body = result.to_string();
return Response::new(Body::from(body)); return Ok(Response::new(Body::from(body)));
} }
let decrypted = { let decrypted = {
@ -129,13 +150,12 @@ impl RequestHandler {
GenericArray::from_slice(iv), GenericArray::from_slice(iv),
); );
cipher.apply_keystream(&mut data); cipher.apply_keystream(&mut data);
String::from_utf8(data).unwrap() data
}; };
let credentials = let credentials = Credentials::with_blob(username, &decrypted, &self.config.device_id)?;
Credentials::with_blob(username.to_string(), &decrypted, &self.config.device_id);
self.tx.send(credentials).unwrap(); self.tx.send(credentials)?;
let result = json!({ let result = json!({
"status": 101, "status": 101,
@ -144,7 +164,7 @@ impl RequestHandler {
}); });
let body = result.to_string(); let body = result.to_string();
Response::new(Body::from(body)) Ok(Response::new(Body::from(body)))
} }
fn not_found(&self) -> Response<hyper::Body> { fn not_found(&self) -> Response<hyper::Body> {
@ -153,7 +173,10 @@ impl RequestHandler {
res res
} }
async fn handle(self: Arc<Self>, request: Request<Body>) -> hyper::Result<Response<Body>> { async fn handle(
self: Arc<Self>,
request: Request<Body>,
) -> Result<hyper::Result<Response<Body>>, Error> {
let mut params = Params::new(); let mut params = Params::new();
let (parts, body) = request.into_parts(); let (parts, body) = request.into_parts();
@ -173,11 +196,11 @@ impl RequestHandler {
let action = params.get("action").map(Cow::as_ref); let action = params.get("action").map(Cow::as_ref);
Ok(match (parts.method, action) { Ok(Ok(match (parts.method, action) {
(Method::GET, Some("getInfo")) => self.handle_get_info(), (Method::GET, Some("getInfo")) => self.handle_get_info(),
(Method::POST, Some("addUser")) => self.handle_add_user(&params), (Method::POST, Some("addUser")) => self.handle_add_user(&params)?,
_ => self.not_found(), _ => self.not_found(),
}) }))
} }
} }
@ -187,7 +210,7 @@ pub struct DiscoveryServer {
} }
impl DiscoveryServer { impl DiscoveryServer {
pub fn new(config: Config, port: &mut u16) -> hyper::Result<Self> { pub fn new(config: Config, port: &mut u16) -> Result<hyper::Result<Self>, Error> {
let (discovery, cred_rx) = RequestHandler::new(config); let (discovery, cred_rx) = RequestHandler::new(config);
let discovery = Arc::new(discovery); let discovery = Arc::new(discovery);
@ -198,7 +221,14 @@ impl DiscoveryServer {
let make_service = make_service_fn(move |_| { let make_service = make_service_fn(move |_| {
let discovery = discovery.clone(); let discovery = discovery.clone();
async move { async move {
Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().handle(request))) Ok::<_, hyper::Error>(service_fn(move |request| {
discovery
.clone()
.handle(request)
.inspect_err(|e| error!("could not handle discovery request: {}", e))
.and_then(|x| async move { Ok(x) })
.map(Result::unwrap) // guaranteed by `and_then` above
}))
} }
}); });
@ -210,8 +240,10 @@ impl DiscoveryServer {
tokio::spawn(async { tokio::spawn(async {
let result = server let result = server
.with_graceful_shutdown(async { .with_graceful_shutdown(async {
close_rx.await.unwrap_err();
debug!("Shutting down discovery server"); debug!("Shutting down discovery server");
if close_rx.await.is_ok() {
debug!("unable to close discovery Rx channel completely");
}
}) })
.await; .await;
@ -220,10 +252,10 @@ impl DiscoveryServer {
} }
}); });
Ok(Self { Ok(Ok(Self {
cred_rx, cred_rx,
_close_tx: close_tx, _close_tx: close_tx,
}) }))
} }
} }

View file

@ -57,20 +57,4 @@ login_data = AES192-DECRYPT(key, data)
``` ```
## Facebook based Authentication ## Facebook based Authentication
The client starts an HTTPS server, and makes the user visit Facebook authentication is currently broken due to Spotify changing the authentication flow. The details of how the new flow works are detailed in https://github.com/librespot-org/librespot/issues/244 and will be implemented at some point in the future.
`https://login.spotify.com/login-facebook-sso/?csrf=CSRF&port=PORT`
in their browser, where CSRF is a random token, and PORT is the HTTPS server's port.
This will redirect to Facebook, where the user must login and authorize Spotify, and
finally make a GET request to
`https://login.spotilocal.com:PORT/login/facebook_login_sso.json?csrf=CSRF&access_token=TOKEN`,
where PORT and CSRF are the same as sent earlier, and TOKEN is the facebook authentication token.
Since `login.spotilocal.com` resolves the 127.0.0.1, the request is received by the client.
The client must then contact Facebook's API at
`https://graph.facebook.com/me?fields=id&access_token=TOKEN`
in order to retrieve the user's Facebook ID.
The Facebook ID is the `username`, the TOKEN the `auth_data`, and `auth_type` is set to `AUTHENTICATION_FACEBOOK_TOKEN`.

View file

@ -30,7 +30,7 @@ async fn main() {
let plist = Playlist::get(&session, plist_uri).await.unwrap(); let plist = Playlist::get(&session, plist_uri).await.unwrap();
println!("{:?}", plist); println!("{:?}", plist);
for track_id in plist.tracks { for track_id in plist.tracks() {
let plist_track = Track::get(&session, track_id).await.unwrap(); let plist_track = Track::get(&session, track_id).await.unwrap();
println!("track: {} ", plist_track.name); println!("track: {} ", plist_track.name);
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "librespot-metadata" name = "librespot-metadata"
version = "0.2.0" version = "0.3.1"
authors = ["Paul Lietar <paul@lietar.net>"] authors = ["Paul Lietar <paul@lietar.net>"]
description = "The metadata logic for librespot" description = "The metadata logic for librespot"
license = "MIT" license = "MIT"
@ -10,12 +10,17 @@ edition = "2018"
[dependencies] [dependencies]
async-trait = "0.1" async-trait = "0.1"
byteorder = "1.3" byteorder = "1.3"
protobuf = "2.14.0" bytes = "1.0"
chrono = "0.4"
log = "0.4" log = "0.4"
protobuf = "2.14.0"
thiserror = "1"
uuid = { version = "0.8", default-features = false }
[dependencies.librespot-core] [dependencies.librespot-core]
path = "../core" path = "../core"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-protocol] [dependencies.librespot-protocol]
path = "../protocol" path = "../protocol"
version = "0.2.0" version = "0.3.1"

137
metadata/src/album.rs Normal file
View file

@ -0,0 +1,137 @@
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Deref,
};
use crate::{
artist::Artists, availability::Availabilities, copyright::Copyrights, external_id::ExternalIds,
image::Images, request::RequestResult, restriction::Restrictions, sale_period::SalePeriods,
track::Tracks, util::try_from_repeated_message, Metadata,
};
use librespot_core::{date::Date, Error, Session, SpotifyId};
use librespot_protocol as protocol;
pub use protocol::metadata::Album_Type as AlbumType;
use protocol::metadata::Disc as DiscMessage;
#[derive(Debug, Clone)]
pub struct Album {
pub id: SpotifyId,
pub name: String,
pub artists: Artists,
pub album_type: AlbumType,
pub label: String,
pub date: Date,
pub popularity: i32,
pub genres: Vec<String>,
pub covers: Images,
pub external_ids: ExternalIds,
pub discs: Discs,
pub reviews: Vec<String>,
pub copyrights: Copyrights,
pub restrictions: Restrictions,
pub related: Albums,
pub sale_periods: SalePeriods,
pub cover_group: Images,
pub original_title: String,
pub version_title: String,
pub type_str: String,
pub availability: Availabilities,
}
#[derive(Debug, Clone)]
pub struct Albums(pub Vec<SpotifyId>);
impl Deref for Albums {
type Target = Vec<SpotifyId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct Disc {
pub number: i32,
pub name: String,
pub tracks: Tracks,
}
#[derive(Debug, Clone)]
pub struct Discs(pub Vec<Disc>);
impl Deref for Discs {
type Target = Vec<Disc>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Album {
pub fn tracks(&self) -> Tracks {
let result = self
.discs
.iter()
.flat_map(|disc| disc.tracks.deref().clone())
.collect();
Tracks(result)
}
}
#[async_trait]
impl Metadata for Album {
type Message = protocol::metadata::Album;
async fn request(session: &Session, album_id: SpotifyId) -> RequestResult {
session.spclient().get_album_metadata(album_id).await
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Album {
type Error = librespot_core::Error;
fn try_from(album: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: album.try_into()?,
name: album.get_name().to_owned(),
artists: album.get_artist().try_into()?,
album_type: album.get_field_type(),
label: album.get_label().to_owned(),
date: album.get_date().into(),
popularity: album.get_popularity(),
genres: album.get_genre().to_vec(),
covers: album.get_cover().into(),
external_ids: album.get_external_id().into(),
discs: album.get_disc().try_into()?,
reviews: album.get_review().to_vec(),
copyrights: album.get_copyright().into(),
restrictions: album.get_restriction().into(),
related: album.get_related().try_into()?,
sale_periods: album.get_sale_period().into(),
cover_group: album.get_cover_group().get_image().into(),
original_title: album.get_original_title().to_owned(),
version_title: album.get_version_title().to_owned(),
type_str: album.get_type_str().to_owned(),
availability: album.get_availability().into(),
})
}
}
try_from_repeated_message!(<Album as Metadata>::Message, Albums);
impl TryFrom<&DiscMessage> for Disc {
type Error = librespot_core::Error;
fn try_from(disc: &DiscMessage) -> Result<Self, Self::Error> {
Ok(Self {
number: disc.get_number(),
name: disc.get_name().to_owned(),
tracks: disc.get_track().try_into()?,
})
}
}
try_from_repeated_message!(DiscMessage, Discs);

129
metadata/src/artist.rs Normal file
View file

@ -0,0 +1,129 @@
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Deref,
};
use crate::{request::RequestResult, track::Tracks, util::try_from_repeated_message, Metadata};
use librespot_core::{Error, Session, SpotifyId};
use librespot_protocol as protocol;
use protocol::metadata::ArtistWithRole as ArtistWithRoleMessage;
pub use protocol::metadata::ArtistWithRole_ArtistRole as ArtistRole;
use protocol::metadata::TopTracks as TopTracksMessage;
#[derive(Debug, Clone)]
pub struct Artist {
pub id: SpotifyId,
pub name: String,
pub top_tracks: CountryTopTracks,
}
#[derive(Debug, Clone)]
pub struct Artists(pub Vec<SpotifyId>);
impl Deref for Artists {
type Target = Vec<SpotifyId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct ArtistWithRole {
pub id: SpotifyId,
pub name: String,
pub role: ArtistRole,
}
#[derive(Debug, Clone)]
pub struct ArtistsWithRole(pub Vec<ArtistWithRole>);
impl Deref for ArtistsWithRole {
type Target = Vec<ArtistWithRole>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct TopTracks {
pub country: String,
pub tracks: Tracks,
}
#[derive(Debug, Clone)]
pub struct CountryTopTracks(pub Vec<TopTracks>);
impl Deref for CountryTopTracks {
type Target = Vec<TopTracks>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl CountryTopTracks {
pub fn for_country(&self, country: &str) -> Tracks {
if let Some(country) = self.0.iter().find(|top_track| top_track.country == country) {
return country.tracks.clone();
}
if let Some(global) = self.0.iter().find(|top_track| top_track.country.is_empty()) {
return global.tracks.clone();
}
Tracks(vec![]) // none found
}
}
#[async_trait]
impl Metadata for Artist {
type Message = protocol::metadata::Artist;
async fn request(session: &Session, artist_id: SpotifyId) -> RequestResult {
session.spclient().get_artist_metadata(artist_id).await
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Artist {
type Error = librespot_core::Error;
fn try_from(artist: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: artist.try_into()?,
name: artist.get_name().to_owned(),
top_tracks: artist.get_top_track().try_into()?,
})
}
}
try_from_repeated_message!(<Artist as Metadata>::Message, Artists);
impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole {
type Error = librespot_core::Error;
fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result<Self, Self::Error> {
Ok(Self {
id: artist_with_role.try_into()?,
name: artist_with_role.get_artist_name().to_owned(),
role: artist_with_role.get_role(),
})
}
}
try_from_repeated_message!(ArtistWithRoleMessage, ArtistsWithRole);
impl TryFrom<&TopTracksMessage> for TopTracks {
type Error = librespot_core::Error;
fn try_from(top_tracks: &TopTracksMessage) -> Result<Self, Self::Error> {
Ok(Self {
country: top_tracks.get_country().to_owned(),
tracks: top_tracks.get_track().try_into()?,
})
}
}
try_from_repeated_message!(TopTracksMessage, CountryTopTracks);

View file

@ -0,0 +1,28 @@
use std::{collections::HashMap, fmt::Debug, ops::Deref};
use librespot_core::FileId;
use librespot_protocol as protocol;
use protocol::metadata::AudioFile as AudioFileMessage;
pub use protocol::metadata::AudioFile_Format as AudioFileFormat;
#[derive(Debug, Clone)]
pub struct AudioFiles(pub HashMap<AudioFileFormat, FileId>);
impl Deref for AudioFiles {
type Target = HashMap<AudioFileFormat, FileId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&[AudioFileMessage]> for AudioFiles {
fn from(files: &[AudioFileMessage]) -> Self {
let audio_files = files
.iter()
.map(|file| (file.get_format(), FileId::from(file.get_file_id())))
.collect();
AudioFiles(audio_files)
}
}

112
metadata/src/audio/item.rs Normal file
View file

@ -0,0 +1,112 @@
use std::fmt::Debug;
use chrono::Local;
use crate::{
availability::{AudioItemAvailability, Availabilities, UnavailabilityReason},
episode::Episode,
error::MetadataError,
restriction::Restrictions,
track::{Track, Tracks},
};
use super::file::AudioFiles;
use librespot_core::{session::UserData, spotify_id::SpotifyItemType, Error, Session, SpotifyId};
pub type AudioItemResult = Result<AudioItem, Error>;
// A wrapper with fields the player needs
#[derive(Debug, Clone)]
pub struct AudioItem {
pub id: SpotifyId,
pub spotify_uri: String,
pub files: AudioFiles,
pub name: String,
pub duration: i32,
pub availability: AudioItemAvailability,
pub alternatives: Option<Tracks>,
}
impl AudioItem {
pub async fn get_file(session: &Session, id: SpotifyId) -> AudioItemResult {
match id.item_type {
SpotifyItemType::Track => Track::get_audio_item(session, id).await,
SpotifyItemType::Episode => Episode::get_audio_item(session, id).await,
_ => Err(Error::unavailable(MetadataError::NonPlayable)),
}
}
}
#[async_trait]
pub trait InnerAudioItem {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult;
fn allowed_for_user(
user_data: &UserData,
restrictions: &Restrictions,
) -> AudioItemAvailability {
let country = &user_data.country;
let user_catalogue = match user_data.attributes.get("catalogue") {
Some(catalogue) => catalogue,
None => "premium",
};
for premium_restriction in restrictions.iter().filter(|restriction| {
restriction
.catalogue_strs
.iter()
.any(|restricted_catalogue| restricted_catalogue == user_catalogue)
}) {
if let Some(allowed_countries) = &premium_restriction.countries_allowed {
// A restriction will specify either a whitelast *or* a blacklist,
// but not both. So restrict availability if there is a whitelist
// and the country isn't on it.
if allowed_countries.iter().any(|allowed| country == allowed) {
return Ok(());
} else {
return Err(UnavailabilityReason::NotWhitelisted);
}
}
if let Some(forbidden_countries) = &premium_restriction.countries_forbidden {
if forbidden_countries
.iter()
.any(|forbidden| country == forbidden)
{
return Err(UnavailabilityReason::Blacklisted);
} else {
return Ok(());
}
}
}
Ok(()) // no restrictions in place
}
fn available(availability: &Availabilities) -> AudioItemAvailability {
if availability.is_empty() {
// not all items have availability specified
return Ok(());
}
if !(availability
.iter()
.any(|availability| Local::now() >= availability.start.as_utc()))
{
return Err(UnavailabilityReason::Embargo);
}
Ok(())
}
fn available_for_user(
user_data: &UserData,
availability: &Availabilities,
restrictions: &Restrictions,
) -> AudioItemAvailability {
Self::available(availability)?;
Self::allowed_for_user(user_data, restrictions)?;
Ok(())
}
}

View file

@ -0,0 +1,5 @@
pub mod file;
pub mod item;
pub use file::AudioFileFormat;
pub use item::AudioItem;

View file

@ -0,0 +1,51 @@
use std::{fmt::Debug, ops::Deref};
use thiserror::Error;
use crate::util::from_repeated_message;
use librespot_core::date::Date;
use librespot_protocol as protocol;
use protocol::metadata::Availability as AvailabilityMessage;
pub type AudioItemAvailability = Result<(), UnavailabilityReason>;
#[derive(Debug, Clone)]
pub struct Availability {
pub catalogue_strs: Vec<String>,
pub start: Date,
}
#[derive(Debug, Clone)]
pub struct Availabilities(pub Vec<Availability>);
impl Deref for Availabilities {
type Target = Vec<Availability>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Copy, Clone, Error)]
pub enum UnavailabilityReason {
#[error("blacklist present and country on it")]
Blacklisted,
#[error("available date is in the future")]
Embargo,
#[error("required data was not present")]
NoData,
#[error("whitelist present and country not on it")]
NotWhitelisted,
}
impl From<&AvailabilityMessage> for Availability {
fn from(availability: &AvailabilityMessage) -> Self {
Self {
catalogue_strs: availability.get_catalogue_str().to_vec(),
start: availability.get_start().into(),
}
}
}
from_repeated_message!(AvailabilityMessage, Availabilities);

View file

@ -0,0 +1,33 @@
use std::{fmt::Debug, ops::Deref};
use crate::util::from_repeated_message;
use librespot_protocol as protocol;
use protocol::metadata::ContentRating as ContentRatingMessage;
#[derive(Debug, Clone)]
pub struct ContentRating {
pub country: String,
pub tags: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ContentRatings(pub Vec<ContentRating>);
impl Deref for ContentRatings {
type Target = Vec<ContentRating>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&ContentRatingMessage> for ContentRating {
fn from(content_rating: &ContentRatingMessage) -> Self {
Self {
country: content_rating.get_country().to_owned(),
tags: content_rating.get_tag().to_vec(),
}
}
}
from_repeated_message!(ContentRatingMessage, ContentRatings);

34
metadata/src/copyright.rs Normal file
View file

@ -0,0 +1,34 @@
use std::{fmt::Debug, ops::Deref};
use crate::util::from_repeated_message;
use librespot_protocol as protocol;
use protocol::metadata::Copyright as CopyrightMessage;
pub use protocol::metadata::Copyright_Type as CopyrightType;
#[derive(Debug, Clone)]
pub struct Copyright {
pub copyright_type: CopyrightType,
pub text: String,
}
#[derive(Debug, Clone)]
pub struct Copyrights(pub Vec<Copyright>);
impl Deref for Copyrights {
type Target = Vec<Copyright>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&CopyrightMessage> for Copyright {
fn from(copyright: &CopyrightMessage) -> Self {
Self {
copyright_type: copyright.get_field_type(),
text: copyright.get_text().to_owned(),
}
}
}
from_repeated_message!(CopyrightMessage, Copyrights);

View file

@ -1,20 +0,0 @@
use byteorder::{BigEndian, WriteBytesExt};
use std::io::Write;
use librespot_core::channel::ChannelData;
use librespot_core::packet::PacketType;
use librespot_core::session::Session;
use librespot_core::spotify_id::FileId;
pub fn get(session: &Session, file: FileId) -> ChannelData {
let (channel_id, channel) = session.channel().allocate();
let (_headers, data) = channel.split();
let mut packet: Vec<u8> = Vec::new();
packet.write_u16::<BigEndian>(channel_id).unwrap();
packet.write_u16::<BigEndian>(0).unwrap();
packet.write(&file.0).unwrap();
session.send_packet(PacketType::Image, packet);
data
}

132
metadata/src/episode.rs Normal file
View file

@ -0,0 +1,132 @@
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Deref,
};
use crate::{
audio::{
file::AudioFiles,
item::{AudioItem, AudioItemResult, InnerAudioItem},
},
availability::Availabilities,
content_rating::ContentRatings,
image::Images,
request::RequestResult,
restriction::Restrictions,
util::try_from_repeated_message,
video::VideoFiles,
Metadata,
};
use librespot_core::{date::Date, Error, Session, SpotifyId};
use librespot_protocol as protocol;
pub use protocol::metadata::Episode_EpisodeType as EpisodeType;
#[derive(Debug, Clone)]
pub struct Episode {
pub id: SpotifyId,
pub name: String,
pub duration: i32,
pub audio: AudioFiles,
pub description: String,
pub number: i32,
pub publish_time: Date,
pub covers: Images,
pub language: String,
pub is_explicit: bool,
pub show: SpotifyId,
pub videos: VideoFiles,
pub video_previews: VideoFiles,
pub audio_previews: AudioFiles,
pub restrictions: Restrictions,
pub freeze_frames: Images,
pub keywords: Vec<String>,
pub allow_background_playback: bool,
pub availability: Availabilities,
pub external_url: String,
pub episode_type: EpisodeType,
pub has_music_and_talk: bool,
pub content_rating: ContentRatings,
pub is_audiobook_chapter: bool,
}
#[derive(Debug, Clone)]
pub struct Episodes(pub Vec<SpotifyId>);
impl Deref for Episodes {
type Target = Vec<SpotifyId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl InnerAudioItem for Episode {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult {
let episode = Self::get(session, id).await?;
let availability = Self::available_for_user(
&session.user_data(),
&episode.availability,
&episode.restrictions,
);
Ok(AudioItem {
id,
spotify_uri: id.to_uri(),
files: episode.audio,
name: episode.name,
duration: episode.duration,
availability,
alternatives: None,
})
}
}
#[async_trait]
impl Metadata for Episode {
type Message = protocol::metadata::Episode;
async fn request(session: &Session, episode_id: SpotifyId) -> RequestResult {
session.spclient().get_episode_metadata(episode_id).await
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Episode {
type Error = librespot_core::Error;
fn try_from(episode: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: episode.try_into()?,
name: episode.get_name().to_owned(),
duration: episode.get_duration().to_owned(),
audio: episode.get_audio().into(),
description: episode.get_description().to_owned(),
number: episode.get_number(),
publish_time: episode.get_publish_time().into(),
covers: episode.get_cover_image().get_image().into(),
language: episode.get_language().to_owned(),
is_explicit: episode.get_explicit().to_owned(),
show: episode.get_show().try_into()?,
videos: episode.get_video().into(),
video_previews: episode.get_video_preview().into(),
audio_previews: episode.get_audio_preview().into(),
restrictions: episode.get_restriction().into(),
freeze_frames: episode.get_freeze_frame().get_image().into(),
keywords: episode.get_keyword().to_vec(),
allow_background_playback: episode.get_allow_background_playback(),
availability: episode.get_availability().into(),
external_url: episode.get_external_url().to_owned(),
episode_type: episode.get_field_type(),
has_music_and_talk: episode.get_music_and_talk(),
content_rating: episode.get_content_rating().into(),
is_audiobook_chapter: episode.get_is_audiobook_chapter(),
})
}
}
try_from_repeated_message!(<Episode as Metadata>::Message, Episodes);

10
metadata/src/error.rs Normal file
View file

@ -0,0 +1,10 @@
use std::fmt::Debug;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MetadataError {
#[error("empty response")]
Empty,
#[error("audio item is non-playable when it should be")]
NonPlayable,
}

View file

@ -0,0 +1,33 @@
use std::{fmt::Debug, ops::Deref};
use crate::util::from_repeated_message;
use librespot_protocol as protocol;
use protocol::metadata::ExternalId as ExternalIdMessage;
#[derive(Debug, Clone)]
pub struct ExternalId {
pub external_type: String,
pub id: String, // this can be anything from a URL to a ISRC, EAN or UPC
}
#[derive(Debug, Clone)]
pub struct ExternalIds(pub Vec<ExternalId>);
impl Deref for ExternalIds {
type Target = Vec<ExternalId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&ExternalIdMessage> for ExternalId {
fn from(external_id: &ExternalIdMessage) -> Self {
Self {
external_type: external_id.get_field_type().to_owned(),
id: external_id.get_id().to_owned(),
}
}
}
from_repeated_message!(ExternalIdMessage, ExternalIds);

101
metadata/src/image.rs Normal file
View file

@ -0,0 +1,101 @@
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Deref,
};
use crate::util::{from_repeated_message, try_from_repeated_message};
use librespot_core::{FileId, SpotifyId};
use librespot_protocol as protocol;
use protocol::metadata::Image as ImageMessage;
pub use protocol::metadata::Image_Size as ImageSize;
use protocol::playlist4_external::PictureSize as PictureSizeMessage;
use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage;
#[derive(Debug, Clone)]
pub struct Image {
pub id: FileId,
pub size: ImageSize,
pub width: i32,
pub height: i32,
}
#[derive(Debug, Clone)]
pub struct Images(pub Vec<Image>);
impl Deref for Images {
type Target = Vec<Image>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct PictureSize {
pub target_name: String,
pub url: String,
}
#[derive(Debug, Clone)]
pub struct PictureSizes(pub Vec<PictureSize>);
impl Deref for PictureSizes {
type Target = Vec<PictureSize>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct TranscodedPicture {
pub target_name: String,
pub uri: SpotifyId,
}
#[derive(Debug, Clone)]
pub struct TranscodedPictures(pub Vec<TranscodedPicture>);
impl Deref for TranscodedPictures {
type Target = Vec<TranscodedPicture>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&ImageMessage> for Image {
fn from(image: &ImageMessage) -> Self {
Self {
id: image.into(),
size: image.get_size(),
width: image.get_width(),
height: image.get_height(),
}
}
}
from_repeated_message!(ImageMessage, Images);
impl From<&PictureSizeMessage> for PictureSize {
fn from(size: &PictureSizeMessage) -> Self {
Self {
target_name: size.get_target_name().to_owned(),
url: size.get_url().to_owned(),
}
}
}
from_repeated_message!(PictureSizeMessage, PictureSizes);
impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture {
type Error = librespot_core::Error;
fn try_from(picture: &TranscodedPictureMessage) -> Result<Self, Self::Error> {
Ok(Self {
target_name: picture.get_target_name().to_owned(),
uri: picture.try_into()?,
})
}
}
try_from_repeated_message!(TranscodedPictureMessage, TranscodedPictures);

View file

@ -1,460 +1,56 @@
#![allow(clippy::unused_io_amount)]
#[macro_use] #[macro_use]
extern crate log; extern crate log;
#[macro_use] #[macro_use]
extern crate async_trait; extern crate async_trait;
pub mod cover;
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;
use protobuf::Message; use protobuf::Message;
pub use crate::protocol::metadata::AudioFile_Format as FileFormat; use librespot_core::{Error, Session, SpotifyId};
fn countrylist_contains(list: &str, country: &str) -> bool { pub mod album;
list.chunks(2).any(|cc| cc == country) pub mod artist;
} pub mod audio;
pub mod availability;
pub mod content_rating;
pub mod copyright;
pub mod episode;
pub mod error;
pub mod external_id;
pub mod image;
pub mod playlist;
mod request;
pub mod restriction;
pub mod sale_period;
pub mod show;
pub mod track;
mod util;
pub mod video;
fn parse_restrictions<'s, I>(restrictions: I, country: &str, catalogue: &str) -> bool pub use error::MetadataError;
where use request::RequestResult;
I: IntoIterator<Item = &'s protocol::metadata::Restriction>,
{
let mut forbidden = "".to_string();
let mut has_forbidden = false;
let mut allowed = "".to_string(); pub use album::Album;
let mut has_allowed = false; pub use artist::Artist;
pub use episode::Episode;
let rs = restrictions pub use playlist::Playlist;
.into_iter() pub use show::Show;
.filter(|r| r.get_catalogue_str().contains(&catalogue.to_owned())); pub use track::Track;
for r in rs {
if r.has_countries_forbidden() {
forbidden.push_str(r.get_countries_forbidden());
has_forbidden = true;
}
if r.has_countries_allowed() {
allowed.push_str(r.get_countries_allowed());
has_allowed = true;
}
}
(has_forbidden || has_allowed)
&& (!has_forbidden || !countrylist_contains(forbidden.as_str(), country))
&& (!has_allowed || countrylist_contains(allowed.as_str(), country))
}
// A wrapper with fields the player needs
#[derive(Debug, Clone)]
pub struct AudioItem {
pub id: SpotifyId,
pub uri: String,
pub files: HashMap<FileFormat, FileId>,
pub name: String,
pub duration: i32,
pub available: bool,
pub alternatives: Option<Vec<SpotifyId>>,
}
impl AudioItem {
pub async fn get_audio_item(session: &Session, id: SpotifyId) -> Result<Self, MercuryError> {
match id.audio_type {
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 {
async fn get_audio_item(session: &Session, id: SpotifyId) -> Result<AudioItem, MercuryError>;
}
#[async_trait]
impl AudioFiles for Track {
async fn get_audio_item(session: &Session, id: SpotifyId) -> Result<AudioItem, MercuryError> {
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 {
async fn get_audio_item(session: &Session, id: SpotifyId) -> Result<AudioItem, MercuryError> {
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] #[async_trait]
pub trait Metadata: Send + Sized + 'static { pub trait Metadata: Send + Sized + 'static {
type Message: protobuf::Message; type Message: protobuf::Message;
fn request_url(id: SpotifyId) -> String; // Request a protobuf
fn parse(msg: &Self::Message, session: &Session) -> Self; async fn request(session: &Session, id: SpotifyId) -> RequestResult;
async fn get(session: &Session, id: SpotifyId) -> Result<Self, MercuryError> { // Request a metadata struct
let uri = Self::request_url(id); async fn get(session: &Session, id: SpotifyId) -> Result<Self, Error> {
let response = session.mercury().get(uri).await?; let response = Self::request(session, id).await?;
let data = response.payload.first().expect("Empty payload"); let msg = Self::Message::parse_from_bytes(&response)?;
let msg = Self::Message::parse_from_bytes(data).unwrap(); trace!("Received metadata: {:#?}", msg);
Self::parse(&msg, id)
Ok(Self::parse(&msg, &session))
}
}
#[derive(Debug, Clone)]
pub struct Track {
pub id: SpotifyId,
pub name: String,
pub duration: i32,
pub album: SpotifyId,
pub artists: Vec<SpotifyId>,
pub files: HashMap<FileFormat, FileId>,
pub alternatives: Vec<SpotifyId>,
pub available: bool,
}
#[derive(Debug, Clone)]
pub struct Album {
pub id: SpotifyId,
pub name: String,
pub artists: Vec<SpotifyId>,
pub tracks: Vec<SpotifyId>,
pub covers: Vec<FileId>,
}
#[derive(Debug, Clone)]
pub struct Episode {
pub id: SpotifyId,
pub name: String,
pub external_url: String,
pub duration: i32,
pub language: String,
pub show: SpotifyId,
pub files: HashMap<FileFormat, FileId>,
pub covers: Vec<FileId>,
pub available: bool,
pub explicit: bool,
}
#[derive(Debug, Clone)]
pub struct Show {
pub id: SpotifyId,
pub name: String,
pub publisher: String,
pub episodes: Vec<SpotifyId>,
pub covers: Vec<FileId>,
}
#[derive(Debug, Clone)]
pub struct Playlist {
pub revision: Vec<u8>,
pub user: String,
pub name: String,
pub tracks: Vec<SpotifyId>,
}
#[derive(Debug, Clone)]
pub struct Artist {
pub id: SpotifyId,
pub name: String,
pub top_tracks: Vec<SpotifyId>,
}
impl Metadata for Track {
type Message = protocol::metadata::Track;
fn request_url(id: SpotifyId) -> String {
format!("hm://metadata/3/track/{}", id.to_base16())
}
fn parse(msg: &Self::Message, session: &Session) -> Self {
let country = session.country();
let artists = msg
.get_artist()
.iter()
.filter(|artist| artist.has_gid())
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
.collect::<Vec<_>>();
let files = msg
.get_file()
.iter()
.filter(|file| file.has_file_id())
.map(|file| {
let mut dst = [0u8; 20];
dst.clone_from_slice(file.get_file_id());
(file.get_format(), FileId(dst))
})
.collect();
Track {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(),
duration: msg.get_duration(),
album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(),
artists,
files,
alternatives: msg
.get_alternative()
.iter()
.map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap())
.collect(),
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
}
}
}
impl Metadata for Album {
type Message = protocol::metadata::Album;
fn request_url(id: SpotifyId) -> String {
format!("hm://metadata/3/album/{}", id.to_base16())
}
fn parse(msg: &Self::Message, _: &Session) -> Self {
let artists = msg
.get_artist()
.iter()
.filter(|artist| artist.has_gid())
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
.collect::<Vec<_>>();
let tracks = msg
.get_disc()
.iter()
.flat_map(|disc| disc.get_track())
.filter(|track| track.has_gid())
.map(|track| SpotifyId::from_raw(track.get_gid()).unwrap())
.collect::<Vec<_>>();
let covers = msg
.get_cover_group()
.get_image()
.iter()
.filter(|image| image.has_file_id())
.map(|image| {
let mut dst = [0u8; 20];
dst.clone_from_slice(image.get_file_id());
FileId(dst)
})
.collect::<Vec<_>>();
Album {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(),
artists,
tracks,
covers,
}
}
}
impl Metadata for Playlist {
type Message = protocol::playlist4changes::SelectedListContent;
fn request_url(id: SpotifyId) -> String {
format!("hm://playlist/v2/playlist/{}", id.to_base62())
}
fn parse(msg: &Self::Message, _: &Session) -> Self {
let tracks = msg
.get_contents()
.get_items()
.iter()
.map(|item| {
let uri_split = item.get_uri().split(':');
let uri_parts: Vec<&str> = uri_split.collect();
SpotifyId::from_base62(uri_parts[2]).unwrap()
})
.collect::<Vec<_>>();
if tracks.len() != msg.get_length() as usize {
warn!(
"Got {} tracks, but the playlist should contain {} tracks.",
tracks.len(),
msg.get_length()
);
}
Playlist {
revision: msg.get_revision().to_vec(),
name: msg.get_attributes().get_name().to_owned(),
tracks,
user: msg.get_owner_username().to_string(),
}
}
}
impl Metadata for Artist {
type Message = protocol::metadata::Artist;
fn request_url(id: SpotifyId) -> String {
format!("hm://metadata/3/artist/{}", id.to_base16())
}
fn parse(msg: &Self::Message, session: &Session) -> Self {
let country = session.country();
let top_tracks: Vec<SpotifyId> = match msg
.get_top_track()
.iter()
.find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country))
{
Some(tracks) => tracks
.get_track()
.iter()
.filter(|track| track.has_gid())
.map(|track| SpotifyId::from_raw(track.get_gid()).unwrap())
.collect::<Vec<_>>(),
None => Vec::new(),
};
Artist {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(),
top_tracks,
}
}
}
// Podcast
impl Metadata for Episode {
type Message = protocol::metadata::Episode;
fn request_url(id: SpotifyId) -> String {
format!("hm://metadata/3/episode/{}", id.to_base16())
}
fn parse(msg: &Self::Message, session: &Session) -> Self {
let country = session.country();
let files = msg
.get_audio()
.iter()
.filter(|file| file.has_file_id())
.map(|file| {
let mut dst = [0u8; 20];
dst.clone_from_slice(file.get_file_id());
(file.get_format(), FileId(dst))
})
.collect();
let covers = msg
.get_cover_image()
.get_image()
.iter()
.filter(|image| image.has_file_id())
.map(|image| {
let mut dst = [0u8; 20];
dst.clone_from_slice(image.get_file_id());
FileId(dst)
})
.collect::<Vec<_>>();
Episode {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(),
external_url: msg.get_external_url().to_owned(),
duration: msg.get_duration().to_owned(),
language: msg.get_language().to_owned(),
show: SpotifyId::from_raw(msg.get_show().get_gid()).unwrap(),
covers,
files,
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
explicit: msg.get_explicit().to_owned(),
}
}
}
impl Metadata for Show {
type Message = protocol::metadata::Show;
fn request_url(id: SpotifyId) -> String {
format!("hm://metadata/3/show/{}", id.to_base16())
}
fn parse(msg: &Self::Message, _: &Session) -> Self {
let episodes = msg
.get_episode()
.iter()
.filter(|episode| episode.has_gid())
.map(|episode| SpotifyId::from_raw(episode.get_gid()).unwrap())
.collect::<Vec<_>>();
let covers = msg
.get_cover_image()
.get_image()
.iter()
.filter(|image| image.has_file_id())
.map(|image| {
let mut dst = [0u8; 20];
dst.clone_from_slice(image.get_file_id());
FileId(dst)
})
.collect::<Vec<_>>();
Show {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(),
publisher: msg.get_publisher().to_owned(),
episodes,
covers,
}
}
}
struct StrChunks<'s>(&'s str, usize);
trait StrChunksExt {
fn chunks(&self, size: usize) -> StrChunks;
}
impl StrChunksExt for str {
fn chunks(&self, size: usize) -> StrChunks {
StrChunks(self, size)
}
}
impl<'s> Iterator for StrChunks<'s> {
type Item = &'s str;
fn next(&mut self) -> Option<&'s str> {
let &mut StrChunks(data, size) = self;
if data.is_empty() {
None
} else {
let ret = Some(&data[..size]);
self.0 = &data[size..];
ret
}
} }
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error>;
} }

View file

@ -0,0 +1,87 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use protobuf::Message;
use crate::{
image::TranscodedPictures,
request::{MercuryRequest, RequestResult},
Metadata,
};
use librespot_core::{Error, Session, SpotifyId};
use librespot_protocol as protocol;
pub use protocol::playlist_annotate3::AbuseReportState;
#[derive(Debug, Clone)]
pub struct PlaylistAnnotation {
pub description: String,
pub picture: String,
pub transcoded_pictures: TranscodedPictures,
pub has_abuse_reporting: bool,
pub abuse_report_state: AbuseReportState,
}
#[async_trait]
impl Metadata for PlaylistAnnotation {
type Message = protocol::playlist_annotate3::PlaylistAnnotation;
async fn request(session: &Session, playlist_id: SpotifyId) -> RequestResult {
let current_user = session.username();
Self::request_for_user(session, &current_user, playlist_id).await
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Ok(Self {
description: msg.get_description().to_owned(),
picture: msg.get_picture().to_owned(), // TODO: is this a URL or Spotify URI?
transcoded_pictures: msg.get_transcoded_picture().try_into()?,
has_abuse_reporting: msg.get_is_abuse_reporting_enabled(),
abuse_report_state: msg.get_abuse_report_state(),
})
}
}
impl PlaylistAnnotation {
async fn request_for_user(
session: &Session,
username: &str,
playlist_id: SpotifyId,
) -> RequestResult {
let uri = format!(
"hm://playlist-annotate/v1/annotation/user/{}/playlist/{}",
username,
playlist_id.to_base62()
);
<Self as MercuryRequest>::request(session, &uri).await
}
#[allow(dead_code)]
async fn get_for_user(
session: &Session,
username: &str,
playlist_id: SpotifyId,
) -> Result<Self, Error> {
let response = Self::request_for_user(session, username, playlist_id).await?;
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
Self::parse(&msg, playlist_id)
}
}
impl MercuryRequest for PlaylistAnnotation {}
impl TryFrom<&<PlaylistAnnotation as Metadata>::Message> for PlaylistAnnotation {
type Error = librespot_core::Error;
fn try_from(
annotation: &<PlaylistAnnotation as Metadata>::Message,
) -> Result<Self, Self::Error> {
Ok(Self {
description: annotation.get_description().to_owned(),
picture: annotation.get_picture().to_owned(),
transcoded_pictures: annotation.get_transcoded_picture().try_into()?,
has_abuse_reporting: annotation.get_is_abuse_reporting_enabled(),
abuse_report_state: annotation.get_abuse_report_state(),
})
}
}

View file

@ -0,0 +1,196 @@
use std::{
collections::HashMap,
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Deref,
};
use crate::{image::PictureSizes, util::from_repeated_enum};
use librespot_core::{date::Date, SpotifyId};
use librespot_protocol as protocol;
use protocol::playlist4_external::FormatListAttribute as PlaylistFormatAttributeMessage;
pub use protocol::playlist4_external::ItemAttributeKind as PlaylistItemAttributeKind;
use protocol::playlist4_external::ItemAttributes as PlaylistItemAttributesMessage;
use protocol::playlist4_external::ItemAttributesPartialState as PlaylistPartialItemAttributesMessage;
pub use protocol::playlist4_external::ListAttributeKind as PlaylistAttributeKind;
use protocol::playlist4_external::ListAttributes as PlaylistAttributesMessage;
use protocol::playlist4_external::ListAttributesPartialState as PlaylistPartialAttributesMessage;
use protocol::playlist4_external::UpdateItemAttributes as PlaylistUpdateItemAttributesMessage;
use protocol::playlist4_external::UpdateListAttributes as PlaylistUpdateAttributesMessage;
#[derive(Debug, Clone)]
pub struct PlaylistAttributes {
pub name: String,
pub description: String,
pub picture: SpotifyId,
pub is_collaborative: bool,
pub pl3_version: String,
pub is_deleted_by_owner: bool,
pub client_id: String,
pub format: String,
pub format_attributes: PlaylistFormatAttribute,
pub picture_sizes: PictureSizes,
}
#[derive(Debug, Clone)]
pub struct PlaylistAttributeKinds(pub Vec<PlaylistAttributeKind>);
impl Deref for PlaylistAttributeKinds {
type Target = Vec<PlaylistAttributeKind>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
from_repeated_enum!(PlaylistAttributeKind, PlaylistAttributeKinds);
#[derive(Debug, Clone)]
pub struct PlaylistFormatAttribute(pub HashMap<String, String>);
impl Deref for PlaylistFormatAttribute {
type Target = HashMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct PlaylistItemAttributes {
pub added_by: String,
pub timestamp: Date,
pub seen_at: Date,
pub is_public: bool,
pub format_attributes: PlaylistFormatAttribute,
pub item_id: SpotifyId,
}
#[derive(Debug, Clone)]
pub struct PlaylistItemAttributeKinds(pub Vec<PlaylistItemAttributeKind>);
impl Deref for PlaylistItemAttributeKinds {
type Target = Vec<PlaylistItemAttributeKind>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
from_repeated_enum!(PlaylistItemAttributeKind, PlaylistItemAttributeKinds);
#[derive(Debug, Clone)]
pub struct PlaylistPartialAttributes {
#[allow(dead_code)]
values: PlaylistAttributes,
#[allow(dead_code)]
no_value: PlaylistAttributeKinds,
}
#[derive(Debug, Clone)]
pub struct PlaylistPartialItemAttributes {
#[allow(dead_code)]
values: PlaylistItemAttributes,
#[allow(dead_code)]
no_value: PlaylistItemAttributeKinds,
}
#[derive(Debug, Clone)]
pub struct PlaylistUpdateAttributes {
pub new_attributes: PlaylistPartialAttributes,
pub old_attributes: PlaylistPartialAttributes,
}
#[derive(Debug, Clone)]
pub struct PlaylistUpdateItemAttributes {
pub index: i32,
pub new_attributes: PlaylistPartialItemAttributes,
pub old_attributes: PlaylistPartialItemAttributes,
}
impl TryFrom<&PlaylistAttributesMessage> for PlaylistAttributes {
type Error = librespot_core::Error;
fn try_from(attributes: &PlaylistAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
name: attributes.get_name().to_owned(),
description: attributes.get_description().to_owned(),
picture: attributes.get_picture().try_into()?,
is_collaborative: attributes.get_collaborative(),
pl3_version: attributes.get_pl3_version().to_owned(),
is_deleted_by_owner: attributes.get_deleted_by_owner(),
client_id: attributes.get_client_id().to_owned(),
format: attributes.get_format().to_owned(),
format_attributes: attributes.get_format_attributes().into(),
picture_sizes: attributes.get_picture_size().into(),
})
}
}
impl From<&[PlaylistFormatAttributeMessage]> for PlaylistFormatAttribute {
fn from(attributes: &[PlaylistFormatAttributeMessage]) -> Self {
let format_attributes = attributes
.iter()
.map(|attribute| {
(
attribute.get_key().to_owned(),
attribute.get_value().to_owned(),
)
})
.collect();
PlaylistFormatAttribute(format_attributes)
}
}
impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes {
type Error = librespot_core::Error;
fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
added_by: attributes.get_added_by().to_owned(),
timestamp: attributes.get_timestamp().try_into()?,
seen_at: attributes.get_seen_at().try_into()?,
is_public: attributes.get_public(),
format_attributes: attributes.get_format_attributes().into(),
item_id: attributes.get_item_id().try_into()?,
})
}
}
impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes {
type Error = librespot_core::Error;
fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
values: attributes.get_values().try_into()?,
no_value: attributes.get_no_value().into(),
})
}
}
impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttributes {
type Error = librespot_core::Error;
fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
values: attributes.get_values().try_into()?,
no_value: attributes.get_no_value().into(),
})
}
}
impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes {
type Error = librespot_core::Error;
fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
new_attributes: update.get_new_attributes().try_into()?,
old_attributes: update.get_old_attributes().try_into()?,
})
}
}
impl TryFrom<&PlaylistUpdateItemAttributesMessage> for PlaylistUpdateItemAttributes {
type Error = librespot_core::Error;
fn try_from(update: &PlaylistUpdateItemAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
index: update.get_index(),
new_attributes: update.get_new_attributes().try_into()?,
old_attributes: update.get_old_attributes().try_into()?,
})
}
}

View file

@ -0,0 +1,29 @@
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
};
use super::operation::PlaylistOperations;
use librespot_core::SpotifyId;
use librespot_protocol as protocol;
use protocol::playlist4_external::Diff as DiffMessage;
#[derive(Debug, Clone)]
pub struct PlaylistDiff {
pub from_revision: SpotifyId,
pub operations: PlaylistOperations,
pub to_revision: SpotifyId,
}
impl TryFrom<&DiffMessage> for PlaylistDiff {
type Error = librespot_core::Error;
fn try_from(diff: &DiffMessage) -> Result<Self, Self::Error> {
Ok(Self {
from_revision: diff.get_from_revision().try_into()?,
operations: diff.get_ops().try_into()?,
to_revision: diff.get_to_revision().try_into()?,
})
}
}

View file

@ -0,0 +1,105 @@
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Deref,
};
use crate::util::try_from_repeated_message;
use super::{
attribute::{PlaylistAttributes, PlaylistItemAttributes},
permission::Capabilities,
};
use librespot_core::{date::Date, SpotifyId};
use librespot_protocol as protocol;
use protocol::playlist4_external::Item as PlaylistItemMessage;
use protocol::playlist4_external::ListItems as PlaylistItemsMessage;
use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage;
#[derive(Debug, Clone)]
pub struct PlaylistItem {
pub id: SpotifyId,
pub attributes: PlaylistItemAttributes,
}
#[derive(Debug, Clone)]
pub struct PlaylistItems(pub Vec<PlaylistItem>);
impl Deref for PlaylistItems {
type Target = Vec<PlaylistItem>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct PlaylistItemList {
pub position: i32,
pub is_truncated: bool,
pub items: PlaylistItems,
pub meta_items: PlaylistMetaItems,
}
#[derive(Debug, Clone)]
pub struct PlaylistMetaItem {
pub revision: SpotifyId,
pub attributes: PlaylistAttributes,
pub length: i32,
pub timestamp: Date,
pub owner_username: String,
pub has_abuse_reporting: bool,
pub capabilities: Capabilities,
}
#[derive(Debug, Clone)]
pub struct PlaylistMetaItems(pub Vec<PlaylistMetaItem>);
impl Deref for PlaylistMetaItems {
type Target = Vec<PlaylistMetaItem>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<&PlaylistItemMessage> for PlaylistItem {
type Error = librespot_core::Error;
fn try_from(item: &PlaylistItemMessage) -> Result<Self, Self::Error> {
Ok(Self {
id: item.try_into()?,
attributes: item.get_attributes().try_into()?,
})
}
}
try_from_repeated_message!(PlaylistItemMessage, PlaylistItems);
impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList {
type Error = librespot_core::Error;
fn try_from(list_items: &PlaylistItemsMessage) -> Result<Self, Self::Error> {
Ok(Self {
position: list_items.get_pos(),
is_truncated: list_items.get_truncated(),
items: list_items.get_items().try_into()?,
meta_items: list_items.get_meta_items().try_into()?,
})
}
}
impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem {
type Error = librespot_core::Error;
fn try_from(item: &PlaylistMetaItemMessage) -> Result<Self, Self::Error> {
Ok(Self {
revision: item.try_into()?,
attributes: item.get_attributes().try_into()?,
length: item.get_length(),
timestamp: item.get_timestamp().try_into()?,
owner_username: item.get_owner_username().to_owned(),
has_abuse_reporting: item.get_abuse_reporting_enabled(),
capabilities: item.get_capabilities().into(),
})
}
}
try_from_repeated_message!(PlaylistMetaItemMessage, PlaylistMetaItems);

View file

@ -0,0 +1,225 @@
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Deref,
};
use protobuf::Message;
use crate::{
request::{MercuryRequest, RequestResult},
util::{from_repeated_enum, try_from_repeated_message},
Metadata,
};
use super::{
attribute::PlaylistAttributes, diff::PlaylistDiff, item::PlaylistItemList,
permission::Capabilities,
};
use librespot_core::{
date::Date,
spotify_id::{NamedSpotifyId, SpotifyId},
Error, Session,
};
use librespot_protocol as protocol;
use protocol::playlist4_external::GeoblockBlockingType as Geoblock;
#[derive(Debug, Clone)]
pub struct Geoblocks(Vec<Geoblock>);
impl Deref for Geoblocks {
type Target = Vec<Geoblock>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct Playlist {
pub id: NamedSpotifyId,
pub revision: SpotifyId,
pub length: i32,
pub attributes: PlaylistAttributes,
pub contents: PlaylistItemList,
pub diff: PlaylistDiff,
pub sync_result: PlaylistDiff,
pub resulting_revisions: Playlists,
pub has_multiple_heads: bool,
pub is_up_to_date: bool,
pub nonces: Vec<i64>,
pub timestamp: Date,
pub has_abuse_reporting: bool,
pub capabilities: Capabilities,
pub geoblocks: Geoblocks,
}
#[derive(Debug, Clone)]
pub struct Playlists(pub Vec<SpotifyId>);
impl Deref for Playlists {
type Target = Vec<SpotifyId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct RootPlaylist(pub SelectedListContent);
impl Deref for RootPlaylist {
type Target = SelectedListContent;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct SelectedListContent {
pub revision: SpotifyId,
pub length: i32,
pub attributes: PlaylistAttributes,
pub contents: PlaylistItemList,
pub diff: PlaylistDiff,
pub sync_result: PlaylistDiff,
pub resulting_revisions: Playlists,
pub has_multiple_heads: bool,
pub is_up_to_date: bool,
pub nonces: Vec<i64>,
pub timestamp: Date,
pub owner_username: String,
pub has_abuse_reporting: bool,
pub capabilities: Capabilities,
pub geoblocks: Geoblocks,
}
impl Playlist {
#[allow(dead_code)]
async fn request_for_user(
session: &Session,
username: &str,
playlist_id: SpotifyId,
) -> RequestResult {
let uri = format!(
"hm://playlist/user/{}/playlist/{}",
username,
playlist_id.to_base62()
);
<Self as MercuryRequest>::request(session, &uri).await
}
#[allow(dead_code)]
pub async fn get_for_user(
session: &Session,
username: &str,
playlist_id: SpotifyId,
) -> Result<Self, Error> {
let response = Self::request_for_user(session, username, playlist_id).await?;
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
Self::parse(&msg, playlist_id)
}
pub fn tracks(&self) -> Vec<SpotifyId> {
let tracks = self
.contents
.items
.iter()
.map(|item| item.id)
.collect::<Vec<_>>();
let length = tracks.len();
let expected_length = self.length as usize;
if length != expected_length {
warn!(
"Got {} tracks, but the list should contain {} tracks.",
length, expected_length,
);
}
tracks
}
pub fn name(&self) -> &str {
&self.attributes.name
}
}
impl MercuryRequest for Playlist {}
#[async_trait]
impl Metadata for Playlist {
type Message = protocol::playlist4_external::SelectedListContent;
async fn request(session: &Session, playlist_id: SpotifyId) -> RequestResult {
let uri = format!("hm://playlist/v2/playlist/{}", playlist_id.to_base62());
<Self as MercuryRequest>::request(session, &uri).await
}
fn parse(msg: &Self::Message, id: SpotifyId) -> Result<Self, Error> {
// the playlist proto doesn't contain the id so we decorate it
let playlist = SelectedListContent::try_from(msg)?;
let id = NamedSpotifyId::from_spotify_id(id, playlist.owner_username);
Ok(Self {
id,
revision: playlist.revision,
length: playlist.length,
attributes: playlist.attributes,
contents: playlist.contents,
diff: playlist.diff,
sync_result: playlist.sync_result,
resulting_revisions: playlist.resulting_revisions,
has_multiple_heads: playlist.has_multiple_heads,
is_up_to_date: playlist.is_up_to_date,
nonces: playlist.nonces,
timestamp: playlist.timestamp,
has_abuse_reporting: playlist.has_abuse_reporting,
capabilities: playlist.capabilities,
geoblocks: playlist.geoblocks,
})
}
}
impl MercuryRequest for RootPlaylist {}
impl RootPlaylist {
#[allow(dead_code)]
async fn request_for_user(session: &Session, username: &str) -> RequestResult {
let uri = format!("hm://playlist/user/{}/rootlist", username,);
<Self as MercuryRequest>::request(session, &uri).await
}
#[allow(dead_code)]
pub async fn get_root_for_user(session: &Session, username: &str) -> Result<Self, Error> {
let response = Self::request_for_user(session, username).await?;
let msg = protocol::playlist4_external::SelectedListContent::parse_from_bytes(&response)?;
Ok(Self(SelectedListContent::try_from(&msg)?))
}
}
impl TryFrom<&<Playlist as Metadata>::Message> for SelectedListContent {
type Error = librespot_core::Error;
fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
revision: playlist.get_revision().try_into()?,
length: playlist.get_length(),
attributes: playlist.get_attributes().try_into()?,
contents: playlist.get_contents().try_into()?,
diff: playlist.get_diff().try_into()?,
sync_result: playlist.get_sync_result().try_into()?,
resulting_revisions: playlist.get_resulting_revisions().try_into()?,
has_multiple_heads: playlist.get_multiple_heads(),
is_up_to_date: playlist.get_up_to_date(),
nonces: playlist.get_nonces().into(),
timestamp: playlist.get_timestamp().try_into()?,
owner_username: playlist.get_owner_username().to_owned(),
has_abuse_reporting: playlist.get_abuse_reporting_enabled(),
capabilities: playlist.get_capabilities().into(),
geoblocks: playlist.get_geoblock().into(),
})
}
}
from_repeated_enum!(Geoblock, Geoblocks);
try_from_repeated_message!(Vec<u8>, Playlists);

View file

@ -0,0 +1,10 @@
pub mod annotation;
pub mod attribute;
pub mod diff;
pub mod item;
pub mod list;
pub mod operation;
pub mod permission;
pub use annotation::PlaylistAnnotation;
pub use list::Playlist;

View file

@ -0,0 +1,113 @@
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Deref,
};
use crate::{
playlist::{
attribute::{PlaylistUpdateAttributes, PlaylistUpdateItemAttributes},
item::PlaylistItems,
},
util::try_from_repeated_message,
};
use librespot_protocol as protocol;
use protocol::playlist4_external::Add as PlaylistAddMessage;
use protocol::playlist4_external::Mov as PlaylistMoveMessage;
use protocol::playlist4_external::Op as PlaylistOperationMessage;
pub use protocol::playlist4_external::Op_Kind as PlaylistOperationKind;
use protocol::playlist4_external::Rem as PlaylistRemoveMessage;
#[derive(Debug, Clone)]
pub struct PlaylistOperation {
pub kind: PlaylistOperationKind,
pub add: PlaylistOperationAdd,
pub rem: PlaylistOperationRemove,
pub mov: PlaylistOperationMove,
pub update_item_attributes: PlaylistUpdateItemAttributes,
pub update_list_attributes: PlaylistUpdateAttributes,
}
#[derive(Debug, Clone)]
pub struct PlaylistOperations(pub Vec<PlaylistOperation>);
impl Deref for PlaylistOperations {
type Target = Vec<PlaylistOperation>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct PlaylistOperationAdd {
pub from_index: i32,
pub items: PlaylistItems,
pub add_last: bool,
pub add_first: bool,
}
#[derive(Debug, Clone)]
pub struct PlaylistOperationMove {
pub from_index: i32,
pub length: i32,
pub to_index: i32,
}
#[derive(Debug, Clone)]
pub struct PlaylistOperationRemove {
pub from_index: i32,
pub length: i32,
pub items: PlaylistItems,
pub has_items_as_key: bool,
}
impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation {
type Error = librespot_core::Error;
fn try_from(operation: &PlaylistOperationMessage) -> Result<Self, Self::Error> {
Ok(Self {
kind: operation.get_kind(),
add: operation.get_add().try_into()?,
rem: operation.get_rem().try_into()?,
mov: operation.get_mov().into(),
update_item_attributes: operation.get_update_item_attributes().try_into()?,
update_list_attributes: operation.get_update_list_attributes().try_into()?,
})
}
}
try_from_repeated_message!(PlaylistOperationMessage, PlaylistOperations);
impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd {
type Error = librespot_core::Error;
fn try_from(add: &PlaylistAddMessage) -> Result<Self, Self::Error> {
Ok(Self {
from_index: add.get_from_index(),
items: add.get_items().try_into()?,
add_last: add.get_add_last(),
add_first: add.get_add_first(),
})
}
}
impl From<&PlaylistMoveMessage> for PlaylistOperationMove {
fn from(mov: &PlaylistMoveMessage) -> Self {
Self {
from_index: mov.get_from_index(),
length: mov.get_length(),
to_index: mov.get_to_index(),
}
}
}
impl TryFrom<&PlaylistRemoveMessage> for PlaylistOperationRemove {
type Error = librespot_core::Error;
fn try_from(remove: &PlaylistRemoveMessage) -> Result<Self, Self::Error> {
Ok(Self {
from_index: remove.get_from_index(),
length: remove.get_length(),
items: remove.get_items().try_into()?,
has_items_as_key: remove.get_items_as_key(),
})
}
}

View file

@ -0,0 +1,42 @@
use std::{fmt::Debug, ops::Deref};
use crate::util::from_repeated_enum;
use librespot_protocol as protocol;
use protocol::playlist_permission::Capabilities as CapabilitiesMessage;
use protocol::playlist_permission::PermissionLevel;
#[derive(Debug, Clone)]
pub struct Capabilities {
pub can_view: bool,
pub can_administrate_permissions: bool,
pub grantable_levels: PermissionLevels,
pub can_edit_metadata: bool,
pub can_edit_items: bool,
pub can_cancel_membership: bool,
}
#[derive(Debug, Clone)]
pub struct PermissionLevels(pub Vec<PermissionLevel>);
impl Deref for PermissionLevels {
type Target = Vec<PermissionLevel>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&CapabilitiesMessage> for Capabilities {
fn from(playlist: &CapabilitiesMessage) -> Self {
Self {
can_view: playlist.get_can_view(),
can_administrate_permissions: playlist.get_can_administrate_permissions(),
grantable_levels: playlist.get_grantable_level().into(),
can_edit_metadata: playlist.get_can_edit_metadata(),
can_edit_items: playlist.get_can_edit_items(),
can_cancel_membership: playlist.get_can_cancel_membership(),
}
}
}
from_repeated_enum!(PermissionLevel, PermissionLevels);

21
metadata/src/request.rs Normal file
View file

@ -0,0 +1,21 @@
use crate::MetadataError;
use librespot_core::{Error, Session};
pub type RequestResult = Result<bytes::Bytes, Error>;
#[async_trait]
pub trait MercuryRequest {
async fn request(session: &Session, uri: &str) -> RequestResult {
let request = session.mercury().get(uri)?;
let response = request.await?;
match response.payload.first() {
Some(data) => {
let data = data.to_vec().into();
trace!("Received metadata: {:?}", data);
Ok(data)
}
None => Err(Error::unavailable(MetadataError::Empty)),
}
}
}

104
metadata/src/restriction.rs Normal file
View file

@ -0,0 +1,104 @@
use std::{fmt::Debug, ops::Deref};
use crate::util::{from_repeated_enum, from_repeated_message};
use protocol::metadata::Restriction as RestrictionMessage;
use librespot_protocol as protocol;
pub use protocol::metadata::Restriction_Catalogue as RestrictionCatalogue;
pub use protocol::metadata::Restriction_Type as RestrictionType;
#[derive(Debug, Clone)]
pub struct Restriction {
pub catalogues: RestrictionCatalogues,
pub restriction_type: RestrictionType,
pub catalogue_strs: Vec<String>,
pub countries_allowed: Option<Vec<String>>,
pub countries_forbidden: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct Restrictions(pub Vec<Restriction>);
impl Deref for Restrictions {
type Target = Vec<Restriction>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct RestrictionCatalogues(pub Vec<RestrictionCatalogue>);
impl Deref for RestrictionCatalogues {
type Target = Vec<RestrictionCatalogue>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Restriction {
fn parse_country_codes(country_codes: &str) -> Vec<String> {
country_codes
.chunks(2)
.map(|country_code| country_code.to_owned())
.collect()
}
}
impl From<&RestrictionMessage> for Restriction {
fn from(restriction: &RestrictionMessage) -> Self {
let countries_allowed = if restriction.has_countries_allowed() {
Some(Self::parse_country_codes(
restriction.get_countries_allowed(),
))
} else {
None
};
let countries_forbidden = if restriction.has_countries_forbidden() {
Some(Self::parse_country_codes(
restriction.get_countries_forbidden(),
))
} else {
None
};
Self {
catalogues: restriction.get_catalogue().into(),
restriction_type: restriction.get_field_type(),
catalogue_strs: restriction.get_catalogue_str().to_vec(),
countries_allowed,
countries_forbidden,
}
}
}
from_repeated_message!(RestrictionMessage, Restrictions);
from_repeated_enum!(RestrictionCatalogue, RestrictionCatalogues);
struct StrChunks<'s>(&'s str, usize);
trait StrChunksExt {
fn chunks(&self, size: usize) -> StrChunks;
}
impl StrChunksExt for str {
fn chunks(&self, size: usize) -> StrChunks {
StrChunks(self, size)
}
}
impl<'s> Iterator for StrChunks<'s> {
type Item = &'s str;
fn next(&mut self) -> Option<&'s str> {
let &mut StrChunks(data, size) = self;
if data.is_empty() {
None
} else {
let ret = Some(&data[..size]);
self.0 = &data[size..];
ret
}
}
}

View file

@ -0,0 +1,37 @@
use std::{fmt::Debug, ops::Deref};
use crate::{restriction::Restrictions, util::from_repeated_message};
use librespot_core::date::Date;
use librespot_protocol as protocol;
use protocol::metadata::SalePeriod as SalePeriodMessage;
#[derive(Debug, Clone)]
pub struct SalePeriod {
pub restrictions: Restrictions,
pub start: Date,
pub end: Date,
}
#[derive(Debug, Clone)]
pub struct SalePeriods(pub Vec<SalePeriod>);
impl Deref for SalePeriods {
type Target = Vec<SalePeriod>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&SalePeriodMessage> for SalePeriod {
fn from(sale_period: &SalePeriodMessage) -> Self {
Self {
restrictions: sale_period.get_restriction().into(),
start: sale_period.get_start().into(),
end: sale_period.get_end().into(),
}
}
}
from_repeated_message!(SalePeriodMessage, SalePeriods);

74
metadata/src/show.rs Normal file
View file

@ -0,0 +1,74 @@
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
};
use crate::{
availability::Availabilities, copyright::Copyrights, episode::Episodes, image::Images,
restriction::Restrictions, Metadata, RequestResult,
};
use librespot_core::{Error, Session, SpotifyId};
use librespot_protocol as protocol;
pub use protocol::metadata::Show_ConsumptionOrder as ShowConsumptionOrder;
pub use protocol::metadata::Show_MediaType as ShowMediaType;
#[derive(Debug, Clone)]
pub struct Show {
pub id: SpotifyId,
pub name: String,
pub description: String,
pub publisher: String,
pub language: String,
pub is_explicit: bool,
pub covers: Images,
pub episodes: Episodes,
pub copyrights: Copyrights,
pub restrictions: Restrictions,
pub keywords: Vec<String>,
pub media_type: ShowMediaType,
pub consumption_order: ShowConsumptionOrder,
pub availability: Availabilities,
pub trailer_uri: SpotifyId,
pub has_music_and_talk: bool,
pub is_audiobook: bool,
}
#[async_trait]
impl Metadata for Show {
type Message = protocol::metadata::Show;
async fn request(session: &Session, show_id: SpotifyId) -> RequestResult {
session.spclient().get_show_metadata(show_id).await
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Show {
type Error = librespot_core::Error;
fn try_from(show: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: show.try_into()?,
name: show.get_name().to_owned(),
description: show.get_description().to_owned(),
publisher: show.get_publisher().to_owned(),
language: show.get_language().to_owned(),
is_explicit: show.get_explicit(),
covers: show.get_cover_image().get_image().into(),
episodes: show.get_episode().try_into()?,
copyrights: show.get_copyright().into(),
restrictions: show.get_restriction().into(),
keywords: show.get_keyword().to_vec(),
media_type: show.get_media_type(),
consumption_order: show.get_consumption_order(),
availability: show.get_availability().into(),
trailer_uri: SpotifyId::from_uri(show.get_trailer_uri())?,
has_music_and_talk: show.get_music_and_talk(),
is_audiobook: show.get_is_audiobook(),
})
}
}

149
metadata/src/track.rs Normal file
View file

@ -0,0 +1,149 @@
use std::{
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Deref,
};
use chrono::Local;
use uuid::Uuid;
use crate::{
artist::{Artists, ArtistsWithRole},
audio::{
file::AudioFiles,
item::{AudioItem, AudioItemResult, InnerAudioItem},
},
availability::{Availabilities, UnavailabilityReason},
content_rating::ContentRatings,
external_id::ExternalIds,
restriction::Restrictions,
sale_period::SalePeriods,
util::try_from_repeated_message,
Metadata, RequestResult,
};
use librespot_core::{date::Date, Error, Session, SpotifyId};
use librespot_protocol as protocol;
#[derive(Debug, Clone)]
pub struct Track {
pub id: SpotifyId,
pub name: String,
pub album: SpotifyId,
pub artists: Artists,
pub number: i32,
pub disc_number: i32,
pub duration: i32,
pub popularity: i32,
pub is_explicit: bool,
pub external_ids: ExternalIds,
pub restrictions: Restrictions,
pub files: AudioFiles,
pub alternatives: Tracks,
pub sale_periods: SalePeriods,
pub previews: AudioFiles,
pub tags: Vec<String>,
pub earliest_live_timestamp: Date,
pub has_lyrics: bool,
pub availability: Availabilities,
pub licensor: Uuid,
pub language_of_performance: Vec<String>,
pub content_ratings: ContentRatings,
pub original_title: String,
pub version_title: String,
pub artists_with_role: ArtistsWithRole,
}
#[derive(Debug, Clone)]
pub struct Tracks(pub Vec<SpotifyId>);
impl Deref for Tracks {
type Target = Vec<SpotifyId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl InnerAudioItem for Track {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult {
let track = Self::get(session, id).await?;
let alternatives = {
if track.alternatives.is_empty() {
None
} else {
Some(track.alternatives.clone())
}
};
// TODO: check meaning of earliest_live_timestamp in
let availability = if Local::now() < track.earliest_live_timestamp.as_utc() {
Err(UnavailabilityReason::Embargo)
} else {
Self::available_for_user(
&session.user_data(),
&track.availability,
&track.restrictions,
)
};
Ok(AudioItem {
id,
spotify_uri: id.to_uri(),
files: track.files,
name: track.name,
duration: track.duration,
availability,
alternatives,
})
}
}
#[async_trait]
impl Metadata for Track {
type Message = protocol::metadata::Track;
async fn request(session: &Session, track_id: SpotifyId) -> RequestResult {
session.spclient().get_track_metadata(track_id).await
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, Error> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Track {
type Error = librespot_core::Error;
fn try_from(track: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: track.try_into()?,
name: track.get_name().to_owned(),
album: track.get_album().try_into()?,
artists: track.get_artist().try_into()?,
number: track.get_number(),
disc_number: track.get_disc_number(),
duration: track.get_duration(),
popularity: track.get_popularity(),
is_explicit: track.get_explicit(),
external_ids: track.get_external_id().into(),
restrictions: track.get_restriction().into(),
files: track.get_file().into(),
alternatives: track.get_alternative().try_into()?,
sale_periods: track.get_sale_period().into(),
previews: track.get_preview().into(),
tags: track.get_tags().to_vec(),
earliest_live_timestamp: track.get_earliest_live_timestamp().try_into()?,
has_lyrics: track.get_has_lyrics(),
availability: track.get_availability().into(),
licensor: Uuid::from_slice(track.get_licensor().get_uuid())
.unwrap_or_else(|_| Uuid::nil()),
language_of_performance: track.get_language_of_performance().to_vec(),
content_ratings: track.get_content_rating().into(),
original_title: track.get_original_title().to_owned(),
version_title: track.get_version_title().to_owned(),
artists_with_role: track.get_artist_with_role().try_into()?,
})
}
}
try_from_repeated_message!(<Track as Metadata>::Message, Tracks);

39
metadata/src/util.rs Normal file
View file

@ -0,0 +1,39 @@
macro_rules! from_repeated_message {
($src:ty, $dst:ty) => {
impl From<&[$src]> for $dst {
fn from(src: &[$src]) -> Self {
let result = src.iter().map(From::from).collect();
Self(result)
}
}
};
}
pub(crate) use from_repeated_message;
macro_rules! from_repeated_enum {
($src:ty, $dst:ty) => {
impl From<&[$src]> for $dst {
fn from(src: &[$src]) -> Self {
let result = src.iter().map(|x| <$src>::from(*x)).collect();
Self(result)
}
}
};
}
pub(crate) use from_repeated_enum;
macro_rules! try_from_repeated_message {
($src:ty, $dst:ty) => {
impl TryFrom<&[$src]> for $dst {
type Error = librespot_core::Error;
fn try_from(src: &[$src]) -> Result<Self, Self::Error> {
let result: Result<Vec<_>, _> = src.iter().map(TryFrom::try_from).collect();
Ok(Self(result?))
}
}
};
}
pub(crate) use try_from_repeated_message;

20
metadata/src/video.rs Normal file
View file

@ -0,0 +1,20 @@
use std::{fmt::Debug, ops::Deref};
use crate::util::from_repeated_message;
use librespot_core::FileId;
use librespot_protocol as protocol;
use protocol::metadata::VideoFile as VideoFileMessage;
#[derive(Debug, Clone)]
pub struct VideoFiles(pub Vec<FileId>);
impl Deref for VideoFiles {
type Target = Vec<FileId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
from_repeated_message!(VideoFileMessage, VideoFiles);

View file

@ -1,6 +1,6 @@
[package] [package]
name = "librespot-playback" name = "librespot-playback"
version = "0.2.0" version = "0.3.1"
authors = ["Sasha Hilton <sashahilton00@gmail.com>"] authors = ["Sasha Hilton <sashahilton00@gmail.com>"]
description = "The audio playback logic for librespot" description = "The audio playback logic for librespot"
license = "MIT" license = "MIT"
@ -9,13 +9,13 @@ edition = "2018"
[dependencies.librespot-audio] [dependencies.librespot-audio]
path = "../audio" path = "../audio"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-core] [dependencies.librespot-core]
path = "../core" path = "../core"
version = "0.2.0" version = "0.3.1"
[dependencies.librespot-metadata] [dependencies.librespot-metadata]
path = "../metadata" path = "../metadata"
version = "0.2.0" version = "0.3.1"
[dependencies] [dependencies]
futures-executor = "0.3" futures-executor = "0.3"
@ -23,7 +23,8 @@ futures-util = { version = "0.3", default_features = false, features = ["alloc"]
log = "0.4" log = "0.4"
byteorder = "1.4" byteorder = "1.4"
shell-words = "1.0.0" shell-words = "1.0.0"
tokio = { version = "1", features = ["sync"] } thiserror = "1.0"
tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] }
zerocopy = { version = "0.3" } zerocopy = { version = "0.3" }
# Backends # Backends
@ -40,22 +41,21 @@ glib = { version = "0.10", optional = true }
# Rodio dependencies # Rodio dependencies
rodio = { version = "0.14", optional = true, default-features = false } rodio = { version = "0.14", optional = true, default-features = false }
cpal = { version = "0.13", optional = true } cpal = { version = "0.13", optional = true }
thiserror = { version = "1", optional = true }
# Decoder # Decoder
lewton = "0.10" lewton = "0.10"
ogg = "0.8" ogg = "0.8"
# Dithering # Dithering
rand = "0.8" rand = { version = "0.8", features = ["small_rng"] }
rand_distr = "0.4" rand_distr = "0.4"
[features] [features]
alsa-backend = ["alsa", "thiserror"] alsa-backend = ["alsa"]
portaudio-backend = ["portaudio-rs"] portaudio-backend = ["portaudio-rs"]
pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"]
jackaudio-backend = ["jack"] jackaudio-backend = ["jack"]
rodio-backend = ["rodio", "cpal", "thiserror"] rodio-backend = ["rodio", "cpal"]
rodiojack-backend = ["rodio", "cpal/jack", "thiserror"] rodiojack-backend = ["rodio", "cpal/jack"]
sdl-backend = ["sdl2"] sdl-backend = ["sdl2"]
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"]

View file

@ -1,51 +1,102 @@
use super::{Open, Sink, SinkAsBytes}; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::{NUM_CHANNELS, SAMPLE_RATE}; use crate::{NUM_CHANNELS, SAMPLE_RATE};
use alsa::device_name::HintIter; use alsa::device_name::HintIter;
use alsa::pcm::{Access, Format, HwParams, PCM}; use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
use alsa::{Direction, ValueOr}; use alsa::{Direction, ValueOr};
use std::cmp::min; use std::cmp::min;
use std::io;
use std::process::exit; use std::process::exit;
use std::time::Duration;
use thiserror::Error; use thiserror::Error;
// 125 ms Period time * 4 periods = 0.5 sec buffer. const MAX_BUFFER: Frames = (SAMPLE_RATE / 2) as Frames;
const PERIOD_TIME: Duration = Duration::from_millis(125); const MIN_BUFFER: Frames = (SAMPLE_RATE / 10) as Frames;
const NUM_PERIODS: u32 = 4; const ZERO_FRAMES: Frames = 0;
const MAX_PERIOD_DIVISOR: Frames = 4;
const MIN_PERIOD_DIVISOR: Frames = 10;
#[derive(Debug, Error)] #[derive(Debug, Error)]
enum AlsaError { enum AlsaError {
#[error("AlsaSink, device {device} may be invalid or busy, {err}")] #[error("<AlsaSink> Device {device} Unsupported Format {alsa_format:?} ({format:?}), {e}")]
PcmSetUp { device: String, err: alsa::Error },
#[error("AlsaSink, device {device} unsupported access type RWInterleaved, {err}")]
UnsupportedAccessType { device: String, err: alsa::Error },
#[error("AlsaSink, device {device} unsupported format {format:?}, {err}")]
UnsupportedFormat { UnsupportedFormat {
device: String, device: String,
alsa_format: Format,
format: AudioFormat, format: AudioFormat,
err: alsa::Error, e: alsa::Error,
}, },
#[error("AlsaSink, device {device} unsupported sample rate {samplerate}, {err}")]
UnsupportedSampleRate { #[error("<AlsaSink> Device {device} Unsupported Channel Count {channel_count}, {e}")]
device: String,
samplerate: u32,
err: alsa::Error,
},
#[error("AlsaSink, device {device} unsupported channel count {channel_count}, {err}")]
UnsupportedChannelCount { UnsupportedChannelCount {
device: String, device: String,
channel_count: u8, channel_count: u8,
err: alsa::Error, e: alsa::Error,
}, },
#[error("AlsaSink Hardware Parameters Error, {0}")]
#[error("<AlsaSink> Device {device} Unsupported Sample Rate {samplerate}, {e}")]
UnsupportedSampleRate {
device: String,
samplerate: u32,
e: alsa::Error,
},
#[error("<AlsaSink> Device {device} Unsupported Access Type RWInterleaved, {e}")]
UnsupportedAccessType { device: String, e: alsa::Error },
#[error("<AlsaSink> Device {device} May be Invalid, Busy, or Already in Use, {e}")]
PcmSetUp { device: String, e: alsa::Error },
#[error("<AlsaSink> Failed to Drain PCM Buffer, {0}")]
DrainFailure(alsa::Error),
#[error("<AlsaSink> {0}")]
OnWrite(alsa::Error),
#[error("<AlsaSink> Hardware, {0}")]
HwParams(alsa::Error), HwParams(alsa::Error),
#[error("AlsaSink Software Parameters Error, {0}")]
#[error("<AlsaSink> Software, {0}")]
SwParams(alsa::Error), SwParams(alsa::Error),
#[error("AlsaSink PCM Error, {0}")]
#[error("<AlsaSink> PCM, {0}")]
Pcm(alsa::Error), Pcm(alsa::Error),
#[error("<AlsaSink> Could Not Parse Ouput Name(s) and/or Description(s)")]
Parsing,
#[error("<AlsaSink>")]
NotConnected,
}
impl From<AlsaError> for SinkError {
fn from(e: AlsaError) -> SinkError {
use AlsaError::*;
let es = e.to_string();
match e {
DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es),
PcmSetUp { .. } => SinkError::ConnectionRefused(es),
NotConnected => SinkError::NotConnected(es),
_ => SinkError::InvalidParams(es),
}
}
}
impl From<AudioFormat> for Format {
fn from(f: AudioFormat) -> Format {
use AudioFormat::*;
match f {
F64 => Format::float64(),
F32 => Format::float(),
S32 => Format::s32(),
S24 => Format::s24(),
S16 => Format::s16(),
#[cfg(target_endian = "little")]
S24_3 => Format::S243LE,
#[cfg(target_endian = "big")]
S24_3 => Format::S243BE,
}
}
} }
pub struct AlsaSink { pub struct AlsaSink {
@ -55,26 +106,50 @@ pub struct AlsaSink {
period_buffer: Vec<u8>, period_buffer: Vec<u8>,
} }
fn list_outputs() -> io::Result<()> { fn list_compatible_devices() -> SinkResult<()> {
println!("Listing available Alsa outputs:"); println!("\n\n\tCompatible alsa device(s):\n");
for t in &["pcm", "ctl", "hwdep"] { println!("\t------------------------------------------------------\n");
println!("{} devices:", t);
let i = match HintIter::new_str(None, &t) { let i = HintIter::new_str(None, "pcm").map_err(|_| AlsaError::Parsing)?;
Ok(i) => i,
Err(e) => {
return Err(io::Error::new(io::ErrorKind::Other, e));
}
};
for a in i { for a in i {
if let Some(Direction::Playback) = a.direction { if let Some(Direction::Playback) = a.direction {
// mimic aplay -L let name = a.name.ok_or(AlsaError::Parsing)?;
let name = a let desc = a.desc.ok_or(AlsaError::Parsing)?;
.name
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not parse name"))?; if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) {
let desc = a if let Ok(hwp) = HwParams::any(&pcm) {
.desc // Only show devices that support
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not parse desc"))?; // 2 ch 44.1 Interleaved.
println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t")); if hwp.set_access(Access::RWInterleaved).is_ok()
&& hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).is_ok()
&& hwp.set_channels(NUM_CHANNELS as u32).is_ok()
{
println!("\tDevice:\n\n\t\t{}\n", name);
println!("\tDescription:\n\n\t\t{}\n", desc.replace("\n", "\n\t\t"));
let mut supported_formats = vec![];
for f in &[
AudioFormat::S16,
AudioFormat::S24,
AudioFormat::S24_3,
AudioFormat::S32,
AudioFormat::F32,
AudioFormat::F64,
] {
if hwp.test_format(Format::from(*f)).is_ok() {
supported_formats.push(format!("{:?}", f));
}
}
println!(
"\tSupported Format(s):\n\n\t\t{}\n",
supported_formats.join(" ")
);
println!("\t------------------------------------------------------\n");
}
};
} }
} }
} }
@ -82,45 +157,36 @@ fn list_outputs() -> io::Result<()> {
Ok(()) Ok(())
} }
fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), AlsaError> { fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> {
let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp { let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp {
device: dev_name.to_string(), device: dev_name.to_string(),
err: e, e,
})?; })?;
let alsa_format = match format {
AudioFormat::F64 => Format::float64(),
AudioFormat::F32 => Format::float(),
AudioFormat::S32 => Format::s32(),
AudioFormat::S24 => Format::s24(),
AudioFormat::S16 => Format::s16(),
#[cfg(target_endian = "little")]
AudioFormat::S24_3 => Format::S243LE,
#[cfg(target_endian = "big")]
AudioFormat::S24_3 => Format::S243BE,
};
let bytes_per_period = { let bytes_per_period = {
let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?; let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?;
hwp.set_access(Access::RWInterleaved) hwp.set_access(Access::RWInterleaved)
.map_err(|e| AlsaError::UnsupportedAccessType { .map_err(|e| AlsaError::UnsupportedAccessType {
device: dev_name.to_string(), device: dev_name.to_string(),
err: e, e,
})?; })?;
let alsa_format = Format::from(format);
hwp.set_format(alsa_format) hwp.set_format(alsa_format)
.map_err(|e| AlsaError::UnsupportedFormat { .map_err(|e| AlsaError::UnsupportedFormat {
device: dev_name.to_string(), device: dev_name.to_string(),
alsa_format,
format, format,
err: e, e,
})?; })?;
hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| { hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| {
AlsaError::UnsupportedSampleRate { AlsaError::UnsupportedSampleRate {
device: dev_name.to_string(), device: dev_name.to_string(),
samplerate: SAMPLE_RATE, samplerate: SAMPLE_RATE,
err: e, e,
} }
})?; })?;
@ -128,47 +194,209 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Alsa
.map_err(|e| AlsaError::UnsupportedChannelCount { .map_err(|e| AlsaError::UnsupportedChannelCount {
device: dev_name.to_string(), device: dev_name.to_string(),
channel_count: NUM_CHANNELS, channel_count: NUM_CHANNELS,
err: e, e,
})?; })?;
// Deal strictly in time and periods. // Clone the hwp while it's in
hwp.set_periods(NUM_PERIODS, ValueOr::Nearest) // a good working state so that
.map_err(AlsaError::HwParams)?; // in the event of an error setting
// the buffer and period sizes
// we can use the good working clone
// instead of the hwp that's in an
// error state.
let hwp_clone = hwp.clone();
hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest) // At a sampling rate of 44100:
.map_err(AlsaError::HwParams)?; // The largest buffer is 22050 Frames (500ms) with 5512 Frame periods (125ms).
// The smallest buffer is 4410 Frames (100ms) with 441 Frame periods (10ms).
// Actual values may vary.
//
// Larger buffer and period sizes are preferred as extremely small values
// will cause high CPU useage.
//
// If no buffer or period size is in those ranges or an error happens
// trying to set the buffer or period size use the device's defaults
// which may not be ideal but are *hopefully* serviceable.
let buffer_size = {
let max = match hwp.get_buffer_size_max() {
Err(e) => {
trace!("Error getting the device's max Buffer size: {}", e);
ZERO_FRAMES
}
Ok(s) => s,
};
let min = match hwp.get_buffer_size_min() {
Err(e) => {
trace!("Error getting the device's min Buffer size: {}", e);
ZERO_FRAMES
}
Ok(s) => s,
};
let buffer_size = if min < max {
match (MIN_BUFFER..=MAX_BUFFER)
.rev()
.find(|f| (min..=max).contains(f))
{
Some(size) => {
trace!("Desired Frames per Buffer: {:?}", size);
match hwp.set_buffer_size_near(size) {
Err(e) => {
trace!("Error setting the device's Buffer size: {}", e);
ZERO_FRAMES
}
Ok(s) => s,
}
}
None => {
trace!("No Desired Buffer size in range reported by the device.");
ZERO_FRAMES
}
}
} else {
trace!("The device's min reported Buffer size was greater than or equal to it's max reported Buffer size.");
ZERO_FRAMES
};
if buffer_size == ZERO_FRAMES {
trace!(
"Desired Buffer Frame range: {:?} - {:?}",
MIN_BUFFER,
MAX_BUFFER
);
trace!(
"Actual Buffer Frame range as reported by the device: {:?} - {:?}",
min,
max
);
}
buffer_size
};
let period_size = {
if buffer_size == ZERO_FRAMES {
ZERO_FRAMES
} else {
let max = match hwp.get_period_size_max() {
Err(e) => {
trace!("Error getting the device's max Period size: {}", e);
ZERO_FRAMES
}
Ok(s) => s,
};
let min = match hwp.get_period_size_min() {
Err(e) => {
trace!("Error getting the device's min Period size: {}", e);
ZERO_FRAMES
}
Ok(s) => s,
};
let max_period = buffer_size / MAX_PERIOD_DIVISOR;
let min_period = buffer_size / MIN_PERIOD_DIVISOR;
let period_size = if min < max && min_period < max_period {
match (min_period..=max_period)
.rev()
.find(|f| (min..=max).contains(f))
{
Some(size) => {
trace!("Desired Frames per Period: {:?}", size);
match hwp.set_period_size_near(size, ValueOr::Nearest) {
Err(e) => {
trace!("Error setting the device's Period size: {}", e);
ZERO_FRAMES
}
Ok(s) => s,
}
}
None => {
trace!("No Desired Period size in range reported by the device.");
ZERO_FRAMES
}
}
} else {
trace!("The device's min reported Period size was greater than or equal to it's max reported Period size,");
trace!("or the desired min Period size was greater than or equal to the desired max Period size.");
ZERO_FRAMES
};
if period_size == ZERO_FRAMES {
trace!("Buffer size: {:?}", buffer_size);
trace!(
"Desired Period Frame range: {:?} (Buffer size / {:?}) - {:?} (Buffer size / {:?})",
min_period,
MIN_PERIOD_DIVISOR,
max_period,
MAX_PERIOD_DIVISOR,
);
trace!(
"Actual Period Frame range as reported by the device: {:?} - {:?}",
min,
max
);
}
period_size
}
};
if buffer_size == ZERO_FRAMES || period_size == ZERO_FRAMES {
trace!(
"Failed to set Buffer and/or Period size, falling back to the device's defaults."
);
trace!("You may experience higher than normal CPU usage and/or audio issues.");
pcm.hw_params(&hwp_clone).map_err(AlsaError::Pcm)?;
} else {
pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?;
}
let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; let hwp = pcm.hw_params_current().map_err(AlsaError::Pcm)?;
// Don't assume we got what we wanted. // Don't assume we got what we wanted. Ask to make sure.
// Ask to make sure.
let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?; let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?;
let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?; let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?;
let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?;
swp.set_start_threshold(frames_per_buffer - frames_per_period) swp.set_start_threshold(frames_per_buffer - frames_per_period)
.map_err(AlsaError::SwParams)?; .map_err(AlsaError::SwParams)?;
pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; pcm.sw_params(&swp).map_err(AlsaError::Pcm)?;
trace!("Actual Frames per Buffer: {:?}", frames_per_buffer);
trace!("Actual Frames per Period: {:?}", frames_per_period);
// Let ALSA do the math for us. // Let ALSA do the math for us.
pcm.frames_to_bytes(frames_per_period) as usize pcm.frames_to_bytes(frames_per_period) as usize
}; };
trace!("Period Buffer size in bytes: {:?}", bytes_per_period);
Ok((pcm, bytes_per_period)) Ok((pcm, bytes_per_period))
} }
impl Open for AlsaSink { impl Open for AlsaSink {
fn open(device: Option<String>, format: AudioFormat) -> Self { fn open(device: Option<String>, format: AudioFormat) -> Self {
let name = match device.as_deref() { let name = match device.as_deref() {
Some("?") => match list_outputs() { Some("?") => match list_compatible_devices() {
Ok(_) => { Ok(_) => {
exit(0); exit(0);
} }
Err(err) => { Err(e) => {
error!("Error listing Alsa outputs, {}", err); error!("{}", e);
exit(1); exit(1);
} }
}, },
@ -189,38 +417,35 @@ impl Open for AlsaSink {
} }
impl Sink for AlsaSink { impl Sink for AlsaSink {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> SinkResult<()> {
if self.pcm.is_none() { if self.pcm.is_none() {
match open_device(&self.device, self.format) { let (pcm, bytes_per_period) = open_device(&self.device, self.format)?;
Ok((pcm, bytes_per_period)) => {
self.pcm = Some(pcm); self.pcm = Some(pcm);
if self.period_buffer.capacity() != bytes_per_period {
self.period_buffer = Vec::with_capacity(bytes_per_period); self.period_buffer = Vec::with_capacity(bytes_per_period);
} }
Err(e) => {
return Err(io::Error::new(io::ErrorKind::Other, e)); // Should always match the "Period Buffer size in bytes: " trace! message.
} trace!(
} "Period Buffer capacity: {:?}",
self.period_buffer.capacity()
);
} }
Ok(()) Ok(())
} }
fn stop(&mut self) -> io::Result<()> { fn stop(&mut self) -> SinkResult<()> {
{ // Zero fill the remainder of the period buffer and
// Write any leftover data in the period buffer // write any leftover data before draining the actual PCM buffer.
// before draining the actual buffer self.period_buffer.resize(self.period_buffer.capacity(), 0);
self.write_bytes(&[])?; self.write_buf()?;
let pcm = self.pcm.as_mut().ok_or_else(|| {
io::Error::new(io::ErrorKind::Other, "Error stopping AlsaSink, PCM is None") let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?;
})?;
pcm.drain().map_err(|e| { pcm.drain().map_err(AlsaError::DrainFailure)?;
io::Error::new(
io::ErrorKind::Other,
format!("Error stopping AlsaSink {}", e),
)
})?
}
self.pcm = None;
Ok(()) Ok(())
} }
@ -228,55 +453,51 @@ impl Sink for AlsaSink {
} }
impl SinkAsBytes for AlsaSink { impl SinkAsBytes for AlsaSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
let mut processed_data = 0; let mut start_index = 0;
while processed_data < data.len() { let data_len = data.len();
let data_to_buffer = min( let capacity = self.period_buffer.capacity();
self.period_buffer.capacity() - self.period_buffer.len(),
data.len() - processed_data, loop {
); let data_left = data_len - start_index;
let space_left = capacity - self.period_buffer.len();
let data_to_buffer = min(data_left, space_left);
let end_index = start_index + data_to_buffer;
self.period_buffer self.period_buffer
.extend_from_slice(&data[processed_data..processed_data + data_to_buffer]); .extend_from_slice(&data[start_index..end_index]);
processed_data += data_to_buffer;
if self.period_buffer.len() == self.period_buffer.capacity() { if self.period_buffer.len() == capacity {
self.write_buf()?; self.write_buf()?;
self.period_buffer.clear();
}
} }
Ok(()) if end_index == data_len {
break Ok(());
}
start_index = end_index;
}
} }
} }
impl AlsaSink { impl AlsaSink {
pub const NAME: &'static str = "alsa"; pub const NAME: &'static str = "alsa";
fn write_buf(&mut self) -> io::Result<()> { fn write_buf(&mut self) -> SinkResult<()> {
let pcm = self.pcm.as_mut().ok_or_else(|| { let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?;
io::Error::new(
io::ErrorKind::Other, if let Err(e) = pcm.io_bytes().writei(&self.period_buffer) {
"Error writing from AlsaSink buffer to PCM, PCM is None",
)
})?;
let io = pcm.io_bytes();
if let Err(err) = io.writei(&self.period_buffer) {
// Capture and log the original error as a warning, and then try to recover. // Capture and log the original error as a warning, and then try to recover.
// If recovery fails then forward that error back to player. // If recovery fails then forward that error back to player.
warn!( warn!(
"Error writing from AlsaSink buffer to PCM, trying to recover {}", "Error writing from AlsaSink buffer to PCM, trying to recover, {}",
err
);
pcm.try_recover(err, false).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!(
"Error writing from AlsaSink buffer to PCM, recovery failed {}",
e e
), );
)
})? pcm.try_recover(e, false).map_err(AlsaError::OnWrite)?
} }
self.period_buffer.clear();
Ok(()) Ok(())
} }
} }

View file

@ -1,4 +1,4 @@
use super::{Open, Sink, SinkAsBytes}; use super::{Open, Sink, SinkAsBytes, SinkResult};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
@ -11,7 +11,7 @@ use gst::prelude::*;
use zerocopy::AsBytes; use zerocopy::AsBytes;
use std::sync::mpsc::{sync_channel, SyncSender}; use std::sync::mpsc::{sync_channel, SyncSender};
use std::{io, thread}; use std::thread;
#[allow(dead_code)] #[allow(dead_code)]
pub struct GstreamerSink { pub struct GstreamerSink {
@ -131,7 +131,7 @@ impl Sink for GstreamerSink {
} }
impl SinkAsBytes for GstreamerSink { impl SinkAsBytes for GstreamerSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
// Copy expensively (in to_vec()) to avoid thread synchronization // Copy expensively (in to_vec()) to avoid thread synchronization
self.tx self.tx
.send(data.to_vec()) .send(data.to_vec())

View file

@ -1,4 +1,4 @@
use super::{Open, Sink}; use super::{Open, Sink, SinkError, SinkResult};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
@ -6,7 +6,6 @@ use crate::NUM_CHANNELS;
use jack::{ use jack::{
AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope,
}; };
use std::io;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
pub struct JackSink { pub struct JackSink {
@ -25,15 +24,12 @@ pub struct JackData {
impl ProcessHandler for JackData { impl ProcessHandler for JackData {
fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control { fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control {
// get output port buffers // get output port buffers
let mut out_r = self.port_r.as_mut_slice(ps); let buf_r: &mut [f32] = self.port_r.as_mut_slice(ps);
let mut out_l = self.port_l.as_mut_slice(ps); let buf_l: &mut [f32] = self.port_l.as_mut_slice(ps);
let buf_r: &mut [f32] = &mut out_r;
let buf_l: &mut [f32] = &mut out_l;
// get queue iterator // get queue iterator
let mut queue_iter = self.rec.try_iter(); let mut queue_iter = self.rec.try_iter();
let buf_size = buf_r.len(); for i in 0..buf_r.len() {
for i in 0..buf_size {
buf_r[i] = queue_iter.next().unwrap_or(0.0); buf_r[i] = queue_iter.next().unwrap_or(0.0);
buf_l[i] = queue_iter.next().unwrap_or(0.0); buf_l[i] = queue_iter.next().unwrap_or(0.0);
} }
@ -70,8 +66,12 @@ impl Open for JackSink {
} }
impl Sink for JackSink { impl Sink for JackSink {
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> {
let samples_f32: &[f32] = &converter.f64_to_f32(packet.samples()); let samples = packet
.samples()
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
for sample in samples_f32.iter() { for sample in samples_f32.iter() {
let res = self.send.send(*sample); let res = self.send.send(*sample);
if res.is_err() { if res.is_err() {

View file

@ -1,26 +1,40 @@
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use std::io; use thiserror::Error;
#[derive(Debug, Error)]
pub enum SinkError {
#[error("Audio Sink Error Not Connected: {0}")]
NotConnected(String),
#[error("Audio Sink Error Connection Refused: {0}")]
ConnectionRefused(String),
#[error("Audio Sink Error On Write: {0}")]
OnWrite(String),
#[error("Audio Sink Error Invalid Parameters: {0}")]
InvalidParams(String),
}
pub type SinkResult<T> = Result<T, SinkError>;
pub trait Open { pub trait Open {
fn open(_: Option<String>, format: AudioFormat) -> Self; fn open(_: Option<String>, format: AudioFormat) -> Self;
} }
pub trait Sink { pub trait Sink {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> SinkResult<()> {
Ok(()) Ok(())
} }
fn stop(&mut self) -> io::Result<()> { fn stop(&mut self) -> SinkResult<()> {
Ok(()) Ok(())
} }
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()>; fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()>;
} }
pub type SinkBuilder = fn(Option<String>, AudioFormat) -> Box<dyn Sink>; pub type SinkBuilder = fn(Option<String>, AudioFormat) -> Box<dyn Sink>;
pub trait SinkAsBytes { pub trait SinkAsBytes {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()>;
} }
fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> { fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
@ -30,7 +44,7 @@ fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat
// reuse code for various backends // reuse code for various backends
macro_rules! sink_as_bytes { macro_rules! sink_as_bytes {
() => { () => {
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> {
use crate::convert::i24; use crate::convert::i24;
use zerocopy::AsBytes; use zerocopy::AsBytes;
match packet { match packet {
@ -90,7 +104,7 @@ use self::gstreamer::GstreamerSink;
#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] #[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))]
mod rodio; mod rodio;
#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] #[cfg(feature = "rodio-backend")]
use self::rodio::RodioSink; use self::rodio::RodioSink;
#[cfg(feature = "sdl-backend")] #[cfg(feature = "sdl-backend")]
@ -132,11 +146,6 @@ pub fn find(name: Option<String>) -> Option<SinkBuilder> {
.find(|backend| name == backend.0) .find(|backend| name == backend.0)
.map(|backend| backend.1) .map(|backend| backend.1)
} else { } else {
Some( BACKENDS.first().map(|backend| backend.1)
BACKENDS
.first()
.expect("No backends were enabled at build time")
.1,
)
} }
} }

View file

@ -1,4 +1,4 @@
use super::{Open, Sink, SinkAsBytes}; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
@ -23,14 +23,14 @@ impl Open for StdoutSink {
} }
impl Sink for StdoutSink { impl Sink for StdoutSink {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> SinkResult<()> {
if self.output.is_none() { if self.output.is_none() {
let output: Box<dyn Write> = match self.path.as_deref() { let output: Box<dyn Write> = match self.path.as_deref() {
Some(path) => { Some(path) => {
let open_op = OpenOptions::new() let open_op = OpenOptions::new()
.write(true) .write(true)
.open(path) .open(path)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?;
Box::new(open_op) Box::new(open_op)
} }
None => Box::new(io::stdout()), None => Box::new(io::stdout()),
@ -46,14 +46,18 @@ impl Sink for StdoutSink {
} }
impl SinkAsBytes for StdoutSink { impl SinkAsBytes for StdoutSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
match self.output.as_deref_mut() { match self.output.as_deref_mut() {
Some(output) => { Some(output) => {
output.write_all(data)?; output
output.flush()?; .write_all(data)
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
output
.flush()
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
} }
None => { None => {
return Err(io::Error::new(io::ErrorKind::Other, "Output is None")); return Err(SinkError::NotConnected("Output is None".to_string()));
} }
} }

View file

@ -1,11 +1,10 @@
use super::{Open, Sink}; use super::{Open, Sink, SinkError, SinkResult};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::{NUM_CHANNELS, SAMPLE_RATE}; use crate::{NUM_CHANNELS, SAMPLE_RATE};
use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo};
use portaudio_rs::stream::*; use portaudio_rs::stream::*;
use std::io;
use std::process::exit; use std::process::exit;
use std::time::Duration; use std::time::Duration;
@ -96,7 +95,7 @@ impl<'a> Open for PortAudioSink<'a> {
} }
impl<'a> Sink for PortAudioSink<'a> { impl<'a> Sink for PortAudioSink<'a> {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> SinkResult<()> {
macro_rules! start_sink { macro_rules! start_sink {
(ref mut $stream: ident, ref $parameters: ident) => {{ (ref mut $stream: ident, ref $parameters: ident) => {{
if $stream.is_none() { if $stream.is_none() {
@ -125,7 +124,7 @@ impl<'a> Sink for PortAudioSink<'a> {
Ok(()) Ok(())
} }
fn stop(&mut self) -> io::Result<()> { fn stop(&mut self) -> SinkResult<()> {
macro_rules! stop_sink { macro_rules! stop_sink {
(ref mut $stream: ident) => {{ (ref mut $stream: ident) => {{
$stream.as_mut().unwrap().stop().unwrap(); $stream.as_mut().unwrap().stop().unwrap();
@ -141,14 +140,17 @@ impl<'a> Sink for PortAudioSink<'a> {
Ok(()) Ok(())
} }
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> {
macro_rules! write_sink { macro_rules! write_sink {
(ref mut $stream: expr, $samples: expr) => { (ref mut $stream: expr, $samples: expr) => {
$stream.as_mut().unwrap().write($samples) $stream.as_mut().unwrap().write($samples)
}; };
} }
let samples = packet.samples(); let samples = packet
.samples()
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
let result = match self { let result = match self {
Self::F32(stream, _parameters) => { Self::F32(stream, _parameters) => {
let samples_f32: &[f32] = &converter.f64_to_f32(samples); let samples_f32: &[f32] = &converter.f64_to_f32(samples);

View file

@ -1,36 +1,87 @@
use super::{Open, Sink, SinkAsBytes}; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::{NUM_CHANNELS, SAMPLE_RATE}; use crate::{NUM_CHANNELS, SAMPLE_RATE};
use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_binding::{self as pulse, error::PAErr, stream::Direction};
use libpulse_simple_binding::Simple; use libpulse_simple_binding::Simple;
use std::io; use thiserror::Error;
const APP_NAME: &str = "librespot"; const APP_NAME: &str = "librespot";
const STREAM_NAME: &str = "Spotify endpoint"; const STREAM_NAME: &str = "Spotify endpoint";
#[derive(Debug, Error)]
enum PulseError {
#[error("<PulseAudioSink> Unsupported Pulseaudio Sample Spec, Format {pulse_format:?} ({format:?}), Channels {channels}, Rate {rate}")]
InvalidSampleSpec {
pulse_format: pulse::sample::Format,
format: AudioFormat,
channels: u8,
rate: u32,
},
#[error("<PulseAudioSink> {0}")]
ConnectionRefused(PAErr),
#[error("<PulseAudioSink> Failed to Drain Pulseaudio Buffer, {0}")]
DrainFailure(PAErr),
#[error("<PulseAudioSink>")]
NotConnected,
#[error("<PulseAudioSink> {0}")]
OnWrite(PAErr),
}
impl From<PulseError> for SinkError {
fn from(e: PulseError) -> SinkError {
use PulseError::*;
let es = e.to_string();
match e {
DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es),
ConnectionRefused(_) => SinkError::ConnectionRefused(es),
NotConnected => SinkError::NotConnected(es),
InvalidSampleSpec { .. } => SinkError::InvalidParams(es),
}
}
}
pub struct PulseAudioSink { pub struct PulseAudioSink {
s: Option<Simple>, s: Option<Simple>,
ss: pulse::sample::Spec,
device: Option<String>, device: Option<String>,
format: AudioFormat, format: AudioFormat,
} }
impl Open for PulseAudioSink { impl Open for PulseAudioSink {
fn open(device: Option<String>, format: AudioFormat) -> Self { fn open(device: Option<String>, format: AudioFormat) -> Self {
info!("Using PulseAudio sink with format: {:?}", format); let mut actual_format = format;
if actual_format == AudioFormat::F64 {
warn!("PulseAudio currently does not support F64 output");
actual_format = AudioFormat::F32;
}
info!("Using PulseAudioSink with format: {:?}", actual_format);
Self {
s: None,
device,
format: actual_format,
}
}
}
impl Sink for PulseAudioSink {
fn start(&mut self) -> SinkResult<()> {
if self.s.is_none() {
// PulseAudio calls S24 and S24_3 different from the rest of the world // PulseAudio calls S24 and S24_3 different from the rest of the world
let pulse_format = match format { let pulse_format = match self.format {
AudioFormat::F32 => pulse::sample::Format::FLOAT32NE, AudioFormat::F32 => pulse::sample::Format::FLOAT32NE,
AudioFormat::S32 => pulse::sample::Format::S32NE, AudioFormat::S32 => pulse::sample::Format::S32NE,
AudioFormat::S24 => pulse::sample::Format::S24_32NE, AudioFormat::S24 => pulse::sample::Format::S24_32NE,
AudioFormat::S24_3 => pulse::sample::Format::S24NE, AudioFormat::S24_3 => pulse::sample::Format::S24NE,
AudioFormat::S16 => pulse::sample::Format::S16NE, AudioFormat::S16 => pulse::sample::Format::S16NE,
_ => { _ => unreachable!(),
unimplemented!("PulseAudio currently does not support {:?} output", format)
}
}; };
let ss = pulse::sample::Spec { let ss = pulse::sample::Spec {
@ -38,47 +89,41 @@ impl Open for PulseAudioSink {
channels: NUM_CHANNELS, channels: NUM_CHANNELS,
rate: SAMPLE_RATE, rate: SAMPLE_RATE,
}; };
debug_assert!(ss.is_valid());
Self { if !ss.is_valid() {
s: None, let pulse_error = PulseError::InvalidSampleSpec {
ss, pulse_format,
device, format: self.format,
format, channels: NUM_CHANNELS,
} rate: SAMPLE_RATE,
} };
}
impl Sink for PulseAudioSink { return Err(SinkError::from(pulse_error));
fn start(&mut self) -> io::Result<()> {
if self.s.is_some() {
return Ok(());
} }
let device = self.device.as_deref(); let s = Simple::new(
let result = Simple::new(
None, // Use the default server. None, // Use the default server.
APP_NAME, // Our application's name. APP_NAME, // Our application's name.
Direction::Playback, // Direction. Direction::Playback, // Direction.
device, // Our device (sink) name. self.device.as_deref(), // Our device (sink) name.
STREAM_NAME, // Description of our stream. STREAM_NAME, // Description of our stream.
&self.ss, // Our sample format. &ss, // Our sample format.
None, // Use default channel map. None, // Use default channel map.
None, // Use default buffering attributes. None, // Use default buffering attributes.
); )
match result { .map_err(PulseError::ConnectionRefused)?;
Ok(s) => {
self.s = Some(s); self.s = Some(s);
Ok(())
}
Err(e) => Err(io::Error::new(
io::ErrorKind::ConnectionRefused,
e.to_string().unwrap(),
)),
}
} }
fn stop(&mut self) -> io::Result<()> { Ok(())
}
fn stop(&mut self) -> SinkResult<()> {
let s = self.s.as_mut().ok_or(PulseError::NotConnected)?;
s.drain().map_err(PulseError::DrainFailure)?;
self.s = None; self.s = None;
Ok(()) Ok(())
} }
@ -87,21 +132,12 @@ impl Sink for PulseAudioSink {
} }
impl SinkAsBytes for PulseAudioSink { impl SinkAsBytes for PulseAudioSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
if let Some(s) = &self.s { let s = self.s.as_mut().ok_or(PulseError::NotConnected)?;
match s.write(data) {
Ok(_) => Ok(()), s.write(data).map_err(PulseError::OnWrite)?;
Err(e) => Err(io::Error::new(
io::ErrorKind::BrokenPipe, Ok(())
e.to_string().unwrap(),
)),
}
} else {
Err(io::Error::new(
io::ErrorKind::NotConnected,
"Not connected to PulseAudio",
))
}
} }
} }

View file

@ -1,11 +1,11 @@
use std::process::exit; use std::process::exit;
use std::thread;
use std::time::Duration; use std::time::Duration;
use std::{io, thread};
use cpal::traits::{DeviceTrait, HostTrait}; use cpal::traits::{DeviceTrait, HostTrait};
use thiserror::Error; use thiserror::Error;
use super::Sink; use super::{Sink, SinkError, SinkResult};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
@ -33,16 +33,30 @@ pub fn mk_rodiojack(device: Option<String>, format: AudioFormat) -> Box<dyn Sink
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum RodioError { pub enum RodioError {
#[error("Rodio: no device available")] #[error("<RodioSink> No Device Available")]
NoDeviceAvailable, NoDeviceAvailable,
#[error("Rodio: device \"{0}\" is not available")] #[error("<RodioSink> device \"{0}\" is Not Available")]
DeviceNotAvailable(String), DeviceNotAvailable(String),
#[error("Rodio play error: {0}")] #[error("<RodioSink> Play Error: {0}")]
PlayError(#[from] rodio::PlayError), PlayError(#[from] rodio::PlayError),
#[error("Rodio stream error: {0}")] #[error("<RodioSink> Stream Error: {0}")]
StreamError(#[from] rodio::StreamError), StreamError(#[from] rodio::StreamError),
#[error("Cannot get audio devices: {0}")] #[error("<RodioSink> Cannot Get Audio Devices: {0}")]
DevicesError(#[from] cpal::DevicesError), DevicesError(#[from] cpal::DevicesError),
#[error("<RodioSink> {0}")]
Samples(String),
}
impl From<RodioError> for SinkError {
fn from(e: RodioError) -> SinkError {
use RodioError::*;
let es = e.to_string();
match e {
StreamError(_) | PlayError(_) | Samples(_) => SinkError::OnWrite(es),
NoDeviceAvailable | DeviceNotAvailable(_) => SinkError::ConnectionRefused(es),
DevicesError(_) => SinkError::InvalidParams(es),
}
}
} }
pub struct RodioSink { pub struct RodioSink {
@ -175,8 +189,10 @@ pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> Ro
} }
impl Sink for RodioSink { impl Sink for RodioSink {
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> {
let samples = packet.samples(); let samples = packet
.samples()
.map_err(|e| RodioError::Samples(e.to_string()))?;
match self.format { match self.format {
AudioFormat::F32 => { AudioFormat::F32 => {
let samples_f32: &[f32] = &converter.f64_to_f32(samples); let samples_f32: &[f32] = &converter.f64_to_f32(samples);
@ -211,5 +227,6 @@ impl Sink for RodioSink {
} }
impl RodioSink { impl RodioSink {
#[allow(dead_code)]
pub const NAME: &'static str = "rodio"; pub const NAME: &'static str = "rodio";
} }

View file

@ -1,11 +1,11 @@
use super::{Open, Sink}; use super::{Open, Sink, SinkError, SinkResult};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::{NUM_CHANNELS, SAMPLE_RATE}; use crate::{NUM_CHANNELS, SAMPLE_RATE};
use sdl2::audio::{AudioQueue, AudioSpecDesired}; use sdl2::audio::{AudioQueue, AudioSpecDesired};
use std::thread;
use std::time::Duration; use std::time::Duration;
use std::{io, thread};
pub enum SdlSink { pub enum SdlSink {
F32(AudioQueue<f32>), F32(AudioQueue<f32>),
@ -52,7 +52,7 @@ impl Open for SdlSink {
} }
impl Sink for SdlSink { impl Sink for SdlSink {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> SinkResult<()> {
macro_rules! start_sink { macro_rules! start_sink {
($queue: expr) => {{ ($queue: expr) => {{
$queue.clear(); $queue.clear();
@ -67,7 +67,7 @@ impl Sink for SdlSink {
Ok(()) Ok(())
} }
fn stop(&mut self) -> io::Result<()> { fn stop(&mut self) -> SinkResult<()> {
macro_rules! stop_sink { macro_rules! stop_sink {
($queue: expr) => {{ ($queue: expr) => {{
$queue.pause(); $queue.pause();
@ -82,7 +82,7 @@ impl Sink for SdlSink {
Ok(()) Ok(())
} }
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> {
macro_rules! drain_sink { macro_rules! drain_sink {
($queue: expr, $size: expr) => {{ ($queue: expr, $size: expr) => {{
// sleep and wait for sdl thread to drain the queue a bit // sleep and wait for sdl thread to drain the queue a bit
@ -92,7 +92,9 @@ impl Sink for SdlSink {
}}; }};
} }
let samples = packet.samples(); let samples = packet
.samples()
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
match self { match self {
Self::F32(queue) => { Self::F32(queue) => {
let samples_f32: &[f32] = &converter.f64_to_f32(samples); let samples_f32: &[f32] = &converter.f64_to_f32(samples);

View file

@ -1,10 +1,10 @@
use super::{Open, Sink, SinkAsBytes}; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use shell_words::split; use shell_words::split;
use std::io::{self, Write}; use std::io::Write;
use std::process::{Child, Command, Stdio}; use std::process::{Child, Command, Stdio};
pub struct SubprocessSink { pub struct SubprocessSink {
@ -30,21 +30,25 @@ impl Open for SubprocessSink {
} }
impl Sink for SubprocessSink { impl Sink for SubprocessSink {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> SinkResult<()> {
let args = split(&self.shell_command).unwrap(); let args = split(&self.shell_command).unwrap();
self.child = Some( let child = Command::new(&args[0])
Command::new(&args[0])
.args(&args[1..]) .args(&args[1..])
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.spawn()?, .spawn()
); .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?;
self.child = Some(child);
Ok(()) Ok(())
} }
fn stop(&mut self) -> io::Result<()> { fn stop(&mut self) -> SinkResult<()> {
if let Some(child) = &mut self.child.take() { if let Some(child) = &mut self.child.take() {
child.kill()?; child
child.wait()?; .kill()
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
child
.wait()
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
} }
Ok(()) Ok(())
} }
@ -53,11 +57,18 @@ impl Sink for SubprocessSink {
} }
impl SinkAsBytes for SubprocessSink { impl SinkAsBytes for SubprocessSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
if let Some(child) = &mut self.child { if let Some(child) = &mut self.child {
let child_stdin = child.stdin.as_mut().unwrap(); let child_stdin = child
child_stdin.write_all(data)?; .stdin
child_stdin.flush()?; .as_mut()
.ok_or_else(|| SinkError::NotConnected("Child is None".to_string()))?;
child_stdin
.write_all(data)
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
child_stdin
.flush()
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
} }
Ok(()) Ok(())
} }

View file

@ -76,10 +76,11 @@ impl AudioFormat {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum NormalisationType { pub enum NormalisationType {
Album, Album,
Track, Track,
Auto,
} }
impl FromStr for NormalisationType { impl FromStr for NormalisationType {
@ -88,6 +89,7 @@ impl FromStr for NormalisationType {
match s.to_lowercase().as_ref() { match s.to_lowercase().as_ref() {
"album" => Ok(Self::Album), "album" => Ok(Self::Album),
"track" => Ok(Self::Track), "track" => Ok(Self::Track),
"auto" => Ok(Self::Auto),
_ => Err(()), _ => Err(()),
} }
} }
@ -95,11 +97,11 @@ impl FromStr for NormalisationType {
impl Default for NormalisationType { impl Default for NormalisationType {
fn default() -> Self { fn default() -> Self {
Self::Album Self::Auto
} }
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum NormalisationMethod { pub enum NormalisationMethod {
Basic, Basic,
Dynamic, Dynamic,
@ -151,7 +153,7 @@ impl Default for PlayerConfig {
normalisation_type: NormalisationType::default(), normalisation_type: NormalisationType::default(),
normalisation_method: NormalisationMethod::default(), normalisation_method: NormalisationMethod::default(),
normalisation_pregain: 0.0, normalisation_pregain: 0.0,
normalisation_threshold: db_to_ratio(-1.0), normalisation_threshold: db_to_ratio(-2.0),
normalisation_attack: Duration::from_millis(5), normalisation_attack: Duration::from_millis(5),
normalisation_release: Duration::from_millis(100), normalisation_release: Duration::from_millis(100),
normalisation_knee: 1.0, normalisation_knee: 1.0,

View file

@ -1,22 +1,23 @@
use super::{AudioDecoder, AudioError, AudioPacket}; use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult};
use lewton::audio::AudioReadError::AudioIsHeader;
use lewton::inside_ogg::OggStreamReader; use lewton::inside_ogg::OggStreamReader;
use lewton::samples::InterleavedSamples; use lewton::samples::InterleavedSamples;
use lewton::OggReadError::NoCapturePatternFound;
use lewton::VorbisError::{BadAudio, OggError};
use std::error;
use std::fmt;
use std::io::{Read, Seek}; use std::io::{Read, Seek};
use std::time::Duration;
pub struct VorbisDecoder<R: Read + Seek>(OggStreamReader<R>); pub struct VorbisDecoder<R: Read + Seek>(OggStreamReader<R>);
pub struct VorbisError(lewton::VorbisError);
impl<R> VorbisDecoder<R> impl<R> VorbisDecoder<R>
where where
R: Read + Seek, R: Read + Seek,
{ {
pub fn new(input: R) -> Result<VorbisDecoder<R>, VorbisError> { pub fn new(input: R) -> DecoderResult<VorbisDecoder<R>> {
Ok(VorbisDecoder(OggStreamReader::new(input)?)) let reader =
OggStreamReader::new(input).map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?;
Ok(VorbisDecoder(reader))
} }
} }
@ -24,51 +25,22 @@ impl<R> AudioDecoder for VorbisDecoder<R>
where where
R: Read + Seek, R: Read + Seek,
{ {
fn seek(&mut self, ms: i64) -> Result<(), AudioError> { fn seek(&mut self, absgp: u64) -> DecoderResult<()> {
let absgp = Duration::from_millis(ms as u64 * crate::SAMPLE_RATE as u64).as_secs(); self.0
match self.0.seek_absgp_pg(absgp as u64) { .seek_absgp_pg(absgp)
Ok(_) => Ok(()), .map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?;
Err(err) => Err(AudioError::VorbisError(err.into())), Ok(())
}
} }
fn next_packet(&mut self) -> Result<Option<AudioPacket>, AudioError> { fn next_packet(&mut self) -> DecoderResult<Option<AudioPacket>> {
use lewton::audio::AudioReadError::AudioIsHeader;
use lewton::OggReadError::NoCapturePatternFound;
use lewton::VorbisError::{BadAudio, OggError};
loop { loop {
match self.0.read_dec_packet_generic::<InterleavedSamples<f32>>() { match self.0.read_dec_packet_generic::<InterleavedSamples<f32>>() {
Ok(Some(packet)) => return Ok(Some(AudioPacket::samples_from_f32(packet.samples))), Ok(Some(packet)) => return Ok(Some(AudioPacket::samples_from_f32(packet.samples))),
Ok(None) => return Ok(None), Ok(None) => return Ok(None),
Err(BadAudio(AudioIsHeader)) => (), Err(BadAudio(AudioIsHeader)) => (),
Err(OggError(NoCapturePatternFound)) => (), Err(OggError(NoCapturePatternFound)) => (),
Err(err) => return Err(AudioError::VorbisError(err.into())), Err(e) => return Err(DecoderError::LewtonDecoder(e.to_string())),
} }
} }
} }
} }
impl From<lewton::VorbisError> for VorbisError {
fn from(err: lewton::VorbisError) -> VorbisError {
VorbisError(err)
}
}
impl fmt::Debug for VorbisError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl fmt::Display for VorbisError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl error::Error for VorbisError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
error::Error::source(&self.0)
}
}

Some files were not shown because too many files have changed in this diff Show more