a first scratch of liquidsoap library
This commit is contained in:
commit
e34f699baa
5 changed files with 307 additions and 0 deletions
65
cmd/direttoforo/main.go
Normal file
65
cmd/direttoforo/main.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.lattuga.net/boyska/direttoforo.git/liquidsoap"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func outUI(output chan liquidsoap.Output) {
|
||||||
|
for msg := range output {
|
||||||
|
if msg.Level <= 2 {
|
||||||
|
fmt.Println(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
killLs := make(chan struct{}) // when it is closed, liquidsoap will die
|
||||||
|
killed := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(killed, os.Interrupt) // ctrl-c
|
||||||
|
output, exit, err := liquidsoap.RunLiquidsoap("foo.liq", killLs)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
go outUI(output)
|
||||||
|
go func() {
|
||||||
|
tick := time.Tick(3 * time.Second)
|
||||||
|
for {
|
||||||
|
<-tick
|
||||||
|
t, err := liquidsoap.NewTelnet("localhost", 1234)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("telnet connection errored", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Conn.SetDeadline(time.Now().Add(3 * time.Second))
|
||||||
|
out, err := t.Outputs()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("telnet cmd errored", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Close()
|
||||||
|
fmt.Println("list=", out)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case how := <-exit: // liquidsoap exits
|
||||||
|
if !how.Success() {
|
||||||
|
fmt.Fprintln(os.Stderr, how.Err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
case <-killed: // we receive a SIGINT: ask liquidsoap to die is enough
|
||||||
|
close(killLs)
|
||||||
|
fmt.Println("Closed by user interaction, waiting for liquidsoap to exit")
|
||||||
|
// TODO: schedule a more aggressive SIGKILL if liquidsoap doesn't exit soon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
45
liquidsoap/parser.go
Normal file
45
liquidsoap/parser.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package liquidsoap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Output represents a liquidsoap message in a structured form
|
||||||
|
type Output struct {
|
||||||
|
Level int // lower means more important
|
||||||
|
Msg string
|
||||||
|
Component string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse needs a reader of the log, and will produce parsed output
|
||||||
|
func Parse(r io.Reader, parsed chan<- Output) {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
out, err := outParseLine(scanner.Text())
|
||||||
|
if err == nil {
|
||||||
|
parsed <- out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineRegex = regexp.MustCompile(`^([0-9]{4}/[0-9]{2}/[0-9]{2}) ([0-9:]{8}) \[([^:\[\]]*):([0-9])\] (.*)$`)
|
||||||
|
|
||||||
|
func outParseLine(line string) (Output, error) {
|
||||||
|
res := lineRegex.FindStringSubmatch(line)
|
||||||
|
if res == nil {
|
||||||
|
return Output{}, errors.New("Invalid line")
|
||||||
|
}
|
||||||
|
if len(res) != 6 {
|
||||||
|
return Output{}, errors.New("Invalid line: missing parts")
|
||||||
|
}
|
||||||
|
level, err := strconv.Atoi(res[4])
|
||||||
|
if err != nil {
|
||||||
|
return Output{}, fmt.Errorf("Invalid line: invalid level `%s`", res[4])
|
||||||
|
}
|
||||||
|
return Output{Level: level, Msg: res[5], Component: res[3]}, nil
|
||||||
|
}
|
36
liquidsoap/parser_test.go
Normal file
36
liquidsoap/parser_test.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package liquidsoap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type lineParseTest struct {
|
||||||
|
Line string
|
||||||
|
Err error
|
||||||
|
Out Output
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = []lineParseTest{
|
||||||
|
{"2014/10/13 02:58:16 [/foo(dot)mp3:2] Connection failed: 401",
|
||||||
|
nil, Output{Level: 2, Msg: "Connection failed: 401", Component: "/foo(dot)mp3"}},
|
||||||
|
{"2014/10/13 02:58:16 [asd:1] Gulp[every,strange,char!]",
|
||||||
|
nil, Output{Level: 1, Msg: "Gulp[every,strange,char!]", Component: "asd"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLine(t *testing.T) {
|
||||||
|
for _, pair := range lines {
|
||||||
|
out, err := outParseLine(pair.Line)
|
||||||
|
if err != pair.Err {
|
||||||
|
t.Error(
|
||||||
|
"For", pair.Line,
|
||||||
|
"error expected", pair.Err,
|
||||||
|
"got", err)
|
||||||
|
}
|
||||||
|
if out != pair.Out {
|
||||||
|
t.Error(
|
||||||
|
"For", pair.Line,
|
||||||
|
"expected", pair.Out,
|
||||||
|
"got", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
liquidsoap/spawn.go
Normal file
57
liquidsoap/spawn.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package liquidsoap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// End describes how liquidsoap ended; successfully? With errors?
|
||||||
|
type End struct {
|
||||||
|
Retval int
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success returns true if the process ended successfully
|
||||||
|
func (e *End) Success() bool {
|
||||||
|
if e.Err == nil && e.Retval == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunLiquidsoap will launch a new instance of liquidsoap and take control of it. It will also read output
|
||||||
|
// messages and send them to out chan. No magic will be done to liquidsoap config file, so if you want output
|
||||||
|
// messages, don't forget to set("log.stderr", true) in your liquidsoap file.
|
||||||
|
//
|
||||||
|
// RunLiquidsoap is an async function, which provides channels as feedback and needs a channel to ask for its
|
||||||
|
// termination
|
||||||
|
func RunLiquidsoap(configfile string, kill <-chan struct{}) (chan Output, chan End, error) {
|
||||||
|
out := make(chan Output)
|
||||||
|
exit := make(chan End, 1)
|
||||||
|
cmd := exec.Command("liquidsoap", "--enable-telnet", configfile)
|
||||||
|
cmd.Stderr = os.Stderr // connect liquidsoap err to process stderr
|
||||||
|
log, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
go Parse(log, out)
|
||||||
|
err = cmd.Start()
|
||||||
|
proc := cmd.Process
|
||||||
|
go func() {
|
||||||
|
<-kill
|
||||||
|
proc.Signal(syscall.SIGINT)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer log.Close()
|
||||||
|
err = cmd.Wait()
|
||||||
|
close(out)
|
||||||
|
if err != nil {
|
||||||
|
exit <- End{Err: err}
|
||||||
|
} else {
|
||||||
|
exit <- End{}
|
||||||
|
}
|
||||||
|
close(exit)
|
||||||
|
}()
|
||||||
|
return out, exit, err
|
||||||
|
}
|
104
liquidsoap/telnet.go
Normal file
104
liquidsoap/telnet.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package liquidsoap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client represents a telnet/unix domain connection to Liquidsoap
|
||||||
|
//
|
||||||
|
// A Client can perform low-level operations, such as Command(), or high-level operations such as Outputs()
|
||||||
|
type Client struct {
|
||||||
|
Conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTelnet returns a liquidsoap.Client created using telnet on the given parameters
|
||||||
|
func NewTelnet(host string, port int) (Client, error) {
|
||||||
|
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||||
|
if err != nil {
|
||||||
|
return Client{}, err
|
||||||
|
}
|
||||||
|
return Client{Conn: conn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the connection
|
||||||
|
func (c *Client) Close() {
|
||||||
|
if c.Conn != nil {
|
||||||
|
c.Conn.Write([]byte("quit\n"))
|
||||||
|
c.Conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command run a command, wait for output response to come and returns it
|
||||||
|
func (c *Client) Command(cmdline string) (string, error) {
|
||||||
|
c.Conn.Write([]byte(cmdline + "\n"))
|
||||||
|
var out, line string
|
||||||
|
var err error
|
||||||
|
reader := bufio.NewReader(c.Conn)
|
||||||
|
for {
|
||||||
|
if line, err = reader.ReadString('\n'); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
line = line[:len(line)-2] + "\n" // \r\n -> \n
|
||||||
|
if line == "END\n" {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
out += line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs will return a map of outputs that liquidsoap is handling.
|
||||||
|
// An output is set to true if it is enabled
|
||||||
|
func (c *Client) Outputs() (outputs map[string]bool, err error) {
|
||||||
|
outputs = make(map[string]bool)
|
||||||
|
var cmdout string
|
||||||
|
cmdout, err = c.Command("list")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lines := strings.Split(cmdout, "\n")
|
||||||
|
for _, l := range lines {
|
||||||
|
parts := strings.SplitN(l, " : ", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Index(parts[1], "output.") == 0 {
|
||||||
|
name := strings.TrimSpace(parts[0])
|
||||||
|
var enabled bool
|
||||||
|
enabled, err = c.GetOutput(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
outputs[name] = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOutput checks whether an output is enabled
|
||||||
|
func (c *Client) GetOutput(name string) (bool, error) {
|
||||||
|
cmdout, err := c.Command(name + ".status")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(cmdout) == "on", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutput enables or disables an output
|
||||||
|
func (c *Client) SetOutput(name string, enabled bool) error {
|
||||||
|
action := "stop"
|
||||||
|
if enabled {
|
||||||
|
action = "start"
|
||||||
|
}
|
||||||
|
cmdout, err := c.Command(fmt.Sprintf("%s.%s", name, action))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cmdout) != "OK" {
|
||||||
|
return fmt.Errorf("Error: liquidsoap replied '%s'", cmdout)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue