diff --git a/internal/cron.go b/internal/cron.go index edc2e81..27e4f8a 100644 --- a/internal/cron.go +++ b/internal/cron.go @@ -1,14 +1,12 @@ package morningalarm import ( - "encoding/json" - "errors" - "os" "time" "github.com/robfig/cron/v3" ) +// nextAlarm returns the time of the next alarm to fire from the perspective of cron func (ma *MorningAlarm) nextAlarm() *time.Time { if len(ma.cr.Entries()) == 0 { return nil @@ -25,102 +23,17 @@ func (ma *MorningAlarm) nextAlarm() *time.Time { return &min } -func (ma *MorningAlarm) addAlarm(spec string, name string) (cron.EntryID, error) { - // Check if alarm already exists - if ma.getAlarm(name) != nil { - return 0, errors.New("Alarm already exists") - } - - id, err := ma.cr.AddFunc(spec, ma.fireAlarm) - - if err != nil { - return 0, err - } - - ma.alarms = append(ma.alarms, alarm{ - id: id, - Time: spec, - Name: name, - }) - - return id, nil +// armAlarm arms an alarm to fire at the specified time +func (ma *MorningAlarm) armAlarm(alarm *alarm) (cron.EntryID, error) { + return ma.cr.AddFunc(alarm.Time, ma.fireAlarm) } -func (ma *MorningAlarm) saveAlarms() error { - content, err := json.Marshal(ma.alarms) - - if err != nil { - return err - } - - err = os.WriteFile("alarms.json", content, 0644) - - if err != nil { - return err - } - - return nil -} - -func (ma *MorningAlarm) loadAlarms() error { - if _, err := os.Stat("alarms.json"); os.IsNotExist(err) { - ma.alarms = []alarm{} - return nil - } - - content, err := os.ReadFile("alarms.json") - - if err != nil { - return err - } - - var ent []alarm - err = json.Unmarshal(content, &ent) - - if err != nil { - return err - } - - for _, entry := range ent { - _, err := ma.addAlarm(entry.Time, entry.Name) - - if err != nil { - return err - } - } - - return nil +// disarmAlarm disarms an alarm and prevents it from firing at the specified time +func (ma *MorningAlarm) disarmAlarm(alarm *alarm) { + ma.cr.Remove(alarm.cronID) } +// fireAlarm function that is called when an alarm is fired func (ma *MorningAlarm) fireAlarm() { ma.playWakeUpMusic() } - -func (ma *MorningAlarm) getAlarm(name string) *alarm { - for _, alarm := range ma.alarms { - if alarm.Name == name { - return &alarm - } - } - - return nil -} - -func (ma *MorningAlarm) deleteAlarm(name string) bool { - for _, alarm := range ma.alarms { - if alarm.Name == name { - ma.cr.Remove(alarm.id) - - for i, a := range ma.alarms { - if a.Name == name { - ma.alarms = append(ma.alarms[:i], ma.alarms[i+1:]...) - break - } - } - - return true - } - } - - return false -} diff --git a/internal/morningalarm.go b/internal/morningalarm.go index 58c499a..81b654a 100644 --- a/internal/morningalarm.go +++ b/internal/morningalarm.go @@ -7,9 +7,10 @@ import ( ) type alarm struct { - Name string `json:"name" binding:"required"` - Time string `json:"time" binding:"required"` - id cron.EntryID + Name string `json:"name" binding:"required"` + Time string `json:"time" binding:"required"` + Disabled bool `json:"disabled"` + cronID cron.EntryID } type MorningAlarmConfig struct { @@ -36,7 +37,10 @@ func New(config MorningAlarmConfig) *MorningAlarm { func (ma *MorningAlarm) Start() { ma.cr = cron.New() - ma.loadAlarms() + + ma.loadAlarmsFromFile() + ma.armAllAlarms() + ma.cr.Start() ma.ro = gin.Default() diff --git a/internal/store.go b/internal/store.go new file mode 100644 index 0000000..47d1aeb --- /dev/null +++ b/internal/store.go @@ -0,0 +1,138 @@ +package morningalarm + +import ( + "encoding/json" + "errors" + "os" +) + +// addAlarm adds an alarm to the list of alarms and starts a cron job to fire it at the specified time +func (ma *MorningAlarm) addAlarm(time, name string) (*alarm, error) { + // Check if alarm already exists + if ma.getAlarm(name) != nil { + return nil, errors.New("Alarm with name " + name + " already exists") + } + + alarm := alarm{ + Name: name, + Time: time, + Disabled: false, + } + + alarm.cronID, _ = ma.armAlarm(&alarm) + + ma.alarms = append(ma.alarms, alarm) + + return &alarm, nil +} + +func (ma *MorningAlarm) getAlarm(name string) *alarm { + for _, alarm := range ma.alarms { + if alarm.Name == name { + return &alarm + } + } + + return nil +} + +// loadAlarmsFromFile loads the list of alarms from alarms.json into memory +func (ma *MorningAlarm) loadAlarmsFromFile() error { + if _, err := os.Stat("alarms.json"); os.IsNotExist(err) { + ma.alarms = []alarm{} + return nil + } + + content, err := os.ReadFile("alarms.json") + + if err != nil { + return err + } + + err = json.Unmarshal(content, &ma.alarms) + + if err != nil { + return err + } + + return nil +} + +// saveAlarmsToFile saves the list of alarms to alarms.json +func (ma *MorningAlarm) saveAlarmsToFile() error { + content, err := json.Marshal(ma.alarms) + + if err != nil { + return err + } + + err = os.WriteFile("alarms.json", content, 0644) + + if err != nil { + return err + } + + return nil +} + +// removeAlarm removes an alarm from the list of alarms and stops cron from firing it. Returns true if the alarm was removed, false if it was not found +func (ma *MorningAlarm) removeAlarm(name string) bool { + for _, alarm := range ma.alarms { + if alarm.Name == name { + + // Stop cron from firing the alarm + ma.disarmAlarm(&alarm) + + // Remove alarm from list of alarms + for i, a := range ma.alarms { + if a.Name == name { + ma.alarms = append(ma.alarms[:i], ma.alarms[i+1:]...) + break + } + } + + return true + } + } + + return false +} + +// armAllAlarms arms all alarms in the list of alarms that are not disabled and returns an error if any of them fail to arm +func (ma *MorningAlarm) armAllAlarms() error { + for _, alarm := range ma.alarms { + if !alarm.Disabled { + id, err := ma.armAlarm(&alarm) + + if err != nil { + return err + } + + alarm.cronID = id + } + } + + return nil +} + +// setDisabled sets the disabled status of an alarm +func (ma *MorningAlarm) setDisabled(name string, disabled bool) { + for i, alarm := range ma.alarms { + if alarm.Name == name { + if alarm.Disabled == disabled { + return + } + + ma.alarms[i].Disabled = disabled + + if disabled { + ma.disarmAlarm(&alarm) + } else { + id, _ := ma.armAlarm(&alarm) + ma.alarms[i].cronID = id + } + + return + } + } +} diff --git a/internal/webserver.go b/internal/webserver.go index fb3cb59..00d8195 100644 --- a/internal/webserver.go +++ b/internal/webserver.go @@ -12,7 +12,12 @@ type Device struct { ID string `json:"id"` } +type AlarmPatch struct { + Disabled bool `json:"disabled"` +} + func (ma *MorningAlarm) setupWebserver() { + // Create a new alarm ma.ro.POST("/api/alarm", func(c *gin.Context) { var body alarm @@ -40,7 +45,7 @@ func (ma *MorningAlarm) setupWebserver() { if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } else { - err = ma.saveAlarms() + err = ma.saveAlarmsToFile() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -52,6 +57,7 @@ func (ma *MorningAlarm) setupWebserver() { } }) + // Get an alarm ma.ro.GET("/api/alarm/:id", func(c *gin.Context) { id := c.Param("id") @@ -65,15 +71,17 @@ func (ma *MorningAlarm) setupWebserver() { c.JSON(http.StatusOK, alarm) }) + // Get all alarms ma.ro.GET("/api/alarm", func(c *gin.Context) { c.JSON(http.StatusOK, ma.alarms) }) + // Delete an alarm ma.ro.DELETE("/api/alarm/:id", func(c *gin.Context) { id := c.Param("id") - if ma.deleteAlarm(id) { - err := ma.saveAlarms() + if ma.removeAlarm(id) { + err := ma.saveAlarmsToFile() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -86,6 +94,7 @@ func (ma *MorningAlarm) setupWebserver() { } }) + // Get info about the server and the alarms ma.ro.GET("/api/info", func(c *gin.Context) { spotifyUser, err := ma.sp.CurrentUser(c) @@ -117,6 +126,7 @@ func (ma *MorningAlarm) setupWebserver() { c.JSON(http.StatusOK, response) }) + // Get available spotify devices ma.ro.GET("/api/device", func(c *gin.Context) { devices, err := ma.getAvailableDevices() @@ -128,6 +138,7 @@ func (ma *MorningAlarm) setupWebserver() { c.JSON(http.StatusOK, devices) }) + // Trigger an alarm immediately ma.ro.POST("/api/trigger", func(c *gin.Context) { if _, err := ma.playWakeUpMusic(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -136,4 +147,33 @@ func (ma *MorningAlarm) setupWebserver() { c.JSON(http.StatusOK, gin.H{}) }) + + ma.ro.PATCH("/api/alarm/:id", func(c *gin.Context) { + id := c.Param("id") + + alarm := ma.getAlarm(id) + + if alarm == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Alarm not found"}) + return + } + + var body AlarmPatch + + if err := c.BindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ma.setDisabled(id, body.Disabled) + + err := ma.saveAlarmsToFile() + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{}) + }) }