diff --git a/Cargo.lock b/Cargo.lock index c059eca2..945aaec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,7 @@ dependencies = [ "rust-crypto 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)", "rust-gmp 0.2.0 (git+https://github.com/plietar/rust-gmp.git)", "shannon 0.1.0 (git+https://github.com/plietar/rust-shannon.git)", + "time 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)", "vergen 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "vorbis 0.0.11 (git+https://github.com/plietar/vorbis-rs)", ] diff --git a/Cargo.toml b/Cargo.toml index da9182c9..f7af1814 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ num = "*" rand = "*" lazy_static = "0.1.*" rust-crypto = "*" +time = "*" [dependencies.protobuf] git = "https://github.com/stepancheg/rust-protobuf.git" diff --git a/src/main.rs b/src/main.rs index 72d57bfb..90fa5787 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ extern crate shannon; extern crate rand; extern crate readall; extern crate vorbis; +extern crate time; extern crate librespot_protocol; @@ -38,6 +39,7 @@ use std::fs::File; use std::io::{Read, Write}; use std::path::Path; use std::sync::mpsc; +use protobuf::core::Message; use metadata::{MetadataCache, AlbumRef, ArtistRef, TrackRef}; use session::{Config, Session}; @@ -64,34 +66,26 @@ fn main() { session.login(username, password); session.poll(); - let mut cache = MetadataCache::new(session.metadata.clone()); + let ident = session.config.device_id.clone(); + SpircManager{ + session: session, + state_update_id: 0, + seq_nr: 0, - let (tx, rx) = mpsc::channel(); - - session.mercury.send(MercuryRequest{ - method: MercuryMethod::SUB, - uri: "hm://remote/user/lietar/v23".to_string(), - content_type: None, - callback: Some(tx) - }).unwrap(); - - for pkt in rx.iter() { - let frame : protocol::spirc::Frame = - protobuf::parse_from_bytes(pkt.payload.front().unwrap()).unwrap(); - - if frame.get_device_state().get_is_active() && - frame.has_state() { - let index = frame.get_state().get_playing_track_index(); - let ref track = frame.get_state().get_track()[index as usize]; - println!("{}", frame.get_device_state().get_name()); - print_track(&mut cache, SpotifyId::from_raw(track.get_gid())); - println!(""); - } - } + name: "BlaBla".to_string(), + ident: ident, + device_type: 5, + volume: 0x8000, + can_play: true, + is_active: false, + became_active_at: 0, + }.run(); + /* loop { session.poll(); } + */ } fn print_track(cache: &mut MetadataCache, track_id: SpotifyId) { @@ -120,3 +114,154 @@ fn print_track(cache: &mut MetadataCache, track_id: SpotifyId) { } } +struct SpircManager { + session: Session, + state_update_id: i64, + seq_nr: u32, + + name: String, + ident: String, + device_type: u8, + + volume: u16, + can_play: bool, + is_active: bool, + became_active_at: i64, +} + +impl SpircManager { + fn run(&mut self) { + let (tx, rx) = mpsc::channel(); + + self.session.mercury.send(MercuryRequest{ + method: MercuryMethod::SUB, + uri: "hm://remote/user/lietar/v23".to_string(), + content_type: None, + callback: Some(tx), + payload: Vec::new() + }).unwrap(); + + self.notify(None); + + for pkt in rx.iter() { + let frame : protocol::spirc::Frame = + protobuf::parse_from_bytes(pkt.payload.front().unwrap()).unwrap(); + + println!("{:?} {} {} {}", + frame.get_typ(), + frame.get_device_state().get_name(), + frame.get_ident(), + frame.get_device_state().get_became_active_at()); + + if frame.get_ident() == self.ident || + (frame.get_recipient().len() > 0 && + !frame.get_recipient().contains(&self.ident)) { + continue; + } + + self.handle(frame); + } + } + + fn handle(&mut self, frame: protocol::spirc::Frame) { + match frame.get_typ() { + protocol::spirc::MessageType::kMessageTypeHello => { + self.notify(Some(frame.get_ident())); + } + protocol::spirc::MessageType::kMessageTypeLoad => { + self.is_active = true; + self.became_active_at = { + let ts = time::now_utc().to_timespec(); + ts.sec * 1000 + ts.nsec as i64 / 1000000 + }; + println!("{:?} {}", frame, self.became_active_at); + self.notify(None) + } + _ => () + } + } + + fn notify(&mut self, recipient: Option<&str>) { + let device_state = self.device_state(); + self.session.mercury.send(MercuryRequest{ + method: MercuryMethod::SEND, + uri: "hm://remote/user/lietar".to_string(), + content_type: None, + callback: None, + payload: vec![ + protobuf_init!(protocol::spirc::Frame::new(), { + version: 1, + ident: self.ident.clone(), + protocol_version: "2.0.0".to_string(), + seq_nr: { self.seq_nr += 1; self.seq_nr }, + typ: protocol::spirc::MessageType::kMessageTypeNotify, + device_state: device_state, + recipient: protobuf::RepeatedField::from_vec( + recipient.map(|r| vec![r.to_string()] ).unwrap_or(vec![]) + ) + }).write_to_bytes().unwrap() + ] + }).unwrap(); + } + + fn device_state(&mut self) -> protocol::spirc::DeviceState { + protobuf_init!(protocol::spirc::DeviceState::new(), { + sw_version: "librespot-0.1.0".to_string(), + is_active: self.is_active, + can_play: self.can_play, + volume: self.volume as u32, + name: self.name.clone(), + error_code: 0, + became_active_at: if self.is_active { self.became_active_at } else { 0 }, + capabilities => [ + @{ + typ: protocol::spirc::CapabilityType::kCanBePlayer, + intValue => [0] + }, + @{ + typ: protocol::spirc::CapabilityType::kDeviceType, + intValue => [ self.device_type as i64 ] + }, + @{ + typ: protocol::spirc::CapabilityType::kGaiaEqConnectId, + intValue => [1] + }, + @{ + typ: protocol::spirc::CapabilityType::kSupportsLogout, + intValue => [0] + }, + @{ + typ: protocol::spirc::CapabilityType::kIsObservable, + intValue => [1] + }, + @{ + typ: protocol::spirc::CapabilityType::kVolumeSteps, + intValue => [10] + }, + @{ + typ: protocol::spirc::CapabilityType::kSupportedContexts, + stringValue => [ + "album".to_string(), + "playlist".to_string(), + "search".to_string(), + "inbox".to_string(), + "toplist".to_string(), + "starred".to_string(), + "publishedstarred".to_string(), + "track".to_string(), + ] + }, + @{ + typ: protocol::spirc::CapabilityType::kSupportedTypes, + stringValue => [ + "audio/local".to_string(), + "audio/track".to_string(), + "local".to_string(), + "track".to_string(), + ] + } + ], + }) + } +} + diff --git a/src/mercury.rs b/src/mercury.rs index 3b36f3db..bf495b15 100644 --- a/src/mercury.rs +++ b/src/mercury.rs @@ -17,13 +17,15 @@ pub enum MercuryMethod { GET, SUB, UNSUB, + SEND, } pub struct MercuryRequest { pub method: MercuryMethod, pub uri: String, pub content_type: Option, - pub callback: Option + pub callback: Option, + pub payload: Vec> } #[derive(Debug)] @@ -55,7 +57,8 @@ impl fmt::Display for MercuryMethod { formatter.write_str(match *self { MercuryMethod::GET => "GET", MercuryMethod::SUB => "SUB", - MercuryMethod::UNSUB => "UNSUB" + MercuryMethod::UNSUB => "UNSUB", + MercuryMethod::SEND => "SEND" }) } } @@ -188,7 +191,7 @@ impl MercuryManager { packet.write_u16::(seq.len() as u16).unwrap(); packet.write_all(seq).unwrap(); packet.write_u8(1).unwrap(); // Flags: FINAL - packet.write_u16::(1).unwrap(); // Part count. Only header + packet.write_u16::(1 + req.payload.len() as u16).unwrap(); // Part count let mut header = protobuf_init!(protocol::mercury::Header::new(), { uri: req.uri.clone(), @@ -201,6 +204,11 @@ impl MercuryManager { packet.write_u16::(header.compute_size() as u16).unwrap(); header.write_to_writer(&mut packet).unwrap(); + for p in &req.payload { + packet.write_u16::(p.len() as u16).unwrap(); + packet.write(&p).unwrap(); + } + packet } } diff --git a/src/metadata.rs b/src/metadata.rs index 0f254f17..a0e33f07 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -237,7 +237,8 @@ impl MetadataManager { method: MercuryMethod::GET, uri: format!("{}/{}", T::base_url(), object.id.to_base16()), content_type: None, - callback: Some(tx) + callback: Some(tx), + payload: Vec::new() }).unwrap(); let response = rx.recv().unwrap(); diff --git a/src/session.rs b/src/session.rs index 434af149..ded674b9 100644 --- a/src/session.rs +++ b/src/session.rs @@ -22,7 +22,7 @@ pub struct Config { } pub struct Session { - config: Config, + pub config: Config, packet_rx: mpsc::Receiver, pub packet_tx: mpsc::Sender,