├── metadata ├── src │ ├── metadata.rs │ ├── cover.rs │ └── lib.rs └── Cargo.toml ├── src ├── lib.in.rs ├── lib.rs ├── keymaster.rs ├── main.rs ├── scrobbler.rs └── spirc.rs ├── core ├── src │ ├── lib.in.rs │ ├── version.rs │ ├── util │ │ ├── subfile.rs │ │ ├── int128.rs │ │ ├── spotify_id.rs │ │ └── mod.rs │ ├── lib.rs │ ├── mercury │ │ ├── sender.rs │ │ ├── types.rs │ │ └── mod.rs │ ├── cache │ │ └── mod.rs │ ├── apresolve.rs │ ├── diffie_hellman.rs │ ├── component.rs │ ├── config.rs │ ├── connection │ │ ├── mod.rs │ │ ├── codec.rs │ │ └── handshake.rs │ ├── audio_key.rs │ ├── channel.rs │ ├── authentication.rs │ └── session.rs ├── Cargo.toml └── build.rs ├── .dockerignore ├── .gitignore ├── .travis.yml ├── 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 │ └── pubsub.rs ├── files.rs ├── build.sh └── build.rs ├── Dockerfile ├── CHANGELOG.md ├── LICENSE ├── Cargo.toml ├── README.md ├── docs ├── connection.md └── authentication.md └── README.librespot.md /metadata/src/metadata.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib.in.rs: -------------------------------------------------------------------------------- 1 | pub mod spirc; 2 | -------------------------------------------------------------------------------- /core/src/lib.in.rs: -------------------------------------------------------------------------------- 1 | pub mod connection; 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | cache 3 | protocol/target 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .cargo 3 | spotify_appkey.key 4 | .vagrant/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - 1.18.0 4 | - stable 5 | - beta 6 | - nightly 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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", 3618770573), 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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust 2 | 3 | COPY . /tmp 4 | WORKDIR /tmp 5 | RUN cargo build --release 6 | 7 | CMD "./target/release/spotify-connect-scrobbler" "--spotify-username" "${SPOTIFY_USERNAME}" "--spotify-password" "${SPOTIFY_PASSWORD}" \ 8 | "--lastfm-username" "${LASTFM_USERNAME}" "--lastfm-password" "${LASTFM_PASSWORD}" "--lastfm-api-key" "${LASTFM_API_KEY}" "--lastfm-api-secret" "${LASTFM_API_SECRET}" -------------------------------------------------------------------------------- /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::util::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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Version 0.1.2 - 2017-11-05 2 | ========================== 3 | 4 | * Replace StdoutSink with NilSink, stops binary music data being piped 5 | to the terminal's stdout. (#9) 6 | * Remove `--backend` flag, will always be set to NilSink (#4) 7 | 8 | Version 0.1.1 - 2017-10-04 9 | ========================== 10 | 11 | * Add album name info to track scrobbles (#2) 12 | * Scrobbling triggered on start of new track, fixes tracks failing to 13 | scrobble under certain conditions (#3) 14 | * Make `--name` flag optional; defaults to 'Scrobbler' 15 | * Significant internal refactoring, including removal of unnecessary 16 | `unsafe` code. 17 | 18 | Version 0.1.0 - 2017-08-26 19 | ========================== 20 | 21 | * Initial release 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![crate_name = "librespot"] 2 | 3 | #![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))] 4 | 5 | // TODO: many items from tokio-core::io have been deprecated in favour of tokio-io 6 | #![allow(deprecated)] 7 | 8 | #[macro_use] extern crate log; 9 | 10 | #[macro_use] 11 | extern crate serde_derive; 12 | extern crate serde_json; 13 | 14 | extern crate futures; 15 | extern crate num_bigint; 16 | extern crate protobuf; 17 | extern crate rand; 18 | extern crate rustfm_scrobble; 19 | extern crate tokio_core; 20 | 21 | pub extern crate librespot_core as core; 22 | pub extern crate librespot_protocol as protocol; 23 | pub extern crate librespot_metadata as metadata; 24 | 25 | pub mod keymaster; 26 | pub mod scrobbler; 27 | 28 | include!(concat!(env!("OUT_DIR"), "/lib.rs")); 29 | -------------------------------------------------------------------------------- /src/keymaster.rs: -------------------------------------------------------------------------------- 1 | use futures::{Future, BoxFuture}; 2 | use serde_json; 3 | 4 | use core::mercury::MercuryError; 5 | use core::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(session: &Session, client_id: &str, scopes: &str) -> BoxFuture { 17 | let url = format!("hm://keymaster/token/authenticated?client_id={}&scope={}", 18 | client_id, scopes); 19 | session.mercury().get(url).map(move |response| { 20 | let data = response.payload.first().expect("Empty payload"); 21 | let data = String::from_utf8(data.clone()).unwrap(); 22 | let token : Token = serde_json::from_str(&data).unwrap(); 23 | 24 | token 25 | }).boxed() 26 | } 27 | -------------------------------------------------------------------------------- /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 | error-chain = { version = "0.9.0", default_features = false } 14 | futures = "0.1.8" 15 | hyper = "0.11.2" 16 | lazy_static = "0.2.0" 17 | log = "0.3.5" 18 | num-bigint = "0.1.35" 19 | num-integer = "0.1.32" 20 | num-traits = "0.1.36" 21 | protobuf = "1.1" 22 | rand = "0.3.13" 23 | rpassword = "0.3.0" 24 | rust-crypto = { git = "https://github.com/awmath/rust-crypto.git", branch = "avx2" } 25 | serde = "0.9.6" 26 | serde_derive = "0.9.6" 27 | serde_json = "0.9.5" 28 | shannon = "0.2.0" 29 | tokio-core = "0.1.2" 30 | uuid = { version = "0.4", features = ["v4"] } 31 | 32 | [build-dependencies] 33 | protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros", features = ["with-syntex"] } 34 | rand = "0.3.13" 35 | vergen = "0.1.0" 36 | -------------------------------------------------------------------------------- /core/src/util/subfile.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Seek, SeekFrom, Result}; 2 | 3 | pub struct Subfile { 4 | stream: T, 5 | offset: u64, 6 | } 7 | 8 | impl Subfile { 9 | pub fn new(mut stream: T, offset: u64) -> Subfile { 10 | stream.seek(SeekFrom::Start(offset)).unwrap(); 11 | Subfile { 12 | stream: stream, 13 | offset: offset, 14 | } 15 | } 16 | } 17 | 18 | impl Read for Subfile { 19 | fn read(&mut self, buf: &mut [u8]) -> Result { 20 | self.stream.read(buf) 21 | } 22 | } 23 | 24 | impl Seek for Subfile { 25 | fn seek(&mut self, mut pos: SeekFrom) -> Result { 26 | pos = match pos { 27 | SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset), 28 | x => x, 29 | }; 30 | 31 | let newpos = try!(self.stream.seek(pos)); 32 | if newpos > self.offset { 33 | Ok(newpos - self.offset) 34 | } else { 35 | Ok(0) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /core/build.rs: -------------------------------------------------------------------------------- 1 | extern crate vergen; 2 | extern crate protobuf_macros; 3 | extern crate rand; 4 | 5 | use rand::Rng; 6 | use std::env; 7 | use std::path::PathBuf; 8 | use std::fs::OpenOptions; 9 | use std::io::Write; 10 | 11 | fn main() { 12 | let out = PathBuf::from(env::var("OUT_DIR").unwrap()); 13 | 14 | vergen::vergen(vergen::OutputFns::all()).unwrap(); 15 | 16 | let build_id: String = rand::thread_rng() 17 | .gen_ascii_chars() 18 | .take(8) 19 | .collect(); 20 | 21 | let mut version_file = 22 | OpenOptions::new() 23 | .write(true) 24 | .append(true) 25 | .open(&out.join("version.rs")) 26 | .unwrap(); 27 | 28 | let build_id_fn = format!(" 29 | /// Generate a random build id. 30 | pub fn build_id() -> &'static str {{ 31 | \"{}\" 32 | }} 33 | ", build_id); 34 | 35 | if let Err(e) = version_file.write_all(build_id_fn.as_bytes()) { 36 | println!("{}", e); 37 | } 38 | 39 | protobuf_macros::expand("src/lib.in.rs", &out.join("lib.rs")).unwrap(); 40 | 41 | println!("cargo:rerun-if-changed=src/lib.in.rs"); 42 | println!("cargo:rerun-if-changed=src/connection"); 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 - 2017 Paul Lietar 4 | Copyright (c) 2017 David Futcher 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))] 2 | 3 | // TODO: many items from tokio-core::io have been deprecated in favour of tokio-io 4 | #![allow(deprecated)] 5 | 6 | #[macro_use] extern crate error_chain; 7 | #[macro_use] extern crate futures; 8 | #[macro_use] extern crate lazy_static; 9 | #[macro_use] extern crate log; 10 | #[macro_use] extern crate serde_derive; 11 | 12 | extern crate base64; 13 | extern crate byteorder; 14 | extern crate crypto; 15 | extern crate hyper; 16 | extern crate num_bigint; 17 | extern crate num_integer; 18 | extern crate num_traits; 19 | extern crate protobuf; 20 | extern crate rand; 21 | extern crate rpassword; 22 | extern crate serde; 23 | extern crate serde_json; 24 | extern crate shannon; 25 | extern crate tokio_core; 26 | extern crate uuid; 27 | 28 | extern crate librespot_protocol as protocol; 29 | 30 | #[macro_use] mod component; 31 | pub mod apresolve; 32 | pub mod audio_key; 33 | pub mod authentication; 34 | pub mod cache; 35 | pub mod channel; 36 | pub mod config; 37 | pub mod diffie_hellman; 38 | pub mod mercury; 39 | pub mod session; 40 | pub mod util; 41 | pub mod version; 42 | 43 | include!(concat!(env!("OUT_DIR"), "/lib.rs")); 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 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spotify-connect-scrobbler" 3 | version = "0.1.2" 4 | authors = ["David Futcher ", "Paul Liétar "] 5 | build = "build.rs" 6 | license = "MIT" 7 | description = "Spotify server-side Scrobbler" 8 | keywords = ["spotify", "last.fm", "scrobble"] 9 | repository = "https://github.com/bobbo/spotify-connect-scrobbler" 10 | readme = "README.md" 11 | 12 | [workspace] 13 | 14 | [lib] 15 | name = "librespot" 16 | path = "src/lib.rs" 17 | 18 | [[bin]] 19 | name = "spotify-connect-scrobbler" 20 | path = "src/main.rs" 21 | doc = false 22 | 23 | [dependencies.librespot-core] 24 | path = "core" 25 | [dependencies.librespot-metadata] 26 | path = "metadata" 27 | [dependencies.librespot-protocol] 28 | path = "protocol" 29 | 30 | [dependencies] 31 | env_logger = "0.4.0" 32 | futures = "0.1.8" 33 | getopts = "0.2" 34 | log = "0.3.5" 35 | num-bigint = "0.1.35" 36 | protobuf = "1.1" 37 | rand = "0.3.13" 38 | rpassword = "0.3.0" 39 | rustfm-scrobble = "1" 40 | serde = "0.9.6" 41 | serde_derive = "0.9.6" 42 | serde_json = "0.9.5" 43 | tokio-core = "0.1.2" 44 | tokio-signal = "0.1.2" 45 | 46 | [build-dependencies] 47 | protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros", features = ["with-syntex"] } 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 std::collections::VecDeque; 2 | use futures::{Async, Poll, Future, Sink, StartSend, AsyncSink}; 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 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 | -------------------------------------------------------------------------------- /core/src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::io::Read; 3 | use std::fs::File; 4 | 5 | use util::{FileId, mkdir_existing}; 6 | use authentication::Credentials; 7 | 8 | #[derive(Clone)] 9 | pub struct Cache { 10 | root: PathBuf, 11 | use_audio_cache: bool, 12 | } 13 | 14 | impl Cache { 15 | pub fn new(location: PathBuf, use_audio_cache: bool) -> Cache { 16 | mkdir_existing(&location).unwrap(); 17 | mkdir_existing(&location.join("files")).unwrap(); 18 | 19 | Cache { 20 | root: location, 21 | use_audio_cache: use_audio_cache 22 | } 23 | } 24 | } 25 | 26 | impl Cache { 27 | fn credentials_path(&self) -> PathBuf { 28 | self.root.join("credentials.json") 29 | } 30 | 31 | pub fn credentials(&self) -> Option { 32 | let path = self.credentials_path(); 33 | Credentials::from_file(path) 34 | } 35 | 36 | pub fn save_credentials(&self, cred: &Credentials) { 37 | let path = self.credentials_path(); 38 | cred.save_to_file(&path); 39 | } 40 | } 41 | 42 | impl Cache { 43 | fn file_path(&self, file: FileId) -> PathBuf { 44 | let name = file.to_base16(); 45 | self.root.join("files").join(&name[0..2]).join(&name[2..]) 46 | } 47 | 48 | pub fn file(&self, file: FileId) -> Option { 49 | File::open(self.file_path(file)).ok() 50 | } 51 | 52 | pub fn save_file(&self, file: FileId, contents: &mut Read) { 53 | if self.use_audio_cache { 54 | let path = self.file_path(file); 55 | 56 | mkdir_existing(path.parent().unwrap()).unwrap(); 57 | 58 | let mut cache_file = File::create(path).unwrap(); 59 | ::std::io::copy(contents, &mut cache_file).unwrap(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/apresolve.rs: -------------------------------------------------------------------------------- 1 | const AP_FALLBACK : &'static str = "ap.spotify.com:80"; 2 | const APRESOLVE_ENDPOINT : &'static str = "http://apresolve.spotify.com/"; 3 | 4 | use std::str::FromStr; 5 | use futures::{Future, Stream}; 6 | use hyper::{self, Uri, Client}; 7 | use serde_json; 8 | use tokio_core::reactor::Handle; 9 | 10 | error_chain! { } 11 | 12 | #[derive(Clone, Debug, Serialize, Deserialize)] 13 | pub struct APResolveData { 14 | ap_list: Vec 15 | } 16 | 17 | pub fn apresolve(handle: &Handle) -> Box> { 18 | let url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL"); 19 | 20 | let client = Client::new(handle); 21 | let response = client.get(url); 22 | 23 | let body = response.and_then(|response| { 24 | response.body().fold(Vec::new(), |mut acc, chunk| { 25 | acc.extend_from_slice(chunk.as_ref()); 26 | Ok::<_, hyper::Error>(acc) 27 | }) 28 | }); 29 | let body = body.then(|result| result.chain_err(|| "HTTP error")); 30 | let body = body.and_then(|body| { 31 | String::from_utf8(body).chain_err(|| "invalid UTF8 in response") 32 | }); 33 | 34 | let data = body.and_then(|body| { 35 | serde_json::from_str::(&body) 36 | .chain_err(|| "invalid JSON") 37 | }); 38 | 39 | let ap = data.and_then(|data| { 40 | let ap = data.ap_list.first().ok_or("empty AP List")?; 41 | Ok(ap.clone()) 42 | }); 43 | 44 | Box::new(ap) 45 | } 46 | 47 | pub fn apresolve_or_fallback(handle: &Handle) 48 | -> Box> 49 | where E: 'static 50 | { 51 | let ap = apresolve(handle).or_else(|e| { 52 | warn!("Failed to resolve Access Point: {}", e.description()); 53 | warn!("Using fallback \"{}\"", AP_FALLBACK); 54 | Ok(AP_FALLBACK.into()) 55 | }); 56 | 57 | Box::new(ap) 58 | } 59 | -------------------------------------------------------------------------------- /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, 11 | 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 12 | 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 13 | 0x08, 0x8a, 0x67, 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 14 | 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 15 | 0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 16 | 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 17 | 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 18 | 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 19 | 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, 20 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff ]); 21 | } 22 | 23 | pub struct DHLocalKeys { 24 | private_key: BigUint, 25 | public_key: BigUint, 26 | } 27 | 28 | impl DHLocalKeys { 29 | pub fn random(rng: &mut R) -> DHLocalKeys { 30 | let key_data = util::rand_vec(rng, 95); 31 | 32 | let private_key = BigUint::from_bytes_be(&key_data); 33 | let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); 34 | 35 | DHLocalKeys { 36 | private_key: private_key, 37 | public_key: public_key, 38 | } 39 | } 40 | 41 | pub fn public_key(&self) -> Vec { 42 | self.public_key.to_bytes_be() 43 | } 44 | 45 | pub fn shared_secret(&self, remote_key: &[u8]) -> Vec { 46 | let shared_key = util::powm(&BigUint::from_bytes_be(remote_key), 47 | &self.private_key, 48 | &DH_PRIME); 49 | shared_key.to_bytes_be() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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 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::sync::Mutex; 40 | use std::cell::UnsafeCell; 41 | 42 | pub 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 fn new() -> Lazy { 49 | Lazy(Mutex::new(false), UnsafeCell::new(None)) 50 | } 51 | 52 | pub 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 | -------------------------------------------------------------------------------- /core/src/config.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | use std::str::FromStr; 3 | use std::fmt; 4 | 5 | use version; 6 | 7 | #[derive(Clone,Debug)] 8 | pub struct SessionConfig { 9 | pub user_agent: String, 10 | pub device_id: String, 11 | } 12 | 13 | impl Default for SessionConfig { 14 | fn default() -> SessionConfig { 15 | let device_id = Uuid::new_v4().hyphenated().to_string(); 16 | SessionConfig { 17 | user_agent: version::version_string(), 18 | device_id: device_id, 19 | } 20 | } 21 | } 22 | 23 | 24 | #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] 25 | pub enum DeviceType { 26 | Unknown = 0, 27 | Computer = 1, 28 | Tablet = 2, 29 | Smartphone = 3, 30 | Speaker = 4, 31 | TV = 5, 32 | AVR = 6, 33 | STB = 7, 34 | AudioDongle = 8, 35 | } 36 | 37 | impl FromStr for DeviceType { 38 | type Err = (); 39 | fn from_str(s: &str) -> Result { 40 | use self::DeviceType::*; 41 | match s.to_lowercase().as_ref() { 42 | "computer" => Ok(Computer), 43 | "tablet" => Ok(Tablet), 44 | "smartphone" => Ok(Smartphone), 45 | "speaker" => Ok(Speaker), 46 | "tv" => Ok(TV), 47 | "avr" => Ok(AVR), 48 | "stb" => Ok(STB), 49 | "audiodongle" => Ok(AudioDongle), 50 | _ => Err(()), 51 | } 52 | } 53 | } 54 | 55 | impl fmt::Display for DeviceType { 56 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 57 | use self::DeviceType::*; 58 | match *self { 59 | Unknown => f.write_str("Unknown"), 60 | Computer => f.write_str("Computer"), 61 | Tablet => f.write_str("Tablet"), 62 | Smartphone => f.write_str("Smartphone"), 63 | Speaker => f.write_str("Speaker"), 64 | TV => f.write_str("TV"), 65 | AVR => f.write_str("AVR"), 66 | STB => f.write_str("STB"), 67 | AudioDongle => f.write_str("AudioDongle"), 68 | } 69 | } 70 | } 71 | 72 | impl Default for DeviceType { 73 | fn default() -> DeviceType { 74 | DeviceType::Speaker 75 | } 76 | } 77 | 78 | #[derive(Clone,Debug)] 79 | pub struct ConnectConfig { 80 | pub name: String, 81 | pub device_type: DeviceType, 82 | } 83 | -------------------------------------------------------------------------------- /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 | } 41 | .to_owned() 42 | } 43 | } 44 | 45 | impl MercuryMethod { 46 | pub fn command(&self) -> u8 { 47 | match *self { 48 | MercuryMethod::GET | MercuryMethod::SEND => 0xb2, 49 | MercuryMethod::SUB => 0xb3, 50 | MercuryMethod::UNSUB => 0xb4, 51 | } 52 | } 53 | } 54 | 55 | impl MercuryRequest { 56 | pub fn encode(&self, seq: &[u8]) -> Vec { 57 | let mut packet = Vec::new(); 58 | packet.write_u16::(seq.len() as u16).unwrap(); 59 | packet.write_all(seq).unwrap(); 60 | packet.write_u8(1).unwrap(); // Flags: FINAL 61 | packet.write_u16::(1 + self.payload.len() as u16).unwrap(); // Part count 62 | 63 | let mut header = protocol::mercury::Header::new(); 64 | header.set_uri(self.uri.clone()); 65 | header.set_method(self.method.to_string()); 66 | 67 | if let Some(ref content_type) = self.content_type { 68 | header.set_content_type(content_type.clone()); 69 | } 70 | 71 | packet.write_u16::(header.compute_size() as u16).unwrap(); 72 | header.write_to_writer(&mut packet).unwrap(); 73 | 74 | for p in &self.payload { 75 | packet.write_u16::(p.len() as u16).unwrap(); 76 | packet.write(p).unwrap(); 77 | } 78 | 79 | packet 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spotify-connect-scrobbler 2 | 3 | **This project deprecated since Spotify changed the way their Last.fm integration works. Scrobbling now happens on Spotify's servers, rather than the client, so all Connect devices can now scrobble. This is left here for posterity as it's one of my favourite hacks.** 4 | 5 | *spotify-connect-scrobbler* is a Last.fm music logging ("scrobbling") service for Spotify. It uses [Spotify Connect](https://www.spotify.com/connect/) to allow you to log music played on any Spotify device, including those which do not have any Last.fm support (such as Amazon Echo). 6 | 7 | # Usage 8 | 9 | To use *spotify-connect-scrobbler* have your Spotify username & password, your Last.fm username & password, plus a [Last.fm API key and API secret](https://www.last.fm/api/account/create) to hand. 10 | 11 | Clone the repo,have Rust installed (`v1.18` minimum required) build with Cargo (`cargo build`). Then run: 12 | 13 | `./target/debug/spotify-connect-scrobbler --spotify-username --spotify-password --lastfm-username --lastfm-password --lastfm-api-key --lastfm-api-secret ` 14 | 15 | The service will sit in the background and log all Spotify tracks played from any Connect enabled client to the given Last.fm account. It is strongly recommended that you turn off Last.fm integration in any Spotify client where it is enabled (Desktop & Mobile apps). Instructions for the opposite [here](https://support.spotify.com/us/using_spotify/app_integrations/scrobble-to-last-fm/). 16 | 17 | #### Other Options 18 | 19 | * `--name ` - Sets the Spotify Connect device name (defaults to 'Scrobbler'), this name is visible in the Spotify Connect device chooser in Spotify clients 20 | 21 | # Implementation 22 | 23 | *spotify-connect-scrobbler* is built on top (more accurately, is a fork of) of Paul Lietar's [librespot](https://github.com/plietar/librespot) project, an open-source Spotify Connect implementation in Rust. It connects to Spotify as a fully-fledged Spotify Connect device. The active Spotify Connect device (the one playing music) broadcasts its status to all other Connect devices on an account, in order to show now-playing track data on other clients. For example, when playing Spotify tracks on an Amazon Echo, the Echo device will broadcast the currently playing track so that it can be shown on the Spotify app on your phone). Thus *spotify-connect-scrobbler* can see the currently playing track and send that to be logged on your Last.fm account. 24 | 25 | # License 26 | 27 | Released under the MIT license. See `LICENSE`. 28 | 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, BoxFuture}; 8 | use std::io; 9 | use std::net::ToSocketAddrs; 10 | use tokio_core::net::TcpStream; 11 | use tokio_core::reactor::Handle; 12 | use tokio_core::io::Framed; 13 | use protobuf::{self, Message}; 14 | 15 | use authentication::Credentials; 16 | use version; 17 | 18 | pub type Transport = Framed; 19 | 20 | pub fn connect(addr: A, handle: &Handle) -> BoxFuture { 21 | let addr = addr.to_socket_addrs().unwrap().next().unwrap(); 22 | let socket = TcpStream::connect(&addr, handle); 23 | let connection = socket.and_then(|socket| { 24 | handshake(socket) 25 | }); 26 | 27 | connection.boxed() 28 | } 29 | 30 | pub fn authenticate(transport: Transport, credentials: Credentials, device_id: String) 31 | -> BoxFuture<(Transport, Credentials), io::Error> 32 | { 33 | use protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; 34 | 35 | let packet = protobuf_init!(ClientResponseEncrypted::new(), { 36 | login_credentials => { 37 | username: credentials.username, 38 | typ: credentials.auth_type, 39 | auth_data: credentials.auth_data, 40 | }, 41 | system_info => { 42 | cpu_family: CpuFamily::CPU_UNKNOWN, 43 | os: Os::OS_UNKNOWN, 44 | system_information_string: format!("librespot_{}_{}", version::short_sha(), version::build_id()), 45 | device_id: device_id, 46 | }, 47 | version_string: version::version_string(), 48 | }); 49 | 50 | let cmd = 0xab; 51 | let data = packet.write_to_bytes().unwrap(); 52 | 53 | transport.send((cmd, data)).and_then(|transport| { 54 | transport.into_future().map_err(|(err, _stream)| err) 55 | }).and_then(|(packet, transport)| { 56 | match packet { 57 | Some((0xac, data)) => { 58 | let welcome_data: APWelcome = 59 | protobuf::parse_from_bytes(data.as_ref()).unwrap(); 60 | 61 | let reusable_credentials = Credentials { 62 | username: welcome_data.get_canonical_username().to_owned(), 63 | auth_type: welcome_data.get_reusable_auth_credentials_type(), 64 | auth_data: welcome_data.get_reusable_auth_credentials().to_owned(), 65 | }; 66 | 67 | Ok((transport, reusable_credentials)) 68 | } 69 | 70 | Some((0xad, _)) => panic!("Authentication failed"), 71 | Some((cmd, _)) => panic!("Unexpected packet {:?}", cmd), 72 | None => panic!("EOF"), 73 | } 74 | }).boxed() 75 | } 76 | -------------------------------------------------------------------------------- /core/src/audio_key.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; 2 | use futures::sync::oneshot; 3 | use futures::{Async, Future, Poll}; 4 | use std::collections::HashMap; 5 | use std::io::Write; 6 | use tokio_core::io::EasyBuf; 7 | 8 | use util::SeqGenerator; 9 | use util::{SpotifyId, FileId}; 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 fn dispatch(&self, cmd: u8, mut data: EasyBuf) { 26 | let seq = BigEndian::read_u32(data.drain_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 | sender.complete(Ok(AudioKey(key))); 36 | } 37 | 0xe => { 38 | warn!("error audio key {:x} {:x}", data.as_ref()[0], data.as_ref()[1]); 39 | sender.complete(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 | 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /core/src/connection/codec.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; 2 | use shannon::Shannon; 3 | use std::io; 4 | use tokio_core::io::{Codec, EasyBuf}; 5 | 6 | const HEADER_SIZE: usize = 3; 7 | const MAC_SIZE: usize = 4; 8 | 9 | #[derive(Debug)] 10 | enum DecodeState { 11 | Header, 12 | Payload(u8, usize), 13 | } 14 | 15 | pub struct APCodec { 16 | encode_nonce: u32, 17 | encode_cipher: Shannon, 18 | 19 | decode_nonce: u32, 20 | decode_cipher: Shannon, 21 | decode_state: DecodeState, 22 | } 23 | 24 | impl APCodec { 25 | pub fn new(send_key: &[u8], recv_key: &[u8]) -> APCodec { 26 | APCodec { 27 | encode_nonce: 0, 28 | encode_cipher: Shannon::new(send_key), 29 | 30 | decode_nonce: 0, 31 | decode_cipher: Shannon::new(recv_key), 32 | decode_state: DecodeState::Header, 33 | } 34 | } 35 | } 36 | 37 | impl Codec for APCodec { 38 | type Out = (u8, Vec); 39 | type In = (u8, EasyBuf); 40 | 41 | fn encode(&mut self, item: (u8, Vec), buf: &mut Vec) -> io::Result<()> { 42 | let (cmd, payload) = item; 43 | let offset = buf.len(); 44 | 45 | buf.write_u8(cmd).unwrap(); 46 | buf.write_u16::(payload.len() as u16).unwrap(); 47 | buf.extend_from_slice(&payload); 48 | 49 | self.encode_cipher.nonce_u32(self.encode_nonce); 50 | self.encode_nonce += 1; 51 | 52 | self.encode_cipher.encrypt(&mut buf[offset..]); 53 | 54 | let mut mac = [0u8; MAC_SIZE]; 55 | self.encode_cipher.finish(&mut mac); 56 | buf.extend_from_slice(&mac); 57 | 58 | Ok(()) 59 | } 60 | 61 | fn decode(&mut self, buf: &mut EasyBuf) -> io::Result> { 62 | if let DecodeState::Header = self.decode_state { 63 | if buf.len() >= HEADER_SIZE { 64 | let mut header = [0u8; HEADER_SIZE]; 65 | header.copy_from_slice(buf.drain_to(HEADER_SIZE).as_slice()); 66 | 67 | self.decode_cipher.nonce_u32(self.decode_nonce); 68 | self.decode_nonce += 1; 69 | 70 | self.decode_cipher.decrypt(&mut header); 71 | 72 | let cmd = header[0]; 73 | let size = BigEndian::read_u16(&header[1..]) as usize; 74 | self.decode_state = DecodeState::Payload(cmd, size); 75 | } 76 | } 77 | 78 | if let DecodeState::Payload(cmd, size) = self.decode_state { 79 | if buf.len() >= size + MAC_SIZE { 80 | self.decode_state = DecodeState::Header; 81 | 82 | let mut payload = buf.drain_to(size + MAC_SIZE); 83 | 84 | self.decode_cipher.decrypt(&mut payload.get_mut()[..size]); 85 | let mac = payload.split_off(size); 86 | self.decode_cipher.check_mac(mac.as_slice())?; 87 | 88 | return Ok(Some((cmd, payload))); 89 | } 90 | } 91 | 92 | 93 | Ok(None) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /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/util/int128.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | 3 | #[derive(Debug,Copy,Clone,PartialEq,Eq,Hash)] 4 | #[allow(non_camel_case_types)] 5 | pub struct u128 { 6 | high: u64, 7 | low: u64, 8 | } 9 | 10 | impl u128 { 11 | pub fn zero() -> u128 { 12 | u128::from_parts(0, 0) 13 | } 14 | 15 | pub fn from_parts(high: u64, low: u64) -> u128 { 16 | u128 { 17 | high: high, 18 | low: low, 19 | } 20 | } 21 | 22 | pub fn parts(&self) -> (u64, u64) { 23 | (self.high, self.low) 24 | } 25 | } 26 | 27 | impl std::ops::Add for u128 { 28 | type Output = u128; 29 | fn add(self, rhs: u128) -> u128 { 30 | let low = self.low + rhs.low; 31 | let high = self.high + rhs.high + 32 | if low < self.low { 33 | 1 34 | } else { 35 | 0 36 | }; 37 | 38 | u128::from_parts(high, low) 39 | } 40 | } 41 | 42 | impl<'a> std::ops::Add<&'a u128> for u128 { 43 | type Output = u128; 44 | fn add(self, rhs: &'a u128) -> u128 { 45 | let low = self.low + rhs.low; 46 | let high = self.high + rhs.high + 47 | if low < self.low { 48 | 1 49 | } else { 50 | 0 51 | }; 52 | 53 | u128::from_parts(high, low) 54 | } 55 | } 56 | 57 | impl std::convert::From for u128 { 58 | fn from(n: u8) -> u128 { 59 | u128::from_parts(0, n as u64) 60 | } 61 | } 62 | 63 | 64 | impl std::ops::Mul for u128 { 65 | type Output = u128; 66 | 67 | fn mul(self, rhs: u128) -> u128 { 68 | let top: [u64; 4] = [self.high >> 32, 69 | self.high & 0xFFFFFFFF, 70 | self.low >> 32, 71 | self.low & 0xFFFFFFFF]; 72 | 73 | let bottom: [u64; 4] = [rhs.high >> 32, 74 | rhs.high & 0xFFFFFFFF, 75 | rhs.low >> 32, 76 | rhs.low & 0xFFFFFFFF]; 77 | 78 | let mut rows = [u128::zero(); 16]; 79 | for i in 0..4 { 80 | for j in 0..4 { 81 | let shift = i + j; 82 | let product = top[3 - i] * bottom[3 - j]; 83 | let (high, low) = match shift { 84 | 0 => (0, product), 85 | 1 => (product >> 32, product << 32), 86 | 2 => (product, 0), 87 | 3 => (product << 32, 0), 88 | _ => { 89 | if product == 0 { 90 | (0, 0) 91 | } else { 92 | panic!("Overflow on mul {:?} {:?} ({} {})", self, rhs, i, j) 93 | } 94 | } 95 | }; 96 | rows[j * 4 + i] = u128::from_parts(high, low); 97 | } 98 | } 99 | 100 | rows.iter().fold(u128::zero(), std::ops::Add::add) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/util/spotify_id.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::fmt; 3 | use util::u128; 4 | use byteorder::{BigEndian, ByteOrder}; 5 | use std::ascii::AsciiExt; 6 | 7 | #[derive(Debug,Copy,Clone,PartialEq,Eq,Hash)] 8 | pub struct SpotifyId(u128); 9 | 10 | const BASE62_DIGITS: &'static [u8] = 11 | b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 12 | const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef"; 13 | 14 | impl SpotifyId { 15 | pub fn from_base16(id: &str) -> SpotifyId { 16 | assert!(id.is_ascii()); 17 | let data = id.as_bytes(); 18 | 19 | let mut n: u128 = u128::zero(); 20 | for c in data { 21 | let d = BASE16_DIGITS.iter().position(|e| e == c).unwrap() as u8; 22 | n = n * u128::from(16); 23 | n = n + u128::from(d); 24 | } 25 | 26 | SpotifyId(n) 27 | } 28 | 29 | pub fn from_base62(id: &str) -> SpotifyId { 30 | assert!(id.is_ascii()); 31 | let data = id.as_bytes(); 32 | 33 | let mut n: u128 = u128::zero(); 34 | for c in data { 35 | let d = BASE62_DIGITS.iter().position(|e| e == c).unwrap() as u8; 36 | n = n * u128::from(62); 37 | n = n + u128::from(d); 38 | } 39 | 40 | SpotifyId(n) 41 | } 42 | 43 | pub fn from_raw(data: &[u8]) -> SpotifyId { 44 | assert_eq!(data.len(), 16); 45 | 46 | let high = BigEndian::read_u64(&data[0..8]); 47 | let low = BigEndian::read_u64(&data[8..16]); 48 | 49 | SpotifyId(u128::from_parts(high, low)) 50 | } 51 | 52 | pub fn to_base16(&self) -> String { 53 | let &SpotifyId(ref n) = self; 54 | let (high, low) = n.parts(); 55 | 56 | let mut data = [0u8; 32]; 57 | for i in 0..16 { 58 | data[31 - i] = BASE16_DIGITS[(low.wrapping_shr(4 * i as u32) & 0xF) as usize]; 59 | } 60 | for i in 0..16 { 61 | data[15 - i] = BASE16_DIGITS[(high.wrapping_shr(4 * i as u32) & 0xF) as usize]; 62 | } 63 | 64 | std::str::from_utf8(&data).unwrap().to_owned() 65 | } 66 | 67 | pub fn to_raw(&self) -> [u8; 16] { 68 | let &SpotifyId(ref n) = self; 69 | let (high, low) = n.parts(); 70 | 71 | let mut data = [0u8; 16]; 72 | 73 | BigEndian::write_u64(&mut data[0..8], high); 74 | BigEndian::write_u64(&mut data[8..16], low); 75 | 76 | data 77 | } 78 | } 79 | 80 | #[derive(Copy,Clone,PartialEq,Eq,PartialOrd,Ord,Hash)] 81 | pub struct FileId(pub [u8; 20]); 82 | 83 | impl FileId { 84 | pub fn to_base16(&self) -> String { 85 | self.0 86 | .iter() 87 | .map(|b| format!("{:02x}", b)) 88 | .collect::>() 89 | .concat() 90 | } 91 | } 92 | 93 | impl fmt::Debug for FileId { 94 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 95 | f.debug_tuple("FileId").field(&self.to_base16()).finish() 96 | } 97 | } 98 | 99 | impl fmt::Display for FileId { 100 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 101 | f.write_str(&self.to_base16()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /core/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | use num_bigint::BigUint; 2 | use num_traits::{Zero, One}; 3 | use num_integer::Integer; 4 | use rand::{Rng, Rand}; 5 | use std::io; 6 | use std::mem; 7 | use std::ops::{Mul, Rem, Shr}; 8 | use std::fs; 9 | use std::path::Path; 10 | use std::process::Command; 11 | use std::time::{UNIX_EPOCH, SystemTime}; 12 | 13 | mod int128; 14 | mod spotify_id; 15 | mod subfile; 16 | 17 | pub use util::int128::u128; 18 | pub use util::spotify_id::{SpotifyId, FileId}; 19 | pub use util::subfile::Subfile; 20 | 21 | pub fn rand_vec(rng: &mut G, size: usize) -> Vec { 22 | rng.gen_iter().take(size).collect() 23 | } 24 | 25 | pub fn now_ms() -> i64 { 26 | let dur = match SystemTime::now().duration_since(UNIX_EPOCH) { 27 | Ok(dur) => dur, 28 | Err(err) => err.duration(), 29 | }; 30 | (dur.as_secs() * 1000 + (dur.subsec_nanos() / 1000_000) as u64) as i64 31 | } 32 | 33 | pub fn mkdir_existing(path: &Path) -> io::Result<()> { 34 | fs::create_dir(path).or_else(|err| { 35 | if err.kind() == io::ErrorKind::AlreadyExists { 36 | Ok(()) 37 | } else { 38 | Err(err) 39 | } 40 | }) 41 | } 42 | 43 | pub fn run_program(program: &str) { 44 | info!("Running {}", program); 45 | let mut v: Vec<&str> = program.split_whitespace().collect(); 46 | let status = Command::new(&v.remove(0)) 47 | .args(&v) 48 | .status() 49 | .expect("program failed to start"); 50 | info!("Exit status: {}", status); 51 | } 52 | 53 | pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { 54 | let mut base = base.clone(); 55 | let mut exp = exp.clone(); 56 | let mut result: BigUint = One::one(); 57 | 58 | while !exp.is_zero() { 59 | if exp.is_odd() { 60 | result = result.mul(&base).rem(modulus); 61 | } 62 | exp = exp.shr(1); 63 | base = (&base).mul(&base).rem(modulus); 64 | } 65 | 66 | result 67 | } 68 | 69 | pub struct StrChunks<'s>(&'s str, usize); 70 | 71 | pub trait StrChunksExt { 72 | fn chunks(&self, size: usize) -> StrChunks; 73 | } 74 | 75 | impl StrChunksExt for str { 76 | fn chunks(&self, size: usize) -> StrChunks { 77 | StrChunks(self, size) 78 | } 79 | } 80 | 81 | impl<'s> Iterator for StrChunks<'s> { 82 | type Item = &'s str; 83 | fn next(&mut self) -> Option<&'s str> { 84 | let &mut StrChunks(data, size) = self; 85 | if data.is_empty() { 86 | None 87 | } else { 88 | let ret = Some(&data[..size]); 89 | self.0 = &data[size..]; 90 | ret 91 | } 92 | } 93 | } 94 | 95 | pub trait ReadSeek : ::std::io::Read + ::std::io::Seek { } 96 | impl ReadSeek for T { } 97 | 98 | pub trait Seq { 99 | fn next(&self) -> Self; 100 | } 101 | 102 | macro_rules! impl_seq { 103 | ($($ty:ty)*) => { $( 104 | impl Seq for $ty { 105 | fn next(&self) -> Self { *self + 1 } 106 | } 107 | )* } 108 | } 109 | 110 | impl_seq!(u8 u16 u32 u64 usize); 111 | 112 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] 113 | pub struct SeqGenerator(T); 114 | 115 | impl SeqGenerator { 116 | pub fn new(value: T) -> Self { 117 | SeqGenerator(value) 118 | } 119 | 120 | pub fn get(&mut self) -> T { 121 | let value = self.0.next(); 122 | mem::replace(&mut self.0, value) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README.librespot.md: -------------------------------------------------------------------------------- 1 | # librespot 2 | *librespot* is an open source client library for Spotify. It enables 3 | applications to use Spotify's service, without using the official but 4 | closed-source libspotify. Additionally, it will provide extra features 5 | which are not available in the official library. 6 | 7 | ## Building 8 | Rust 1.15.0 or later is required to build librespot. 9 | 10 | It also requires a C, with portaudio. 11 | 12 | On debian / ubuntu, the following command will install these dependencies : 13 | ```shell 14 | sudo apt-get install build-essential portaudio19-dev 15 | ``` 16 | 17 | On Fedora systems, the following command will install these dependencies : 18 | ```shell 19 | sudo dnf install portaudio-devel make gcc 20 | ``` 21 | 22 | On OS X, using homebrew : 23 | ```shell 24 | brew install portaudio 25 | ``` 26 | 27 | Once you've cloned this repository you can build *librespot* using `cargo`. 28 | ```shell 29 | cargo build --release 30 | ``` 31 | 32 | ## Usage 33 | A sample program implementing a headless Spotify Connect receiver is provided. 34 | Once you've built *librespot*, run it using : 35 | ```shell 36 | target/release/librespot --username USERNAME --cache CACHEDIR --name DEVICENAME 37 | ``` 38 | 39 | ## Discovery mode 40 | *librespot* can be run in discovery mode, in which case no password is required at startup. 41 | For that, simply omit the `--username` argument. 42 | 43 | ## Audio Backends 44 | *librespot* supports various audio backends. Multiple backends can be enabled at compile time by enabling the 45 | corresponding cargo feature. By default, only PortAudio is enabled. 46 | 47 | A specific backend can selected at runtime using the `--backend` switch. 48 | 49 | ```shell 50 | cargo build --features portaudio-backend 51 | target/release/librespot [...] --backend portaudio 52 | ``` 53 | 54 | The following backends are currently available : 55 | - ALSA 56 | - PortAudio 57 | - PulseAudio 58 | 59 | ## Cross-compiling 60 | A cross compilation environment is provided as a docker image. 61 | Build the image from the root of the project with the following command : 62 | 63 | ``` 64 | $ docker build -t librespot-cross -f contrib/Dockerfile . 65 | ``` 66 | 67 | The resulting image can be used to build librespot for linux x86_64, armhf and armel. 68 | The compiled binaries will be located in /tmp/librespot-build 69 | 70 | ``` 71 | docker run -v /tmp/librespot-build:/build librespot-cross 72 | ``` 73 | 74 | If only one architecture is desired, cargo can be invoked directly with the appropriate options : 75 | ```shell 76 | docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features alsa-backend 77 | docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend 78 | docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend 79 | ``` 80 | 81 | ## Development 82 | When developing *librespot*, it is preferable to use Rust nightly, and build it using the following : 83 | ```shell 84 | cargo build --no-default-features --features "nightly portaudio-backend" 85 | ``` 86 | 87 | This produces better compilation error messages than with the default configuration. 88 | 89 | ## Disclaimer 90 | Using this code to connect to Spotify's API is probably forbidden by them. 91 | Use at your own risk. 92 | 93 | ## Contact 94 | Come and hang out on gitter if you need help or want to offer some. 95 | https://gitter.im/sashahilton00/spotify-connect-resources 96 | 97 | ## License 98 | Everything in this repository is licensed under the MIT license. 99 | 100 | -------------------------------------------------------------------------------- /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 | } 79 | 80 | message Goodbye { 81 | optional string reason = 0x1; 82 | } 83 | 84 | message State { 85 | optional string context_uri = 0x2; 86 | optional uint32 index = 0x3; 87 | optional uint32 position_ms = 0x4; 88 | optional PlayStatus status = 0x5; 89 | optional uint64 position_measured_at = 0x7; 90 | optional string context_description = 0x8; 91 | optional bool shuffle = 0xd; 92 | optional bool repeat = 0xe; 93 | optional string last_command_ident = 0x14; 94 | optional uint32 last_command_msgid = 0x15; 95 | optional bool playing_from_fallback = 0x18; 96 | optional uint32 row = 0x19; 97 | optional uint32 playing_track_index = 0x1a; 98 | repeated TrackRef track = 0x1b; 99 | optional Ad ad = 0x1c; 100 | } 101 | 102 | enum PlayStatus { 103 | kPlayStatusStop = 0x0; 104 | kPlayStatusPlay = 0x1; 105 | kPlayStatusPause = 0x2; 106 | kPlayStatusLoading = 0x3; 107 | } 108 | 109 | message TrackRef { 110 | optional bytes gid = 0x1; 111 | optional string uri = 0x2; 112 | optional bool queued = 0x3; 113 | optional string context = 0x4; 114 | } 115 | 116 | message Ad { 117 | optional int32 next = 0x1; 118 | optional bytes ogg_fid = 0x2; 119 | optional bytes image_fid = 0x3; 120 | optional int32 duration = 0x4; 121 | optional string click_url = 0x5; 122 | optional string impression_url = 0x6; 123 | optional string product = 0x7; 124 | optional string advertiser = 0x8; 125 | optional bytes gid = 0x9; 126 | } 127 | 128 | message Metadata { 129 | optional string type = 0x1; 130 | optional string metadata = 0x2; 131 | } 132 | -------------------------------------------------------------------------------- /protocol/build.rs: -------------------------------------------------------------------------------- 1 | use std::io::prelude::*; 2 | use std::fs::File; 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, 42 | 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, 43 | 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, 44 | 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 45 | 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, 46 | 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, 47 | 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 48 | 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, 49 | 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, 50 | 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 51 | 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, 52 | 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, 53 | 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 54 | 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, 55 | 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, 56 | 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 57 | 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, 58 | 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, 59 | 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 60 | 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, 61 | 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, 62 | 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 63 | 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, 64 | 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, 65 | 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 66 | 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, 67 | 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, 68 | 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 69 | 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, 70 | 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, 71 | 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 72 | 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, 73 | 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, 74 | 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, 75 | 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, 76 | 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, 77 | 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 78 | 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, 79 | 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, 80 | 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 81 | 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, 82 | 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, 83 | 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 84 | 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, 85 | 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, 86 | 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 87 | 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, 88 | 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, 89 | 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 90 | 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, 91 | 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, 92 | 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, 93 | 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, 94 | 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, 95 | 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 96 | 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, 97 | 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, 98 | 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, 99 | 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, 100 | 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, 101 | 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 102 | 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, 103 | 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, 104 | 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 105 | ]; 106 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 futures::sync::{BiLock, mpsc}; 3 | use futures::{Poll, Async, Stream}; 4 | use std::collections::HashMap; 5 | use tokio_core::io::EasyBuf; 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, EasyBuf)>, 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(EasyBuf), 30 | } 31 | 32 | #[derive(Clone)] 33 | enum ChannelState { 34 | Header(EasyBuf), 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(EasyBuf::new()), 52 | }; 53 | 54 | (seq, channel) 55 | } 56 | 57 | pub fn dispatch(&self, cmd: u8, mut data: EasyBuf) { 58 | use std::collections::hash_map::Entry; 59 | 60 | let id: u16 = BigEndian::read_u16(data.drain_to(2).as_ref()); 61 | 62 | self.lock(|inner| { 63 | if let Entry::Occupied(entry) = inner.channels.entry(id) { 64 | let _ = entry.get().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.drain_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.drain_to(1).as_ref()[0]; 116 | let header_data = data.drain_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 = EasyBuf; 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 | -------------------------------------------------------------------------------- /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 rpassword; 11 | use serde; 12 | use serde_json; 13 | use std::io::{self, stderr, Read, Write}; 14 | use std::fs::File; 15 | use std::path::Path; 16 | 17 | use protocol::authentication::AuthenticationType; 18 | 19 | #[derive(Debug, Clone)] 20 | #[derive(Serialize, Deserialize)] 21 | pub struct Credentials { 22 | pub username: String, 23 | 24 | #[serde(serialize_with="serialize_protobuf_enum")] 25 | #[serde(deserialize_with="deserialize_protobuf_enum")] 26 | pub auth_type: AuthenticationType, 27 | 28 | #[serde(serialize_with="serialize_base64")] 29 | #[serde(deserialize_with="deserialize_base64")] 30 | pub auth_data: Vec, 31 | } 32 | 33 | impl Credentials { 34 | pub fn with_password(username: String, password: String) -> Credentials { 35 | Credentials { 36 | username: username, 37 | auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, 38 | auth_data: password.into_bytes(), 39 | } 40 | } 41 | 42 | pub fn with_blob(username: String, encrypted_blob: &str, device_id: &str) -> Credentials { 43 | fn read_u8(stream: &mut R) -> io::Result { 44 | let mut data = [0u8]; 45 | try!(stream.read_exact(&mut data)); 46 | Ok(data[0]) 47 | } 48 | 49 | fn read_int(stream: &mut R) -> io::Result { 50 | let lo = try!(read_u8(stream)) as u32; 51 | if lo & 0x80 == 0 { 52 | return Ok(lo); 53 | } 54 | 55 | let hi = try!(read_u8(stream)) as u32; 56 | Ok(lo & 0x7f | hi << 7) 57 | } 58 | 59 | fn read_bytes(stream: &mut R) -> io::Result> { 60 | let length = try!(read_int(stream)); 61 | let mut data = vec![0u8; length as usize]; 62 | try!(stream.read_exact(&mut data)); 63 | 64 | Ok(data) 65 | } 66 | 67 | let encrypted_blob = base64::decode(encrypted_blob).unwrap(); 68 | 69 | let secret = { 70 | let mut data = [0u8; 20]; 71 | let mut h = crypto::sha1::Sha1::new(); 72 | h.input(device_id.as_bytes()); 73 | h.result(&mut data); 74 | data 75 | }; 76 | 77 | let key = { 78 | let mut data = [0u8; 24]; 79 | let mut mac = Hmac::new(Sha1::new(), &secret); 80 | pbkdf2(&mut mac, username.as_bytes(), 0x100, &mut data[0..20]); 81 | 82 | let mut hash = Sha1::new(); 83 | hash.input(&data[0..20]); 84 | hash.result(&mut data[0..20]); 85 | BigEndian::write_u32(&mut data[20..], 20); 86 | data 87 | }; 88 | 89 | let blob = { 90 | // Anyone know what this block mode is ? 91 | let mut data = vec![0u8; encrypted_blob.len()]; 92 | let mut cipher = aes::ecb_decryptor(aes::KeySize::KeySize192, 93 | &key, 94 | crypto::blockmodes::NoPadding); 95 | cipher.decrypt(&mut crypto::buffer::RefReadBuffer::new(&encrypted_blob), 96 | &mut crypto::buffer::RefWriteBuffer::new(&mut data), 97 | true) 98 | .unwrap(); 99 | 100 | let l = encrypted_blob.len(); 101 | for i in 0..l - 0x10 { 102 | data[l - i - 1] ^= data[l - i - 0x11]; 103 | } 104 | 105 | data 106 | }; 107 | 108 | let mut cursor = io::Cursor::new(&blob); 109 | read_u8(&mut cursor).unwrap(); 110 | read_bytes(&mut cursor).unwrap(); 111 | read_u8(&mut cursor).unwrap(); 112 | let auth_type = read_int(&mut cursor).unwrap(); 113 | let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap(); 114 | read_u8(&mut cursor).unwrap(); 115 | let auth_data = read_bytes(&mut cursor).unwrap();; 116 | 117 | Credentials { 118 | username: username, 119 | auth_type: auth_type, 120 | auth_data: auth_data, 121 | } 122 | } 123 | 124 | pub fn from_reader(mut reader: R) -> Credentials { 125 | let mut contents = String::new(); 126 | reader.read_to_string(&mut contents).unwrap(); 127 | 128 | serde_json::from_str(&contents).unwrap() 129 | } 130 | 131 | pub fn from_file>(path: P) -> Option { 132 | File::open(path).ok().map(Credentials::from_reader) 133 | } 134 | 135 | pub fn save_to_writer(&self, writer: &mut W) { 136 | let contents = serde_json::to_string(&self.clone()).unwrap(); 137 | writer.write_all(contents.as_bytes()).unwrap(); 138 | } 139 | 140 | pub fn save_to_file>(&self, path: P) { 141 | let mut file = File::create(path).unwrap(); 142 | self.save_to_writer(&mut file) 143 | } 144 | } 145 | 146 | fn serialize_protobuf_enum(v: &T, ser: S) -> Result 147 | where T: ProtobufEnum, S: serde::Serializer { 148 | 149 | serde::Serialize::serialize(&v.value(), ser) 150 | } 151 | 152 | fn deserialize_protobuf_enum(de: D) -> Result 153 | where T: ProtobufEnum, D: serde::Deserializer { 154 | 155 | let v : i32 = try!(serde::Deserialize::deserialize(de)); 156 | T::from_i32(v).ok_or_else(|| serde::de::Error::custom("Invalid enum value")) 157 | } 158 | 159 | fn serialize_base64(v: &T, ser: S) -> Result 160 | where T: AsRef<[u8]>, S: serde::Serializer { 161 | 162 | serde::Serialize::serialize(&base64::encode(v.as_ref()), ser) 163 | } 164 | 165 | fn deserialize_base64(de: D) -> Result, D::Error> 166 | where D: serde::Deserializer { 167 | 168 | let v : String = try!(serde::Deserialize::deserialize(de)); 169 | base64::decode(&v).map_err(|e| serde::de::Error::custom(e.to_string())) 170 | } 171 | 172 | pub fn get_credentials(username: Option, password: Option, 173 | cached_credentials: Option) 174 | -> Option 175 | { 176 | match (username, password, cached_credentials) { 177 | 178 | (Some(username), Some(password), _) 179 | => Some(Credentials::with_password(username, password)), 180 | 181 | (Some(ref username), _, Some(ref credentials)) 182 | if *username == credentials.username => Some(credentials.clone()), 183 | 184 | (Some(username), None, _) => { 185 | write!(stderr(), "Password for {}: ", username).unwrap(); 186 | stderr().flush().unwrap(); 187 | let password = rpassword::read_password().unwrap(); 188 | Some(Credentials::with_password(username.clone(), password)) 189 | } 190 | 191 | (None, _, Some(credentials)) 192 | => Some(credentials), 193 | 194 | (None, _, None) => None, 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /core/src/connection/handshake.rs: -------------------------------------------------------------------------------- 1 | use crypto::sha1::Sha1; 2 | use crypto::hmac::Hmac; 3 | use crypto::mac::Mac;use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; 4 | use protobuf::{self, Message, MessageStatic}; 5 | use rand::thread_rng; 6 | use std::io::{self, Read, Write}; 7 | use std::marker::PhantomData; 8 | use tokio_core::io::{Io, Framed, write_all, WriteAll, read_exact, ReadExact, Window}; 9 | use futures::{Poll, Async, Future}; 10 | 11 | use diffie_hellman::DHLocalKeys; 12 | use protocol; 13 | use protocol::keyexchange::{ClientHello, APResponseMessage, ClientResponsePlaintext}; 14 | use util; 15 | use super::codec::APCodec; 16 | 17 | pub struct Handshake { 18 | keys: DHLocalKeys, 19 | state: HandshakeState, 20 | } 21 | 22 | enum HandshakeState { 23 | ClientHello(WriteAll>), 24 | APResponse(RecvPacket), 25 | ClientResponse(Option, WriteAll>), 26 | } 27 | 28 | pub fn handshake(connection: T) -> Handshake { 29 | let local_keys = DHLocalKeys::random(&mut thread_rng()); 30 | let client_hello = client_hello(connection, local_keys.public_key()); 31 | 32 | Handshake { 33 | keys: local_keys, 34 | state: HandshakeState::ClientHello(client_hello), 35 | } 36 | } 37 | 38 | impl Future for Handshake { 39 | type Item = Framed; 40 | type Error = io::Error; 41 | 42 | fn poll(&mut self) -> Poll { 43 | use self::HandshakeState::*; 44 | loop { 45 | self.state = match self.state { 46 | ClientHello(ref mut write) => { 47 | let (connection, accumulator) = try_ready!(write.poll()); 48 | 49 | let read = recv_packet(connection, accumulator); 50 | APResponse(read) 51 | } 52 | 53 | APResponse(ref mut read) => { 54 | let (connection, message, accumulator) = try_ready!(read.poll()); 55 | let remote_key = message.get_challenge() 56 | .get_login_crypto_challenge() 57 | .get_diffie_hellman() 58 | .get_gs() 59 | .to_owned(); 60 | 61 | let shared_secret = self.keys.shared_secret(&remote_key); 62 | let (challenge, send_key, recv_key) = compute_keys(&shared_secret, 63 | &accumulator); 64 | let codec = APCodec::new(&send_key, &recv_key); 65 | 66 | let write = client_response(connection, challenge); 67 | ClientResponse(Some(codec), write) 68 | } 69 | 70 | ClientResponse(ref mut codec, ref mut write) => { 71 | let (connection, _) = try_ready!(write.poll()); 72 | let codec = codec.take().unwrap(); 73 | let framed = connection.framed(codec); 74 | return Ok(Async::Ready(framed)); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | fn client_hello(connection: T, gc: Vec) -> WriteAll> { 82 | let packet = protobuf_init!(ClientHello::new(), { 83 | build_info => { 84 | product: protocol::keyexchange::Product::PRODUCT_PARTNER, 85 | platform: protocol::keyexchange::Platform::PLATFORM_LINUX_X86, 86 | version: 0x10800000000, 87 | }, 88 | cryptosuites_supported => [ 89 | protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON, 90 | ], 91 | login_crypto_hello.diffie_hellman => { 92 | gc: gc, 93 | server_keys_known: 1, 94 | }, 95 | client_nonce: util::rand_vec(&mut thread_rng(), 0x10), 96 | padding: vec![0x1e], 97 | }); 98 | 99 | let mut buffer = vec![0, 4]; 100 | let size = 2 + 4 + packet.compute_size(); 101 | buffer.write_u32::(size).unwrap(); 102 | packet.write_to_vec(&mut buffer).unwrap(); 103 | 104 | write_all(connection, buffer) 105 | } 106 | 107 | fn client_response(connection: T, challenge: Vec) -> WriteAll> { 108 | let packet = protobuf_init!(ClientResponsePlaintext::new(), { 109 | login_crypto_response.diffie_hellman => { 110 | hmac: challenge 111 | }, 112 | pow_response => {}, 113 | crypto_response => {}, 114 | }); 115 | 116 | let mut buffer = vec![]; 117 | let size = 4 + packet.compute_size(); 118 | buffer.write_u32::(size).unwrap(); 119 | packet.write_to_vec(&mut buffer).unwrap(); 120 | 121 | write_all(connection, buffer) 122 | } 123 | 124 | enum RecvPacket { 125 | Header(ReadExact>>, PhantomData), 126 | Body(ReadExact>>, PhantomData), 127 | } 128 | 129 | fn recv_packet(connection: T, acc: Vec) -> RecvPacket 130 | where T: Read, 131 | M: MessageStatic 132 | { 133 | RecvPacket::Header(read_into_accumulator(connection, 4, acc), PhantomData) 134 | } 135 | 136 | impl Future for RecvPacket 137 | where T: Read, 138 | M: MessageStatic 139 | { 140 | type Item = (T, M, Vec); 141 | type Error = io::Error; 142 | 143 | fn poll(&mut self) -> Poll { 144 | use self::RecvPacket::*; 145 | loop { 146 | *self = match *self { 147 | Header(ref mut read, _) => { 148 | let (connection, header) = try_ready!(read.poll()); 149 | let size = BigEndian::read_u32(header.as_ref()) as usize; 150 | 151 | let acc = header.into_inner(); 152 | let read = read_into_accumulator(connection, size - 4, acc); 153 | RecvPacket::Body(read, PhantomData) 154 | } 155 | 156 | Body(ref mut read, _) => { 157 | let (connection, data) = try_ready!(read.poll()); 158 | let message = protobuf::parse_from_bytes(data.as_ref()).unwrap(); 159 | 160 | let acc = data.into_inner(); 161 | return Ok(Async::Ready((connection, message, acc))); 162 | } 163 | } 164 | } 165 | } 166 | } 167 | 168 | fn read_into_accumulator(connection: T, size: usize, mut acc: Vec) -> ReadExact>> { 169 | let offset = acc.len(); 170 | acc.resize(offset + size, 0); 171 | 172 | let mut window = Window::new(acc); 173 | window.set_start(offset); 174 | 175 | read_exact(connection, window) 176 | } 177 | 178 | fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec, Vec, Vec) { 179 | let mut data = Vec::with_capacity(0x64); 180 | let mut mac = Hmac::new(Sha1::new(), &shared_secret); 181 | 182 | for i in 1..6 { 183 | mac.input(packets); 184 | mac.input(&[i]); 185 | data.extend_from_slice(&mac.result().code()); 186 | mac.reset(); 187 | } 188 | 189 | mac = Hmac::new(Sha1::new(), &data[..0x14]); 190 | mac.input(packets); 191 | 192 | (mac.result().code().to_vec(), data[0x14..0x34].to_vec(), data[0x34..0x54].to_vec()) 193 | } 194 | -------------------------------------------------------------------------------- /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, BoxFuture}; 12 | use linear_map::LinearMap; 13 | 14 | use core::mercury::MercuryError; 15 | use core::session::Session; 16 | use core::util::{SpotifyId, FileId, StrChunksExt}; 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 I: IntoIterator 26 | { 27 | let mut forbidden = "".to_string(); 28 | let mut has_forbidden = false; 29 | 30 | let mut allowed = "".to_string(); 31 | let mut has_allowed = false; 32 | 33 | let rs = restrictions.into_iter().filter(|r| 34 | r.get_catalogue_str().contains(&catalogue.to_owned()) 35 | ); 36 | 37 | for r in rs { 38 | if r.has_countries_forbidden() { 39 | forbidden.push_str(r.get_countries_forbidden()); 40 | has_forbidden = true; 41 | } 42 | 43 | if r.has_countries_allowed() { 44 | allowed.push_str(r.get_countries_allowed()); 45 | has_allowed = true; 46 | } 47 | } 48 | 49 | (has_forbidden || has_allowed) && 50 | (!has_forbidden || !countrylist_contains(forbidden.as_str(), country)) && 51 | (!has_allowed || countrylist_contains(allowed.as_str(), country)) 52 | } 53 | 54 | pub trait Metadata : Send + Sized + 'static { 55 | type Message: protobuf::MessageStatic; 56 | 57 | fn base_url() -> &'static str; 58 | fn parse(msg: &Self::Message, session: &Session) -> Self; 59 | 60 | fn get(session: &Session, id: SpotifyId) -> BoxFuture { 61 | let uri = format!("{}/{}", Self::base_url(), id.to_base16()); 62 | let request = session.mercury().get(uri); 63 | 64 | let session = session.clone(); 65 | request.and_then(move |response| { 66 | let data = response.payload.first().expect("Empty payload"); 67 | let msg: Self::Message = protobuf::parse_from_bytes(data).unwrap(); 68 | 69 | Ok(Self::parse(&msg, &session)) 70 | }).boxed() 71 | } 72 | } 73 | 74 | #[derive(Debug, Clone)] 75 | pub struct Track { 76 | pub id: SpotifyId, 77 | pub name: String, 78 | pub album: SpotifyId, 79 | pub artists: Vec, 80 | pub files: LinearMap, 81 | pub alternatives: Vec, 82 | pub available: bool, 83 | } 84 | 85 | #[derive(Debug, Clone)] 86 | pub struct Album { 87 | pub id: SpotifyId, 88 | pub name: String, 89 | pub artists: Vec, 90 | pub tracks: Vec, 91 | pub covers: Vec, 92 | } 93 | 94 | #[derive(Debug, Clone)] 95 | pub struct Artist { 96 | pub id: SpotifyId, 97 | pub name: String, 98 | pub top_tracks: Vec, 99 | } 100 | 101 | impl Metadata for Track { 102 | type Message = protocol::metadata::Track; 103 | 104 | fn base_url() -> &'static str { 105 | "hm://metadata/3/track" 106 | } 107 | 108 | fn parse(msg: &Self::Message, session: &Session) -> Self { 109 | let country = session.country(); 110 | 111 | let artists = msg.get_artist() 112 | .iter() 113 | .filter(|artist| artist.has_gid()) 114 | .map(|artist| SpotifyId::from_raw(artist.get_gid())) 115 | .collect::>(); 116 | 117 | let files = msg.get_file() 118 | .iter() 119 | .filter(|file| file.has_file_id()) 120 | .map(|file| { 121 | let mut dst = [0u8; 20]; 122 | dst.clone_from_slice(file.get_file_id()); 123 | (file.get_format(), FileId(dst)) 124 | }) 125 | .collect(); 126 | 127 | Track { 128 | id: SpotifyId::from_raw(msg.get_gid()), 129 | name: msg.get_name().to_owned(), 130 | album: SpotifyId::from_raw(msg.get_album().get_gid()), 131 | artists: artists, 132 | files: files, 133 | alternatives: msg.get_alternative() 134 | .iter() 135 | .map(|alt| SpotifyId::from_raw(alt.get_gid())) 136 | .collect(), 137 | available: parse_restrictions(msg.get_restriction(), 138 | &country, 139 | "premium"), 140 | } 141 | } 142 | } 143 | 144 | impl Metadata for Album { 145 | type Message = protocol::metadata::Album; 146 | 147 | fn base_url() -> &'static str { 148 | "hm://metadata/3/album" 149 | } 150 | 151 | fn parse(msg: &Self::Message, _: &Session) -> Self { 152 | let artists = msg.get_artist() 153 | .iter() 154 | .filter(|artist| artist.has_gid()) 155 | .map(|artist| SpotifyId::from_raw(artist.get_gid())) 156 | .collect::>(); 157 | 158 | let tracks = msg.get_disc() 159 | .iter() 160 | .flat_map(|disc| disc.get_track()) 161 | .filter(|track| track.has_gid()) 162 | .map(|track| SpotifyId::from_raw(track.get_gid())) 163 | .collect::>(); 164 | 165 | let covers = msg.get_cover_group() 166 | .get_image() 167 | .iter() 168 | .filter(|image| image.has_file_id()) 169 | .map(|image| { 170 | let mut dst = [0u8; 20]; 171 | dst.clone_from_slice(image.get_file_id()); 172 | FileId(dst) 173 | }) 174 | .collect::>(); 175 | 176 | Album { 177 | id: SpotifyId::from_raw(msg.get_gid()), 178 | name: msg.get_name().to_owned(), 179 | artists: artists, 180 | tracks: tracks, 181 | covers: covers, 182 | } 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 | Some(tracks) => { 201 | tracks.get_track() 202 | .iter() 203 | .filter(|track| track.has_gid()) 204 | .map(|track| SpotifyId::from_raw(track.get_gid())) 205 | .collect::>() 206 | }, 207 | None => Vec::new() 208 | }; 209 | 210 | 211 | Artist { 212 | id: SpotifyId::from_raw(msg.get_gid()), 213 | name: msg.get_name().to_owned(), 214 | top_tracks: top_tracks 215 | } 216 | } 217 | } 218 | 219 | -------------------------------------------------------------------------------- /core/src/mercury/mod.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ByteOrder}; 2 | use futures::sync::{oneshot, mpsc}; 3 | use futures::{Async, Poll, BoxFuture, Future}; 4 | use protobuf; 5 | use protocol; 6 | use std::collections::HashMap; 7 | use std::mem; 8 | use tokio_core::io::EasyBuf; 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 | pub fn request(&self, req: MercuryRequest) 55 | -> MercuryFuture 56 | { 57 | let (tx, rx) = oneshot::channel(); 58 | 59 | let pending = MercuryPending { 60 | parts: Vec::new(), 61 | partial: None, 62 | callback: Some(tx), 63 | }; 64 | 65 | let seq = self.next_seq(); 66 | self.lock(|inner| inner.pending.insert(seq.clone(), pending)); 67 | 68 | let cmd = req.method.command(); 69 | let data = req.encode(&seq); 70 | 71 | self.session().send_packet(cmd, data); 72 | MercuryFuture(rx) 73 | } 74 | 75 | pub fn get>(&self, uri: T) 76 | -> MercuryFuture 77 | { 78 | self.request(MercuryRequest { 79 | method: MercuryMethod::GET, 80 | uri: uri.into(), 81 | content_type: None, 82 | payload: Vec::new(), 83 | }) 84 | } 85 | 86 | pub fn send>(&self, uri: T, data: Vec) 87 | -> MercuryFuture 88 | { 89 | self.request(MercuryRequest { 90 | method: MercuryMethod::SEND, 91 | uri: uri.into(), 92 | content_type: None, 93 | payload: vec![data], 94 | }) 95 | } 96 | 97 | pub fn sender>(&self, uri: T) -> MercurySender { 98 | MercurySender::new(self.clone(), uri.into()) 99 | } 100 | 101 | pub fn subscribe>(&self, uri: T) 102 | -> BoxFuture, MercuryError> 103 | { 104 | let uri = uri.into(); 105 | let request = self.request(MercuryRequest { 106 | method: MercuryMethod::SUB, 107 | uri: uri.clone(), 108 | content_type: None, 109 | payload: Vec::new(), 110 | }); 111 | 112 | let manager = self.clone(); 113 | request.map(move |response| { 114 | let (tx, rx) = mpsc::unbounded(); 115 | 116 | manager.lock(move |inner| { 117 | debug!("subscribed uri={} count={}", uri, response.payload.len()); 118 | if response.payload.len() > 0 { 119 | // Old subscription protocol, watch the provided list of URIs 120 | for sub in response.payload { 121 | let mut sub : protocol::pubsub::Subscription 122 | = protobuf::parse_from_bytes(&sub).unwrap(); 123 | let sub_uri = sub.take_uri(); 124 | 125 | debug!("subscribed sub_uri={}", sub_uri); 126 | 127 | inner.subscriptions.push((sub_uri, tx.clone())); 128 | } 129 | } else { 130 | // New subscription protocol, watch the requested URI 131 | inner.subscriptions.push((uri, tx)); 132 | } 133 | }); 134 | 135 | rx 136 | }).boxed() 137 | } 138 | 139 | pub fn dispatch(&self, cmd: u8, mut data: EasyBuf) { 140 | let seq_len = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize; 141 | let seq = data.drain_to(seq_len).as_ref().to_owned(); 142 | 143 | let flags = data.drain_to(1).as_ref()[0]; 144 | let count = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize; 145 | 146 | let pending = self.lock(|inner| inner.pending.remove(&seq)); 147 | 148 | let mut pending = match pending { 149 | Some(pending) => pending, 150 | None if cmd == 0xb5 => { 151 | MercuryPending { 152 | parts: Vec::new(), 153 | partial: None, 154 | callback: None, 155 | } 156 | } 157 | None => { 158 | warn!("Ignore seq {:?} cmd {:x}", seq, cmd); 159 | return; 160 | } 161 | }; 162 | 163 | for i in 0..count { 164 | let mut part = Self::parse_part(&mut data); 165 | if let Some(mut partial) = mem::replace(&mut pending.partial, None) { 166 | partial.extend_from_slice(&part); 167 | part = partial; 168 | } 169 | 170 | if i == count - 1 && (flags == 2) { 171 | pending.partial = Some(part) 172 | } else { 173 | pending.parts.push(part); 174 | } 175 | } 176 | 177 | if flags == 0x1 { 178 | self.complete_request(cmd, pending); 179 | } else { 180 | self.lock(move |inner| inner.pending.insert(seq, pending)); 181 | } 182 | } 183 | 184 | fn parse_part(data: &mut EasyBuf) -> Vec { 185 | let size = BigEndian::read_u16(data.drain_to(2).as_ref()) as usize; 186 | data.drain_to(size).as_ref().to_owned() 187 | } 188 | 189 | fn complete_request(&self, cmd: u8, mut pending: MercuryPending) { 190 | let header_data = pending.parts.remove(0); 191 | let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap(); 192 | 193 | let response = MercuryResponse { 194 | uri: header.get_uri().to_owned(), 195 | status_code: header.get_status_code(), 196 | payload: pending.parts, 197 | }; 198 | 199 | if response.status_code >= 400 { 200 | warn!("error {} for uri {}", response.status_code, &response.uri); 201 | if let Some(cb) = pending.callback { 202 | cb.complete(Err(MercuryError)); 203 | } 204 | } else { 205 | if cmd == 0xb5 { 206 | self.lock(|inner| { 207 | let mut found = false; 208 | inner.subscriptions.retain(|&(ref prefix, ref sub)| { 209 | if response.uri.starts_with(prefix) { 210 | found = true; 211 | 212 | // if send fails, remove from list of subs 213 | // TODO: send unsub message 214 | sub.send(response.clone()).is_ok() 215 | } else { 216 | // URI doesn't match 217 | true 218 | } 219 | }); 220 | 221 | if !found { 222 | debug!("unknown subscription uri={}", response.uri); 223 | } 224 | }) 225 | } else if let Some(cb) = pending.callback { 226 | cb.complete(Ok(response)); 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /core/src/session.rs: -------------------------------------------------------------------------------- 1 | use crypto::digest::Digest; 2 | use crypto::sha1::Sha1; 3 | use futures::sync::mpsc; 4 | use futures::{Future, Stream, BoxFuture, IntoFuture, Poll, Async}; 5 | use std::io; 6 | use std::sync::{RwLock, Arc, Weak}; 7 | use tokio_core::io::EasyBuf; 8 | use tokio_core::reactor::{Handle, Remote}; 9 | use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering}; 10 | 11 | use apresolve::apresolve_or_fallback; 12 | use authentication::Credentials; 13 | use cache::Cache; 14 | use component::Lazy; 15 | use connection; 16 | use config::SessionConfig; 17 | 18 | use audio_key::AudioKeyManager; 19 | use channel::ChannelManager; 20 | use mercury::MercuryManager; 21 | 22 | pub struct SessionData { 23 | country: String, 24 | canonical_username: String, 25 | } 26 | 27 | pub struct SessionInternal { 28 | config: SessionConfig, 29 | data: RwLock, 30 | 31 | tx_connection: mpsc::UnboundedSender<(u8, Vec)>, 32 | 33 | audio_key: Lazy, 34 | channel: Lazy, 35 | mercury: Lazy, 36 | cache: Option>, 37 | 38 | handle: Remote, 39 | 40 | session_id: usize, 41 | } 42 | 43 | static SESSION_COUNTER : AtomicUsize = ATOMIC_USIZE_INIT; 44 | 45 | #[derive(Clone)] 46 | pub struct Session(pub Arc); 47 | 48 | pub fn device_id(name: &str) -> String { 49 | let mut h = Sha1::new(); 50 | h.input_str(name); 51 | h.result_str() 52 | } 53 | 54 | impl Session { 55 | pub fn connect(config: SessionConfig, credentials: Credentials, 56 | cache: Option, handle: Handle) 57 | -> Box> 58 | { 59 | let access_point = apresolve_or_fallback::(&handle); 60 | 61 | 62 | let handle_ = handle.clone(); 63 | let connection = access_point.and_then(move |addr| { 64 | info!("Connecting to AP \"{}\"", addr); 65 | connection::connect::<&str>(&addr, &handle_) 66 | }); 67 | 68 | let device_id = config.device_id.clone(); 69 | let authentication = connection.and_then(move |connection| { 70 | connection::authenticate(connection, credentials, device_id) 71 | }); 72 | 73 | let result = authentication.map(move |(transport, reusable_credentials)| { 74 | info!("Authenticated as \"{}\" !", reusable_credentials.username); 75 | if let Some(ref cache) = cache { 76 | cache.save_credentials(&reusable_credentials); 77 | } 78 | 79 | let (session, task) = Session::create( 80 | &handle, transport, config, cache, reusable_credentials.username.clone() 81 | ); 82 | 83 | handle.spawn(task.map_err(|e| panic!(e))); 84 | 85 | session 86 | }); 87 | 88 | Box::new(result) 89 | } 90 | 91 | fn create(handle: &Handle, transport: connection::Transport, 92 | config: SessionConfig, cache: Option, username: String) 93 | -> (Session, BoxFuture<(), io::Error>) 94 | { 95 | let (sink, stream) = transport.split(); 96 | 97 | let (sender_tx, sender_rx) = mpsc::unbounded(); 98 | let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); 99 | 100 | debug!("new Session[{}]", session_id); 101 | 102 | let session = Session(Arc::new(SessionInternal { 103 | config: config, 104 | data: RwLock::new(SessionData { 105 | country: String::new(), 106 | canonical_username: username, 107 | }), 108 | 109 | tx_connection: sender_tx, 110 | 111 | cache: cache.map(Arc::new), 112 | 113 | audio_key: Lazy::new(), 114 | channel: Lazy::new(), 115 | mercury: Lazy::new(), 116 | 117 | handle: handle.remote().clone(), 118 | 119 | session_id: session_id, 120 | })); 121 | 122 | let sender_task = sender_rx 123 | .map_err(|e| -> io::Error { panic!(e) }) 124 | .forward(sink).map(|_| ()); 125 | let receiver_task = DispatchTask(stream, session.weak()); 126 | 127 | let task = (receiver_task, sender_task).into_future() 128 | .map(|((), ())| ()).boxed(); 129 | 130 | (session, task) 131 | } 132 | 133 | pub fn audio_key(&self) -> &AudioKeyManager { 134 | self.0.audio_key.get(|| AudioKeyManager::new(self.weak())) 135 | } 136 | 137 | pub fn channel(&self) -> &ChannelManager { 138 | self.0.channel.get(|| ChannelManager::new(self.weak())) 139 | } 140 | 141 | pub fn mercury(&self) -> &MercuryManager { 142 | self.0.mercury.get(|| MercuryManager::new(self.weak())) 143 | } 144 | 145 | pub fn spawn(&self, f: F) 146 | where F: FnOnce(&Handle) -> R + Send + 'static, 147 | R: IntoFuture, 148 | R::Future: 'static 149 | { 150 | self.0.handle.spawn(f) 151 | } 152 | 153 | fn debug_info(&self) { 154 | debug!("Session[{}] strong={} weak={}", 155 | self.0.session_id, Arc::strong_count(&self.0), Arc::weak_count(&self.0)); 156 | } 157 | 158 | #[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))] 159 | fn dispatch(&self, cmd: u8, data: EasyBuf) { 160 | match cmd { 161 | 0x4 => { 162 | self.debug_info(); 163 | self.send_packet(0x49, data.as_ref().to_owned()); 164 | }, 165 | 0x4a => (), 166 | 0x1b => { 167 | let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); 168 | info!("Country: {:?}", country); 169 | self.0.data.write().unwrap().country = country; 170 | } 171 | 172 | 0x9 | 0xa => self.channel().dispatch(cmd, data), 173 | 0xd | 0xe => self.audio_key().dispatch(cmd, data), 174 | 0xb2...0xb6 => self.mercury().dispatch(cmd, data), 175 | _ => (), 176 | } 177 | } 178 | 179 | pub fn send_packet(&self, cmd: u8, data: Vec) { 180 | self.0.tx_connection.send((cmd, data)).unwrap(); 181 | } 182 | 183 | pub fn cache(&self) -> Option<&Arc> { 184 | self.0.cache.as_ref() 185 | } 186 | 187 | pub fn config(&self) -> &SessionConfig { 188 | &self.0.config 189 | } 190 | 191 | pub fn username(&self) -> String { 192 | self.0.data.read().unwrap().canonical_username.clone() 193 | } 194 | 195 | pub fn country(&self) -> String { 196 | self.0.data.read().unwrap().country.clone() 197 | } 198 | 199 | pub fn device_id(&self) -> &str { 200 | &self.config().device_id 201 | } 202 | 203 | pub fn weak(&self) -> SessionWeak { 204 | SessionWeak(Arc::downgrade(&self.0)) 205 | } 206 | 207 | pub fn session_id(&self) -> usize { 208 | self.0.session_id 209 | } 210 | } 211 | 212 | #[derive(Clone)] 213 | pub struct SessionWeak(pub Weak); 214 | 215 | impl SessionWeak { 216 | pub fn try_upgrade(&self) -> Option { 217 | self.0.upgrade().map(Session) 218 | } 219 | 220 | pub fn upgrade(&self) -> Session { 221 | self.try_upgrade().expect("Session died") 222 | } 223 | } 224 | 225 | impl Drop for SessionInternal { 226 | fn drop(&mut self) { 227 | debug!("drop Session[{}]", self.session_id); 228 | } 229 | } 230 | 231 | struct DispatchTask(S, SessionWeak) 232 | where S: Stream; 233 | 234 | impl Future for DispatchTask 235 | where S: Stream 236 | { 237 | type Item = (); 238 | type Error = S::Error; 239 | 240 | fn poll(&mut self) -> Poll { 241 | let session = match self.1.try_upgrade() { 242 | Some(session) => session, 243 | None => { 244 | return Ok(Async::Ready(())) 245 | }, 246 | }; 247 | 248 | loop { 249 | let (cmd, data) = try_ready!(self.0.poll()).expect("connection closed"); 250 | session.dispatch(cmd, data); 251 | } 252 | } 253 | } 254 | 255 | impl Drop for DispatchTask 256 | where S: Stream 257 | { 258 | fn drop(&mut self) { 259 | debug!("drop Dispatch"); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // TODO: many items from tokio-core::io have been deprecated in favour of tokio-io 2 | #![allow(deprecated)] 3 | 4 | #[macro_use] extern crate log; 5 | extern crate env_logger; 6 | extern crate futures; 7 | extern crate getopts; 8 | extern crate librespot; 9 | extern crate tokio_core; 10 | extern crate tokio_signal; 11 | 12 | use env_logger::LogBuilder; 13 | use futures::{Future, Async, Poll, Stream}; 14 | use std::env; 15 | use std::io::{self, stderr, Write}; 16 | use std::path::PathBuf; 17 | use std::process::exit; 18 | use std::str::FromStr; 19 | use tokio_core::reactor::{Handle, Core}; 20 | use tokio_core::io::IoStream; 21 | use std::mem; 22 | 23 | use librespot::core::authentication::{get_credentials, Credentials}; 24 | use librespot::core::cache::Cache; 25 | use librespot::core::config::{DeviceType, SessionConfig, ConnectConfig}; 26 | use librespot::core::session::Session; 27 | use librespot::core::version; 28 | 29 | use librespot::scrobbler::ScrobblerConfig; 30 | use librespot::spirc::{Spirc, SpircTask}; 31 | 32 | fn usage(program: &str, opts: &getopts::Options) -> String { 33 | let brief = format!("Usage: {} [options]", program); 34 | opts.usage(&brief) 35 | } 36 | 37 | fn setup_logging(verbose: bool) { 38 | let mut builder = LogBuilder::new(); 39 | match env::var("RUST_LOG") { 40 | Ok(config) => { 41 | builder.parse(&config); 42 | builder.init().unwrap(); 43 | 44 | if verbose { 45 | warn!("`--verbose` flag overidden by `RUST_LOG` environment variable"); 46 | } 47 | } 48 | Err(_) => { 49 | if verbose { 50 | builder.parse("mdns=info,librespot=trace"); 51 | } else { 52 | builder.parse("mdns=info,librespot=info"); 53 | } 54 | builder.init().unwrap(); 55 | } 56 | } 57 | } 58 | 59 | struct Setup { 60 | cache: Option, 61 | session_config: SessionConfig, 62 | connect_config: ConnectConfig, 63 | credentials: Option, 64 | scrobbler_config: ScrobblerConfig 65 | } 66 | 67 | fn setup(args: &[String]) -> Setup { 68 | let mut opts = getopts::Options::new(); 69 | opts.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE") 70 | .optopt("n", "name", "Device name (defaults to Scrobbler)", "NAME") 71 | .optopt("", "device-type", "Displayed device type", "DEVICE_TYPE") 72 | .optflag("v", "verbose", "Enable verbose output") 73 | .optopt("", "spotify-username", "Username to sign in with", "USERNAME") 74 | .optopt("", "spotify-password", "Password", "PASSWORD") 75 | .optopt("", "lastfm-username", "Last.fm Username", "LASTFM_USERNAME") 76 | .optopt("", "lastfm-password", "Last.fm Password", "LASTFM_PASSWORD") 77 | .optopt("", "lastfm-api-key", "Last.fm API Key", "API_KEY") 78 | .optopt("", "lastfm-api-secret", "Last.fm API Secret", "SECRET"); 79 | 80 | let matches = match opts.parse(&args[1..]) { 81 | Ok(m) => m, 82 | Err(f) => { 83 | writeln!(stderr(), "error: {}\n{}", f.to_string(), usage(&args[0], &opts)).unwrap(); 84 | exit(1); 85 | } 86 | }; 87 | 88 | let verbose = matches.opt_present("verbose"); 89 | setup_logging(verbose); 90 | 91 | info!("librespot {} ({}). Built on {}. Build ID: {}", 92 | version::short_sha(), 93 | version::commit_date(), 94 | version::short_now(), 95 | version::build_id()); 96 | 97 | let name = matches.opt_str("name").unwrap_or(String::from("Scrobbler")); 98 | let use_audio_cache = false; 99 | 100 | let cache = matches.opt_str("c").map(|cache_location| { 101 | Cache::new(PathBuf::from(cache_location), use_audio_cache) 102 | }); 103 | 104 | let cached_credentials = cache.as_ref().and_then(Cache::credentials); 105 | let credentials = get_credentials(matches.opt_str("spotify-username"), 106 | matches.opt_str("spotify-password"), 107 | cached_credentials); 108 | 109 | let session_config = { 110 | let device_id = librespot::core::session::device_id(&name); 111 | 112 | SessionConfig { 113 | user_agent: version::version_string(), 114 | device_id: device_id, 115 | } 116 | }; 117 | 118 | let api_key = matches.opt_str("lastfm-api-key").expect("Invalid Last.fm API key"); 119 | let api_secret = matches.opt_str("lastfm-api-secret").expect("Invalid Last.fm API secret"); 120 | let username = matches.opt_str("lastfm-username").expect("Invalid Last.fm username"); 121 | let password = matches.opt_str("lastfm-password").expect("Invalid Last.fm password"); 122 | 123 | let scrobbler_config = ScrobblerConfig { 124 | api_key: api_key, 125 | api_secret: api_secret, 126 | username: username, 127 | password: password, 128 | }; 129 | 130 | let connect_config = { 131 | let device_type = matches.opt_str("device-type").as_ref() 132 | .map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type")) 133 | .unwrap_or(DeviceType::default()); 134 | 135 | ConnectConfig { 136 | name: name, 137 | device_type: device_type, 138 | } 139 | }; 140 | 141 | Setup { 142 | cache: cache, 143 | session_config: session_config, 144 | connect_config: connect_config, 145 | credentials: credentials, 146 | scrobbler_config: scrobbler_config 147 | } 148 | } 149 | 150 | struct Main { 151 | cache: Option, 152 | session_config: SessionConfig, 153 | connect_config: ConnectConfig, 154 | handle: Handle, 155 | 156 | signal: IoStream<()>, 157 | 158 | spirc: Option, 159 | spirc_task: Option, 160 | connect: Box>, 161 | 162 | scrobbler_config: ScrobblerConfig, 163 | 164 | shutdown: bool, 165 | } 166 | 167 | impl Main { 168 | fn new(handle: Handle, setup: Setup) -> Main { 169 | let mut task = Main { 170 | handle: handle.clone(), 171 | cache: setup.cache, 172 | session_config: setup.session_config, 173 | connect_config: setup.connect_config, 174 | 175 | connect: Box::new(futures::future::empty()), 176 | spirc: None, 177 | spirc_task: None, 178 | shutdown: false, 179 | signal: tokio_signal::ctrl_c(&handle).flatten_stream().boxed(), 180 | scrobbler_config: setup.scrobbler_config 181 | }; 182 | 183 | if let Some(credentials) = setup.credentials { 184 | task.credentials(credentials); 185 | } 186 | 187 | task 188 | } 189 | 190 | fn credentials(&mut self, credentials: Credentials) { 191 | let config = self.session_config.clone(); 192 | let handle = self.handle.clone(); 193 | 194 | let connection = Session::connect(config, credentials, self.cache.clone(), handle); 195 | 196 | self.connect = connection; 197 | self.spirc = None; 198 | let task = mem::replace(&mut self.spirc_task, None); 199 | if let Some(task) = task { 200 | self.handle.spawn(task); 201 | } 202 | } 203 | } 204 | 205 | impl Future for Main { 206 | type Item = (); 207 | type Error = (); 208 | 209 | fn poll(&mut self) -> Poll<(), ()> { 210 | loop { 211 | let mut progress = false; 212 | 213 | if let Async::Ready(session) = self.connect.poll().unwrap() { 214 | self.connect = Box::new(futures::future::empty()); 215 | let connect_config = self.connect_config.clone(); 216 | 217 | let (spirc, spirc_task) = Spirc::new(connect_config, session, self.scrobbler_config.clone()); 218 | self.spirc = Some(spirc); 219 | self.spirc_task = Some(spirc_task); 220 | 221 | progress = true; 222 | } 223 | 224 | if let Async::Ready(Some(())) = self.signal.poll().unwrap() { 225 | if !self.shutdown { 226 | if let Some(ref spirc) = self.spirc { 227 | spirc.shutdown(); 228 | } 229 | self.shutdown = true; 230 | } else { 231 | return Ok(Async::Ready(())); 232 | } 233 | 234 | progress = true; 235 | } 236 | 237 | if let Some(ref mut spirc_task) = self.spirc_task { 238 | if let Async::Ready(()) = spirc_task.poll().unwrap() { 239 | if self.shutdown { 240 | return Ok(Async::Ready(())); 241 | } else { 242 | panic!("Spirc shut down unexpectedly"); 243 | } 244 | } 245 | } 246 | 247 | if !progress { 248 | return Ok(Async::NotReady); 249 | } 250 | } 251 | } 252 | } 253 | 254 | fn main() { 255 | let mut core = Core::new().unwrap(); 256 | let handle = core.handle(); 257 | 258 | let args: Vec = std::env::args().collect(); 259 | 260 | core.run(Main::new(handle, setup(&args))).unwrap() 261 | } 262 | -------------------------------------------------------------------------------- /src/scrobbler.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use futures::{Future, BoxFuture, Async, Poll}; 4 | use futures::future; 5 | use rustfm_scrobble::{self, Scrobble}; 6 | 7 | use metadata::{Track, Artist, Album, Metadata}; 8 | use core::session::Session; 9 | use core::util::SpotifyId; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct ScrobblerConfig { 13 | pub api_key: String, 14 | pub api_secret: String, 15 | pub username: String, 16 | pub password: String, 17 | } 18 | 19 | pub struct Scrobbler { 20 | config: ScrobblerConfig, 21 | scrobbler: rustfm_scrobble::Scrobbler, 22 | 23 | session: Box, 24 | current_track_id: Option, 25 | current_track_start: Option, 26 | current_track_meta: Option, 27 | current_track_scrobbled: bool, 28 | 29 | auth_future: BoxFuture<(), rustfm_scrobble::ScrobblerError>, 30 | new_track_future: BoxFuture<(), ()>, 31 | now_playing_future: BoxFuture<(), ScrobbleError>, 32 | meta_fetch_future: BoxFuture, 33 | scrobble_future: Option> 34 | } 35 | 36 | #[derive(Debug)] 37 | pub struct ScrobbleError { 38 | msg: String 39 | } 40 | 41 | impl ScrobbleError { 42 | 43 | pub fn new(msg: String) -> ScrobbleError { 44 | ScrobbleError { 45 | msg: msg 46 | } 47 | } 48 | 49 | } 50 | 51 | impl Scrobbler { 52 | 53 | pub fn new(config: ScrobblerConfig, session: Session) -> Scrobbler { 54 | let mut scrobbler = Scrobbler { 55 | session: Box::new(session), 56 | scrobbler: rustfm_scrobble::Scrobbler::new(&config.api_key, &config.api_secret), 57 | current_track_id: None, 58 | current_track_start: None, 59 | current_track_meta: None, 60 | current_track_scrobbled: false, 61 | auth_future: future::empty().boxed(), 62 | new_track_future: future::empty().boxed(), 63 | now_playing_future: future::empty().boxed(), 64 | meta_fetch_future: future::empty().boxed(), 65 | scrobble_future: None, 66 | config: config 67 | }; 68 | 69 | scrobbler.start_auth(); 70 | scrobbler 71 | } 72 | 73 | pub fn start_auth(&mut self) { 74 | self.auth_future = self.auth(); 75 | } 76 | 77 | pub fn auth(&mut self) -> BoxFuture<(), rustfm_scrobble::ScrobblerError> { 78 | match self.scrobbler.authenticate_with_password(&self.config.username, &self.config.password) { 79 | Ok(_) => future::ok(()), 80 | Err(err) => future::err(err) 81 | }.boxed() 82 | } 83 | 84 | pub fn update_current_track(&mut self, track_id: SpotifyId, force_new_track: bool) { 85 | if !force_new_track { 86 | let mut new_track_detected = false; 87 | match self.current_track_id { 88 | None => { 89 | new_track_detected = true; 90 | }, 91 | Some(id) => { 92 | if id != track_id { 93 | new_track_detected = true; 94 | } 95 | } 96 | } 97 | 98 | if !new_track_detected { 99 | return 100 | } 101 | } 102 | 103 | if self.can_scrobble_track() { 104 | self.start_scrobble(); 105 | } 106 | 107 | self.new_track_future = self.set_new_track(track_id); 108 | } 109 | 110 | pub fn set_new_track(&mut self, track_id: SpotifyId) -> BoxFuture<(), ()> { 111 | self.current_track_id = Some(track_id); 112 | self.current_track_start = Some(Instant::now()); 113 | self.current_track_meta = None; 114 | self.current_track_scrobbled = false; 115 | 116 | future::ok(()).boxed() 117 | } 118 | 119 | pub fn get_track_meta(&mut self, track_id: SpotifyId) -> BoxFuture { 120 | let session = self.session.clone(); 121 | 122 | Track::get(&session, track_id).and_then(move |track| { 123 | let track_name = track.clone().name; 124 | let artist = *track.artists.first().expect("No artists"); 125 | Artist::get(&session, artist).map(move |artist| (track_name, artist.name.clone(), track, session)) 126 | }).and_then(move |(track_name, artist_name, track_meta, session)| { 127 | Album::get(&session, track_meta.album).map(|album| (track_name, artist_name, album.name.clone())) 128 | }).map_err(move |err| { 129 | ScrobbleError::new(format!("{:?}", err).to_owned()) 130 | }).and_then(move |(track, artist, album)| { 131 | future::ok(Scrobble::new(&artist, &track, &album)) 132 | }).boxed() 133 | } 134 | 135 | pub fn send_now_playing(&self, track: &Scrobble) -> BoxFuture<(), ScrobbleError> { 136 | info!("Now-playing scrobble: {:?}", track); 137 | 138 | match self.scrobbler.now_playing(track) { 139 | Ok(_) => future::ok(()), 140 | Err(err) => future::err(ScrobbleError::new(format!("{:?}", err))) 141 | }.boxed() 142 | } 143 | 144 | pub fn start_scrobble(&mut self) { 145 | self.scrobble_future = match self.current_track_meta { 146 | Some(ref meta) => { 147 | let scrobble = &meta.clone(); 148 | Some(self.send_scrobble(scrobble)) 149 | }, 150 | None => { 151 | error!("No track meta-data available for scrobble"); 152 | None 153 | } 154 | } 155 | } 156 | 157 | pub fn send_scrobble(&self, scrobble: &Scrobble) -> BoxFuture<(), ScrobbleError> { 158 | info!("Scrobbling: {:?}", scrobble); 159 | 160 | match self.scrobbler.scrobble(scrobble) { 161 | Ok(_) => future::ok(()), 162 | Err(err) => future::err(ScrobbleError::new(format!("{:?}", err))) 163 | }.boxed() 164 | } 165 | 166 | fn can_scrobble_track(&self) -> bool { 167 | if self.current_track_scrobbled { 168 | return false 169 | } 170 | 171 | match self.scrobble_future { 172 | Some(_) => { 173 | return false 174 | }, 175 | None => {} 176 | } 177 | 178 | match self.current_track_start { 179 | Some(start_time) => { 180 | let play_time = start_time.elapsed(); 181 | 182 | if play_time > Duration::new(20, 0) { 183 | return true 184 | } 185 | 186 | false 187 | }, 188 | _ => false 189 | } 190 | } 191 | 192 | } 193 | 194 | impl Future for Scrobbler { 195 | type Item = Result<(), ()>; 196 | type Error = (); 197 | 198 | fn poll(&mut self) -> Poll, ()> { 199 | 200 | match self.auth_future.poll() { 201 | Ok(Async::Ready(_)) => { 202 | info!("Authenticated with Last.fm"); 203 | self.auth_future = future::empty().boxed(); 204 | }, 205 | Ok(Async::NotReady) => { 206 | }, 207 | Err(err) => { 208 | error!("Authentication error: {:?}", err); 209 | return Err(()) 210 | } 211 | } 212 | 213 | if self.can_scrobble_track() { 214 | self.start_scrobble(); 215 | } 216 | 217 | let mut track_scrobbled = false; 218 | match self.scrobble_future { 219 | Some(ref mut scrobble_future) => { 220 | match scrobble_future.poll() { 221 | Ok(Async::Ready(_)) => { 222 | track_scrobbled = true; 223 | }, 224 | Ok(Async::NotReady) => { 225 | return Ok(Async::NotReady) 226 | }, 227 | Err(err) => { 228 | error!("Scrobbling error: {:?}", err); 229 | return Err(()) 230 | } 231 | } 232 | }, 233 | None => () 234 | } 235 | 236 | if track_scrobbled { 237 | self.scrobble_future = None; 238 | self.current_track_scrobbled = true; 239 | } 240 | 241 | match self.new_track_future.poll() { 242 | Ok(Async::Ready(_)) => { 243 | self.new_track_future = future::empty().boxed(); 244 | self.current_track_scrobbled = false; 245 | 246 | match self.current_track_id { 247 | Some(track_id) => { 248 | self.meta_fetch_future = self.get_track_meta(track_id); 249 | }, 250 | None => { 251 | 252 | } 253 | } 254 | }, 255 | Ok(Async::NotReady) => { 256 | 257 | }, 258 | Err(err) => { 259 | error!("Failed to set new current track: {:?}", err); 260 | return Err(()) 261 | } 262 | } 263 | 264 | match self.meta_fetch_future.poll() { 265 | Ok(Async::Ready(ref track)) => { 266 | self.meta_fetch_future = future::empty().boxed(); 267 | self.now_playing_future = self.send_now_playing(track); 268 | self.current_track_meta = Some(track.clone()); 269 | }, 270 | Ok(Async::NotReady) => { 271 | 272 | }, 273 | Err(err) => { 274 | error!("Metadata fetch error: {:?}", err); 275 | return Err(()) 276 | } 277 | } 278 | 279 | match self.now_playing_future.poll() { 280 | Ok(Async::Ready(_)) => { 281 | self.now_playing_future = future::empty().boxed(); 282 | }, 283 | Ok(Async::NotReady) => { 284 | 285 | }, 286 | Err(err) => { 287 | error!("Now Playing error: {:?}", err); 288 | return Err(()) 289 | } 290 | } 291 | 292 | Ok(Async::NotReady) 293 | } 294 | 295 | } 296 | -------------------------------------------------------------------------------- /src/spirc.rs: -------------------------------------------------------------------------------- 1 | use futures::sink::BoxSink; 2 | use futures::stream::BoxStream; 3 | use futures::sync::mpsc; 4 | use futures::{Future, Stream, Sink, Async, Poll}; 5 | use protobuf::{self, Message}; 6 | 7 | use core::config::ConnectConfig; 8 | use core::mercury::MercuryError; 9 | use scrobbler::{Scrobbler, ScrobblerConfig}; 10 | use core::session::Session; 11 | use core::util::{now_ms, SpotifyId, SeqGenerator}; 12 | use core::version; 13 | 14 | use protocol; 15 | use protocol::spirc::{PlayStatus, State, MessageType, Frame, DeviceState}; 16 | 17 | 18 | pub struct SpircTask { 19 | sequence: SeqGenerator, 20 | 21 | ident: String, 22 | device: DeviceState, 23 | state: State, 24 | 25 | subscription: BoxStream, 26 | sender: BoxSink, 27 | commands: mpsc::UnboundedReceiver, 28 | 29 | shutdown: bool, 30 | session: Session, 31 | 32 | scrobbler: Scrobbler 33 | } 34 | 35 | pub enum SpircCommand { 36 | Shutdown 37 | } 38 | 39 | pub struct Spirc { 40 | commands: mpsc::UnboundedSender, 41 | } 42 | 43 | fn initial_state() -> State { 44 | protobuf_init!(protocol::spirc::State::new(), { 45 | repeat: false, 46 | shuffle: false, 47 | status: PlayStatus::kPlayStatusStop, 48 | position_ms: 0, 49 | position_measured_at: 0, 50 | }) 51 | } 52 | 53 | fn initial_device_state(config: ConnectConfig, volume: u16) -> DeviceState { 54 | protobuf_init!(DeviceState::new(), { 55 | sw_version: version::version_string(), 56 | is_active: false, 57 | can_play: true, 58 | volume: volume as u32, 59 | name: config.name, 60 | capabilities => [ 61 | @{ 62 | typ: protocol::spirc::CapabilityType::kCanBePlayer, 63 | intValue => [1] 64 | }, 65 | @{ 66 | typ: protocol::spirc::CapabilityType::kDeviceType, 67 | intValue => [config.device_type as i64] 68 | }, 69 | @{ 70 | typ: protocol::spirc::CapabilityType::kGaiaEqConnectId, 71 | intValue => [1] 72 | }, 73 | @{ 74 | typ: protocol::spirc::CapabilityType::kSupportsLogout, 75 | intValue => [0] 76 | }, 77 | @{ 78 | typ: protocol::spirc::CapabilityType::kIsObservable, 79 | intValue => [1] 80 | }, 81 | @{ 82 | typ: protocol::spirc::CapabilityType::kVolumeSteps, 83 | intValue => [64] 84 | }, 85 | @{ 86 | typ: protocol::spirc::CapabilityType::kSupportedContexts, 87 | stringValue => [ 88 | "album", 89 | "playlist", 90 | "search", 91 | "inbox", 92 | "toplist", 93 | "starred", 94 | "publishedstarred", 95 | "track", 96 | ] 97 | }, 98 | @{ 99 | typ: protocol::spirc::CapabilityType::kSupportedTypes, 100 | stringValue => [ 101 | "audio/local", 102 | "audio/track", 103 | "local", 104 | "track", 105 | ] 106 | } 107 | ], 108 | }) 109 | } 110 | 111 | impl Spirc { 112 | pub fn new(config: ConnectConfig, session: Session, scrobbler_config: ScrobblerConfig) 113 | -> (Spirc, SpircTask) 114 | { 115 | debug!("new Spirc[{}]", session.session_id()); 116 | 117 | let ident = session.device_id().to_owned(); 118 | 119 | let uri = format!("hm://remote/3/user/{}/", session.username()); 120 | 121 | let subscription = session.mercury().subscribe(&uri as &str); 122 | let subscription = subscription.map(|stream| stream.map_err(|_| MercuryError)).flatten_stream(); 123 | let subscription = subscription.map(|response| -> Frame { 124 | let data = response.payload.first().unwrap(); 125 | protobuf::parse_from_bytes(data).unwrap() 126 | }).boxed(); 127 | 128 | let sender = Box::new(session.mercury().sender(uri).with(|frame: Frame| { 129 | Ok(frame.write_to_bytes().unwrap()) 130 | })); 131 | 132 | let (cmd_tx, cmd_rx) = mpsc::unbounded(); 133 | 134 | let volume = 0xFFFF; 135 | let device = initial_device_state(config, volume); 136 | 137 | let scrobbler = Scrobbler::new(scrobbler_config, session.clone()); 138 | 139 | let mut task = SpircTask { 140 | sequence: SeqGenerator::new(1), 141 | 142 | ident: ident, 143 | 144 | device: device, 145 | state: initial_state(), 146 | 147 | subscription: subscription, 148 | sender: sender, 149 | commands: cmd_rx, 150 | 151 | shutdown: false, 152 | session: session.clone(), 153 | 154 | scrobbler: scrobbler 155 | }; 156 | 157 | let spirc = Spirc { 158 | commands: cmd_tx, 159 | }; 160 | 161 | task.hello(); 162 | 163 | (spirc, task) 164 | } 165 | 166 | pub fn shutdown(&self) { 167 | let _ = mpsc::UnboundedSender::send(&self.commands, SpircCommand::Shutdown); 168 | } 169 | } 170 | 171 | impl Future for SpircTask { 172 | type Item = (); 173 | type Error = (); 174 | 175 | fn poll(&mut self) -> Poll<(), ()> { 176 | loop { 177 | let mut progress = false; 178 | 179 | if !self.shutdown { 180 | match self.subscription.poll().unwrap() { 181 | Async::Ready(Some(frame)) => { 182 | progress = true; 183 | self.handle_frame(frame); 184 | } 185 | Async::Ready(None) => panic!("subscription terminated"), 186 | Async::NotReady => (), 187 | } 188 | 189 | match self.commands.poll().unwrap() { 190 | Async::Ready(Some(command)) => { 191 | progress = true; 192 | self.handle_command(command); 193 | } 194 | Async::Ready(None) => (), 195 | Async::NotReady => (), 196 | } 197 | 198 | match self.scrobbler.poll() { 199 | Ok(Async::Ready(_)) => { 200 | progress = true; 201 | }, 202 | Ok(Async::NotReady) => { 203 | 204 | }, 205 | Err(err) => { 206 | error!("Scrobbler error: {:?}", err); 207 | } 208 | } 209 | } 210 | 211 | let poll_sender = self.sender.poll_complete().unwrap(); 212 | 213 | // Only shutdown once we've flushed out all our messages 214 | if self.shutdown && poll_sender.is_ready() { 215 | return Ok(Async::Ready(())); 216 | } 217 | 218 | if !progress { 219 | return Ok(Async::NotReady); 220 | } 221 | } 222 | } 223 | } 224 | 225 | impl SpircTask { 226 | fn handle_command(&mut self, cmd: SpircCommand) { 227 | match cmd { 228 | SpircCommand::Shutdown => { 229 | CommandSender::new(self, MessageType::kMessageTypeGoodbye).send(); 230 | self.shutdown = true; 231 | self.commands.close(); 232 | } 233 | } 234 | } 235 | 236 | fn handle_frame(&mut self, frame: Frame) { 237 | debug!("{:?} {:?} {} {} {}", 238 | frame.get_typ(), 239 | frame.get_device_state().get_name(), 240 | frame.get_ident(), 241 | frame.get_seq_nr(), 242 | frame.get_state_update_id()); 243 | 244 | if frame.get_ident() == self.ident || 245 | (frame.get_recipient().len() > 0 && !frame.get_recipient().contains(&self.ident)) { 246 | return; 247 | } 248 | 249 | match frame.get_typ() { 250 | MessageType::kMessageTypeHello => { 251 | self.notify(Some(frame.get_ident())); 252 | } 253 | 254 | MessageType::kMessageTypeNotify => { 255 | // Inactive devices won't be playing anything, so we don't need to scrobble it 256 | if !frame.get_device_state().get_is_active() { 257 | return (); 258 | } 259 | 260 | //info!("{:?}", frame); 261 | let state = frame.get_state(); 262 | let playing_index = state.get_playing_track_index(); 263 | let tracks = state.get_track(); 264 | if tracks.len() > 0 { 265 | let playing_track_ref = state.get_track()[playing_index as usize].clone(); 266 | let playing_track_spotify_id = SpotifyId::from_raw(playing_track_ref.get_gid()); 267 | let force_new_track = state.get_position_ms() == 0; 268 | 269 | self.scrobbler.update_current_track(playing_track_spotify_id, force_new_track); 270 | info!("Relevant SPIRC frame; Current track Spotify ID: {:?}", playing_track_spotify_id); 271 | } 272 | 273 | } 274 | _ => (), 275 | } 276 | } 277 | 278 | fn hello(&mut self) { 279 | CommandSender::new(self, MessageType::kMessageTypeHello).send(); 280 | } 281 | 282 | fn notify(&mut self, recipient: Option<&str>) { 283 | let mut cs = CommandSender::new(self, MessageType::kMessageTypeNotify); 284 | if let Some(s) = recipient { 285 | cs = cs.recipient(&s); 286 | } 287 | cs.send(); 288 | } 289 | } 290 | 291 | impl Drop for SpircTask { 292 | fn drop(&mut self) { 293 | debug!("drop Spirc[{}]", self.session.session_id()); 294 | } 295 | } 296 | 297 | struct CommandSender<'a> { 298 | spirc: &'a mut SpircTask, 299 | frame: protocol::spirc::Frame, 300 | } 301 | 302 | impl<'a> CommandSender<'a> { 303 | fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> CommandSender { 304 | let frame = protobuf_init!(protocol::spirc::Frame::new(), { 305 | version: 1, 306 | protocol_version: "2.0.0", 307 | ident: spirc.ident.clone(), 308 | seq_nr: spirc.sequence.get(), 309 | typ: cmd, 310 | 311 | device_state: spirc.device.clone(), 312 | state_update_id: now_ms(), 313 | }); 314 | 315 | CommandSender { 316 | spirc: spirc, 317 | frame: frame, 318 | } 319 | } 320 | 321 | fn recipient(mut self, recipient: &'a str) -> CommandSender { 322 | self.frame.mut_recipient().push(recipient.to_owned()); 323 | self 324 | } 325 | 326 | #[allow(dead_code)] 327 | fn state(mut self, state: protocol::spirc::State) -> CommandSender<'a> { 328 | self.frame.set_state(state); 329 | self 330 | } 331 | 332 | fn send(mut self) { 333 | if !self.frame.has_state() && self.spirc.device.get_is_active() { 334 | self.frame.set_state(self.spirc.state.clone()); 335 | } 336 | 337 | let send = self.spirc.sender.start_send(self.frame).unwrap(); 338 | assert!(send.is_ready()); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /protocol/src/pubsub.rs: -------------------------------------------------------------------------------- 1 | // This file is generated. Do not edit 2 | // @generated 3 | 4 | // https://github.com/Manishearth/rust-clippy/issues/702 5 | #![allow(unknown_lints)] 6 | #![allow(clippy)] 7 | 8 | #![cfg_attr(rustfmt, rustfmt_skip)] 9 | 10 | #![allow(box_pointers)] 11 | #![allow(dead_code)] 12 | #![allow(missing_docs)] 13 | #![allow(non_camel_case_types)] 14 | #![allow(non_snake_case)] 15 | #![allow(non_upper_case_globals)] 16 | #![allow(trivial_casts)] 17 | #![allow(unsafe_code)] 18 | #![allow(unused_imports)] 19 | #![allow(unused_results)] 20 | 21 | use protobuf::Message as Message_imported_for_functions; 22 | use protobuf::ProtobufEnum as ProtobufEnum_imported_for_functions; 23 | 24 | #[derive(PartialEq,Clone,Default)] 25 | pub struct Subscription { 26 | // message fields 27 | uri: ::protobuf::SingularField<::std::string::String>, 28 | expiry: ::std::option::Option, 29 | status_code: ::std::option::Option, 30 | // special fields 31 | unknown_fields: ::protobuf::UnknownFields, 32 | cached_size: ::protobuf::CachedSize, 33 | } 34 | 35 | // see codegen.rs for the explanation why impl Sync explicitly 36 | unsafe impl ::std::marker::Sync for Subscription {} 37 | 38 | impl Subscription { 39 | pub fn new() -> Subscription { 40 | ::std::default::Default::default() 41 | } 42 | 43 | pub fn default_instance() -> &'static Subscription { 44 | static mut instance: ::protobuf::lazy::Lazy = ::protobuf::lazy::Lazy { 45 | lock: ::protobuf::lazy::ONCE_INIT, 46 | ptr: 0 as *const Subscription, 47 | }; 48 | unsafe { 49 | instance.get(Subscription::new) 50 | } 51 | } 52 | 53 | // optional string uri = 1; 54 | 55 | pub fn clear_uri(&mut self) { 56 | self.uri.clear(); 57 | } 58 | 59 | pub fn has_uri(&self) -> bool { 60 | self.uri.is_some() 61 | } 62 | 63 | // Param is passed by value, moved 64 | pub fn set_uri(&mut self, v: ::std::string::String) { 65 | self.uri = ::protobuf::SingularField::some(v); 66 | } 67 | 68 | // Mutable pointer to the field. 69 | // If field is not initialized, it is initialized with default value first. 70 | pub fn mut_uri(&mut self) -> &mut ::std::string::String { 71 | if self.uri.is_none() { 72 | self.uri.set_default(); 73 | } 74 | self.uri.as_mut().unwrap() 75 | } 76 | 77 | // Take field 78 | pub fn take_uri(&mut self) -> ::std::string::String { 79 | self.uri.take().unwrap_or_else(|| ::std::string::String::new()) 80 | } 81 | 82 | pub fn get_uri(&self) -> &str { 83 | match self.uri.as_ref() { 84 | Some(v) => &v, 85 | None => "", 86 | } 87 | } 88 | 89 | fn get_uri_for_reflect(&self) -> &::protobuf::SingularField<::std::string::String> { 90 | &self.uri 91 | } 92 | 93 | fn mut_uri_for_reflect(&mut self) -> &mut ::protobuf::SingularField<::std::string::String> { 94 | &mut self.uri 95 | } 96 | 97 | // optional int32 expiry = 2; 98 | 99 | pub fn clear_expiry(&mut self) { 100 | self.expiry = ::std::option::Option::None; 101 | } 102 | 103 | pub fn has_expiry(&self) -> bool { 104 | self.expiry.is_some() 105 | } 106 | 107 | // Param is passed by value, moved 108 | pub fn set_expiry(&mut self, v: i32) { 109 | self.expiry = ::std::option::Option::Some(v); 110 | } 111 | 112 | pub fn get_expiry(&self) -> i32 { 113 | self.expiry.unwrap_or(0) 114 | } 115 | 116 | fn get_expiry_for_reflect(&self) -> &::std::option::Option { 117 | &self.expiry 118 | } 119 | 120 | fn mut_expiry_for_reflect(&mut self) -> &mut ::std::option::Option { 121 | &mut self.expiry 122 | } 123 | 124 | // optional int32 status_code = 3; 125 | 126 | pub fn clear_status_code(&mut self) { 127 | self.status_code = ::std::option::Option::None; 128 | } 129 | 130 | pub fn has_status_code(&self) -> bool { 131 | self.status_code.is_some() 132 | } 133 | 134 | // Param is passed by value, moved 135 | pub fn set_status_code(&mut self, v: i32) { 136 | self.status_code = ::std::option::Option::Some(v); 137 | } 138 | 139 | pub fn get_status_code(&self) -> i32 { 140 | self.status_code.unwrap_or(0) 141 | } 142 | 143 | fn get_status_code_for_reflect(&self) -> &::std::option::Option { 144 | &self.status_code 145 | } 146 | 147 | fn mut_status_code_for_reflect(&mut self) -> &mut ::std::option::Option { 148 | &mut self.status_code 149 | } 150 | } 151 | 152 | impl ::protobuf::Message for Subscription { 153 | fn is_initialized(&self) -> bool { 154 | true 155 | } 156 | 157 | fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream) -> ::protobuf::ProtobufResult<()> { 158 | while !is.eof()? { 159 | let (field_number, wire_type) = is.read_tag_unpack()?; 160 | match field_number { 161 | 1 => { 162 | ::protobuf::rt::read_singular_string_into(wire_type, is, &mut self.uri)?; 163 | }, 164 | 2 => { 165 | if wire_type != ::protobuf::wire_format::WireTypeVarint { 166 | return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type)); 167 | } 168 | let tmp = is.read_int32()?; 169 | self.expiry = ::std::option::Option::Some(tmp); 170 | }, 171 | 3 => { 172 | if wire_type != ::protobuf::wire_format::WireTypeVarint { 173 | return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type)); 174 | } 175 | let tmp = is.read_int32()?; 176 | self.status_code = ::std::option::Option::Some(tmp); 177 | }, 178 | _ => { 179 | ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; 180 | }, 181 | }; 182 | } 183 | ::std::result::Result::Ok(()) 184 | } 185 | 186 | // Compute sizes of nested messages 187 | #[allow(unused_variables)] 188 | fn compute_size(&self) -> u32 { 189 | let mut my_size = 0; 190 | if let Some(ref v) = self.uri.as_ref() { 191 | my_size += ::protobuf::rt::string_size(1, &v); 192 | } 193 | if let Some(v) = self.expiry { 194 | my_size += ::protobuf::rt::value_size(2, v, ::protobuf::wire_format::WireTypeVarint); 195 | } 196 | if let Some(v) = self.status_code { 197 | my_size += ::protobuf::rt::value_size(3, v, ::protobuf::wire_format::WireTypeVarint); 198 | } 199 | my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); 200 | self.cached_size.set(my_size); 201 | my_size 202 | } 203 | 204 | fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream) -> ::protobuf::ProtobufResult<()> { 205 | if let Some(ref v) = self.uri.as_ref() { 206 | os.write_string(1, &v)?; 207 | } 208 | if let Some(v) = self.expiry { 209 | os.write_int32(2, v)?; 210 | } 211 | if let Some(v) = self.status_code { 212 | os.write_int32(3, v)?; 213 | } 214 | os.write_unknown_fields(self.get_unknown_fields())?; 215 | ::std::result::Result::Ok(()) 216 | } 217 | 218 | fn get_cached_size(&self) -> u32 { 219 | self.cached_size.get() 220 | } 221 | 222 | fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { 223 | &self.unknown_fields 224 | } 225 | 226 | fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { 227 | &mut self.unknown_fields 228 | } 229 | 230 | fn as_any(&self) -> &::std::any::Any { 231 | self as &::std::any::Any 232 | } 233 | fn as_any_mut(&mut self) -> &mut ::std::any::Any { 234 | self as &mut ::std::any::Any 235 | } 236 | fn into_any(self: Box) -> ::std::boxed::Box<::std::any::Any> { 237 | self 238 | } 239 | 240 | fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { 241 | ::protobuf::MessageStatic::descriptor_static(None::) 242 | } 243 | } 244 | 245 | impl ::protobuf::MessageStatic for Subscription { 246 | fn new() -> Subscription { 247 | Subscription::new() 248 | } 249 | 250 | fn descriptor_static(_: ::std::option::Option) -> &'static ::protobuf::reflect::MessageDescriptor { 251 | static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy { 252 | lock: ::protobuf::lazy::ONCE_INIT, 253 | ptr: 0 as *const ::protobuf::reflect::MessageDescriptor, 254 | }; 255 | unsafe { 256 | descriptor.get(|| { 257 | let mut fields = ::std::vec::Vec::new(); 258 | fields.push(::protobuf::reflect::accessor::make_singular_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( 259 | "uri", 260 | Subscription::get_uri_for_reflect, 261 | Subscription::mut_uri_for_reflect, 262 | )); 263 | fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>( 264 | "expiry", 265 | Subscription::get_expiry_for_reflect, 266 | Subscription::mut_expiry_for_reflect, 267 | )); 268 | fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>( 269 | "status_code", 270 | Subscription::get_status_code_for_reflect, 271 | Subscription::mut_status_code_for_reflect, 272 | )); 273 | ::protobuf::reflect::MessageDescriptor::new::( 274 | "Subscription", 275 | fields, 276 | file_descriptor_proto() 277 | ) 278 | }) 279 | } 280 | } 281 | } 282 | 283 | impl ::protobuf::Clear for Subscription { 284 | fn clear(&mut self) { 285 | self.clear_uri(); 286 | self.clear_expiry(); 287 | self.clear_status_code(); 288 | self.unknown_fields.clear(); 289 | } 290 | } 291 | 292 | impl ::std::fmt::Debug for Subscription { 293 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 294 | ::protobuf::text_format::fmt(self, f) 295 | } 296 | } 297 | 298 | impl ::protobuf::reflect::ProtobufValue for Subscription { 299 | fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef { 300 | ::protobuf::reflect::ProtobufValueRef::Message(self) 301 | } 302 | } 303 | 304 | static file_descriptor_proto_data: &'static [u8] = b"\ 305 | \n\x0cpubsub.proto\"Y\n\x0cSubscription\x12\x10\n\x03uri\x18\x01\x20\x01\ 306 | (\tR\x03uri\x12\x16\n\x06expiry\x18\x02\x20\x01(\x05R\x06expiry\x12\x1f\ 307 | \n\x0bstatus_code\x18\x03\x20\x01(\x05R\nstatusCodeJ\xf9\x01\n\x06\x12\ 308 | \x04\0\0\x06\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\ 309 | \x02\0\x06\x01\n\n\n\x03\x04\0\x01\x12\x03\x02\x08\x14\n\x0b\n\x04\x04\0\ 310 | \x02\0\x12\x03\x03\x04\x1e\n\x0c\n\x05\x04\0\x02\0\x04\x12\x03\x03\x04\ 311 | \x0c\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\r\x13\n\x0c\n\x05\x04\0\x02\ 312 | \0\x01\x12\x03\x03\x14\x17\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x1a\ 313 | \x1d\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x04\x20\n\x0c\n\x05\x04\0\x02\ 314 | \x01\x04\x12\x03\x04\x04\x0c\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x04\r\ 315 | \x12\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x04\x13\x19\n\x0c\n\x05\x04\0\ 316 | \x02\x01\x03\x12\x03\x04\x1c\x1f\n\x0b\n\x04\x04\0\x02\x02\x12\x03\x05\ 317 | \x04%\n\x0c\n\x05\x04\0\x02\x02\x04\x12\x03\x05\x04\x0c\n\x0c\n\x05\x04\ 318 | \0\x02\x02\x05\x12\x03\x05\r\x12\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x03\ 319 | \x05\x13\x1e\n\x0c\n\x05\x04\0\x02\x02\x03\x12\x03\x05!$\ 320 | "; 321 | 322 | static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy { 323 | lock: ::protobuf::lazy::ONCE_INIT, 324 | ptr: 0 as *const ::protobuf::descriptor::FileDescriptorProto, 325 | }; 326 | 327 | fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto { 328 | ::protobuf::parse_from_bytes(file_descriptor_proto_data).unwrap() 329 | } 330 | 331 | pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { 332 | unsafe { 333 | file_descriptor_proto_lazy.get(|| { 334 | parse_descriptor_proto() 335 | }) 336 | } 337 | } 338 | --------------------------------------------------------------------------------