Compare commits
No commits in common. "v0.1.1" and "master" have entirely different histories.
35
.drone.yml
35
.drone.yml
@ -13,12 +13,11 @@ workspace:
|
||||
|
||||
steps:
|
||||
- name: build front
|
||||
image: node:18-alpine
|
||||
image: node:22
|
||||
commands:
|
||||
- mkdir deploy
|
||||
- cd ui
|
||||
- npm install --network-timeout=100000
|
||||
- sed -i 's!@popperjs/core/dist/esm/popper!@popperjs/core!' node_modules/sveltestrap/src/*.js node_modules/sveltestrap/src/*.svelte
|
||||
- npm run build
|
||||
- tar chjf ../deploy/static.tar.bz2 build
|
||||
|
||||
@ -27,39 +26,53 @@ steps:
|
||||
commands:
|
||||
- apk --no-cache add alsa-lib-dev build-base git pkgconf
|
||||
- go get -v -d
|
||||
- go vet -v
|
||||
- go build -v -ldflags '-w -X main.Version="${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- go vet -v -tags pulse
|
||||
- go build -tags pulse -ldflags '-w -X main.Version=${DRONE_BRANCH}-${DRONE_COMMIT} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- ln deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} reveil
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: build tag
|
||||
- name: build armv7 tag
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk --no-cache add alsa-lib-dev build-base git pkgconf
|
||||
- go get -v -d
|
||||
- go vet -v
|
||||
- go build -v -ldflags '-w -X main.Version="${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- ln deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} reveil
|
||||
- go vet -v -tags pulse
|
||||
- go build -tags pulse -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}v7
|
||||
- ln deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}v7 reveil
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: build armv6 tag
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk --no-cache add alsa-lib-dev build-base git pkgconf
|
||||
- go build -tags pulse,netgo -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}hf
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOARM: 6
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: gitea release
|
||||
image: plugins/gitea-release
|
||||
image: plugins/gitea-release:linux-arm
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: gitea_api_key
|
||||
base_url: https://git.nemunai.re/
|
||||
files: deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
files:
|
||||
- deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}hf
|
||||
- deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}v7
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
image: plugins/docker:linux-arm
|
||||
settings:
|
||||
registry: registry.nemunai.re
|
||||
repo: registry.nemunai.re/reveil
|
||||
|
@ -1,11 +1,10 @@
|
||||
FROM node:18-alpine as nodebuild
|
||||
FROM node:22-alpine as nodebuild
|
||||
|
||||
WORKDIR /ui
|
||||
|
||||
COPY ui/ .
|
||||
|
||||
RUN npm install --network-timeout=100000 && \
|
||||
sed -i 's!@popperjs/core/dist/esm/popper!@popperjs/core!' node_modules/sveltestrap/src/*.js node_modules/sveltestrap/src/*.svelte && \
|
||||
npm run build
|
||||
|
||||
|
||||
@ -16,10 +15,10 @@ RUN apk --no-cache add git go-bindata
|
||||
COPY . /go/src/git.nemunai.re/nemunaire/reveil
|
||||
COPY --from=nodebuild /ui/build /go/src/git.nemunai.re/nemunaire/reveil/ui/build
|
||||
WORKDIR /go/src/git.nemunai.re/nemunaire/reveil
|
||||
RUN go get -v && go generate -v && go build -v -ldflags="-s -w"
|
||||
RUN go get -v && go generate -v && go build -tags pulse -ldflags="-s -w"
|
||||
|
||||
|
||||
FROM alpine:3.16
|
||||
FROM alpine:3.20
|
||||
|
||||
VOLUME /data
|
||||
WORKDIR /data
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM alpine:3.16
|
||||
FROM alpine:3.20
|
||||
|
||||
VOLUME /data
|
||||
WORKDIR /data
|
||||
|
@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -90,4 +91,29 @@ func declareActionsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
|
||||
c.JSON(http.StatusOK, nil)
|
||||
})
|
||||
|
||||
actionsRoutes.POST("/run", func(c *gin.Context) {
|
||||
action := c.MustGet("action").(*reveil.Action)
|
||||
|
||||
settings, err := reveil.ReadSettings(cfg.SettingsFile)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to run the action: unable to read settings: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
cmd, err := action.Launch(settings)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to run the action: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
if err != nil {
|
||||
log.Printf("%q: %s", action.Name, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
20
api/alarm.go
20
api/alarm.go
@ -1,11 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.nemunai.re/nemunaire/reveil/config"
|
||||
"git.nemunai.re/nemunaire/reveil/model"
|
||||
"git.nemunai.re/nemunaire/reveil/player"
|
||||
)
|
||||
|
||||
@ -20,7 +23,7 @@ func declareAlarmRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
|
||||
router.POST("/alarm/run", func(c *gin.Context) {
|
||||
if player.CommonPlayer == nil {
|
||||
err := player.WakeUp(cfg)
|
||||
err := player.WakeUp(cfg, nil, true)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
@ -51,6 +54,21 @@ func declareAlarmRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := reveil.ReadSettings(cfg.SettingsFile)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("Unable to read settings: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
for k, srv := range settings.Federation {
|
||||
err = srv.WakeStop()
|
||||
if err != nil {
|
||||
log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error())
|
||||
} else {
|
||||
log.Printf("Federated wakeup on %s: launched!", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
|
||||
func declareAlarmsRoutes(cfg *config.Config, db *reveil.LevelDBStorage, resetTimer func(), router *gin.RouterGroup) {
|
||||
router.GET("/alarms/next", func(c *gin.Context) {
|
||||
alarm, err := reveil.GetNextAlarm(db)
|
||||
alarm, _, _, err := reveil.GetNextAlarm(cfg, db)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
@ -22,6 +22,18 @@ func declareAlarmsRoutes(cfg *config.Config, db *reveil.LevelDBStorage, resetTim
|
||||
c.JSON(http.StatusOK, alarm)
|
||||
})
|
||||
|
||||
router.DELETE("/alarms/next", func(c *gin.Context) {
|
||||
err := reveil.DropNextAlarm(cfg, db)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
resetTimer()
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
|
||||
router.GET("/alarms/single", func(c *gin.Context) {
|
||||
alarms, err := reveil.GetAlarmsSingle(db)
|
||||
if err != nil {
|
||||
@ -200,8 +212,8 @@ func declareAlarmsRoutes(cfg *config.Config, db *reveil.LevelDBStorage, resetTim
|
||||
|
||||
repeatedAlarmsRoutes.GET("", func(c *gin.Context) {
|
||||
alarm := c.MustGet("alarm").(*reveil.AlarmRepeated)
|
||||
alarm.FillExcepts(db)
|
||||
alarm.NextTime = alarm.GetNextOccurence(db)
|
||||
alarm.FillExcepts(cfg, db)
|
||||
alarm.NextTime = alarm.GetNextOccurence(cfg, db)
|
||||
|
||||
c.JSON(http.StatusOK, alarm)
|
||||
})
|
||||
|
176
api/federation.go
Normal file
176
api/federation.go
Normal file
@ -0,0 +1,176 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.nemunai.re/nemunaire/reveil/config"
|
||||
"git.nemunai.re/nemunaire/reveil/model"
|
||||
"git.nemunai.re/nemunaire/reveil/player"
|
||||
)
|
||||
|
||||
func declareFederationRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
router.POST("/federation/wakeup", func(c *gin.Context) {
|
||||
var s map[string]interface{}
|
||||
c.ShouldBind(s)
|
||||
|
||||
if player.CommonPlayer == nil {
|
||||
var seed int64
|
||||
if tmp, ok := s["seed"].(int64); ok {
|
||||
seed = tmp
|
||||
} else {
|
||||
seed := time.Now().Unix()
|
||||
seed -= seed % 172800
|
||||
}
|
||||
|
||||
err := player.WakeUpFromFederation(cfg, seed, nil)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Player already running"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
router.POST("/federation/wakeok", func(c *gin.Context) {
|
||||
if player.CommonPlayer != nil {
|
||||
err := player.CommonPlayer.Stop()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
|
||||
router.GET("/federation", func(c *gin.Context) {
|
||||
settings, err := reveil.ReadSettings(cfg.SettingsFile)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, settings.Federation)
|
||||
})
|
||||
|
||||
federationsRoutes := router.Group("/federation/:fid")
|
||||
federationsRoutes.Use(func(c *gin.Context) {
|
||||
settings, err := reveil.ReadSettings(cfg.SettingsFile)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
f, ok := settings.Federation[string(c.Param("fid"))]
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Action not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("federation", &f)
|
||||
c.Next()
|
||||
})
|
||||
federationsRoutes.GET("", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, c.MustGet("federation"))
|
||||
})
|
||||
federationsRoutes.POST("sync", func(c *gin.Context) {
|
||||
srv := c.MustGet("federation").(*reveil.FederationServer)
|
||||
|
||||
// Retrieve music list on remote
|
||||
remoteMusics, err := srv.GetMusics()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to retrieve remote tracks lists: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve local music list
|
||||
localMusics, err := reveil.LoadTracks(cfg)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to retrieve local tracks: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
// Compute diff
|
||||
var newMusics []reveil.Track
|
||||
var oldMusics []reveil.Track
|
||||
var musicsToEnable []reveil.Track
|
||||
|
||||
for _, rTrack := range remoteMusics {
|
||||
found := false
|
||||
for _, lTrack := range localMusics {
|
||||
if bytes.Compare(lTrack.Id, rTrack.Id) == 0 || lTrack.Name == rTrack.Name {
|
||||
if lTrack.Enabled != rTrack.Enabled {
|
||||
if lTrack.Enabled {
|
||||
musicsToEnable = append(musicsToEnable, rTrack)
|
||||
} else {
|
||||
oldMusics = append(oldMusics, *lTrack)
|
||||
}
|
||||
}
|
||||
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found && rTrack.Enabled {
|
||||
oldMusics = append(oldMusics, rTrack)
|
||||
}
|
||||
}
|
||||
|
||||
for _, lTrack := range localMusics {
|
||||
found := false
|
||||
for _, rTrack := range remoteMusics {
|
||||
if bytes.Compare(lTrack.Id, rTrack.Id) == 0 || lTrack.Name == rTrack.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found && lTrack.Enabled {
|
||||
newMusics = append(newMusics, *lTrack)
|
||||
}
|
||||
}
|
||||
|
||||
// Disable unexistant musics on local
|
||||
for _, t := range oldMusics {
|
||||
t.Enabled = false
|
||||
err = srv.UpdateTrack(&t)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs when disabling remote tracks (unexistant on local): %s: %s", t.Id.ToString(), err.Error())})
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Enable existant musics on remote
|
||||
for _, t := range musicsToEnable {
|
||||
t.Enabled = true
|
||||
err = srv.UpdateTrack(&t)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Send new musics
|
||||
for _, t := range newMusics {
|
||||
err = srv.SendTrack(&t)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
@ -13,6 +13,7 @@ func DeclareRoutes(router *gin.Engine, cfg *config.Config, db *reveil.LevelDBSto
|
||||
declareActionsRoutes(cfg, apiRoutes)
|
||||
declareAlarmRoutes(cfg, apiRoutes)
|
||||
declareAlarmsRoutes(cfg, db, resetTimer, apiRoutes)
|
||||
declareFederationRoutes(cfg, apiRoutes)
|
||||
declareGongsRoutes(cfg, apiRoutes)
|
||||
declareHistoryRoutes(cfg, apiRoutes)
|
||||
declareQuotesRoutes(cfg, apiRoutes)
|
||||
|
@ -78,4 +78,12 @@ func declareRoutinesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
|
||||
c.JSON(http.StatusOK, nil)
|
||||
})
|
||||
|
||||
routinesRoutes.POST("/run", func(c *gin.Context) {
|
||||
routine := c.MustGet("routine").(*reveil.Routine)
|
||||
|
||||
go routine.Launch(cfg)
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
54
app.go
54
app.go
@ -16,11 +16,12 @@ import (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
cfg *config.Config
|
||||
db *reveil.LevelDBStorage
|
||||
router *gin.Engine
|
||||
srv *http.Server
|
||||
nextAlarm *time.Timer
|
||||
cfg *config.Config
|
||||
db *reveil.LevelDBStorage
|
||||
router *gin.Engine
|
||||
srv *http.Server
|
||||
nextAlarm *time.Timer
|
||||
nextPreAlarm *time.Timer
|
||||
}
|
||||
|
||||
func NewApp(cfg *config.Config) *App {
|
||||
@ -49,6 +50,7 @@ func NewApp(cfg *config.Config) *App {
|
||||
|
||||
// Register routes
|
||||
ui.DeclareRoutes(router, cfg)
|
||||
ui.DeclareNoJSRoutes(router, cfg, db, app.ResetTimer)
|
||||
api.DeclareRoutes(router, cfg, db, app.ResetTimer)
|
||||
|
||||
router.GET("/api/version", func(c *gin.Context) {
|
||||
@ -75,14 +77,52 @@ func (app *App) Start() {
|
||||
func (app *App) ResetTimer() {
|
||||
if app.nextAlarm != nil {
|
||||
app.nextAlarm.Stop()
|
||||
app.nextPreAlarm = nil
|
||||
app.nextAlarm = nil
|
||||
}
|
||||
|
||||
if na, err := reveil.GetNextAlarm(app.db); err == nil && na != nil {
|
||||
settings, _ := reveil.ReadSettings(app.cfg.SettingsFile)
|
||||
|
||||
if na, routines, federated, err := reveil.GetNextAlarm(app.cfg, app.db); err == nil && na != nil {
|
||||
if settings != nil && settings.PreAlarmAction != "" {
|
||||
app.nextPreAlarm = time.AfterFunc(time.Until(*na)-settings.PreAlarmActionDelay*time.Minute, func() {
|
||||
app.nextPreAlarm = nil
|
||||
|
||||
settings, err := reveil.ReadSettings(app.cfg.SettingsFile)
|
||||
if err != nil {
|
||||
log.Println("Unable to read settings:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
action, err := reveil.LoadAction(app.cfg, settings.PreAlarmAction)
|
||||
if err != nil {
|
||||
log.Println("Unable to load pre-alarm action:", err.Error())
|
||||
}
|
||||
|
||||
cmd, err := action.Launch(settings)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
if err != nil {
|
||||
log.Printf("%q: %s", action.Name, err.Error())
|
||||
}
|
||||
}()
|
||||
})
|
||||
log.Println("Next pre-alarm programmed for", time.Time(*na).Add(settings.PreAlarmActionDelay*-1*time.Minute))
|
||||
}
|
||||
app.nextAlarm = time.AfterFunc(time.Until(*na), func() {
|
||||
app.nextPreAlarm = nil
|
||||
app.nextAlarm = nil
|
||||
reveil.RemoveOldAlarmsSingle(app.db)
|
||||
err := player.WakeUp(app.cfg)
|
||||
|
||||
// Rearm timer for the next time
|
||||
app.ResetTimer()
|
||||
|
||||
err := player.WakeUp(app.cfg, routines, federated)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return
|
||||
|
@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JWTSecretKey []byte
|
||||
@ -42,3 +43,33 @@ func (i *URL) Set(value string) error {
|
||||
i.URL = u
|
||||
return nil
|
||||
}
|
||||
|
||||
type Timezone struct {
|
||||
tz *time.Location
|
||||
}
|
||||
|
||||
func (tz *Timezone) GetLocation() *time.Location {
|
||||
if tz.tz != nil {
|
||||
return tz.tz
|
||||
} else {
|
||||
return time.Local
|
||||
}
|
||||
}
|
||||
|
||||
func (tz *Timezone) String() string {
|
||||
if tz.tz != nil {
|
||||
return tz.tz.String()
|
||||
} else {
|
||||
return time.Local.String()
|
||||
}
|
||||
}
|
||||
|
||||
func (tz *Timezone) Set(value string) error {
|
||||
newtz, err := time.LoadLocation(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tz.tz = newtz
|
||||
return nil
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ import (
|
||||
)
|
||||
|
||||
// FromEnv analyzes all the environment variables to find each one
|
||||
// starting by GUSTUS_
|
||||
// starting by REVEIL_
|
||||
func (c *Config) FromEnv() error {
|
||||
for _, line := range os.Environ() {
|
||||
if strings.HasPrefix(line, "GUSTUS_") {
|
||||
if strings.HasPrefix(line, "REVEIL_") {
|
||||
err := c.parseLine(line)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in environment (%q): %w", line, err)
|
||||
|
40
go.mod
40
go.mod
@ -4,37 +4,47 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/faiface/beep v1.1.0
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
||||
github.com/goccy/go-json v0.9.7 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/hajimehoshi/go-mp3 v0.3.0 // indirect
|
||||
github.com/hajimehoshi/oto v0.7.1 // indirect
|
||||
github.com/icza/bitio v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mewkiz/flac v1.0.7 // indirect
|
||||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
99
go.sum
99
go.sum
@ -1,4 +1,18 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -7,12 +21,20 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
|
||||
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||
@ -20,12 +42,22 @@ github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBY
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
@ -49,6 +81,12 @@ github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps
|
||||
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
@ -59,9 +97,17 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
|
||||
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
|
||||
@ -69,6 +115,8 @@ github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXi
|
||||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@ -78,6 +126,10 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@ -88,19 +140,43 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
@ -113,6 +189,10 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -122,20 +202,35 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew=
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@ -151,3 +246,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
@ -4,9 +4,11 @@ import (
|
||||
"bufio"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@ -19,9 +21,10 @@ type Action struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Path string `json:"path"`
|
||||
Enabled bool `json:"enabled"`
|
||||
fullPath string
|
||||
}
|
||||
|
||||
func LoadAction(path string) (string, string, error) {
|
||||
func loadAction(path string) (string, string, error) {
|
||||
fd, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@ -64,6 +67,45 @@ func LoadAction(path string) (string, string, error) {
|
||||
return name, description, nil
|
||||
}
|
||||
|
||||
func LoadAction(cfg *config.Config, path string) (*Action, error) {
|
||||
actionsDir, err := filepath.Abs(cfg.ActionsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path = filepath.Join(actionsDir, path)
|
||||
|
||||
d, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !d.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("%q is not a file, it cannot be an action.", path)
|
||||
}
|
||||
|
||||
hash := sha512.Sum512([]byte(path))
|
||||
|
||||
// Parse content
|
||||
name, description, err := loadAction(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid action file (trying to parse %s): %s", path, err.Error())
|
||||
}
|
||||
|
||||
if apath, err := filepath.Abs(path); err == nil {
|
||||
path = apath
|
||||
}
|
||||
|
||||
return &Action{
|
||||
Id: hash[:],
|
||||
Name: name,
|
||||
Description: description,
|
||||
Path: strings.TrimPrefix(path, actionsDir+"/"),
|
||||
Enabled: d.Mode().Perm()&0111 != 0,
|
||||
fullPath: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func LoadActions(cfg *config.Config) (actions []*Action, err error) {
|
||||
actionsDir, err := filepath.Abs(cfg.ActionsDir)
|
||||
if err != nil {
|
||||
@ -75,7 +117,7 @@ func LoadActions(cfg *config.Config) (actions []*Action, err error) {
|
||||
hash := sha512.Sum512([]byte(path))
|
||||
|
||||
// Parse content
|
||||
name, description, err := LoadAction(path)
|
||||
name, description, err := loadAction(path)
|
||||
if err != nil {
|
||||
log.Printf("Invalid action file (trying to parse %s): %s", path, err.Error())
|
||||
// Ignore invalid files
|
||||
@ -96,6 +138,7 @@ func LoadActions(cfg *config.Config) (actions []*Action, err error) {
|
||||
Description: description,
|
||||
Path: strings.TrimPrefix(path, actionsDir+"/"),
|
||||
Enabled: d.Mode().Perm()&0111 != 0,
|
||||
fullPath: path,
|
||||
})
|
||||
}
|
||||
|
||||
@ -130,3 +173,10 @@ func (a *Action) Disable() error {
|
||||
func (a *Action) Remove() error {
|
||||
return os.Remove(a.Path)
|
||||
}
|
||||
|
||||
func (a *Action) Launch(settings *Settings) (cmd *exec.Cmd, err error) {
|
||||
cmd = exec.Command(a.fullPath)
|
||||
cmd.Env = append(cmd.Environ(), fmt.Sprintf("LANG=%s", settings.Language))
|
||||
err = cmd.Start()
|
||||
return
|
||||
}
|
||||
|
120
model/alarm.go
120
model/alarm.go
@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.nemunai.re/nemunaire/reveil/config"
|
||||
)
|
||||
|
||||
type Date time.Time
|
||||
@ -38,33 +40,95 @@ func (h *Hour) UnmarshalJSON(src []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetNextAlarm(db *LevelDBStorage) (*time.Time, error) {
|
||||
func GetNextAlarm(cfg *config.Config, db *LevelDBStorage) (*time.Time, []Identifier, bool, error) {
|
||||
alarmsRepeated, err := GetAlarmsRepeated(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
var closestAlarm *time.Time
|
||||
var closestAlarmRoutines []Identifier
|
||||
var closestAlarmFederated bool
|
||||
for _, alarm := range alarmsRepeated {
|
||||
next := alarm.GetNextOccurence(db)
|
||||
next := alarm.GetNextOccurence(cfg, db)
|
||||
if next != nil && (closestAlarm == nil || closestAlarm.After(*next)) {
|
||||
closestAlarm = next
|
||||
closestAlarmRoutines = alarm.FollowingRoutines
|
||||
closestAlarmFederated = alarm.EnableFederation
|
||||
}
|
||||
}
|
||||
|
||||
alarmsSingle, err := GetAlarmsSingle(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, alarm := range alarmsSingle {
|
||||
if closestAlarm == nil || (closestAlarm.After(alarm.Time) && alarm.Time.After(now)) {
|
||||
closestAlarm = &alarm.Time
|
||||
closestAlarmRoutines = alarm.FollowingRoutines
|
||||
closestAlarmFederated = alarm.EnableFederation
|
||||
}
|
||||
}
|
||||
|
||||
return closestAlarm, nil
|
||||
return closestAlarm, closestAlarmRoutines, closestAlarmFederated, nil
|
||||
}
|
||||
|
||||
func GetNextException(cfg *config.Config, db *LevelDBStorage) (*time.Time, error) {
|
||||
alarmsExceptions, err := GetAlarmExceptions(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var closestException *time.Time
|
||||
for _, except := range alarmsExceptions {
|
||||
if except != nil && time.Time(*except.End).After(time.Now()) && (closestException == nil || closestException.After(time.Time(*except.Start))) {
|
||||
tmp := time.Time(*except.Start)
|
||||
closestException = &tmp
|
||||
}
|
||||
}
|
||||
|
||||
return closestException, nil
|
||||
}
|
||||
|
||||
func DropNextAlarm(cfg *config.Config, db *LevelDBStorage) error {
|
||||
timenext, _, _, err := GetNextAlarm(cfg, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
alarmsRepeated, err := GetAlarmsRepeated(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, alarm := range alarmsRepeated {
|
||||
next := alarm.GetNextOccurence(cfg, db)
|
||||
if next != nil && *next == *timenext {
|
||||
start := Date(*next)
|
||||
stop := Date((*next).Add(time.Second))
|
||||
|
||||
return PutAlarmException(db, &AlarmException{
|
||||
Start: &start,
|
||||
End: &stop,
|
||||
Comment: fmt.Sprintf("Automatic exception to cancel recurrent alarm %s", next.Format("Mon at 15:04")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
alarmsSingle, err := GetAlarmsSingle(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, alarm := range alarmsSingle {
|
||||
if alarm.Time == *timenext {
|
||||
return DeleteAlarmSingle(db, alarm)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Unable to find the next alarm")
|
||||
}
|
||||
|
||||
type Exceptions []time.Time
|
||||
@ -88,11 +152,13 @@ type AlarmRepeated struct {
|
||||
FollowingRoutines []Identifier `json:"routines"`
|
||||
IgnoreExceptions bool `json:"ignore_exceptions"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
Excepts Exceptions `json:"excepts,omitempty"`
|
||||
NextTime *time.Time `json:"next_time,omitempty"`
|
||||
EnableFederation bool `json:"enable_federation,omitempty"`
|
||||
}
|
||||
|
||||
func (a *AlarmRepeated) FillExcepts(db *LevelDBStorage) error {
|
||||
func (a *AlarmRepeated) FillExcepts(cfg *config.Config, db *LevelDBStorage) error {
|
||||
if a.IgnoreExceptions {
|
||||
return nil
|
||||
}
|
||||
@ -105,15 +171,15 @@ func (a *AlarmRepeated) FillExcepts(db *LevelDBStorage) error {
|
||||
now := time.Now()
|
||||
|
||||
for _, exception := range exceptions {
|
||||
if now.After(time.Time(*exception.Start)) {
|
||||
end := time.Time(*exception.End).AddDate(0, 0, 1)
|
||||
if now.After(end) {
|
||||
continue
|
||||
}
|
||||
|
||||
end := time.Time(*exception.End).AddDate(0, 0, 1)
|
||||
for t := time.Time(*exception.Start); end.After(t); t = t.AddDate(0, 0, 1) {
|
||||
if t.Weekday() == a.Weekday {
|
||||
a.Excepts = append(a.Excepts, time.Date(t.Year(), t.Month(), t.Day(), time.Time(*a.StartTime).Hour(), time.Time(*a.StartTime).Minute(), time.Time(*a.StartTime).Second(), 0, now.Location()))
|
||||
t.AddDate(0, 0, 6)
|
||||
a.Excepts = append(a.Excepts, time.Date(t.Year(), t.Month(), t.Day(), time.Time(*a.StartTime).Hour(), time.Time(*a.StartTime).Minute(), time.Time(*a.StartTime).Second(), 0, time.Local))
|
||||
t = t.AddDate(0, 0, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,14 +189,18 @@ func (a *AlarmRepeated) FillExcepts(db *LevelDBStorage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AlarmRepeated) GetNextOccurence(db *LevelDBStorage) *time.Time {
|
||||
func (a *AlarmRepeated) GetNextOccurence(cfg *config.Config, db *LevelDBStorage) *time.Time {
|
||||
if a.Disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a.Excepts) == 0 {
|
||||
a.FillExcepts(db)
|
||||
a.FillExcepts(cfg, db)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), time.Time(*a.StartTime).Hour(), time.Time(*a.StartTime).Minute(), time.Time(*a.StartTime).Second(), 0, now.Location())
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), time.Time(*a.StartTime).Hour(), time.Time(*a.StartTime).Minute(), time.Time(*a.StartTime).Second(), 0, time.Local)
|
||||
if now.After(today) {
|
||||
today = today.AddDate(0, 0, 1)
|
||||
}
|
||||
@ -178,16 +248,19 @@ func GetAlarmsRepeated(db *LevelDBStorage) (alarms []*AlarmRepeated, err error)
|
||||
|
||||
func PutAlarmRepeated(db *LevelDBStorage, alarm *AlarmRepeated) (err error) {
|
||||
var key string
|
||||
var id Identifier
|
||||
|
||||
if alarm.Id.IsEmpty() {
|
||||
var id Identifier
|
||||
|
||||
key, id, err = db.findBytesKey("alarm-repeated-", IDENTIFIER_LEN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alarm.Id = id
|
||||
} else {
|
||||
key = fmt.Sprintf("alarm-repeated-%s", alarm.Id.ToString())
|
||||
}
|
||||
|
||||
alarm.Id = id
|
||||
// Don't store this, this is autocalculated
|
||||
alarm.Excepts = nil
|
||||
alarm.NextTime = nil
|
||||
@ -204,6 +277,7 @@ type AlarmSingle struct {
|
||||
Time time.Time `json:"time"`
|
||||
FollowingRoutines []Identifier `json:"routines"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
EnableFederation bool `json:"enable_federation,omitempty"`
|
||||
}
|
||||
|
||||
func GetAlarmSingle(db *LevelDBStorage, id Identifier) (alarm *AlarmSingle, err error) {
|
||||
@ -232,17 +306,19 @@ func GetAlarmsSingle(db *LevelDBStorage) (alarms []*AlarmSingle, err error) {
|
||||
|
||||
func PutAlarmSingle(db *LevelDBStorage, alarm *AlarmSingle) (err error) {
|
||||
var key string
|
||||
var id Identifier
|
||||
|
||||
if alarm.Id.IsEmpty() {
|
||||
var id Identifier
|
||||
|
||||
key, id, err = db.findBytesKey("alarm-single-", IDENTIFIER_LEN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alarm.Id = id
|
||||
} else {
|
||||
key = fmt.Sprintf("alarm-single-%s", alarm.Id.ToString())
|
||||
}
|
||||
|
||||
alarm.Id = id
|
||||
|
||||
return db.put(key, alarm)
|
||||
}
|
||||
|
||||
@ -302,17 +378,19 @@ func GetAlarmExceptions(db *LevelDBStorage) (alarms []*AlarmException, err error
|
||||
|
||||
func PutAlarmException(db *LevelDBStorage, alarm *AlarmException) (err error) {
|
||||
var key string
|
||||
var id Identifier
|
||||
|
||||
if alarm.Id.IsEmpty() {
|
||||
var id Identifier
|
||||
|
||||
key, id, err = db.findBytesKey("alarm-exception-", IDENTIFIER_LEN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alarm.Id = id
|
||||
} else {
|
||||
key = fmt.Sprintf("alarm-exception-%s", alarm.Id.ToString())
|
||||
}
|
||||
|
||||
alarm.Id = id
|
||||
|
||||
return db.put(key, alarm)
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ func NewLevelDBStorage(path string) (s *LevelDBStorage, err error) {
|
||||
db, err = leveldb.OpenFile(path, nil)
|
||||
if err != nil {
|
||||
if _, ok := err.(*errors.ErrCorrupted); ok {
|
||||
log.Println("LevelDB was corrupted; attempting recovery (%s)", err.Error())
|
||||
log.Printf("LevelDB was corrupted; attempting recovery (%s)", err.Error())
|
||||
_, err = leveldb.RecoverFile(path, nil)
|
||||
if err != nil {
|
||||
return
|
||||
|
137
model/federation.go
Normal file
137
model/federation.go
Normal file
@ -0,0 +1,137 @@
|
||||
package reveil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type FederationServer struct {
|
||||
URL string `json:"url"`
|
||||
Delay uint `json:"delay"`
|
||||
}
|
||||
|
||||
func (srv *FederationServer) WakeUp(seed int64) error {
|
||||
req := map[string]interface{}{"seed": seed}
|
||||
req_enc, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := http.Post(srv.URL+"/api/federation/wakeup", "application/json", bytes.NewBuffer(req_enc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *FederationServer) WakeStop() error {
|
||||
res, err := http.Post(srv.URL+"/api/federation/wakeok", "application/json", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *FederationServer) GetMusics() ([]Track, error) {
|
||||
res, err := http.Get(srv.URL + "/api/tracks")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var tracks []Track
|
||||
err = json.NewDecoder(res.Body).Decode(&tracks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
func (srv *FederationServer) UpdateTrack(t *Track) error {
|
||||
req_enc, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", srv.URL+"/api/tracks/"+t.Id.ToString(), bytes.NewBuffer(req_enc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode == http.StatusOK {
|
||||
var track Track
|
||||
err = json.NewDecoder(res.Body).Decode(&track)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var errmsg map[string]string
|
||||
err = json.NewDecoder(res.Body).Decode(&errmsg)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
return fmt.Errorf("%s", errmsg["errmsg"])
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *FederationServer) SendTrack(track *Track) error {
|
||||
// Retrieve file
|
||||
fd, err := track.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
var b bytes.Buffer
|
||||
w := multipart.NewWriter(&b)
|
||||
|
||||
var fw io.Writer
|
||||
// Add an image file
|
||||
if fw, err = w.CreateFormFile("trackfile", fd.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = io.Copy(fw, fd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Close()
|
||||
|
||||
//
|
||||
req, err := http.NewRequest("POST", srv.URL+"/api/tracks", &b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", w.FormDataContentType())
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", res.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package reveil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@ -9,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.nemunai.re/nemunaire/reveil/config"
|
||||
)
|
||||
@ -19,6 +22,10 @@ type RoutineStep struct {
|
||||
Args []string `json:"args,omitempty"`
|
||||
}
|
||||
|
||||
func (s *RoutineStep) GetAction(cfg *config.Config) (*Action, error) {
|
||||
return LoadAction(cfg, s.Action)
|
||||
}
|
||||
|
||||
type Routine struct {
|
||||
Id Identifier `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@ -72,6 +79,21 @@ func LoadRoutine(path string, cfg *config.Config) ([]RoutineStep, error) {
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func LoadRoutineFromId(id Identifier, cfg *config.Config) (*Routine, error) {
|
||||
routines, err := LoadRoutines(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, routine := range routines {
|
||||
if bytes.Equal(routine.Id, id) {
|
||||
return routine, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unable to find routine %x", id)
|
||||
}
|
||||
|
||||
func LoadRoutines(cfg *config.Config) (routines []*Routine, err error) {
|
||||
err = filepath.Walk(cfg.RoutinesDir, func(path string, d fs.FileInfo, err error) error {
|
||||
if d.IsDir() && path != cfg.RoutinesDir {
|
||||
@ -117,3 +139,34 @@ func (r *Routine) Rename(newName string) error {
|
||||
func (a *Routine) Remove() error {
|
||||
return os.Remove(a.Path)
|
||||
}
|
||||
|
||||
func (a *Routine) Launch(cfg *config.Config) error {
|
||||
for _, s := range a.Steps {
|
||||
act, err := s.GetAction(cfg)
|
||||
if err != nil {
|
||||
log.Printf("Unable to get action: %s: %s", s.Action, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
settings, err := ReadSettings(cfg.SettingsFile)
|
||||
if err != nil {
|
||||
log.Printf("Unable to read settings: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(s.Delay) * time.Second)
|
||||
|
||||
cmd, err := act.Launch(settings)
|
||||
if err != nil {
|
||||
log.Printf("Unable to launch the action %q: %s", s.Action, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
log.Printf("Something goes wrong when waiting for the action %q's end: %s", s.Action, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -8,11 +8,15 @@ import (
|
||||
|
||||
// Settings represents the settings panel.
|
||||
type Settings struct {
|
||||
Language string `json:"language"`
|
||||
GongInterval time.Duration `json:"gong_interval"`
|
||||
WeatherDelay time.Duration `json:"weather_delay"`
|
||||
WeatherAction string `json:"weather_action"`
|
||||
MaxRunTime time.Duration `json:"max_run_time"`
|
||||
Language string `json:"language"`
|
||||
GongInterval time.Duration `json:"gong_interval"`
|
||||
WeatherDelay time.Duration `json:"weather_delay"`
|
||||
WeatherAction string `json:"weather_action"`
|
||||
PreAlarmActionDelay time.Duration `json:"pre_alarm_delay"`
|
||||
PreAlarmAction string `json:"pre_alarm_action"`
|
||||
MaxRunTime time.Duration `json:"max_run_time"`
|
||||
MaxVolume uint16 `json:"max_volume"`
|
||||
Federation map[string]FederationServer `json:"federation"`
|
||||
}
|
||||
|
||||
// ExistsSettings checks if the settings file can by found at the given path.
|
||||
|
29
player/federation.go
Normal file
29
player/federation.go
Normal file
@ -0,0 +1,29 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.nemunai.re/nemunaire/reveil/model"
|
||||
)
|
||||
|
||||
func FederatedWakeUp(k string, srv reveil.FederationServer, seed int64) {
|
||||
if srv.Delay == 0 {
|
||||
err := srv.WakeUp(seed)
|
||||
if err != nil {
|
||||
log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error())
|
||||
} else {
|
||||
log.Printf("Federated wakeup on %s: launched!", k)
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
time.Sleep(time.Duration(srv.Delay) * time.Millisecond)
|
||||
err := srv.WakeUp(seed)
|
||||
if err != nil {
|
||||
log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error())
|
||||
} else {
|
||||
log.Printf("Federated wakeup on %s: launched!", k)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
113
player/player.go
113
player/player.go
@ -21,15 +21,21 @@ var CommonPlayer *Player
|
||||
type Player struct {
|
||||
Playlist []string
|
||||
MaxRunTime time.Duration
|
||||
MaxVolume uint16
|
||||
Stopper chan bool
|
||||
currentCmd *exec.Cmd
|
||||
currentCmdCh chan bool
|
||||
|
||||
weatherTime time.Duration
|
||||
weatherAction *reveil.Action
|
||||
|
||||
claironTime time.Duration
|
||||
claironFile string
|
||||
|
||||
endRoutines []*reveil.Routine
|
||||
|
||||
ntick int64
|
||||
hasClaironed bool
|
||||
hasSpokeWeather bool
|
||||
launched time.Time
|
||||
volume uint16
|
||||
dontUpdateVolume bool
|
||||
@ -37,38 +43,74 @@ type Player struct {
|
||||
playedItem int
|
||||
}
|
||||
|
||||
func WakeUp(cfg *config.Config) (err error) {
|
||||
func WakeUp(cfg *config.Config, routine []reveil.Identifier, federated bool) (err error) {
|
||||
if CommonPlayer != nil {
|
||||
return fmt.Errorf("Unable to start the player: a player is already running")
|
||||
}
|
||||
|
||||
seed := time.Now().Unix()
|
||||
seed -= seed % 172800
|
||||
|
||||
if federated {
|
||||
settings, err := reveil.ReadSettings(cfg.SettingsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to read settings: %w", err)
|
||||
}
|
||||
|
||||
for k, srv := range settings.Federation {
|
||||
FederatedWakeUp(k, srv, seed)
|
||||
}
|
||||
}
|
||||
|
||||
return WakeUpFromFederation(cfg, seed, routine)
|
||||
}
|
||||
|
||||
func WakeUpFromFederation(cfg *config.Config, seed int64, routine []reveil.Identifier) (err error) {
|
||||
rand.Seed(seed)
|
||||
|
||||
CommonPlayer, err = NewPlayer(cfg)
|
||||
CommonPlayer, err = NewPlayer(cfg, routine)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go CommonPlayer.WakeUp()
|
||||
go CommonPlayer.WakeUp(cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPlayer(cfg *config.Config) (*Player, error) {
|
||||
func NewPlayer(cfg *config.Config, routines []reveil.Identifier) (*Player, error) {
|
||||
// Load our settings
|
||||
settings, err := reveil.ReadSettings(cfg.SettingsFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to read settings: %w", err)
|
||||
}
|
||||
|
||||
// Load weather action
|
||||
wact, err := reveil.LoadAction(cfg, settings.WeatherAction)
|
||||
if err != nil {
|
||||
log.Println("Unable to load weather action:", err.Error())
|
||||
}
|
||||
|
||||
p := Player{
|
||||
Stopper: make(chan bool, 1),
|
||||
currentCmdCh: make(chan bool, 1),
|
||||
MaxRunTime: settings.MaxRunTime * time.Minute,
|
||||
claironTime: settings.GongInterval * time.Minute,
|
||||
claironFile: reveil.CurrentGongPath(cfg),
|
||||
reverseOrder: int(time.Now().Unix()/86400)%2 == 0,
|
||||
Stopper: make(chan bool, 1),
|
||||
currentCmdCh: make(chan bool, 1),
|
||||
MaxRunTime: settings.MaxRunTime * time.Minute,
|
||||
MaxVolume: uint16(settings.MaxVolume),
|
||||
weatherTime: settings.WeatherDelay * time.Minute,
|
||||
weatherAction: wact,
|
||||
claironTime: settings.GongInterval * time.Minute,
|
||||
claironFile: reveil.CurrentGongPath(cfg),
|
||||
reverseOrder: int(time.Now().Unix()/86400)%2 == 0,
|
||||
}
|
||||
|
||||
// Load routines
|
||||
for _, routine := range routines {
|
||||
r, err := reveil.LoadRoutineFromId(routine, cfg)
|
||||
if err != nil {
|
||||
log.Printf("Unable to load routine %x: %s", routine, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
p.endRoutines = append(p.endRoutines, r)
|
||||
}
|
||||
|
||||
// Load our track list
|
||||
@ -77,8 +119,6 @@ func NewPlayer(cfg *config.Config) (*Player, error) {
|
||||
return nil, fmt.Errorf("Unable to load tracks: %w", err)
|
||||
}
|
||||
|
||||
var playlist []string
|
||||
|
||||
// Creating playlist
|
||||
log.Println("Loading playlist...")
|
||||
for _, track := range tracks {
|
||||
@ -91,15 +131,30 @@ func NewPlayer(cfg *config.Config) (*Player, error) {
|
||||
|
||||
log.Println("Shuffling playlist...")
|
||||
// Shuffle the playlist
|
||||
rand.Shuffle(len(playlist), func(i, j int) {
|
||||
playlist[i], playlist[j] = playlist[j], playlist[i]
|
||||
rand.Shuffle(len(p.Playlist), func(i, j int) {
|
||||
p.Playlist[i], p.Playlist[j] = p.Playlist[j], p.Playlist[i]
|
||||
})
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (p *Player) launchAction(cfg *config.Config, a *reveil.Action) (err error) {
|
||||
settings, err := reveil.ReadSettings(cfg.SettingsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read settings: %w", err)
|
||||
}
|
||||
|
||||
p.currentCmd, err = a.Launch(settings)
|
||||
log.Println("Running action ", a.Name)
|
||||
|
||||
err = p.currentCmd.Wait()
|
||||
p.currentCmdCh <- true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Player) playFile(filepath string) (err error) {
|
||||
p.currentCmd = exec.Command("paplay", filepath)
|
||||
p.currentCmd = exec.Command(playCommand, filepath)
|
||||
if err = p.currentCmd.Start(); err != nil {
|
||||
log.Println("Running paplay err: ", err.Error())
|
||||
p.currentCmdCh <- true
|
||||
@ -114,7 +169,7 @@ func (p *Player) playFile(filepath string) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Player) WakeUp() {
|
||||
func (p *Player) WakeUp(cfg *config.Config) {
|
||||
log.Println("Playlist in use:", strings.Join(p.Playlist, " ; "))
|
||||
|
||||
// Prepare sound player
|
||||
@ -135,15 +190,20 @@ loop:
|
||||
for {
|
||||
select {
|
||||
case <-p.currentCmdCh:
|
||||
if !p.hasClaironed && time.Since(p.launched) >= p.claironTime {
|
||||
if time.Since(p.launched) >= p.claironTime {
|
||||
log.Println("clairon time!")
|
||||
p.claironTime += p.claironTime / 2
|
||||
p.SetVolume(65535)
|
||||
p.dontUpdateVolume = true
|
||||
go p.playFile(p.claironFile)
|
||||
} else if p.weatherAction != nil && !p.hasSpokeWeather && time.Since(p.launched) >= p.weatherTime {
|
||||
log.Println("weather time!")
|
||||
p.dontUpdateVolume = true
|
||||
p.hasSpokeWeather = true
|
||||
go p.launchAction(cfg, p.weatherAction)
|
||||
} else {
|
||||
p.dontUpdateVolume = false
|
||||
p.volume = 3500 + uint16(math.Log(1+float64(p.ntick)/8)*9500)
|
||||
p.volume = uint16(math.Log(1+float64(p.ntick)/8) * 9500)
|
||||
p.SetVolume(p.volume)
|
||||
|
||||
if p.reverseOrder {
|
||||
@ -186,10 +246,10 @@ loop:
|
||||
|
||||
// Calm down music
|
||||
loopcalm:
|
||||
for i := 0; i < 128 && p.volume >= 15000; i += 1 {
|
||||
for i := 0; i < 128 && p.volume >= 768; i += 1 {
|
||||
timer := time.NewTimer(40 * time.Millisecond)
|
||||
|
||||
p.volume -= 256
|
||||
p.volume -= 768
|
||||
p.SetVolume(p.volume)
|
||||
|
||||
select {
|
||||
@ -214,6 +274,11 @@ loopcalm:
|
||||
|
||||
CommonPlayer = nil
|
||||
}
|
||||
|
||||
// TODO: Start Routine if any
|
||||
for _, r := range p.endRoutines {
|
||||
go r.Launch(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) NextTrack() {
|
||||
@ -223,7 +288,11 @@ func (p *Player) NextTrack() {
|
||||
}
|
||||
|
||||
func (p *Player) SetVolume(volume uint16) error {
|
||||
cmd := exec.Command("amixer", "-D", "pulse", "set", "Master", fmt.Sprintf("%d", volume))
|
||||
if p.MaxVolume == 0 {
|
||||
p.MaxVolume = 65535
|
||||
}
|
||||
|
||||
cmd := exec.Command("amixer", "-D", mixerCard, "set", mixerName, fmt.Sprintf("%d", uint32(volume)*uint32(p.MaxVolume)/65535))
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
|
9
player/player_pulse.go
Normal file
9
player/player_pulse.go
Normal file
@ -0,0 +1,9 @@
|
||||
//go:build pulse
|
||||
|
||||
package player
|
||||
|
||||
const (
|
||||
playCommand = "paplay"
|
||||
mixerCard = "pulse"
|
||||
mixerName = "Master"
|
||||
)
|
@ -1,5 +1,15 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
"local>iac/renovate-config",
|
||||
"local>iac/renovate-config//automerge-common"
|
||||
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"automerge": true
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": ["alpine", "github.com/gin-gonic/gin"],
|
||||
|
1
ui/.gitignore
vendored
1
ui/.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/.vite
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
|
152
ui/nojs.go
Normal file
152
ui/nojs.go
Normal file
@ -0,0 +1,152 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.nemunai.re/nemunaire/reveil/config"
|
||||
"git.nemunai.re/nemunaire/reveil/model"
|
||||
"git.nemunai.re/nemunaire/reveil/player"
|
||||
)
|
||||
|
||||
//go:embed nojs_templates/*
|
||||
var nojs_tpl embed.FS
|
||||
|
||||
func DeclareNoJSRoutes(router *gin.Engine, cfg *config.Config, db *reveil.LevelDBStorage, resetTimer func()) {
|
||||
templ := template.Must(template.New("").ParseFS(nojs_tpl, "nojs_templates/*.tmpl"))
|
||||
router.SetHTMLTemplate(templ)
|
||||
|
||||
router.GET("/nojs.html", func(c *gin.Context) {
|
||||
alarm, _, _, err := reveil.GetNextAlarm(cfg, db)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
defaultAlarm := time.Now().Add(460 * time.Minute)
|
||||
|
||||
if alarm == nil {
|
||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"defaultAlarm": defaultAlarm.Format("15:04"),
|
||||
"noAlarm": true,
|
||||
"isPlaying": player.CommonPlayer != nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
nCycles := int(time.Until(*alarm) / (90 * time.Minute))
|
||||
nDays := int(time.Until(*alarm) / (24 * time.Hour))
|
||||
nMinutes := int((time.Until(*alarm) / time.Minute) % 90)
|
||||
|
||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"defaultAlarm": defaultAlarm.Format("15:04"),
|
||||
"noAlarm": false,
|
||||
"nextAlarmDate": alarm.Format("Mon 2"),
|
||||
"nextAlarmTime": alarm.Format("15:04"),
|
||||
"sameDay": time.Now().Day() == alarm.Day(),
|
||||
"nCycles": nCycles,
|
||||
"nDays": nDays,
|
||||
"nMinutes": nMinutes,
|
||||
"isPlaying": player.CommonPlayer != nil,
|
||||
})
|
||||
})
|
||||
|
||||
router.POST("/nojs.html", func(c *gin.Context) {
|
||||
var form struct {
|
||||
Action string `form:"action"`
|
||||
Time *string `form:"time"`
|
||||
}
|
||||
c.Bind(&form)
|
||||
|
||||
switch form.Action {
|
||||
case "new":
|
||||
if form.Time == nil {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"errmsg": "This time is invalid."})
|
||||
return
|
||||
}
|
||||
|
||||
alarm := time.Now()
|
||||
|
||||
if len(*form.Time) == 2 && (*form.Time)[1] == 'c' {
|
||||
n, err := strconv.Atoi((*form.Time)[:1])
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"errmsg": fmt.Sprintf("This number of cycle is invalid: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
alarm = alarm.Add((time.Duration(90*n) + 10) * time.Minute)
|
||||
} else {
|
||||
tmp := strings.Split(*form.Time, ":")
|
||||
if len(tmp) != 2 {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"errmsg": "This time is invalid."})
|
||||
return
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(fmt.Sprintf("%sh%sm", tmp[0], tmp[1]))
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"errmsg": fmt.Sprintf("This time is invalid: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
alarm = alarm.Local().Truncate(24 * time.Hour)
|
||||
alarm = alarm.Add(-time.Duration(alarm.Hour())*time.Hour + duration)
|
||||
}
|
||||
|
||||
if time.Now().After(alarm) {
|
||||
alarm = alarm.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
if err := reveil.PutAlarmSingle(db, &reveil.AlarmSingle{
|
||||
Time: alarm,
|
||||
}); err != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
resetTimer()
|
||||
|
||||
case "cancel":
|
||||
err := reveil.DropNextAlarm(cfg, db)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
resetTimer()
|
||||
|
||||
case "start":
|
||||
if player.CommonPlayer == nil {
|
||||
err := player.WakeUp(cfg, nil, true)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"errmsg": "Player already running"})
|
||||
return
|
||||
}
|
||||
|
||||
case "nexttrack":
|
||||
if player.CommonPlayer != nil {
|
||||
player.CommonPlayer.NextTrack()
|
||||
}
|
||||
|
||||
case "stop":
|
||||
if player.CommonPlayer != nil {
|
||||
err := player.CommonPlayer.Stop()
|
||||
if err != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/nojs.html")
|
||||
})
|
||||
}
|
4
ui/nojs_templates/error.tmpl
Normal file
4
ui/nojs_templates/error.tmpl
Normal file
@ -0,0 +1,4 @@
|
||||
<h1>
|
||||
Une erreur inattendue s'est produite :
|
||||
{{ .errmsg }}
|
||||
</h1>
|
81
ui/nojs_templates/index.tmpl
Normal file
81
ui/nojs_templates/index.tmpl
Normal file
@ -0,0 +1,81 @@
|
||||
<h1 style="margin-bottom: 0">
|
||||
Prochain réveil le :
|
||||
{{ if .noAlarm}}
|
||||
aucun défini
|
||||
{{ else }}
|
||||
{{ if .sameDay }}
|
||||
aujourd'hui
|
||||
{{ else if lt .nCycles 16 }}
|
||||
demain
|
||||
{{ else }}
|
||||
{{ .nextAlarmDate }}
|
||||
{{ end }}
|
||||
à {{ .nextAlarmTime }}
|
||||
{{ end }}
|
||||
</h1>
|
||||
{{ if not .noAlarm }}
|
||||
<h2 style="color: gray; margin-top: 0; margin-left: 5em">
|
||||
{{ if gt .nDays 2 }}(dans {{ .nDays }} jours){{ else }}(dans {{ .nCycles }} cycles + {{ .nMinutes }} min){{ end }}
|
||||
</h2>
|
||||
{{ end }}
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
{{ if not .noAlarm }}
|
||||
<form method="post" action="/nojs.html">
|
||||
<input type="hidden" name="action" value="cancel">
|
||||
<button type="submit">
|
||||
Annuler la prochaine alarme
|
||||
</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
{{ if .isPlaying }}
|
||||
<form method="post" action="/nojs.html">
|
||||
<input type="hidden" name="action" value="stop">
|
||||
<button type="submit">
|
||||
Arrêter le réveil
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/nojs.html">
|
||||
<input type="hidden" name="action" value="nexttrack">
|
||||
<button type="submit">
|
||||
Prochaine musique
|
||||
</button>
|
||||
</form>
|
||||
{{ else }}
|
||||
<form method="post" action="/nojs.html">
|
||||
<input type="hidden" name="action" value="start">
|
||||
<button type="submit">
|
||||
Lancer le réveil
|
||||
</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<h3>Programmer une nouvelle alarme</h3>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<form method="post" action="/nojs.html">
|
||||
<input type="hidden" name="action" value="new">
|
||||
<input type="text" required name="time" placeholder="00:00" value="{{ .defaultAlarm }}">
|
||||
<button type="submit">
|
||||
Nouvelle alarme
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/nojs.html">
|
||||
<input type="hidden" name="action" value="new">
|
||||
<input type="hidden" name="time" value="5c">
|
||||
<button type="submit">
|
||||
+ 5 cycles
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/nojs.html">
|
||||
<input type="hidden" name="action" value="new">
|
||||
<input type="hidden" name="time" value="6c">
|
||||
<button type="submit">
|
||||
+ 6 cycles
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
18
ui/nojs_templates/single.tmpl
Normal file
18
ui/nojs_templates/single.tmpl
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" class="d-flex flex-column mh-100 h-100">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="#ffffff"/>
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/img/apple-touch-icon.png">
|
||||
<meta name="author" content="nemucorp">
|
||||
<meta name="robots" content="none">
|
||||
<base href="/">
|
||||
</head>
|
||||
<body class="flex-fill d-flex flex-column">
|
||||
<div class="flex-fill d-flex flex-column justify-content-between" style="min-height: 100%">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
3825
ui/package-lock.json
generated
Normal file
3825
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -12,31 +12,31 @@
|
||||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^1.0.0-next.18",
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.26",
|
||||
"@sveltejs/kit": "^1.0.0-next.260",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"bootstrap-icons": "^1.8.0",
|
||||
"bootswatch": "^5.1.3",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"prettier": "^2.4.1",
|
||||
"prettier-plugin-svelte": "^2.6.0",
|
||||
"svelte": "^3.46.4",
|
||||
"svelte-check": "^2.4.2",
|
||||
"svelte-preprocess": "^4.10.2",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-svelte": "^2.33.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-preprocess": "^6.0.0",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.5.5"
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.5",
|
||||
"sass": "^1.49.7",
|
||||
"sass-loader": "^13.0.0",
|
||||
"sveltestrap": "^5.8.3",
|
||||
"vite": "^3.0.0"
|
||||
"sass-loader": "^16.0.0",
|
||||
"@sveltestrap/sveltestrap": "^6.0.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -58,8 +58,10 @@ func DeclareRoutes(router *gin.Engine, cfg *config.Config) {
|
||||
router.GET("/.svelte-kit/*_", serveOrReverse("", cfg))
|
||||
router.GET("/node_modules/*_", serveOrReverse("", cfg))
|
||||
router.GET("/@vite/*_", serveOrReverse("", cfg))
|
||||
router.GET("/@id/*_", serveOrReverse("", cfg))
|
||||
router.GET("/@fs/*_", serveOrReverse("", cfg))
|
||||
router.GET("/src/*_", serveOrReverse("", cfg))
|
||||
router.GET("/home/*_", serveOrReverse("", cfg))
|
||||
}
|
||||
|
||||
router.GET("/", serveOrReverse("", cfg))
|
||||
|
@ -14,6 +14,7 @@
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body class="flex-fill d-flex flex-column">
|
||||
<noscript>Si la page ne charge pas, essayez <a href="/nojs.html">la version sans JavaScript</a>.</noscript>
|
||||
<div class="flex-fill d-flex flex-column justify-content-between" style="min-height: 100%">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
22
ui/src/lib/Toaster.svelte
Normal file
22
ui/src/lib/Toaster.svelte
Normal file
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import {
|
||||
Toast,
|
||||
ToastBody,
|
||||
ToastHeader,
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { ToastsStore } from '$lib/stores/toasts';
|
||||
</script>
|
||||
|
||||
<div class="toast-container position-absolute top-0 end-0 p-3">
|
||||
{#each $ToastsStore.toasts as toast}
|
||||
<Toast>
|
||||
<ToastHeader toggle={toast.close} icon={toast.color}>
|
||||
{#if toast.title}{toast.title}{:else}Réveil{/if}
|
||||
</ToastHeader>
|
||||
<ToastBody>
|
||||
{toast.msg}
|
||||
</ToastBody>
|
||||
</Toast>
|
||||
{/each}
|
||||
</div>
|
@ -25,6 +25,18 @@ export class Action {
|
||||
}
|
||||
}
|
||||
|
||||
async launch() {
|
||||
const res = await fetch(`api/actions/${this.id}/run`, {
|
||||
method: 'POST',
|
||||
headers: {'Accept': 'application/json'}
|
||||
});
|
||||
if (res.status == 200) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
toggleEnable() {
|
||||
this.enabled = !this.enabled;
|
||||
this.save();
|
||||
@ -50,7 +62,12 @@ export class Action {
|
||||
export async function getActions() {
|
||||
const res = await fetch(`api/actions`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).map((t) => new Action(t));
|
||||
const data = await res.json();
|
||||
if (data == null) {
|
||||
return []
|
||||
} else {
|
||||
return data.map((t) => new Action(t));
|
||||
}
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
|
@ -33,6 +33,18 @@ export async function alarmNextTrack() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteNextAlarm() {
|
||||
const res = await fetch('api/alarms/next', {
|
||||
method: 'DELETE',
|
||||
headers: {'Accept': 'application/json'},
|
||||
});
|
||||
if (res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
export async function alarmStop() {
|
||||
const res = await fetch('api/alarm', {
|
||||
method: 'DELETE',
|
||||
|
@ -5,15 +5,23 @@ export class AlarmRepeated {
|
||||
}
|
||||
}
|
||||
|
||||
update({ id, weekday, time, routines, ignore_exceptions, comment, excepts, next_time }) {
|
||||
update({ id, weekday, time, routines, disabled, ignore_exceptions, comment, excepts, next_time, enable_federation }) {
|
||||
this.id = id;
|
||||
this.weekday = weekday;
|
||||
this.time = time;
|
||||
this.routines = routines;
|
||||
this.routines = routines == null ? [] : routines;
|
||||
this.ignore_exceptions = ignore_exceptions;
|
||||
this.comment = comment;
|
||||
this.excepts = excepts;
|
||||
this.next_time = next_time;
|
||||
this.disabled = disabled == true;
|
||||
this.enable_federation = enable_federation == true;
|
||||
if (excepts !== undefined)
|
||||
this.excepts = excepts;
|
||||
if (next_time !== undefined)
|
||||
this.next_time = next_time;
|
||||
|
||||
if (this.routines.length < 1) {
|
||||
this.routines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
|
@ -5,11 +5,16 @@ export class AlarmSingle {
|
||||
}
|
||||
}
|
||||
|
||||
update({ id, time, routines, comment }) {
|
||||
update({ id, time, routines, comment, enable_federation }) {
|
||||
this.id = id;
|
||||
this.time = new Date(time);
|
||||
this.routines = routines;
|
||||
this.routines = routines == null ? [] : routines;
|
||||
this.comment = comment;
|
||||
this.enable_federation = enable_federation == true;
|
||||
|
||||
if (this.routines.length < 1) {
|
||||
this.routines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
|
@ -5,7 +5,7 @@
|
||||
Button,
|
||||
Icon,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { actions } from '$lib/stores/actions';
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
Button,
|
||||
Icon,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import DateRangeFormat from '$lib/components/DateRangeFormat.svelte';
|
||||
import { alarmsExceptions } from '$lib/stores/alarmexceptions';
|
||||
|
@ -5,7 +5,7 @@
|
||||
Button,
|
||||
Icon,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { weekdayStr } from '$lib/alarmrepeated';
|
||||
import { alarmsRepeated } from '$lib/stores/alarmrepeated';
|
||||
@ -35,6 +35,8 @@
|
||||
href="alarms/repeated/{alarm.id}"
|
||||
class="list-group-item list-group-item-action"
|
||||
class:active={$page.params.kind === "repeated" && $page.params.aid === alarm.id}
|
||||
class:text-muted={alarm.disabled}
|
||||
style:text-decoration={alarm.disabled?"line-through":null}
|
||||
>
|
||||
Les {weekdayStr(alarm.weekday)}s à {alarm.time}
|
||||
</a>
|
||||
|
@ -5,7 +5,7 @@
|
||||
Button,
|
||||
Icon,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import DateFormat from '$lib/components/DateFormat.svelte';
|
||||
import { alarmsSingle } from '$lib/stores/alarmsingle';
|
||||
|
@ -11,7 +11,7 @@
|
||||
ListGroupItem,
|
||||
Row,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { actions_idx } from '$lib/stores/actions';
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
color="outline-danger"
|
||||
size="sm"
|
||||
class="float-end ms-1"
|
||||
on:click={() => routine.delete()}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
@ -37,6 +38,14 @@
|
||||
>
|
||||
<Icon name="pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
color="outline-success"
|
||||
size="sm"
|
||||
class="float-end ms-1"
|
||||
on:click={() => routine.launch()}
|
||||
>
|
||||
<Icon name="play-fill" />
|
||||
</Button>
|
||||
{routine.name}
|
||||
</CardHeader>
|
||||
{#if routine.steps}
|
||||
|
@ -5,7 +5,7 @@
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
export let awakingList = [
|
||||
{
|
||||
|
@ -6,7 +6,7 @@
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
export let routinesStats = [
|
||||
{
|
||||
|
@ -4,7 +4,7 @@
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import {
|
||||
Input,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
export let format = 'YYYY-MM-DD HH:mm';
|
||||
export let date = new Date();
|
||||
|
134
ui/src/lib/components/FederationSettings.svelte
Normal file
134
ui/src/lib/components/FederationSettings.svelte
Normal file
@ -0,0 +1,134 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Icon,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Row,
|
||||
Spinner,
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let id = "";
|
||||
export let value = { };
|
||||
|
||||
function changeKey(bak, to) {
|
||||
if (bak === null && to.target.value) {
|
||||
if (!value) value = { };
|
||||
value[to.target.value] = { url:"" };
|
||||
to.target.value = "";
|
||||
value = value;
|
||||
} else {
|
||||
value[to.target.value] = value[bak];
|
||||
delete value[bak];
|
||||
}
|
||||
}
|
||||
|
||||
const syncInProgress = { };
|
||||
async function syncMusic(srv) {
|
||||
syncInProgress[srv] = true;
|
||||
|
||||
const res = await fetch(`api/federation/${srv}/sync`, {
|
||||
method: 'POST',
|
||||
headers: {'Accept': 'application/json'}
|
||||
});
|
||||
if (res.status != 200) {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
|
||||
syncInProgress[srv] = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#each Object.keys(value) as key}
|
||||
<Row class="mb-3">
|
||||
<Col>
|
||||
<Input
|
||||
type="string"
|
||||
value={key}
|
||||
on:change={() => dispatch("input")}
|
||||
on:input={(e) => changeKey(key, e)}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="string"
|
||||
placeholder="https://reveil.fr/"
|
||||
bind:value={value[key].url}
|
||||
on:change={() => dispatch("input")}
|
||||
/>
|
||||
<Button
|
||||
href={value[key].url}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon name="globe" />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col>
|
||||
<Row>
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="60"
|
||||
bind:value={value[key].delay}
|
||||
on:change={() => dispatch("input")}
|
||||
/>
|
||||
<InputGroupText>ms</InputGroupText>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<Button
|
||||
color="info"
|
||||
disabled={syncInProgress[key]}
|
||||
title="Synchroniser les musiques"
|
||||
type="button"
|
||||
on:click={() => syncMusic(key)}
|
||||
>
|
||||
{#if syncInProgress[key]}
|
||||
<Spinner size="sm" />
|
||||
{:else}
|
||||
<Icon name="music-note-list" />
|
||||
{/if}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{/each}
|
||||
{/if}
|
||||
<Row>
|
||||
<Col>
|
||||
<Input
|
||||
type="string"
|
||||
{id}
|
||||
placeholder="name"
|
||||
value=""
|
||||
on:input={(e) => changeKey(null, e)}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Input
|
||||
type="string"
|
||||
placeholder="https://reveil.fr/"
|
||||
disabled
|
||||
value=""
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="60"
|
||||
disabled
|
||||
value=""
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
@ -6,7 +6,7 @@
|
||||
Button,
|
||||
Icon,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { gongs } from '$lib/stores/gongs';
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
Nav,
|
||||
NavItem,
|
||||
NavLink,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
const version = fetch('api/version', {headers: {'Accept': 'application/json'}}).then((res) => res.json())
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
Toast,
|
||||
ToastBody,
|
||||
ToastHeader,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { ToastsStore } from '$lib/stores/toasts';
|
||||
</script>
|
||||
|
@ -6,7 +6,7 @@
|
||||
Button,
|
||||
Icon,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { tracks } from '$lib/stores/tracks';
|
||||
|
||||
|
@ -26,6 +26,18 @@ export class Routine {
|
||||
}
|
||||
}
|
||||
|
||||
async launch() {
|
||||
const res = await fetch(`api/routines/${this.id}/run`, {
|
||||
method: 'POST',
|
||||
headers: {'Accept': 'application/json'}
|
||||
});
|
||||
if (res.status == 200) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
const res = await fetch(this.id?`api/routines/${this.id}`:'api/routines', {
|
||||
method: this.id?'PUT':'POST',
|
||||
@ -45,7 +57,12 @@ export class Routine {
|
||||
export async function getRoutines() {
|
||||
const res = await fetch(`api/routines`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).map((r) => new Routine(r));
|
||||
const data = await res.json();
|
||||
if (data == null) {
|
||||
return []
|
||||
} else {
|
||||
return data.map((r) => new Routine(r));
|
||||
}
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
|
@ -5,12 +5,16 @@ export class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
update({ language, gong_interval, weather_delay, weather_action, max_run_time }) {
|
||||
update({ language, gong_interval, weather_delay, weather_action, pre_alarm_delay, pre_alarm_action, max_run_time, max_volume, federation }) {
|
||||
this.language = language;
|
||||
this.gong_interval = gong_interval;
|
||||
this.weather_delay = weather_delay;
|
||||
this.weather_action = weather_action;
|
||||
this.pre_alarm_delay = pre_alarm_delay;
|
||||
this.pre_alarm_action = pre_alarm_action;
|
||||
this.max_run_time = max_run_time;
|
||||
this.max_volume = max_volume;
|
||||
this.federation = federation;
|
||||
}
|
||||
|
||||
async save() {
|
||||
|
@ -2,6 +2,18 @@ import { writable } from 'svelte/store';
|
||||
|
||||
import { getTracks } from '$lib/track'
|
||||
|
||||
function cmpTracks(a, b) {
|
||||
if (a.enabled && !b.enabled) return -1;
|
||||
if (!a.enabled && b.enabled) return 1;
|
||||
|
||||
if (a.path.toLowerCase() > b.path.toLowerCase())
|
||||
return 1;
|
||||
if (a.path.toLowerCase() < b.path.toLowerCase())
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function createTracksStore() {
|
||||
const { subscribe, set, update } = writable({list: null});
|
||||
|
||||
@ -14,6 +26,7 @@ function createTracksStore() {
|
||||
|
||||
refresh: async () => {
|
||||
const list = await getTracks();
|
||||
list.sort(cmpTracks);
|
||||
update((m) => Object.assign(m, {list}));
|
||||
return list;
|
||||
},
|
||||
|
@ -1,2 +1 @@
|
||||
export const ssr = false;
|
||||
export const prerender = true;
|
||||
|
@ -4,10 +4,18 @@
|
||||
|
||||
import {
|
||||
//Styles,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import Toaster from '$lib/components/Toaster.svelte';
|
||||
import { ToastsStore } from '$lib/stores/toasts';
|
||||
|
||||
window.onunhandledrejection = (e) => {
|
||||
ToastsStore.addErrorToast({
|
||||
message: e.reason,
|
||||
timeout: 7500,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@ -21,7 +29,7 @@
|
||||
/>
|
||||
<div class="flex-fill d-flex flex-column bg-light">
|
||||
<slot></slot>
|
||||
<div class="d-flex d-lg-none mt-1 mb-4"></div>
|
||||
<div class="d-flex d-lg-none mt-3 mb-5"></div>
|
||||
</div>
|
||||
<Toaster />
|
||||
<Header
|
||||
|
@ -3,12 +3,13 @@
|
||||
Container,
|
||||
Icon,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import CycleCounter from '$lib/components/CycleCounter.svelte';
|
||||
import DateFormat from '$lib/components/DateFormat.svelte';
|
||||
import { isAlarmActive, alarmNextTrack, runAlarm, alarmStop } from '$lib/alarm';
|
||||
import { isAlarmActive, alarmNextTrack, runAlarm, alarmStop, deleteNextAlarm } from '$lib/alarm';
|
||||
import { getNextAlarm, newNCyclesAlarm } from '$lib/alarmsingle';
|
||||
import { alarmsExceptions } from '$lib/stores/alarmexceptions';
|
||||
import { alarmsSingle } from '$lib/stores/alarmsingle';
|
||||
import { quotes } from '$lib/stores/quotes';
|
||||
|
||||
@ -39,10 +40,19 @@
|
||||
reloadIsActiveAlarm().then((isActive) => {
|
||||
if (isActive) {
|
||||
setTimeout(reloadIsActiveAlarm, 10000);
|
||||
setTimeout(reloadIsActiveAlarm, 25000);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function dropNextAlarm() {
|
||||
deleteNextAlarm().then(() => {
|
||||
alarmsExceptions.clear();
|
||||
alarmsSingle.clear();
|
||||
reloadNextAlarm();
|
||||
});
|
||||
}
|
||||
|
||||
let extinctionInProgress = false;
|
||||
</script>
|
||||
|
||||
@ -90,6 +100,13 @@
|
||||
{:else}
|
||||
<DateFormat date={nextalarm} dateStyle="short" timeStyle="long" />
|
||||
{/if}
|
||||
<button
|
||||
class="btn btn-lg btn-link"
|
||||
title="Supprimer ce prochain réveil"
|
||||
on:click={dropNextAlarm}
|
||||
>
|
||||
<Icon name="x-circle-fill" />
|
||||
</button>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
@ -98,10 +115,11 @@
|
||||
<div class="d-flex gap-3 justify-content-center">
|
||||
<a
|
||||
href="alarms/single/new"
|
||||
class="btn btn-primary"
|
||||
title="Programmer un nouveau réveil"
|
||||
class="btn btn-primary d-flex align-items-center justify-content-center gap-2"
|
||||
>
|
||||
<Icon name="node-plus" />
|
||||
Programmer un nouveau réveil
|
||||
Programmer <span class="d-none d-lg-inline">un nouveau réveil</span>
|
||||
</a>
|
||||
<button
|
||||
class="btn btn-info"
|
||||
@ -119,10 +137,11 @@
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-warning"
|
||||
on:click={() => { runAlarm(); reloadIsActiveAlarm(); }}
|
||||
title="Lancer le réveil"
|
||||
on:click={() => { runAlarm(); setTimeout(reloadIsActiveAlarm, 500); }}
|
||||
>
|
||||
<Icon name="play-circle" />
|
||||
Lancer le réveil
|
||||
Lancer <span class="d-none d-lg-inline">le réveil</span>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
|
@ -4,7 +4,7 @@
|
||||
Container,
|
||||
Row,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import AlarmSingleList from '$lib/components/AlarmSingleList.svelte';
|
||||
import AlarmRepeatedList from '$lib/components/AlarmRepeatedList.svelte';
|
||||
|
@ -5,7 +5,7 @@
|
||||
Container,
|
||||
Row,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import {
|
||||
Container,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import AlarmSingleList from '$lib/components/AlarmSingleList.svelte';
|
||||
import AlarmRepeatedList from '$lib/components/AlarmRepeatedList.svelte';
|
||||
|
@ -4,11 +4,12 @@
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
Input,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Row,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
@ -94,6 +95,10 @@
|
||||
<ListGroupItem>
|
||||
<strong>Heure du réveil</strong> <DateFormat date={alarm.time} timeStyle="long" />
|
||||
</ListGroupItem>
|
||||
<ListGroupItem class="d-flex">
|
||||
<strong>Fédération activée ?</strong>
|
||||
<Input type="switch" class="ms-2" on:change={() => {obj.enable_federation = !obj.enable_federation; obj.save();}} checked={obj.enable_federation} /> {obj.enable_federation?"oui":"non"}
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
{/await}
|
||||
{:else if $page.params["kind"] == "repeated"}
|
||||
@ -117,12 +122,21 @@
|
||||
<ListGroupItem>
|
||||
<strong>Heure du réveil</strong> {alarm.time}
|
||||
</ListGroupItem>
|
||||
<ListGroupItem>
|
||||
<strong>Ignorer les exceptions ?</strong> {alarm.ignore_exceptions?"oui":"non"}
|
||||
<ListGroupItem class="d-flex">
|
||||
<strong>Alarme active ?</strong>
|
||||
<Input type="switch" class="ms-2" on:change={() => {obj.disabled = !obj.disabled; obj.save().then(() => {obj.next_time = null; alarmsRepeated.refresh()});}} checked={!obj.disabled} /> {!obj.disabled?"oui":"non"}
|
||||
</ListGroupItem>
|
||||
<ListGroupItem class="d-flex">
|
||||
<strong>Ignorer les exceptions ?</strong>
|
||||
<Input type="switch" class="ms-2" on:change={() => {obj.ignore_exceptions = !obj.ignore_exceptions; obj.save();}} checked={obj.ignore_exceptions} /> {obj.ignore_exceptions?"oui":"non"}
|
||||
</ListGroupItem>
|
||||
<ListGroupItem class="d-flex">
|
||||
<strong>Fédération activée ?</strong>
|
||||
<Input type="switch" class="ms-2" on:change={() => {obj.enable_federation = !obj.enable_federation; obj.save();}} checked={obj.enable_federation} /> {obj.enable_federation?"oui":"non"}
|
||||
</ListGroupItem>
|
||||
{#if alarm.next_time}
|
||||
<ListGroupItem>
|
||||
<strong>Prochaine occurrence</strong> <DateFormat date={new Date(alarm.next_time)} dateStyle="long" />
|
||||
<strong>Prochaine occurrence</strong> <DateFormat date={new Date(obj.next_time)} dateStyle="long" />
|
||||
</ListGroupItem>
|
||||
{/if}
|
||||
</ListGroup>
|
||||
|
@ -13,7 +13,7 @@
|
||||
Label,
|
||||
Row,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import DateTimeInput from '$lib/components/DateTimeInput.svelte';
|
||||
import { AlarmSingle } from '$lib/alarmsingle';
|
||||
@ -37,15 +37,22 @@
|
||||
|
||||
let obj;
|
||||
|
||||
const vtime = new Date(Date.now() + 7.6*3600000);
|
||||
|
||||
switch($page.params["kind"]) {
|
||||
case "single":
|
||||
obj = new AlarmSingle();
|
||||
obj.time = vtime;
|
||||
break;
|
||||
case "repeated":
|
||||
obj = new AlarmRepeated();
|
||||
obj.weekday = vtime.getDay();
|
||||
obj.time = (vtime.getHours() < 10 ? "0" : "") + vtime.getHours() + ":" + (vtime.getMinutes() < 10 ? "0" : "") + vtime.getMinutes();
|
||||
break;
|
||||
case "exceptions":
|
||||
obj = new AlarmException();
|
||||
obj.start = new Date(Date.now()).toISOString().substring(0,10);
|
||||
obj.end = new Date(Date.now() + 7.5*86400000).toISOString().substring(0,10);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -147,6 +154,11 @@
|
||||
<Input id="exceptionEnd" type="date" required bind:value={obj.end} />
|
||||
</FormGroup>
|
||||
{/if}
|
||||
{#if $page.params["kind"] != "exceptions"}
|
||||
<FormGroup>
|
||||
<Input id="enable_federation" type="checkbox" label="Activer la fédération" bind:checked={obj.enable_federation} />
|
||||
</FormGroup>
|
||||
{/if}
|
||||
|
||||
<FormGroup>
|
||||
<Label for="comment">Commentaire</Label>
|
||||
|
@ -4,7 +4,7 @@
|
||||
Container,
|
||||
Row,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import CardStatAlarms from '$lib/components/CardStatAlarms.svelte';
|
||||
import CardStatTimeAwaking from '$lib/components/CardStatTimeAwaking.svelte';
|
||||
|
@ -4,7 +4,7 @@
|
||||
Container,
|
||||
Row,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import MusiksLastPlayedList from '$lib/components/MusiksLastPlayedList.svelte';
|
||||
import TrackList from '$lib/components/TrackList.svelte';
|
||||
|
@ -5,7 +5,7 @@
|
||||
Container,
|
||||
Row,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import GongsList from '$lib/components/GongsList.svelte';
|
||||
</script>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import {
|
||||
Container,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import TrackList from '$lib/components/TrackList.svelte';
|
||||
</script>
|
||||
|
@ -10,7 +10,7 @@
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { getGong } from '$lib/gong';
|
||||
import { gongs } from '$lib/stores/gongs';
|
||||
|
@ -10,7 +10,7 @@
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { gongs } from '$lib/stores/gongs';
|
||||
import { uploadGong } from '$lib/gong';
|
||||
|
@ -5,7 +5,7 @@
|
||||
Container,
|
||||
Row,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import TrackList from '$lib/components/TrackList.svelte';
|
||||
</script>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import {
|
||||
Container,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import TrackList from '$lib/components/TrackList.svelte';
|
||||
</script>
|
||||
|
@ -10,7 +10,7 @@
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { getTrack } from '$lib/track';
|
||||
import { tracks } from '$lib/stores/tracks';
|
||||
|
@ -10,7 +10,7 @@
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { tracks } from '$lib/stores/tracks';
|
||||
import { uploadTrack } from '$lib/track';
|
||||
|
0
ui/src/routes/nojs/+page.svelte
Normal file
0
ui/src/routes/nojs/+page.svelte
Normal file
@ -6,7 +6,7 @@
|
||||
Icon,
|
||||
Row,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { routines } from '$lib/stores/routines';
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
Container,
|
||||
Row,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import ActionList from '$lib/components/ActionList.svelte';
|
||||
</script>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import {
|
||||
Container,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import ActionList from '$lib/components/ActionList.svelte';
|
||||
</script>
|
||||
|
@ -3,13 +3,22 @@
|
||||
|
||||
import {
|
||||
Container,
|
||||
Icon,
|
||||
Input,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { getAction } from '$lib/action';
|
||||
import { actions } from '$lib/stores/actions';
|
||||
|
||||
function deleteThis(action) {
|
||||
action.delete().then(() => {
|
||||
actions.refresh();
|
||||
goto('routines/actions/');
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await getAction($page.params.aid)}
|
||||
@ -34,5 +43,26 @@
|
||||
<Input type="switch" on:change={() => action.toggleEnable()} checked={action.enabled} />
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
|
||||
<ListGroup class="my-2 text-center">
|
||||
<ListGroupItem
|
||||
action
|
||||
tag="button"
|
||||
class="text-success fw-bold"
|
||||
on:click={() => action.launch()}
|
||||
>
|
||||
<Icon name="play-fill" />
|
||||
Lancer cette action
|
||||
</ListGroupItem>
|
||||
<ListGroupItem
|
||||
action
|
||||
tag="button"
|
||||
class="text-danger fw-bold"
|
||||
on:click={() => deleteThis(action)}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
Supprimer cette action
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
</Container>
|
||||
{/await}
|
||||
|
@ -1,4 +1,6 @@
|
||||
<script>
|
||||
import { tick } from 'svelte';
|
||||
|
||||
import {
|
||||
Container,
|
||||
Form,
|
||||
@ -9,23 +11,29 @@
|
||||
InputGroupText,
|
||||
Label,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { actions } from '$lib/stores/actions';
|
||||
import { getSettings } from '$lib/settings';
|
||||
import FederationSettings from '$lib/components/FederationSettings.svelte';
|
||||
|
||||
let settingsP = getSettings();
|
||||
|
||||
$: settingsP.then((s) => settings = s);
|
||||
|
||||
let settings;
|
||||
|
||||
async function submitSettings() {
|
||||
await tick();
|
||||
settings.save();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container class="flex-fill d-flex flex-column py-2">
|
||||
<h2>
|
||||
Paramètres
|
||||
</h2>
|
||||
<Form>
|
||||
<Form on:submit={submitSettings}>
|
||||
{#await settingsP}
|
||||
<div class="d-flex justify-content-center align-items-center gap-2">
|
||||
<Spinner color="primary" /> Chargement en cours…
|
||||
@ -39,7 +47,7 @@
|
||||
id="gongIntervals"
|
||||
placeholder="20"
|
||||
bind:value={settings.gong_interval}
|
||||
on:change={() => settings.save()}
|
||||
on:input={submitSettings}
|
||||
/>
|
||||
<InputGroupText>min</InputGroupText>
|
||||
</InputGroup>
|
||||
@ -53,7 +61,7 @@
|
||||
id="weatherDelay"
|
||||
placeholder="5"
|
||||
bind:value={settings.weather_delay}
|
||||
on:change={() => settings.save()}
|
||||
on:input={submitSettings}
|
||||
/>
|
||||
<InputGroupText>min</InputGroupText>
|
||||
</InputGroup>
|
||||
@ -66,7 +74,7 @@
|
||||
type="select"
|
||||
id="weatherRituel"
|
||||
bind:value={settings.weather_action}
|
||||
on:change={() => settings.save()}
|
||||
on:input={submitSettings}
|
||||
>
|
||||
{#each $actions.list as action (action.id)}
|
||||
<option value="{action.path}">{action.name}</option>
|
||||
@ -81,13 +89,47 @@
|
||||
{/if}
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<Label for="preAlarmDelay">Lancement action pré-alarme</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="number"
|
||||
id="preAlarmDelay"
|
||||
placeholder="5"
|
||||
bind:value={settings.pre_alarm_delay}
|
||||
on:input={submitSettings}
|
||||
/>
|
||||
<InputGroupText>min</InputGroupText>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<Label for="preAlarmRituel">Action pour l'action pré-alarme</Label>
|
||||
{#if $actions.list}
|
||||
<Input
|
||||
type="select"
|
||||
id="preAlarmRituel"
|
||||
bind:value={settings.pre_alarm_action}
|
||||
on:input={submitSettings}
|
||||
>
|
||||
{#each $actions.list as action (action.id)}
|
||||
<option value="{action.path}">{action.name}</option>
|
||||
{/each}
|
||||
</Input>
|
||||
{:else}
|
||||
<div class="d-flex justify-content-center align-items-center gap-2">
|
||||
<Spinner color="primary" /> Chargement en cours…
|
||||
</div>
|
||||
{/if}
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<Label for="greetingLanguage">Langue de salutation</Label>
|
||||
<Input
|
||||
type="select"
|
||||
id="greetingLanguage"
|
||||
bind:value={settings.lang}
|
||||
on:change={() => settings.save()}
|
||||
bind:value={settings.language}
|
||||
on:input={submitSettings}
|
||||
>
|
||||
<option value="fr_FR">Français</option>
|
||||
<option value="en_US">Anglais</option>
|
||||
@ -104,11 +146,34 @@
|
||||
id="maxRunTime"
|
||||
placeholder="60"
|
||||
bind:value={settings.max_run_time}
|
||||
on:change={() => settings.save()}
|
||||
on:input={submitSettings}
|
||||
/>
|
||||
<InputGroupText>min</InputGroupText>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<Label for="maxVolume">Volume maximum</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="range"
|
||||
id="maxVolume"
|
||||
min="0"
|
||||
max="65535"
|
||||
bind:value={settings.max_volume}
|
||||
on:input={submitSettings}
|
||||
/>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<Label for="federation">Federation</Label>
|
||||
<FederationSettings
|
||||
id="federation"
|
||||
bind:value={settings.federation}
|
||||
on:input={submitSettings}
|
||||
/>
|
||||
</FormGroup>
|
||||
{/await}
|
||||
</Form>
|
||||
</Container>
|
||||
|
@ -1,80 +0,0 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import { build, files, timestamp } from '$service-worker';
|
||||
|
||||
const worker = (self as unknown) as ServiceWorkerGlobalScope;
|
||||
const FILES = `cache${timestamp}`;
|
||||
|
||||
// `build` is an array of all the files generated by the bundler,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = build.concat(files);
|
||||
const staticAssets = new Set(to_cache);
|
||||
|
||||
worker.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(FILES)
|
||||
.then((cache) => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
worker.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
worker.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async (keys) => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== FILES) await caches.delete(key);
|
||||
}
|
||||
|
||||
worker.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch the asset from the network and store it in the cache.
|
||||
* Fall back to the cache if the user is offline.
|
||||
*/
|
||||
async function fetchAndCache(request: Request) {
|
||||
const cache = await caches.open(`offline${timestamp}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
cache.put(request, response.clone());
|
||||
return response;
|
||||
} catch (err) {
|
||||
const response = await cache.match(request);
|
||||
if (response) return response;
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
worker.addEventListener('fetch', (event) => {
|
||||
if (event.request.method !== 'GET' || event.request.headers.has('range')) return;
|
||||
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
const isHttp = url.protocol.startsWith('http');
|
||||
const isDevServerRequest =
|
||||
url.hostname === self.location.hostname && url.port !== self.location.port;
|
||||
const isStaticAsset = url.host === self.location.host && staticAssets.has(url.pathname);
|
||||
const skipBecauseUncached = event.request.cache === 'only-if-cached' && !isStaticAsset;
|
||||
|
||||
if (isHttp && !isDevServerRequest && !skipBecauseUncached) {
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
// always serve static files and bundler-generated assets from cache.
|
||||
// if your application has other URLs with data that will never change,
|
||||
// set this variable to true for them and they will only be fetched once.
|
||||
const cachedAsset = isStaticAsset && (await caches.match(event.request));
|
||||
|
||||
return cachedAsset || fetchAndCache(event.request);
|
||||
})()
|
||||
);
|
||||
}
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"short_name": "Gustus",
|
||||
"name": "Gustus",
|
||||
"short_name": "Réveil",
|
||||
"name": "Réveil",
|
||||
"version": "0.1",
|
||||
"author": "nemucorp",
|
||||
"start_url": "/",
|
||||
@ -12,9 +12,9 @@
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"background_color": "#d62a49",
|
||||
"background_color": "#e83e8c",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#ffffff",
|
||||
"description": "Retrouvez facilement toutes vos recettes préférées"
|
||||
"description": "Quand est-ce qu'on se lève ?"
|
||||
}
|
||||
|
0
ui/static/nojs.html
Normal file
0
ui/static/nojs.html
Normal file
@ -9,11 +9,8 @@ const config = {
|
||||
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
fallback: '404.html'
|
||||
fallback: 'index.html'
|
||||
}),
|
||||
paths: {
|
||||
// base: '{{.urlbase}}',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,32 +1,3 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "es2020",
|
||||
"lib": ["es2020", "DOM"],
|
||||
"target": "es2020",
|
||||
/**
|
||||
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
|
||||
to enforce using \`import type\` instead of \`import\` for Types.
|
||||
*/
|
||||
"importsNotUsedAsValues": "error",
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
To have warnings/errors of the Svelte compiler at the correct position,
|
||||
enable source maps by default.
|
||||
*/
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"paths": {
|
||||
"$lib": ["src/lib"],
|
||||
"$lib/*": ["src/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
|
||||
"extends": "./.svelte-kit/tsconfig.json"
|
||||
}
|
||||
|
@ -2,6 +2,12 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
server: {
|
||||
hmr: {
|
||||
port: 10000
|
||||
}
|
||||
},
|
||||
|
||||
plugins: [sveltekit()]
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user