summary history branches tags files
commit:249f785ba7f2d8518dbcd2ab2b1ddb3e6ed946aa
author:Trevor Bentley
committer:Trevor Bentley
date:Fri Apr 14 20:28:17 2017 +0200
parents:c6c187da00b3a80ac47b4a8ddbde9f1605d0a6d5
OSX menu bar app: Device selection, play/pause, presets, and tooltip
diff --git a/Cargo.toml b/Cargo.toml
line changes: +13/-1
index 6b4f174..d03ce74
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,6 +16,9 @@ debug-assertions = false
 codegen-units = 1
 panic = 'unwind'
 
+[features]
+verbose_http = []
+
 [dependencies]
 curl = "0.4.6"
 open = "1.2.0"
@@ -28,5 +31,14 @@ timer = "0.1.6"
 chrono = "0.3.0"
 
 [target."cfg(windows)".dependencies]
+
 [target."cfg(all(unix, not(target_os = \"macos\")))".dependencies]
-[target."cfg(macos)".dependencies]
+
+[target."cfg(target_os = \"macos\")".dependencies]
+cocoa = "0.8.1"
+objc-foundation = "0.1.1"
+objc_id = "0.1"
+
+[target."cfg(target_os = \"macos\")".dependencies.objc]
+version = "0.2.2"
+features = ["exception"] 
\ No newline at end of file

diff --git a/spotify.png b/spotify.png
line changes: +0/-0
index 0000000..0b949bb
--- /dev/null
+++ b/spotify.png

diff --git a/src/http/mod.rs b/src/http/mod.rs
line changes: +3/-1
index efd5a86..91af8cb
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -27,7 +27,7 @@ pub enum HttpMethod {
 pub type HttpErrorString = String;
 pub struct HttpResponse {
     pub code: Option<u32>,
-    data: Result<String, HttpErrorString>,
+    pub data: Result<String, HttpErrorString>,
 }
 
 impl HttpResponse {
@@ -142,6 +142,8 @@ pub fn http(url: &str, query: &str, body: &str,
         Ok(x) => { Ok(x) }
         Err(x) => { Err(x.utf8_error().description().to_string()) }
     };
+    #[cfg(feature = "verbose_http")]
+    println!("HTTP response: {}", result.clone().unwrap());
     HttpResponse {code: response, data: result }
 }
 

diff --git a/src/lib.rs b/src/lib.rs
line changes: +8/-0
index 96220b7..865cf41
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,8 +2,16 @@ pub mod http;
 pub mod settings;
 pub mod webapi;
 
+// Re-export webapi interface to connectr root
 pub use webapi::*;
 
+#[cfg(target_os = "macos")]
+pub mod osx;
+
+#[cfg(target_os = "macos")]
+#[macro_use]
+extern crate objc;
+
 extern crate rustc_serialize;
 
 pub mod spotify_api {

diff --git a/src/main.rs b/src/main.rs
line changes: +175/-43
index 7b3927f..3d23401
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,68 +1,200 @@
 extern crate connectr;
 use connectr::SpotifyResponse;
 
-//extern crate systray;
-
-//#[cfg(target_os = "windows")]
-//fn systray(player_state: PlayerState) {
-//    let mut app;
-//    match systray::Application::new() {
-//        Ok(w) => app = w,
-//        Err(e) => panic!("Can't create systray window.")
-//    }
-//    let mut w = &mut app.window;
-//    let _ = w.set_icon_from_file(&"spotify.ico".to_string());
-//    let _ = w.set_tooltip(&"Whatever".to_string());
-//    let _ = w.add_menu_item(&"Print a thing".to_string(), |window| {
-//        println!("Printing a thing!");
-//    });
-//    println!("Waiting on message!");
-//    w.wait_for_message();
-//}
+use std::ptr;
+use std::thread::sleep;
+use std::time::Duration;
+use std::sync::mpsc::channel;
 
-fn require(response: SpotifyResponse) {
-    match response.code.unwrap() {
-        200 ... 299 => (),
-        _ => panic!("{}", response)
-    }
+extern crate rustc_serialize;
+use rustc_serialize::json;
+
+#[derive(RustcDecodable, RustcEncodable, Debug)]
+enum CallbackAction {
+    SelectDevice,
+    PlayPause,
+    Preset,
+}
+
+#[derive(RustcDecodable, RustcEncodable, Debug)]
+struct MenuCallbackCommand {
+    action: CallbackAction,
+    sender: u64,
+    data: String,
+}
+
+#[cfg(target_os = "macos")]
+use connectr::osx;
+#[cfg(target_os = "macos")]
+use connectr::osx::TStatusBar;
+#[cfg(target_os = "macos")]
+use connectr::osx::MenuItem;
+
+struct MenuItems {
+    device: Vec<MenuItem>,
+    play: MenuItem,
+    preset: Vec<MenuItem>,
+}
+struct ConnectrApp {
+    menu: MenuItems,
 }
 
 fn main() {
+    let mut app = ConnectrApp {
+        menu: MenuItems {
+            device: Vec::<MenuItem>::new(),
+            play: ptr::null_mut(),
+            preset: Vec::<MenuItem>::new(),
+        }
+    };
+    let (tx,rx) = channel::<String>();
     let mut spotify = connectr::SpotifyConnectr::new();
     spotify.connect();
+    spotify.set_target_device(None);
+    let mut status = osx::OSXStatusBar::new(tx);
 
     let device_list = spotify.request_device_list();
-    let player_state = spotify.request_player_state();
+
+    status.add_label("DEVICES:");
+    status.add_separator();
 
     println!("Visible Devices:");
     for dev in device_list {
         println!("{}", dev);
+        let id = dev.id.clone();
+        let cb: osx::NSCallback = Box::new(move |sender, tx| {
+            let cmd = MenuCallbackCommand {
+                action: CallbackAction::SelectDevice,
+                sender: sender,
+                data: id.to_owned(),
+            };
+            let _ = tx.send(json::encode(&cmd).unwrap());
+        });
+        let item = status.add_item(&dev.name, cb, dev.is_active);
+        app.menu.device.push(item);
     }
     println!("");
 
+    let player_state = spotify.request_player_state();
     println!("Playback State:\n{}", player_state);
+    let play_str = format!("{: ^50}\n{: ^50}\n{: ^50}",
+                           &player_state.item.name,
+                           &player_state.item.artists[0].name,
+                           &player_state.item.album.name);
+    status.set_tooltip(&play_str);
 
-    let ctx = connectr::PlayContext::new()
-        .context_uri("spotify:user:mrmekon:playlist:4XqYlbPdDUsranzjicPCgf")
-        .offset_position(2)
-        .build();
-
-    spotify.set_target_device(None);
-    require(spotify.play(Some(&ctx)));
-    require(spotify.pause());
-    require(spotify.next());
-    require(spotify.previous());
-    require(spotify.seek(5000));
-    require(spotify.volume(10));
-    require(spotify.shuffle(true));
-    require(spotify.repeat(connectr::SpotifyRepeat::Context));
-    require(spotify.transfer_multi(vec!["1a793f2a23989a1c35d05b2fd1ff00e9a67e7134".to_string()], false));
-    require(spotify.transfer("1a793f2a23989a1c35d05b2fd1ff00e9a67e7134".to_string(), false));
+    status.add_label("");
+    status.add_label("ACTIONS:");
+    status.add_separator();
+    {
+        let play_str = match player_state.is_playing {
+            true => "PAUSE",
+            false => "PLAY",
+        };
+        let cb: osx::NSCallback = Box::new(move |sender, tx| {
+            let is_playing = &player_state.is_playing;
+            let cmd = MenuCallbackCommand {
+                action: CallbackAction::PlayPause,
+                sender: sender,
+                data: is_playing.to_string(),
+            };
+            let _ = tx.send(json::encode(&cmd).unwrap());
+        });
+        app.menu.play = status.add_item(&play_str, cb, false);
+    }
 
-    let player_state = spotify.request_player_state();
-    println!("Final state:\n{}", player_state);
+    status.add_label("");
+    status.add_label("PRESETS:");
+    status.add_separator();
+    {
+        for uri in vec!["spotify:user:mrmekon:playlist:4c8eKK6kKrcdt1HToEX7Jc",
+                        "spotify:user:spotify:playlist:37i9dQZEVXcOmDhsenkuCu"] {
+            let cb: osx::NSCallback = Box::new(move |sender, tx| {
+                let cmd = MenuCallbackCommand {
+                    action: CallbackAction::Preset,
+                    sender: sender,
+                    data: uri.to_owned(),
+                };
+                let _ = tx.send(json::encode(&cmd).unwrap());
+            });
+            let item = status.add_item(uri, cb, false);
+            app.menu.preset.push(item);
+        }
+    }
 
     loop {
-        spotify.await_once(true);
+        spotify.await_once(false);
+        if let Ok(s) = rx.try_recv() {
+            println!("Received {}", s);
+            let cmd: MenuCallbackCommand = json::decode(&s).unwrap();
+            match cmd.action {
+                CallbackAction::SelectDevice => {
+                    let device = &app.menu.device;
+                    for item in device {
+                        status.unsel_item(*item as u64);
+                    }
+                    status.sel_item(cmd.sender);
+                    // Spotify is broken.  Must be 'true', always starts playing.
+                    require(spotify.transfer(cmd.data, true));
+                },
+                CallbackAction::PlayPause => {
+                    let player_state = spotify.request_player_state();
+                    match player_state.is_playing {
+                        true => {require(spotify.pause());},
+                        false => {require(spotify.play(None));},
+                    }
+                    //let player_state = spotify.request_player_state();
+                    let play_str = match player_state.is_playing {
+                        false => "PAUSE",
+                        true => "PLAY",
+                    };
+                    status.update_item(app.menu.play, play_str);
+                },
+                CallbackAction::Preset => {
+                    play_uri(&mut spotify, None, Some(&cmd.data));
+                }
+            }
+        }
+        status.run(false);
+        sleep(Duration::from_millis(10));
+    }
+}
+
+fn require(response: SpotifyResponse) {
+    match response.code.unwrap() {
+        200 ... 299 => (),
+        _ => panic!("{}", response)
     }
+    println!("Response: {}", response.code.unwrap());
 }
+
+fn play_uri(spotify: &mut connectr::SpotifyConnectr, device: Option<&str>, uri: Option<&str>) {
+    match device {
+        Some(dev) => { spotify.set_target_device(Some(dev.to_string())); },
+        None => { spotify.set_target_device(None); },
+    }
+    match uri {
+        Some(s) => {
+            let ctx = connectr::PlayContext::new()
+                .context_uri(s)
+                .offset_position(0)
+                .build();
+            require(spotify.play(Some(&ctx)));
+        }
+        None => {
+            println!("Transfer!");
+            require(spotify.play(None));
+        }
+    };
+}
+
+//    spotify.set_target_device(None);
+//    require(spotify.pause());
+//    require(spotify.next());
+//    require(spotify.previous());
+//    require(spotify.seek(5000));
+//    require(spotify.volume(10));
+//    require(spotify.shuffle(true));
+//    require(spotify.repeat(connectr::SpotifyRepeat::Context));
+//    require(spotify.transfer_multi(vec!["1a793f2a23989a1c35d05b2fd1ff00e9a67e7134".to_string()], false));
+//    require(spotify.transfer("1a793f2a23989a1c35d05b2fd1ff00e9a67e7134".to_string(), false));

diff --git a/src/osx/mod.rs b/src/osx/mod.rs
line changes: +166/-0
index 0000000..51407f4
--- /dev/null
+++ b/src/osx/mod.rs
@@ -0,0 +1,166 @@
+pub mod rustnsobject;
+
+extern crate objc;
+extern crate objc_foundation;
+extern crate cocoa;
+
+pub use self::rustnsobject::NSCallback;
+
+use objc::runtime::{Class, Object};
+
+use self::cocoa::base::{nil, YES};
+use self::cocoa::appkit::NSStatusBar;
+use self::cocoa::foundation::{NSAutoreleasePool,NSString};
+use self::cocoa::appkit::{NSApp,
+                          NSApplication,
+                          NSApplicationActivationPolicyAccessory,
+                          NSMenu,
+                          NSMenuItem,
+                          NSImage,
+                          NSVariableStatusItemLength,
+                          NSStatusItem,
+                          NSButton};
+
+use self::rustnsobject::{NSObj, NSObjTrait, NSObjCallbackTrait};
+
+use std::sync::mpsc::Sender;
+
+use std::thread::sleep;
+use std::time::Duration;
+
+extern crate objc_id;
+use self::objc_id::Id;
+
+pub type MenuItem = *mut Object;
+
+pub struct OSXStatusBar {
+    object: NSObj,
+    app: *mut objc::runtime::Object,
+    status_bar_item: *mut objc::runtime::Object,
+    menu_bar: *mut objc::runtime::Object,
+}
+pub trait TStatusBar {
+    type S: TStatusBar;
+    fn new(tx: Sender<String>) -> Self::S;
+    fn add_separator(&mut self);
+    fn add_label(&mut self, label: &str);
+    fn add_item(&mut self, item: &str, callback: NSCallback, selected: bool) -> *mut Object;
+    fn update_item(&mut self, item: *mut Object, label: &str);
+    fn sel_item(&mut self, sender: u64);
+    fn unsel_item(&mut self, sender: u64);
+    fn set_tooltip(&self, text: &str);
+    fn run(&mut self, block: bool);
+}
+
+impl TStatusBar for OSXStatusBar {
+    type S = OSXStatusBar;
+    fn new(tx: Sender<String>) -> OSXStatusBar {
+        let mut bar;
+        unsafe {
+            let _ = NSAutoreleasePool::new(nil);
+            let app = NSApp();
+            let status_bar = NSStatusBar::systemStatusBar(nil);
+            bar = OSXStatusBar {
+                app: app,
+                //status_bar_item: status_bar.statusItemWithLength_(NSSquareStatusItemLength),
+                status_bar_item: status_bar.statusItemWithLength_(NSVariableStatusItemLength),
+                menu_bar: NSMenu::new(nil).autorelease(),
+                object: NSObj::alloc(tx).setup(),
+            };
+            bar.app.setActivationPolicy_(NSApplicationActivationPolicyAccessory);
+            msg_send![bar.status_bar_item, setHighlightMode:YES];
+            let img = NSString::alloc(nil).init_str("spotify.png");
+            let icon = NSImage::alloc(nil).initWithContentsOfFile_(img);
+            NSButton::setTitle_(bar.status_bar_item, NSString::alloc(nil).init_str("connectr"));
+            bar.status_bar_item.button().setImage_(icon);
+            bar.status_bar_item.setMenu_(bar.menu_bar);
+            bar.object.cb_fn = Some(Box::new(
+                move |s, sender| {
+                    let cb = s.get_value(sender);
+                    cb(sender, &s.tx);
+                }
+            ));
+        }
+        bar
+    }
+    fn set_tooltip(&self, text: &str) {
+        unsafe {
+            let img = NSString::alloc(nil).init_str(text);
+            let _ = msg_send![self.status_bar_item.button(), setToolTip: img];
+        }
+    }
+    fn add_label(&mut self, label: &str) {
+        unsafe {
+            let txt = NSString::alloc(nil).init_str(label);
+            let quit_key = NSString::alloc(nil).init_str("");
+            let app_menu_item = NSMenuItem::alloc(nil)
+                .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key)
+                .autorelease();
+            self.menu_bar.addItem_(app_menu_item);
+        }
+    }
+    fn add_separator(&mut self) {
+        unsafe {
+            let cls = Class::get("NSMenuItem").unwrap();
+            let sep: *mut Object = msg_send![cls, separatorItem];
+            self.menu_bar.addItem_(sep);
+        }
+    }
+    fn add_item(&mut self, item: &str, callback: NSCallback, selected: bool) -> *mut Object {
+        unsafe {
+            let txt = NSString::alloc(nil).init_str(item);
+            let quit_key = NSString::alloc(nil).init_str("");
+            let app_menu_item = NSMenuItem::alloc(nil)
+                .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key)
+                .autorelease();
+            self.object.add_callback(app_menu_item, callback);
+            let objc = self.object.take_objc();
+            let _: () = msg_send![app_menu_item, setTarget: objc];
+            if selected {
+                let _: () = msg_send![app_menu_item, setState: 1];
+            }
+            let item: *mut Object = app_menu_item;
+            self.menu_bar.addItem_(app_menu_item);
+            item
+        }
+    }
+    fn update_item(&mut self, item: *mut Object, label: &str) {
+        unsafe {
+            let ns_label = NSString::alloc(nil).init_str(label);
+            let _: () = msg_send![item, setTitle: ns_label];
+        }
+    }
+    fn sel_item(&mut self, sender: u64) {
+        let target: *mut Object = sender as *mut Object;
+        unsafe {
+            let _: () = msg_send![target, setState: 1];
+        }
+    }
+    fn unsel_item(&mut self, sender: u64) {
+        let target: *mut Object = sender as *mut Object;
+        unsafe {
+            let _: () = msg_send![target, setState: 0];
+        }
+    }
+    fn run(&mut self, block: bool) {
+        //unsafe {
+            //self.app.run();
+        //}
+        let _ = unsafe {NSAutoreleasePool::new(nil)};
+        unsafe { let _: () = msg_send![self.app, finishLaunching]; }
+        loop {
+            sleep(Duration::from_millis(50));
+            unsafe {
+                let _ = NSAutoreleasePool::new(nil);
+                let cls = Class::get("NSDate").unwrap();
+                let date: Id<Object> = msg_send![cls, distantPast];
+                let mode = NSString::alloc(nil).init_str("kCFRunLoopDefaultMode");
+                let event: Id<Object> = msg_send![self.app, nextEventMatchingMask: -1
+                                                  untilDate: date inMode:mode dequeue: YES];
+                let _ = msg_send![self.app, sendEvent: event];
+                let _ = msg_send![self.app, updateWindows];
+            }
+            if !block { break; }
+        }
+    }
+}

diff --git a/src/osx/rustnsobject.rs b/src/osx/rustnsobject.rs
line changes: +162/-0
index 0000000..3418fe6
--- /dev/null
+++ b/src/osx/rustnsobject.rs
@@ -0,0 +1,162 @@
+// Copied from rustc-objc-foundation project by SSheldon, examples/custom_class.rs
+// https://github.com/SSheldon/rust-objc-foundation/blob/master/examples/custom_class.rs
+// Covered by MIT License: https://en.wikipedia.org/wiki/MIT_License
+
+extern crate objc;
+extern crate objc_foundation;
+extern crate objc_id;
+
+use std::sync::{Once, ONCE_INIT};
+
+use objc::Message;
+use objc::declare::ClassDecl;
+use objc::runtime::{Class, Object, Sel};
+use self::objc_foundation::{INSObject, NSObject};
+
+use std::collections::BTreeMap;
+
+use self::objc_id::Id;
+use self::objc_id::WeakId;
+use self::objc_id::Shared;
+
+use std::sync::mpsc::{channel, Sender, Receiver};
+
+pub struct RustWrapperClass {
+    pub objc: Id<ObjcSubclass, Shared>,
+    pub value: u64,
+    pub cb_fn: Option<Box<Fn(&mut RustWrapperClass, u64)>>,
+    pub channel: (Sender<u32>, Receiver<u32>),
+    pub map: BTreeMap<u64, NSCallback>,
+    pub tx: Sender<String>,
+}
+
+pub type NSObj = Box<RustWrapperClass>;
+pub type NSObjc = Id<ObjcSubclass, Shared>;
+
+pub trait NSObjCallbackTrait {
+    fn set_value(&mut self, u64, NSCallback);
+    fn get_value(&self, u64) -> &NSCallback;
+}
+
+impl NSObjCallbackTrait for RustWrapperClass {
+    fn set_value(&mut self, key: u64, val: NSCallback) {
+        self.map.insert(key, val);
+    }
+    fn get_value(&self, key: u64) -> &NSCallback {
+        self.map.get(&key).unwrap()
+    }
+}
+
+pub trait NSObjTrait {
+    fn alloc(tx: Sender<String>) -> NSObj;
+    fn setup(self) -> NSObj;
+    fn selector(&self) -> Sel;
+    fn take_objc(&mut self) -> NSObjc;
+    fn add_callback(&mut self, *const Object, NSCallback);
+}
+
+impl NSObjTrait for NSObj {
+    fn add_callback(&mut self, item: *const Object, cb: NSCallback) {
+        let sender: u64 = item as u64;
+        self.set_value(sender, cb);
+    }
+    fn alloc(tx: Sender<String>) -> NSObj {
+        let objc = ObjcSubclass::new().share();
+        let rust = Box::new(RustWrapperClass {
+            objc: objc,
+            value: 716,
+            channel: channel(),
+            map: BTreeMap::<u64,NSCallback>::new(),
+            cb_fn: None,
+            tx: tx,
+        });
+        unsafe {
+            let ptr: u64 = &*rust as *const RustWrapperClass as u64;
+            let _ = msg_send![rust.objc, setRustData: ptr];
+        }
+        return rust
+    }
+    fn setup(self) -> NSObj {
+        self
+    }
+    fn selector(&self) -> Sel {
+        sel!(cb:)
+    }
+    fn take_objc(&mut self) -> NSObjc {
+        let weak = WeakId::new(&self.objc);
+        weak.load().unwrap()
+    }
+}
+
+pub type NSObjCallback<T> = Box<Fn(&mut T, u64)>;
+pub type NSCallback = Box<Fn(u64, &Sender<String>)>;
+
+impl NSObjCallbackTrait for NSObj {
+    fn set_value(&mut self, key: u64, val: NSCallback) {
+        self.map.insert(key, val);
+    }
+    fn get_value(&self, key: u64) -> &NSCallback {
+        self.map.get(&key).unwrap()
+    }
+}
+
+// ObjcSubclass is a subclass of the objective-c NSObject base class.
+// This is registered with the objc runtime, so instances of this class
+// are "owned" by objc, and have no associated Rust data.
+//
+// This can be wrapped with a RustWrapperClass, which is a proper Rust struct
+// with its own storage, and holds an instance of ObjcSubclass.
+//
+// An ObjcSubclass can "talk" to its Rust wrapper class through function
+// pointers, as long as the storage is on the heap with a Box and the underlying
+// memory address doesn't change.  The NSObj type wraps RustWrapperClass up
+// in a Box.  The functions in the NSObjTrait trait operate on the boxed struct,
+// while keeping its storage location on the heap persistent.
+//
+pub enum ObjcSubclass {}
+impl ObjcSubclass {}
+
+unsafe impl Message for ObjcSubclass { }
+
+static OBJC_SUBCLASS_REGISTER_CLASS: Once = ONCE_INIT;
+
+impl INSObject for ObjcSubclass {
+    fn class() -> &'static Class {
+        OBJC_SUBCLASS_REGISTER_CLASS.call_once(|| {
+            let superclass = NSObject::class();
+            let mut decl = ClassDecl::new("ObjcSubclass", superclass).unwrap();
+            decl.add_ivar::<u64>("_rustdata");
+
+            extern fn objc_cb(this: &mut Object, _cmd: Sel, sender: u64) {
+                unsafe {
+                    let ptr: u64 = *this.get_ivar("_rustdata");
+                    let rustdata: &mut RustWrapperClass = &mut *(ptr as *mut RustWrapperClass);
+                    if let Some(ref cb) = rustdata.cb_fn {
+                        // Ownership?  Fuck ownership!
+                        let mut rustdata: &mut RustWrapperClass = &mut *(ptr as *mut RustWrapperClass);
+                        cb(rustdata, sender);
+                    }
+                }
+            }
+            extern fn objc_set_rust_data(this: &mut Object, _cmd: Sel, ptr: u64) {
+                unsafe {this.set_ivar("_rustdata", ptr);}
+            }
+            extern fn objc_get_rust_data(this: &Object, _cmd: Sel) -> u64 {
+                unsafe {*this.get_ivar("_rustdata")}
+            }
+            
+            unsafe {
+                let f: extern fn(&mut Object, Sel, u64) = objc_cb;
+                decl.add_method(sel!(cb:), f);
+                let f: extern fn(&mut Object, Sel, u64) = objc_set_rust_data;
+                decl.add_method(sel!(setRustData:), f);
+                let f: extern fn(&Object, Sel) -> u64 = objc_get_rust_data;
+                decl.add_method(sel!(rustData), f);
+            }
+
+            decl.register();
+        });
+
+        Class::get("ObjcSubclass").unwrap()
+    }
+}

diff --git a/src/osx/subclass.rs b/src/osx/subclass.rs
line changes: +81/-0
index 0000000..aa814ff
--- /dev/null
+++ b/src/osx/subclass.rs
@@ -0,0 +1,81 @@
+// Copied from rustc-objc-foundation project by SSheldon, examples/custom_class.rs
+// https://github.com/SSheldon/rust-objc-foundation/blob/master/examples/custom_class.rs
+// Covered by MIT License: https://en.wikipedia.org/wiki/MIT_License
+
+#[macro_use]
+extern crate objc;
+extern crate objc_foundation;
+
+use std::sync::{Once, ONCE_INIT};
+
+use objc::Message;
+use objc::declare::ClassDecl;
+use objc::runtime::{Class, Object, Sel};
+use objc_foundation::{INSObject, NSObject};
+
+pub enum RustNSObject { }
+
+impl RustNSObject {
+    fn number(&self) -> u32 {
+        unsafe {
+            let obj = &*(self as *const _ as *const Object);
+            *obj.get_ivar("_number")
+        }
+    }
+
+    fn set_number(&mut self, number: u32) {
+        unsafe {
+            let obj =  &mut *(self as *mut _ as *mut Object);
+            obj.set_ivar("_number", number);
+        }
+    }
+}
+
+unsafe impl Message for RustNSObject { }
+
+static RUSTNSOBJECT_REGISTER_CLASS: Once = ONCE_INIT;
+
+impl INSObject for RustNSObject {
+    fn class() -> &'static Class {
+        RUSTNSOBJECT_REGISTER_CLASS.call_once(|| {
+            let superclass = NSObject::class();
+            let mut decl = ClassDecl::new("RustNSObject", superclass).unwrap();
+            decl.add_ivar::<u32>("_number");
+
+            // Add ObjC methods for getting and setting the number
+            extern fn my_object_set_number(this: &mut Object, _cmd: Sel, number: u32) {
+                unsafe { this.set_ivar("_number", number); }
+            }
+
+            extern fn my_object_get_number(this: &Object, _cmd: Sel) -> u32 {
+                unsafe { *this.get_ivar("_number") }
+            }
+
+            unsafe {
+                let set_number: extern fn(&mut Object, Sel, u32) = my_object_set_number;
+                decl.add_method(sel!(setNumber:), set_number);
+                let get_number: extern fn(&Object, Sel) -> u32 = my_object_get_number;
+                decl.add_method(sel!(number), get_number);
+            }
+
+            decl.register();
+        });
+
+        Class::get("RustNSObject").unwrap()
+    }
+}
+
+fn main() {
+    let mut obj = RustNSObject::new();
+
+    obj.set_number(7);
+    println!("Number: {}", unsafe {
+        let number: u32 = msg_send![obj, number];
+        number
+    });
+
+    unsafe {
+        let _: () = msg_send![obj, setNumber:12u32];
+    }
+    println!("Number: {}", obj.number());
+}

diff --git a/src/webapi/mod.rs b/src/webapi/mod.rs
line changes: +26/-10
index f83a3e4..2f80959
--- a/src/webapi/mod.rs
+++ b/src/webapi/mod.rs
@@ -17,6 +17,9 @@ use super::settings;
 use super::spotify_api;
 use super::http::HttpResponse;
 
+pub type DeviceId = String;
+pub type SpotifyResponse = HttpResponse;
+
 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();
@@ -93,10 +96,24 @@ impl iter::IntoIterator for ConnectDeviceList {
 }
 
 #[derive(RustcDecodable, RustcEncodable, Debug)]
+pub struct ConnectPlaybackArtist {
+    pub name: String,
+    pub uri: String,
+}
+
+#[derive(RustcDecodable, RustcEncodable, Debug)]
+pub struct ConnectPlaybackAlbum {
+    pub name: String,
+    pub uri: String,
+}
+
+#[derive(RustcDecodable, RustcEncodable, Debug)]
 pub struct ConnectPlaybackItem {
-    duration_ms: u32,
-    name: String,
-    uri: String,
+    pub duration_ms: u32,
+    pub name: String,
+    pub uri: String,
+    pub album: ConnectPlaybackAlbum,
+    pub artists: Vec<ConnectPlaybackArtist>,
 }
 
 #[derive(RustcDecodable, RustcEncodable, Debug)]
@@ -153,8 +170,6 @@ pub fn request_oauth_tokens(auth_code: &str, settings: &settings::Settings) -> (
     parse_spotify_token(&json_response)
 }
 
-pub type DeviceId = String;
-
 #[derive(RustcDecodable, RustcEncodable)]
 pub struct PlayContextOffset {
     pub position: Option<u32>,
@@ -259,8 +274,6 @@ impl QueryString {
     }
 }
 
-pub type SpotifyResponse = HttpResponse;
-
 pub enum SpotifyRepeat {
     Off,
     Track,
@@ -466,12 +479,15 @@ impl SpotifyConnectr {
             .build();
         http::http(spotify_api::REPEAT, &query, "", http::HttpMethod::PUT, self.bearer_token())
     }
-    pub fn transfer_multi(&self, devices: Vec<String>, play: bool) -> SpotifyResponse {
+    pub fn transfer_multi(&mut self, devices: Vec<String>, play: bool) -> SpotifyResponse {
+        let device = devices[0].clone();
         let body = json::encode(&DeviceIdList {device_ids: devices, play: play}).unwrap();
+        self.set_target_device(Some(device));
         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();
+    pub fn transfer(&mut self, device: String, play: bool) -> SpotifyResponse {
+        let body = json::encode(&DeviceIdList {device_ids: vec![device.clone()], play: play}).unwrap();
+        self.set_target_device(Some(device));
         http::http(spotify_api::PLAYER, "", &body, http::HttpMethod::PUT, self.bearer_token())
     }
 }