From a9c6cdcd0f2c03daa1046da21187df5df084d0bf Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 13 Nov 2023 18:50:42 +0100 Subject: [PATCH] New features --- api/inputs.go | 87 ++++++++++++++++ api/routes.go | 1 + api/sources.go | 48 +++++++-- go.mod | 2 + go.sum | 11 +- inputs/interfaces.go | 15 +++ inputs/mpris/input.go | 149 ++++++++++++++++++++++++++++ main.go | 1 + sources/amp1_gpio/source.go | 2 +- sources/mpv/source.go | 75 +++++++++++++- ui/src/lib/components/Inputs.svelte | 66 ++++++++++++ ui/src/lib/input.js | 56 +++++++++++ ui/src/lib/source.js | 10 +- ui/src/lib/stores/inputs.js | 41 ++++++++ ui/src/routes/+layout.svelte | 4 + ui/src/routes/+page.svelte | 35 +++++-- 16 files changed, 584 insertions(+), 19 deletions(-) create mode 100644 api/inputs.go create mode 100644 inputs/interfaces.go create mode 100644 inputs/mpris/input.go create mode 100644 ui/src/lib/components/Inputs.svelte create mode 100644 ui/src/lib/input.js create mode 100644 ui/src/lib/stores/inputs.js diff --git a/api/inputs.go b/api/inputs.go new file mode 100644 index 0000000..68f0f76 --- /dev/null +++ b/api/inputs.go @@ -0,0 +1,87 @@ +package api + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + + "git.nemunai.re/nemunaire/hathoris/config" + "git.nemunai.re/nemunaire/hathoris/inputs" +) + +type InputState struct { + Name string `json:"name"` + Active bool `json:"active"` + Controlable bool `json:"controlable"` +} + +func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) { + router.GET("/inputs", func(c *gin.Context) { + ret := map[string]*InputState{} + + for k, inp := range inputs.SoundInputs { + _, controlable := inp.(inputs.ControlableInput) + + ret[k] = &InputState{ + Name: inp.GetName(), + Active: inp.IsActive(), + Controlable: controlable, + } + } + + c.JSON(http.StatusOK, ret) + }) + + inputsRoutes := router.Group("/inputs/:input") + inputsRoutes.Use(InputHandler) + + inputsRoutes.GET("", func(c *gin.Context) { + src := c.MustGet("input").(inputs.SoundInput) + + c.JSON(http.StatusOK, &InputState{ + Name: src.GetName(), + Active: src.IsActive(), + }) + }) + inputsRoutes.GET("/settings", func(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("input")) + }) + inputsRoutes.GET("/currently", func(c *gin.Context) { + src := c.MustGet("input").(inputs.SoundInput) + + if !src.IsActive() { + c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{"errmsg": "Input not active"}) + return + } + + c.JSON(http.StatusOK, src.CurrentlyPlaying()) + }) + inputsRoutes.POST("/pause", func(c *gin.Context) { + input, ok := c.MustGet("input").(inputs.ControlableInput) + if !ok { + c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support that"}) + return + } + + err := input.TogglePause() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to pause the input: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, true) + }) +} + +func InputHandler(c *gin.Context) { + src, ok := inputs.SoundInputs[c.Param("input")] + if !ok { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Input not found: %s", c.Param("input"))}) + return + } + + c.Set("input", src) + + c.Next() +} diff --git a/api/routes.go b/api/routes.go index cb7babb..7473389 100644 --- a/api/routes.go +++ b/api/routes.go @@ -9,6 +9,7 @@ import ( func DeclareRoutes(router *gin.Engine, cfg *config.Config) { apiRoutes := router.Group("/api") + declareInputsRoutes(cfg, apiRoutes) declareSourcesRoutes(cfg, apiRoutes) declareVolumeRoutes(cfg, apiRoutes) } diff --git a/api/sources.go b/api/sources.go index f4e3029..9907bbe 100644 --- a/api/sources.go +++ b/api/sources.go @@ -2,18 +2,21 @@ package api import ( "fmt" + "log" "net/http" "github.com/gin-gonic/gin" "git.nemunai.re/nemunaire/hathoris/config" + "git.nemunai.re/nemunaire/hathoris/inputs" "git.nemunai.re/nemunaire/hathoris/sources" ) type SourceState struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Active *bool `json:"active,omitempty"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + Active *bool `json:"active,omitempty"` + Controlable bool `json:"controlable,omitempty"` } func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) { @@ -22,11 +25,13 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) { for k, src := range sources.SoundSources { active := src.IsActive() + _, controlable := src.(inputs.ControlableInput) ret[k] = &SourceState{ - Name: src.GetName(), - Enabled: src.IsEnabled(), - Active: &active, + Name: src.GetName(), + Enabled: src.IsEnabled(), + Active: &active, + Controlable: controlable, } } @@ -69,6 +74,21 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) { sourcesRoutes.POST("/enable", func(c *gin.Context) { src := c.MustGet("source").(sources.SoundSource) + if src.IsEnabled() { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "The source is already enabled"}) + return + } + + // Disable all sources + for k, src := range sources.SoundSources { + if src.IsEnabled() { + err := src.Disable() + if err != nil { + log.Printf("Unable to disable %s: %s", k, err.Error()) + } + } + } + err := src.Enable() if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to enable the source: %s", err.Error())}) @@ -88,6 +108,22 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) { c.JSON(http.StatusOK, true) }) + sourcesRoutes.POST("/pause", func(c *gin.Context) { + src := c.MustGet("source").(sources.SoundSource) + + if !src.IsActive() { + c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{"errmsg": "Source not active"}) + return + } + + s, ok := src.(inputs.ControlableInput) + if !ok { + c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support"}) + return + } + + c.JSON(http.StatusOK, s.TogglePause()) + }) } func SourceHandler(c *gin.Context) { diff --git a/go.mod b/go.mod index b4ce353..1561caf 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.21 require ( github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37 github.com/gin-gonic/gin v1.9.1 + github.com/godbus/dbus/v5 v5.0.6 + github.com/leberKleber/go-mpris v1.1.0 ) require ( diff --git a/go.sum b/go.sum index 339e777..018bb68 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 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/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -34,6 +36,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm 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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leberKleber/go-mpris v1.1.0 h1:bHAnmUjVoxAs4uMHH9lfQ8bOm284UWtI7JhLvkiF7O8= +github.com/leberKleber/go-mpris v1.1.0/go.mod h1:OwKywFZwFGC0p/8xBUTUXMIFZy0Rq/7C6EayfeASTA0= 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/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= @@ -43,6 +49,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -80,8 +88,9 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 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= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/inputs/interfaces.go b/inputs/interfaces.go new file mode 100644 index 0000000..6b86973 --- /dev/null +++ b/inputs/interfaces.go @@ -0,0 +1,15 @@ +package inputs + +import () + +var SoundInputs = map[string]SoundInput{} + +type SoundInput interface { + GetName() string + IsActive() bool + CurrentlyPlaying() *string +} + +type ControlableInput interface { + TogglePause() error +} diff --git a/inputs/mpris/input.go b/inputs/mpris/input.go new file mode 100644 index 0000000..cfbec1a --- /dev/null +++ b/inputs/mpris/input.go @@ -0,0 +1,149 @@ +package mpris + +import ( + "fmt" + "log" + "strings" + + "git.nemunai.re/nemunaire/hathoris/inputs" + + "github.com/godbus/dbus/v5" + "github.com/leberKleber/go-mpris" +) + +type MPRISClient struct { + Id string + Name string + Path string +} + +var KNOWN_CLIENTS = []MPRISClient{ + MPRISClient{"shairport", "ShairportSync", "org.mpris.MediaPlayer2.ShairportSync."}, + MPRISClient{"firefox", "Firefox", "org.mpris.MediaPlayer2.firefox."}, +} + +type MPRISInput struct { + player *mpris.Player + Name string + Path string +} + +var dbusConn *dbus.Conn + +func init() { + var err error + dbusConn, err = dbus.ConnectSessionBus() + if err != nil { + dbusConn, err = dbus.ConnectSystemBus() + if err != nil { + log.Println("Unable to connect to DBus. MPRIS will be unavailable:", err.Error()) + return + } + } + + var s []string + err = dbusConn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&s) + if err != nil { + log.Println("DBus unavailable:", err.Error()) + return + } + log.Println("Available DBus entries:", strings.Join(s, ",")) + + for _, ss := range s { + for _, c := range KNOWN_CLIENTS { + if strings.HasPrefix(ss, c.Path) { + inputs.SoundInputs[c.Id] = &MPRISInput{ + Name: c.Name, + Path: c.Path, + } + } + } + } +} + +func (i *MPRISInput) getPlayer() (*mpris.Player, error) { + if i.player == nil { + var s []string + err := dbusConn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&s) + if err != nil { + return nil, err + } + + for _, ss := range s { + if strings.HasPrefix(ss, i.Path) { + player := mpris.NewPlayerWithConnection(ss, dbusConn) + if err != nil { + return nil, err + } + i.player = &player + break + } + } + + if i.player == nil { + return nil, fmt.Errorf("Unable to find such dBus entry") + } + } + + return i.player, nil +} + +func (i *MPRISInput) GetName() string { + return i.Name +} + +func (i *MPRISInput) IsActive() bool { + p, err := i.getPlayer() + if err != nil || p == nil { + log.Println(err) + return false + } + + _, err = p.Metadata() + return err == nil +} + +func (i *MPRISInput) CurrentlyPlaying() *string { + p, err := i.getPlayer() + if err != nil || p == nil { + log.Println(err) + return nil + } + + meta, err := p.Metadata() + if err != nil { + log.Println(err) + return nil + } + + var infos []string + if artists, err := meta.XESAMArtist(); err == nil { + for _, artist := range artists { + if artist != "" { + infos = append(infos, artist) + } + } + } + if title, err := meta.XESAMTitle(); err == nil && title != "" { + infos = append(infos, title) + } + + ret := strings.Join(infos, " - ") + return &ret +} + +func (i *MPRISInput) TogglePause() error { + p, err := i.getPlayer() + if err != nil { + return err + } + + if ok, err := p.CanPause(); err != nil { + return err + } else if !ok { + return fmt.Errorf("The player doesn't support pause") + } + + p.Pause() + return nil +} diff --git a/main.go b/main.go index cceccfb..bbdcd23 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "syscall" "git.nemunai.re/nemunaire/hathoris/config" + _ "git.nemunai.re/nemunaire/hathoris/inputs/mpris" _ "git.nemunai.re/nemunaire/hathoris/sources/amp1_gpio" _ "git.nemunai.re/nemunaire/hathoris/sources/mpv" _ "git.nemunai.re/nemunaire/hathoris/sources/spdif" diff --git a/sources/amp1_gpio/source.go b/sources/amp1_gpio/source.go index 1cc6b50..0c57df7 100644 --- a/sources/amp1_gpio/source.go +++ b/sources/amp1_gpio/source.go @@ -28,7 +28,7 @@ func init() { } func (s *AMP1GPIOSource) GetName() string { - return "entrée analogique" + return "analog." } func (s *AMP1GPIOSource) read() ([]byte, error) { diff --git a/sources/mpv/source.go b/sources/mpv/source.go index 6009855..8392440 100644 --- a/sources/mpv/source.go +++ b/sources/mpv/source.go @@ -98,20 +98,60 @@ func (s *MPVSource) Enable() (err error) { _, err = conn.Get("media-title") } - conn.Set("ao-volume", 50) + conn.Set("ao-volume", 1) err = conn.Set("pause", false) if err != nil { return err } + + var pfc interface{} + pfc, err = conn.Get("paused-for-cache") + for err == nil && !pfc.(bool) { + time.Sleep(250 * time.Millisecond) + pfc, err = conn.Get("paused-for-cache") + } + err = nil + + s.FadeIn(conn, 3, 50) } return } +func (s *MPVSource) FadeIn(conn *mpvipc.Connection, speed int, level int) { + volume, err := conn.Get("ao-volume") + if err != nil { + volume = 1.0 + } + + for i := int(volume.(float64)) + 1; i <= level; i += speed { + conn.Set("ao-volume", i) + time.Sleep(time.Duration(300/speed) * time.Millisecond) + } +} + +func (s *MPVSource) FadeOut(conn *mpvipc.Connection, speed int) { + volume, err := conn.Get("ao-volume") + if err == nil { + for i := int(volume.(float64)) - 1; i > 0; i -= speed { + if conn.Set("ao-volume", i) == nil { + time.Sleep(time.Duration(300/speed) * time.Millisecond) + } + } + } +} + func (s *MPVSource) Disable() error { if s.process != nil { if s.process.Process != nil { + conn := mpvipc.NewConnection(s.ipcSocket) + err := conn.Open() + if err == nil { + s.FadeOut(conn, 3) + conn.Close() + } + s.process.Process.Kill() } } @@ -139,3 +179,36 @@ func (s *MPVSource) CurrentlyPlaying() string { return "-" } + +func (s *MPVSource) TogglePause() error { + if s.ipcSocket == "" { + return fmt.Errorf("Not supported") + } + + conn := mpvipc.NewConnection(s.ipcSocket) + err := conn.Open() + if err != nil { + return err + } + defer conn.Close() + + paused, err := conn.Get("pause") + if err != nil { + return err + } + + if !paused.(bool) { + s.FadeOut(conn, 5) + } + + err = conn.Set("pause", !paused.(bool)) + if err != nil { + return err + } + + if paused.(bool) { + s.FadeIn(conn, 5, 50) + } + + return nil +} diff --git a/ui/src/lib/components/Inputs.svelte b/ui/src/lib/components/Inputs.svelte new file mode 100644 index 0000000..b49b664 --- /dev/null +++ b/ui/src/lib/components/Inputs.svelte @@ -0,0 +1,66 @@ + + + diff --git a/ui/src/lib/input.js b/ui/src/lib/input.js new file mode 100644 index 0000000..1a458b6 --- /dev/null +++ b/ui/src/lib/input.js @@ -0,0 +1,56 @@ +export class Input { + constructor(id, res) { + this.id = id; + if (res) { + this.update(res); + } + } + + update({ name, active, controlable }) { + this.name = name; + this.active = active; + this.controlable = controlable; + } + + async currently() { + const data = await fetch(`api/inputs/${this.id}/currently`, {headers: {'Accept': 'application/json'}}); + if (data.status == 200) { + return await data.json(); + } else { + throw new Error((await res.json()).errmsg); + } + } + + async playpause() { + const data = await fetch(`api/inputs/${this.id}/pause`, {headers: {'Accept': 'application/json'}, method: 'POST'}); + if (data.status != 200) { + throw new Error((await res.json()).errmsg); + } + } +} + +export async function getInputs() { + const res = await fetch(`api/inputs`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + const data = await res.json(); + if (data == null) { + return {} + } else { + Object.keys(data).forEach((k) => { + data[k] = new Input(k, data[k]); + }); + return data; + } + } else { + throw new Error((await res.json()).errmsg); + } +} + +export async function getInput(sid) { + const res = await fetch(`api/inputs/${sid}`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + return new Input(sid, await res.json()); + } else { + throw new Error((await res.json()).errmsg); + } +} diff --git a/ui/src/lib/source.js b/ui/src/lib/source.js index c41770a..10ca66f 100644 --- a/ui/src/lib/source.js +++ b/ui/src/lib/source.js @@ -6,10 +6,11 @@ export class Source { } } - update({ name, enabled, active }) { + update({ name, enabled, active, controlable }) { this.name = name; this.enabled = enabled; this.active = active; + this.controlable = controlable; } async activate() { @@ -28,6 +29,13 @@ export class Source { throw new Error((await res.json()).errmsg); } } + + async playpause() { + const data = await fetch(`api/sources/${this.id}/pause`, {headers: {'Accept': 'application/json'}, method: 'POST'}); + if (data.status != 200) { + throw new Error((await res.json()).errmsg); + } + } } export async function getSources() { diff --git a/ui/src/lib/stores/inputs.js b/ui/src/lib/stores/inputs.js new file mode 100644 index 0000000..23585bc --- /dev/null +++ b/ui/src/lib/stores/inputs.js @@ -0,0 +1,41 @@ +import { derived, writable } from 'svelte/store'; + +import { getInputs } from '$lib/input' + +function createInputsStore() { + const { subscribe, set, update } = writable(null); + + return { + subscribe, + + set: (v) => { + update((m) => v); + }, + + refresh: async () => { + const list = await getInputs(); + update((m) => list); + return list; + }, + }; + +} + +export const inputs = createInputsStore(); + +export const inputsList = derived( + inputs, + ($inputs) => { + if (!$inputs) { + return []; + } + return Object.keys($inputs).map((k) => $inputs[k]); + }, +); + +export const activeInputs = derived( + inputsList, + ($inputsList) => { + return $inputsList.filter((s) => s.active); + }, +); diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index 5033f71..ee8b2a0 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -6,6 +6,10 @@ sources.refresh(); setInterval(sources.refresh, 5000); + import { inputs } from '$lib/stores/inputs'; + inputs.refresh(); + setInterval(inputs.refresh, 4500); + const version = fetch('api/version', {headers: {'Accept': 'application/json'}}).then((res) => res.json()) diff --git a/ui/src/routes/+page.svelte b/ui/src/routes/+page.svelte index eb241ce..2d94ab7 100644 --- a/ui/src/routes/+page.svelte +++ b/ui/src/routes/+page.svelte @@ -1,7 +1,9 @@ @@ -11,7 +13,7 @@
- {#if $activeSources.length === 0} + {#if $activeSources.length === 0 && $activeInputs.length === 0}
Aucune source active pour l'instant.
@@ -21,15 +23,31 @@
- {source.name} : {#await source.currently()}
Loading... -
+
@ {source.name} {:then title} - {title} + {title} @ {source.name} {:catch error} - activée + {source.name} activée + {/await} +
+
+
+ {/each} + {#each $activeInputs as input} +
+
+
+ {#await input.currently()} +
+ Loading... +
@ {input.name} + {:then title} + {title} @ {input.name} + {:catch error} + {input.name} activée {/await}
@@ -39,7 +57,7 @@ {/if}
-
+

@@ -61,14 +79,13 @@

-
+

Sources

-
-
+