├── cache └── .gitignore ├── .dockerignore ├── .gitignore ├── rustfmt.toml ├── protocol ├── proto │ ├── toplist.proto │ ├── pubsub.proto │ ├── mergedprofile.proto │ ├── popcount.proto │ ├── social.proto │ ├── playlist4content.proto │ ├── socialgraph.proto │ ├── suggest.proto │ ├── facebook-publish.proto │ ├── search.proto │ ├── mercury.proto │ ├── playlist4issues.proto │ ├── radio.proto │ ├── playlist4meta.proto │ ├── ad-hermes-proxy.proto │ ├── playlist4changes.proto │ ├── appstore.proto │ ├── presence.proto │ ├── playlist4ops.proto │ ├── spirc.proto │ ├── metadata.proto │ ├── authentication.proto │ ├── facebook.proto │ └── keyexchange.proto ├── Cargo.toml ├── src │ └── lib.rs ├── files.rs ├── build.sh └── build.rs ├── core ├── src │ ├── version.rs │ ├── keymaster.rs │ ├── volume.rs │ ├── lib.rs │ ├── util │ │ └── mod.rs │ ├── mercury │ │ ├── sender.rs │ │ ├── types.rs │ │ └── mod.rs │ ├── diffie_hellman.rs │ ├── component.rs │ ├── config.rs │ ├── cache.rs │ ├── audio_key.rs │ ├── connection │ │ ├── codec.rs │ │ ├── mod.rs │ │ └── handshake.rs │ ├── apresolve.rs │ ├── spotify_id.rs │ ├── proxytunnel.rs │ ├── channel.rs │ ├── authentication.rs │ └── session.rs ├── build.rs └── Cargo.toml ├── contrib ├── librespot.service ├── docker-build.sh ├── docker-build-pi-armv6hf.sh └── Dockerfile ├── metadata ├── Cargo.toml └── src │ ├── cover.rs │ └── lib.rs ├── audio ├── Cargo.toml └── src │ ├── lib.rs │ ├── decrypt.rs │ ├── libvorbis_decoder.rs │ └── lewton_decoder.rs ├── src ├── lib.rs └── player_event_handler.rs ├── connect ├── src │ ├── lib.rs │ └── discovery.rs └── Cargo.toml ├── playback ├── src │ ├── lib.rs │ ├── mixer │ │ ├── mod.rs │ │ └── softmixer.rs │ ├── audio_backend │ │ ├── pipe.rs │ │ ├── alsa.rs │ │ ├── mod.rs │ │ ├── jackaudio.rs │ │ ├── portaudio.rs │ │ └── pulseaudio.rs │ └── config.rs └── Cargo.toml ├── LICENSE ├── .travis.yml ├── examples └── play.rs ├── Cargo.toml ├── docs ├── connection.md └── authentication.md ├── README.md └── CONTRIBUTING.md /cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | cache 3 | protocol/target 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .cargo 3 | spotify_appkey.key 4 | .vagrant/ 5 | .project 6 | .history -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 105 2 | reorder_imports = true 3 | reorder_imports_in_group = true 4 | reorder_modules = true 5 | -------------------------------------------------------------------------------- /protocol/proto/toplist.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Toplist { 4 | repeated string items = 0x1; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /core/src/version.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/version.rs")); 2 | 3 | pub fn version_string() -> String { 4 | format!("librespot-{}", short_sha()) 5 | } 6 | -------------------------------------------------------------------------------- /protocol/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot-protocol" 3 | version = "0.1.0" 4 | authors = ["Paul Liétar "] 5 | build = "build.rs" 6 | 7 | [dependencies] 8 | protobuf = "1.0.10" 9 | -------------------------------------------------------------------------------- /protocol/proto/pubsub.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Subscription { 4 | optional string uri = 0x1; 5 | optional int32 expiry = 0x2; 6 | optional int32 status_code = 0x3; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /protocol/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Autogenerated by build.sh 2 | 3 | extern crate protobuf; 4 | pub mod authentication; 5 | pub mod keyexchange; 6 | pub mod mercury; 7 | pub mod metadata; 8 | pub mod pubsub; 9 | pub mod spirc; 10 | -------------------------------------------------------------------------------- /protocol/proto/mergedprofile.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message MergedProfileRequest { 4 | } 5 | 6 | message MergedProfileReply { 7 | optional string username = 0x1; 8 | optional string artistid = 0x2; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /contrib/librespot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Librespot 3 | Requires=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | User=nobody 8 | Group=audio 9 | Restart=always 10 | RestartSec=10 11 | ExecStart=/usr/bin/librespot -n "%p on %H" 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | 16 | -------------------------------------------------------------------------------- /protocol/proto/popcount.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message PopcountRequest { 4 | } 5 | 6 | message PopcountResult { 7 | optional sint64 count = 0x1; 8 | optional bool truncated = 0x2; 9 | repeated string user = 0x3; 10 | repeated sint64 subscriptionTimestamps = 0x4; 11 | repeated sint64 insertionTimestamps = 0x5; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /metadata/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot-metadata" 3 | version = "0.1.0" 4 | authors = ["Paul Lietar "] 5 | 6 | [dependencies] 7 | byteorder = "1.0" 8 | futures = "0.1.8" 9 | linear-map = "1.0" 10 | protobuf = "1.1" 11 | 12 | [dependencies.librespot-core] 13 | path = "../core" 14 | [dependencies.librespot-protocol] 15 | path = "../protocol" 16 | -------------------------------------------------------------------------------- /protocol/files.rs: -------------------------------------------------------------------------------- 1 | // Autogenerated by build.sh 2 | 3 | pub const FILES : &'static [(&'static str, u32)] = &[ 4 | ("proto/authentication.proto", 2098196376), 5 | ("proto/keyexchange.proto", 451735664), 6 | ("proto/mercury.proto", 709993906), 7 | ("proto/metadata.proto", 2474472423), 8 | ("proto/pubsub.proto", 2686584829), 9 | ("proto/spirc.proto", 2406852191), 10 | ]; 11 | -------------------------------------------------------------------------------- /protocol/proto/social.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message DecorationData { 4 | optional string username = 0x1; 5 | optional string full_name = 0x2; 6 | optional string image_url = 0x3; 7 | optional string large_image_url = 0x5; 8 | optional string first_name = 0x6; 9 | optional string last_name = 0x7; 10 | optional string facebook_uid = 0x8; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /contrib/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | cargo build --release --no-default-features --features alsa-backend 5 | cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend 6 | cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend 7 | cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend 8 | cargo build --release --target mipsel-unknown-linux-gnu --no-default-features --features alsa-backend 9 | -------------------------------------------------------------------------------- /audio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot-audio" 3 | version = "0.1.0" 4 | authors = ["Paul Lietar "] 5 | 6 | [dependencies.librespot-core] 7 | path = "../core" 8 | 9 | [dependencies] 10 | bit-set = "0.4.0" 11 | byteorder = "1.0" 12 | futures = "0.1.8" 13 | lewton = "0.8.0" 14 | log = "0.3.5" 15 | num-bigint = "0.1.35" 16 | num-traits = "0.1.36" 17 | rust-crypto = "0.2.36" 18 | tempfile = "2.1" 19 | 20 | tremor = { git = "https://github.com/plietar/rust-tremor", optional = true } 21 | vorbis = { version ="0.1.0", optional = true } 22 | 23 | [features] 24 | with-tremor = ["tremor"] 25 | with-vorbis = ["vorbis"] 26 | -------------------------------------------------------------------------------- /metadata/src/cover.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, WriteBytesExt}; 2 | use std::io::Write; 3 | 4 | use core::channel::ChannelData; 5 | use core::session::Session; 6 | use core::spotify_id::FileId; 7 | 8 | pub fn get(session: &Session, file: FileId) -> ChannelData { 9 | let (channel_id, channel) = session.channel().allocate(); 10 | let (_headers, data) = channel.split(); 11 | 12 | let mut packet: Vec = Vec::new(); 13 | packet.write_u16::(channel_id).unwrap(); 14 | packet.write_u16::(0).unwrap(); 15 | packet.write(&file.0).unwrap(); 16 | session.send_packet(0x19, packet); 17 | 18 | data 19 | } 20 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![crate_name = "librespot"] 2 | #![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))] 3 | 4 | extern crate base64; 5 | extern crate crypto; 6 | extern crate futures; 7 | extern crate hyper; 8 | extern crate num_bigint; 9 | extern crate protobuf; 10 | extern crate rand; 11 | extern crate tokio_core; 12 | extern crate url; 13 | 14 | pub extern crate librespot_audio as audio; 15 | pub extern crate librespot_connect as connect; 16 | pub extern crate librespot_core as core; 17 | pub extern crate librespot_metadata as metadata; 18 | pub extern crate librespot_playback as playback; 19 | pub extern crate librespot_protocol as protocol; 20 | -------------------------------------------------------------------------------- /connect/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | #[macro_use] 4 | extern crate serde_json; 5 | 6 | extern crate base64; 7 | extern crate crypto; 8 | extern crate futures; 9 | extern crate hyper; 10 | extern crate num_bigint; 11 | extern crate protobuf; 12 | extern crate rand; 13 | extern crate tokio_core; 14 | extern crate url; 15 | 16 | #[cfg(feature = "with-dns-sd")] 17 | extern crate dns_sd; 18 | 19 | #[cfg(not(feature = "with-dns-sd"))] 20 | extern crate mdns; 21 | 22 | extern crate librespot_core as core; 23 | extern crate librespot_playback as playback; 24 | extern crate librespot_protocol as protocol; 25 | 26 | pub mod discovery; 27 | pub mod spirc; 28 | -------------------------------------------------------------------------------- /protocol/build.sh: -------------------------------------------------------------------------------- 1 | set -eu 2 | 3 | SRC="authentication keyexchange mercury 4 | metadata pubsub spirc" 5 | 6 | cat > src/lib.rs < files.rs <> src/lib.rs 26 | echo " (\"$src\", $checksum)," >> files.rs 27 | done 28 | 29 | echo "];" >> files.rs 30 | -------------------------------------------------------------------------------- /playback/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | extern crate byteorder; 5 | extern crate futures; 6 | 7 | #[cfg(feature = "alsa-backend")] 8 | extern crate alsa; 9 | 10 | #[cfg(feature = "portaudio-rs")] 11 | extern crate portaudio_rs; 12 | 13 | #[cfg(feature = "libpulse-sys")] 14 | extern crate libpulse_sys; 15 | 16 | #[cfg(feature = "jackaudio-backend")] 17 | extern crate jack; 18 | 19 | #[cfg(feature = "libc")] 20 | extern crate libc; 21 | 22 | extern crate librespot_audio as audio; 23 | extern crate librespot_core as core; 24 | extern crate librespot_metadata as metadata; 25 | 26 | pub mod audio_backend; 27 | pub mod config; 28 | pub mod mixer; 29 | pub mod player; 30 | -------------------------------------------------------------------------------- /playback/src/mixer/mod.rs: -------------------------------------------------------------------------------- 1 | pub trait Mixer: Send { 2 | fn open() -> Self 3 | where 4 | Self: Sized; 5 | fn start(&self); 6 | fn stop(&self); 7 | fn set_volume(&self, volume: u16); 8 | fn volume(&self) -> u16; 9 | fn get_audio_filter(&self) -> Option> { 10 | None 11 | } 12 | } 13 | 14 | pub trait AudioFilter { 15 | fn modify_stream(&self, data: &mut [i16]); 16 | } 17 | 18 | pub mod softmixer; 19 | use self::softmixer::SoftMixer; 20 | 21 | fn mk_sink() -> Box { 22 | Box::new(M::open()) 23 | } 24 | 25 | pub fn find>(name: Option) -> Option Box> { 26 | match name.as_ref().map(AsRef::as_ref) { 27 | None | Some("softvol") => Some(mk_sink::), 28 | _ => None, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /connect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot-connect" 3 | version = "0.1.0" 4 | authors = ["Paul Lietar "] 5 | 6 | [dependencies.librespot-core] 7 | path = "../core" 8 | [dependencies.librespot-playback] 9 | path = "../playback" 10 | [dependencies.librespot-protocol] 11 | path = "../protocol" 12 | 13 | [dependencies] 14 | base64 = "0.5.0" 15 | futures = "0.1.8" 16 | hyper = "0.11.2" 17 | log = "0.3.5" 18 | num-bigint = "0.1.35" 19 | protobuf = "1.1" 20 | rand = "0.3.13" 21 | rust-crypto = "0.2.36" 22 | serde = "0.9.6" 23 | serde_derive = "0.9.6" 24 | serde_json = "0.9.5" 25 | tokio-core = "0.1.2" 26 | url = "1.3" 27 | 28 | dns-sd = { version = "0.1.3", optional = true } 29 | mdns = { git = "https://github.com/plietar/rust-mdns", optional = true } 30 | 31 | [features] 32 | default = ["mdns"] 33 | with-dns-sd = ["dns-sd"] 34 | -------------------------------------------------------------------------------- /playback/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot-playback" 3 | version = "0.1.0" 4 | authors = ["Sasha Hilton "] 5 | 6 | [dependencies.librespot-audio] 7 | path = "../audio" 8 | [dependencies.librespot-core] 9 | path = "../core" 10 | [dependencies.librespot-metadata] 11 | path = "../metadata" 12 | 13 | [dependencies] 14 | futures = "0.1.8" 15 | log = "0.3.5" 16 | byteorder = "1.2.1" 17 | 18 | alsa = { git = "https://github.com/plietar/rust-alsa", optional = true } 19 | portaudio-rs = { version = "0.3.0", optional = true } 20 | libpulse-sys = { version = "0.0.0", optional = true } 21 | jack = { version = "0.5.3", optional = true } 22 | libc = { version = "0.2", optional = true } 23 | 24 | [features] 25 | alsa-backend = ["alsa"] 26 | portaudio-backend = ["portaudio-rs"] 27 | pulseaudio-backend = ["libpulse-sys", "libc"] 28 | jackaudio-backend = ["jack"] 29 | -------------------------------------------------------------------------------- /audio/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate futures; 3 | #[macro_use] 4 | extern crate log; 5 | 6 | extern crate bit_set; 7 | extern crate byteorder; 8 | extern crate crypto; 9 | extern crate num_bigint; 10 | extern crate num_traits; 11 | extern crate tempfile; 12 | 13 | extern crate librespot_core as core; 14 | 15 | mod decrypt; 16 | mod fetch; 17 | 18 | #[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] 19 | mod lewton_decoder; 20 | #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] 21 | mod libvorbis_decoder; 22 | 23 | pub use decrypt::AudioDecrypt; 24 | pub use fetch::{AudioFile, AudioFileOpen}; 25 | 26 | #[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] 27 | pub use lewton_decoder::{VorbisDecoder, VorbisError, VorbisPacket}; 28 | #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] 29 | pub use libvorbis_decoder::{VorbisDecoder, VorbisError, VorbisPacket}; 30 | -------------------------------------------------------------------------------- /core/build.rs: -------------------------------------------------------------------------------- 1 | extern crate rand; 2 | extern crate vergen; 3 | 4 | use rand::Rng; 5 | use std::env; 6 | use std::fs::OpenOptions; 7 | use std::io::Write; 8 | use std::path::PathBuf; 9 | 10 | fn main() { 11 | let out = PathBuf::from(env::var("OUT_DIR").unwrap()); 12 | 13 | vergen::vergen(vergen::OutputFns::all()).unwrap(); 14 | 15 | let build_id: String = rand::thread_rng().gen_ascii_chars().take(8).collect(); 16 | 17 | let mut version_file = OpenOptions::new() 18 | .write(true) 19 | .append(true) 20 | .open(&out.join("version.rs")) 21 | .unwrap(); 22 | 23 | let build_id_fn = format!( 24 | " 25 | /// Generate a random build id. 26 | pub fn build_id() -> &'static str {{ 27 | \"{}\" 28 | }} 29 | ", 30 | build_id 31 | ); 32 | 33 | if let Err(e) = version_file.write_all(build_id_fn.as_bytes()) { 34 | println!("{}", e); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/keymaster.rs: -------------------------------------------------------------------------------- 1 | use futures::Future; 2 | use serde_json; 3 | 4 | use mercury::MercuryError; 5 | use session::Session; 6 | 7 | #[derive(Deserialize, Debug, Clone)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct Token { 10 | pub access_token: String, 11 | pub expires_in: u32, 12 | pub token_type: String, 13 | pub scope: Vec, 14 | } 15 | 16 | pub fn get_token( 17 | session: &Session, 18 | client_id: &str, 19 | scopes: &str, 20 | ) -> Box> { 21 | let url = format!( 22 | "hm://keymaster/token/authenticated?client_id={}&scope={}", 23 | client_id, scopes 24 | ); 25 | Box::new(session.mercury().get(url).map(move |response| { 26 | let data = response.payload.first().expect("Empty payload"); 27 | let data = String::from_utf8(data.clone()).unwrap(); 28 | let token: Token = serde_json::from_str(&data).unwrap(); 29 | 30 | token 31 | })) 32 | } 33 | -------------------------------------------------------------------------------- /core/src/volume.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{Read, Write}; 3 | use std::path::Path; 4 | 5 | #[derive(Clone, Copy, Debug)] 6 | pub struct Volume { 7 | pub volume: u16, 8 | } 9 | 10 | impl Volume { 11 | // read volume from file 12 | fn from_reader(mut reader: R) -> u16 { 13 | let mut contents = String::new(); 14 | reader.read_to_string(&mut contents).unwrap(); 15 | contents.trim().parse::().unwrap() 16 | } 17 | 18 | pub(crate) fn from_file>(path: P) -> Option { 19 | File::open(path).ok().map(Volume::from_reader) 20 | } 21 | 22 | // write volume to file 23 | fn save_to_writer(&self, writer: &mut W) { 24 | writer.write_all(self.volume.to_string().as_bytes()).unwrap(); 25 | } 26 | 27 | pub(crate) fn save_to_file>(&self, path: P) { 28 | let mut file = File::create(path).unwrap(); 29 | self.save_to_writer(&mut file) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot-core" 3 | version = "0.1.0" 4 | authors = ["Paul Lietar "] 5 | build = "build.rs" 6 | 7 | [dependencies.librespot-protocol] 8 | path = "../protocol" 9 | 10 | [dependencies] 11 | base64 = "0.5.0" 12 | byteorder = "1.0" 13 | bytes = "0.4" 14 | error-chain = { version = "0.11.0", default_features = false } 15 | extprim = "1.5.1" 16 | futures = "0.1.8" 17 | httparse = "1.2.4" 18 | hyper = "0.11.2" 19 | hyper-proxy = { version = "0.4.1", default_features = false } 20 | lazy_static = "0.2.0" 21 | log = "0.3.5" 22 | num-bigint = "0.1.35" 23 | num-integer = "0.1.32" 24 | num-traits = "0.1.36" 25 | protobuf = "1.1" 26 | rand = "0.3.13" 27 | rpassword = "0.3.0" 28 | rust-crypto = "0.2.36" 29 | serde = "0.9.6" 30 | serde_derive = "0.9.6" 31 | serde_json = "0.9.5" 32 | shannon = "0.2.0" 33 | tokio-core = "0.1.2" 34 | tokio-io = "0.1" 35 | url = "1.7.0" 36 | uuid = { version = "0.4", features = ["v4"] } 37 | 38 | [build-dependencies] 39 | rand = "0.3.13" 40 | vergen = "0.1.0" 41 | -------------------------------------------------------------------------------- /playback/src/audio_backend/pipe.rs: -------------------------------------------------------------------------------- 1 | use super::{Open, Sink}; 2 | use std::fs::OpenOptions; 3 | use std::io::{self, Write}; 4 | use std::mem; 5 | use std::slice; 6 | 7 | pub struct StdoutSink(Box); 8 | 9 | impl Open for StdoutSink { 10 | fn open(path: Option) -> StdoutSink { 11 | if let Some(path) = path { 12 | let file = OpenOptions::new().write(true).open(path).unwrap(); 13 | StdoutSink(Box::new(file)) 14 | } else { 15 | StdoutSink(Box::new(io::stdout())) 16 | } 17 | } 18 | } 19 | 20 | impl Sink for StdoutSink { 21 | fn start(&mut self) -> io::Result<()> { 22 | Ok(()) 23 | } 24 | 25 | fn stop(&mut self) -> io::Result<()> { 26 | Ok(()) 27 | } 28 | 29 | fn write(&mut self, data: &[i16]) -> io::Result<()> { 30 | let data: &[u8] = unsafe { 31 | slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * mem::size_of::()) 32 | }; 33 | 34 | self.0.write_all(data)?; 35 | self.0.flush()?; 36 | 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /playback/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] 4 | pub enum Bitrate { 5 | Bitrate96, 6 | Bitrate160, 7 | Bitrate320, 8 | } 9 | 10 | impl FromStr for Bitrate { 11 | type Err = (); 12 | fn from_str(s: &str) -> Result { 13 | match s { 14 | "96" => Ok(Bitrate::Bitrate96), 15 | "160" => Ok(Bitrate::Bitrate160), 16 | "320" => Ok(Bitrate::Bitrate320), 17 | _ => Err(()), 18 | } 19 | } 20 | } 21 | 22 | impl Default for Bitrate { 23 | fn default() -> Bitrate { 24 | Bitrate::Bitrate160 25 | } 26 | } 27 | 28 | #[derive(Clone, Debug)] 29 | pub struct PlayerConfig { 30 | pub bitrate: Bitrate, 31 | pub normalisation: bool, 32 | pub normalisation_pregain: f32, 33 | } 34 | 35 | impl Default for PlayerConfig { 36 | fn default() -> PlayerConfig { 37 | PlayerConfig { 38 | bitrate: Bitrate::default(), 39 | normalisation: false, 40 | normalisation_pregain: 0.0, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /protocol/proto/playlist4content.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "playlist4meta.proto"; 4 | import "playlist4issues.proto"; 5 | 6 | message Item { 7 | optional string uri = 0x1; 8 | optional ItemAttributes attributes = 0x2; 9 | } 10 | 11 | message ListItems { 12 | optional int32 pos = 0x1; 13 | optional bool truncated = 0x2; 14 | repeated Item items = 0x3; 15 | } 16 | 17 | message ContentRange { 18 | optional int32 pos = 0x1; 19 | optional int32 length = 0x2; 20 | } 21 | 22 | message ListContentSelection { 23 | optional bool wantRevision = 0x1; 24 | optional bool wantLength = 0x2; 25 | optional bool wantAttributes = 0x3; 26 | optional bool wantChecksum = 0x4; 27 | optional bool wantContent = 0x5; 28 | optional ContentRange contentRange = 0x6; 29 | optional bool wantDiff = 0x7; 30 | optional bytes baseRevision = 0x8; 31 | optional bytes hintRevision = 0x9; 32 | optional bool wantNothingIfUpToDate = 0xa; 33 | optional bool wantResolveAction = 0xc; 34 | repeated ClientIssue issues = 0xd; 35 | repeated ClientResolveAction resolveAction = 0xe; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /protocol/proto/socialgraph.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message CountReply { 4 | repeated int32 counts = 0x1; 5 | } 6 | 7 | message UserListRequest { 8 | optional string last_result = 0x1; 9 | optional int32 count = 0x2; 10 | optional bool include_length = 0x3; 11 | } 12 | 13 | message UserListReply { 14 | repeated User users = 0x1; 15 | optional int32 length = 0x2; 16 | } 17 | 18 | message User { 19 | optional string username = 0x1; 20 | optional int32 subscriber_count = 0x2; 21 | optional int32 subscription_count = 0x3; 22 | } 23 | 24 | message ArtistListReply { 25 | repeated Artist artists = 0x1; 26 | } 27 | 28 | message Artist { 29 | optional string artistid = 0x1; 30 | optional int32 subscriber_count = 0x2; 31 | } 32 | 33 | message StringListRequest { 34 | repeated string args = 0x1; 35 | } 36 | 37 | message StringListReply { 38 | repeated string reply = 0x1; 39 | } 40 | 41 | message TopPlaylistsRequest { 42 | optional string username = 0x1; 43 | optional int32 count = 0x2; 44 | } 45 | 46 | message TopPlaylistsReply { 47 | repeated string uris = 0x1; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Paul Lietar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /protocol/proto/suggest.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Track { 4 | optional bytes gid = 0x1; 5 | optional string name = 0x2; 6 | optional bytes image = 0x3; 7 | repeated string artist_name = 0x4; 8 | repeated bytes artist_gid = 0x5; 9 | optional uint32 rank = 0x6; 10 | } 11 | 12 | message Artist { 13 | optional bytes gid = 0x1; 14 | optional string name = 0x2; 15 | optional bytes image = 0x3; 16 | optional uint32 rank = 0x6; 17 | } 18 | 19 | message Album { 20 | optional bytes gid = 0x1; 21 | optional string name = 0x2; 22 | optional bytes image = 0x3; 23 | repeated string artist_name = 0x4; 24 | repeated bytes artist_gid = 0x5; 25 | optional uint32 rank = 0x6; 26 | } 27 | 28 | message Playlist { 29 | optional string uri = 0x1; 30 | optional string name = 0x2; 31 | optional string image_uri = 0x3; 32 | optional string owner_name = 0x4; 33 | optional string owner_uri = 0x5; 34 | optional uint32 rank = 0x6; 35 | } 36 | 37 | message Suggestions { 38 | repeated Track track = 0x1; 39 | repeated Album album = 0x2; 40 | repeated Artist artist = 0x3; 41 | repeated Playlist playlist = 0x4; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /protocol/proto/facebook-publish.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message EventReply { 4 | optional int32 queued = 0x1; 5 | optional RetryInfo retry = 0x2; 6 | } 7 | 8 | message RetryInfo { 9 | optional int32 retry_delay = 0x1; 10 | optional int32 max_retry = 0x2; 11 | } 12 | 13 | message Id { 14 | optional string uri = 0x1; 15 | optional int64 start_time = 0x2; 16 | } 17 | 18 | message Start { 19 | optional int32 length = 0x1; 20 | optional string context_uri = 0x2; 21 | optional int64 end_time = 0x3; 22 | } 23 | 24 | message Seek { 25 | optional int64 end_time = 0x1; 26 | } 27 | 28 | message Pause { 29 | optional int32 seconds_played = 0x1; 30 | optional int64 end_time = 0x2; 31 | } 32 | 33 | message Resume { 34 | optional int32 seconds_played = 0x1; 35 | optional int64 end_time = 0x2; 36 | } 37 | 38 | message End { 39 | optional int32 seconds_played = 0x1; 40 | optional int64 end_time = 0x2; 41 | } 42 | 43 | message Event { 44 | optional Id id = 0x1; 45 | optional Start start = 0x2; 46 | optional Seek seek = 0x3; 47 | optional Pause pause = 0x4; 48 | optional Resume resume = 0x5; 49 | optional End end = 0x6; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /protocol/proto/search.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message SearchRequest { 4 | optional string query = 0x1; 5 | optional Type type = 0x2; 6 | enum Type { 7 | TRACK = 0x0; 8 | ALBUM = 0x1; 9 | ARTIST = 0x2; 10 | PLAYLIST = 0x3; 11 | USER = 0x4; 12 | } 13 | optional int32 limit = 0x3; 14 | optional int32 offset = 0x4; 15 | optional bool did_you_mean = 0x5; 16 | optional string spotify_uri = 0x2; 17 | repeated bytes file_id = 0x3; 18 | optional string url = 0x4; 19 | optional string slask_id = 0x5; 20 | } 21 | 22 | message Playlist { 23 | optional string uri = 0x1; 24 | optional string name = 0x2; 25 | repeated Image image = 0x3; 26 | } 27 | 28 | message User { 29 | optional string username = 0x1; 30 | optional string full_name = 0x2; 31 | repeated Image image = 0x3; 32 | optional sint32 followers = 0x4; 33 | } 34 | 35 | message SearchReply { 36 | optional sint32 hits = 0x1; 37 | repeated Track track = 0x2; 38 | repeated Album album = 0x3; 39 | repeated Artist artist = 0x4; 40 | repeated Playlist playlist = 0x5; 41 | optional string did_you_mean = 0x6; 42 | repeated User user = 0x7; 43 | } 44 | 45 | -------------------------------------------------------------------------------- /protocol/proto/mercury.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message MercuryMultiGetRequest { 4 | repeated MercuryRequest request = 0x1; 5 | } 6 | 7 | message MercuryMultiGetReply { 8 | repeated MercuryReply reply = 0x1; 9 | } 10 | 11 | message MercuryRequest { 12 | optional string uri = 0x1; 13 | optional string content_type = 0x2; 14 | optional bytes body = 0x3; 15 | optional bytes etag = 0x4; 16 | } 17 | 18 | message MercuryReply { 19 | optional sint32 status_code = 0x1; 20 | optional string status_message = 0x2; 21 | optional CachePolicy cache_policy = 0x3; 22 | enum CachePolicy { 23 | CACHE_NO = 0x1; 24 | CACHE_PRIVATE = 0x2; 25 | CACHE_PUBLIC = 0x3; 26 | } 27 | optional sint32 ttl = 0x4; 28 | optional bytes etag = 0x5; 29 | optional string content_type = 0x6; 30 | optional bytes body = 0x7; 31 | } 32 | 33 | 34 | message Header { 35 | optional string uri = 0x01; 36 | optional string content_type = 0x02; 37 | optional string method = 0x03; 38 | optional sint32 status_code = 0x04; 39 | repeated UserField user_fields = 0x06; 40 | } 41 | 42 | message UserField { 43 | optional string key = 0x01; 44 | optional bytes value = 0x02; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /protocol/proto/playlist4issues.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ClientIssue { 4 | optional Level level = 0x1; 5 | enum Level { 6 | LEVEL_UNKNOWN = 0x0; 7 | LEVEL_DEBUG = 0x1; 8 | LEVEL_INFO = 0x2; 9 | LEVEL_NOTICE = 0x3; 10 | LEVEL_WARNING = 0x4; 11 | LEVEL_ERROR = 0x5; 12 | } 13 | optional Code code = 0x2; 14 | enum Code { 15 | CODE_UNKNOWN = 0x0; 16 | CODE_INDEX_OUT_OF_BOUNDS = 0x1; 17 | CODE_VERSION_MISMATCH = 0x2; 18 | CODE_CACHED_CHANGE = 0x3; 19 | CODE_OFFLINE_CHANGE = 0x4; 20 | CODE_CONCURRENT_CHANGE = 0x5; 21 | } 22 | optional int32 repeatCount = 0x3; 23 | } 24 | 25 | message ClientResolveAction { 26 | optional Code code = 0x1; 27 | enum Code { 28 | CODE_UNKNOWN = 0x0; 29 | CODE_NO_ACTION = 0x1; 30 | CODE_RETRY = 0x2; 31 | CODE_RELOAD = 0x3; 32 | CODE_DISCARD_LOCAL_CHANGES = 0x4; 33 | CODE_SEND_DUMP = 0x5; 34 | CODE_DISPLAY_ERROR_MESSAGE = 0x6; 35 | } 36 | optional Initiator initiator = 0x2; 37 | enum Initiator { 38 | INITIATOR_UNKNOWN = 0x0; 39 | INITIATOR_SERVER = 0x1; 40 | INITIATOR_CLIENT = 0x2; 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))] 2 | 3 | #[macro_use] 4 | extern crate error_chain; 5 | #[macro_use] 6 | extern crate futures; 7 | #[macro_use] 8 | extern crate lazy_static; 9 | #[macro_use] 10 | extern crate log; 11 | #[macro_use] 12 | extern crate serde_derive; 13 | 14 | extern crate base64; 15 | extern crate byteorder; 16 | extern crate bytes; 17 | extern crate crypto; 18 | extern crate extprim; 19 | extern crate httparse; 20 | extern crate hyper; 21 | extern crate hyper_proxy; 22 | extern crate num_bigint; 23 | extern crate num_integer; 24 | extern crate num_traits; 25 | extern crate protobuf; 26 | extern crate rand; 27 | extern crate rpassword; 28 | extern crate serde; 29 | extern crate serde_json; 30 | extern crate shannon; 31 | extern crate tokio_core; 32 | extern crate tokio_io; 33 | extern crate url; 34 | extern crate uuid; 35 | 36 | extern crate librespot_protocol as protocol; 37 | 38 | #[macro_use] 39 | mod component; 40 | mod apresolve; 41 | pub mod audio_key; 42 | pub mod authentication; 43 | pub mod cache; 44 | pub mod channel; 45 | pub mod config; 46 | mod connection; 47 | pub mod diffie_hellman; 48 | pub mod keymaster; 49 | pub mod mercury; 50 | mod proxytunnel; 51 | pub mod session; 52 | pub mod spotify_id; 53 | pub mod util; 54 | pub mod version; 55 | pub mod volume; 56 | -------------------------------------------------------------------------------- /playback/src/mixer/softmixer.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicUsize, Ordering}; 2 | use std::sync::Arc; 3 | 4 | use super::AudioFilter; 5 | use super::Mixer; 6 | 7 | #[derive(Clone)] 8 | pub struct SoftMixer { 9 | volume: Arc, 10 | } 11 | 12 | impl Mixer for SoftMixer { 13 | fn open() -> SoftMixer { 14 | SoftMixer { 15 | volume: Arc::new(AtomicUsize::new(0xFFFF)), 16 | } 17 | } 18 | fn start(&self) {} 19 | fn stop(&self) {} 20 | fn volume(&self) -> u16 { 21 | self.volume.load(Ordering::Relaxed) as u16 22 | } 23 | fn set_volume(&self, volume: u16) { 24 | self.volume.store(volume as usize, Ordering::Relaxed); 25 | } 26 | fn get_audio_filter(&self) -> Option> { 27 | Some(Box::new(SoftVolumeApplier { 28 | volume: self.volume.clone(), 29 | })) 30 | } 31 | } 32 | 33 | struct SoftVolumeApplier { 34 | volume: Arc, 35 | } 36 | 37 | impl AudioFilter for SoftVolumeApplier { 38 | fn modify_stream(&self, data: &mut [i16]) { 39 | let volume = self.volume.load(Ordering::Relaxed) as u16; 40 | if volume != 0xFFFF { 41 | for x in data.iter_mut() { 42 | *x = (*x as i32 * volume as i32 / 0xFFFF) as i16; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/player_event_handler.rs: -------------------------------------------------------------------------------- 1 | use librespot::playback::player::PlayerEvent; 2 | use std::collections::HashMap; 3 | use std::process::Command; 4 | 5 | fn run_program(program: &str, env_vars: HashMap<&str, String>) { 6 | let mut v: Vec<&str> = program.split_whitespace().collect(); 7 | info!("Running {:?} with environment variables {:?}", v, env_vars); 8 | Command::new(&v.remove(0)) 9 | .args(&v) 10 | .envs(env_vars.iter()) 11 | .spawn() 12 | .expect("program failed to start"); 13 | } 14 | 15 | pub fn run_program_on_events(event: PlayerEvent, onevent: &str) { 16 | let mut env_vars = HashMap::new(); 17 | match event { 18 | PlayerEvent::Changed { 19 | old_track_id, 20 | new_track_id, 21 | } => { 22 | env_vars.insert("PLAYER_EVENT", "change".to_string()); 23 | env_vars.insert("OLD_TRACK_ID", old_track_id.to_base16()); 24 | env_vars.insert("TRACK_ID", new_track_id.to_base16()); 25 | } 26 | PlayerEvent::Started { track_id } => { 27 | env_vars.insert("PLAYER_EVENT", "start".to_string()); 28 | env_vars.insert("TRACK_ID", track_id.to_base16()); 29 | } 30 | PlayerEvent::Stopped { track_id } => { 31 | env_vars.insert("PLAYER_EVENT", "stop".to_string()); 32 | env_vars.insert("TRACK_ID", track_id.to_base16()); 33 | } 34 | } 35 | run_program(onevent, env_vars); 36 | } 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - 1.21.0 4 | - stable 5 | - beta 6 | - nightly 7 | 8 | cache: cargo 9 | 10 | addons: 11 | apt: 12 | packages: 13 | - gcc-arm-linux-gnueabihf 14 | - libc6-dev-armhf-cross 15 | - libpulse-dev 16 | - portaudio19-dev 17 | 18 | before_script: 19 | - mkdir -p ~/.cargo 20 | - echo '[target.armv7-unknown-linux-gnueabihf]' > ~/.cargo/config 21 | - echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config 22 | - rustup target add armv7-unknown-linux-gnueabihf 23 | 24 | script: 25 | - cargo build --locked --no-default-features 26 | - cargo build --locked --no-default-features --features "with-tremor" 27 | - cargo build --locked --no-default-features --features "with-vorbis" 28 | - cargo build --locked --no-default-features --features "portaudio-backend" 29 | - cargo build --locked --no-default-features --features "pulseaudio-backend" 30 | - cargo build --locked --no-default-features --features "alsa-backend" 31 | - cargo build --locked --no-default-features --target armv7-unknown-linux-gnueabihf 32 | 33 | notifications: 34 | email: false 35 | webhooks: 36 | urls: 37 | - https://webhooks.gitter.im/e/780b178b15811059752e 38 | on_success: change # options: [always|never|change] default: always 39 | on_failure: always # options: [always|never|change] default: always 40 | on_start: never # options: [always|never|change] default: always 41 | -------------------------------------------------------------------------------- /protocol/proto/radio.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message RadioRequest { 4 | repeated string uris = 0x1; 5 | optional int32 salt = 0x2; 6 | optional int32 length = 0x4; 7 | optional string stationId = 0x5; 8 | repeated string lastTracks = 0x6; 9 | } 10 | 11 | message MultiSeedRequest { 12 | repeated string uris = 0x1; 13 | } 14 | 15 | message Feedback { 16 | optional string uri = 0x1; 17 | optional string type = 0x2; 18 | optional double timestamp = 0x3; 19 | } 20 | 21 | message Tracks { 22 | repeated string gids = 0x1; 23 | optional string source = 0x2; 24 | optional string identity = 0x3; 25 | repeated string tokens = 0x4; 26 | repeated Feedback feedback = 0x5; 27 | } 28 | 29 | message Station { 30 | optional string id = 0x1; 31 | optional string title = 0x2; 32 | optional string titleUri = 0x3; 33 | optional string subtitle = 0x4; 34 | optional string subtitleUri = 0x5; 35 | optional string imageUri = 0x6; 36 | optional double lastListen = 0x7; 37 | repeated string seeds = 0x8; 38 | optional int32 thumbsUp = 0x9; 39 | optional int32 thumbsDown = 0xa; 40 | } 41 | 42 | message Rules { 43 | optional string js = 0x1; 44 | } 45 | 46 | message StationResponse { 47 | optional Station station = 0x1; 48 | repeated Feedback feedback = 0x2; 49 | } 50 | 51 | message StationList { 52 | repeated Station stations = 0x1; 53 | } 54 | 55 | message LikedPlaylist { 56 | optional string uri = 0x1; 57 | } 58 | 59 | -------------------------------------------------------------------------------- /playback/src/audio_backend/alsa.rs: -------------------------------------------------------------------------------- 1 | use super::{Open, Sink}; 2 | use alsa::{Access, Format, Mode, Stream, PCM}; 3 | use std::io; 4 | 5 | pub struct AlsaSink(Option, String); 6 | 7 | impl Open for AlsaSink { 8 | fn open(device: Option) -> AlsaSink { 9 | info!("Using alsa sink"); 10 | 11 | let name = device.unwrap_or("default".to_string()); 12 | 13 | AlsaSink(None, name) 14 | } 15 | } 16 | 17 | impl Sink for AlsaSink { 18 | fn start(&mut self) -> io::Result<()> { 19 | if self.0.is_none() { 20 | match PCM::open( 21 | &*self.1, 22 | Stream::Playback, 23 | Mode::Blocking, 24 | Format::Signed16, 25 | Access::Interleaved, 26 | 2, 27 | 44100, 28 | ) { 29 | Ok(f) => self.0 = Some(f), 30 | Err(e) => { 31 | error!("Alsa error PCM open {}", e); 32 | return Err(io::Error::new( 33 | io::ErrorKind::Other, 34 | "Alsa error: PCM open failed", 35 | )); 36 | } 37 | } 38 | } 39 | Ok(()) 40 | } 41 | 42 | fn stop(&mut self) -> io::Result<()> { 43 | self.0 = None; 44 | Ok(()) 45 | } 46 | 47 | fn write(&mut self, data: &[i16]) -> io::Result<()> { 48 | self.0.as_mut().unwrap().write_interleaved(&data).unwrap(); 49 | Ok(()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/play.rs: -------------------------------------------------------------------------------- 1 | extern crate librespot; 2 | extern crate tokio_core; 3 | 4 | use std::env; 5 | use tokio_core::reactor::Core; 6 | 7 | use librespot::core::authentication::Credentials; 8 | use librespot::core::config::SessionConfig; 9 | use librespot::core::session::Session; 10 | use librespot::core::spotify_id::SpotifyId; 11 | use librespot::playback::config::PlayerConfig; 12 | 13 | use librespot::playback::audio_backend; 14 | use librespot::playback::player::Player; 15 | 16 | fn main() { 17 | let mut core = Core::new().unwrap(); 18 | let handle = core.handle(); 19 | 20 | let session_config = SessionConfig::default(); 21 | let player_config = PlayerConfig::default(); 22 | 23 | let args: Vec<_> = env::args().collect(); 24 | if args.len() != 4 { 25 | println!("Usage: {} USERNAME PASSWORD TRACK", args[0]); 26 | } 27 | let username = args[1].to_owned(); 28 | let password = args[2].to_owned(); 29 | let credentials = Credentials::with_password(username, password); 30 | 31 | let track = SpotifyId::from_base62(&args[3]).unwrap(); 32 | 33 | let backend = audio_backend::find(None).unwrap(); 34 | 35 | println!("Connecting .."); 36 | let session = core.run(Session::connect(session_config, credentials, None, handle)) 37 | .unwrap(); 38 | 39 | let (player, _) = Player::new(player_config, session.clone(), None, move || (backend)(None)); 40 | 41 | println!("Playing..."); 42 | core.run(player.load(track, true, 0)).unwrap(); 43 | 44 | println!("Done"); 45 | } 46 | -------------------------------------------------------------------------------- /protocol/proto/playlist4meta.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ListChecksum { 4 | optional int32 version = 0x1; 5 | optional bytes sha1 = 0x4; 6 | } 7 | 8 | message DownloadFormat { 9 | optional Codec codec = 0x1; 10 | enum Codec { 11 | CODEC_UNKNOWN = 0x0; 12 | OGG_VORBIS = 0x1; 13 | FLAC = 0x2; 14 | MPEG_1_LAYER_3 = 0x3; 15 | } 16 | } 17 | 18 | message ListAttributes { 19 | optional string name = 0x1; 20 | optional string description = 0x2; 21 | optional bytes picture = 0x3; 22 | optional bool collaborative = 0x4; 23 | optional string pl3_version = 0x5; 24 | optional bool deleted_by_owner = 0x6; 25 | optional bool restricted_collaborative = 0x7; 26 | optional int64 deprecated_client_id = 0x8; 27 | optional bool public_starred = 0x9; 28 | optional string client_id = 0xa; 29 | } 30 | 31 | message ItemAttributes { 32 | optional string added_by = 0x1; 33 | optional int64 timestamp = 0x2; 34 | optional string message = 0x3; 35 | optional bool seen = 0x4; 36 | optional int64 download_count = 0x5; 37 | optional DownloadFormat download_format = 0x6; 38 | optional string sevendigital_id = 0x7; 39 | optional int64 sevendigital_left = 0x8; 40 | optional int64 seen_at = 0x9; 41 | optional bool public = 0xa; 42 | } 43 | 44 | message StringAttribute { 45 | optional string key = 0x1; 46 | optional string value = 0x2; 47 | } 48 | 49 | message StringAttributes { 50 | repeated StringAttribute attribute = 0x1; 51 | } 52 | 53 | -------------------------------------------------------------------------------- /core/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | use num_bigint::BigUint; 2 | use num_integer::Integer; 3 | use num_traits::{One, Zero}; 4 | use rand::{Rand, Rng}; 5 | use std::mem; 6 | use std::ops::{Mul, Rem, Shr}; 7 | 8 | pub fn rand_vec(rng: &mut G, size: usize) -> Vec { 9 | rng.gen_iter().take(size).collect() 10 | } 11 | 12 | pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { 13 | let mut base = base.clone(); 14 | let mut exp = exp.clone(); 15 | let mut result: BigUint = One::one(); 16 | 17 | while !exp.is_zero() { 18 | if exp.is_odd() { 19 | result = result.mul(&base).rem(modulus); 20 | } 21 | exp = exp.shr(1); 22 | base = (&base).mul(&base).rem(modulus); 23 | } 24 | 25 | result 26 | } 27 | 28 | pub trait ReadSeek: ::std::io::Read + ::std::io::Seek {} 29 | impl ReadSeek for T {} 30 | 31 | pub trait Seq { 32 | fn next(&self) -> Self; 33 | } 34 | 35 | macro_rules! impl_seq { 36 | ($($ty:ty)*) => { $( 37 | impl Seq for $ty { 38 | fn next(&self) -> Self { *self + 1 } 39 | } 40 | )* } 41 | } 42 | 43 | impl_seq!(u8 u16 u32 u64 usize); 44 | 45 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] 46 | pub struct SeqGenerator(T); 47 | 48 | impl SeqGenerator { 49 | pub fn new(value: T) -> Self { 50 | SeqGenerator(value) 51 | } 52 | 53 | pub fn get(&mut self) -> T { 54 | let value = self.0.next(); 55 | mem::replace(&mut self.0, value) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /protocol/proto/ad-hermes-proxy.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Rule { 4 | optional string type = 0x1; 5 | optional uint32 times = 0x2; 6 | optional uint64 interval = 0x3; 7 | } 8 | 9 | message AdRequest { 10 | optional string client_language = 0x1; 11 | optional string product = 0x2; 12 | optional uint32 version = 0x3; 13 | optional string type = 0x4; 14 | repeated string avoidAds = 0x5; 15 | } 16 | 17 | message AdQueueResponse { 18 | repeated AdQueueEntry adQueueEntry = 0x1; 19 | } 20 | 21 | message AdFile { 22 | optional string id = 0x1; 23 | optional string format = 0x2; 24 | } 25 | 26 | message AdQueueEntry { 27 | optional uint64 start_time = 0x1; 28 | optional uint64 end_time = 0x2; 29 | optional double priority = 0x3; 30 | optional string token = 0x4; 31 | optional uint32 ad_version = 0x5; 32 | optional string id = 0x6; 33 | optional string type = 0x7; 34 | optional string campaign = 0x8; 35 | optional string advertiser = 0x9; 36 | optional string url = 0xa; 37 | optional uint64 duration = 0xb; 38 | optional uint64 expiry = 0xc; 39 | optional string tracking_url = 0xd; 40 | optional string banner_type = 0xe; 41 | optional string html = 0xf; 42 | optional string image = 0x10; 43 | optional string background_image = 0x11; 44 | optional string background_url = 0x12; 45 | optional string background_color = 0x13; 46 | optional string title = 0x14; 47 | optional string caption = 0x15; 48 | repeated AdFile file = 0x16; 49 | repeated Rule rule = 0x17; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /core/src/mercury/sender.rs: -------------------------------------------------------------------------------- 1 | use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend}; 2 | use std::collections::VecDeque; 3 | 4 | use super::*; 5 | 6 | pub struct MercurySender { 7 | mercury: MercuryManager, 8 | uri: String, 9 | pending: VecDeque>, 10 | } 11 | 12 | impl MercurySender { 13 | // TODO: pub(super) when stable 14 | pub(crate) fn new(mercury: MercuryManager, uri: String) -> MercurySender { 15 | MercurySender { 16 | mercury: mercury, 17 | uri: uri, 18 | pending: VecDeque::new(), 19 | } 20 | } 21 | } 22 | 23 | impl Clone for MercurySender { 24 | fn clone(&self) -> MercurySender { 25 | MercurySender { 26 | mercury: self.mercury.clone(), 27 | uri: self.uri.clone(), 28 | pending: VecDeque::new(), 29 | } 30 | } 31 | } 32 | 33 | impl Sink for MercurySender { 34 | type SinkItem = Vec; 35 | type SinkError = MercuryError; 36 | 37 | fn start_send(&mut self, item: Self::SinkItem) -> StartSend { 38 | let task = self.mercury.send(self.uri.clone(), item); 39 | self.pending.push_back(task); 40 | Ok(AsyncSink::Ready) 41 | } 42 | 43 | fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { 44 | loop { 45 | match self.pending.front_mut() { 46 | Some(task) => { 47 | try_ready!(task.poll()); 48 | } 49 | None => { 50 | return Ok(Async::Ready(())); 51 | } 52 | } 53 | self.pending.pop_front(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /contrib/docker-build-pi-armv6hf.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Snipped and tucked from https://github.com/plietar/librespot/pull/202/commits/21549641d39399cbaec0bc92b36c9951d1b87b90 4 | # and further inputs from https://github.com/kingosticks/librespot/commit/c55dd20bd6c7e44dd75ff33185cf50b2d3bd79c3 5 | 6 | set -eux 7 | # Get alsa lib and headers 8 | ALSA_VER="1.0.25-4" 9 | DEPS=( \ 10 | "http://mirrordirector.raspbian.org/raspbian/pool/main/a/alsa-lib/libasound2_${ALSA_VER}_armhf.deb" \ 11 | "http://mirrordirector.raspbian.org/raspbian/pool/main/a/alsa-lib/libasound2-dev_${ALSA_VER}_armhf.deb" \ 12 | ) 13 | 14 | # Collect Paths 15 | SYSROOT="/pi-tools/arm-bcm2708/arm-bcm2708hardfp-linux-gnueabi/arm-bcm2708hardfp-linux-gnueabi/sysroot" 16 | GCC="/pi-tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin" 17 | GCC_SYSROOT="$GCC/gcc-sysroot" 18 | 19 | 20 | export PATH=/pi-tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin/:$PATH 21 | 22 | # Link the compiler 23 | export TARGET_CC="$GCC/arm-linux-gnueabihf-gcc" 24 | 25 | # Create wrapper around gcc to point to rpi sysroot 26 | echo -e '#!/bin/bash' "\n$TARGET_CC --sysroot $SYSROOT \"\$@\"" > $GCC_SYSROOT 27 | chmod +x $GCC_SYSROOT 28 | 29 | # Add extra target dependencies to our rpi sysroot 30 | for path in "${DEPS[@]}"; do 31 | curl -OL $path 32 | dpkg -x $(basename $path) $SYSROOT 33 | done 34 | 35 | # i don't why this is neccessary 36 | # ln -s ld-linux.so.3 $SYSROOT/lib/ld-linux-armhf.so.3 37 | 38 | # point cargo to use gcc wrapper as linker 39 | echo -e '[target.arm-unknown-linux-gnueabihf]\nlinker = "gcc-sysroot"' > /.cargo/config 40 | 41 | # Build 42 | cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" 43 | -------------------------------------------------------------------------------- /core/src/diffie_hellman.rs: -------------------------------------------------------------------------------- 1 | use num_bigint::BigUint; 2 | use num_traits::FromPrimitive; 3 | use rand::Rng; 4 | 5 | use util; 6 | 7 | lazy_static! { 8 | pub static ref DH_GENERATOR: BigUint = BigUint::from_u64(0x2).unwrap(); 9 | pub static ref DH_PRIME: BigUint = BigUint::from_bytes_be(&[ 10 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 11 | 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, 12 | 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, 13 | 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, 14 | 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 15 | 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 16 | ]); 17 | } 18 | 19 | pub struct DHLocalKeys { 20 | private_key: BigUint, 21 | public_key: BigUint, 22 | } 23 | 24 | impl DHLocalKeys { 25 | pub fn random(rng: &mut R) -> DHLocalKeys { 26 | let key_data = util::rand_vec(rng, 95); 27 | 28 | let private_key = BigUint::from_bytes_be(&key_data); 29 | let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); 30 | 31 | DHLocalKeys { 32 | private_key: private_key, 33 | public_key: public_key, 34 | } 35 | } 36 | 37 | pub fn public_key(&self) -> Vec { 38 | self.public_key.to_bytes_be() 39 | } 40 | 41 | pub fn shared_secret(&self, remote_key: &[u8]) -> Vec { 42 | let shared_key = util::powm(&BigUint::from_bytes_be(remote_key), &self.private_key, &DH_PRIME); 43 | shared_key.to_bytes_be() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /audio/src/decrypt.rs: -------------------------------------------------------------------------------- 1 | use crypto::aes; 2 | use crypto::symmetriccipher::SynchronousStreamCipher; 3 | use num_bigint::BigUint; 4 | use num_traits::FromPrimitive; 5 | use std::io; 6 | use std::ops::Add; 7 | 8 | use core::audio_key::AudioKey; 9 | 10 | const AUDIO_AESIV: &'static [u8] = &[ 11 | 0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93, 12 | ]; 13 | 14 | pub struct AudioDecrypt { 15 | cipher: Box, 16 | key: AudioKey, 17 | reader: T, 18 | } 19 | 20 | impl AudioDecrypt { 21 | pub fn new(key: AudioKey, reader: T) -> AudioDecrypt { 22 | let cipher = aes::ctr(aes::KeySize::KeySize128, &key.0, AUDIO_AESIV); 23 | AudioDecrypt { 24 | cipher: cipher, 25 | key: key, 26 | reader: reader, 27 | } 28 | } 29 | } 30 | 31 | impl io::Read for AudioDecrypt { 32 | fn read(&mut self, output: &mut [u8]) -> io::Result { 33 | let mut buffer = vec![0u8; output.len()]; 34 | let len = try!(self.reader.read(&mut buffer)); 35 | 36 | self.cipher.process(&buffer[..len], &mut output[..len]); 37 | 38 | Ok(len) 39 | } 40 | } 41 | 42 | impl io::Seek for AudioDecrypt { 43 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 44 | let newpos = try!(self.reader.seek(pos)); 45 | let skip = newpos % 16; 46 | 47 | let iv = BigUint::from_bytes_be(AUDIO_AESIV) 48 | .add(BigUint::from_u64(newpos / 16).unwrap()) 49 | .to_bytes_be(); 50 | self.cipher = aes::ctr(aes::KeySize::KeySize128, &self.key.0, &iv); 51 | 52 | let buf = vec![0u8; skip as usize]; 53 | let mut buf2 = vec![0u8; skip as usize]; 54 | self.cipher.process(&buf, &mut buf2); 55 | 56 | Ok(newpos as u64) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /playback/src/audio_backend/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub trait Open { 4 | fn open(Option) -> Self; 5 | } 6 | 7 | pub trait Sink { 8 | fn start(&mut self) -> io::Result<()>; 9 | fn stop(&mut self) -> io::Result<()>; 10 | fn write(&mut self, data: &[i16]) -> io::Result<()>; 11 | } 12 | 13 | fn mk_sink(device: Option) -> Box { 14 | Box::new(S::open(device)) 15 | } 16 | 17 | #[cfg(feature = "alsa-backend")] 18 | mod alsa; 19 | #[cfg(feature = "alsa-backend")] 20 | use self::alsa::AlsaSink; 21 | 22 | #[cfg(feature = "portaudio-backend")] 23 | mod portaudio; 24 | #[cfg(feature = "portaudio-backend")] 25 | use self::portaudio::PortAudioSink; 26 | 27 | #[cfg(feature = "pulseaudio-backend")] 28 | mod pulseaudio; 29 | #[cfg(feature = "pulseaudio-backend")] 30 | use self::pulseaudio::PulseAudioSink; 31 | 32 | #[cfg(feature = "jackaudio-backend")] 33 | mod jackaudio; 34 | #[cfg(feature = "jackaudio-backend")] 35 | use self::jackaudio::JackSink; 36 | 37 | mod pipe; 38 | use self::pipe::StdoutSink; 39 | 40 | pub const BACKENDS: &'static [(&'static str, fn(Option) -> Box)] = &[ 41 | #[cfg(feature = "alsa-backend")] 42 | ("alsa", mk_sink::), 43 | #[cfg(feature = "portaudio-backend")] 44 | ("portaudio", mk_sink::), 45 | #[cfg(feature = "pulseaudio-backend")] 46 | ("pulseaudio", mk_sink::), 47 | #[cfg(feature = "jackaudio-backend")] 48 | ("jackaudio", mk_sink::), 49 | ("pipe", mk_sink::), 50 | ]; 51 | 52 | pub fn find(name: Option) -> Option) -> Box> { 53 | if let Some(name) = name { 54 | BACKENDS 55 | .iter() 56 | .find(|backend| name == backend.0) 57 | .map(|backend| backend.1) 58 | } else { 59 | Some( 60 | BACKENDS 61 | .first() 62 | .expect("No backends were enabled at build time") 63 | .1, 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src/component.rs: -------------------------------------------------------------------------------- 1 | macro_rules! component { 2 | ($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => { 3 | #[derive(Clone)] 4 | pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::std::sync::Mutex<$inner>)>); 5 | impl $name { 6 | #[allow(dead_code)] 7 | pub(crate) fn new(session: $crate::session::SessionWeak) -> $name { 8 | debug!(target:"librespot::component", "new {}", stringify!($name)); 9 | 10 | $name(::std::sync::Arc::new((session, ::std::sync::Mutex::new($inner { 11 | $($key : $value,)* 12 | })))) 13 | } 14 | 15 | #[allow(dead_code)] 16 | fn lock R, R>(&self, f: F) -> R { 17 | let mut inner = (self.0).1.lock().expect("Mutex poisoned"); 18 | f(&mut inner) 19 | } 20 | 21 | #[allow(dead_code)] 22 | fn session(&self) -> $crate::session::Session { 23 | (self.0).0.upgrade() 24 | } 25 | } 26 | 27 | struct $inner { 28 | $($key : $ty,)* 29 | } 30 | 31 | impl Drop for $inner { 32 | fn drop(&mut self) { 33 | debug!(target:"librespot::component", "drop {}", stringify!($name)); 34 | } 35 | } 36 | } 37 | } 38 | 39 | use std::cell::UnsafeCell; 40 | use std::sync::Mutex; 41 | 42 | pub(crate) struct Lazy(Mutex, UnsafeCell>); 43 | unsafe impl Sync for Lazy {} 44 | unsafe impl Send for Lazy {} 45 | 46 | #[cfg_attr(feature = "cargo-clippy", allow(mutex_atomic))] 47 | impl Lazy { 48 | pub(crate) fn new() -> Lazy { 49 | Lazy(Mutex::new(false), UnsafeCell::new(None)) 50 | } 51 | 52 | pub(crate) fn get T>(&self, f: F) -> &T { 53 | let mut inner = self.0.lock().unwrap(); 54 | if !*inner { 55 | unsafe { 56 | *self.1.get() = Some(f()); 57 | } 58 | *inner = true; 59 | } 60 | 61 | unsafe { &*self.1.get() }.as_ref().unwrap() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /audio/src/libvorbis_decoder.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-tremor")] 2 | extern crate tremor as vorbis; 3 | #[cfg(not(feature = "with-tremor"))] 4 | extern crate vorbis; 5 | 6 | use std::error; 7 | use std::fmt; 8 | use std::io::{Read, Seek}; 9 | 10 | pub struct VorbisDecoder(vorbis::Decoder); 11 | pub struct VorbisPacket(vorbis::Packet); 12 | pub struct VorbisError(vorbis::VorbisError); 13 | 14 | impl VorbisDecoder 15 | where 16 | R: Read + Seek, 17 | { 18 | pub fn new(input: R) -> Result, VorbisError> { 19 | Ok(VorbisDecoder(vorbis::Decoder::new(input)?)) 20 | } 21 | 22 | #[cfg(not(feature = "with-tremor"))] 23 | pub fn seek(&mut self, ms: i64) -> Result<(), VorbisError> { 24 | self.0.time_seek(ms as f64 / 1000f64)?; 25 | Ok(()) 26 | } 27 | 28 | #[cfg(feature = "with-tremor")] 29 | pub fn seek(&mut self, ms: i64) -> Result<(), VorbisError> { 30 | self.0.time_seek(ms)?; 31 | Ok(()) 32 | } 33 | 34 | pub fn next_packet(&mut self) -> Result, VorbisError> { 35 | loop { 36 | match self.0.packets().next() { 37 | Some(Ok(packet)) => return Ok(Some(VorbisPacket(packet))), 38 | None => return Ok(None), 39 | 40 | Some(Err(vorbis::VorbisError::Hole)) => (), 41 | Some(Err(err)) => return Err(err.into()), 42 | } 43 | } 44 | } 45 | } 46 | 47 | impl VorbisPacket { 48 | pub fn data(&self) -> &[i16] { 49 | &self.0.data 50 | } 51 | 52 | pub fn data_mut(&mut self) -> &mut [i16] { 53 | &mut self.0.data 54 | } 55 | } 56 | 57 | impl From for VorbisError { 58 | fn from(err: vorbis::VorbisError) -> VorbisError { 59 | VorbisError(err) 60 | } 61 | } 62 | 63 | impl fmt::Debug for VorbisError { 64 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 65 | fmt::Debug::fmt(&self.0, f) 66 | } 67 | } 68 | 69 | impl fmt::Display for VorbisError { 70 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 71 | fmt::Display::fmt(&self.0, f) 72 | } 73 | } 74 | 75 | impl error::Error for VorbisError { 76 | fn description(&self) -> &str { 77 | error::Error::description(&self.0) 78 | } 79 | 80 | fn cause(&self) -> Option<&error::Error> { 81 | error::Error::cause(&self.0) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /audio/src/lewton_decoder.rs: -------------------------------------------------------------------------------- 1 | extern crate lewton; 2 | 3 | use self::lewton::inside_ogg::OggStreamReader; 4 | 5 | use std::error; 6 | use std::fmt; 7 | use std::io::{Read, Seek}; 8 | 9 | pub struct VorbisDecoder(OggStreamReader); 10 | pub struct VorbisPacket(Vec); 11 | pub struct VorbisError(lewton::VorbisError); 12 | 13 | impl VorbisDecoder 14 | where 15 | R: Read + Seek, 16 | { 17 | pub fn new(input: R) -> Result, VorbisError> { 18 | Ok(VorbisDecoder(OggStreamReader::new(input)?)) 19 | } 20 | 21 | pub fn seek(&mut self, ms: i64) -> Result<(), VorbisError> { 22 | let absgp = ms * 44100 / 1000; 23 | self.0.seek_absgp_pg(absgp as u64)?; 24 | Ok(()) 25 | } 26 | 27 | pub fn next_packet(&mut self) -> Result, VorbisError> { 28 | use self::lewton::audio::AudioReadError::AudioIsHeader; 29 | use self::lewton::OggReadError::NoCapturePatternFound; 30 | use self::lewton::VorbisError::BadAudio; 31 | use self::lewton::VorbisError::OggError; 32 | loop { 33 | match self.0.read_dec_packet_itl() { 34 | Ok(Some(packet)) => return Ok(Some(VorbisPacket(packet))), 35 | Ok(None) => return Ok(None), 36 | 37 | Err(BadAudio(AudioIsHeader)) => (), 38 | Err(OggError(NoCapturePatternFound)) => (), 39 | Err(err) => return Err(err.into()), 40 | } 41 | } 42 | } 43 | } 44 | 45 | impl VorbisPacket { 46 | pub fn data(&self) -> &[i16] { 47 | &self.0 48 | } 49 | 50 | pub fn data_mut(&mut self) -> &mut [i16] { 51 | &mut self.0 52 | } 53 | } 54 | 55 | impl From for VorbisError { 56 | fn from(err: lewton::VorbisError) -> VorbisError { 57 | VorbisError(err) 58 | } 59 | } 60 | 61 | impl fmt::Debug for VorbisError { 62 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 63 | fmt::Debug::fmt(&self.0, f) 64 | } 65 | } 66 | 67 | impl fmt::Display for VorbisError { 68 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 69 | fmt::Display::fmt(&self.0, f) 70 | } 71 | } 72 | 73 | impl error::Error for VorbisError { 74 | fn description(&self) -> &str { 75 | error::Error::description(&self.0) 76 | } 77 | 78 | fn cause(&self) -> Option<&error::Error> { 79 | error::Error::cause(&self.0) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::str::FromStr; 3 | use url::Url; 4 | use uuid::Uuid; 5 | 6 | use version; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct SessionConfig { 10 | pub user_agent: String, 11 | pub device_id: String, 12 | pub proxy: Option, 13 | } 14 | 15 | impl Default for SessionConfig { 16 | fn default() -> SessionConfig { 17 | let device_id = Uuid::new_v4().hyphenated().to_string(); 18 | SessionConfig { 19 | user_agent: version::version_string(), 20 | device_id: device_id, 21 | proxy: None, 22 | } 23 | } 24 | } 25 | 26 | #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] 27 | pub enum DeviceType { 28 | Unknown = 0, 29 | Computer = 1, 30 | Tablet = 2, 31 | Smartphone = 3, 32 | Speaker = 4, 33 | TV = 5, 34 | AVR = 6, 35 | STB = 7, 36 | AudioDongle = 8, 37 | } 38 | 39 | impl FromStr for DeviceType { 40 | type Err = (); 41 | fn from_str(s: &str) -> Result { 42 | use self::DeviceType::*; 43 | match s.to_lowercase().as_ref() { 44 | "computer" => Ok(Computer), 45 | "tablet" => Ok(Tablet), 46 | "smartphone" => Ok(Smartphone), 47 | "speaker" => Ok(Speaker), 48 | "tv" => Ok(TV), 49 | "avr" => Ok(AVR), 50 | "stb" => Ok(STB), 51 | "audiodongle" => Ok(AudioDongle), 52 | _ => Err(()), 53 | } 54 | } 55 | } 56 | 57 | impl fmt::Display for DeviceType { 58 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 59 | use self::DeviceType::*; 60 | match *self { 61 | Unknown => f.write_str("Unknown"), 62 | Computer => f.write_str("Computer"), 63 | Tablet => f.write_str("Tablet"), 64 | Smartphone => f.write_str("Smartphone"), 65 | Speaker => f.write_str("Speaker"), 66 | TV => f.write_str("TV"), 67 | AVR => f.write_str("AVR"), 68 | STB => f.write_str("STB"), 69 | AudioDongle => f.write_str("AudioDongle"), 70 | } 71 | } 72 | } 73 | 74 | impl Default for DeviceType { 75 | fn default() -> DeviceType { 76 | DeviceType::Speaker 77 | } 78 | } 79 | 80 | #[derive(Clone, Debug)] 81 | pub struct ConnectConfig { 82 | pub name: String, 83 | pub device_type: DeviceType, 84 | pub volume: u16, 85 | pub linear_volume: bool, 86 | } 87 | -------------------------------------------------------------------------------- /core/src/mercury/types.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, WriteBytesExt}; 2 | use protobuf::Message; 3 | use std::io::Write; 4 | 5 | use protocol; 6 | 7 | #[derive(Debug, PartialEq, Eq)] 8 | pub enum MercuryMethod { 9 | GET, 10 | SUB, 11 | UNSUB, 12 | SEND, 13 | } 14 | 15 | #[derive(Debug)] 16 | pub struct MercuryRequest { 17 | pub method: MercuryMethod, 18 | pub uri: String, 19 | pub content_type: Option, 20 | pub payload: Vec>, 21 | } 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct MercuryResponse { 25 | pub uri: String, 26 | pub status_code: i32, 27 | pub payload: Vec>, 28 | } 29 | 30 | #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] 31 | pub struct MercuryError; 32 | 33 | impl ToString for MercuryMethod { 34 | fn to_string(&self) -> String { 35 | match *self { 36 | MercuryMethod::GET => "GET", 37 | MercuryMethod::SUB => "SUB", 38 | MercuryMethod::UNSUB => "UNSUB", 39 | MercuryMethod::SEND => "SEND", 40 | }.to_owned() 41 | } 42 | } 43 | 44 | impl MercuryMethod { 45 | pub fn command(&self) -> u8 { 46 | match *self { 47 | MercuryMethod::GET | MercuryMethod::SEND => 0xb2, 48 | MercuryMethod::SUB => 0xb3, 49 | MercuryMethod::UNSUB => 0xb4, 50 | } 51 | } 52 | } 53 | 54 | impl MercuryRequest { 55 | pub fn encode(&self, seq: &[u8]) -> Vec { 56 | let mut packet = Vec::new(); 57 | packet.write_u16::(seq.len() as u16).unwrap(); 58 | packet.write_all(seq).unwrap(); 59 | packet.write_u8(1).unwrap(); // Flags: FINAL 60 | packet 61 | .write_u16::(1 + self.payload.len() as u16) 62 | .unwrap(); // Part count 63 | 64 | let mut header = protocol::mercury::Header::new(); 65 | header.set_uri(self.uri.clone()); 66 | header.set_method(self.method.to_string()); 67 | 68 | if let Some(ref content_type) = self.content_type { 69 | header.set_content_type(content_type.clone()); 70 | } 71 | 72 | packet 73 | .write_u16::(header.compute_size() as u16) 74 | .unwrap(); 75 | header.write_to_writer(&mut packet).unwrap(); 76 | 77 | for p in &self.payload { 78 | packet.write_u16::(p.len() as u16).unwrap(); 79 | packet.write(p).unwrap(); 80 | } 81 | 82 | packet 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /protocol/proto/playlist4changes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "playlist4ops.proto"; 4 | import "playlist4meta.proto"; 5 | import "playlist4content.proto"; 6 | import "playlist4issues.proto"; 7 | 8 | message ChangeInfo { 9 | optional string user = 0x1; 10 | optional int32 timestamp = 0x2; 11 | optional bool admin = 0x3; 12 | optional bool undo = 0x4; 13 | optional bool redo = 0x5; 14 | optional bool merge = 0x6; 15 | optional bool compressed = 0x7; 16 | optional bool migration = 0x8; 17 | } 18 | 19 | message Delta { 20 | optional bytes base_version = 0x1; 21 | repeated Op ops = 0x2; 22 | optional ChangeInfo info = 0x4; 23 | } 24 | 25 | message Merge { 26 | optional bytes base_version = 0x1; 27 | optional bytes merge_version = 0x2; 28 | optional ChangeInfo info = 0x4; 29 | } 30 | 31 | message ChangeSet { 32 | optional Kind kind = 0x1; 33 | enum Kind { 34 | KIND_UNKNOWN = 0x0; 35 | DELTA = 0x2; 36 | MERGE = 0x3; 37 | } 38 | optional Delta delta = 0x2; 39 | optional Merge merge = 0x3; 40 | } 41 | 42 | message RevisionTaggedChangeSet { 43 | optional bytes revision = 0x1; 44 | optional ChangeSet change_set = 0x2; 45 | } 46 | 47 | message Diff { 48 | optional bytes from_revision = 0x1; 49 | repeated Op ops = 0x2; 50 | optional bytes to_revision = 0x3; 51 | } 52 | 53 | message ListDump { 54 | optional bytes latestRevision = 0x1; 55 | optional int32 length = 0x2; 56 | optional ListAttributes attributes = 0x3; 57 | optional ListChecksum checksum = 0x4; 58 | optional ListItems contents = 0x5; 59 | repeated Delta pendingDeltas = 0x7; 60 | } 61 | 62 | message ListChanges { 63 | optional bytes baseRevision = 0x1; 64 | repeated Delta deltas = 0x2; 65 | optional bool wantResultingRevisions = 0x3; 66 | optional bool wantSyncResult = 0x4; 67 | optional ListDump dump = 0x5; 68 | repeated int32 nonces = 0x6; 69 | } 70 | 71 | message SelectedListContent { 72 | optional bytes revision = 0x1; 73 | optional int32 length = 0x2; 74 | optional ListAttributes attributes = 0x3; 75 | optional ListChecksum checksum = 0x4; 76 | optional ListItems contents = 0x5; 77 | optional Diff diff = 0x6; 78 | optional Diff syncResult = 0x7; 79 | repeated bytes resultingRevisions = 0x8; 80 | optional bool multipleHeads = 0x9; 81 | optional bool upToDate = 0xa; 82 | repeated ClientResolveAction resolveAction = 0xc; 83 | repeated ClientIssue issues = 0xd; 84 | repeated int32 nonces = 0xe; 85 | } 86 | 87 | -------------------------------------------------------------------------------- /core/src/cache.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::fs::File; 3 | use std::io; 4 | use std::io::Read; 5 | use std::path::Path; 6 | use std::path::PathBuf; 7 | 8 | use authentication::Credentials; 9 | use spotify_id::FileId; 10 | use volume::Volume; 11 | 12 | #[derive(Clone)] 13 | pub struct Cache { 14 | root: PathBuf, 15 | use_audio_cache: bool, 16 | } 17 | 18 | fn mkdir_existing(path: &Path) -> io::Result<()> { 19 | fs::create_dir(path).or_else(|err| { 20 | if err.kind() == io::ErrorKind::AlreadyExists { 21 | Ok(()) 22 | } else { 23 | Err(err) 24 | } 25 | }) 26 | } 27 | 28 | impl Cache { 29 | pub fn new(location: PathBuf, use_audio_cache: bool) -> Cache { 30 | mkdir_existing(&location).unwrap(); 31 | mkdir_existing(&location.join("files")).unwrap(); 32 | 33 | Cache { 34 | root: location, 35 | use_audio_cache: use_audio_cache, 36 | } 37 | } 38 | } 39 | 40 | impl Cache { 41 | fn credentials_path(&self) -> PathBuf { 42 | self.root.join("credentials.json") 43 | } 44 | 45 | pub fn credentials(&self) -> Option { 46 | let path = self.credentials_path(); 47 | Credentials::from_file(path) 48 | } 49 | 50 | pub fn save_credentials(&self, cred: &Credentials) { 51 | let path = self.credentials_path(); 52 | cred.save_to_file(&path); 53 | } 54 | } 55 | 56 | // cache volume to root/volume 57 | impl Cache { 58 | fn volume_path(&self) -> PathBuf { 59 | self.root.join("volume") 60 | } 61 | 62 | pub fn volume(&self) -> Option { 63 | let path = self.volume_path(); 64 | Volume::from_file(path) 65 | } 66 | 67 | pub fn save_volume(&self, volume: Volume) { 68 | let path = self.volume_path(); 69 | volume.save_to_file(&path); 70 | } 71 | } 72 | 73 | impl Cache { 74 | fn file_path(&self, file: FileId) -> PathBuf { 75 | let name = file.to_base16(); 76 | self.root.join("files").join(&name[0..2]).join(&name[2..]) 77 | } 78 | 79 | pub fn file(&self, file: FileId) -> Option { 80 | File::open(self.file_path(file)).ok() 81 | } 82 | 83 | pub fn save_file(&self, file: FileId, contents: &mut Read) { 84 | if self.use_audio_cache { 85 | let path = self.file_path(file); 86 | 87 | mkdir_existing(path.parent().unwrap()).unwrap(); 88 | 89 | let mut cache_file = File::create(path).unwrap(); 90 | ::std::io::copy(contents, &mut cache_file).unwrap(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librespot" 3 | version = "0.1.0" 4 | authors = ["Paul Liétar "] 5 | license = "MIT" 6 | description = "Open Source Spotify client library" 7 | keywords = ["spotify"] 8 | repository = "https://github.com/plietar/librespot" 9 | readme = "README.md" 10 | 11 | [workspace] 12 | 13 | [lib] 14 | name = "librespot" 15 | path = "src/lib.rs" 16 | 17 | [[bin]] 18 | name = "librespot" 19 | path = "src/main.rs" 20 | doc = false 21 | 22 | [dependencies.librespot-audio] 23 | path = "audio" 24 | [dependencies.librespot-connect] 25 | path = "connect" 26 | [dependencies.librespot-core] 27 | path = "core" 28 | [dependencies.librespot-metadata] 29 | path = "metadata" 30 | [dependencies.librespot-playback] 31 | path = "playback" 32 | [dependencies.librespot-protocol] 33 | path = "protocol" 34 | 35 | [dependencies] 36 | base64 = "0.5.0" 37 | env_logger = "0.4.0" 38 | futures = "0.1.8" 39 | getopts = "0.2.14" 40 | hyper = "0.11.2" 41 | log = "0.3.5" 42 | num-bigint = "0.1.35" 43 | protobuf = "1.1" 44 | rand = "0.3.13" 45 | rpassword = "0.3.0" 46 | rust-crypto = "0.2.36" 47 | serde = "0.9.6" 48 | serde_derive = "0.9.6" 49 | serde_json = "0.9.5" 50 | tokio-core = "0.1.2" 51 | tokio-io = "0.1" 52 | tokio-signal = "0.1.2" 53 | url = "1.7.0" 54 | 55 | [build-dependencies] 56 | rand = "0.3.13" 57 | vergen = "0.1.0" 58 | 59 | [replace] 60 | "rust-crypto:0.2.36" = { git = "https://github.com/awmath/rust-crypto.git", branch = "avx2" } 61 | 62 | [features] 63 | alsa-backend = ["librespot-playback/alsa-backend"] 64 | portaudio-backend = ["librespot-playback/portaudio-backend"] 65 | pulseaudio-backend = ["librespot-playback/pulseaudio-backend"] 66 | jackaudio-backend = ["librespot-playback/jackaudio-backend"] 67 | 68 | with-tremor = ["librespot-audio/with-tremor"] 69 | with-vorbis = ["librespot-audio/with-vorbis"] 70 | 71 | with-dns-sd = ["librespot-connect/with-dns-sd"] 72 | 73 | default = ["librespot-playback/portaudio-backend"] 74 | 75 | [package.metadata.deb] 76 | maintainer = "librespot-org" 77 | copyright = "2018 Paul Liétar" 78 | license_file = ["LICENSE", "4"] 79 | depends = "$auto" 80 | extended_description = """\ 81 | librespot is an open source client library for Spotify. It enables applications \ 82 | to use Spotify's service, without using the official but closed-source \ 83 | libspotify. Additionally, it will provide extra features which are not \ 84 | available in the official library.""" 85 | section = "sound" 86 | priority = "optional" 87 | assets = [ 88 | ["target/release/librespot", "usr/bin/", "755"], 89 | ["contrib/librespot.service", "lib/systemd/system/", "644"] 90 | ] 91 | -------------------------------------------------------------------------------- /protocol/proto/appstore.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message AppInfo { 4 | optional string identifier = 0x1; 5 | optional int32 version_int = 0x2; 6 | } 7 | 8 | message AppInfoList { 9 | repeated AppInfo items = 0x1; 10 | } 11 | 12 | message SemanticVersion { 13 | optional int32 major = 0x1; 14 | optional int32 minor = 0x2; 15 | optional int32 patch = 0x3; 16 | } 17 | 18 | message RequestHeader { 19 | optional string market = 0x1; 20 | optional Platform platform = 0x2; 21 | enum Platform { 22 | WIN32_X86 = 0x0; 23 | OSX_X86 = 0x1; 24 | LINUX_X86 = 0x2; 25 | IPHONE_ARM = 0x3; 26 | SYMBIANS60_ARM = 0x4; 27 | OSX_POWERPC = 0x5; 28 | ANDROID_ARM = 0x6; 29 | WINCE_ARM = 0x7; 30 | LINUX_X86_64 = 0x8; 31 | OSX_X86_64 = 0x9; 32 | PALM_ARM = 0xa; 33 | LINUX_SH = 0xb; 34 | FREEBSD_X86 = 0xc; 35 | FREEBSD_X86_64 = 0xd; 36 | BLACKBERRY_ARM = 0xe; 37 | SONOS_UNKNOWN = 0xf; 38 | LINUX_MIPS = 0x10; 39 | LINUX_ARM = 0x11; 40 | LOGITECH_ARM = 0x12; 41 | LINUX_BLACKFIN = 0x13; 42 | ONKYO_ARM = 0x15; 43 | QNXNTO_ARM = 0x16; 44 | BADPLATFORM = 0xff; 45 | } 46 | optional AppInfoList app_infos = 0x6; 47 | optional string bridge_identifier = 0x7; 48 | optional SemanticVersion bridge_version = 0x8; 49 | optional DeviceClass device_class = 0x9; 50 | enum DeviceClass { 51 | DESKTOP = 0x1; 52 | TABLET = 0x2; 53 | MOBILE = 0x3; 54 | WEB = 0x4; 55 | TV = 0x5; 56 | } 57 | } 58 | 59 | message AppItem { 60 | optional string identifier = 0x1; 61 | optional Requirement requirement = 0x2; 62 | enum Requirement { 63 | REQUIRED_INSTALL = 0x1; 64 | LAZYLOAD = 0x2; 65 | OPTIONAL_INSTALL = 0x3; 66 | } 67 | optional string manifest = 0x4; 68 | optional string checksum = 0x5; 69 | optional string bundle_uri = 0x6; 70 | optional string small_icon_uri = 0x7; 71 | optional string large_icon_uri = 0x8; 72 | optional string medium_icon_uri = 0x9; 73 | optional Type bundle_type = 0xa; 74 | enum Type { 75 | APPLICATION = 0x0; 76 | FRAMEWORK = 0x1; 77 | BRIDGE = 0x2; 78 | } 79 | optional SemanticVersion version = 0xb; 80 | optional uint32 ttl_in_seconds = 0xc; 81 | optional IdentifierList categories = 0xd; 82 | } 83 | 84 | message AppList { 85 | repeated AppItem items = 0x1; 86 | } 87 | 88 | message IdentifierList { 89 | repeated string identifiers = 0x1; 90 | } 91 | 92 | message BannerConfig { 93 | optional string json = 0x1; 94 | } 95 | 96 | -------------------------------------------------------------------------------- /contrib/Dockerfile: -------------------------------------------------------------------------------- 1 | # Cross compilation environment for librespot 2 | # Build the docker image from the root of the project with the following command : 3 | # $ docker build -t librespot-cross -f contrib/Dockerfile . 4 | # 5 | # The resulting image can be used to build librespot for linux x86_64, armhf(with support for armv6hf), armel, mipsel, aarch64 6 | # $ docker run -v /tmp/librespot-build:/build librespot-cross 7 | # 8 | # The compiled binaries will be located in /tmp/librespot-build 9 | # 10 | # If only one architecture is desired, cargo can be invoked directly with the appropriate options : 11 | # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend" 12 | # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" 13 | # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend" 14 | # $ docker run -v /tmp/librespot-build:/build librespot-cross contrib/docker-build-pi-armv6hf.sh 15 | 16 | FROM debian:stretch 17 | 18 | RUN dpkg --add-architecture arm64 19 | RUN dpkg --add-architecture armhf 20 | RUN dpkg --add-architecture armel 21 | RUN dpkg --add-architecture mipsel 22 | RUN apt-get update 23 | 24 | RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 crossbuild-essential-armel crossbuild-essential-armhf crossbuild-essential-mipsel 25 | RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf libasound2-dev:mipsel 26 | 27 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y 28 | ENV PATH="/root/.cargo/bin/:${PATH}" 29 | RUN rustup target add aarch64-unknown-linux-gnu 30 | RUN rustup target add arm-unknown-linux-gnueabi 31 | RUN rustup target add arm-unknown-linux-gnueabihf 32 | RUN rustup target add mipsel-unknown-linux-gnu 33 | 34 | RUN mkdir /.cargo && \ 35 | echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \ 36 | echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config && \ 37 | echo '[target.arm-unknown-linux-gnueabi]\nlinker = "arm-linux-gnueabi-gcc"' >> /.cargo/config && \ 38 | echo '[target.mipsel-unknown-linux-gnu]\nlinker = "mipsel-linux-gnu-gcc"' >> /.cargo/config 39 | 40 | RUN mkdir /build && \ 41 | mkdir /pi-tools && \ 42 | curl -L https://github.com/raspberrypi/tools/archive/648a6eeb1e3c2b40af4eb34d88941ee0edeb3e9a.tar.gz | tar xz --strip-components 1 -C /pi-tools 43 | 44 | ENV CARGO_TARGET_DIR /build 45 | ENV CARGO_HOME /build/cache 46 | 47 | ADD . /src 48 | WORKDIR /src 49 | CMD ["/src/contrib/docker-build.sh"] 50 | -------------------------------------------------------------------------------- /playback/src/audio_backend/jackaudio.rs: -------------------------------------------------------------------------------- 1 | use super::{Open, Sink}; 2 | use jack::prelude::{ 3 | client_options, AsyncClient, AudioOutPort, AudioOutSpec, Client, JackControl, Port, ProcessHandler, 4 | ProcessScope, 5 | }; 6 | use std::io; 7 | use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; 8 | 9 | pub struct JackSink { 10 | send: SyncSender, 11 | active_client: AsyncClient<(), JackData>, 12 | } 13 | 14 | pub struct JackData { 15 | rec: Receiver, 16 | port_l: Port, 17 | port_r: Port, 18 | } 19 | 20 | fn pcm_to_f32(sample: i16) -> f32 { 21 | sample as f32 / 32768.0 22 | } 23 | 24 | impl ProcessHandler for JackData { 25 | fn process(&mut self, _: &Client, ps: &ProcessScope) -> JackControl { 26 | // get output port buffers 27 | let mut out_r = AudioOutPort::new(&mut self.port_r, ps); 28 | let mut out_l = AudioOutPort::new(&mut self.port_l, ps); 29 | let buf_r: &mut [f32] = &mut out_r; 30 | let buf_l: &mut [f32] = &mut out_l; 31 | // get queue iterator 32 | let mut queue_iter = self.rec.try_iter(); 33 | 34 | let buf_size = buf_r.len(); 35 | for i in 0..buf_size { 36 | buf_r[i] = pcm_to_f32(queue_iter.next().unwrap_or(0)); 37 | buf_l[i] = pcm_to_f32(queue_iter.next().unwrap_or(0)); 38 | } 39 | JackControl::Continue 40 | } 41 | } 42 | 43 | impl Open for JackSink { 44 | fn open(client_name: Option) -> JackSink { 45 | info!("Using jack sink!"); 46 | 47 | let client_name = client_name.unwrap_or("librespot".to_string()); 48 | let (client, _status) = Client::new(&client_name[..], client_options::NO_START_SERVER).unwrap(); 49 | let ch_r = client.register_port("out_0", AudioOutSpec::default()).unwrap(); 50 | let ch_l = client.register_port("out_1", AudioOutSpec::default()).unwrap(); 51 | // buffer for samples from librespot (~10ms) 52 | let (tx, rx) = sync_channel(2 * 1024 * 4); 53 | let jack_data = JackData { 54 | rec: rx, 55 | port_l: ch_l, 56 | port_r: ch_r, 57 | }; 58 | let active_client = AsyncClient::new(client, (), jack_data).unwrap(); 59 | 60 | JackSink { 61 | send: tx, 62 | active_client: active_client, 63 | } 64 | } 65 | } 66 | 67 | impl Sink for JackSink { 68 | fn start(&mut self) -> io::Result<()> { 69 | Ok(()) 70 | } 71 | 72 | fn stop(&mut self) -> io::Result<()> { 73 | Ok(()) 74 | } 75 | 76 | fn write(&mut self, data: &[i16]) -> io::Result<()> { 77 | for s in data.iter() { 78 | let res = self.send.send(*s); 79 | if res.is_err() { 80 | error!("jackaudio: cannot write to channel"); 81 | } 82 | } 83 | Ok(()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /core/src/audio_key.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; 2 | use bytes::Bytes; 3 | use futures::sync::oneshot; 4 | use futures::{Async, Future, Poll}; 5 | use std::collections::HashMap; 6 | use std::io::Write; 7 | 8 | use spotify_id::{FileId, SpotifyId}; 9 | use util::SeqGenerator; 10 | 11 | #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] 12 | pub struct AudioKey(pub [u8; 16]); 13 | 14 | #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] 15 | pub struct AudioKeyError; 16 | 17 | component! { 18 | AudioKeyManager : AudioKeyManagerInner { 19 | sequence: SeqGenerator = SeqGenerator::new(0), 20 | pending: HashMap>> = HashMap::new(), 21 | } 22 | } 23 | 24 | impl AudioKeyManager { 25 | pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { 26 | let seq = BigEndian::read_u32(data.split_to(4).as_ref()); 27 | 28 | let sender = self.lock(|inner| inner.pending.remove(&seq)); 29 | 30 | if let Some(sender) = sender { 31 | match cmd { 32 | 0xd => { 33 | let mut key = [0u8; 16]; 34 | key.copy_from_slice(data.as_ref()); 35 | let _ = sender.send(Ok(AudioKey(key))); 36 | } 37 | 0xe => { 38 | warn!("error audio key {:x} {:x}", data.as_ref()[0], data.as_ref()[1]); 39 | let _ = sender.send(Err(AudioKeyError)); 40 | } 41 | _ => (), 42 | } 43 | } 44 | } 45 | 46 | pub fn request(&self, track: SpotifyId, file: FileId) -> AudioKeyFuture { 47 | let (tx, rx) = oneshot::channel(); 48 | 49 | let seq = self.lock(move |inner| { 50 | let seq = inner.sequence.get(); 51 | inner.pending.insert(seq, tx); 52 | seq 53 | }); 54 | 55 | self.send_key_request(seq, track, file); 56 | AudioKeyFuture(rx) 57 | } 58 | 59 | fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) { 60 | let mut data: Vec = Vec::new(); 61 | data.write(&file.0).unwrap(); 62 | data.write(&track.to_raw()).unwrap(); 63 | data.write_u32::(seq).unwrap(); 64 | data.write_u16::(0x0000).unwrap(); 65 | 66 | self.session().send_packet(0xc, data) 67 | } 68 | } 69 | 70 | pub struct AudioKeyFuture(oneshot::Receiver>); 71 | impl Future for AudioKeyFuture { 72 | type Item = T; 73 | type Error = AudioKeyError; 74 | 75 | fn poll(&mut self) -> Poll { 76 | match self.0.poll() { 77 | Ok(Async::Ready(Ok(value))) => Ok(Async::Ready(value)), 78 | Ok(Async::Ready(Err(err))) => Err(err), 79 | Ok(Async::NotReady) => Ok(Async::NotReady), 80 | Err(oneshot::Canceled) => Err(AudioKeyError), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /docs/connection.md: -------------------------------------------------------------------------------- 1 | # Connection Setup 2 | ## Access point Connection 3 | The first step to connecting to Spotify's servers is finding an Access Point (AP) to do so. 4 | Clients make an HTTP GET request to `http://apresolve.spotify.com` to retrieve a list of hostname an port combination in JSON format. 5 | An AP is randomly picked from that list to connect to. 6 | 7 | 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. 8 | 9 | If `http://apresolve.spotify.com` is unresponsive, `ap.spotify.com:80` is used as a fallback. 10 | 11 | ## Connection Hello 12 | The first 3 packets exchanged are unencrypted, and have the following format : 13 | 14 | header | length | payload 15 | ---------|--------|--------- 16 | variable | 32 | variable 17 | 18 | Length is a 32 bit, big endian encoded, integer. 19 | It is the length of the entire packet, ie `len(header) + 4 + len(payload)`. 20 | 21 | The header is only present in the very first packet sent by the client, and is two bytes long, `[0, 4]`. 22 | It probably corresponds to the protocol version used. 23 | 24 | The payload is a protobuf encoded message. 25 | 26 | The client starts by sending a `ClientHello` message, describing the client info, a random nonce and client's Diffie Hellman public key. 27 | 28 | The AP replies by a `APResponseMessage` message, containing a random nonce and the server's DH key. 29 | 30 | The client solves a challenge based on these two packets, and sends it back using a `ClientResponsePlaintext`. 31 | It also computes the shared keys used to encrypt the rest of the communication. 32 | 33 | ## Login challenge and cipher key computation. 34 | The client starts by computing the DH shared secret using it's private key and the server's public key. 35 | HMAC-SHA1 is then used to compute the send and receive keys, as well as the login challenge. 36 | 37 | ``` 38 | data = [] 39 | for i in 1..6 { 40 | data += HMAC(client_hello || ap_response || [ i ], shared) 41 | } 42 | 43 | challenge = HMAC(client_hello || ap_response, data[:20]) 44 | send_key = data[20:52] 45 | recv_key = data[52:84] 46 | ``` 47 | 48 | `client_hello` and `ap_response` are the first packets sent respectively by the client and the AP. 49 | These include the header and length fields. 50 | 51 | ## Encrypted packets 52 | Every packet after ClientResponsePlaintext is encrypted using a Shannon cipher. 53 | 54 | The cipher is setup with 4 bytes big endian nonce, incremented after each packet, starting at zero. 55 | Two independent ciphers and accompanying nonces are used, one for transmission and one for reception, 56 | using respectively `send_key` and `recv_key` as keys. 57 | 58 | The packet format is as followed : 59 | 60 | cmd | length | payload | mac 61 | ----|--------|----------|---- 62 | 8 | 16 | variable | 32 63 | 64 | Each packet has a type identified by the 8 bit `cmd` field. 65 | The 16 bit big endian length only includes the length of the payload. 66 | 67 | -------------------------------------------------------------------------------- /protocol/proto/presence.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message PlaylistPublishedState { 4 | optional string uri = 0x1; 5 | optional int64 timestamp = 0x2; 6 | } 7 | 8 | message PlaylistTrackAddedState { 9 | optional string playlist_uri = 0x1; 10 | optional string track_uri = 0x2; 11 | optional int64 timestamp = 0x3; 12 | } 13 | 14 | message TrackFinishedPlayingState { 15 | optional string uri = 0x1; 16 | optional string context_uri = 0x2; 17 | optional int64 timestamp = 0x3; 18 | optional string referrer_uri = 0x4; 19 | } 20 | 21 | message FavoriteAppAddedState { 22 | optional string app_uri = 0x1; 23 | optional int64 timestamp = 0x2; 24 | } 25 | 26 | message TrackStartedPlayingState { 27 | optional string uri = 0x1; 28 | optional string context_uri = 0x2; 29 | optional int64 timestamp = 0x3; 30 | optional string referrer_uri = 0x4; 31 | } 32 | 33 | message UriSharedState { 34 | optional string uri = 0x1; 35 | optional string message = 0x2; 36 | optional int64 timestamp = 0x3; 37 | } 38 | 39 | message ArtistFollowedState { 40 | optional string uri = 0x1; 41 | optional string artist_name = 0x2; 42 | optional string artist_cover_uri = 0x3; 43 | optional int64 timestamp = 0x4; 44 | } 45 | 46 | message DeviceInformation { 47 | optional string os = 0x1; 48 | optional string type = 0x2; 49 | } 50 | 51 | message GenericPresenceState { 52 | optional int32 type = 0x1; 53 | optional int64 timestamp = 0x2; 54 | optional string item_uri = 0x3; 55 | optional string item_name = 0x4; 56 | optional string item_image = 0x5; 57 | optional string context_uri = 0x6; 58 | optional string context_name = 0x7; 59 | optional string context_image = 0x8; 60 | optional string referrer_uri = 0x9; 61 | optional string referrer_name = 0xa; 62 | optional string referrer_image = 0xb; 63 | optional string message = 0xc; 64 | optional DeviceInformation device_information = 0xd; 65 | } 66 | 67 | message State { 68 | optional int64 timestamp = 0x1; 69 | optional Type type = 0x2; 70 | enum Type { 71 | PLAYLIST_PUBLISHED = 0x1; 72 | PLAYLIST_TRACK_ADDED = 0x2; 73 | TRACK_FINISHED_PLAYING = 0x3; 74 | FAVORITE_APP_ADDED = 0x4; 75 | TRACK_STARTED_PLAYING = 0x5; 76 | URI_SHARED = 0x6; 77 | ARTIST_FOLLOWED = 0x7; 78 | GENERIC = 0xb; 79 | } 80 | optional string uri = 0x3; 81 | optional PlaylistPublishedState playlist_published = 0x4; 82 | optional PlaylistTrackAddedState playlist_track_added = 0x5; 83 | optional TrackFinishedPlayingState track_finished_playing = 0x6; 84 | optional FavoriteAppAddedState favorite_app_added = 0x7; 85 | optional TrackStartedPlayingState track_started_playing = 0x8; 86 | optional UriSharedState uri_shared = 0x9; 87 | optional ArtistFollowedState artist_followed = 0xa; 88 | optional GenericPresenceState generic = 0xb; 89 | } 90 | 91 | message StateList { 92 | repeated State states = 0x1; 93 | } 94 | 95 | -------------------------------------------------------------------------------- /core/src/connection/codec.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder}; 2 | use bytes::{BufMut, Bytes, BytesMut}; 3 | use shannon::Shannon; 4 | use std::io; 5 | use tokio_io::codec::{Decoder, Encoder}; 6 | 7 | const HEADER_SIZE: usize = 3; 8 | const MAC_SIZE: usize = 4; 9 | 10 | #[derive(Debug)] 11 | enum DecodeState { 12 | Header, 13 | Payload(u8, usize), 14 | } 15 | 16 | pub struct APCodec { 17 | encode_nonce: u32, 18 | encode_cipher: Shannon, 19 | 20 | decode_nonce: u32, 21 | decode_cipher: Shannon, 22 | decode_state: DecodeState, 23 | } 24 | 25 | impl APCodec { 26 | pub fn new(send_key: &[u8], recv_key: &[u8]) -> APCodec { 27 | APCodec { 28 | encode_nonce: 0, 29 | encode_cipher: Shannon::new(send_key), 30 | 31 | decode_nonce: 0, 32 | decode_cipher: Shannon::new(recv_key), 33 | decode_state: DecodeState::Header, 34 | } 35 | } 36 | } 37 | 38 | impl Encoder for APCodec { 39 | type Item = (u8, Vec); 40 | type Error = io::Error; 41 | 42 | fn encode(&mut self, item: (u8, Vec), buf: &mut BytesMut) -> io::Result<()> { 43 | let (cmd, payload) = item; 44 | let offset = buf.len(); 45 | 46 | buf.reserve(3 + payload.len()); 47 | buf.put_u8(cmd); 48 | buf.put_u16_be(payload.len() as u16); 49 | buf.extend_from_slice(&payload); 50 | 51 | self.encode_cipher.nonce_u32(self.encode_nonce); 52 | self.encode_nonce += 1; 53 | 54 | self.encode_cipher.encrypt(&mut buf[offset..]); 55 | 56 | let mut mac = [0u8; MAC_SIZE]; 57 | self.encode_cipher.finish(&mut mac); 58 | buf.extend_from_slice(&mac); 59 | 60 | Ok(()) 61 | } 62 | } 63 | 64 | impl Decoder for APCodec { 65 | type Item = (u8, Bytes); 66 | type Error = io::Error; 67 | 68 | fn decode(&mut self, buf: &mut BytesMut) -> io::Result> { 69 | if let DecodeState::Header = self.decode_state { 70 | if buf.len() >= HEADER_SIZE { 71 | let mut header = [0u8; HEADER_SIZE]; 72 | header.copy_from_slice(buf.split_to(HEADER_SIZE).as_ref()); 73 | 74 | self.decode_cipher.nonce_u32(self.decode_nonce); 75 | self.decode_nonce += 1; 76 | 77 | self.decode_cipher.decrypt(&mut header); 78 | 79 | let cmd = header[0]; 80 | let size = BigEndian::read_u16(&header[1..]) as usize; 81 | self.decode_state = DecodeState::Payload(cmd, size); 82 | } 83 | } 84 | 85 | if let DecodeState::Payload(cmd, size) = self.decode_state { 86 | if buf.len() >= size + MAC_SIZE { 87 | self.decode_state = DecodeState::Header; 88 | 89 | let mut payload = buf.split_to(size + MAC_SIZE); 90 | 91 | self.decode_cipher.decrypt(&mut payload.get_mut(..size).unwrap()); 92 | let mac = payload.split_off(size); 93 | self.decode_cipher.check_mac(mac.as_ref())?; 94 | 95 | return Ok(Some((cmd, payload.freeze()))); 96 | } 97 | } 98 | 99 | Ok(None) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /core/src/apresolve.rs: -------------------------------------------------------------------------------- 1 | const AP_FALLBACK: &'static str = "ap.spotify.com:443"; 2 | const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com/"; 3 | 4 | use futures::{Future, Stream}; 5 | use hyper::client::HttpConnector; 6 | use hyper::{self, Client, Method, Request, Uri}; 7 | use hyper_proxy::{Intercept, Proxy, ProxyConnector}; 8 | use serde_json; 9 | use std::str::FromStr; 10 | use tokio_core::reactor::Handle; 11 | use url::Url; 12 | 13 | error_chain!{} 14 | 15 | #[derive(Clone, Debug, Serialize, Deserialize)] 16 | pub struct APResolveData { 17 | ap_list: Vec, 18 | } 19 | 20 | fn apresolve(handle: &Handle, proxy: &Option) -> Box> { 21 | let url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL"); 22 | let use_proxy = proxy.is_some(); 23 | 24 | let mut req = Request::new(Method::Get, url.clone()); 25 | let response = match *proxy { 26 | Some(ref val) => { 27 | let proxy_url = Uri::from_str(val.as_str()).expect("invalid http proxy"); 28 | let proxy = Proxy::new(Intercept::All, proxy_url); 29 | let connector = HttpConnector::new(4, handle); 30 | let proxy_connector = ProxyConnector::from_proxy_unsecured(connector, proxy); 31 | if let Some(headers) = proxy_connector.http_headers(&url) { 32 | req.headers_mut().extend(headers.iter()); 33 | req.set_proxy(true); 34 | } 35 | let client = Client::configure().connector(proxy_connector).build(handle); 36 | client.request(req) 37 | } 38 | _ => { 39 | let client = Client::new(handle); 40 | client.request(req) 41 | } 42 | }; 43 | 44 | let body = response.and_then(|response| { 45 | response.body().fold(Vec::new(), |mut acc, chunk| { 46 | acc.extend_from_slice(chunk.as_ref()); 47 | Ok::<_, hyper::Error>(acc) 48 | }) 49 | }); 50 | let body = body.then(|result| result.chain_err(|| "HTTP error")); 51 | let body = body.and_then(|body| String::from_utf8(body).chain_err(|| "invalid UTF8 in response")); 52 | 53 | let data = 54 | body.and_then(|body| serde_json::from_str::(&body).chain_err(|| "invalid JSON")); 55 | 56 | let ap = data.and_then(move |data| { 57 | let mut aps = data.ap_list.iter().filter(|ap| { 58 | if use_proxy { 59 | // It is unlikely that the proxy will accept CONNECT on anything other than 443. 60 | Uri::from_str(ap) 61 | .ok() 62 | .map_or(false, |uri| uri.port().map_or(false, |port| port == 443)) 63 | } else { 64 | true 65 | } 66 | }); 67 | 68 | let ap = aps.next().ok_or("empty AP List")?; 69 | Ok(ap.clone()) 70 | }); 71 | 72 | Box::new(ap) 73 | } 74 | 75 | pub(crate) fn apresolve_or_fallback( 76 | handle: &Handle, 77 | proxy: &Option, 78 | ) -> Box> 79 | where 80 | E: 'static, 81 | { 82 | let ap = apresolve(handle, proxy).or_else(|e| { 83 | warn!("Failed to resolve Access Point: {}", e.description()); 84 | warn!("Using fallback \"{}\"", AP_FALLBACK); 85 | Ok(AP_FALLBACK.into()) 86 | }); 87 | 88 | Box::new(ap) 89 | } 90 | -------------------------------------------------------------------------------- /protocol/proto/playlist4ops.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "playlist4meta.proto"; 4 | import "playlist4content.proto"; 5 | 6 | message Add { 7 | optional int32 fromIndex = 0x1; 8 | repeated Item items = 0x2; 9 | optional ListChecksum list_checksum = 0x3; 10 | optional bool addLast = 0x4; 11 | optional bool addFirst = 0x5; 12 | } 13 | 14 | message Rem { 15 | optional int32 fromIndex = 0x1; 16 | optional int32 length = 0x2; 17 | repeated Item items = 0x3; 18 | optional ListChecksum list_checksum = 0x4; 19 | optional ListChecksum items_checksum = 0x5; 20 | optional ListChecksum uris_checksum = 0x6; 21 | optional bool itemsAsKey = 0x7; 22 | } 23 | 24 | message Mov { 25 | optional int32 fromIndex = 0x1; 26 | optional int32 length = 0x2; 27 | optional int32 toIndex = 0x3; 28 | optional ListChecksum list_checksum = 0x4; 29 | optional ListChecksum items_checksum = 0x5; 30 | optional ListChecksum uris_checksum = 0x6; 31 | } 32 | 33 | message ItemAttributesPartialState { 34 | optional ItemAttributes values = 0x1; 35 | repeated ItemAttributeKind no_value = 0x2; 36 | 37 | enum ItemAttributeKind { 38 | ITEM_UNKNOWN = 0x0; 39 | ITEM_ADDED_BY = 0x1; 40 | ITEM_TIMESTAMP = 0x2; 41 | ITEM_MESSAGE = 0x3; 42 | ITEM_SEEN = 0x4; 43 | ITEM_DOWNLOAD_COUNT = 0x5; 44 | ITEM_DOWNLOAD_FORMAT = 0x6; 45 | ITEM_SEVENDIGITAL_ID = 0x7; 46 | ITEM_SEVENDIGITAL_LEFT = 0x8; 47 | ITEM_SEEN_AT = 0x9; 48 | ITEM_PUBLIC = 0xa; 49 | } 50 | } 51 | 52 | message ListAttributesPartialState { 53 | optional ListAttributes values = 0x1; 54 | repeated ListAttributeKind no_value = 0x2; 55 | 56 | enum ListAttributeKind { 57 | LIST_UNKNOWN = 0x0; 58 | LIST_NAME = 0x1; 59 | LIST_DESCRIPTION = 0x2; 60 | LIST_PICTURE = 0x3; 61 | LIST_COLLABORATIVE = 0x4; 62 | LIST_PL3_VERSION = 0x5; 63 | LIST_DELETED_BY_OWNER = 0x6; 64 | LIST_RESTRICTED_COLLABORATIVE = 0x7; 65 | } 66 | } 67 | 68 | message UpdateItemAttributes { 69 | optional int32 index = 0x1; 70 | optional ItemAttributesPartialState new_attributes = 0x2; 71 | optional ItemAttributesPartialState old_attributes = 0x3; 72 | optional ListChecksum list_checksum = 0x4; 73 | optional ListChecksum old_attributes_checksum = 0x5; 74 | } 75 | 76 | message UpdateListAttributes { 77 | optional ListAttributesPartialState new_attributes = 0x1; 78 | optional ListAttributesPartialState old_attributes = 0x2; 79 | optional ListChecksum list_checksum = 0x3; 80 | optional ListChecksum old_attributes_checksum = 0x4; 81 | } 82 | 83 | message Op { 84 | optional Kind kind = 0x1; 85 | enum Kind { 86 | KIND_UNKNOWN = 0x0; 87 | ADD = 0x2; 88 | REM = 0x3; 89 | MOV = 0x4; 90 | UPDATE_ITEM_ATTRIBUTES = 0x5; 91 | UPDATE_LIST_ATTRIBUTES = 0x6; 92 | } 93 | optional Add add = 0x2; 94 | optional Rem rem = 0x3; 95 | optional Mov mov = 0x4; 96 | optional UpdateItemAttributes update_item_attributes = 0x5; 97 | optional UpdateListAttributes update_list_attributes = 0x6; 98 | } 99 | 100 | message OpList { 101 | repeated Op ops = 0x1; 102 | } 103 | 104 | -------------------------------------------------------------------------------- /docs/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | Once the connection is setup, the client can authenticate with the AP. For this, it sends an 3 | `ClientResponseEncrypted` message, using packet type `0xab`. 4 | 5 | A few different authentication methods are available. They are described below. 6 | 7 | The AP will then reply with either a `APWelcome` message using packet type `0xac` if authentication 8 | is successful, or an `APLoginFailed` with packet type `0xad` otherwise. 9 | 10 | ## Password based Authentication 11 | Password authentication is trivial. 12 | The `ClientResponseEncrypted` message's `LoginCredentials` is simply filled with the username 13 | and setting the password as the `auth_data`, and type `AUTHENTICATION_USER_PASS`. 14 | 15 | ## Zeroconf based Authentication 16 | Rather than relying on the user entering a username and password, devices can use zeroconf based 17 | authentication. This is especially useful for headless Spotify Connect devices. 18 | 19 | In this case, an already authenticated device, a phone or computer for example, discovers Spotify 20 | Connect receivers on the local network using Zeroconf. The receiver exposes an HTTP server with 21 | service type `_spotify-connect._tcp`, 22 | 23 | Two actions on the HTTP server are exposed, `getInfo` and `addUser`. 24 | The former returns information about the receiver, including its DH public key, in JSON format. 25 | The latter is used to send the username, the controller's DH public key, as well as the encrypted 26 | blob used to authenticate with Spotify's servers. 27 | 28 | The blob is decrypted using the following algorithm. 29 | 30 | ``` 31 | # encrypted_blob is the blob sent by the controller, decoded using base64 32 | # shared_secret is the result of the DH key exchange 33 | 34 | IV = encrypted_blob[:0x10] 35 | expected_mac = encrypted_blob[-0x14:] 36 | encrypted = encrypted_blob[0x10:-0x14] 37 | 38 | base_key = SHA1(shared_secret) 39 | checksum_key = HMAC-SHA1(base_key, "checksum") 40 | encryption_key = HMAC-SHA1(base_key, "encryption")[:0x10] 41 | 42 | mac = HMAC-SHA1(checksum_key, encrypted) 43 | assert mac == expected_mac 44 | 45 | blob = AES128-CTR-DECRYPT(encryption_key, IV, encrypted) 46 | ``` 47 | 48 | The blob is then used as described in the next section. 49 | 50 | ## Blob based Authentication 51 | 52 | ``` 53 | data = b64_decode(blob) 54 | base_key = PBKDF2(SHA1(deviceID), username, 0x100, 1) 55 | key = SHA1(base_key) || htonl(len(base_key)) 56 | login_data = AES192-DECRYPT(key, data) 57 | ``` 58 | 59 | ## Facebook based Authentication 60 | The client starts an HTTPS server, and makes the user visit 61 | `https://login.spotify.com/login-facebook-sso/?csrf=CSRF&port=PORT` 62 | in their browser, where CSRF is a random token, and PORT is the HTTPS server's port. 63 | 64 | This will redirect to Facebook, where the user must login and authorize Spotify, and 65 | finally make a GET request to 66 | `https://login.spotilocal.com:PORT/login/facebook_login_sso.json?csrf=CSRF&access_token=TOKEN`, 67 | where PORT and CSRF are the same as sent earlier, and TOKEN is the facebook authentication token. 68 | 69 | Since `login.spotilocal.com` resolves the 127.0.0.1, the request is received by the client. 70 | 71 | The client must then contact Facebook's API at 72 | `https://graph.facebook.com/me?fields=id&access_token=TOKEN` 73 | in order to retrieve the user's Facebook ID. 74 | 75 | The Facebook ID is the `username`, the TOKEN the `auth_data`, and `auth_type` is set to `AUTHENTICATION_FACEBOOK_TOKEN`. 76 | 77 | -------------------------------------------------------------------------------- /playback/src/audio_backend/portaudio.rs: -------------------------------------------------------------------------------- 1 | use super::{Open, Sink}; 2 | use portaudio_rs; 3 | use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; 4 | use portaudio_rs::stream::*; 5 | use std::io; 6 | use std::process::exit; 7 | use std::time::Duration; 8 | 9 | pub struct PortAudioSink<'a>( 10 | Option>, 11 | StreamParameters, 12 | ); 13 | 14 | fn output_devices() -> Box> { 15 | let count = portaudio_rs::device::get_count().unwrap(); 16 | let devices = (0..count) 17 | .filter_map(|idx| portaudio_rs::device::get_info(idx).map(|info| (idx, info))) 18 | .filter(|&(_, ref info)| info.max_output_channels > 0); 19 | 20 | Box::new(devices) 21 | } 22 | 23 | fn list_outputs() { 24 | let default = get_default_output_index(); 25 | 26 | for (idx, info) in output_devices() { 27 | if Some(idx) == default { 28 | println!("- {} (default)", info.name); 29 | } else { 30 | println!("- {}", info.name) 31 | } 32 | } 33 | } 34 | 35 | fn find_output(device: &str) -> Option { 36 | output_devices() 37 | .find(|&(_, ref info)| info.name == device) 38 | .map(|(idx, _)| idx) 39 | } 40 | 41 | impl<'a> Open for PortAudioSink<'a> { 42 | fn open(device: Option) -> PortAudioSink<'a> { 43 | debug!("Using PortAudio sink"); 44 | 45 | portaudio_rs::initialize().unwrap(); 46 | 47 | let device_idx = match device.as_ref().map(AsRef::as_ref) { 48 | Some("?") => { 49 | list_outputs(); 50 | exit(0) 51 | } 52 | Some(device) => find_output(device), 53 | None => get_default_output_index(), 54 | }.expect("Could not find device"); 55 | 56 | let info = portaudio_rs::device::get_info(device_idx); 57 | let latency = match info { 58 | Some(info) => info.default_high_output_latency, 59 | None => Duration::new(0, 0), 60 | }; 61 | 62 | let params = StreamParameters { 63 | device: device_idx, 64 | channel_count: 2, 65 | suggested_latency: latency, 66 | data: 0i16, 67 | }; 68 | 69 | PortAudioSink(None, params) 70 | } 71 | } 72 | 73 | impl<'a> Sink for PortAudioSink<'a> { 74 | fn start(&mut self) -> io::Result<()> { 75 | if self.0.is_none() { 76 | self.0 = Some( 77 | Stream::open( 78 | None, 79 | Some(self.1), 80 | 44100.0, 81 | FRAMES_PER_BUFFER_UNSPECIFIED, 82 | StreamFlags::empty(), 83 | None, 84 | ).unwrap(), 85 | );; 86 | } 87 | 88 | self.0.as_mut().unwrap().start().unwrap(); 89 | Ok(()) 90 | } 91 | fn stop(&mut self) -> io::Result<()> { 92 | self.0.as_mut().unwrap().stop().unwrap(); 93 | self.0 = None; 94 | Ok(()) 95 | } 96 | fn write(&mut self, data: &[i16]) -> io::Result<()> { 97 | match self.0.as_mut().unwrap().write(data) { 98 | Ok(_) => (), 99 | Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"), 100 | Err(e) => panic!("PA Error {}", e), 101 | }; 102 | 103 | Ok(()) 104 | } 105 | } 106 | 107 | impl<'a> Drop for PortAudioSink<'a> { 108 | fn drop(&mut self) { 109 | portaudio_rs::terminate().unwrap(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /core/src/spotify_id.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder}; 2 | use extprim::u128::u128; 3 | use std; 4 | use std::fmt; 5 | 6 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 7 | pub struct SpotifyId(u128); 8 | 9 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 10 | pub struct SpotifyIdError; 11 | 12 | const BASE62_DIGITS: &'static [u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 13 | const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef"; 14 | 15 | impl SpotifyId { 16 | pub fn from_base16(id: &str) -> Result { 17 | let data = id.as_bytes(); 18 | 19 | let mut n: u128 = u128::zero(); 20 | for c in data { 21 | let d = match BASE16_DIGITS.iter().position(|e| e == c) { 22 | None => return Err(SpotifyIdError), 23 | Some(x) => x as u64, 24 | }; 25 | n = n * u128::new(16); 26 | n = n + u128::new(d); 27 | } 28 | 29 | Ok(SpotifyId(n)) 30 | } 31 | 32 | pub fn from_base62(id: &str) -> Result { 33 | let data = id.as_bytes(); 34 | 35 | let mut n: u128 = u128::zero(); 36 | for c in data { 37 | let d = match BASE62_DIGITS.iter().position(|e| e == c) { 38 | None => return Err(SpotifyIdError), 39 | Some(x) => x as u64, 40 | }; 41 | n = n * u128::new(62); 42 | n = n + u128::new(d); 43 | } 44 | 45 | Ok(SpotifyId(n)) 46 | } 47 | 48 | pub fn from_raw(data: &[u8]) -> Result { 49 | if data.len() != 16 { 50 | return Err(SpotifyIdError); 51 | }; 52 | 53 | let high = BigEndian::read_u64(&data[0..8]); 54 | let low = BigEndian::read_u64(&data[8..16]); 55 | 56 | Ok(SpotifyId(u128::from_parts(high, low))) 57 | } 58 | 59 | pub fn to_base16(&self) -> String { 60 | let &SpotifyId(ref n) = self; 61 | 62 | let mut data = [0u8; 32]; 63 | for i in 0..32 { 64 | data[31 - i] = BASE16_DIGITS[(n.wrapping_shr(4 * i as u32).low64() & 0xF) as usize]; 65 | } 66 | 67 | std::str::from_utf8(&data).unwrap().to_owned() 68 | } 69 | 70 | pub fn to_base62(&self) -> String { 71 | let &SpotifyId(mut n) = self; 72 | 73 | let mut data = [0u8; 22]; 74 | let sixty_two = u128::new(62); 75 | for i in 0..22 { 76 | data[21 - i] = BASE62_DIGITS[(n % sixty_two).low64() as usize]; 77 | n /= sixty_two; 78 | } 79 | 80 | std::str::from_utf8(&data).unwrap().to_owned() 81 | } 82 | 83 | pub fn to_raw(&self) -> [u8; 16] { 84 | let &SpotifyId(ref n) = self; 85 | 86 | let mut data = [0u8; 16]; 87 | 88 | BigEndian::write_u64(&mut data[0..8], n.high64()); 89 | BigEndian::write_u64(&mut data[8..16], n.low64()); 90 | 91 | data 92 | } 93 | } 94 | 95 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 96 | pub struct FileId(pub [u8; 20]); 97 | 98 | impl FileId { 99 | pub fn to_base16(&self) -> String { 100 | self.0 101 | .iter() 102 | .map(|b| format!("{:02x}", b)) 103 | .collect::>() 104 | .concat() 105 | } 106 | } 107 | 108 | impl fmt::Debug for FileId { 109 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 110 | f.debug_tuple("FileId").field(&self.to_base16()).finish() 111 | } 112 | } 113 | 114 | impl fmt::Display for FileId { 115 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 116 | f.write_str(&self.to_base16()) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /core/src/connection/mod.rs: -------------------------------------------------------------------------------- 1 | mod codec; 2 | mod handshake; 3 | 4 | pub use self::codec::APCodec; 5 | pub use self::handshake::handshake; 6 | 7 | use futures::{Future, Sink, Stream}; 8 | use protobuf::{self, Message}; 9 | use std::io; 10 | use std::net::ToSocketAddrs; 11 | use tokio_core::net::TcpStream; 12 | use tokio_core::reactor::Handle; 13 | use tokio_io::codec::Framed; 14 | use url::Url; 15 | 16 | use authentication::Credentials; 17 | use version; 18 | 19 | use proxytunnel; 20 | 21 | pub type Transport = Framed; 22 | 23 | pub fn connect( 24 | addr: String, 25 | handle: &Handle, 26 | proxy: &Option, 27 | ) -> Box> { 28 | let (addr, connect_url) = match *proxy { 29 | Some(ref url) => { 30 | info!("Using proxy \"{}\"", url); 31 | (url.to_socket_addrs().unwrap().next().unwrap(), Some(addr)) 32 | } 33 | None => (addr.to_socket_addrs().unwrap().next().unwrap(), None), 34 | }; 35 | 36 | let socket = TcpStream::connect(&addr, handle); 37 | if let Some(connect_url) = connect_url { 38 | let connection = socket 39 | .and_then(move |socket| proxytunnel::connect(socket, &connect_url).and_then(handshake)); 40 | Box::new(connection) 41 | } else { 42 | let connection = socket.and_then(handshake); 43 | Box::new(connection) 44 | } 45 | } 46 | 47 | pub fn authenticate( 48 | transport: Transport, 49 | credentials: Credentials, 50 | device_id: String, 51 | ) -> Box> { 52 | use protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; 53 | use protocol::keyexchange::APLoginFailed; 54 | 55 | let mut packet = ClientResponseEncrypted::new(); 56 | packet.mut_login_credentials().set_username(credentials.username); 57 | packet.mut_login_credentials().set_typ(credentials.auth_type); 58 | packet 59 | .mut_login_credentials() 60 | .set_auth_data(credentials.auth_data); 61 | packet.mut_system_info().set_cpu_family(CpuFamily::CPU_UNKNOWN); 62 | packet.mut_system_info().set_os(Os::OS_UNKNOWN); 63 | packet.mut_system_info().set_system_information_string(format!( 64 | "librespot_{}_{}", 65 | version::short_sha(), 66 | version::build_id() 67 | )); 68 | packet.mut_system_info().set_device_id(device_id); 69 | packet.set_version_string(version::version_string()); 70 | 71 | let cmd = 0xab; 72 | let data = packet.write_to_bytes().unwrap(); 73 | 74 | Box::new( 75 | transport 76 | .send((cmd, data)) 77 | .and_then(|transport| transport.into_future().map_err(|(err, _stream)| err)) 78 | .and_then(|(packet, transport)| match packet { 79 | Some((0xac, data)) => { 80 | let welcome_data: APWelcome = protobuf::parse_from_bytes(data.as_ref()).unwrap(); 81 | 82 | let reusable_credentials = Credentials { 83 | username: welcome_data.get_canonical_username().to_owned(), 84 | auth_type: welcome_data.get_reusable_auth_credentials_type(), 85 | auth_data: welcome_data.get_reusable_auth_credentials().to_owned(), 86 | }; 87 | 88 | Ok((transport, reusable_credentials)) 89 | } 90 | 91 | Some((0xad, data)) => { 92 | let error_data: APLoginFailed = protobuf::parse_from_bytes(data.as_ref()).unwrap(); 93 | panic!( 94 | "Authentication failed with reason: {:?}", 95 | error_data.get_error_code() 96 | ) 97 | } 98 | 99 | Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd), 100 | None => panic!("EOF"), 101 | }), 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /playback/src/audio_backend/pulseaudio.rs: -------------------------------------------------------------------------------- 1 | use super::{Open, Sink}; 2 | use libc; 3 | use libpulse_sys::*; 4 | use std::ffi::CStr; 5 | use std::ffi::CString; 6 | use std::io; 7 | use std::mem; 8 | use std::ptr::{null, null_mut}; 9 | 10 | pub struct PulseAudioSink { 11 | s: *mut pa_simple, 12 | ss: pa_sample_spec, 13 | name: CString, 14 | desc: CString, 15 | } 16 | 17 | fn call_pulseaudio(f: F, fail_check: FailCheck, kind: io::ErrorKind) -> io::Result 18 | where 19 | T: Copy, 20 | F: Fn(*mut libc::c_int) -> T, 21 | FailCheck: Fn(T) -> bool, 22 | { 23 | let mut error: libc::c_int = 0; 24 | let ret = f(&mut error); 25 | if fail_check(ret) { 26 | let err_cstr = unsafe { CStr::from_ptr(pa_strerror(error)) }; 27 | let errstr = err_cstr.to_string_lossy().into_owned(); 28 | Err(io::Error::new(kind, errstr)) 29 | } else { 30 | Ok(ret) 31 | } 32 | } 33 | 34 | impl PulseAudioSink { 35 | fn free_connection(&mut self) { 36 | if self.s != null_mut() { 37 | unsafe { 38 | pa_simple_free(self.s); 39 | } 40 | self.s = null_mut(); 41 | } 42 | } 43 | } 44 | 45 | impl Drop for PulseAudioSink { 46 | fn drop(&mut self) { 47 | self.free_connection(); 48 | } 49 | } 50 | 51 | impl Open for PulseAudioSink { 52 | fn open(device: Option) -> PulseAudioSink { 53 | debug!("Using PulseAudio sink"); 54 | 55 | if device.is_some() { 56 | panic!("pulseaudio sink does not support specifying a device name"); 57 | } 58 | 59 | let ss = pa_sample_spec { 60 | format: PA_SAMPLE_S16LE, 61 | channels: 2, // stereo 62 | rate: 44100, 63 | }; 64 | 65 | let name = CString::new("librespot").unwrap(); 66 | let description = CString::new("Spotify endpoint").unwrap(); 67 | 68 | PulseAudioSink { 69 | s: null_mut(), 70 | ss: ss, 71 | name: name, 72 | desc: description, 73 | } 74 | } 75 | } 76 | 77 | impl Sink for PulseAudioSink { 78 | fn start(&mut self) -> io::Result<()> { 79 | if self.s == null_mut() { 80 | self.s = call_pulseaudio( 81 | |err| unsafe { 82 | pa_simple_new( 83 | null(), // Use the default server. 84 | self.name.as_ptr(), // Our application's name. 85 | PA_STREAM_PLAYBACK, 86 | null(), // Use the default device. 87 | self.desc.as_ptr(), // desc of our stream. 88 | &self.ss, // Our sample format. 89 | null(), // Use default channel map 90 | null(), // Use default buffering attributes. 91 | err, 92 | ) 93 | }, 94 | |ptr| ptr == null_mut(), 95 | io::ErrorKind::ConnectionRefused, 96 | )?; 97 | } 98 | Ok(()) 99 | } 100 | 101 | fn stop(&mut self) -> io::Result<()> { 102 | self.free_connection(); 103 | Ok(()) 104 | } 105 | 106 | fn write(&mut self, data: &[i16]) -> io::Result<()> { 107 | if self.s == null_mut() { 108 | Err(io::Error::new( 109 | io::ErrorKind::NotConnected, 110 | "Not connected to pulseaudio", 111 | )) 112 | } else { 113 | let ptr = data.as_ptr() as *const libc::c_void; 114 | let len = data.len() as usize * mem::size_of::(); 115 | assert!(len > 0); 116 | call_pulseaudio( 117 | |err| unsafe { pa_simple_write(self.s, ptr, len, err) }, 118 | |ret| ret < 0, 119 | io::ErrorKind::BrokenPipe, 120 | )?; 121 | Ok(()) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /protocol/proto/spirc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Frame { 4 | optional uint32 version = 0x1; 5 | optional string ident = 0x2; 6 | optional string protocol_version = 0x3; 7 | optional uint32 seq_nr = 0x4; 8 | optional MessageType typ = 0x5; 9 | optional DeviceState device_state = 0x7; 10 | optional Goodbye goodbye = 0xb; 11 | optional State state = 0xc; 12 | optional uint32 position = 0xd; 13 | optional uint32 volume = 0xe; 14 | optional int64 state_update_id = 0x11; 15 | repeated string recipient = 0x12; 16 | optional bytes context_player_state = 0x13; 17 | optional string new_name = 0x14; 18 | optional Metadata metadata = 0x19; 19 | } 20 | 21 | enum MessageType { 22 | kMessageTypeHello = 0x1; 23 | kMessageTypeGoodbye = 0x2; 24 | kMessageTypeProbe = 0x3; 25 | kMessageTypeNotify = 0xa; 26 | kMessageTypeLoad = 0x14; 27 | kMessageTypePlay = 0x15; 28 | kMessageTypePause = 0x16; 29 | kMessageTypePlayPause = 0x17; 30 | kMessageTypeSeek = 0x18; 31 | kMessageTypePrev = 0x19; 32 | kMessageTypeNext = 0x1a; 33 | kMessageTypeVolume = 0x1b; 34 | kMessageTypeShuffle = 0x1c; 35 | kMessageTypeRepeat = 0x1d; 36 | kMessageTypeVolumeDown = 0x1f; 37 | kMessageTypeVolumeUp = 0x20; 38 | kMessageTypeReplace = 0x21; 39 | kMessageTypeLogout = 0x22; 40 | kMessageTypeAction = 0x23; 41 | kMessageTypeRename = 0x24; 42 | kMessageTypeUpdateMetadata = 0x80; 43 | } 44 | 45 | message DeviceState { 46 | optional string sw_version = 0x1; 47 | optional bool is_active = 0xa; 48 | optional bool can_play = 0xb; 49 | optional uint32 volume = 0xc; 50 | optional string name = 0xd; 51 | optional uint32 error_code = 0xe; 52 | optional int64 became_active_at = 0xf; 53 | optional string error_message = 0x10; 54 | repeated Capability capabilities = 0x11; 55 | optional string context_player_error = 0x14; 56 | repeated Metadata metadata = 0x19; 57 | } 58 | 59 | message Capability { 60 | optional CapabilityType typ = 0x1; 61 | repeated int64 intValue = 0x2; 62 | repeated string stringValue = 0x3; 63 | } 64 | 65 | enum CapabilityType { 66 | kSupportedContexts = 0x1; 67 | kCanBePlayer = 0x2; 68 | kRestrictToLocal = 0x3; 69 | kDeviceType = 0x4; 70 | kGaiaEqConnectId = 0x5; 71 | kSupportsLogout = 0x6; 72 | kIsObservable = 0x7; 73 | kVolumeSteps = 0x8; 74 | kSupportedTypes = 0x9; 75 | kCommandAcks = 0xa; 76 | kSupportsRename = 0xb; 77 | kHidden = 0xc; 78 | kSupportsPlaylistV2 = 0xd; 79 | kUnknown = 0xe; 80 | } 81 | 82 | message Goodbye { 83 | optional string reason = 0x1; 84 | } 85 | 86 | message State { 87 | optional string context_uri = 0x2; 88 | optional uint32 index = 0x3; 89 | optional uint32 position_ms = 0x4; 90 | optional PlayStatus status = 0x5; 91 | optional uint64 position_measured_at = 0x7; 92 | optional string context_description = 0x8; 93 | optional bool shuffle = 0xd; 94 | optional bool repeat = 0xe; 95 | optional string last_command_ident = 0x14; 96 | optional uint32 last_command_msgid = 0x15; 97 | optional bool playing_from_fallback = 0x18; 98 | optional uint32 row = 0x19; 99 | optional uint32 playing_track_index = 0x1a; 100 | repeated TrackRef track = 0x1b; 101 | optional Ad ad = 0x1c; 102 | } 103 | 104 | enum PlayStatus { 105 | kPlayStatusStop = 0x0; 106 | kPlayStatusPlay = 0x1; 107 | kPlayStatusPause = 0x2; 108 | kPlayStatusLoading = 0x3; 109 | } 110 | 111 | message TrackRef { 112 | optional bytes gid = 0x1; 113 | optional string uri = 0x2; 114 | optional bool queued = 0x3; 115 | optional string context = 0x4; 116 | } 117 | 118 | message Ad { 119 | optional int32 next = 0x1; 120 | optional bytes ogg_fid = 0x2; 121 | optional bytes image_fid = 0x3; 122 | optional int32 duration = 0x4; 123 | optional string click_url = 0x5; 124 | optional string impression_url = 0x6; 125 | optional string product = 0x7; 126 | optional string advertiser = 0x8; 127 | optional bytes gid = 0x9; 128 | } 129 | 130 | message Metadata { 131 | optional string type = 0x1; 132 | optional string metadata = 0x2; 133 | } 134 | -------------------------------------------------------------------------------- /protocol/build.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::prelude::*; 3 | 4 | mod files; 5 | 6 | fn main() { 7 | for &(path, expected_checksum) in files::FILES { 8 | let actual = cksum_file(path).unwrap(); 9 | if expected_checksum != actual { 10 | panic!("Checksum for {:?} does not match. Try running build.sh", path); 11 | } 12 | } 13 | } 14 | 15 | fn cksum_file>(path: T) -> std::io::Result { 16 | let mut file = File::open(path)?; 17 | let mut contents = Vec::new(); 18 | file.read_to_end(&mut contents)?; 19 | 20 | Ok(cksum(&contents)) 21 | } 22 | 23 | fn cksum>(data: T) -> u32 { 24 | let data = data.as_ref(); 25 | 26 | let mut value = 0u32; 27 | for x in data { 28 | value = (value << 8) ^ CRC_LOOKUP_ARRAY[(*x as u32 ^ (value >> 24)) as usize]; 29 | } 30 | 31 | let mut n = data.len(); 32 | while n != 0 { 33 | value = (value << 8) ^ CRC_LOOKUP_ARRAY[((n & 0xFF) as u32 ^ (value >> 24)) as usize]; 34 | n >>= 8; 35 | } 36 | 37 | !value 38 | } 39 | 40 | static CRC_LOOKUP_ARRAY: &'static [u32] = &[ 41 | 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, 42 | 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 43 | 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, 44 | 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, 45 | 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 46 | 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, 47 | 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, 48 | 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 49 | 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, 50 | 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, 51 | 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 52 | 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, 53 | 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, 54 | 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 55 | 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, 56 | 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, 57 | 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, 58 | 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, 59 | 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, 60 | 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 61 | 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, 62 | 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, 63 | 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 64 | 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, 65 | 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, 66 | 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, 67 | 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, 68 | 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, 69 | 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, 70 | 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, 71 | 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, 72 | 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4, 73 | ]; 74 | -------------------------------------------------------------------------------- /core/src/proxytunnel.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::io; 3 | use std::str::FromStr; 4 | 5 | use futures::{Async, Future, Poll}; 6 | use httparse; 7 | use hyper::Uri; 8 | use tokio_io::io::{read, write_all, Read, Window, WriteAll}; 9 | use tokio_io::{AsyncRead, AsyncWrite}; 10 | 11 | pub struct ProxyTunnel { 12 | state: ProxyState, 13 | } 14 | 15 | enum ProxyState { 16 | ProxyConnect(WriteAll>), 17 | ProxyResponse(Read>>), 18 | } 19 | 20 | pub fn connect(connection: T, connect_url: &str) -> ProxyTunnel { 21 | let proxy = proxy_connect(connection, connect_url); 22 | ProxyTunnel { 23 | state: ProxyState::ProxyConnect(proxy), 24 | } 25 | } 26 | 27 | impl Future for ProxyTunnel { 28 | type Item = T; 29 | type Error = io::Error; 30 | 31 | fn poll(&mut self) -> Poll { 32 | use self::ProxyState::*; 33 | loop { 34 | self.state = match self.state { 35 | ProxyConnect(ref mut write) => { 36 | let (connection, mut accumulator) = try_ready!(write.poll()); 37 | 38 | let capacity = accumulator.capacity(); 39 | accumulator.resize(capacity, 0); 40 | let window = Window::new(accumulator); 41 | 42 | let read = read(connection, window); 43 | ProxyResponse(read) 44 | } 45 | 46 | ProxyResponse(ref mut read_f) => { 47 | let (connection, mut window, bytes_read) = try_ready!(read_f.poll()); 48 | 49 | if bytes_read == 0 { 50 | return Err(io::Error::new(io::ErrorKind::Other, "Early EOF from proxy")); 51 | } 52 | 53 | let data_end = window.start() + bytes_read; 54 | 55 | let buf = window.get_ref()[0..data_end].to_vec(); 56 | let mut headers = [httparse::EMPTY_HEADER; 16]; 57 | let mut response = httparse::Response::new(&mut headers); 58 | let status = match response.parse(&buf) { 59 | Ok(status) => status, 60 | Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err.description())), 61 | }; 62 | 63 | if status.is_complete() { 64 | if let Some(code) = response.code { 65 | if code == 200 { 66 | // Proxy says all is well 67 | return Ok(Async::Ready(connection)); 68 | } else { 69 | let reason = response.reason.unwrap_or("no reason"); 70 | let msg = format!("Proxy responded with {}: {}", code, reason); 71 | 72 | return Err(io::Error::new(io::ErrorKind::Other, msg)); 73 | } 74 | } else { 75 | return Err(io::Error::new( 76 | io::ErrorKind::Other, 77 | "Malformed response from proxy", 78 | )); 79 | } 80 | } else { 81 | if data_end >= window.end() { 82 | // Allocate some more buffer space 83 | let newsize = data_end + 100; 84 | window.get_mut().resize(newsize, 0); 85 | window.set_end(newsize); 86 | } 87 | // We did not get a full header 88 | window.set_start(data_end); 89 | let read = read(connection, window); 90 | ProxyResponse(read) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | fn proxy_connect(connection: T, connect_url: &str) -> WriteAll> { 99 | let uri = Uri::from_str(connect_url).unwrap(); 100 | let buffer = format!( 101 | "CONNECT {0}:{1} HTTP/1.1\r\n\ 102 | \r\n", 103 | uri.host().expect(&format!("No host in {}", uri)), 104 | uri.port().expect(&format!("No port in {}", uri)) 105 | ).into_bytes(); 106 | 107 | write_all(connection, buffer) 108 | } 109 | -------------------------------------------------------------------------------- /protocol/proto/metadata.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message TopTracks { 4 | optional string country = 0x1; 5 | repeated Track track = 0x2; 6 | } 7 | 8 | message ActivityPeriod { 9 | optional sint32 start_year = 0x1; 10 | optional sint32 end_year = 0x2; 11 | optional sint32 decade = 0x3; 12 | } 13 | 14 | message Artist { 15 | optional bytes gid = 0x1; 16 | optional string name = 0x2; 17 | optional sint32 popularity = 0x3; 18 | repeated TopTracks top_track = 0x4; 19 | repeated AlbumGroup album_group = 0x5; 20 | repeated AlbumGroup single_group = 0x6; 21 | repeated AlbumGroup compilation_group = 0x7; 22 | repeated AlbumGroup appears_on_group = 0x8; 23 | repeated string genre = 0x9; 24 | repeated ExternalId external_id = 0xa; 25 | repeated Image portrait = 0xb; 26 | repeated Biography biography = 0xc; 27 | repeated ActivityPeriod activity_period = 0xd; 28 | repeated Restriction restriction = 0xe; 29 | repeated Artist related = 0xf; 30 | optional bool is_portrait_album_cover = 0x10; 31 | optional ImageGroup portrait_group = 0x11; 32 | } 33 | 34 | message AlbumGroup { 35 | repeated Album album = 0x1; 36 | } 37 | 38 | message Date { 39 | optional sint32 year = 0x1; 40 | optional sint32 month = 0x2; 41 | optional sint32 day = 0x3; 42 | } 43 | 44 | message Album { 45 | optional bytes gid = 0x1; 46 | optional string name = 0x2; 47 | repeated Artist artist = 0x3; 48 | optional Type typ = 0x4; 49 | enum Type { 50 | ALBUM = 0x1; 51 | SINGLE = 0x2; 52 | COMPILATION = 0x3; 53 | EP = 0x4; 54 | } 55 | optional string label = 0x5; 56 | optional Date date = 0x6; 57 | optional sint32 popularity = 0x7; 58 | repeated string genre = 0x8; 59 | repeated Image cover = 0x9; 60 | repeated ExternalId external_id = 0xa; 61 | repeated Disc disc = 0xb; 62 | repeated string review = 0xc; 63 | repeated Copyright copyright = 0xd; 64 | repeated Restriction restriction = 0xe; 65 | repeated Album related = 0xf; 66 | repeated SalePeriod sale_period = 0x10; 67 | optional ImageGroup cover_group = 0x11; 68 | } 69 | 70 | message Track { 71 | optional bytes gid = 0x1; 72 | optional string name = 0x2; 73 | optional Album album = 0x3; 74 | repeated Artist artist = 0x4; 75 | optional sint32 number = 0x5; 76 | optional sint32 disc_number = 0x6; 77 | optional sint32 duration = 0x7; 78 | optional sint32 popularity = 0x8; 79 | optional bool explicit = 0x9; 80 | repeated ExternalId external_id = 0xa; 81 | repeated Restriction restriction = 0xb; 82 | repeated AudioFile file = 0xc; 83 | repeated Track alternative = 0xd; 84 | repeated SalePeriod sale_period = 0xe; 85 | repeated AudioFile preview = 0xf; 86 | } 87 | 88 | message Image { 89 | optional bytes file_id = 0x1; 90 | optional Size size = 0x2; 91 | enum Size { 92 | DEFAULT = 0x0; 93 | SMALL = 0x1; 94 | LARGE = 0x2; 95 | XLARGE = 0x3; 96 | } 97 | optional sint32 width = 0x3; 98 | optional sint32 height = 0x4; 99 | } 100 | 101 | message ImageGroup { 102 | repeated Image image = 0x1; 103 | } 104 | 105 | message Biography { 106 | optional string text = 0x1; 107 | repeated Image portrait = 0x2; 108 | repeated ImageGroup portrait_group = 0x3; 109 | } 110 | 111 | message Disc { 112 | optional sint32 number = 0x1; 113 | optional string name = 0x2; 114 | repeated Track track = 0x3; 115 | } 116 | 117 | message Copyright { 118 | optional Type typ = 0x1; 119 | enum Type { 120 | P = 0x0; 121 | C = 0x1; 122 | } 123 | optional string text = 0x2; 124 | } 125 | 126 | message Restriction { 127 | optional string countries_allowed = 0x2; 128 | optional string countries_forbidden = 0x3; 129 | optional Type typ = 0x4; 130 | enum Type { 131 | STREAMING = 0x0; 132 | } 133 | repeated string catalogue_str = 0x5; 134 | } 135 | 136 | message SalePeriod { 137 | repeated Restriction restriction = 0x1; 138 | optional Date start = 0x2; 139 | optional Date end = 0x3; 140 | } 141 | 142 | message ExternalId { 143 | optional string typ = 0x1; 144 | optional string id = 0x2; 145 | } 146 | 147 | message AudioFile { 148 | optional bytes file_id = 0x1; 149 | optional Format format = 0x2; 150 | enum Format { 151 | OGG_VORBIS_96 = 0x0; 152 | OGG_VORBIS_160 = 0x1; 153 | OGG_VORBIS_320 = 0x2; 154 | MP3_256 = 0x3; 155 | MP3_320 = 0x4; 156 | MP3_160 = 0x5; 157 | MP3_96 = 0x6; 158 | MP3_160_ENC = 0x7; 159 | OTHER2 = 0x8; 160 | OTHER3 = 0x9; 161 | AAC_160 = 0xa; 162 | AAC_320 = 0xb; 163 | OTHER4 = 0xc; 164 | OTHER5 = 0xd; 165 | } 166 | } 167 | 168 | -------------------------------------------------------------------------------- /protocol/proto/authentication.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ClientResponseEncrypted { 4 | required LoginCredentials login_credentials = 0xa; 5 | optional AccountCreation account_creation = 0x14; 6 | optional FingerprintResponseUnion fingerprint_response = 0x1e; 7 | optional PeerTicketUnion peer_ticket = 0x28; 8 | required SystemInfo system_info = 0x32; 9 | optional string platform_model = 0x3c; 10 | optional string version_string = 0x46; 11 | optional LibspotifyAppKey appkey = 0x50; 12 | optional ClientInfo client_info = 0x5a; 13 | } 14 | 15 | message LoginCredentials { 16 | optional string username = 0xa; 17 | required AuthenticationType typ = 0x14; 18 | optional bytes auth_data = 0x1e; 19 | } 20 | 21 | enum AuthenticationType { 22 | AUTHENTICATION_USER_PASS = 0x0; 23 | AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS = 0x1; 24 | AUTHENTICATION_STORED_FACEBOOK_CREDENTIALS = 0x2; 25 | AUTHENTICATION_SPOTIFY_TOKEN = 0x3; 26 | AUTHENTICATION_FACEBOOK_TOKEN = 0x4; 27 | } 28 | 29 | enum AccountCreation { 30 | ACCOUNT_CREATION_ALWAYS_PROMPT = 0x1; 31 | ACCOUNT_CREATION_ALWAYS_CREATE = 0x3; 32 | } 33 | 34 | message FingerprintResponseUnion { 35 | optional FingerprintGrainResponse grain = 0xa; 36 | optional FingerprintHmacRipemdResponse hmac_ripemd = 0x14; 37 | } 38 | 39 | message FingerprintGrainResponse { 40 | required bytes encrypted_key = 0xa; 41 | } 42 | 43 | message FingerprintHmacRipemdResponse { 44 | required bytes hmac = 0xa; 45 | } 46 | 47 | message PeerTicketUnion { 48 | optional PeerTicketPublicKey public_key = 0xa; 49 | optional PeerTicketOld old_ticket = 0x14; 50 | } 51 | 52 | message PeerTicketPublicKey { 53 | required bytes public_key = 0xa; 54 | } 55 | 56 | message PeerTicketOld { 57 | required bytes peer_ticket = 0xa; 58 | required bytes peer_ticket_signature = 0x14; 59 | } 60 | 61 | message SystemInfo { 62 | required CpuFamily cpu_family = 0xa; 63 | optional uint32 cpu_subtype = 0x14; 64 | optional uint32 cpu_ext = 0x1e; 65 | optional Brand brand = 0x28; 66 | optional uint32 brand_flags = 0x32; 67 | required Os os = 0x3c; 68 | optional uint32 os_version = 0x46; 69 | optional uint32 os_ext = 0x50; 70 | optional string system_information_string = 0x5a; 71 | optional string device_id = 0x64; 72 | } 73 | 74 | enum CpuFamily { 75 | CPU_UNKNOWN = 0x0; 76 | CPU_X86 = 0x1; 77 | CPU_X86_64 = 0x2; 78 | CPU_PPC = 0x3; 79 | CPU_PPC_64 = 0x4; 80 | CPU_ARM = 0x5; 81 | CPU_IA64 = 0x6; 82 | CPU_SH = 0x7; 83 | CPU_MIPS = 0x8; 84 | CPU_BLACKFIN = 0x9; 85 | } 86 | 87 | enum Brand { 88 | BRAND_UNBRANDED = 0x0; 89 | BRAND_INQ = 0x1; 90 | BRAND_HTC = 0x2; 91 | BRAND_NOKIA = 0x3; 92 | } 93 | 94 | enum Os { 95 | OS_UNKNOWN = 0x0; 96 | OS_WINDOWS = 0x1; 97 | OS_OSX = 0x2; 98 | OS_IPHONE = 0x3; 99 | OS_S60 = 0x4; 100 | OS_LINUX = 0x5; 101 | OS_WINDOWS_CE = 0x6; 102 | OS_ANDROID = 0x7; 103 | OS_PALM = 0x8; 104 | OS_FREEBSD = 0x9; 105 | OS_BLACKBERRY = 0xa; 106 | OS_SONOS = 0xb; 107 | OS_LOGITECH = 0xc; 108 | OS_WP7 = 0xd; 109 | OS_ONKYO = 0xe; 110 | OS_PHILIPS = 0xf; 111 | OS_WD = 0x10; 112 | OS_VOLVO = 0x11; 113 | OS_TIVO = 0x12; 114 | OS_AWOX = 0x13; 115 | OS_MEEGO = 0x14; 116 | OS_QNXNTO = 0x15; 117 | OS_BCO = 0x16; 118 | } 119 | 120 | message LibspotifyAppKey { 121 | required uint32 version = 0x1; 122 | required bytes devkey = 0x2; 123 | required bytes signature = 0x3; 124 | required string useragent = 0x4; 125 | required bytes callback_hash = 0x5; 126 | } 127 | 128 | message ClientInfo { 129 | optional bool limited = 0x1; 130 | optional ClientInfoFacebook fb = 0x2; 131 | optional string language = 0x3; 132 | } 133 | 134 | message ClientInfoFacebook { 135 | optional string machine_id = 0x1; 136 | } 137 | 138 | message APWelcome { 139 | required string canonical_username = 0xa; 140 | required AccountType account_type_logged_in = 0x14; 141 | required AccountType credentials_type_logged_in = 0x19; 142 | required AuthenticationType reusable_auth_credentials_type = 0x1e; 143 | required bytes reusable_auth_credentials = 0x28; 144 | optional bytes lfs_secret = 0x32; 145 | optional AccountInfo account_info = 0x3c; 146 | optional AccountInfoFacebook fb = 0x46; 147 | } 148 | 149 | enum AccountType { 150 | Spotify = 0x0; 151 | Facebook = 0x1; 152 | } 153 | 154 | message AccountInfo { 155 | optional AccountInfoSpotify spotify = 0x1; 156 | optional AccountInfoFacebook facebook = 0x2; 157 | } 158 | 159 | message AccountInfoSpotify { 160 | } 161 | 162 | message AccountInfoFacebook { 163 | optional string access_token = 0x1; 164 | optional string machine_id = 0x2; 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/librespot-org/librespot.svg?branch=master)](https://travis-ci.org/librespot-org/librespot) 2 | [![Gitter chat](https://badges.gitter.im/librespot-org/librespot.png)](https://gitter.im/librespot-org/spotify-connect-resources) 3 | 4 | # librespot 5 | *librespot* is an open source client library for Spotify. It enables 6 | applications to use Spotify's service, without using the official but 7 | closed-source libspotify. Additionally, it will provide extra features 8 | which are not available in the official library. 9 | 10 | Note: librespot only works with Spotify Premium 11 | 12 | ## This fork 13 | 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. 14 | 15 | # Documentation 16 | Documentation is currently a work in progress. 17 | 18 | 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). 19 | 20 | [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. 21 | 22 | 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. 23 | 24 | # Issues 25 | 26 | 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. 27 | 28 | # Building 29 | Rust 1.21.0 or later is required to build librespot. 30 | 31 | **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.** 32 | 33 | **We strongly suggest you install rust using rustup, for ease of installation and maintenance.** 34 | 35 | It also requires a C, with portaudio. 36 | 37 | On debian / ubuntu, the following command will install these dependencies : 38 | ```shell 39 | sudo apt-get install build-essential portaudio19-dev 40 | ``` 41 | 42 | On Fedora systems, the following command will install these dependencies : 43 | ```shell 44 | sudo dnf install portaudio-devel make gcc 45 | ``` 46 | 47 | On macOS, using homebrew : 48 | ```shell 49 | brew install portaudio 50 | ``` 51 | 52 | Once you've cloned this repository you can build *librespot* using `cargo`. 53 | ```shell 54 | cargo build --release 55 | ``` 56 | 57 | ## Usage 58 | A sample program implementing a headless Spotify Connect receiver is provided. 59 | Once you've built *librespot*, run it using : 60 | ```shell 61 | target/release/librespot --name DEVICENAME 62 | ``` 63 | 64 | The above is a minimal example. Here is a more fully fledged one: 65 | ```shell 66 | target/release/librespot -n "Librespot" -b 320 -c ./cache --enable-volume-normalisation --initial-volume 75 --device-type avr 67 | ``` 68 | The above command will create a receiver named ```Librespot```, with bitrate set to 320kbps, initial volume at 75%, with volume normalisation enabled, and the device displayed in the app as an Audio/Video Receiver. A folder named ```cache``` will be created/used in the current directory, and be used to cache audio data and credentials. 69 | 70 | A full list of runtime options are available [here](https://github.com/librespot-org/librespot/wiki/Options) 71 | 72 | ## Contact 73 | Come and hang out on gitter if you need help or want to offer some. 74 | https://gitter.im/librespot-org/spotify-connect-resources 75 | 76 | ## Disclaimer 77 | Using this code to connect to Spotify's API is probably forbidden by them. 78 | Use at your own risk. 79 | 80 | ## License 81 | Everything in this repository is licensed under the MIT license. 82 | 83 | ## Related Projects 84 | This is a non exhaustive list of projects that either use or have modified librespot. If you'd like to include yours, submit a PR. 85 | 86 | - [librespot-golang](https://github.com/librespot-org/librespot-golang) - A golang port of librespot. 87 | - [plugin.audio.spotify](https://github.com/marcelveldt/plugin.audio.spotify) - A Kodi plugin for Spotify. 88 | - [raspotify](https://github.com/dtcooper/raspotify) - Spotify Connect client for the Raspberry Pi that Just Works™ 89 | - [Spotifyd](https://github.com/Spotifyd/spotifyd) - A stripped down librespot UNIX daemon. 90 | - [Spotcontrol](https://github.com/badfortrains/spotcontrol) - A golang implementation of a Spotify Connect controller. No playback functionality. 91 | 92 | -------------------------------------------------------------------------------- /protocol/proto/facebook.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Credential { 4 | optional string facebook_uid = 0x1; 5 | optional string access_token = 0x2; 6 | } 7 | 8 | message EnableRequest { 9 | optional Credential credential = 0x1; 10 | } 11 | 12 | message EnableReply { 13 | optional Credential credential = 0x1; 14 | } 15 | 16 | message DisableRequest { 17 | optional Credential credential = 0x1; 18 | } 19 | 20 | message RevokeRequest { 21 | optional Credential credential = 0x1; 22 | } 23 | 24 | message InspectCredentialRequest { 25 | optional Credential credential = 0x1; 26 | } 27 | 28 | message InspectCredentialReply { 29 | optional Credential alternative_credential = 0x1; 30 | optional bool app_user = 0x2; 31 | optional bool permanent_error = 0x3; 32 | optional bool transient_error = 0x4; 33 | } 34 | 35 | message UserState { 36 | optional Credential credential = 0x1; 37 | } 38 | 39 | message UpdateUserStateRequest { 40 | optional Credential credential = 0x1; 41 | } 42 | 43 | message OpenGraphError { 44 | repeated string permanent = 0x1; 45 | repeated string invalid_token = 0x2; 46 | repeated string retries = 0x3; 47 | } 48 | 49 | message OpenGraphScrobble { 50 | optional int32 create_delay = 0x1; 51 | } 52 | 53 | message OpenGraphConfig { 54 | optional OpenGraphError error = 0x1; 55 | optional OpenGraphScrobble scrobble = 0x2; 56 | } 57 | 58 | message AuthConfig { 59 | optional string url = 0x1; 60 | repeated string permissions = 0x2; 61 | repeated string blacklist = 0x3; 62 | repeated string whitelist = 0x4; 63 | repeated string cancel = 0x5; 64 | } 65 | 66 | message ConfigReply { 67 | optional string domain = 0x1; 68 | optional string app_id = 0x2; 69 | optional string app_namespace = 0x3; 70 | optional AuthConfig auth = 0x4; 71 | optional OpenGraphConfig og = 0x5; 72 | } 73 | 74 | message UserFields { 75 | optional bool app_user = 0x1; 76 | optional bool display_name = 0x2; 77 | optional bool first_name = 0x3; 78 | optional bool middle_name = 0x4; 79 | optional bool last_name = 0x5; 80 | optional bool picture_large = 0x6; 81 | optional bool picture_square = 0x7; 82 | optional bool gender = 0x8; 83 | optional bool email = 0x9; 84 | } 85 | 86 | message UserOptions { 87 | optional bool cache_is_king = 0x1; 88 | } 89 | 90 | message UserRequest { 91 | optional UserOptions options = 0x1; 92 | optional UserFields fields = 0x2; 93 | } 94 | 95 | message User { 96 | optional string spotify_username = 0x1; 97 | optional string facebook_uid = 0x2; 98 | optional bool app_user = 0x3; 99 | optional string display_name = 0x4; 100 | optional string first_name = 0x5; 101 | optional string middle_name = 0x6; 102 | optional string last_name = 0x7; 103 | optional string picture_large = 0x8; 104 | optional string picture_square = 0x9; 105 | optional string gender = 0xa; 106 | optional string email = 0xb; 107 | } 108 | 109 | message FriendsFields { 110 | optional bool app_user = 0x1; 111 | optional bool display_name = 0x2; 112 | optional bool picture_large = 0x6; 113 | } 114 | 115 | message FriendsOptions { 116 | optional int32 limit = 0x1; 117 | optional int32 offset = 0x2; 118 | optional bool cache_is_king = 0x3; 119 | optional bool app_friends = 0x4; 120 | optional bool non_app_friends = 0x5; 121 | } 122 | 123 | message FriendsRequest { 124 | optional FriendsOptions options = 0x1; 125 | optional FriendsFields fields = 0x2; 126 | } 127 | 128 | message FriendsReply { 129 | repeated User friends = 0x1; 130 | optional bool more = 0x2; 131 | } 132 | 133 | message ShareRequest { 134 | optional Credential credential = 0x1; 135 | optional string uri = 0x2; 136 | optional string message_text = 0x3; 137 | } 138 | 139 | message ShareReply { 140 | optional string post_id = 0x1; 141 | } 142 | 143 | message InboxRequest { 144 | optional Credential credential = 0x1; 145 | repeated string facebook_uids = 0x3; 146 | optional string message_text = 0x4; 147 | optional string message_link = 0x5; 148 | } 149 | 150 | message InboxReply { 151 | optional string message_id = 0x1; 152 | optional string thread_id = 0x2; 153 | } 154 | 155 | message PermissionsOptions { 156 | optional bool cache_is_king = 0x1; 157 | } 158 | 159 | message PermissionsRequest { 160 | optional Credential credential = 0x1; 161 | optional PermissionsOptions options = 0x2; 162 | } 163 | 164 | message PermissionsReply { 165 | repeated string permissions = 0x1; 166 | } 167 | 168 | message GrantPermissionsRequest { 169 | optional Credential credential = 0x1; 170 | repeated string permissions = 0x2; 171 | } 172 | 173 | message GrantPermissionsReply { 174 | repeated string granted = 0x1; 175 | repeated string failed = 0x2; 176 | } 177 | 178 | message TransferRequest { 179 | optional Credential credential = 0x1; 180 | optional string source_username = 0x2; 181 | optional string target_username = 0x3; 182 | } 183 | 184 | -------------------------------------------------------------------------------- /core/src/channel.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder}; 2 | use bytes::Bytes; 3 | use futures::sync::{mpsc, BiLock}; 4 | use futures::{Async, Poll, Stream}; 5 | use std::collections::HashMap; 6 | 7 | use util::SeqGenerator; 8 | 9 | component! { 10 | ChannelManager : ChannelManagerInner { 11 | sequence: SeqGenerator = SeqGenerator::new(0), 12 | channels: HashMap> = HashMap::new(), 13 | } 14 | } 15 | 16 | #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] 17 | pub struct ChannelError; 18 | 19 | pub struct Channel { 20 | receiver: mpsc::UnboundedReceiver<(u8, Bytes)>, 21 | state: ChannelState, 22 | } 23 | 24 | pub struct ChannelHeaders(BiLock); 25 | pub struct ChannelData(BiLock); 26 | 27 | pub enum ChannelEvent { 28 | Header(u8, Vec), 29 | Data(Bytes), 30 | } 31 | 32 | #[derive(Clone)] 33 | enum ChannelState { 34 | Header(Bytes), 35 | Data, 36 | Closed, 37 | } 38 | 39 | impl ChannelManager { 40 | pub fn allocate(&self) -> (u16, Channel) { 41 | let (tx, rx) = mpsc::unbounded(); 42 | 43 | let seq = self.lock(|inner| { 44 | let seq = inner.sequence.get(); 45 | inner.channels.insert(seq, tx); 46 | seq 47 | }); 48 | 49 | let channel = Channel { 50 | receiver: rx, 51 | state: ChannelState::Header(Bytes::new()), 52 | }; 53 | 54 | (seq, channel) 55 | } 56 | 57 | pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { 58 | use std::collections::hash_map::Entry; 59 | 60 | let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref()); 61 | 62 | self.lock(|inner| { 63 | if let Entry::Occupied(entry) = inner.channels.entry(id) { 64 | let _ = entry.get().unbounded_send((cmd, data)); 65 | } 66 | }); 67 | } 68 | } 69 | 70 | impl Channel { 71 | fn recv_packet(&mut self) -> Poll { 72 | let (cmd, packet) = match self.receiver.poll() { 73 | Ok(Async::Ready(t)) => t.expect("channel closed"), 74 | Ok(Async::NotReady) => return Ok(Async::NotReady), 75 | Err(()) => unreachable!(), 76 | }; 77 | 78 | if cmd == 0xa { 79 | let code = BigEndian::read_u16(&packet.as_ref()[..2]); 80 | error!("channel error: {} {}", packet.len(), code); 81 | 82 | self.state = ChannelState::Closed; 83 | 84 | Err(ChannelError) 85 | } else { 86 | Ok(Async::Ready(packet)) 87 | } 88 | } 89 | 90 | pub fn split(self) -> (ChannelHeaders, ChannelData) { 91 | let (headers, data) = BiLock::new(self); 92 | 93 | (ChannelHeaders(headers), ChannelData(data)) 94 | } 95 | } 96 | 97 | impl Stream for Channel { 98 | type Item = ChannelEvent; 99 | type Error = ChannelError; 100 | 101 | fn poll(&mut self) -> Poll, Self::Error> { 102 | loop { 103 | match self.state.clone() { 104 | ChannelState::Closed => panic!("Polling already terminated channel"), 105 | ChannelState::Header(mut data) => { 106 | if data.len() == 0 { 107 | data = try_ready!(self.recv_packet()); 108 | } 109 | 110 | let length = BigEndian::read_u16(data.split_to(2).as_ref()) as usize; 111 | if length == 0 { 112 | assert_eq!(data.len(), 0); 113 | self.state = ChannelState::Data; 114 | } else { 115 | let header_id = data.split_to(1).as_ref()[0]; 116 | let header_data = data.split_to(length - 1).as_ref().to_owned(); 117 | 118 | self.state = ChannelState::Header(data); 119 | 120 | let event = ChannelEvent::Header(header_id, header_data); 121 | return Ok(Async::Ready(Some(event))); 122 | } 123 | } 124 | 125 | ChannelState::Data => { 126 | let data = try_ready!(self.recv_packet()); 127 | if data.len() == 0 { 128 | self.receiver.close(); 129 | self.state = ChannelState::Closed; 130 | return Ok(Async::Ready(None)); 131 | } else { 132 | let event = ChannelEvent::Data(data); 133 | return Ok(Async::Ready(Some(event))); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | impl Stream for ChannelData { 142 | type Item = Bytes; 143 | type Error = ChannelError; 144 | 145 | fn poll(&mut self) -> Poll, Self::Error> { 146 | let mut channel = match self.0.poll_lock() { 147 | Async::Ready(c) => c, 148 | Async::NotReady => return Ok(Async::NotReady), 149 | }; 150 | 151 | loop { 152 | match try_ready!(channel.poll()) { 153 | Some(ChannelEvent::Header(..)) => (), 154 | Some(ChannelEvent::Data(data)) => return Ok(Async::Ready(Some(data))), 155 | None => return Ok(Async::Ready(None)), 156 | } 157 | } 158 | } 159 | } 160 | 161 | impl Stream for ChannelHeaders { 162 | type Item = (u8, Vec); 163 | type Error = ChannelError; 164 | 165 | fn poll(&mut self) -> Poll, Self::Error> { 166 | let mut channel = match self.0.poll_lock() { 167 | Async::Ready(c) => c, 168 | Async::NotReady => return Ok(Async::NotReady), 169 | }; 170 | 171 | match try_ready!(channel.poll()) { 172 | Some(ChannelEvent::Header(id, data)) => Ok(Async::Ready(Some((id, data)))), 173 | Some(ChannelEvent::Data(..)) | None => Ok(Async::Ready(None)), 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /protocol/proto/keyexchange.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ClientHello { 4 | required BuildInfo build_info = 0xa; 5 | repeated Fingerprint fingerprints_supported = 0x14; 6 | repeated Cryptosuite cryptosuites_supported = 0x1e; 7 | repeated Powscheme powschemes_supported = 0x28; 8 | required LoginCryptoHelloUnion login_crypto_hello = 0x32; 9 | required bytes client_nonce = 0x3c; 10 | optional bytes padding = 0x46; 11 | optional FeatureSet feature_set = 0x50; 12 | } 13 | 14 | 15 | message BuildInfo { 16 | required Product product = 0xa; 17 | repeated ProductFlags product_flags = 0x14; 18 | required Platform platform = 0x1e; 19 | required uint64 version = 0x28; 20 | } 21 | 22 | enum Product { 23 | PRODUCT_CLIENT = 0x0; 24 | PRODUCT_LIBSPOTIFY= 0x1; 25 | PRODUCT_MOBILE = 0x2; 26 | PRODUCT_PARTNER = 0x3; 27 | PRODUCT_LIBSPOTIFY_EMBEDDED = 0x5; 28 | } 29 | 30 | enum ProductFlags { 31 | PRODUCT_FLAG_NONE = 0x0; 32 | PRODUCT_FLAG_DEV_BUILD = 0x1; 33 | } 34 | 35 | enum Platform { 36 | PLATFORM_WIN32_X86 = 0x0; 37 | PLATFORM_OSX_X86 = 0x1; 38 | PLATFORM_LINUX_X86 = 0x2; 39 | PLATFORM_IPHONE_ARM = 0x3; 40 | PLATFORM_S60_ARM = 0x4; 41 | PLATFORM_OSX_PPC = 0x5; 42 | PLATFORM_ANDROID_ARM = 0x6; 43 | PLATFORM_WINDOWS_CE_ARM = 0x7; 44 | PLATFORM_LINUX_X86_64 = 0x8; 45 | PLATFORM_OSX_X86_64 = 0x9; 46 | PLATFORM_PALM_ARM = 0xa; 47 | PLATFORM_LINUX_SH = 0xb; 48 | PLATFORM_FREEBSD_X86 = 0xc; 49 | PLATFORM_FREEBSD_X86_64 = 0xd; 50 | PLATFORM_BLACKBERRY_ARM = 0xe; 51 | PLATFORM_SONOS = 0xf; 52 | PLATFORM_LINUX_MIPS = 0x10; 53 | PLATFORM_LINUX_ARM = 0x11; 54 | PLATFORM_LOGITECH_ARM = 0x12; 55 | PLATFORM_LINUX_BLACKFIN = 0x13; 56 | PLATFORM_WP7_ARM = 0x14; 57 | PLATFORM_ONKYO_ARM = 0x15; 58 | PLATFORM_QNXNTO_ARM = 0x16; 59 | PLATFORM_BCO_ARM = 0x17; 60 | } 61 | 62 | enum Fingerprint { 63 | FINGERPRINT_GRAIN = 0x0; 64 | FINGERPRINT_HMAC_RIPEMD = 0x1; 65 | } 66 | 67 | enum Cryptosuite { 68 | CRYPTO_SUITE_SHANNON = 0x0; 69 | CRYPTO_SUITE_RC4_SHA1_HMAC = 0x1; 70 | } 71 | 72 | enum Powscheme { 73 | POW_HASH_CASH = 0x0; 74 | } 75 | 76 | 77 | message LoginCryptoHelloUnion { 78 | optional LoginCryptoDiffieHellmanHello diffie_hellman = 0xa; 79 | } 80 | 81 | 82 | message LoginCryptoDiffieHellmanHello { 83 | required bytes gc = 0xa; 84 | required uint32 server_keys_known = 0x14; 85 | } 86 | 87 | 88 | message FeatureSet { 89 | optional bool autoupdate2 = 0x1; 90 | optional bool current_location = 0x2; 91 | } 92 | 93 | 94 | message APResponseMessage { 95 | optional APChallenge challenge = 0xa; 96 | optional UpgradeRequiredMessage upgrade = 0x14; 97 | optional APLoginFailed login_failed = 0x1e; 98 | } 99 | 100 | message APChallenge { 101 | required LoginCryptoChallengeUnion login_crypto_challenge = 0xa; 102 | required FingerprintChallengeUnion fingerprint_challenge = 0x14; 103 | required PoWChallengeUnion pow_challenge = 0x1e; 104 | required CryptoChallengeUnion crypto_challenge = 0x28; 105 | required bytes server_nonce = 0x32; 106 | optional bytes padding = 0x3c; 107 | } 108 | 109 | message LoginCryptoChallengeUnion { 110 | optional LoginCryptoDiffieHellmanChallenge diffie_hellman = 0xa; 111 | } 112 | 113 | message LoginCryptoDiffieHellmanChallenge { 114 | required bytes gs = 0xa; 115 | required int32 server_signature_key = 0x14; 116 | required bytes gs_signature = 0x1e; 117 | } 118 | 119 | message FingerprintChallengeUnion { 120 | optional FingerprintGrainChallenge grain = 0xa; 121 | optional FingerprintHmacRipemdChallenge hmac_ripemd = 0x14; 122 | } 123 | 124 | 125 | message FingerprintGrainChallenge { 126 | required bytes kek = 0xa; 127 | } 128 | 129 | 130 | message FingerprintHmacRipemdChallenge { 131 | required bytes challenge = 0xa; 132 | } 133 | 134 | 135 | message PoWChallengeUnion { 136 | optional PoWHashCashChallenge hash_cash = 0xa; 137 | } 138 | 139 | message PoWHashCashChallenge { 140 | optional bytes prefix = 0xa; 141 | optional int32 length = 0x14; 142 | optional int32 target = 0x1e; 143 | } 144 | 145 | 146 | message CryptoChallengeUnion { 147 | optional CryptoShannonChallenge shannon = 0xa; 148 | optional CryptoRc4Sha1HmacChallenge rc4_sha1_hmac = 0x14; 149 | } 150 | 151 | 152 | message CryptoShannonChallenge { 153 | } 154 | 155 | 156 | message CryptoRc4Sha1HmacChallenge { 157 | } 158 | 159 | 160 | message UpgradeRequiredMessage { 161 | required bytes upgrade_signed_part = 0xa; 162 | required bytes signature = 0x14; 163 | optional string http_suffix = 0x1e; 164 | } 165 | 166 | message APLoginFailed { 167 | required ErrorCode error_code = 0xa; 168 | optional int32 retry_delay = 0x14; 169 | optional int32 expiry = 0x1e; 170 | optional string error_description = 0x28; 171 | } 172 | 173 | enum ErrorCode { 174 | ProtocolError = 0x0; 175 | TryAnotherAP = 0x2; 176 | BadConnectionId = 0x5; 177 | TravelRestriction = 0x9; 178 | PremiumAccountRequired = 0xb; 179 | BadCredentials = 0xc; 180 | CouldNotValidateCredentials = 0xd; 181 | AccountExists = 0xe; 182 | ExtraVerificationRequired = 0xf; 183 | InvalidAppKey = 0x10; 184 | ApplicationBanned = 0x11; 185 | } 186 | 187 | message ClientResponsePlaintext { 188 | required LoginCryptoResponseUnion login_crypto_response = 0xa; 189 | required PoWResponseUnion pow_response = 0x14; 190 | required CryptoResponseUnion crypto_response = 0x1e; 191 | } 192 | 193 | 194 | message LoginCryptoResponseUnion { 195 | optional LoginCryptoDiffieHellmanResponse diffie_hellman = 0xa; 196 | } 197 | 198 | 199 | message LoginCryptoDiffieHellmanResponse { 200 | required bytes hmac = 0xa; 201 | } 202 | 203 | 204 | message PoWResponseUnion { 205 | optional PoWHashCashResponse hash_cash = 0xa; 206 | } 207 | 208 | 209 | message PoWHashCashResponse { 210 | required bytes hash_suffix = 0xa; 211 | } 212 | 213 | 214 | message CryptoResponseUnion { 215 | optional CryptoShannonResponse shannon = 0xa; 216 | optional CryptoRc4Sha1HmacResponse rc4_sha1_hmac = 0x14; 217 | } 218 | 219 | 220 | message CryptoShannonResponse { 221 | optional int32 dummy = 0x1; 222 | } 223 | 224 | 225 | message CryptoRc4Sha1HmacResponse { 226 | optional int32 dummy = 0x1; 227 | } 228 | 229 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Setup 4 | 5 | In order to contribute to librespot, you will first need to set up a suitable rust build environment, with the necessary dependenices installed. These instructions will walk you through setting up a simple build environment. 6 | 7 | You will need to have C compiler, rust, and portaudio installed. 8 | 9 | ### Install Rust 10 | 11 | The easiest, and recommended way to get rust setu is to use [rustup](https://rustup.rs). You can install rustup with this command: 12 | 13 | ```bash 14 | curl https://sh.rustup.rs -sSf | sh 15 | ``` 16 | 17 | Follow any prompts it gives you to install rust. Once that’s done, rust is ready to use. 18 | 19 | ### Install Other Dependencies 20 | On debian / ubuntu, the following command will install these dependencies : 21 | 22 | ```bash 23 | sudo apt-get install build-essential portaudio19-dev 24 | ``` 25 | 26 | On Fedora systems, the following command will install these dependencies : 27 | 28 | ```bash 29 | sudo dnf install portaudio-devel make gcc 30 | ``` 31 | 32 | On macOS, using homebrew : 33 | 34 | ```bash 35 | brew install portaudio 36 | ``` 37 | 38 | ### Getting the Source 39 | 40 | The recommended method is to first fork the repo, so that you have a copy that you have read/write access to. After that, it’s a simple case of git cloning. 41 | 42 | ```bash 43 | git clone git@github.com:YOURUSERNAME/librespot.git 44 | ``` 45 | 46 | CD to the newly cloned repo... 47 | 48 | ```bash 49 | cd librespot 50 | ``` 51 | 52 | ### Development Extra Steps 53 | 54 | If you are looking to carry out development on librespot: 55 | 56 | ```bash 57 | rustup override set nightly 58 | ``` 59 | 60 | The command above overrides the default rust in the directory housing librespot to use the ```nightly``` version, as opposed to the ```stable``` version. 61 | 62 | Then, run the command below to install [rustfmt](https://github.com/rust-lang-nursery/rustfmt) for the ```nightly``` toolchain. This is not optional, as Travis CI is set up to check that code is compliant with rustfmt. 63 | 64 | ```bash 65 | rustup component add rustfmt-preview 66 | ``` 67 | 68 | ## Compiling & Running 69 | 70 | Once your build environment is setup, compiling the code is pretty simple. 71 | 72 | ### Compiling 73 | 74 | To build a ```debug``` build, from the project root: 75 | 76 | ```bash 77 | cargo build 78 | ``` 79 | 80 | And for ```release```: 81 | 82 | ```bash 83 | cargo build --release 84 | ``` 85 | 86 | You will most likely want to build debug builds when developing, as they are faster, and more verbose, for the purposes of debugging. 87 | 88 | There are also a number of compiler feature flags that you can add, in the event that you want to have certain additional features also compiled. The list of these is available on the [wiki](https://github.com/librespot-org/librespot/wiki/Compiling#addition-features). 89 | 90 | By default, librespot compiles with the ```portaudio-backend``` feature. To compile without default features, you can run with: 91 | 92 | ```bash 93 | cargo build --no-default-features 94 | ``` 95 | 96 | ### Running 97 | 98 | Assuming you just compiled a ```debug``` build, you can run librespot with the following command: 99 | 100 | ```bash 101 | ./target/debug/librespot -n Librespot 102 | ``` 103 | 104 | There are various runtime options, documented in the wiki, and visible by running librespot with the ```-h``` argument. 105 | 106 | ## Reporting an Issue 107 | 108 | Issues are tracked in the Github issue tracker of the librespot repo. 109 | 110 | If you have encountered a bug, please report it, as we rely on user reports to fix them. 111 | 112 | Please also make sure that your issues are helpful. To ensure that your issue is helpful, please read over this brief checklist to avoid the more common pitfalls: 113 | 114 | - Please take a moment to search/read previous similar issues to ensure you aren’t posting a duplicate. Duplicates will be closed immediately. 115 | - Please include a clear description of what the issue is. Issues with descriptions such as ‘It hangs after 40 minutes’ will be closed immediately. 116 | - Please include, where possible, steps to reproduce the bug, along with any other material that is related to the bug. For example, if librespot consistently crashes when you try to play a song, please include the Spotify URI of that song. This can be immensely helpful in quickly pinpointing and resolving issues. 117 | - Lastly, and perhaps most importantly, please include a backtrace where possible. Recent versions of librespot should produce these automatically when it crashes, and print them to the console, but in some cases, you may need to run ‘export RUST_BACKTRACE=full’ before running librespot to enable backtraces. 118 | 119 | ## Contributing Code 120 | 121 | If there is an issue that you would like to write a fix for, or a feature you would like to implement, we use the following flow for updating code in the librespot repo: 122 | 123 | ``` 124 | Fork -> Fix -> PR -> Review -> Merge 125 | ``` 126 | 127 | This is how all code is added to the repository, even by those with write access. 128 | 129 | #### Steps before Commiting 130 | 131 | In order to prepare for a PR, you will need to do a couple of things first: 132 | 133 | Make any changes that you are going to make to the code, but do not commit yet. 134 | 135 | Make sure you are using rust ```nightly``` to build librespot. Once this is confirmed, you will need to run the following command: 136 | 137 | ```bash 138 | cargo fmt --all 139 | ``` 140 | 141 | This command runs the previously installed ```rustfmt```, a code formatting tool that will automatically correct any formatting that you have used that does not conform with the librespot code style. Once that command has run, you will need to rebuild the project: 142 | 143 | ```bash 144 | cargo build 145 | ``` 146 | 147 | Once it has built, and you have confirmed there are no warnings or errors, you should commit your changes. 148 | 149 | ```bash 150 | git commit -a -m “My fancy fix” 151 | ``` 152 | 153 | **N.B.** Please, for the sake of a readable history, do not bundle multipe major changes into a single commit. Instead, break it up into multiple commits. 154 | 155 | Once you have made the commits you wish to have merged, push them to your forked repo: 156 | 157 | ```bash 158 | git push 159 | ``` 160 | 161 | Then open a pull request on the main librespot repo. 162 | 163 | Once a pull request is under way, it will be reviewed by one of the project maintainers, and either approved for merging, or have changes requested. Please be alert in the review period for possible questions about implementation decisions, implemented behaviour, and requests for changes. Once the PR is approved, it will be merged into the main repo. 164 | 165 | Happy Contributing :) 166 | -------------------------------------------------------------------------------- /core/src/authentication.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use byteorder::{BigEndian, ByteOrder}; 3 | use crypto; 4 | use crypto::aes; 5 | use crypto::digest::Digest; 6 | use crypto::hmac::Hmac; 7 | use crypto::pbkdf2::pbkdf2; 8 | use crypto::sha1::Sha1; 9 | use protobuf::ProtobufEnum; 10 | use serde; 11 | use serde_json; 12 | use std::fs::File; 13 | use std::io::{self, Read, Write}; 14 | use std::ops::FnOnce; 15 | use std::path::Path; 16 | 17 | use protocol::authentication::AuthenticationType; 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | pub struct Credentials { 21 | pub username: String, 22 | 23 | #[serde(serialize_with = "serialize_protobuf_enum")] 24 | #[serde(deserialize_with = "deserialize_protobuf_enum")] 25 | pub auth_type: AuthenticationType, 26 | 27 | #[serde(serialize_with = "serialize_base64")] 28 | #[serde(deserialize_with = "deserialize_base64")] 29 | pub auth_data: Vec, 30 | } 31 | 32 | impl Credentials { 33 | pub fn with_password(username: String, password: String) -> Credentials { 34 | Credentials { 35 | username: username, 36 | auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, 37 | auth_data: password.into_bytes(), 38 | } 39 | } 40 | 41 | pub fn with_blob(username: String, encrypted_blob: &str, device_id: &str) -> Credentials { 42 | fn read_u8(stream: &mut R) -> io::Result { 43 | let mut data = [0u8]; 44 | try!(stream.read_exact(&mut data)); 45 | Ok(data[0]) 46 | } 47 | 48 | fn read_int(stream: &mut R) -> io::Result { 49 | let lo = try!(read_u8(stream)) as u32; 50 | if lo & 0x80 == 0 { 51 | return Ok(lo); 52 | } 53 | 54 | let hi = try!(read_u8(stream)) as u32; 55 | Ok(lo & 0x7f | hi << 7) 56 | } 57 | 58 | fn read_bytes(stream: &mut R) -> io::Result> { 59 | let length = try!(read_int(stream)); 60 | let mut data = vec![0u8; length as usize]; 61 | try!(stream.read_exact(&mut data)); 62 | 63 | Ok(data) 64 | } 65 | 66 | let encrypted_blob = base64::decode(encrypted_blob).unwrap(); 67 | 68 | let secret = { 69 | let mut data = [0u8; 20]; 70 | let mut h = crypto::sha1::Sha1::new(); 71 | h.input(device_id.as_bytes()); 72 | h.result(&mut data); 73 | data 74 | }; 75 | 76 | let key = { 77 | let mut data = [0u8; 24]; 78 | let mut mac = Hmac::new(Sha1::new(), &secret); 79 | pbkdf2(&mut mac, username.as_bytes(), 0x100, &mut data[0..20]); 80 | 81 | let mut hash = Sha1::new(); 82 | hash.input(&data[0..20]); 83 | hash.result(&mut data[0..20]); 84 | BigEndian::write_u32(&mut data[20..], 20); 85 | data 86 | }; 87 | 88 | let blob = { 89 | // Anyone know what this block mode is ? 90 | let mut data = vec![0u8; encrypted_blob.len()]; 91 | let mut cipher = 92 | aes::ecb_decryptor(aes::KeySize::KeySize192, &key, crypto::blockmodes::NoPadding); 93 | cipher 94 | .decrypt( 95 | &mut crypto::buffer::RefReadBuffer::new(&encrypted_blob), 96 | &mut crypto::buffer::RefWriteBuffer::new(&mut data), 97 | true, 98 | ) 99 | .unwrap(); 100 | 101 | let l = encrypted_blob.len(); 102 | for i in 0..l - 0x10 { 103 | data[l - i - 1] ^= data[l - i - 0x11]; 104 | } 105 | 106 | data 107 | }; 108 | 109 | let mut cursor = io::Cursor::new(&blob); 110 | read_u8(&mut cursor).unwrap(); 111 | read_bytes(&mut cursor).unwrap(); 112 | read_u8(&mut cursor).unwrap(); 113 | let auth_type = read_int(&mut cursor).unwrap(); 114 | let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap(); 115 | read_u8(&mut cursor).unwrap(); 116 | let auth_data = read_bytes(&mut cursor).unwrap(); 117 | 118 | Credentials { 119 | username: username, 120 | auth_type: auth_type, 121 | auth_data: auth_data, 122 | } 123 | } 124 | 125 | fn from_reader(mut reader: R) -> Credentials { 126 | let mut contents = String::new(); 127 | reader.read_to_string(&mut contents).unwrap(); 128 | 129 | serde_json::from_str(&contents).unwrap() 130 | } 131 | 132 | pub(crate) fn from_file>(path: P) -> Option { 133 | File::open(path).ok().map(Credentials::from_reader) 134 | } 135 | 136 | fn save_to_writer(&self, writer: &mut W) { 137 | let contents = serde_json::to_string(&self.clone()).unwrap(); 138 | writer.write_all(contents.as_bytes()).unwrap(); 139 | } 140 | 141 | pub(crate) fn save_to_file>(&self, path: P) { 142 | let mut file = File::create(path).unwrap(); 143 | self.save_to_writer(&mut file) 144 | } 145 | } 146 | 147 | fn serialize_protobuf_enum(v: &T, ser: S) -> Result 148 | where 149 | T: ProtobufEnum, 150 | S: serde::Serializer, 151 | { 152 | serde::Serialize::serialize(&v.value(), ser) 153 | } 154 | 155 | fn deserialize_protobuf_enum(de: D) -> Result 156 | where 157 | T: ProtobufEnum, 158 | D: serde::Deserializer, 159 | { 160 | let v: i32 = try!(serde::Deserialize::deserialize(de)); 161 | T::from_i32(v).ok_or_else(|| serde::de::Error::custom("Invalid enum value")) 162 | } 163 | 164 | fn serialize_base64(v: &T, ser: S) -> Result 165 | where 166 | T: AsRef<[u8]>, 167 | S: serde::Serializer, 168 | { 169 | serde::Serialize::serialize(&base64::encode(v.as_ref()), ser) 170 | } 171 | 172 | fn deserialize_base64(de: D) -> Result, D::Error> 173 | where 174 | D: serde::Deserializer, 175 | { 176 | let v: String = try!(serde::Deserialize::deserialize(de)); 177 | base64::decode(&v).map_err(|e| serde::de::Error::custom(e.to_string())) 178 | } 179 | 180 | pub fn get_credentials String>( 181 | username: Option, 182 | password: Option, 183 | cached_credentials: Option, 184 | prompt: F, 185 | ) -> Option { 186 | match (username, password, cached_credentials) { 187 | (Some(username), Some(password), _) => Some(Credentials::with_password(username, password)), 188 | 189 | (Some(ref username), _, Some(ref credentials)) if *username == credentials.username => { 190 | Some(credentials.clone()) 191 | } 192 | 193 | (Some(username), None, _) => { 194 | Some(Credentials::with_password(username.clone(), prompt(&username))) 195 | } 196 | 197 | (None, _, Some(credentials)) => Some(credentials), 198 | 199 | (None, _, None) => None, 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /core/src/connection/handshake.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; 2 | use crypto::hmac::Hmac; 3 | use crypto::mac::Mac; 4 | use crypto::sha1::Sha1; 5 | use futures::{Async, Future, Poll}; 6 | use protobuf::{self, Message, MessageStatic}; 7 | use rand::thread_rng; 8 | use std::io::{self, Read}; 9 | use std::marker::PhantomData; 10 | use tokio_io::codec::Framed; 11 | use tokio_io::io::{read_exact, write_all, ReadExact, Window, WriteAll}; 12 | use tokio_io::{AsyncRead, AsyncWrite}; 13 | 14 | use super::codec::APCodec; 15 | use diffie_hellman::DHLocalKeys; 16 | use protocol; 17 | use protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext}; 18 | use util; 19 | 20 | pub struct Handshake { 21 | keys: DHLocalKeys, 22 | state: HandshakeState, 23 | } 24 | 25 | enum HandshakeState { 26 | ClientHello(WriteAll>), 27 | APResponse(RecvPacket), 28 | ClientResponse(Option, WriteAll>), 29 | } 30 | 31 | pub fn handshake(connection: T) -> Handshake { 32 | let local_keys = DHLocalKeys::random(&mut thread_rng()); 33 | let client_hello = client_hello(connection, local_keys.public_key()); 34 | 35 | Handshake { 36 | keys: local_keys, 37 | state: HandshakeState::ClientHello(client_hello), 38 | } 39 | } 40 | 41 | impl Future for Handshake { 42 | type Item = Framed; 43 | type Error = io::Error; 44 | 45 | fn poll(&mut self) -> Poll { 46 | use self::HandshakeState::*; 47 | loop { 48 | self.state = match self.state { 49 | ClientHello(ref mut write) => { 50 | let (connection, accumulator) = try_ready!(write.poll()); 51 | 52 | let read = recv_packet(connection, accumulator); 53 | APResponse(read) 54 | } 55 | 56 | APResponse(ref mut read) => { 57 | let (connection, message, accumulator) = try_ready!(read.poll()); 58 | let remote_key = message 59 | .get_challenge() 60 | .get_login_crypto_challenge() 61 | .get_diffie_hellman() 62 | .get_gs() 63 | .to_owned(); 64 | 65 | let shared_secret = self.keys.shared_secret(&remote_key); 66 | let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator); 67 | let codec = APCodec::new(&send_key, &recv_key); 68 | 69 | let write = client_response(connection, challenge); 70 | ClientResponse(Some(codec), write) 71 | } 72 | 73 | ClientResponse(ref mut codec, ref mut write) => { 74 | let (connection, _) = try_ready!(write.poll()); 75 | let codec = codec.take().unwrap(); 76 | let framed = connection.framed(codec); 77 | return Ok(Async::Ready(framed)); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | fn client_hello(connection: T, gc: Vec) -> WriteAll> { 85 | let mut packet = ClientHello::new(); 86 | packet 87 | .mut_build_info() 88 | .set_product(protocol::keyexchange::Product::PRODUCT_PARTNER); 89 | packet 90 | .mut_build_info() 91 | .set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86); 92 | packet.mut_build_info().set_version(0x10800000000); 93 | packet 94 | .mut_cryptosuites_supported() 95 | .push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON); 96 | packet.mut_login_crypto_hello().mut_diffie_hellman().set_gc(gc); 97 | packet 98 | .mut_login_crypto_hello() 99 | .mut_diffie_hellman() 100 | .set_server_keys_known(1); 101 | packet.set_client_nonce(util::rand_vec(&mut thread_rng(), 0x10)); 102 | packet.set_padding(vec![0x1e]); 103 | 104 | let mut buffer = vec![0, 4]; 105 | let size = 2 + 4 + packet.compute_size(); 106 | buffer.write_u32::(size).unwrap(); 107 | packet.write_to_vec(&mut buffer).unwrap(); 108 | 109 | write_all(connection, buffer) 110 | } 111 | 112 | fn client_response(connection: T, challenge: Vec) -> WriteAll> { 113 | let mut packet = ClientResponsePlaintext::new(); 114 | packet 115 | .mut_login_crypto_response() 116 | .mut_diffie_hellman() 117 | .set_hmac(challenge); 118 | packet.mut_pow_response(); 119 | packet.mut_crypto_response(); 120 | 121 | let mut buffer = vec![]; 122 | let size = 4 + packet.compute_size(); 123 | buffer.write_u32::(size).unwrap(); 124 | packet.write_to_vec(&mut buffer).unwrap(); 125 | 126 | write_all(connection, buffer) 127 | } 128 | 129 | enum RecvPacket { 130 | Header(ReadExact>>, PhantomData), 131 | Body(ReadExact>>, PhantomData), 132 | } 133 | 134 | fn recv_packet(connection: T, acc: Vec) -> RecvPacket 135 | where 136 | T: Read, 137 | M: MessageStatic, 138 | { 139 | RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData) 140 | } 141 | 142 | impl Future for RecvPacket 143 | where 144 | T: Read, 145 | M: MessageStatic, 146 | { 147 | type Item = (T, M, Vec); 148 | type Error = io::Error; 149 | 150 | fn poll(&mut self) -> Poll { 151 | use self::RecvPacket::*; 152 | loop { 153 | *self = match *self { 154 | Header(ref mut read, _) => { 155 | let (connection, header) = try_ready!(read.poll()); 156 | let size = BigEndian::read_u32(header.as_ref()) as usize; 157 | 158 | let acc = header.into_inner(); 159 | let read = read_into_accumulator(connection, size - 4, acc); 160 | RecvPacket::Body(read, PhantomData) 161 | } 162 | 163 | Body(ref mut read, _) => { 164 | let (connection, data) = try_ready!(read.poll()); 165 | let message = protobuf::parse_from_bytes(data.as_ref()).unwrap(); 166 | 167 | let acc = data.into_inner(); 168 | return Ok(Async::Ready((connection, message, acc))); 169 | } 170 | } 171 | } 172 | } 173 | } 174 | 175 | fn read_into_accumulator( 176 | connection: T, 177 | size: usize, 178 | mut acc: Vec, 179 | ) -> ReadExact>> { 180 | let offset = acc.len(); 181 | acc.resize(offset + size, 0); 182 | 183 | let mut window = Window::new(acc); 184 | window.set_start(offset); 185 | 186 | read_exact(connection, window) 187 | } 188 | 189 | fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec, Vec, Vec) { 190 | let mut data = Vec::with_capacity(0x64); 191 | let mut mac = Hmac::new(Sha1::new(), &shared_secret); 192 | 193 | for i in 1..6 { 194 | mac.input(packets); 195 | mac.input(&[i]); 196 | data.extend_from_slice(&mac.result().code()); 197 | mac.reset(); 198 | } 199 | 200 | mac = Hmac::new(Sha1::new(), &data[..0x14]); 201 | mac.input(packets); 202 | 203 | ( 204 | mac.result().code().to_vec(), 205 | data[0x14..0x34].to_vec(), 206 | data[0x34..0x54].to_vec(), 207 | ) 208 | } 209 | -------------------------------------------------------------------------------- /metadata/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate byteorder; 2 | extern crate futures; 3 | extern crate linear_map; 4 | extern crate protobuf; 5 | 6 | extern crate librespot_core as core; 7 | extern crate librespot_protocol as protocol; 8 | 9 | pub mod cover; 10 | 11 | use futures::Future; 12 | use linear_map::LinearMap; 13 | 14 | use core::mercury::MercuryError; 15 | use core::session::Session; 16 | use core::spotify_id::{FileId, SpotifyId}; 17 | 18 | pub use protocol::metadata::AudioFile_Format as FileFormat; 19 | 20 | fn countrylist_contains(list: &str, country: &str) -> bool { 21 | list.chunks(2).any(|cc| cc == country) 22 | } 23 | 24 | fn parse_restrictions<'s, I>(restrictions: I, country: &str, catalogue: &str) -> bool 25 | where 26 | I: IntoIterator, 27 | { 28 | let mut forbidden = "".to_string(); 29 | let mut has_forbidden = false; 30 | 31 | let mut allowed = "".to_string(); 32 | let mut has_allowed = false; 33 | 34 | let rs = restrictions 35 | .into_iter() 36 | .filter(|r| r.get_catalogue_str().contains(&catalogue.to_owned())); 37 | 38 | for r in rs { 39 | if r.has_countries_forbidden() { 40 | forbidden.push_str(r.get_countries_forbidden()); 41 | has_forbidden = true; 42 | } 43 | 44 | if r.has_countries_allowed() { 45 | allowed.push_str(r.get_countries_allowed()); 46 | has_allowed = true; 47 | } 48 | } 49 | 50 | (has_forbidden || has_allowed) 51 | && (!has_forbidden || !countrylist_contains(forbidden.as_str(), country)) 52 | && (!has_allowed || countrylist_contains(allowed.as_str(), country)) 53 | } 54 | 55 | pub trait Metadata: Send + Sized + 'static { 56 | type Message: protobuf::MessageStatic; 57 | 58 | fn base_url() -> &'static str; 59 | fn parse(msg: &Self::Message, session: &Session) -> Self; 60 | 61 | fn get(session: &Session, id: SpotifyId) -> Box> { 62 | let uri = format!("{}/{}", Self::base_url(), id.to_base16()); 63 | let request = session.mercury().get(uri); 64 | 65 | let session = session.clone(); 66 | Box::new(request.and_then(move |response| { 67 | let data = response.payload.first().expect("Empty payload"); 68 | let msg: Self::Message = protobuf::parse_from_bytes(data).unwrap(); 69 | 70 | Ok(Self::parse(&msg, &session)) 71 | })) 72 | } 73 | } 74 | 75 | #[derive(Debug, Clone)] 76 | pub struct Track { 77 | pub id: SpotifyId, 78 | pub name: String, 79 | pub duration: i32, 80 | pub album: SpotifyId, 81 | pub artists: Vec, 82 | pub files: LinearMap, 83 | pub alternatives: Vec, 84 | pub available: bool, 85 | } 86 | 87 | #[derive(Debug, Clone)] 88 | pub struct Album { 89 | pub id: SpotifyId, 90 | pub name: String, 91 | pub artists: Vec, 92 | pub tracks: Vec, 93 | pub covers: Vec, 94 | } 95 | 96 | #[derive(Debug, Clone)] 97 | pub struct Artist { 98 | pub id: SpotifyId, 99 | pub name: String, 100 | pub top_tracks: Vec, 101 | } 102 | 103 | impl Metadata for Track { 104 | type Message = protocol::metadata::Track; 105 | 106 | fn base_url() -> &'static str { 107 | "hm://metadata/3/track" 108 | } 109 | 110 | fn parse(msg: &Self::Message, session: &Session) -> Self { 111 | let country = session.country(); 112 | 113 | let artists = msg.get_artist() 114 | .iter() 115 | .filter(|artist| artist.has_gid()) 116 | .map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap()) 117 | .collect::>(); 118 | 119 | let files = msg.get_file() 120 | .iter() 121 | .filter(|file| file.has_file_id()) 122 | .map(|file| { 123 | let mut dst = [0u8; 20]; 124 | dst.clone_from_slice(file.get_file_id()); 125 | (file.get_format(), FileId(dst)) 126 | }) 127 | .collect(); 128 | 129 | Track { 130 | id: SpotifyId::from_raw(msg.get_gid()).unwrap(), 131 | name: msg.get_name().to_owned(), 132 | duration: msg.get_duration(), 133 | album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(), 134 | artists: artists, 135 | files: files, 136 | alternatives: msg.get_alternative() 137 | .iter() 138 | .map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap()) 139 | .collect(), 140 | available: parse_restrictions(msg.get_restriction(), &country, "premium"), 141 | } 142 | } 143 | } 144 | 145 | impl Metadata for Album { 146 | type Message = protocol::metadata::Album; 147 | 148 | fn base_url() -> &'static str { 149 | "hm://metadata/3/album" 150 | } 151 | 152 | fn parse(msg: &Self::Message, _: &Session) -> Self { 153 | let artists = msg.get_artist() 154 | .iter() 155 | .filter(|artist| artist.has_gid()) 156 | .map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap()) 157 | .collect::>(); 158 | 159 | let tracks = msg.get_disc() 160 | .iter() 161 | .flat_map(|disc| disc.get_track()) 162 | .filter(|track| track.has_gid()) 163 | .map(|track| SpotifyId::from_raw(track.get_gid()).unwrap()) 164 | .collect::>(); 165 | 166 | let covers = msg.get_cover_group() 167 | .get_image() 168 | .iter() 169 | .filter(|image| image.has_file_id()) 170 | .map(|image| { 171 | let mut dst = [0u8; 20]; 172 | dst.clone_from_slice(image.get_file_id()); 173 | FileId(dst) 174 | }) 175 | .collect::>(); 176 | 177 | Album { 178 | id: SpotifyId::from_raw(msg.get_gid()).unwrap(), 179 | name: msg.get_name().to_owned(), 180 | artists: artists, 181 | tracks: tracks, 182 | covers: covers, 183 | } 184 | } 185 | } 186 | 187 | impl Metadata for Artist { 188 | type Message = protocol::metadata::Artist; 189 | 190 | fn base_url() -> &'static str { 191 | "hm://metadata/3/artist" 192 | } 193 | 194 | fn parse(msg: &Self::Message, session: &Session) -> Self { 195 | let country = session.country(); 196 | 197 | let top_tracks: Vec = match msg.get_top_track() 198 | .iter() 199 | .find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country)) 200 | { 201 | Some(tracks) => tracks 202 | .get_track() 203 | .iter() 204 | .filter(|track| track.has_gid()) 205 | .map(|track| SpotifyId::from_raw(track.get_gid()).unwrap()) 206 | .collect::>(), 207 | None => Vec::new(), 208 | }; 209 | 210 | Artist { 211 | id: SpotifyId::from_raw(msg.get_gid()).unwrap(), 212 | name: msg.get_name().to_owned(), 213 | top_tracks: top_tracks, 214 | } 215 | } 216 | } 217 | 218 | struct StrChunks<'s>(&'s str, usize); 219 | 220 | trait StrChunksExt { 221 | fn chunks(&self, size: usize) -> StrChunks; 222 | } 223 | 224 | impl StrChunksExt for str { 225 | fn chunks(&self, size: usize) -> StrChunks { 226 | StrChunks(self, size) 227 | } 228 | } 229 | 230 | impl<'s> Iterator for StrChunks<'s> { 231 | type Item = &'s str; 232 | fn next(&mut self) -> Option<&'s str> { 233 | let &mut StrChunks(data, size) = self; 234 | if data.is_empty() { 235 | None 236 | } else { 237 | let ret = Some(&data[..size]); 238 | self.0 = &data[size..]; 239 | ret 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /core/src/mercury/mod.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder}; 2 | use bytes::Bytes; 3 | use futures::sync::{mpsc, oneshot}; 4 | use futures::{Async, Future, Poll}; 5 | use protobuf; 6 | use protocol; 7 | use std::collections::HashMap; 8 | use std::mem; 9 | 10 | use util::SeqGenerator; 11 | 12 | mod types; 13 | pub use self::types::*; 14 | 15 | mod sender; 16 | pub use self::sender::MercurySender; 17 | 18 | component! { 19 | MercuryManager : MercuryManagerInner { 20 | sequence: SeqGenerator = SeqGenerator::new(0), 21 | pending: HashMap, MercuryPending> = HashMap::new(), 22 | subscriptions: Vec<(String, mpsc::UnboundedSender)> = Vec::new(), 23 | } 24 | } 25 | 26 | pub struct MercuryPending { 27 | parts: Vec>, 28 | partial: Option>, 29 | callback: Option>>, 30 | } 31 | 32 | pub struct MercuryFuture(oneshot::Receiver>); 33 | impl Future for MercuryFuture { 34 | type Item = T; 35 | type Error = MercuryError; 36 | 37 | fn poll(&mut self) -> Poll { 38 | match self.0.poll() { 39 | Ok(Async::Ready(Ok(value))) => Ok(Async::Ready(value)), 40 | Ok(Async::Ready(Err(err))) => Err(err), 41 | Ok(Async::NotReady) => Ok(Async::NotReady), 42 | Err(oneshot::Canceled) => Err(MercuryError), 43 | } 44 | } 45 | } 46 | 47 | impl MercuryManager { 48 | fn next_seq(&self) -> Vec { 49 | let mut seq = vec![0u8; 8]; 50 | BigEndian::write_u64(&mut seq, self.lock(|inner| inner.sequence.get())); 51 | seq 52 | } 53 | 54 | fn request(&self, req: MercuryRequest) -> MercuryFuture { 55 | let (tx, rx) = oneshot::channel(); 56 | 57 | let pending = MercuryPending { 58 | parts: Vec::new(), 59 | partial: None, 60 | callback: Some(tx), 61 | }; 62 | 63 | let seq = self.next_seq(); 64 | self.lock(|inner| inner.pending.insert(seq.clone(), pending)); 65 | 66 | let cmd = req.method.command(); 67 | let data = req.encode(&seq); 68 | 69 | self.session().send_packet(cmd, data); 70 | MercuryFuture(rx) 71 | } 72 | 73 | pub fn get>(&self, uri: T) -> MercuryFuture { 74 | self.request(MercuryRequest { 75 | method: MercuryMethod::GET, 76 | uri: uri.into(), 77 | content_type: None, 78 | payload: Vec::new(), 79 | }) 80 | } 81 | 82 | pub fn send>(&self, uri: T, data: Vec) -> MercuryFuture { 83 | self.request(MercuryRequest { 84 | method: MercuryMethod::SEND, 85 | uri: uri.into(), 86 | content_type: None, 87 | payload: vec![data], 88 | }) 89 | } 90 | 91 | pub fn sender>(&self, uri: T) -> MercurySender { 92 | MercurySender::new(self.clone(), uri.into()) 93 | } 94 | 95 | pub fn subscribe>( 96 | &self, 97 | uri: T, 98 | ) -> Box, Error = MercuryError>> { 99 | let uri = uri.into(); 100 | let request = self.request(MercuryRequest { 101 | method: MercuryMethod::SUB, 102 | uri: uri.clone(), 103 | content_type: None, 104 | payload: Vec::new(), 105 | }); 106 | 107 | let manager = self.clone(); 108 | Box::new(request.map(move |response| { 109 | let (tx, rx) = mpsc::unbounded(); 110 | 111 | manager.lock(move |inner| { 112 | debug!("subscribed uri={} count={}", uri, response.payload.len()); 113 | if response.payload.len() > 0 { 114 | // Old subscription protocol, watch the provided list of URIs 115 | for sub in response.payload { 116 | let mut sub: protocol::pubsub::Subscription = 117 | protobuf::parse_from_bytes(&sub).unwrap(); 118 | let sub_uri = sub.take_uri(); 119 | 120 | debug!("subscribed sub_uri={}", sub_uri); 121 | 122 | inner.subscriptions.push((sub_uri, tx.clone())); 123 | } 124 | } else { 125 | // New subscription protocol, watch the requested URI 126 | inner.subscriptions.push((uri, tx)); 127 | } 128 | }); 129 | 130 | rx 131 | })) 132 | } 133 | 134 | pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { 135 | let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize; 136 | let seq = data.split_to(seq_len).as_ref().to_owned(); 137 | 138 | let flags = data.split_to(1).as_ref()[0]; 139 | let count = BigEndian::read_u16(data.split_to(2).as_ref()) as usize; 140 | 141 | let pending = self.lock(|inner| inner.pending.remove(&seq)); 142 | 143 | let mut pending = match pending { 144 | Some(pending) => pending, 145 | None if cmd == 0xb5 => MercuryPending { 146 | parts: Vec::new(), 147 | partial: None, 148 | callback: None, 149 | }, 150 | None => { 151 | warn!("Ignore seq {:?} cmd {:x}", seq, cmd); 152 | return; 153 | } 154 | }; 155 | 156 | for i in 0..count { 157 | let mut part = Self::parse_part(&mut data); 158 | if let Some(mut partial) = mem::replace(&mut pending.partial, None) { 159 | partial.extend_from_slice(&part); 160 | part = partial; 161 | } 162 | 163 | if i == count - 1 && (flags == 2) { 164 | pending.partial = Some(part) 165 | } else { 166 | pending.parts.push(part); 167 | } 168 | } 169 | 170 | if flags == 0x1 { 171 | self.complete_request(cmd, pending); 172 | } else { 173 | self.lock(move |inner| inner.pending.insert(seq, pending)); 174 | } 175 | } 176 | 177 | fn parse_part(data: &mut Bytes) -> Vec { 178 | let size = BigEndian::read_u16(data.split_to(2).as_ref()) as usize; 179 | data.split_to(size).as_ref().to_owned() 180 | } 181 | 182 | fn complete_request(&self, cmd: u8, mut pending: MercuryPending) { 183 | let header_data = pending.parts.remove(0); 184 | let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap(); 185 | 186 | let response = MercuryResponse { 187 | uri: header.get_uri().to_owned(), 188 | status_code: header.get_status_code(), 189 | payload: pending.parts, 190 | }; 191 | 192 | if response.status_code >= 500 { 193 | panic!("Spotify servers returned an error. Restart librespot."); 194 | } else if response.status_code >= 400 { 195 | warn!("error {} for uri {}", response.status_code, &response.uri); 196 | if let Some(cb) = pending.callback { 197 | let _ = cb.send(Err(MercuryError)); 198 | } 199 | } else { 200 | if cmd == 0xb5 { 201 | self.lock(|inner| { 202 | let mut found = false; 203 | inner.subscriptions.retain(|&(ref prefix, ref sub)| { 204 | if response.uri.starts_with(prefix) { 205 | found = true; 206 | 207 | // if send fails, remove from list of subs 208 | // TODO: send unsub message 209 | sub.unbounded_send(response.clone()).is_ok() 210 | } else { 211 | // URI doesn't match 212 | true 213 | } 214 | }); 215 | 216 | if !found { 217 | debug!("unknown subscription uri={}", response.uri); 218 | } 219 | }) 220 | } else if let Some(cb) = pending.callback { 221 | let _ = cb.send(Ok(response)); 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /core/src/session.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use futures::sync::mpsc; 3 | use futures::{Async, Future, IntoFuture, Poll, Stream}; 4 | use std::io; 5 | use std::sync::atomic::{AtomicUsize, Ordering, ATOMIC_USIZE_INIT}; 6 | use std::sync::{Arc, RwLock, Weak}; 7 | use tokio_core::reactor::{Handle, Remote}; 8 | 9 | use apresolve::apresolve_or_fallback; 10 | use authentication::Credentials; 11 | use cache::Cache; 12 | use component::Lazy; 13 | use config::SessionConfig; 14 | use connection; 15 | 16 | use audio_key::AudioKeyManager; 17 | use channel::ChannelManager; 18 | use mercury::MercuryManager; 19 | 20 | struct SessionData { 21 | country: String, 22 | canonical_username: String, 23 | invalid: bool, 24 | } 25 | 26 | struct SessionInternal { 27 | config: SessionConfig, 28 | data: RwLock, 29 | 30 | tx_connection: mpsc::UnboundedSender<(u8, Vec)>, 31 | 32 | audio_key: Lazy, 33 | channel: Lazy, 34 | mercury: Lazy, 35 | cache: Option>, 36 | 37 | handle: Remote, 38 | 39 | session_id: usize, 40 | } 41 | 42 | static SESSION_COUNTER: AtomicUsize = ATOMIC_USIZE_INIT; 43 | 44 | #[derive(Clone)] 45 | pub struct Session(Arc); 46 | 47 | impl Session { 48 | pub fn connect( 49 | config: SessionConfig, 50 | credentials: Credentials, 51 | cache: Option, 52 | handle: Handle, 53 | ) -> Box> { 54 | let access_point = apresolve_or_fallback::(&handle, &config.proxy); 55 | 56 | let handle_ = handle.clone(); 57 | let proxy = config.proxy.clone(); 58 | let connection = access_point.and_then(move |addr| { 59 | info!("Connecting to AP \"{}\"", addr); 60 | connection::connect(addr, &handle_, &proxy) 61 | }); 62 | 63 | let device_id = config.device_id.clone(); 64 | let authentication = connection 65 | .and_then(move |connection| connection::authenticate(connection, credentials, device_id)); 66 | 67 | let result = authentication.map(move |(transport, reusable_credentials)| { 68 | info!("Authenticated as \"{}\" !", reusable_credentials.username); 69 | if let Some(ref cache) = cache { 70 | cache.save_credentials(&reusable_credentials); 71 | } 72 | 73 | let (session, task) = Session::create( 74 | &handle, 75 | transport, 76 | config, 77 | cache, 78 | reusable_credentials.username.clone(), 79 | ); 80 | 81 | handle.spawn(task.map_err(|e| { 82 | error!("{:?}", e); 83 | })); 84 | 85 | session 86 | }); 87 | 88 | Box::new(result) 89 | } 90 | 91 | fn create( 92 | handle: &Handle, 93 | transport: connection::Transport, 94 | config: SessionConfig, 95 | cache: Option, 96 | username: String, 97 | ) -> (Session, Box>) { 98 | let (sink, stream) = transport.split(); 99 | 100 | let (sender_tx, sender_rx) = mpsc::unbounded(); 101 | let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); 102 | 103 | debug!("new Session[{}]", session_id); 104 | 105 | let session = Session(Arc::new(SessionInternal { 106 | config: config, 107 | data: RwLock::new(SessionData { 108 | country: String::new(), 109 | canonical_username: username, 110 | invalid: false, 111 | }), 112 | 113 | tx_connection: sender_tx, 114 | 115 | cache: cache.map(Arc::new), 116 | 117 | audio_key: Lazy::new(), 118 | channel: Lazy::new(), 119 | mercury: Lazy::new(), 120 | 121 | handle: handle.remote().clone(), 122 | 123 | session_id: session_id, 124 | })); 125 | 126 | let sender_task = sender_rx 127 | .map_err(|e| -> io::Error { panic!(e) }) 128 | .forward(sink) 129 | .map(|_| ()); 130 | let receiver_task = DispatchTask(stream, session.weak()); 131 | 132 | let task = Box::new((receiver_task, sender_task).into_future().map(|((), ())| ())); 133 | 134 | (session, task) 135 | } 136 | 137 | pub fn audio_key(&self) -> &AudioKeyManager { 138 | self.0.audio_key.get(|| AudioKeyManager::new(self.weak())) 139 | } 140 | 141 | pub fn channel(&self) -> &ChannelManager { 142 | self.0.channel.get(|| ChannelManager::new(self.weak())) 143 | } 144 | 145 | pub fn mercury(&self) -> &MercuryManager { 146 | self.0.mercury.get(|| MercuryManager::new(self.weak())) 147 | } 148 | 149 | pub fn spawn(&self, f: F) 150 | where 151 | F: FnOnce(&Handle) -> R + Send + 'static, 152 | R: IntoFuture, 153 | R::Future: 'static, 154 | { 155 | self.0.handle.spawn(f) 156 | } 157 | 158 | fn debug_info(&self) { 159 | debug!( 160 | "Session[{}] strong={} weak={}", 161 | self.0.session_id, 162 | Arc::strong_count(&self.0), 163 | Arc::weak_count(&self.0) 164 | ); 165 | } 166 | 167 | #[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))] 168 | fn dispatch(&self, cmd: u8, data: Bytes) { 169 | match cmd { 170 | 0x4 => { 171 | self.debug_info(); 172 | self.send_packet(0x49, data.as_ref().to_owned()); 173 | } 174 | 0x4a => (), 175 | 0x1b => { 176 | let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); 177 | info!("Country: {:?}", country); 178 | self.0.data.write().unwrap().country = country; 179 | } 180 | 181 | 0x9 | 0xa => self.channel().dispatch(cmd, data), 182 | 0xd | 0xe => self.audio_key().dispatch(cmd, data), 183 | 0xb2...0xb6 => self.mercury().dispatch(cmd, data), 184 | _ => (), 185 | } 186 | } 187 | 188 | pub fn send_packet(&self, cmd: u8, data: Vec) { 189 | self.0.tx_connection.unbounded_send((cmd, data)).unwrap(); 190 | } 191 | 192 | pub fn cache(&self) -> Option<&Arc> { 193 | self.0.cache.as_ref() 194 | } 195 | 196 | fn config(&self) -> &SessionConfig { 197 | &self.0.config 198 | } 199 | 200 | pub fn username(&self) -> String { 201 | self.0.data.read().unwrap().canonical_username.clone() 202 | } 203 | 204 | pub fn country(&self) -> String { 205 | self.0.data.read().unwrap().country.clone() 206 | } 207 | 208 | pub fn device_id(&self) -> &str { 209 | &self.config().device_id 210 | } 211 | 212 | fn weak(&self) -> SessionWeak { 213 | SessionWeak(Arc::downgrade(&self.0)) 214 | } 215 | 216 | pub fn session_id(&self) -> usize { 217 | self.0.session_id 218 | } 219 | 220 | pub fn shutdown(&self) { 221 | debug!("Invalidating session[{}]", self.0.session_id); 222 | self.0.data.write().unwrap().invalid = true; 223 | } 224 | 225 | pub fn is_invalid(&self) -> bool { 226 | self.0.data.read().unwrap().invalid 227 | } 228 | } 229 | 230 | #[derive(Clone)] 231 | pub struct SessionWeak(Weak); 232 | 233 | impl SessionWeak { 234 | fn try_upgrade(&self) -> Option { 235 | self.0.upgrade().map(Session) 236 | } 237 | 238 | pub(crate) fn upgrade(&self) -> Session { 239 | self.try_upgrade().expect("Session died") 240 | } 241 | } 242 | 243 | impl Drop for SessionInternal { 244 | fn drop(&mut self) { 245 | debug!("drop Session[{}]", self.session_id); 246 | } 247 | } 248 | 249 | struct DispatchTask(S, SessionWeak) 250 | where 251 | S: Stream; 252 | 253 | impl Future for DispatchTask 254 | where 255 | S: Stream, 256 | ::Error: ::std::fmt::Debug, 257 | { 258 | type Item = (); 259 | type Error = S::Error; 260 | 261 | fn poll(&mut self) -> Poll { 262 | let session = match self.1.try_upgrade() { 263 | Some(session) => session, 264 | None => return Ok(Async::Ready(())), 265 | }; 266 | 267 | loop { 268 | let (cmd, data) = match self.0.poll() { 269 | Ok(Async::Ready(t)) => t, 270 | Ok(Async::NotReady) => return Ok(Async::NotReady), 271 | Err(e) => { 272 | session.shutdown(); 273 | return Err(From::from(e)); 274 | } 275 | }.expect("connection closed"); 276 | 277 | session.dispatch(cmd, data); 278 | } 279 | } 280 | } 281 | 282 | impl Drop for DispatchTask 283 | where 284 | S: Stream, 285 | { 286 | fn drop(&mut self) { 287 | debug!("drop Dispatch"); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /connect/src/discovery.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use crypto; 3 | use crypto::digest::Digest; 4 | use crypto::mac::Mac; 5 | use futures::sync::mpsc; 6 | use futures::{Future, Poll, Stream}; 7 | use hyper::server::{Http, Request, Response, Service}; 8 | use hyper::{self, Get, Post, StatusCode}; 9 | 10 | #[cfg(feature = "with-dns-sd")] 11 | use dns_sd::DNSService; 12 | 13 | #[cfg(not(feature = "with-dns-sd"))] 14 | use mdns; 15 | 16 | use num_bigint::BigUint; 17 | use rand; 18 | use std::collections::BTreeMap; 19 | use std::io; 20 | use std::sync::Arc; 21 | use tokio_core::reactor::Handle; 22 | use url; 23 | 24 | use core::authentication::Credentials; 25 | use core::config::ConnectConfig; 26 | use core::diffie_hellman::{DH_GENERATOR, DH_PRIME}; 27 | use core::util; 28 | 29 | #[derive(Clone)] 30 | struct Discovery(Arc); 31 | struct DiscoveryInner { 32 | config: ConnectConfig, 33 | device_id: String, 34 | private_key: BigUint, 35 | public_key: BigUint, 36 | tx: mpsc::UnboundedSender, 37 | } 38 | 39 | impl Discovery { 40 | fn new( 41 | config: ConnectConfig, 42 | device_id: String, 43 | ) -> (Discovery, mpsc::UnboundedReceiver) { 44 | let (tx, rx) = mpsc::unbounded(); 45 | 46 | let key_data = util::rand_vec(&mut rand::thread_rng(), 95); 47 | let private_key = BigUint::from_bytes_be(&key_data); 48 | let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); 49 | 50 | let discovery = Discovery(Arc::new(DiscoveryInner { 51 | config: config, 52 | device_id: device_id, 53 | private_key: private_key, 54 | public_key: public_key, 55 | tx: tx, 56 | })); 57 | 58 | (discovery, rx) 59 | } 60 | } 61 | 62 | impl Discovery { 63 | fn handle_get_info( 64 | &self, 65 | _params: &BTreeMap, 66 | ) -> ::futures::Finished { 67 | let public_key = self.0.public_key.to_bytes_be(); 68 | let public_key = base64::encode(&public_key); 69 | 70 | let result = json!({ 71 | "status": 101, 72 | "statusString": "ERROR-OK", 73 | "spotifyError": 0, 74 | "version": "2.1.0", 75 | "deviceID": (self.0.device_id), 76 | "remoteName": (self.0.config.name), 77 | "activeUser": "", 78 | "publicKey": (public_key), 79 | "deviceType": (self.0.config.device_type.to_string().to_uppercase()), 80 | "libraryVersion": "0.1.0", 81 | "accountReq": "PREMIUM", 82 | "brandDisplayName": "librespot", 83 | "modelDisplayName": "librespot", 84 | }); 85 | 86 | let body = result.to_string(); 87 | ::futures::finished(Response::new().with_body(body)) 88 | } 89 | 90 | fn handle_add_user( 91 | &self, 92 | params: &BTreeMap, 93 | ) -> ::futures::Finished { 94 | let username = params.get("userName").unwrap(); 95 | let encrypted_blob = params.get("blob").unwrap(); 96 | let client_key = params.get("clientKey").unwrap(); 97 | 98 | let encrypted_blob = base64::decode(encrypted_blob).unwrap(); 99 | 100 | let client_key = base64::decode(client_key).unwrap(); 101 | let client_key = BigUint::from_bytes_be(&client_key); 102 | 103 | let shared_key = util::powm(&client_key, &self.0.private_key, &DH_PRIME); 104 | 105 | let iv = &encrypted_blob[0..16]; 106 | let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; 107 | let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; 108 | 109 | let base_key = { 110 | let mut data = [0u8; 20]; 111 | let mut h = crypto::sha1::Sha1::new(); 112 | h.input(&shared_key.to_bytes_be()); 113 | h.result(&mut data); 114 | data[..16].to_owned() 115 | }; 116 | 117 | let checksum_key = { 118 | let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &base_key); 119 | h.input(b"checksum"); 120 | h.result().code().to_owned() 121 | }; 122 | 123 | let encryption_key = { 124 | let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &base_key); 125 | h.input(b"encryption"); 126 | h.result().code().to_owned() 127 | }; 128 | 129 | let mac = { 130 | let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &checksum_key); 131 | h.input(encrypted); 132 | h.result().code().to_owned() 133 | }; 134 | 135 | assert_eq!(&mac[..], cksum); 136 | 137 | let decrypted = { 138 | let mut data = vec![0u8; encrypted.len()]; 139 | let mut cipher = 140 | crypto::aes::ctr(crypto::aes::KeySize::KeySize128, &encryption_key[0..16], iv); 141 | cipher.process(encrypted, &mut data); 142 | String::from_utf8(data).unwrap() 143 | }; 144 | 145 | let credentials = Credentials::with_blob(username.to_owned(), &decrypted, &self.0.device_id); 146 | 147 | self.0.tx.unbounded_send(credentials).unwrap(); 148 | 149 | let result = json!({ 150 | "status": 101, 151 | "spotifyError": 0, 152 | "statusString": "ERROR-OK" 153 | }); 154 | 155 | let body = result.to_string(); 156 | ::futures::finished(Response::new().with_body(body)) 157 | } 158 | 159 | fn not_found(&self) -> ::futures::Finished { 160 | ::futures::finished(Response::new().with_status(StatusCode::NotFound)) 161 | } 162 | } 163 | 164 | impl Service for Discovery { 165 | type Request = Request; 166 | type Response = Response; 167 | type Error = hyper::Error; 168 | type Future = Box>; 169 | 170 | fn call(&self, request: Request) -> Self::Future { 171 | let mut params = BTreeMap::new(); 172 | 173 | let (method, uri, _, _, body) = request.deconstruct(); 174 | if let Some(query) = uri.query() { 175 | params.extend(url::form_urlencoded::parse(query.as_bytes()).into_owned()); 176 | } 177 | 178 | if method != Get { 179 | debug!("{:?} {:?} {:?}", method, uri.path(), params); 180 | } 181 | 182 | let this = self.clone(); 183 | Box::new( 184 | body.fold(Vec::new(), |mut acc, chunk| { 185 | acc.extend_from_slice(chunk.as_ref()); 186 | Ok::<_, hyper::Error>(acc) 187 | }).map(move |body| { 188 | params.extend(url::form_urlencoded::parse(&body).into_owned()); 189 | params 190 | }) 191 | .and_then( 192 | move |params| match (method, params.get("action").map(AsRef::as_ref)) { 193 | (Get, Some("getInfo")) => this.handle_get_info(¶ms), 194 | (Post, Some("addUser")) => this.handle_add_user(¶ms), 195 | _ => this.not_found(), 196 | }, 197 | ), 198 | ) 199 | } 200 | } 201 | 202 | #[cfg(feature = "with-dns-sd")] 203 | pub struct DiscoveryStream { 204 | credentials: mpsc::UnboundedReceiver, 205 | _svc: DNSService, 206 | } 207 | 208 | #[cfg(not(feature = "with-dns-sd"))] 209 | pub struct DiscoveryStream { 210 | credentials: mpsc::UnboundedReceiver, 211 | _svc: mdns::Service, 212 | } 213 | 214 | pub fn discovery( 215 | handle: &Handle, 216 | config: ConnectConfig, 217 | device_id: String, 218 | port: u16, 219 | ) -> io::Result { 220 | let (discovery, creds_rx) = Discovery::new(config.clone(), device_id); 221 | 222 | let serve = { 223 | let http = Http::new(); 224 | debug!("Zeroconf server listening on 0.0.0.0:{}", port); 225 | http.serve_addr_handle( 226 | &format!("0.0.0.0:{}", port).parse().unwrap(), 227 | &handle, 228 | move || Ok(discovery.clone()), 229 | ).unwrap() 230 | }; 231 | 232 | let s_port = serve.incoming_ref().local_addr().port(); 233 | 234 | let server_future = { 235 | let handle = handle.clone(); 236 | serve 237 | .for_each(move |connection| { 238 | handle.spawn(connection.then(|_| Ok(()))); 239 | Ok(()) 240 | }) 241 | .then(|_| Ok(())) 242 | }; 243 | handle.spawn(server_future); 244 | 245 | #[cfg(feature = "with-dns-sd")] 246 | let svc = DNSService::register( 247 | Some(&*config.name), 248 | "_spotify-connect._tcp", 249 | None, 250 | None, 251 | s_port, 252 | &["VERSION=1.0", "CPath=/"], 253 | ).unwrap(); 254 | 255 | #[cfg(not(feature = "with-dns-sd"))] 256 | let responder = mdns::Responder::spawn(&handle)?; 257 | 258 | #[cfg(not(feature = "with-dns-sd"))] 259 | let svc = responder.register( 260 | "_spotify-connect._tcp".to_owned(), 261 | config.name, 262 | s_port, 263 | &["VERSION=1.0", "CPath=/"], 264 | ); 265 | 266 | Ok(DiscoveryStream { 267 | credentials: creds_rx, 268 | _svc: svc, 269 | }) 270 | } 271 | 272 | impl Stream for DiscoveryStream { 273 | type Item = Credentials; 274 | type Error = (); 275 | 276 | fn poll(&mut self) -> Poll, Self::Error> { 277 | self.credentials.poll() 278 | } 279 | } 280 | --------------------------------------------------------------------------------