Merge pull request #665 from librespot-org/tokio_migration

Tokio migration
This commit is contained in:
Sasha Hilton 2021-04-13 02:06:01 +01:00 committed by GitHub
commit 7b537550ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 3425 additions and 4468 deletions

View file

@ -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

View file

@ -13,7 +13,7 @@ curl https://sh.rustup.rs -sSf | sh
Follow any prompts it gives you to install Rust. Once thats done, Rust's standard tools should be setup and ready to use. Follow any prompts it gives you to install Rust. Once thats done, Rust's standard tools should be setup and ready to use.
*Note: The current minimum required Rust version at the time of writing is 1.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

File diff suppressed because it is too large Load diff

View file

@ -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"

View file

@ -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 }

View file

@ -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)
}
} }

File diff suppressed because it is too large Load diff

509
audio/src/fetch/mod.rs Normal file
View 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
View 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();
}
}
}
}
}

View file

@ -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

View file

@ -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)
} }
} }

View file

@ -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;

View file

@ -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));
} }

View file

@ -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
} }
} }

View file

@ -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"]

View file

@ -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 {

View file

@ -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(&params),
(Post, Some("addUser")) => this.handle_add_user(&params),
_ => 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)
} }
} }

View file

@ -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;

View file

@ -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());
} }
} }

View file

@ -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"]

View file

@ -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);

View file

@ -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);
}
} }

View file

@ -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),
}
}
}

View file

@ -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(),
}
}
}

View file

@ -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),
} }
} }
} }

View file

@ -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()
}
}

View file

@ -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"),

View file

@ -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;

View file

@ -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>) {

View file

@ -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"), }
}),
)
} }

View file

@ -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,

View file

@ -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
}))
} }

View file

@ -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();
}
}

View file

@ -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));
} }
} }

View file

@ -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();
}
}
}

View file

@ -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,
} }
} }
} }

View file

@ -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)
}

View file

@ -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");

View file

@ -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
View 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)
}
}

View file

@ -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
View 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"),
};
}

View file

@ -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()
); );
} }

View file

@ -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");
} }

View file

@ -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);
} }
} }

View file

@ -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"

View file

@ -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,
} }
} }
} }

View file

@ -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"]

View file

@ -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(())
} }
} }

View file

@ -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,
} }
} }
} }

View file

@ -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,
} }
} }
} }

View file

@ -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()

View file

@ -18,10 +18,7 @@ impl Open for StdoutSink {
_ => Box::new(io::stdout()), _ => Box::new(io::stdout()),
}; };
Self { Self { output, format }
output: output,
format: format,
}
} }
} }

View file

@ -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)
} }
}; };

View file

@ -38,9 +38,9 @@ impl Open for PulseAudioSink {
Self { Self {
s: None, s: None,
ss: ss, ss,
device: device, device,
format: format, format,
} }
} }
} }

View file

@ -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(())
} }
} }

View file

@ -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)
} }

View file

@ -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");

View file

@ -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;

View file

@ -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;

View file

@ -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,
}, },
}) })
} }

View file

@ -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")]

View file

@ -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,

View file

@ -1,3 +1,4 @@
# max_width = 105 # max_width = 105
reorder_imports = true reorder_imports = true
reorder_modules = true reorder_modules = true
edition = "2018"

View file

@ -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;

View file

@ -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()
}

View file

@ -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()
} }