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