997 lines
28 KiB
Go
997 lines
28 KiB
Go
// Copyright 2009 The GoMPD Authors. All rights reserved.
|
|
// Use of this source code is governed by the MIT
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package mpd provides the client side interface to MPD (Music Player Daemon).
|
|
// The protocol reference can be found at http://www.musicpd.org/doc/protocol/index.html
|
|
package mpd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/textproto"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Quote quotes string VALUES in the format understood by MPD.
|
|
// See: https://github.com/MusicPlayerDaemon/MPD/blob/master/src/util/Tokenizer.cxx
|
|
// NB: this function shouldn't be used on the PROTOCOL LEVEL because it considers single quotes special chars and
|
|
// escapes them.
|
|
func quote(s string) string {
|
|
q := make([]byte, 2+2*len(s))
|
|
i := 0
|
|
q[i], i = '"', i+1
|
|
for _, c := range []byte(s) {
|
|
// We need to escape single/double quotes and a backslash by prepending them with a '\'
|
|
if c == '"' || c == '\\' || c == '\'' {
|
|
q[i] = '\\'
|
|
i++
|
|
}
|
|
q[i] = c
|
|
i++
|
|
}
|
|
q[i], i = '"', i+1
|
|
return string(q[:i])
|
|
}
|
|
|
|
// Quote quotes each string of args in the format understood by MPD.
|
|
// See: https://github.com/MusicPlayerDaemon/MPD/blob/master/src/util/Tokenizer.cxx
|
|
func quoteArgs(args []string) string {
|
|
quoted := make([]string, len(args))
|
|
for index, arg := range args {
|
|
quoted[index] = quote(arg)
|
|
}
|
|
return strings.Join(quoted, " ")
|
|
}
|
|
|
|
// Client represents a client connection to a MPD server.
|
|
type Client struct {
|
|
text *textproto.Conn
|
|
version string
|
|
}
|
|
|
|
// Error represents an error returned by the MPD server.
|
|
// It contains the error number, the index of the causing command in the command list,
|
|
// the name of the command in the command list and the error message.
|
|
type Error struct {
|
|
Code ErrorCode
|
|
CommandListIndex int
|
|
CommandName string
|
|
Message string
|
|
}
|
|
|
|
// ErrorCode is the error code of a Error.
|
|
type ErrorCode int
|
|
|
|
// ErrorCodes as defined in MPD source (https://www.musicpd.org/doc/api/html/Ack_8hxx_source.html)
|
|
// version 0.21.
|
|
const (
|
|
ErrorNotList ErrorCode = 1
|
|
ErrorArg ErrorCode = 2
|
|
ErrorPassword ErrorCode = 3
|
|
ErrorPermission ErrorCode = 4
|
|
ErrorUnknown ErrorCode = 5
|
|
ErrorNoExist ErrorCode = 50
|
|
ErrorPlaylistMax ErrorCode = 51
|
|
ErrorSystem ErrorCode = 52
|
|
ErrorPlaylistLoad ErrorCode = 53
|
|
ErrorUpdateAlready ErrorCode = 54
|
|
ErrorPlayerSync ErrorCode = 55
|
|
ErrorExist ErrorCode = 56
|
|
)
|
|
|
|
func (e Error) Error() string {
|
|
if e.CommandName != "" {
|
|
return fmt.Sprintf("command '%s' failed: %s", e.CommandName, e.Message)
|
|
}
|
|
return e.Message
|
|
}
|
|
|
|
// Attrs is a set of attributes returned by MPD.
|
|
type Attrs map[string]string
|
|
|
|
// Dial connects to MPD listening on address addr (e.g. "127.0.0.1:6600")
|
|
// on network network (e.g. "tcp").
|
|
func Dial(network, addr string) (c *Client, err error) {
|
|
text, err := textproto.Dial(network, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
line, err := text.ReadLine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if line[0:6] != "OK MPD" {
|
|
return nil, textproto.ProtocolError("no greeting")
|
|
}
|
|
return &Client{text: text, version: line[7:]}, nil
|
|
}
|
|
|
|
// DialAuthenticated connects to MPD listening on address addr (e.g. "127.0.0.1:6600")
|
|
// on network network (e.g. "tcp"). It then authenticates with MPD
|
|
// using the plaintext password password if it's not empty.
|
|
func DialAuthenticated(network, addr, password string) (c *Client, err error) {
|
|
c, err = Dial(network, addr)
|
|
if err == nil && len(password) > 0 {
|
|
err = c.Command("password %s", password).OK()
|
|
}
|
|
return c, err
|
|
}
|
|
|
|
// Version returns the protocol version used as provided during the handshake.
|
|
func (c *Client) Version() string {
|
|
return c.version
|
|
}
|
|
|
|
// We are reimplemeting Cmd() and PrintfLine() from textproto here, because
|
|
// the original functions append CR-LF to the end of commands. This behavior
|
|
// violates the MPD protocol: Commands must be terminated by '\n'.
|
|
func (c *Client) cmd(format string, args ...interface{}) (uint, error) {
|
|
id := c.text.Next()
|
|
c.text.StartRequest(id)
|
|
defer c.text.EndRequest(id)
|
|
if err := c.printfLine(format, args...); err != nil {
|
|
return 0, err
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
func (c *Client) printfLine(format string, args ...interface{}) error {
|
|
fmt.Fprintf(c.text.W, format, args...)
|
|
c.text.W.WriteByte('\n')
|
|
return c.text.W.Flush()
|
|
}
|
|
|
|
// Close terminates the connection with MPD.
|
|
func (c *Client) Close() (err error) {
|
|
if c.text != nil {
|
|
c.printfLine("close")
|
|
err = c.text.Close()
|
|
c.text = nil
|
|
}
|
|
return
|
|
}
|
|
|
|
// Ping sends a no-op message to MPD. It's useful for keeping the connection alive.
|
|
func (c *Client) Ping() error {
|
|
return c.Command("ping").OK()
|
|
}
|
|
|
|
func (c *Client) readList(key string) (list []string, err error) {
|
|
list = []string{}
|
|
key += ": "
|
|
for {
|
|
line, err := c.readLine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if line == "OK" {
|
|
break
|
|
}
|
|
if !strings.HasPrefix(line, key) {
|
|
return nil, textproto.ProtocolError("unexpected: " + line)
|
|
}
|
|
list = append(list, line[len(key):])
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *Client) readLine() (string, error) {
|
|
line, err := c.text.ReadLine()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if strings.HasPrefix(line, "ACK ") {
|
|
cur := line[4:]
|
|
var code, idx int
|
|
if strings.HasPrefix(cur, "[") {
|
|
sep := strings.Index(cur, "@")
|
|
end := strings.Index(cur, "] ")
|
|
if sep > 0 && end > 0 {
|
|
code, err = strconv.Atoi(cur[1:sep])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
idx, err = strconv.Atoi(cur[sep+1 : end])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cur = cur[end+2:]
|
|
}
|
|
}
|
|
var cmd string
|
|
if strings.HasPrefix(cur, "{") {
|
|
if end := strings.Index(cur, "} "); end > 0 {
|
|
cmd = cur[1:end]
|
|
cur = cur[end+2:]
|
|
}
|
|
}
|
|
msg := strings.TrimSpace(cur)
|
|
return "", Error{
|
|
Code: ErrorCode(code),
|
|
CommandListIndex: idx,
|
|
CommandName: cmd,
|
|
Message: msg,
|
|
}
|
|
}
|
|
return line, nil
|
|
}
|
|
|
|
func (c *Client) readBytes(length int) ([]byte, error) {
|
|
// Read the entire chunk of data. ReadFull() makes sure the data length matches the expectation
|
|
data := make([]byte, length)
|
|
if _, err := io.ReadFull(c.text.R, data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify there's a linebreak afterwards and skip it
|
|
termByte, err := c.text.R.ReadByte()
|
|
if err != nil {
|
|
return nil, textproto.ProtocolError("failed to read binary data terminator: " + err.Error())
|
|
}
|
|
if termByte != '\n' {
|
|
return nil, textproto.ProtocolError(fmt.Sprintf("wrong binary data terminator: want 0x0a, got %x", termByte))
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func (c *Client) readAttrsList(startKey string) (attrs []Attrs, err error) {
|
|
attrs = []Attrs{}
|
|
startKey += ": "
|
|
for {
|
|
line, err := c.readLine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if line == "OK" {
|
|
break
|
|
}
|
|
if strings.HasPrefix(line, startKey) { // new entry begins
|
|
attrs = append(attrs, Attrs{})
|
|
}
|
|
if len(attrs) == 0 {
|
|
return nil, textproto.ProtocolError("unexpected: " + line)
|
|
}
|
|
i := strings.Index(line, ": ")
|
|
if i < 0 {
|
|
return nil, textproto.ProtocolError("can't parse line: " + line)
|
|
}
|
|
attrs[len(attrs)-1][line[0:i]] = line[i+2:]
|
|
}
|
|
return attrs, nil
|
|
}
|
|
|
|
func (c *Client) readAttrs(terminator string) (attrs Attrs, err error) {
|
|
attrs = make(Attrs)
|
|
for {
|
|
line, err := c.readLine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if line == terminator {
|
|
break
|
|
}
|
|
z := strings.Index(line, ": ")
|
|
if z < 0 {
|
|
return nil, textproto.ProtocolError("can't parse line: " + line)
|
|
}
|
|
key := line[0:z]
|
|
attrs[key] = line[z+2:]
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *Client) readBinary() ([]byte, int, error) {
|
|
size := -1
|
|
for {
|
|
line, err := c.readLine()
|
|
switch {
|
|
case err != nil:
|
|
return nil, 0, err
|
|
|
|
// Check for the size key
|
|
case strings.HasPrefix(line, "size: "):
|
|
if size, err = strconv.Atoi(line[6:]); err != nil {
|
|
return nil, 0, textproto.ProtocolError("failed to parse size: " + err.Error())
|
|
}
|
|
|
|
// Check for the binary key
|
|
case strings.HasPrefix(line, "binary: "):
|
|
length := -1
|
|
if length, err = strconv.Atoi(line[8:]); err != nil {
|
|
return nil, 0, textproto.ProtocolError("failed to parse binary: " + err.Error())
|
|
}
|
|
|
|
// If no size is given, assume it's equal to the provided data's length
|
|
if size < 0 {
|
|
size = length
|
|
}
|
|
|
|
// The binary data must follow the 'binary:' key
|
|
data, err := c.readBytes(length)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// The binary data must be followed by the "OK" line
|
|
if s, err := c.readLine(); err != nil {
|
|
return nil, 0, err
|
|
} else if s != "OK" {
|
|
return nil, 0, textproto.ProtocolError("expected 'OK', got " + s)
|
|
}
|
|
return data, size, nil
|
|
|
|
// No more data. Obviously, no binary data encountered
|
|
case line == "", line == "OK":
|
|
return nil, 0, textproto.ProtocolError("no binary data found in response")
|
|
}
|
|
}
|
|
}
|
|
|
|
// CurrentSong returns information about the current song in the playlist.
|
|
func (c *Client) CurrentSong() (Attrs, error) {
|
|
return c.Command("currentsong").Attrs()
|
|
}
|
|
|
|
// Status returns information about the current status of MPD.
|
|
func (c *Client) Status() (Attrs, error) {
|
|
return c.Command("status").Attrs()
|
|
}
|
|
|
|
// Stats displays statistics (number of artists, songs, playtime, etc)
|
|
func (c *Client) Stats() (Attrs, error) {
|
|
return c.Command("stats").Attrs()
|
|
}
|
|
|
|
func (c *Client) readOKLine(terminator string) (err error) {
|
|
line, err := c.readLine()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if line == terminator {
|
|
return nil
|
|
}
|
|
return textproto.ProtocolError("unexpected response: " + line)
|
|
}
|
|
|
|
func (c *Client) idle(subsystems ...string) ([]string, error) {
|
|
return c.Command("idle %s", Quoted(strings.Join(subsystems, " "))).Strings("changed")
|
|
}
|
|
|
|
func (c *Client) noIdle() (err error) {
|
|
id, err := c.cmd("noidle")
|
|
if err == nil {
|
|
c.text.StartResponse(id)
|
|
c.text.EndResponse(id)
|
|
}
|
|
return
|
|
}
|
|
|
|
//
|
|
// Playback control
|
|
//
|
|
|
|
// Next plays next song in the playlist.
|
|
func (c *Client) Next() error {
|
|
return c.Command("next").OK()
|
|
}
|
|
|
|
// Pause pauses playback if pause is true; resumes playback otherwise.
|
|
func (c *Client) Pause(pause bool) error {
|
|
if pause {
|
|
return c.Command("pause 1").OK()
|
|
}
|
|
return c.Command("pause 0").OK()
|
|
}
|
|
|
|
// Play starts playing the song at playlist position pos. If pos is negative,
|
|
// start playing at the current position in the playlist.
|
|
func (c *Client) Play(pos int) error {
|
|
if pos < 0 {
|
|
return c.Command("play").OK()
|
|
}
|
|
return c.Command("play %d", pos).OK()
|
|
}
|
|
|
|
// PlayID plays the song identified by id. If id is negative, start playing
|
|
// at the current position in playlist.
|
|
func (c *Client) PlayID(id int) error {
|
|
if id < 0 {
|
|
return c.Command("playid").OK()
|
|
}
|
|
return c.Command("playid %d", id).OK()
|
|
}
|
|
|
|
// Previous plays previous song in the playlist.
|
|
func (c *Client) Previous() error {
|
|
return c.Command("previous").OK()
|
|
}
|
|
|
|
// Seek seeks to the position time (in seconds) of the song at playlist position pos.
|
|
// Deprecated: Use SeekPos instead.
|
|
func (c *Client) Seek(pos, time int) error {
|
|
return c.Command("seek %d %d", pos, time).OK()
|
|
}
|
|
|
|
// SeekID is identical to Seek except the song is identified by it's id
|
|
// (not position in playlist).
|
|
// Deprecated: Use SeekSongID instead.
|
|
func (c *Client) SeekID(id, time int) error {
|
|
return c.Command("seekid %d %d", id, time).OK()
|
|
}
|
|
|
|
// SeekPos seeks to the position d of the song at playlist position pos.
|
|
func (c *Client) SeekPos(pos int, d time.Duration) error {
|
|
return c.Command("seek %d %f", pos, d.Seconds()).OK()
|
|
}
|
|
|
|
// SeekSongID seeks to the position d of the song identified by id.
|
|
func (c *Client) SeekSongID(id int, d time.Duration) error {
|
|
return c.Command("seekid %d %f", id, d.Seconds()).OK()
|
|
}
|
|
|
|
// SeekCur seeks to the position d within the current song.
|
|
// If relative is true, then the time is relative to the current playing position.
|
|
func (c *Client) SeekCur(d time.Duration, relative bool) error {
|
|
if relative {
|
|
return c.Command("seekcur %+f", d.Seconds()).OK()
|
|
}
|
|
return c.Command("seekcur %f", d.Seconds()).OK()
|
|
}
|
|
|
|
// Stop stops playback.
|
|
func (c *Client) Stop() error {
|
|
return c.Command("stop").OK()
|
|
}
|
|
|
|
// SetVolume sets the volume to volume. The range of volume is 0-100.
|
|
func (c *Client) SetVolume(volume int) error {
|
|
return c.Command("setvol %d", volume).OK()
|
|
}
|
|
|
|
// Random enables random playback, if random is true, disables it otherwise.
|
|
func (c *Client) Random(random bool) error {
|
|
if random {
|
|
return c.Command("random 1").OK()
|
|
}
|
|
return c.Command("random 0").OK()
|
|
}
|
|
|
|
// Repeat enables repeat mode, if repeat is true, disables it otherwise.
|
|
func (c *Client) Repeat(repeat bool) error {
|
|
if repeat {
|
|
return c.Command("repeat 1").OK()
|
|
}
|
|
return c.Command("repeat 0").OK()
|
|
}
|
|
|
|
// Single enables single song mode, if single is true, disables it otherwise.
|
|
func (c *Client) Single(single bool) error {
|
|
if single {
|
|
return c.Command("single 1").OK()
|
|
}
|
|
return c.Command("single 0").OK()
|
|
}
|
|
|
|
// Consume enables consume mode, if consume is true, disables it otherwise.
|
|
func (c *Client) Consume(consume bool) error {
|
|
if consume {
|
|
return c.Command("consume 1").OK()
|
|
}
|
|
return c.Command("consume 0").OK()
|
|
}
|
|
|
|
//
|
|
// Playlist related functions
|
|
//
|
|
|
|
// PlaylistInfo returns attributes for songs in the current playlist. If
|
|
// both start and end are negative, it does this for all songs in
|
|
// playlist. If end is negative but start is positive, it does it for the
|
|
// song at position start. If both start and end are positive, it does it
|
|
// for positions in range [start, end).
|
|
func (c *Client) PlaylistInfo(start, end int) ([]Attrs, error) {
|
|
var cmd *Command
|
|
switch {
|
|
case start < 0 && end < 0:
|
|
// Request all playlist items.
|
|
cmd = c.Command("playlistinfo")
|
|
case start >= 0 && end >= 0:
|
|
// Request this range of playlist items.
|
|
cmd = c.Command("playlistinfo %d:%d", start, end)
|
|
case start >= 0 && end < 0:
|
|
// Request the single playlist item at this position.
|
|
cmd = c.Command("playlistinfo %d", start)
|
|
case start < 0 && end >= 0:
|
|
return nil, errors.New("negative start index")
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
return cmd.AttrsList("file")
|
|
}
|
|
|
|
// SetPriority set the priority of the specified songs. If end is negative but
|
|
// start is non-negative, it does it for the song at position start. If both
|
|
// start and end are non-negative, it does it for positions in range
|
|
// [start, end).
|
|
func (c *Client) SetPriority(priority, start, end int) error {
|
|
switch {
|
|
case start < 0 && end < 0:
|
|
return errors.New("negative start and end index")
|
|
case start >= 0 && end >= 0:
|
|
// Update the prio for this range of playlist items.
|
|
return c.Command("prio %d %d:%d", priority, start, end).OK()
|
|
case start >= 0 && end < 0:
|
|
// Update the prio for a single playlist item at this position.
|
|
return c.Command("prio %d %d", priority, start).OK()
|
|
case start < 0 && end >= 0:
|
|
return errors.New("negative start index")
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
}
|
|
|
|
// SetPriorityID sets the prio of the song with the given id.
|
|
func (c *Client) SetPriorityID(priority, id int) error {
|
|
return c.Command("prioid %d %d", priority, id).OK()
|
|
}
|
|
|
|
// Delete deletes songs from playlist. If both start and end are positive,
|
|
// it deletes those at positions in range [start, end). If end is negative,
|
|
// it deletes the song at position start.
|
|
func (c *Client) Delete(start, end int) error {
|
|
if start < 0 {
|
|
return errors.New("negative start index")
|
|
}
|
|
if end < 0 {
|
|
return c.Command("delete %d", start).OK()
|
|
}
|
|
return c.Command("delete %d:%d", start, end).OK()
|
|
}
|
|
|
|
// DeleteID deletes the song identified by id.
|
|
func (c *Client) DeleteID(id int) error {
|
|
return c.Command("deleteid %d", id).OK()
|
|
}
|
|
|
|
// Move moves the songs between the positions start and end to the new position
|
|
// position. If end is negative, only the song at position start is moved.
|
|
func (c *Client) Move(start, end, position int) error {
|
|
if start < 0 {
|
|
return errors.New("negative start index")
|
|
}
|
|
if end < 0 {
|
|
return c.Command("move %d %d", start, position).OK()
|
|
}
|
|
return c.Command("move %d:%d %d", start, end, position).OK()
|
|
}
|
|
|
|
// MoveID moves songid to position on the plyalist.
|
|
func (c *Client) MoveID(songid, position int) error {
|
|
return c.Command("moveid %d %d", songid, position).OK()
|
|
}
|
|
|
|
// Add adds the file/directory uri to playlist. Directories add recursively.
|
|
func (c *Client) Add(uri string) error {
|
|
return c.Command("add %s", uri).OK()
|
|
}
|
|
|
|
// AddID adds the file/directory uri to playlist and returns the identity
|
|
// id of the song added. If pos is positive, the song is added to position
|
|
// pos.
|
|
func (c *Client) AddID(uri string, pos int) (int, error) {
|
|
var cmd *Command
|
|
if pos >= 0 {
|
|
cmd = c.Command("addid %s %d", uri, pos)
|
|
} else {
|
|
cmd = c.Command("addid %s", uri)
|
|
}
|
|
attrs, err := cmd.Attrs()
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
tok, ok := attrs["Id"]
|
|
if !ok {
|
|
return -1, textproto.ProtocolError("addid did not return Id")
|
|
}
|
|
return strconv.Atoi(tok)
|
|
}
|
|
|
|
// Clear clears the current playlist.
|
|
func (c *Client) Clear() error {
|
|
return c.Command("clear").OK()
|
|
}
|
|
|
|
// Shuffle shuffles the tracks from position start to position end in the
|
|
// current playlist. If start or end is negative, the whole playlist is
|
|
// shuffled.
|
|
func (c *Client) Shuffle(start, end int) error {
|
|
if start < 0 || end < 0 {
|
|
return c.Command("shuffle").OK()
|
|
}
|
|
return c.Command("shuffle %d:%d", start, end).OK()
|
|
}
|
|
|
|
// Database related commands
|
|
|
|
// GetFiles returns the entire list of files in MPD database.
|
|
func (c *Client) GetFiles() ([]string, error) {
|
|
return c.Command("list file").Strings("file")
|
|
}
|
|
|
|
// Update updates MPD's database: find new files, remove deleted files, update
|
|
// modified files. uri is a particular directory or file to update. If it is an
|
|
// empty string, everything is updated.
|
|
//
|
|
// The returned jobID identifies the update job, enqueued by MPD.
|
|
func (c *Client) Update(uri string) (jobID int, err error) {
|
|
id, err := c.cmd("update %s", quote(uri))
|
|
if err != nil {
|
|
return
|
|
}
|
|
c.text.StartResponse(id)
|
|
defer c.text.EndResponse(id)
|
|
|
|
line, err := c.readLine()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if !strings.HasPrefix(line, "updating_db: ") {
|
|
return 0, textproto.ProtocolError("unexpected response: " + line)
|
|
}
|
|
jobID, err = strconv.Atoi(line[13:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
return jobID, c.readOKLine("OK")
|
|
}
|
|
|
|
// Rescan updates MPD's database like Update, but it also rescans unmodified
|
|
// files. uri is a particular directory or file to update. If it is an empty
|
|
// string, everything is updated.
|
|
//
|
|
// The returned jobID identifies the update job, enqueued by MPD.
|
|
func (c *Client) Rescan(uri string) (jobID int, err error) {
|
|
id, err := c.cmd("rescan %s", quote(uri))
|
|
if err != nil {
|
|
return
|
|
}
|
|
c.text.StartResponse(id)
|
|
defer c.text.EndResponse(id)
|
|
|
|
line, err := c.readLine()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if !strings.HasPrefix(line, "updating_db: ") {
|
|
return 0, textproto.ProtocolError("unexpected response: " + line)
|
|
}
|
|
jobID, err = strconv.Atoi(line[13:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
return jobID, c.readOKLine("OK")
|
|
}
|
|
|
|
// ListAllInfo returns attributes for songs in the library. Information about
|
|
// any song that is either inside or matches the passed in uri is returned.
|
|
// To get information about every song in the library, pass in "/".
|
|
func (c *Client) ListAllInfo(uri string) ([]Attrs, error) {
|
|
id, err := c.cmd("listallinfo %s ", quote(uri))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.text.StartResponse(id)
|
|
defer c.text.EndResponse(id)
|
|
|
|
attrs := []Attrs{}
|
|
inEntry := false
|
|
for {
|
|
line, err := c.readLine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if line == "OK" {
|
|
break
|
|
} else if strings.HasPrefix(line, "file: ") { // new entry begins
|
|
attrs = append(attrs, Attrs{})
|
|
inEntry = true
|
|
} else if strings.HasPrefix(line, "directory: ") {
|
|
inEntry = false
|
|
}
|
|
|
|
if inEntry {
|
|
i := strings.Index(line, ": ")
|
|
if i < 0 {
|
|
return nil, textproto.ProtocolError("can't parse line: " + line)
|
|
}
|
|
attrs[len(attrs)-1][line[0:i]] = line[i+2:]
|
|
}
|
|
}
|
|
return attrs, nil
|
|
}
|
|
|
|
// ListInfo lists the contents of the directory URI using MPD's lsinfo command.
|
|
func (c *Client) ListInfo(uri string) ([]Attrs, error) {
|
|
id, err := c.cmd("lsinfo %s", quote(uri))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.text.StartResponse(id)
|
|
defer c.text.EndResponse(id)
|
|
attrs := []Attrs{}
|
|
for {
|
|
line, err := c.readLine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if line == "OK" {
|
|
break
|
|
}
|
|
if strings.HasPrefix(line, "file: ") ||
|
|
strings.HasPrefix(line, "directory: ") ||
|
|
strings.HasPrefix(line, "playlist: ") {
|
|
attrs = append(attrs, Attrs{})
|
|
}
|
|
i := strings.Index(line, ": ")
|
|
if i < 0 {
|
|
return nil, textproto.ProtocolError("can't parse line: " + line)
|
|
}
|
|
attrs[len(attrs)-1][strings.ToLower(line[0:i])] = line[i+2:]
|
|
}
|
|
return attrs, nil
|
|
}
|
|
|
|
// ReadComments reads "comments" (audio metadata) from the song URI using
|
|
// MPD's readcomments command.
|
|
func (c *Client) ReadComments(uri string) (Attrs, error) {
|
|
return c.Command("readcomments %s", uri).Attrs()
|
|
}
|
|
|
|
// Find searches the library for songs and returns attributes for each matching song.
|
|
// The args are the raw arguments passed to MPD. For example, to search for
|
|
// songs that belong to a specific artist and album:
|
|
//
|
|
// Find("artist", "Artist Name", "album", "Album Name")
|
|
//
|
|
// Searches are case sensitive. Use Search for case insensitive search.
|
|
func (c *Client) Find(args ...string) ([]Attrs, error) {
|
|
return c.Command("find " + quoteArgs(args)).AttrsList("file")
|
|
}
|
|
|
|
// Search behaves exactly the same as Find, but the searches are not case sensitive.
|
|
func (c *Client) Search(args ...string) ([]Attrs, error) {
|
|
return c.Command("search " + quoteArgs(args)).AttrsList("file")
|
|
}
|
|
|
|
// List searches the database for your query. You can use something simple like
|
|
// `artist` for your search, or something like `artist album <Album Name>` if
|
|
// you want the artist that has an album with a specified album name.
|
|
func (c *Client) List(args ...string) ([]string, error) {
|
|
id, err := c.cmd("list " + quoteArgs(args))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.text.StartResponse(id)
|
|
defer c.text.EndResponse(id)
|
|
|
|
var ret []string
|
|
for {
|
|
line, err := c.readLine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
i := strings.Index(line, ": ")
|
|
if i > 0 {
|
|
ret = append(ret, line[i+2:])
|
|
} else if line == "OK" {
|
|
break
|
|
} else {
|
|
return nil, textproto.ProtocolError("can't parse line: " + line)
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// Output related commands.
|
|
|
|
// ListOutputs lists all configured outputs with their name, id & enabled state.
|
|
func (c *Client) ListOutputs() ([]Attrs, error) {
|
|
return c.Command("outputs").AttrsList("outputid")
|
|
}
|
|
|
|
// EnableOutput enables the audio output with the given id.
|
|
func (c *Client) EnableOutput(id int) error {
|
|
return c.Command("enableoutput %d", id).OK()
|
|
}
|
|
|
|
// DisableOutput disables the audio output with the given id.
|
|
func (c *Client) DisableOutput(id int) error {
|
|
return c.Command("disableoutput %d", id).OK()
|
|
}
|
|
|
|
// Stored playlists related commands
|
|
|
|
// ListPlaylists lists all stored playlists.
|
|
func (c *Client) ListPlaylists() ([]Attrs, error) {
|
|
return c.Command("listplaylists").AttrsList("playlist")
|
|
}
|
|
|
|
// PlaylistContents returns a list of attributes for songs in the specified
|
|
// stored playlist.
|
|
func (c *Client) PlaylistContents(name string) ([]Attrs, error) {
|
|
return c.Command("listplaylistinfo %s", name).AttrsList("file")
|
|
}
|
|
|
|
// PlaylistLoad loads the specfied playlist into the current queue.
|
|
// If start and end are non-negative, only songs in this range are loaded.
|
|
func (c *Client) PlaylistLoad(name string, start, end int) error {
|
|
if start < 0 || end < 0 {
|
|
return c.Command("load %s", name).OK()
|
|
}
|
|
return c.Command("load %s %d:%d", name, start, end).OK()
|
|
}
|
|
|
|
// PlaylistAdd adds a song identified by uri to a stored playlist identified
|
|
// by name.
|
|
func (c *Client) PlaylistAdd(name string, uri string) error {
|
|
return c.Command("playlistadd %s %s", name, uri).OK()
|
|
}
|
|
|
|
// PlaylistClear clears the specified playlist.
|
|
func (c *Client) PlaylistClear(name string) error {
|
|
return c.Command("playlistclear %s", name).OK()
|
|
}
|
|
|
|
// PlaylistDelete deletes the song at position pos from the specified playlist.
|
|
func (c *Client) PlaylistDelete(name string, pos int) error {
|
|
return c.Command("playlistdelete %s %d", name, pos).OK()
|
|
}
|
|
|
|
// PlaylistMove moves a song identified by id in a playlist identified by name
|
|
// to the position pos.
|
|
func (c *Client) PlaylistMove(name string, id, pos int) error {
|
|
return c.Command("playlistmove %s %d %d", name, id, pos).OK()
|
|
}
|
|
|
|
// PlaylistRename renames the playlist identified by name to newName.
|
|
func (c *Client) PlaylistRename(name, newName string) error {
|
|
return c.Command("rename %s %s", name, newName).OK()
|
|
}
|
|
|
|
// PlaylistRemove removes the playlist identified by name from the playlist
|
|
// directory.
|
|
func (c *Client) PlaylistRemove(name string) error {
|
|
return c.Command("rm %s", name).OK()
|
|
}
|
|
|
|
// PlaylistSave saves the current playlist as name in the playlist directory.
|
|
func (c *Client) PlaylistSave(name string) error {
|
|
return c.Command("save %s", name).OK()
|
|
}
|
|
|
|
// A Sticker represents a name/value pair associated to a song. Stickers
|
|
// are managed and shared by MPD clients, and MPD server does not assume
|
|
// any special meaning in them.
|
|
type Sticker struct {
|
|
Name, Value string
|
|
}
|
|
|
|
func newSticker(name, value string) *Sticker {
|
|
return &Sticker{
|
|
Name: name,
|
|
Value: value,
|
|
}
|
|
}
|
|
|
|
func parseSticker(s string) (*Sticker, error) {
|
|
// Since '=' can appear in the sticker name and in the sticker value,
|
|
// it's impossible to determine where the name ends and value starts.
|
|
// Assume that '=' is more likely to occur in the value
|
|
// (e.g. base64 encoded data -- see #39).
|
|
i := strings.Index(s, "=")
|
|
if i < 0 {
|
|
return nil, textproto.ProtocolError("parsing sticker failed")
|
|
}
|
|
return newSticker(s[:i], s[i+1:]), nil
|
|
}
|
|
|
|
// StickerDelete deletes sticker for the song with given URI.
|
|
func (c *Client) StickerDelete(uri string, name string) error {
|
|
return c.Command("sticker delete song %s %s", uri, name).OK()
|
|
}
|
|
|
|
// StickerFind finds songs inside directory with URI which have a sticker with given name.
|
|
// It returns a slice of URIs of matching songs and a slice of corresponding stickers.
|
|
func (c *Client) StickerFind(uri string, name string) ([]string, []Sticker, error) {
|
|
attrs, err := c.Command("sticker find song %s %s", uri, name).AttrsList("file")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
files := make([]string, len(attrs))
|
|
stks := make([]Sticker, len(attrs))
|
|
for i, attr := range attrs {
|
|
if _, ok := attr["file"]; !ok {
|
|
return nil, nil, textproto.ProtocolError("file attribute not found")
|
|
}
|
|
if _, ok := attr["sticker"]; !ok {
|
|
return nil, nil, textproto.ProtocolError("sticker attribute not found")
|
|
}
|
|
files[i] = attr["file"]
|
|
stk, err := parseSticker(attr["sticker"])
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
stks[i] = *stk
|
|
}
|
|
return files, stks, nil
|
|
}
|
|
|
|
// StickerGet gets sticker value for the song with given URI.
|
|
func (c *Client) StickerGet(uri string, name string) (*Sticker, error) {
|
|
attrs, err := c.Command("sticker get song %s %s", uri, name).Attrs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
attr, ok := attrs["sticker"]
|
|
if !ok {
|
|
return nil, textproto.ProtocolError("sticker not found")
|
|
}
|
|
stk, err := parseSticker(attr)
|
|
if stk == nil {
|
|
return nil, err
|
|
}
|
|
return stk, nil
|
|
}
|
|
|
|
// StickerList returns a slice of stickers for the song with given URI.
|
|
func (c *Client) StickerList(uri string) ([]Sticker, error) {
|
|
attrs, err := c.Command("sticker list song %s", uri).AttrsList("sticker")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stks := make([]Sticker, len(attrs))
|
|
for i, attr := range attrs {
|
|
s, ok := attr["sticker"]
|
|
if !ok {
|
|
return nil, textproto.ProtocolError("sticker attribute not found")
|
|
}
|
|
stk, err := parseSticker(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stks[i] = *stk
|
|
}
|
|
return stks, nil
|
|
}
|
|
|
|
// StickerSet sets sticker value for the song with given URI.
|
|
func (c *Client) StickerSet(uri string, name string, value string) error {
|
|
return c.Command("sticker set song %s %s %s", uri, name, value).OK()
|
|
}
|
|
|
|
// AlbumArt retrieves an album artwork image for a song with the given URI using MPD's albumart command.
|
|
func (c *Client) AlbumArt(uri string) ([]byte, error) {
|
|
offset := 0
|
|
var data []byte
|
|
for {
|
|
// Read the data in chunks
|
|
chunk, size, err := c.Command("albumart %s %d", uri, offset).Binary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Accumulate the data
|
|
data = append(data, chunk...)
|
|
offset = len(data)
|
|
if offset >= size {
|
|
break
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|