commit: | e9970c17c73bba2d0cb66ddfdb48a7187fa27fab |
author: | Trevor Bentley |
committer: | Trevor Bentley |
date: | Sat Aug 5 23:42:12 2017 +0200 |
parents: | 6585d8351cda6837ee9579c254446ed6cd6e93a0 |
diff --git a/Cargo.toml b/Cargo.toml line changes: +0/-5 index b82f548..07b8293 --- a/Cargo.toml +++ b/Cargo.toml
@@ -59,23 +59,18 @@ systray = {path = "deps/systray-rs", version="0.1.1-connectr"} [target."cfg(windows)".dependencies.rubrail] default-features=false version = "0.5" -#path = "deps/rubrail-rs" -#version="0.4.3-rc" [target."cfg(all(unix, not(target_os = \"macos\")))".dependencies] [target."cfg(all(unix, not(target_os = \"macos\")))".dependencies.rubrail] default-features = false version = "0.5" -#path = "deps/rubrail-rs" -#version = "0.4.3-rc" [target."cfg(target_os = \"macos\")".dependencies] cocoa = "0.9" objc-foundation = "0.1.1" objc_id = "0.1" rubrail = "0.5" -#rubrail = {path = "deps/rubrail-rs", version="0.4.3-rc"} [target."cfg(target_os = \"macos\")".dependencies.objc] version = "0.2.2"
diff --git a/README.md b/README.md line changes: +32/-1 index 3fb0dc1..5312e22 --- a/README.md +++ b/README.md
@@ -21,6 +21,7 @@ What it can do: * Play/pause * Skip tracks * Quick-play a saved 'preset' +* Quick-save playing track to a playlist * Select playback device * Change volume
@@ -88,6 +89,20 @@ On the first launch, Connectr will guide you through setting up a Spotify develo * Add a Redirect URI: <em>http://127.0.0.1:5432</em> * Copy your <em>Client ID</em> and <em>Client Secret</em> to `connectr.ini` (see below). +### Mac Touch Bar interface + +#### Setup +* Be sure the "Control Strip" is enabled in the Keyboard section of System Preferences, under `Touch Bar shows`. +* Press the Connectr icon in the Control Strip to expand it. It will stay expanded until you press the `x` button on the left side. +* The Control Strip only supports 4 icons, and stacks all new ones on the left-most icon. If the Connectr icon is missing, it may be hidden "under" another icon. You can keep clicking the left-most icon to open them all. + +#### Controls +* Double-tap the track title to rotate through modes: + * Track and Artist + * Track + * Artist +* To quick-save a track, swipe right on track title until a box is drawn around it and release. Configure quick-save in `connectr.ini` first. + ### Configuration file (connectr.ini) format **Note:** connectr uses `~/.connectr.ini` if it exists. If it does _not_ exist, connectr will fallback to trying `connectr.ini` in the directory it is run from. A template is provided in `connectr.ini.in`.
@@ -102,7 +117,23 @@ connectr's configuration is read from a regular INI file with these sections: * **secret** - Spotify web application's Client Secret (string). _ex: `secret = DEFDEFDEFDEF456456456`_ #### [presets] -* **Your Preset Name** - Key is the display name of a playable preset, as it will appear in the menu. The value is a Spotify URI to play, in the _spotify:type:aLoNgUnIqUeIdENTIFier_ format. (string). _ex: `Bakesale = spotify:album:70XjdLKH7HHsFVWoQipP0T` will show as 'Bakesale' in the menu, and will play the specified Sebadoh album when clicked._ + +One preset per line, in either format: + +* [Preset Name] = [Context URI] +* [Preset Name] = [Context URI],[Quick-save Playlist URI] + +Where: + +* `Preset Name` is any name you want for the preset +* `Context URI` is the Spotify context (album, playlist, etc) to play when selected +* `Quick-save URI` is (optionally) a playlist to save the current track to if 'Quick-Save' is clicked while this preset is playing. + +*Example:* + +Make a preset called `Bakesale` that plays a Sebadoh album when selected, and saves my favorite tracks from that album to a private playlist: + +`Bakesale = spotify:album:70XjdLKH7HHsFVWoQipP0T,spotify:user:mrmekon:playlist:4aqg0RkXSxknWvIXARV7or` #### [tokens] _note: This section is auto-generated and auto-updated at runtime. You can leave it empty if you make your own config file._
diff --git a/src/http/mod.rs b/src/http/mod.rs line changes: +26/-15 index e7846f1..5c54514 --- a/src/http/mod.rs +++ b/src/http/mod.rs
@@ -69,28 +69,40 @@ pub enum AccessToken<'a> { None, } -pub fn http(url: &str, query: &str, body: &str, +pub fn http(url: &str, query: Option<&str>, body: Option<&str>, 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() }, - _ => { body.as_bytes() }, - //_ => { query.as_bytes() } + let mut headers = List::new(); + let data = match method { + HttpMethod::POST => { + match query { + Some(q) => { + let enc_query = percent_encoding::utf8_percent_encode(&q, percent_encoding::QUERY_ENCODE_SET).collect::<String>(); + enc_query + }, + None => { + let header = format!("Content-Type: application/json"); + headers.append(&header).unwrap(); + body.unwrap_or("").to_string() + } + } + }, + _ => { body.unwrap_or("").to_string() }, }; - let query_url = &format!("{}?{}", url, query); + let mut data = data.as_bytes(); + let url = match method { - HttpMethod::GET | HttpMethod::PUT => match query.len() { - 0 => url, - _ => query_url, + HttpMethod::GET | HttpMethod::PUT => match query { + None => url.to_string(), + Some(q) => format!("{}?{}", url, q), }, - _ => url + _ => url.to_string() }; let mut response = None; let mut json_bytes = Vec::<u8>::new(); { let mut easy = Easy::new(); - easy.url(url).unwrap(); + easy.url(&url).unwrap(); match method { HttpMethod::POST => { easy.post(true).unwrap();
@@ -111,10 +123,9 @@ pub fn http(url: &str, query: &str, body: &str, AccessToken::Basic(token) => ("Basic", token), _ => ("",""), }; - let mut list = List::new(); let header = format!("Authorization: {} {}", request.0, request.1); - list.append(&header).unwrap(); - easy.http_headers(list).unwrap(); + headers.append(&header).unwrap(); + easy.http_headers(headers).unwrap(); } }
diff --git a/src/lib.rs b/src/lib.rs line changes: +5/-1 index 1969713..656330a --- a/src/lib.rs +++ b/src/lib.rs
@@ -28,6 +28,7 @@ extern crate serde_derive; #[derive(Clone, Copy)] pub struct SpotifyEndpoints<'a> { scopes: &'a str, + scopes_version: u32, authorize: &'a str, token: &'a str, devices: &'a str,
@@ -41,10 +42,12 @@ pub struct SpotifyEndpoints<'a> { shuffle: &'a str, repeat: &'a str, player: &'a str, + add_to_playlist: &'a str, } pub const SPOTIFY_API: SpotifyEndpoints = SpotifyEndpoints { - scopes: "user-read-private streaming user-read-playback-state", + scopes: "user-read-private streaming user-read-playback-state playlist-modify-public playlist-modify-private", + scopes_version: 1, // increment if scopes change authorize: "https://accounts.spotify.com/en/authorize", token: "https://accounts.spotify.com/api/token", devices: "https://api.spotify.com/v1/me/player/devices",
@@ -58,6 +61,7 @@ pub const SPOTIFY_API: SpotifyEndpoints = SpotifyEndpoints { shuffle: "https://api.spotify.com/v1/me/player/shuffle", repeat: "https://api.spotify.com/v1/me/player/repeat", player: "https://api.spotify.com/v1/me/player", + add_to_playlist: "https://api.spotify.com/v1/users", }; #[cfg(target_os = "linux")]
diff --git a/src/main.rs b/src/main.rs line changes: +59/-20 index aa6c0e9..ad6b8c0 --- a/src/main.rs +++ b/src/main.rs
@@ -69,6 +69,7 @@ enum CallbackAction { Preset, Redraw, Reconfigure, + SaveTrack, } #[derive(Serialize, Deserialize, Debug)]
@@ -83,6 +84,7 @@ struct MenuItems { play: MenuItem, next: MenuItem, prev: MenuItem, + save: MenuItem, preset: Vec<MenuItem>, volume: Vec<MenuItem>, }
@@ -206,28 +208,33 @@ impl TouchbarUI { // Fade text color to green as finger swipes right across the label, and // add a solid white border indicating the label is 'selected' after a // long enough slide. Will be used for 'swipe to save' feature. + let tx_clone = tx.clone(); touchbar.add_item_swipe_gesture(&playing_label, Box::new(move |item,state,translation| { - let color: f64 = match translation.abs().trunc() as u32 { - t if t < 10 => 1.0, - t if t > 150 => 0.0, - _ => (45. / translation.abs()), + let rgba = match translation { + t if t > 170. => (0.1, 1.0, 0.7, 1.0), + _ => (0.9, 0.9, 0.9, 1.0), }; - let rgba = match state { - SwipeState::Ended => (1.0, 1.0, 1.0, 1.0), - _ => { - match translation.is_sign_positive() { - true => (color, 1.0, color, 1.0), - false => (1.0, 1.0, 1.0, 1.0), + match state { + SwipeState::Cancelled | SwipeState::Failed | SwipeState::Unknown => { + unsafe { rubrail::util::set_text_color(item, 1., 1., 1., 1.); } + }, + SwipeState::Ended => { + unsafe { rubrail::util::set_text_color(item, 1., 1., 1., 1.); } + match translation { + t if t > 170. => { + let cmd = MenuCallbackCommand { + action: CallbackAction::SaveTrack, + sender: 0, + data: String::new(), + }; + let _ = tx_clone.send(serde_json::to_string(&cmd).unwrap()); + }, + _ => {}, } } - }; - unsafe { rubrail::util::set_text_color(item, rgba.0, rgba.1, rgba.2, rgba.3); } - if translation > 170. && state != SwipeState::Ended { - unsafe { rubrail::util::set_bg_color(item, 1.0, 1.0, 1.0, 0.95); } - // TODO: save track as favorite - } - else { - unsafe { rubrail::util::set_bg_color(item, 0.0, 0.0, 0.0, 0.0); } + _ => { + unsafe { rubrail::util::set_text_color(item, rgba.0, rgba.1, rgba.2, rgba.3); } + } } }));
@@ -509,6 +516,16 @@ fn fill_menu<T: TStatusBar>(app: &mut ConnectrApp, let _ = tx.send(serde_json::to_string(&cmd).unwrap()); }); app.menu.prev = status.add_item("Previous", cb, false); + + let cb: NSCallback = Box::new(move |sender, tx| { + let cmd = MenuCallbackCommand { + action: CallbackAction::SaveTrack, + sender: sender, + data: String::new(), + }; + let _ = tx.send(serde_json::to_string(&cmd).unwrap()); + }); + app.menu.save = status.add_item("Quick-Save", cb, false); } status.add_label("");
@@ -530,7 +547,8 @@ fn fill_menu<T: TStatusBar>(app: &mut ConnectrApp, }; let _ = tx.send(serde_json::to_string(&cmd).unwrap()); }); - let item = status.add_item(&name.clone(), cb, false); + let selected = player_state.playing_from_context(&preset.1); + let item = status.add_item(&name.clone(), cb, selected); app.menu.preset.push(item); } }
@@ -608,7 +626,7 @@ fn fill_menu<T: TStatusBar>(app: &mut ConnectrApp, let cb: NSCallback = Box::new(move |_sender, _tx| { let _ = open::that("https://open.spotify.com/search/"); }); - let item = status.add_item("Search Spotify", cb, false); + let _ = status.add_item("Search Spotify", cb, false); status.add_separator(); status.add_quit("Exit");
@@ -620,6 +638,7 @@ fn clear_menu<T: TStatusBar>(app: &mut ConnectrApp, status: &mut T) { play: ptr::null_mut(), next: ptr::null_mut(), prev: ptr::null_mut(), + save: ptr::null_mut(), preset: Vec::<MenuItem>::new(), volume: Vec::<MenuItem>::new(), };
@@ -659,6 +678,25 @@ fn handle_callback(player_state: Option<&connectr::PlayerState>, CallbackAction::Redraw => { refresh = RefreshTime::Redraw; } + CallbackAction::SaveTrack => { + if let Some(player_state) = player_state { + if let Some(ref ctx) = player_state.context { + let playlist: Option<String>; + { + match spotify.quick_save_playlist(&ctx.uri) { + Some(u) => playlist = Some(u.to_owned()), + None => playlist = None, + } + } + if let Some(playlist) = playlist { + if let Some(ref item) = player_state.item { + let track = item.uri.to_owned(); + require(spotify.save_track(track, playlist)); + } + } + } + } + } CallbackAction::Reconfigure => {} } refresh
@@ -879,6 +917,7 @@ fn main() { play: ptr::null_mut(), next: ptr::null_mut(), prev: ptr::null_mut(), + save: ptr::null_mut(), preset: Vec::<MenuItem>::new(), volume: Vec::<MenuItem>::new(), },
diff --git a/src/settings/mod.rs b/src/settings/mod.rs line changes: +75/-14 index a2b8694..2fc8411 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs
@@ -22,6 +22,22 @@ pub struct Settings { pub refresh_token: Option<String>, pub expire_utc: Option<u64>, pub presets: Vec<(String,String)>, + pub default_quicksave: Option<String>, + pub quicksave: BTreeMap<String, String>, +} + +impl Settings { + pub fn quick_save_playlist(&self, context: &str) -> Option<&str> { + match self.quicksave.get(context) { + Some(ref uri) => Some(&uri), + None => { + match self.default_quicksave { + Some(ref uri) => Some(&uri), + None => None, + } + } + } + } } fn default_inifile() -> String {
@@ -68,11 +84,30 @@ To create your free developer application for Connectr, follow these instruction <li> Submit this configuration form </ul></p> <form method="POST" action="#" accept-charset="UTF-8"><table> -<tr><td>Client ID:</td><td><input type="text" name="client_id"></td></tr> -<tr><td>Client Secret:</td><td><input type="text" name="secret"></td></tr> +<tr><td colspan=2><h3>Spotify Credentials (all fields required):</h3></td></tr> +<tr><td>Client ID:</td><td><input type="text" name="client_id" style="width:400px;"></td></tr> +<tr><td>Client Secret:</td><td><input type="text" name="secret" style="width:400px;"></td></tr> <tr><td colspan=2></br></br></tr></tr> -<tr><td>Presets:</br>(optional, one per line)</td><td><textarea rows="10" cols="80" name="presets" placeholder="First Preset Name=spotify:user:spotify:playlist:37i9dQZEVXboyJ0IJdpcuT"></textarea></td></tr> -<tr><td colspan=2><center><input type="submit" value="Write config file"></center></td></tr> +<tr><td colspan=2><h3>Presets (all fields optional):</h3> + <div style="width:600px;"> + Presets let you start your favorite Spotify contexts (playlist, album, artist, etc) from Connectr. + You can add an optional "quick save" playlist for each preset, to quickly save tracks you like to a known playlist. + For instance, you might have a "Discover Weekly" preset, and a quick save to a "Best of Discover Weekly" playlist. + You can also set a global "quick save" playlist, where tracks are saved if not playing from a preset with an associated quick-save playlist.</br> + </br> + All contexts must be specified in Spotify's URI format: ex: <code>spotify:album:2p2UgYlbg4yG44IKDp08Q8</code> + </div> + </br> + One preset per line, in either format::</br> + <code>[Preset Name] = [Context URI]</code></br> + <code>[Preset Name] = [Context URI],[Quick-save Playlist URI]</code> + </br></br> +</td></tr> +<tr><td>Presets:</br>(one per line)</td><td><textarea rows="10" cols="100" name="presets" placeholder="First Preset Name = spotify:user:spotify:playlist:37i9dQZEVXboyJ0IJdpcuT"></textarea></td></tr> +<tr><td style="width:150px;">Quick-Save URI:</br>(playlist URI)</td><td> + <input type="text" name="quicksave_default" style="width:400px;"></td></tr> +<tr><td colspan=2></br></br></tr></tr> +<tr><td colspan=2><center><input type="submit" value="Save Configuration" style="height:50px; width: 300px; font-size:20px;"></center></td></tr> </br> </table></form> </br>
@@ -100,8 +135,12 @@ pub fn save_web_config(mut config: BTreeMap<String,String>) -> Ini { let client_id = config.remove("client_id").unwrap_or("<PLACEHOLDER>".to_string()); let presets = config.remove("presets").unwrap_or(String::new()); c.with_section(Some("application".to_owned())) - .set("secret", secret) - .set("client_id", client_id); + .set("secret", secret.trim()) + .set("client_id", client_id.trim()); + if let Some(quicksave) = config.remove("quicksave_default") { + c.with_section(Some("connectr".to_owned())) + .set("quicksave_default", quicksave.trim()); + } { // TODO: INI uses HashMap, doesn't support maintaining order for preset in presets.split("\n") {
@@ -117,7 +156,7 @@ pub fn save_web_config(mut config: BTreeMap<String,String>) -> Ini { c } -pub fn read_settings() -> Option<Settings> { +pub fn read_settings(scopes_version: u32) -> Option<Settings> { info!("Attempting to read config file."); let conf = match Ini::load_from_file(&inifile()) { Ok(c) => c,
@@ -134,6 +173,10 @@ pub fn read_settings() -> Option<Settings> { let section = conf.section(Some("connectr".to_owned())).unwrap(); let port = section.get("port").unwrap().parse().unwrap(); + let quicksave_default = match section.get("quicksave_default") { + Some(uri) => Some(uri.to_string()), + None => None, + }; let section = conf.section(Some("application".to_owned())).unwrap(); let secret = section.get("secret").unwrap();
@@ -156,30 +199,48 @@ pub fn read_settings() -> Option<Settings> { 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()); - info!("Read access token from INI!"); + let saved_version = section.get("version"); + // Only accept saved tokens if the scopes version matches. Otherwise + // it will authenticate but some actions will be invalid. + if saved_version.is_some() && + saved_version.unwrap().parse::<u32>().unwrap() == scopes_version { + access = Some(section.get("access").unwrap().clone()); + refresh = Some(section.get("refresh").unwrap().clone()); + expire_utc = Some(section.get("expire").unwrap().parse().unwrap()); + info!("Read access token from INI!"); + } } let mut presets = Vec::<(String,String)>::new(); + let mut quicksave = BTreeMap::<String,String>::new(); if let Some(section) = conf.section(Some("presets".to_owned())) { for (key, value) in section { - presets.push((key.to_owned(), value.to_owned())); + let mut fields = value.split(","); + let uri = fields.next().unwrap().trim(); // URI is required + let save_uri = fields.next(); // quicksave is optional + presets.push((key.to_owned(), uri.to_owned())); + if let Some(save_uri) = save_uri { + quicksave.insert(uri.to_owned(), save_uri.trim().to_owned()); + } } } Some(Settings { secret: secret.to_string(), client_id: client_id.to_string(), port: port, access_token: access, refresh_token: refresh, expire_utc: expire_utc, - presets: presets}) + presets: presets, + default_quicksave: quicksave_default, + quicksave: quicksave, + }) } pub type SettingsError = String; -pub fn save_tokens(access: &str, refresh: &str, expire_utc: u64) -> Result<(), SettingsError> { +pub fn save_tokens(version: u32, access: &str, + refresh: &str, expire_utc: u64) -> Result<(), SettingsError> { let mut conf = Ini::load_from_file(&inifile()).unwrap(); conf.with_section(Some("tokens".to_owned())) .set("access", access) .set("refresh", refresh) + .set("version", format!("{}",version)) .set("expire", expire_utc.to_string()); conf.write_to_file(&inifile()).unwrap(); Ok(())
diff --git a/src/webapi/mod.rs b/src/webapi/mod.rs line changes: +47/-18 index 9e1ac82..d7b8b25 --- a/src/webapi/mod.rs +++ b/src/webapi/mod.rs
@@ -111,7 +111,7 @@ pub struct ConnectPlaybackItem { #[derive(Deserialize, Debug)] pub struct ConnectContext { - uri: String, + pub uri: String, } #[derive(Deserialize, Debug)]
@@ -126,6 +126,15 @@ pub struct PlayerState { pub context: Option<ConnectContext>, } +impl PlayerState { + pub fn playing_from_context(&self, context: &str) -> bool { + match self.context { + Some(ref ctx) => ctx.uri == context, + None => false, + } + } +} + impl fmt::Display for PlayerState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let play_state = match self.is_playing {
@@ -277,6 +286,11 @@ impl ToString for SpotifyRepeat { } #[derive(Serialize)] +struct UriList { + uris: Vec<String>, +} + +#[derive(Serialize)] struct DeviceIdList { device_ids: Vec<String>, play: bool,
@@ -322,7 +336,7 @@ impl<'a> SpotifyConnectrBuilder<'a> { pub fn build(&mut self) -> Option<SpotifyConnectr<'a>> { let mut settings: settings::Settings = Default::default(); if self.expire.is_none() { - settings = match settings::read_settings() { + settings = match settings::read_settings(self.api.scopes_version) { Some(s) => s, None => { return None }, };
@@ -365,6 +379,9 @@ impl<'a> SpotifyConnectr<'a> { expire: None, } } + pub fn quick_save_playlist(&self, context: &str) -> Option<&str> { + self.settings.quick_save_playlist(context) + } fn expire_offset_to_utc(&self, expires_in: u64) -> u64 { let now = time::now_utc().to_timespec().sec as u64; now + expires_in
@@ -416,7 +433,8 @@ impl<'a> SpotifyConnectr<'a> { let access_token = self.access_token.clone().unwrap(); let refresh_token = self.refresh_token.clone().unwrap(); - let _ = settings::save_tokens(&access_token, + let _ = settings::save_tokens(self.api.scopes_version, + &access_token, &refresh_token, self.expire_utc.unwrap()); }
@@ -440,7 +458,8 @@ impl<'a> SpotifyConnectr<'a> { self.auth_code = http::authenticate(self.api.scopes, self.api.authorize, &self.settings); let (access_token, refresh_token, expires_in) = self.request_oauth_tokens(&self.auth_code, &self.settings); let expire_utc = self.expire_offset_to_utc(expires_in); - let _ = settings::save_tokens(&access_token, &refresh_token, expire_utc); + let _ = settings::save_tokens(self.api.scopes_version, &access_token, + &refresh_token, expire_utc); self.access_token = Some(access_token); self.refresh_token = Some(refresh_token); self.expire_utc = Some(expire_utc);
@@ -454,7 +473,7 @@ impl<'a> SpotifyConnectr<'a> { .add("client_id", settings.client_id.clone()) .add("client_secret", settings.secret.clone()) .build(); - let json_response = http::http(self.api.token, &query, "", http::HttpMethod::POST, + let json_response = http::http(self.api.token, Some(&query), None, http::HttpMethod::POST, http::AccessToken::None).unwrap(); parse_spotify_token(&json_response) }
@@ -485,7 +504,7 @@ impl<'a> SpotifyConnectr<'a> { .add("client_id", self.settings.client_id.clone()) .add("client_secret", self.settings.secret.clone()) .build(); - let json_response = http::http(self.api.token, &query, "", + let json_response = http::http(self.api.token, Some(&query), None, http::HttpMethod::POST, http::AccessToken::None); match json_response.code { Some(200) => {
@@ -496,7 +515,7 @@ impl<'a> SpotifyConnectr<'a> { } } pub fn request_device_list(&mut self) -> Option<ConnectDeviceList> { - let json_response = http::http(self.api.devices, "", "", + let json_response = http::http(self.api.devices, None, None, http::HttpMethod::GET, self.bearer_token()); match json_response.code { Some(200) => serde_json::from_str(&json_response.data.unwrap()).unwrap(),
@@ -509,7 +528,7 @@ impl<'a> SpotifyConnectr<'a> { } } pub fn request_player_state(&mut self) -> Option<PlayerState> { - let json_response = http::http(self.api.player_state, "", "", + let json_response = http::http(self.api.player_state, None, None, http::HttpMethod::GET, self.bearer_token()); match json_response.code { Some(200) => match serde_json::from_str(&json_response.data.unwrap()) {
@@ -533,58 +552,68 @@ impl<'a> SpotifyConnectr<'a> { Some(x) => serde_json::to_string(x).unwrap(), None => String::new(), }; - http::http(self.api.play, &query, &body, http::HttpMethod::PUT, self.bearer_token()) + http::http(self.api.play, Some(&query), Some(&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(self.api.pause, &query, "", http::HttpMethod::PUT, self.bearer_token()) + http::http(self.api.pause, Some(&query), None, 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(self.api.next, &query, "", http::HttpMethod::POST, self.bearer_token()) + http::http(self.api.next, Some(&query), None, 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(self.api.previous, &query, "", http::HttpMethod::POST, self.bearer_token()) + http::http(self.api.previous, Some(&query), None, 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(self.api.seek, &query, "", http::HttpMethod::PUT, self.bearer_token()) + http::http(self.api.seek, Some(&query), None, 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(self.api.volume, &query, "", http::HttpMethod::PUT, self.bearer_token()) + http::http(self.api.volume, Some(&query), None, 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(self.api.shuffle, &query, "", http::HttpMethod::PUT, self.bearer_token()) + http::http(self.api.shuffle, Some(&query), None, 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(self.api.repeat, &query, "", http::HttpMethod::PUT, self.bearer_token()) + http::http(self.api.repeat, Some(&query), None, http::HttpMethod::PUT, self.bearer_token()) } pub fn transfer_multi(&mut self, devices: Vec<String>, play: bool) -> SpotifyResponse { let device = devices[0].clone(); let body = serde_json::to_string(&DeviceIdList {device_ids: devices, play: play}).unwrap(); self.set_target_device(Some(device)); - http::http(self.api.player, "", &body, http::HttpMethod::PUT, self.bearer_token()) + http::http(self.api.player, None, Some(&body), http::HttpMethod::PUT, self.bearer_token()) } pub fn transfer(&mut self, device: String, play: bool) -> SpotifyResponse { let body = serde_json::to_string(&DeviceIdList {device_ids: vec![device.clone()], play: play}).unwrap(); self.set_target_device(Some(device)); - http::http(self.api.player, "", &body, http::HttpMethod::PUT, self.bearer_token()) + http::http(self.api.player, None, Some(&body), http::HttpMethod::PUT, self.bearer_token()) + } + pub fn save_track(&mut self, track: String, playlist: String) -> SpotifyResponse { + let playlist_id = playlist.split(":").last().unwrap(); + let user_id = playlist.split(":").nth(2).unwrap(); + let uri = format!("{}/{}/playlists/{}/tracks", + self.api.add_to_playlist, + user_id, playlist_id); + + let body = serde_json::to_string(&UriList {uris: vec![track.clone()]}).unwrap(); + http::http(&uri, None, Some(&body), http::HttpMethod::POST, self.bearer_token()) } pub fn get_presets(&mut self) -> &Vec<(String,String)> { &self.settings.presets
diff --git a/src/webapi/test.rs b/src/webapi/test.rs line changes: +2/-0 index ec8fc57..5653699 --- a/src/webapi/test.rs +++ b/src/webapi/test.rs
@@ -30,6 +30,7 @@ mod tests { pub const TEST_API: SpotifyEndpoints = SpotifyEndpoints { scopes: "user-read-private streaming user-read-playback-state", + scopes_version: 1, authorize: "http://127.0.0.1:9799/en/authorize", token: "http://127.0.0.1:9799/api/token", devices: "http://127.0.0.1:9799/v1/me/player/devices",
@@ -43,6 +44,7 @@ mod tests { shuffle: "http://127.0.0.1:9799/v1/me/player/shuffle", repeat: "http://127.0.0.1:9799/v1/me/player/repeat", player: "http://127.0.0.1:9799/v1/me/player", + add_to_playlist: "http://127.0.0.1:9799/v1/users", }; /// Macro to parse the body of a POST request and send a response.