Merge branch 'master' into master

This commit is contained in:
Artyom Pavlov 2018-12-06 09:38:33 +00:00 committed by GitHub
commit 772591576a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 3473 additions and 9899 deletions

1961
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -48,6 +48,7 @@ serde_derive = "0.9.6"
serde_json = "0.9.5" serde_json = "0.9.5"
tokio-core = "0.1.2" tokio-core = "0.1.2"
tokio-io = "0.1" tokio-io = "0.1"
tokio-process = "0.2.2"
tokio-signal = "0.1.2" tokio-signal = "0.1.2"
url = "1.7.0" url = "1.7.0"
sha-1 = "0.8.0" sha-1 = "0.8.0"

View file

@ -13,11 +13,11 @@ Note: librespot only works with Spotify Premium
As the origin by [plietar](https://github.com/plietar/) is no longer actively maintained, this organisation and repository have been set up so that the project may be maintained and upgraded in the future. As the origin by [plietar](https://github.com/plietar/) is no longer actively maintained, this organisation and repository have been set up so that the project may be maintained and upgraded in the future.
# Documentation # Documentation
Documentation is currently a work in progress. Documentation is currently a work in progress.
There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder, and more general usage and compilation information is available on the [wiki](https://github.com/librespot-org/librespot/wiki). There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder, and more general usage and compilation information is available on the [wiki](https://github.com/librespot-org/librespot/wiki).
[CONTRIBUTING.md](https://github.com/librespot-org/librespot/blob/master/CONTRIBUTING.md) also contains detailed instructions on setting up a development environment, compilation, and contributing guidelines. [CONTRIBUTING.md](https://github.com/librespot-org/librespot/blob/master/CONTRIBUTING.md) also contains detailed instructions on setting up a development environment, compilation, and contributing guidelines.
If you wish to learn more about how librespot works overall, the best way is to simply read the code, and ask any questions you have in the Gitter chat linked above. If you wish to learn more about how librespot works overall, the best way is to simply read the code, and ask any questions you have in the Gitter chat linked above.
@ -26,7 +26,7 @@ If you wish to learn more about how librespot works overall, the best way is to
If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, eg. the Spotify URI of the song that caused the crash. If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, eg. the Spotify URI of the song that caused the crash.
# Building # Building
Rust 1.21.0 or later is required to build librespot. Rust 1.23.0 or later is required to build librespot.
**If you are building librespot on macOS, the homebrew provided rust may fail due to the way in which homebrew installs rust. In this case, uninstall the homebrew version of rust and use [rustup](https://www.rustup.rs/), and librespot should then build. This should have been fixed in more recent versions of Homebrew, but we're leaving this notice here as a warning.** **If you are building librespot on macOS, the homebrew provided rust may fail due to the way in which homebrew installs rust. In this case, uninstall the homebrew version of rust and use [rustup](https://www.rustup.rs/), and librespot should then build. This should have been fixed in more recent versions of Homebrew, but we're leaving this notice here as a warning.**
@ -58,7 +58,7 @@ cargo build --release
A sample program implementing a headless Spotify Connect receiver is provided. A sample program implementing a headless Spotify Connect receiver is provided.
Once you've built *librespot*, run it using : Once you've built *librespot*, run it using :
```shell ```shell
target/release/librespot --name DEVICENAME target/release/librespot --name DEVICENAME
``` ```
The above is a minimal example. Here is a more fully fledged one: The above is a minimal example. Here is a more fully fledged one:
@ -87,5 +87,6 @@ This is a non exhaustive list of projects that either use or have modified libre
- [plugin.audio.spotify](https://github.com/marcelveldt/plugin.audio.spotify) - A Kodi plugin for Spotify. - [plugin.audio.spotify](https://github.com/marcelveldt/plugin.audio.spotify) - A Kodi plugin for Spotify.
- [raspotify](https://github.com/dtcooper/raspotify) - Spotify Connect client for the Raspberry Pi that Just Works™ - [raspotify](https://github.com/dtcooper/raspotify) - Spotify Connect client for the Raspberry Pi that Just Works™
- [Spotifyd](https://github.com/Spotifyd/spotifyd) - A stripped down librespot UNIX daemon. - [Spotifyd](https://github.com/Spotifyd/spotifyd) - A stripped down librespot UNIX daemon.
- [Spotcontrol](https://github.com/badfortrains/spotcontrol) - A golang implementation of a Spotify Connect controller. No playback functionality. - [Spotcontrol](https://github.com/badfortrains/spotcontrol) - A golang implementation of a Spotify Connect controller. No playback
functionality.
- [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot.

View file

@ -10,7 +10,7 @@ path = "../core"
bit-set = "0.4.0" bit-set = "0.4.0"
byteorder = "1.0" byteorder = "1.0"
futures = "0.1.8" futures = "0.1.8"
lewton = "0.8.0" lewton = "0.9.3"
log = "0.3.5" log = "0.3.5"
num-bigint = "0.1.35" num-bigint = "0.1.35"
num-traits = "0.1.36" num-traits = "0.1.36"

View file

@ -348,11 +348,16 @@ impl Read for AudioFileStreaming {
impl Seek for AudioFileStreaming { impl Seek for AudioFileStreaming {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.position = try!(self.read_file.seek(pos)); self.position = try!(self.read_file.seek(pos));
// Do not seek past EOF
if (self.position as usize % CHUNK_SIZE) != 0 {
// Notify the fetch thread to get the correct block
// This can fail if fetch thread has completed, in which case the
// block is ready. Just ignore the error.
let _ = self.seek.unbounded_send(self.position);
} else {
warn!("Trying to seek past EOF");
}
// Notify the fetch thread to get the correct block
// This can fail if fetch thread has completed, in which case the
// block is ready. Just ignore the error.
let _ = self.seek.unbounded_send(self.position);
Ok(self.position) Ok(self.position)
} }
} }

View file

@ -16,7 +16,7 @@ futures = "0.1.8"
hyper = "0.11.2" hyper = "0.11.2"
log = "0.3.5" log = "0.3.5"
num-bigint = "0.1.35" num-bigint = "0.1.35"
protobuf = "1.1" protobuf = "2.0.5"
rand = "0.3.13" rand = "0.3.13"
serde = "0.9.6" serde = "0.9.6"
serde_derive = "0.9.6" serde_derive = "0.9.6"

54
contrib/Dockerfile.Rpi Normal file
View file

@ -0,0 +1,54 @@
# Create a docker image for the RPI
# Build the docker image from the root of the project with the following command :
# $ docker build -t librespot-rpi -f .\contrib\Dockerfile.Rpi .
#
# This builds a docker image which is usable when running docker on the rpi.
#
# This Dockerfile builds in windows without any requirements, for linux based systems you might need to run the following line:
# docker run --rm --privileged multiarch/qemu-user-static:register --reset
# (see here for more info: https://gist.github.com/PieterScheffers/d50f609d9628383e4c9d8d7d269b7643 )
#
# Save the docker image to a file:
# $ docker save -o contrib/librespot-rpi librespot-rpi
#
# Move it to the rpi and import it with:
# docker load -i librespot-rpi
#
# Run it with:
# docker run -d --restart unless-stopped $(for DEV in $(find /dev/snd -type c); do echo --device=$DEV:$DEV; done) --net=host --name librespot-rpi librespot-rpi --name {devicename}
FROM debian:stretch
RUN dpkg --add-architecture armhf
RUN apt-get update
RUN apt-get install -y curl git build-essential crossbuild-essential-armhf
RUN apt-get install -y libasound2-dev libasound2-dev:armhf
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add arm-unknown-linux-gnueabihf
RUN mkdir /.cargo && \
echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
ADD . /src
WORKDIR /src
RUN cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend"
FROM resin/rpi-raspbian
RUN apt-get update && \
apt-get install libasound2 && \
rm -rf /var/lib/apt/lists/*
RUN mkdir /librespot
WORKDIR /librespot
COPY --from=0 /build/arm-unknown-linux-gnueabihf/release/librespot .
RUN chmod +x librespot
ENTRYPOINT ["./librespot"]

View file

@ -22,7 +22,7 @@ log = "0.3.5"
num-bigint = "0.1.35" num-bigint = "0.1.35"
num-integer = "0.1.32" num-integer = "0.1.32"
num-traits = "0.1.36" num-traits = "0.1.36"
protobuf = "1.1" protobuf = "2.0.5"
rand = "0.3.13" rand = "0.3.13"
rpassword = "0.3.0" rpassword = "0.3.0"
serde = "0.9.6" serde = "0.9.6"

View file

@ -17,7 +17,11 @@ pub struct APResolveData {
ap_list: Vec<String>, ap_list: Vec<String>,
} }
fn apresolve(handle: &Handle, proxy: &Option<Url>) -> Box<Future<Item = String, Error = Error>> { fn apresolve(
handle: &Handle,
proxy: &Option<Url>,
ap_port: &Option<u16>,
) -> Box<Future<Item = String, Error = Error>> {
let url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL"); let url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL");
let use_proxy = proxy.is_some(); let use_proxy = proxy.is_some();
@ -53,9 +57,15 @@ fn apresolve(handle: &Handle, proxy: &Option<Url>) -> Box<Future<Item = String,
let data = let data =
body.and_then(|body| serde_json::from_str::<APResolveData>(&body).chain_err(|| "invalid JSON")); body.and_then(|body| serde_json::from_str::<APResolveData>(&body).chain_err(|| "invalid JSON"));
let p = ap_port.clone();
let ap = data.and_then(move |data| { let ap = data.and_then(move |data| {
let mut aps = data.ap_list.iter().filter(|ap| { let mut aps = data.ap_list.iter().filter(|ap| {
if use_proxy { 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. // It is unlikely that the proxy will accept CONNECT on anything other than 443.
Uri::from_str(ap) Uri::from_str(ap)
.ok() .ok()
@ -75,11 +85,12 @@ fn apresolve(handle: &Handle, proxy: &Option<Url>) -> Box<Future<Item = String,
pub(crate) fn apresolve_or_fallback<E>( pub(crate) fn apresolve_or_fallback<E>(
handle: &Handle, handle: &Handle,
proxy: &Option<Url>, proxy: &Option<Url>,
ap_port: &Option<u16>,
) -> Box<Future<Item = String, Error = E>> ) -> Box<Future<Item = String, Error = E>>
where where
E: 'static, E: 'static,
{ {
let ap = apresolve(handle, proxy).or_else(|e| { let ap = apresolve(handle, proxy, ap_port).or_else(|e| {
warn!("Failed to resolve Access Point: {}", e.description()); warn!("Failed to resolve Access Point: {}", e.description());
warn!("Using fallback \"{}\"", AP_FALLBACK); warn!("Using fallback \"{}\"", AP_FALLBACK);
Ok(AP_FALLBACK.into()) Ok(AP_FALLBACK.into())

View file

@ -10,6 +10,7 @@ pub struct SessionConfig {
pub user_agent: String, pub user_agent: String,
pub device_id: String, pub device_id: String,
pub proxy: Option<Url>, pub proxy: Option<Url>,
pub ap_port: Option<u16>,
} }
impl Default for SessionConfig { impl Default for SessionConfig {
@ -19,6 +20,7 @@ impl Default for SessionConfig {
user_agent: version::version_string(), user_agent: version::version_string(),
device_id: device_id, device_id: device_id,
proxy: None, proxy: None,
ap_port: None,
} }
} }
} }

View file

@ -2,7 +2,7 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use sha1::Sha1; use sha1::Sha1;
use futures::{Async, Future, Poll}; use futures::{Async, Future, Poll};
use protobuf::{self, Message, MessageStatic}; use protobuf::{self, Message};
use rand::thread_rng; use rand::thread_rng;
use std::io::{self, Read}; use std::io::{self, Read};
use std::marker::PhantomData; use std::marker::PhantomData;
@ -125,7 +125,7 @@ fn client_response<T: AsyncWrite>(connection: T, challenge: Vec<u8>) -> WriteAll
write_all(connection, buffer) write_all(connection, buffer)
} }
enum RecvPacket<T, M: MessageStatic> { enum RecvPacket<T, M: Message> {
Header(ReadExact<T, Window<Vec<u8>>>, PhantomData<M>), Header(ReadExact<T, Window<Vec<u8>>>, PhantomData<M>),
Body(ReadExact<T, Window<Vec<u8>>>, PhantomData<M>), Body(ReadExact<T, Window<Vec<u8>>>, PhantomData<M>),
} }
@ -133,7 +133,7 @@ enum RecvPacket<T, M: MessageStatic> {
fn recv_packet<T: AsyncRead, M>(connection: T, acc: Vec<u8>) -> RecvPacket<T, M> fn recv_packet<T: AsyncRead, M>(connection: T, acc: Vec<u8>) -> RecvPacket<T, M>
where where
T: Read, T: Read,
M: MessageStatic, M: Message,
{ {
RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData) RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData)
} }
@ -141,7 +141,7 @@ where
impl<T: AsyncRead, M> Future for RecvPacket<T, M> impl<T: AsyncRead, M> Future for RecvPacket<T, M>
where where
T: Read, T: Read,
M: MessageStatic, M: Message,
{ {
type Item = (T, M, Vec<u8>); type Item = (T, M, Vec<u8>);
type Error = io::Error; type Error = io::Error;

View file

@ -51,7 +51,7 @@ impl Session {
cache: Option<Cache>, cache: Option<Cache>,
handle: Handle, handle: Handle,
) -> Box<Future<Item = Session, Error = io::Error>> { ) -> Box<Future<Item = Session, Error = io::Error>> {
let access_point = apresolve_or_fallback::<io::Error>(&handle, &config.proxy); let access_point = apresolve_or_fallback::<io::Error>(&handle, &config.proxy, &config.ap_port);
let handle_ = handle.clone(); let handle_ = handle.clone();
let proxy = config.proxy.clone(); let proxy = config.proxy.clone();

View file

@ -6,7 +6,7 @@ An AP is randomly picked from that list to connect to.
The connection is done using a bare TCP socket. Despite many APs using ports 80 and 443, neither HTTP nor TLS are used to connect. The connection is done using a bare TCP socket. Despite many APs using ports 80 and 443, neither HTTP nor TLS are used to connect.
If `http://apresolve.spotify.com` is unresponsive, `ap.spotify.com:80` is used as a fallback. If `http://apresolve.spotify.com` is unresponsive, `ap.spotify.com:443` is used as a fallback.
## Connection Hello ## Connection Hello
The first 3 packets exchanged are unencrypted, and have the following format : The first 3 packets exchanged are unencrypted, and have the following format :

View file

@ -33,7 +33,8 @@ fn main() {
let backend = audio_backend::find(None).unwrap(); let backend = audio_backend::find(None).unwrap();
println!("Connecting .."); println!("Connecting ..");
let session = core.run(Session::connect(session_config, credentials, None, handle)) let session = core
.run(Session::connect(session_config, credentials, None, handle))
.unwrap(); .unwrap();
let (player, _) = Player::new(player_config, session.clone(), None, move || (backend)(None)); let (player, _) = Player::new(player_config, session.clone(), None, move || (backend)(None));

View file

@ -7,7 +7,7 @@ authors = ["Paul Lietar <paul@lietar.net>"]
byteorder = "1.0" byteorder = "1.0"
futures = "0.1.8" futures = "0.1.8"
linear-map = "1.0" linear-map = "1.0"
protobuf = "1.1" protobuf = "2.0.5"
[dependencies.librespot-core] [dependencies.librespot-core]
path = "../core" path = "../core"

View file

@ -53,7 +53,7 @@ where
} }
pub trait Metadata: Send + Sized + 'static { pub trait Metadata: Send + Sized + 'static {
type Message: protobuf::MessageStatic; type Message: protobuf::Message;
fn base_url() -> &'static str; fn base_url() -> &'static str;
fn parse(msg: &Self::Message, session: &Session) -> Self; fn parse(msg: &Self::Message, session: &Session) -> Self;
@ -110,13 +110,15 @@ impl Metadata for Track {
fn parse(msg: &Self::Message, session: &Session) -> Self { fn parse(msg: &Self::Message, session: &Session) -> Self {
let country = session.country(); let country = session.country();
let artists = msg.get_artist() let artists = msg
.get_artist()
.iter() .iter()
.filter(|artist| artist.has_gid()) .filter(|artist| artist.has_gid())
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap()) .map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let files = msg.get_file() let files = msg
.get_file()
.iter() .iter()
.filter(|file| file.has_file_id()) .filter(|file| file.has_file_id())
.map(|file| { .map(|file| {
@ -133,7 +135,8 @@ impl Metadata for Track {
album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(), album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(),
artists: artists, artists: artists,
files: files, files: files,
alternatives: msg.get_alternative() alternatives: msg
.get_alternative()
.iter() .iter()
.map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap()) .map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap())
.collect(), .collect(),
@ -150,20 +153,23 @@ impl Metadata for Album {
} }
fn parse(msg: &Self::Message, _: &Session) -> Self { fn parse(msg: &Self::Message, _: &Session) -> Self {
let artists = msg.get_artist() let artists = msg
.get_artist()
.iter() .iter()
.filter(|artist| artist.has_gid()) .filter(|artist| artist.has_gid())
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap()) .map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let tracks = msg.get_disc() let tracks = msg
.get_disc()
.iter() .iter()
.flat_map(|disc| disc.get_track()) .flat_map(|disc| disc.get_track())
.filter(|track| track.has_gid()) .filter(|track| track.has_gid())
.map(|track| SpotifyId::from_raw(track.get_gid()).unwrap()) .map(|track| SpotifyId::from_raw(track.get_gid()).unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let covers = msg.get_cover_group() let covers = msg
.get_cover_group()
.get_image() .get_image()
.iter() .iter()
.filter(|image| image.has_file_id()) .filter(|image| image.has_file_id())
@ -194,7 +200,8 @@ impl Metadata for Artist {
fn parse(msg: &Self::Message, session: &Session) -> Self { fn parse(msg: &Self::Message, session: &Session) -> Self {
let country = session.country(); let country = session.country();
let top_tracks: Vec<SpotifyId> = match msg.get_top_track() let top_tracks: Vec<SpotifyId> = match msg
.get_top_track()
.iter() .iter()
.find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country)) .find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country))
{ {

View file

@ -557,7 +557,8 @@ impl PlayerInternal {
} }
}; };
let key = self.session let key = self
.session
.audio_key() .audio_key()
.request(track.id, file_id) .request(track.id, file_id)
.wait() .wait()
@ -599,7 +600,8 @@ impl Drop for PlayerInternal {
impl ::std::fmt::Debug for PlayerCommand { impl ::std::fmt::Debug for PlayerCommand {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
match *self { match *self {
PlayerCommand::Load(track, play, position, _) => f.debug_tuple("Load") PlayerCommand::Load(track, play, position, _) => f
.debug_tuple("Load")
.field(&track) .field(&track)
.field(&play) .field(&play)
.field(&position) .field(&position)

View file

@ -5,4 +5,7 @@ authors = ["Paul Liétar <paul@lietar.net>"]
build = "build.rs" build = "build.rs"
[dependencies] [dependencies]
protobuf = "1.0.10" protobuf = "2.0.5"
[build-dependencies]
protoc-rust = "2.0.5"

View file

@ -1,15 +1,36 @@
extern crate protoc_rust;
use protoc_rust::Customize;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
mod files; mod files;
fn main() { fn main() {
let mut changed = false;
let mut file = File::open("files.rs").unwrap();
let mut f_str = String::new();
file.read_to_string(&mut f_str).unwrap();
drop(file);
for &(path, expected_checksum) in files::FILES { for &(path, expected_checksum) in files::FILES {
let actual = cksum_file(path).unwrap(); let actual = cksum_file(path).unwrap();
if expected_checksum != actual { if expected_checksum != actual {
panic!("Checksum for {:?} does not match. Try running build.sh", path); protoc_rust::run(protoc_rust::Args {
out_dir: "src",
input: &[path],
includes: &["proto"],
customize: Customize { ..Default::default() },
}).expect("protoc");
let new_checksum = cksum_file(path).unwrap();
f_str = f_str.replace(&expected_checksum.to_string(), &new_checksum.to_string());
changed = true;
} }
} }
if changed {
// Write new checksums to file
let mut file = File::create("files.rs").unwrap();
println!("f_str: {:?}",f_str);
file.write_all(f_str.as_bytes()).unwrap();
}
} }
fn cksum_file<T: AsRef<std::path::Path>>(path: T) -> std::io::Result<u32> { fn cksum_file<T: AsRef<std::path::Path>>(path: T) -> std::io::Result<u32> {

View file

@ -1,10 +1,10 @@
// Autogenerated by build.sh // Autogenerated by build.rs
pub const FILES : &'static [(&'static str, u32)] = &[ pub const FILES: &'static [(&'static str, u32)] = &[
("proto/authentication.proto", 2098196376), ("proto/authentication.proto", 2098196376),
("proto/keyexchange.proto", 451735664), ("proto/keyexchange.proto", 451735664),
("proto/mercury.proto", 709993906), ("proto/mercury.proto", 709993906),
("proto/metadata.proto", 2474472423), ("proto/metadata.proto", 2474472423),
("proto/pubsub.proto", 2686584829), ("proto/pubsub.proto", 2686584829),
("proto/spirc.proto", 2406852191), ("proto/spirc.proto", 1587493382),
]; ];

View file

@ -76,7 +76,7 @@ enum CapabilityType {
kSupportsRename = 0xb; kSupportsRename = 0xb;
kHidden = 0xc; kHidden = 0xc;
kSupportsPlaylistV2 = 0xd; kSupportsPlaylistV2 = 0xd;
kUnknown = 0xe; kSupportsExternalEpisodes = 0xe;
} }
message Goodbye { message Goodbye {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
// This file is generated. Do not edit // This file is generated by rust-protobuf 2.0.5. Do not edit
// @generated // @generated
// https://github.com/Manishearth/rust-clippy/issues/702 // https://github.com/Manishearth/rust-clippy/issues/702
@ -32,24 +32,11 @@ pub struct Subscription {
cached_size: ::protobuf::CachedSize, cached_size: ::protobuf::CachedSize,
} }
// see codegen.rs for the explanation why impl Sync explicitly
unsafe impl ::std::marker::Sync for Subscription {}
impl Subscription { impl Subscription {
pub fn new() -> Subscription { pub fn new() -> Subscription {
::std::default::Default::default() ::std::default::Default::default()
} }
pub fn default_instance() -> &'static Subscription {
static mut instance: ::protobuf::lazy::Lazy<Subscription> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const Subscription,
};
unsafe {
instance.get(Subscription::new)
}
}
// optional string uri = 1; // optional string uri = 1;
pub fn clear_uri(&mut self) { pub fn clear_uri(&mut self) {
@ -70,7 +57,7 @@ impl Subscription {
pub fn mut_uri(&mut self) -> &mut ::std::string::String { pub fn mut_uri(&mut self) -> &mut ::std::string::String {
if self.uri.is_none() { if self.uri.is_none() {
self.uri.set_default(); self.uri.set_default();
}; }
self.uri.as_mut().unwrap() self.uri.as_mut().unwrap()
} }
@ -86,14 +73,6 @@ impl Subscription {
} }
} }
fn get_uri_for_reflect(&self) -> &::protobuf::SingularField<::std::string::String> {
&self.uri
}
fn mut_uri_for_reflect(&mut self) -> &mut ::protobuf::SingularField<::std::string::String> {
&mut self.uri
}
// optional int32 expiry = 2; // optional int32 expiry = 2;
pub fn clear_expiry(&mut self) { pub fn clear_expiry(&mut self) {
@ -113,14 +92,6 @@ impl Subscription {
self.expiry.unwrap_or(0) self.expiry.unwrap_or(0)
} }
fn get_expiry_for_reflect(&self) -> &::std::option::Option<i32> {
&self.expiry
}
fn mut_expiry_for_reflect(&mut self) -> &mut ::std::option::Option<i32> {
&mut self.expiry
}
// optional int32 status_code = 3; // optional int32 status_code = 3;
pub fn clear_status_code(&mut self) { pub fn clear_status_code(&mut self) {
@ -139,14 +110,6 @@ impl Subscription {
pub fn get_status_code(&self) -> i32 { pub fn get_status_code(&self) -> i32 {
self.status_code.unwrap_or(0) self.status_code.unwrap_or(0)
} }
fn get_status_code_for_reflect(&self) -> &::std::option::Option<i32> {
&self.status_code
}
fn mut_status_code_for_reflect(&mut self) -> &mut ::std::option::Option<i32> {
&mut self.status_code
}
} }
impl ::protobuf::Message for Subscription { impl ::protobuf::Message for Subscription {
@ -164,14 +127,14 @@ impl ::protobuf::Message for Subscription {
2 => { 2 => {
if wire_type != ::protobuf::wire_format::WireTypeVarint { if wire_type != ::protobuf::wire_format::WireTypeVarint {
return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type)); return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
}; }
let tmp = is.read_int32()?; let tmp = is.read_int32()?;
self.expiry = ::std::option::Option::Some(tmp); self.expiry = ::std::option::Option::Some(tmp);
}, },
3 => { 3 => {
if wire_type != ::protobuf::wire_format::WireTypeVarint { if wire_type != ::protobuf::wire_format::WireTypeVarint {
return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type)); return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
}; }
let tmp = is.read_int32()?; let tmp = is.read_int32()?;
self.status_code = ::std::option::Option::Some(tmp); self.status_code = ::std::option::Option::Some(tmp);
}, },
@ -187,30 +150,30 @@ impl ::protobuf::Message for Subscription {
#[allow(unused_variables)] #[allow(unused_variables)]
fn compute_size(&self) -> u32 { fn compute_size(&self) -> u32 {
let mut my_size = 0; let mut my_size = 0;
if let Some(v) = self.uri.as_ref() { if let Some(ref v) = self.uri.as_ref() {
my_size += ::protobuf::rt::string_size(1, &v); my_size += ::protobuf::rt::string_size(1, &v);
}; }
if let Some(v) = self.expiry { if let Some(v) = self.expiry {
my_size += ::protobuf::rt::value_size(2, v, ::protobuf::wire_format::WireTypeVarint); my_size += ::protobuf::rt::value_size(2, v, ::protobuf::wire_format::WireTypeVarint);
}; }
if let Some(v) = self.status_code { if let Some(v) = self.status_code {
my_size += ::protobuf::rt::value_size(3, v, ::protobuf::wire_format::WireTypeVarint); my_size += ::protobuf::rt::value_size(3, v, ::protobuf::wire_format::WireTypeVarint);
}; }
my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
self.cached_size.set(my_size); self.cached_size.set(my_size);
my_size my_size
} }
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream) -> ::protobuf::ProtobufResult<()> { fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream) -> ::protobuf::ProtobufResult<()> {
if let Some(v) = self.uri.as_ref() { if let Some(ref v) = self.uri.as_ref() {
os.write_string(1, &v)?; os.write_string(1, &v)?;
}; }
if let Some(v) = self.expiry { if let Some(v) = self.expiry {
os.write_int32(2, v)?; os.write_int32(2, v)?;
}; }
if let Some(v) = self.status_code { if let Some(v) = self.status_code {
os.write_int32(3, v)?; os.write_int32(3, v)?;
}; }
os.write_unknown_fields(self.get_unknown_fields())?; os.write_unknown_fields(self.get_unknown_fields())?;
::std::result::Result::Ok(()) ::std::result::Result::Ok(())
} }
@ -238,16 +201,14 @@ impl ::protobuf::Message for Subscription {
} }
fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
::protobuf::MessageStatic::descriptor_static(None::<Self>) Self::descriptor_static()
} }
}
impl ::protobuf::MessageStatic for Subscription {
fn new() -> Subscription { fn new() -> Subscription {
Subscription::new() Subscription::new()
} }
fn descriptor_static(_: ::std::option::Option<Subscription>) -> &'static ::protobuf::reflect::MessageDescriptor { fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy { static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT, lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const ::protobuf::reflect::MessageDescriptor, ptr: 0 as *const ::protobuf::reflect::MessageDescriptor,
@ -257,18 +218,18 @@ impl ::protobuf::MessageStatic for Subscription {
let mut fields = ::std::vec::Vec::new(); let mut fields = ::std::vec::Vec::new();
fields.push(::protobuf::reflect::accessor::make_singular_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( fields.push(::protobuf::reflect::accessor::make_singular_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
"uri", "uri",
Subscription::get_uri_for_reflect, |m: &Subscription| { &m.uri },
Subscription::mut_uri_for_reflect, |m: &mut Subscription| { &mut m.uri },
)); ));
fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>( fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>(
"expiry", "expiry",
Subscription::get_expiry_for_reflect, |m: &Subscription| { &m.expiry },
Subscription::mut_expiry_for_reflect, |m: &mut Subscription| { &mut m.expiry },
)); ));
fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>( fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>(
"status_code", "status_code",
Subscription::get_status_code_for_reflect, |m: &Subscription| { &m.status_code },
Subscription::mut_status_code_for_reflect, |m: &mut Subscription| { &mut m.status_code },
)); ));
::protobuf::reflect::MessageDescriptor::new::<Subscription>( ::protobuf::reflect::MessageDescriptor::new::<Subscription>(
"Subscription", "Subscription",
@ -278,6 +239,16 @@ impl ::protobuf::MessageStatic for Subscription {
}) })
} }
} }
fn default_instance() -> &'static Subscription {
static mut instance: ::protobuf::lazy::Lazy<Subscription> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const Subscription,
};
unsafe {
instance.get(Subscription::new)
}
}
} }
impl ::protobuf::Clear for Subscription { impl ::protobuf::Clear for Subscription {
@ -301,31 +272,11 @@ impl ::protobuf::reflect::ProtobufValue for Subscription {
} }
} }
static file_descriptor_proto_data: &'static [u8] = &[ static file_descriptor_proto_data: &'static [u8] = b"\
0x0a, 0x0c, 0x70, 0x75, 0x62, 0x73, 0x75, 0x62, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x59, \n\x0cpubsub.proto\"Y\n\x0cSubscription\x12\x10\n\x03uri\x18\x01\x20\x01\
0x0a, 0x0c, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, (\tR\x03uri\x12\x16\n\x06expiry\x18\x02\x20\x01(\x05R\x06expiry\x12\x1f\
0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, \n\x0bstatus_code\x18\x03\x20\x01(\x05R\nstatusCode\
0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, ";
0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x74, 0x61, 0x74,
0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x73,
0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x4a, 0xf9, 0x01, 0x0a, 0x06, 0x12, 0x04,
0x00, 0x00, 0x06, 0x01, 0x0a, 0x08, 0x0a, 0x01, 0x0c, 0x12, 0x03, 0x00, 0x00, 0x12, 0x0a, 0x0a,
0x0a, 0x02, 0x04, 0x00, 0x12, 0x04, 0x02, 0x00, 0x06, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x00,
0x01, 0x12, 0x03, 0x02, 0x08, 0x14, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x00, 0x02, 0x00, 0x12, 0x03,
0x03, 0x04, 0x1e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x04, 0x12, 0x03, 0x03, 0x04,
0x0c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x05, 0x12, 0x03, 0x03, 0x0d, 0x13, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03, 0x03, 0x14, 0x17, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x00, 0x02, 0x00, 0x03, 0x12, 0x03, 0x03, 0x1a, 0x1d, 0x0a, 0x0b, 0x0a, 0x04, 0x04,
0x00, 0x02, 0x01, 0x12, 0x03, 0x04, 0x04, 0x20, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x01,
0x04, 0x12, 0x03, 0x04, 0x04, 0x0c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x01, 0x05, 0x12,
0x03, 0x04, 0x0d, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x01, 0x01, 0x12, 0x03, 0x04,
0x13, 0x19, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x01, 0x03, 0x12, 0x03, 0x04, 0x1c, 0x1f,
0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x00, 0x02, 0x02, 0x12, 0x03, 0x05, 0x04, 0x25, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x00, 0x02, 0x02, 0x04, 0x12, 0x03, 0x05, 0x04, 0x0c, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x00, 0x02, 0x02, 0x05, 0x12, 0x03, 0x05, 0x0d, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02,
0x02, 0x01, 0x12, 0x03, 0x05, 0x13, 0x1e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x02, 0x03,
0x12, 0x03, 0x05, 0x21, 0x24,
];
static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy { static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT, lock: ::protobuf::lazy::ONCE_INIT,

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ extern crate log;
extern crate rpassword; extern crate rpassword;
extern crate tokio_core; extern crate tokio_core;
extern crate tokio_io; extern crate tokio_io;
extern crate tokio_process;
extern crate tokio_signal; extern crate tokio_signal;
extern crate url; extern crate url;
extern crate sha1; extern crate sha1;
@ -127,6 +128,7 @@ fn setup(args: &[String]) -> Setup {
.optopt("u", "username", "Username to sign in with", "USERNAME") .optopt("u", "username", "Username to sign in with", "USERNAME")
.optopt("p", "password", "Password", "PASSWORD") .optopt("p", "password", "Password", "PASSWORD")
.optopt("", "proxy", "HTTP proxy to use when connecting", "PROXY") .optopt("", "proxy", "HTTP proxy to use when connecting", "PROXY")
.optopt("", "ap-port", "Connect to AP with specified port. If no AP with that port are present fallback AP will be used. Available ports are usually 80, 443 and 4070", "AP_PORT")
.optflag("", "disable-discovery", "Disable discovery mode") .optflag("", "disable-discovery", "Disable discovery mode")
.optopt( .optopt(
"", "",
@ -253,20 +255,23 @@ fn setup(args: &[String]) -> Setup {
proxy: matches.opt_str("proxy").or(std::env::var("http_proxy").ok()).map( proxy: matches.opt_str("proxy").or(std::env::var("http_proxy").ok()).map(
|s| { |s| {
match Url::parse(&s) { match Url::parse(&s) {
Ok(url) => { Ok(url) => {
if url.host().is_none() || url.port().is_none() { if url.host().is_none() || url.port().is_none() {
panic!("Invalid proxy url, only urls on the format \"http://host:port\" are allowed"); panic!("Invalid proxy url, only urls on the format \"http://host:port\" are allowed");
} }
if url.scheme() != "http" { if url.scheme() != "http" {
panic!("Only unsecure http:// proxies are supported"); panic!("Only unsecure http:// proxies are supported");
}
url
},
Err(err) => panic!("Invalid proxy url: {}, only urls on the format \"http://host:port\" are allowed", err)
} }
url
},
Err(err) => panic!("Invalid proxy url: {}, only urls on the format \"http://host:port\" are allowed", err)
}
}, },
), ),
ap_port: matches
.opt_str("ap-port")
.map(|port| port.parse::<u16>().expect("Invalid port")),
} }
}; };
@ -460,7 +465,15 @@ impl Future for Main {
if let Some(ref mut player_event_channel) = self.player_event_channel { if let Some(ref mut player_event_channel) = self.player_event_channel {
if let Async::Ready(Some(event)) = player_event_channel.poll().unwrap() { if let Async::Ready(Some(event)) = player_event_channel.poll().unwrap() {
if let Some(ref program) = self.player_event_program { if let Some(ref program) = self.player_event_program {
run_program_on_events(event, program); let child = run_program_on_events(event, program)
.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);
} }
} }
} }

View file

@ -1,18 +1,19 @@
use librespot::playback::player::PlayerEvent; use librespot::playback::player::PlayerEvent;
use tokio_process::{Child, CommandExt};
use std::collections::HashMap; use std::collections::HashMap;
use std::io;
use std::process::Command; use std::process::Command;
fn run_program(program: &str, env_vars: HashMap<&str, String>) { fn run_program(program: &str, env_vars: HashMap<&str, String>) -> io::Result<Child> {
let mut v: Vec<&str> = program.split_whitespace().collect(); let mut v: Vec<&str> = program.split_whitespace().collect();
info!("Running {:?} with environment variables {:?}", v, env_vars); info!("Running {:?} with environment variables {:?}", v, env_vars);
Command::new(&v.remove(0)) Command::new(&v.remove(0))
.args(&v) .args(&v)
.envs(env_vars.iter()) .envs(env_vars.iter())
.spawn() .spawn_async()
.expect("program failed to start");
} }
pub fn run_program_on_events(event: PlayerEvent, onevent: &str) { pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> io::Result<Child> {
let mut env_vars = HashMap::new(); let mut env_vars = HashMap::new();
match event { match event {
PlayerEvent::Changed { PlayerEvent::Changed {
@ -20,17 +21,17 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) {
new_track_id, new_track_id,
} => { } => {
env_vars.insert("PLAYER_EVENT", "change".to_string()); env_vars.insert("PLAYER_EVENT", "change".to_string());
env_vars.insert("OLD_TRACK_ID", old_track_id.to_base16()); env_vars.insert("OLD_TRACK_ID", old_track_id.to_base62());
env_vars.insert("TRACK_ID", new_track_id.to_base16()); env_vars.insert("TRACK_ID", new_track_id.to_base62());
} }
PlayerEvent::Started { track_id } => { PlayerEvent::Started { track_id } => {
env_vars.insert("PLAYER_EVENT", "start".to_string()); env_vars.insert("PLAYER_EVENT", "start".to_string());
env_vars.insert("TRACK_ID", track_id.to_base16()); env_vars.insert("TRACK_ID", track_id.to_base62());
} }
PlayerEvent::Stopped { track_id } => { PlayerEvent::Stopped { track_id } => {
env_vars.insert("PLAYER_EVENT", "stop".to_string()); env_vars.insert("PLAYER_EVENT", "stop".to_string());
env_vars.insert("TRACK_ID", track_id.to_base16()); env_vars.insert("TRACK_ID", track_id.to_base62());
} }
} }
run_program(onevent, env_vars); run_program(onevent, env_vars)
} }