pax_global_header00006660000000000000000000000064143253262350014517gustar00rootroot0000000000000052 comment=7b6a87a188075b95841b0a73ce7735a3d4fae754 gompd-2.3.0/000077500000000000000000000000001432532623500126275ustar00rootroot00000000000000gompd-2.3.0/.gitattributes000066400000000000000000000000311432532623500155140ustar00rootroot00000000000000* text eol=lf gompd-2.3.0/.github/000077500000000000000000000000001432532623500141675ustar00rootroot00000000000000gompd-2.3.0/.github/workflows/000077500000000000000000000000001432532623500162245ustar00rootroot00000000000000gompd-2.3.0/.github/workflows/test.yml000066400000000000000000000013501432532623500177250ustar00rootroot00000000000000on: [push, pull_request] name: Test jobs: test: strategy: matrix: go-version: [1.14.x, 1.15.x] platform: [ubuntu-latest, macos-latest, windows-latest] fail-fast: false runs-on: ${{ matrix.platform }} env: GO111MODULE: on steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Go environment run: | go version go env echo PATH is $PATH shell: bash - name: Checkout code uses: actions/checkout@v2 - name: Run tests run: go test -race -v ./... - name: Check gofmt run: | gofmt -l . test `gofmt -l . | wc -l` = 0 shell: bash gompd-2.3.0/.gitignore000066400000000000000000000000501432532623500146120ustar00rootroot00000000000000# VIM *.swp *.swo # Idea /.idea/ *.iml gompd-2.3.0/AUTHORS000066400000000000000000000011531432532623500136770ustar00rootroot00000000000000# This is the official list of GoMPD authors for copyright purposes. # This file is distinct from the CONTRIBUTORS files. # See the latter for an explanation. # Names should be added to this file as # Name or Organization # The email address is not required for organizations. # Please keep the list sorted. Andy O'Neill Don Kuntz Eric Butler Fazlul Shahriar Joe Roberts Josh Butts Michael Peterson ushi gompd-2.3.0/CONTRIBUTORS000066400000000000000000000016551432532623500145160ustar00rootroot00000000000000# This is the official list of people who can contribute # (and typically have contributed) code to the GoMPD repository. # The AUTHORS file lists the copyright holders; this file # lists people. # # The submission process automatically checks to make sure # that people submitting code are listed in this file (by email address). # Names should be added to this file like so: # Name # # An entry with two email addresses specifies that the # first address should be used in the submit logs and # that the second address should be recognized as the # same person when interacting with Rietveld. # Please keep the list sorted. Andy O'Neill Don Kuntz Eric Butler Fazlul Shahriar Joe Roberts Josh Butts Michael Peterson ushi gompd-2.3.0/LICENSE000066400000000000000000000020721432532623500136350ustar00rootroot00000000000000Copyright © 2013 The GoMPD Authors. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gompd-2.3.0/README.md000066400000000000000000000012271432532623500141100ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/fhs/gompd.png)](https://travis-ci.org/fhs/gompd) [![GoDoc](https://godoc.org/github.com/fhs/gompd/v2/mpd/?status.svg)](https://godoc.org/github.com/fhs/gompd/v2/mpd/) [![Go Report Card](https://goreportcard.com/badge/github.com/fhs/gompd/v2)](https://goreportcard.com/report/github.com/fhs/gompd/v2) ## Overview This is a Go package for accessing [Music Player Daemon (MPD)](https://www.musicpd.org/). Old repository: https://code.google.com/p/gompd/ ## Installation How to install: $ go get github.com/fhs/gompd/v2/mpd ## Documentation Documentation can be found here: http://godoc.org/github.com/fhs/gompd/v2/mpd gompd-2.3.0/go.mod000066400000000000000000000000501432532623500137300ustar00rootroot00000000000000module github.com/fhs/gompd/v2 go 1.11 gompd-2.3.0/go.sum000066400000000000000000000000001432532623500137500ustar00rootroot00000000000000gompd-2.3.0/lib/000077500000000000000000000000001432532623500133755ustar00rootroot00000000000000gompd-2.3.0/lib/mpdconf000066400000000000000000000002761432532623500147530ustar00rootroot00000000000000# This MPD config file exists only for testing purposes. # It's used by ../.travis.yml db_file "~/mpd.db" log_file "~/mpd.log" music_directory "/dev/null" bind_to_address "/tmp/mpd.socket" gompd-2.3.0/mpd/000077500000000000000000000000001432532623500134075ustar00rootroot00000000000000gompd-2.3.0/mpd/client.go000066400000000000000000000725511432532623500152260ustar00rootroot00000000000000// 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 { // TODO: We are using strings.Builder even tough it's not ideal. // When unsafe.{String,Slice}{,Data} is available, we should use buffer+unsafe. // q := make([]byte, 2+2*len(s)) // return unsafe.String(unsafe.SliceData(q), len(q)) // [issue53003]: https://github.com/golang/go/issues/53003 var q strings.Builder q.Grow(2 + 2*len(s)) q.WriteByte('"') for _, c := range []byte(s) { // We need to escape single/double quotes and a backslash by prepending them with a '\' switch c { case '"', '\\', '\'': q.WriteByte('\\') } q.WriteByte(c) } q.WriteByte('"') return q.String() } // 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 ` 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 } // Partition commands // Partition switches the client to a different partition. func (c *Client) Partition(name string) error { return c.Command("partition %s", name).OK() } // ListPartitions returns a list of partitions and their information. func (c *Client) ListPartitions() ([]Attrs, error) { return c.Command("listpartitions").AttrsList("partition") } // NewPartition creates a new partition with the given name. func (c *Client) NewPartition(name string) error { return c.Command("newpartition %s", name).OK() } // DelPartition deletes partition with the given name. func (c *Client) DelPartition(name string) error { return c.Command("delpartition %s", name).OK() } // MoveOutput moves an output with the given name to the current partition. func (c *Client) MoveOutput(name string) error { return c.Command("moveoutput %s", name).OK() } // 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 } // ReadPicture retrieves the embedded album artwork image for a song with the given URI using MPD's readpicture command. func (c *Client) ReadPicture(uri string) ([]byte, error) { offset := 0 var data []byte for { // Read the data in chunks chunk, size, err := c.Command("readpicture %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 } gompd-2.3.0/mpd/client_test.go000066400000000000000000000433231432532623500162600ustar00rootroot00000000000000// 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 import ( "os" "reflect" "testing" "github.com/fhs/gompd/v2/mpd/internal/server" ) // Tests we want to run var quoteTests = &[...]*struct { source, expected string }{ {`test.ogg`, `"test.ogg"`}, {`test "song".ogg`, `"test \"song\".ogg"`}, {`test with 'single' and "double" quotes`, `"test with \'single\' and \"double\" quotes"`}, {`escape \"escaped\"`, `"escape \\\"escaped\\\""`}, {`just a \`, `"just a \\"`}, {`04 - ILL - DECAYED LOVE feat.℃iel.ogg`, `"04 - ILL - DECAYED LOVE feat.℃iel.ogg"`}, // Test case provided at https://www.musicpd.org/doc/html/protocol.html#escaping-string-values. // NB: we don't support quoting in the "protocol level" mode, hence single quotes get the same treatment as // double quotes and there are 3 backslashes before the single quote, too. {`(Artist == "foo\'bar\"")`, `"(Artist == \"foo\\\'bar\\\"\")"`}, } func TestQuote(t *testing.T) { // Run tests for _, test := range quoteTests { if q := quote(test.source); q != test.expected { t.Errorf("quote(%s) returned %s; expected %s", test.source, q, test.expected) } } } var ( benchmarkQuoteInput = quoteTests[len(quoteTests)-1].source benchmarkQuoteOutput = "" ) func BenchmarkQuote(b *testing.B) { for i := 0; i < b.N; i++ { benchmarkQuoteOutput = quote(benchmarkQuoteInput) } } func TestQuoteArgs(t *testing.T) { quoteArgsTest := []string{`Artist`, `Nightingale`, `Title`, `"Don't Go Away"`} expected := `"Artist" "Nightingale" "Title" "\"Don\'t Go Away\""` if got := quoteArgs(quoteArgsTest); got != expected { t.Errorf("quoteArgs(%v) returned %s; expected %s", quoteArgsTest, got, expected) } } var ( serverRunning = false useGoMPDServer = true ) func localAddr() (net, addr string) { if useGoMPDServer { // Don't clash with standard MPD port 6600 return "tcp", "127.0.0.1:6603" } net = "unix" addr = os.Getenv("MPD_HOST") if len(addr) > 0 && addr[0] == '/' { return } net = "tcp" if len(addr) == 0 { addr = "127.0.0.1" } port := os.Getenv("MPD_PORT") if len(port) == 0 { port = "6600" } return net, addr + ":" + port } func localDial(t *testing.T) *Client { net, addr := localAddr() if useGoMPDServer && !serverRunning { running := make(chan bool) go server.Listen(net, addr, running) serverRunning = true <-running } cli, err := Dial(net, addr) if err != nil { t.Fatalf("Dial(%q) = %v, %s want PTR, nil", addr, cli, err) } return cli } func teardown(cli *Client, t *testing.T) { if err := cli.Close(); err != nil { t.Errorf("Client.Close() = %s need nil", err) } } func attrsEqual(left, right Attrs) bool { if len(left) != len(right) { return false } for key, lval := range left { if rval, ok := right[key]; !ok || lval != rval { return false } } return true } func TestPlaylistInfo(t *testing.T) { cli := localDial(t) defer teardown(cli, t) // Add songs to the current playlist. all := 4 files, err := cli.GetFiles() if err != nil { t.Fatalf("Client.GetFiles failed: %s\n", err) } if len(files) < all { t.Fatalf("Add more then %d audio file to your MPD to run this test.", all) } for i := 0; i < all; i++ { if err = cli.Add(files[i]); err != nil { t.Fatalf("Client.Add failed: %s\n", err) } } pls, err := cli.PlaylistInfo(-1, -1) if err != nil { t.Fatalf("Client.PlaylistInfo(-1, -1) = %v, %s need _, nil", pls, err) } if len(pls) != all { t.Fatalf("Client.PlaylistInfo(-1, -1) len = %d need %d", len(pls), all) } for i, song := range pls { if _, ok := song["file"]; !ok { t.Errorf(`PlaylistInfo: song %d has no "file" attribute`, i) } pls1, err := cli.PlaylistInfo(i, -1) if err != nil { t.Errorf("Client.PlaylistInfo(%d, -1) = %v, %s need _, nil", i, pls1, err) } if !attrsEqual(pls[i], pls1[0]) { t.Errorf("song at position %d is %v; want %v", i, pls[i], pls1[0]) } } pls, err = cli.PlaylistInfo(2, 4) if err != nil { t.Fatalf("Client.PlaylistInfo(2, 4) = %v, %s need _, nil", pls, err) } if len(pls) != 2 { t.Fatalf("Client.PlaylistInfo(2, 4) len = %d need 2", len(pls)) } } func TestListInfo(t *testing.T) { cli := localDial(t) defer teardown(cli, t) fileCount, dirCount, plsCount := 0, 0, 0 ls, err := cli.ListInfo("foo") if err != nil { t.Fatalf(`Client.ListInfo("") = %v, %s need _, nil`, ls, err) } for i, item := range ls { if _, ok := item["file"]; ok { fileCount++ for _, field := range []string{"last-modified", "artist", "title", "track"} { if _, ok := item[field]; !ok { t.Errorf(`ListInfo: file item %d has no "%s" field`, i, field) } } } else if _, ok := item["directory"]; ok { dirCount++ } else if _, ok := item["playlist"]; ok { plsCount++ } else { t.Errorf("ListInfo: item %d has no file/directory/playlist attribute", i) } } if expected := 100; fileCount != expected { t.Errorf(`ListInfo: expected %d files, got %d`, expected, fileCount) } if expected := 2; dirCount != expected { t.Errorf(`ListInfo: expected %d directories, got %d`, expected, dirCount) } if expected := 1; plsCount != expected { t.Errorf(`ListInfo: expected %d playlists, got %d`, expected, plsCount) } } func TestSticker(t *testing.T) { testCases := []struct { Song, Name, Value string }{ {"song0000.ogg", "rating", "superb"}, {"song0000.ogg", "num_rating", "10"}, } cli := localDial(t) defer teardown(cli, t) t.Run("Set", func(t *testing.T) { for _, tc := range testCases { if err := cli.StickerSet(tc.Song, tc.Name, tc.Value); err != nil { t.Fatalf("Client.StickerSet of %v failed: %v", tc, err) } } }) t.Run("Get", func(t *testing.T) { for _, tc := range testCases { s, err := cli.StickerGet(tc.Song, tc.Name) if err != nil { t.Fatalf("Client.StickerGet of %v failed: %v", tc, err) } if s.Value != tc.Value { t.Errorf("Client.StickerGet of %v is %v; want %v", tc, s.Value, tc.Value) } } }) t.Run("List", func(t *testing.T) { stks, err := cli.StickerList(testCases[0].Song) if err != nil { t.Fatalf("Client.StickerList failed: %v", err) } if len(stks) != len(testCases) { t.Errorf("Client.StickerList returned %v stickers; want %v", len(stks), len(testCases)) } }) t.Run("Find", func(t *testing.T) { for _, tc := range testCases { files, stks, err := cli.StickerFind("", tc.Name) if err != nil { t.Fatalf("Client.StickerFind(%q) failed: %v", tc.Name, err) } if len(files) != len(stks) { t.Errorf("Client.StickerFind(%q) returned %v files and %v stickers", tc.Name, len(files), len(stks)) } if len(files) != 1 { t.Errorf("Client.StickerFind(%q) returned %v file; need 1", tc.Name, len(files)) } } }) t.Run("Delete", func(t *testing.T) { for i, tc := range testCases { if err := cli.StickerDelete(tc.Song, tc.Name); err != nil { t.Fatalf("Client.StickerDelete of %v failed: %v", tc, err) } stks, err := cli.StickerList(testCases[0].Song) if err != nil { t.Fatalf("Client.StickerList failed: %v", err) } if len(stks) != len(testCases)-i-1 { t.Fatalf("Client.StickerList returned %v stickers; want %v", len(stks), len(testCases)-i-1) } } }) } func TestCurrentSong(t *testing.T) { cli := localDial(t) defer teardown(cli, t) attrs, err := cli.CurrentSong() if err != nil { t.Fatalf("Client.CurrentSong() = %v, %s need _, nil", attrs, err) } if len(attrs) == 0 { return // no current song } if _, ok := attrs["file"]; !ok { t.Fatalf("current song (attrs=%v) has no file attribute", attrs) } } func TestReadComments(t *testing.T) { cli := localDial(t) defer teardown(cli, t) attrs, err := cli.ReadComments("foo.mp3") if err != nil { t.Fatalf(`Client.ReadComments("foo.mp3") = %v, %s need _, nil`, attrs, err) } if _, ok := attrs["TITLE"]; !ok { t.Fatalf("comments (attrs=%v) has no ARTIST attribute", attrs) } } func TestVersion(t *testing.T) { cli := localDial(t) defer teardown(cli, t) if cli.Version() != "gompd0.1" { t.Errorf("Client.Version failed: %s != gompd0.1", cli.Version()) } } func TestPing(t *testing.T) { cli := localDial(t) defer teardown(cli, t) if err := cli.Ping(); err != nil { t.Errorf("Client.Ping failed: %s", err) } } func TestUpdate(t *testing.T) { cli := localDial(t) defer teardown(cli, t) id, err := cli.Update("foo") if err != nil { t.Fatalf("Client.Update failed: %s\n", err) } if id < 1 { t.Errorf("job id is too small: %d", id) } } func TestRescan(t *testing.T) { cli := localDial(t) defer teardown(cli, t) id, err := cli.Rescan("foo") if err != nil { t.Fatalf("Client.Rescan failed: %s\n", err) } if id < 1 { t.Errorf("job id is too small: %d", id) } } func TestListOutputs(t *testing.T) { cli := localDial(t) defer teardown(cli, t) outputs, err := cli.ListOutputs() if err != nil { t.Fatalf(`Client.ListOutputs() = %v, %s need _, nil`, outputs, err) } expected := []Attrs{{ "outputid": "0", "outputname": "downstairs", "outputenabled": "1", }, { "outputid": "1", "outputname": "upstairs", "outputenabled": "0", }} if len(outputs) != 2 { t.Errorf(`Listed %d outputs, expected %d`, len(outputs), 2) } for i, o := range outputs { if len(o) != 3 { t.Errorf(`Output should contain 3 keys, got %d`, len(o)) } for k, v := range expected[i] { if outputs[i][k] != v { t.Errorf(`Expected property %s for key "%s", got %s`, v, k, outputs[i][k]) } } } } func TestEnableOutput(t *testing.T) { cli := localDial(t) defer teardown(cli, t) if err := cli.EnableOutput(1); err != nil { t.Fatalf("Client.EnableOutput failed: %s\n", err) } } func TestDisableOutput(t *testing.T) { cli := localDial(t) defer teardown(cli, t) if err := cli.DisableOutput(1); err != nil { t.Fatalf("Client.DisableOutput failed: %s\n", err) } } func TestPlaylistFunctions(t *testing.T) { cli := localDial(t) defer teardown(cli, t) files, err := cli.GetFiles() if err != nil { t.Fatalf("Client.GetFiles failed: %s\n", err) } if len(files) < 2 { t.Log("Add more then 1 audio file to your MPD to run this test.") return } for i := 0; i < 2; i++ { if err = cli.PlaylistAdd("Test Playlist", files[i]); err != nil { t.Fatalf("Client.PlaylistAdd failed: %s\n", err) } } attrs, err := cli.ListPlaylists() if err != nil { t.Fatalf("Client.ListPlaylists failed: %s\n", err) } if i := attrsListIndex(attrs, "playlist", "Test Playlist"); i < 0 { t.Fatalf("Couldn't find playlist \"Test Playlist\" in %v\n", attrs) } attrs, err = cli.PlaylistContents("Test Playlist") if err != nil { t.Fatalf("Client.PlaylistContents failed: %s\n", err) } if i := attrsListIndex(attrs, "file", files[0]); i < 0 { t.Fatalf("Couldn't find song %q in %v", files[0], attrs) } if err = cli.PlaylistDelete("Test Playlist", 0); err != nil { t.Fatalf("Client.PlaylistDelete failed: %s\n", err) } playlist, err := cli.PlaylistContents("Test Playlist") if err != nil { t.Fatalf("Client.PlaylistContents failed: %s\n", err) } if !attrsListEqual(playlist, attrs[1:]) { t.Fatalf("PlaylistContents returned %v; want %v", playlist, attrs[1:]) } cli.PlaylistRemove("Test Playlist 2") if err = cli.PlaylistRename("Test Playlist", "Test Playlist 2"); err != nil { t.Fatalf("Client.PlaylistRename failed: %s\n", err) } if err = cli.Clear(); err != nil { t.Fatalf("Client.Clear failed: %s\n", err) } if err = cli.PlaylistLoad("Test Playlist 2", -1, -1); err != nil { t.Fatalf("Client.Load failed: %s\n", err) } attrs, err = cli.PlaylistInfo(-1, -1) if err != nil { t.Fatalf("Client.PlaylistInfo failed: %s\n", err) } if !attrsListEqualKey(playlist, attrs, "file") { t.Fatalf("Unexpected playlist: %v != %v\n", attrs, playlist) } if err = cli.PlaylistClear("Test Playlist 2"); err != nil { t.Fatalf("Client.PlaylistClear failed: %s\n", err) } attrs, err = cli.PlaylistContents("Test Playlist 2") if err != nil { t.Fatalf("Client.PlaylistContents failed: %s\n", err) } if len(attrs) != 0 { t.Fatalf("Unexpected number of songs: %d != 0\n", len(attrs)) } if err = cli.PlaylistRemove("Test Playlist 2"); err != nil { t.Fatalf("Client.PlaylistRemove failed: %s\n", err) } attrs, err = cli.ListPlaylists() if err != nil { t.Fatalf("Client.ListPlaylists failed: %s\n", err) } if i := attrsListIndex(attrs, "playlist", "Test Playlist 2"); i > -1 { t.Fatalf("Found playlist \"Test Playlist 2\" in %v\n", attrs) } if err = cli.PlaylistSave("Test Playlist"); err != nil { t.Fatalf("Client.PlaylistSave failed: %s\n", err) } attrs, err = cli.PlaylistContents("Test Playlist") if err != nil { t.Fatalf("Client.PlaylistContents failed: %s\n", err) } if !attrsListEqual(playlist, attrs) { t.Fatalf("Unexpected playlist: %v != %v\n", attrs, playlist) } } func attrsListIndex(attrs []Attrs, key, value string) int { for i, attr := range attrs { if attr[key] == value { return i } } return -1 } func attrsListEqual(a, b []Attrs) bool { if len(a) != len(b) { return false } for i := range a { if !attrsEqual(a[i], b[i]) { return false } } return true } func attrsListEqualKey(a, b []Attrs, key string) bool { if len(a) != len(b) { return false } for i := range a { if a[i][key] != b[i][key] { return false } } return true } func TestPriority(t *testing.T) { cli := localDial(t) defer teardown(cli, t) for _, tc := range []struct { priority, start, end int ok bool }{ {255, 1, 1, true}, {255, 1, 1, true}, {256, 1, -1, false}, {-1, 1, 1, false}, } { // if tc.ok is true,, err should be nil err := cli.SetPriority(tc.priority, tc.start, tc.end) if err != nil && tc.ok { t.Errorf("Client.SetPriority failed: %s", err) } if err == nil && !tc.ok { t.Errorf("Client.SetPriority succeeded when it should fail") } } } func TestPriorityID(t *testing.T) { cli := localDial(t) defer teardown(cli, t) for _, tc := range []struct { priority, id int ok bool }{ {255, 1, true}, {255, 1, true}, {256, 1, false}, {-1, 1, false}, {1, -1, false}, } { // if tc.ok is true,, err should be nil err := cli.SetPriorityID(tc.priority, tc.id) if err != nil && tc.ok { t.Errorf("Client.SetPriorityID failed: %s", err) } if err == nil && !tc.ok { t.Errorf("Client.SetPriorityID succeeded when it should fail") } } } // TODO test adding at position // TODO test “addid” failures // - invalid position // - unexpected result (not an ID) // - invalid song func TestAddIDAndDeleteID(t *testing.T) { cli := localDial(t) defer teardown(cli, t) id1, err := cli.AddID("song0042.ogg", -1) if err != nil { t.Fatalf("Client.AddID failed: %s\n", err) } id2, err := cli.AddID("song0042.ogg", -1) if err != nil { t.Fatalf("Client.AddID failed: %s\n", err) } if id1 == id2 { t.Fatalf("Client.AddID returned the same ID twice\n") } if err := cli.DeleteID(id1); err != nil { t.Fatalf("Client.DeleteID failed: %s\n", err) } err = cli.DeleteID(id1) if err == nil { t.Fatalf("Client.DeleteID did not fail on second delete of an ID\n") } mpdErr, ok := err.(Error) if !ok { t.Fatalf("Client.DeleteID did not fail with an mpd.Error\n") } if mpdErr.Code != ErrorNoExist { t.Fatalf("Unexpected error code: expected %d, got %d\n", ErrorNoExist, mpdErr.Code) } if err := cli.DeleteID(id2); err != nil { t.Fatalf("Client.DeleteID failed: %s\n", err) } } func TestResponseErrorHandling(t *testing.T) { cli := localDial(t) defer teardown(cli, t) for name, fn := range map[string]func() error{ // “list” requires an argument "in readList": func() error { _, err := cli.Command("list").Strings("file"); return err }, "in readAttrsList": func() error { _, err := cli.PlaylistContents("does_not_exist"); return err }, "in readAttrs": func() error { _, err := cli.ReadComments(""); return err }, "in readOKLine": func() error { return cli.DeleteID(123) }, "in Update": func() error { _, err := cli.Update(""); return err }, "in ListAllInfo": func() error { _, err := cli.ListAllInfo(""); return err }, "in ListInfo": func() error { _, err := cli.ListInfo(""); return err }, "in List": func() error { _, err := cli.List(""); return err }, } { t.Run(name, func(t *testing.T) { if err := fn(); err == nil { t.Errorf("did not fail on MPD error response") } else if _, ok := err.(Error); !ok { t.Errorf("did not fail with an mpd.Error") } }) } } func TestAlbumArt(t *testing.T) { cli := localDial(t) defer teardown(cli, t) tests := []struct { name string uri string want []byte wantErr bool }{ {"artwork as a whole", "/file/with/small-artwork", []byte{0x01, 0x02, 0x03, 0x04, 0x05}, false}, {"artwork in chunks", "/file/with/huge-artwork", []byte{0x01, 0x02, 0x03, 0x04, 0x05}, false}, {"nonexistent artwork", "some_wrong_file", nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := cli.AlbumArt(tt.uri) if (err != nil) != tt.wantErr { t.Errorf("AlbumArt() error = %v, wantErr %v", err, tt.wantErr) } else if !reflect.DeepEqual(got, tt.want) { t.Errorf("AlbumArt() got = %v, want %v", got, tt.want) } }) } } func TestReadPicture(t *testing.T) { cli := localDial(t) defer teardown(cli, t) tests := []struct { name string uri string want []byte wantErr bool }{ {"artwork as a whole", "/file/with/small-artwork", []byte{0x01, 0x02, 0x03, 0x04, 0x05}, false}, {"artwork in chunks", "/file/with/huge-artwork", []byte{0x01, 0x02, 0x03, 0x04, 0x05}, false}, {"nonexistent artwork", "some_wrong_file", nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := cli.ReadPicture(tt.uri) if (err != nil) != tt.wantErr { t.Errorf("ReadPicture() error = %v, wantErr %v", err, tt.wantErr) } else if !reflect.DeepEqual(got, tt.want) { t.Errorf("ReadPicture() got = %v, want %v", got, tt.want) } }) } } gompd-2.3.0/mpd/commandlist.go000066400000000000000000000312131432532623500162500ustar00rootroot00000000000000// Copyright 2013 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 import ( "container/list" "errors" "fmt" "strconv" ) type cmdType uint const ( cmdNoReturn cmdType = iota cmdAttrReturn cmdIDReturn ) type command struct { cmd string promise interface{} typeOf cmdType } // CommandList is for batch/mass MPD commands. // See http://www.musicpd.org/doc/protocol/command_lists.html // for more details. type CommandList struct { client *Client cmdQ *list.List } // PromisedAttrs is a set of promised attributes (to be) returned by MPD. type PromisedAttrs struct { attrs Attrs computed bool } func newPromisedAttrs() *PromisedAttrs { return &PromisedAttrs{attrs: make(Attrs), computed: false} } // PromisedID is a promised identifier (to be) returned by MPD. type PromisedID int // Value returns the Attrs that were computed when CommandList.End was // called. Returns an error if CommandList.End has not yet been called. func (pa *PromisedAttrs) Value() (Attrs, error) { if !pa.computed { return nil, errors.New("value has not been computed yet") } return pa.attrs, nil } // Value returns the ID that was computed when CommandList.End was // called. Returns an error if CommandList.End has not yet been called. func (pi *PromisedID) Value() (int, error) { if *pi == -1 { return -1, errors.New("value has not been computed yet") } return (int)(*pi), nil } // BeginCommandList creates a new CommandList structure using // this connection. func (c *Client) BeginCommandList() *CommandList { return &CommandList{c, list.New()} } // Ping sends a no-op message to MPD. It's useful for keeping the connection alive. func (cl *CommandList) Ping() { cl.cmdQ.PushBack(&command{"ping", nil, cmdNoReturn}) } // CurrentSong returns information about the current song in the playlist. func (cl *CommandList) CurrentSong() *PromisedAttrs { pa := newPromisedAttrs() cl.cmdQ.PushBack(&command{"currentsong", pa, cmdAttrReturn}) return pa } // Status returns information about the current status of MPD. func (cl *CommandList) Status() *PromisedAttrs { pa := newPromisedAttrs() cl.cmdQ.PushBack(&command{"status", pa, cmdAttrReturn}) return pa } // // Playback control // // Next plays next song in the playlist. func (cl *CommandList) Next() { cl.cmdQ.PushBack(&command{"next", nil, cmdNoReturn}) } // Pause pauses playback if pause is true; resumes playback otherwise. func (cl *CommandList) Pause(pause bool) { if pause { cl.cmdQ.PushBack(&command{"pause 1", nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{"pause 0", nil, cmdNoReturn}) } } // Play starts playing the song at playlist position pos. If pos is negative, // start playing at the current position in the playlist. func (cl *CommandList) Play(pos int) { if pos < 0 { cl.cmdQ.PushBack(&command{"play", nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{fmt.Sprintf("play %d", pos), nil, cmdNoReturn}) } } // PlayID plays the song identified by id. If id is negative, start playing // at the currect position in playlist. func (cl *CommandList) PlayID(id int) { if id < 0 { cl.cmdQ.PushBack(&command{"playid", nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{fmt.Sprintf("playid %d", id), nil, cmdNoReturn}) } } // Previous plays previous song in the playlist. func (cl *CommandList) Previous() { cl.cmdQ.PushBack(&command{"previous", nil, cmdNoReturn}) } // Seek seeks to the position time (in seconds) of the song at playlist position pos. func (cl *CommandList) Seek(pos, time int) { cl.cmdQ.PushBack(&command{fmt.Sprintf("seek %d %d", pos, time), nil, cmdNoReturn}) } // SeekID is identical to Seek except the song is identified by it's id // (not position in playlist). func (cl *CommandList) SeekID(id, time int) { cl.cmdQ.PushBack(&command{fmt.Sprintf("seekid %d %d", id, time), nil, cmdNoReturn}) } // Stop stops playback. func (cl *CommandList) Stop() { cl.cmdQ.PushBack(&command{"stop", nil, cmdNoReturn}) } // SetVolume sets the MPD volume level. func (cl *CommandList) SetVolume(volume int) { cl.cmdQ.PushBack(&command{fmt.Sprintf("setvol %d", volume), nil, cmdNoReturn}) } // Random enables random playback, if random is true, disables it otherwise. func (cl *CommandList) Random(random bool) { if random { cl.cmdQ.PushBack(&command{"random 1", nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{"random 0", nil, cmdNoReturn}) } } // Repeat enables repeat mode, if repeat is true, disables it otherwise. func (cl *CommandList) Repeat(repeat bool) { if repeat { cl.cmdQ.PushBack(&command{"repeat 1", nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{"repeat 0", nil, cmdNoReturn}) } } // Single enables single song mode, if single is true, disables it otherwise. func (cl *CommandList) Single(single bool) { if single { cl.cmdQ.PushBack(&command{"single 1", nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{"single 0", nil, cmdNoReturn}) } } // Consume enables consume mode, if consume is true, disables it otherwise. func (cl *CommandList) Consume(consume bool) { if consume { cl.cmdQ.PushBack(&command{"consume 1", nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{"consume 0", nil, cmdNoReturn}) } } // // Playlist related functions // // SetPriority sets the priority for songs in the playlist. If both start and // end are non-negative, it updates those at positions in range [start, end). // If end is negative, it updates the song at position start. func (cl *CommandList) SetPriority(priority, start, end int) error { if start < 0 { return errors.New("negative start index") } if end < 0 { cl.cmdQ.PushBack(&command{fmt.Sprintf("prio %d %d", priority, start), nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{fmt.Sprintf("prio %d %d:%d", priority, start, end), nil, cmdNoReturn}) } return nil } // SetPriorityID sets the priority for the song identified by id. func (cl *CommandList) SetPriorityID(priority, id int) { cl.cmdQ.PushBack(&command{fmt.Sprintf("prioid %d %d", priority, id), nil, cmdNoReturn}) } // 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 (cl *CommandList) Delete(start, end int) error { if start < 0 { return errors.New("negative start index") } if end < 0 { cl.cmdQ.PushBack(&command{fmt.Sprintf("delete %d", start), nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{fmt.Sprintf("delete %d:%d", start, end), nil, cmdNoReturn}) } return nil } // DeleteID deletes the song identified by id. func (cl *CommandList) DeleteID(id int) { cl.cmdQ.PushBack(&command{fmt.Sprintf("deleteid %d", id), nil, cmdNoReturn}) } // 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 (cl *CommandList) Move(start, end, position int) error { if start < 0 { return errors.New("negative start index") } if end < 0 { cl.cmdQ.PushBack(&command{fmt.Sprintf("move %d %d", start, position), nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{fmt.Sprintf("move %d:%d %d", start, end, position), nil, cmdNoReturn}) } return nil } // MoveID moves songid to position on the playlist. func (cl *CommandList) MoveID(songid, position int) { cl.cmdQ.PushBack(&command{fmt.Sprintf("moveid %d %d", songid, position), nil, cmdNoReturn}) } // Add adds the file/directory uri to playlist. Directories add recursively. func (cl *CommandList) Add(uri string) { cl.cmdQ.PushBack(&command{fmt.Sprintf("add %s", quote(uri)), nil, cmdNoReturn}) } // 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 (cl *CommandList) AddID(uri string, pos int) *PromisedID { var id PromisedID = -1 if pos >= 0 { cl.cmdQ.PushBack(&command{fmt.Sprintf("addid %s %d", quote(uri), pos), &id, cmdIDReturn}) } else { cl.cmdQ.PushBack(&command{fmt.Sprintf("addid %s", quote(uri)), &id, cmdIDReturn}) } return &id } // Clear clears the current playlist. func (cl *CommandList) Clear() { cl.cmdQ.PushBack(&command{"clear", nil, cmdNoReturn}) } // 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 (cl *CommandList) Shuffle(start, end int) { if start < 0 || end < 0 { cl.cmdQ.PushBack(&command{"shuffle", nil, cmdNoReturn}) return } cl.cmdQ.PushBack(&command{fmt.Sprintf("shuffle %d:%d", start, end), nil, cmdNoReturn}) } // 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. func (cl *CommandList) Update(uri string) (attrs *PromisedAttrs) { attrs = newPromisedAttrs() cl.cmdQ.PushBack(&command{fmt.Sprintf("update %s", quote(uri)), attrs, cmdAttrReturn}) return } // Stored playlists related commands. // PlaylistLoad loads the specfied playlist into the current queue. // If start and end are non-negative, only songs in this range are loaded. func (cl *CommandList) PlaylistLoad(name string, start, end int) { if start < 0 || end < 0 { cl.cmdQ.PushBack(&command{fmt.Sprintf("load %s", quote(name)), nil, cmdNoReturn}) } else { cl.cmdQ.PushBack(&command{fmt.Sprintf("load %s %d:%d", quote(name), start, end), nil, cmdNoReturn}) } } // PlaylistAdd adds a song identified by uri to a stored playlist identified // by name. func (cl *CommandList) PlaylistAdd(name string, uri string) { cl.cmdQ.PushBack(&command{fmt.Sprintf("playlistadd %s %s", quote(name), quote(uri)), nil, cmdNoReturn}) } // PlaylistClear clears the specified playlist. func (cl *CommandList) PlaylistClear(name string) { cl.cmdQ.PushBack(&command{fmt.Sprintf("playlistclear %s", quote(name)), nil, cmdNoReturn}) } // PlaylistDelete deletes the song at position pos from the specified playlist. func (cl *CommandList) PlaylistDelete(name string, pos int) { cl.cmdQ.PushBack(&command{fmt.Sprintf("playlistdelete %s %d", quote(name), pos), nil, cmdNoReturn}) } // PlaylistMove moves a song identified by id in a playlist identified by name // to the position pos. func (cl *CommandList) PlaylistMove(name string, id, pos int) { cl.cmdQ.PushBack(&command{fmt.Sprintf("playlistmove %s %d %d", quote(name), id, pos), nil, cmdNoReturn}) } // PlaylistRename renames the playlist identified by name to newName. func (cl *CommandList) PlaylistRename(name, newName string) { cl.cmdQ.PushBack(&command{fmt.Sprintf("rename %s %s", quote(name), quote(newName)), nil, cmdNoReturn}) } // PlaylistRemove removes the playlist identified by name from the playlist // directory. func (cl *CommandList) PlaylistRemove(name string) { cl.cmdQ.PushBack(&command{fmt.Sprintf("rm %s", quote(name)), nil, cmdNoReturn}) } // PlaylistSave saves the current playlist as name in the playlist directory. func (cl *CommandList) PlaylistSave(name string) { cl.cmdQ.PushBack(&command{fmt.Sprintf("save %s", quote(name)), nil, cmdNoReturn}) } // End executes the command list. func (cl *CommandList) End() error { // Tell MPD to start an OK command list: beginID, beginErr := cl.client.cmd("command_list_ok_begin") if beginErr != nil { return beginErr } cl.client.text.StartResponse(beginID) cl.client.text.EndResponse(beginID) // Ensure the queue is cleared regardless. defer cl.cmdQ.Init() // Issue all of the queued up commands in the list: for e := cl.cmdQ.Front(); e != nil; e = e.Next() { cmdID, cmdErr := cl.client.cmd(e.Value.(*command).cmd) if cmdErr != nil { return cmdErr } cl.client.text.StartResponse(cmdID) cl.client.text.EndResponse(cmdID) } // Tell MPD to end the command list and do the operations. endID, endErr := cl.client.cmd("command_list_end") if endErr != nil { return endErr } cl.client.text.StartResponse(endID) defer cl.client.text.EndResponse(endID) // Get the responses back and check for errors: for e := cl.cmdQ.Front(); e != nil; e = e.Next() { switch e.Value.(*command).typeOf { case cmdNoReturn: if err := cl.client.readOKLine("list_OK"); err != nil { return err } case cmdAttrReturn: a, aErr := cl.client.readAttrs("list_OK") if aErr != nil { return aErr } pa := e.Value.(*command).promise.(*PromisedAttrs) pa.attrs = a pa.computed = true case cmdIDReturn: a, aErr := cl.client.readAttrs("list_OK") if aErr != nil { return aErr } rid, ridErr := strconv.Atoi(a["Id"]) if ridErr != nil { return ridErr } *(e.Value.(*command).promise.(*PromisedID)) = PromisedID(rid) } } // Finalize the command list with the last OK: return cl.client.readOKLine("OK") } gompd-2.3.0/mpd/commandlist_test.go000066400000000000000000000021371432532623500173120ustar00rootroot00000000000000// Copyright 2013 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 import ( "testing" ) func TestCurrentSongPromise(t *testing.T) { cli := localDial(t) defer teardown(cli, t) cmdl := cli.BeginCommandList() pa := cmdl.CurrentSong() if err := cmdl.End(); err != nil { t.Errorf("CommandList.End failed: %s", err) } if _, err := pa.Value(); err != nil { t.Errorf("Promise did not compute: %s", err) } } func TestCommandList(t *testing.T) { cli := localDial(t) defer teardown(cli, t) // Normal command list: cmdl := cli.BeginCommandList() cmdl.Next() cmdl.Next() cmdl.Next() if err := cmdl.End(); err != nil { t.Errorf("CommandList.End failed: %s", err) } // Test empty command list: cmdl2 := cli.BeginCommandList() if err := cmdl2.End(); err != nil { t.Errorf("CommandList.End failed: %s", err) } // Reuse old comandlist (should work): cmdl.Previous() cmdl.Previous() cmdl.Previous() if err := cmdl.End(); err != nil { t.Errorf("CommandList.End failed: %s", err) } } gompd-2.3.0/mpd/example_test.go000066400000000000000000000035161432532623500164350ustar00rootroot00000000000000// 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_test import ( "fmt" "log" "time" "github.com/fhs/gompd/v2/mpd" ) func ExampleDial() { // Connect to MPD server conn, err := mpd.Dial("tcp", "localhost:6600") if err != nil { log.Fatalln(err) } defer conn.Close() line := "" line1 := "" // Loop printing the current status of MPD. for { status, err := conn.Status() if err != nil { log.Fatalln(err) } song, err := conn.CurrentSong() if err != nil { log.Fatalln(err) } if status["state"] == "play" { line1 = fmt.Sprintf("%s - %s", song["Artist"], song["Title"]) } else { line1 = fmt.Sprintf("State: %s", status["state"]) } if line != line1 { line = line1 fmt.Println(line) } time.Sleep(1e9) } } func ExampleNewWatcher() { w, err := mpd.NewWatcher("tcp", ":6600", "") if err != nil { log.Fatalln(err) } defer w.Close() // Log errors. go func() { for err := range w.Error { log.Println("Error:", err) } }() // Log events. go func() { for subsystem := range w.Event { log.Println("Changed subsystem:", subsystem) } }() // Do other stuff... time.Sleep(3 * time.Minute) } func ExampleBeginCommandList() { // Connect to the MPD server. conn, err := mpd.Dial("tcp", "localhost:6600") if err != nil { log.Fatalln(err) } defer conn.Close() // Create a *CommandList. cl := conn.BeginCommandList() cl.Play(0) promisedAttrs := cl.CurrentSong() // Execute the *CommandList. if err := cl.End(); err != nil { log.Fatalln("CommandList.End failed:", err) } // Use the returned attributes. attrs, err := promisedAttrs.Value() if err != nil { log.Fatalln("PromisedAttrs.Value failed: ", err) } log.Println("Currently playing file:", attrs["file"]) } gompd-2.3.0/mpd/internal/000077500000000000000000000000001432532623500152235ustar00rootroot00000000000000gompd-2.3.0/mpd/internal/server/000077500000000000000000000000001432532623500165315ustar00rootroot00000000000000gompd-2.3.0/mpd/internal/server/server.go000066400000000000000000000502661432532623500203770ustar00rootroot00000000000000// Copyright 2013 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 server implements a fake MPD server that's used to test gompd client. package server import ( "fmt" "io" "log" "net" "net/textproto" "os" "sort" "strconv" "strings" ) // attrs is a set of attributes returned by MPD. type attrs map[string]string const ( accErrorNoExist = 50 ) func unquote(line string, start int) (string, int) { i := start if line[i] != '"' { for i < len(line) && (line[i] != ' ' && line[i] != '\t') { i++ } return line[start:i], i } i++ // beginning " s := make([]byte, len(line[i:])) n := 0 for i < len(line) { if line[i] == '"' { // ending " i++ break } if line[i] == '\\' && i+1 < len(line) { i++ } s[n] = line[i] i++ n++ } return string(s[:n]), i } func parseArgs(line string) (args []string) { var s string i := 0 for i < len(line) { if line[i] == ' ' || line[i] == '\t' { i++ continue } s, i = unquote(line, i) args = append(args, s) } return } type playlistEntry struct { song int id int } type playlist struct { songs []playlistEntry maxid int } func newPlaylist() *playlist { return &playlist{songs: make([]playlistEntry, 0), maxid: 0} } func (p *playlist) At(i int) int { return p.songs[i].song } func (p *playlist) Len() int { return len(p.songs) } func (p *playlist) Add(song int) int { entry := playlistEntry{song: song, id: p.maxid} p.songs = append(p.songs, entry) p.maxid++ return entry.id } func (p *playlist) Delete(i int) { if i < 0 || i >= len(p.songs) { return } copy(p.songs[i:], p.songs[i+1:]) p.songs = p.songs[:len(p.songs)-1] } func (p *playlist) Clear() { p.songs = p.songs[:0] } func (p *playlist) Append(q *playlist) { // TODO: do at most one allocation for i := 0; i < q.Len(); i++ { p.Add(q.At(i)) } } type sticker struct { Name, Value string } func newSticker(name, value string) *sticker { return &sticker{ Name: name, Value: value, } } func (s *sticker) String() string { return s.Name + "=" + s.Value } type stickers map[string]*sticker func newStickers() stickers { return make(stickers) } func (ss stickers) Set(name, value string) { if _, ok := ss[name]; !ok { ss[name] = newSticker(name, value) return } ss[name].Value = value } func (ss stickers) Get(name string) *sticker { return ss[name] } func (ss stickers) Delete(name string) { delete(ss, name) } func (ss stickers) Sorted() []*sticker { var v []*sticker for _, s := range ss { v = append(v, s) } sort.Slice(v, func(i, j int) bool { return v[i].Name < v[j].Name }) return v } type server struct { state string database []attrs // database of songs index map[string]int // maps URI to database index playlists map[string]*playlist currentPlaylist *playlist songStickers map[string]stickers pos int // in currentPlaylist artwork []byte idleEventc chan string idleStartc chan *idleRequest idleEndc chan uint } func newServer() *server { s := &server{ state: "stop", database: make([]attrs, 100), index: make(map[string]int, 100), songStickers: make(map[string]stickers, 100), playlists: make(map[string]*playlist), currentPlaylist: newPlaylist(), pos: 0, artwork: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, idleEventc: make(chan string), idleStartc: make(chan *idleRequest), idleEndc: make(chan uint), } for i := 0; i < len(s.database); i++ { s.database[i] = make(attrs, 5) filename := fmt.Sprintf("song%04d.ogg", i) s.database[i]["file"] = filename s.index[filename] = i s.songStickers[filename] = newStickers() } return s } func writeBinaryChunk(p *textproto.Conn, data []byte, offset, length int) { p.PrintfLine("size: %d", len(data)) if offset+length > len(data) { length = len(data) - offset } p.PrintfLine("binary: %d", length) // Can't use p.PrintfLine() for writing bytes since it'd terminate the data with \r\n instead of just \n p.W.Write(data[offset : offset+length]) p.W.WriteByte('\n') } func (s *server) writeResponse(p *textproto.Conn, args []string, okLine string) (cmdOk, closed bool) { if len(args) < 1 { p.PrintfLine("No command given") return } ack := func(format string, a ...interface{}) error { return p.PrintfLine("ACK {"+args[0]+"} "+format, a...) } ackWithCode := func(code int, format string, a ...interface{}) error { return p.PrintfLine(fmt.Sprintf("ACK [%d@0] {%s} %s", code, args[0], format), a...) } switch args[0] { case "close": closed = true return case "list": if len(args) < 2 || args[1] == "" { ack("too few arguments") return } if args[1] == "file" { for _, a := range s.database { p.PrintfLine("file: %s", a["file"]) } } case "listallinfo": if len(args) < 2 || args[1] == "" { ack("too few arguments") return } p.PrintfLine("OK") case "lsinfo": if len(args) < 2 || args[1] == "" { ack("too few arguments") return } for _, a := range s.database { p.PrintfLine("file: %s", a["file"]) p.PrintfLine("Last-Modified: 2014-07-02T12:32:26Z") p.PrintfLine("Artist: Newcleus") p.PrintfLine("Title: Jam On It") p.PrintfLine("Track: 02") } for _, a := range []string{ "music/Buck 65 - Dirtbike 1", "music/Howlin' Wolf - Moanin' in the Moonlight", } { p.PrintfLine("directory: %s", a) } p.PrintfLine("playlist: BBC 6 Music.m3u") case "readcomments": if len(args) < 2 || args[1] == "" { ack("too few arguments") return } p.PrintfLine("TITLE: Jam On It") p.PrintfLine("ARTIST: Newcleus") p.PrintfLine("ALBUM: Jam on Revenge") case "listplaylists": for k := range s.playlists { p.PrintfLine("playlist: %s", k) } case "playlistinfo": var rng []string var start int end := s.currentPlaylist.Len() if len(args) >= 2 { rng = strings.Split(args[1], ":") } if len(rng) == 1 { // Requesting a single song from the playlist at position i. i, err := strconv.Atoi(rng[0]) if err != nil { ack("invalid song position") return } start = i end = i + 1 } else if len(rng) == 2 { // Requesting a range of the playlist from specified start/end positions. var err error start, err = strconv.Atoi(rng[0]) if err != nil { ack("integer or range expected") return } end, err := strconv.Atoi(rng[1]) if err != nil { ack("integer or range expected") return } if start < 0 || end < 0 { ack("number is negative") return } } for i := start; i < end; i++ { p.PrintfLine("file: %s", s.database[s.currentPlaylist.At(i)]["file"]) } case "listplaylistinfo": if len(args) < 2 { ack("too few arguments") return } pl, ok := s.playlists[args[1]] if !ok { ack("no such playlist") return } for i := 0; i < pl.Len(); i++ { p.PrintfLine("file: %s", s.database[pl.At(i)]["file"]) } case "playlistadd": if len(args) != 3 { ack("wrong number of arguments") return } name, uri := args[1], args[2] i, ok := s.index[uri] if !ok { ack("URI not found") return } if s.playlists[name] == nil { s.playlists[name] = newPlaylist() } s.playlists[name].Add(i) case "playlistdelete": if len(args) != 3 { ack("wrong number of arguments") return } name := args[1] pos, err := strconv.Atoi(args[2]) if err != nil { ack("invalid position number") return } pl, ok := s.playlists[name] if !ok { ack("playlist not found") return } if pos < 0 || pos >= pl.Len() { ack("invalid song position") return } pl.Delete(pos) case "playlistclear": if len(args) != 2 { ack("wrong number of arguments") return } pl, ok := s.playlists[args[1]] if !ok { ack("playlist not found") return } pl.Clear() case "rm": if len(args) != 2 { ack("wrong number of arguments") return } _, ok := s.playlists[args[1]] if !ok { ack("playlist not found") return } delete(s.playlists, args[1]) case "rename": if len(args) != 3 { ack("wrong number of arguments") return } old, new := args[1], args[2] _, ok := s.playlists[old] if !ok { ack("playlist %s does not exist", old) return } _, ok = s.playlists[new] if ok { ack("playlist %s already exists", new) return } s.playlists[new] = s.playlists[old] delete(s.playlists, old) case "load": if len(args) != 2 { ack("wrong number of arguments") return } pl, ok := s.playlists[args[1]] if !ok { ack("playlist %s does not exist", args[1]) return } s.currentPlaylist.Append(pl) case "clear": s.currentPlaylist.Clear() case "add": if len(args) != 2 { ack("wrong number of arguments") return } i, ok := s.index[args[1]] if !ok { ack("URI not found") return } s.currentPlaylist.Add(i) case "addid": if len(args) < 2 || len(args) > 3 { ack("wrong number of arguments") return } i, ok := s.index[args[1]] if !ok { ack("URI not found") return } id := s.currentPlaylist.Add(i) p.PrintfLine("Id: %d", id) case "prio": if len(args) != 3 { ack("wrong number of arguments") return } rng := strings.Split(args[2], ":") prio, err := strconv.Atoi(args[1]) if err != nil || prio < 0 || prio > 255 { ack("invalid priority") return } switch len(rng) { case 1: // Updating a single song from the playlist at position i. i, err := strconv.Atoi(rng[0]) if err != nil { ack("invalid song position") return } // Note: we don't check end as it does not matter for our test case if i < 0 { ack("invalid song position") } case 2: // Updating a range of the playlist from specified start/end positions. start, err := strconv.Atoi(rng[0]) if err != nil { ack("integer or range expected") return } end, err := strconv.Atoi(rng[1]) if err != nil { ack("integer or range expected") return } if start < 0 || end < 0 { ack("number is negative") return } default: ack("invalid range") } case "prioid": if len(args) != 3 { ack("wrong number of arguments") return } prio, err := strconv.Atoi(args[1]) if err != nil || prio < 0 || prio > 255 { ack("invalid priority") return } id, err := strconv.Atoi(args[2]) if err != nil || id < 0 { ack("invalid song ID") return } case "delete": if len(args) != 2 { ack("wrong number of arguments") return } i, err := strconv.Atoi(args[1]) if err != nil { ack("invalid song position") return } s.idleEventc <- "playlist" if i < 0 || i >= s.currentPlaylist.Len() { ack("invalid song position") return } s.currentPlaylist.Delete(i) case "deleteid": if len(args) != 2 { ack("wrong number of arguments") return } id, err := strconv.Atoi(args[1]) if err != nil { ack("invalid song ID") return } s.idleEventc <- "playlist" deleted := false for i, song := range s.currentPlaylist.songs { if song.id == id { deleted = true s.currentPlaylist.Delete(i) break } } if !deleted { ackWithCode(accErrorNoExist, "No such song") return } case "save": if len(args) != 2 { ack("wrong number of arguments") return } name := args[1] _, ok := s.playlists[name] if ok { ack("playlist %s already exists", name) return } s.playlists[name] = newPlaylist() s.playlists[name].Append(s.currentPlaylist) case "play", "stop": s.idleEventc <- "player" s.state = args[0] case "next": s.idleEventc <- "player" if s.pos < 0 || s.pos >= s.currentPlaylist.Len() { s.pos = 0 break } s.pos = (s.pos + 1) % s.currentPlaylist.Len() case "previous": s.idleEventc <- "player" if s.pos < 0 || s.pos >= s.currentPlaylist.Len() { s.pos = 0 break } if s.pos == 0 { s.pos = s.currentPlaylist.Len() - 1 break } s.pos-- case "pause": if s.state != "stop" { s.state = args[0] } case "status": state := s.state p.PrintfLine("state: %s", state) case "update", "rescan": if len(args) < 2 || args[1] == "" { ack("too few arguments") return } p.PrintfLine("updating_db: 1") case "ping": case "currentsong": if s.currentPlaylist.Len() == 0 { break } if s.pos >= s.currentPlaylist.Len() { s.pos = 0 } p.PrintfLine("file: %s", s.database[s.currentPlaylist.At(s.pos)]["file"]) case "albumart": if len(args) < 2 || len(args) > 3 { ack("wrong number of arguments") return } artLen, offset := len(s.artwork), 0 var err error if len(args) == 3 { if offset, err = strconv.Atoi(args[2]); err != nil { ack("invalid offset value: %v", err) return } else if offset >= artLen { ack("offset beyond end of file") return } } switch args[1] { case "/file/with/small-artwork": // Give away the entire "file" at once writeBinaryChunk(p, s.artwork, offset, len(s.artwork)) case "/file/with/huge-artwork": // Give away the "file" 3 bytes at a time writeBinaryChunk(p, s.artwork, offset, 3) default: ack("no artwork found") } case "readpicture": if len(args) < 2 || len(args) > 3 { ack("wrong number of arguments") return } artLen, offset := len(s.artwork), 0 var err error if len(args) == 3 { if offset, err = strconv.Atoi(args[2]); err != nil { ack("invalid offset value: %v", err) return } else if offset >= artLen { ack("offset beyond end of file") return } } switch args[1] { case "/file/with/small-artwork": // Give away the entire "file" at once writeBinaryChunk(p, s.artwork, offset, len(s.artwork)) case "/file/with/huge-artwork": // Give away the "file" 3 bytes at a time writeBinaryChunk(p, s.artwork, offset, 3) default: ack("no artwork found") } case "outputs": p.PrintfLine("outputid: 0") p.PrintfLine("outputenabled: 1") p.PrintfLine("outputname: downstairs") p.PrintfLine("outputid: 1") p.PrintfLine("outputenabled: 0") p.PrintfLine("outputname: upstairs") case "disableoutput", "enableoutput": case "sticker": if len(args) < 4 { ack("too few arguments") return } if args[2] != "song" { ack("Invalid object type %q", args[2]) return } uri := args[3] switch args[1] { case "get": if len(args) < 5 { ack("bad request") return } name := args[4] v, ok := s.songStickers[uri] if !ok { ack("No such song %q", uri) return } stk := v.Get(name) if stk == nil { ack("no such sticker %q", name) return } p.PrintfLine("sticker: %s", stk) case "set": if len(args) < 6 { ack("bad request") return } v, ok := s.songStickers[uri] if !ok { ack("No such song %q", uri) return } v.Set(args[4], args[5]) case "delete": if len(args) < 5 { ack("bad request") return } name := args[4] v, ok := s.songStickers[uri] if !ok { ack("No such song %q", uri) return } if stk := v.Get(name); stk == nil { ack("No such sticker %q", name) return } v.Delete(name) case "list": v, ok := s.songStickers[uri] if !ok { ack("No such song %q", uri) return } for _, stk := range v.Sorted() { p.PrintfLine("sticker: %s", stk) } case "find": if len(args) < 5 { ack("bad request") return } name := args[4] for file, v := range s.songStickers { if stk := v.Get(name); stk != nil { p.PrintfLine("file: %s", file) p.PrintfLine("sticker: %s", stk) } } } default: p.PrintfLine("ACK {} unknown command %q", args[0]) log.Printf("unknown command: %s\n", args[0]) return } cmdOk = true p.PrintfLine(okLine) return } type requestType int const ( simple requestType = iota commandListOk idle noIdle ) type request struct { typ requestType args []string cmdList [][]string } func (s *server) readRequest(p *textproto.Conn) (*request, error) { line, err := p.ReadLine() if err == io.EOF { return nil, err } if err != nil { log.Printf("reading request failed: %v\n", err) return nil, err } args := parseArgs(line) if len(args) == 0 { return &request{typ: simple, args: args}, nil } switch args[0] { case "command_list_ok_begin": var cmdList [][]string for { line, err := p.ReadLine() if err == io.EOF { return nil, err } if err != nil { log.Printf("reading request failed: %v\n", err) return nil, err } args = parseArgs(line) if len(args) > 0 && args[0] == "command_list_end" { break } cmdList = append(cmdList, args) } return &request{typ: commandListOk, cmdList: cmdList}, nil case "idle": return &request{typ: idle, args: args}, nil case "noidle": return &request{typ: noIdle, args: args}, nil } return &request{typ: simple, args: args}, nil } type idleRequest struct { endTokenc chan uint // for token used to end event broadcast eventc chan string // for subsystem name subsystems []string // subsystems to listen for changes } func (s *server) writeIdleResponse(p *textproto.Conn, id uint, quit chan bool, subsystems []string) { p.StartResponse(id) defer p.EndResponse(id) req := &idleRequest{ endTokenc: make(chan uint), eventc: make(chan string, 1), subsystems: subsystems, } s.idleStartc <- req token := <-req.endTokenc select { case name := <-req.eventc: p.PrintfLine("changed: %s", name) p.PrintfLine("OK") <-quit case <-quit: p.PrintfLine("OK") } s.idleEndc <- token } func (s *server) handleConnection(p *textproto.Conn) { id := p.Next() p.StartRequest(id) p.EndRequest(id) p.StartResponse(id) p.PrintfLine("OK MPD gompd0.1") p.EndResponse(id) endIdle := make(chan bool) inIdle := false defer p.Close() for { id := p.Next() p.StartRequest(id) req, err := s.readRequest(p) if err != nil { return } // We need to do this inside request because idle response // may not have ended yet, but it will end after the following. if inIdle { endIdle <- true } p.EndRequest(id) if req.typ == idle { inIdle = true go s.writeIdleResponse(p, id, endIdle, req.args[1:]) // writeIdleResponse does it's own StartResponse/EndResponse continue } p.StartResponse(id) if inIdle { inIdle = false } switch req.typ { case noIdle: case commandListOk: var ok, closed bool ok = true for _, args := range req.cmdList { ok, closed = s.writeResponse(p, args, "list_OK") if closed { return } if !ok { break } } if ok { p.PrintfLine("OK") } case simple: if _, closed := s.writeResponse(p, req.args, "OK"); closed { return } } p.EndResponse(id) } } var knownSubsystems = []string{ "database", "update", "stored_playlist", "playlist", "player", "mixer", "output", "options", } func indexID(v []uint, id uint) int { for i, n := range v { if id == n { return i } } return -1 } func deleteID(v []uint, id uint) []uint { i := indexID(v, id) if i < 0 { return v } copy(v[i:], v[i+1:]) return v[:len(v)-1] } func (s *server) broadcastIdleEvents() { clientChans := make(map[uint]chan string) subsys := make(map[string][]uint) for _, name := range knownSubsystems { subsys[name] = make([]uint, 0) } token := uint(0) for { select { case req := <-s.idleStartc: clientChans[token] = req.eventc names := req.subsystems if len(req.subsystems) == 0 { names = knownSubsystems } for _, name := range names { if _, ok := subsys[name]; !ok { subsys[name] = make([]uint, 0) } subsys[name] = append(subsys[name], token) } req.endTokenc <- token token++ case client := <-s.idleEndc: delete(clientChans, client) for name := range subsys { subsys[name] = deleteID(subsys[name], client) } case name := <-s.idleEventc: if clients, ok := subsys[name]; ok { for _, c := range clients { select { case clientChans[c] <- name: default: } } } } } } // Listen starts the server on the network network and address addr. // Once the server has started, a value is sent to listening channel. func Listen(network, addr string, listening chan bool) { ln, err := net.Listen(network, addr) if err != nil { log.Fatalf("Listen failed: %v\n", err) os.Exit(1) } s := newServer() go s.broadcastIdleEvents() listening <- true for { conn, err := ln.Accept() if err != nil { log.Printf("Accept failed: %v\n", err) continue } go s.handleConnection(textproto.NewConn(conn)) } } gompd-2.3.0/mpd/response.go000066400000000000000000000053151432532623500156000ustar00rootroot00000000000000// Copyright 2018 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 import "fmt" // Quoted is a string that do no need to be quoted. type Quoted string // Command returns a command that can be sent to MPD sever. // It enables low-level access to MPD protocol and should be avoided if // the user is not familiar with MPD protocol. // // Strings in args are automatically quoted so that spaces are preserved. // Pass strings as Quoted type if this is not desired. func (c *Client) Command(format string, args ...interface{}) *Command { for i := range args { switch s := args[i].(type) { case Quoted: // ignore case string: args[i] = quote(s) } } return &Command{ client: c, cmd: fmt.Sprintf(format, args...), } } // A Command represents a MPD command. type Command struct { client *Client cmd string } // String returns the encoded command. func (cmd *Command) String() string { return cmd.cmd } // OK sends command to server and checks for error. func (cmd *Command) OK() error { id, err := cmd.client.cmd("%v", cmd.cmd) if err != nil { return err } cmd.client.text.StartResponse(id) defer cmd.client.text.EndResponse(id) return cmd.client.readOKLine("OK") } // Attrs sends command to server and reads attributes returned in response. func (cmd *Command) Attrs() (Attrs, error) { id, err := cmd.client.cmd(cmd.cmd) if err != nil { return nil, err } cmd.client.text.StartResponse(id) defer cmd.client.text.EndResponse(id) return cmd.client.readAttrs("OK") } // AttrsList sends command to server and reads a list of attributes returned in response. // Each attribute group starts with key startKey. func (cmd *Command) AttrsList(startKey string) ([]Attrs, error) { id, err := cmd.client.cmd(cmd.cmd) if err != nil { return nil, err } cmd.client.text.StartResponse(id) defer cmd.client.text.EndResponse(id) return cmd.client.readAttrsList(startKey) } // Strings sends command to server and reads a list of strings returned in response. // Each string have the key key. func (cmd *Command) Strings(key string) ([]string, error) { id, err := cmd.client.cmd(cmd.cmd) if err != nil { return nil, err } cmd.client.text.StartResponse(id) defer cmd.client.text.EndResponse(id) return cmd.client.readList(key) } // Binary sends command to server and reads its binary response, returning the data and its total size (which can be // greater than the returned chunk). func (cmd *Command) Binary() ([]byte, int, error) { id, err := cmd.client.cmd(cmd.cmd) if err != nil { return nil, 0, err } cmd.client.text.StartResponse(id) defer cmd.client.text.EndResponse(id) return cmd.client.readBinary() } gompd-2.3.0/mpd/watcher.go000066400000000000000000000061211432532623500153730ustar00rootroot00000000000000// Copyright 2013 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 // Watcher represents a MPD client connection that can be watched for events. type Watcher struct { conn *Client // client connection to MPD exit chan bool // channel used to ask loop to terminate done chan bool // channel indicating loop has terminated names chan []string // channel to set new subsystems to watch Event chan string // event channel Error chan error // error channel } // NewWatcher connects to MPD server and watches for changes in subsystems // names. If no subsystem is specified, all changes are reported. // // See http://www.musicpd.org/doc/protocol/command_reference.html#command_idle // for valid subsystem names. func NewWatcher(net, addr, passwd string, names ...string) (w *Watcher, err error) { conn, err := DialAuthenticated(net, addr, passwd) if err != nil { return } w = &Watcher{ conn: conn, Event: make(chan string), Error: make(chan error), done: make(chan bool), // Buffer channels to avoid race conditions with noIdle names: make(chan []string, 1), exit: make(chan bool, 1), } go w.watch(names...) return } func (w *Watcher) watch(names ...string) { defer w.closeChans() // We can block in two places: idle and sending on Event/Error channels. // We need to check w.exit and w.names after each. for { changed, err := w.conn.idle(names...) select { case <-w.exit: // If Close interrupted idle with a noidle, and we don't // exit now, we will block trying to send on Event/Error. return case names = <-w.names: // Received new subsystems to watch. Ignore results. changed = []string{} err = nil default: // continue } switch { case err != nil: w.Error <- err default: for _, name := range changed { w.Event <- name } } select { case <-w.exit: // If Close unblocks us from sending on Event/Error channels, // we should exit now because noidle might be sent out // before we get to idle. return case names = <-w.names: // If method Subsystems unblocks us from sending on Event/Error // channels, the next call to idle should be on the new names. default: // continue } } } func (w *Watcher) closeChans() { close(w.Event) close(w.Error) close(w.names) close(w.exit) close(w.done) } func (w *Watcher) consume() { for { select { case <-w.Event: case <-w.Error: default: return } } } // Subsystems interrupts watching current subsystems, consumes all // outstanding values from Event and Error channels, and then // changes the subsystems to watch for to names. func (w *Watcher) Subsystems(names ...string) { w.names <- names w.consume() w.conn.noIdle() } // Close closes Event and Error channels, and the connection to MPD server. func (w *Watcher) Close() error { w.exit <- true w.consume() w.conn.noIdle() <-w.done // wait for idle to finish and channels to close // At this point, watch goroutine has ended, // so it's safe to close connection. return w.conn.Close() } gompd-2.3.0/mpd/watcher_test.go000066400000000000000000000042711432532623500164360ustar00rootroot00000000000000// Copyright 2013 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 import ( "testing" "time" ) func localWatch(t *testing.T, names ...string) *Watcher { net, addr := localAddr() w, err := NewWatcher(net, addr, "", names...) if err != nil { t.Fatalf("NewWatcher(%q) = %v, %s want PTR, nil", addr, w, err) } return w } func loadTestFiles(t *testing.T, cli *Client, n int) (ok bool) { if err := cli.Clear(); err != nil { t.Fatalf("Client.Clear failed: %s\n", err) } files, err := cli.GetFiles() if err != nil { t.Fatalf("Client.GetFiles failed: %s\n", err) } if len(files) < n { t.Log("Add files to your MPD to run this test.") return } for i := 0; i < n; i++ { if err = cli.Add(files[i]); err != nil { t.Fatalf("Client.Add failed: %s\n", err) } } return true } func TestWatcher(t *testing.T) { t.Skipf("skipping racy test. See https://github.com/fhs/gompd/issues/52") c := localDial(t) defer teardown(c, t) if !loadTestFiles(t, c, 10) { return } w := localWatch(t, "player") defer w.Close() // Give the watcher a chance. <-time.After(time.Second) if err := c.Play(-1); err != nil { // player change t.Fatalf("Client.Play failed: %s\n", err) } if err := c.Next(); err != nil { // player change t.Fatalf("Client.Next failed: %s\n", err) } if err := c.Previous(); err != nil { // player change t.Fatalf("Client.Previous failed: %s\n", err) } select { case subsystem := <-w.Event: if subsystem != "player" { t.Fatalf("Unexpected result: %q != \"player\"\n", subsystem) } case err := <-w.Error: t.Fatalf("Client.idle failed: %s\n", err) } w.Subsystems("options", "playlist") <-time.After(time.Second) // Give the watcher a chance. if err := c.Stop(); err != nil { // player change t.Fatalf("Client.Stop failed: %s\n", err) } if err := c.Delete(5, -1); err != nil { // playlist change t.Fatalf("Client.Delete failed: %s\n", err) } select { case subsystem := <-w.Event: if subsystem != "playlist" { t.Fatalf("Unexpected result: %q != \"playlist\"\n", subsystem) } case err := <-w.Error: t.Fatalf("Client.idle failed: %s\n", err) } }