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:
- name: build front
image: node:18-alpine
image: node:22
commands:
- mkdir deploy
- cd ui
- npm install --network-timeout=100000
- sed -i 's!@popperjs/core/dist/esm/popper!@popperjs/core!' node_modules/sveltestrap/src/*.js node_modules/sveltestrap/src/*.svelte
- npm run build
- tar chjf ../deploy/static.tar.bz2 build
@ -27,39 +26,53 @@ steps:
commands:
- apk --no-cache add alsa-lib-dev build-base git pkgconf
- go get -v -d
- go vet -v
- go build -v -ldflags '-w -X main.Version="${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
- go vet -v -tags pulse
- go build -tags pulse -ldflags '-w -X main.Version=${DRONE_BRANCH}-${DRONE_COMMIT} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
- ln deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} reveil
when:
event:
exclude:
- tag
- name: build tag
- name: build armv7 tag
image: golang:1-alpine
commands:
- apk --no-cache add alsa-lib-dev build-base git pkgconf
- go get -v -d
- go vet -v
- go build -v -ldflags '-w -X main.Version="${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
- ln deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} reveil
- go vet -v -tags pulse
- go build -tags pulse -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}v7
- ln deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}v7 reveil
when:
event:
- tag
- name: build armv6 tag
image: golang:1-alpine
commands:
- apk --no-cache add alsa-lib-dev build-base git pkgconf
- go build -tags pulse,netgo -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}hf
environment:
CGO_ENABLED: 0
GOARM: 6
when:
event:
- tag
- name: gitea release
image: plugins/gitea-release
image: plugins/gitea-release:linux-arm
settings:
api_key:
from_secret: gitea_api_key
base_url: https://git.nemunai.re/
files: deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
files:
- deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}hf
- deploy/reveil-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}v7
when:
event:
- tag
- name: docker
image: plugins/docker
image: plugins/docker:linux-arm
settings:
registry: registry.nemunai.re
repo: registry.nemunai.re/reveil

View File

@ -1,11 +1,10 @@
FROM node:18-alpine as nodebuild
FROM node:22-alpine as nodebuild
WORKDIR /ui
COPY ui/ .
RUN npm install --network-timeout=100000 && \
sed -i 's!@popperjs/core/dist/esm/popper!@popperjs/core!' node_modules/sveltestrap/src/*.js node_modules/sveltestrap/src/*.svelte && \
npm run build
@ -16,10 +15,10 @@ RUN apk --no-cache add git go-bindata
COPY . /go/src/git.nemunai.re/nemunaire/reveil
COPY --from=nodebuild /ui/build /go/src/git.nemunai.re/nemunaire/reveil/ui/build
WORKDIR /go/src/git.nemunai.re/nemunaire/reveil
RUN go get -v && go generate -v && go build -v -ldflags="-s -w"
RUN go get -v && go generate -v && go build -tags pulse -ldflags="-s -w"
FROM alpine:3.16
FROM alpine:3.20
VOLUME /data
WORKDIR /data

View File

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

View File

@ -2,6 +2,7 @@ package api
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
@ -90,4 +91,29 @@ func declareActionsRoutes(cfg *config.Config, router *gin.RouterGroup) {
c.JSON(http.StatusOK, nil)
})
actionsRoutes.POST("/run", func(c *gin.Context) {
action := c.MustGet("action").(*reveil.Action)
settings, err := reveil.ReadSettings(cfg.SettingsFile)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to run the action: unable to read settings: %s", err.Error())})
return
}
cmd, err := action.Launch(settings)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to run the action: %s", err.Error())})
return
}
go func() {
err := cmd.Wait()
if err != nil {
log.Printf("%q: %s", action.Name, err.Error())
}
}()
c.JSON(http.StatusOK, true)
})
}

View File

@ -1,11 +1,14 @@
package api
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
"git.nemunai.re/nemunaire/reveil/config"
"git.nemunai.re/nemunaire/reveil/model"
"git.nemunai.re/nemunaire/reveil/player"
)
@ -20,7 +23,7 @@ func declareAlarmRoutes(cfg *config.Config, router *gin.RouterGroup) {
router.POST("/alarm/run", func(c *gin.Context) {
if player.CommonPlayer == nil {
err := player.WakeUp(cfg)
err := player.WakeUp(cfg, nil, true)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
@ -51,6 +54,21 @@ func declareAlarmRoutes(cfg *config.Config, router *gin.RouterGroup) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
settings, err := reveil.ReadSettings(cfg.SettingsFile)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("Unable to read settings: %s", err.Error())})
return
}
for k, srv := range settings.Federation {
err = srv.WakeStop()
if err != nil {
log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error())
} else {
log.Printf("Federated wakeup on %s: launched!", k)
}
}
}
c.JSON(http.StatusOK, true)

View File

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

176
api/federation.go Normal file
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)
declareAlarmRoutes(cfg, apiRoutes)
declareAlarmsRoutes(cfg, db, resetTimer, apiRoutes)
declareFederationRoutes(cfg, apiRoutes)
declareGongsRoutes(cfg, apiRoutes)
declareHistoryRoutes(cfg, apiRoutes)
declareQuotesRoutes(cfg, apiRoutes)

View File

@ -78,4 +78,12 @@ func declareRoutinesRoutes(cfg *config.Config, router *gin.RouterGroup) {
c.JSON(http.StatusOK, nil)
})
routinesRoutes.POST("/run", func(c *gin.Context) {
routine := c.MustGet("routine").(*reveil.Routine)
go routine.Launch(cfg)
c.JSON(http.StatusOK, true)
})
}

44
app.go
View File

@ -21,6 +21,7 @@ type App struct {
router *gin.Engine
srv *http.Server
nextAlarm *time.Timer
nextPreAlarm *time.Timer
}
func NewApp(cfg *config.Config) *App {
@ -49,6 +50,7 @@ func NewApp(cfg *config.Config) *App {
// Register routes
ui.DeclareRoutes(router, cfg)
ui.DeclareNoJSRoutes(router, cfg, db, app.ResetTimer)
api.DeclareRoutes(router, cfg, db, app.ResetTimer)
router.GET("/api/version", func(c *gin.Context) {
@ -75,14 +77,52 @@ func (app *App) Start() {
func (app *App) ResetTimer() {
if app.nextAlarm != nil {
app.nextAlarm.Stop()
app.nextPreAlarm = nil
app.nextAlarm = nil
}
if na, err := reveil.GetNextAlarm(app.db); err == nil && na != nil {
settings, _ := reveil.ReadSettings(app.cfg.SettingsFile)
if na, routines, federated, err := reveil.GetNextAlarm(app.cfg, app.db); err == nil && na != nil {
if settings != nil && settings.PreAlarmAction != "" {
app.nextPreAlarm = time.AfterFunc(time.Until(*na)-settings.PreAlarmActionDelay*time.Minute, func() {
app.nextPreAlarm = nil
settings, err := reveil.ReadSettings(app.cfg.SettingsFile)
if err != nil {
log.Println("Unable to read settings:", err.Error())
return
}
action, err := reveil.LoadAction(app.cfg, settings.PreAlarmAction)
if err != nil {
log.Println("Unable to load pre-alarm action:", err.Error())
}
cmd, err := action.Launch(settings)
if err != nil {
log.Println(err.Error())
return
}
go func() {
err := cmd.Wait()
if err != nil {
log.Printf("%q: %s", action.Name, err.Error())
}
}()
})
log.Println("Next pre-alarm programmed for", time.Time(*na).Add(settings.PreAlarmActionDelay*-1*time.Minute))
}
app.nextAlarm = time.AfterFunc(time.Until(*na), func() {
app.nextPreAlarm = nil
app.nextAlarm = nil
reveil.RemoveOldAlarmsSingle(app.db)
err := player.WakeUp(app.cfg)
// Rearm timer for the next time
app.ResetTimer()
err := player.WakeUp(app.cfg, routines, federated)
if err != nil {
log.Println(err.Error())
return

View File

@ -3,6 +3,7 @@ package config
import (
"encoding/base64"
"net/url"
"time"
)
type JWTSecretKey []byte
@ -42,3 +43,33 @@ func (i *URL) Set(value string) error {
i.URL = u
return nil
}
type Timezone struct {
tz *time.Location
}
func (tz *Timezone) GetLocation() *time.Location {
if tz.tz != nil {
return tz.tz
} else {
return time.Local
}
}
func (tz *Timezone) String() string {
if tz.tz != nil {
return tz.tz.String()
} else {
return time.Local.String()
}
}
func (tz *Timezone) Set(value string) error {
newtz, err := time.LoadLocation(value)
if err != nil {
return err
}
tz.tz = newtz
return nil
}

View File

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

40
go.mod
View File

@ -4,37 +4,47 @@ go 1.18
require (
github.com/faiface/beep v1.1.0
github.com/gin-gonic/gin v1.8.1
github.com/gin-gonic/gin v1.10.0
github.com/syndtr/goleveldb v1.0.0
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/hajimehoshi/go-mp3 v0.3.0 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect
github.com/icza/bitio v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mewkiz/flac v1.0.7 // indirect
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

99
go.sum
View File

@ -1,4 +1,18 @@
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -7,12 +21,20 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
@ -20,12 +42,22 @@ github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBY
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@ -49,6 +81,12 @@ github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
@ -59,9 +97,17 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
@ -69,6 +115,8 @@ github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXi
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -78,6 +126,10 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -88,19 +140,43 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
@ -113,6 +189,10 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -122,20 +202,35 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@ -151,3 +246,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -4,9 +4,11 @@ import (
"bufio"
"crypto/sha512"
"errors"
"fmt"
"io/fs"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
@ -19,9 +21,10 @@ type Action struct {
Description string `json:"description,omitempty"`
Path string `json:"path"`
Enabled bool `json:"enabled"`
fullPath string
}
func LoadAction(path string) (string, string, error) {
func loadAction(path string) (string, string, error) {
fd, err := os.Open(path)
if err != nil {
return "", "", err
@ -64,6 +67,45 @@ func LoadAction(path string) (string, string, error) {
return name, description, nil
}
func LoadAction(cfg *config.Config, path string) (*Action, error) {
actionsDir, err := filepath.Abs(cfg.ActionsDir)
if err != nil {
return nil, err
}
path = filepath.Join(actionsDir, path)
d, err := os.Stat(path)
if err != nil {
return nil, err
}
if !d.Mode().IsRegular() {
return nil, fmt.Errorf("%q is not a file, it cannot be an action.", path)
}
hash := sha512.Sum512([]byte(path))
// Parse content
name, description, err := loadAction(path)
if err != nil {
return nil, fmt.Errorf("Invalid action file (trying to parse %s): %s", path, err.Error())
}
if apath, err := filepath.Abs(path); err == nil {
path = apath
}
return &Action{
Id: hash[:],
Name: name,
Description: description,
Path: strings.TrimPrefix(path, actionsDir+"/"),
Enabled: d.Mode().Perm()&0111 != 0,
fullPath: path,
}, nil
}
func LoadActions(cfg *config.Config) (actions []*Action, err error) {
actionsDir, err := filepath.Abs(cfg.ActionsDir)
if err != nil {
@ -75,7 +117,7 @@ func LoadActions(cfg *config.Config) (actions []*Action, err error) {
hash := sha512.Sum512([]byte(path))
// Parse content
name, description, err := LoadAction(path)
name, description, err := loadAction(path)
if err != nil {
log.Printf("Invalid action file (trying to parse %s): %s", path, err.Error())
// Ignore invalid files
@ -96,6 +138,7 @@ func LoadActions(cfg *config.Config) (actions []*Action, err error) {
Description: description,
Path: strings.TrimPrefix(path, actionsDir+"/"),
Enabled: d.Mode().Perm()&0111 != 0,
fullPath: path,
})
}
@ -130,3 +173,10 @@ func (a *Action) Disable() error {
func (a *Action) Remove() error {
return os.Remove(a.Path)
}
func (a *Action) Launch(settings *Settings) (cmd *exec.Cmd, err error) {
cmd = exec.Command(a.fullPath)
cmd.Env = append(cmd.Environ(), fmt.Sprintf("LANG=%s", settings.Language))
err = cmd.Start()
return
}

View File

@ -4,6 +4,8 @@ import (
"fmt"
"sort"
"time"
"git.nemunai.re/nemunaire/reveil/config"
)
type Date time.Time
@ -38,33 +40,95 @@ func (h *Hour) UnmarshalJSON(src []byte) error {
return nil
}
func GetNextAlarm(db *LevelDBStorage) (*time.Time, error) {
func GetNextAlarm(cfg *config.Config, db *LevelDBStorage) (*time.Time, []Identifier, bool, error) {
alarmsRepeated, err := GetAlarmsRepeated(db)
if err != nil {
return nil, err
return nil, nil, false, err
}
var closestAlarm *time.Time
var closestAlarmRoutines []Identifier
var closestAlarmFederated bool
for _, alarm := range alarmsRepeated {
next := alarm.GetNextOccurence(db)
next := alarm.GetNextOccurence(cfg, db)
if next != nil && (closestAlarm == nil || closestAlarm.After(*next)) {
closestAlarm = next
closestAlarmRoutines = alarm.FollowingRoutines
closestAlarmFederated = alarm.EnableFederation
}
}
alarmsSingle, err := GetAlarmsSingle(db)
if err != nil {
return nil, err
return nil, nil, false, err
}
now := time.Now()
for _, alarm := range alarmsSingle {
if closestAlarm == nil || (closestAlarm.After(alarm.Time) && alarm.Time.After(now)) {
closestAlarm = &alarm.Time
closestAlarmRoutines = alarm.FollowingRoutines
closestAlarmFederated = alarm.EnableFederation
}
}
return closestAlarm, nil
return closestAlarm, closestAlarmRoutines, closestAlarmFederated, nil
}
func GetNextException(cfg *config.Config, db *LevelDBStorage) (*time.Time, error) {
alarmsExceptions, err := GetAlarmExceptions(db)
if err != nil {
return nil, err
}
var closestException *time.Time
for _, except := range alarmsExceptions {
if except != nil && time.Time(*except.End).After(time.Now()) && (closestException == nil || closestException.After(time.Time(*except.Start))) {
tmp := time.Time(*except.Start)
closestException = &tmp
}
}
return closestException, nil
}
func DropNextAlarm(cfg *config.Config, db *LevelDBStorage) error {
timenext, _, _, err := GetNextAlarm(cfg, db)
if err != nil {
return err
}
alarmsRepeated, err := GetAlarmsRepeated(db)
if err != nil {
return err
}
for _, alarm := range alarmsRepeated {
next := alarm.GetNextOccurence(cfg, db)
if next != nil && *next == *timenext {
start := Date(*next)
stop := Date((*next).Add(time.Second))
return PutAlarmException(db, &AlarmException{
Start: &start,
End: &stop,
Comment: fmt.Sprintf("Automatic exception to cancel recurrent alarm %s", next.Format("Mon at 15:04")),
})
}
}
alarmsSingle, err := GetAlarmsSingle(db)
if err != nil {
return err
}
for _, alarm := range alarmsSingle {
if alarm.Time == *timenext {
return DeleteAlarmSingle(db, alarm)
}
}
return fmt.Errorf("Unable to find the next alarm")
}
type Exceptions []time.Time
@ -88,11 +152,13 @@ type AlarmRepeated struct {
FollowingRoutines []Identifier `json:"routines"`
IgnoreExceptions bool `json:"ignore_exceptions"`
Comment string `json:"comment,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Excepts Exceptions `json:"excepts,omitempty"`
NextTime *time.Time `json:"next_time,omitempty"`
EnableFederation bool `json:"enable_federation,omitempty"`
}
func (a *AlarmRepeated) FillExcepts(db *LevelDBStorage) error {
func (a *AlarmRepeated) FillExcepts(cfg *config.Config, db *LevelDBStorage) error {
if a.IgnoreExceptions {
return nil
}
@ -105,15 +171,15 @@ func (a *AlarmRepeated) FillExcepts(db *LevelDBStorage) error {
now := time.Now()
for _, exception := range exceptions {
if now.After(time.Time(*exception.Start)) {
end := time.Time(*exception.End).AddDate(0, 0, 1)
if now.After(end) {
continue
}
end := time.Time(*exception.End).AddDate(0, 0, 1)
for t := time.Time(*exception.Start); end.After(t); t = t.AddDate(0, 0, 1) {
if t.Weekday() == a.Weekday {
a.Excepts = append(a.Excepts, time.Date(t.Year(), t.Month(), t.Day(), time.Time(*a.StartTime).Hour(), time.Time(*a.StartTime).Minute(), time.Time(*a.StartTime).Second(), 0, now.Location()))
t.AddDate(0, 0, 6)
a.Excepts = append(a.Excepts, time.Date(t.Year(), t.Month(), t.Day(), time.Time(*a.StartTime).Hour(), time.Time(*a.StartTime).Minute(), time.Time(*a.StartTime).Second(), 0, time.Local))
t = t.AddDate(0, 0, 6)
}
}
}
@ -123,14 +189,18 @@ func (a *AlarmRepeated) FillExcepts(db *LevelDBStorage) error {
return nil
}
func (a *AlarmRepeated) GetNextOccurence(db *LevelDBStorage) *time.Time {
func (a *AlarmRepeated) GetNextOccurence(cfg *config.Config, db *LevelDBStorage) *time.Time {
if a.Disabled {
return nil
}
if len(a.Excepts) == 0 {
a.FillExcepts(db)
a.FillExcepts(cfg, db)
}
now := time.Now()
today := time.Date(now.Year(), now.Month(), now.Day(), time.Time(*a.StartTime).Hour(), time.Time(*a.StartTime).Minute(), time.Time(*a.StartTime).Second(), 0, now.Location())
today := time.Date(now.Year(), now.Month(), now.Day(), time.Time(*a.StartTime).Hour(), time.Time(*a.StartTime).Minute(), time.Time(*a.StartTime).Second(), 0, time.Local)
if now.After(today) {
today = today.AddDate(0, 0, 1)
}
@ -178,16 +248,19 @@ func GetAlarmsRepeated(db *LevelDBStorage) (alarms []*AlarmRepeated, err error)
func PutAlarmRepeated(db *LevelDBStorage, alarm *AlarmRepeated) (err error) {
var key string
var id Identifier
if alarm.Id.IsEmpty() {
var id Identifier
key, id, err = db.findBytesKey("alarm-repeated-", IDENTIFIER_LEN)
if err != nil {
return err
}
alarm.Id = id
} else {
key = fmt.Sprintf("alarm-repeated-%s", alarm.Id.ToString())
}
alarm.Id = id
// Don't store this, this is autocalculated
alarm.Excepts = nil
alarm.NextTime = nil
@ -204,6 +277,7 @@ type AlarmSingle struct {
Time time.Time `json:"time"`
FollowingRoutines []Identifier `json:"routines"`
Comment string `json:"comment,omitempty"`
EnableFederation bool `json:"enable_federation,omitempty"`
}
func GetAlarmSingle(db *LevelDBStorage, id Identifier) (alarm *AlarmSingle, err error) {
@ -232,16 +306,18 @@ func GetAlarmsSingle(db *LevelDBStorage) (alarms []*AlarmSingle, err error) {
func PutAlarmSingle(db *LevelDBStorage, alarm *AlarmSingle) (err error) {
var key string
var id Identifier
if alarm.Id.IsEmpty() {
var id Identifier
key, id, err = db.findBytesKey("alarm-single-", IDENTIFIER_LEN)
if err != nil {
return err
}
}
alarm.Id = id
} else {
key = fmt.Sprintf("alarm-single-%s", alarm.Id.ToString())
}
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) {
var key string
var id Identifier
if alarm.Id.IsEmpty() {
var id Identifier
key, id, err = db.findBytesKey("alarm-exception-", IDENTIFIER_LEN)
if err != nil {
return err
}
}
alarm.Id = id
} else {
key = fmt.Sprintf("alarm-exception-%s", alarm.Id.ToString())
}
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)
if err != nil {
if _, ok := err.(*errors.ErrCorrupted); ok {
log.Println("LevelDB was corrupted; attempting recovery (%s)", err.Error())
log.Printf("LevelDB was corrupted; attempting recovery (%s)", err.Error())
_, err = leveldb.RecoverFile(path, nil)
if err != nil {
return

137
model/federation.go Normal file
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
import (
"bytes"
"crypto/sha512"
"fmt"
"io/fs"
"io/ioutil"
"log"
@ -9,6 +11,7 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
"git.nemunai.re/nemunaire/reveil/config"
)
@ -19,6 +22,10 @@ type RoutineStep struct {
Args []string `json:"args,omitempty"`
}
func (s *RoutineStep) GetAction(cfg *config.Config) (*Action, error) {
return LoadAction(cfg, s.Action)
}
type Routine struct {
Id Identifier `json:"id"`
Name string `json:"name"`
@ -72,6 +79,21 @@ func LoadRoutine(path string, cfg *config.Config) ([]RoutineStep, error) {
return steps, nil
}
func LoadRoutineFromId(id Identifier, cfg *config.Config) (*Routine, error) {
routines, err := LoadRoutines(cfg)
if err != nil {
return nil, err
}
for _, routine := range routines {
if bytes.Equal(routine.Id, id) {
return routine, nil
}
}
return nil, fmt.Errorf("Unable to find routine %x", id)
}
func LoadRoutines(cfg *config.Config) (routines []*Routine, err error) {
err = filepath.Walk(cfg.RoutinesDir, func(path string, d fs.FileInfo, err error) error {
if d.IsDir() && path != cfg.RoutinesDir {
@ -117,3 +139,34 @@ func (r *Routine) Rename(newName string) error {
func (a *Routine) Remove() error {
return os.Remove(a.Path)
}
func (a *Routine) Launch(cfg *config.Config) error {
for _, s := range a.Steps {
act, err := s.GetAction(cfg)
if err != nil {
log.Printf("Unable to get action: %s: %s", s.Action, err.Error())
continue
}
settings, err := ReadSettings(cfg.SettingsFile)
if err != nil {
log.Printf("Unable to read settings: %s", err.Error())
continue
}
time.Sleep(time.Duration(s.Delay) * time.Second)
cmd, err := act.Launch(settings)
if err != nil {
log.Printf("Unable to launch the action %q: %s", s.Action, err.Error())
continue
}
err = cmd.Wait()
if err != nil {
log.Printf("Something goes wrong when waiting for the action %q's end: %s", s.Action, err.Error())
}
}
return nil
}

View File

@ -12,7 +12,11 @@ type Settings struct {
GongInterval time.Duration `json:"gong_interval"`
WeatherDelay time.Duration `json:"weather_delay"`
WeatherAction string `json:"weather_action"`
PreAlarmActionDelay time.Duration `json:"pre_alarm_delay"`
PreAlarmAction string `json:"pre_alarm_action"`
MaxRunTime time.Duration `json:"max_run_time"`
MaxVolume uint16 `json:"max_volume"`
Federation map[string]FederationServer `json:"federation"`
}
// ExistsSettings checks if the settings file can by found at the given path.

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

9
player/player_pulse.go Normal file
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",
"extends": [
"config:base",
"local>iac/renovate-config",
"local>iac/renovate-config//automerge-common"
],
"lockFileMaintenance": {
"enabled": true,
"automerge": true
},
"packageRules": [
{
"matchPackageNames": ["alpine", "github.com/gin-gonic/gin"],

1
ui/.gitignore vendored
View File

@ -2,6 +2,7 @@
node_modules
/build
/.svelte-kit
/.vite
/package
.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=. ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^1.0.0-next.18",
"@sveltejs/adapter-static": "^1.0.0-next.26",
"@sveltejs/kit": "^1.0.0-next.260",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"bootstrap": "^5.1.3",
"bootstrap-icons": "^1.8.0",
"bootswatch": "^5.1.3",
"eslint": "^8.0.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.4.1",
"prettier-plugin-svelte": "^2.6.0",
"svelte": "^3.46.4",
"svelte-check": "^2.4.2",
"svelte-preprocess": "^4.10.2",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-svelte": "^2.33.0",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-preprocess": "^6.0.0",
"tslib": "^2.3.1",
"typescript": "^4.5.5"
"typescript": "^5.0.0"
},
"type": "module",
"dependencies": {
"dayjs": "^1.11.5",
"sass": "^1.49.7",
"sass-loader": "^13.0.0",
"sveltestrap": "^5.8.3",
"vite": "^3.0.0"
"sass-loader": "^16.0.0",
"@sveltestrap/sveltestrap": "^6.0.0",
"vite": "^6.0.0"
}
}

View File

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

View File

@ -14,6 +14,7 @@
%sveltekit.head%
</head>
<body class="flex-fill d-flex flex-column">
<noscript>Si la page ne charge pas, essayez <a href="/nojs.html">la version sans JavaScript</a>.</noscript>
<div class="flex-fill d-flex flex-column justify-content-between" style="min-height: 100%">%sveltekit.body%</div>
</body>
</html>

22
ui/src/lib/Toaster.svelte Normal file
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() {
this.enabled = !this.enabled;
this.save();
@ -50,7 +62,12 @@ export class Action {
export async function getActions() {
const res = await fetch(`api/actions`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return (await res.json()).map((t) => new Action(t));
const data = await res.json();
if (data == null) {
return []
} else {
return data.map((t) => new Action(t));
}
} else {
throw new Error((await res.json()).errmsg);
}

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() {
const res = await fetch('api/alarm', {
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.weekday = weekday;
this.time = time;
this.routines = routines;
this.routines = routines == null ? [] : routines;
this.ignore_exceptions = ignore_exceptions;
this.comment = comment;
this.disabled = disabled == true;
this.enable_federation = enable_federation == true;
if (excepts !== undefined)
this.excepts = excepts;
if (next_time !== undefined)
this.next_time = next_time;
if (this.routines.length < 1) {
this.routines.push("");
}
}
async delete() {

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.time = new Date(time);
this.routines = routines;
this.routines = routines == null ? [] : routines;
this.comment = comment;
this.enable_federation = enable_federation == true;
if (this.routines.length < 1) {
this.routines.push("");
}
}
async delete() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
import {
Input,
} from 'sveltestrap';
} from '@sveltestrap/sveltestrap';
export let format = 'YYYY-MM-DD HH:mm';
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,
Icon,
Spinner,
} from 'sveltestrap';
} from '@sveltestrap/sveltestrap';
import { gongs } from '$lib/stores/gongs';

View File

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

View File

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

View File

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

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

View File

@ -2,6 +2,18 @@ import { writable } from 'svelte/store';
import { getTracks } from '$lib/track'
function cmpTracks(a, b) {
if (a.enabled && !b.enabled) return -1;
if (!a.enabled && b.enabled) return 1;
if (a.path.toLowerCase() > b.path.toLowerCase())
return 1;
if (a.path.toLowerCase() < b.path.toLowerCase())
return -1;
return 0;
}
function createTracksStore() {
const { subscribe, set, update } = writable({list: null});
@ -14,6 +26,7 @@ function createTracksStore() {
refresh: async () => {
const list = await getTracks();
list.sort(cmpTracks);
update((m) => Object.assign(m, {list}));
return list;
},

View File

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

View File

@ -4,10 +4,18 @@
import {
//Styles,
} from 'sveltestrap';
} from '@sveltestrap/sveltestrap';
import Header from '$lib/components/Header.svelte';
import Toaster from '$lib/components/Toaster.svelte';
import { ToastsStore } from '$lib/stores/toasts';
window.onunhandledrejection = (e) => {
ToastsStore.addErrorToast({
message: e.reason,
timeout: 7500,
})
}
</script>
<svelte:head>
@ -21,7 +29,7 @@
/>
<div class="flex-fill d-flex flex-column bg-light">
<slot></slot>
<div class="d-flex d-lg-none mt-1 mb-4"></div>
<div class="d-flex d-lg-none mt-3 mb-5"></div>
</div>
<Toaster />
<Header

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

@ -3,13 +3,22 @@
import {
Container,
Icon,
Input,
ListGroup,
ListGroupItem,
Spinner,
} from 'sveltestrap';
} from '@sveltestrap/sveltestrap';
import { getAction } from '$lib/action';
import { actions } from '$lib/stores/actions';
function deleteThis(action) {
action.delete().then(() => {
actions.refresh();
goto('routines/actions/');
})
}
</script>
{#await getAction($page.params.aid)}
@ -34,5 +43,26 @@
<Input type="switch" on:change={() => action.toggleEnable()} checked={action.enabled} />
</ListGroupItem>
</ListGroup>
<ListGroup class="my-2 text-center">
<ListGroupItem
action
tag="button"
class="text-success fw-bold"
on:click={() => action.launch()}
>
<Icon name="play-fill" />
Lancer cette action
</ListGroupItem>
<ListGroupItem
action
tag="button"
class="text-danger fw-bold"
on:click={() => deleteThis(action)}
>
<Icon name="trash" />
Supprimer cette action
</ListGroupItem>
</ListGroup>
</Container>
{/await}

View File

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

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,
"short_name": "Gustus",
"name": "Gustus",
"short_name": "Réveil",
"name": "Réveil",
"version": "0.1",
"author": "nemucorp",
"start_url": "/",
@ -12,9 +12,9 @@
"sizes": "512x512"
}
],
"background_color": "#d62a49",
"background_color": "#e83e8c",
"display": "standalone",
"scope": "/",
"theme_color": "#ffffff",
"description": "Retrouvez facilement toutes vos recettes préférées"
"description": "Quand est-ce qu'on se lève ?"
}

0
ui/static/nojs.html Normal file
View File

View File

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

View File

@ -1,32 +1,3 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"moduleResolution": "node",
"module": "es2020",
"lib": ["es2020", "DOM"],
"target": "es2020",
/**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
to enforce using \`import type\` instead of \`import\` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
To have warnings/errors of the Svelte compiler at the correct position,
enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"allowJs": true,
"checkJs": true,
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
"extends": "./.svelte-kit/tsconfig.json"
}

View File

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