summary history branches tags files
commit:1f032131fdceb583231cf6fcba569617f9e3a0ee
author:Trevor Bentley
committer:Trevor Bentley
date:Thu Apr 13 14:25:52 2017 +0200
parents:abaae1f00088af3c0e1fba850e00b240a4bf5fd3
Support refreshing the oauth token.
diff --git a/Cargo.toml b/Cargo.toml
line changes: +1/-0
index 3e3efe4..6b4f174
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,7 @@ url = "1.4.0"
 rust-ini = "0.9"
 time = "0.1"
 timer = "0.1.6"
+chrono = "0.3.0"
 
 [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: +15/-4
index 48a5909..efd5a86
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -59,8 +59,14 @@ impl fmt::Display for HttpResponse {
     }
 }
 
+pub enum AccessToken<'a> {
+    Bearer(&'a str),
+    Basic(&'a str),
+    None,
+}
+
 pub fn http(url: &str, query: &str, body: &str,
-            method: HttpMethod, access_token: Option<&String>) -> HttpResponse {
+            method: HttpMethod, access_token: AccessToken) -> 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() },
@@ -94,13 +100,18 @@ pub fn http(url: &str, query: &str, body: &str,
         }
 
         match access_token {
-            Some(access_token) => {
+            AccessToken::None => {},
+            access_token => {
+                let request = match access_token {
+                    AccessToken::Bearer(token) => ("Bearer", token),
+                    AccessToken::Basic(token) => ("Basic", token),
+                    _ => ("",""),
+                };
                 let mut list = List::new();
-                let header = format!("Authorization: Bearer {}", access_token);
+                let header = format!("Authorization: {} {}", request.0, request.1);
                 list.append(&header).unwrap();
                 easy.http_headers(list).unwrap();
             }
-            None => {}
         }
 
         {

diff --git a/src/main.rs b/src/main.rs
line changes: +4/-2
index b5f4932..7b3927f
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,6 @@
 extern crate connectr;
 use connectr::SpotifyResponse;
 
-use std::process;
-
 //extern crate systray;
 
 //#[cfg(target_os = "windows")]
@@ -63,4 +61,8 @@ fn main() {
 
     let player_state = spotify.request_player_state();
     println!("Final state:\n{}", player_state);
+
+    loop {
+        spotify.await_once(true);
+    }
 }

diff --git a/src/settings/mod.rs b/src/settings/mod.rs
line changes: +1/-3
index 68b805d..263e7f3
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -49,10 +49,8 @@ pub fn read_settings() -> Option<Settings> {
 }
 
 pub type SettingsError = String;
-pub fn save_tokens(access: &str, refresh: &str, expire: u64) -> Result<(), SettingsError> {
+pub fn save_tokens(access: &str, refresh: &str, expire_utc: 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)

diff --git a/src/webapi/mod.rs b/src/webapi/mod.rs
line changes: +106/-19
index 3cb2ec4..ad8af34
--- a/src/webapi/mod.rs
+++ b/src/webapi/mod.rs
@@ -1,10 +1,12 @@
 extern crate time;
 extern crate timer;
+extern crate chrono;
 
 use std::fmt;
 use std::iter;
 use std::process;
 use std::collections::BTreeMap;
+use std::sync::mpsc::{channel, Receiver};
 
 extern crate rustc_serialize;
 use self::rustc_serialize::{Decodable, Decoder, json};
@@ -19,7 +21,10 @@ 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();
+    let refresh_token = match obj.get("refresh_token") {
+        Some(j) => j.as_string().unwrap(),
+        None => "",
+    };
     let expires_in = obj.get("expires_in").unwrap().as_u64().unwrap();
     (String::from(access_token),String::from(refresh_token), expires_in)
 }
@@ -143,7 +148,8 @@ pub fn request_oauth_tokens(auth_code: &str, settings: &settings::Settings) -> (
         .add("client_secret", settings.secret.clone())
         .build();
 
-    let json_response = http::http(spotify_api::TOKEN, &query, "", http::HttpMethod::POST, None).unwrap();
+    let json_response = http::http(spotify_api::TOKEN, &query, "", http::HttpMethod::POST,
+                                   http::AccessToken::None).unwrap();
     parse_spotify_token(&json_response)
 }
 
@@ -283,6 +289,10 @@ pub struct SpotifyConnectr {
     refresh_token: Option<String>,
     expire_utc: Option<u64>,
     device: Option<DeviceId>,
+
+    refresh_timer: timer::Timer,
+    refresh_timer_guard: Option<timer::Guard>,
+    refresh_timer_channel: Option<Receiver<()>>,
 }
 
 impl SpotifyConnectr {
@@ -299,33 +309,110 @@ impl SpotifyConnectr {
                          access_token: access,
                          refresh_token: refresh,
                          expire_utc: expire,
-                         device: None}
+                         device: None,
+                         refresh_timer: timer::Timer::new(),
+                         refresh_timer_guard: None,
+                         refresh_timer_channel: None}
     }
     fn is_token_expired(&self) -> bool {
         let now = time::now_utc().to_timespec().sec as u64;
         let expire_utc = self.expire_utc.unwrap_or(0);
         expire_utc <= (now - 60)
     }
+    fn expire_offset_to_utc(&self, expires_in: u64) -> u64 {
+        let now = time::now_utc().to_timespec().sec as u64;
+        now + expires_in
+    }
+    fn expire_utc_to_offset(&self, expire_utc: u64) -> u64 {
+        let now = time::now_utc().to_timespec().sec as i64;
+        let offset = expire_utc as i64 - now;
+        match offset {
+            x if x > 0 => x as u64,
+            _ => 0,
+        }
+    }
+    fn schedule_token_refresh(&mut self) -> Result<(), ()> {
+        match self.expire_utc {
+            Some(expire_utc) => {
+                let (tx, rx) = channel::<()>();
+                self.refresh_timer_channel = Some(rx);
+                let expire_offset = self.expire_utc_to_offset(expire_utc) as i64;
+                let expire_offset = chrono::Duration::seconds(expire_offset);
+                let closure = move || { tx.send(()).unwrap(); };
+                self.refresh_timer_guard = Some(self.refresh_timer.schedule_with_delay(expire_offset, closure));
+                Ok(())
+            }
+            _ => Err(())
+        }
+    }
+    pub fn await_once(&mut self, blocking: bool) {
+        // Choose between blocking or non-blocking receive.
+        let recv_fn: Box<Fn(&Receiver<()>) -> bool> = match blocking {
+            true  => Box::new(move |rx| { match rx.recv() { Ok(_) => true, Err(_) => false } }),
+            false => Box::new(move |rx| { match rx.try_recv() { Ok(_) => true, Err(_) => false } }),
+        };
+        let need_refresh = match self.refresh_timer_channel.as_ref() {
+            Some(rx) => recv_fn(rx),
+            _ => false,
+        };
+        if !need_refresh {
+            return ()
+        }
+        self.refresh_timer_channel = None;
+        let (access_token, expires_in) = self.refresh_oauth_tokens();
+        self.access_token = Some(access_token);
+        self.expire_utc = Some(self.expire_offset_to_utc(expires_in));
+        println!("Refreshed credentials.");
+        let _ = self.schedule_token_refresh();
+    }
     pub fn connect(&mut self) {
-        if self.access_token.is_some() && !self.is_token_expired() {
-            println!("Reusing saved tokens");
+        if self.access_token.is_some() && !self.is_token_expired() && false {
+            println!("Reusing saved credentials.");
+            let _ = self.schedule_token_refresh();
             return ()
         }
-        println!("Requesting new auth code");
+        println!("Requesting fresh credentials.");
         self.auth_code = http::authenticate(&self.settings);
         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);
+        let expire_utc = self.expire_offset_to_utc(expires_in);
+        let _ = settings::save_tokens(&access_token, &refresh_token, expire_utc);
         self.access_token = Some(access_token);
         self.refresh_token = Some(refresh_token);
+        self.expire_utc = Some(expire_utc);
+        let _ = self.schedule_token_refresh();
+    }
+    pub fn bearer_token(&self) -> http::AccessToken {
+        match self.access_token {
+            Some(ref x) => http::AccessToken::Bearer(x),
+            None => http::AccessToken::None,
+        }
+    }
+    pub fn basic_token(&self) -> http::AccessToken {
+        match self.access_token {
+            Some(ref x) => http::AccessToken::Basic(x),
+            None => http::AccessToken::None,
+        }
+    }
+    pub fn refresh_oauth_tokens(&self) -> (String, u64) {
+        let query = QueryString::new()
+            .add("grant_type", "refresh_token")
+            .add("refresh_token", self.refresh_token.as_ref().unwrap())
+            .add("client_id", self.settings.client_id.clone())
+            .add("client_secret", self.settings.secret.clone())
+            .build();
+        let json_response = http::http(spotify_api::TOKEN, &query, "",
+                                       http::HttpMethod::POST, http::AccessToken::None).unwrap();
+        let (access_token, _, expires_in) = parse_spotify_token(&json_response);
+        (access_token, expires_in)
     }
     pub fn request_device_list(&self) -> ConnectDeviceList {
         let json_response = http::http(spotify_api::DEVICES, "", "",
-                                       http::HttpMethod::GET, self.access_token.as_ref()).unwrap();
+                                       http::HttpMethod::GET, self.bearer_token()).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, self.access_token.as_ref()).unwrap();
+                                       http::HttpMethod::GET, self.bearer_token()).unwrap();
         json::decode(&json_response).unwrap()
     }
     pub fn set_target_device(&mut self, device: Option<DeviceId>) {
@@ -337,54 +424,54 @@ impl SpotifyConnectr {
             Some(x) => json::encode(x).unwrap(),
             None => String::new(),
         };
-        http::http(spotify_api::PLAY, &query, &body, http::HttpMethod::PUT, self.access_token.as_ref())
+        http::http(spotify_api::PLAY, &query, &body, http::HttpMethod::PUT, self.bearer_token())
     }
     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, self.access_token.as_ref())
+        http::http(spotify_api::PAUSE, &query, "", http::HttpMethod::PUT, self.bearer_token())
     }
     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, self.access_token.as_ref())
+        http::http(spotify_api::NEXT, &query, "", http::HttpMethod::POST, self.bearer_token())
     }
     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, self.access_token.as_ref())
+        http::http(spotify_api::PREVIOUS, &query, "", http::HttpMethod::POST, self.bearer_token())
     }
     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, self.access_token.as_ref())
+        http::http(spotify_api::SEEK, &query, "", http::HttpMethod::PUT, self.bearer_token())
     }
     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, self.access_token.as_ref())
+        http::http(spotify_api::VOLUME, &query, "", http::HttpMethod::PUT, self.bearer_token())
     }
     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, self.access_token.as_ref())
+        http::http(spotify_api::SHUFFLE, &query, "", http::HttpMethod::PUT, self.bearer_token())
     }
     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, self.access_token.as_ref())
+        http::http(spotify_api::REPEAT, &query, "", http::HttpMethod::PUT, self.bearer_token())
     }
     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, self.access_token.as_ref())
+        http::http(spotify_api::PLAYER, "", &body, http::HttpMethod::PUT, self.bearer_token())
     }
     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, self.access_token.as_ref())
+        http::http(spotify_api::PLAYER, "", &body, http::HttpMethod::PUT, self.bearer_token())
     }
 }