summary history branches tags files
commit:ab10f87209243e66d9cdd5b04bdb3893f5549935
author:Trevor Bentley
committer:Trevor Bentley
date:Tue Aug 15 23:05:22 2017 +0200
parents:4b3810d01560e3d587f55c117679010aaa260f2c
Setting multiple alarms via web config works
diff --git a/src/http/mod.rs b/src/http/mod.rs
line changes: +2/-0
index 5c54514..4229684
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -291,6 +291,8 @@ pub fn config_request_local_webserver(port: u32, form: String, reply: String) ->
         return config;
     }
     // Run web server for an hour.
+    // In a proper world, this would be an async 'future'.  The whole Spotify
+    // thread is blocked until the user saves.
     let timeout = Duration::from_secs(60*60);
     match rx.recv_timeout(timeout) {
         Ok(_) => {

diff --git a/src/main.rs b/src/main.rs
line changes: +33/-14
index 08bbf4b..ceebd1a
--- a/src/main.rs
+++ b/src/main.rs
@@ -51,6 +51,8 @@ pub const REFRESH_PERIOD: i64 = 30;
 enum SpotifyThreadCommand {
     Update,
     InvalidSettings,
+    ConfigActive,
+    ConfigInactive,
 }
 
 #[allow(dead_code)]
@@ -439,7 +441,23 @@ fn reconfig_menu<T: TStatusBar>(status: &mut T) {
 fn fill_menu<T: TStatusBar>(app: &mut ConnectrApp,
                             spotify: &SpotifyThread,
                             status: &mut T,
-                            touchbar: &mut TouchbarUI) {
+                            touchbar: &mut TouchbarUI,
+                            web_config_active: bool) {
+    if web_config_active {
+        // This is a leaky-abstraction way of handling the webapi thread being
+        // blocked, and thus unable to respond to a second 'Edit Alarms' request.
+        // That's problematic if you close the browser window, since you'll have
+        // to wait an hour for it to time out... so block all actions until it
+        // is answered.
+        let cb: NSCallback = Box::new(move |_sender, _tx| {
+            let _ = open::that(format!("http://127.0.0.1:{}", connectr::settings::WEB_PORT));
+        });
+        let _ = status.add_item("Edit Alarms (relaunch)", cb, false);
+        status.add_separator();
+        status.add_quit("Exit");
+        return;
+    }
+
     let device_list = spotify.device_list.read().unwrap();
     let player_state = spotify.player_state.read().unwrap();
     let presets = spotify.presets.read().unwrap();
@@ -822,17 +840,6 @@ fn create_spotify_thread(rx_cmd: Receiver<String>) -> SpotifyThread {
             *preset_writer = spotify.get_presets().clone();
             let _ = tx.send(SpotifyThreadCommand::Update);
         }
-        let alarm = spotify.schedule_alarm(connectr::AlarmEntry {
-            time: "19:46".to_string(),
-            repeat: connectr::AlarmRepeat::Daily,
-            context: connectr::PlayContext::new()
-                .context_uri("spotify:user:mrmekon:playlist:2NZx9rQlpDTEhjrbCIwh0Q")
-                .offset_position(0)
-                .build(),
-            device: "adf47bd71923ad681f33e3a778c56fc33f4a63a8".to_string(),
-        }).unwrap();
-        let _ = spotify.alarm_disable(alarm);
-        let _ = spotify.alarm_reschedule(alarm);
         loop {
             if rx.try_recv().is_ok() {
                 // Main thread tells us to shutdown
@@ -847,7 +854,10 @@ fn create_spotify_thread(rx_cmd: Receiver<String>) -> SpotifyThread {
                 info!("Received {}", s);
                 let cmd: MenuCallbackCommand = serde_json::from_str(&s).unwrap();
                 if cmd.action == CallbackAction::EditAlarms {
-                    spotify.alarm_configure();
+                    let devs = device_list.read().unwrap();
+                    let _ = tx.send(SpotifyThreadCommand::ConfigActive);
+                    spotify.alarm_configure((*devs).as_ref());
+                    let _ = tx.send(SpotifyThreadCommand::ConfigInactive);
                 }
                 let refresh_strategy =  handle_callback(player_state.read().unwrap().as_ref(),
                                                         &mut spotify, &cmd);
@@ -978,6 +988,7 @@ fn main() {
         warn!("Didn't find Wine in search path.");
     }
 
+    let mut web_config_active: bool = false;
     let mut need_redraw: bool = false;
     while running.load(Ordering::SeqCst) {
         match spotify_thread.rx.recv_timeout(Duration::from_millis(100)) {
@@ -988,13 +999,21 @@ fn main() {
                         clear_menu(&mut app, &mut status);
                         reconfig_menu(&mut status);
                     }
+                    SpotifyThreadCommand::ConfigActive => {
+                        web_config_active = true;
+                        need_redraw = true;
+                    },
+                    SpotifyThreadCommand::ConfigInactive => {
+                        web_config_active = false;
+                        need_redraw = true;
+                    },
                 }
             },
             Err(_) => {}
         }
         if need_redraw && status.can_redraw() {
             clear_menu(&mut app, &mut status);
-            fill_menu(&mut app, &spotify_thread, &mut status, &mut touchbar);
+            fill_menu(&mut app, &spotify_thread, &mut status, &mut touchbar, web_config_active);
             need_redraw = false;
         }
         status.run(false);

diff --git a/src/settings/mod.rs b/src/settings/mod.rs
line changes: +78/-23
index 4f60a8e..b7a59d2
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -3,6 +3,7 @@ use self::ini::Ini;
 use super::http;
 use super::AlarmRepeat;
 use super::AlarmConfig;
+use super::ConnectDeviceList;
 
 extern crate time;
 extern crate fruitbasket;
@@ -14,7 +15,7 @@ use std::collections::BTreeMap;
 
 const INIFILE: &'static str = "connectr.ini";
 const PORT: u32 = 5432;
-const WEB_PORT: u32 = 5676;
+pub const WEB_PORT: u32 = 5676;
 
 #[derive(Default)]
 pub struct Settings {
@@ -81,7 +82,7 @@ To create your free developer application for Connectr, follow these instruction
 <p><ul>
 <li> Go to your <a href="https://developer.spotify.com/my-applications/#!/applications/create">Spotify Applications</a> page (login with your Spotify credentials)
 <li> Click "CREATE AN APP" in the upper-right corner
-<li> Enter a name (perhaps "Connectr") and description ("Use Connectr app with my account.")
+<li> Enter a name (perhaps 'Connectr') and description ("Use Connectr app with my account.")
 <li> Add a Redirect URI: <em>http://127.0.0.1:{}</em>
 <li> Copy your <em>Client ID</em> and <em>Client Secret</em> to the fields below.
 <li> Press the <em>SAVE</em> button at the bottom of Spotify's webpage
@@ -130,29 +131,77 @@ If something goes wrong or changes, edit or delete that file.</small>
     config
 }
 
-pub fn request_web_alarm_config() -> BTreeMap<String,String> {
-    let form = format!(r###"
+pub fn request_web_alarm_config(alarms: &Vec<AlarmConfig>,
+                                devices: Option<&ConnectDeviceList>) -> BTreeMap<String,String> {
+    let mut form = format!(
+        r###"
 {}
 <!DOCTYPE HTML>
-<html><head><title>Connectr Alarm Clocks</title><style> tr:nth-child(even) {{ background: #f2f2f2; }} </style></head>
-<body><h2>Connectr Alarm Clocks</h2> Input in 24-hour time format:
+<html><head><title>Connectr Alarm Clocks</title><style>
+tr:nth-child(even) {{ background: #f2f2f2; }}
+th {{ width:120px;border-bottom: 1px solid #ddd; }}
+a.tooltip {{ position: relative; }}
+a.tooltip::before {{ content: attr(data-tip); position:absolute; z-index: 999;
+white-space:normal; bottom:9999px; left: 50%; background:#000; color:#e0e0e0;
+padding:2px 7px 2px 7px; line-height: 16px; opacity: 0; width: inherit; max-width: 500px;
+transition:opacity 0.4s ease-out; font-weight: normal; text-align: left; min-width: 100px; }}
+a.tooltip:hover::before {{ opacity: 1; bottom:-35px; }}
+a.tooltip {{ color: #4e4e4e; text-decoration: none; vertical-align: super; font-size: 11px; font-weight: normal; }}
+</style>
+<meta http-equiv="cache-control" content="no-cache" /><meta http-equiv="expires" content="0"></head>
+<body><h2>Connectr Alarm Clocks</h2>
+    <div style="background-color: #9e9e9e; padding: 10px 10px 10px 10px;"><strong>IMPORTANT:</strong> Do NOT close this window without saving or cancelling.  Connectr is paused internally until this is dismissed!</div><br/>
+    <h3>Connected Devices: <a href="#" style="width: 300px;" class="tooltip" data-tip="These are the devices that are currently logged in and online.  You can specify any device for your alarm, but be sure it is on and logged in at the right time.  Make sure your device doesn't become unavailable when idle.">eh?</a></h3>
+    <table><tr><th style="width:300px;">Name</th><th>Device ID</th></tr>
+"###,
+        "HTTP/1.1 200 OK\r\nCache-Control: no-cache, no-store, must-revalidate, max-age=0\r\n\r\n");
+    if let Some(devices) = devices {
+        for dev in devices {
+            form.push_str(&format!(
+                r###"
+      <tr><td>{}</td><td>{}</td></tr>
+"###, dev.name, dev.id.as_ref().unwrap_or(&"unknown".to_string())));
+        }
+    }
+    form.push_str(&format!(
+    r###"
+    </table><br/>
+    <h3>Alarm Schedule:</h3>
 <form method="POST" action="#" accept-charset="UTF-8"><table>
-<tr><th style="width:100px; border-bottom: 1px solid #ddd;" align="center">Time</th><th align="center" style="width:100px;border-bottom: 1px solid #ddd;">Repeat</th></tr>
-
-<tr><td align="center"><input style="width:40px;" type="number" name="hour_0" min="0" max="23" size="3" maxlength="2">:<input style="width:40px;" type="number" name="minute_0" min="0" max="59" size="3" maxlength="2"></td><td align="center"><select name="repeat_0"><option value="daily">Daily</option><option value="weekdays">Weekdays</option><option value="weekends">Weekends</option></select></td></tr>
-
-<tr><td align="center"><input style="width:40px;" type="number" name="hour_1" min="0" max="23" size="3" maxlength="2">:<input style="width:40px;" type="number" name="minute_1" min="0" max="59" size="3" maxlength="2"></td><td align="center"><select name="repeat_1"><option value="daily">Daily</option><option value="weekdays">Weekdays</option><option value="weekends">Weekends</option></select></td></tr>
-
-<tr><td align="center"><input style="width:40px;" type="number" name="hour_2" min="0" max="23" size="3" maxlength="2">:<input style="width:40px;" type="number" name="minute_2" min="0" max="59" size="3" maxlength="2"></td><td align="center"><select name="repeat_2"><option value="daily">Daily</option><option value="weekdays">Weekdays</option><option value="weekends">Weekends</option></select></td></tr>
-
-<tr><td align="center"><input style="width:40px;" type="number" name="hour_3" min="0" max="23" size="3" maxlength="2">:<input style="width:40px;" type="number" name="minute_3" min="0" max="59" size="3" maxlength="2"></td><td align="center"><select name="repeat_3"><option value="daily">Daily</option><option value="weekdays">Weekdays</option><option value="weekends">Weekends</option></select></td></tr>
-
-<tr><td align="center"><input style="width:40px;" type="number" name="hour_4" min="0" max="23" size="3" maxlength="2">:<input style="width:40px;" type="number" name="minute_4" min="0" max="59" size="3" maxlength="2"></td><td align="center"><select name="repeat_4"><option value="daily">Daily</option><option value="weekdays">Weekdays</option><option value="weekends">Weekends</option></select></td></tr>
+<tr><th align="center">Time <a href="#" class="tooltip" data-tip="24-hour format">eh?</a></th><th align="center">Repeat</th><th>Spotify URI <a href="#" class="tooltip" data-tip="In Spotify app: right-click playlist, click 'Share', click 'URI'">eh?</a></th><th>Device ID <a href="#" class="tooltip" data-tip="Unique identifier of Spotify hardware.  Devices are listed above.">eh?</a></th></tr>
+"###));
+    for i in 0..5 {
+        form.push_str(&format!(
+            r###"
+<tr><td align="center"><input style="width:40px;" type="number" name="hour_{}" min="0" max="23" size="3" maxlength="2" value="{}">:<input style="width:40px;" type="number" name="minute_{}" min="0" max="59" size="3" maxlength="2" value="{}"></td><td align="center"><select name="repeat_{}"><option value="daily" {}>Daily</option><option value="weekdays" {}>Weekdays</option><option value="weekends" {}>Weekends</option></select></td><td><input type="text" name="context_{}" style="width:350px;" value="{}"></td><td><input type="text" name="device_{}" size="42" value="{}"></td></tr>
+"###,
+            i,
+            alarms.get(i).map_or(0, |a| {a.hour}).to_string(),
+            i,
+            alarms.get(i).map_or(0, |a| {a.minute}).to_string(),
+            i,
+            match alarms.get(i).map_or(false, |a| {a.repeat == AlarmRepeat::Daily   }) {
+                true => "selected", _ => "" },
+            match alarms.get(i).map_or(false, |a| {a.repeat == AlarmRepeat::Weekdays}) {
+                true => "selected", _ => ""},
+            match alarms.get(i).map_or(false, |a| {a.repeat == AlarmRepeat::Weekends}) {
+                true => "selected", _ => ""},
+            i,
+            alarms.get(i).map_or("", |a| {&a.context}),
+            i,
+            alarms.get(i).map_or("", |a| {&a.device}),
+        ));
+    }
 
-<tr><td colspan=2><center><input type="submit" value="Save Configuration" style="height:50px; width: 300px; font-size:20px;"></center></td></tr></br>
+    form.push_str(&format!(
+        r###"
+<tr><td colspan="4"><br/><center><input type="submit" name="cancel" value="Cancel" style="height:50px; width: 300px; font-size:20px;"> &nbsp; <input type="submit" name="submit" value="Save Configuration" style="height:50px; width: 300px; font-size:20px;"></center></td></tr></br>
 </table></form>
+<br/><small>You can manually change or add more alarms by editing: <em>{}</em></br></br>
+It is wise to have a backup alarm, in case your internet or Spotify is down.</small>
 </body></html>
-"###, "HTTP/1.1 200 OK\r\n\r\n");
+"###, inifile()));
+
     let reply = format!("{}Configuration saved.  You can close this window.",
                         "HTTP/1.1 200 OK\r\n\r\n");
     let mut config = BTreeMap::<String,String>::new();
@@ -204,8 +253,11 @@ pub fn save_web_alarm_config(config: BTreeMap<String,String>) -> Result<(), Sett
     for pair in config.iter() {
         let key = pair.0;
         let value = pair.1;
+        if key == "cancel" {
+            return Err("Canceled by user.".to_string());
+        }
         // are you kidding me??
-        let idx = key.chars().rev().take(1).collect::<Vec<char>>()[0].to_digit(10).unwrap() as usize;
+        let idx = key.chars().rev().take(1).collect::<Vec<char>>()[0].to_digit(10).unwrap_or(0) as usize;
         let entry = entries.get_mut(idx).unwrap();
         match key.split("_").next().unwrap() {
             "hour" => {
@@ -225,12 +277,15 @@ pub fn save_web_alarm_config(config: BTreeMap<String,String>) -> Result<(), Sett
                     _ => AlarmRepeat::Daily,
                 };
             },
+            "context" => {
+                entry.context = value.clone();
+            },
+            "device" => {
+                entry.device = value.clone();
+            },
             _ => {},
         }
     }
-    for i in 0..5 {
-        info!("Entry {}: {:?}", i, entries.get(i).unwrap());
-    }
     for (idx,entry) in entries.iter().enumerate() {
         conf.set_to(Some("alarms"), format!("alarm{}", idx+1), entry.to_string());
     }

diff --git a/src/webapi/mod.rs b/src/webapi/mod.rs
line changes: +55/-19
index df577e5..d580b22
--- a/src/webapi/mod.rs
+++ b/src/webapi/mod.rs
@@ -275,7 +275,7 @@ impl QueryString {
     }
 }
 
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Debug, Clone, Copy)]
 pub enum AlarmRepeat {
     Daily,
     Weekdays,
@@ -314,7 +314,21 @@ pub struct AlarmEntry {
     pub now: Option<DateTime<Local>>,
 }
 
-#[derive(Default, Debug)]
+impl<'a> From<&'a AlarmConfig> for AlarmEntry {
+    fn from(alarm: &AlarmConfig) -> Self {
+        AlarmEntry {
+            time: format!("{:02}:{:02}", alarm.hour, alarm.minute),
+            repeat: alarm.repeat,
+            context: PlayContext::new()
+                .context_uri(&alarm.context)
+                .offset_position(0)
+                .build(),
+            device: alarm.device.clone(),
+        }
+    }
+}
+
+#[derive(Default, Debug, Clone)]
 pub struct AlarmConfig {
     pub hour: u32,
     pub minute: u32,
@@ -440,19 +454,25 @@ impl<'a> SpotifyConnectrBuilder<'a> {
             self.access = settings.access_token.clone();
             self.refresh = settings.refresh_token.clone();
         }
-        Some(SpotifyConnectr {api: self.api,
-                              settings: settings,
-                              auth_code: String::new(),
-                              access_token: self.access.clone(),
-                              refresh_token: self.refresh.clone(),
-                              expire_utc: self.expire,
-                              device: None,
-                              refresh_timer: timer::Timer::new(),
-                              refresh_timer_guard: None,
-                              refresh_timer_channel: None,
-                              alarms: Vec::new(),
-                              next_alarm_id: AtomicUsize::new(0),
-        })
+        let mut cnr = SpotifyConnectr {
+            api: self.api,
+            settings: settings,
+            auth_code: String::new(),
+            access_token: self.access.clone(),
+            refresh_token: self.refresh.clone(),
+            expire_utc: self.expire,
+            device: None,
+            refresh_timer: timer::Timer::new(),
+            refresh_timer_guard: None,
+            refresh_timer_channel: None,
+            alarms: Vec::new(),
+            next_alarm_id: AtomicUsize::new(0),
+        };
+        let alarms: Vec<AlarmConfig> = cnr.settings.alarms.clone();
+        for alarm in &alarms {
+            let _ = cnr.schedule_alarm(alarm.into());
+        }
+        Some(cnr)
     }
     #[cfg(test)]
     fn with_api(&mut self, api: SpotifyEndpoints<'a>) -> &mut Self {
@@ -574,17 +594,28 @@ impl<'a> SpotifyConnectr<'a> {
         let closure = move || { tx.send(()).unwrap(); };
         let guard = alarm.timer.schedule_with_date(alarm_time, closure);
         let duration = alarm_time.signed_duration_since(Local::now());
-        info!("Alarm set for {} hours from now ({} mins)", duration.num_hours(), duration.num_minutes());
+        info!("Alarm {} set for {} hours from now ({} mins)", alarm.entry.time,
+              duration.num_hours(), duration.num_minutes());
 
         alarm.channel = Some(rx);
         alarm.guard = Some(guard);
         alarm.time = Some(alarm_time);
         Ok(())
     }
-    pub fn alarm_configure(&mut self) {
-        let alarm_config = settings::request_web_alarm_config();
+    pub fn alarm_configure(&mut self, devices: Option<&ConnectDeviceList>) {
+        let alarm_config = settings::request_web_alarm_config(&self.settings.alarms, devices);
         if settings::save_web_alarm_config(alarm_config).is_ok() {
             if let Some(settings) = settings::read_settings(self.api.scopes_version) {
+                let ids: Vec<AlarmId> = self.alarms.iter().map(|x| {x.id}).collect();
+                for id in ids {
+                    let _ = self.alarm_disable(id);
+                }
+                self.alarms.clear();
+                for alarm in &settings.alarms {
+                    // TODO: clear all existing alarms?  or return these as a list?
+                    // save them in self and add an accessor?
+                    let _ = self.schedule_alarm(alarm.into());
+                }
                 self.settings = settings;
             };
         }
@@ -666,9 +697,14 @@ impl<'a> SpotifyConnectr<'a> {
         for (idx, exp) in expired.iter().enumerate() {
             if *exp {
                 let old_alarm = self.alarms.swap_remove(idx);
+                info!("Alarm started: {} on {}",
+                      old_alarm.entry.context.context_uri.as_ref().unwrap(),
+                      old_alarm.entry.device);
                 self.set_target_device(Some(old_alarm.entry.device.clone()));
                 self.play(Some(&old_alarm.entry.context));
-                let _ = self.schedule_alarm(old_alarm.entry);
+                let id = old_alarm.id;
+                self.alarms.push(old_alarm);
+                let _ = self.alarm_reschedule(id);
             }
         }