Refactor federation + can sync track between instances
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing

This commit is contained in:
nemunaire 2024-07-30 10:59:14 +02:00
parent e99efdb43f
commit c8c6282216
6 changed files with 318 additions and 54 deletions

View File

@ -62,7 +62,7 @@ func declareAlarmRoutes(cfg *config.Config, router *gin.RouterGroup) {
} }
for k, srv := range settings.Federation { for k, srv := range settings.Federation {
err = player.FederatedStop(srv) err = srv.WakeStop()
if err != nil { if err != nil {
log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error()) log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error())
} else { } else {

View File

@ -1,12 +1,15 @@
package api package api
import ( import (
"bytes"
"fmt"
"net/http" "net/http"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.nemunai.re/nemunaire/reveil/config" "git.nemunai.re/nemunaire/reveil/config"
"git.nemunai.re/nemunaire/reveil/model"
"git.nemunai.re/nemunaire/reveil/player" "git.nemunai.re/nemunaire/reveil/player"
) )
@ -47,4 +50,127 @@ func declareFederationRoutes(cfg *config.Config, router *gin.RouterGroup) {
c.JSON(http.StatusOK, true) 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)
})
} }

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

@ -6,20 +6,15 @@ import (
"time" "time"
) )
type FederationSettings struct {
URL string `json:"url"`
Delay uint `json:"delay"`
}
// Settings represents the settings panel. // Settings represents the settings panel.
type Settings struct { type Settings struct {
Language string `json:"language"` Language string `json:"language"`
GongInterval time.Duration `json:"gong_interval"` GongInterval time.Duration `json:"gong_interval"`
WeatherDelay time.Duration `json:"weather_delay"` WeatherDelay time.Duration `json:"weather_delay"`
WeatherAction string `json:"weather_action"` WeatherAction string `json:"weather_action"`
MaxRunTime time.Duration `json:"max_run_time"` MaxRunTime time.Duration `json:"max_run_time"`
MaxVolume uint16 `json:"max_volume"` MaxVolume uint16 `json:"max_volume"`
Federation map[string]FederationSettings `json:"federation"` Federation map[string]FederationServer `json:"federation"`
} }
// ExistsSettings checks if the settings file can by found at the given path. // ExistsSettings checks if the settings file can by found at the given path.

View File

@ -1,18 +1,15 @@
package player package player
import ( import (
"bytes"
"encoding/json"
"log" "log"
"net/http"
"time" "time"
"git.nemunai.re/nemunaire/reveil/model" "git.nemunai.re/nemunaire/reveil/model"
) )
func FederatedWakeUp(k string, srv reveil.FederationSettings, seed int64) { func FederatedWakeUp(k string, srv reveil.FederationServer, seed int64) {
if srv.Delay == 0 { if srv.Delay == 0 {
err := federatedWakeUp(srv, seed) err := srv.WakeUp(seed)
if err != nil { if err != nil {
log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error()) log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error())
} else { } else {
@ -21,7 +18,7 @@ func FederatedWakeUp(k string, srv reveil.FederationSettings, seed int64) {
} else { } else {
go func() { go func() {
time.Sleep(time.Duration(srv.Delay) * time.Millisecond) time.Sleep(time.Duration(srv.Delay) * time.Millisecond)
err := federatedWakeUp(srv, seed) err := srv.WakeUp(seed)
if err != nil { if err != nil {
log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error()) log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error())
} else { } else {
@ -30,29 +27,3 @@ func FederatedWakeUp(k string, srv reveil.FederationSettings, seed int64) {
}() }()
} }
} }
func federatedWakeUp(srv reveil.FederationSettings, 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 FederatedStop(srv reveil.FederationSettings) error {
res, err := http.Post(srv.URL+"/api/federation/wakeok", "application/json", nil)
if err != nil {
return err
}
res.Body.Close()
return nil
}

View File

@ -9,6 +9,7 @@
InputGroup, InputGroup,
InputGroupText, InputGroupText,
Row, Row,
Spinner,
} from '@sveltestrap/sveltestrap'; } from '@sveltestrap/sveltestrap';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -27,6 +28,21 @@
delete 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> </script>
{#if value} {#if value}
@ -57,15 +73,34 @@
</InputGroup> </InputGroup>
</Col> </Col>
<Col> <Col>
<InputGroup> <Row>
<Input <Col>
type="number" <InputGroup>
placeholder="60" <Input
bind:value={value[key].delay} type="number"
on:change={() => dispatch("input")} placeholder="60"
/> bind:value={value[key].delay}
<InputGroupText>ms</InputGroupText> on:change={() => dispatch("input")}
</InputGroup> />
<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> </Col>
</Row> </Row>
{/each} {/each}