Compare commits
15 Commits
Author | SHA1 | Date |
---|---|---|
nemunaire | 85ecd89104 | |
nemunaire | 19fa419d89 | |
nemunaire | 677f93723b | |
nemunaire | f7760416b9 | |
nemunaire | 1d091be264 | |
nemunaire | a5e3452342 | |
nemunaire | e224b6a986 | |
nemunaire | 4891d5f7b7 | |
nemunaire | 9926248109 | |
nemunaire | bfd0bb866a | |
nemunaire | d2090bee67 | |
nemunaire | 5d32f30a54 | |
nemunaire | b8bff830ca | |
nemunaire | a591ed17a6 | |
nemunaire | e89099a18c |
|
@ -13,7 +13,7 @@ workspace:
|
|||
|
||||
steps:
|
||||
- name: build front
|
||||
image: node:20-alpine
|
||||
image: node:21
|
||||
commands:
|
||||
- mkdir deploy
|
||||
- cd ui
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
FROM node:21 as nodebuild
|
||||
|
||||
WORKDIR /ui
|
||||
|
||||
COPY ui/ .
|
||||
|
||||
RUN npm install --network-timeout=100000 && \
|
||||
npm run build
|
||||
|
||||
|
||||
FROM golang:1-alpine AS build
|
||||
|
||||
RUN apk --no-cache add git go-bindata
|
||||
|
||||
COPY . /go/src/git.nemunai.re/nemunaire/hathoris
|
||||
COPY --from=nodebuild /ui/build /go/src/git.nemunai.re/nemunaire/hathoris/ui/build
|
||||
WORKDIR /go/src/git.nemunai.re/nemunaire/hathoris
|
||||
RUN go get && go generate && go build -ldflags="-s -w"
|
||||
|
||||
|
||||
FROM alpine:3.18
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["/srv/hathoris"]
|
||||
|
||||
COPY --from=build /go/src/git.nemunai.re/nemunaire/hathoris/hathoris /srv/hathoris
|
|
@ -0,0 +1,6 @@
|
|||
FROM alpine:3.18
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["/srv/hathoris"]
|
||||
|
||||
COPY hathoris /srv/hathoris
|
138
api/inputs.go
138
api/inputs.go
|
@ -11,9 +11,13 @@ import (
|
|||
)
|
||||
|
||||
type InputState struct {
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
Controlable bool `json:"controlable"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
Controlable bool `json:"controlable"`
|
||||
HasPlaylist bool `json:"hasplaylist"`
|
||||
Streams map[string]string `json:"streams,omitempty"`
|
||||
Mixable bool `json:"mixable"`
|
||||
Mixer map[string]*inputs.InputMixer `json:"mixer,omitempty"`
|
||||
}
|
||||
|
||||
func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
|
@ -21,12 +25,27 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||
ret := map[string]*InputState{}
|
||||
|
||||
for k, inp := range inputs.SoundInputs {
|
||||
var mixer map[string]*inputs.InputMixer
|
||||
|
||||
_, controlable := inp.(inputs.ControlableInput)
|
||||
im, mixable := inp.(inputs.MixableInput)
|
||||
if mixable {
|
||||
mixer, _ = im.GetMixers()
|
||||
}
|
||||
|
||||
var hasPlaylist bool
|
||||
if p, withPlaylist := inp.(inputs.PlaylistInput); withPlaylist {
|
||||
hasPlaylist = p.HasPlaylist()
|
||||
}
|
||||
|
||||
ret[k] = &InputState{
|
||||
Name: inp.GetName(),
|
||||
Active: inp.IsActive(),
|
||||
Controlable: controlable,
|
||||
HasPlaylist: hasPlaylist,
|
||||
Streams: inp.CurrentlyPlaying(),
|
||||
Mixable: mixable,
|
||||
Mixer: mixer,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,30 +56,47 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||
inputsRoutes.Use(InputHandler)
|
||||
|
||||
inputsRoutes.GET("", func(c *gin.Context) {
|
||||
src := c.MustGet("input").(inputs.SoundInput)
|
||||
inp := c.MustGet("input").(inputs.SoundInput)
|
||||
|
||||
var mixer map[string]*inputs.InputMixer
|
||||
_, controlable := inp.(inputs.ControlableInput)
|
||||
im, mixable := inp.(inputs.MixableInput)
|
||||
if mixable {
|
||||
mixer, _ = im.GetMixers()
|
||||
}
|
||||
var hasPlaylist bool
|
||||
if p, withPlaylist := inp.(inputs.PlaylistInput); withPlaylist {
|
||||
hasPlaylist = p.HasPlaylist()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &InputState{
|
||||
Name: src.GetName(),
|
||||
Active: src.IsActive(),
|
||||
Name: inp.GetName(),
|
||||
Active: inp.IsActive(),
|
||||
Controlable: controlable,
|
||||
HasPlaylist: hasPlaylist,
|
||||
Streams: inp.CurrentlyPlaying(),
|
||||
Mixable: mixable,
|
||||
Mixer: mixer,
|
||||
})
|
||||
})
|
||||
inputsRoutes.GET("/settings", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, c.MustGet("input"))
|
||||
})
|
||||
inputsRoutes.GET("/streams", func(c *gin.Context) {
|
||||
src := c.MustGet("input").(inputs.SoundInput)
|
||||
inp := c.MustGet("input").(inputs.SoundInput)
|
||||
|
||||
if !src.IsActive() {
|
||||
if !inp.IsActive() {
|
||||
c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{"errmsg": "Input not active"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, src.CurrentlyPlaying())
|
||||
c.JSON(http.StatusOK, inp.CurrentlyPlaying())
|
||||
})
|
||||
|
||||
streamRoutes := inputsRoutes.Group("/streams/:stream")
|
||||
streamRoutes.Use(StreamHandler)
|
||||
|
||||
// ControlableInput
|
||||
streamRoutes.POST("/pause", func(c *gin.Context) {
|
||||
input, ok := c.MustGet("input").(inputs.ControlableInput)
|
||||
if !ok {
|
||||
|
@ -76,16 +112,96 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
|
||||
// MixableInput
|
||||
streamRoutes.POST("/volume", func(c *gin.Context) {
|
||||
input, ok := c.MustGet("input").(inputs.MixableInput)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support that"})
|
||||
return
|
||||
}
|
||||
|
||||
var mixer inputs.InputMixer
|
||||
err := c.BindJSON(&mixer)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err = input.SetMixer(c.MustGet("streamid").(string), &mixer)
|
||||
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)
|
||||
})
|
||||
|
||||
// PlaylistInput
|
||||
streamRoutes.POST("/has_playlist", func(c *gin.Context) {
|
||||
input, ok := c.MustGet("input").(inputs.PlaylistInput)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support that"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, input.HasPlaylist())
|
||||
})
|
||||
streamRoutes.POST("/next_track", func(c *gin.Context) {
|
||||
input, ok := c.MustGet("input").(inputs.PlaylistInput)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support that"})
|
||||
return
|
||||
}
|
||||
|
||||
err := input.NextTrack()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
streamRoutes.POST("/next_random_track", func(c *gin.Context) {
|
||||
input, ok := c.MustGet("input").(inputs.PlaylistInput)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support that"})
|
||||
return
|
||||
}
|
||||
|
||||
err := input.NextRandomTrack()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
streamRoutes.POST("/prev_track", func(c *gin.Context) {
|
||||
input, ok := c.MustGet("input").(inputs.PlaylistInput)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support that"})
|
||||
return
|
||||
}
|
||||
|
||||
err := input.PreviousTrack()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func InputHandler(c *gin.Context) {
|
||||
src, ok := inputs.SoundInputs[c.Param("input")]
|
||||
inp, 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.Set("input", inp)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
|
136
api/sources.go
136
api/sources.go
|
@ -13,10 +13,12 @@ import (
|
|||
)
|
||||
|
||||
type SourceState struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
Controlable bool `json:"controlable,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
Controlable bool `json:"controlable,omitempty"`
|
||||
HasPlaylist bool `json:"hasplaylist,omitempty"`
|
||||
CurrentTitle string `json:"currentTitle,omitempty"`
|
||||
}
|
||||
|
||||
func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
|
@ -27,11 +29,23 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||
active := src.IsActive()
|
||||
_, controlable := src.(inputs.ControlableInput)
|
||||
|
||||
var hasPlaylist bool
|
||||
if p, withPlaylist := src.(inputs.PlaylistInput); withPlaylist {
|
||||
hasPlaylist = p.HasPlaylist()
|
||||
}
|
||||
|
||||
var title string
|
||||
if s, ok := src.(sources.PlayingSource); ok && active {
|
||||
title = s.CurrentlyPlaying()
|
||||
}
|
||||
|
||||
ret[k] = &SourceState{
|
||||
Name: src.GetName(),
|
||||
Enabled: src.IsEnabled(),
|
||||
Active: &active,
|
||||
Controlable: controlable,
|
||||
Name: src.GetName(),
|
||||
Enabled: src.IsEnabled(),
|
||||
Active: &active,
|
||||
Controlable: controlable,
|
||||
HasPlaylist: hasPlaylist,
|
||||
CurrentTitle: title,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,11 +59,25 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||
src := c.MustGet("source").(sources.SoundSource)
|
||||
|
||||
active := src.IsActive()
|
||||
_, controlable := src.(inputs.ControlableInput)
|
||||
|
||||
var hasPlaylist bool
|
||||
if p, withPlaylist := src.(inputs.PlaylistInput); withPlaylist {
|
||||
hasPlaylist = p.HasPlaylist()
|
||||
}
|
||||
|
||||
var title string
|
||||
if s, ok := src.(sources.PlayingSource); ok && active {
|
||||
title = s.CurrentlyPlaying()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &SourceState{
|
||||
Name: src.GetName(),
|
||||
Enabled: src.IsEnabled(),
|
||||
Active: &active,
|
||||
Name: src.GetName(),
|
||||
Enabled: src.IsEnabled(),
|
||||
Active: &active,
|
||||
Controlable: controlable,
|
||||
HasPlaylist: hasPlaylist,
|
||||
CurrentTitle: title,
|
||||
})
|
||||
})
|
||||
sourcesRoutes.GET("/settings", func(c *gin.Context) {
|
||||
|
@ -108,6 +136,8 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
|
||||
// ControlableInput
|
||||
sourcesRoutes.POST("/pause", func(c *gin.Context) {
|
||||
src := c.MustGet("source").(sources.SoundSource)
|
||||
|
||||
|
@ -124,6 +154,90 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||
|
||||
c.JSON(http.StatusOK, s.TogglePause("default"))
|
||||
})
|
||||
|
||||
// PlaylistInput
|
||||
sourcesRoutes.POST("/has_playlist", 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.PlaylistInput)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, s.HasPlaylist())
|
||||
})
|
||||
sourcesRoutes.POST("/next_track", 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.PlaylistInput)
|
||||
if !ok || !s.HasPlaylist() {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support"})
|
||||
return
|
||||
}
|
||||
|
||||
err := s.NextTrack()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
sourcesRoutes.POST("/next_random_track", 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.PlaylistInput)
|
||||
if !ok || !s.HasPlaylist() {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support"})
|
||||
return
|
||||
}
|
||||
|
||||
err := s.NextRandomTrack()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
sourcesRoutes.POST("/prev_track", 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.PlaylistInput)
|
||||
if !ok || !s.HasPlaylist() {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support"})
|
||||
return
|
||||
}
|
||||
|
||||
err := s.PreviousTrack()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func SourceHandler(c *gin.Context) {
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
@ -285,7 +284,6 @@ func (cc *CardControl) CsetAmixer(values ...string) error {
|
|||
fmt.Sprintf("numid=%d", cc.NumID),
|
||||
}
|
||||
opts = append(opts, strings.Join(values, ","))
|
||||
log.Println(opts)
|
||||
cmd := exec.Command("amixer", opts...)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
|
|
22
app.go
22
app.go
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"git.nemunai.re/nemunaire/hathoris/api"
|
||||
"git.nemunai.re/nemunaire/hathoris/config"
|
||||
"git.nemunai.re/nemunaire/hathoris/sources"
|
||||
"git.nemunai.re/nemunaire/hathoris/ui"
|
||||
)
|
||||
|
||||
|
@ -60,6 +61,27 @@ func (app *App) Start() {
|
|||
}
|
||||
|
||||
func (app *App) Stop() {
|
||||
// Disable all sources
|
||||
someEnabled := false
|
||||
for k, src := range sources.SoundSources {
|
||||
if src.IsEnabled() {
|
||||
someEnabled = true
|
||||
go func(k string, src sources.SoundSource) {
|
||||
log.Printf("Stopping %s...", k)
|
||||
err := src.Disable()
|
||||
if err != nil {
|
||||
log.Printf("Unable to disable %s source", k)
|
||||
}
|
||||
log.Printf("%s stopped", k)
|
||||
}(k, src)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for fadeout
|
||||
if someEnabled {
|
||||
time.Sleep(2000 * time.Millisecond)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := app.srv.Shutdown(ctx); err != nil {
|
||||
|
|
|
@ -4,6 +4,14 @@ import ()
|
|||
|
||||
var SoundInputs = map[string]SoundInput{}
|
||||
|
||||
type InputMixer struct {
|
||||
Volume uint `json:"volume"`
|
||||
VolumePercent string `json:"volume_percent"`
|
||||
VolumeDB string `json:"volume_db"`
|
||||
Mute bool `json:"mute"`
|
||||
Balance float64 `json:"balance"`
|
||||
}
|
||||
|
||||
type SoundInput interface {
|
||||
GetName() string
|
||||
IsActive() bool
|
||||
|
@ -13,3 +21,15 @@ type SoundInput interface {
|
|||
type ControlableInput interface {
|
||||
TogglePause(string) error
|
||||
}
|
||||
|
||||
type PlaylistInput interface {
|
||||
HasPlaylist() bool
|
||||
NextTrack() error
|
||||
NextRandomTrack() error
|
||||
PreviousTrack() error
|
||||
}
|
||||
|
||||
type MixableInput interface {
|
||||
GetMixers() (map[string]*InputMixer, error)
|
||||
SetMixer(string, *InputMixer) error
|
||||
}
|
||||
|
|
|
@ -82,16 +82,24 @@ func (s *PulseaudioInput) IsActive() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (s *PulseaudioInput) CurrentlyPlaying() map[string]string {
|
||||
func (s *PulseaudioInput) getPASinkInputs() ([]PASinkInput, error) {
|
||||
cmd := exec.Command("pactl", "-f", "json", "list", "sink-inputs")
|
||||
stdoutStderr, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Println("Unable to list sink-inputs:", err.Error())
|
||||
return nil
|
||||
return nil, fmt.Errorf("unable to list sink-inputs: %w", err)
|
||||
}
|
||||
|
||||
var sinkinputs []PASinkInput
|
||||
err = json.Unmarshal(stdoutStderr, &sinkinputs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse sink-inputs list: %w", err)
|
||||
}
|
||||
|
||||
return sinkinputs, nil
|
||||
}
|
||||
|
||||
func (s *PulseaudioInput) CurrentlyPlaying() map[string]string {
|
||||
sinkinputs, err := s.getPASinkInputs()
|
||||
if err != nil {
|
||||
log.Println("Unable to list sink-inputs:", err.Error())
|
||||
return nil
|
||||
|
@ -110,3 +118,59 @@ func (s *PulseaudioInput) CurrentlyPlaying() map[string]string {
|
|||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *PulseaudioInput) GetMixers() (map[string]*inputs.InputMixer, error) {
|
||||
sinkinputs, err := s.getPASinkInputs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := map[string]*inputs.InputMixer{}
|
||||
for _, input := range sinkinputs {
|
||||
var maxvolume string
|
||||
for k, vol := range input.Volume {
|
||||
if maxvolume == "" || vol.Value > input.Volume[maxvolume].Value {
|
||||
maxvolume = k
|
||||
}
|
||||
}
|
||||
|
||||
ret[strconv.FormatInt(input.Index, 10)] = &inputs.InputMixer{
|
||||
Volume: input.Volume[maxvolume].Value,
|
||||
VolumePercent: input.Volume[maxvolume].ValuePercent,
|
||||
VolumeDB: input.Volume[maxvolume].DB,
|
||||
Balance: input.Balance,
|
||||
Mute: input.Mute,
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s *PulseaudioInput) SetMixer(stream string, volume *inputs.InputMixer) error {
|
||||
sinkinputs, err := s.getPASinkInputs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, input := range sinkinputs {
|
||||
if strconv.FormatInt(input.Index, 10) == stream {
|
||||
if input.Mute != volume.Mute {
|
||||
cmd := exec.Command("pactl", "set-sink-input-mute", stream, "toggle")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to change mute state: %w", err)
|
||||
}
|
||||
} else {
|
||||
cmd := exec.Command("pactl", "set-sink-input-volume", stream, strconv.FormatUint(uint64(volume.Volume), 10))
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to set volume: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to find stream %q", stream)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/DexterLB/mpvipc"
|
||||
|
@ -13,25 +14,29 @@ import (
|
|||
)
|
||||
|
||||
type MPVSource struct {
|
||||
process *exec.Cmd
|
||||
ipcSocket string
|
||||
Name string
|
||||
Options []string
|
||||
File string
|
||||
process *exec.Cmd
|
||||
ipcSocketDir string
|
||||
Name string
|
||||
Options []string
|
||||
File string
|
||||
}
|
||||
|
||||
func init() {
|
||||
sources.SoundSources["mpv-1"] = &MPVSource{
|
||||
Name: "Radio 1",
|
||||
ipcSocket: "/tmp/tmpmpv.radio-1",
|
||||
Options: []string{"--no-video", "--no-terminal"},
|
||||
File: "https://mediaserv38.live-streams.nl:18030/stream",
|
||||
sources.SoundSources["mpv-nig"] = &MPVSource{
|
||||
Name: "Radio NIG",
|
||||
File: "http://stream.syntheticfm.com:8030/stream",
|
||||
}
|
||||
sources.SoundSources["mpv-2"] = &MPVSource{
|
||||
Name: "Radio 2",
|
||||
ipcSocket: "/tmp/tmpmpv.radio-2",
|
||||
Options: []string{"--no-video", "--no-terminal"},
|
||||
File: "https://mediaserv38.live-streams.nl:18040/live",
|
||||
sources.SoundSources["mpv-synthfm"] = &MPVSource{
|
||||
Name: "Radio Synthetic FM",
|
||||
File: "http://stream.syntheticfm.com:8040/stream",
|
||||
}
|
||||
sources.SoundSources["mpv-nrw"] = &MPVSource{
|
||||
Name: "NewRetroWave",
|
||||
File: "https://youtube.com/channel/UCD-4g5w1h8xQpLaNS_ghU4g/videos",
|
||||
}
|
||||
sources.SoundSources["mpv-abgt"] = &MPVSource{
|
||||
Name: "ABGT",
|
||||
File: "https://youtube.com/playlist?list=PL6RLee9oArCArCAjnOtZ17dlVZQxaHG8G",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,15 +52,20 @@ func (s *MPVSource) IsEnabled() bool {
|
|||
return s.process != nil
|
||||
}
|
||||
|
||||
func (s *MPVSource) ipcSocket() string {
|
||||
return path.Join(s.ipcSocketDir, "mpv.socket")
|
||||
}
|
||||
|
||||
func (s *MPVSource) Enable() (err error) {
|
||||
if s.process != nil {
|
||||
return fmt.Errorf("Already running")
|
||||
}
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, s.Options...)
|
||||
if s.ipcSocket != "" {
|
||||
opts = append(opts, "--input-ipc-server="+s.ipcSocket, "--pause")
|
||||
s.ipcSocketDir, err = os.MkdirTemp("", "hathoris")
|
||||
|
||||
opts := append([]string{"--no-video", "--no-terminal"}, s.Options...)
|
||||
if s.ipcSocketDir != "" {
|
||||
opts = append(opts, "--input-ipc-server="+s.ipcSocket(), "--pause")
|
||||
}
|
||||
opts = append(opts, s.File)
|
||||
|
||||
|
@ -70,18 +80,22 @@ func (s *MPVSource) Enable() (err error) {
|
|||
s.process.Process.Kill()
|
||||
}
|
||||
|
||||
if s.ipcSocketDir != "" {
|
||||
os.RemoveAll(s.ipcSocketDir)
|
||||
}
|
||||
|
||||
s.process = nil
|
||||
}()
|
||||
|
||||
if s.ipcSocket != "" {
|
||||
_, err = os.Stat(s.ipcSocket)
|
||||
if s.ipcSocketDir != "" {
|
||||
_, err = os.Stat(s.ipcSocket())
|
||||
for i := 20; i >= 0 && err != nil; i-- {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, err = os.Stat(s.ipcSocket)
|
||||
_, err = os.Stat(s.ipcSocket())
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
conn := mpvipc.NewConnection(s.ipcSocket)
|
||||
conn := mpvipc.NewConnection(s.ipcSocket())
|
||||
err = conn.Open()
|
||||
for i := 20; i >= 0 && err != nil; i-- {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
@ -145,11 +159,13 @@ func (s *MPVSource) FadeOut(conn *mpvipc.Connection, speed int) {
|
|||
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()
|
||||
if s.ipcSocketDir != "" {
|
||||
conn := mpvipc.NewConnection(s.ipcSocket())
|
||||
err := conn.Open()
|
||||
if err == nil {
|
||||
s.FadeOut(conn, 3)
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
s.process.Process.Kill()
|
||||
|
@ -160,8 +176,8 @@ func (s *MPVSource) Disable() error {
|
|||
}
|
||||
|
||||
func (s *MPVSource) CurrentlyPlaying() string {
|
||||
if s.ipcSocket != "" {
|
||||
conn := mpvipc.NewConnection(s.ipcSocket)
|
||||
if s.ipcSocketDir != "" {
|
||||
conn := mpvipc.NewConnection(s.ipcSocket())
|
||||
err := conn.Open()
|
||||
if err != nil {
|
||||
log.Println("Unable to open mpv socket:", err.Error())
|
||||
|
@ -181,11 +197,11 @@ func (s *MPVSource) CurrentlyPlaying() string {
|
|||
}
|
||||
|
||||
func (s *MPVSource) TogglePause(id string) error {
|
||||
if s.ipcSocket == "" {
|
||||
if s.ipcSocketDir == "" {
|
||||
return fmt.Errorf("Not supported")
|
||||
}
|
||||
|
||||
conn := mpvipc.NewConnection(s.ipcSocket)
|
||||
conn := mpvipc.NewConnection(s.ipcSocket())
|
||||
err := conn.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -212,3 +228,93 @@ func (s *MPVSource) TogglePause(id string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MPVSource) HasPlaylist() bool {
|
||||
if s.ipcSocketDir == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
conn := mpvipc.NewConnection(s.ipcSocket())
|
||||
err := conn.Open()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
plistCount, err := conn.Get("playlist-count")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return plistCount.(float64) > 1
|
||||
}
|
||||
|
||||
func (s *MPVSource) NextTrack() error {
|
||||
if s.ipcSocketDir == "" {
|
||||
return fmt.Errorf("Not supported")
|
||||
}
|
||||
|
||||
conn := mpvipc.NewConnection(s.ipcSocket())
|
||||
err := conn.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Call("playlist-next", "weak")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MPVSource) NextRandomTrack() error {
|
||||
if s.ipcSocketDir == "" {
|
||||
return fmt.Errorf("Not supported")
|
||||
}
|
||||
|
||||
conn := mpvipc.NewConnection(s.ipcSocket())
|
||||
err := conn.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Call("playlist-shuffle")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = conn.Call("playlist-next", "weak")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = conn.Call("playlist-unshuffle")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MPVSource) PreviousTrack() error {
|
||||
if s.ipcSocketDir == "" {
|
||||
return fmt.Errorf("Not supported")
|
||||
}
|
||||
|
||||
conn := mpvipc.NewConnection(s.ipcSocket())
|
||||
err := conn.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Call("playlist-prev", "weak")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ func (s *SPDIFSource) IsActive() bool {
|
|||
}
|
||||
|
||||
func (s *SPDIFSource) IsEnabled() bool {
|
||||
return s.processRec != nil
|
||||
return s.processRec != nil || s.processPlay != nil
|
||||
}
|
||||
|
||||
func (s *SPDIFSource) Enable() error {
|
||||
|
@ -77,10 +77,11 @@ func (s *SPDIFSource) Enable() error {
|
|||
go func() {
|
||||
err := s.processPlay.Wait()
|
||||
if err != nil {
|
||||
s.processPlay.Process.Kill()
|
||||
pipeR.Close()
|
||||
pipeW.Close()
|
||||
if s.processPlay != nil && s.processPlay.Process != nil {
|
||||
s.processPlay.Process.Kill()
|
||||
}
|
||||
}
|
||||
pipeR.Close()
|
||||
|
||||
s.processPlay = nil
|
||||
}()
|
||||
|
@ -97,6 +98,7 @@ func (s *SPDIFSource) Enable() error {
|
|||
if err != nil {
|
||||
s.processRec.Process.Kill()
|
||||
}
|
||||
pipeW.Close()
|
||||
|
||||
s.processRec = nil
|
||||
}()
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -10,21 +10,22 @@
|
|||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.5",
|
||||
"vite": "^4.4.2"
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap-icons": "^1.11.1",
|
||||
"sass": "^1.69.5"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import { activeSources } from '$lib/stores/sources';
|
||||
</script>
|
||||
|
||||
<ul class="list-group list-group-flush">
|
||||
{#each $activeSources as source}
|
||||
<li class="list-group-item py-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>{source.name}</strong>
|
||||
{#if source.currentTitle}
|
||||
<span class="text-muted">{source.currentTitle}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if source.controlable || source.hasplaylist}
|
||||
<div class="d-flex gap-1">
|
||||
{#if source.hasplaylist}
|
||||
<div class="btn-group" role="group">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
on:click={() => source.prevtrack()}
|
||||
>
|
||||
<i class="bi bi-skip-backward-fill"></i>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
on:click={() => source.nexttrack()}
|
||||
on:dblclick={() => source.nextrandomtrack()}
|
||||
>
|
||||
<i class="bi bi-skip-forward-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if source.controlable}
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
on:click={() => source.playpause()}
|
||||
>
|
||||
<i class="bi bi-pause"></i>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
|
@ -1,71 +1,82 @@
|
|||
<script>
|
||||
import { activeInputs, inputsList } from '$lib/stores/inputs';
|
||||
import { activeSources } from '$lib/stores/sources';
|
||||
import { activeInputs, inputs, inputsList } from '$lib/stores/inputs';
|
||||
|
||||
export let showInactives = false;
|
||||
|
||||
let altering_mixer = null;
|
||||
async function alterMixer(input, streamid, volume) {
|
||||
if (altering_mixer) altering_mixer.abort();
|
||||
altering_mixer = setTimeout(() => {
|
||||
fetch(`api/inputs/${input.name}/streams/${streamid}/volume`, {headers: {'Accept': 'application/json'}, method: 'POST', body: JSON.stringify({'volume': volume ? volume : input.mixer[streamid].volume})}).then(() => inputs.refresh());
|
||||
altering_mixer = null;
|
||||
}, 450);
|
||||
}
|
||||
async function muteMixer(input, streamid, mute) {
|
||||
fetch(`api/inputs/${input.name}/streams/${streamid}/volume`, {headers: {'Accept': 'application/json'}, method: 'POST', body: JSON.stringify({'mute': mute !== undefined ? mute : input.mixer[streamid].mute})}).then(() => inputs.refresh());
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="list-group list-group-flush">
|
||||
{#if $activeSources.length === 0 && ((showInactives && $inputsList.length === 0) || (!showInactives && $activeInputs.length === 0))}
|
||||
{#if (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}
|
||||
{#each $inputsList as input, iid}
|
||||
{#if showInactives || input.active}
|
||||
<li class="list-group-item py-3 d-flex flex-column">
|
||||
<strong>{input.name}</strong>
|
||||
{#await input.streams()}
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{:then streams}
|
||||
{#each Object.keys(streams) as idstream}
|
||||
{@const title = streams[idstream]}
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<span class="text-muted">{title}</span>
|
||||
</div>
|
||||
{#each Object.keys(input.streams) as idstream}
|
||||
{@const title = input.streams[idstream]}
|
||||
<li class="list-group-item py-3 d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<label for={'input' + iid + 'stream' + idstream} class="form-label d-inline">{title}</label>
|
||||
<span class="text-muted">({input.name})</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
{#if input.mixable && input.mixer[idstream]}
|
||||
<button
|
||||
class="btn btn-sm ms-1"
|
||||
class:btn-primary={input.mixer[idstream].mute}
|
||||
class:btn-secondary={!input.mixer[idstream].mute}
|
||||
on:click={() => {muteMixer(input, idstream, !input.mixer[idstream].mute);}}
|
||||
>
|
||||
<i class="bi bi-volume-mute-fill"></i>
|
||||
</button>
|
||||
{/if}
|
||||
{#if input.controlable}
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
on:click={() => input.playpause(idstream)}
|
||||
>
|
||||
<i class="bi bi-pause"></i>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-primary ms-1"
|
||||
on:click={() => input.playpause(idstream)}
|
||||
>
|
||||
<i class="bi bi-pause"></i>
|
||||
</button>
|
||||
{:else if input.mixable && input.mixer[idstream]}
|
||||
<div
|
||||
class="badge bg-primary ms-1"
|
||||
title={input.mixer[idstream].volume_percent}
|
||||
>
|
||||
{input.mixer[idstream].volume_db}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/await}
|
||||
</li>
|
||||
</div>
|
||||
{#if input.mixable && input.mixer[idstream]}
|
||||
<div>
|
||||
<input
|
||||
type="range"
|
||||
class="form-range"
|
||||
id={'input' + iid + 'stream' + idstream}
|
||||
min={0}
|
||||
max={65536}
|
||||
bind:value={input.mixer[idstream].volume}
|
||||
on:change={() => alterMixer(input, idstream)}
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
|
|
|
@ -6,13 +6,17 @@ export class Input {
|
|||
}
|
||||
}
|
||||
|
||||
update({ name, active, controlable }) {
|
||||
update({ name, active, controlable, hasplaylist, streams, mixable, mixer }) {
|
||||
this.name = name;
|
||||
this.active = active;
|
||||
this.controlable = controlable;
|
||||
this.hasplaylist = hasplaylist;
|
||||
this.streams = streams;
|
||||
this.mixable = mixable;
|
||||
this.mixer = mixer;
|
||||
}
|
||||
|
||||
async streams() {
|
||||
async getStreams() {
|
||||
const data = await fetch(`api/inputs/${this.id}/streams`, {headers: {'Accept': 'application/json'}});
|
||||
if (data.status == 200) {
|
||||
return await data.json();
|
||||
|
@ -27,6 +31,27 @@ export class Input {
|
|||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async nexttrack(idstream) {
|
||||
const data = await fetch(`api/inputs/${this.id}/streams/${idstream}/next_track`, {headers: {'Accept': 'application/json'}, method: 'POST'});
|
||||
if (data.status != 200) {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async nextrandomtrack(idstream) {
|
||||
const data = await fetch(`api/inputs/${this.id}/streams/${idstream}/next_random_track`, {headers: {'Accept': 'application/json'}, method: 'POST'});
|
||||
if (data.status != 200) {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async prevtrack(idstream) {
|
||||
const data = await fetch(`api/inputs/${this.id}/streams/${idstream}/prev_track`, {headers: {'Accept': 'application/json'}, method: 'POST'});
|
||||
if (data.status != 200) {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getInputs() {
|
||||
|
|
|
@ -6,11 +6,13 @@ export class Source {
|
|||
}
|
||||
}
|
||||
|
||||
update({ name, enabled, active, controlable }) {
|
||||
update({ name, enabled, active, controlable, hasplaylist, currentTitle }) {
|
||||
this.name = name;
|
||||
this.enabled = enabled;
|
||||
this.active = active;
|
||||
this.controlable = controlable;
|
||||
this.hasplaylist = hasplaylist;
|
||||
this.currentTitle = currentTitle;
|
||||
}
|
||||
|
||||
async activate() {
|
||||
|
@ -36,6 +38,27 @@ export class Source {
|
|||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async nexttrack() {
|
||||
const data = await fetch(`api/sources/${this.id}/next_track`, {headers: {'Accept': 'application/json'}, method: 'POST'});
|
||||
if (data.status != 200) {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async nextrandomtrack() {
|
||||
const data = await fetch(`api/sources/${this.id}/next_random_track`, {headers: {'Accept': 'application/json'}, method: 'POST'});
|
||||
if (data.status != 200) {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async prevtrack() {
|
||||
const data = await fetch(`api/sources/${this.id}/prev_track`, {headers: {'Accept': 'application/json'}, method: 'POST'});
|
||||
if (data.status != 200) {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import Applications from '$lib/components/Applications.svelte';
|
||||
import Inputs from '$lib/components/Inputs.svelte';
|
||||
import Mixer from '$lib/components/Mixer.svelte';
|
||||
import SourceSelection from '$lib/components/SourceSelection.svelte';
|
||||
|
@ -6,9 +7,10 @@
|
|||
import { activeInputs } from '$lib/stores/inputs';
|
||||
|
||||
let mixerAdvanced = false;
|
||||
let showInactiveInputs = false;
|
||||
</script>
|
||||
|
||||
<div class="my-2">
|
||||
<div class="my-3">
|
||||
<SourceSelection />
|
||||
</div>
|
||||
|
||||
|
@ -23,15 +25,11 @@
|
|||
<div class="d-inline-block me-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
{#await source.currently()}
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div> <span class="text-muted">@ {source.name}</span>
|
||||
{:then title}
|
||||
<strong>{title}</strong> <span class="text-muted">@ {source.name}</span>
|
||||
{:catch error}
|
||||
{#if source.currentTitle}
|
||||
<strong>{source.currentTitle}</strong> <span class="text-muted">@ {source.name}</span>
|
||||
{:else}
|
||||
{source.name} activée
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,19 +38,15 @@
|
|||
<div class="d-inline-block me-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
{#await input.streams()}
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div> <span class="text-muted">@ {input.name}</span>
|
||||
{:then streams}
|
||||
{#each Object.keys(streams) as idstream}
|
||||
{@const title = streams[idstream]}
|
||||
{#if input.streams.length}
|
||||
{#each Object.keys(input.streams) as idstream}
|
||||
{@const title = input.streams[idstream]}
|
||||
<strong>{title}</strong>
|
||||
{/each}
|
||||
<span class="text-muted">@ {input.name}</span>
|
||||
{:catch error}
|
||||
{:else}
|
||||
{input.name} activée
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -62,7 +56,7 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="card my-2">
|
||||
<div class="card my-3">
|
||||
<h4 class="card-header">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
|
@ -84,12 +78,37 @@
|
|||
</div>
|
||||
|
||||
<div class="col-md">
|
||||
<div class="card my-2">
|
||||
{#if $activeSources.length > 0}
|
||||
<div class="card my-3">
|
||||
<h4 class="card-header">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<i class="bi bi-window-stack"></i>
|
||||
Applications
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
<Applications />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card my-3">
|
||||
<h4 class="card-header">
|
||||
<i class="bi bi-speaker"></i>
|
||||
Sources
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<i class="bi bi-speaker"></i>
|
||||
Sources
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:btn-info={showInactiveInputs}
|
||||
class:btn-secondary={!showInactiveInputs}
|
||||
on:click={() => { showInactiveInputs = !showInactiveInputs; }}
|
||||
>
|
||||
<i class="bi bi-eye-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</h4>
|
||||
<Inputs />
|
||||
<Inputs showInactive={showInactiveInputs} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue