Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: MPD server #487

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cmd/gonic/gonic.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ import (
"go.senan.xyz/gonic/scrobble"
"go.senan.xyz/gonic/server/ctrladmin"
"go.senan.xyz/gonic/server/ctrlsubsonic"
"go.senan.xyz/gonic/server/mpd"
"go.senan.xyz/gonic/tags/tagcommon"
"go.senan.xyz/gonic/tags/taglib"
"go.senan.xyz/gonic/transcode"
)

func main() {
confListenAddr := flag.String("listen-addr", "0.0.0.0:4747", "listen address (optional)")
confMPDListenAddr := flag.String("mpd-listen-addr", "0.0.0.0:6600", "listen address for MPD protocol (optional)")

confTLSCert := flag.String("tls-cert", "", "path to TLS certificate (optional)")
confTLSKey := flag.String("tls-key", "", "path to TLS private key (optional)")
Expand Down Expand Up @@ -318,6 +320,17 @@ func main() {
return server.ListenAndServe()
})

errgrp.Go(func() error {
defer logJob("mpd")()

server, err := mpd.New(jukebx)
if err != nil {
return fmt.Errorf("mpd init: %w", err)
}

return server.ListenAndServe(ctx, *confMPDListenAddr)
})

errgrp.Go(func() error {
if !*confScanWatcher {
return nil
Expand Down
20 changes: 16 additions & 4 deletions jukebox/jukebox.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,20 +237,32 @@ func (j *Jukebox) ClearPlaylist() error {
}

func (j *Jukebox) Pause() error {
return j.SetPlay(false)
}

func (j *Jukebox) Play() error {
return j.SetPlay(true)
}

func (j *Jukebox) SetPlay(state bool) error {
defer lock(&j.mu)()

if err := j.conn.Set("pause", true); err != nil {
pause := !state

if err := j.conn.Set("pause", pause); err != nil {
return fmt.Errorf("pause: %w", err)
}

return nil
}

func (j *Jukebox) Play() error {
func (j *Jukebox) TogglePlay() error {
defer lock(&j.mu)()

if err := j.conn.Set("pause", false); err != nil {
return fmt.Errorf("pause: %w", err)
if _, err := j.conn.Call("cycle", "pause"); err != nil {
return fmt.Errorf("cycle pause: %w", err)
}

return nil
}

Expand Down
46 changes: 46 additions & 0 deletions server/mpd/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package mpd

import "fmt"

// ACK error codes
// https://github.com/MusicPlayerDaemon/MPD/blob/master/src/protocol/Ack.hxx
type ackError uint

const (
ackErrorNotList ackError = 1
ackErrorArg = 2
ackErrorPassword = 3
ackErrorPermission = 4
ackErrorUnknown = 5

ackErrorNoExist = 50
ackErrorPlaylistMax = 51
ackErrorSystem = 52
ackErrorPlaylistLoad = 53
ackErrorUpdateAlready = 54
ackErrorPlayerSync = 55
ackErrorExist = 56
)

type errorResponse struct {
code ackError
cmdListNum uint
cmdName string
err error
}

func newError(code ackError, cmdIdx uint, cmdName string, err error) *errorResponse {
return &errorResponse{code, cmdIdx, cmdName, err}
}

func (e errorResponse) String() string {
return fmt.Sprintf("ACK [%d@%d] {%s} %s", e.code, e.cmdListNum, e.cmdName, e.err.Error())
}

func (e errorResponse) Error() string {
return fmt.Sprintf("%s: %v", e.cmdName, e.err)
}

func (e errorResponse) Unwrap() error {
return e.err
}
97 changes: 97 additions & 0 deletions server/mpd/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package mpd

import (
"bufio"
"strings"
)

type lineReader struct {
inner *bufio.Reader
idx uint
}

func newLineReader(rd *bufio.Reader) *lineReader {
return &lineReader{rd, 0}
}

func (lr *lineReader) Next() (uint, string, error) {
line, err := lr.inner.ReadString('\n')
if err != nil {
return 0, "", err
}

lr.idx += 1
line = strings.TrimSuffix(line, "\n")

return lr.idx, line, nil
}

// argParser splits a line into a sequence of arguments.
// The first argument is mandatory and is the command.
type argParser struct {
line string
buff []rune
idx uint // argument index
}

func newArgParser(line string) *argParser {
return &argParser{line, []rune(line), 0}
}

func (p *argParser) pos() int {
return len(p.line) - len(p.buff)
}

func (p *argParser) Next() (string, bool) {
const (
linearSpace = " \t"

stateSpace = iota
stateArg
stateQuote
)

if len(p.buff) == 0 {
return "", false
}

res := make([]rune, 0, len(p.buff))

i := 0
loop:
for state := stateSpace; i < len(p.buff); i++ {
r := p.buff[i]
isSpace := strings.IndexRune(linearSpace, r) != -1

if state == stateSpace {
if isSpace {
continue
}

state = stateArg
}

switch {
case state == stateArg && isSpace:
break loop // space -> end of argument

case state == stateArg && r == '"':
state = stateQuote // quote begin
continue

case state == stateQuote && r == '\\':
i++ // skip the backslash

case state == stateQuote && r == '"':
state = stateArg // quote end
continue
}

res = append(res, p.buff[i])
}

p.idx += 1
p.buff = p.buff[i:]

return string(res), true
}
32 changes: 32 additions & 0 deletions server/mpd/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package mpd

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestArgParser(t *testing.T) {
t.Parallel()

expectations := map[string][]string{
`cmd`: {"cmd"},
`cmd arg`: {"cmd", "arg"},
`cmd "arg one" arg-two`: {"cmd", "arg one", "arg-two"},
`find "(Artist == \"foo\\'bar\\\"\")"`: {"find", `(Artist == "foo\'bar\"")`},
}

for line, args := range expectations {
p := newArgParser(line)

for i, expected := range args {
parsed, ok := p.Next()
assert.True(t, ok)
assert.Equal(t, uint(i+1), p.idx)
assert.Equal(t, expected, parsed)
}

arg, ok := p.Next()
assert.False(t, ok, "parser return extra arg: %q", arg)
}
}
Loading
Loading