New features
This commit is contained in:
parent
c67b43ab3c
commit
a9c6cdcd0f
87
api/inputs.go
Normal file
87
api/inputs.go
Normal file
@ -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()
|
||||||
|
}
|
@ -9,6 +9,7 @@ import (
|
|||||||
func DeclareRoutes(router *gin.Engine, cfg *config.Config) {
|
func DeclareRoutes(router *gin.Engine, cfg *config.Config) {
|
||||||
apiRoutes := router.Group("/api")
|
apiRoutes := router.Group("/api")
|
||||||
|
|
||||||
|
declareInputsRoutes(cfg, apiRoutes)
|
||||||
declareSourcesRoutes(cfg, apiRoutes)
|
declareSourcesRoutes(cfg, apiRoutes)
|
||||||
declareVolumeRoutes(cfg, apiRoutes)
|
declareVolumeRoutes(cfg, apiRoutes)
|
||||||
}
|
}
|
||||||
|
@ -2,18 +2,21 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"git.nemunai.re/nemunaire/hathoris/config"
|
"git.nemunai.re/nemunaire/hathoris/config"
|
||||||
|
"git.nemunai.re/nemunaire/hathoris/inputs"
|
||||||
"git.nemunai.re/nemunaire/hathoris/sources"
|
"git.nemunai.re/nemunaire/hathoris/sources"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SourceState struct {
|
type SourceState struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Active *bool `json:"active,omitempty"`
|
Active *bool `json:"active,omitempty"`
|
||||||
|
Controlable bool `json:"controlable,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
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 {
|
for k, src := range sources.SoundSources {
|
||||||
active := src.IsActive()
|
active := src.IsActive()
|
||||||
|
_, controlable := src.(inputs.ControlableInput)
|
||||||
|
|
||||||
ret[k] = &SourceState{
|
ret[k] = &SourceState{
|
||||||
Name: src.GetName(),
|
Name: src.GetName(),
|
||||||
Enabled: src.IsEnabled(),
|
Enabled: src.IsEnabled(),
|
||||||
Active: &active,
|
Active: &active,
|
||||||
|
Controlable: controlable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +74,21 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
sourcesRoutes.POST("/enable", func(c *gin.Context) {
|
sourcesRoutes.POST("/enable", func(c *gin.Context) {
|
||||||
src := c.MustGet("source").(sources.SoundSource)
|
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()
|
err := src.Enable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to enable the source: %s", err.Error())})
|
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)
|
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) {
|
func SourceHandler(c *gin.Context) {
|
||||||
|
2
go.mod
2
go.mod
@ -5,6 +5,8 @@ go 1.21
|
|||||||
require (
|
require (
|
||||||
github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37
|
github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37
|
||||||
github.com/gin-gonic/gin v1.9.1
|
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 (
|
require (
|
||||||
|
11
go.sum
11
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/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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
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/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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
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.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 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
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 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.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
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/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
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 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.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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.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 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
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 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 h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
|
||||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
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=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
15
inputs/interfaces.go
Normal file
15
inputs/interfaces.go
Normal file
@ -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
|
||||||
|
}
|
149
inputs/mpris/input.go
Normal file
149
inputs/mpris/input.go
Normal file
@ -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
|
||||||
|
}
|
1
main.go
1
main.go
@ -7,6 +7,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.nemunai.re/nemunaire/hathoris/config"
|
"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/amp1_gpio"
|
||||||
_ "git.nemunai.re/nemunaire/hathoris/sources/mpv"
|
_ "git.nemunai.re/nemunaire/hathoris/sources/mpv"
|
||||||
_ "git.nemunai.re/nemunaire/hathoris/sources/spdif"
|
_ "git.nemunai.re/nemunaire/hathoris/sources/spdif"
|
||||||
|
@ -28,7 +28,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *AMP1GPIOSource) GetName() string {
|
func (s *AMP1GPIOSource) GetName() string {
|
||||||
return "entrée analogique"
|
return "analog."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AMP1GPIOSource) read() ([]byte, error) {
|
func (s *AMP1GPIOSource) read() ([]byte, error) {
|
||||||
|
@ -98,20 +98,60 @@ func (s *MPVSource) Enable() (err error) {
|
|||||||
_, err = conn.Get("media-title")
|
_, err = conn.Get("media-title")
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Set("ao-volume", 50)
|
conn.Set("ao-volume", 1)
|
||||||
|
|
||||||
err = conn.Set("pause", false)
|
err = conn.Set("pause", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
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 {
|
func (s *MPVSource) Disable() error {
|
||||||
if s.process != nil {
|
if s.process != nil {
|
||||||
if s.process.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()
|
s.process.Process.Kill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,3 +179,36 @@ func (s *MPVSource) CurrentlyPlaying() string {
|
|||||||
|
|
||||||
return "-"
|
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
|
||||||
|
}
|
||||||
|
66
ui/src/lib/components/Inputs.svelte
Normal file
66
ui/src/lib/components/Inputs.svelte
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<script>
|
||||||
|
import { activeInputs, inputsList } from '$lib/stores/inputs';
|
||||||
|
import { activeSources } from '$lib/stores/sources';
|
||||||
|
|
||||||
|
export let showInactives = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{#if $activeSources.length === 0 && ((showInactives && $inputsList.length === 0) || (!showInactives && $activeInputs.length === 0))}
|
||||||
|
<li class="list-group-item py-3">
|
||||||
|
<span class="text-muted">
|
||||||
|
Aucune source active.
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#each $activeSources as source}
|
||||||
|
<li class="list-group-item py-3 d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<strong>{source.name}</strong>
|
||||||
|
{#await source.currently()}
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
{:then title}
|
||||||
|
<span class="text-muted">{title}</span>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
{#if source.controlable}
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
on:click={() => source.playpause()}
|
||||||
|
>
|
||||||
|
<i class="bi bi-pause"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{#each $inputsList as input}
|
||||||
|
{#if showInactives || input.active}
|
||||||
|
<li class="list-group-item py-3 d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<strong>{input.name}</strong>
|
||||||
|
{#await input.currently()}
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
{:then title}
|
||||||
|
<span class="text-muted">{title}</span>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
{#if input.controlable}
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
on:click={() => input.playpause()}
|
||||||
|
>
|
||||||
|
<i class="bi bi-pause"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
56
ui/src/lib/input.js
Normal file
56
ui/src/lib/input.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -6,10 +6,11 @@ export class Source {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update({ name, enabled, active }) {
|
update({ name, enabled, active, controlable }) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
this.active = active;
|
this.active = active;
|
||||||
|
this.controlable = controlable;
|
||||||
}
|
}
|
||||||
|
|
||||||
async activate() {
|
async activate() {
|
||||||
@ -28,6 +29,13 @@ export class Source {
|
|||||||
throw new Error((await res.json()).errmsg);
|
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() {
|
export async function getSources() {
|
||||||
|
41
ui/src/lib/stores/inputs.js
Normal file
41
ui/src/lib/stores/inputs.js
Normal file
@ -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);
|
||||||
|
},
|
||||||
|
);
|
@ -6,6 +6,10 @@
|
|||||||
sources.refresh();
|
sources.refresh();
|
||||||
setInterval(sources.refresh, 5000);
|
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())
|
const version = fetch('api/version', {headers: {'Accept': 'application/json'}}).then((res) => res.json())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import Inputs from '$lib/components/Inputs.svelte';
|
||||||
import Mixer from '$lib/components/Mixer.svelte';
|
import Mixer from '$lib/components/Mixer.svelte';
|
||||||
import SourceSelection from '$lib/components/SourceSelection.svelte';
|
import SourceSelection from '$lib/components/SourceSelection.svelte';
|
||||||
import { activeSources } from '$lib/stores/sources';
|
import { activeSources } from '$lib/stores/sources';
|
||||||
|
import { activeInputs } from '$lib/stores/inputs';
|
||||||
|
|
||||||
let mixerAdvanced = false;
|
let mixerAdvanced = false;
|
||||||
</script>
|
</script>
|
||||||
@ -11,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{#if $activeSources.length === 0}
|
{#if $activeSources.length === 0 && $activeInputs.length === 0}
|
||||||
<div class="text-muted text-center mt-1 mb-1">
|
<div class="text-muted text-center mt-1 mb-1">
|
||||||
Aucune source active pour l'instant.
|
Aucune source active pour l'instant.
|
||||||
</div>
|
</div>
|
||||||
@ -21,15 +23,31 @@
|
|||||||
<div class="d-inline-block me-3">
|
<div class="d-inline-block me-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<strong>{source.name} :</strong>
|
|
||||||
{#await source.currently()}
|
{#await source.currently()}
|
||||||
<div class="spinner-border spinner-border-sm" role="status">
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div> <span class="text-muted">@ {source.name}</span>
|
||||||
{:then title}
|
{:then title}
|
||||||
{title}
|
<strong>{title}</strong> <span class="text-muted">@ {source.name}</span>
|
||||||
{:catch error}
|
{:catch error}
|
||||||
activée
|
{source.name} activée
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#each $activeInputs as input}
|
||||||
|
<div class="d-inline-block me-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
{#await input.currently()}
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div> <span class="text-muted">@ {input.name}</span>
|
||||||
|
{:then title}
|
||||||
|
<strong>{title}</strong> <span class="text-muted">@ {input.name}</span>
|
||||||
|
{:catch error}
|
||||||
|
{input.name} activée
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -39,7 +57,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col-md">
|
||||||
<div class="card my-2">
|
<div class="card my-2">
|
||||||
<h4 class="card-header">
|
<h4 class="card-header">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
@ -61,14 +79,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col">
|
<div class="col-md">
|
||||||
<div class="card my-2">
|
<div class="card my-2">
|
||||||
<h4 class="card-header">
|
<h4 class="card-header">
|
||||||
<i class="bi bi-speaker"></i>
|
<i class="bi bi-speaker"></i>
|
||||||
Sources
|
Sources
|
||||||
</h4>
|
</h4>
|
||||||
<div class="card-body">
|
<Inputs />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user