From 06494d1c91fb2f37d9ec9e7bdfae3711f3bf71c2 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 4 Oct 2022 12:44:59 +0200 Subject: [PATCH] Handle gongs --- .gitignore | 1 + api/gongs.go | 77 ++++++++++++++--- model/gong.go | 86 +++++++++++++++++++ ui/src/components/GongsList.svelte | 84 ++++++++++-------- ui/src/lib/gong.js | 64 ++++++++++++++ ui/src/routes/musiks/gongs/[gid]/+page.svelte | 42 ++++++++- ui/src/stores/gongs.js | 36 ++++++++ 7 files changed, 339 insertions(+), 51 deletions(-) create mode 100644 model/gong.go create mode 100644 ui/src/lib/gong.js create mode 100644 ui/src/stores/gongs.js diff --git a/.gitignore b/.gitignore index 009f82e..2a4d07c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +gongs reveil tracks vendor diff --git a/api/gongs.go b/api/gongs.go index 985b777..7410243 100644 --- a/api/gongs.go +++ b/api/gongs.go @@ -1,37 +1,92 @@ package api import ( + "encoding/base64" + "fmt" "net/http" "github.com/gin-gonic/gin" "git.nemunai.re/nemunaire/reveil/config" + "git.nemunai.re/nemunaire/reveil/model" ) func declareGongsRoutes(cfg *config.Config, router *gin.RouterGroup) { router.GET("/gongs", func(c *gin.Context) { + gongs, err := reveil.LoadGongs(cfg) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + c.JSON(http.StatusOK, gongs) }) router.POST("/gongs", func(c *gin.Context) { - + c.AbortWithStatusJSON(http.StatusNotImplemented, gin.H{"errmsg": "TODO"}) }) - gongsRoutes := router.Group("/gongs/:gid") - gongsRoutes.Use(gongHandler) + gongsRoutes := router.Group("/gongs/:tid") + gongsRoutes.Use(func(c *gin.Context) { + gongs, err := reveil.LoadGongs(cfg) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + for _, t := range gongs { + if base64.StdEncoding.EncodeToString(t.Id) == c.Param("tid") { + c.Set("gong", t) + c.Next() + return + } + } + + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Gong not found"}) + }) gongsRoutes.GET("", func(c *gin.Context) { c.JSON(http.StatusOK, c.MustGet("gong")) }) gongsRoutes.PUT("", func(c *gin.Context) { - c.JSON(http.StatusOK, c.MustGet("gong")) + oldgong := c.MustGet("gong").(*reveil.Gong) + + var gong reveil.Gong + if err := c.ShouldBindJSON(&gong); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + if gong.Name != oldgong.Name { + err := oldgong.Rename(gong.Name) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to rename the gong: %s", err.Error())}) + return + } + } + + if gong.Enabled != oldgong.Enabled { + var err error + if gong.Enabled { + err = oldgong.SetDefault(cfg) + } + + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to set the new default gong: %s", err.Error())}) + return + } + } + + c.JSON(http.StatusOK, oldgong) }) gongsRoutes.DELETE("", func(c *gin.Context) { - c.JSON(http.StatusOK, c.MustGet("gong")) + gong := c.MustGet("gong").(*reveil.Gong) + + err := gong.Remove() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to remove the gong: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, nil) }) } - -func gongHandler(c *gin.Context) { - c.Set("gong", nil) - - c.Next() -} diff --git a/model/gong.go b/model/gong.go new file mode 100644 index 0000000..0e3c81a --- /dev/null +++ b/model/gong.go @@ -0,0 +1,86 @@ +package reveil + +import ( + "crypto/sha512" + "io/fs" + "os" + "path/filepath" + "strings" + + "git.nemunai.re/nemunaire/reveil/config" +) + +const CURRENT_GONG = "_current_gong" + +type Gong struct { + Id []byte `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Enabled bool `json:"enabled"` +} + +func currentGongPath(cfg *config.Config) string { + return filepath.Join(cfg.GongsDir, CURRENT_GONG) +} + +func LoadGongs(cfg *config.Config) (gongs []*Gong, err error) { + // Retrieve the path of the current gong + current_gong, err := os.Readlink(currentGongPath(cfg)) + if err == nil { + current_gong, _ = filepath.Abs(filepath.Join(cfg.GongsDir, current_gong)) + } + + // Retrieve the list + err = filepath.Walk(cfg.GongsDir, func(path string, d fs.FileInfo, err error) error { + if d.Mode().IsRegular() { + hash := sha512.Sum512([]byte(path)) + pabs, _ := filepath.Abs(path) + gongs = append(gongs, &Gong{ + Id: hash[:63], + Name: strings.TrimSuffix(d.Name(), filepath.Ext(d.Name())), + Path: path, + Enabled: current_gong == pabs, + }) + } + + return nil + }) + + return +} + +func (g *Gong) Rename(newName string) error { + newPath := filepath.Join(filepath.Dir(g.Path), newName+filepath.Ext(g.Path)) + + err := os.Rename( + g.Path, + newPath, + ) + if err != nil { + return err + } + + g.Path = newPath + return nil +} + +func (g *Gong) SetDefault(cfg *config.Config) error { + linkpath := currentGongPath(cfg) + os.Remove(linkpath) + + pabs, err := filepath.Abs(g.Path) + if err != nil { + pabs = g.Path + } + + gdirabs, err := filepath.Abs(cfg.GongsDir) + if err != nil { + gdirabs = cfg.GongsDir + } + + return os.Symlink(strings.TrimPrefix(strings.TrimPrefix(pabs, gdirabs), "/"), linkpath) +} + +func (g *Gong) Remove() error { + return os.Remove(g.Path) +} diff --git a/ui/src/components/GongsList.svelte b/ui/src/components/GongsList.svelte index 6af8935..ecc6115 100644 --- a/ui/src/components/GongsList.svelte +++ b/ui/src/components/GongsList.svelte @@ -5,29 +5,15 @@ import { Button, Icon, + Spinner, } from 'sveltestrap'; - let gongs = [ - { - id: 1, - title: "Coq", - }, - { - id: 2, - title: "Marseillaise", - enabled: true, - }, - { - id: 3, - title: "Trompette de l'armée française", - }, - ]; + import { gongs } from '../stores/gongs'; function chooseGong(gong) { - gongs = gongs.map((g) => { - g.enabled = g.id == gong.id - return g; - }) + gong.setDefault().then(() => { + gongs.refresh(); + }); } export let flush = false; @@ -35,6 +21,14 @@ export { className as class }; let className = ''; + + let refreshInProgress = false; + function refresh_gongs() { + refreshInProgress = true; + gongs.refresh().then(() => { + refreshInProgress = false; + }); + }
@@ -61,27 +55,43 @@
- {#each gongs as gong (gong.id)} - - {/each} + {#if $gongs.list} + {#each $gongs.list as gong (gong.id)} + + {/each} + {:else} + {#await gongs.refresh()} +
+ Chargement en cours… +
+ {:then gongs} + {/await} + {/if}
diff --git a/ui/src/lib/gong.js b/ui/src/lib/gong.js new file mode 100644 index 0000000..71128ce --- /dev/null +++ b/ui/src/lib/gong.js @@ -0,0 +1,64 @@ +export class Gong { + constructor(res) { + if (res) { + this.update(res); + } + } + + update({ id, name, path, enabled }) { + this.id = id; + this.name = name; + this.path = path; + this.enabled = enabled; + } + + async delete() { + const res = await fetch(`api/gongs/${this.id}`, { + method: 'DELETE', + headers: {'Accept': 'application/json'} + }); + if (res.status == 200) { + return true; + } else { + throw new Error((await res.json()).errmsg); + } + } + + async setDefault() { + this.enabled = !this.enabled; + return await this.save(); + } + + async save() { + const res = await fetch(this.id?`api/gongs/${this.id}`:'api/gongs', { + method: this.id?'PUT':'POST', + headers: {'Accept': 'application/json'}, + body: JSON.stringify(this), + }); + if (res.status == 200) { + const data = await res.json(); + this.update(data); + return data; + } else { + throw new Error((await res.json()).errmsg); + } + } +} + +export async function getGongs() { + const res = await fetch(`api/gongs`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + return (await res.json()).map((g) => new Gong(g)); + } else { + throw new Error((await res.json()).errmsg); + } +} + +export async function getGong(gid) { + const res = await fetch(`api/gongs/${gid}`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + return new Gong(await res.json()); + } else { + throw new Error((await res.json()).errmsg); + } +} diff --git a/ui/src/routes/musiks/gongs/[gid]/+page.svelte b/ui/src/routes/musiks/gongs/[gid]/+page.svelte index 61e1517..f2dcdc7 100644 --- a/ui/src/routes/musiks/gongs/[gid]/+page.svelte +++ b/ui/src/routes/musiks/gongs/[gid]/+page.svelte @@ -1,3 +1,39 @@ -

- Gong -

+ + +{#await getGong($page.params.gid)} +
+ Chargement en cours… +
+{:then gong} + +

+ {gong.name} +

+ + + Chemin + {gong.path} + + + Par défaut ? + gong.setDefault()} checked={gong.enabled} disabled={gong.enabled} /> + + + ID + {gong.id} + + +
+{/await} diff --git a/ui/src/stores/gongs.js b/ui/src/stores/gongs.js new file mode 100644 index 0000000..2d9e1b2 --- /dev/null +++ b/ui/src/stores/gongs.js @@ -0,0 +1,36 @@ +import { writable } from 'svelte/store'; + +import { getGongs } from '../lib/gong' + +function createGongsStore() { + const { subscribe, set, update } = writable({list: null}); + + return { + subscribe, + + set: (v) => { + update((m) => Object.assign(m, v)); + }, + + refresh: async () => { + const list = await getGongs(); + update((m) => Object.assign(m, {list})); + return list; + }, + + update: (res_gongs, cb=null) => { + if (res_gongs.status === 200) { + res_gongs.json().then((list) => { + update((m) => (Object.assign(m, {list}))); + + if (cb) { + cb(list); + } + }); + } + }, + }; + +} + +export const gongs = createGongsStore();