summary history branches tags files
commit:47b539c02240468027cbc0f261c7a135507f2e8b
author:Trevor Bentley
committer:Trevor Bentley
date:Fri Apr 7 11:49:17 2017 +0200
parents:8235e8349239a782de9cbddd182753ec0e8b804b
Initial check in.  Basic enumeration of Connect devices possible.
diff --git a/Cargo.toml b/Cargo.toml
line changes: +13/-0
index 0000000..4843c2d
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+
+name = "connectr"
+version = "0.0.1"
+authors = [ "Trevor Bentley <trevor@trevorbentley.com>" ]
+
+[dependencies]
+curl = "0.4.6"
+open = "1.2.0"
+regex = "0.2"
+rustc-serialize = "0.3.23"
+url = "1.4.0"
+rust-ini = "0.9"

diff --git a/connectr.ini b/connectr.ini
line changes: +6/-0
index 0000000..cd17682
--- /dev/null
+++ b/connectr.ini
@@ -0,0 +1,6 @@
+[connectr]
+port = 5432
+
+[application]
+client_id = <SPOTIFY_CLIENT_ID>
+secret = <SPOTIFY_CLIENT_SECRET>

diff --git a/src/main.rs b/src/main.rs
line changes: +206/-0
index 0000000..7fe65c6
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,206 @@
+mod spotify_api {
+    pub const SCOPES: &'static [&'static str] = &["user-read-private", "streaming", "user-read-playback-state"];
+    pub const AUTHORIZE: &'static str = "https://accounts.spotify.com/en/authorize";
+    pub const TOKEN: &'static str = "https://accounts.spotify.com/api/token";
+    pub const DEVICES: &'static str = "https://api.spotify.com/v1/me/player/devices";
+}
+
+#[derive(PartialEq)]
+enum HttpMethod {
+    GET,
+    POST,
+}
+
+extern crate curl;
+extern crate open;
+extern crate regex;
+extern crate rustc_serialize;
+extern crate url;
+extern crate ini;
+
+use std::net::{TcpListener};
+use std::io::{Read, Write, BufReader, BufRead};
+
+use curl::easy::{Easy, List};
+use regex::Regex;
+use rustc_serialize::json::Json;
+use rustc_serialize::json;
+use rustc_serialize::Decodable;
+use rustc_serialize::Decoder;
+use url::percent_encoding;
+use ini::Ini;
+
+fn oauth_request_with_local_webserver(port: u32, url: &str, reply: &str) -> Vec<String> {
+    if !open::that(url).is_ok() {
+        return Vec::<String>::new()
+    }
+    let host = format!("127.0.0.1:{}", port);
+    let listener = TcpListener::bind(host).unwrap();
+    let stream = listener.accept().unwrap().0;
+    let mut reader = BufReader::new(stream);
+    let mut response = Vec::<String>::new();
+    for line in reader.by_ref().lines() {
+        let line_str = line.unwrap();
+        response.push(line_str.clone());
+        if line_str == "" {
+            break;
+        }
+    }
+    let _ = reader.into_inner().write(reply.as_bytes());
+    response
+}
+
+fn spotify_auth_code(lines: Vec<String>) -> String {
+    let mut auth_code = String::new();
+    for line in lines {
+        let line_str = line;
+        let re = Regex::new(r"code=([^?& ]+)").unwrap();
+        let ismatch = re.is_match(line_str.as_str());
+        if ismatch {
+            let cap = re.captures(line_str.as_str()).unwrap();
+            auth_code = auth_code + &cap[1];
+        }
+    }
+    auth_code
+}
+
+macro_rules! vec_of_strings {
+    ($($x:expr),*) => (vec![$($x.to_string()),*]);
+}
+
+fn authenticate(settings: &Settings) -> String {
+    let scopes = spotify_api::SCOPES.join(" ");
+    let host = format!("http://127.0.0.1:{}", settings.port);
+    let url = format!("{}?client_id={}&response_type=code&scope={}&redirect_uri={}",
+                      spotify_api::AUTHORIZE,settings.client_id, scopes, host);
+    let query = percent_encoding::utf8_percent_encode(&url, percent_encoding::QUERY_ENCODE_SET).collect::<String>();
+    let response = "HTTP/1.1 200 OK\r\n\r\nAuthenticated with Spotify.\r\n\r\nYou can close this window.\r\n";
+    let auth_lines = oauth_request_with_local_webserver(settings.port, &query, response);
+    let auth_code = spotify_auth_code(auth_lines);
+    auth_code
+}
+
+fn http(url: &str, query: &str, method: HttpMethod, access_token: Option<&str>) -> String {
+    let mut data = query.as_bytes();
+    let mut json_bytes = Vec::<u8>::new();
+    {
+        let mut easy = Easy::new();
+        match method {
+            HttpMethod::POST => {
+                easy.url(url).unwrap();
+                easy.post(true).unwrap();
+                easy.post_field_size(data.len() as u64).unwrap();
+            }
+            _ => {
+                let get_url = format!("{}?{}", url, query);
+                easy.url(&get_url).unwrap();
+            }
+        }
+
+        match access_token {
+            Some(access_token) => {
+                let mut list = List::new();
+                let header = format!("Authorization: Bearer {}", access_token);
+                list.append(&header).unwrap();
+                easy.http_headers(list).unwrap();
+            }
+            None => {}
+        }
+
+        {
+            let mut transfer = easy.transfer();
+            if method == HttpMethod::POST {
+                transfer.read_function(|buf| {
+                    Ok(data.read(buf).unwrap_or(0))
+                }).unwrap();
+            }
+            transfer.write_function(|x| {
+                json_bytes.extend(x);
+                Ok(x.len())
+            }).unwrap();
+            transfer.perform().unwrap();
+        }
+    }
+    String::from_utf8(json_bytes).unwrap()
+}
+
+fn parse_spotify_token(json: &str) -> (String, String) {
+    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))
+}
+
+//#[derive(RustcDecodable, RustcEncodable, Debug)]
+#[derive(RustcEncodable, Debug)]
+struct ConnectDevice {
+    id: String,
+    is_active: bool,
+    is_restricted: bool,
+    name: String,
+    device_type: String,
+    volume_percent: u32
+}
+
+impl Decodable for ConnectDevice {
+    fn decode<D: Decoder>(d: &mut D) -> Result<ConnectDevice, D::Error> {
+        d.read_struct("ConnectDevice", 6, |d| {
+            let id = try!(d.read_struct_field("id", 0, |d| { d.read_str() }));
+            let is_active = try!(d.read_struct_field("is_active", 1, |d| { d.read_bool() }));
+            let is_restricted = try!(d.read_struct_field("is_restricted", 2, |d| { d.read_bool() }));
+            let name = try!(d.read_struct_field("name", 3, |d| { d.read_str() }));
+            let device_type = try!(d.read_struct_field("type", 4, |d| { d.read_str() }));
+            let volume_percent = try!(d.read_struct_field("volume_percent", 5, |d| { d.read_u32() }));
+            Ok(ConnectDevice{ id: id,
+                              is_active: is_active,
+                              is_restricted: is_restricted,
+                              name: name,
+                              device_type: device_type,
+                              volume_percent: volume_percent})
+        })
+    }
+}
+
+#[derive(RustcDecodable, RustcEncodable)]
+struct ConnectDeviceList {
+    devices: Vec<ConnectDevice>
+}
+
+struct Settings {
+    port: u32,
+    secret: String,
+    client_id: String,
+}
+
+fn read_settings() -> Settings {
+    let conf = Ini::load_from_file("connectr.ini").unwrap();
+
+    let section = conf.section(Some("connectr".to_owned())).unwrap();
+    let port = section.get("port").unwrap().parse().unwrap();
+
+    let section = conf.section(Some("application".to_owned())).unwrap();
+    let secret = section.get("secret").unwrap();
+    let client_id = section.get("client_id").unwrap();
+    Settings { secret: secret.to_string(), client_id: client_id.to_string(), port: port }
+}
+
+fn main() {
+    let settings = read_settings();
+    let auth_code = authenticate(&settings);
+    let query = format!("grant_type=authorization_code&code={}&redirect_uri=http://127.0.0.1:{}&client_id={}&client_secret={}",
+                        auth_code, settings.port, settings.client_id, settings.secret);
+    let query = percent_encoding::utf8_percent_encode(&query, percent_encoding::QUERY_ENCODE_SET).collect::<String>();
+    let json_response = http(spotify_api::TOKEN, &query, HttpMethod::POST, None);
+    let (access_token, refresh_token) = parse_spotify_token(&json_response);
+
+    let json_response = http(spotify_api::DEVICES, "", HttpMethod::GET, Some(&access_token));
+
+    let decoded: ConnectDeviceList = json::decode(&json_response).unwrap();
+
+    println!("Auth Code: {}...", &auth_code[0..5]);
+    println!("Access: {}... / Refresh: {}...", &access_token[0..5], &refresh_token[0..5]);
+    for dev in decoded.devices {
+        println!("{:?}", dev);
+    }
+}