diff --git a/Cargo.toml b/Cargo.toml index fd8012e0..186139bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,14 @@ version = "0.1.0" authors = ["Paul LiƩtar "] build = "build.rs" +[lib] +name = "librespot" +path = "src/lib.rs" + +[[bin]] +name = "main" +path = "src/main.rs" + [dependencies.librespot-protocol] path = "protocol" diff --git a/src/audio_file.rs b/src/audio_file.rs index b84f5f84..2e1f0ee0 100644 --- a/src/audio_file.rs +++ b/src/audio_file.rs @@ -20,7 +20,7 @@ pub enum AudioFile<'s> { Loading(AudioFileLoading<'s>) } -struct AudioFileLoading<'s> { +pub struct AudioFileLoading<'s> { read_file: TempFile, position: u64, diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..915227b1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,38 @@ +#![crate_name = "librespot"] + +#![feature(plugin,scoped,zero_one,iter_arith,slice_position_elem,slice_bytes,bitset,arc_weak,append,future)] +#![allow(deprecated)] +#![allow(unused_imports,dead_code)] + +#![plugin(protobuf_macros)] +#[macro_use] extern crate lazy_static; + + +extern crate byteorder; +extern crate crypto; +extern crate gmp; +extern crate num; +extern crate portaudio; +extern crate protobuf; +extern crate shannon; +extern crate rand; +extern crate readall; +extern crate vorbis; +extern crate time; +extern crate tempfile; + +extern crate librespot_protocol; + +#[macro_use] pub mod util; +pub mod audio_decrypt; +pub mod audio_file; +pub mod audio_key; +pub mod connection; +pub mod keys; +pub mod mercury; +pub mod metadata; +pub mod player; +pub mod session; +pub mod spirc; +pub mod stream; +pub mod subsystem; diff --git a/src/main.rs b/src/main.rs index e86c5004..d304b798 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,4 @@ -#![crate_name = "librespot"] - -#![feature(plugin,scoped,zero_one,iter_arith,slice_position_elem,slice_bytes,bitset,arc_weak,append,future)] +#![feature(plugin,scoped)] #![allow(deprecated)] //#![allow(unused_imports,dead_code)] @@ -18,40 +16,22 @@ extern crate shannon; extern crate rand; extern crate readall; extern crate vorbis; -extern crate time; -extern crate tempfile; -extern crate librespot_protocol; - -#[macro_use] mod util; -mod audio_decrypt; -mod audio_file; -mod audio_key; -mod connection; -mod keys; -mod mercury; -mod metadata; -mod player; -mod session; -mod stream; -mod subsystem; +#[macro_use] extern crate librespot; use std::clone::Clone; use std::fs::File; use std::io::{Read, Write}; use std::path::Path; -use protobuf::core::Message; use std::thread; use std::path::PathBuf; -use metadata::{AlbumRef, ArtistRef, TrackRef}; -use session::{Config, Session}; -use util::SpotifyId; -use util::version::version_string; -use player::{Player, PlayerCommand}; -use mercury::{MercuryRequest, MercuryMethod}; -use librespot_protocol as protocol; -use librespot_protocol::spirc::PlayStatus; +use librespot::metadata::{AlbumRef, ArtistRef, TrackRef}; +use librespot::session::{Config, Session}; +use librespot::util::SpotifyId; +use librespot::util::version::version_string; +use librespot::player::Player; +use librespot::spirc::SpircManager; fn main() { let mut args = std::env::args().skip(1); @@ -82,31 +62,8 @@ fn main() { let player = Player::new(&session); - SpircManager { - session: &session, - player: &player, - - username: username.clone(), - state_update_id: 0, - seq_nr: 0, - - name: name, - ident: session.config.device_id.clone(), - device_type: 5, - can_play: true, - - repeat: false, - shuffle: false, - volume: 0x8000, - - is_active: false, - became_active_at: 0, - - last_command_ident: String::new(), - last_command_msgid: 0, - - track: None - }.run(); + let mut spirc_manager = SpircManager::new(&session, &player, username, name); + spirc_manager.run(); poll_thread.join(); } @@ -136,215 +93,3 @@ fn print_track(session: &Session, track_id: SpotifyId) { eprintln!("{}", data.name); } } - -struct SpircManager<'s> { - player: &'s Player<'s>, - session: &'s Session, - - username: String, - state_update_id: i64, - seq_nr: u32, - - name: String, - ident: String, - device_type: u8, - can_play: bool, - - repeat: bool, - shuffle: bool, - volume: u16, - - is_active: bool, - became_active_at: i64, - - last_command_ident: String, - last_command_msgid: u32, - - track: Option -} - -impl <'s> SpircManager<'s> { - fn run(&mut self) { - let rx = self.session.mercury_sub(format!("hm://remote/user/{}/v23", self.username)); - - self.notify(None); - - loop { - if let Ok(pkt) = rx.try_recv() { - let 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_seq_nr(), - frame.get_state_update_id()); - if frame.get_ident() != self.ident && - (frame.get_recipient().len() == 0 || - frame.get_recipient().contains(&self.ident)) { - self.handle(frame); - } - } - - let h = self.player.state.0.lock().unwrap(); - if h.update_time > self.state_update_id { - self.state_update_id = util::now_ms(); - drop(h); - self.notify(None); - } - } - } - - fn handle(&mut self, frame: protocol::spirc::Frame) { - if frame.get_recipient().len() > 0 { - self.last_command_ident = frame.get_ident().to_string(); - self.last_command_msgid = frame.get_seq_nr(); - } - match frame.get_typ() { - protocol::spirc::MessageType::kMessageTypeHello => { - self.notify(Some(frame.get_ident())); - } - protocol::spirc::MessageType::kMessageTypeLoad => { - if !self.is_active { - self.is_active = true; - self.became_active_at = util::now_ms(); - } - - let index = frame.get_state().get_playing_track_index() as usize; - let track = SpotifyId::from_raw(frame.get_state().get_track()[index].get_gid()); - self.track = Some(track); - self.player.command(PlayerCommand::Load(track, - frame.get_state().get_status() == PlayStatus::kPlayStatusPlay, - frame.get_state().get_position_ms())); - } - protocol::spirc::MessageType::kMessageTypePlay => { - self.player.command(PlayerCommand::Play); - } - protocol::spirc::MessageType::kMessageTypePause => { - self.player.command(PlayerCommand::Pause); - } - protocol::spirc::MessageType::kMessageTypeSeek => { - self.player.command(PlayerCommand::Seek(frame.get_position())); - } - protocol::spirc::MessageType::kMessageTypeNotify => { - if self.is_active && frame.get_device_state().get_is_active() { - self.is_active = false; - self.player.command(PlayerCommand::Stop); - } - } - _ => () - } - } - - fn notify(&mut self, recipient: Option<&str>) { - let mut pkt = 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: self.device_state(), - recipient: protobuf::RepeatedField::from_vec( - recipient.map(|r| vec![r.to_string()] ).unwrap_or(vec![]) - ), - state_update_id: self.state_update_id - }); - - if self.is_active { - pkt.set_state(self.state()); - } - - self.session.mercury(MercuryRequest{ - method: MercuryMethod::SEND, - uri: format!("hm://remote/user/{}", self.username), - content_type: None, - payload: vec![ pkt.write_to_bytes().unwrap() ] - }); - } - - fn state(&mut self) -> protocol::spirc::State { - let state = self.player.state.0.lock().unwrap(); - - protobuf_init!(protocol::spirc::State::new(), { - status: state.status, - position_ms: state.position_ms, - position_measured_at: state.position_measured_at as u64, - - playing_track_index: 0, - track => [ - @{ - gid: self.track.unwrap().to_raw().to_vec() - } - ], - - shuffle: self.shuffle, - repeat: self.repeat, - - playing_from_fallback: true, - - last_command_ident: self.last_command_ident.clone(), - last_command_msgid: self.last_command_msgid - }) - } - - fn device_state(&mut self) -> protocol::spirc::DeviceState { - protobuf_init!(protocol::spirc::DeviceState::new(), { - sw_version: version_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/spirc.rs b/src/spirc.rs new file mode 100644 index 00000000..adcfce75 --- /dev/null +++ b/src/spirc.rs @@ -0,0 +1,251 @@ + +use protobuf::{self, Message}; + +use util; +use session::{Config, Session}; +use util::SpotifyId; +use util::version::version_string; +use player::{Player, PlayerCommand}; +use mercury::{MercuryRequest, MercuryMethod}; + +use librespot_protocol as protocol; +use librespot_protocol::spirc::PlayStatus; + +pub struct SpircManager<'s> { + player: &'s Player<'s>, + session: &'s Session, + + username: String, + state_update_id: i64, + seq_nr: u32, + + name: String, + ident: String, + device_type: u8, + can_play: bool, + + repeat: bool, + shuffle: bool, + volume: u16, + + is_active: bool, + became_active_at: i64, + + last_command_ident: String, + last_command_msgid: u32, + + track: Option +} + +impl <'s> SpircManager<'s> { + pub fn new(session: &'s Session, player: &'s Player<'s>, username: String, name: String) -> SpircManager<'s> { + SpircManager { + session: &session, + player: &player, + + username: username.clone(), + state_update_id: 0, + seq_nr: 0, + + name: name, + ident: session.config.device_id.clone(), + device_type: 5, + can_play: true, + + repeat: false, + shuffle: false, + volume: 0x8000, + + is_active: false, + became_active_at: 0, + + last_command_ident: String::new(), + last_command_msgid: 0, + + track: None + } + } + + pub fn run(&mut self) { + let rx = self.session.mercury_sub(format!("hm://remote/user/{}/v23", self.username)); + + self.notify(None); + + loop { + if let Ok(pkt) = rx.try_recv() { + let 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_seq_nr(), + frame.get_state_update_id()); + if frame.get_ident() != self.ident && + (frame.get_recipient().len() == 0 || + frame.get_recipient().contains(&self.ident)) { + self.handle(frame); + } + } + + let h = self.player.state.0.lock().unwrap(); + if h.update_time > self.state_update_id { + self.state_update_id = util::now_ms(); + drop(h); + self.notify(None); + } + } + } + + fn handle(&mut self, frame: protocol::spirc::Frame) { + if frame.get_recipient().len() > 0 { + self.last_command_ident = frame.get_ident().to_string(); + self.last_command_msgid = frame.get_seq_nr(); + } + match frame.get_typ() { + protocol::spirc::MessageType::kMessageTypeHello => { + self.notify(Some(frame.get_ident())); + } + protocol::spirc::MessageType::kMessageTypeLoad => { + if !self.is_active { + self.is_active = true; + self.became_active_at = util::now_ms(); + } + + let index = frame.get_state().get_playing_track_index() as usize; + let track = SpotifyId::from_raw(frame.get_state().get_track()[index].get_gid()); + self.track = Some(track); + self.player.command(PlayerCommand::Load(track, + frame.get_state().get_status() == PlayStatus::kPlayStatusPlay, + frame.get_state().get_position_ms())); + } + protocol::spirc::MessageType::kMessageTypePlay => { + self.player.command(PlayerCommand::Play); + } + protocol::spirc::MessageType::kMessageTypePause => { + self.player.command(PlayerCommand::Pause); + } + protocol::spirc::MessageType::kMessageTypeSeek => { + self.player.command(PlayerCommand::Seek(frame.get_position())); + } + protocol::spirc::MessageType::kMessageTypeNotify => { + if self.is_active && frame.get_device_state().get_is_active() { + self.is_active = false; + self.player.command(PlayerCommand::Stop); + } + } + _ => () + } + } + + fn notify(&mut self, recipient: Option<&str>) { + let mut pkt = 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: self.device_state(), + recipient: protobuf::RepeatedField::from_vec( + recipient.map(|r| vec![r.to_string()] ).unwrap_or(vec![]) + ), + state_update_id: self.state_update_id + }); + + if self.is_active { + pkt.set_state(self.state()); + } + + self.session.mercury(MercuryRequest{ + method: MercuryMethod::SEND, + uri: format!("hm://remote/user/{}", self.username), + content_type: None, + payload: vec![ pkt.write_to_bytes().unwrap() ] + }); + } + + fn state(&mut self) -> protocol::spirc::State { + let state = self.player.state.0.lock().unwrap(); + + protobuf_init!(protocol::spirc::State::new(), { + status: state.status, + position_ms: state.position_ms, + position_measured_at: state.position_measured_at as u64, + + playing_track_index: 0, + track => [ + @{ + gid: self.track.unwrap().to_raw().to_vec() + } + ], + + shuffle: self.shuffle, + repeat: self.repeat, + + playing_from_fallback: true, + + last_command_ident: self.last_command_ident.clone(), + last_command_msgid: self.last_command_msgid + }) + } + + fn device_state(&mut self) -> protocol::spirc::DeviceState { + protobuf_init!(protocol::spirc::DeviceState::new(), { + sw_version: version_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(), + ] + } + ], + }) + } +}