Compare commits

..

No commits in common. "v0.1.1" and "master" have entirely different histories.

85 changed files with 5531 additions and 291 deletions

View File

@ -13,12 +13,11 @@ workspace:
steps: steps:
- name: build front - name: build front
image: node:18-alpine image: node:22
commands: commands:
- mkdir deploy - mkdir deploy
- cd ui - cd ui
- npm install --network-timeout=100000 - 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 - npm run build
- tar chjf ../deploy/static.tar.bz2 build - tar chjf ../deploy/static.tar.bz2 build
@ -27,39 +26,53 @@ steps:
commands: commands:
- apk --no-cache add alsa-lib-dev build-base git pkgconf - apk --no-cache add alsa-lib-dev build-base git pkgconf
- go get -v -d - go get -v -d
- go vet -v - go vet -v -tags pulse
- 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 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 - ln deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} reveil
when: when:
event: event:
exclude: exclude:
- tag - tag
- name: build tag - name: build armv7 tag
image: golang:1-alpine image: golang:1-alpine
commands: commands:
- apk --no-cache add alsa-lib-dev build-base git pkgconf - apk --no-cache add alsa-lib-dev build-base git pkgconf
- go get -v -d - go get -v -d
- go vet -v - go vet -v -tags pulse
- 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} - 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} reveil - 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: when:
event: event:
- tag - tag
- name: gitea release - name: gitea release
image: plugins/gitea-release image: plugins/gitea-release:linux-arm
settings: settings:
api_key: api_key:
from_secret: gitea_api_key from_secret: gitea_api_key
base_url: https://git.nemunai.re/ 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: when:
event: event:
- tag - tag
- name: docker - name: docker
image: plugins/docker image: plugins/docker:linux-arm
settings: settings:
registry: registry.nemunai.re registry: registry.nemunai.re
repo: registry.nemunai.re/reveil repo: registry.nemunai.re/reveil

View File

@ -1,11 +1,10 @@
FROM node:18-alpine as nodebuild FROM node:22-alpine as nodebuild
WORKDIR /ui WORKDIR /ui
COPY ui/ . COPY ui/ .
RUN npm install --network-timeout=100000 && \ 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 npm run build
@ -16,10 +15,10 @@ RUN apk --no-cache add git go-bindata
COPY . /go/src/git.nemunai.re/nemunaire/reveil COPY . /go/src/git.nemunai.re/nemunaire/reveil
COPY --from=nodebuild /ui/build /go/src/git.nemunai.re/nemunaire/reveil/ui/build COPY --from=nodebuild /ui/build /go/src/git.nemunai.re/nemunaire/reveil/ui/build
WORKDIR /go/src/git.nemunai.re/nemunaire/reveil 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 VOLUME /data
WORKDIR /data WORKDIR /data

View File

@ -1,4 +1,4 @@
FROM alpine:3.16 FROM alpine:3.20
VOLUME /data VOLUME /data
WORKDIR /data WORKDIR /data

View File

@ -2,6 +2,7 @@ package api
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -90,4 +91,29 @@ func declareActionsRoutes(cfg *config.Config, router *gin.RouterGroup) {
c.JSON(http.StatusOK, nil) 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)
})
} }

View File

@ -1,11 +1,14 @@
package api package api
import ( import (
"fmt"
"log"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.nemunai.re/nemunaire/reveil/config" "git.nemunai.re/nemunaire/reveil/config"
"git.nemunai.re/nemunaire/reveil/model"
"git.nemunai.re/nemunaire/reveil/player" "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) { router.POST("/alarm/run", func(c *gin.Context) {
if player.CommonPlayer == nil { if player.CommonPlayer == nil {
err := player.WakeUp(cfg) err := player.WakeUp(cfg, nil, true)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return return
@ -51,6 +54,21 @@ func declareAlarmRoutes(cfg *config.Config, router *gin.RouterGroup) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return 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) c.JSON(http.StatusOK, true)

View File

@ -13,7 +13,7 @@ import (
func declareAlarmsRoutes(cfg *config.Config, db *reveil.LevelDBStorage, resetTimer func(), router *gin.RouterGroup) { func declareAlarmsRoutes(cfg *config.Config, db *reveil.LevelDBStorage, resetTimer func(), router *gin.RouterGroup) {
router.GET("/alarms/next", func(c *gin.Context) { router.GET("/alarms/next", func(c *gin.Context) {
alarm, err := reveil.GetNextAlarm(db) alarm, _, _, err := reveil.GetNextAlarm(cfg, db)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return return
@ -22,6 +22,18 @@ func declareAlarmsRoutes(cfg *config.Config, db *reveil.LevelDBStorage, resetTim
c.JSON(http.StatusOK, alarm) 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) { router.GET("/alarms/single", func(c *gin.Context) {
alarms, err := reveil.GetAlarmsSingle(db) alarms, err := reveil.GetAlarmsSingle(db)
if err != nil { if err != nil {
@ -200,8 +212,8 @@ func declareAlarmsRoutes(cfg *config.Config, db *reveil.LevelDBStorage, resetTim
repeatedAlarmsRoutes.GET("", func(c *gin.Context) { repeatedAlarmsRoutes.GET("", func(c *gin.Context) {
alarm := c.MustGet("alarm").(*reveil.AlarmRepeated) alarm := c.MustGet("alarm").(*reveil.AlarmRepeated)
alarm.FillExcepts(db) alarm.FillExcepts(cfg, db)
alarm.NextTime = alarm.GetNextOccurence(db) alarm.NextTime = alarm.GetNextOccurence(cfg, db)
c.JSON(http.StatusOK, alarm) c.JSON(http.StatusOK, alarm)
}) })

176
api/federation.go Normal file
View 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)
})
}

View File

@ -13,6 +13,7 @@ func DeclareRoutes(router *gin.Engine, cfg *config.Config, db *reveil.LevelDBSto
declareActionsRoutes(cfg, apiRoutes) declareActionsRoutes(cfg, apiRoutes)
declareAlarmRoutes(cfg, apiRoutes) declareAlarmRoutes(cfg, apiRoutes)
declareAlarmsRoutes(cfg, db, resetTimer, apiRoutes) declareAlarmsRoutes(cfg, db, resetTimer, apiRoutes)
declareFederationRoutes(cfg, apiRoutes)
declareGongsRoutes(cfg, apiRoutes) declareGongsRoutes(cfg, apiRoutes)
declareHistoryRoutes(cfg, apiRoutes) declareHistoryRoutes(cfg, apiRoutes)
declareQuotesRoutes(cfg, apiRoutes) declareQuotesRoutes(cfg, apiRoutes)

View File

@ -78,4 +78,12 @@ func declareRoutinesRoutes(cfg *config.Config, router *gin.RouterGroup) {
c.JSON(http.StatusOK, nil) 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)
})
} }

44
app.go
View File

@ -21,6 +21,7 @@ type App struct {
router *gin.Engine router *gin.Engine
srv *http.Server srv *http.Server
nextAlarm *time.Timer nextAlarm *time.Timer
nextPreAlarm *time.Timer
} }
func NewApp(cfg *config.Config) *App { func NewApp(cfg *config.Config) *App {
@ -49,6 +50,7 @@ func NewApp(cfg *config.Config) *App {
// Register routes // Register routes
ui.DeclareRoutes(router, cfg) ui.DeclareRoutes(router, cfg)
ui.DeclareNoJSRoutes(router, cfg, db, app.ResetTimer)
api.DeclareRoutes(router, cfg, db, app.ResetTimer) api.DeclareRoutes(router, cfg, db, app.ResetTimer)
router.GET("/api/version", func(c *gin.Context) { router.GET("/api/version", func(c *gin.Context) {
@ -75,14 +77,52 @@ func (app *App) Start() {
func (app *App) ResetTimer() { func (app *App) ResetTimer() {
if app.nextAlarm != nil { if app.nextAlarm != nil {
app.nextAlarm.Stop() app.nextAlarm.Stop()
app.nextPreAlarm = nil
app.nextAlarm = 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.nextAlarm = time.AfterFunc(time.Until(*na), func() {
app.nextPreAlarm = nil
app.nextAlarm = nil app.nextAlarm = nil
reveil.RemoveOldAlarmsSingle(app.db) 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 { if err != nil {
log.Println(err.Error()) log.Println(err.Error())
return return

View File

@ -3,6 +3,7 @@ package config
import ( import (
"encoding/base64" "encoding/base64"
"net/url" "net/url"
"time"
) )
type JWTSecretKey []byte type JWTSecretKey []byte
@ -42,3 +43,33 @@ func (i *URL) Set(value string) error {
i.URL = u i.URL = u
return nil 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
}

View File

@ -7,10 +7,10 @@ import (
) )
// FromEnv analyzes all the environment variables to find each one // FromEnv analyzes all the environment variables to find each one
// starting by GUSTUS_ // starting by REVEIL_
func (c *Config) FromEnv() error { func (c *Config) FromEnv() error {
for _, line := range os.Environ() { for _, line := range os.Environ() {
if strings.HasPrefix(line, "GUSTUS_") { if strings.HasPrefix(line, "REVEIL_") {
err := c.parseLine(line) err := c.parseLine(line)
if err != nil { if err != nil {
return fmt.Errorf("error in environment (%q): %w", line, err) return fmt.Errorf("error in environment (%q): %w", line, err)

40
go.mod
View File

@ -4,37 +4,47 @@ go 1.18
require ( require (
github.com/faiface/beep v1.1.0 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 github.com/syndtr/goleveldb v1.0.0
) )
require ( 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/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/snappy v0.0.1 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/hajimehoshi/go-mp3 v0.3.0 // indirect github.com/hajimehoshi/go-mp3 v0.3.0 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect github.com/hajimehoshi/oto v0.7.1 // indirect
github.com/icza/bitio v1.0.0 // indirect github.com/icza/bitio v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/mattn/go-isatty v0.0.14 // 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/flac v1.0.7 // indirect
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 // 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/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/pkg/errors v0.9.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // 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/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

99
go.sum
View File

@ -1,4 +1,18 @@
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 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/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/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= 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 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4= 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/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/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= 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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 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 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.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/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/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= 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/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 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 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 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.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 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.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 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 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= 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/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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 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/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 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 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/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 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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/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 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= 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/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 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-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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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= 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/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 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.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/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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 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.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.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.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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 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.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 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= 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 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 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 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-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 h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 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= 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-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 h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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/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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-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-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-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 h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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/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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.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 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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-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 h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=

View File

@ -4,9 +4,11 @@ import (
"bufio" "bufio"
"crypto/sha512" "crypto/sha512"
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"log" "log"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
@ -19,9 +21,10 @@ type Action struct {
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Path string `json:"path"` Path string `json:"path"`
Enabled bool `json:"enabled"` 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) fd, err := os.Open(path)
if err != nil { if err != nil {
return "", "", err return "", "", err
@ -64,6 +67,45 @@ func LoadAction(path string) (string, string, error) {
return name, description, nil 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) { func LoadActions(cfg *config.Config) (actions []*Action, err error) {
actionsDir, err := filepath.Abs(cfg.ActionsDir) actionsDir, err := filepath.Abs(cfg.ActionsDir)
if err != nil { if err != nil {
@ -75,7 +117,7 @@ func LoadActions(cfg *config.Config) (actions []*Action, err error) {
hash := sha512.Sum512([]byte(path)) hash := sha512.Sum512([]byte(path))
// Parse content // Parse content
name, description, err := LoadAction(path) name, description, err := loadAction(path)
if err != nil { if err != nil {
log.Printf("Invalid action file (trying to parse %s): %s", path, err.Error()) log.Printf("Invalid action file (trying to parse %s): %s", path, err.Error())
// Ignore invalid files // Ignore invalid files
@ -96,6 +138,7 @@ func LoadActions(cfg *config.Config) (actions []*Action, err error) {
Description: description, Description: description,
Path: strings.TrimPrefix(path, actionsDir+"/"), Path: strings.TrimPrefix(path, actionsDir+"/"),
Enabled: d.Mode().Perm()&0111 != 0, Enabled: d.Mode().Perm()&0111 != 0,
fullPath: path,
}) })
} }
@ -130,3 +173,10 @@ func (a *Action) Disable() error {
func (a *Action) Remove() error { func (a *Action) Remove() error {
return os.Remove(a.Path) 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
}

View File

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"sort" "sort"
"time" "time"
"git.nemunai.re/nemunaire/reveil/config"
) )
type Date time.Time type Date time.Time
@ -38,33 +40,95 @@ func (h *Hour) UnmarshalJSON(src []byte) error {
return nil 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) alarmsRepeated, err := GetAlarmsRepeated(db)
if err != nil { if err != nil {
return nil, err return nil, nil, false, err
} }
var closestAlarm *time.Time var closestAlarm *time.Time
var closestAlarmRoutines []Identifier
var closestAlarmFederated bool
for _, alarm := range alarmsRepeated { for _, alarm := range alarmsRepeated {
next := alarm.GetNextOccurence(db) next := alarm.GetNextOccurence(cfg, db)
if next != nil && (closestAlarm == nil || closestAlarm.After(*next)) { if next != nil && (closestAlarm == nil || closestAlarm.After(*next)) {
closestAlarm = next closestAlarm = next
closestAlarmRoutines = alarm.FollowingRoutines
closestAlarmFederated = alarm.EnableFederation
} }
} }
alarmsSingle, err := GetAlarmsSingle(db) alarmsSingle, err := GetAlarmsSingle(db)
if err != nil { if err != nil {
return nil, err return nil, nil, false, err
} }
now := time.Now() now := time.Now()
for _, alarm := range alarmsSingle { for _, alarm := range alarmsSingle {
if closestAlarm == nil || (closestAlarm.After(alarm.Time) && alarm.Time.After(now)) { if closestAlarm == nil || (closestAlarm.After(alarm.Time) && alarm.Time.After(now)) {
closestAlarm = &alarm.Time 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 type Exceptions []time.Time
@ -88,11 +152,13 @@ type AlarmRepeated struct {
FollowingRoutines []Identifier `json:"routines"` FollowingRoutines []Identifier `json:"routines"`
IgnoreExceptions bool `json:"ignore_exceptions"` IgnoreExceptions bool `json:"ignore_exceptions"`
Comment string `json:"comment,omitempty"` Comment string `json:"comment,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Excepts Exceptions `json:"excepts,omitempty"` Excepts Exceptions `json:"excepts,omitempty"`
NextTime *time.Time `json:"next_time,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 { if a.IgnoreExceptions {
return nil return nil
} }
@ -105,15 +171,15 @@ func (a *AlarmRepeated) FillExcepts(db *LevelDBStorage) error {
now := time.Now() now := time.Now()
for _, exception := range exceptions { 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 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) { for t := time.Time(*exception.Start); end.After(t); t = t.AddDate(0, 0, 1) {
if t.Weekday() == a.Weekday { 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())) 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.AddDate(0, 0, 6) t = t.AddDate(0, 0, 6)
} }
} }
} }
@ -123,14 +189,18 @@ func (a *AlarmRepeated) FillExcepts(db *LevelDBStorage) error {
return nil 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 { if len(a.Excepts) == 0 {
a.FillExcepts(db) a.FillExcepts(cfg, db)
} }
now := time.Now() 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) { if now.After(today) {
today = today.AddDate(0, 0, 1) 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) { func PutAlarmRepeated(db *LevelDBStorage, alarm *AlarmRepeated) (err error) {
var key string var key string
var id Identifier
if alarm.Id.IsEmpty() { if alarm.Id.IsEmpty() {
var id Identifier
key, id, err = db.findBytesKey("alarm-repeated-", IDENTIFIER_LEN) key, id, err = db.findBytesKey("alarm-repeated-", IDENTIFIER_LEN)
if err != nil { if err != nil {
return err 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 // Don't store this, this is autocalculated
alarm.Excepts = nil alarm.Excepts = nil
alarm.NextTime = nil alarm.NextTime = nil
@ -204,6 +277,7 @@ type AlarmSingle struct {
Time time.Time `json:"time"` Time time.Time `json:"time"`
FollowingRoutines []Identifier `json:"routines"` FollowingRoutines []Identifier `json:"routines"`
Comment string `json:"comment,omitempty"` Comment string `json:"comment,omitempty"`
EnableFederation bool `json:"enable_federation,omitempty"`
} }
func GetAlarmSingle(db *LevelDBStorage, id Identifier) (alarm *AlarmSingle, err error) { func GetAlarmSingle(db *LevelDBStorage, id Identifier) (alarm *AlarmSingle, err error) {
@ -232,16 +306,18 @@ func GetAlarmsSingle(db *LevelDBStorage) (alarms []*AlarmSingle, err error) {
func PutAlarmSingle(db *LevelDBStorage, alarm *AlarmSingle) (err error) { func PutAlarmSingle(db *LevelDBStorage, alarm *AlarmSingle) (err error) {
var key string var key string
var id Identifier
if alarm.Id.IsEmpty() { if alarm.Id.IsEmpty() {
var id Identifier
key, id, err = db.findBytesKey("alarm-single-", IDENTIFIER_LEN) key, id, err = db.findBytesKey("alarm-single-", IDENTIFIER_LEN)
if err != nil { if err != nil {
return err return err
} }
}
alarm.Id = id alarm.Id = id
} else {
key = fmt.Sprintf("alarm-single-%s", alarm.Id.ToString())
}
return db.put(key, alarm) return db.put(key, alarm)
} }
@ -302,16 +378,18 @@ func GetAlarmExceptions(db *LevelDBStorage) (alarms []*AlarmException, err error
func PutAlarmException(db *LevelDBStorage, alarm *AlarmException) (err error) { func PutAlarmException(db *LevelDBStorage, alarm *AlarmException) (err error) {
var key string var key string
var id Identifier
if alarm.Id.IsEmpty() { if alarm.Id.IsEmpty() {
var id Identifier
key, id, err = db.findBytesKey("alarm-exception-", IDENTIFIER_LEN) key, id, err = db.findBytesKey("alarm-exception-", IDENTIFIER_LEN)
if err != nil { if err != nil {
return err return err
} }
}
alarm.Id = id alarm.Id = id
} else {
key = fmt.Sprintf("alarm-exception-%s", alarm.Id.ToString())
}
return db.put(key, alarm) return db.put(key, alarm)
} }

View File

@ -23,7 +23,7 @@ func NewLevelDBStorage(path string) (s *LevelDBStorage, err error) {
db, err = leveldb.OpenFile(path, nil) db, err = leveldb.OpenFile(path, nil)
if err != nil { if err != nil {
if _, ok := err.(*errors.ErrCorrupted); ok { 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) _, err = leveldb.RecoverFile(path, nil)
if err != nil { if err != nil {
return return

137
model/federation.go Normal file
View 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
}

View File

@ -1,7 +1,9 @@
package reveil package reveil
import ( import (
"bytes"
"crypto/sha512" "crypto/sha512"
"fmt"
"io/fs" "io/fs"
"io/ioutil" "io/ioutil"
"log" "log"
@ -9,6 +11,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
"git.nemunai.re/nemunaire/reveil/config" "git.nemunai.re/nemunaire/reveil/config"
) )
@ -19,6 +22,10 @@ type RoutineStep struct {
Args []string `json:"args,omitempty"` Args []string `json:"args,omitempty"`
} }
func (s *RoutineStep) GetAction(cfg *config.Config) (*Action, error) {
return LoadAction(cfg, s.Action)
}
type Routine struct { type Routine struct {
Id Identifier `json:"id"` Id Identifier `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -72,6 +79,21 @@ func LoadRoutine(path string, cfg *config.Config) ([]RoutineStep, error) {
return steps, nil 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) { func LoadRoutines(cfg *config.Config) (routines []*Routine, err error) {
err = filepath.Walk(cfg.RoutinesDir, func(path string, d fs.FileInfo, err error) error { err = filepath.Walk(cfg.RoutinesDir, func(path string, d fs.FileInfo, err error) error {
if d.IsDir() && path != cfg.RoutinesDir { if d.IsDir() && path != cfg.RoutinesDir {
@ -117,3 +139,34 @@ func (r *Routine) Rename(newName string) error {
func (a *Routine) Remove() error { func (a *Routine) Remove() error {
return os.Remove(a.Path) 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
}

View File

@ -12,7 +12,11 @@ type Settings struct {
GongInterval time.Duration `json:"gong_interval"` GongInterval time.Duration `json:"gong_interval"`
WeatherDelay time.Duration `json:"weather_delay"` WeatherDelay time.Duration `json:"weather_delay"`
WeatherAction string `json:"weather_action"` 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"` 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. // ExistsSettings checks if the settings file can by found at the given path.

29
player/federation.go Normal file
View 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)
}
}()
}
}

View File

@ -21,15 +21,21 @@ var CommonPlayer *Player
type Player struct { type Player struct {
Playlist []string Playlist []string
MaxRunTime time.Duration MaxRunTime time.Duration
MaxVolume uint16
Stopper chan bool Stopper chan bool
currentCmd *exec.Cmd currentCmd *exec.Cmd
currentCmdCh chan bool currentCmdCh chan bool
weatherTime time.Duration
weatherAction *reveil.Action
claironTime time.Duration claironTime time.Duration
claironFile string claironFile string
endRoutines []*reveil.Routine
ntick int64 ntick int64
hasClaironed bool hasSpokeWeather bool
launched time.Time launched time.Time
volume uint16 volume uint16
dontUpdateVolume bool dontUpdateVolume bool
@ -37,48 +43,82 @@ type Player struct {
playedItem int playedItem int
} }
func WakeUp(cfg *config.Config) (err error) { func WakeUp(cfg *config.Config, routine []reveil.Identifier, federated bool) (err error) {
if CommonPlayer != nil { if CommonPlayer != nil {
return fmt.Errorf("Unable to start the player: a player is already running") return fmt.Errorf("Unable to start the player: a player is already running")
} }
seed := time.Now().Unix() seed := time.Now().Unix()
seed -= seed % 172800 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) rand.Seed(seed)
CommonPlayer, err = NewPlayer(cfg) CommonPlayer, err = NewPlayer(cfg, routine)
if err != nil { if err != nil {
return err return err
} }
go CommonPlayer.WakeUp() go CommonPlayer.WakeUp(cfg)
return nil return nil
} }
func NewPlayer(cfg *config.Config) (*Player, error) { func NewPlayer(cfg *config.Config, routines []reveil.Identifier) (*Player, error) {
// Load our settings // Load our settings
settings, err := reveil.ReadSettings(cfg.SettingsFile) settings, err := reveil.ReadSettings(cfg.SettingsFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("Unable to read settings: %w", err) 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{ p := Player{
Stopper: make(chan bool, 1), Stopper: make(chan bool, 1),
currentCmdCh: make(chan bool, 1), currentCmdCh: make(chan bool, 1),
MaxRunTime: settings.MaxRunTime * time.Minute, MaxRunTime: settings.MaxRunTime * time.Minute,
MaxVolume: uint16(settings.MaxVolume),
weatherTime: settings.WeatherDelay * time.Minute,
weatherAction: wact,
claironTime: settings.GongInterval * time.Minute, claironTime: settings.GongInterval * time.Minute,
claironFile: reveil.CurrentGongPath(cfg), claironFile: reveil.CurrentGongPath(cfg),
reverseOrder: int(time.Now().Unix()/86400)%2 == 0, 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 // Load our track list
tracks, err := reveil.LoadTracks(cfg) tracks, err := reveil.LoadTracks(cfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("Unable to load tracks: %w", err) return nil, fmt.Errorf("Unable to load tracks: %w", err)
} }
var playlist []string
// Creating playlist // Creating playlist
log.Println("Loading playlist...") log.Println("Loading playlist...")
for _, track := range tracks { for _, track := range tracks {
@ -91,15 +131,30 @@ func NewPlayer(cfg *config.Config) (*Player, error) {
log.Println("Shuffling playlist...") log.Println("Shuffling playlist...")
// Shuffle the playlist // Shuffle the playlist
rand.Shuffle(len(playlist), func(i, j int) { rand.Shuffle(len(p.Playlist), func(i, j int) {
playlist[i], playlist[j] = playlist[j], playlist[i] p.Playlist[i], p.Playlist[j] = p.Playlist[j], p.Playlist[i]
}) })
return &p, nil 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) { 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 { if err = p.currentCmd.Start(); err != nil {
log.Println("Running paplay err: ", err.Error()) log.Println("Running paplay err: ", err.Error())
p.currentCmdCh <- true p.currentCmdCh <- true
@ -114,7 +169,7 @@ func (p *Player) playFile(filepath string) (err error) {
return return
} }
func (p *Player) WakeUp() { func (p *Player) WakeUp(cfg *config.Config) {
log.Println("Playlist in use:", strings.Join(p.Playlist, " ; ")) log.Println("Playlist in use:", strings.Join(p.Playlist, " ; "))
// Prepare sound player // Prepare sound player
@ -135,15 +190,20 @@ loop:
for { for {
select { select {
case <-p.currentCmdCh: case <-p.currentCmdCh:
if !p.hasClaironed && time.Since(p.launched) >= p.claironTime { if time.Since(p.launched) >= p.claironTime {
log.Println("clairon time!") log.Println("clairon time!")
p.claironTime += p.claironTime / 2 p.claironTime += p.claironTime / 2
p.SetVolume(65535) p.SetVolume(65535)
p.dontUpdateVolume = true p.dontUpdateVolume = true
go p.playFile(p.claironFile) 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 { } else {
p.dontUpdateVolume = false 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) p.SetVolume(p.volume)
if p.reverseOrder { if p.reverseOrder {
@ -186,10 +246,10 @@ loop:
// Calm down music // Calm down music
loopcalm: 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) timer := time.NewTimer(40 * time.Millisecond)
p.volume -= 256 p.volume -= 768
p.SetVolume(p.volume) p.SetVolume(p.volume)
select { select {
@ -214,6 +274,11 @@ loopcalm:
CommonPlayer = nil CommonPlayer = nil
} }
// TODO: Start Routine if any
for _, r := range p.endRoutines {
go r.Launch(cfg)
}
} }
func (p *Player) NextTrack() { func (p *Player) NextTrack() {
@ -223,7 +288,11 @@ func (p *Player) NextTrack() {
} }
func (p *Player) SetVolume(volume uint16) error { 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() return cmd.Run()
} }

9
player/player_pulse.go Normal file
View File

@ -0,0 +1,9 @@
//go:build pulse
package player
const (
playCommand = "paplay"
mixerCard = "pulse"
mixerName = "Master"
)

View File

@ -1,5 +1,15 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$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": [ "packageRules": [
{ {
"matchPackageNames": ["alpine", "github.com/gin-gonic/gin"], "matchPackageNames": ["alpine", "github.com/gin-gonic/gin"],

1
ui/.gitignore vendored
View File

@ -2,6 +2,7 @@
node_modules node_modules
/build /build
/.svelte-kit /.svelte-kit
/.vite
/package /package
.env .env
.env.* .env.*

152
ui/nojs.go Normal file
View 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")
})
}

View File

@ -0,0 +1,4 @@
<h1>
Une erreur inattendue s'est produite&nbsp;:
{{ .errmsg }}
</h1>

View File

@ -0,0 +1,81 @@
<h1 style="margin-bottom: 0">
Prochain réveil le&nbsp;:
{{ 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>

View 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

File diff suppressed because it is too large Load Diff

View File

@ -12,31 +12,31 @@
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ." "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^1.0.0-next.18", "@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/adapter-static": "^1.0.0-next.26", "@sveltejs/kit": "^2.0.0",
"@sveltejs/kit": "^1.0.0-next.260", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^8.0.0",
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"bootstrap-icons": "^1.8.0", "bootstrap-icons": "^1.8.0",
"bootswatch": "^5.1.3", "bootswatch": "^5.1.3",
"eslint": "^8.0.0", "eslint": "^9.0.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte": "^2.33.0",
"prettier": "^2.4.1", "prettier": "^3.1.1",
"prettier-plugin-svelte": "^2.6.0", "prettier-plugin-svelte": "^3.1.2",
"svelte": "^3.46.4", "svelte": "^5.0.0",
"svelte-check": "^2.4.2", "svelte-check": "^4.0.0",
"svelte-preprocess": "^4.10.2", "svelte-preprocess": "^6.0.0",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"typescript": "^4.5.5" "typescript": "^5.0.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"sass": "^1.49.7", "sass": "^1.49.7",
"sass-loader": "^13.0.0", "sass-loader": "^16.0.0",
"sveltestrap": "^5.8.3", "@sveltestrap/sveltestrap": "^6.0.0",
"vite": "^3.0.0" "vite": "^6.0.0"
} }
} }

View File

@ -58,8 +58,10 @@ func DeclareRoutes(router *gin.Engine, cfg *config.Config) {
router.GET("/.svelte-kit/*_", serveOrReverse("", cfg)) router.GET("/.svelte-kit/*_", serveOrReverse("", cfg))
router.GET("/node_modules/*_", serveOrReverse("", cfg)) router.GET("/node_modules/*_", serveOrReverse("", cfg))
router.GET("/@vite/*_", serveOrReverse("", cfg)) router.GET("/@vite/*_", serveOrReverse("", cfg))
router.GET("/@id/*_", serveOrReverse("", cfg))
router.GET("/@fs/*_", serveOrReverse("", cfg)) router.GET("/@fs/*_", serveOrReverse("", cfg))
router.GET("/src/*_", serveOrReverse("", cfg)) router.GET("/src/*_", serveOrReverse("", cfg))
router.GET("/home/*_", serveOrReverse("", cfg))
} }
router.GET("/", serveOrReverse("", cfg)) router.GET("/", serveOrReverse("", cfg))

View File

@ -14,6 +14,7 @@
%sveltekit.head% %sveltekit.head%
</head> </head>
<body class="flex-fill d-flex flex-column"> <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> <div class="flex-fill d-flex flex-column justify-content-between" style="min-height: 100%">%sveltekit.body%</div>
</body> </body>
</html> </html>

22
ui/src/lib/Toaster.svelte Normal file
View 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>

View File

@ -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() { toggleEnable() {
this.enabled = !this.enabled; this.enabled = !this.enabled;
this.save(); this.save();
@ -50,7 +62,12 @@ export class Action {
export async function getActions() { export async function getActions() {
const res = await fetch(`api/actions`, {headers: {'Accept': 'application/json'}}) const res = await fetch(`api/actions`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) { 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 { } else {
throw new Error((await res.json()).errmsg); throw new Error((await res.json()).errmsg);
} }

View File

@ -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() { export async function alarmStop() {
const res = await fetch('api/alarm', { const res = await fetch('api/alarm', {
method: 'DELETE', method: 'DELETE',

View File

@ -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.id = id;
this.weekday = weekday; this.weekday = weekday;
this.time = time; this.time = time;
this.routines = routines; this.routines = routines == null ? [] : routines;
this.ignore_exceptions = ignore_exceptions; this.ignore_exceptions = ignore_exceptions;
this.comment = comment; this.comment = comment;
this.disabled = disabled == true;
this.enable_federation = enable_federation == true;
if (excepts !== undefined)
this.excepts = excepts; this.excepts = excepts;
if (next_time !== undefined)
this.next_time = next_time; this.next_time = next_time;
if (this.routines.length < 1) {
this.routines.push("");
}
} }
async delete() { async delete() {

View File

@ -5,11 +5,16 @@ export class AlarmSingle {
} }
} }
update({ id, time, routines, comment }) { update({ id, time, routines, comment, enable_federation }) {
this.id = id; this.id = id;
this.time = new Date(time); this.time = new Date(time);
this.routines = routines; this.routines = routines == null ? [] : routines;
this.comment = comment; this.comment = comment;
this.enable_federation = enable_federation == true;
if (this.routines.length < 1) {
this.routines.push("");
}
} }
async delete() { async delete() {

View File

@ -5,7 +5,7 @@
Button, Button,
Icon, Icon,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { actions } from '$lib/stores/actions'; import { actions } from '$lib/stores/actions';

View File

@ -5,7 +5,7 @@
Button, Button,
Icon, Icon,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import DateRangeFormat from '$lib/components/DateRangeFormat.svelte'; import DateRangeFormat from '$lib/components/DateRangeFormat.svelte';
import { alarmsExceptions } from '$lib/stores/alarmexceptions'; import { alarmsExceptions } from '$lib/stores/alarmexceptions';

View File

@ -5,7 +5,7 @@
Button, Button,
Icon, Icon,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { weekdayStr } from '$lib/alarmrepeated'; import { weekdayStr } from '$lib/alarmrepeated';
import { alarmsRepeated } from '$lib/stores/alarmrepeated'; import { alarmsRepeated } from '$lib/stores/alarmrepeated';
@ -35,6 +35,8 @@
href="alarms/repeated/{alarm.id}" href="alarms/repeated/{alarm.id}"
class="list-group-item list-group-item-action" class="list-group-item list-group-item-action"
class:active={$page.params.kind === "repeated" && $page.params.aid === alarm.id} 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} Les {weekdayStr(alarm.weekday)}s à {alarm.time}
</a> </a>

View File

@ -5,7 +5,7 @@
Button, Button,
Icon, Icon,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import DateFormat from '$lib/components/DateFormat.svelte'; import DateFormat from '$lib/components/DateFormat.svelte';
import { alarmsSingle } from '$lib/stores/alarmsingle'; import { alarmsSingle } from '$lib/stores/alarmsingle';

View File

@ -11,7 +11,7 @@
ListGroupItem, ListGroupItem,
Row, Row,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { actions_idx } from '$lib/stores/actions'; import { actions_idx } from '$lib/stores/actions';
@ -27,6 +27,7 @@
color="outline-danger" color="outline-danger"
size="sm" size="sm"
class="float-end ms-1" class="float-end ms-1"
on:click={() => routine.delete()}
> >
<Icon name="trash" /> <Icon name="trash" />
</Button> </Button>
@ -37,6 +38,14 @@
> >
<Icon name="pencil" /> <Icon name="pencil" />
</Button> </Button>
<Button
color="outline-success"
size="sm"
class="float-end ms-1"
on:click={() => routine.launch()}
>
<Icon name="play-fill" />
</Button>
{routine.name} {routine.name}
</CardHeader> </CardHeader>
{#if routine.steps} {#if routine.steps}

View File

@ -5,7 +5,7 @@
ListGroup, ListGroup,
ListGroupItem, ListGroupItem,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
export let awakingList = [ export let awakingList = [
{ {

View File

@ -6,7 +6,7 @@
ListGroup, ListGroup,
ListGroupItem, ListGroupItem,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
export let routinesStats = [ export let routinesStats = [
{ {

View File

@ -4,7 +4,7 @@
CardHeader, CardHeader,
CardBody, CardBody,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
</script> </script>
<Card> <Card>

View File

@ -3,7 +3,7 @@
import { import {
Input, Input,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
export let format = 'YYYY-MM-DD HH:mm'; export let format = 'YYYY-MM-DD HH:mm';
export let date = new Date(); export let date = new Date();

View 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>

View File

@ -6,7 +6,7 @@
Button, Button,
Icon, Icon,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { gongs } from '$lib/stores/gongs'; import { gongs } from '$lib/stores/gongs';

View File

@ -8,7 +8,7 @@
Nav, Nav,
NavItem, NavItem,
NavLink, NavLink,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
const version = fetch('api/version', {headers: {'Accept': 'application/json'}}).then((res) => res.json()) const version = fetch('api/version', {headers: {'Accept': 'application/json'}}).then((res) => res.json())

View File

@ -3,7 +3,7 @@
Toast, Toast,
ToastBody, ToastBody,
ToastHeader, ToastHeader,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { ToastsStore } from '$lib/stores/toasts'; import { ToastsStore } from '$lib/stores/toasts';
</script> </script>

View File

@ -6,7 +6,7 @@
Button, Button,
Icon, Icon,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { tracks } from '$lib/stores/tracks'; import { tracks } from '$lib/stores/tracks';

View File

@ -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() { async save() {
const res = await fetch(this.id?`api/routines/${this.id}`:'api/routines', { const res = await fetch(this.id?`api/routines/${this.id}`:'api/routines', {
method: this.id?'PUT':'POST', method: this.id?'PUT':'POST',
@ -45,7 +57,12 @@ export class Routine {
export async function getRoutines() { export async function getRoutines() {
const res = await fetch(`api/routines`, {headers: {'Accept': 'application/json'}}) const res = await fetch(`api/routines`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) { 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 { } else {
throw new Error((await res.json()).errmsg); throw new Error((await res.json()).errmsg);
} }

View File

@ -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.language = language;
this.gong_interval = gong_interval; this.gong_interval = gong_interval;
this.weather_delay = weather_delay; this.weather_delay = weather_delay;
this.weather_action = weather_action; 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_run_time = max_run_time;
this.max_volume = max_volume;
this.federation = federation;
} }
async save() { async save() {

View File

@ -2,6 +2,18 @@ import { writable } from 'svelte/store';
import { getTracks } from '$lib/track' 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() { function createTracksStore() {
const { subscribe, set, update } = writable({list: null}); const { subscribe, set, update } = writable({list: null});
@ -14,6 +26,7 @@ function createTracksStore() {
refresh: async () => { refresh: async () => {
const list = await getTracks(); const list = await getTracks();
list.sort(cmpTracks);
update((m) => Object.assign(m, {list})); update((m) => Object.assign(m, {list}));
return list; return list;
}, },

View File

@ -1,2 +1 @@
export const ssr = false; export const ssr = false;
export const prerender = true;

View File

@ -4,10 +4,18 @@
import { import {
//Styles, //Styles,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
import Toaster from '$lib/components/Toaster.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> </script>
<svelte:head> <svelte:head>
@ -21,7 +29,7 @@
/> />
<div class="flex-fill d-flex flex-column bg-light"> <div class="flex-fill d-flex flex-column bg-light">
<slot></slot> <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> </div>
<Toaster /> <Toaster />
<Header <Header

View File

@ -3,12 +3,13 @@
Container, Container,
Icon, Icon,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import CycleCounter from '$lib/components/CycleCounter.svelte'; import CycleCounter from '$lib/components/CycleCounter.svelte';
import DateFormat from '$lib/components/DateFormat.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 { getNextAlarm, newNCyclesAlarm } from '$lib/alarmsingle';
import { alarmsExceptions } from '$lib/stores/alarmexceptions';
import { alarmsSingle } from '$lib/stores/alarmsingle'; import { alarmsSingle } from '$lib/stores/alarmsingle';
import { quotes } from '$lib/stores/quotes'; import { quotes } from '$lib/stores/quotes';
@ -39,10 +40,19 @@
reloadIsActiveAlarm().then((isActive) => { reloadIsActiveAlarm().then((isActive) => {
if (isActive) { if (isActive) {
setTimeout(reloadIsActiveAlarm, 10000); setTimeout(reloadIsActiveAlarm, 10000);
setTimeout(reloadIsActiveAlarm, 25000);
} }
}) })
} }
function dropNextAlarm() {
deleteNextAlarm().then(() => {
alarmsExceptions.clear();
alarmsSingle.clear();
reloadNextAlarm();
});
}
let extinctionInProgress = false; let extinctionInProgress = false;
</script> </script>
@ -90,6 +100,13 @@
{:else} {:else}
<DateFormat date={nextalarm} dateStyle="short" timeStyle="long" /> <DateFormat date={nextalarm} dateStyle="short" timeStyle="long" />
{/if} {/if}
<button
class="btn btn-lg btn-link"
title="Supprimer ce prochain réveil"
on:click={dropNextAlarm}
>
<Icon name="x-circle-fill" />
</button>
{/if} {/if}
{/await} {/await}
</div> </div>
@ -98,10 +115,11 @@
<div class="d-flex gap-3 justify-content-center"> <div class="d-flex gap-3 justify-content-center">
<a <a
href="alarms/single/new" 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" /> <Icon name="node-plus" />
Programmer un nouveau réveil Programmer <span class="d-none d-lg-inline">un nouveau réveil</span>
</a> </a>
<button <button
class="btn btn-info" class="btn btn-info"
@ -119,10 +137,11 @@
</button> </button>
<button <button
class="btn btn-outline-warning" class="btn btn-outline-warning"
on:click={() => { runAlarm(); reloadIsActiveAlarm(); }} title="Lancer le réveil"
on:click={() => { runAlarm(); setTimeout(reloadIsActiveAlarm, 500); }}
> >
<Icon name="play-circle" /> <Icon name="play-circle" />
Lancer le réveil Lancer <span class="d-none d-lg-inline">le réveil</span>
</button> </button>
</div> </div>
{:else} {:else}

View File

@ -4,7 +4,7 @@
Container, Container,
Row, Row,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import AlarmSingleList from '$lib/components/AlarmSingleList.svelte'; import AlarmSingleList from '$lib/components/AlarmSingleList.svelte';
import AlarmRepeatedList from '$lib/components/AlarmRepeatedList.svelte'; import AlarmRepeatedList from '$lib/components/AlarmRepeatedList.svelte';

View File

@ -5,7 +5,7 @@
Container, Container,
Row, Row,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { page } from '$app/stores'; import { page } from '$app/stores';

View File

@ -3,7 +3,7 @@
import { import {
Container, Container,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import AlarmSingleList from '$lib/components/AlarmSingleList.svelte'; import AlarmSingleList from '$lib/components/AlarmSingleList.svelte';
import AlarmRepeatedList from '$lib/components/AlarmRepeatedList.svelte'; import AlarmRepeatedList from '$lib/components/AlarmRepeatedList.svelte';

View File

@ -4,11 +4,12 @@
Col, Col,
Container, Container,
Icon, Icon,
Input,
ListGroup, ListGroup,
ListGroupItem, ListGroupItem,
Row, Row,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
@ -94,6 +95,10 @@
<ListGroupItem> <ListGroupItem>
<strong>Heure du réveil</strong> <DateFormat date={alarm.time} timeStyle="long" /> <strong>Heure du réveil</strong> <DateFormat date={alarm.time} timeStyle="long" />
</ListGroupItem> </ListGroupItem>
<ListGroupItem class="d-flex">
<strong>Fédération activée&nbsp;?</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> </ListGroup>
{/await} {/await}
{:else if $page.params["kind"] == "repeated"} {:else if $page.params["kind"] == "repeated"}
@ -117,12 +122,21 @@
<ListGroupItem> <ListGroupItem>
<strong>Heure du réveil</strong> {alarm.time} <strong>Heure du réveil</strong> {alarm.time}
</ListGroupItem> </ListGroupItem>
<ListGroupItem> <ListGroupItem class="d-flex">
<strong>Ignorer les exceptions&nbsp;?</strong> {alarm.ignore_exceptions?"oui":"non"} <strong>Alarme active&nbsp;?</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&nbsp;?</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&nbsp;?</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> </ListGroupItem>
{#if alarm.next_time} {#if alarm.next_time}
<ListGroupItem> <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> </ListGroupItem>
{/if} {/if}
</ListGroup> </ListGroup>

View File

@ -13,7 +13,7 @@
Label, Label,
Row, Row,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import DateTimeInput from '$lib/components/DateTimeInput.svelte'; import DateTimeInput from '$lib/components/DateTimeInput.svelte';
import { AlarmSingle } from '$lib/alarmsingle'; import { AlarmSingle } from '$lib/alarmsingle';
@ -37,15 +37,22 @@
let obj; let obj;
const vtime = new Date(Date.now() + 7.6*3600000);
switch($page.params["kind"]) { switch($page.params["kind"]) {
case "single": case "single":
obj = new AlarmSingle(); obj = new AlarmSingle();
obj.time = vtime;
break; break;
case "repeated": case "repeated":
obj = new AlarmRepeated(); obj = new AlarmRepeated();
obj.weekday = vtime.getDay();
obj.time = (vtime.getHours() < 10 ? "0" : "") + vtime.getHours() + ":" + (vtime.getMinutes() < 10 ? "0" : "") + vtime.getMinutes();
break; break;
case "exceptions": case "exceptions":
obj = new AlarmException(); 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; break;
} }
@ -147,6 +154,11 @@
<Input id="exceptionEnd" type="date" required bind:value={obj.end} /> <Input id="exceptionEnd" type="date" required bind:value={obj.end} />
</FormGroup> </FormGroup>
{/if} {/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> <FormGroup>
<Label for="comment">Commentaire</Label> <Label for="comment">Commentaire</Label>

View File

@ -4,7 +4,7 @@
Container, Container,
Row, Row,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import CardStatAlarms from '$lib/components/CardStatAlarms.svelte'; import CardStatAlarms from '$lib/components/CardStatAlarms.svelte';
import CardStatTimeAwaking from '$lib/components/CardStatTimeAwaking.svelte'; import CardStatTimeAwaking from '$lib/components/CardStatTimeAwaking.svelte';

View File

@ -4,7 +4,7 @@
Container, Container,
Row, Row,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import MusiksLastPlayedList from '$lib/components/MusiksLastPlayedList.svelte'; import MusiksLastPlayedList from '$lib/components/MusiksLastPlayedList.svelte';
import TrackList from '$lib/components/TrackList.svelte'; import TrackList from '$lib/components/TrackList.svelte';

View File

@ -5,7 +5,7 @@
Container, Container,
Row, Row,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import GongsList from '$lib/components/GongsList.svelte'; import GongsList from '$lib/components/GongsList.svelte';
</script> </script>

View File

@ -2,7 +2,7 @@
import { import {
Container, Container,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import TrackList from '$lib/components/TrackList.svelte'; import TrackList from '$lib/components/TrackList.svelte';
</script> </script>

View File

@ -10,7 +10,7 @@
ListGroup, ListGroup,
ListGroupItem, ListGroupItem,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { getGong } from '$lib/gong'; import { getGong } from '$lib/gong';
import { gongs } from '$lib/stores/gongs'; import { gongs } from '$lib/stores/gongs';

View File

@ -10,7 +10,7 @@
ListGroup, ListGroup,
ListGroupItem, ListGroupItem,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { gongs } from '$lib/stores/gongs'; import { gongs } from '$lib/stores/gongs';
import { uploadGong } from '$lib/gong'; import { uploadGong } from '$lib/gong';

View File

@ -5,7 +5,7 @@
Container, Container,
Row, Row,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import TrackList from '$lib/components/TrackList.svelte'; import TrackList from '$lib/components/TrackList.svelte';
</script> </script>

View File

@ -2,7 +2,7 @@
import { import {
Container, Container,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import TrackList from '$lib/components/TrackList.svelte'; import TrackList from '$lib/components/TrackList.svelte';
</script> </script>

View File

@ -10,7 +10,7 @@
ListGroup, ListGroup,
ListGroupItem, ListGroupItem,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { getTrack } from '$lib/track'; import { getTrack } from '$lib/track';
import { tracks } from '$lib/stores/tracks'; import { tracks } from '$lib/stores/tracks';

View File

@ -10,7 +10,7 @@
ListGroup, ListGroup,
ListGroupItem, ListGroupItem,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { tracks } from '$lib/stores/tracks'; import { tracks } from '$lib/stores/tracks';
import { uploadTrack } from '$lib/track'; import { uploadTrack } from '$lib/track';

View File

View File

@ -6,7 +6,7 @@
Icon, Icon,
Row, Row,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { routines } from '$lib/stores/routines'; import { routines } from '$lib/stores/routines';

View File

@ -5,7 +5,7 @@
Container, Container,
Row, Row,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import ActionList from '$lib/components/ActionList.svelte'; import ActionList from '$lib/components/ActionList.svelte';
</script> </script>

View File

@ -2,7 +2,7 @@
import { import {
Container, Container,
Icon, Icon,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import ActionList from '$lib/components/ActionList.svelte'; import ActionList from '$lib/components/ActionList.svelte';
</script> </script>

View File

@ -3,13 +3,22 @@
import { import {
Container, Container,
Icon,
Input, Input,
ListGroup, ListGroup,
ListGroupItem, ListGroupItem,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { getAction } from '$lib/action'; import { getAction } from '$lib/action';
import { actions } from '$lib/stores/actions';
function deleteThis(action) {
action.delete().then(() => {
actions.refresh();
goto('routines/actions/');
})
}
</script> </script>
{#await getAction($page.params.aid)} {#await getAction($page.params.aid)}
@ -34,5 +43,26 @@
<Input type="switch" on:change={() => action.toggleEnable()} checked={action.enabled} /> <Input type="switch" on:change={() => action.toggleEnable()} checked={action.enabled} />
</ListGroupItem> </ListGroupItem>
</ListGroup> </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> </Container>
{/await} {/await}

View File

@ -1,4 +1,6 @@
<script> <script>
import { tick } from 'svelte';
import { import {
Container, Container,
Form, Form,
@ -9,23 +11,29 @@
InputGroupText, InputGroupText,
Label, Label,
Spinner, Spinner,
} from 'sveltestrap'; } from '@sveltestrap/sveltestrap';
import { actions } from '$lib/stores/actions'; import { actions } from '$lib/stores/actions';
import { getSettings } from '$lib/settings'; import { getSettings } from '$lib/settings';
import FederationSettings from '$lib/components/FederationSettings.svelte';
let settingsP = getSettings(); let settingsP = getSettings();
$: settingsP.then((s) => settings = s); $: settingsP.then((s) => settings = s);
let settings; let settings;
async function submitSettings() {
await tick();
settings.save();
}
</script> </script>
<Container class="flex-fill d-flex flex-column py-2"> <Container class="flex-fill d-flex flex-column py-2">
<h2> <h2>
Paramètres Paramètres
</h2> </h2>
<Form> <Form on:submit={submitSettings}>
{#await settingsP} {#await settingsP}
<div class="d-flex justify-content-center align-items-center gap-2"> <div class="d-flex justify-content-center align-items-center gap-2">
<Spinner color="primary" /> Chargement en cours&hellip; <Spinner color="primary" /> Chargement en cours&hellip;
@ -39,7 +47,7 @@
id="gongIntervals" id="gongIntervals"
placeholder="20" placeholder="20"
bind:value={settings.gong_interval} bind:value={settings.gong_interval}
on:change={() => settings.save()} on:input={submitSettings}
/> />
<InputGroupText>min</InputGroupText> <InputGroupText>min</InputGroupText>
</InputGroup> </InputGroup>
@ -53,7 +61,7 @@
id="weatherDelay" id="weatherDelay"
placeholder="5" placeholder="5"
bind:value={settings.weather_delay} bind:value={settings.weather_delay}
on:change={() => settings.save()} on:input={submitSettings}
/> />
<InputGroupText>min</InputGroupText> <InputGroupText>min</InputGroupText>
</InputGroup> </InputGroup>
@ -66,7 +74,7 @@
type="select" type="select"
id="weatherRituel" id="weatherRituel"
bind:value={settings.weather_action} bind:value={settings.weather_action}
on:change={() => settings.save()} on:input={submitSettings}
> >
{#each $actions.list as action (action.id)} {#each $actions.list as action (action.id)}
<option value="{action.path}">{action.name}</option> <option value="{action.path}">{action.name}</option>
@ -81,13 +89,47 @@
{/if} {/if}
</FormGroup> </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&hellip;
</div>
{/if}
</FormGroup>
<FormGroup> <FormGroup>
<Label for="greetingLanguage">Langue de salutation</Label> <Label for="greetingLanguage">Langue de salutation</Label>
<Input <Input
type="select" type="select"
id="greetingLanguage" id="greetingLanguage"
bind:value={settings.lang} bind:value={settings.language}
on:change={() => settings.save()} on:input={submitSettings}
> >
<option value="fr_FR">Français</option> <option value="fr_FR">Français</option>
<option value="en_US">Anglais</option> <option value="en_US">Anglais</option>
@ -104,11 +146,34 @@
id="maxRunTime" id="maxRunTime"
placeholder="60" placeholder="60"
bind:value={settings.max_run_time} bind:value={settings.max_run_time}
on:change={() => settings.save()} on:input={submitSettings}
/> />
<InputGroupText>min</InputGroupText> <InputGroupText>min</InputGroupText>
</InputGroup> </InputGroup>
</FormGroup> </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} {/await}
</Form> </Form>
</Container> </Container>

View File

@ -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);
})()
);
}
});

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"short_name": "Gustus", "short_name": "Réveil",
"name": "Gustus", "name": "Réveil",
"version": "0.1", "version": "0.1",
"author": "nemucorp", "author": "nemucorp",
"start_url": "/", "start_url": "/",
@ -12,9 +12,9 @@
"sizes": "512x512" "sizes": "512x512"
} }
], ],
"background_color": "#d62a49", "background_color": "#e83e8c",
"display": "standalone", "display": "standalone",
"scope": "/", "scope": "/",
"theme_color": "#ffffff", "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
View File

View File

@ -9,11 +9,8 @@ const config = {
kit: { kit: {
adapter: adapter({ adapter: adapter({
fallback: '404.html' fallback: 'index.html'
}), }),
paths: {
// base: '{{.urlbase}}',
}
} }
}; };

View File

@ -1,32 +1,3 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "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"]
} }

View File

@ -2,6 +2,12 @@ import { sveltekit } from '@sveltejs/kit/vite';
/** @type {import('vite').UserConfig} */ /** @type {import('vite').UserConfig} */
const config = { const config = {
server: {
hmr: {
port: 10000
}
},
plugins: [sveltekit()] plugins: [sveltekit()]
}; };