initial commit
This commit is contained in:
91
internal/cron.go
Normal file
91
internal/cron.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package morningalarm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
func (ma *MorningAlarm) nextAlarm() *time.Time {
|
||||
if len(ma.cr.Entries()) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
min := ma.cr.Entries()[0].Next
|
||||
|
||||
for _, entry := range ma.cr.Entries() {
|
||||
if entry.Next.Before(min) {
|
||||
min = entry.Next
|
||||
}
|
||||
}
|
||||
|
||||
return &min
|
||||
}
|
||||
|
||||
func (ma *MorningAlarm) addAlarm(spec string, name string) (cron.EntryID, error) {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (ma *MorningAlarm) fireAlarm() {
|
||||
ma.playWakeUpMusic()
|
||||
}
|
||||
50
internal/morningalarm.go
Normal file
50
internal/morningalarm.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package morningalarm
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
type alarm struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Time string `json:"time" binding:"required"`
|
||||
id cron.EntryID
|
||||
}
|
||||
|
||||
type MorningAlarmConfig struct {
|
||||
SpotifyClientID string `json:"spotifyClientId"`
|
||||
SpotifyClientSecret string `json:"spotifyClientSecret"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
PlaylistID string `json:"wakeupContext"`
|
||||
RedirectURL string `json:"redirectUrl"`
|
||||
ListenAddr string `json:"listenAddr"`
|
||||
}
|
||||
|
||||
type MorningAlarm struct {
|
||||
config MorningAlarmConfig
|
||||
sp *spotify.Client
|
||||
cr *cron.Cron
|
||||
ro *gin.Engine
|
||||
alarms []alarm
|
||||
}
|
||||
|
||||
func New(config MorningAlarmConfig) *MorningAlarm {
|
||||
return &MorningAlarm{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (ma *MorningAlarm) Start() {
|
||||
ma.cr = cron.New()
|
||||
ma.loadAlarms()
|
||||
ma.cr.Start()
|
||||
|
||||
ma.ro = gin.Default()
|
||||
|
||||
ma.setupSpotify()
|
||||
|
||||
ma.setupWebserver()
|
||||
|
||||
ma.ro.Run(ma.config.ListenAddr)
|
||||
}
|
||||
136
internal/spotify.go
Normal file
136
internal/spotify.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package morningalarm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/zmb3/spotify/v2"
|
||||
spotifyauth "github.com/zmb3/spotify/v2/auth"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const redirectURI = "/api/spotifycb"
|
||||
|
||||
func (ma *MorningAlarm) setupSpotify() error {
|
||||
auth := spotifyauth.New(
|
||||
spotifyauth.WithRedirectURL(ma.config.RedirectURL+redirectURI),
|
||||
spotifyauth.WithClientID(ma.config.SpotifyClientID),
|
||||
spotifyauth.WithClientSecret(ma.config.SpotifyClientSecret),
|
||||
spotifyauth.WithScopes(
|
||||
spotifyauth.ScopeUserReadPrivate,
|
||||
spotifyauth.ScopeUserModifyPlaybackState,
|
||||
spotifyauth.ScopeStreaming,
|
||||
spotifyauth.ScopeUserReadPlaybackState,
|
||||
),
|
||||
)
|
||||
|
||||
loadedTokenJSON, err := os.ReadFile("spotifyToken.json")
|
||||
|
||||
if err == nil {
|
||||
fmt.Println("Found access token in file")
|
||||
|
||||
var token oauth2.Token
|
||||
err = json.Unmarshal(loadedTokenJSON, &token)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ma.sp = spotify.New(auth.Client(context.Background(), &token), spotify.WithRetry(true))
|
||||
|
||||
newToken, err := ma.sp.Token()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenJSON, err := json.Marshal(newToken)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.WriteFile("spotifyToken.json", tokenJSON, os.ModePerm)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
state := "abc123"
|
||||
|
||||
ma.ro.GET(redirectURI, func(c *gin.Context) {
|
||||
token, err := auth.Token(c, state, c.Request)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tokenJSON, err := json.Marshal(token)
|
||||
os.WriteFile("spotifyToken.json", tokenJSON, os.ModePerm)
|
||||
|
||||
ma.sp = spotify.New(auth.Client(c, token))
|
||||
})
|
||||
|
||||
url := auth.AuthURL(state)
|
||||
|
||||
fmt.Println("Please log in to Spotify by visiting the following page in your browser:", url)
|
||||
return nil
|
||||
}
|
||||
|
||||
// playWakeUpMusic plays the playlist specified in the config at the specified device.
|
||||
// Returns true if the playback was started successfully.
|
||||
// Returns error if any error occurred.
|
||||
// Can still return an error even if the playback was started successfully.
|
||||
func (ma *MorningAlarm) playWakeUpMusic() (bool, error) {
|
||||
playContextURI := spotify.URI("spotify:playlist:" + ma.config.PlaylistID)
|
||||
deviceID := spotify.ID(ma.config.DeviceID)
|
||||
|
||||
playlist, err := ma.sp.GetPlaylistItems(context.Background(), spotify.ID(ma.config.PlaylistID))
|
||||
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
randomTrackIndex := rand.Intn(playlist.Total)
|
||||
|
||||
err = ma.sp.PlayOpt(context.Background(), &spotify.PlayOptions{
|
||||
PlaybackContext: &playContextURI,
|
||||
DeviceID: &deviceID,
|
||||
PlaybackOffset: &spotify.PlaybackOffset{Position: randomTrackIndex},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
err = ma.sp.ShuffleOpt(context.Background(), true, &spotify.PlayOptions{
|
||||
DeviceID: &deviceID,
|
||||
})
|
||||
|
||||
return true, err
|
||||
}
|
||||
|
||||
func (ma *MorningAlarm) getAvailableDevices() ([]Device, error) {
|
||||
devices, err := ma.sp.PlayerDevices(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var availableDevices []Device
|
||||
for _, device := range devices {
|
||||
if !device.Restricted {
|
||||
availableDevices = append(availableDevices, Device{
|
||||
ID: string(device.ID),
|
||||
Name: device.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return availableDevices, nil
|
||||
}
|
||||
90
internal/webserver.go
Normal file
90
internal/webserver.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package morningalarm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (ma *MorningAlarm) setupWebserver() {
|
||||
ma.ro.POST("/api/alarm", func(c *gin.Context) {
|
||||
var body alarm
|
||||
|
||||
if err := c.BindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
_, err := ma.addAlarm(body.Time, body.Name)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
err = ma.saveAlarms()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{})
|
||||
}
|
||||
})
|
||||
|
||||
ma.ro.GET("/api/info", func(c *gin.Context) {
|
||||
spotifyUser, err := ma.sp.CurrentUser(c)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
currentTime := time.Now()
|
||||
zone, offset := currentTime.Zone()
|
||||
|
||||
nextIn := ma.nextAlarm()
|
||||
|
||||
response := gin.H{
|
||||
"spotifyUser": spotifyUser.User.DisplayName,
|
||||
"serverTime": currentTime.Format("15:04:05"),
|
||||
"timezone": zone,
|
||||
"timezoneOffsetInH": offset / 60 / 60,
|
||||
"alarms": len(ma.cr.Entries()),
|
||||
"wakeupContext": ma.config.PlaylistID,
|
||||
"deviceId": ma.config.DeviceID,
|
||||
}
|
||||
|
||||
if nextIn != nil {
|
||||
response["nextAlarmAt"] = nextIn.String()
|
||||
response["nextAlarmIn"] = nextIn.Sub(currentTime).String()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
|
||||
ma.ro.GET("/api/device", func(c *gin.Context) {
|
||||
devices, err := ma.getAvailableDevices()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, devices)
|
||||
})
|
||||
|
||||
ma.ro.POST("/api/trigger", func(c *gin.Context) {
|
||||
if _, err := ma.playWakeUpMusic(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user