Can control playlist
This commit is contained in:
parent
1d091be264
commit
f7760416b9
@ -14,6 +14,7 @@ type InputState struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
Controlable bool `json:"controlable"`
|
Controlable bool `json:"controlable"`
|
||||||
|
HasPlaylist bool `json:"hasplaylist"`
|
||||||
Streams map[string]string `json:"streams,omitempty"`
|
Streams map[string]string `json:"streams,omitempty"`
|
||||||
Mixable bool `json:"mixable"`
|
Mixable bool `json:"mixable"`
|
||||||
Mixer map[string]*inputs.InputMixer `json:"mixer,omitempty"`
|
Mixer map[string]*inputs.InputMixer `json:"mixer,omitempty"`
|
||||||
@ -32,10 +33,16 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
mixer, _ = im.GetMixers()
|
mixer, _ = im.GetMixers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasPlaylist bool
|
||||||
|
if p, withPlaylist := inp.(inputs.PlaylistInput); withPlaylist {
|
||||||
|
hasPlaylist = p.HasPlaylist()
|
||||||
|
}
|
||||||
|
|
||||||
ret[k] = &InputState{
|
ret[k] = &InputState{
|
||||||
Name: inp.GetName(),
|
Name: inp.GetName(),
|
||||||
Active: inp.IsActive(),
|
Active: inp.IsActive(),
|
||||||
Controlable: controlable,
|
Controlable: controlable,
|
||||||
|
HasPlaylist: hasPlaylist,
|
||||||
Streams: inp.CurrentlyPlaying(),
|
Streams: inp.CurrentlyPlaying(),
|
||||||
Mixable: mixable,
|
Mixable: mixable,
|
||||||
Mixer: mixer,
|
Mixer: mixer,
|
||||||
@ -57,11 +64,16 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
if mixable {
|
if mixable {
|
||||||
mixer, _ = im.GetMixers()
|
mixer, _ = im.GetMixers()
|
||||||
}
|
}
|
||||||
|
var hasPlaylist bool
|
||||||
|
if p, withPlaylist := inp.(inputs.PlaylistInput); withPlaylist {
|
||||||
|
hasPlaylist = p.HasPlaylist()
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &InputState{
|
c.JSON(http.StatusOK, &InputState{
|
||||||
Name: inp.GetName(),
|
Name: inp.GetName(),
|
||||||
Active: inp.IsActive(),
|
Active: inp.IsActive(),
|
||||||
Controlable: controlable,
|
Controlable: controlable,
|
||||||
|
HasPlaylist: hasPlaylist,
|
||||||
Streams: inp.CurrentlyPlaying(),
|
Streams: inp.CurrentlyPlaying(),
|
||||||
Mixable: mixable,
|
Mixable: mixable,
|
||||||
Mixer: mixer,
|
Mixer: mixer,
|
||||||
@ -84,6 +96,7 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
streamRoutes := inputsRoutes.Group("/streams/:stream")
|
streamRoutes := inputsRoutes.Group("/streams/:stream")
|
||||||
streamRoutes.Use(StreamHandler)
|
streamRoutes.Use(StreamHandler)
|
||||||
|
|
||||||
|
// ControlableInput
|
||||||
streamRoutes.POST("/pause", func(c *gin.Context) {
|
streamRoutes.POST("/pause", func(c *gin.Context) {
|
||||||
input, ok := c.MustGet("input").(inputs.ControlableInput)
|
input, ok := c.MustGet("input").(inputs.ControlableInput)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -99,6 +112,8 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, true)
|
c.JSON(http.StatusOK, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// MixableInput
|
||||||
streamRoutes.POST("/volume", func(c *gin.Context) {
|
streamRoutes.POST("/volume", func(c *gin.Context) {
|
||||||
input, ok := c.MustGet("input").(inputs.MixableInput)
|
input, ok := c.MustGet("input").(inputs.MixableInput)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -121,6 +136,47 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, true)
|
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("/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) {
|
func InputHandler(c *gin.Context) {
|
||||||
|
@ -17,6 +17,7 @@ type SourceState struct {
|
|||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Active *bool `json:"active,omitempty"`
|
Active *bool `json:"active,omitempty"`
|
||||||
Controlable bool `json:"controlable,omitempty"`
|
Controlable bool `json:"controlable,omitempty"`
|
||||||
|
HasPlaylist bool `json:"hasplaylist,omitempty"`
|
||||||
CurrentTitle string `json:"currentTitle,omitempty"`
|
CurrentTitle string `json:"currentTitle,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +29,11 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
active := src.IsActive()
|
active := src.IsActive()
|
||||||
_, controlable := src.(inputs.ControlableInput)
|
_, controlable := src.(inputs.ControlableInput)
|
||||||
|
|
||||||
|
var hasPlaylist bool
|
||||||
|
if p, withPlaylist := src.(inputs.PlaylistInput); withPlaylist {
|
||||||
|
hasPlaylist = p.HasPlaylist()
|
||||||
|
}
|
||||||
|
|
||||||
var title string
|
var title string
|
||||||
if s, ok := src.(sources.PlayingSource); ok && active {
|
if s, ok := src.(sources.PlayingSource); ok && active {
|
||||||
title = s.CurrentlyPlaying()
|
title = s.CurrentlyPlaying()
|
||||||
@ -38,6 +44,7 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
Enabled: src.IsEnabled(),
|
Enabled: src.IsEnabled(),
|
||||||
Active: &active,
|
Active: &active,
|
||||||
Controlable: controlable,
|
Controlable: controlable,
|
||||||
|
HasPlaylist: hasPlaylist,
|
||||||
CurrentTitle: title,
|
CurrentTitle: title,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,6 +61,11 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
active := src.IsActive()
|
active := src.IsActive()
|
||||||
_, controlable := src.(inputs.ControlableInput)
|
_, controlable := src.(inputs.ControlableInput)
|
||||||
|
|
||||||
|
var hasPlaylist bool
|
||||||
|
if p, withPlaylist := src.(inputs.PlaylistInput); withPlaylist {
|
||||||
|
hasPlaylist = p.HasPlaylist()
|
||||||
|
}
|
||||||
|
|
||||||
var title string
|
var title string
|
||||||
if s, ok := src.(sources.PlayingSource); ok && active {
|
if s, ok := src.(sources.PlayingSource); ok && active {
|
||||||
title = s.CurrentlyPlaying()
|
title = s.CurrentlyPlaying()
|
||||||
@ -64,6 +76,7 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
Enabled: src.IsEnabled(),
|
Enabled: src.IsEnabled(),
|
||||||
Active: &active,
|
Active: &active,
|
||||||
Controlable: controlable,
|
Controlable: controlable,
|
||||||
|
HasPlaylist: hasPlaylist,
|
||||||
CurrentTitle: title,
|
CurrentTitle: title,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -123,6 +136,8 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, true)
|
c.JSON(http.StatusOK, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ControlableInput
|
||||||
sourcesRoutes.POST("/pause", func(c *gin.Context) {
|
sourcesRoutes.POST("/pause", func(c *gin.Context) {
|
||||||
src := c.MustGet("source").(sources.SoundSource)
|
src := c.MustGet("source").(sources.SoundSource)
|
||||||
|
|
||||||
@ -139,6 +154,68 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, s.TogglePause("default"))
|
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("/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) {
|
func SourceHandler(c *gin.Context) {
|
||||||
|
@ -22,6 +22,12 @@ type ControlableInput interface {
|
|||||||
TogglePause(string) error
|
TogglePause(string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlaylistInput interface {
|
||||||
|
HasPlaylist() bool
|
||||||
|
NextTrack() error
|
||||||
|
PreviousTrack() error
|
||||||
|
}
|
||||||
|
|
||||||
type MixableInput interface {
|
type MixableInput interface {
|
||||||
GetMixers() (map[string]*InputMixer, error)
|
GetMixers() (map[string]*InputMixer, error)
|
||||||
SetMixer(string, *InputMixer) error
|
SetMixer(string, *InputMixer) error
|
||||||
|
@ -228,3 +228,63 @@ func (s *MPVSource) TogglePause(id string) error {
|
|||||||
|
|
||||||
return nil
|
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) 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
|
||||||
|
}
|
||||||
|
@ -11,14 +11,32 @@
|
|||||||
<span class="text-muted">{source.currentTitle}</span>
|
<span class="text-muted">{source.currentTitle}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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()}
|
||||||
|
>
|
||||||
|
<i class="bi bi-skip-forward-fill"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if source.controlable}
|
{#if source.controlable}
|
||||||
<div>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
on:click={() => source.playpause()}
|
on:click={() => source.playpause()}
|
||||||
>
|
>
|
||||||
<i class="bi bi-pause"></i>
|
<i class="bi bi-pause"></i>
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
|
@ -6,10 +6,11 @@ export class Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update({ name, active, controlable, streams, mixable, mixer }) {
|
update({ name, active, controlable, hasplaylist, streams, mixable, mixer }) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.active = active;
|
this.active = active;
|
||||||
this.controlable = controlable;
|
this.controlable = controlable;
|
||||||
|
this.hasplaylist = hasplaylist;
|
||||||
this.streams = streams;
|
this.streams = streams;
|
||||||
this.mixable = mixable;
|
this.mixable = mixable;
|
||||||
this.mixer = mixer;
|
this.mixer = mixer;
|
||||||
@ -30,6 +31,20 @@ export class Input {
|
|||||||
throw new Error((await res.json()).errmsg);
|
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 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() {
|
export async function getInputs() {
|
||||||
|
@ -6,11 +6,12 @@ export class Source {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update({ name, enabled, active, controlable, currentTitle }) {
|
update({ name, enabled, active, controlable, hasplaylist, currentTitle }) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
this.active = active;
|
this.active = active;
|
||||||
this.controlable = controlable;
|
this.controlable = controlable;
|
||||||
|
this.hasplaylist = hasplaylist;
|
||||||
this.currentTitle = currentTitle;
|
this.currentTitle = currentTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,6 +38,20 @@ export class Source {
|
|||||||
throw new Error((await res.json()).errmsg);
|
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 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() {
|
export async function getSources() {
|
||||||
|
Loading…
Reference in New Issue
Block a user