summary history branches tags files
commit:60644155047b95fd5a6464aa32aa5e6cb7242948
author:Trevor Bentley
committer:Trevor Bentley
date:Wed Apr 12 18:46:55 2017 +0200
parents:45b66f7b5d45f4644358aaf1c6ce95cbad9af833
Pretty print, and save/re-use access tokens
diff --git a/Cargo.toml b/Cargo.toml
line changes: +1/-0
index 5777c6e..a3c29b9
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,6 +23,7 @@ regex = "0.2"
 rustc-serialize = "0.3.23"
 url = "1.4.0"
 rust-ini = "0.9"
+time = "0.1"
 
 [target."cfg(windows)".dependencies]
 [target."cfg(all(unix, not(target_os = \"macos\")))".dependencies]

diff --git a/src/http/mod.rs b/src/http/mod.rs
line changes: +1/-1
index 8e6df9e..48a5909
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -60,7 +60,7 @@ impl fmt::Display for HttpResponse {
 }
 
 pub fn http(url: &str, query: &str, body: &str,
-            method: HttpMethod, access_token: Option<&str>) -> HttpResponse {
+            method: HttpMethod, access_token: Option<&String>) -> HttpResponse {
     let enc_query = percent_encoding::utf8_percent_encode(&query, percent_encoding::QUERY_ENCODE_SET).collect::<String>();
     let mut data = match method {
         HttpMethod::POST => { enc_query.as_bytes() },

diff --git a/src/main.rs b/src/main.rs
line changes: +8/-8
index 2386814..b5f4932
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,4 @@
 extern crate connectr;
-use connectr::settings;
 use connectr::SpotifyResponse;
 
 use std::process;
@@ -31,18 +30,19 @@ fn require(response: SpotifyResponse) {
 }
 
 fn main() {
-    let settings = match settings::read_settings() {
-        Some(s) => s,
-        None => process::exit(0),
-    };
-    let mut spotify = connectr::SpotifyConnectr::new(settings);
+    let mut spotify = connectr::SpotifyConnectr::new();
     spotify.connect();
 
     let device_list = spotify.request_device_list();
     let player_state = spotify.request_player_state();
 
-    println!("Devices:\n{}", device_list);
-    println!("State:\n{}", player_state);
+    println!("Visible Devices:");
+    for dev in device_list {
+        println!("{}", dev);
+    }
+    println!("");
+
+    println!("Playback State:\n{}", player_state);
 
     let ctx = connectr::PlayContext::new()
         .context_uri("spotify:user:mrmekon:playlist:4XqYlbPdDUsranzjicPCgf")

diff --git a/src/settings/mod.rs b/src/settings/mod.rs
line changes: +31/-1
index e2485d7..68b805d
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -1,10 +1,15 @@
 extern crate ini;
 use self::ini::Ini;
 
+extern crate time;
+
 pub struct Settings {
     pub port: u32,
     pub secret: String,
     pub client_id: String,
+    pub access_token: Option<String>,
+    pub refresh_token: Option<String>,
+    pub expire_utc: Option<u64>,
 }
 
 pub fn read_settings() -> Option<Settings> {
@@ -28,5 +33,30 @@ pub fn read_settings() -> Option<Settings> {
         println!("");
         return None;
     }
-    Some(Settings { secret: secret.to_string(), client_id: client_id.to_string(), port: port })
+
+    let mut access = None;
+    let mut refresh = None;
+    let mut expire_utc = None;
+    if let Some(section) = conf.section(Some("tokens".to_owned())) {
+        access = Some(section.get("access").unwrap().clone());
+        refresh = Some(section.get("refresh").unwrap().clone());
+        expire_utc = Some(section.get("expire").unwrap().parse().unwrap());
+        println!("Read access token from INI!");
+    }
+
+    Some(Settings { secret: secret.to_string(), client_id: client_id.to_string(), port: port,
+                    access_token: access, refresh_token: refresh, expire_utc: expire_utc})
+}
+
+pub type SettingsError = String;
+pub fn save_tokens(access: &str, refresh: &str, expire: u64) -> Result<(), SettingsError> {
+    let mut conf = Ini::load_from_file("connectr.ini").unwrap();
+    let now = time::now_utc().to_timespec().sec as u64;
+    let expire_utc = now + expire;
+    conf.with_section(Some("tokens".to_owned()))
+        .set("access", access)
+        .set("refresh", refresh)
+        .set("expire", expire_utc.to_string());
+    conf.write_to_file("connectr.ini").unwrap();
+    Ok(())
 }

diff --git a/src/webapi/mod.rs b/src/webapi/mod.rs
line changes: +103/-26
index 217c435..33b9a22
--- a/src/webapi/mod.rs
+++ b/src/webapi/mod.rs
@@ -1,4 +1,8 @@
+extern crate time;
+
 use std::fmt;
+use std::iter;
+use std::process;
 use std::collections::BTreeMap;
 
 extern crate rustc_serialize;
@@ -10,12 +14,13 @@ use super::settings;
 use super::spotify_api;
 use super::http::HttpResponse;
 
-pub fn parse_spotify_token(json: &str) -> (String, String) {
+pub fn parse_spotify_token(json: &str) -> (String, String, u64) {
     let json_data = Json::from_str(&json).unwrap();
     let obj = json_data.as_object().unwrap();
     let access_token = obj.get("access_token").unwrap().as_string().unwrap();
     let refresh_token = obj.get("refresh_token").unwrap().as_string().unwrap();
-    (String::from(access_token),String::from(refresh_token))
+    let expires_in = obj.get("expires_in").unwrap().as_u64().unwrap();
+    (String::from(access_token),String::from(refresh_token), expires_in)
 }
 
 //#[derive(RustcDecodable, RustcEncodable, Debug)]
@@ -29,6 +34,12 @@ pub struct ConnectDevice {
     pub volume_percent: Option<u32>
 }
 
+impl fmt::Display for ConnectDevice {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{:<40} <{}>", self.name, self.id)
+    }
+}
+
 impl Decodable for ConnectDevice {
     fn decode<D: Decoder>(d: &mut D) -> Result<ConnectDevice, D::Error> {
         d.read_struct("ConnectDevice", 6, |d| {
@@ -55,35 +66,74 @@ impl Decodable for ConnectDevice {
 
 #[derive(RustcDecodable, RustcEncodable)]
 pub struct ConnectDeviceList {
-    pub devices: Vec<ConnectDevice>
+    pub devices: Vec<ConnectDevice>,
 }
 
 impl fmt::Display for ConnectDeviceList {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         for dev in &self.devices {
-            let _ = write!(f, "{:?}\n", dev);
+            let _ = write!(f, "{}\n", dev);
         }
         Ok(())
     }
 }
 
+impl iter::IntoIterator for ConnectDeviceList {
+    type Item = ConnectDevice;
+    type IntoIter = ::std::vec::IntoIter<ConnectDevice>;
+    fn into_iter(self) -> Self::IntoIter {
+        self.devices.into_iter()
+    }
+}
+
+#[derive(RustcDecodable, RustcEncodable, Debug)]
+pub struct ConnectPlaybackItem {
+    duration_ms: u32,
+    name: String,
+    uri: String,
+}
+
+#[derive(RustcDecodable, RustcEncodable, Debug)]
+pub struct ConnectContext {
+    uri: String,
+}
+
 #[derive(RustcDecodable, RustcEncodable, Debug)]
 pub struct PlayerState {
     pub timestamp: u64,
     pub device: ConnectDevice,
     pub progress_ms: Option<u32>,
     pub is_playing: bool,
+    pub item: ConnectPlaybackItem,
     pub shuffle_state: bool,
     pub repeat_state: String,
+    pub context: ConnectContext,
 }
 
 impl fmt::Display for PlayerState {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{:?}\n", self)
+        let play_state = match self.is_playing {
+            true => "Playing",
+            false => "Paused",
+        };
+        let volume = match self.device.volume_percent {
+            Some(x) => x.to_string(),
+            None => "???".to_string(),
+        };
+        let position: f64 = match self.progress_ms {
+            Some(x) => (x as f64)/1000.0,
+            None => 0.0,
+        };
+        let duration: f64 = (self.item.duration_ms as f64) / 1000.0;
+        let progress: f64 = position/duration*100.0;
+        write!(f, "{} on {} [Volume {}%]\n{} <{}>\n{}s / {}s ({:.1}%)\n",
+               play_state, self.device.name, volume,
+               &self.item.name, &self.item.uri,
+               position, duration, progress)
     }
 }
 
-pub fn request_oauth_tokens(auth_code: &str, settings: &settings::Settings) -> (String, String) {
+pub fn request_oauth_tokens(auth_code: &str, settings: &settings::Settings) -> (String, String, u64) {
     let query = QueryString::new()
         .add("grant_type", "authorization_code")
         .add("code", auth_code)
@@ -228,31 +278,58 @@ struct DeviceIdList {
 pub struct SpotifyConnectr {
     settings: settings::Settings,
     auth_code: String,
-    access_token: String,
-    refresh_token: String,
+    access_token: Option<String>,
+    refresh_token: Option<String>,
+    expire_utc: Option<u64>,
     device: Option<DeviceId>,
 }
 
 impl SpotifyConnectr {
-    pub fn new(settings: settings::Settings) -> SpotifyConnectr {
-        SpotifyConnectr {settings: settings, auth_code: String::new(),
-                         access_token: String::new(), refresh_token: String::new(),
+    pub fn new() -> SpotifyConnectr {
+        let settings = match settings::read_settings() {
+            Some(s) => s,
+            None => process::exit(0),
+        };
+        let access = match settings.access_token {
+            Some(ref x) => x.clone(),
+            None => String::new(),
+        };
+        let refresh = match settings.refresh_token {
+            Some(ref x) => x.clone(),
+            None => String::new(),
+        };
+        let expire = settings.expire_utc;
+        let access = settings.access_token.clone();
+        let refresh = settings.refresh_token.clone();
+        SpotifyConnectr {settings: settings,
+                         auth_code: String::new(),
+                         access_token: access,
+                         refresh_token: refresh,
+                         expire_utc: expire,
                          device: None}
     }
     pub fn connect(&mut self) {
+        let now = time::now_utc().to_timespec().sec as u64;
+        let expire_utc = self.expire_utc.unwrap_or(0);
+        if self.access_token.is_some() && now < expire_utc {
+            println!("Reusing saved tokens");
+            return ()
+        }
+        println!("Requesting new auth code");
         self.auth_code = http::authenticate(&self.settings);
-        let (access_token, refresh_token) = request_oauth_tokens(&self.auth_code, &self.settings);
-        self.access_token = access_token;
-        self.refresh_token = refresh_token;
+        let (access_token, refresh_token, expires_in) = request_oauth_tokens(&self.auth_code, &self.settings);
+        let _ = settings::save_tokens(&access_token, &refresh_token, expires_in);
+        self.access_token = Some(access_token);
+        self.refresh_token = Some(refresh_token);
     }
     pub fn request_device_list(&self) -> ConnectDeviceList {
         let json_response = http::http(spotify_api::DEVICES, "", "",
-                                       http::HttpMethod::GET, Some(&self.access_token)).unwrap();
+                                       http::HttpMethod::GET, self.access_token.as_ref()).unwrap();
         json::decode(&json_response).unwrap()
     }
     pub fn request_player_state(&self) -> PlayerState {
         let json_response = http::http(spotify_api::PLAYER_STATE, "", "",
-                                       http::HttpMethod::GET, Some(&self.access_token)).unwrap();
+                                       http::HttpMethod::GET, self.access_token.as_ref()).unwrap();
         json::decode(&json_response).unwrap()
     }
     pub fn set_target_device(&mut self, device: Option<DeviceId>) {
@@ -264,54 +341,54 @@ impl SpotifyConnectr {
             Some(x) => json::encode(x).unwrap(),
             None => String::new(),
         };
-        http::http(spotify_api::PLAY, &query, &body, http::HttpMethod::PUT, Some(&self.access_token))
+        http::http(spotify_api::PLAY, &query, &body, http::HttpMethod::PUT, self.access_token.as_ref())
     }
     pub fn pause(&self) -> SpotifyResponse {
         let query = QueryString::new().add_opt("device_id", self.device.clone()).build();
-        http::http(spotify_api::PAUSE, &query, "", http::HttpMethod::PUT, Some(&self.access_token))
+        http::http(spotify_api::PAUSE, &query, "", http::HttpMethod::PUT, self.access_token.as_ref())
     }
     pub fn next(&self) -> SpotifyResponse {
         let query = QueryString::new().add_opt("device_id", self.device.clone()).build();
-        http::http(spotify_api::NEXT, &query, "", http::HttpMethod::POST, Some(&self.access_token))
+        http::http(spotify_api::NEXT, &query, "", http::HttpMethod::POST, self.access_token.as_ref())
     }
     pub fn previous(&self) -> SpotifyResponse {
         let query = QueryString::new().add_opt("device_id", self.device.clone()).build();
-        http::http(spotify_api::PREVIOUS, &query, "", http::HttpMethod::POST, Some(&self.access_token))
+        http::http(spotify_api::PREVIOUS, &query, "", http::HttpMethod::POST, self.access_token.as_ref())
     }
     pub fn seek(&self, position: u32) -> SpotifyResponse {
         let query = QueryString::new()
             .add_opt("device_id", self.device.clone())
             .add("position_ms", position)
             .build();
-        http::http(spotify_api::SEEK, &query, "", http::HttpMethod::PUT, Some(&self.access_token))
+        http::http(spotify_api::SEEK, &query, "", http::HttpMethod::PUT, self.access_token.as_ref())
     }
     pub fn volume(&self, volume: u32) -> SpotifyResponse {
         let query = QueryString::new()
             .add_opt("device_id", self.device.clone())
             .add("volume_percent", volume)
             .build();
-        http::http(spotify_api::VOLUME, &query, "", http::HttpMethod::PUT, Some(&self.access_token))
+        http::http(spotify_api::VOLUME, &query, "", http::HttpMethod::PUT, self.access_token.as_ref())
     }
     pub fn shuffle(&self, shuffle: bool) -> SpotifyResponse {
         let query = QueryString::new()
             .add_opt("device_id", self.device.clone())
             .add("state", shuffle)
             .build();
-        http::http(spotify_api::SHUFFLE, &query, "", http::HttpMethod::PUT, Some(&self.access_token))
+        http::http(spotify_api::SHUFFLE, &query, "", http::HttpMethod::PUT, self.access_token.as_ref())
     }
     pub fn repeat(&self, repeat: SpotifyRepeat) -> SpotifyResponse {
         let query = QueryString::new()
             .add_opt("device_id", self.device.clone())
             .add("state", repeat)
             .build();
-        http::http(spotify_api::REPEAT, &query, "", http::HttpMethod::PUT, Some(&self.access_token))
+        http::http(spotify_api::REPEAT, &query, "", http::HttpMethod::PUT, self.access_token.as_ref())
     }
     pub fn transfer_multi(&self, devices: Vec<String>, play: bool) -> SpotifyResponse {
         let body = json::encode(&DeviceIdList {device_ids: devices, play: play}).unwrap();
-        http::http(spotify_api::PLAYER, "", &body, http::HttpMethod::PUT, Some(&self.access_token))
+        http::http(spotify_api::PLAYER, "", &body, http::HttpMethod::PUT, self.access_token.as_ref())
     }
     pub fn transfer(&self, device: String, play: bool) -> SpotifyResponse {
         let body = json::encode(&DeviceIdList {device_ids: vec![device], play: play}).unwrap();
-        http::http(spotify_api::PLAYER, "", &body, http::HttpMethod::PUT, Some(&self.access_token))
+        http::http(spotify_api::PLAYER, "", &body, http::HttpMethod::PUT, self.access_token.as_ref())
     }
 }