mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge pull request #665 from librespot-org/tokio_migration
Tokio migration
This commit is contained in:
commit
7b537550ea
67 changed files with 3425 additions and 4468 deletions
184
.github/workflows/test.yml
vendored
184
.github/workflows/test.yml
vendored
|
@ -4,61 +4,81 @@ name: test
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master, dev]
|
branches: [master, dev]
|
||||||
paths: ['**.rs', '**.toml', '**.lock', '**.yml']
|
paths:
|
||||||
|
[
|
||||||
|
"**.rs",
|
||||||
|
"Cargo.toml",
|
||||||
|
"/Cargo.lock",
|
||||||
|
"/rustfmt.toml",
|
||||||
|
"/.github/workflows",
|
||||||
|
]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, dev]
|
paths:
|
||||||
paths: ['**.rs', '**.toml', '**.lock', '**.yml']
|
[
|
||||||
|
"**.rs",
|
||||||
|
"Cargo.toml",
|
||||||
|
"/Cargo.lock",
|
||||||
|
"/rustfmt.toml",
|
||||||
|
"/.github/workflows",
|
||||||
|
]
|
||||||
|
schedule:
|
||||||
|
# Run CI every week
|
||||||
|
- cron: "00 01 * * 0"
|
||||||
|
|
||||||
|
env:
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fmt:
|
fmt:
|
||||||
name: 'Rust: format check'
|
name: rustfmt
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
# Only run the formatting check for stable
|
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
toolchain: stable
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Install toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
# Use default profile to get rustfmt
|
|
||||||
profile: default
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
|
||||||
override: true
|
|
||||||
- run: cargo fmt --verbose --all -- --check
|
|
||||||
|
|
||||||
test:
|
|
||||||
needs: fmt
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
continue-on-error: ${{ matrix.experimental }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
toolchain:
|
|
||||||
- 1.41.1 # MSRV (Minimum supported rust version)
|
|
||||||
- stable
|
|
||||||
- beta
|
|
||||||
experimental: [false]
|
|
||||||
# Ignore failures in nightly, not ideal, but necessary
|
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
toolchain: nightly
|
|
||||||
experimental: true
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
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:
|
||||||
|
needs: fmt
|
||||||
|
name: cargo +${{ matrix.toolchain }} build (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
continue-on-error: ${{ matrix.experimental }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
toolchain:
|
||||||
|
- 1.45 # MSRV (Minimum supported rust version)
|
||||||
|
- stable
|
||||||
|
- beta
|
||||||
|
experimental: [false]
|
||||||
|
# Ignore failures in nightly
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
toolchain: nightly
|
||||||
|
experimental: true
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install toolchain
|
- name: Install toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
toolchain: ${{ matrix.toolchain }}
|
toolchain: ${{ matrix.toolchain }}
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Get Rustc version
|
||||||
|
id: get-rustc-version
|
||||||
|
run: echo "::set-output name=version::$(rustc -V)"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Cache Rust dependencies
|
- name: Cache Rust dependencies
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
|
@ -67,20 +87,65 @@ jobs:
|
||||||
~/.cargo/registry/cache
|
~/.cargo/registry/cache
|
||||||
~/.cargo/git
|
~/.cargo/git
|
||||||
target
|
target
|
||||||
key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Install developer package dependencies
|
- name: Install developer package dependencies
|
||||||
run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev libavahi-compat-libdnssd-dev
|
||||||
- run: cargo build --locked --no-default-features
|
|
||||||
- run: cargo build --locked --examples
|
- run: cargo build --workspace --examples
|
||||||
- run: cargo build --locked --no-default-features --features "with-tremor"
|
- run: cargo test --workspace
|
||||||
- run: cargo build --locked --no-default-features --features "with-vorbis"
|
|
||||||
- run: cargo build --locked --no-default-features --features "alsa-backend"
|
- run: cargo install cargo-hack
|
||||||
- run: cargo build --locked --no-default-features --features "portaudio-backend"
|
- run: cargo hack --workspace --remove-dev-deps
|
||||||
- run: cargo build --locked --no-default-features --features "pulseaudio-backend"
|
- run: cargo build -p librespot-core --no-default-features
|
||||||
- run: cargo build --locked --no-default-features --features "jackaudio-backend"
|
- run: cargo build -p librespot-core
|
||||||
- run: cargo build --locked --no-default-features --features "rodio-backend"
|
- run: cargo hack build --each-feature -p librespot-audio
|
||||||
- run: cargo build --locked --no-default-features --features "sdl-backend"
|
- run: cargo build -p librespot-connect
|
||||||
- run: cargo build --locked --no-default-features --features "gstreamer-backend"
|
- run: cargo build -p librespot-connect --no-default-features --features with-dns-sd
|
||||||
|
- run: cargo hack build --locked --each-feature
|
||||||
|
|
||||||
|
test-windows:
|
||||||
|
needs: fmt
|
||||||
|
name: cargo build (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest]
|
||||||
|
toolchain: [stable]
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Get Rustc version
|
||||||
|
id: get-rustc-version
|
||||||
|
run: echo "::set-output name=version::$(rustc -V)"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry/index
|
||||||
|
~/.cargo/registry/cache
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- run: cargo build --workspace --examples
|
||||||
|
- run: cargo test --workspace
|
||||||
|
|
||||||
|
- run: cargo install cargo-hack
|
||||||
|
- run: cargo hack --workspace --remove-dev-deps
|
||||||
|
- run: cargo build --no-default-features
|
||||||
|
- run: cargo build
|
||||||
|
|
||||||
test-cross-arm:
|
test-cross-arm:
|
||||||
needs: fmt
|
needs: fmt
|
||||||
|
@ -96,6 +161,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install toolchain
|
- name: Install toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
|
@ -103,6 +169,12 @@ jobs:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
toolchain: ${{ matrix.toolchain }}
|
toolchain: ${{ matrix.toolchain }}
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Get Rustc version
|
||||||
|
id: get-rustc-version
|
||||||
|
run: echo "::set-output name=version::$(rustc -V)"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Cache Rust dependencies
|
- name: Cache Rust dependencies
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
|
@ -111,7 +183,7 @@ jobs:
|
||||||
~/.cargo/registry/cache
|
~/.cargo/registry/cache
|
||||||
~/.cargo/git
|
~/.cargo/git
|
||||||
target
|
target
|
||||||
key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-${{ matrix.target }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
- name: Install cross
|
- name: Install cross
|
||||||
run: cargo install cross || true
|
run: cargo install cross || true
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|
|
@ -13,7 +13,7 @@ curl https://sh.rustup.rs -sSf | sh
|
||||||
|
|
||||||
Follow any prompts it gives you to install Rust. Once that’s done, Rust's standard tools should be setup and ready to use.
|
Follow any prompts it gives you to install Rust. Once that’s done, Rust's standard tools should be setup and ready to use.
|
||||||
|
|
||||||
*Note: The current minimum required Rust version at the time of writing is 1.41, you can find the current minimum version specified in the `.github/workflow/test.yml` file.*
|
*Note: The current minimum required Rust version at the time of writing is 1.45, you can find the current minimum version specified in the `.github/workflow/test.yml` file.*
|
||||||
|
|
||||||
#### Additional Rust tools - `rustfmt`
|
#### 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), which is installed by default with `rustup` these days, else it can be installed manually with:
|
||||||
|
|
1411
Cargo.lock
generated
1411
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
30
Cargo.toml
30
Cargo.toml
|
@ -23,18 +23,24 @@ doc = false
|
||||||
[dependencies.librespot-audio]
|
[dependencies.librespot-audio]
|
||||||
path = "audio"
|
path = "audio"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
||||||
[dependencies.librespot-connect]
|
[dependencies.librespot-connect]
|
||||||
path = "connect"
|
path = "connect"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
||||||
[dependencies.librespot-core]
|
[dependencies.librespot-core]
|
||||||
path = "core"
|
path = "core"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
features = ["apresolve"]
|
||||||
|
|
||||||
[dependencies.librespot-metadata]
|
[dependencies.librespot-metadata]
|
||||||
path = "metadata"
|
path = "metadata"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
||||||
[dependencies.librespot-playback]
|
[dependencies.librespot-playback]
|
||||||
path = "playback"
|
path = "playback"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
||||||
[dependencies.librespot-protocol]
|
[dependencies.librespot-protocol]
|
||||||
path = "protocol"
|
path = "protocol"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
@ -42,29 +48,23 @@ version = "0.1.6"
|
||||||
[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 = "0.1"
|
futures-util = { version = "0.3", default_features = false }
|
||||||
getopts = "0.2"
|
getopts = "0.2"
|
||||||
hyper = "0.11"
|
|
||||||
log = "0.4"
|
|
||||||
num-bigint = "0.3"
|
|
||||||
protobuf = "~2.14.0"
|
|
||||||
rand = "0.7"
|
|
||||||
rpassword = "5.0"
|
|
||||||
tokio-core = "0.1"
|
|
||||||
tokio-io = "0.1"
|
|
||||||
tokio-process = "0.2"
|
|
||||||
tokio-signal = "0.2"
|
|
||||||
url = "1.7"
|
|
||||||
sha-1 = "0.8"
|
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
hyper = "0.14"
|
||||||
|
log = "0.4"
|
||||||
|
rpassword = "5.0"
|
||||||
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "signal", "sync", "process"] }
|
||||||
|
url = "2.1"
|
||||||
|
sha-1 = "0.9"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
alsa-backend = ["librespot-playback/alsa-backend"]
|
alsa-backend = ["librespot-playback/alsa-backend"]
|
||||||
portaudio-backend = ["librespot-playback/portaudio-backend"]
|
portaudio-backend = ["librespot-playback/portaudio-backend"]
|
||||||
pulseaudio-backend = ["librespot-playback/pulseaudio-backend"]
|
pulseaudio-backend = ["librespot-playback/pulseaudio-backend"]
|
||||||
jackaudio-backend = ["librespot-playback/jackaudio-backend"]
|
jackaudio-backend = ["librespot-playback/jackaudio-backend"]
|
||||||
rodiojack-backend = ["librespot-playback/rodiojack-backend"]
|
|
||||||
rodio-backend = ["librespot-playback/rodio-backend"]
|
rodio-backend = ["librespot-playback/rodio-backend"]
|
||||||
|
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"]
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ with-vorbis = ["librespot-audio/with-vorbis"]
|
||||||
|
|
||||||
with-dns-sd = ["librespot-connect/with-dns-sd"]
|
with-dns-sd = ["librespot-connect/with-dns-sd"]
|
||||||
|
|
||||||
default = ["librespot-playback/rodio-backend"]
|
default = ["rodio-backend"]
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
maintainer = "librespot-org"
|
maintainer = "librespot-org"
|
||||||
|
|
|
@ -12,16 +12,15 @@ version = "0.1.6"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes-ctr = "0.6"
|
aes-ctr = "0.6"
|
||||||
bit-set = "0.5"
|
byteorder = "1.4"
|
||||||
byteorder = "1.3"
|
bytes = "1.0"
|
||||||
bytes = "0.4"
|
cfg-if = "1"
|
||||||
futures = "0.1"
|
|
||||||
lewton = "0.10"
|
lewton = "0.10"
|
||||||
ogg = "0.8"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
num-bigint = "0.3"
|
futures-util = { version = "0.3", default_features = false }
|
||||||
num-traits = "0.2"
|
ogg = "0.8"
|
||||||
tempfile = "3.1"
|
tempfile = "3.1"
|
||||||
|
tokio = { version = "1", features = ["sync", "macros"] }
|
||||||
zerocopy = "0.3"
|
zerocopy = "0.3"
|
||||||
|
|
||||||
librespot-tremor = { version = "0.2", optional = true }
|
librespot-tremor = { version = "0.2", optional = true }
|
||||||
|
|
|
@ -36,24 +36,21 @@ macro_rules! convert_samples_to {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SamplesConverter {}
|
pub fn to_s32(samples: &[f32]) -> Vec<i32> {
|
||||||
impl SamplesConverter {
|
convert_samples_to!(i32, samples)
|
||||||
pub fn to_s32(samples: &[f32]) -> Vec<i32> {
|
}
|
||||||
convert_samples_to!(i32, samples)
|
|
||||||
}
|
pub fn to_s24(samples: &[f32]) -> Vec<i32> {
|
||||||
|
convert_samples_to!(i32, samples, 8)
|
||||||
pub fn to_s24(samples: &[f32]) -> Vec<i32> {
|
}
|
||||||
convert_samples_to!(i32, samples, 8)
|
|
||||||
}
|
pub fn to_s24_3(samples: &[f32]) -> Vec<i24> {
|
||||||
|
to_s32(samples)
|
||||||
pub fn to_s24_3(samples: &[f32]) -> Vec<i24> {
|
.iter()
|
||||||
Self::to_s32(samples)
|
.map(|sample| i24::pcm_from_i32(*sample))
|
||||||
.iter()
|
.collect()
|
||||||
.map(|sample| i24::pcm_from_i32(*sample))
|
}
|
||||||
.collect()
|
|
||||||
}
|
pub fn to_s16(samples: &[f32]) -> Vec<i16> {
|
||||||
|
convert_samples_to!(i16, samples)
|
||||||
pub fn to_s16(samples: &[f32]) -> Vec<i16> {
|
|
||||||
convert_samples_to!(i16, samples)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
1099
audio/src/fetch.rs
1099
audio/src/fetch.rs
File diff suppressed because it is too large
Load diff
509
audio/src/fetch/mod.rs
Normal file
509
audio/src/fetch/mod.rs
Normal file
|
@ -0,0 +1,509 @@
|
||||||
|
mod receive;
|
||||||
|
|
||||||
|
use std::cmp::{max, min};
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{self, Read, Seek, SeekFrom};
|
||||||
|
use std::sync::atomic::{self, AtomicUsize};
|
||||||
|
use std::sync::{Arc, Condvar, Mutex};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
|
use futures_util::{future, StreamExt, TryFutureExt, TryStreamExt};
|
||||||
|
use librespot_core::channel::{ChannelData, ChannelError, ChannelHeaders};
|
||||||
|
use librespot_core::session::Session;
|
||||||
|
use librespot_core::spotify_id::FileId;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
|
use self::receive::{audio_file_fetch, request_range};
|
||||||
|
use crate::range_set::{Range, RangeSet};
|
||||||
|
|
||||||
|
const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 16;
|
||||||
|
// The minimum size of a block that is requested from the Spotify servers in one request.
|
||||||
|
// This is the block size that is typically requested while doing a seek() on a file.
|
||||||
|
// Note: smaller requests can happen if part of the block is downloaded already.
|
||||||
|
|
||||||
|
const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 16;
|
||||||
|
// The amount of data that is requested when initially opening a file.
|
||||||
|
// Note: if the file is opened to play from the beginning, the amount of data to
|
||||||
|
// read ahead is requested in addition to this amount. If the file is opened to seek to
|
||||||
|
// another position, then only this amount is requested on the first request.
|
||||||
|
|
||||||
|
const INITIAL_PING_TIME_ESTIMATE_SECONDS: f64 = 0.5;
|
||||||
|
// The pig time that is used for calculations before a ping time was actually measured.
|
||||||
|
|
||||||
|
const MAXIMUM_ASSUMED_PING_TIME_SECONDS: f64 = 1.5;
|
||||||
|
// If the measured ping time to the Spotify server is larger than this value, it is capped
|
||||||
|
// to avoid run-away block sizes and pre-fetching.
|
||||||
|
|
||||||
|
pub const READ_AHEAD_BEFORE_PLAYBACK_SECONDS: f64 = 1.0;
|
||||||
|
// Before playback starts, this many seconds of data must be present.
|
||||||
|
// Note: the calculations are done using the nominal bitrate of the file. The actual amount
|
||||||
|
// of audio data may be larger or smaller.
|
||||||
|
|
||||||
|
pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f64 = 2.0;
|
||||||
|
// Same as READ_AHEAD_BEFORE_PLAYBACK_SECONDS, but the time is taken as a factor of the ping
|
||||||
|
// time to the Spotify server.
|
||||||
|
// Both, READ_AHEAD_BEFORE_PLAYBACK_SECONDS and READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS are
|
||||||
|
// obeyed.
|
||||||
|
// Note: the calculations are done using the nominal bitrate of the file. The actual amount
|
||||||
|
// of audio data may be larger or smaller.
|
||||||
|
|
||||||
|
pub const READ_AHEAD_DURING_PLAYBACK_SECONDS: f64 = 5.0;
|
||||||
|
// While playing back, this many seconds of data ahead of the current read position are
|
||||||
|
// requested.
|
||||||
|
// Note: the calculations are done using the nominal bitrate of the file. The actual amount
|
||||||
|
// of audio data may be larger or smaller.
|
||||||
|
|
||||||
|
pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f64 = 10.0;
|
||||||
|
// Same as READ_AHEAD_DURING_PLAYBACK_SECONDS, but the time is taken as a factor of the ping
|
||||||
|
// time to the Spotify server.
|
||||||
|
// Note: the calculations are done using the nominal bitrate of the file. The actual amount
|
||||||
|
// of audio data may be larger or smaller.
|
||||||
|
|
||||||
|
const PREFETCH_THRESHOLD_FACTOR: f64 = 4.0;
|
||||||
|
// If the amount of data that is pending (requested but not received) is less than a certain amount,
|
||||||
|
// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more
|
||||||
|
// data is calculated as
|
||||||
|
// <pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>
|
||||||
|
|
||||||
|
const FAST_PREFETCH_THRESHOLD_FACTOR: f64 = 1.5;
|
||||||
|
// Similar to PREFETCH_THRESHOLD_FACTOR, but it also takes the current download rate into account.
|
||||||
|
// The formula used is
|
||||||
|
// <pending bytes> < FAST_PREFETCH_THRESHOLD_FACTOR * <ping time> * <measured download rate>
|
||||||
|
// This mechanism allows for fast downloading of the remainder of the file. The number should be larger
|
||||||
|
// than 1 so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster
|
||||||
|
// the download rate ramps up. However, this comes at the cost that it might hurt ping-time if a seek is
|
||||||
|
// performed while downloading. Values smaller than 1 cause the download rate to collapse and effectively
|
||||||
|
// only PREFETCH_THRESHOLD_FACTOR is in effect. Thus, set to zero if bandwidth saturation is not wanted.
|
||||||
|
|
||||||
|
const MAX_PREFETCH_REQUESTS: usize = 4;
|
||||||
|
// Limit the number of requests that are pending simultaneously before pre-fetching data. Pending
|
||||||
|
// requests share bandwidth. Thus, havint too many requests can lead to the one that is needed next
|
||||||
|
// for playback to be delayed leading to a buffer underrun. This limit has the effect that a new
|
||||||
|
// pre-fetch request is only sent if less than MAX_PREFETCH_REQUESTS are pending.
|
||||||
|
|
||||||
|
pub enum AudioFile {
|
||||||
|
Cached(fs::File),
|
||||||
|
Streaming(AudioFileStreaming),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum StreamLoaderCommand {
|
||||||
|
Fetch(Range), // signal the stream loader to fetch a range of the file
|
||||||
|
RandomAccessMode(), // optimise download strategy for random access
|
||||||
|
StreamMode(), // optimise download strategy for streaming
|
||||||
|
Close(), // terminate and don't load any more data
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct StreamLoaderController {
|
||||||
|
channel_tx: Option<mpsc::UnboundedSender<StreamLoaderCommand>>,
|
||||||
|
stream_shared: Option<Arc<AudioFileShared>>,
|
||||||
|
file_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamLoaderController {
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.file_size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.file_size == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range_available(&self, range: Range) -> bool {
|
||||||
|
if let Some(ref shared) = self.stream_shared {
|
||||||
|
let download_status = shared.download_status.lock().unwrap();
|
||||||
|
range.length
|
||||||
|
<= download_status
|
||||||
|
.downloaded
|
||||||
|
.contained_length_from_value(range.start)
|
||||||
|
} else {
|
||||||
|
range.length <= self.len() - range.start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range_to_end_available(&self) -> bool {
|
||||||
|
self.stream_shared.as_ref().map_or(true, |shared| {
|
||||||
|
let read_position = shared.read_position.load(atomic::Ordering::Relaxed);
|
||||||
|
self.range_available(Range::new(read_position, self.len() - read_position))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ping_time_ms(&self) -> usize {
|
||||||
|
self.stream_shared.as_ref().map_or(0, |shared| {
|
||||||
|
shared.ping_time_ms.load(atomic::Ordering::Relaxed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_stream_loader_command(&self, command: StreamLoaderCommand) {
|
||||||
|
if let Some(ref channel) = self.channel_tx {
|
||||||
|
// ignore the error in case the channel has been closed already.
|
||||||
|
let _ = channel.send(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch(&self, range: Range) {
|
||||||
|
// signal the stream loader to fetch a range of the file
|
||||||
|
self.send_stream_loader_command(StreamLoaderCommand::Fetch(range));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_blocking(&self, mut range: Range) {
|
||||||
|
// signal the stream loader to tech a range of the file and block until it is loaded.
|
||||||
|
|
||||||
|
// ensure the range is within the file's bounds.
|
||||||
|
if range.start >= self.len() {
|
||||||
|
range.length = 0;
|
||||||
|
} else if range.end() > self.len() {
|
||||||
|
range.length = self.len() - range.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fetch(range);
|
||||||
|
|
||||||
|
if let Some(ref shared) = self.stream_shared {
|
||||||
|
let mut download_status = shared.download_status.lock().unwrap();
|
||||||
|
while range.length
|
||||||
|
> download_status
|
||||||
|
.downloaded
|
||||||
|
.contained_length_from_value(range.start)
|
||||||
|
{
|
||||||
|
download_status = shared
|
||||||
|
.cond
|
||||||
|
.wait_timeout(download_status, Duration::from_millis(1000))
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
if range.length
|
||||||
|
> (download_status
|
||||||
|
.downloaded
|
||||||
|
.union(&download_status.requested)
|
||||||
|
.contained_length_from_value(range.start))
|
||||||
|
{
|
||||||
|
// For some reason, the requested range is neither downloaded nor requested.
|
||||||
|
// This could be due to a network error. Request it again.
|
||||||
|
self.fetch(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_next(&self, length: usize) {
|
||||||
|
if let Some(ref shared) = self.stream_shared {
|
||||||
|
let range = Range {
|
||||||
|
start: shared.read_position.load(atomic::Ordering::Relaxed),
|
||||||
|
length,
|
||||||
|
};
|
||||||
|
self.fetch(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_next_blocking(&self, length: usize) {
|
||||||
|
if let Some(ref shared) = self.stream_shared {
|
||||||
|
let range = Range {
|
||||||
|
start: shared.read_position.load(atomic::Ordering::Relaxed),
|
||||||
|
length,
|
||||||
|
};
|
||||||
|
self.fetch_blocking(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_random_access_mode(&self) {
|
||||||
|
// optimise download strategy for random access
|
||||||
|
self.send_stream_loader_command(StreamLoaderCommand::RandomAccessMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_stream_mode(&self) {
|
||||||
|
// optimise download strategy for streaming
|
||||||
|
self.send_stream_loader_command(StreamLoaderCommand::StreamMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(&self) {
|
||||||
|
// terminate stream loading and don't load any more data for this file.
|
||||||
|
self.send_stream_loader_command(StreamLoaderCommand::Close());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AudioFileStreaming {
|
||||||
|
read_file: fs::File,
|
||||||
|
position: u64,
|
||||||
|
stream_loader_command_tx: mpsc::UnboundedSender<StreamLoaderCommand>,
|
||||||
|
shared: Arc<AudioFileShared>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AudioFileDownloadStatus {
|
||||||
|
requested: RangeSet,
|
||||||
|
downloaded: RangeSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
enum DownloadStrategy {
|
||||||
|
RandomAccess(),
|
||||||
|
Streaming(),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AudioFileShared {
|
||||||
|
file_id: FileId,
|
||||||
|
file_size: usize,
|
||||||
|
stream_data_rate: usize,
|
||||||
|
cond: Condvar,
|
||||||
|
download_status: Mutex<AudioFileDownloadStatus>,
|
||||||
|
download_strategy: Mutex<DownloadStrategy>,
|
||||||
|
ping_time_ms: AtomicUsize,
|
||||||
|
read_position: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFile {
|
||||||
|
pub async fn open(
|
||||||
|
session: &Session,
|
||||||
|
file_id: FileId,
|
||||||
|
bytes_per_second: usize,
|
||||||
|
play_from_beginning: bool,
|
||||||
|
) -> Result<AudioFile, ChannelError> {
|
||||||
|
if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) {
|
||||||
|
debug!("File {} already in cache", file_id);
|
||||||
|
return Ok(AudioFile::Cached(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Downloading file {}", file_id);
|
||||||
|
|
||||||
|
let (complete_tx, complete_rx) = oneshot::channel();
|
||||||
|
let mut initial_data_length = if play_from_beginning {
|
||||||
|
INITIAL_DOWNLOAD_SIZE
|
||||||
|
+ max(
|
||||||
|
(READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize,
|
||||||
|
(INITIAL_PING_TIME_ESTIMATE_SECONDS
|
||||||
|
* READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
|
||||||
|
* bytes_per_second as f64) as usize,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
INITIAL_DOWNLOAD_SIZE
|
||||||
|
};
|
||||||
|
if initial_data_length % 4 != 0 {
|
||||||
|
initial_data_length += 4 - (initial_data_length % 4);
|
||||||
|
}
|
||||||
|
let (headers, data) = request_range(session, file_id, 0, initial_data_length).split();
|
||||||
|
|
||||||
|
let streaming = AudioFileStreaming::open(
|
||||||
|
session.clone(),
|
||||||
|
data,
|
||||||
|
initial_data_length,
|
||||||
|
Instant::now(),
|
||||||
|
headers,
|
||||||
|
file_id,
|
||||||
|
complete_tx,
|
||||||
|
bytes_per_second,
|
||||||
|
);
|
||||||
|
|
||||||
|
let session_ = session.clone();
|
||||||
|
session.spawn(complete_rx.map_ok(move |mut file| {
|
||||||
|
if let Some(cache) = session_.cache() {
|
||||||
|
debug!("File {} complete, saving to cache", file_id);
|
||||||
|
cache.save_file(file_id, &mut file);
|
||||||
|
} else {
|
||||||
|
debug!("File {} complete", file_id);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok(AudioFile::Streaming(streaming.await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_stream_loader_controller(&self) -> StreamLoaderController {
|
||||||
|
match self {
|
||||||
|
AudioFile::Streaming(ref stream) => StreamLoaderController {
|
||||||
|
channel_tx: Some(stream.stream_loader_command_tx.clone()),
|
||||||
|
stream_shared: Some(stream.shared.clone()),
|
||||||
|
file_size: stream.shared.file_size,
|
||||||
|
},
|
||||||
|
AudioFile::Cached(ref file) => StreamLoaderController {
|
||||||
|
channel_tx: None,
|
||||||
|
stream_shared: None,
|
||||||
|
file_size: file.metadata().unwrap().len() as usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_cached(&self) -> bool {
|
||||||
|
matches!(self, AudioFile::Cached { .. })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFileStreaming {
|
||||||
|
pub async fn open(
|
||||||
|
session: Session,
|
||||||
|
initial_data_rx: ChannelData,
|
||||||
|
initial_data_length: usize,
|
||||||
|
initial_request_sent_time: Instant,
|
||||||
|
headers: ChannelHeaders,
|
||||||
|
file_id: FileId,
|
||||||
|
complete_tx: oneshot::Sender<NamedTempFile>,
|
||||||
|
streaming_data_rate: usize,
|
||||||
|
) -> Result<AudioFileStreaming, ChannelError> {
|
||||||
|
let (_, data) = headers
|
||||||
|
.try_filter(|(id, _)| future::ready(*id == 0x3))
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.unwrap()?;
|
||||||
|
|
||||||
|
let size = BigEndian::read_u32(&data) as usize * 4;
|
||||||
|
|
||||||
|
let shared = Arc::new(AudioFileShared {
|
||||||
|
file_id,
|
||||||
|
file_size: size,
|
||||||
|
stream_data_rate: streaming_data_rate,
|
||||||
|
cond: Condvar::new(),
|
||||||
|
download_status: Mutex::new(AudioFileDownloadStatus {
|
||||||
|
requested: RangeSet::new(),
|
||||||
|
downloaded: RangeSet::new(),
|
||||||
|
}),
|
||||||
|
download_strategy: Mutex::new(DownloadStrategy::RandomAccess()), // start with random access mode until someone tells us otherwise
|
||||||
|
ping_time_ms: AtomicUsize::new(0),
|
||||||
|
read_position: AtomicUsize::new(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut write_file = NamedTempFile::new().unwrap();
|
||||||
|
write_file.as_file().set_len(size as u64).unwrap();
|
||||||
|
write_file.seek(SeekFrom::Start(0)).unwrap();
|
||||||
|
|
||||||
|
let read_file = write_file.reopen().unwrap();
|
||||||
|
|
||||||
|
//let (seek_tx, seek_rx) = mpsc::unbounded();
|
||||||
|
let (stream_loader_command_tx, stream_loader_command_rx) =
|
||||||
|
mpsc::unbounded_channel::<StreamLoaderCommand>();
|
||||||
|
|
||||||
|
session.spawn(audio_file_fetch(
|
||||||
|
session.clone(),
|
||||||
|
shared.clone(),
|
||||||
|
initial_data_rx,
|
||||||
|
initial_request_sent_time,
|
||||||
|
initial_data_length,
|
||||||
|
write_file,
|
||||||
|
stream_loader_command_rx,
|
||||||
|
complete_tx,
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(AudioFileStreaming {
|
||||||
|
read_file,
|
||||||
|
position: 0,
|
||||||
|
stream_loader_command_tx,
|
||||||
|
shared,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for AudioFileStreaming {
|
||||||
|
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
|
||||||
|
let offset = self.position as usize;
|
||||||
|
|
||||||
|
if offset >= self.shared.file_size {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = min(output.len(), self.shared.file_size - offset);
|
||||||
|
|
||||||
|
let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) {
|
||||||
|
DownloadStrategy::RandomAccess() => length,
|
||||||
|
DownloadStrategy::Streaming() => {
|
||||||
|
// Due to the read-ahead stuff, we potentially request more than the actual reqeust demanded.
|
||||||
|
let ping_time_seconds =
|
||||||
|
0.0001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64;
|
||||||
|
|
||||||
|
let length_to_request = length
|
||||||
|
+ max(
|
||||||
|
(READ_AHEAD_DURING_PLAYBACK_SECONDS * self.shared.stream_data_rate as f64)
|
||||||
|
as usize,
|
||||||
|
(READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
|
||||||
|
* ping_time_seconds
|
||||||
|
* self.shared.stream_data_rate as f64) as usize,
|
||||||
|
);
|
||||||
|
min(length_to_request, self.shared.file_size - offset)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ranges_to_request = RangeSet::new();
|
||||||
|
ranges_to_request.add_range(&Range::new(offset, length_to_request));
|
||||||
|
|
||||||
|
let mut download_status = self.shared.download_status.lock().unwrap();
|
||||||
|
ranges_to_request.subtract_range_set(&download_status.downloaded);
|
||||||
|
ranges_to_request.subtract_range_set(&download_status.requested);
|
||||||
|
|
||||||
|
for &range in ranges_to_request.iter() {
|
||||||
|
self.stream_loader_command_tx
|
||||||
|
.send(StreamLoaderCommand::Fetch(range))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut download_message_printed = false;
|
||||||
|
while !download_status.downloaded.contains(offset) {
|
||||||
|
if let DownloadStrategy::Streaming() = *self.shared.download_strategy.lock().unwrap() {
|
||||||
|
if !download_message_printed {
|
||||||
|
debug!("Stream waiting for download of file position {}. Downloaded ranges: {}. Pending ranges: {}", offset, download_status.downloaded, download_status.requested.minus(&download_status.downloaded));
|
||||||
|
download_message_printed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
download_status = self
|
||||||
|
.shared
|
||||||
|
.cond
|
||||||
|
.wait_timeout(download_status, Duration::from_millis(1000))
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
}
|
||||||
|
let available_length = download_status
|
||||||
|
.downloaded
|
||||||
|
.contained_length_from_value(offset);
|
||||||
|
assert!(available_length > 0);
|
||||||
|
drop(download_status);
|
||||||
|
|
||||||
|
self.position = self.read_file.seek(SeekFrom::Start(offset as u64)).unwrap();
|
||||||
|
let read_len = min(length, available_length);
|
||||||
|
let read_len = self.read_file.read(&mut output[..read_len])?;
|
||||||
|
|
||||||
|
if download_message_printed {
|
||||||
|
debug!(
|
||||||
|
"Read at postion {} completed. {} bytes returned, {} bytes were requested.",
|
||||||
|
offset,
|
||||||
|
read_len,
|
||||||
|
output.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.position += read_len as u64;
|
||||||
|
self.shared
|
||||||
|
.read_position
|
||||||
|
.store(self.position as usize, atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
|
Ok(read_len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Seek for AudioFileStreaming {
|
||||||
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||||
|
self.position = self.read_file.seek(pos)?;
|
||||||
|
// Do not seek past EOF
|
||||||
|
self.shared
|
||||||
|
.read_position
|
||||||
|
.store(self.position as usize, atomic::Ordering::Relaxed);
|
||||||
|
Ok(self.position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for AudioFile {
|
||||||
|
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
|
||||||
|
match *self {
|
||||||
|
AudioFile::Cached(ref mut file) => file.read(output),
|
||||||
|
AudioFile::Streaming(ref mut file) => file.read(output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Seek for AudioFile {
|
||||||
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||||
|
match *self {
|
||||||
|
AudioFile::Cached(ref mut file) => file.seek(pos),
|
||||||
|
AudioFile::Streaming(ref mut file) => file.seek(pos),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
455
audio/src/fetch/receive.rs
Normal file
455
audio/src/fetch/receive.rs
Normal file
|
@ -0,0 +1,455 @@
|
||||||
|
use std::cmp::{max, min};
|
||||||
|
use std::io::{Seek, SeekFrom, Write};
|
||||||
|
use std::sync::{atomic, Arc};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, WriteBytesExt};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use librespot_core::channel::{Channel, ChannelData};
|
||||||
|
use librespot_core::session::Session;
|
||||||
|
use librespot_core::spotify_id::FileId;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
|
use crate::range_set::{Range, RangeSet};
|
||||||
|
|
||||||
|
use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand};
|
||||||
|
use super::{
|
||||||
|
FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME_SECONDS, MAX_PREFETCH_REQUESTS,
|
||||||
|
MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn request_range(session: &Session, file: FileId, offset: usize, length: usize) -> Channel {
|
||||||
|
assert!(
|
||||||
|
offset % 4 == 0,
|
||||||
|
"Range request start positions must be aligned by 4 bytes."
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
length % 4 == 0,
|
||||||
|
"Range request range lengths must be aligned by 4 bytes."
|
||||||
|
);
|
||||||
|
let start = offset / 4;
|
||||||
|
let end = (offset + length) / 4;
|
||||||
|
|
||||||
|
let (id, channel) = session.channel().allocate();
|
||||||
|
|
||||||
|
let mut data: Vec<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(0x8, data);
|
||||||
|
|
||||||
|
channel
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PartialFileData {
|
||||||
|
offset: usize,
|
||||||
|
data: Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReceivedData {
|
||||||
|
ResponseTimeMs(usize),
|
||||||
|
Data(PartialFileData),
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_data(
|
||||||
|
shared: Arc<AudioFileShared>,
|
||||||
|
file_data_tx: mpsc::UnboundedSender<ReceivedData>,
|
||||||
|
mut data_rx: ChannelData,
|
||||||
|
initial_data_offset: usize,
|
||||||
|
initial_request_length: usize,
|
||||||
|
request_sent_time: Instant,
|
||||||
|
mut measure_ping_time: bool,
|
||||||
|
finish_tx: mpsc::UnboundedSender<()>,
|
||||||
|
) {
|
||||||
|
let mut data_offset = initial_data_offset;
|
||||||
|
let mut request_length = initial_request_length;
|
||||||
|
|
||||||
|
let result = loop {
|
||||||
|
let data = match data_rx.next().await {
|
||||||
|
Some(Ok(data)) => data,
|
||||||
|
Some(Err(e)) => break Err(e),
|
||||||
|
None => break Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if measure_ping_time {
|
||||||
|
let duration = Instant::now() - request_sent_time;
|
||||||
|
let duration_ms: u64;
|
||||||
|
if 0.001 * (duration.as_millis() as f64) > MAXIMUM_ASSUMED_PING_TIME_SECONDS {
|
||||||
|
duration_ms = (MAXIMUM_ASSUMED_PING_TIME_SECONDS * 1000.0) as u64;
|
||||||
|
} else {
|
||||||
|
duration_ms = duration.as_millis() as u64;
|
||||||
|
}
|
||||||
|
let _ = file_data_tx.send(ReceivedData::ResponseTimeMs(duration_ms as usize));
|
||||||
|
measure_ping_time = false;
|
||||||
|
}
|
||||||
|
let data_size = data.len();
|
||||||
|
let _ = file_data_tx.send(ReceivedData::Data(PartialFileData {
|
||||||
|
offset: data_offset,
|
||||||
|
data,
|
||||||
|
}));
|
||||||
|
data_offset += data_size;
|
||||||
|
if request_length < data_size {
|
||||||
|
warn!(
|
||||||
|
"Data receiver for range {} (+{}) received more data from server than requested.",
|
||||||
|
initial_data_offset, initial_request_length
|
||||||
|
);
|
||||||
|
request_length = 0;
|
||||||
|
} else {
|
||||||
|
request_length -= data_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if request_length == 0 {
|
||||||
|
break Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if request_length > 0 {
|
||||||
|
let missing_range = Range::new(data_offset, request_length);
|
||||||
|
|
||||||
|
let mut download_status = shared.download_status.lock().unwrap();
|
||||||
|
download_status.requested.subtract_range(&missing_range);
|
||||||
|
shared.cond.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = finish_tx.send(());
|
||||||
|
|
||||||
|
if result.is_err() {
|
||||||
|
warn!(
|
||||||
|
"Error from channel for data receiver for range {} (+{}).",
|
||||||
|
initial_data_offset, initial_request_length
|
||||||
|
);
|
||||||
|
} else if request_length > 0 {
|
||||||
|
warn!(
|
||||||
|
"Data receiver for range {} (+{}) received less data from server than requested.",
|
||||||
|
initial_data_offset, initial_request_length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AudioFileFetch {
|
||||||
|
session: Session,
|
||||||
|
shared: Arc<AudioFileShared>,
|
||||||
|
output: Option<NamedTempFile>,
|
||||||
|
|
||||||
|
file_data_tx: mpsc::UnboundedSender<ReceivedData>,
|
||||||
|
complete_tx: Option<oneshot::Sender<NamedTempFile>>,
|
||||||
|
network_response_times_ms: Vec<usize>,
|
||||||
|
number_of_open_requests: usize,
|
||||||
|
|
||||||
|
download_finish_tx: mpsc::UnboundedSender<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Might be replaced by enum from std once stable
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
enum ControlFlow {
|
||||||
|
Break,
|
||||||
|
Continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFileFetch {
|
||||||
|
fn get_download_strategy(&mut self) -> DownloadStrategy {
|
||||||
|
*(self.shared.download_strategy.lock().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_range(&mut self, mut offset: usize, mut length: usize) {
|
||||||
|
if length < MINIMUM_DOWNLOAD_SIZE {
|
||||||
|
length = MINIMUM_DOWNLOAD_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the values are within the bounds and align them by 4 for the spotify protocol.
|
||||||
|
if offset >= self.shared.file_size {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset + length > self.shared.file_size {
|
||||||
|
length = self.shared.file_size - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset % 4 != 0 {
|
||||||
|
length += offset % 4;
|
||||||
|
offset -= offset % 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if length % 4 != 0 {
|
||||||
|
length += 4 - (length % 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ranges_to_request = RangeSet::new();
|
||||||
|
ranges_to_request.add_range(&Range::new(offset, length));
|
||||||
|
|
||||||
|
let mut download_status = self.shared.download_status.lock().unwrap();
|
||||||
|
|
||||||
|
ranges_to_request.subtract_range_set(&download_status.downloaded);
|
||||||
|
ranges_to_request.subtract_range_set(&download_status.requested);
|
||||||
|
|
||||||
|
for range in ranges_to_request.iter() {
|
||||||
|
let (_headers, data) = request_range(
|
||||||
|
&self.session,
|
||||||
|
self.shared.file_id,
|
||||||
|
range.start,
|
||||||
|
range.length,
|
||||||
|
)
|
||||||
|
.split();
|
||||||
|
|
||||||
|
download_status.requested.add_range(range);
|
||||||
|
|
||||||
|
self.session.spawn(receive_data(
|
||||||
|
self.shared.clone(),
|
||||||
|
self.file_data_tx.clone(),
|
||||||
|
data,
|
||||||
|
range.start,
|
||||||
|
range.length,
|
||||||
|
Instant::now(),
|
||||||
|
self.number_of_open_requests == 0,
|
||||||
|
self.download_finish_tx.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
self.number_of_open_requests += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pre_fetch_more_data(&mut self, bytes: usize, max_requests_to_send: usize) {
|
||||||
|
let mut bytes_to_go = bytes;
|
||||||
|
let mut requests_to_go = max_requests_to_send;
|
||||||
|
|
||||||
|
while bytes_to_go > 0 && requests_to_go > 0 {
|
||||||
|
// determine what is still missing
|
||||||
|
let mut missing_data = RangeSet::new();
|
||||||
|
missing_data.add_range(&Range::new(0, self.shared.file_size));
|
||||||
|
{
|
||||||
|
let download_status = self.shared.download_status.lock().unwrap();
|
||||||
|
missing_data.subtract_range_set(&download_status.downloaded);
|
||||||
|
missing_data.subtract_range_set(&download_status.requested);
|
||||||
|
}
|
||||||
|
|
||||||
|
// download data from after the current read position first
|
||||||
|
let mut tail_end = RangeSet::new();
|
||||||
|
let read_position = self.shared.read_position.load(atomic::Ordering::Relaxed);
|
||||||
|
tail_end.add_range(&Range::new(
|
||||||
|
read_position,
|
||||||
|
self.shared.file_size - read_position,
|
||||||
|
));
|
||||||
|
let tail_end = tail_end.intersection(&missing_data);
|
||||||
|
|
||||||
|
if !tail_end.is_empty() {
|
||||||
|
let range = tail_end.get_range(0);
|
||||||
|
let offset = range.start;
|
||||||
|
let length = min(range.length, bytes_to_go);
|
||||||
|
self.download_range(offset, length);
|
||||||
|
requests_to_go -= 1;
|
||||||
|
bytes_to_go -= length;
|
||||||
|
} else if !missing_data.is_empty() {
|
||||||
|
// ok, the tail is downloaded, download something fom the beginning.
|
||||||
|
let range = missing_data.get_range(0);
|
||||||
|
let offset = range.start;
|
||||||
|
let length = min(range.length, bytes_to_go);
|
||||||
|
self.download_range(offset, length);
|
||||||
|
requests_to_go -= 1;
|
||||||
|
bytes_to_go -= length;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow {
|
||||||
|
match data {
|
||||||
|
ReceivedData::ResponseTimeMs(response_time_ms) => {
|
||||||
|
trace!("Ping time estimated as: {} ms.", response_time_ms);
|
||||||
|
|
||||||
|
// record the response time
|
||||||
|
self.network_response_times_ms.push(response_time_ms);
|
||||||
|
|
||||||
|
// prune old response times. Keep at most three.
|
||||||
|
while self.network_response_times_ms.len() > 3 {
|
||||||
|
self.network_response_times_ms.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// stats::median is experimental. So we calculate the median of up to three ourselves.
|
||||||
|
let ping_time_ms: usize = match self.network_response_times_ms.len() {
|
||||||
|
1 => self.network_response_times_ms[0] as usize,
|
||||||
|
2 => {
|
||||||
|
((self.network_response_times_ms[0] + self.network_response_times_ms[1])
|
||||||
|
/ 2) as usize
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
let mut times = self.network_response_times_ms.clone();
|
||||||
|
times.sort_unstable();
|
||||||
|
times[1]
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// store our new estimate for everyone to see
|
||||||
|
self.shared
|
||||||
|
.ping_time_ms
|
||||||
|
.store(ping_time_ms, atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
ReceivedData::Data(data) => {
|
||||||
|
self.output
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.seek(SeekFrom::Start(data.offset as u64))
|
||||||
|
.unwrap();
|
||||||
|
self.output
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.write_all(data.data.as_ref())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut download_status = self.shared.download_status.lock().unwrap();
|
||||||
|
|
||||||
|
let received_range = Range::new(data.offset, data.data.len());
|
||||||
|
download_status.downloaded.add_range(&received_range);
|
||||||
|
self.shared.cond.notify_all();
|
||||||
|
|
||||||
|
let full = download_status.downloaded.contained_length_from_value(0)
|
||||||
|
>= self.shared.file_size;
|
||||||
|
|
||||||
|
drop(download_status);
|
||||||
|
|
||||||
|
if full {
|
||||||
|
self.finish();
|
||||||
|
return ControlFlow::Break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ControlFlow::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_stream_loader_command(&mut self, cmd: StreamLoaderCommand) -> ControlFlow {
|
||||||
|
match cmd {
|
||||||
|
StreamLoaderCommand::Fetch(request) => {
|
||||||
|
self.download_range(request.start, request.length);
|
||||||
|
}
|
||||||
|
StreamLoaderCommand::RandomAccessMode() => {
|
||||||
|
*(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::RandomAccess();
|
||||||
|
}
|
||||||
|
StreamLoaderCommand::StreamMode() => {
|
||||||
|
*(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::Streaming();
|
||||||
|
self.trigger_preload();
|
||||||
|
}
|
||||||
|
StreamLoaderCommand::Close() => return ControlFlow::Break,
|
||||||
|
}
|
||||||
|
ControlFlow::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&mut self) {
|
||||||
|
let mut output = self.output.take().unwrap();
|
||||||
|
let complete_tx = self.complete_tx.take().unwrap();
|
||||||
|
|
||||||
|
output.seek(SeekFrom::Start(0)).unwrap();
|
||||||
|
let _ = complete_tx.send(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trigger_preload(&mut self) {
|
||||||
|
if self.number_of_open_requests >= MAX_PREFETCH_REQUESTS {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_requests_to_send = MAX_PREFETCH_REQUESTS - self.number_of_open_requests;
|
||||||
|
|
||||||
|
let bytes_pending: usize = {
|
||||||
|
let download_status = self.shared.download_status.lock().unwrap();
|
||||||
|
download_status
|
||||||
|
.requested
|
||||||
|
.minus(&download_status.downloaded)
|
||||||
|
.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
let ping_time_seconds =
|
||||||
|
0.001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64;
|
||||||
|
let download_rate = self.session.channel().get_download_rate_estimate();
|
||||||
|
|
||||||
|
let desired_pending_bytes = max(
|
||||||
|
(PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * self.shared.stream_data_rate as f64)
|
||||||
|
as usize,
|
||||||
|
(FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f64) as usize,
|
||||||
|
);
|
||||||
|
|
||||||
|
if bytes_pending < desired_pending_bytes {
|
||||||
|
self.pre_fetch_more_data(desired_pending_bytes - bytes_pending, max_requests_to_send);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn audio_file_fetch(
|
||||||
|
session: Session,
|
||||||
|
shared: Arc<AudioFileShared>,
|
||||||
|
initial_data_rx: ChannelData,
|
||||||
|
initial_request_sent_time: Instant,
|
||||||
|
initial_data_length: usize,
|
||||||
|
|
||||||
|
output: NamedTempFile,
|
||||||
|
mut stream_loader_command_rx: mpsc::UnboundedReceiver<StreamLoaderCommand>,
|
||||||
|
complete_tx: oneshot::Sender<NamedTempFile>,
|
||||||
|
) {
|
||||||
|
let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel();
|
||||||
|
let (download_finish_tx, mut download_finish_rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
{
|
||||||
|
let requested_range = Range::new(0, initial_data_length);
|
||||||
|
let mut download_status = shared.download_status.lock().unwrap();
|
||||||
|
download_status.requested.add_range(&requested_range);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.spawn(receive_data(
|
||||||
|
shared.clone(),
|
||||||
|
file_data_tx.clone(),
|
||||||
|
initial_data_rx,
|
||||||
|
0,
|
||||||
|
initial_data_length,
|
||||||
|
initial_request_sent_time,
|
||||||
|
true,
|
||||||
|
download_finish_tx.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut fetch = AudioFileFetch {
|
||||||
|
session,
|
||||||
|
shared,
|
||||||
|
output: Some(output),
|
||||||
|
|
||||||
|
file_data_tx,
|
||||||
|
complete_tx: Some(complete_tx),
|
||||||
|
network_response_times_ms: Vec::new(),
|
||||||
|
number_of_open_requests: 1,
|
||||||
|
|
||||||
|
download_finish_tx,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
cmd = stream_loader_command_rx.recv() => {
|
||||||
|
if cmd.map_or(true, |cmd| fetch.handle_stream_loader_command(cmd) == ControlFlow::Break) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data = file_data_rx.recv() => {
|
||||||
|
if data.map_or(true, |data| fetch.handle_file_data(data) == ControlFlow::Break) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ = download_finish_rx.recv() => {
|
||||||
|
fetch.number_of_open_requests -= 1;
|
||||||
|
|
||||||
|
if fetch.get_download_strategy() == DownloadStrategy::Streaming() {
|
||||||
|
fetch.trigger_preload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
extern crate lewton;
|
|
||||||
|
|
||||||
use self::lewton::inside_ogg::OggStreamReader;
|
|
||||||
|
|
||||||
use super::{AudioDecoder, AudioError, AudioPacket};
|
use super::{AudioDecoder, AudioError, AudioPacket};
|
||||||
|
|
||||||
|
use lewton::inside_ogg::OggStreamReader;
|
||||||
|
|
||||||
use std::error;
|
use std::error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::{Read, Seek};
|
use std::io::{Read, Seek};
|
||||||
|
@ -26,16 +25,15 @@ where
|
||||||
fn seek(&mut self, ms: i64) -> Result<(), AudioError> {
|
fn seek(&mut self, ms: i64) -> Result<(), AudioError> {
|
||||||
let absgp = ms * 44100 / 1000;
|
let absgp = ms * 44100 / 1000;
|
||||||
match self.0.seek_absgp_pg(absgp as u64) {
|
match self.0.seek_absgp_pg(absgp as u64) {
|
||||||
Ok(_) => return Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => return Err(AudioError::VorbisError(err.into())),
|
Err(err) => Err(AudioError::VorbisError(err.into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_packet(&mut self) -> Result<Option<AudioPacket>, AudioError> {
|
fn next_packet(&mut self) -> Result<Option<AudioPacket>, AudioError> {
|
||||||
use self::lewton::audio::AudioReadError::AudioIsHeader;
|
use lewton::audio::AudioReadError::AudioIsHeader;
|
||||||
use self::lewton::OggReadError::NoCapturePatternFound;
|
use lewton::OggReadError::NoCapturePatternFound;
|
||||||
use self::lewton::VorbisError::BadAudio;
|
use lewton::VorbisError::{BadAudio, OggError};
|
||||||
use self::lewton::VorbisError::OggError;
|
|
||||||
loop {
|
loop {
|
||||||
match self
|
match self
|
||||||
.0
|
.0
|
||||||
|
|
|
@ -1,33 +1,31 @@
|
||||||
#[macro_use]
|
#![allow(clippy::unused_io_amount, clippy::too_many_arguments)]
|
||||||
extern crate futures;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
extern crate aes_ctr;
|
pub mod convert;
|
||||||
extern crate bit_set;
|
|
||||||
extern crate byteorder;
|
|
||||||
extern crate bytes;
|
|
||||||
extern crate num_bigint;
|
|
||||||
extern crate num_traits;
|
|
||||||
extern crate tempfile;
|
|
||||||
|
|
||||||
extern crate librespot_core;
|
|
||||||
|
|
||||||
mod convert;
|
|
||||||
mod decrypt;
|
mod decrypt;
|
||||||
mod fetch;
|
mod fetch;
|
||||||
|
|
||||||
#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))]
|
use cfg_if::cfg_if;
|
||||||
mod lewton_decoder;
|
|
||||||
#[cfg(any(feature = "with-tremor", feature = "with-vorbis"))]
|
cfg_if! {
|
||||||
mod libvorbis_decoder;
|
if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] {
|
||||||
|
mod libvorbis_decoder;
|
||||||
|
pub use crate::libvorbis_decoder::{VorbisDecoder, VorbisError};
|
||||||
|
} else {
|
||||||
|
mod lewton_decoder;
|
||||||
|
pub use lewton_decoder::{VorbisDecoder, VorbisError};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod passthrough_decoder;
|
mod passthrough_decoder;
|
||||||
|
pub use passthrough_decoder::{PassthroughDecoder, PassthroughError};
|
||||||
|
|
||||||
mod range_set;
|
mod range_set;
|
||||||
|
|
||||||
pub use convert::{i24, SamplesConverter};
|
|
||||||
pub use decrypt::AudioDecrypt;
|
pub use decrypt::AudioDecrypt;
|
||||||
pub use fetch::{AudioFile, AudioFileOpen, StreamLoaderController};
|
pub use fetch::{AudioFile, StreamLoaderController};
|
||||||
pub use fetch::{
|
pub use fetch::{
|
||||||
READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS,
|
READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS,
|
||||||
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS,
|
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS,
|
||||||
|
@ -62,12 +60,6 @@ impl AudioPacket {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))]
|
|
||||||
pub use crate::lewton_decoder::{VorbisDecoder, VorbisError};
|
|
||||||
#[cfg(any(feature = "with-tremor", feature = "with-vorbis"))]
|
|
||||||
pub use libvorbis_decoder::{VorbisDecoder, VorbisError};
|
|
||||||
pub use passthrough_decoder::{PassthroughDecoder, PassthroughError};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AudioError {
|
pub enum AudioError {
|
||||||
PassthroughError(PassthroughError),
|
PassthroughError(PassthroughError),
|
||||||
|
@ -85,13 +77,13 @@ impl fmt::Display for AudioError {
|
||||||
|
|
||||||
impl From<VorbisError> for AudioError {
|
impl From<VorbisError> for AudioError {
|
||||||
fn from(err: VorbisError) -> AudioError {
|
fn from(err: VorbisError) -> AudioError {
|
||||||
AudioError::VorbisError(VorbisError::from(err))
|
AudioError::VorbisError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PassthroughError> for AudioError {
|
impl From<PassthroughError> for AudioError {
|
||||||
fn from(err: PassthroughError) -> AudioError {
|
fn from(err: PassthroughError) -> AudioError {
|
||||||
AudioError::PassthroughError(PassthroughError::from(err))
|
AudioError::PassthroughError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#[cfg(feature = "with-tremor")]
|
#[cfg(feature = "with-tremor")]
|
||||||
extern crate librespot_tremor as vorbis;
|
use librespot_tremor as vorbis;
|
||||||
#[cfg(not(feature = "with-tremor"))]
|
|
||||||
extern crate vorbis;
|
|
||||||
|
|
||||||
use super::{AudioDecoder, AudioError, AudioPacket};
|
use super::{AudioDecoder, AudioError, AudioPacket};
|
||||||
use std::error;
|
use std::error;
|
||||||
|
|
|
@ -18,7 +18,7 @@ where
|
||||||
return Err(PassthroughError(OggReadError::InvalidData));
|
return Err(PassthroughError(OggReadError::InvalidData));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(pck.data.into_boxed_slice());
|
Ok(pck.data.into_boxed_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PassthroughDecoder<R: Read + Seek> {
|
pub struct PassthroughDecoder<R: Read + Seek> {
|
||||||
|
@ -54,7 +54,7 @@ impl<R: Read + Seek> PassthroughDecoder<R> {
|
||||||
// remove un-needed packets
|
// remove un-needed packets
|
||||||
rdr.delete_unread_packets();
|
rdr.delete_unread_packets();
|
||||||
|
|
||||||
return Ok(PassthroughDecoder {
|
Ok(PassthroughDecoder {
|
||||||
rdr,
|
rdr,
|
||||||
wtr: PacketWriter::new(Vec::new()),
|
wtr: PacketWriter::new(Vec::new()),
|
||||||
ofsgp_page: 0,
|
ofsgp_page: 0,
|
||||||
|
@ -64,7 +64,7 @@ impl<R: Read + Seek> PassthroughDecoder<R> {
|
||||||
setup,
|
setup,
|
||||||
eos: false,
|
eos: false,
|
||||||
bos: false,
|
bos: false,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,15 +102,15 @@ impl<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
|
||||||
let pck = self.rdr.read_packet().unwrap().unwrap();
|
let pck = self.rdr.read_packet().unwrap().unwrap();
|
||||||
self.ofsgp_page = pck.absgp_page();
|
self.ofsgp_page = pck.absgp_page();
|
||||||
debug!("Seek to offset page {}", self.ofsgp_page);
|
debug!("Seek to offset page {}", self.ofsgp_page);
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => return Err(AudioError::PassthroughError(err.into())),
|
Err(err) => Err(AudioError::PassthroughError(err.into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_packet(&mut self) -> Result<Option<AudioPacket>, AudioError> {
|
fn next_packet(&mut self) -> Result<Option<AudioPacket>, AudioError> {
|
||||||
// write headers if we are (re)starting
|
// write headers if we are (re)starting
|
||||||
if self.bos == false {
|
if !self.bos {
|
||||||
self.wtr
|
self.wtr
|
||||||
.write_packet(
|
.write_packet(
|
||||||
self.ident.clone(),
|
self.ident.clone(),
|
||||||
|
@ -177,7 +177,7 @@ impl<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
|
||||||
|
|
||||||
let data = self.wtr.inner_mut();
|
let data = self.wtr.inner_mut();
|
||||||
|
|
||||||
if data.len() > 0 {
|
if !data.is_empty() {
|
||||||
let result = AudioPacket::OggData(std::mem::take(data));
|
let result = AudioPacket::OggData(std::mem::take(data));
|
||||||
return Ok(Some(result));
|
return Ok(Some(result));
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::cmp::{max, min};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::slice::Iter;
|
use std::slice::Iter;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct Range {
|
pub struct Range {
|
||||||
pub start: usize,
|
pub start: usize,
|
||||||
pub length: usize,
|
pub length: usize,
|
||||||
|
@ -16,14 +16,11 @@ impl fmt::Display for Range {
|
||||||
|
|
||||||
impl Range {
|
impl Range {
|
||||||
pub fn new(start: usize, length: usize) -> Range {
|
pub fn new(start: usize, length: usize) -> Range {
|
||||||
return Range {
|
Range { start, length }
|
||||||
start: start,
|
|
||||||
length: length,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn end(&self) -> usize {
|
pub fn end(&self) -> usize {
|
||||||
return self.start + self.length;
|
self.start + self.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,23 +47,19 @@ impl RangeSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
return self.ranges.is_empty();
|
self.ranges.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
let mut result = 0;
|
self.ranges.iter().map(|r| r.length).sum()
|
||||||
for range in self.ranges.iter() {
|
|
||||||
result += range.length;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_range(&self, index: usize) -> Range {
|
pub fn get_range(&self, index: usize) -> Range {
|
||||||
return self.ranges[index].clone();
|
self.ranges[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> Iter<Range> {
|
pub fn iter(&self) -> Iter<Range> {
|
||||||
return self.ranges.iter();
|
self.ranges.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains(&self, value: usize) -> bool {
|
pub fn contains(&self, value: usize) -> bool {
|
||||||
|
@ -77,7 +70,7 @@ impl RangeSet {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contained_length_from_value(&self, value: usize) -> usize {
|
pub fn contained_length_from_value(&self, value: usize) -> usize {
|
||||||
|
@ -88,7 +81,7 @@ impl RangeSet {
|
||||||
return range.end() - value;
|
return range.end() - value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -98,12 +91,12 @@ impl RangeSet {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_range(&mut self, range: &Range) {
|
pub fn add_range(&mut self, range: &Range) {
|
||||||
if range.length <= 0 {
|
if range.length == 0 {
|
||||||
// the interval is empty or invalid -> nothing to do.
|
// the interval is empty -> nothing to do.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +104,7 @@ impl RangeSet {
|
||||||
// the new range is clear of any ranges we already iterated over.
|
// the new range is clear of any ranges we already iterated over.
|
||||||
if range.end() < self.ranges[index].start {
|
if range.end() < self.ranges[index].start {
|
||||||
// the new range starts after anything we already passed and ends before the next range starts (they don't touch) -> insert it.
|
// the new range starts after anything we already passed and ends before the next range starts (they don't touch) -> insert it.
|
||||||
self.ranges.insert(index, range.clone());
|
self.ranges.insert(index, *range);
|
||||||
return;
|
return;
|
||||||
} else if range.start <= self.ranges[index].end()
|
} else if range.start <= self.ranges[index].end()
|
||||||
&& self.ranges[index].start <= range.end()
|
&& self.ranges[index].start <= range.end()
|
||||||
|
@ -119,7 +112,7 @@ impl RangeSet {
|
||||||
// the new range overlaps (or touches) the first range. They are to be merged.
|
// the new range overlaps (or touches) the first range. They are to be merged.
|
||||||
// In addition we might have to merge further ranges in as well.
|
// In addition we might have to merge further ranges in as well.
|
||||||
|
|
||||||
let mut new_range = range.clone();
|
let mut new_range = *range;
|
||||||
|
|
||||||
while index < self.ranges.len() && self.ranges[index].start <= new_range.end() {
|
while index < self.ranges.len() && self.ranges[index].start <= new_range.end() {
|
||||||
let new_end = max(new_range.end(), self.ranges[index].end());
|
let new_end = max(new_range.end(), self.ranges[index].end());
|
||||||
|
@ -134,7 +127,7 @@ impl RangeSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
// the new range is after everything else -> just add it
|
// the new range is after everything else -> just add it
|
||||||
self.ranges.push(range.clone());
|
self.ranges.push(*range);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -148,11 +141,11 @@ impl RangeSet {
|
||||||
pub fn union(&self, other: &RangeSet) -> RangeSet {
|
pub fn union(&self, other: &RangeSet) -> RangeSet {
|
||||||
let mut result = self.clone();
|
let mut result = self.clone();
|
||||||
result.add_range_set(other);
|
result.add_range_set(other);
|
||||||
return result;
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subtract_range(&mut self, range: &Range) {
|
pub fn subtract_range(&mut self, range: &Range) {
|
||||||
if range.length <= 0 {
|
if range.length == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +201,7 @@ impl RangeSet {
|
||||||
pub fn minus(&self, other: &RangeSet) -> RangeSet {
|
pub fn minus(&self, other: &RangeSet) -> RangeSet {
|
||||||
let mut result = self.clone();
|
let mut result = self.clone();
|
||||||
result.subtract_range_set(other);
|
result.subtract_range_set(other);
|
||||||
return result;
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn intersection(&self, other: &RangeSet) -> RangeSet {
|
pub fn intersection(&self, other: &RangeSet) -> RangeSet {
|
||||||
|
@ -244,6 +237,6 @@ impl RangeSet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,37 +7,40 @@ license = "MIT"
|
||||||
repository = "https://github.com/librespot-org/librespot"
|
repository = "https://github.com/librespot-org/librespot"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies.librespot-core]
|
|
||||||
path = "../core"
|
|
||||||
version = "0.1.6"
|
|
||||||
[dependencies.librespot-playback]
|
|
||||||
path = "../playback"
|
|
||||||
version = "0.1.6"
|
|
||||||
[dependencies.librespot-protocol]
|
|
||||||
path = "../protocol"
|
|
||||||
version = "0.1.6"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes-ctr = "0.6"
|
aes-ctr = "0.6"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
block-modes = "0.7"
|
block-modes = "0.7"
|
||||||
futures = "0.1"
|
form_urlencoded = "1.0"
|
||||||
|
futures-core = "0.3"
|
||||||
|
futures-util = { version = "0.3", default_features = false }
|
||||||
hmac = "0.10"
|
hmac = "0.10"
|
||||||
hyper = "0.11"
|
hyper = { version = "0.14", features = ["server", "http1", "tcp"] }
|
||||||
|
libmdns = "0.6"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
num-bigint = "0.3"
|
|
||||||
protobuf = "~2.14.0"
|
protobuf = "~2.14.0"
|
||||||
rand = "0.7"
|
rand = "0.8"
|
||||||
serde = "1.0"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_derive = "1.0"
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
sha-1 = "0.9"
|
sha-1 = "0.9"
|
||||||
tokio-core = "0.1"
|
tokio = { version = "1.0", features = ["macros", "rt", "sync"] }
|
||||||
url = "1.7"
|
tokio-stream = { version = "0.1" }
|
||||||
|
url = "2.1"
|
||||||
|
|
||||||
dns-sd = { version = "0.1.3", optional = true }
|
dns-sd = { version = "0.1.3", optional = true }
|
||||||
libmdns = { version = "0.2.7", optional = true }
|
|
||||||
|
[dependencies.librespot-core]
|
||||||
|
path = "../core"
|
||||||
|
version = "0.1.6"
|
||||||
|
|
||||||
|
[dependencies.librespot-playback]
|
||||||
|
path = "../playback"
|
||||||
|
version = "0.1.6"
|
||||||
|
|
||||||
|
[dependencies.librespot-protocol]
|
||||||
|
path = "../protocol"
|
||||||
|
version = "0.1.6"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["libmdns"]
|
|
||||||
with-dns-sd = ["dns-sd"]
|
with-dns-sd = ["dns-sd"]
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
use crate::core::spotify_id::SpotifyId;
|
||||||
use crate::protocol::spirc::TrackRef;
|
use crate::protocol::spirc::TrackRef;
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
|
||||||
|
|
||||||
use serde;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct StationContext {
|
pub struct StationContext {
|
||||||
|
|
|
@ -1,32 +1,29 @@
|
||||||
use aes_ctr::cipher::generic_array::GenericArray;
|
use aes_ctr::cipher::generic_array::GenericArray;
|
||||||
use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher};
|
use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher};
|
||||||
use aes_ctr::Aes128Ctr;
|
use aes_ctr::Aes128Ctr;
|
||||||
use base64;
|
use futures_core::Stream;
|
||||||
use futures::sync::mpsc;
|
|
||||||
use futures::{Future, Poll, Stream};
|
|
||||||
use hmac::{Hmac, Mac, NewMac};
|
use hmac::{Hmac, Mac, NewMac};
|
||||||
use hyper::server::{Http, Request, Response, Service};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use hyper::{self, Get, Post, StatusCode};
|
use hyper::{Body, Method, Request, Response, StatusCode};
|
||||||
|
use serde_json::json;
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
#[cfg(feature = "with-dns-sd")]
|
#[cfg(feature = "with-dns-sd")]
|
||||||
use dns_sd::DNSService;
|
use dns_sd::DNSService;
|
||||||
|
|
||||||
#[cfg(not(feature = "with-dns-sd"))]
|
|
||||||
use libmdns;
|
|
||||||
|
|
||||||
use num_bigint::BigUint;
|
|
||||||
use rand;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::io;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio_core::reactor::Handle;
|
|
||||||
use url;
|
|
||||||
|
|
||||||
use librespot_core::authentication::Credentials;
|
use librespot_core::authentication::Credentials;
|
||||||
use librespot_core::config::ConnectConfig;
|
use librespot_core::config::ConnectConfig;
|
||||||
use librespot_core::diffie_hellman::{DH_GENERATOR, DH_PRIME};
|
use librespot_core::diffie_hellman::DhLocalKeys;
|
||||||
use librespot_core::util;
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::io;
|
||||||
|
use std::net::{Ipv4Addr, SocketAddr};
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
type HmacSha1 = Hmac<Sha1>;
|
type HmacSha1 = Hmac<Sha1>;
|
||||||
|
|
||||||
|
@ -35,8 +32,7 @@ struct Discovery(Arc<DiscoveryInner>);
|
||||||
struct DiscoveryInner {
|
struct DiscoveryInner {
|
||||||
config: ConnectConfig,
|
config: ConnectConfig,
|
||||||
device_id: String,
|
device_id: String,
|
||||||
private_key: BigUint,
|
keys: DhLocalKeys,
|
||||||
public_key: BigUint,
|
|
||||||
tx: mpsc::UnboundedSender<Credentials>,
|
tx: mpsc::UnboundedSender<Credentials>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,31 +41,20 @@ impl Discovery {
|
||||||
config: ConnectConfig,
|
config: ConnectConfig,
|
||||||
device_id: String,
|
device_id: String,
|
||||||
) -> (Discovery, mpsc::UnboundedReceiver<Credentials>) {
|
) -> (Discovery, mpsc::UnboundedReceiver<Credentials>) {
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
let key_data = util::rand_vec(&mut rand::thread_rng(), 95);
|
|
||||||
let private_key = BigUint::from_bytes_be(&key_data);
|
|
||||||
let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME);
|
|
||||||
|
|
||||||
let discovery = Discovery(Arc::new(DiscoveryInner {
|
let discovery = Discovery(Arc::new(DiscoveryInner {
|
||||||
config: config,
|
config,
|
||||||
device_id: device_id,
|
device_id,
|
||||||
private_key: private_key,
|
keys: DhLocalKeys::random(&mut rand::thread_rng()),
|
||||||
public_key: public_key,
|
tx,
|
||||||
tx: tx,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
(discovery, rx)
|
(discovery, rx)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Discovery {
|
fn handle_get_info(&self, _: BTreeMap<Cow<'_, str>, Cow<'_, str>>) -> Response<hyper::Body> {
|
||||||
fn handle_get_info(
|
let public_key = base64::encode(&self.0.keys.public_key());
|
||||||
&self,
|
|
||||||
_params: &BTreeMap<String, String>,
|
|
||||||
) -> ::futures::Finished<Response, hyper::Error> {
|
|
||||||
let public_key = self.0.public_key.to_bytes_be();
|
|
||||||
let public_key = base64::encode(&public_key);
|
|
||||||
|
|
||||||
let result = json!({
|
let result = json!({
|
||||||
"status": 101,
|
"status": 101,
|
||||||
|
@ -91,29 +76,29 @@ impl Discovery {
|
||||||
});
|
});
|
||||||
|
|
||||||
let body = result.to_string();
|
let body = result.to_string();
|
||||||
::futures::finished(Response::new().with_body(body))
|
Response::new(Body::from(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_add_user(
|
fn handle_add_user(
|
||||||
&self,
|
&self,
|
||||||
params: &BTreeMap<String, String>,
|
params: BTreeMap<Cow<'_, str>, Cow<'_, str>>,
|
||||||
) -> ::futures::Finished<Response, hyper::Error> {
|
) -> Response<hyper::Body> {
|
||||||
let username = params.get("userName").unwrap();
|
let username = params.get("userName").unwrap().as_ref();
|
||||||
let encrypted_blob = params.get("blob").unwrap();
|
let encrypted_blob = params.get("blob").unwrap();
|
||||||
let client_key = params.get("clientKey").unwrap();
|
let client_key = params.get("clientKey").unwrap();
|
||||||
|
|
||||||
let encrypted_blob = base64::decode(encrypted_blob).unwrap();
|
let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap();
|
||||||
|
|
||||||
let client_key = base64::decode(client_key).unwrap();
|
let shared_key = self
|
||||||
let client_key = BigUint::from_bytes_be(&client_key);
|
.0
|
||||||
|
.keys
|
||||||
let shared_key = util::powm(&client_key, &self.0.private_key, &DH_PRIME);
|
.shared_secret(&base64::decode(client_key.as_bytes()).unwrap());
|
||||||
|
|
||||||
let iv = &encrypted_blob[0..16];
|
let iv = &encrypted_blob[0..16];
|
||||||
let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20];
|
let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20];
|
||||||
let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()];
|
let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()];
|
||||||
|
|
||||||
let base_key = Sha1::digest(&shared_key.to_bytes_be());
|
let base_key = Sha1::digest(&shared_key);
|
||||||
let base_key = &base_key[..16];
|
let base_key = &base_key[..16];
|
||||||
|
|
||||||
let checksum_key = {
|
let checksum_key = {
|
||||||
|
@ -130,7 +115,7 @@ impl Discovery {
|
||||||
|
|
||||||
let mut h = HmacSha1::new_varkey(&checksum_key).expect("HMAC can take key of any size");
|
let mut h = HmacSha1::new_varkey(&checksum_key).expect("HMAC can take key of any size");
|
||||||
h.update(encrypted);
|
h.update(encrypted);
|
||||||
if let Err(_) = h.verify(cksum) {
|
if h.verify(cksum).is_err() {
|
||||||
warn!("Login error for user {:?}: MAC mismatch", username);
|
warn!("Login error for user {:?}: MAC mismatch", username);
|
||||||
let result = json!({
|
let result = json!({
|
||||||
"status": 102,
|
"status": 102,
|
||||||
|
@ -139,7 +124,7 @@ impl Discovery {
|
||||||
});
|
});
|
||||||
|
|
||||||
let body = result.to_string();
|
let body = result.to_string();
|
||||||
return ::futures::finished(Response::new().with_body(body));
|
return Response::new(Body::from(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
let decrypted = {
|
let decrypted = {
|
||||||
|
@ -153,9 +138,9 @@ impl Discovery {
|
||||||
};
|
};
|
||||||
|
|
||||||
let credentials =
|
let credentials =
|
||||||
Credentials::with_blob(username.to_owned(), &decrypted, &self.0.device_id);
|
Credentials::with_blob(username.to_string(), &decrypted, &self.0.device_id);
|
||||||
|
|
||||||
self.0.tx.unbounded_send(credentials).unwrap();
|
self.0.tx.send(credentials).unwrap();
|
||||||
|
|
||||||
let result = json!({
|
let result = json!({
|
||||||
"status": 101,
|
"status": 101,
|
||||||
|
@ -164,49 +149,39 @@ impl Discovery {
|
||||||
});
|
});
|
||||||
|
|
||||||
let body = result.to_string();
|
let body = result.to_string();
|
||||||
::futures::finished(Response::new().with_body(body))
|
Response::new(Body::from(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn not_found(&self) -> ::futures::Finished<Response, hyper::Error> {
|
fn not_found(&self) -> Response<hyper::Body> {
|
||||||
::futures::finished(Response::new().with_status(StatusCode::NotFound))
|
let mut res = Response::default();
|
||||||
|
*res.status_mut() = StatusCode::NOT_FOUND;
|
||||||
|
res
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Service for Discovery {
|
async fn call(self, request: Request<Body>) -> hyper::Result<Response<Body>> {
|
||||||
type Request = Request;
|
|
||||||
type Response = Response;
|
|
||||||
type Error = hyper::Error;
|
|
||||||
type Future = Box<dyn Future<Item = Response, Error = hyper::Error>>;
|
|
||||||
|
|
||||||
fn call(&self, request: Request) -> Self::Future {
|
|
||||||
let mut params = BTreeMap::new();
|
let mut params = BTreeMap::new();
|
||||||
|
|
||||||
let (method, uri, _, _, body) = request.deconstruct();
|
let (parts, body) = request.into_parts();
|
||||||
if let Some(query) = uri.query() {
|
|
||||||
params.extend(url::form_urlencoded::parse(query.as_bytes()).into_owned());
|
if let Some(query) = parts.uri.query() {
|
||||||
|
let query_params = url::form_urlencoded::parse(query.as_bytes());
|
||||||
|
params.extend(query_params);
|
||||||
}
|
}
|
||||||
|
|
||||||
if method != Get {
|
if parts.method != Method::GET {
|
||||||
debug!("{:?} {:?} {:?}", method, uri.path(), params);
|
debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params);
|
||||||
}
|
}
|
||||||
|
|
||||||
let this = self.clone();
|
let body = hyper::body::to_bytes(body).await?;
|
||||||
Box::new(
|
|
||||||
body.fold(Vec::new(), |mut acc, chunk| {
|
params.extend(url::form_urlencoded::parse(&body));
|
||||||
acc.extend_from_slice(chunk.as_ref());
|
|
||||||
Ok::<_, hyper::Error>(acc)
|
Ok(
|
||||||
})
|
match (parts.method, params.get("action").map(AsRef::as_ref)) {
|
||||||
.map(move |body| {
|
(Method::GET, Some("getInfo")) => self.handle_get_info(params),
|
||||||
params.extend(url::form_urlencoded::parse(&body).into_owned());
|
(Method::POST, Some("addUser")) => self.handle_add_user(params),
|
||||||
params
|
_ => self.not_found(),
|
||||||
})
|
},
|
||||||
.and_then(move |params| {
|
|
||||||
match (method, params.get("action").map(AsRef::as_ref)) {
|
|
||||||
(Get, Some("getInfo")) => this.handle_get_info(¶ms),
|
|
||||||
(Post, Some("addUser")) => this.handle_add_user(¶ms),
|
|
||||||
_ => this.not_found(),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -215,45 +190,40 @@ impl Service for Discovery {
|
||||||
pub struct DiscoveryStream {
|
pub struct DiscoveryStream {
|
||||||
credentials: mpsc::UnboundedReceiver<Credentials>,
|
credentials: mpsc::UnboundedReceiver<Credentials>,
|
||||||
_svc: DNSService,
|
_svc: DNSService,
|
||||||
|
_close_tx: oneshot::Sender<Infallible>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "with-dns-sd"))]
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
pub struct DiscoveryStream {
|
pub struct DiscoveryStream {
|
||||||
credentials: mpsc::UnboundedReceiver<Credentials>,
|
credentials: mpsc::UnboundedReceiver<Credentials>,
|
||||||
_svc: libmdns::Service,
|
_svc: libmdns::Service,
|
||||||
|
_close_tx: oneshot::Sender<Infallible>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn discovery(
|
pub fn discovery(
|
||||||
handle: &Handle,
|
|
||||||
config: ConnectConfig,
|
config: ConnectConfig,
|
||||||
device_id: String,
|
device_id: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
) -> io::Result<DiscoveryStream> {
|
) -> io::Result<DiscoveryStream> {
|
||||||
let (discovery, creds_rx) = Discovery::new(config.clone(), device_id);
|
let (discovery, creds_rx) = Discovery::new(config.clone(), device_id);
|
||||||
|
let (close_tx, close_rx) = oneshot::channel();
|
||||||
|
|
||||||
let serve = {
|
let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port);
|
||||||
let http = Http::new();
|
|
||||||
http.serve_addr_handle(
|
|
||||||
&format!("0.0.0.0:{}", port).parse().unwrap(),
|
|
||||||
&handle,
|
|
||||||
move || Ok(discovery.clone()),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let s_port = serve.incoming_ref().local_addr().port();
|
let make_service = make_service_fn(move |_| {
|
||||||
|
let discovery = discovery.clone();
|
||||||
|
async move { Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().call(request))) }
|
||||||
|
});
|
||||||
|
|
||||||
|
let server = hyper::Server::bind(&address).serve(make_service);
|
||||||
|
|
||||||
|
let s_port = server.local_addr().port();
|
||||||
debug!("Zeroconf server listening on 0.0.0.0:{}", s_port);
|
debug!("Zeroconf server listening on 0.0.0.0:{}", s_port);
|
||||||
|
|
||||||
let server_future = {
|
tokio::spawn(server.with_graceful_shutdown(async {
|
||||||
let handle = handle.clone();
|
close_rx.await.unwrap_err();
|
||||||
serve
|
debug!("Shutting down discovery server");
|
||||||
.for_each(move |connection| {
|
}));
|
||||||
handle.spawn(connection.then(|_| Ok(())));
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.then(|_| Ok(()))
|
|
||||||
};
|
|
||||||
handle.spawn(server_future);
|
|
||||||
|
|
||||||
#[cfg(feature = "with-dns-sd")]
|
#[cfg(feature = "with-dns-sd")]
|
||||||
let svc = DNSService::register(
|
let svc = DNSService::register(
|
||||||
|
@ -267,7 +237,7 @@ pub fn discovery(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
#[cfg(not(feature = "with-dns-sd"))]
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
let responder = libmdns::Responder::spawn(&handle)?;
|
let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?;
|
||||||
|
|
||||||
#[cfg(not(feature = "with-dns-sd"))]
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
let svc = responder.register(
|
let svc = responder.register(
|
||||||
|
@ -280,14 +250,14 @@ pub fn discovery(
|
||||||
Ok(DiscoveryStream {
|
Ok(DiscoveryStream {
|
||||||
credentials: creds_rx,
|
credentials: creds_rx,
|
||||||
_svc: svc,
|
_svc: svc,
|
||||||
|
_close_tx: close_tx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for DiscoveryStream {
|
impl Stream for DiscoveryStream {
|
||||||
type Item = Credentials;
|
type Item = Credentials;
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
self.credentials.poll()
|
self.credentials.poll_recv(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,9 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_json;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_derive;
|
|
||||||
extern crate serde;
|
|
||||||
|
|
||||||
extern crate base64;
|
use librespot_core as core;
|
||||||
extern crate futures;
|
use librespot_playback as playback;
|
||||||
extern crate hyper;
|
use librespot_protocol as protocol;
|
||||||
extern crate num_bigint;
|
|
||||||
extern crate protobuf;
|
|
||||||
extern crate rand;
|
|
||||||
extern crate tokio_core;
|
|
||||||
extern crate url;
|
|
||||||
|
|
||||||
extern crate aes_ctr;
|
|
||||||
extern crate block_modes;
|
|
||||||
extern crate hmac;
|
|
||||||
extern crate sha1;
|
|
||||||
|
|
||||||
#[cfg(feature = "with-dns-sd")]
|
|
||||||
extern crate dns_sd;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "with-dns-sd"))]
|
|
||||||
extern crate libmdns;
|
|
||||||
|
|
||||||
extern crate librespot_core;
|
|
||||||
extern crate librespot_playback as playback;
|
|
||||||
extern crate librespot_protocol as protocol;
|
|
||||||
|
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod discovery;
|
pub mod discovery;
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
use std;
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use futures::future;
|
|
||||||
use futures::sync::mpsc;
|
|
||||||
use futures::{Async, Future, Poll, Sink, Stream};
|
|
||||||
use protobuf::{self, Message};
|
|
||||||
use rand;
|
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use crate::context::StationContext;
|
use crate::context::StationContext;
|
||||||
|
use crate::core::config::{ConnectConfig, VolumeCtrl};
|
||||||
|
use crate::core::mercury::{MercuryError, MercurySender};
|
||||||
|
use crate::core::session::Session;
|
||||||
|
use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError};
|
||||||
|
use crate::core::util::SeqGenerator;
|
||||||
|
use crate::core::version;
|
||||||
use crate::playback::mixer::Mixer;
|
use crate::playback::mixer::Mixer;
|
||||||
use crate::playback::player::{Player, PlayerEvent, PlayerEventChannel};
|
use crate::playback::player::{Player, PlayerEvent, PlayerEventChannel};
|
||||||
use crate::protocol;
|
use crate::protocol;
|
||||||
use crate::protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef};
|
use crate::protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef};
|
||||||
use librespot_core::config::{ConnectConfig, VolumeCtrl};
|
|
||||||
use librespot_core::mercury::MercuryError;
|
use futures_util::future::{self, FusedFuture};
|
||||||
use librespot_core::session::Session;
|
use futures_util::stream::FusedStream;
|
||||||
use librespot_core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError};
|
use futures_util::{FutureExt, StreamExt};
|
||||||
use librespot_core::util::url_encode;
|
use protobuf::{self, Message};
|
||||||
use librespot_core::util::SeqGenerator;
|
use rand::seq::SliceRandom;
|
||||||
use librespot_core::version;
|
use tokio::sync::mpsc;
|
||||||
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
|
||||||
enum SpircPlayStatus {
|
enum SpircPlayStatus {
|
||||||
Stopped,
|
Stopped,
|
||||||
|
@ -40,7 +40,10 @@ enum SpircPlayStatus {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SpircTask {
|
type BoxedFuture<T> = Pin<Box<dyn FusedFuture<Output = T> + Send>>;
|
||||||
|
type BoxedStream<T> = Pin<Box<dyn FusedStream<Item = T> + Send>>;
|
||||||
|
|
||||||
|
struct SpircTask {
|
||||||
player: Player,
|
player: Player,
|
||||||
mixer: Box<dyn Mixer>,
|
mixer: Box<dyn Mixer>,
|
||||||
config: SpircTaskConfig,
|
config: SpircTaskConfig,
|
||||||
|
@ -54,15 +57,15 @@ pub struct SpircTask {
|
||||||
mixer_started: bool,
|
mixer_started: bool,
|
||||||
play_status: SpircPlayStatus,
|
play_status: SpircPlayStatus,
|
||||||
|
|
||||||
subscription: Box<dyn Stream<Item = Frame, Error = MercuryError>>,
|
subscription: BoxedStream<Frame>,
|
||||||
sender: Box<dyn Sink<SinkItem = Frame, SinkError = MercuryError>>,
|
sender: MercurySender,
|
||||||
commands: mpsc::UnboundedReceiver<SpircCommand>,
|
commands: Option<mpsc::UnboundedReceiver<SpircCommand>>,
|
||||||
player_events: PlayerEventChannel,
|
player_events: Option<PlayerEventChannel>,
|
||||||
|
|
||||||
shutdown: bool,
|
shutdown: bool,
|
||||||
session: Session,
|
session: Session,
|
||||||
context_fut: Box<dyn Future<Item = serde_json::Value, Error = MercuryError>>,
|
context_fut: BoxedFuture<Result<serde_json::Value, MercuryError>>,
|
||||||
autoplay_fut: Box<dyn Future<Item = String, Error = MercuryError>>,
|
autoplay_fut: BoxedFuture<Result<String, MercuryError>>,
|
||||||
context: Option<StationContext>,
|
context: Option<StationContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,38 +243,41 @@ fn volume_to_mixer(volume: u16, volume_ctrl: &VolumeCtrl) -> u16 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn url_encode(bytes: impl AsRef<[u8]>) -> String {
|
||||||
|
form_urlencoded::byte_serialize(bytes.as_ref()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl Spirc {
|
impl Spirc {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
config: ConnectConfig,
|
config: ConnectConfig,
|
||||||
session: Session,
|
session: Session,
|
||||||
player: Player,
|
player: Player,
|
||||||
mixer: Box<dyn Mixer>,
|
mixer: Box<dyn Mixer>,
|
||||||
) -> (Spirc, SpircTask) {
|
) -> (Spirc, impl Future<Output = ()>) {
|
||||||
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: {}", url_encode(&session.username()));
|
debug!("canonical_username: {}", &session.username());
|
||||||
let uri = format!("hm://remote/user/{}/", url_encode(&session.username()));
|
let uri = format!("hm://remote/user/{}/", url_encode(&session.username()));
|
||||||
|
|
||||||
let subscription = session.mercury().subscribe(&uri as &str);
|
let subscription = Box::pin(
|
||||||
let subscription = subscription
|
|
||||||
.map(|stream| stream.map_err(|_| MercuryError))
|
|
||||||
.flatten_stream();
|
|
||||||
let subscription = Box::new(subscription.map(|response| -> Frame {
|
|
||||||
let data = response.payload.first().unwrap();
|
|
||||||
protobuf::parse_from_bytes(data).unwrap()
|
|
||||||
}));
|
|
||||||
|
|
||||||
let sender = Box::new(
|
|
||||||
session
|
session
|
||||||
.mercury()
|
.mercury()
|
||||||
.sender(uri)
|
.subscribe(uri.clone())
|
||||||
.with(|frame: Frame| Ok(frame.write_to_bytes().unwrap())),
|
.map(Result::unwrap)
|
||||||
|
.map(UnboundedReceiverStream::new)
|
||||||
|
.flatten_stream()
|
||||||
|
.map(|response| -> Frame {
|
||||||
|
let data = response.payload.first().unwrap();
|
||||||
|
protobuf::parse_from_bytes(data).unwrap()
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (cmd_tx, cmd_rx) = mpsc::unbounded();
|
let sender = session.mercury().sender(uri);
|
||||||
|
|
||||||
|
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
let volume = config.volume;
|
let volume = config.volume;
|
||||||
let task_config = SpircTaskConfig {
|
let task_config = SpircTaskConfig {
|
||||||
|
@ -284,30 +290,30 @@ impl Spirc {
|
||||||
let player_events = player.get_player_event_channel();
|
let player_events = player.get_player_event_channel();
|
||||||
|
|
||||||
let mut task = SpircTask {
|
let mut task = SpircTask {
|
||||||
player: player,
|
player,
|
||||||
mixer: mixer,
|
mixer,
|
||||||
config: task_config,
|
config: task_config,
|
||||||
|
|
||||||
sequence: SeqGenerator::new(1),
|
sequence: SeqGenerator::new(1),
|
||||||
|
|
||||||
ident: ident,
|
ident,
|
||||||
|
|
||||||
device: device,
|
device,
|
||||||
state: initial_state(),
|
state: initial_state(),
|
||||||
play_request_id: None,
|
play_request_id: None,
|
||||||
mixer_started: false,
|
mixer_started: false,
|
||||||
play_status: SpircPlayStatus::Stopped,
|
play_status: SpircPlayStatus::Stopped,
|
||||||
|
|
||||||
subscription: subscription,
|
subscription,
|
||||||
sender: sender,
|
sender,
|
||||||
commands: cmd_rx,
|
commands: Some(cmd_rx),
|
||||||
player_events: player_events,
|
player_events: Some(player_events),
|
||||||
|
|
||||||
shutdown: false,
|
shutdown: false,
|
||||||
session: session.clone(),
|
session,
|
||||||
|
|
||||||
context_fut: Box::new(future::empty()),
|
context_fut: Box::pin(future::pending()),
|
||||||
autoplay_fut: Box::new(future::empty()),
|
autoplay_fut: Box::pin(future::pending()),
|
||||||
context: None,
|
context: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -317,150 +323,114 @@ impl Spirc {
|
||||||
|
|
||||||
task.hello();
|
task.hello();
|
||||||
|
|
||||||
(spirc, task)
|
(spirc, task.run())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play(&self) {
|
pub fn play(&self) {
|
||||||
let _ = self.commands.unbounded_send(SpircCommand::Play);
|
let _ = self.commands.send(SpircCommand::Play);
|
||||||
}
|
}
|
||||||
pub fn play_pause(&self) {
|
pub fn play_pause(&self) {
|
||||||
let _ = self.commands.unbounded_send(SpircCommand::PlayPause);
|
let _ = self.commands.send(SpircCommand::PlayPause);
|
||||||
}
|
}
|
||||||
pub fn pause(&self) {
|
pub fn pause(&self) {
|
||||||
let _ = self.commands.unbounded_send(SpircCommand::Pause);
|
let _ = self.commands.send(SpircCommand::Pause);
|
||||||
}
|
}
|
||||||
pub fn prev(&self) {
|
pub fn prev(&self) {
|
||||||
let _ = self.commands.unbounded_send(SpircCommand::Prev);
|
let _ = self.commands.send(SpircCommand::Prev);
|
||||||
}
|
}
|
||||||
pub fn next(&self) {
|
pub fn next(&self) {
|
||||||
let _ = self.commands.unbounded_send(SpircCommand::Next);
|
let _ = self.commands.send(SpircCommand::Next);
|
||||||
}
|
}
|
||||||
pub fn volume_up(&self) {
|
pub fn volume_up(&self) {
|
||||||
let _ = self.commands.unbounded_send(SpircCommand::VolumeUp);
|
let _ = self.commands.send(SpircCommand::VolumeUp);
|
||||||
}
|
}
|
||||||
pub fn volume_down(&self) {
|
pub fn volume_down(&self) {
|
||||||
let _ = self.commands.unbounded_send(SpircCommand::VolumeDown);
|
let _ = self.commands.send(SpircCommand::VolumeDown);
|
||||||
}
|
}
|
||||||
pub fn shutdown(&self) {
|
pub fn shutdown(&self) {
|
||||||
let _ = self.commands.unbounded_send(SpircCommand::Shutdown);
|
let _ = self.commands.send(SpircCommand::Shutdown);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Future for SpircTask {
|
|
||||||
type Item = ();
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<(), ()> {
|
|
||||||
loop {
|
|
||||||
let mut progress = false;
|
|
||||||
|
|
||||||
if self.session.is_invalid() {
|
|
||||||
return Ok(Async::Ready(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.shutdown {
|
|
||||||
match self.subscription.poll().unwrap() {
|
|
||||||
Async::Ready(Some(frame)) => {
|
|
||||||
progress = true;
|
|
||||||
self.handle_frame(frame);
|
|
||||||
}
|
|
||||||
Async::Ready(None) => {
|
|
||||||
error!("subscription terminated");
|
|
||||||
self.shutdown = true;
|
|
||||||
self.commands.close();
|
|
||||||
}
|
|
||||||
Async::NotReady => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.commands.poll().unwrap() {
|
|
||||||
Async::Ready(Some(command)) => {
|
|
||||||
progress = true;
|
|
||||||
self.handle_command(command);
|
|
||||||
}
|
|
||||||
Async::Ready(None) => (),
|
|
||||||
Async::NotReady => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.player_events.poll() {
|
|
||||||
Ok(Async::NotReady) => (),
|
|
||||||
Ok(Async::Ready(None)) => (),
|
|
||||||
Err(_) => (),
|
|
||||||
Ok(Async::Ready(Some(event))) => {
|
|
||||||
progress = true;
|
|
||||||
self.handle_player_event(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: Refactor
|
|
||||||
match self.context_fut.poll() {
|
|
||||||
Ok(Async::Ready(value)) => {
|
|
||||||
let r_context = serde_json::from_value::<StationContext>(value.clone());
|
|
||||||
self.context = match r_context {
|
|
||||||
Ok(context) => {
|
|
||||||
info!(
|
|
||||||
"Resolved {:?} tracks from <{:?}>",
|
|
||||||
context.tracks.len(),
|
|
||||||
self.state.get_context_uri(),
|
|
||||||
);
|
|
||||||
Some(context)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Unable to parse JSONContext {:?}\n{:?}", e, value);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// It needn't be so verbose - can be as simple as
|
|
||||||
// if let Some(ref context) = r_context {
|
|
||||||
// info!("Got {:?} tracks from <{}>", context.tracks.len(), context.uri);
|
|
||||||
// }
|
|
||||||
// self.context = r_context;
|
|
||||||
|
|
||||||
progress = true;
|
|
||||||
self.context_fut = Box::new(future::empty());
|
|
||||||
}
|
|
||||||
Ok(Async::NotReady) => (),
|
|
||||||
Err(err) => {
|
|
||||||
self.context_fut = Box::new(future::empty());
|
|
||||||
error!("ContextError: {:?}", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.autoplay_fut.poll() {
|
|
||||||
Ok(Async::Ready(autoplay_station_uri)) => {
|
|
||||||
info!("Autoplay uri resolved to <{:?}>", autoplay_station_uri);
|
|
||||||
self.context_fut = self.resolve_station(&autoplay_station_uri);
|
|
||||||
progress = true;
|
|
||||||
self.autoplay_fut = Box::new(future::empty());
|
|
||||||
}
|
|
||||||
Ok(Async::NotReady) => (),
|
|
||||||
Err(err) => {
|
|
||||||
self.autoplay_fut = Box::new(future::empty());
|
|
||||||
error!("AutoplayError: {:?}", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let poll_sender = self.sender.poll_complete().unwrap();
|
|
||||||
|
|
||||||
// Only shutdown once we've flushed out all our messages
|
|
||||||
if self.shutdown && poll_sender.is_ready() {
|
|
||||||
return Ok(Async::Ready(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !progress {
|
|
||||||
return Ok(Async::NotReady);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpircTask {
|
impl SpircTask {
|
||||||
|
async fn run(mut self) {
|
||||||
|
while !self.session.is_invalid() && !self.shutdown {
|
||||||
|
let commands = self.commands.as_mut();
|
||||||
|
let player_events = self.player_events.as_mut();
|
||||||
|
tokio::select! {
|
||||||
|
frame = self.subscription.next() => match frame {
|
||||||
|
Some(frame) => self.handle_frame(frame),
|
||||||
|
None => {
|
||||||
|
error!("subscription terminated");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cmd = async { commands.unwrap().recv().await }, if commands.is_some() => if let Some(cmd) = cmd {
|
||||||
|
self.handle_command(cmd);
|
||||||
|
},
|
||||||
|
event = async { player_events.unwrap().recv().await }, if player_events.is_some() => if let Some(event) = event {
|
||||||
|
self.handle_player_event(event)
|
||||||
|
},
|
||||||
|
result = self.sender.flush(), if !self.sender.is_flushed() => if result.is_err() {
|
||||||
|
error!("Cannot flush spirc event sender.");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
context = &mut self.context_fut, if !self.context_fut.is_terminated() => {
|
||||||
|
match context {
|
||||||
|
Ok(value) => {
|
||||||
|
let r_context = serde_json::from_value::<StationContext>(value);
|
||||||
|
self.context = match r_context {
|
||||||
|
Ok(context) => {
|
||||||
|
info!(
|
||||||
|
"Resolved {:?} tracks from <{:?}>",
|
||||||
|
context.tracks.len(),
|
||||||
|
self.state.get_context_uri(),
|
||||||
|
);
|
||||||
|
Some(context)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Unable to parse JSONContext {:?}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// It needn't be so verbose - can be as simple as
|
||||||
|
// if let Some(ref context) = r_context {
|
||||||
|
// info!("Got {:?} tracks from <{}>", context.tracks.len(), context.uri);
|
||||||
|
// }
|
||||||
|
// self.context = r_context;
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("ContextError: {:?}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
autoplay = &mut self.autoplay_fut, if !self.autoplay_fut.is_terminated() => {
|
||||||
|
match autoplay {
|
||||||
|
Ok(autoplay_station_uri) => {
|
||||||
|
info!("Autoplay uri resolved to <{:?}>", autoplay_station_uri);
|
||||||
|
self.context_fut = self.resolve_station(&autoplay_station_uri);
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("AutoplayError: {:?}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.sender.flush().await.is_err() {
|
||||||
|
warn!("Cannot flush spirc event sender.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn now_ms(&mut self) -> i64 {
|
fn now_ms(&mut self) -> i64 {
|
||||||
let dur = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
let dur = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||||
Ok(dur) => dur,
|
Ok(dur) => dur,
|
||||||
Err(err) => err.duration(),
|
Err(err) => err.duration(),
|
||||||
};
|
};
|
||||||
(dur.as_secs() as i64 + self.session.time_delta()) * 1000
|
|
||||||
+ (dur.subsec_nanos() / 1000_000) as i64
|
dur.as_millis() as i64 + 1000 * self.session.time_delta()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_mixer_started(&mut self) {
|
fn ensure_mixer_started(&mut self) {
|
||||||
|
@ -545,7 +515,9 @@ impl SpircTask {
|
||||||
SpircCommand::Shutdown => {
|
SpircCommand::Shutdown => {
|
||||||
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send();
|
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send();
|
||||||
self.shutdown = true;
|
self.shutdown = true;
|
||||||
self.commands.close();
|
if let Some(rx) = self.commands.as_mut() {
|
||||||
|
rx.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -653,7 +625,7 @@ impl SpircTask {
|
||||||
);
|
);
|
||||||
|
|
||||||
if frame.get_ident() == self.ident
|
if frame.get_ident() == self.ident
|
||||||
|| (frame.get_recipient().len() > 0 && !frame.get_recipient().contains(&self.ident))
|
|| (!frame.get_recipient().is_empty() && !frame.get_recipient().contains(&self.ident))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -672,7 +644,7 @@ impl SpircTask {
|
||||||
|
|
||||||
self.update_tracks(&frame);
|
self.update_tracks(&frame);
|
||||||
|
|
||||||
if self.state.get_track().len() > 0 {
|
if !self.state.get_track().is_empty() {
|
||||||
let start_playing =
|
let start_playing =
|
||||||
frame.get_state().get_status() == PlayStatus::kPlayStatusPlay;
|
frame.get_state().get_status() == PlayStatus::kPlayStatusPlay;
|
||||||
self.load_track(start_playing, frame.get_state().get_position_ms());
|
self.load_track(start_playing, frame.get_state().get_position_ms());
|
||||||
|
@ -895,7 +867,7 @@ impl SpircTask {
|
||||||
|
|
||||||
fn preview_next_track(&mut self) -> Option<SpotifyId> {
|
fn preview_next_track(&mut self) -> Option<SpotifyId> {
|
||||||
self.get_track_id_to_play_from_playlist(self.state.get_playing_track_index() + 1)
|
self.get_track_id_to_play_from_playlist(self.state.get_playing_track_index() + 1)
|
||||||
.and_then(|(track_id, _)| Some(track_id))
|
.map(|(track_id, _)| track_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_preload_next_track(&mut self) {
|
fn handle_preload_next_track(&mut self) {
|
||||||
|
@ -1014,7 +986,7 @@ impl SpircTask {
|
||||||
};
|
};
|
||||||
// Reinsert queued tracks after the new playing track.
|
// Reinsert queued tracks after the new playing track.
|
||||||
let mut pos = (new_index + 1) as usize;
|
let mut pos = (new_index + 1) as usize;
|
||||||
for track in queue_tracks.into_iter() {
|
for track in queue_tracks {
|
||||||
self.state.mut_track().insert(pos, track);
|
self.state.mut_track().insert(pos, track);
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
@ -1060,52 +1032,53 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_station(
|
fn resolve_station(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, MercuryError>> {
|
||||||
&self,
|
|
||||||
uri: &str,
|
|
||||||
) -> Box<dyn Future<Item = serde_json::Value, Error = MercuryError>> {
|
|
||||||
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(
|
fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture<Result<String, MercuryError>> {
|
||||||
&self,
|
|
||||||
uri: &str,
|
|
||||||
) -> Box<dyn Future<Item = String, Error = MercuryError>> {
|
|
||||||
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::new(request.and_then(move |response| {
|
Box::pin(
|
||||||
if response.status_code == 200 {
|
async {
|
||||||
|
let response = request.await?;
|
||||||
|
|
||||||
|
if response.status_code == 200 {
|
||||||
|
let data = response
|
||||||
|
.payload
|
||||||
|
.first()
|
||||||
|
.expect("Empty autoplay uri")
|
||||||
|
.to_vec();
|
||||||
|
let autoplay_uri = String::from_utf8(data).unwrap();
|
||||||
|
Ok(autoplay_uri)
|
||||||
|
} else {
|
||||||
|
warn!("No autoplay_uri found");
|
||||||
|
Err(MercuryError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fuse(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_uri(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, MercuryError>> {
|
||||||
|
let request = self.session.mercury().get(uri);
|
||||||
|
|
||||||
|
Box::pin(
|
||||||
|
async move {
|
||||||
|
let response = request.await?;
|
||||||
|
|
||||||
let data = response
|
let data = response
|
||||||
.payload
|
.payload
|
||||||
.first()
|
.first()
|
||||||
.expect("Empty autoplay uri")
|
.expect("Empty payload on context uri");
|
||||||
.to_vec();
|
let response: serde_json::Value = serde_json::from_slice(&data).unwrap();
|
||||||
let autoplay_uri = String::from_utf8(data).unwrap();
|
|
||||||
Ok(autoplay_uri)
|
Ok(response)
|
||||||
} else {
|
|
||||||
warn!("No autoplay_uri found");
|
|
||||||
Err(MercuryError)
|
|
||||||
}
|
}
|
||||||
}))
|
.fuse(),
|
||||||
}
|
)
|
||||||
|
|
||||||
fn resolve_uri(
|
|
||||||
&self,
|
|
||||||
uri: &str,
|
|
||||||
) -> Box<dyn Future<Item = serde_json::Value, Error = MercuryError>> {
|
|
||||||
let request = self.session.mercury().get(uri);
|
|
||||||
|
|
||||||
Box::new(request.and_then(move |response| {
|
|
||||||
let data = response
|
|
||||||
.payload
|
|
||||||
.first()
|
|
||||||
.expect("Empty payload on context uri");
|
|
||||||
let response: serde_json::Value = serde_json::from_slice(&data).unwrap();
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_tracks_from_context(&mut self) {
|
fn update_tracks_from_context(&mut self) {
|
||||||
|
@ -1152,7 +1125,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state.set_playing_track_index(index);
|
self.state.set_playing_track_index(index);
|
||||||
self.state.set_track(tracks.into_iter().cloned().collect());
|
self.state.set_track(tracks.iter().cloned().collect());
|
||||||
self.state.set_context_uri(context_uri);
|
self.state.set_context_uri(context_uri);
|
||||||
// has_shuffle/repeat seem to always be true in these replace msgs,
|
// has_shuffle/repeat seem to always be true in these replace msgs,
|
||||||
// but to replicate the behaviour of the Android client we have to
|
// but to replicate the behaviour of the Android client we have to
|
||||||
|
@ -1323,10 +1296,7 @@ impl<'a> CommandSender<'a> {
|
||||||
frame.set_typ(cmd);
|
frame.set_typ(cmd);
|
||||||
frame.set_device_state(spirc.device.clone());
|
frame.set_device_state(spirc.device.clone());
|
||||||
frame.set_state_update_id(spirc.now_ms());
|
frame.set_state_update_id(spirc.now_ms());
|
||||||
CommandSender {
|
CommandSender { spirc, frame }
|
||||||
spirc: spirc,
|
|
||||||
frame: frame,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recipient(mut self, recipient: &'a str) -> CommandSender {
|
fn recipient(mut self, recipient: &'a str) -> CommandSender {
|
||||||
|
@ -1345,7 +1315,6 @@ impl<'a> CommandSender<'a> {
|
||||||
self.frame.set_state(self.spirc.state.clone());
|
self.frame.set_state(self.spirc.state.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let send = self.spirc.sender.start_send(self.frame).unwrap();
|
self.spirc.sender.send(self.frame.write_to_bytes().unwrap());
|
||||||
assert!(send.is_ready());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,33 +15,42 @@ version = "0.1.6"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes = "0.6"
|
aes = "0.6"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
byteorder = "1.3"
|
byteorder = "1.4"
|
||||||
bytes = "0.4"
|
bytes = "1.0"
|
||||||
error-chain = { version = "0.12", default_features = false }
|
form_urlencoded = "1.0"
|
||||||
futures = "0.1"
|
futures-core = { version = "0.3", default-features = false }
|
||||||
|
futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "unstable", "sink"] }
|
||||||
hmac = "0.10"
|
hmac = "0.10"
|
||||||
httparse = "1.3"
|
httparse = "1.3"
|
||||||
hyper = "0.11"
|
http = "0.2"
|
||||||
hyper-proxy = { version = "0.4", default_features = false }
|
hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] }
|
||||||
lazy_static = "1.3"
|
hyper-proxy = { version = "0.9.1", optional = true, default-features = false }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
num-bigint = "0.3"
|
num-bigint = { version = "0.4", features = ["rand"] }
|
||||||
num-integer = "0.1"
|
num-integer = "0.1"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
pbkdf2 = { version = "0.7", default_features = false, features = ["hmac"] }
|
once_cell = "1.5.2"
|
||||||
|
pbkdf2 = { version = "0.7", default-features = false, features = ["hmac"] }
|
||||||
protobuf = "~2.14.0"
|
protobuf = "~2.14.0"
|
||||||
rand = "0.7"
|
rand = "0.8"
|
||||||
serde = "1.0"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_derive = "1.0"
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
sha-1 = "0.9"
|
sha-1 = "0.9"
|
||||||
shannon = "0.2.0"
|
shannon = "0.2.0"
|
||||||
tokio-codec = "0.1"
|
thiserror = "1"
|
||||||
tokio-core = "0.1"
|
tokio = { version = "1.0", features = ["io-util", "net", "rt", "sync"] }
|
||||||
tokio-io = "0.1"
|
tokio-stream = "0.1"
|
||||||
url = "1.7"
|
tokio-util = { version = "0.6", features = ["codec"] }
|
||||||
uuid = { version = "0.8", features = ["v4"] }
|
url = "2.1"
|
||||||
|
uuid = { version = "0.8", default-features = false, features = ["v4"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
rand = "0.7"
|
rand = "0.8"
|
||||||
vergen = "3.0.4"
|
vergen = "3.0.4"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "*"
|
||||||
|
tokio = {version = "1.0", features = ["macros"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
apresolve = ["hyper", "hyper-proxy"]
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
extern crate rand;
|
|
||||||
extern crate vergen;
|
|
||||||
|
|
||||||
use rand::distributions::Alphanumeric;
|
use rand::distributions::Alphanumeric;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use vergen::{generate_cargo_keys, ConstantsFlags};
|
use vergen::{generate_cargo_keys, ConstantsFlags};
|
||||||
|
@ -10,10 +7,10 @@ fn main() {
|
||||||
flags.toggle(ConstantsFlags::REBUILD_ON_HEAD_CHANGE);
|
flags.toggle(ConstantsFlags::REBUILD_ON_HEAD_CHANGE);
|
||||||
generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!");
|
generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!");
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
let build_id: String = rand::thread_rng()
|
||||||
let build_id: String = ::std::iter::repeat(())
|
.sample_iter(Alphanumeric)
|
||||||
.map(|()| rng.sample(Alphanumeric))
|
|
||||||
.take(8)
|
.take(8)
|
||||||
|
.map(char::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id);
|
println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id);
|
||||||
|
|
|
@ -1,101 +1,91 @@
|
||||||
const AP_FALLBACK: &'static str = "ap.spotify.com:443";
|
use std::error::Error;
|
||||||
const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com/";
|
|
||||||
|
|
||||||
use futures::{Future, Stream};
|
|
||||||
use hyper::client::HttpConnector;
|
use hyper::client::HttpConnector;
|
||||||
use hyper::{self, Client, Method, Request, Uri};
|
use hyper::{Body, Client, Method, Request, Uri};
|
||||||
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
|
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
|
||||||
use serde_json;
|
use serde::Deserialize;
|
||||||
use std::str::FromStr;
|
|
||||||
use tokio_core::reactor::Handle;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
error_chain! {}
|
use super::AP_FALLBACK;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80";
|
||||||
pub struct APResolveData {
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
struct ApResolveData {
|
||||||
ap_list: Vec<String>,
|
ap_list: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apresolve(
|
async fn try_apresolve(
|
||||||
handle: &Handle,
|
proxy: Option<&Url>,
|
||||||
proxy: &Option<Url>,
|
ap_port: Option<u16>,
|
||||||
ap_port: &Option<u16>,
|
) -> Result<String, Box<dyn Error>> {
|
||||||
) -> Box<dyn Future<Item = String, Error = Error>> {
|
let port = ap_port.unwrap_or(443);
|
||||||
let url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL");
|
|
||||||
let use_proxy = proxy.is_some();
|
|
||||||
|
|
||||||
let mut req = Request::new(Method::Get, url.clone());
|
let mut req = Request::new(Body::empty());
|
||||||
let response = match *proxy {
|
*req.method_mut() = Method::GET;
|
||||||
Some(ref val) => {
|
// panic safety: APRESOLVE_ENDPOINT above is valid url.
|
||||||
let proxy_url = Uri::from_str(val.as_str()).expect("invalid http proxy");
|
*req.uri_mut() = APRESOLVE_ENDPOINT.parse().expect("invalid AP resolve URL");
|
||||||
let proxy = Proxy::new(Intercept::All, proxy_url);
|
|
||||||
let connector = HttpConnector::new(4, handle);
|
let response = if let Some(url) = proxy {
|
||||||
let proxy_connector = ProxyConnector::from_proxy_unsecured(connector, proxy);
|
// Panic safety: all URLs are valid URIs
|
||||||
if let Some(headers) = proxy_connector.http_headers(&url) {
|
let uri = url.to_string().parse().unwrap();
|
||||||
req.headers_mut().extend(headers.iter());
|
let proxy = Proxy::new(Intercept::All, uri);
|
||||||
req.set_proxy(true);
|
let connector = HttpConnector::new();
|
||||||
}
|
let proxy_connector = ProxyConnector::from_proxy_unsecured(connector, proxy);
|
||||||
let client = Client::configure().connector(proxy_connector).build(handle);
|
Client::builder()
|
||||||
client.request(req)
|
.build(proxy_connector)
|
||||||
}
|
.request(req)
|
||||||
_ => {
|
.await?
|
||||||
let client = Client::new(handle);
|
} else {
|
||||||
client.request(req)
|
Client::new().request(req).await?
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = response.and_then(|response| {
|
let body = hyper::body::to_bytes(response.into_body()).await?;
|
||||||
response.body().fold(Vec::new(), |mut acc, chunk| {
|
let data: ApResolveData = serde_json::from_slice(body.as_ref())?;
|
||||||
acc.extend_from_slice(chunk.as_ref());
|
|
||||||
Ok::<_, hyper::Error>(acc)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let body = body.then(|result| result.chain_err(|| "HTTP error"));
|
|
||||||
let body =
|
|
||||||
body.and_then(|body| String::from_utf8(body).chain_err(|| "invalid UTF8 in response"));
|
|
||||||
|
|
||||||
let data = body
|
let ap = if ap_port.is_some() || proxy.is_some() {
|
||||||
.and_then(|body| serde_json::from_str::<APResolveData>(&body).chain_err(|| "invalid JSON"));
|
data.ap_list.into_iter().find_map(|ap| {
|
||||||
|
if ap.parse::<Uri>().ok()?.port()? == port {
|
||||||
let p = ap_port.clone();
|
Some(ap)
|
||||||
|
|
||||||
let ap = data.and_then(move |data| {
|
|
||||||
let mut aps = data.ap_list.iter().filter(|ap| {
|
|
||||||
if p.is_some() {
|
|
||||||
Uri::from_str(ap).ok().map_or(false, |uri| {
|
|
||||||
uri.port().map_or(false, |port| port == p.unwrap())
|
|
||||||
})
|
|
||||||
} else if use_proxy {
|
|
||||||
// It is unlikely that the proxy will accept CONNECT on anything other than 443.
|
|
||||||
Uri::from_str(ap)
|
|
||||||
.ok()
|
|
||||||
.map_or(false, |uri| uri.port().map_or(false, |port| port == 443))
|
|
||||||
} else {
|
} else {
|
||||||
true
|
None
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
} else {
|
||||||
|
data.ap_list.into_iter().next()
|
||||||
|
}
|
||||||
|
.ok_or("empty AP List")?;
|
||||||
|
|
||||||
let ap = aps.next().ok_or("empty AP List")?;
|
Ok(ap)
|
||||||
Ok(ap.clone())
|
|
||||||
});
|
|
||||||
|
|
||||||
Box::new(ap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn apresolve_or_fallback<E>(
|
pub async fn apresolve(proxy: Option<&Url>, ap_port: Option<u16>) -> String {
|
||||||
handle: &Handle,
|
try_apresolve(proxy, ap_port).await.unwrap_or_else(|e| {
|
||||||
proxy: &Option<Url>,
|
warn!("Failed to resolve Access Point: {}", e);
|
||||||
ap_port: &Option<u16>,
|
|
||||||
) -> Box<dyn Future<Item = String, Error = E>>
|
|
||||||
where
|
|
||||||
E: 'static,
|
|
||||||
{
|
|
||||||
let ap = apresolve(handle, proxy, ap_port).or_else(|e| {
|
|
||||||
warn!("Failed to resolve Access Point: {}", e.description());
|
|
||||||
warn!("Using fallback \"{}\"", AP_FALLBACK);
|
warn!("Using fallback \"{}\"", AP_FALLBACK);
|
||||||
Ok(AP_FALLBACK.into())
|
AP_FALLBACK.into()
|
||||||
});
|
})
|
||||||
|
}
|
||||||
Box::new(ap)
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
|
||||||
|
use super::try_apresolve;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_apresolve() {
|
||||||
|
let ap = try_apresolve(None, None).await.unwrap();
|
||||||
|
|
||||||
|
// Assert that the result contains a valid host and port
|
||||||
|
ap.to_socket_addrs().unwrap().next().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_apresolve_port_443() {
|
||||||
|
let ap = try_apresolve(None, Some(443)).await.unwrap();
|
||||||
|
|
||||||
|
let port = ap.to_socket_addrs().unwrap().next().unwrap().port();
|
||||||
|
assert_eq!(port, 443);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::sync::oneshot;
|
|
||||||
use futures::{Async, Future, Poll};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
use crate::spotify_id::{FileId, SpotifyId};
|
use crate::spotify_id::{FileId, SpotifyId};
|
||||||
use crate::util::SeqGenerator;
|
use crate::util::SeqGenerator;
|
||||||
|
@ -47,7 +46,7 @@ impl AudioKeyManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request(&self, track: SpotifyId, file: FileId) -> AudioKeyFuture<AudioKey> {
|
pub async fn request(&self, track: SpotifyId, file: FileId) -> Result<AudioKey, AudioKeyError> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
let seq = self.lock(move |inner| {
|
let seq = self.lock(move |inner| {
|
||||||
|
@ -57,7 +56,7 @@ impl AudioKeyManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
self.send_key_request(seq, track, file);
|
self.send_key_request(seq, track, file);
|
||||||
AudioKeyFuture(rx)
|
rx.await.map_err(|_| AudioKeyError)?
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) {
|
fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) {
|
||||||
|
@ -70,18 +69,3 @@ impl AudioKeyManager {
|
||||||
self.session().send_packet(0xc, data)
|
self.session().send_packet(0xc, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AudioKeyFuture<T>(oneshot::Receiver<Result<T, AudioKeyError>>);
|
|
||||||
impl<T> Future for AudioKeyFuture<T> {
|
|
||||||
type Item = T;
|
|
||||||
type Error = AudioKeyError;
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<T, AudioKeyError> {
|
|
||||||
match self.0.poll() {
|
|
||||||
Ok(Async::Ready(Ok(value))) => Ok(Async::Ready(value)),
|
|
||||||
Ok(Async::Ready(Err(err))) => Err(err),
|
|
||||||
Ok(Async::NotReady) => Ok(Async::NotReady),
|
|
||||||
Err(oneshot::Canceled) => Err(AudioKeyError),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
|
use std::io::{self, Read};
|
||||||
|
|
||||||
use aes::Aes192;
|
use aes::Aes192;
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
use hmac::Hmac;
|
use hmac::Hmac;
|
||||||
use pbkdf2::pbkdf2;
|
use pbkdf2::pbkdf2;
|
||||||
use protobuf::ProtobufEnum;
|
use protobuf::ProtobufEnum;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use std::io::{self, Read};
|
|
||||||
|
|
||||||
use crate::protocol::authentication::AuthenticationType;
|
use crate::protocol::authentication::AuthenticationType;
|
||||||
use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
|
|
||||||
|
|
||||||
|
/// The credentials are used to log into the Spotify API.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
@ -24,11 +26,19 @@ pub struct Credentials {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Credentials {
|
impl Credentials {
|
||||||
pub fn with_password(username: String, password: String) -> Credentials {
|
/// Intialize these credentials from a username and a password.
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
/// ```rust
|
||||||
|
/// use librespot_core::authentication::Credentials;
|
||||||
|
///
|
||||||
|
/// let creds = Credentials::with_password("my account", "my password");
|
||||||
|
/// ```
|
||||||
|
pub fn with_password(username: impl Into<String>, password: impl Into<String>) -> Credentials {
|
||||||
Credentials {
|
Credentials {
|
||||||
username: username,
|
username: username.into(),
|
||||||
auth_type: AuthenticationType::AUTHENTICATION_USER_PASS,
|
auth_type: AuthenticationType::AUTHENTICATION_USER_PASS,
|
||||||
auth_data: password.into_bytes(),
|
auth_data: password.into().into_bytes(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,9 +112,9 @@ impl Credentials {
|
||||||
let auth_data = read_bytes(&mut cursor).unwrap();
|
let auth_data = read_bytes(&mut cursor).unwrap();
|
||||||
|
|
||||||
Credentials {
|
Credentials {
|
||||||
username: username,
|
username,
|
||||||
auth_type: auth_type,
|
auth_type,
|
||||||
auth_data: auth_data,
|
auth_data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,61 +151,3 @@ where
|
||||||
let v: String = serde::Deserialize::deserialize(de)?;
|
let v: String = serde::Deserialize::deserialize(de)?;
|
||||||
base64::decode(&v).map_err(|e| serde::de::Error::custom(e.to_string()))
|
base64::decode(&v).map_err(|e| serde::de::Error::custom(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_credentials<F: FnOnce(&String) -> String>(
|
|
||||||
username: Option<String>,
|
|
||||||
password: Option<String>,
|
|
||||||
cached_credentials: Option<Credentials>,
|
|
||||||
prompt: F,
|
|
||||||
) -> Option<Credentials> {
|
|
||||||
match (username, password, cached_credentials) {
|
|
||||||
(Some(username), Some(password), _) => Some(Credentials::with_password(username, password)),
|
|
||||||
|
|
||||||
(Some(ref username), _, Some(ref credentials)) if *username == credentials.username => {
|
|
||||||
Some(credentials.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
(Some(username), None, _) => Some(Credentials::with_password(
|
|
||||||
username.clone(),
|
|
||||||
prompt(&username),
|
|
||||||
)),
|
|
||||||
|
|
||||||
(None, _, Some(credentials)) => Some(credentials),
|
|
||||||
|
|
||||||
(None, _, None) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
error_chain! {
|
|
||||||
types {
|
|
||||||
AuthenticationError, AuthenticationErrorKind, AuthenticationResultExt, AuthenticationResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreign_links {
|
|
||||||
Io(::std::io::Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
errors {
|
|
||||||
BadCredentials {
|
|
||||||
description("Bad credentials")
|
|
||||||
display("Authentication failed with error: Bad credentials")
|
|
||||||
}
|
|
||||||
PremiumAccountRequired {
|
|
||||||
description("Premium account required")
|
|
||||||
display("Authentication failed with error: Premium account required")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<APLoginFailed> for AuthenticationError {
|
|
||||||
fn from(login_failure: APLoginFailed) -> Self {
|
|
||||||
let error_code = login_failure.get_error_code();
|
|
||||||
match error_code {
|
|
||||||
ErrorCode::BadCredentials => Self::from_kind(AuthenticationErrorKind::BadCredentials),
|
|
||||||
ErrorCode::PremiumAccountRequired => {
|
|
||||||
Self::from_kind(AuthenticationErrorKind::PremiumAccountRequired)
|
|
||||||
}
|
|
||||||
_ => format!("Authentication failed with error: {:?}", error_code).into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::sync::{mpsc, BiLock};
|
use futures_core::Stream;
|
||||||
use futures::{Async, Poll, Stream};
|
use futures_util::lock::BiLock;
|
||||||
use std::collections::HashMap;
|
use futures_util::StreamExt;
|
||||||
use std::time::Instant;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::util::SeqGenerator;
|
use crate::util::SeqGenerator;
|
||||||
|
|
||||||
|
@ -43,7 +48,7 @@ enum ChannelState {
|
||||||
|
|
||||||
impl ChannelManager {
|
impl ChannelManager {
|
||||||
pub fn allocate(&self) -> (u16, Channel) {
|
pub fn allocate(&self) -> (u16, Channel) {
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
let seq = self.lock(|inner| {
|
let seq = self.lock(|inner| {
|
||||||
let seq = inner.sequence.get();
|
let seq = inner.sequence.get();
|
||||||
|
@ -82,13 +87,13 @@ 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().unbounded_send((cmd, data));
|
let _ = entry.get().send((cmd, data));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_download_rate_estimate(&self) -> usize {
|
pub fn get_download_rate_estimate(&self) -> usize {
|
||||||
return self.lock(|inner| inner.download_rate_estimate);
|
self.lock(|inner| inner.download_rate_estimate)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn shutdown(&self) {
|
pub(crate) fn shutdown(&self) {
|
||||||
|
@ -101,12 +106,10 @@ impl ChannelManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
fn recv_packet(&mut self) -> Poll<Bytes, ChannelError> {
|
fn recv_packet(&mut self, cx: &mut Context<'_>) -> Poll<Result<Bytes, ChannelError>> {
|
||||||
let (cmd, packet) = match self.receiver.poll() {
|
let (cmd, packet) = match self.receiver.poll_recv(cx) {
|
||||||
Ok(Async::Ready(Some(t))) => t,
|
Poll::Pending => return Poll::Pending,
|
||||||
Ok(Async::Ready(None)) => return Err(ChannelError), // The channel has been closed.
|
Poll::Ready(o) => o.ok_or(ChannelError)?,
|
||||||
Ok(Async::NotReady) => return Ok(Async::NotReady),
|
|
||||||
Err(()) => unreachable!(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if cmd == 0xa {
|
if cmd == 0xa {
|
||||||
|
@ -115,9 +118,9 @@ impl Channel {
|
||||||
|
|
||||||
self.state = ChannelState::Closed;
|
self.state = ChannelState::Closed;
|
||||||
|
|
||||||
Err(ChannelError)
|
Poll::Ready(Err(ChannelError))
|
||||||
} else {
|
} else {
|
||||||
Ok(Async::Ready(packet))
|
Poll::Ready(Ok(packet))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,16 +132,19 @@ impl Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for Channel {
|
impl Stream for Channel {
|
||||||
type Item = ChannelEvent;
|
type Item = Result<ChannelEvent, ChannelError>;
|
||||||
type Error = ChannelError;
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
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 => panic!("Polling already terminated channel"),
|
||||||
ChannelState::Header(mut data) => {
|
ChannelState::Header(mut data) => {
|
||||||
if data.len() == 0 {
|
if data.is_empty() {
|
||||||
data = try_ready!(self.recv_packet());
|
data = match self.recv_packet(cx) {
|
||||||
|
Poll::Ready(Ok(x)) => x,
|
||||||
|
Poll::Ready(Err(x)) => return Poll::Ready(Some(Err(x))),
|
||||||
|
Poll::Pending => return Poll::Pending,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let length = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
|
let length = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
|
||||||
|
@ -152,19 +158,23 @@ impl Stream for Channel {
|
||||||
self.state = ChannelState::Header(data);
|
self.state = ChannelState::Header(data);
|
||||||
|
|
||||||
let event = ChannelEvent::Header(header_id, header_data);
|
let event = ChannelEvent::Header(header_id, header_data);
|
||||||
return Ok(Async::Ready(Some(event)));
|
return Poll::Ready(Some(Ok(event)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelState::Data => {
|
ChannelState::Data => {
|
||||||
let data = try_ready!(self.recv_packet());
|
let data = match self.recv_packet(cx) {
|
||||||
if data.len() == 0 {
|
Poll::Ready(Ok(x)) => x,
|
||||||
|
Poll::Ready(Err(x)) => return Poll::Ready(Some(Err(x))),
|
||||||
|
Poll::Pending => return Poll::Pending,
|
||||||
|
};
|
||||||
|
if data.is_empty() {
|
||||||
self.receiver.close();
|
self.receiver.close();
|
||||||
self.state = ChannelState::Closed;
|
self.state = ChannelState::Closed;
|
||||||
return Ok(Async::Ready(None));
|
return Poll::Ready(None);
|
||||||
} else {
|
} else {
|
||||||
let event = ChannelEvent::Data(data);
|
let event = ChannelEvent::Data(data);
|
||||||
return Ok(Async::Ready(Some(event)));
|
return Poll::Ready(Some(Ok(event)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,38 +183,46 @@ impl Stream for Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for ChannelData {
|
impl Stream for ChannelData {
|
||||||
type Item = Bytes;
|
type Item = Result<Bytes, ChannelError>;
|
||||||
type Error = ChannelError;
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
let mut channel = match self.0.poll_lock() {
|
let mut channel = match self.0.poll_lock(cx) {
|
||||||
Async::Ready(c) => c,
|
Poll::Ready(c) => c,
|
||||||
Async::NotReady => return Ok(Async::NotReady),
|
Poll::Pending => return Poll::Pending,
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match try_ready!(channel.poll()) {
|
let event = match channel.poll_next_unpin(cx) {
|
||||||
|
Poll::Ready(x) => x.transpose()?,
|
||||||
|
Poll::Pending => return Poll::Pending,
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
Some(ChannelEvent::Header(..)) => (),
|
Some(ChannelEvent::Header(..)) => (),
|
||||||
Some(ChannelEvent::Data(data)) => return Ok(Async::Ready(Some(data))),
|
Some(ChannelEvent::Data(data)) => return Poll::Ready(Some(Ok(data))),
|
||||||
None => return Ok(Async::Ready(None)),
|
None => return Poll::Ready(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for ChannelHeaders {
|
impl Stream for ChannelHeaders {
|
||||||
type Item = (u8, Vec<u8>);
|
type Item = Result<(u8, Vec<u8>), ChannelError>;
|
||||||
type Error = ChannelError;
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
let mut channel = match self.0.poll_lock() {
|
let mut channel = match self.0.poll_lock(cx) {
|
||||||
Async::Ready(c) => c,
|
Poll::Ready(c) => c,
|
||||||
Async::NotReady => return Ok(Async::NotReady),
|
Poll::Pending => return Poll::Pending,
|
||||||
};
|
};
|
||||||
|
|
||||||
match try_ready!(channel.poll()) {
|
let event = match channel.poll_next_unpin(cx) {
|
||||||
Some(ChannelEvent::Header(id, data)) => Ok(Async::Ready(Some((id, data)))),
|
Poll::Ready(x) => x.transpose()?,
|
||||||
Some(ChannelEvent::Data(..)) | None => Ok(Async::Ready(None)),
|
Poll::Pending => return Poll::Pending,
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Some(ChannelEvent::Header(id, data)) => Poll::Ready(Some(Ok((id, data)))),
|
||||||
|
Some(ChannelEvent::Data(..)) | None => Poll::Ready(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,29 +35,3 @@ macro_rules! component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::cell::UnsafeCell;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
pub(crate) struct Lazy<T>(Mutex<bool>, UnsafeCell<Option<T>>);
|
|
||||||
unsafe impl<T: Sync> Sync for Lazy<T> {}
|
|
||||||
unsafe impl<T: Send> Send for Lazy<T> {}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(mutex_atomic))]
|
|
||||||
impl<T> Lazy<T> {
|
|
||||||
pub(crate) fn new() -> Lazy<T> {
|
|
||||||
Lazy(Mutex::new(false), UnsafeCell::new(None))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get<F: FnOnce() -> T>(&self, f: F) -> &T {
|
|
||||||
let mut inner = self.0.lock().unwrap();
|
|
||||||
if !*inner {
|
|
||||||
unsafe {
|
|
||||||
*self.1.get() = Some(f());
|
|
||||||
}
|
|
||||||
*inner = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe { &*self.1.get() }.as_ref().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::version;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct SessionConfig {
|
pub struct SessionConfig {
|
||||||
|
@ -15,10 +12,10 @@ pub struct SessionConfig {
|
||||||
|
|
||||||
impl Default for SessionConfig {
|
impl Default for SessionConfig {
|
||||||
fn default() -> SessionConfig {
|
fn default() -> SessionConfig {
|
||||||
let device_id = Uuid::new_v4().to_hyphenated().to_string();
|
let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string();
|
||||||
SessionConfig {
|
SessionConfig {
|
||||||
user_agent: version::VERSION_STRING.to_string(),
|
user_agent: crate::version::VERSION_STRING.to_string(),
|
||||||
device_id: device_id,
|
device_id,
|
||||||
proxy: None,
|
proxy: None,
|
||||||
ap_port: None,
|
ap_port: None,
|
||||||
}
|
}
|
||||||
|
@ -32,9 +29,9 @@ pub enum DeviceType {
|
||||||
Tablet = 2,
|
Tablet = 2,
|
||||||
Smartphone = 3,
|
Smartphone = 3,
|
||||||
Speaker = 4,
|
Speaker = 4,
|
||||||
TV = 5,
|
Tv = 5,
|
||||||
AVR = 6,
|
Avr = 6,
|
||||||
STB = 7,
|
Stb = 7,
|
||||||
AudioDongle = 8,
|
AudioDongle = 8,
|
||||||
GameConsole = 9,
|
GameConsole = 9,
|
||||||
CastAudio = 10,
|
CastAudio = 10,
|
||||||
|
@ -57,9 +54,9 @@ impl FromStr for DeviceType {
|
||||||
"tablet" => Ok(Tablet),
|
"tablet" => Ok(Tablet),
|
||||||
"smartphone" => Ok(Smartphone),
|
"smartphone" => Ok(Smartphone),
|
||||||
"speaker" => Ok(Speaker),
|
"speaker" => Ok(Speaker),
|
||||||
"tv" => Ok(TV),
|
"tv" => Ok(Tv),
|
||||||
"avr" => Ok(AVR),
|
"avr" => Ok(Avr),
|
||||||
"stb" => Ok(STB),
|
"stb" => Ok(Stb),
|
||||||
"audiodongle" => Ok(AudioDongle),
|
"audiodongle" => Ok(AudioDongle),
|
||||||
"gameconsole" => Ok(GameConsole),
|
"gameconsole" => Ok(GameConsole),
|
||||||
"castaudio" => Ok(CastAudio),
|
"castaudio" => Ok(CastAudio),
|
||||||
|
@ -83,9 +80,9 @@ impl fmt::Display for DeviceType {
|
||||||
Tablet => f.write_str("Tablet"),
|
Tablet => f.write_str("Tablet"),
|
||||||
Smartphone => f.write_str("Smartphone"),
|
Smartphone => f.write_str("Smartphone"),
|
||||||
Speaker => f.write_str("Speaker"),
|
Speaker => f.write_str("Speaker"),
|
||||||
TV => f.write_str("TV"),
|
Tv => f.write_str("TV"),
|
||||||
AVR => f.write_str("AVR"),
|
Avr => f.write_str("AVR"),
|
||||||
STB => f.write_str("STB"),
|
Stb => f.write_str("STB"),
|
||||||
AudioDongle => f.write_str("AudioDongle"),
|
AudioDongle => f.write_str("AudioDongle"),
|
||||||
GameConsole => f.write_str("GameConsole"),
|
GameConsole => f.write_str("GameConsole"),
|
||||||
CastAudio => f.write_str("CastAudio"),
|
CastAudio => f.write_str("CastAudio"),
|
||||||
|
|
|
@ -2,7 +2,7 @@ use byteorder::{BigEndian, ByteOrder};
|
||||||
use bytes::{BufMut, Bytes, BytesMut};
|
use bytes::{BufMut, Bytes, BytesMut};
|
||||||
use shannon::Shannon;
|
use shannon::Shannon;
|
||||||
use std::io;
|
use std::io;
|
||||||
use tokio_io::codec::{Decoder, Encoder};
|
use tokio_util::codec::{Decoder, Encoder};
|
||||||
|
|
||||||
const HEADER_SIZE: usize = 3;
|
const HEADER_SIZE: usize = 3;
|
||||||
const MAC_SIZE: usize = 4;
|
const MAC_SIZE: usize = 4;
|
||||||
|
@ -13,7 +13,7 @@ enum DecodeState {
|
||||||
Payload(u8, usize),
|
Payload(u8, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct APCodec {
|
pub struct ApCodec {
|
||||||
encode_nonce: u32,
|
encode_nonce: u32,
|
||||||
encode_cipher: Shannon,
|
encode_cipher: Shannon,
|
||||||
|
|
||||||
|
@ -22,9 +22,9 @@ pub struct APCodec {
|
||||||
decode_state: DecodeState,
|
decode_state: DecodeState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl APCodec {
|
impl ApCodec {
|
||||||
pub fn new(send_key: &[u8], recv_key: &[u8]) -> APCodec {
|
pub fn new(send_key: &[u8], recv_key: &[u8]) -> ApCodec {
|
||||||
APCodec {
|
ApCodec {
|
||||||
encode_nonce: 0,
|
encode_nonce: 0,
|
||||||
encode_cipher: Shannon::new(send_key),
|
encode_cipher: Shannon::new(send_key),
|
||||||
|
|
||||||
|
@ -35,8 +35,7 @@ impl APCodec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Encoder for APCodec {
|
impl Encoder<(u8, Vec<u8>)> for ApCodec {
|
||||||
type Item = (u8, Vec<u8>);
|
|
||||||
type Error = io::Error;
|
type Error = io::Error;
|
||||||
|
|
||||||
fn encode(&mut self, item: (u8, Vec<u8>), buf: &mut BytesMut) -> io::Result<()> {
|
fn encode(&mut self, item: (u8, Vec<u8>), buf: &mut BytesMut) -> io::Result<()> {
|
||||||
|
@ -45,7 +44,7 @@ impl Encoder for APCodec {
|
||||||
|
|
||||||
buf.reserve(3 + payload.len());
|
buf.reserve(3 + payload.len());
|
||||||
buf.put_u8(cmd);
|
buf.put_u8(cmd);
|
||||||
buf.put_u16_be(payload.len() as u16);
|
buf.put_u16(payload.len() as u16);
|
||||||
buf.extend_from_slice(&payload);
|
buf.extend_from_slice(&payload);
|
||||||
|
|
||||||
self.encode_cipher.nonce_u32(self.encode_nonce);
|
self.encode_cipher.nonce_u32(self.encode_nonce);
|
||||||
|
@ -61,7 +60,7 @@ impl Encoder for APCodec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Decoder for APCodec {
|
impl Decoder for ApCodec {
|
||||||
type Item = (u8, Bytes);
|
type Item = (u8, Bytes);
|
||||||
type Error = io::Error;
|
type Error = io::Error;
|
||||||
|
|
||||||
|
|
|
@ -1,87 +1,47 @@
|
||||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||||
use futures::{Async, Future, Poll};
|
|
||||||
use hmac::{Hmac, Mac, NewMac};
|
use hmac::{Hmac, Mac, NewMac};
|
||||||
use protobuf::{self, Message};
|
use protobuf::{self, Message};
|
||||||
use rand::thread_rng;
|
use rand::{thread_rng, RngCore};
|
||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
use std::io::{self, Read};
|
use std::io;
|
||||||
use std::marker::PhantomData;
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
use tokio_codec::{Decoder, Framed};
|
use tokio_util::codec::{Decoder, Framed};
|
||||||
use tokio_io::io::{read_exact, write_all, ReadExact, Window, WriteAll};
|
|
||||||
use tokio_io::{AsyncRead, AsyncWrite};
|
|
||||||
|
|
||||||
use super::codec::APCodec;
|
use super::codec::ApCodec;
|
||||||
use crate::diffie_hellman::DHLocalKeys;
|
use crate::diffie_hellman::DhLocalKeys;
|
||||||
use crate::protocol;
|
use crate::protocol;
|
||||||
use crate::protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext};
|
use crate::protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext};
|
||||||
use crate::util;
|
|
||||||
|
|
||||||
pub struct Handshake<T> {
|
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
||||||
keys: DHLocalKeys,
|
mut connection: T,
|
||||||
state: HandshakeState<T>,
|
) -> io::Result<Framed<T, ApCodec>> {
|
||||||
|
let local_keys = DhLocalKeys::random(&mut thread_rng());
|
||||||
|
let gc = local_keys.public_key();
|
||||||
|
let mut accumulator = client_hello(&mut connection, gc).await?;
|
||||||
|
let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?;
|
||||||
|
let remote_key = message
|
||||||
|
.get_challenge()
|
||||||
|
.get_login_crypto_challenge()
|
||||||
|
.get_diffie_hellman()
|
||||||
|
.get_gs()
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let shared_secret = local_keys.shared_secret(&remote_key);
|
||||||
|
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator);
|
||||||
|
let codec = ApCodec::new(&send_key, &recv_key);
|
||||||
|
|
||||||
|
client_response(&mut connection, challenge).await?;
|
||||||
|
|
||||||
|
Ok(codec.framed(connection))
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HandshakeState<T> {
|
async fn client_hello<T>(connection: &mut T, gc: Vec<u8>) -> io::Result<Vec<u8>>
|
||||||
ClientHello(WriteAll<T, Vec<u8>>),
|
where
|
||||||
APResponse(RecvPacket<T, APResponseMessage>),
|
T: AsyncWrite + Unpin,
|
||||||
ClientResponse(Option<APCodec>, WriteAll<T, Vec<u8>>),
|
{
|
||||||
}
|
let mut client_nonce = vec![0; 0x10];
|
||||||
|
thread_rng().fill_bytes(&mut client_nonce);
|
||||||
|
|
||||||
pub fn handshake<T: AsyncRead + AsyncWrite>(connection: T) -> Handshake<T> {
|
|
||||||
let local_keys = DHLocalKeys::random(&mut thread_rng());
|
|
||||||
let client_hello = client_hello(connection, local_keys.public_key());
|
|
||||||
|
|
||||||
Handshake {
|
|
||||||
keys: local_keys,
|
|
||||||
state: HandshakeState::ClientHello(client_hello),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsyncRead + AsyncWrite> Future for Handshake<T> {
|
|
||||||
type Item = Framed<T, APCodec>;
|
|
||||||
type Error = io::Error;
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Self::Item, io::Error> {
|
|
||||||
use self::HandshakeState::*;
|
|
||||||
loop {
|
|
||||||
self.state = match self.state {
|
|
||||||
ClientHello(ref mut write) => {
|
|
||||||
let (connection, accumulator) = try_ready!(write.poll());
|
|
||||||
|
|
||||||
let read = recv_packet(connection, accumulator);
|
|
||||||
APResponse(read)
|
|
||||||
}
|
|
||||||
|
|
||||||
APResponse(ref mut read) => {
|
|
||||||
let (connection, message, accumulator) = try_ready!(read.poll());
|
|
||||||
let remote_key = message
|
|
||||||
.get_challenge()
|
|
||||||
.get_login_crypto_challenge()
|
|
||||||
.get_diffie_hellman()
|
|
||||||
.get_gs()
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
let shared_secret = self.keys.shared_secret(&remote_key);
|
|
||||||
let (challenge, send_key, recv_key) =
|
|
||||||
compute_keys(&shared_secret, &accumulator);
|
|
||||||
let codec = APCodec::new(&send_key, &recv_key);
|
|
||||||
|
|
||||||
let write = client_response(connection, challenge);
|
|
||||||
ClientResponse(Some(codec), write)
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientResponse(ref mut codec, ref mut write) => {
|
|
||||||
let (connection, _) = try_ready!(write.poll());
|
|
||||||
let codec = codec.take().unwrap();
|
|
||||||
let framed = codec.framed(connection);
|
|
||||||
return Ok(Async::Ready(framed));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn client_hello<T: AsyncWrite>(connection: T, gc: Vec<u8>) -> WriteAll<T, Vec<u8>> {
|
|
||||||
let mut packet = ClientHello::new();
|
let mut packet = ClientHello::new();
|
||||||
packet
|
packet
|
||||||
.mut_build_info()
|
.mut_build_info()
|
||||||
|
@ -101,18 +61,22 @@ fn client_hello<T: AsyncWrite>(connection: T, gc: Vec<u8>) -> WriteAll<T, Vec<u8
|
||||||
.mut_login_crypto_hello()
|
.mut_login_crypto_hello()
|
||||||
.mut_diffie_hellman()
|
.mut_diffie_hellman()
|
||||||
.set_server_keys_known(1);
|
.set_server_keys_known(1);
|
||||||
packet.set_client_nonce(util::rand_vec(&mut thread_rng(), 0x10));
|
packet.set_client_nonce(client_nonce);
|
||||||
packet.set_padding(vec![0x1e]);
|
packet.set_padding(vec![0x1e]);
|
||||||
|
|
||||||
let mut buffer = vec![0, 4];
|
let mut buffer = vec![0, 4];
|
||||||
let size = 2 + 4 + packet.compute_size();
|
let size = 2 + 4 + packet.compute_size();
|
||||||
buffer.write_u32::<BigEndian>(size).unwrap();
|
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap();
|
||||||
packet.write_to_vec(&mut buffer).unwrap();
|
packet.write_to_vec(&mut buffer).unwrap();
|
||||||
|
|
||||||
write_all(connection, buffer)
|
connection.write_all(&buffer[..]).await?;
|
||||||
|
Ok(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_response<T: AsyncWrite>(connection: T, challenge: Vec<u8>) -> WriteAll<T, Vec<u8>> {
|
async fn client_response<T>(connection: &mut T, challenge: Vec<u8>) -> io::Result<()>
|
||||||
|
where
|
||||||
|
T: AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
let mut packet = ClientResponsePlaintext::new();
|
let mut packet = ClientResponsePlaintext::new();
|
||||||
packet
|
packet
|
||||||
.mut_login_crypto_response()
|
.mut_login_crypto_response()
|
||||||
|
@ -123,70 +87,35 @@ fn client_response<T: AsyncWrite>(connection: T, challenge: Vec<u8>) -> WriteAll
|
||||||
|
|
||||||
let mut buffer = vec![];
|
let mut buffer = vec![];
|
||||||
let size = 4 + packet.compute_size();
|
let size = 4 + packet.compute_size();
|
||||||
buffer.write_u32::<BigEndian>(size).unwrap();
|
<Vec<u8> as WriteBytesExt>::write_u32::<BigEndian>(&mut buffer, size).unwrap();
|
||||||
packet.write_to_vec(&mut buffer).unwrap();
|
packet.write_to_vec(&mut buffer).unwrap();
|
||||||
|
|
||||||
write_all(connection, buffer)
|
connection.write_all(&buffer[..]).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RecvPacket<T, M: Message> {
|
async fn recv_packet<T, M>(connection: &mut T, acc: &mut Vec<u8>) -> io::Result<M>
|
||||||
Header(ReadExact<T, Window<Vec<u8>>>, PhantomData<M>),
|
|
||||||
Body(ReadExact<T, Window<Vec<u8>>>, PhantomData<M>),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recv_packet<T: AsyncRead, M>(connection: T, acc: Vec<u8>) -> RecvPacket<T, M>
|
|
||||||
where
|
where
|
||||||
T: Read,
|
T: AsyncRead + Unpin,
|
||||||
M: Message,
|
M: Message,
|
||||||
{
|
{
|
||||||
RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData)
|
let header = read_into_accumulator(connection, 4, acc).await?;
|
||||||
|
let size = BigEndian::read_u32(header) as usize;
|
||||||
|
let data = read_into_accumulator(connection, size - 4, acc).await?;
|
||||||
|
let message = protobuf::parse_from_bytes(data).unwrap();
|
||||||
|
Ok(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: AsyncRead, M> Future for RecvPacket<T, M>
|
async fn read_into_accumulator<'a, 'b, T: AsyncRead + Unpin>(
|
||||||
where
|
connection: &'a mut T,
|
||||||
T: Read,
|
|
||||||
M: Message,
|
|
||||||
{
|
|
||||||
type Item = (T, M, Vec<u8>);
|
|
||||||
type Error = io::Error;
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Self::Item, io::Error> {
|
|
||||||
use self::RecvPacket::*;
|
|
||||||
loop {
|
|
||||||
*self = match *self {
|
|
||||||
Header(ref mut read, _) => {
|
|
||||||
let (connection, header) = try_ready!(read.poll());
|
|
||||||
let size = BigEndian::read_u32(header.as_ref()) as usize;
|
|
||||||
|
|
||||||
let acc = header.into_inner();
|
|
||||||
let read = read_into_accumulator(connection, size - 4, acc);
|
|
||||||
RecvPacket::Body(read, PhantomData)
|
|
||||||
}
|
|
||||||
|
|
||||||
Body(ref mut read, _) => {
|
|
||||||
let (connection, data) = try_ready!(read.poll());
|
|
||||||
let message = protobuf::parse_from_bytes(data.as_ref()).unwrap();
|
|
||||||
|
|
||||||
let acc = data.into_inner();
|
|
||||||
return Ok(Async::Ready((connection, message, acc)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_into_accumulator<T: AsyncRead>(
|
|
||||||
connection: T,
|
|
||||||
size: usize,
|
size: usize,
|
||||||
mut acc: Vec<u8>,
|
acc: &'b mut Vec<u8>,
|
||||||
) -> ReadExact<T, Window<Vec<u8>>> {
|
) -> io::Result<&'b mut [u8]> {
|
||||||
let offset = acc.len();
|
let offset = acc.len();
|
||||||
acc.resize(offset + size, 0);
|
acc.resize(offset + size, 0);
|
||||||
|
|
||||||
let mut window = Window::new(acc);
|
connection.read_exact(&mut acc[offset..]).await?;
|
||||||
window.set_start(offset);
|
Ok(&mut acc[offset..])
|
||||||
|
|
||||||
read_exact(connection, window)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
|
fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
|
||||||
|
|
|
@ -1,74 +1,117 @@
|
||||||
mod codec;
|
mod codec;
|
||||||
mod handshake;
|
mod handshake;
|
||||||
|
|
||||||
pub use self::codec::APCodec;
|
pub use self::codec::ApCodec;
|
||||||
pub use self::handshake::handshake;
|
pub use self::handshake::handshake;
|
||||||
|
|
||||||
use futures::{Future, Sink, Stream};
|
use std::io::{self, ErrorKind};
|
||||||
use protobuf::{self, Message};
|
|
||||||
use std::io;
|
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
use tokio_codec::Framed;
|
|
||||||
use tokio_core::net::TcpStream;
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use tokio_core::reactor::Handle;
|
use protobuf::{self, Message, ProtobufError};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio_util::codec::Framed;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::authentication::{AuthenticationError, Credentials};
|
use crate::authentication::Credentials;
|
||||||
|
use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
|
||||||
|
use crate::proxytunnel;
|
||||||
use crate::version;
|
use crate::version;
|
||||||
|
|
||||||
use crate::proxytunnel;
|
pub type Transport = Framed<TcpStream, ApCodec>;
|
||||||
|
|
||||||
pub type Transport = Framed<TcpStream, APCodec>;
|
fn login_error_message(code: &ErrorCode) -> &'static str {
|
||||||
|
pub use ErrorCode::*;
|
||||||
pub fn connect(
|
match code {
|
||||||
addr: String,
|
ProtocolError => "Protocol error",
|
||||||
handle: &Handle,
|
TryAnotherAP => "Try another AP",
|
||||||
proxy: &Option<Url>,
|
BadConnectionId => "Bad connection id",
|
||||||
) -> Box<dyn Future<Item = Transport, Error = io::Error>> {
|
TravelRestriction => "Travel restriction",
|
||||||
let (addr, connect_url) = match *proxy {
|
PremiumAccountRequired => "Premium account required",
|
||||||
Some(ref url) => {
|
BadCredentials => "Bad credentials",
|
||||||
info!("Using proxy \"{}\"", url);
|
CouldNotValidateCredentials => "Could not validate credentials",
|
||||||
match url.to_socket_addrs().and_then(|mut iter| {
|
AccountExists => "Account exists",
|
||||||
iter.next().ok_or(io::Error::new(
|
ExtraVerificationRequired => "Extra verification required",
|
||||||
io::ErrorKind::NotFound,
|
InvalidAppKey => "Invalid app key",
|
||||||
"Can't resolve proxy server address",
|
ApplicationBanned => "Application banned",
|
||||||
))
|
|
||||||
}) {
|
|
||||||
Ok(socket_addr) => (socket_addr, Some(addr)),
|
|
||||||
Err(error) => return Box::new(futures::future::err(error)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
match addr.to_socket_addrs().and_then(|mut iter| {
|
|
||||||
iter.next().ok_or(io::Error::new(
|
|
||||||
io::ErrorKind::NotFound,
|
|
||||||
"Can't resolve server address",
|
|
||||||
))
|
|
||||||
}) {
|
|
||||||
Ok(socket_addr) => (socket_addr, None),
|
|
||||||
Err(error) => return Box::new(futures::future::err(error)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let socket = TcpStream::connect(&addr, handle);
|
|
||||||
if let Some(connect_url) = connect_url {
|
|
||||||
let connection = socket
|
|
||||||
.and_then(move |socket| proxytunnel::connect(socket, &connect_url).and_then(handshake));
|
|
||||||
Box::new(connection)
|
|
||||||
} else {
|
|
||||||
let connection = socket.and_then(handshake);
|
|
||||||
Box::new(connection)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn authenticate(
|
#[derive(Debug, Error)]
|
||||||
transport: Transport,
|
pub enum AuthenticationError {
|
||||||
|
#[error("Login failed with reason: {}", login_error_message(.0))]
|
||||||
|
LoginFailed(ErrorCode),
|
||||||
|
#[error("Authentication failed: {0}")]
|
||||||
|
IoError(#[from] io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProtobufError> for AuthenticationError {
|
||||||
|
fn from(e: ProtobufError) -> Self {
|
||||||
|
io::Error::new(ErrorKind::InvalidData, e).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<APLoginFailed> for AuthenticationError {
|
||||||
|
fn from(login_failure: APLoginFailed) -> Self {
|
||||||
|
Self::LoginFailed(login_failure.get_error_code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect(addr: String, proxy: Option<&Url>) -> io::Result<Transport> {
|
||||||
|
let socket = if let Some(proxy_url) = proxy {
|
||||||
|
info!("Using proxy \"{}\"", proxy_url);
|
||||||
|
|
||||||
|
let socket_addr = proxy_url.socket_addrs(|| None).and_then(|addrs| {
|
||||||
|
addrs.into_iter().next().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"Can't resolve proxy server address",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
let socket = TcpStream::connect(&socket_addr).await?;
|
||||||
|
|
||||||
|
let uri = addr.parse::<http::Uri>().map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"Can't parse access point address",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let host = uri.host().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"The access point address contains no hostname",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let port = uri.port().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"The access point address contains no port",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
proxytunnel::proxy_connect(socket, host, port.as_str()).await?
|
||||||
|
} else {
|
||||||
|
let socket_addr = addr.to_socket_addrs()?.next().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"Can't resolve access point address",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
TcpStream::connect(&socket_addr).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
handshake(socket).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn authenticate(
|
||||||
|
transport: &mut Transport,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
device_id: String,
|
device_id: &str,
|
||||||
) -> Box<dyn Future<Item = (Transport, Credentials), Error = AuthenticationError>> {
|
) -> Result<Credentials, AuthenticationError> {
|
||||||
use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
|
use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os};
|
||||||
use crate::protocol::keyexchange::APLoginFailed;
|
|
||||||
|
|
||||||
let mut packet = ClientResponseEncrypted::new();
|
let mut packet = ClientResponseEncrypted::new();
|
||||||
packet
|
packet
|
||||||
|
@ -91,39 +134,35 @@ pub fn authenticate(
|
||||||
version::SHA_SHORT,
|
version::SHA_SHORT,
|
||||||
version::BUILD_ID
|
version::BUILD_ID
|
||||||
));
|
));
|
||||||
packet.mut_system_info().set_device_id(device_id);
|
packet
|
||||||
|
.mut_system_info()
|
||||||
|
.set_device_id(device_id.to_string());
|
||||||
packet.set_version_string(version::VERSION_STRING.to_string());
|
packet.set_version_string(version::VERSION_STRING.to_string());
|
||||||
|
|
||||||
let cmd = 0xab;
|
let cmd = 0xab;
|
||||||
let data = packet.write_to_bytes().unwrap();
|
let data = packet.write_to_bytes().unwrap();
|
||||||
|
|
||||||
Box::new(
|
transport.send((cmd, data)).await?;
|
||||||
transport
|
let (cmd, data) = transport.next().await.expect("EOF")?;
|
||||||
.send((cmd, data))
|
match cmd {
|
||||||
.and_then(|transport| transport.into_future().map_err(|(err, _stream)| err))
|
0xac => {
|
||||||
.map_err(|io_err| io_err.into())
|
let welcome_data: APWelcome = protobuf::parse_from_bytes(data.as_ref())?;
|
||||||
.and_then(|(packet, transport)| match packet {
|
|
||||||
Some((0xac, data)) => {
|
|
||||||
let welcome_data: APWelcome =
|
|
||||||
protobuf::parse_from_bytes(data.as_ref()).unwrap();
|
|
||||||
|
|
||||||
let reusable_credentials = Credentials {
|
let reusable_credentials = Credentials {
|
||||||
username: welcome_data.get_canonical_username().to_owned(),
|
username: welcome_data.get_canonical_username().to_owned(),
|
||||||
auth_type: welcome_data.get_reusable_auth_credentials_type(),
|
auth_type: welcome_data.get_reusable_auth_credentials_type(),
|
||||||
auth_data: welcome_data.get_reusable_auth_credentials().to_owned(),
|
auth_data: welcome_data.get_reusable_auth_credentials().to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((transport, reusable_credentials))
|
Ok(reusable_credentials)
|
||||||
}
|
}
|
||||||
|
0xad => {
|
||||||
Some((0xad, data)) => {
|
let error_data: APLoginFailed = protobuf::parse_from_bytes(data.as_ref())?;
|
||||||
let error_data: APLoginFailed =
|
Err(error_data.into())
|
||||||
protobuf::parse_from_bytes(data.as_ref()).unwrap();
|
}
|
||||||
Err(error_data.into())
|
_ => {
|
||||||
}
|
let msg = format!("Received invalid packet: {}", cmd);
|
||||||
|
Err(io::Error::new(ErrorKind::InvalidData, msg).into())
|
||||||
Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd),
|
}
|
||||||
None => panic!("EOF"),
|
}
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use num_bigint::BigUint;
|
use num_bigint::{BigUint, RandBigInt};
|
||||||
use num_traits::FromPrimitive;
|
use num_integer::Integer;
|
||||||
use rand::Rng;
|
use num_traits::{One, Zero};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use rand::{CryptoRng, Rng};
|
||||||
|
|
||||||
use crate::util;
|
static DH_GENERATOR: Lazy<BigUint> = Lazy::new(|| BigUint::from_bytes_be(&[0x02]));
|
||||||
|
static DH_PRIME: Lazy<BigUint> = Lazy::new(|| {
|
||||||
lazy_static! {
|
BigUint::from_bytes_be(&[
|
||||||
pub static ref DH_GENERATOR: BigUint = BigUint::from_u64(0x2).unwrap();
|
|
||||||
pub static ref DH_PRIME: BigUint = BigUint::from_bytes_be(&[
|
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2,
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2,
|
||||||
0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
|
0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
|
||||||
0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
|
0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
|
||||||
|
@ -14,24 +14,38 @@ lazy_static! {
|
||||||
0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5,
|
0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5,
|
||||||
0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff,
|
0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff,
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||||
]);
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
|
||||||
|
let mut base = base.clone();
|
||||||
|
let mut exp = exp.clone();
|
||||||
|
let mut result: BigUint = One::one();
|
||||||
|
|
||||||
|
while !exp.is_zero() {
|
||||||
|
if exp.is_odd() {
|
||||||
|
result = (result * &base) % modulus;
|
||||||
|
}
|
||||||
|
exp >>= 1;
|
||||||
|
base = (&base * &base) % modulus;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DHLocalKeys {
|
pub struct DhLocalKeys {
|
||||||
private_key: BigUint,
|
private_key: BigUint,
|
||||||
public_key: BigUint,
|
public_key: BigUint,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DHLocalKeys {
|
impl DhLocalKeys {
|
||||||
pub fn random<R: Rng>(rng: &mut R) -> DHLocalKeys {
|
pub fn random<R: Rng + CryptoRng>(rng: &mut R) -> DhLocalKeys {
|
||||||
let key_data = util::rand_vec(rng, 95);
|
let private_key = rng.gen_biguint(95 * 8);
|
||||||
|
let public_key = powm(&DH_GENERATOR, &private_key, &DH_PRIME);
|
||||||
|
|
||||||
let private_key = BigUint::from_bytes_be(&key_data);
|
DhLocalKeys {
|
||||||
let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME);
|
private_key,
|
||||||
|
public_key,
|
||||||
DHLocalKeys {
|
|
||||||
private_key: private_key,
|
|
||||||
public_key: public_key,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +54,7 @@ impl DHLocalKeys {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shared_secret(&self, remote_key: &[u8]) -> Vec<u8> {
|
pub fn shared_secret(&self, remote_key: &[u8]) -> Vec<u8> {
|
||||||
let shared_key = util::powm(
|
let shared_key = powm(
|
||||||
&BigUint::from_bytes_be(remote_key),
|
&BigUint::from_bytes_be(remote_key),
|
||||||
&self.private_key,
|
&self.private_key,
|
||||||
&DH_PRIME,
|
&DH_PRIME,
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
use futures::Future;
|
use serde::Deserialize;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use crate::mercury::MercuryError;
|
use crate::{mercury::MercuryError, session::Session};
|
||||||
use crate::session::Session;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -13,20 +11,16 @@ pub struct Token {
|
||||||
pub scope: Vec<String>,
|
pub scope: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_token(
|
pub async fn get_token(
|
||||||
session: &Session,
|
session: &Session,
|
||||||
client_id: &str,
|
client_id: &str,
|
||||||
scopes: &str,
|
scopes: &str,
|
||||||
) -> Box<dyn Future<Item = Token, Error = MercuryError>> {
|
) -> Result<Token, MercuryError> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"hm://keymaster/token/authenticated?client_id={}&scope={}",
|
"hm://keymaster/token/authenticated?client_id={}&scope={}",
|
||||||
client_id, scopes
|
client_id, scopes
|
||||||
);
|
);
|
||||||
Box::new(session.mercury().get(url).map(move |response| {
|
let response = session.mercury().get(url).await?;
|
||||||
let data = response.payload.first().expect("Empty payload");
|
let data = response.payload.first().expect("Empty payload");
|
||||||
let data = String::from_utf8(data.clone()).unwrap();
|
serde_json::from_slice(data.as_ref()).map_err(|_| MercuryError)
|
||||||
let token: Token = serde_json::from_str(&data).unwrap();
|
|
||||||
|
|
||||||
token
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +1,38 @@
|
||||||
#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))]
|
#![allow(clippy::unused_io_amount)]
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate error_chain;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate futures;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_derive;
|
|
||||||
|
|
||||||
extern crate aes;
|
use librespot_protocol as protocol;
|
||||||
extern crate base64;
|
|
||||||
extern crate byteorder;
|
|
||||||
extern crate bytes;
|
|
||||||
extern crate hmac;
|
|
||||||
extern crate httparse;
|
|
||||||
extern crate hyper;
|
|
||||||
extern crate hyper_proxy;
|
|
||||||
extern crate num_bigint;
|
|
||||||
extern crate num_integer;
|
|
||||||
extern crate num_traits;
|
|
||||||
extern crate pbkdf2;
|
|
||||||
extern crate protobuf;
|
|
||||||
extern crate rand;
|
|
||||||
extern crate serde;
|
|
||||||
extern crate serde_json;
|
|
||||||
extern crate sha1;
|
|
||||||
extern crate shannon;
|
|
||||||
extern crate tokio_codec;
|
|
||||||
extern crate tokio_core;
|
|
||||||
extern crate tokio_io;
|
|
||||||
extern crate url;
|
|
||||||
extern crate uuid;
|
|
||||||
|
|
||||||
extern crate librespot_protocol as protocol;
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod component;
|
mod component;
|
||||||
mod apresolve;
|
|
||||||
pub mod audio_key;
|
pub mod audio_key;
|
||||||
pub mod authentication;
|
pub mod authentication;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
mod connection;
|
mod connection;
|
||||||
|
#[doc(hidden)]
|
||||||
pub mod diffie_hellman;
|
pub mod diffie_hellman;
|
||||||
pub mod keymaster;
|
pub mod keymaster;
|
||||||
pub mod mercury;
|
pub mod mercury;
|
||||||
mod proxytunnel;
|
mod proxytunnel;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod spotify_id;
|
pub mod spotify_id;
|
||||||
|
#[doc(hidden)]
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
|
|
||||||
|
const AP_FALLBACK: &str = "ap.spotify.com:443";
|
||||||
|
|
||||||
|
#[cfg(feature = "apresolve")]
|
||||||
|
mod apresolve;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "apresolve"))]
|
||||||
|
mod apresolve {
|
||||||
|
pub async fn apresolve(_: Option<&url::Url>, _: Option<u16>) -> String {
|
||||||
|
return super::AP_FALLBACK.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
use crate::protocol;
|
use std::collections::HashMap;
|
||||||
use crate::util::url_encode;
|
use std::future::Future;
|
||||||
|
use std::mem;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::task::Context;
|
||||||
|
use std::task::Poll;
|
||||||
|
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
use futures::{Async, Future, Poll};
|
|
||||||
use protobuf;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
|
use crate::protocol;
|
||||||
use crate::util::SeqGenerator;
|
use crate::util::SeqGenerator;
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
|
@ -31,17 +33,18 @@ pub struct MercuryPending {
|
||||||
callback: Option<oneshot::Sender<Result<MercuryResponse, MercuryError>>>,
|
callback: Option<oneshot::Sender<Result<MercuryResponse, MercuryError>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MercuryFuture<T>(oneshot::Receiver<Result<T, MercuryError>>);
|
pub struct MercuryFuture<T> {
|
||||||
impl<T> Future for MercuryFuture<T> {
|
receiver: oneshot::Receiver<Result<T, MercuryError>>,
|
||||||
type Item = T;
|
}
|
||||||
type Error = MercuryError;
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<T, MercuryError> {
|
impl<T> Future for MercuryFuture<T> {
|
||||||
match self.0.poll() {
|
type Output = Result<T, MercuryError>;
|
||||||
Ok(Async::Ready(Ok(value))) => Ok(Async::Ready(value)),
|
|
||||||
Ok(Async::Ready(Err(err))) => Err(err),
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
Ok(Async::NotReady) => Ok(Async::NotReady),
|
match Pin::new(&mut self.receiver).poll(cx) {
|
||||||
Err(oneshot::Canceled) => Err(MercuryError),
|
Poll::Ready(Ok(x)) => Poll::Ready(x),
|
||||||
|
Poll::Ready(Err(_)) => Poll::Ready(Err(MercuryError)),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,12 +76,12 @@ impl MercuryManager {
|
||||||
let data = req.encode(&seq);
|
let data = req.encode(&seq);
|
||||||
|
|
||||||
self.session().send_packet(cmd, data);
|
self.session().send_packet(cmd, data);
|
||||||
MercuryFuture(rx)
|
MercuryFuture { receiver: rx }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get<T: Into<String>>(&self, uri: T) -> MercuryFuture<MercuryResponse> {
|
pub fn get<T: Into<String>>(&self, uri: T) -> MercuryFuture<MercuryResponse> {
|
||||||
self.request(MercuryRequest {
|
self.request(MercuryRequest {
|
||||||
method: MercuryMethod::GET,
|
method: MercuryMethod::Get,
|
||||||
uri: uri.into(),
|
uri: uri.into(),
|
||||||
content_type: None,
|
content_type: None,
|
||||||
payload: Vec::new(),
|
payload: Vec::new(),
|
||||||
|
@ -87,7 +90,7 @@ 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>) -> MercuryFuture<MercuryResponse> {
|
||||||
self.request(MercuryRequest {
|
self.request(MercuryRequest {
|
||||||
method: MercuryMethod::SEND,
|
method: MercuryMethod::Send,
|
||||||
uri: uri.into(),
|
uri: uri.into(),
|
||||||
content_type: None,
|
content_type: None,
|
||||||
payload: vec![data],
|
payload: vec![data],
|
||||||
|
@ -101,24 +104,26 @@ impl MercuryManager {
|
||||||
pub fn subscribe<T: Into<String>>(
|
pub fn subscribe<T: Into<String>>(
|
||||||
&self,
|
&self,
|
||||||
uri: T,
|
uri: T,
|
||||||
) -> Box<dyn Future<Item = mpsc::UnboundedReceiver<MercuryResponse>, Error = MercuryError>>
|
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<MercuryResponse>, MercuryError>> + 'static
|
||||||
{
|
{
|
||||||
let uri = uri.into();
|
let uri = uri.into();
|
||||||
let request = self.request(MercuryRequest {
|
let request = self.request(MercuryRequest {
|
||||||
method: MercuryMethod::SUB,
|
method: MercuryMethod::Sub,
|
||||||
uri: uri.clone(),
|
uri: uri.clone(),
|
||||||
content_type: None,
|
content_type: None,
|
||||||
payload: Vec::new(),
|
payload: Vec::new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let manager = self.clone();
|
let manager = self.clone();
|
||||||
Box::new(request.map(move |response| {
|
async move {
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let response = request.await?;
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
manager.lock(move |inner| {
|
manager.lock(move |inner| {
|
||||||
if !inner.invalid {
|
if !inner.invalid {
|
||||||
debug!("subscribed uri={} count={}", uri, response.payload.len());
|
debug!("subscribed uri={} count={}", uri, response.payload.len());
|
||||||
if response.payload.len() > 0 {
|
if !response.payload.is_empty() {
|
||||||
// Old subscription protocol, watch the provided list of URIs
|
// Old subscription protocol, watch the provided list of URIs
|
||||||
for sub in response.payload {
|
for sub in response.payload {
|
||||||
let mut sub: protocol::pubsub::Subscription =
|
let mut sub: protocol::pubsub::Subscription =
|
||||||
|
@ -136,8 +141,8 @@ impl MercuryManager {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rx
|
Ok(rx)
|
||||||
}))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) {
|
pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) {
|
||||||
|
@ -193,7 +198,7 @@ impl MercuryManager {
|
||||||
let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap();
|
let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap();
|
||||||
|
|
||||||
let response = MercuryResponse {
|
let response = MercuryResponse {
|
||||||
uri: url_encode(header.get_uri()).to_owned(),
|
uri: header.get_uri().to_string(),
|
||||||
status_code: header.get_status_code(),
|
status_code: header.get_status_code(),
|
||||||
payload: pending.parts,
|
payload: pending.parts,
|
||||||
};
|
};
|
||||||
|
@ -205,30 +210,41 @@ impl MercuryManager {
|
||||||
if let Some(cb) = pending.callback {
|
if let Some(cb) = pending.callback {
|
||||||
let _ = cb.send(Err(MercuryError));
|
let _ = cb.send(Err(MercuryError));
|
||||||
}
|
}
|
||||||
} else {
|
} else if cmd == 0xb5 {
|
||||||
if cmd == 0xb5 {
|
self.lock(|inner| {
|
||||||
self.lock(|inner| {
|
let mut found = false;
|
||||||
let mut found = false;
|
|
||||||
inner.subscriptions.retain(|&(ref prefix, ref sub)| {
|
|
||||||
if response.uri.starts_with(prefix) {
|
|
||||||
found = true;
|
|
||||||
|
|
||||||
// if send fails, remove from list of subs
|
// TODO: This is just a workaround to make utf-8 encoded usernames work.
|
||||||
// TODO: send unsub message
|
// A better solution would be to use an uri struct and urlencode it directly
|
||||||
sub.unbounded_send(response.clone()).is_ok()
|
// before sending while saving the subscription under its unencoded form.
|
||||||
} else {
|
let mut uri_split = response.uri.split('/');
|
||||||
// URI doesn't match
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if !found {
|
let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string())
|
||||||
debug!("unknown subscription uri={}", response.uri);
|
.chain(uri_split.map(|component| {
|
||||||
|
form_urlencoded::byte_serialize(component.as_bytes()).collect::<String>()
|
||||||
|
}))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("/");
|
||||||
|
|
||||||
|
inner.subscriptions.retain(|&(ref prefix, ref sub)| {
|
||||||
|
if encoded_uri.starts_with(prefix) {
|
||||||
|
found = true;
|
||||||
|
|
||||||
|
// if send fails, remove from list of subs
|
||||||
|
// TODO: send unsub message
|
||||||
|
sub.send(response.clone()).is_ok()
|
||||||
|
} else {
|
||||||
|
// URI doesn't match
|
||||||
|
true
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
} else if let Some(cb) = pending.callback {
|
|
||||||
let _ = cb.send(Ok(response));
|
if !found {
|
||||||
}
|
debug!("unknown subscription uri={}", response.uri);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if let Some(cb) = pending.callback {
|
||||||
|
let _ = cb.send(Ok(response));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend};
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -13,11 +12,27 @@ impl MercurySender {
|
||||||
// TODO: pub(super) when stable
|
// TODO: pub(super) when stable
|
||||||
pub(crate) fn new(mercury: MercuryManager, uri: String) -> MercurySender {
|
pub(crate) fn new(mercury: MercuryManager, uri: String) -> MercurySender {
|
||||||
MercurySender {
|
MercurySender {
|
||||||
mercury: mercury,
|
mercury,
|
||||||
uri: uri,
|
uri,
|
||||||
pending: VecDeque::new(),
|
pending: VecDeque::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_flushed(&self) -> bool {
|
||||||
|
self.pending.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&mut self, item: Vec<u8>) {
|
||||||
|
let task = self.mercury.send(self.uri.clone(), item);
|
||||||
|
self.pending.push_back(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn flush(&mut self) -> Result<(), MercuryError> {
|
||||||
|
for fut in self.pending.drain(..) {
|
||||||
|
fut.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for MercurySender {
|
impl Clone for MercurySender {
|
||||||
|
@ -29,28 +44,3 @@ impl Clone for MercurySender {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sink for MercurySender {
|
|
||||||
type SinkItem = Vec<u8>;
|
|
||||||
type SinkError = MercuryError;
|
|
||||||
|
|
||||||
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
|
|
||||||
let task = self.mercury.send(self.uri.clone(), item);
|
|
||||||
self.pending.push_back(task);
|
|
||||||
Ok(AsyncSink::Ready)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
|
|
||||||
loop {
|
|
||||||
match self.pending.front_mut() {
|
|
||||||
Some(task) => {
|
|
||||||
try_ready!(task.poll());
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
return Ok(Async::Ready(()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.pending.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,10 +6,10 @@ use crate::protocol;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum MercuryMethod {
|
pub enum MercuryMethod {
|
||||||
GET,
|
Get,
|
||||||
SUB,
|
Sub,
|
||||||
UNSUB,
|
Unsub,
|
||||||
SEND,
|
Send,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -33,10 +33,10 @@ pub struct MercuryError;
|
||||||
impl ToString for MercuryMethod {
|
impl ToString for MercuryMethod {
|
||||||
fn to_string(&self) -> String {
|
fn to_string(&self) -> String {
|
||||||
match *self {
|
match *self {
|
||||||
MercuryMethod::GET => "GET",
|
MercuryMethod::Get => "GET",
|
||||||
MercuryMethod::SUB => "SUB",
|
MercuryMethod::Sub => "SUB",
|
||||||
MercuryMethod::UNSUB => "UNSUB",
|
MercuryMethod::Unsub => "UNSUB",
|
||||||
MercuryMethod::SEND => "SEND",
|
MercuryMethod::Send => "SEND",
|
||||||
}
|
}
|
||||||
.to_owned()
|
.to_owned()
|
||||||
}
|
}
|
||||||
|
@ -45,9 +45,9 @@ impl ToString for MercuryMethod {
|
||||||
impl MercuryMethod {
|
impl MercuryMethod {
|
||||||
pub fn command(&self) -> u8 {
|
pub fn command(&self) -> u8 {
|
||||||
match *self {
|
match *self {
|
||||||
MercuryMethod::GET | MercuryMethod::SEND => 0xb2,
|
MercuryMethod::Get | MercuryMethod::Send => 0xb2,
|
||||||
MercuryMethod::SUB => 0xb3,
|
MercuryMethod::Sub => 0xb3,
|
||||||
MercuryMethod::UNSUB => 0xb4,
|
MercuryMethod::Unsub => 0xb4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,110 +1,55 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use futures::{Async, Future, Poll};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
use httparse;
|
|
||||||
use hyper::Uri;
|
|
||||||
use tokio_io::io::{read, write_all, Read, Window, WriteAll};
|
|
||||||
use tokio_io::{AsyncRead, AsyncWrite};
|
|
||||||
|
|
||||||
pub struct ProxyTunnel<T> {
|
pub async fn proxy_connect<T: AsyncRead + AsyncWrite + Unpin>(
|
||||||
state: ProxyState<T>,
|
mut proxy_connection: T,
|
||||||
}
|
connect_host: &str,
|
||||||
|
connect_port: &str,
|
||||||
|
) -> io::Result<T> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
buffer.extend_from_slice(b"CONNECT ");
|
||||||
|
buffer.extend_from_slice(connect_host.as_bytes());
|
||||||
|
buffer.push(b':');
|
||||||
|
buffer.extend_from_slice(connect_port.as_bytes());
|
||||||
|
buffer.extend_from_slice(b" HTTP/1.1\r\n\r\n");
|
||||||
|
|
||||||
enum ProxyState<T> {
|
proxy_connection.write_all(buffer.as_ref()).await?;
|
||||||
ProxyConnect(WriteAll<T, Vec<u8>>),
|
|
||||||
ProxyResponse(Read<T, Window<Vec<u8>>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connect<T: AsyncRead + AsyncWrite>(connection: T, connect_url: &str) -> ProxyTunnel<T> {
|
buffer.resize(buffer.capacity(), 0);
|
||||||
let proxy = proxy_connect(connection, connect_url);
|
|
||||||
ProxyTunnel {
|
|
||||||
state: ProxyState::ProxyConnect(proxy),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsyncRead + AsyncWrite> Future for ProxyTunnel<T> {
|
let mut offset = 0;
|
||||||
type Item = T;
|
loop {
|
||||||
type Error = io::Error;
|
let bytes_read = proxy_connection.read(&mut buffer[offset..]).await?;
|
||||||
|
if bytes_read == 0 {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::Other, "Early EOF from proxy"));
|
||||||
|
}
|
||||||
|
offset += bytes_read;
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Self::Item, io::Error> {
|
let mut headers = [httparse::EMPTY_HEADER; 16];
|
||||||
use self::ProxyState::*;
|
let mut response = httparse::Response::new(&mut headers);
|
||||||
loop {
|
|
||||||
self.state = match self.state {
|
|
||||||
ProxyConnect(ref mut write) => {
|
|
||||||
let (connection, mut accumulator) = try_ready!(write.poll());
|
|
||||||
|
|
||||||
let capacity = accumulator.capacity();
|
let status = response
|
||||||
accumulator.resize(capacity, 0);
|
.parse(&buffer[..offset])
|
||||||
let window = Window::new(accumulator);
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||||
|
|
||||||
let read = read(connection, window);
|
if status.is_complete() {
|
||||||
ProxyResponse(read)
|
return match response.code {
|
||||||
|
Some(200) => Ok(proxy_connection), // Proxy says all is well
|
||||||
|
Some(code) => {
|
||||||
|
let reason = response.reason.unwrap_or("no reason");
|
||||||
|
let msg = format!("Proxy responded with {}: {}", code, reason);
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, msg))
|
||||||
}
|
}
|
||||||
|
None => Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"Malformed response from proxy",
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
ProxyResponse(ref mut read_f) => {
|
if offset >= buffer.len() {
|
||||||
let (connection, mut window, bytes_read) = try_ready!(read_f.poll());
|
buffer.resize(buffer.len() + 100, 0);
|
||||||
|
|
||||||
if bytes_read == 0 {
|
|
||||||
return Err(io::Error::new(io::ErrorKind::Other, "Early EOF from proxy"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let data_end = window.start() + bytes_read;
|
|
||||||
|
|
||||||
let buf = window.get_ref()[0..data_end].to_vec();
|
|
||||||
let mut headers = [httparse::EMPTY_HEADER; 16];
|
|
||||||
let mut response = httparse::Response::new(&mut headers);
|
|
||||||
let status = match response.parse(&buf) {
|
|
||||||
Ok(status) => status,
|
|
||||||
Err(err) => {
|
|
||||||
return Err(io::Error::new(io::ErrorKind::Other, err.to_string()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if status.is_complete() {
|
|
||||||
if let Some(code) = response.code {
|
|
||||||
if code == 200 {
|
|
||||||
// Proxy says all is well
|
|
||||||
return Ok(Async::Ready(connection));
|
|
||||||
} else {
|
|
||||||
let reason = response.reason.unwrap_or("no reason");
|
|
||||||
let msg = format!("Proxy responded with {}: {}", code, reason);
|
|
||||||
|
|
||||||
return Err(io::Error::new(io::ErrorKind::Other, msg));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::Other,
|
|
||||||
"Malformed response from proxy",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if data_end >= window.end() {
|
|
||||||
// Allocate some more buffer space
|
|
||||||
let newsize = data_end + 100;
|
|
||||||
window.get_mut().resize(newsize, 0);
|
|
||||||
window.set_end(newsize);
|
|
||||||
}
|
|
||||||
// We did not get a full header
|
|
||||||
window.set_start(data_end);
|
|
||||||
let read = read(connection, window);
|
|
||||||
ProxyResponse(read)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn proxy_connect<T: AsyncWrite>(connection: T, connect_url: &str) -> WriteAll<T, Vec<u8>> {
|
|
||||||
let uri = Uri::from_str(connect_url).unwrap();
|
|
||||||
let buffer = format!(
|
|
||||||
"CONNECT {0}:{1} HTTP/1.1\r\n\
|
|
||||||
\r\n",
|
|
||||||
uri.host().expect(&format!("No host in {}", uri)),
|
|
||||||
uri.port().expect(&format!("No port in {}", uri))
|
|
||||||
)
|
|
||||||
.into_bytes();
|
|
||||||
|
|
||||||
write_all(connection, buffer)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,25 +1,37 @@
|
||||||
|
use std::future::Future;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::pin::Pin;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, RwLock, Weak};
|
use std::sync::{Arc, RwLock, Weak};
|
||||||
|
use std::task::Context;
|
||||||
|
use std::task::Poll;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::sync::mpsc;
|
use futures_core::TryStream;
|
||||||
use futures::{Async, Future, IntoFuture, Poll, Stream};
|
use futures_util::{FutureExt, StreamExt, TryStreamExt};
|
||||||
use tokio_core::reactor::{Handle, Remote};
|
use once_cell::sync::OnceCell;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
|
||||||
use crate::apresolve::apresolve_or_fallback;
|
use crate::apresolve::apresolve;
|
||||||
use crate::audio_key::AudioKeyManager;
|
use crate::audio_key::AudioKeyManager;
|
||||||
use crate::authentication::Credentials;
|
use crate::authentication::Credentials;
|
||||||
use crate::cache::Cache;
|
use crate::cache::Cache;
|
||||||
use crate::channel::ChannelManager;
|
use crate::channel::ChannelManager;
|
||||||
use crate::component::Lazy;
|
|
||||||
use crate::config::SessionConfig;
|
use crate::config::SessionConfig;
|
||||||
use crate::connection;
|
use crate::connection::{self, AuthenticationError};
|
||||||
use crate::mercury::MercuryManager;
|
use crate::mercury::MercuryManager;
|
||||||
|
|
||||||
pub use crate::authentication::{AuthenticationError, AuthenticationErrorKind};
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SessionError {
|
||||||
|
#[error(transparent)]
|
||||||
|
AuthenticationError(#[from] AuthenticationError),
|
||||||
|
#[error("Cannot create session: {0}")]
|
||||||
|
IoError(#[from] io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
struct SessionData {
|
struct SessionData {
|
||||||
country: String,
|
country: String,
|
||||||
|
@ -34,12 +46,12 @@ struct SessionInternal {
|
||||||
|
|
||||||
tx_connection: mpsc::UnboundedSender<(u8, Vec<u8>)>,
|
tx_connection: mpsc::UnboundedSender<(u8, Vec<u8>)>,
|
||||||
|
|
||||||
audio_key: Lazy<AudioKeyManager>,
|
audio_key: OnceCell<AudioKeyManager>,
|
||||||
channel: Lazy<ChannelManager>,
|
channel: OnceCell<ChannelManager>,
|
||||||
mercury: Lazy<MercuryManager>,
|
mercury: OnceCell<MercuryManager>,
|
||||||
cache: Option<Arc<Cache>>,
|
cache: Option<Arc<Cache>>,
|
||||||
|
|
||||||
handle: Remote,
|
handle: tokio::runtime::Handle,
|
||||||
|
|
||||||
session_id: usize,
|
session_id: usize,
|
||||||
}
|
}
|
||||||
|
@ -50,127 +62,104 @@ static SESSION_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||||
pub struct Session(Arc<SessionInternal>);
|
pub struct Session(Arc<SessionInternal>);
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn connect(
|
pub async fn connect(
|
||||||
config: SessionConfig,
|
config: SessionConfig,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
cache: Option<Cache>,
|
cache: Option<Cache>,
|
||||||
handle: Handle,
|
) -> Result<Session, SessionError> {
|
||||||
) -> Box<dyn Future<Item = Session, Error = AuthenticationError>> {
|
let ap = apresolve(config.proxy.as_ref(), config.ap_port).await;
|
||||||
let access_point =
|
|
||||||
apresolve_or_fallback::<io::Error>(&handle, &config.proxy, &config.ap_port);
|
|
||||||
|
|
||||||
let handle_ = handle.clone();
|
info!("Connecting to AP \"{}\"", ap);
|
||||||
let proxy = config.proxy.clone();
|
let mut conn = connection::connect(ap, config.proxy.as_ref()).await?;
|
||||||
let connection = access_point
|
|
||||||
.and_then(move |addr| {
|
|
||||||
info!("Connecting to AP \"{}\"", addr);
|
|
||||||
connection::connect(addr, &handle_, &proxy)
|
|
||||||
})
|
|
||||||
.map_err(|io_err| io_err.into());
|
|
||||||
|
|
||||||
let device_id = config.device_id.clone();
|
let reusable_credentials =
|
||||||
let authentication = connection.and_then(move |connection| {
|
connection::authenticate(&mut conn, credentials, &config.device_id).await?;
|
||||||
connection::authenticate(connection, credentials, device_id)
|
info!("Authenticated as \"{}\" !", reusable_credentials.username);
|
||||||
});
|
if let Some(cache) = &cache {
|
||||||
|
cache.save_credentials(&reusable_credentials);
|
||||||
|
}
|
||||||
|
|
||||||
let result = authentication.map(move |(transport, reusable_credentials)| {
|
let session = Session::create(
|
||||||
info!("Authenticated as \"{}\" !", reusable_credentials.username);
|
conn,
|
||||||
if let Some(ref cache) = cache {
|
config,
|
||||||
cache.save_credentials(&reusable_credentials);
|
cache,
|
||||||
}
|
reusable_credentials.username,
|
||||||
|
tokio::runtime::Handle::current(),
|
||||||
|
);
|
||||||
|
|
||||||
let (session, task) = Session::create(
|
Ok(session)
|
||||||
&handle,
|
|
||||||
transport,
|
|
||||||
config,
|
|
||||||
cache,
|
|
||||||
reusable_credentials.username.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
handle.spawn(task.map_err(|e| {
|
|
||||||
error!("{:?}", e);
|
|
||||||
}));
|
|
||||||
|
|
||||||
session
|
|
||||||
});
|
|
||||||
|
|
||||||
Box::new(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create(
|
fn create(
|
||||||
handle: &Handle,
|
|
||||||
transport: connection::Transport,
|
transport: connection::Transport,
|
||||||
config: SessionConfig,
|
config: SessionConfig,
|
||||||
cache: Option<Cache>,
|
cache: Option<Cache>,
|
||||||
username: String,
|
username: String,
|
||||||
) -> (Session, Box<dyn Future<Item = (), Error = io::Error>>) {
|
handle: tokio::runtime::Handle,
|
||||||
|
) -> Session {
|
||||||
let (sink, stream) = transport.split();
|
let (sink, stream) = transport.split();
|
||||||
|
|
||||||
let (sender_tx, sender_rx) = mpsc::unbounded();
|
let (sender_tx, sender_rx) = mpsc::unbounded_channel();
|
||||||
let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
|
let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
debug!("new Session[{}]", session_id);
|
debug!("new Session[{}]", session_id);
|
||||||
|
|
||||||
let session = Session(Arc::new(SessionInternal {
|
let session = Session(Arc::new(SessionInternal {
|
||||||
config: config,
|
config,
|
||||||
data: RwLock::new(SessionData {
|
data: RwLock::new(SessionData {
|
||||||
country: String::new(),
|
country: String::new(),
|
||||||
canonical_username: username,
|
canonical_username: username,
|
||||||
invalid: false,
|
invalid: false,
|
||||||
time_delta: 0,
|
time_delta: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
tx_connection: sender_tx,
|
tx_connection: sender_tx,
|
||||||
|
|
||||||
cache: cache.map(Arc::new),
|
cache: cache.map(Arc::new),
|
||||||
|
audio_key: OnceCell::new(),
|
||||||
audio_key: Lazy::new(),
|
channel: OnceCell::new(),
|
||||||
channel: Lazy::new(),
|
mercury: OnceCell::new(),
|
||||||
mercury: Lazy::new(),
|
handle,
|
||||||
|
session_id,
|
||||||
handle: handle.remote().clone(),
|
|
||||||
|
|
||||||
session_id: session_id,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let sender_task = sender_rx
|
let sender_task = UnboundedReceiverStream::new(sender_rx)
|
||||||
.map_err(|e| -> io::Error { panic!(e) })
|
.map(Ok)
|
||||||
.forward(sink)
|
.forward(sink);
|
||||||
.map(|_| ());
|
|
||||||
let receiver_task = DispatchTask(stream, session.weak());
|
let receiver_task = DispatchTask(stream, session.weak());
|
||||||
|
|
||||||
let task = Box::new(
|
let task =
|
||||||
(receiver_task, sender_task)
|
futures_util::future::join(sender_task, receiver_task).map(|_| io::Result::<_>::Ok(()));
|
||||||
.into_future()
|
tokio::spawn(task);
|
||||||
.map(|((), ())| ()),
|
session
|
||||||
);
|
|
||||||
|
|
||||||
(session, task)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn audio_key(&self) -> &AudioKeyManager {
|
pub fn audio_key(&self) -> &AudioKeyManager {
|
||||||
self.0.audio_key.get(|| AudioKeyManager::new(self.weak()))
|
self.0
|
||||||
|
.audio_key
|
||||||
|
.get_or_init(|| AudioKeyManager::new(self.weak()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn channel(&self) -> &ChannelManager {
|
pub fn channel(&self) -> &ChannelManager {
|
||||||
self.0.channel.get(|| ChannelManager::new(self.weak()))
|
self.0
|
||||||
|
.channel
|
||||||
|
.get_or_init(|| ChannelManager::new(self.weak()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mercury(&self) -> &MercuryManager {
|
pub fn mercury(&self) -> &MercuryManager {
|
||||||
self.0.mercury.get(|| MercuryManager::new(self.weak()))
|
self.0
|
||||||
|
.mercury
|
||||||
|
.get_or_init(|| MercuryManager::new(self.weak()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time_delta(&self) -> i64 {
|
pub fn time_delta(&self) -> i64 {
|
||||||
self.0.data.read().unwrap().time_delta
|
self.0.data.read().unwrap().time_delta
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn<F, R>(&self, f: F)
|
pub fn spawn<T>(&self, task: T)
|
||||||
where
|
where
|
||||||
F: FnOnce(&Handle) -> R + Send + 'static,
|
T: Future + Send + 'static,
|
||||||
R: IntoFuture<Item = (), Error = ()>,
|
T::Output: Send + 'static,
|
||||||
R::Future: 'static,
|
|
||||||
{
|
{
|
||||||
self.0.handle.spawn(f)
|
self.0.handle.spawn(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug_info(&self) {
|
fn debug_info(&self) {
|
||||||
|
@ -182,7 +171,7 @@ impl Session {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
|
#[allow(clippy::match_same_arms)]
|
||||||
fn dispatch(&self, cmd: u8, data: Bytes) {
|
fn dispatch(&self, cmd: u8, data: Bytes) {
|
||||||
match cmd {
|
match cmd {
|
||||||
0x4 => {
|
0x4 => {
|
||||||
|
@ -213,7 +202,7 @@ impl Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_packet(&self, cmd: u8, data: Vec<u8>) {
|
pub fn send_packet(&self, cmd: u8, data: Vec<u8>) {
|
||||||
self.0.tx_connection.unbounded_send((cmd, data)).unwrap();
|
self.0.tx_connection.send((cmd, data)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cache(&self) -> Option<&Arc<Cache>> {
|
pub fn cache(&self) -> Option<&Arc<Cache>> {
|
||||||
|
@ -277,35 +266,34 @@ impl Drop for SessionInternal {
|
||||||
|
|
||||||
struct DispatchTask<S>(S, SessionWeak)
|
struct DispatchTask<S>(S, SessionWeak)
|
||||||
where
|
where
|
||||||
S: Stream<Item = (u8, Bytes)>;
|
S: TryStream<Ok = (u8, Bytes)> + Unpin;
|
||||||
|
|
||||||
impl<S> Future for DispatchTask<S>
|
impl<S> Future for DispatchTask<S>
|
||||||
where
|
where
|
||||||
S: Stream<Item = (u8, Bytes)>,
|
S: TryStream<Ok = (u8, Bytes)> + Unpin,
|
||||||
<S as Stream>::Error: ::std::fmt::Debug,
|
<S as TryStream>::Ok: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
type Item = ();
|
type Output = Result<(), S::Error>;
|
||||||
type Error = S::Error;
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
let session = match self.1.try_upgrade() {
|
let session = match self.1.try_upgrade() {
|
||||||
Some(session) => session,
|
Some(session) => session,
|
||||||
None => return Ok(Async::Ready(())),
|
None => return Poll::Ready(Ok(())),
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (cmd, data) = match self.0.poll() {
|
let (cmd, data) = match self.0.try_poll_next_unpin(cx) {
|
||||||
Ok(Async::Ready(Some(t))) => t,
|
Poll::Ready(Some(Ok(t))) => t,
|
||||||
Ok(Async::Ready(None)) => {
|
Poll::Ready(None) => {
|
||||||
warn!("Connection to server closed.");
|
warn!("Connection to server closed.");
|
||||||
session.shutdown();
|
session.shutdown();
|
||||||
return Ok(Async::Ready(()));
|
return Poll::Ready(Ok(()));
|
||||||
}
|
}
|
||||||
Ok(Async::NotReady) => return Ok(Async::NotReady),
|
Poll::Ready(Some(Err(e))) => {
|
||||||
Err(e) => {
|
|
||||||
session.shutdown();
|
session.shutdown();
|
||||||
return Err(From::from(e));
|
return Poll::Ready(Err(e));
|
||||||
}
|
}
|
||||||
|
Poll::Pending => return Poll::Pending,
|
||||||
};
|
};
|
||||||
|
|
||||||
session.dispatch(cmd, data);
|
session.dispatch(cmd, data);
|
||||||
|
@ -315,7 +303,7 @@ where
|
||||||
|
|
||||||
impl<S> Drop for DispatchTask<S>
|
impl<S> Drop for DispatchTask<S>
|
||||||
where
|
where
|
||||||
S: Stream<Item = (u8, Bytes)>,
|
S: TryStream<Ok = (u8, Bytes)> + Unpin,
|
||||||
{
|
{
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
debug!("drop Dispatch");
|
debug!("drop Dispatch");
|
||||||
|
|
|
@ -18,9 +18,9 @@ impl From<&str> for SpotifyAudioType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<&str> for SpotifyAudioType {
|
impl From<SpotifyAudioType> for &str {
|
||||||
fn into(self) -> &'static str {
|
fn from(audio_type: SpotifyAudioType) -> &'static str {
|
||||||
match self {
|
match audio_type {
|
||||||
SpotifyAudioType::Track => "track",
|
SpotifyAudioType::Track => "track",
|
||||||
SpotifyAudioType::Podcast => "episode",
|
SpotifyAudioType::Podcast => "episode",
|
||||||
SpotifyAudioType::NonPlayable => "unknown",
|
SpotifyAudioType::NonPlayable => "unknown",
|
||||||
|
@ -45,7 +45,7 @@ impl SpotifyId {
|
||||||
const SIZE_BASE16: usize = 32;
|
const SIZE_BASE16: usize = 32;
|
||||||
const SIZE_BASE62: usize = 22;
|
const SIZE_BASE62: usize = 22;
|
||||||
|
|
||||||
fn as_track(n: u128) -> SpotifyId {
|
fn track(n: u128) -> SpotifyId {
|
||||||
SpotifyId {
|
SpotifyId {
|
||||||
id: n,
|
id: n,
|
||||||
audio_type: SpotifyAudioType::Track,
|
audio_type: SpotifyAudioType::Track,
|
||||||
|
@ -71,7 +71,7 @@ impl SpotifyId {
|
||||||
dst += p;
|
dst += p;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SpotifyId::as_track(dst))
|
Ok(SpotifyId::track(dst))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a base62 encoded [Spotify ID] into a `SpotifyId`.
|
/// Parses a base62 encoded [Spotify ID] into a `SpotifyId`.
|
||||||
|
@ -94,7 +94,7 @@ impl SpotifyId {
|
||||||
dst += p;
|
dst += p;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SpotifyId::as_track(dst))
|
Ok(SpotifyId::track(dst))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `SpotifyId` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
|
/// Creates a `SpotifyId` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
|
||||||
|
@ -102,7 +102,7 @@ impl SpotifyId {
|
||||||
/// The resulting `SpotifyId` will default to a `SpotifyAudioType::TRACK`.
|
/// The resulting `SpotifyId` will default to a `SpotifyAudioType::TRACK`.
|
||||||
pub fn from_raw(src: &[u8]) -> Result<SpotifyId, SpotifyIdError> {
|
pub fn from_raw(src: &[u8]) -> Result<SpotifyId, SpotifyIdError> {
|
||||||
match src.try_into() {
|
match src.try_into() {
|
||||||
Ok(dst) => Ok(SpotifyId::as_track(u128::from_be_bytes(dst))),
|
Ok(dst) => Ok(SpotifyId::track(u128::from_be_bytes(dst))),
|
||||||
Err(_) => Err(SpotifyIdError),
|
Err(_) => Err(SpotifyIdError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
core/src/util.rs
Normal file
29
core/src/util.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
pub trait Seq {
|
||||||
|
fn next(&self) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_seq {
|
||||||
|
($($ty:ty)*) => { $(
|
||||||
|
impl Seq for $ty {
|
||||||
|
fn next(&self) -> Self { (*self).wrapping_add(1) }
|
||||||
|
}
|
||||||
|
)* }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_seq!(u8 u16 u32 u64 usize);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||||
|
pub struct SeqGenerator<T: Seq>(T);
|
||||||
|
|
||||||
|
impl<T: Seq> SeqGenerator<T> {
|
||||||
|
pub fn new(value: T) -> Self {
|
||||||
|
SeqGenerator(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&mut self) -> T {
|
||||||
|
let value = self.0.next();
|
||||||
|
mem::replace(&mut self.0, value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,75 +0,0 @@
|
||||||
use num_bigint::BigUint;
|
|
||||||
use num_integer::Integer;
|
|
||||||
use num_traits::{One, Zero};
|
|
||||||
use rand::Rng;
|
|
||||||
use std::mem;
|
|
||||||
use std::ops::{Mul, Rem, Shr};
|
|
||||||
|
|
||||||
pub fn rand_vec<G: Rng>(rng: &mut G, size: usize) -> Vec<u8> {
|
|
||||||
::std::iter::repeat(())
|
|
||||||
.map(|()| rng.gen())
|
|
||||||
.take(size)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn url_encode(inp: &str) -> String {
|
|
||||||
let mut encoded = String::new();
|
|
||||||
|
|
||||||
for c in inp.as_bytes().iter() {
|
|
||||||
match *c as char {
|
|
||||||
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' | ':' | '/' => {
|
|
||||||
encoded.push(*c as char)
|
|
||||||
}
|
|
||||||
c => encoded.push_str(format!("%{:02X}", c as u32).as_str()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
|
|
||||||
let mut base = base.clone();
|
|
||||||
let mut exp = exp.clone();
|
|
||||||
let mut result: BigUint = One::one();
|
|
||||||
|
|
||||||
while !exp.is_zero() {
|
|
||||||
if exp.is_odd() {
|
|
||||||
result = result.mul(&base).rem(modulus);
|
|
||||||
}
|
|
||||||
exp = exp.shr(1);
|
|
||||||
base = (&base).mul(&base).rem(modulus);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ReadSeek: ::std::io::Read + ::std::io::Seek {}
|
|
||||||
impl<T: ::std::io::Read + ::std::io::Seek> ReadSeek for T {}
|
|
||||||
|
|
||||||
pub trait Seq {
|
|
||||||
fn next(&self) -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_seq {
|
|
||||||
($($ty:ty)*) => { $(
|
|
||||||
impl Seq for $ty {
|
|
||||||
fn next(&self) -> Self { (*self).wrapping_add(1) }
|
|
||||||
}
|
|
||||||
)* }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_seq!(u8 u16 u32 u64 usize);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
|
|
||||||
pub struct SeqGenerator<T: Seq>(T);
|
|
||||||
|
|
||||||
impl<T: Seq> SeqGenerator<T> {
|
|
||||||
pub fn new(value: T) -> Self {
|
|
||||||
SeqGenerator(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&mut self) -> T {
|
|
||||||
let value = self.0.next();
|
|
||||||
mem::replace(&mut self.0, value)
|
|
||||||
}
|
|
||||||
}
|
|
18
core/tests/connect.rs
Normal file
18
core/tests/connect.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
use librespot_core::authentication::Credentials;
|
||||||
|
use librespot_core::config::SessionConfig;
|
||||||
|
use librespot_core::session::Session;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_connection() {
|
||||||
|
let result = Session::connect(
|
||||||
|
SessionConfig::default(),
|
||||||
|
Credentials::with_password("test", "test"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => panic!("Authentication succeeded despite of bad credentials."),
|
||||||
|
Err(e) => assert_eq!(e.to_string(), "Login failed with reason: Bad credentials"),
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
use tokio_core::reactor::Core;
|
|
||||||
|
|
||||||
use librespot::core::authentication::Credentials;
|
use librespot::core::authentication::Credentials;
|
||||||
use librespot::core::config::SessionConfig;
|
use librespot::core::config::SessionConfig;
|
||||||
|
@ -9,29 +8,26 @@ use librespot::core::session::Session;
|
||||||
const SCOPES: &str =
|
const SCOPES: &str =
|
||||||
"streaming,user-read-playback-state,user-modify-playback-state,user-read-currently-playing";
|
"streaming,user-read-playback-state,user-modify-playback-state,user-read-currently-playing";
|
||||||
|
|
||||||
fn main() {
|
#[tokio::main]
|
||||||
let mut core = Core::new().unwrap();
|
async fn main() {
|
||||||
let handle = core.handle();
|
|
||||||
|
|
||||||
let session_config = SessionConfig::default();
|
let session_config = SessionConfig::default();
|
||||||
|
|
||||||
let args: Vec<_> = env::args().collect();
|
let args: Vec<_> = env::args().collect();
|
||||||
if args.len() != 4 {
|
if args.len() != 4 {
|
||||||
println!("Usage: {} USERNAME PASSWORD CLIENT_ID", args[0]);
|
eprintln!("Usage: {} USERNAME PASSWORD CLIENT_ID", args[0]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
let username = args[1].to_owned();
|
|
||||||
let password = args[2].to_owned();
|
|
||||||
let client_id = &args[3];
|
|
||||||
|
|
||||||
println!("Connecting..");
|
println!("Connecting..");
|
||||||
let credentials = Credentials::with_password(username, password);
|
let credentials = Credentials::with_password(&args[1], &args[2]);
|
||||||
let session = core
|
let session = Session::connect(session_config, credentials, None)
|
||||||
.run(Session::connect(session_config, credentials, None, handle))
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Token: {:#?}",
|
"Token: {:#?}",
|
||||||
core.run(keymaster::get_token(&session, &client_id, SCOPES))
|
keymaster::get_token(&session, &args[3], SCOPES)
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,44 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
use tokio_core::reactor::Core;
|
|
||||||
|
|
||||||
use librespot::core::authentication::Credentials;
|
use librespot::core::authentication::Credentials;
|
||||||
use librespot::core::config::SessionConfig;
|
use librespot::core::config::SessionConfig;
|
||||||
use librespot::core::session::Session;
|
use librespot::core::session::Session;
|
||||||
use librespot::core::spotify_id::SpotifyId;
|
use librespot::core::spotify_id::SpotifyId;
|
||||||
use librespot::playback::config::{AudioFormat, PlayerConfig};
|
|
||||||
|
|
||||||
use librespot::playback::audio_backend;
|
use librespot::playback::audio_backend;
|
||||||
|
use librespot::playback::config::{AudioFormat, PlayerConfig};
|
||||||
use librespot::playback::player::Player;
|
use librespot::playback::player::Player;
|
||||||
|
|
||||||
fn main() {
|
#[tokio::main]
|
||||||
let mut core = Core::new().unwrap();
|
async fn main() {
|
||||||
let handle = core.handle();
|
|
||||||
|
|
||||||
let session_config = SessionConfig::default();
|
let session_config = SessionConfig::default();
|
||||||
let player_config = PlayerConfig::default();
|
let player_config = PlayerConfig::default();
|
||||||
let audio_format = AudioFormat::default();
|
let audio_format = AudioFormat::default();
|
||||||
|
|
||||||
let args: Vec<_> = env::args().collect();
|
let args: Vec<_> = env::args().collect();
|
||||||
if args.len() != 4 {
|
if args.len() != 4 {
|
||||||
println!("Usage: {} USERNAME PASSWORD TRACK", args[0]);
|
eprintln!("Usage: {} USERNAME PASSWORD TRACK", args[0]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
let username = args[1].to_owned();
|
let credentials = Credentials::with_password(&args[1], &args[2]);
|
||||||
let password = args[2].to_owned();
|
|
||||||
let credentials = Credentials::with_password(username, password);
|
|
||||||
|
|
||||||
let track = SpotifyId::from_base62(&args[3]).unwrap();
|
let track = SpotifyId::from_base62(&args[3]).unwrap();
|
||||||
|
|
||||||
let backend = audio_backend::find(None).unwrap();
|
let backend = audio_backend::find(None).unwrap();
|
||||||
|
|
||||||
println!("Connecting ..");
|
println!("Connecting ..");
|
||||||
let session = core
|
let session = Session::connect(session_config, credentials, None)
|
||||||
.run(Session::connect(session_config, credentials, None, handle))
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (mut player, _) = Player::new(player_config, session.clone(), None, move || {
|
let (mut player, _) = Player::new(player_config, session, None, move || {
|
||||||
(backend)(None, audio_format)
|
backend(None, audio_format)
|
||||||
});
|
});
|
||||||
|
|
||||||
player.load(track, true, 0);
|
player.load(track, true, 0);
|
||||||
|
|
||||||
println!("Playing...");
|
println!("Playing...");
|
||||||
core.run(player.get_end_of_track_future()).unwrap();
|
|
||||||
|
player.await_end_of_track().await;
|
||||||
|
|
||||||
println!("Done");
|
println!("Done");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use env_logger;
|
use env_logger;
|
||||||
use std::env;
|
use std::env;
|
||||||
use tokio_core::reactor::Core;
|
|
||||||
|
|
||||||
use librespot::core::authentication::Credentials;
|
use librespot::core::authentication::Credentials;
|
||||||
use librespot::core::config::SessionConfig;
|
use librespot::core::config::SessionConfig;
|
||||||
|
@ -8,35 +7,32 @@ use librespot::core::session::Session;
|
||||||
use librespot::core::spotify_id::SpotifyId;
|
use librespot::core::spotify_id::SpotifyId;
|
||||||
use librespot::metadata::{Metadata, Playlist, Track};
|
use librespot::metadata::{Metadata, Playlist, Track};
|
||||||
|
|
||||||
fn main() {
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let mut core = Core::new().unwrap();
|
|
||||||
let handle = core.handle();
|
|
||||||
|
|
||||||
let session_config = SessionConfig::default();
|
let session_config = SessionConfig::default();
|
||||||
|
|
||||||
let args: Vec<_> = env::args().collect();
|
let args: Vec<_> = env::args().collect();
|
||||||
if args.len() != 4 {
|
if args.len() != 4 {
|
||||||
println!("Usage: {} USERNAME PASSWORD PLAYLIST", args[0]);
|
eprintln!("Usage: {} USERNAME PASSWORD PLAYLIST", args[0]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
let username = args[1].to_owned();
|
let credentials = Credentials::with_password(&args[1], &args[2]);
|
||||||
let password = args[2].to_owned();
|
|
||||||
let credentials = Credentials::with_password(username, password);
|
|
||||||
|
|
||||||
let uri_split = args[3].split(":");
|
let uri_split = args[3].split(':');
|
||||||
let uri_parts: Vec<&str> = uri_split.collect();
|
let uri_parts: Vec<&str> = uri_split.collect();
|
||||||
println!("{}, {}, {}", uri_parts[0], uri_parts[1], uri_parts[2]);
|
println!("{}, {}, {}", uri_parts[0], uri_parts[1], uri_parts[2]);
|
||||||
|
|
||||||
let plist_uri = SpotifyId::from_base62(uri_parts[2]).unwrap();
|
let plist_uri = SpotifyId::from_base62(uri_parts[2]).unwrap();
|
||||||
|
|
||||||
let session = core
|
let session = Session::connect(session_config, credentials, None)
|
||||||
.run(Session::connect(session_config, credentials, None, handle))
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let plist = core.run(Playlist::get(&session, plist_uri)).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 = core.run(Track::get(&session, track_id)).unwrap();
|
let plist_track = Track::get(&session, track_id).await.unwrap();
|
||||||
println!("track: {} ", plist_track.name);
|
println!("track: {} ", plist_track.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,8 @@ repository = "https://github.com/librespot-org/librespot"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-trait = "0.1"
|
||||||
byteorder = "1.3"
|
byteorder = "1.3"
|
||||||
futures = "0.1"
|
|
||||||
linear-map = "1.2"
|
|
||||||
protobuf = "~2.14.0"
|
protobuf = "~2.14.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,19 @@
|
||||||
|
#![allow(clippy::unused_io_amount)]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
extern crate byteorder;
|
#[macro_use]
|
||||||
extern crate futures;
|
extern crate async_trait;
|
||||||
extern crate linear_map;
|
|
||||||
extern crate protobuf;
|
|
||||||
|
|
||||||
extern crate librespot_core;
|
|
||||||
extern crate librespot_protocol as protocol;
|
|
||||||
|
|
||||||
pub mod cover;
|
pub mod cover;
|
||||||
|
|
||||||
use futures::future;
|
use std::collections::HashMap;
|
||||||
use futures::Future;
|
|
||||||
use linear_map::LinearMap;
|
|
||||||
|
|
||||||
use librespot_core::mercury::MercuryError;
|
use librespot_core::mercury::MercuryError;
|
||||||
use librespot_core::session::Session;
|
use librespot_core::session::Session;
|
||||||
use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId};
|
use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId};
|
||||||
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
pub use crate::protocol::metadata::AudioFile_Format as FileFormat;
|
pub use crate::protocol::metadata::AudioFile_Format as FileFormat;
|
||||||
|
|
||||||
|
@ -61,7 +57,7 @@ where
|
||||||
pub struct AudioItem {
|
pub struct AudioItem {
|
||||||
pub id: SpotifyId,
|
pub id: SpotifyId,
|
||||||
pub uri: String,
|
pub uri: String,
|
||||||
pub files: LinearMap<FileFormat, FileId>,
|
pub files: HashMap<FileFormat, FileId>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub duration: i32,
|
pub duration: i32,
|
||||||
pub available: bool,
|
pub available: bool,
|
||||||
|
@ -69,81 +65,67 @@ pub struct AudioItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioItem {
|
impl AudioItem {
|
||||||
pub fn get_audio_item(
|
pub async fn get_audio_item(session: &Session, id: SpotifyId) -> Result<Self, MercuryError> {
|
||||||
session: &Session,
|
|
||||||
id: SpotifyId,
|
|
||||||
) -> Box<dyn Future<Item = AudioItem, Error = MercuryError>> {
|
|
||||||
match id.audio_type {
|
match id.audio_type {
|
||||||
SpotifyAudioType::Track => Track::get_audio_item(session, id),
|
SpotifyAudioType::Track => Track::get_audio_item(session, id).await,
|
||||||
SpotifyAudioType::Podcast => Episode::get_audio_item(session, id),
|
SpotifyAudioType::Podcast => Episode::get_audio_item(session, id).await,
|
||||||
SpotifyAudioType::NonPlayable => {
|
SpotifyAudioType::NonPlayable => Err(MercuryError),
|
||||||
Box::new(future::err::<AudioItem, MercuryError>(MercuryError))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
trait AudioFiles {
|
trait AudioFiles {
|
||||||
fn get_audio_item(
|
async fn get_audio_item(session: &Session, id: SpotifyId) -> Result<AudioItem, MercuryError>;
|
||||||
session: &Session,
|
|
||||||
id: SpotifyId,
|
|
||||||
) -> Box<dyn Future<Item = AudioItem, Error = MercuryError>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl AudioFiles for Track {
|
impl AudioFiles for Track {
|
||||||
fn get_audio_item(
|
async fn get_audio_item(session: &Session, id: SpotifyId) -> Result<AudioItem, MercuryError> {
|
||||||
session: &Session,
|
let item = Self::get(session, id).await?;
|
||||||
id: SpotifyId,
|
Ok(AudioItem {
|
||||||
) -> Box<dyn Future<Item = AudioItem, Error = MercuryError>> {
|
id,
|
||||||
Box::new(Self::get(session, id).and_then(move |item| {
|
uri: format!("spotify:track:{}", id.to_base62()),
|
||||||
Ok(AudioItem {
|
files: item.files,
|
||||||
id: id,
|
name: item.name,
|
||||||
uri: format!("spotify:track:{}", id.to_base62()),
|
duration: item.duration,
|
||||||
files: item.files,
|
available: item.available,
|
||||||
name: item.name,
|
alternatives: Some(item.alternatives),
|
||||||
duration: item.duration,
|
})
|
||||||
available: item.available,
|
|
||||||
alternatives: Some(item.alternatives),
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl AudioFiles for Episode {
|
impl AudioFiles for Episode {
|
||||||
fn get_audio_item(
|
async fn get_audio_item(session: &Session, id: SpotifyId) -> Result<AudioItem, MercuryError> {
|
||||||
session: &Session,
|
let item = Self::get(session, id).await?;
|
||||||
id: SpotifyId,
|
|
||||||
) -> Box<dyn Future<Item = AudioItem, Error = MercuryError>> {
|
Ok(AudioItem {
|
||||||
Box::new(Self::get(session, id).and_then(move |item| {
|
id,
|
||||||
Ok(AudioItem {
|
uri: format!("spotify:episode:{}", id.to_base62()),
|
||||||
id: id,
|
files: item.files,
|
||||||
uri: format!("spotify:episode:{}", id.to_base62()),
|
name: item.name,
|
||||||
files: item.files,
|
duration: item.duration,
|
||||||
name: item.name,
|
available: item.available,
|
||||||
duration: item.duration,
|
alternatives: None,
|
||||||
available: item.available,
|
})
|
||||||
alternatives: None,
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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;
|
fn request_url(id: SpotifyId) -> String;
|
||||||
fn parse(msg: &Self::Message, session: &Session) -> Self;
|
fn parse(msg: &Self::Message, session: &Session) -> Self;
|
||||||
|
|
||||||
fn get(session: &Session, id: SpotifyId) -> Box<dyn Future<Item = Self, Error = MercuryError>> {
|
async fn get(session: &Session, id: SpotifyId) -> Result<Self, MercuryError> {
|
||||||
let uri = Self::request_url(id);
|
let uri = Self::request_url(id);
|
||||||
let request = session.mercury().get(uri);
|
let response = session.mercury().get(uri).await?;
|
||||||
|
let data = response.payload.first().expect("Empty payload");
|
||||||
|
let msg: Self::Message = protobuf::parse_from_bytes(data).unwrap();
|
||||||
|
|
||||||
let session = session.clone();
|
Ok(Self::parse(&msg, &session))
|
||||||
Box::new(request.and_then(move |response| {
|
|
||||||
let data = response.payload.first().expect("Empty payload");
|
|
||||||
let msg: Self::Message = protobuf::parse_from_bytes(data).unwrap();
|
|
||||||
|
|
||||||
Ok(Self::parse(&msg, &session))
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +136,7 @@ pub struct Track {
|
||||||
pub duration: i32,
|
pub duration: i32,
|
||||||
pub album: SpotifyId,
|
pub album: SpotifyId,
|
||||||
pub artists: Vec<SpotifyId>,
|
pub artists: Vec<SpotifyId>,
|
||||||
pub files: LinearMap<FileFormat, FileId>,
|
pub files: HashMap<FileFormat, FileId>,
|
||||||
pub alternatives: Vec<SpotifyId>,
|
pub alternatives: Vec<SpotifyId>,
|
||||||
pub available: bool,
|
pub available: bool,
|
||||||
}
|
}
|
||||||
|
@ -176,7 +158,7 @@ pub struct Episode {
|
||||||
pub duration: i32,
|
pub duration: i32,
|
||||||
pub language: String,
|
pub language: String,
|
||||||
pub show: SpotifyId,
|
pub show: SpotifyId,
|
||||||
pub files: LinearMap<FileFormat, FileId>,
|
pub files: HashMap<FileFormat, FileId>,
|
||||||
pub covers: Vec<FileId>,
|
pub covers: Vec<FileId>,
|
||||||
pub available: bool,
|
pub available: bool,
|
||||||
pub explicit: bool,
|
pub explicit: bool,
|
||||||
|
@ -239,8 +221,8 @@ impl Metadata for Track {
|
||||||
name: msg.get_name().to_owned(),
|
name: msg.get_name().to_owned(),
|
||||||
duration: msg.get_duration(),
|
duration: msg.get_duration(),
|
||||||
album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(),
|
album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(),
|
||||||
artists: artists,
|
artists,
|
||||||
files: files,
|
files,
|
||||||
alternatives: msg
|
alternatives: msg
|
||||||
.get_alternative()
|
.get_alternative()
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -289,9 +271,9 @@ impl Metadata for Album {
|
||||||
Album {
|
Album {
|
||||||
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
||||||
name: msg.get_name().to_owned(),
|
name: msg.get_name().to_owned(),
|
||||||
artists: artists,
|
artists,
|
||||||
tracks: tracks,
|
tracks,
|
||||||
covers: covers,
|
covers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,7 +291,7 @@ impl Metadata for Playlist {
|
||||||
.get_items()
|
.get_items()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|item| {
|
.map(|item| {
|
||||||
let uri_split = item.get_uri().split(":");
|
let uri_split = item.get_uri().split(':');
|
||||||
let uri_parts: Vec<&str> = uri_split.collect();
|
let uri_parts: Vec<&str> = uri_split.collect();
|
||||||
SpotifyId::from_base62(uri_parts[2]).unwrap()
|
SpotifyId::from_base62(uri_parts[2]).unwrap()
|
||||||
})
|
})
|
||||||
|
@ -326,7 +308,7 @@ impl Metadata for Playlist {
|
||||||
Playlist {
|
Playlist {
|
||||||
revision: msg.get_revision().to_vec(),
|
revision: msg.get_revision().to_vec(),
|
||||||
name: msg.get_attributes().get_name().to_owned(),
|
name: msg.get_attributes().get_name().to_owned(),
|
||||||
tracks: tracks,
|
tracks,
|
||||||
user: msg.get_owner_username().to_string(),
|
user: msg.get_owner_username().to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -359,7 +341,7 @@ impl Metadata for Artist {
|
||||||
Artist {
|
Artist {
|
||||||
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
||||||
name: msg.get_name().to_owned(),
|
name: msg.get_name().to_owned(),
|
||||||
top_tracks: top_tracks,
|
top_tracks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -405,8 +387,8 @@ impl Metadata for Episode {
|
||||||
duration: msg.get_duration().to_owned(),
|
duration: msg.get_duration().to_owned(),
|
||||||
language: msg.get_language().to_owned(),
|
language: msg.get_language().to_owned(),
|
||||||
show: SpotifyId::from_raw(msg.get_show().get_gid()).unwrap(),
|
show: SpotifyId::from_raw(msg.get_show().get_gid()).unwrap(),
|
||||||
covers: covers,
|
covers,
|
||||||
files: files,
|
files,
|
||||||
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
|
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
|
||||||
explicit: msg.get_explicit().to_owned(),
|
explicit: msg.get_explicit().to_owned(),
|
||||||
}
|
}
|
||||||
|
@ -444,8 +426,8 @@ impl Metadata for Show {
|
||||||
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
||||||
name: msg.get_name().to_owned(),
|
name: msg.get_name().to_owned(),
|
||||||
publisher: msg.get_publisher().to_owned(),
|
publisher: msg.get_publisher().to_owned(),
|
||||||
episodes: episodes,
|
episodes,
|
||||||
covers: covers,
|
covers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,12 @@ path = "../metadata"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures = "0.1"
|
futures-executor = "0.3"
|
||||||
|
futures-util = { version = "0.3", default_features = false, features = ["alloc"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
byteorder = "1.3"
|
byteorder = "1.4"
|
||||||
shell-words = "1.0.0"
|
shell-words = "1.0.0"
|
||||||
|
tokio = { version = "1", features = ["sync"] }
|
||||||
|
|
||||||
alsa = { version = "0.5", optional = true }
|
alsa = { version = "0.5", optional = true }
|
||||||
portaudio-rs = { version = "0.3", optional = true }
|
portaudio-rs = { version = "0.3", optional = true }
|
||||||
|
@ -29,20 +31,23 @@ libpulse-binding = { version = "2", optional = true, default-features = f
|
||||||
libpulse-simple-binding = { version = "2", optional = true, default-features = false }
|
libpulse-simple-binding = { version = "2", optional = true, default-features = false }
|
||||||
jack = { version = "0.6", optional = true }
|
jack = { version = "0.6", optional = true }
|
||||||
libc = { version = "0.2", optional = true }
|
libc = { version = "0.2", optional = true }
|
||||||
rodio = { version = "0.13", optional = true, default-features = false }
|
|
||||||
cpal = { version = "0.13", optional = true }
|
|
||||||
sdl2 = { version = "0.34.3", optional = true }
|
sdl2 = { version = "0.34.3", optional = true }
|
||||||
gstreamer = { version = "0.16", optional = true }
|
gstreamer = { version = "0.16", optional = true }
|
||||||
gstreamer-app = { version = "0.16", optional = true }
|
gstreamer-app = { version = "0.16", optional = true }
|
||||||
glib = { version = "0.10", optional = true }
|
glib = { version = "0.10", optional = true }
|
||||||
zerocopy = { version = "0.3" }
|
zerocopy = { version = "0.3" }
|
||||||
|
|
||||||
|
# Rodio dependencies
|
||||||
|
rodio = { version = "0.13", optional = true, default-features = false }
|
||||||
|
cpal = { version = "0.13", optional = true }
|
||||||
|
thiserror = { version = "1", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
alsa-backend = ["alsa"]
|
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"]
|
||||||
rodiojack-backend = ["rodio", "cpal/jack"]
|
rodio-backend = ["rodio", "cpal", "thiserror"]
|
||||||
rodio-backend = ["rodio", "cpal"]
|
rodiojack-backend = ["rodio", "cpal/jack", "thiserror"]
|
||||||
sdl-backend = ["sdl2"]
|
sdl-backend = ["sdl2"]
|
||||||
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"]
|
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"]
|
||||||
|
|
|
@ -87,7 +87,7 @@ impl Open for AlsaSink {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
pcm: None,
|
pcm: None,
|
||||||
format: format,
|
format,
|
||||||
device: name,
|
device: name,
|
||||||
buffer: vec![],
|
buffer: vec![],
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@ impl SinkAsBytes for AlsaSink {
|
||||||
.extend_from_slice(&data[processed_data..processed_data + data_to_buffer]);
|
.extend_from_slice(&data[processed_data..processed_data + data_to_buffer]);
|
||||||
processed_data += data_to_buffer;
|
processed_data += data_to_buffer;
|
||||||
if self.buffer.len() == self.buffer.capacity() {
|
if self.buffer.len() == self.buffer.capacity() {
|
||||||
self.write_buf().expect("could not append to buffer");
|
self.write_buf();
|
||||||
self.buffer.clear();
|
self.buffer.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,14 +156,12 @@ impl SinkAsBytes for AlsaSink {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AlsaSink {
|
impl AlsaSink {
|
||||||
fn write_buf(&mut self) -> io::Result<()> {
|
fn write_buf(&mut self) {
|
||||||
let pcm = self.pcm.as_mut().unwrap();
|
let pcm = self.pcm.as_mut().unwrap();
|
||||||
let io = pcm.io_bytes();
|
let io = pcm.io_bytes();
|
||||||
match io.writei(&self.buffer) {
|
match io.writei(&self.buffer) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(err) => pcm.try_recover(err, false).unwrap(),
|
Err(err) => pcm.try_recover(err, false).unwrap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,15 @@ use super::{Open, Sink, SinkAsBytes};
|
||||||
use crate::audio::AudioPacket;
|
use crate::audio::AudioPacket;
|
||||||
use crate::config::AudioFormat;
|
use crate::config::AudioFormat;
|
||||||
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
||||||
|
|
||||||
|
use gstreamer as gst;
|
||||||
|
use gstreamer_app as gst_app;
|
||||||
|
|
||||||
use gst::prelude::*;
|
use gst::prelude::*;
|
||||||
use gst::*;
|
use zerocopy::AsBytes;
|
||||||
|
|
||||||
use std::sync::mpsc::{sync_channel, SyncSender};
|
use std::sync::mpsc::{sync_channel, SyncSender};
|
||||||
use std::{io, thread};
|
use std::{io, thread};
|
||||||
use zerocopy::AsBytes;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct GstreamerSink {
|
pub struct GstreamerSink {
|
||||||
|
@ -68,14 +72,13 @@ impl Open for GstreamerSink {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
for data in rx {
|
for data in rx {
|
||||||
let buffer = bufferpool.acquire_buffer(None);
|
let buffer = bufferpool.acquire_buffer(None);
|
||||||
if !buffer.is_err() {
|
if let Ok(mut buffer) = buffer {
|
||||||
let mut okbuffer = buffer.unwrap();
|
let mutbuf = buffer.make_mut();
|
||||||
let mutbuf = okbuffer.make_mut();
|
|
||||||
mutbuf.set_size(data.len());
|
mutbuf.set_size(data.len());
|
||||||
mutbuf
|
mutbuf
|
||||||
.copy_from_slice(0, data.as_bytes())
|
.copy_from_slice(0, data.as_bytes())
|
||||||
.expect("failed to copy from slice");
|
.expect("Failed to copy from slice");
|
||||||
let _eat = appsrc.push_buffer(okbuffer);
|
let _eat = appsrc.push_buffer(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -85,8 +88,8 @@ impl Open for GstreamerSink {
|
||||||
let watch_mainloop = thread_mainloop.clone();
|
let watch_mainloop = thread_mainloop.clone();
|
||||||
bus.add_watch(move |_, msg| {
|
bus.add_watch(move |_, msg| {
|
||||||
match msg.view() {
|
match msg.view() {
|
||||||
MessageView::Eos(..) => watch_mainloop.quit(),
|
gst::MessageView::Eos(..) => watch_mainloop.quit(),
|
||||||
MessageView::Error(err) => {
|
gst::MessageView::Error(err) => {
|
||||||
println!(
|
println!(
|
||||||
"Error from {:?}: {} ({:?})",
|
"Error from {:?}: {} ({:?})",
|
||||||
err.get_src().map(|s| s.get_path_string()),
|
err.get_src().map(|s| s.get_path_string()),
|
||||||
|
@ -109,9 +112,9 @@ impl Open for GstreamerSink {
|
||||||
.expect("unable to set the pipeline to the `Playing` state");
|
.expect("unable to set the pipeline to the `Playing` state");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
tx: tx,
|
tx,
|
||||||
pipeline: pipeline,
|
pipeline,
|
||||||
format: format,
|
format,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ impl Open for JackSink {
|
||||||
}
|
}
|
||||||
info!("Using JACK sink with format {:?}", AudioFormat::F32);
|
info!("Using JACK sink with format {:?}", AudioFormat::F32);
|
||||||
|
|
||||||
let client_name = client_name.unwrap_or("librespot".to_string());
|
let client_name = client_name.unwrap_or_else(|| "librespot".to_string());
|
||||||
let (client, _status) =
|
let (client, _status) =
|
||||||
Client::new(&client_name[..], ClientOptions::NO_START_SERVER).unwrap();
|
Client::new(&client_name[..], ClientOptions::NO_START_SERVER).unwrap();
|
||||||
let ch_r = client.register_port("out_0", AudioOut::default()).unwrap();
|
let ch_r = client.register_port("out_0", AudioOut::default()).unwrap();
|
||||||
|
@ -63,7 +63,7 @@ impl Open for JackSink {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
send: tx,
|
send: tx,
|
||||||
active_client: active_client,
|
active_client,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ pub trait Sink {
|
||||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()>;
|
fn write(&mut self, packet: &AudioPacket) -> io::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]) -> io::Result<()>;
|
||||||
}
|
}
|
||||||
|
@ -24,25 +26,25 @@ fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat
|
||||||
macro_rules! sink_as_bytes {
|
macro_rules! sink_as_bytes {
|
||||||
() => {
|
() => {
|
||||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
||||||
use crate::audio::{i24, SamplesConverter};
|
use crate::audio::convert::{self, i24};
|
||||||
use zerocopy::AsBytes;
|
use zerocopy::AsBytes;
|
||||||
match packet {
|
match packet {
|
||||||
AudioPacket::Samples(samples) => match self.format {
|
AudioPacket::Samples(samples) => match self.format {
|
||||||
AudioFormat::F32 => self.write_bytes(samples.as_bytes()),
|
AudioFormat::F32 => self.write_bytes(samples.as_bytes()),
|
||||||
AudioFormat::S32 => {
|
AudioFormat::S32 => {
|
||||||
let samples_s32: &[i32] = &SamplesConverter::to_s32(samples);
|
let samples_s32: &[i32] = &convert::to_s32(samples);
|
||||||
self.write_bytes(samples_s32.as_bytes())
|
self.write_bytes(samples_s32.as_bytes())
|
||||||
}
|
}
|
||||||
AudioFormat::S24 => {
|
AudioFormat::S24 => {
|
||||||
let samples_s24: &[i32] = &SamplesConverter::to_s24(samples);
|
let samples_s24: &[i32] = &convert::to_s24(samples);
|
||||||
self.write_bytes(samples_s24.as_bytes())
|
self.write_bytes(samples_s24.as_bytes())
|
||||||
}
|
}
|
||||||
AudioFormat::S24_3 => {
|
AudioFormat::S24_3 => {
|
||||||
let samples_s24_3: &[i24] = &SamplesConverter::to_s24_3(samples);
|
let samples_s24_3: &[i24] = &convert::to_s24_3(samples);
|
||||||
self.write_bytes(samples_s24_3.as_bytes())
|
self.write_bytes(samples_s24_3.as_bytes())
|
||||||
}
|
}
|
||||||
AudioFormat::S16 => {
|
AudioFormat::S16 => {
|
||||||
let samples_s16: &[i16] = &SamplesConverter::to_s16(samples);
|
let samples_s16: &[i16] = &convert::to_s16(samples);
|
||||||
self.write_bytes(samples_s16.as_bytes())
|
self.write_bytes(samples_s16.as_bytes())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -83,18 +85,6 @@ mod jackaudio;
|
||||||
#[cfg(feature = "jackaudio-backend")]
|
#[cfg(feature = "jackaudio-backend")]
|
||||||
use self::jackaudio::JackSink;
|
use self::jackaudio::JackSink;
|
||||||
|
|
||||||
#[cfg(all(
|
|
||||||
feature = "rodiojack-backend",
|
|
||||||
not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))
|
|
||||||
))]
|
|
||||||
compile_error!("Rodio JACK backend is currently only supported on linux.");
|
|
||||||
|
|
||||||
#[cfg(all(
|
|
||||||
feature = "rodiojack-backend",
|
|
||||||
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")
|
|
||||||
))]
|
|
||||||
use self::rodio::JackRodioSink;
|
|
||||||
|
|
||||||
#[cfg(feature = "gstreamer-backend")]
|
#[cfg(feature = "gstreamer-backend")]
|
||||||
mod gstreamer;
|
mod gstreamer;
|
||||||
#[cfg(feature = "gstreamer-backend")]
|
#[cfg(feature = "gstreamer-backend")]
|
||||||
|
@ -102,8 +92,6 @@ 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(feature = "rodio-backend")]
|
|
||||||
use self::rodio::RodioSink;
|
|
||||||
|
|
||||||
#[cfg(feature = "sdl-backend")]
|
#[cfg(feature = "sdl-backend")]
|
||||||
mod sdl;
|
mod sdl;
|
||||||
|
@ -116,10 +104,7 @@ use self::pipe::StdoutSink;
|
||||||
mod subprocess;
|
mod subprocess;
|
||||||
use self::subprocess::SubprocessSink;
|
use self::subprocess::SubprocessSink;
|
||||||
|
|
||||||
pub const BACKENDS: &'static [(
|
pub const BACKENDS: &[(&str, SinkBuilder)] = &[
|
||||||
&'static str,
|
|
||||||
fn(Option<String>, AudioFormat) -> Box<dyn Sink>,
|
|
||||||
)] = &[
|
|
||||||
#[cfg(feature = "alsa-backend")]
|
#[cfg(feature = "alsa-backend")]
|
||||||
("alsa", mk_sink::<AlsaSink>),
|
("alsa", mk_sink::<AlsaSink>),
|
||||||
#[cfg(feature = "portaudio-backend")]
|
#[cfg(feature = "portaudio-backend")]
|
||||||
|
@ -128,22 +113,19 @@ pub const BACKENDS: &'static [(
|
||||||
("pulseaudio", mk_sink::<PulseAudioSink>),
|
("pulseaudio", mk_sink::<PulseAudioSink>),
|
||||||
#[cfg(feature = "jackaudio-backend")]
|
#[cfg(feature = "jackaudio-backend")]
|
||||||
("jackaudio", mk_sink::<JackSink>),
|
("jackaudio", mk_sink::<JackSink>),
|
||||||
#[cfg(all(
|
|
||||||
feature = "rodiojack-backend",
|
|
||||||
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")
|
|
||||||
))]
|
|
||||||
("rodiojack", mk_sink::<JackRodioSink>),
|
|
||||||
#[cfg(feature = "gstreamer-backend")]
|
#[cfg(feature = "gstreamer-backend")]
|
||||||
("gstreamer", mk_sink::<GstreamerSink>),
|
("gstreamer", mk_sink::<GstreamerSink>),
|
||||||
#[cfg(feature = "rodio-backend")]
|
#[cfg(feature = "rodio-backend")]
|
||||||
("rodio", mk_sink::<RodioSink>),
|
("rodio", rodio::mk_rodio),
|
||||||
|
#[cfg(feature = "rodiojack-backend")]
|
||||||
|
("rodiojack", rodio::mk_rodiojack),
|
||||||
#[cfg(feature = "sdl-backend")]
|
#[cfg(feature = "sdl-backend")]
|
||||||
("sdl", mk_sink::<SdlSink>),
|
("sdl", mk_sink::<SdlSink>),
|
||||||
("pipe", mk_sink::<StdoutSink>),
|
("pipe", mk_sink::<StdoutSink>),
|
||||||
("subprocess", mk_sink::<SubprocessSink>),
|
("subprocess", mk_sink::<SubprocessSink>),
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn find(name: Option<String>) -> Option<fn(Option<String>, AudioFormat) -> Box<dyn Sink>> {
|
pub fn find(name: Option<String>) -> Option<SinkBuilder> {
|
||||||
if let Some(name) = name {
|
if let Some(name) = name {
|
||||||
BACKENDS
|
BACKENDS
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -18,10 +18,7 @@ impl Open for StdoutSink {
|
||||||
_ => Box::new(io::stdout()),
|
_ => Box::new(io::stdout()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self { output, format }
|
||||||
output: output,
|
|
||||||
format: format,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use super::{Open, Sink};
|
use super::{Open, Sink};
|
||||||
use crate::audio::{AudioPacket, SamplesConverter};
|
use crate::audio::{convert, AudioPacket};
|
||||||
use crate::config::AudioFormat;
|
use crate::config::AudioFormat;
|
||||||
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
||||||
use portaudio_rs;
|
|
||||||
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::io;
|
||||||
|
@ -157,11 +156,11 @@ impl<'a> Sink for PortAudioSink<'a> {
|
||||||
write_sink!(ref mut stream, samples)
|
write_sink!(ref mut stream, samples)
|
||||||
}
|
}
|
||||||
Self::S32(stream, _parameters) => {
|
Self::S32(stream, _parameters) => {
|
||||||
let samples_s32: &[i32] = &SamplesConverter::to_s32(samples);
|
let samples_s32: &[i32] = &convert::to_s32(samples);
|
||||||
write_sink!(ref mut stream, samples_s32)
|
write_sink!(ref mut stream, samples_s32)
|
||||||
}
|
}
|
||||||
Self::S16(stream, _parameters) => {
|
Self::S16(stream, _parameters) => {
|
||||||
let samples_s16: &[i16] = &SamplesConverter::to_s16(samples);
|
let samples_s16: &[i16] = &convert::to_s16(samples);
|
||||||
write_sink!(ref mut stream, samples_s16)
|
write_sink!(ref mut stream, samples_s16)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -38,9 +38,9 @@ impl Open for PulseAudioSink {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
s: None,
|
s: None,
|
||||||
ss: ss,
|
ss,
|
||||||
device: device,
|
device,
|
||||||
format: format,
|
format,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,197 +1,212 @@
|
||||||
use super::{Open, Sink};
|
|
||||||
extern crate cpal;
|
|
||||||
extern crate rodio;
|
|
||||||
use crate::audio::{AudioPacket, SamplesConverter};
|
|
||||||
use crate::config::AudioFormat;
|
|
||||||
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
|
||||||
use cpal::traits::{DeviceTrait, HostTrait};
|
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::{io, thread, time};
|
use std::{io, thread, time};
|
||||||
|
|
||||||
// most code is shared between RodioSink and JackRodioSink
|
use cpal::traits::{DeviceTrait, HostTrait};
|
||||||
macro_rules! rodio_sink {
|
use thiserror::Error;
|
||||||
($name: ident) => {
|
|
||||||
pub struct $name {
|
|
||||||
rodio_sink: rodio::Sink,
|
|
||||||
// We have to keep hold of this object, or the Sink can't play...
|
|
||||||
#[allow(dead_code)]
|
|
||||||
stream: rodio::OutputStream,
|
|
||||||
format: AudioFormat,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sink for $name {
|
use super::Sink;
|
||||||
start_stop_noop!();
|
use crate::audio::{convert, AudioPacket};
|
||||||
|
use crate::config::AudioFormat;
|
||||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
||||||
let samples = packet.samples();
|
|
||||||
match self.format {
|
|
||||||
AudioFormat::F32 => {
|
|
||||||
let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples);
|
|
||||||
self.rodio_sink.append(source);
|
|
||||||
},
|
|
||||||
AudioFormat::S16 => {
|
|
||||||
let samples_s16: &[i16] = &SamplesConverter::to_s16(samples);
|
|
||||||
let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples_s16);
|
|
||||||
self.rodio_sink.append(source);
|
|
||||||
},
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Chunk sizes seem to be about 256 to 3000 ish items long.
|
|
||||||
// Assuming they're on average 1628 then a half second buffer is:
|
|
||||||
// 44100 elements --> about 27 chunks
|
|
||||||
while self.rodio_sink.len() > 26 {
|
|
||||||
// sleep and wait for rodio to drain a bit
|
|
||||||
thread::sleep(time::Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl $name {
|
|
||||||
fn open_sink(host: &cpal::Host, device: Option<String>, format: AudioFormat) -> $name {
|
|
||||||
match format {
|
|
||||||
AudioFormat::F32 => {
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
warn!("Rodio output to Alsa is known to cause garbled sound, consider using `--backend alsa`");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
AudioFormat::S16 => {},
|
|
||||||
_ => unimplemented!("Rodio currently only supports F32 and S16 formats"),
|
|
||||||
}
|
|
||||||
|
|
||||||
let rodio_device = match_device(&host, device);
|
|
||||||
debug!("Using cpal device");
|
|
||||||
let (stream, stream_handle) = rodio::OutputStream::try_from_device(&rodio_device)
|
|
||||||
.expect("couldn't open output stream.");
|
|
||||||
debug!("Using Rodio stream");
|
|
||||||
let sink = rodio::Sink::try_new(&stream_handle).expect("couldn't create output sink.");
|
|
||||||
debug!("Using Rodio sink");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
rodio_sink: sink,
|
|
||||||
stream: stream,
|
|
||||||
format: format,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
rodio_sink!(RodioSink);
|
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
feature = "rodiojack-backend",
|
feature = "rodiojack-backend",
|
||||||
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")
|
not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))
|
||||||
))]
|
))]
|
||||||
rodio_sink!(JackRodioSink);
|
compile_error!("Rodio JACK backend is currently only supported on linux.");
|
||||||
|
|
||||||
fn list_formats(ref device: &rodio::Device) {
|
#[cfg(feature = "rodio-backend")]
|
||||||
let default_fmt = match device.default_output_config() {
|
pub fn mk_rodio(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
|
||||||
Ok(fmt) => cpal::SupportedStreamConfig::from(fmt),
|
Box::new(open(cpal::default_host(), device, format))
|
||||||
Err(e) => {
|
}
|
||||||
warn!("Error getting default Rodio output config: {}", e);
|
|
||||||
return;
|
#[cfg(feature = "rodiojack-backend")]
|
||||||
|
pub fn mk_rodiojack(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
|
||||||
|
Box::new(open(
|
||||||
|
cpal::host_from_id(cpal::HostId::Jack).unwrap(),
|
||||||
|
device,
|
||||||
|
format,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RodioError {
|
||||||
|
#[error("Rodio: no device available")]
|
||||||
|
NoDeviceAvailable,
|
||||||
|
#[error("Rodio: device \"{0}\" is not available")]
|
||||||
|
DeviceNotAvailable(String),
|
||||||
|
#[error("Rodio play error: {0}")]
|
||||||
|
PlayError(#[from] rodio::PlayError),
|
||||||
|
#[error("Rodio stream error: {0}")]
|
||||||
|
StreamError(#[from] rodio::StreamError),
|
||||||
|
#[error("Cannot get audio devices: {0}")]
|
||||||
|
DevicesError(#[from] cpal::DevicesError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RodioSink {
|
||||||
|
rodio_sink: rodio::Sink,
|
||||||
|
format: AudioFormat,
|
||||||
|
_stream: rodio::OutputStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_formats(device: &rodio::Device) {
|
||||||
|
match device.default_output_config() {
|
||||||
|
Ok(cfg) => {
|
||||||
|
debug!(" Default config:");
|
||||||
|
debug!(" {:?}", cfg);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
debug!(" Default config:");
|
|
||||||
debug!(" {:?}", default_fmt);
|
|
||||||
|
|
||||||
let mut output_configs = match device.supported_output_configs() {
|
|
||||||
Ok(f) => f.peekable(),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Error getting supported Rodio output configs: {}", e);
|
// Use loglevel debug, since even the output is only debug
|
||||||
return;
|
debug!("Error getting default rodio::Sink config: {}", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if output_configs.peek().is_some() {
|
match device.supported_output_configs() {
|
||||||
debug!(" Available output configs:");
|
Ok(mut cfgs) => {
|
||||||
for format in output_configs {
|
if let Some(first) = cfgs.next() {
|
||||||
debug!(" {:?}", format);
|
debug!(" Available configs:");
|
||||||
|
debug!(" {:?}", first);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for cfg in cfgs {
|
||||||
|
debug!(" {:?}", cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Error getting supported rodio::Sink configs: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_outputs(ref host: &cpal::Host) {
|
fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> {
|
||||||
let default_device = get_default_device(host);
|
let mut default_device_name = None;
|
||||||
let default_device_name = default_device.name().expect("cannot get output name");
|
|
||||||
println!("Default audio device:\n {}", default_device_name);
|
|
||||||
list_formats(&default_device);
|
|
||||||
|
|
||||||
println!("Other available audio devices:");
|
if let Some(default_device) = host.default_output_device() {
|
||||||
|
default_device_name = default_device.name().ok();
|
||||||
|
println!(
|
||||||
|
"Default Audio Device:\n {}",
|
||||||
|
default_device_name.as_deref().unwrap_or("[unknown name]")
|
||||||
|
);
|
||||||
|
|
||||||
let found_devices = host.output_devices().expect(&format!(
|
list_formats(&default_device);
|
||||||
"Cannot get list of output devices of host: {:?}",
|
|
||||||
host.id()
|
println!("Other Available Audio Devices:");
|
||||||
));
|
} else {
|
||||||
for device in found_devices {
|
warn!("No default device was found");
|
||||||
let device_name = device.name().expect("cannot get output name");
|
}
|
||||||
if device_name != default_device_name {
|
|
||||||
println!(" {}", device_name);
|
for device in host.output_devices()? {
|
||||||
list_formats(&device);
|
match device.name() {
|
||||||
|
Ok(name) if Some(&name) == default_device_name.as_ref() => (),
|
||||||
|
Ok(name) => {
|
||||||
|
println!(" {}", name);
|
||||||
|
list_formats(&device);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Cannot get device name: {}", e);
|
||||||
|
println!(" [unknown name]");
|
||||||
|
list_formats(&device);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_default_device(ref host: &cpal::Host) -> rodio::Device {
|
fn create_sink(
|
||||||
host.default_output_device()
|
host: &cpal::Host,
|
||||||
.expect("no default output device available")
|
device: Option<String>,
|
||||||
}
|
) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> {
|
||||||
|
let rodio_device = match device {
|
||||||
fn match_device(ref host: &cpal::Host, device: Option<String>) -> rodio::Device {
|
Some(ask) if &ask == "?" => {
|
||||||
match device {
|
let exit_code = match list_outputs(host) {
|
||||||
|
Ok(()) => 0,
|
||||||
|
Err(e) => {
|
||||||
|
error!("{}", e);
|
||||||
|
1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exit(exit_code)
|
||||||
|
}
|
||||||
Some(device_name) => {
|
Some(device_name) => {
|
||||||
if device_name == "?".to_string() {
|
host.output_devices()?
|
||||||
list_outputs(host);
|
.find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails
|
||||||
exit(0)
|
.ok_or(RodioError::DeviceNotAvailable(device_name))?
|
||||||
}
|
|
||||||
|
|
||||||
let found_devices = host.output_devices().expect(&format!(
|
|
||||||
"cannot get list of output devices of host: {:?}",
|
|
||||||
host.id()
|
|
||||||
));
|
|
||||||
for d in found_devices {
|
|
||||||
if d.name().expect("cannot get output name") == device_name {
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!("No output sink matching '{}' found.", device_name);
|
|
||||||
exit(0)
|
|
||||||
}
|
}
|
||||||
None => return get_default_device(host),
|
None => host
|
||||||
|
.default_output_device()
|
||||||
|
.ok_or(RodioError::NoDeviceAvailable)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = rodio_device.name().ok();
|
||||||
|
info!(
|
||||||
|
"Using audio device: {}",
|
||||||
|
name.as_deref().unwrap_or("[unknown name]")
|
||||||
|
);
|
||||||
|
|
||||||
|
let (stream, handle) = rodio::OutputStream::try_from_device(&rodio_device)?;
|
||||||
|
let sink = rodio::Sink::try_new(&handle)?;
|
||||||
|
Ok((sink, stream))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> RodioSink {
|
||||||
|
debug!(
|
||||||
|
"Using rodio sink with format {:?} and cpal host: {}",
|
||||||
|
format,
|
||||||
|
host.id().name()
|
||||||
|
);
|
||||||
|
|
||||||
|
match format {
|
||||||
|
AudioFormat::F32 => {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
warn!("Rodio output to Alsa is known to cause garbled sound, consider using `--backend alsa`")
|
||||||
|
}
|
||||||
|
AudioFormat::S16 => (),
|
||||||
|
_ => unimplemented!("Rodio currently only supports F32 and S16 formats"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let (sink, stream) = create_sink(&host, device).unwrap();
|
||||||
|
|
||||||
|
debug!("Rodio sink was created");
|
||||||
|
RodioSink {
|
||||||
|
rodio_sink: sink,
|
||||||
|
format,
|
||||||
|
_stream: stream,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Open for RodioSink {
|
impl Sink for RodioSink {
|
||||||
fn open(device: Option<String>, format: AudioFormat) -> RodioSink {
|
start_stop_noop!();
|
||||||
let host = cpal::default_host();
|
|
||||||
info!(
|
|
||||||
"Using Rodio sink with format {:?} and cpal host: {:?}",
|
|
||||||
format,
|
|
||||||
host.id()
|
|
||||||
);
|
|
||||||
Self::open_sink(&host, device, format)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(
|
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
||||||
feature = "rodiojack-backend",
|
let samples = packet.samples();
|
||||||
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")
|
match self.format {
|
||||||
))]
|
AudioFormat::F32 => {
|
||||||
impl Open for JackRodioSink {
|
let source =
|
||||||
fn open(device: Option<String>, format: AudioFormat) -> JackRodioSink {
|
rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples);
|
||||||
let host = cpal::host_from_id(
|
self.rodio_sink.append(source);
|
||||||
cpal::available_hosts()
|
}
|
||||||
.into_iter()
|
AudioFormat::S16 => {
|
||||||
.find(|id| *id == cpal::HostId::Jack)
|
let samples_s16: &[i16] = &convert::to_s16(samples);
|
||||||
.expect("JACK host not found"),
|
let source = rodio::buffer::SamplesBuffer::new(
|
||||||
)
|
NUM_CHANNELS as u16,
|
||||||
.expect("JACK host not found");
|
SAMPLE_RATE,
|
||||||
info!(
|
samples_s16,
|
||||||
"Using JACK Rodio sink with format {:?} and cpal JACK host",
|
);
|
||||||
format
|
self.rodio_sink.append(source);
|
||||||
);
|
}
|
||||||
Self::open_sink(&host, device, format)
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chunk sizes seem to be about 256 to 3000 ish items long.
|
||||||
|
// Assuming they're on average 1628 then a half second buffer is:
|
||||||
|
// 44100 elements --> about 27 chunks
|
||||||
|
while self.rodio_sink.len() > 26 {
|
||||||
|
// sleep and wait for rodio to drain a bit
|
||||||
|
thread::sleep(time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::{Open, Sink};
|
use super::{Open, Sink};
|
||||||
use crate::audio::{AudioPacket, SamplesConverter};
|
use crate::audio::{convert, AudioPacket};
|
||||||
use crate::config::AudioFormat;
|
use crate::config::AudioFormat;
|
||||||
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
||||||
use sdl2::audio::{AudioQueue, AudioSpecDesired};
|
use sdl2::audio::{AudioQueue, AudioSpecDesired};
|
||||||
|
@ -97,12 +97,12 @@ impl Sink for SdlSink {
|
||||||
queue.queue(samples)
|
queue.queue(samples)
|
||||||
}
|
}
|
||||||
Self::S32(queue) => {
|
Self::S32(queue) => {
|
||||||
let samples_s32: &[i32] = &SamplesConverter::to_s32(samples);
|
let samples_s32: &[i32] = &convert::to_s32(samples);
|
||||||
drain_sink!(queue, AudioFormat::S32.size());
|
drain_sink!(queue, AudioFormat::S32.size());
|
||||||
queue.queue(samples_s32)
|
queue.queue(samples_s32)
|
||||||
}
|
}
|
||||||
Self::S16(queue) => {
|
Self::S16(queue) => {
|
||||||
let samples_s16: &[i16] = &SamplesConverter::to_s16(samples);
|
let samples_s16: &[i16] = &convert::to_s16(samples);
|
||||||
drain_sink!(queue, AudioFormat::S16.size());
|
drain_sink!(queue, AudioFormat::S16.size());
|
||||||
queue.queue(samples_s16)
|
queue.queue(samples_s16)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use super::{Open, Sink, SinkAsBytes};
|
||||||
use crate::audio::AudioPacket;
|
use crate::audio::AudioPacket;
|
||||||
use crate::config::AudioFormat;
|
use crate::config::AudioFormat;
|
||||||
use shell_words::split;
|
use shell_words::split;
|
||||||
|
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
|
|
||||||
|
@ -17,9 +18,9 @@ impl Open for SubprocessSink {
|
||||||
|
|
||||||
if let Some(shell_command) = shell_command {
|
if let Some(shell_command) = shell_command {
|
||||||
SubprocessSink {
|
SubprocessSink {
|
||||||
shell_command: shell_command,
|
shell_command,
|
||||||
child: None,
|
child: None,
|
||||||
format: format,
|
format,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
panic!("subprocess sink requires specifying a shell command");
|
panic!("subprocess sink requires specifying a shell command");
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::audio::i24;
|
use crate::audio::convert::i24;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
|
@ -1,39 +1,9 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
extern crate byteorder;
|
use librespot_audio as audio;
|
||||||
extern crate futures;
|
use librespot_core as core;
|
||||||
extern crate shell_words;
|
use librespot_metadata as metadata;
|
||||||
|
|
||||||
#[cfg(feature = "alsa-backend")]
|
|
||||||
extern crate alsa;
|
|
||||||
|
|
||||||
#[cfg(feature = "portaudio-backend")]
|
|
||||||
extern crate portaudio_rs;
|
|
||||||
|
|
||||||
#[cfg(feature = "pulseaudio-backend")]
|
|
||||||
extern crate libpulse_binding;
|
|
||||||
#[cfg(feature = "pulseaudio-backend")]
|
|
||||||
extern crate libpulse_simple_binding;
|
|
||||||
|
|
||||||
#[cfg(feature = "jackaudio-backend")]
|
|
||||||
extern crate jack;
|
|
||||||
|
|
||||||
#[cfg(feature = "gstreamer-backend")]
|
|
||||||
extern crate glib;
|
|
||||||
#[cfg(feature = "gstreamer-backend")]
|
|
||||||
extern crate gstreamer as gst;
|
|
||||||
#[cfg(feature = "gstreamer-backend")]
|
|
||||||
extern crate gstreamer_app as gst_app;
|
|
||||||
#[cfg(feature = "gstreamer-backend")]
|
|
||||||
extern crate zerocopy;
|
|
||||||
|
|
||||||
#[cfg(feature = "sdl-backend")]
|
|
||||||
extern crate sdl2;
|
|
||||||
|
|
||||||
extern crate librespot_audio as audio;
|
|
||||||
extern crate librespot_core;
|
|
||||||
extern crate librespot_metadata as metadata;
|
|
||||||
|
|
||||||
pub mod audio_backend;
|
pub mod audio_backend;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
use super::AudioFilter;
|
use super::AudioFilter;
|
||||||
use super::{Mixer, MixerConfig};
|
use super::{Mixer, MixerConfig};
|
||||||
use std;
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use alsa;
|
|
||||||
|
|
||||||
const SND_CTL_TLV_DB_GAIN_MUTE: i64 = -9999999;
|
const SND_CTL_TLV_DB_GAIN_MUTE: i64 = -9999999;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -36,13 +33,12 @@ impl AlsaMixer {
|
||||||
let mixer = alsa::mixer::Mixer::new(&config.card, false)?;
|
let mixer = alsa::mixer::Mixer::new(&config.card, false)?;
|
||||||
let sid = alsa::mixer::SelemId::new(&config.mixer, config.index);
|
let sid = alsa::mixer::SelemId::new(&config.mixer, config.index);
|
||||||
|
|
||||||
let selem = mixer.find_selem(&sid).expect(
|
let selem = mixer.find_selem(&sid).unwrap_or_else(|| {
|
||||||
format!(
|
panic!(
|
||||||
"Couldn't find simple mixer control for {},{}",
|
"Couldn't find simple mixer control for {},{}",
|
||||||
&config.mixer, &config.index,
|
&config.mixer, &config.index,
|
||||||
)
|
)
|
||||||
.as_str(),
|
});
|
||||||
);
|
|
||||||
let (min, max) = selem.get_playback_volume_range();
|
let (min, max) = selem.get_playback_volume_range();
|
||||||
let (min_db, max_db) = selem.get_playback_db_range();
|
let (min_db, max_db) = selem.get_playback_db_range();
|
||||||
let hw_mix = selem
|
let hw_mix = selem
|
||||||
|
@ -72,14 +68,14 @@ impl AlsaMixer {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(AlsaMixer {
|
Ok(AlsaMixer {
|
||||||
config: config,
|
config,
|
||||||
params: AlsaMixerVolumeParams {
|
params: AlsaMixerVolumeParams {
|
||||||
min: min,
|
min,
|
||||||
max: max,
|
max,
|
||||||
range: (max - min) as f64,
|
range: (max - min) as f64,
|
||||||
min_db: min_db,
|
min_db,
|
||||||
max_db: max_db,
|
max_db,
|
||||||
has_switch: has_switch,
|
has_switch,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,11 +42,13 @@ impl Default for MixerConfig {
|
||||||
pub mod softmixer;
|
pub mod softmixer;
|
||||||
use self::softmixer::SoftMixer;
|
use self::softmixer::SoftMixer;
|
||||||
|
|
||||||
|
type MixerFn = fn(Option<MixerConfig>) -> Box<dyn Mixer>;
|
||||||
|
|
||||||
fn mk_sink<M: Mixer + 'static>(device: Option<MixerConfig>) -> Box<dyn Mixer> {
|
fn mk_sink<M: Mixer + 'static>(device: Option<MixerConfig>) -> Box<dyn Mixer> {
|
||||||
Box::new(M::open(device))
|
Box::new(M::open(device))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find<T: AsRef<str>>(name: Option<T>) -> Option<fn(Option<MixerConfig>) -> Box<dyn Mixer>> {
|
pub fn find<T: AsRef<str>>(name: Option<T>) -> Option<MixerFn> {
|
||||||
match name.as_ref().map(AsRef::as_ref) {
|
match name.as_ref().map(AsRef::as_ref) {
|
||||||
None | Some("softvol") => Some(mk_sink::<SoftMixer>),
|
None | Some("softvol") => Some(mk_sink::<SoftMixer>),
|
||||||
#[cfg(feature = "alsa-backend")]
|
#[cfg(feature = "alsa-backend")]
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
|
||||||
use futures;
|
|
||||||
use futures::{future, Async, Future, Poll, Stream};
|
|
||||||
use std;
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::io::{Read, Result, Seek, SeekFrom};
|
use std::future::Future;
|
||||||
use std::mem;
|
use std::io::{self, Read, Seek, SeekFrom};
|
||||||
use std::thread;
|
use std::pin::Pin;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use std::{mem, thread};
|
||||||
|
|
||||||
use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig};
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
use librespot_core::session::Session;
|
use futures_util::stream::futures_unordered::FuturesUnordered;
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
use futures_util::{future, StreamExt, TryFutureExt};
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
use librespot_core::util::SeqGenerator;
|
|
||||||
|
|
||||||
use crate::audio::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder};
|
use crate::audio::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder};
|
||||||
use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController};
|
use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController};
|
||||||
|
@ -22,6 +18,10 @@ use crate::audio::{
|
||||||
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS,
|
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS,
|
||||||
};
|
};
|
||||||
use crate::audio_backend::Sink;
|
use crate::audio_backend::Sink;
|
||||||
|
use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig};
|
||||||
|
use crate::core::session::Session;
|
||||||
|
use crate::core::spotify_id::SpotifyId;
|
||||||
|
use crate::core::util::SeqGenerator;
|
||||||
use crate::metadata::{AudioItem, FileFormat};
|
use crate::metadata::{AudioItem, FileFormat};
|
||||||
use crate::mixer::AudioFilter;
|
use crate::mixer::AudioFilter;
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
|
||||||
const DB_VOLTAGE_RATIO: f32 = 20.0;
|
const DB_VOLTAGE_RATIO: f32 = 20.0;
|
||||||
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
commands: Option<futures::sync::mpsc::UnboundedSender<PlayerCommand>>,
|
commands: Option<mpsc::UnboundedSender<PlayerCommand>>,
|
||||||
thread_handle: Option<thread::JoinHandle<()>>,
|
thread_handle: Option<thread::JoinHandle<()>>,
|
||||||
play_request_id_generator: SeqGenerator<u64>,
|
play_request_id_generator: SeqGenerator<u64>,
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ pub type SinkEventCallback = Box<dyn Fn(SinkStatus) + Send>;
|
||||||
struct PlayerInternal {
|
struct PlayerInternal {
|
||||||
session: Session,
|
session: Session,
|
||||||
config: PlayerConfig,
|
config: PlayerConfig,
|
||||||
commands: futures::sync::mpsc::UnboundedReceiver<PlayerCommand>,
|
commands: mpsc::UnboundedReceiver<PlayerCommand>,
|
||||||
|
|
||||||
state: PlayerState,
|
state: PlayerState,
|
||||||
preload: PlayerPreload,
|
preload: PlayerPreload,
|
||||||
|
@ -58,7 +58,7 @@ struct PlayerInternal {
|
||||||
sink_status: SinkStatus,
|
sink_status: SinkStatus,
|
||||||
sink_event_callback: Option<SinkEventCallback>,
|
sink_event_callback: Option<SinkEventCallback>,
|
||||||
audio_filter: Option<Box<dyn AudioFilter + Send>>,
|
audio_filter: Option<Box<dyn AudioFilter + Send>>,
|
||||||
event_senders: Vec<futures::sync::mpsc::UnboundedSender<PlayerEvent>>,
|
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
|
||||||
|
|
||||||
limiter_active: bool,
|
limiter_active: bool,
|
||||||
limiter_attack_counter: u32,
|
limiter_attack_counter: u32,
|
||||||
|
@ -82,7 +82,7 @@ enum PlayerCommand {
|
||||||
Pause,
|
Pause,
|
||||||
Stop,
|
Stop,
|
||||||
Seek(u32),
|
Seek(u32),
|
||||||
AddEventSender(futures::sync::mpsc::UnboundedSender<PlayerEvent>),
|
AddEventSender(mpsc::UnboundedSender<PlayerEvent>),
|
||||||
SetSinkEventCallback(Option<SinkEventCallback>),
|
SetSinkEventCallback(Option<SinkEventCallback>),
|
||||||
EmitVolumeSetEvent(u16),
|
EmitVolumeSetEvent(u16),
|
||||||
}
|
}
|
||||||
|
@ -194,7 +194,7 @@ impl PlayerEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver<PlayerEvent>;
|
pub type PlayerEventChannel = mpsc::UnboundedReceiver<PlayerEvent>;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct NormalisationData {
|
pub struct NormalisationData {
|
||||||
|
@ -206,28 +206,27 @@ pub struct NormalisationData {
|
||||||
|
|
||||||
impl NormalisationData {
|
impl NormalisationData {
|
||||||
pub fn db_to_ratio(db: f32) -> f32 {
|
pub fn db_to_ratio(db: f32) -> f32 {
|
||||||
return f32::powf(10.0, db / DB_VOLTAGE_RATIO);
|
f32::powf(10.0, db / DB_VOLTAGE_RATIO)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ratio_to_db(ratio: f32) -> f32 {
|
pub fn ratio_to_db(ratio: f32) -> f32 {
|
||||||
return ratio.log10() * DB_VOLTAGE_RATIO;
|
ratio.log10() * DB_VOLTAGE_RATIO
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_from_file<T: Read + Seek>(mut file: T) -> Result<NormalisationData> {
|
fn parse_from_file<T: Read + Seek>(mut file: T) -> io::Result<NormalisationData> {
|
||||||
const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144;
|
const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144;
|
||||||
file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))
|
file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let track_gain_db = file.read_f32::<LittleEndian>().unwrap();
|
let track_gain_db = file.read_f32::<LittleEndian>()?;
|
||||||
let track_peak = file.read_f32::<LittleEndian>().unwrap();
|
let track_peak = file.read_f32::<LittleEndian>()?;
|
||||||
let album_gain_db = file.read_f32::<LittleEndian>().unwrap();
|
let album_gain_db = file.read_f32::<LittleEndian>()?;
|
||||||
let album_peak = file.read_f32::<LittleEndian>().unwrap();
|
let album_peak = file.read_f32::<LittleEndian>()?;
|
||||||
|
|
||||||
let r = NormalisationData {
|
let r = NormalisationData {
|
||||||
track_gain_db: track_gain_db,
|
track_gain_db,
|
||||||
track_peak: track_peak,
|
track_peak,
|
||||||
album_gain_db: album_gain_db,
|
album_gain_db,
|
||||||
album_peak: album_peak,
|
album_peak,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(r)
|
Ok(r)
|
||||||
|
@ -288,15 +287,15 @@ impl Player {
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Box<dyn Sink> + Send + 'static,
|
F: FnOnce() -> Box<dyn Sink> + Send + 'static,
|
||||||
{
|
{
|
||||||
let (cmd_tx, cmd_rx) = futures::sync::mpsc::unbounded();
|
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
||||||
let (event_sender, event_receiver) = futures::sync::mpsc::unbounded();
|
let (event_sender, event_receiver) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
let handle = thread::spawn(move || {
|
let handle = thread::spawn(move || {
|
||||||
debug!("new Player[{}]", session.session_id());
|
debug!("new Player[{}]", session.session_id());
|
||||||
|
|
||||||
let internal = PlayerInternal {
|
let internal = PlayerInternal {
|
||||||
session: session,
|
session,
|
||||||
config: config,
|
config,
|
||||||
commands: cmd_rx,
|
commands: cmd_rx,
|
||||||
|
|
||||||
state: PlayerState::Stopped,
|
state: PlayerState::Stopped,
|
||||||
|
@ -304,7 +303,7 @@ impl Player {
|
||||||
sink: sink_builder(),
|
sink: sink_builder(),
|
||||||
sink_status: SinkStatus::Closed,
|
sink_status: SinkStatus::Closed,
|
||||||
sink_event_callback: None,
|
sink_event_callback: None,
|
||||||
audio_filter: audio_filter,
|
audio_filter,
|
||||||
event_senders: [event_sender].to_vec(),
|
event_senders: [event_sender].to_vec(),
|
||||||
|
|
||||||
limiter_active: false,
|
limiter_active: false,
|
||||||
|
@ -316,8 +315,8 @@ impl Player {
|
||||||
};
|
};
|
||||||
|
|
||||||
// While PlayerInternal is written as a future, it still contains blocking code.
|
// While PlayerInternal is written as a future, it still contains blocking code.
|
||||||
// It must be run by using wait() in a dedicated thread.
|
// It must be run by using block_on() in a dedicated thread.
|
||||||
let _ = internal.wait();
|
futures_executor::block_on(internal);
|
||||||
debug!("PlayerInternal thread finished.");
|
debug!("PlayerInternal thread finished.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -332,7 +331,7 @@ impl Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn command(&self, cmd: PlayerCommand) {
|
fn command(&self, cmd: PlayerCommand) {
|
||||||
self.commands.as_ref().unwrap().unbounded_send(cmd).unwrap();
|
self.commands.as_ref().unwrap().send(cmd).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(&mut self, track_id: SpotifyId, start_playing: bool, position_ms: u32) -> u64 {
|
pub fn load(&mut self, track_id: SpotifyId, start_playing: bool, position_ms: u32) -> u64 {
|
||||||
|
@ -368,22 +367,21 @@ impl Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_player_event_channel(&self) -> PlayerEventChannel {
|
pub fn get_player_event_channel(&self) -> PlayerEventChannel {
|
||||||
let (event_sender, event_receiver) = futures::sync::mpsc::unbounded();
|
let (event_sender, event_receiver) = mpsc::unbounded_channel();
|
||||||
self.command(PlayerCommand::AddEventSender(event_sender));
|
self.command(PlayerCommand::AddEventSender(event_sender));
|
||||||
event_receiver
|
event_receiver
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_end_of_track_future(&self) -> Box<dyn Future<Item = (), Error = ()>> {
|
pub async fn await_end_of_track(&self) {
|
||||||
let result = self
|
let mut channel = self.get_player_event_channel();
|
||||||
.get_player_event_channel()
|
while let Some(event) = channel.recv().await {
|
||||||
.filter(|event| match event {
|
if matches!(
|
||||||
PlayerEvent::EndOfTrack { .. } | PlayerEvent::Stopped { .. } => true,
|
event,
|
||||||
_ => false,
|
PlayerEvent::EndOfTrack { .. } | PlayerEvent::Stopped { .. }
|
||||||
})
|
) {
|
||||||
.into_future()
|
return;
|
||||||
.map_err(|_| ())
|
}
|
||||||
.map(|_| ());
|
}
|
||||||
Box::new(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_sink_event_callback(&self, callback: Option<SinkEventCallback>) {
|
pub fn set_sink_event_callback(&self, callback: Option<SinkEventCallback>) {
|
||||||
|
@ -421,11 +419,11 @@ enum PlayerPreload {
|
||||||
None,
|
None,
|
||||||
Loading {
|
Loading {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyId,
|
||||||
loader: Box<dyn Future<Item = PlayerLoadedTrackData, Error = ()>>,
|
loader: Pin<Box<dyn Future<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
|
||||||
},
|
},
|
||||||
Ready {
|
Ready {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyId,
|
||||||
loaded_track: PlayerLoadedTrackData,
|
loaded_track: Box<PlayerLoadedTrackData>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,7 +435,7 @@ enum PlayerState {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyId,
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
start_playback: bool,
|
start_playback: bool,
|
||||||
loader: Box<dyn Future<Item = PlayerLoadedTrackData, Error = ()>>,
|
loader: Pin<Box<dyn Future<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
|
||||||
},
|
},
|
||||||
Paused {
|
Paused {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyId,
|
||||||
|
@ -483,18 +481,12 @@ impl PlayerState {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn is_stopped(&self) -> bool {
|
fn is_stopped(&self) -> bool {
|
||||||
use self::PlayerState::*;
|
use self::PlayerState::*;
|
||||||
match *self {
|
matches!(self, Stopped)
|
||||||
Stopped => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_loading(&self) -> bool {
|
fn is_loading(&self) -> bool {
|
||||||
use self::PlayerState::*;
|
use self::PlayerState::*;
|
||||||
match *self {
|
matches!(self, Loading { .. })
|
||||||
Loading { .. } => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decoder(&mut self) -> Option<&mut Decoder> {
|
fn decoder(&mut self) -> Option<&mut Decoder> {
|
||||||
|
@ -627,22 +619,22 @@ struct PlayerTrackLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayerTrackLoader {
|
impl PlayerTrackLoader {
|
||||||
fn find_available_alternative<'a>(&self, audio: &'a AudioItem) -> Option<Cow<'a, AudioItem>> {
|
async fn find_available_alternative(&self, audio: AudioItem) -> Option<AudioItem> {
|
||||||
if audio.available {
|
if audio.available {
|
||||||
Some(Cow::Borrowed(audio))
|
Some(audio)
|
||||||
|
} else if let Some(alternatives) = &audio.alternatives {
|
||||||
|
let alternatives: FuturesUnordered<_> = alternatives
|
||||||
|
.iter()
|
||||||
|
.map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
alternatives
|
||||||
|
.filter_map(|x| future::ready(x.ok()))
|
||||||
|
.filter(|x| future::ready(x.available))
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
} else {
|
} else {
|
||||||
if let Some(alternatives) = &audio.alternatives {
|
None
|
||||||
let alternatives = alternatives
|
|
||||||
.iter()
|
|
||||||
.map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id));
|
|
||||||
let alternatives = future::join_all(alternatives).wait().unwrap();
|
|
||||||
alternatives
|
|
||||||
.into_iter()
|
|
||||||
.find(|alt| alt.available)
|
|
||||||
.map(Cow::Owned)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,8 +657,12 @@ impl PlayerTrackLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_track(&self, spotify_id: SpotifyId, position_ms: u32) -> Option<PlayerLoadedTrackData> {
|
async fn load_track(
|
||||||
let audio = match AudioItem::get_audio_item(&self.session, spotify_id).wait() {
|
&self,
|
||||||
|
spotify_id: SpotifyId,
|
||||||
|
position_ms: u32,
|
||||||
|
) -> Option<PlayerLoadedTrackData> {
|
||||||
|
let audio = match AudioItem::get_audio_item(&self.session, spotify_id).await {
|
||||||
Ok(audio) => audio,
|
Ok(audio) => audio,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
error!("Unable to load audio item.");
|
error!("Unable to load audio item.");
|
||||||
|
@ -676,10 +672,10 @@ impl PlayerTrackLoader {
|
||||||
|
|
||||||
info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri);
|
info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri);
|
||||||
|
|
||||||
let audio = match self.find_available_alternative(&audio) {
|
let audio = match self.find_available_alternative(audio).await {
|
||||||
Some(audio) => audio,
|
Some(audio) => audio,
|
||||||
None => {
|
None => {
|
||||||
warn!("<{}> is not available", audio.uri);
|
warn!("<{}> is not available", spotify_id.to_uri());
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -725,128 +721,132 @@ impl PlayerTrackLoader {
|
||||||
let bytes_per_second = self.stream_data_rate(format);
|
let bytes_per_second = self.stream_data_rate(format);
|
||||||
let play_from_beginning = position_ms == 0;
|
let play_from_beginning = position_ms == 0;
|
||||||
|
|
||||||
let key = self.session.audio_key().request(spotify_id, file_id);
|
// This is only a loop to be able to reload the file if an error occured
|
||||||
let encrypted_file = AudioFile::open(
|
// while opening a cached file.
|
||||||
&self.session,
|
loop {
|
||||||
file_id,
|
let encrypted_file = AudioFile::open(
|
||||||
bytes_per_second,
|
&self.session,
|
||||||
play_from_beginning,
|
file_id,
|
||||||
);
|
bytes_per_second,
|
||||||
|
play_from_beginning,
|
||||||
|
);
|
||||||
|
|
||||||
let encrypted_file = match encrypted_file.wait() {
|
let encrypted_file = match encrypted_file.await {
|
||||||
Ok(encrypted_file) => encrypted_file,
|
Ok(encrypted_file) => encrypted_file,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
error!("Unable to load encrypted file.");
|
error!("Unable to load encrypted file.");
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let is_cached = encrypted_file.is_cached();
|
|
||||||
|
|
||||||
let mut stream_loader_controller = encrypted_file.get_stream_loader_controller();
|
|
||||||
|
|
||||||
if play_from_beginning {
|
|
||||||
// No need to seek -> we stream from the beginning
|
|
||||||
stream_loader_controller.set_stream_mode();
|
|
||||||
} else {
|
|
||||||
// we need to seek -> we set stream mode after the initial seek.
|
|
||||||
stream_loader_controller.set_random_access_mode();
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = match key.wait() {
|
|
||||||
Ok(key) => key,
|
|
||||||
Err(_) => {
|
|
||||||
error!("Unable to load decryption key");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
|
||||||
|
|
||||||
let normalisation_factor = match NormalisationData::parse_from_file(&mut decrypted_file) {
|
|
||||||
Ok(normalisation_data) => {
|
|
||||||
NormalisationData::get_factor(&self.config, normalisation_data)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
warn!("Unable to extract normalisation data, using default value.");
|
|
||||||
1.0 as f32
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let audio_file = Subfile::new(decrypted_file, 0xa7);
|
|
||||||
|
|
||||||
let result = if self.config.passthrough {
|
|
||||||
match PassthroughDecoder::new(audio_file) {
|
|
||||||
Ok(result) => Ok(Box::new(result) as Decoder),
|
|
||||||
Err(e) => Err(AudioError::PassthroughError(e)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match VorbisDecoder::new(audio_file) {
|
|
||||||
Ok(result) => Ok(Box::new(result) as Decoder),
|
|
||||||
Err(e) => Err(AudioError::VorbisError(e)),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut decoder = match result {
|
|
||||||
Ok(decoder) => decoder,
|
|
||||||
Err(e) if is_cached => {
|
|
||||||
warn!(
|
|
||||||
"Unable to read cached audio file: {}. Trying to download it.",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
|
|
||||||
// unwrap safety: The file is cached, so session must have a cache
|
|
||||||
if !self.session.cache().unwrap().remove_file(file_id) {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
let is_cached = encrypted_file.is_cached();
|
||||||
|
|
||||||
// Just try it again
|
let stream_loader_controller = encrypted_file.get_stream_loader_controller();
|
||||||
return self.load_track(spotify_id, position_ms);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Unable to read audio file: {}", e);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if position_ms != 0 {
|
if play_from_beginning {
|
||||||
if let Err(err) = decoder.seek(position_ms as i64) {
|
// No need to seek -> we stream from the beginning
|
||||||
error!("Vorbis error: {}", err);
|
stream_loader_controller.set_stream_mode();
|
||||||
|
} else {
|
||||||
|
// we need to seek -> we set stream mode after the initial seek.
|
||||||
|
stream_loader_controller.set_random_access_mode();
|
||||||
}
|
}
|
||||||
stream_loader_controller.set_stream_mode();
|
|
||||||
|
let key = match self.session.audio_key().request(spotify_id, file_id).await {
|
||||||
|
Ok(key) => key,
|
||||||
|
Err(_) => {
|
||||||
|
error!("Unable to load decryption key");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
||||||
|
|
||||||
|
let normalisation_factor = match NormalisationData::parse_from_file(&mut decrypted_file)
|
||||||
|
{
|
||||||
|
Ok(normalisation_data) => {
|
||||||
|
NormalisationData::get_factor(&self.config, normalisation_data)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!("Unable to extract normalisation data, using default value.");
|
||||||
|
1.0_f32
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let audio_file = Subfile::new(decrypted_file, 0xa7);
|
||||||
|
|
||||||
|
let result = if self.config.passthrough {
|
||||||
|
match PassthroughDecoder::new(audio_file) {
|
||||||
|
Ok(result) => Ok(Box::new(result) as Decoder),
|
||||||
|
Err(e) => Err(AudioError::PassthroughError(e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match VorbisDecoder::new(audio_file) {
|
||||||
|
Ok(result) => Ok(Box::new(result) as Decoder),
|
||||||
|
Err(e) => Err(AudioError::VorbisError(e)),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut decoder = match result {
|
||||||
|
Ok(decoder) => decoder,
|
||||||
|
Err(e) if is_cached => {
|
||||||
|
warn!(
|
||||||
|
"Unable to read cached audio file: {}. Trying to download it.",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
|
||||||
|
// unwrap safety: The file is cached, so session must have a cache
|
||||||
|
if !self.session.cache().unwrap().remove_file(file_id) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just try it again
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Unable to read audio file: {}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if position_ms != 0 {
|
||||||
|
if let Err(err) = decoder.seek(position_ms as i64) {
|
||||||
|
error!("Vorbis error: {}", err);
|
||||||
|
}
|
||||||
|
stream_loader_controller.set_stream_mode();
|
||||||
|
}
|
||||||
|
let stream_position_pcm = PlayerInternal::position_ms_to_pcm(position_ms);
|
||||||
|
info!("<{}> ({} ms) loaded", audio.name, audio.duration);
|
||||||
|
|
||||||
|
return Some(PlayerLoadedTrackData {
|
||||||
|
decoder,
|
||||||
|
normalisation_factor,
|
||||||
|
stream_loader_controller,
|
||||||
|
bytes_per_second,
|
||||||
|
duration_ms,
|
||||||
|
stream_position_pcm,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let stream_position_pcm = PlayerInternal::position_ms_to_pcm(position_ms);
|
|
||||||
info!("<{}> ({} ms) loaded", audio.name, audio.duration);
|
|
||||||
Some(PlayerLoadedTrackData {
|
|
||||||
decoder,
|
|
||||||
normalisation_factor,
|
|
||||||
stream_loader_controller,
|
|
||||||
bytes_per_second,
|
|
||||||
duration_ms,
|
|
||||||
stream_position_pcm,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Future for PlayerInternal {
|
impl Future for PlayerInternal {
|
||||||
type Item = ();
|
type Output = ();
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<(), ()> {
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
||||||
// While this is written as a future, it still contains blocking code.
|
// While this is written as a future, it still contains blocking code.
|
||||||
// It must be run on its own thread.
|
// It must be run on its own thread.
|
||||||
|
let passthrough = self.config.passthrough;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let mut all_futures_completed_or_not_ready = true;
|
let mut all_futures_completed_or_not_ready = true;
|
||||||
|
|
||||||
// process commands that were sent to us
|
// process commands that were sent to us
|
||||||
let cmd = match self.commands.poll() {
|
let cmd = match self.commands.poll_recv(cx) {
|
||||||
Ok(Async::Ready(None)) => return Ok(Async::Ready(())), // client has disconnected - shut down.
|
Poll::Ready(None) => return Poll::Ready(()), // client has disconnected - shut down.
|
||||||
Ok(Async::Ready(Some(cmd))) => {
|
Poll::Ready(Some(cmd)) => {
|
||||||
all_futures_completed_or_not_ready = false;
|
all_futures_completed_or_not_ready = false;
|
||||||
Some(cmd)
|
Some(cmd)
|
||||||
}
|
}
|
||||||
Ok(Async::NotReady) => None,
|
_ => None,
|
||||||
Err(_) => None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(cmd) = cmd {
|
if let Some(cmd) = cmd {
|
||||||
|
@ -861,8 +861,8 @@ impl Future for PlayerInternal {
|
||||||
play_request_id,
|
play_request_id,
|
||||||
} = self.state
|
} = self.state
|
||||||
{
|
{
|
||||||
match loader.poll() {
|
match loader.as_mut().poll(cx) {
|
||||||
Ok(Async::Ready(loaded_track)) => {
|
Poll::Ready(Ok(loaded_track)) => {
|
||||||
self.start_playback(
|
self.start_playback(
|
||||||
track_id,
|
track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
|
@ -873,8 +873,7 @@ impl Future for PlayerInternal {
|
||||||
panic!("The state wasn't changed by start_playback()");
|
panic!("The state wasn't changed by start_playback()");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Async::NotReady) => (),
|
Poll::Ready(Err(_)) => {
|
||||||
Err(_) => {
|
|
||||||
warn!("Unable to load <{:?}>\nSkipping to next track", track_id);
|
warn!("Unable to load <{:?}>\nSkipping to next track", track_id);
|
||||||
assert!(self.state.is_loading());
|
assert!(self.state.is_loading());
|
||||||
self.send_event(PlayerEvent::EndOfTrack {
|
self.send_event(PlayerEvent::EndOfTrack {
|
||||||
|
@ -882,6 +881,7 @@ impl Future for PlayerInternal {
|
||||||
play_request_id,
|
play_request_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Poll::Pending => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -891,16 +891,15 @@ impl Future for PlayerInternal {
|
||||||
track_id,
|
track_id,
|
||||||
} = self.preload
|
} = self.preload
|
||||||
{
|
{
|
||||||
match loader.poll() {
|
match loader.as_mut().poll(cx) {
|
||||||
Ok(Async::Ready(loaded_track)) => {
|
Poll::Ready(Ok(loaded_track)) => {
|
||||||
self.send_event(PlayerEvent::Preloading { track_id });
|
self.send_event(PlayerEvent::Preloading { track_id });
|
||||||
self.preload = PlayerPreload::Ready {
|
self.preload = PlayerPreload::Ready {
|
||||||
track_id,
|
track_id,
|
||||||
loaded_track,
|
loaded_track: Box::new(loaded_track),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Ok(Async::NotReady) => (),
|
Poll::Ready(Err(_)) => {
|
||||||
Err(_) => {
|
|
||||||
debug!("Unable to preload {:?}", track_id);
|
debug!("Unable to preload {:?}", track_id);
|
||||||
self.preload = PlayerPreload::None;
|
self.preload = PlayerPreload::None;
|
||||||
// Let Spirc know that the track was unavailable.
|
// Let Spirc know that the track was unavailable.
|
||||||
|
@ -917,6 +916,7 @@ impl Future for PlayerInternal {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Poll::Pending => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -936,10 +936,10 @@ impl Future for PlayerInternal {
|
||||||
{
|
{
|
||||||
let packet = decoder.next_packet().expect("Vorbis error");
|
let packet = decoder.next_packet().expect("Vorbis error");
|
||||||
|
|
||||||
if !self.config.passthrough {
|
if !passthrough {
|
||||||
if let Some(ref packet) = packet {
|
if let Some(ref packet) = packet {
|
||||||
*stream_position_pcm = *stream_position_pcm
|
*stream_position_pcm +=
|
||||||
+ (packet.samples().len() / NUM_CHANNELS as usize) as u64;
|
(packet.samples().len() / NUM_CHANNELS as usize) as u64;
|
||||||
let stream_position_millis =
|
let stream_position_millis =
|
||||||
Self::position_pcm_to_ms(*stream_position_pcm);
|
Self::position_pcm_to_ms(*stream_position_pcm);
|
||||||
|
|
||||||
|
@ -951,11 +951,7 @@ impl Future for PlayerInternal {
|
||||||
.as_millis()
|
.as_millis()
|
||||||
as i64
|
as i64
|
||||||
- stream_position_millis as i64;
|
- stream_position_millis as i64;
|
||||||
if lag > 1000 {
|
lag > 1000
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if notify_about_position {
|
if notify_about_position {
|
||||||
|
@ -1015,11 +1011,11 @@ impl Future for PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.session.is_invalid() {
|
if self.session.is_invalid() {
|
||||||
return Ok(Async::Ready(()));
|
return Poll::Ready(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!self.state.is_playing()) && all_futures_completed_or_not_ready {
|
if (!self.state.is_playing()) && all_futures_completed_or_not_ready {
|
||||||
return Ok(Async::NotReady);
|
return Poll::Pending;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1165,7 +1161,7 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.config.normalisation
|
if self.config.normalisation
|
||||||
&& (normalisation_factor != 1.0
|
&& (f32::abs(normalisation_factor - 1.0) < f32::EPSILON
|
||||||
|| self.config.normalisation_method != NormalisationMethod::Basic)
|
|| self.config.normalisation_method != NormalisationMethod::Basic)
|
||||||
{
|
{
|
||||||
for sample in data.iter_mut() {
|
for sample in data.iter_mut() {
|
||||||
|
@ -1333,8 +1329,8 @@ impl PlayerInternal {
|
||||||
});
|
});
|
||||||
|
|
||||||
self.state = PlayerState::Playing {
|
self.state = PlayerState::Playing {
|
||||||
track_id: track_id,
|
track_id,
|
||||||
play_request_id: play_request_id,
|
play_request_id,
|
||||||
decoder: loaded_track.decoder,
|
decoder: loaded_track.decoder,
|
||||||
normalisation_factor: loaded_track.normalisation_factor,
|
normalisation_factor: loaded_track.normalisation_factor,
|
||||||
stream_loader_controller: loaded_track.stream_loader_controller,
|
stream_loader_controller: loaded_track.stream_loader_controller,
|
||||||
|
@ -1350,8 +1346,8 @@ impl PlayerInternal {
|
||||||
self.ensure_sink_stopped(false);
|
self.ensure_sink_stopped(false);
|
||||||
|
|
||||||
self.state = PlayerState::Paused {
|
self.state = PlayerState::Paused {
|
||||||
track_id: track_id,
|
track_id,
|
||||||
play_request_id: play_request_id,
|
play_request_id,
|
||||||
decoder: loaded_track.decoder,
|
decoder: loaded_track.decoder,
|
||||||
normalisation_factor: loaded_track.normalisation_factor,
|
normalisation_factor: loaded_track.normalisation_factor,
|
||||||
stream_loader_controller: loaded_track.stream_loader_controller,
|
stream_loader_controller: loaded_track.stream_loader_controller,
|
||||||
|
@ -1398,7 +1394,7 @@ impl PlayerInternal {
|
||||||
track_id: old_track_id,
|
track_id: old_track_id,
|
||||||
..
|
..
|
||||||
} => self.send_event(PlayerEvent::Changed {
|
} => self.send_event(PlayerEvent::Changed {
|
||||||
old_track_id: old_track_id,
|
old_track_id,
|
||||||
new_track_id: track_id,
|
new_track_id: track_id,
|
||||||
}),
|
}),
|
||||||
PlayerState::Stopped => self.send_event(PlayerEvent::Started {
|
PlayerState::Stopped => self.send_event(PlayerEvent::Started {
|
||||||
|
@ -1536,7 +1532,7 @@ impl PlayerInternal {
|
||||||
let _ = loaded_track.decoder.seek(position_ms as i64); // This may be blocking
|
let _ = loaded_track.decoder.seek(position_ms as i64); // This may be blocking
|
||||||
loaded_track.stream_loader_controller.set_stream_mode();
|
loaded_track.stream_loader_controller.set_stream_mode();
|
||||||
}
|
}
|
||||||
self.start_playback(track_id, play_request_id, loaded_track, play);
|
self.start_playback(track_id, play_request_id, *loaded_track, play);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
unreachable!();
|
unreachable!();
|
||||||
|
@ -1578,9 +1574,7 @@ impl PlayerInternal {
|
||||||
self.preload = PlayerPreload::None;
|
self.preload = PlayerPreload::None;
|
||||||
|
|
||||||
// If we don't have a loader yet, create one from scratch.
|
// If we don't have a loader yet, create one from scratch.
|
||||||
let loader = loader
|
let loader = loader.unwrap_or_else(|| Box::pin(self.load_track(track_id, position_ms)));
|
||||||
.or_else(|| Some(self.load_track(track_id, position_ms)))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Set ourselves to a loading state.
|
// Set ourselves to a loading state.
|
||||||
self.state = PlayerState::Loading {
|
self.state = PlayerState::Loading {
|
||||||
|
@ -1635,7 +1629,10 @@ impl PlayerInternal {
|
||||||
// schedule the preload of the current track if desired.
|
// schedule the preload of the current track if desired.
|
||||||
if preload_track {
|
if preload_track {
|
||||||
let loader = self.load_track(track_id, 0);
|
let loader = self.load_track(track_id, 0);
|
||||||
self.preload = PlayerPreload::Loading { track_id, loader }
|
self.preload = PlayerPreload::Loading {
|
||||||
|
track_id,
|
||||||
|
loader: Box::pin(loader),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1738,7 +1735,7 @@ impl PlayerInternal {
|
||||||
fn send_event(&mut self, event: PlayerEvent) {
|
fn send_event(&mut self, event: PlayerEvent) {
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
while index < self.event_senders.len() {
|
while index < self.event_senders.len() {
|
||||||
match self.event_senders[index].unbounded_send(event.clone()) {
|
match self.event_senders[index].send(event.clone()) {
|
||||||
Ok(_) => index += 1,
|
Ok(_) => index += 1,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
self.event_senders.remove(index);
|
self.event_senders.remove(index);
|
||||||
|
@ -1751,7 +1748,7 @@ impl PlayerInternal {
|
||||||
&self,
|
&self,
|
||||||
spotify_id: SpotifyId,
|
spotify_id: SpotifyId,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
) -> Box<dyn Future<Item = PlayerLoadedTrackData, Error = ()>> {
|
) -> impl Future<Output = Result<PlayerLoadedTrackData, ()>> + Send + 'static {
|
||||||
// This method creates a future that returns the loaded stream and associated info.
|
// This method creates a future that returns the loaded stream and associated info.
|
||||||
// Ideally all work should be done using asynchronous code. However, seek() on the
|
// Ideally all work should be done using asynchronous code. However, seek() on the
|
||||||
// audio stream is implemented in a blocking fashion. Thus, we can't turn it into future
|
// audio stream is implemented in a blocking fashion. Thus, we can't turn it into future
|
||||||
|
@ -1763,18 +1760,16 @@ impl PlayerInternal {
|
||||||
config: self.config.clone(),
|
config: self.config.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (result_tx, result_rx) = futures::sync::oneshot::channel();
|
let (result_tx, result_rx) = oneshot::channel();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
loader
|
let data = futures_executor::block_on(loader.load_track(spotify_id, position_ms));
|
||||||
.load_track(spotify_id, position_ms)
|
if let Some(data) = data {
|
||||||
.and_then(move |data| {
|
let _ = result_tx.send(data);
|
||||||
let _ = result_tx.send(data);
|
}
|
||||||
Some(())
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Box::new(result_rx.map_err(|_| ()))
|
result_rx.map_err(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preload_data_before_playback(&mut self) {
|
fn preload_data_before_playback(&mut self) {
|
||||||
|
@ -1896,21 +1891,18 @@ struct Subfile<T: Read + Seek> {
|
||||||
impl<T: Read + Seek> Subfile<T> {
|
impl<T: Read + Seek> Subfile<T> {
|
||||||
pub fn new(mut stream: T, offset: u64) -> Subfile<T> {
|
pub fn new(mut stream: T, offset: u64) -> Subfile<T> {
|
||||||
stream.seek(SeekFrom::Start(offset)).unwrap();
|
stream.seek(SeekFrom::Start(offset)).unwrap();
|
||||||
Subfile {
|
Subfile { stream, offset }
|
||||||
stream: stream,
|
|
||||||
offset: offset,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Read + Seek> Read for Subfile<T> {
|
impl<T: Read + Seek> Read for Subfile<T> {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
self.stream.read(buf)
|
self.stream.read(buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Read + Seek> Seek for Subfile<T> {
|
impl<T: Read + Seek> Seek for Subfile<T> {
|
||||||
fn seek(&mut self, mut pos: SeekFrom) -> Result<u64> {
|
fn seek(&mut self, mut pos: SeekFrom) -> io::Result<u64> {
|
||||||
pos = match pos {
|
pos = match pos {
|
||||||
SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset),
|
SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset),
|
||||||
x => x,
|
x => x,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
# max_width = 105
|
# max_width = 105
|
||||||
reorder_imports = true
|
reorder_imports = true
|
||||||
reorder_modules = true
|
reorder_modules = true
|
||||||
|
edition = "2018"
|
||||||
|
|
13
src/lib.rs
13
src/lib.rs
|
@ -1,9 +1,8 @@
|
||||||
#![crate_name = "librespot"]
|
#![crate_name = "librespot"]
|
||||||
#![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))]
|
|
||||||
|
|
||||||
pub extern crate librespot_audio as audio;
|
pub use librespot_audio as audio;
|
||||||
pub extern crate librespot_connect as connect;
|
pub use librespot_connect as connect;
|
||||||
pub extern crate librespot_core as core;
|
pub use librespot_core as core;
|
||||||
pub extern crate librespot_metadata as metadata;
|
pub use librespot_metadata as metadata;
|
||||||
pub extern crate librespot_playback as playback;
|
pub use librespot_playback as playback;
|
||||||
pub extern crate librespot_protocol as protocol;
|
pub use librespot_protocol as protocol;
|
||||||
|
|
471
src/main.rs
471
src/main.rs
|
@ -1,36 +1,35 @@
|
||||||
use futures::sync::mpsc::UnboundedReceiver;
|
use futures_util::{future, FutureExt, StreamExt};
|
||||||
use futures::{Async, Future, Poll, Stream};
|
use librespot_playback::player::PlayerEvent;
|
||||||
use log::{error, info, trace, warn};
|
use log::{error, info, warn};
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use std::convert::TryFrom;
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
use std::env;
|
|
||||||
use std::io::{stderr, Write};
|
|
||||||
use std::mem;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::process::exit;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::Instant;
|
|
||||||
use tokio_core::reactor::{Core, Handle};
|
|
||||||
use tokio_io::IoStream;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use librespot::core::authentication::{get_credentials, Credentials};
|
use librespot::connect::spirc::Spirc;
|
||||||
|
use librespot::core::authentication::Credentials;
|
||||||
use librespot::core::cache::Cache;
|
use librespot::core::cache::Cache;
|
||||||
use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig, VolumeCtrl};
|
use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig, VolumeCtrl};
|
||||||
use librespot::core::session::{AuthenticationError, Session};
|
use librespot::core::session::Session;
|
||||||
use librespot::core::version;
|
use librespot::core::version;
|
||||||
|
|
||||||
use librespot::connect::discovery::{discovery, DiscoveryStream};
|
|
||||||
use librespot::connect::spirc::{Spirc, SpircTask};
|
|
||||||
use librespot::playback::audio_backend::{self, Sink, BACKENDS};
|
use librespot::playback::audio_backend::{self, Sink, BACKENDS};
|
||||||
use librespot::playback::config::{
|
use librespot::playback::config::{
|
||||||
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig,
|
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig,
|
||||||
};
|
};
|
||||||
use librespot::playback::mixer::{self, Mixer, MixerConfig};
|
use librespot::playback::mixer::{self, Mixer, MixerConfig};
|
||||||
use librespot::playback::player::{NormalisationData, Player, PlayerEvent};
|
use librespot::playback::player::{NormalisationData, Player};
|
||||||
|
|
||||||
mod player_event_handler;
|
mod player_event_handler;
|
||||||
use crate::player_event_handler::{emit_sink_event, run_program_on_events};
|
use player_event_handler::{emit_sink_event, run_program_on_events};
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::exit;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::{env, time::Instant};
|
||||||
|
use std::{
|
||||||
|
io::{stderr, Write},
|
||||||
|
pin::Pin,
|
||||||
|
};
|
||||||
|
|
||||||
const MILLIS: f32 = 1000.0;
|
const MILLIS: f32 = 1000.0;
|
||||||
|
|
||||||
|
@ -76,6 +75,29 @@ fn list_backends() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_credentials<F: FnOnce(&String) -> Option<String>>(
|
||||||
|
username: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
|
cached_credentials: Option<Credentials>,
|
||||||
|
prompt: F,
|
||||||
|
) -> Option<Credentials> {
|
||||||
|
if let Some(username) = username {
|
||||||
|
if let Some(password) = password {
|
||||||
|
return Some(Credentials::with_password(username, password));
|
||||||
|
}
|
||||||
|
|
||||||
|
match cached_credentials {
|
||||||
|
Some(credentials) if username == credentials.username => Some(credentials),
|
||||||
|
_ => {
|
||||||
|
let password = prompt(&username)?;
|
||||||
|
Some(Credentials::with_password(username, password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cached_credentials
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn print_version() {
|
fn print_version() {
|
||||||
println!(
|
println!(
|
||||||
"librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})",
|
"librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})",
|
||||||
|
@ -89,7 +111,7 @@ fn print_version() {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Setup {
|
struct Setup {
|
||||||
format: AudioFormat,
|
format: AudioFormat,
|
||||||
backend: fn(Option<String>, AudioFormat) -> Box<dyn Sink>,
|
backend: fn(Option<String>, AudioFormat) -> Box<dyn Sink + 'static>,
|
||||||
device: Option<String>,
|
device: Option<String>,
|
||||||
|
|
||||||
mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>,
|
mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>,
|
||||||
|
@ -106,7 +128,7 @@ struct Setup {
|
||||||
emit_sink_events: bool,
|
emit_sink_events: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup(args: &[String]) -> Setup {
|
fn get_setup(args: &[String]) -> Setup {
|
||||||
let mut opts = getopts::Options::new();
|
let mut opts = getopts::Options::new();
|
||||||
opts.optopt(
|
opts.optopt(
|
||||||
"c",
|
"c",
|
||||||
|
@ -267,13 +289,7 @@ fn setup(args: &[String]) -> Setup {
|
||||||
let matches = match opts.parse(&args[1..]) {
|
let matches = match opts.parse(&args[1..]) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(f) => {
|
Err(f) => {
|
||||||
writeln!(
|
eprintln!("error: {}\n{}", f.to_string(), usage(&args[0], &opts));
|
||||||
stderr(),
|
|
||||||
"error: {}\n{}",
|
|
||||||
f.to_string(),
|
|
||||||
usage(&args[0], &opts)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -306,7 +322,7 @@ fn setup(args: &[String]) -> Setup {
|
||||||
.opt_str("format")
|
.opt_str("format")
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|format| AudioFormat::try_from(format).expect("Invalid output format"))
|
.map(|format| AudioFormat::try_from(format).expect("Invalid output format"))
|
||||||
.unwrap_or(AudioFormat::default());
|
.unwrap_or_default();
|
||||||
|
|
||||||
let device = matches.opt_str("device");
|
let device = matches.opt_str("device");
|
||||||
if device == Some("?".into()) {
|
if device == Some("?".into()) {
|
||||||
|
@ -320,8 +336,10 @@ fn setup(args: &[String]) -> Setup {
|
||||||
let mixer_config = MixerConfig {
|
let mixer_config = MixerConfig {
|
||||||
card: matches
|
card: matches
|
||||||
.opt_str("mixer-card")
|
.opt_str("mixer-card")
|
||||||
.unwrap_or(String::from("default")),
|
.unwrap_or_else(|| String::from("default")),
|
||||||
mixer: matches.opt_str("mixer-name").unwrap_or(String::from("PCM")),
|
mixer: matches
|
||||||
|
.opt_str("mixer-name")
|
||||||
|
.unwrap_or_else(|| String::from("PCM")),
|
||||||
index: matches
|
index: matches
|
||||||
.opt_str("mixer-index")
|
.opt_str("mixer-index")
|
||||||
.map(|index| index.parse::<u32>().unwrap())
|
.map(|index| index.parse::<u32>().unwrap())
|
||||||
|
@ -345,7 +363,7 @@ fn setup(args: &[String]) -> Setup {
|
||||||
.map(|p| AsRef::<Path>::as_ref(p).join("files"));
|
.map(|p| AsRef::<Path>::as_ref(p).join("files"));
|
||||||
system_dir = matches
|
system_dir = matches
|
||||||
.opt_str("system-cache")
|
.opt_str("system-cache")
|
||||||
.or_else(|| cache_dir)
|
.or(cache_dir)
|
||||||
.map(|p| p.into());
|
.map(|p| p.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,15 +393,17 @@ fn setup(args: &[String]) -> Setup {
|
||||||
.map(|port| port.parse::<u16>().unwrap())
|
.map(|port| port.parse::<u16>().unwrap())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
let name = matches.opt_str("name").unwrap_or("Librespot".to_string());
|
let name = matches
|
||||||
|
.opt_str("name")
|
||||||
|
.unwrap_or_else(|| "Librespot".to_string());
|
||||||
|
|
||||||
let credentials = {
|
let credentials = {
|
||||||
let cached_credentials = cache.as_ref().and_then(Cache::credentials);
|
let cached_credentials = cache.as_ref().and_then(Cache::credentials);
|
||||||
|
|
||||||
let password = |username: &String| -> String {
|
let password = |username: &String| -> Option<String> {
|
||||||
write!(stderr(), "Password for {}: ", username).unwrap();
|
write!(stderr(), "Password for {}: ", username).ok()?;
|
||||||
stderr().flush().unwrap();
|
stderr().flush().ok()?;
|
||||||
rpassword::read_password().unwrap()
|
rpassword::read_password().ok()
|
||||||
};
|
};
|
||||||
|
|
||||||
get_credentials(
|
get_credentials(
|
||||||
|
@ -399,8 +419,8 @@ fn setup(args: &[String]) -> Setup {
|
||||||
|
|
||||||
SessionConfig {
|
SessionConfig {
|
||||||
user_agent: version::VERSION_STRING.to_string(),
|
user_agent: version::VERSION_STRING.to_string(),
|
||||||
device_id: device_id,
|
device_id,
|
||||||
proxy: matches.opt_str("proxy").or(std::env::var("http_proxy").ok()).map(
|
proxy: matches.opt_str("proxy").or_else(|| std::env::var("http_proxy").ok()).map(
|
||||||
|s| {
|
|s| {
|
||||||
match Url::parse(&s) {
|
match Url::parse(&s) {
|
||||||
Ok(url) => {
|
Ok(url) => {
|
||||||
|
@ -430,26 +450,27 @@ fn setup(args: &[String]) -> Setup {
|
||||||
.opt_str("b")
|
.opt_str("b")
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
|
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
|
||||||
.unwrap_or(Bitrate::default());
|
.unwrap_or_default();
|
||||||
let gain_type = matches
|
let gain_type = matches
|
||||||
.opt_str("normalisation-gain-type")
|
.opt_str("normalisation-gain-type")
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|gain_type| {
|
.map(|gain_type| {
|
||||||
NormalisationType::from_str(gain_type).expect("Invalid normalisation type")
|
NormalisationType::from_str(gain_type).expect("Invalid normalisation type")
|
||||||
})
|
})
|
||||||
.unwrap_or(NormalisationType::default());
|
.unwrap_or_default();
|
||||||
let normalisation_method = matches
|
let normalisation_method = matches
|
||||||
.opt_str("normalisation-method")
|
.opt_str("normalisation-method")
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|gain_type| {
|
.map(|gain_type| {
|
||||||
NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method")
|
NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method")
|
||||||
})
|
})
|
||||||
.unwrap_or(NormalisationMethod::default());
|
.unwrap_or_default();
|
||||||
|
|
||||||
PlayerConfig {
|
PlayerConfig {
|
||||||
bitrate: bitrate,
|
bitrate,
|
||||||
gapless: !matches.opt_present("disable-gapless"),
|
gapless: !matches.opt_present("disable-gapless"),
|
||||||
normalisation: matches.opt_present("enable-volume-normalisation"),
|
normalisation: matches.opt_present("enable-volume-normalisation"),
|
||||||
normalisation_method: normalisation_method,
|
normalisation_method,
|
||||||
normalisation_type: gain_type,
|
normalisation_type: gain_type,
|
||||||
normalisation_pregain: matches
|
normalisation_pregain: matches
|
||||||
.opt_str("normalisation-pregain")
|
.opt_str("normalisation-pregain")
|
||||||
|
@ -488,19 +509,19 @@ fn setup(args: &[String]) -> Setup {
|
||||||
.opt_str("device-type")
|
.opt_str("device-type")
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type"))
|
.map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type"))
|
||||||
.unwrap_or(DeviceType::default());
|
.unwrap_or_default();
|
||||||
|
|
||||||
let volume_ctrl = matches
|
let volume_ctrl = matches
|
||||||
.opt_str("volume-ctrl")
|
.opt_str("volume-ctrl")
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|volume_ctrl| VolumeCtrl::from_str(volume_ctrl).expect("Invalid volume ctrl type"))
|
.map(|volume_ctrl| VolumeCtrl::from_str(volume_ctrl).expect("Invalid volume ctrl type"))
|
||||||
.unwrap_or(VolumeCtrl::default());
|
.unwrap_or_default();
|
||||||
|
|
||||||
ConnectConfig {
|
ConnectConfig {
|
||||||
name: name,
|
name,
|
||||||
device_type: device_type,
|
device_type,
|
||||||
volume: initial_volume,
|
volume: initial_volume,
|
||||||
volume_ctrl: volume_ctrl,
|
volume_ctrl,
|
||||||
autoplay: matches.opt_present("autoplay"),
|
autoplay: matches.opt_present("autoplay"),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -508,251 +529,197 @@ fn setup(args: &[String]) -> Setup {
|
||||||
let enable_discovery = !matches.opt_present("disable-discovery");
|
let enable_discovery = !matches.opt_present("disable-discovery");
|
||||||
|
|
||||||
Setup {
|
Setup {
|
||||||
format: format,
|
format,
|
||||||
backend: backend,
|
backend,
|
||||||
cache: cache,
|
cache,
|
||||||
session_config: session_config,
|
session_config,
|
||||||
player_config: player_config,
|
player_config,
|
||||||
connect_config: connect_config,
|
connect_config,
|
||||||
credentials: credentials,
|
credentials,
|
||||||
device: device,
|
device,
|
||||||
enable_discovery: enable_discovery,
|
enable_discovery,
|
||||||
zeroconf_port: zeroconf_port,
|
zeroconf_port,
|
||||||
mixer: mixer,
|
mixer,
|
||||||
mixer_config: mixer_config,
|
mixer_config,
|
||||||
player_event_program: matches.opt_str("onevent"),
|
player_event_program: matches.opt_str("onevent"),
|
||||||
emit_sink_events: matches.opt_present("emit-sink-events"),
|
emit_sink_events: matches.opt_present("emit-sink-events"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Main {
|
#[tokio::main(flavor = "current_thread")]
|
||||||
cache: Option<Cache>,
|
async fn main() {
|
||||||
player_config: PlayerConfig,
|
if env::var("RUST_BACKTRACE").is_err() {
|
||||||
session_config: SessionConfig,
|
env::set_var("RUST_BACKTRACE", "full")
|
||||||
connect_config: ConnectConfig,
|
|
||||||
format: AudioFormat,
|
|
||||||
backend: fn(Option<String>, AudioFormat) -> Box<dyn Sink>,
|
|
||||||
device: Option<String>,
|
|
||||||
mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>,
|
|
||||||
mixer_config: MixerConfig,
|
|
||||||
handle: Handle,
|
|
||||||
|
|
||||||
discovery: Option<DiscoveryStream>,
|
|
||||||
signal: IoStream<()>,
|
|
||||||
|
|
||||||
spirc: Option<Spirc>,
|
|
||||||
spirc_task: Option<SpircTask>,
|
|
||||||
connect: Box<dyn Future<Item = Session, Error = AuthenticationError>>,
|
|
||||||
|
|
||||||
shutdown: bool,
|
|
||||||
last_credentials: Option<Credentials>,
|
|
||||||
auto_connect_times: Vec<Instant>,
|
|
||||||
|
|
||||||
player_event_channel: Option<UnboundedReceiver<PlayerEvent>>,
|
|
||||||
player_event_program: Option<String>,
|
|
||||||
emit_sink_events: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Main {
|
|
||||||
fn new(handle: Handle, setup: Setup) -> Main {
|
|
||||||
let mut task = Main {
|
|
||||||
handle: handle.clone(),
|
|
||||||
cache: setup.cache,
|
|
||||||
session_config: setup.session_config,
|
|
||||||
player_config: setup.player_config,
|
|
||||||
connect_config: setup.connect_config,
|
|
||||||
format: setup.format,
|
|
||||||
backend: setup.backend,
|
|
||||||
device: setup.device,
|
|
||||||
mixer: setup.mixer,
|
|
||||||
mixer_config: setup.mixer_config,
|
|
||||||
|
|
||||||
connect: Box::new(futures::future::empty()),
|
|
||||||
discovery: None,
|
|
||||||
spirc: None,
|
|
||||||
spirc_task: None,
|
|
||||||
shutdown: false,
|
|
||||||
last_credentials: None,
|
|
||||||
auto_connect_times: Vec::new(),
|
|
||||||
signal: Box::new(tokio_signal::ctrl_c().flatten_stream()),
|
|
||||||
|
|
||||||
player_event_channel: None,
|
|
||||||
player_event_program: setup.player_event_program,
|
|
||||||
emit_sink_events: setup.emit_sink_events,
|
|
||||||
};
|
|
||||||
|
|
||||||
if setup.enable_discovery {
|
|
||||||
let config = task.connect_config.clone();
|
|
||||||
let device_id = task.session_config.device_id.clone();
|
|
||||||
|
|
||||||
task.discovery =
|
|
||||||
Some(discovery(&handle, config, device_id, setup.zeroconf_port).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(credentials) = setup.credentials {
|
|
||||||
task.credentials(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
task
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn credentials(&mut self, credentials: Credentials) {
|
let args: Vec<String> = std::env::args().collect();
|
||||||
self.last_credentials = Some(credentials.clone());
|
let setup = get_setup(&args);
|
||||||
let config = self.session_config.clone();
|
|
||||||
let handle = self.handle.clone();
|
|
||||||
|
|
||||||
let connection = Session::connect(config, credentials, self.cache.clone(), handle);
|
let mut last_credentials = None;
|
||||||
|
let mut spirc: Option<Spirc> = None;
|
||||||
|
let mut spirc_task: Option<Pin<_>> = None;
|
||||||
|
let mut player_event_channel: Option<UnboundedReceiver<PlayerEvent>> = None;
|
||||||
|
let mut auto_connect_times: Vec<Instant> = vec![];
|
||||||
|
let mut discovery = None;
|
||||||
|
let mut connecting: Pin<Box<dyn future::FusedFuture<Output = _>>> = Box::pin(future::pending());
|
||||||
|
|
||||||
self.connect = connection;
|
if setup.enable_discovery {
|
||||||
self.spirc = None;
|
let config = setup.connect_config.clone();
|
||||||
let task = mem::replace(&mut self.spirc_task, None);
|
let device_id = setup.session_config.device_id.clone();
|
||||||
if let Some(task) = task {
|
|
||||||
self.handle.spawn(task);
|
discovery = Some(
|
||||||
}
|
librespot_connect::discovery::discovery(config, device_id, setup.zeroconf_port)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Future for Main {
|
if let Some(credentials) = setup.credentials {
|
||||||
type Item = ();
|
last_credentials = Some(credentials.clone());
|
||||||
type Error = ();
|
connecting = Box::pin(
|
||||||
|
Session::connect(
|
||||||
|
setup.session_config.clone(),
|
||||||
|
credentials,
|
||||||
|
setup.cache.clone(),
|
||||||
|
)
|
||||||
|
.fuse(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn poll(&mut self) -> Poll<(), ()> {
|
loop {
|
||||||
loop {
|
tokio::select! {
|
||||||
let mut progress = false;
|
credentials = async { discovery.as_mut().unwrap().next().await }, if discovery.is_some() => {
|
||||||
|
match credentials {
|
||||||
|
Some(credentials) => {
|
||||||
|
last_credentials = Some(credentials.clone());
|
||||||
|
auto_connect_times.clear();
|
||||||
|
|
||||||
if let Some(Async::Ready(Some(creds))) =
|
if let Some(spirc) = spirc.take() {
|
||||||
self.discovery.as_mut().map(|d| d.poll().unwrap())
|
spirc.shutdown();
|
||||||
{
|
}
|
||||||
if let Some(ref spirc) = self.spirc {
|
if let Some(spirc_task) = spirc_task.take() {
|
||||||
spirc.shutdown();
|
// Continue shutdown in its own task
|
||||||
|
tokio::spawn(spirc_task);
|
||||||
|
}
|
||||||
|
|
||||||
|
connecting = Box::pin(Session::connect(
|
||||||
|
setup.session_config.clone(),
|
||||||
|
credentials,
|
||||||
|
setup.cache.clone(),
|
||||||
|
).fuse());
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
warn!("Discovery stopped!");
|
||||||
|
discovery = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.auto_connect_times.clear();
|
},
|
||||||
self.credentials(creds);
|
session = &mut connecting, if !connecting.is_terminated() => match session {
|
||||||
|
Ok(session) => {
|
||||||
progress = true;
|
let mixer_config = setup.mixer_config.clone();
|
||||||
}
|
let mixer = (setup.mixer)(Some(mixer_config));
|
||||||
|
let player_config = setup.player_config.clone();
|
||||||
match self.connect.poll() {
|
let connect_config = setup.connect_config.clone();
|
||||||
Ok(Async::Ready(session)) => {
|
|
||||||
self.connect = Box::new(futures::future::empty());
|
|
||||||
let mixer_config = self.mixer_config.clone();
|
|
||||||
let mixer = (self.mixer)(Some(mixer_config));
|
|
||||||
let player_config = self.player_config.clone();
|
|
||||||
let connect_config = self.connect_config.clone();
|
|
||||||
|
|
||||||
let audio_filter = mixer.get_audio_filter();
|
let audio_filter = mixer.get_audio_filter();
|
||||||
let format = self.format;
|
let format = setup.format;
|
||||||
let backend = self.backend;
|
let backend = setup.backend;
|
||||||
let device = self.device.clone();
|
let device = setup.device.clone();
|
||||||
let (player, event_channel) =
|
let (player, event_channel) =
|
||||||
Player::new(player_config, session.clone(), audio_filter, move || {
|
Player::new(player_config, session.clone(), audio_filter, move || {
|
||||||
(backend)(device, format)
|
(backend)(device, format)
|
||||||
});
|
});
|
||||||
|
|
||||||
if self.emit_sink_events {
|
if setup.emit_sink_events {
|
||||||
if let Some(player_event_program) = &self.player_event_program {
|
if let Some(player_event_program) = setup.player_event_program.clone() {
|
||||||
let player_event_program = player_event_program.clone();
|
|
||||||
player.set_sink_event_callback(Some(Box::new(move |sink_status| {
|
player.set_sink_event_callback(Some(Box::new(move |sink_status| {
|
||||||
emit_sink_event(sink_status, &player_event_program)
|
match emit_sink_event(sink_status, &player_event_program) {
|
||||||
|
Ok(e) if e.success() => (),
|
||||||
|
Ok(e) => {
|
||||||
|
if let Some(code) = e.code() {
|
||||||
|
warn!("Sink event prog returned exit code {}", code);
|
||||||
|
} else {
|
||||||
|
warn!("Sink event prog returned failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Emitting sink event failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer);
|
let (spirc_, spirc_task_) = Spirc::new(connect_config, session, player, mixer);
|
||||||
self.spirc = Some(spirc);
|
|
||||||
self.spirc_task = Some(spirc_task);
|
|
||||||
self.player_event_channel = Some(event_channel);
|
|
||||||
|
|
||||||
progress = true;
|
spirc = Some(spirc_);
|
||||||
|
spirc_task = Some(Box::pin(spirc_task_));
|
||||||
|
player_event_channel = Some(event_channel);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Connection failed: {}", e);
|
||||||
}
|
}
|
||||||
Ok(Async::NotReady) => (),
|
},
|
||||||
Err(error) => {
|
_ = async { spirc_task.as_mut().unwrap().await }, if spirc_task.is_some() => {
|
||||||
error!("Could not connect to server: {}", error);
|
spirc_task = None;
|
||||||
self.connect = Box::new(futures::future::empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Async::Ready(Some(())) = self.signal.poll().unwrap() {
|
warn!("Spirc shut down unexpectedly");
|
||||||
trace!("Ctrl-C received");
|
while !auto_connect_times.is_empty()
|
||||||
if !self.shutdown {
|
&& ((Instant::now() - auto_connect_times[0]).as_secs() > 600)
|
||||||
if let Some(ref spirc) = self.spirc {
|
|
||||||
spirc.shutdown();
|
|
||||||
} else {
|
|
||||||
return Ok(Async::Ready(()));
|
|
||||||
}
|
|
||||||
self.shutdown = true;
|
|
||||||
} else {
|
|
||||||
return Ok(Async::Ready(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
progress = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut drop_spirc_and_try_to_reconnect = false;
|
|
||||||
if let Some(ref mut spirc_task) = self.spirc_task {
|
|
||||||
if let Async::Ready(()) = spirc_task.poll().unwrap() {
|
|
||||||
if self.shutdown {
|
|
||||||
return Ok(Async::Ready(()));
|
|
||||||
} else {
|
|
||||||
warn!("Spirc shut down unexpectedly");
|
|
||||||
drop_spirc_and_try_to_reconnect = true;
|
|
||||||
}
|
|
||||||
progress = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if drop_spirc_and_try_to_reconnect {
|
|
||||||
self.spirc_task = None;
|
|
||||||
while (!self.auto_connect_times.is_empty())
|
|
||||||
&& ((Instant::now() - self.auto_connect_times[0]).as_secs() > 600)
|
|
||||||
{
|
{
|
||||||
let _ = self.auto_connect_times.remove(0);
|
let _ = auto_connect_times.remove(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(credentials) = self.last_credentials.clone() {
|
if let Some(credentials) = last_credentials.clone() {
|
||||||
if self.auto_connect_times.len() >= 5 {
|
if auto_connect_times.len() >= 5 {
|
||||||
warn!("Spirc shut down too often. Not reconnecting automatically.");
|
warn!("Spirc shut down too often. Not reconnecting automatically.");
|
||||||
} else {
|
} else {
|
||||||
self.auto_connect_times.push(Instant::now());
|
auto_connect_times.push(Instant::now());
|
||||||
self.credentials(credentials);
|
|
||||||
|
connecting = Box::pin(Session::connect(
|
||||||
|
setup.session_config.clone(),
|
||||||
|
credentials,
|
||||||
|
setup.cache.clone(),
|
||||||
|
).fuse());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
event = async { player_event_channel.as_mut().unwrap().recv().await }, if player_event_channel.is_some() => match event {
|
||||||
if let Some(ref mut player_event_channel) = self.player_event_channel {
|
Some(event) => {
|
||||||
if let Async::Ready(Some(event)) = player_event_channel.poll().unwrap() {
|
if let Some(program) = &setup.player_event_program {
|
||||||
progress = true;
|
|
||||||
if let Some(ref program) = self.player_event_program {
|
|
||||||
if let Some(child) = run_program_on_events(event, program) {
|
if let Some(child) = run_program_on_events(event, program) {
|
||||||
let child = child
|
let mut child = child.expect("program failed to start");
|
||||||
.expect("program failed to start")
|
|
||||||
.map(|status| {
|
|
||||||
if !status.success() {
|
|
||||||
error!("child exited with status {:?}", status.code());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map_err(|e| error!("failed to wait on child process: {}", e));
|
|
||||||
|
|
||||||
self.handle.spawn(child);
|
tokio::spawn(async move {
|
||||||
|
match child.wait().await {
|
||||||
|
Ok(status) if !status.success() => error!("child exited with status {:?}", status.code()),
|
||||||
|
Err(e) => error!("failed to wait on child process: {}", e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
player_event_channel = None;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !progress {
|
info!("Gracefully shutting down");
|
||||||
return Ok(Async::NotReady);
|
|
||||||
|
// Shutdown spirc if necessary
|
||||||
|
if let Some(spirc) = spirc {
|
||||||
|
spirc.shutdown();
|
||||||
|
|
||||||
|
if let Some(mut spirc_task) = spirc_task {
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::signal::ctrl_c() => (),
|
||||||
|
_ = spirc_task.as_mut() => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
|
||||||
if env::var("RUST_BACKTRACE").is_err() {
|
|
||||||
env::set_var("RUST_BACKTRACE", "full")
|
|
||||||
}
|
|
||||||
let mut core = Core::new().unwrap();
|
|
||||||
let handle = core.handle();
|
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
|
||||||
|
|
||||||
core.run(Main::new(handle, setup(&args))).unwrap()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,23 +1,13 @@
|
||||||
use librespot::playback::player::PlayerEvent;
|
use librespot::playback::player::PlayerEvent;
|
||||||
|
use librespot::playback::player::SinkStatus;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use tokio::process::{Child as AsyncChild, Command as AsyncCommand};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::process::Command;
|
use std::process::{Command, ExitStatus};
|
||||||
use tokio_process::{Child, CommandExt};
|
|
||||||
|
|
||||||
use futures::Future;
|
pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<io::Result<AsyncChild>> {
|
||||||
use librespot::playback::player::SinkStatus;
|
|
||||||
|
|
||||||
fn run_program(program: &str, env_vars: HashMap<&str, String>) -> io::Result<Child> {
|
|
||||||
let mut v: Vec<&str> = program.split_whitespace().collect();
|
|
||||||
info!("Running {:?} with environment variables {:?}", v, env_vars);
|
|
||||||
Command::new(&v.remove(0))
|
|
||||||
.args(&v)
|
|
||||||
.envs(env_vars.iter())
|
|
||||||
.spawn_async()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<io::Result<Child>> {
|
|
||||||
let mut env_vars = HashMap::new();
|
let mut env_vars = HashMap::new();
|
||||||
match event {
|
match event {
|
||||||
PlayerEvent::Changed {
|
PlayerEvent::Changed {
|
||||||
|
@ -68,10 +58,18 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<io::Re
|
||||||
}
|
}
|
||||||
_ => return None,
|
_ => return None,
|
||||||
}
|
}
|
||||||
Some(run_program(onevent, env_vars))
|
|
||||||
|
let mut v: Vec<&str> = onevent.split_whitespace().collect();
|
||||||
|
info!("Running {:?} with environment variables {:?}", v, env_vars);
|
||||||
|
Some(
|
||||||
|
AsyncCommand::new(&v.remove(0))
|
||||||
|
.args(&v)
|
||||||
|
.envs(env_vars.iter())
|
||||||
|
.spawn(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) {
|
pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) -> io::Result<ExitStatus> {
|
||||||
let mut env_vars = HashMap::new();
|
let mut env_vars = HashMap::new();
|
||||||
env_vars.insert("PLAYER_EVENT", "sink".to_string());
|
env_vars.insert("PLAYER_EVENT", "sink".to_string());
|
||||||
let sink_status = match sink_status {
|
let sink_status = match sink_status {
|
||||||
|
@ -80,6 +78,12 @@ pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) {
|
||||||
SinkStatus::Closed => "closed",
|
SinkStatus::Closed => "closed",
|
||||||
};
|
};
|
||||||
env_vars.insert("SINK_STATUS", sink_status.to_string());
|
env_vars.insert("SINK_STATUS", sink_status.to_string());
|
||||||
|
let mut v: Vec<&str> = onevent.split_whitespace().collect();
|
||||||
|
info!("Running {:?} with environment variables {:?}", v, env_vars);
|
||||||
|
|
||||||
let _ = run_program(onevent, env_vars).and_then(|child| child.wait());
|
Command::new(&v.remove(0))
|
||||||
|
.args(&v)
|
||||||
|
.envs(env_vars.iter())
|
||||||
|
.spawn()?
|
||||||
|
.wait()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue