schnutibox/vendor/github.com/fhs/gompd/v2/mpd/client.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
}