From 3addf9d1eec02395200aadf6007299339f921de9 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 4 Oct 2022 12:29:50 +0200 Subject: [PATCH] Handle tracks --- .gitignore | 1 + api/tracks.go | 77 +++++++++++++--- model/track.go | 88 +++++++++++++++++++ ui/src/components/TrackList.svelte | 88 ++++++++++--------- ui/src/lib/track.js | 65 ++++++++++++++ .../routes/musiks/tracks/[tid]/+page.svelte | 42 ++++++++- ui/src/stores/tracks.js | 36 ++++++++ 7 files changed, 344 insertions(+), 53 deletions(-) create mode 100644 model/track.go create mode 100644 ui/src/lib/track.js create mode 100644 ui/src/stores/tracks.js diff --git a/.gitignore b/.gitignore index 11ac438..009f82e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ reveil +tracks vendor ui/build ui/node_modules \ No newline at end of file diff --git a/api/tracks.go b/api/tracks.go index be54d2e..9a2bc6a 100644 --- a/api/tracks.go +++ b/api/tracks.go @@ -1,37 +1,94 @@ 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 declareTracksRoutes(cfg *config.Config, router *gin.RouterGroup) { router.GET("/tracks", func(c *gin.Context) { + tracks, err := reveil.LoadTracks(cfg) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + c.JSON(http.StatusOK, tracks) }) router.POST("/tracks", func(c *gin.Context) { - + c.AbortWithStatusJSON(http.StatusNotImplemented, gin.H{"errmsg": "TODO"}) }) tracksRoutes := router.Group("/tracks/:tid") - tracksRoutes.Use(trackHandler) + tracksRoutes.Use(func(c *gin.Context) { + tracks, err := reveil.LoadTracks(cfg) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + for _, t := range tracks { + if base64.StdEncoding.EncodeToString(t.Id) == c.Param("tid") { + c.Set("track", t) + c.Next() + return + } + } + + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Track not found"}) + }) tracksRoutes.GET("", func(c *gin.Context) { c.JSON(http.StatusOK, c.MustGet("track")) }) tracksRoutes.PUT("", func(c *gin.Context) { - c.JSON(http.StatusOK, c.MustGet("track")) + oldtrack := c.MustGet("track").(*reveil.Track) + + var track reveil.Track + if err := c.ShouldBindJSON(&track); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + if track.Name != oldtrack.Name { + err := oldtrack.Rename(track.Name) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to rename the track: %s", err.Error())}) + return + } + } + + if track.Enabled != oldtrack.Enabled { + var err error + if track.Enabled { + err = oldtrack.Enable(cfg) + } else { + err = oldtrack.Disable() + } + + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to enable/disable the track: %s", err.Error())}) + return + } + } + + c.JSON(http.StatusOK, oldtrack) }) tracksRoutes.DELETE("", func(c *gin.Context) { - c.JSON(http.StatusOK, c.MustGet("track")) + track := c.MustGet("track").(*reveil.Track) + + err := track.Remove() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to remove the track: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, nil) }) } - -func trackHandler(c *gin.Context) { - c.Set("track", nil) - - c.Next() -} diff --git a/model/track.go b/model/track.go new file mode 100644 index 0000000..8c6e019 --- /dev/null +++ b/model/track.go @@ -0,0 +1,88 @@ +package reveil + +import ( + "crypto/sha512" + "io/fs" + "os" + "path/filepath" + "strings" + "time" + + "git.nemunai.re/nemunaire/reveil/config" +) + +type Track struct { + Id []byte `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Enabled bool `json:"enabled"` +} + +func LoadTracks(cfg *config.Config) (tracks []*Track, err error) { + err = filepath.Walk(cfg.TracksDir, func(path string, d fs.FileInfo, err error) error { + if d.Mode().IsRegular() { + hash := sha512.Sum512([]byte(path)) + tracks = append(tracks, &Track{ + Id: hash[:63], + Name: strings.TrimSuffix(d.Name(), filepath.Ext(d.Name())), + Path: path, + Enabled: len(strings.Split(path, "/")) == 2, + }) + } + + return nil + }) + + return +} + +func (t *Track) Rename(newName string) error { + newPath := filepath.Join(filepath.Dir(t.Path), newName+filepath.Ext(t.Path)) + + err := os.Rename( + t.Path, + newPath, + ) + if err != nil { + return err + } + + t.Path = newPath + return nil +} + +func (t *Track) MoveTo(path string) error { + os.Mkdir(filepath.Dir(path), 0755) + + err := os.Rename( + t.Path, + path, + ) + if err != nil { + return err + } + + t.Path = path + return nil +} + +func (t *Track) Disable() error { + if t.Enabled { + date := time.Now() + return t.MoveTo(filepath.Join(filepath.Dir(t.Path), date.Format("20060102"), filepath.Base(t.Path))) + } + + return nil +} + +func (t *Track) Enable(cfg *config.Config) error { + if !t.Enabled { + return t.MoveTo(filepath.Join(cfg.TracksDir, filepath.Base(t.Path))) + } + + return nil +} + +func (t *Track) Remove() error { + return os.Remove(t.Path) +} diff --git a/ui/src/components/TrackList.svelte b/ui/src/components/TrackList.svelte index 1c557d2..f6b8ee5 100644 --- a/ui/src/components/TrackList.svelte +++ b/ui/src/components/TrackList.svelte @@ -5,33 +5,24 @@ import { Button, Icon, + Spinner, } from 'sveltestrap'; - let musiks = [ - { - id: 1, - title: "Hall Of Fame", - artist: "The Script", - enabled: true, - }, - { - id: 2, - title: "Poker face", - artist: "Lady Gaga", - }, - { - id: 3, - title: "Puisque tu m'aimes encore", - artist: "Céline Dion", - enabled: true, - }, - ]; + import { tracks } from '../stores/tracks'; export let flush = false; export let edit = false; export { className as class }; let className = ''; + + let refreshInProgress = false; + function refresh_tracks() { + refreshInProgress = true; + tracks.refresh().then(() => { + refreshInProgress = false; + }); + }
@@ -58,30 +49,47 @@
- {#each musiks as track (track.id)} - - {/each} + {#if $tracks.list} + {#each $tracks.list as track (track.id)} + + {/each} + {:else} + {#await tracks.refresh()} +
+ Chargement en cours… +
+ {:then} + test + {/await} + {/if}
diff --git a/ui/src/lib/track.js b/ui/src/lib/track.js new file mode 100644 index 0000000..a056409 --- /dev/null +++ b/ui/src/lib/track.js @@ -0,0 +1,65 @@ +export class Track { + 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/tracks/${this.id}`, { + method: 'DELETE', + 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(); + return this; + } + + async save() { + const res = await fetch(this.id?`api/tracks/${this.id}`:'api/tracks', { + 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 getTracks() { + const res = await fetch(`api/tracks`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + return (await res.json()).map((t) => new Track(t)); + } else { + throw new Error((await res.json()).errmsg); + } +} + +export async function getTrack(tid) { + const res = await fetch(`api/tracks/${tid}`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + return new Track(await res.json()); + } else { + throw new Error((await res.json()).errmsg); + } +} diff --git a/ui/src/routes/musiks/tracks/[tid]/+page.svelte b/ui/src/routes/musiks/tracks/[tid]/+page.svelte index f736dca..4c500b3 100644 --- a/ui/src/routes/musiks/tracks/[tid]/+page.svelte +++ b/ui/src/routes/musiks/tracks/[tid]/+page.svelte @@ -1,3 +1,39 @@ -

- Track -

+ + +{#await getTrack($page.params.tid)} +
+ Chargement en cours… +
+{:then track} + +

+ {track.name} +

+ + + Chemin + {track.path} + + + Active ? + track.toggleEnable()} checked={track.enabled} /> + + + ID + {track.id} + + +
+{/await} diff --git a/ui/src/stores/tracks.js b/ui/src/stores/tracks.js new file mode 100644 index 0000000..7a7cd4f --- /dev/null +++ b/ui/src/stores/tracks.js @@ -0,0 +1,36 @@ +import { writable } from 'svelte/store'; + +import { getTracks } from '../lib/track' + +function createTracksStore() { + const { subscribe, set, update } = writable({list: null}); + + return { + subscribe, + + set: (v) => { + update((m) => Object.assign(m, v)); + }, + + refresh: async () => { + const list = await getTracks(); + update((m) => Object.assign(m, {list})); + return list; + }, + + update: (res_tracks, cb=null) => { + if (res_tracks.status === 200) { + res_tracks.json().then((list) => { + update((m) => (Object.assign(m, {list}))); + + if (cb) { + cb(list); + } + }); + } + }, + }; + +} + +export const tracks = createTracksStore();