mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
772591576a
29 changed files with 3473 additions and 9899 deletions
1961
Cargo.lock
generated
1961
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -48,6 +48,7 @@ serde_derive = "0.9.6"
|
|||
serde_json = "0.9.5"
|
||||
tokio-core = "0.1.2"
|
||||
tokio-io = "0.1"
|
||||
tokio-process = "0.2.2"
|
||||
tokio-signal = "0.1.2"
|
||||
url = "1.7.0"
|
||||
sha-1 = "0.8.0"
|
||||
|
|
15
README.md
15
README.md
|
@ -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.
|
||||
|
||||
# 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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
# 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.**
|
||||
|
||||
|
@ -58,7 +58,7 @@ cargo build --release
|
|||
A sample program implementing a headless Spotify Connect receiver is provided.
|
||||
Once you've built *librespot*, run it using :
|
||||
```shell
|
||||
target/release/librespot --name DEVICENAME
|
||||
target/release/librespot --name DEVICENAME
|
||||
```
|
||||
|
||||
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.
|
||||
- [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.
|
||||
- [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.
|
||||
|
|
|
@ -10,7 +10,7 @@ path = "../core"
|
|||
bit-set = "0.4.0"
|
||||
byteorder = "1.0"
|
||||
futures = "0.1.8"
|
||||
lewton = "0.8.0"
|
||||
lewton = "0.9.3"
|
||||
log = "0.3.5"
|
||||
num-bigint = "0.1.35"
|
||||
num-traits = "0.1.36"
|
||||
|
|
|
@ -348,11 +348,16 @@ impl Read for AudioFileStreaming {
|
|||
impl Seek for AudioFileStreaming {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ futures = "0.1.8"
|
|||
hyper = "0.11.2"
|
||||
log = "0.3.5"
|
||||
num-bigint = "0.1.35"
|
||||
protobuf = "1.1"
|
||||
protobuf = "2.0.5"
|
||||
rand = "0.3.13"
|
||||
serde = "0.9.6"
|
||||
serde_derive = "0.9.6"
|
||||
|
|
54
contrib/Dockerfile.Rpi
Normal file
54
contrib/Dockerfile.Rpi
Normal 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"]
|
|
@ -22,7 +22,7 @@ log = "0.3.5"
|
|||
num-bigint = "0.1.35"
|
||||
num-integer = "0.1.32"
|
||||
num-traits = "0.1.36"
|
||||
protobuf = "1.1"
|
||||
protobuf = "2.0.5"
|
||||
rand = "0.3.13"
|
||||
rpassword = "0.3.0"
|
||||
serde = "0.9.6"
|
||||
|
|
|
@ -17,7 +17,11 @@ pub struct APResolveData {
|
|||
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 use_proxy = proxy.is_some();
|
||||
|
||||
|
@ -53,9 +57,15 @@ fn apresolve(handle: &Handle, proxy: &Option<Url>) -> Box<Future<Item = String,
|
|||
let data =
|
||||
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 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.
|
||||
Uri::from_str(ap)
|
||||
.ok()
|
||||
|
@ -75,11 +85,12 @@ fn apresolve(handle: &Handle, proxy: &Option<Url>) -> Box<Future<Item = String,
|
|||
pub(crate) fn apresolve_or_fallback<E>(
|
||||
handle: &Handle,
|
||||
proxy: &Option<Url>,
|
||||
ap_port: &Option<u16>,
|
||||
) -> Box<Future<Item = String, Error = E>>
|
||||
where
|
||||
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!("Using fallback \"{}\"", AP_FALLBACK);
|
||||
Ok(AP_FALLBACK.into())
|
||||
|
|
|
@ -10,6 +10,7 @@ pub struct SessionConfig {
|
|||
pub user_agent: String,
|
||||
pub device_id: String,
|
||||
pub proxy: Option<Url>,
|
||||
pub ap_port: Option<u16>,
|
||||
}
|
||||
|
||||
impl Default for SessionConfig {
|
||||
|
@ -19,6 +20,7 @@ impl Default for SessionConfig {
|
|||
user_agent: version::version_string(),
|
||||
device_id: device_id,
|
||||
proxy: None,
|
||||
ap_port: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
|||
use hmac::{Hmac, Mac};
|
||||
use sha1::Sha1;
|
||||
use futures::{Async, Future, Poll};
|
||||
use protobuf::{self, Message, MessageStatic};
|
||||
use protobuf::{self, Message};
|
||||
use rand::thread_rng;
|
||||
use std::io::{self, Read};
|
||||
use std::marker::PhantomData;
|
||||
|
@ -125,7 +125,7 @@ fn client_response<T: AsyncWrite>(connection: T, challenge: Vec<u8>) -> WriteAll
|
|||
write_all(connection, buffer)
|
||||
}
|
||||
|
||||
enum RecvPacket<T, M: MessageStatic> {
|
||||
enum RecvPacket<T, M: Message> {
|
||||
Header(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>
|
||||
where
|
||||
T: Read,
|
||||
M: MessageStatic,
|
||||
M: Message,
|
||||
{
|
||||
RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData)
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ where
|
|||
impl<T: AsyncRead, M> Future for RecvPacket<T, M>
|
||||
where
|
||||
T: Read,
|
||||
M: MessageStatic,
|
||||
M: Message,
|
||||
{
|
||||
type Item = (T, M, Vec<u8>);
|
||||
type Error = io::Error;
|
||||
|
|
|
@ -51,7 +51,7 @@ impl Session {
|
|||
cache: Option<Cache>,
|
||||
handle: Handle,
|
||||
) -> 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 proxy = config.proxy.clone();
|
||||
|
|
|
@ -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.
|
||||
|
||||
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
|
||||
The first 3 packets exchanged are unencrypted, and have the following format :
|
||||
|
|
|
@ -33,7 +33,8 @@ fn main() {
|
|||
let backend = audio_backend::find(None).unwrap();
|
||||
|
||||
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();
|
||||
|
||||
let (player, _) = Player::new(player_config, session.clone(), None, move || (backend)(None));
|
||||
|
|
|
@ -7,7 +7,7 @@ authors = ["Paul Lietar <paul@lietar.net>"]
|
|||
byteorder = "1.0"
|
||||
futures = "0.1.8"
|
||||
linear-map = "1.0"
|
||||
protobuf = "1.1"
|
||||
protobuf = "2.0.5"
|
||||
|
||||
[dependencies.librespot-core]
|
||||
path = "../core"
|
||||
|
|
|
@ -53,7 +53,7 @@ where
|
|||
}
|
||||
|
||||
pub trait Metadata: Send + Sized + 'static {
|
||||
type Message: protobuf::MessageStatic;
|
||||
type Message: protobuf::Message;
|
||||
|
||||
fn base_url() -> &'static str;
|
||||
fn parse(msg: &Self::Message, session: &Session) -> Self;
|
||||
|
@ -110,13 +110,15 @@ impl Metadata for Track {
|
|||
fn parse(msg: &Self::Message, session: &Session) -> Self {
|
||||
let country = session.country();
|
||||
|
||||
let artists = msg.get_artist()
|
||||
let artists = msg
|
||||
.get_artist()
|
||||
.iter()
|
||||
.filter(|artist| artist.has_gid())
|
||||
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let files = msg.get_file()
|
||||
let files = msg
|
||||
.get_file()
|
||||
.iter()
|
||||
.filter(|file| file.has_file_id())
|
||||
.map(|file| {
|
||||
|
@ -133,7 +135,8 @@ impl Metadata for Track {
|
|||
album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(),
|
||||
artists: artists,
|
||||
files: files,
|
||||
alternatives: msg.get_alternative()
|
||||
alternatives: msg
|
||||
.get_alternative()
|
||||
.iter()
|
||||
.map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap())
|
||||
.collect(),
|
||||
|
@ -150,20 +153,23 @@ impl Metadata for Album {
|
|||
}
|
||||
|
||||
fn parse(msg: &Self::Message, _: &Session) -> Self {
|
||||
let artists = msg.get_artist()
|
||||
let artists = msg
|
||||
.get_artist()
|
||||
.iter()
|
||||
.filter(|artist| artist.has_gid())
|
||||
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let tracks = msg.get_disc()
|
||||
let tracks = msg
|
||||
.get_disc()
|
||||
.iter()
|
||||
.flat_map(|disc| disc.get_track())
|
||||
.filter(|track| track.has_gid())
|
||||
.map(|track| SpotifyId::from_raw(track.get_gid()).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let covers = msg.get_cover_group()
|
||||
let covers = msg
|
||||
.get_cover_group()
|
||||
.get_image()
|
||||
.iter()
|
||||
.filter(|image| image.has_file_id())
|
||||
|
@ -194,7 +200,8 @@ impl Metadata for Artist {
|
|||
fn parse(msg: &Self::Message, session: &Session) -> Self {
|
||||
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()
|
||||
.find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country))
|
||||
{
|
||||
|
|
|
@ -557,7 +557,8 @@ impl PlayerInternal {
|
|||
}
|
||||
};
|
||||
|
||||
let key = self.session
|
||||
let key = self
|
||||
.session
|
||||
.audio_key()
|
||||
.request(track.id, file_id)
|
||||
.wait()
|
||||
|
@ -599,7 +600,8 @@ impl Drop for PlayerInternal {
|
|||
impl ::std::fmt::Debug for PlayerCommand {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
|
||||
match *self {
|
||||
PlayerCommand::Load(track, play, position, _) => f.debug_tuple("Load")
|
||||
PlayerCommand::Load(track, play, position, _) => f
|
||||
.debug_tuple("Load")
|
||||
.field(&track)
|
||||
.field(&play)
|
||||
.field(&position)
|
||||
|
|
|
@ -5,4 +5,7 @@ authors = ["Paul Liétar <paul@lietar.net>"]
|
|||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
protobuf = "1.0.10"
|
||||
protobuf = "2.0.5"
|
||||
|
||||
[build-dependencies]
|
||||
protoc-rust = "2.0.5"
|
||||
|
|
|
@ -1,15 +1,36 @@
|
|||
extern crate protoc_rust;
|
||||
use protoc_rust::Customize;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
|
||||
mod files;
|
||||
|
||||
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 {
|
||||
let actual = cksum_file(path).unwrap();
|
||||
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> {
|
||||
|
|
|
@ -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/keyexchange.proto", 451735664),
|
||||
("proto/mercury.proto", 709993906),
|
||||
("proto/metadata.proto", 2474472423),
|
||||
("proto/pubsub.proto", 2686584829),
|
||||
("proto/spirc.proto", 2406852191),
|
||||
("proto/spirc.proto", 1587493382),
|
||||
];
|
||||
|
|
|
@ -76,7 +76,7 @@ enum CapabilityType {
|
|||
kSupportsRename = 0xb;
|
||||
kHidden = 0xc;
|
||||
kSupportsPlaylistV2 = 0xd;
|
||||
kUnknown = 0xe;
|
||||
kSupportsExternalEpisodes = 0xe;
|
||||
}
|
||||
|
||||
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
|
@ -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
|
||||
|
||||
// https://github.com/Manishearth/rust-clippy/issues/702
|
||||
|
@ -32,24 +32,11 @@ pub struct Subscription {
|
|||
cached_size: ::protobuf::CachedSize,
|
||||
}
|
||||
|
||||
// see codegen.rs for the explanation why impl Sync explicitly
|
||||
unsafe impl ::std::marker::Sync for Subscription {}
|
||||
|
||||
impl Subscription {
|
||||
pub fn new() -> Subscription {
|
||||
::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;
|
||||
|
||||
pub fn clear_uri(&mut self) {
|
||||
|
@ -70,7 +57,7 @@ impl Subscription {
|
|||
pub fn mut_uri(&mut self) -> &mut ::std::string::String {
|
||||
if self.uri.is_none() {
|
||||
self.uri.set_default();
|
||||
};
|
||||
}
|
||||
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;
|
||||
|
||||
pub fn clear_expiry(&mut self) {
|
||||
|
@ -113,14 +92,6 @@ impl Subscription {
|
|||
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;
|
||||
|
||||
pub fn clear_status_code(&mut self) {
|
||||
|
@ -139,14 +110,6 @@ impl Subscription {
|
|||
pub fn get_status_code(&self) -> i32 {
|
||||
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 {
|
||||
|
@ -164,14 +127,14 @@ impl ::protobuf::Message for Subscription {
|
|||
2 => {
|
||||
if wire_type != ::protobuf::wire_format::WireTypeVarint {
|
||||
return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
|
||||
};
|
||||
}
|
||||
let tmp = is.read_int32()?;
|
||||
self.expiry = ::std::option::Option::Some(tmp);
|
||||
},
|
||||
3 => {
|
||||
if wire_type != ::protobuf::wire_format::WireTypeVarint {
|
||||
return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
|
||||
};
|
||||
}
|
||||
let tmp = is.read_int32()?;
|
||||
self.status_code = ::std::option::Option::Some(tmp);
|
||||
},
|
||||
|
@ -187,30 +150,30 @@ impl ::protobuf::Message for Subscription {
|
|||
#[allow(unused_variables)]
|
||||
fn compute_size(&self) -> u32 {
|
||||
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);
|
||||
};
|
||||
}
|
||||
if let Some(v) = self.expiry {
|
||||
my_size += ::protobuf::rt::value_size(2, v, ::protobuf::wire_format::WireTypeVarint);
|
||||
};
|
||||
}
|
||||
if let Some(v) = self.status_code {
|
||||
my_size += ::protobuf::rt::value_size(3, v, ::protobuf::wire_format::WireTypeVarint);
|
||||
};
|
||||
}
|
||||
my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
|
||||
self.cached_size.set(my_size);
|
||||
my_size
|
||||
}
|
||||
|
||||
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)?;
|
||||
};
|
||||
}
|
||||
if let Some(v) = self.expiry {
|
||||
os.write_int32(2, v)?;
|
||||
};
|
||||
}
|
||||
if let Some(v) = self.status_code {
|
||||
os.write_int32(3, v)?;
|
||||
};
|
||||
}
|
||||
os.write_unknown_fields(self.get_unknown_fields())?;
|
||||
::std::result::Result::Ok(())
|
||||
}
|
||||
|
@ -238,16 +201,14 @@ impl ::protobuf::Message for Subscription {
|
|||
}
|
||||
|
||||
fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
|
||||
::protobuf::MessageStatic::descriptor_static(None::<Self>)
|
||||
Self::descriptor_static()
|
||||
}
|
||||
}
|
||||
|
||||
impl ::protobuf::MessageStatic for Subscription {
|
||||
fn new() -> Subscription {
|
||||
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 {
|
||||
lock: ::protobuf::lazy::ONCE_INIT,
|
||||
ptr: 0 as *const ::protobuf::reflect::MessageDescriptor,
|
||||
|
@ -257,18 +218,18 @@ impl ::protobuf::MessageStatic for Subscription {
|
|||
let mut fields = ::std::vec::Vec::new();
|
||||
fields.push(::protobuf::reflect::accessor::make_singular_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
|
||||
"uri",
|
||||
Subscription::get_uri_for_reflect,
|
||||
Subscription::mut_uri_for_reflect,
|
||||
|m: &Subscription| { &m.uri },
|
||||
|m: &mut Subscription| { &mut m.uri },
|
||||
));
|
||||
fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>(
|
||||
"expiry",
|
||||
Subscription::get_expiry_for_reflect,
|
||||
Subscription::mut_expiry_for_reflect,
|
||||
|m: &Subscription| { &m.expiry },
|
||||
|m: &mut Subscription| { &mut m.expiry },
|
||||
));
|
||||
fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>(
|
||||
"status_code",
|
||||
Subscription::get_status_code_for_reflect,
|
||||
Subscription::mut_status_code_for_reflect,
|
||||
|m: &Subscription| { &m.status_code },
|
||||
|m: &mut Subscription| { &mut m.status_code },
|
||||
));
|
||||
::protobuf::reflect::MessageDescriptor::new::<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 {
|
||||
|
@ -301,31 +272,11 @@ impl ::protobuf::reflect::ProtobufValue for Subscription {
|
|||
}
|
||||
}
|
||||
|
||||
static file_descriptor_proto_data: &'static [u8] = &[
|
||||
0x0a, 0x0c, 0x70, 0x75, 0x62, 0x73, 0x75, 0x62, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x59,
|
||||
0x0a, 0x0c, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10,
|
||||
0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69,
|
||||
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 file_descriptor_proto_data: &'static [u8] = b"\
|
||||
\n\x0cpubsub.proto\"Y\n\x0cSubscription\x12\x10\n\x03uri\x18\x01\x20\x01\
|
||||
(\tR\x03uri\x12\x16\n\x06expiry\x18\x02\x20\x01(\x05R\x06expiry\x12\x1f\
|
||||
\n\x0bstatus_code\x18\x03\x20\x01(\x05R\nstatusCode\
|
||||
";
|
||||
|
||||
static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {
|
||||
lock: ::protobuf::lazy::ONCE_INIT,
|
||||
|
|
File diff suppressed because it is too large
Load diff
35
src/main.rs
35
src/main.rs
|
@ -7,6 +7,7 @@ extern crate log;
|
|||
extern crate rpassword;
|
||||
extern crate tokio_core;
|
||||
extern crate tokio_io;
|
||||
extern crate tokio_process;
|
||||
extern crate tokio_signal;
|
||||
extern crate url;
|
||||
extern crate sha1;
|
||||
|
@ -127,6 +128,7 @@ fn setup(args: &[String]) -> Setup {
|
|||
.optopt("u", "username", "Username to sign in with", "USERNAME")
|
||||
.optopt("p", "password", "Password", "PASSWORD")
|
||||
.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")
|
||||
.optopt(
|
||||
"",
|
||||
|
@ -253,20 +255,23 @@ fn setup(args: &[String]) -> Setup {
|
|||
proxy: matches.opt_str("proxy").or(std::env::var("http_proxy").ok()).map(
|
||||
|s| {
|
||||
match Url::parse(&s) {
|
||||
Ok(url) => {
|
||||
if url.host().is_none() || url.port().is_none() {
|
||||
panic!("Invalid proxy url, only urls on the format \"http://host:port\" are allowed");
|
||||
}
|
||||
Ok(url) => {
|
||||
if url.host().is_none() || url.port().is_none() {
|
||||
panic!("Invalid proxy url, only urls on the format \"http://host:port\" are allowed");
|
||||
}
|
||||
|
||||
if url.scheme() != "http" {
|
||||
panic!("Only unsecure http:// proxies are supported");
|
||||
if url.scheme() != "http" {
|
||||
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 Async::Ready(Some(event)) = player_event_channel.poll().unwrap() {
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
use librespot::playback::player::PlayerEvent;
|
||||
use tokio_process::{Child, CommandExt};
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
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();
|
||||
info!("Running {:?} with environment variables {:?}", v, env_vars);
|
||||
Command::new(&v.remove(0))
|
||||
.args(&v)
|
||||
.envs(env_vars.iter())
|
||||
.spawn()
|
||||
.expect("program failed to start");
|
||||
.spawn_async()
|
||||
}
|
||||
|
||||
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();
|
||||
match event {
|
||||
PlayerEvent::Changed {
|
||||
|
@ -20,17 +21,17 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) {
|
|||
new_track_id,
|
||||
} => {
|
||||
env_vars.insert("PLAYER_EVENT", "change".to_string());
|
||||
env_vars.insert("OLD_TRACK_ID", old_track_id.to_base16());
|
||||
env_vars.insert("TRACK_ID", new_track_id.to_base16());
|
||||
env_vars.insert("OLD_TRACK_ID", old_track_id.to_base62());
|
||||
env_vars.insert("TRACK_ID", new_track_id.to_base62());
|
||||
}
|
||||
PlayerEvent::Started { track_id } => {
|
||||
env_vars.insert("PLAYER_EVENT", "start".to_string());
|
||||
env_vars.insert("TRACK_ID", track_id.to_base16());
|
||||
env_vars.insert("TRACK_ID", track_id.to_base62());
|
||||
}
|
||||
PlayerEvent::Stopped { track_id } => {
|
||||
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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue