<?php
/* vim:set softtabstop=4 shiftwidth=4 expandtab: */
/*
 *  mpd.class.php - PHP Object Interface to the MPD Music Player Daemon
 *  Version 1.3
 *
 *  Copyright (C) 2003-2004  Benjamin Carlisle (bcarlisle@24oz.com)
 *  Copyright 2010 Paul Arthur MacIain
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

class mpd
{
    // Command names
    // Status queries
    const COMMAND_CLEARERROR  = 'clearerror';
    const COMMAND_CURRENTSONG = 'currentsong';
    const COMMAND_IDLE        = 'idle';
    const COMMAND_STATUS      = 'status';
    const COMMAND_STATISTICS  = 'stats';

    // Playback options
    const COMMAND_CONSUME            = 'consume';
    const COMMAND_CROSSFADE          = 'crossfade';
    const COMMAND_RANDOM             = 'random';
    const COMMAND_REPEAT             = 'repeat';
    const COMMAND_SETVOL             = 'setvol';
    const COMMAND_SINGLE             = 'single';
    const COMMAND_REPLAY_GAIN_MODE   = 'replay_gain_mode';
    const COMMAND_REPLAY_GAIN_STATUS = 'replay_gain_status';

    // Playback control
    const COMMAND_NEXT     = 'next';
    const COMMAND_PAUSE    = 'pause';
    const COMMAND_PLAY     = 'play';
    const COMMAND_PLAYID   = 'playid';
    const COMMAND_PREVIOUS = 'previous';
    const COMMAND_SEEK     = 'seek';
    const COMMAND_SEEKID   = 'seekid';
    const COMMAND_STOP     = 'stop';

    // Current playlist control
    const COMMAND_ADD            = 'add';
    const COMMAND_ADDID          = 'addid';
    const COMMAND_CLEAR          = 'clear';
    const COMMAND_DELETE         = 'delete';
    const COMMAND_DELETEID       = 'deleteid';
    const COMMAND_MOVETRACK      = 'move';
    const COMMAND_MOVEID         = 'moveid';
    const COMMAND_PLFIND         = 'playlistfind';
    const COMMAND_PLID           = 'playlistid';
    const COMMAND_PLINFO         = 'playlistinfo';
    const COMMAND_PLSEARCH       = 'playlistsearch';
    const COMMAND_PLCHANGES      = 'plchanges';
    const COMMAND_PLCHANGESPOSID = 'plchangesposid';
    const COMMAND_PLSHUFFLE      = 'shuffle';
    const COMMAND_PLSWAPTRACK    = 'swap';
    const COMMAND_PLSWAPID       = 'swapid';

    // Stored playlists
    const COMMAND_LISTPL        = 'listplaylist';
    const COMMAND_LISTPLINFO    = 'listplaylistinfo';
    const COMMAND_LISTPLAYLISTS = 'listplaylists';
    const COMMAND_PLLOAD        = 'load';
    const COMMAND_PLADD         = 'playlistadd';
    const COMMAND_PLCLEAR       = 'playlistclear';
    const COMMAND_PLDELETE      = 'playlistdelete';
    const COMMAND_PLMOVE        = 'playlistmove';
    const COMMAND_RENAME        = 'rename';
    const COMMAND_RM            = 'rm';
    const COMMAND_PLSAVE        = 'save';

    // Music database
    const COMMAND_COUNT       = 'count';
    const COMMAND_FIND        = 'find';
    const COMMAND_FINDADD     = 'findadd';
    const COMMAND_TABLE       = 'list';
    const COMMAND_LISTALL     = 'listall';
    const COMMAND_LISTALLINFO = 'listallinfo';
    const COMMAND_LSDIR       = 'lsinfo';
    const COMMAND_SEARCH      = 'search';
    const COMMAND_REFRESH     = 'update';
    const COMMAND_RESCAN      = 'rescan';

    // Stickers
    const COMMAND_STICKER = 'sticker';
    const STICKER_GET     = 'get';
    const STICKER_SET     = 'set';
    const STICKER_DELETE  = 'delete';
    const STICKER_LIST    = 'list';
    const STICKER_FIND    = 'find';

    // Connection
    const COMMAND_CLOSE    = 'close';
    const COMMAND_KILL     = 'kill';
    const COMMAND_PASSWORD = 'password';
    const COMMAND_PING     = 'ping';

    // Deprecated commands
    const COMMAND_VOLUME      = 'volume';

    // Bulk commands
    const COMMAND_START_BULK  = 'command_list_begin';
    const COMMAND_END_BULK    = 'command_list_end';

    // Predefined MPD Response messages
    const RESPONSE_ERR = 'ACK';
    const RESPONSE_OK  = 'OK';

    // MPD State Constants
    const STATE_PLAYING = 'play';
    const STATE_STOPPED = 'stop';
    const STATE_PAUSED  = 'pause';

    // MPD Searching Constants
    const SEARCH_ARTIST = 'artist';
    const SEARCH_TITLE  = 'title';
    const SEARCH_ALBUM  = 'album';

    // MPD Cache Tables
    const TABLE_ARTIST = 'artist';
    const TABLE_ALBUM  = 'album';

    // Table holding version compatibility information
    private static $_COMPATIBILITY_TABLE = array(
    self::COMMAND_CONSUME => array('min' => '0.15.0', 'max' => false),
    self::COMMAND_IDLE => array('min' => '0.14.0', 'max' => false),
    self::COMMAND_PASSWORD => array('min' => '0.10.0', 'max' => false),
    self::COMMAND_MOVETRACK => array('min' => '0.9.1',  'max' => false),
    self::COMMAND_PLSWAPTRACK => array('min' => '0.9.1',  'max' => false),
    self::COMMAND_RANDOM => array('min' => '0.9.1',  'max' => false),
    self::COMMAND_SEEK => array('min' => '0.9.1',  'max' => false),
    self::COMMAND_SETVOL => array('min' => '0.10.0', 'max' => false),
    self::COMMAND_SINGLE => array('min' => '0.15.0', 'max' => false),
    self::COMMAND_STICKER => array('min' => '0.15.0', 'max' => false),
    self::COMMAND_VOLUME => array('min' => false,    'max' => '0.10.0')
    );

    // TCP/Connection variables
    private $host;
    private $port;
    private $password;

    private $_mpd_sock = null;
    public $connected  = false;

    // MPD Status variables
    public $mpd_version = "(unknown)";

    public $stats;
    public $status;
    public $playlist;

    // Misc Other Vars
    public $mpd_class_version = '1.3';

    public $err_str = ''; // Stores the latest error message
    private $_command_queue; // The list of commands for bulk command sending

    private $_debug_callback = null; // Optional callback to be run on debug
    public $debugging        = false;

    /* Constructor
     * Builds the MPD object, connects to the server, and refreshes all
     * local object properties.
     */
    public function __construct($server, $port, $password = null, $debug_callback = null)
    {
        $this->host     = $server;
        $this->port     = $port;
        $this->password = $password;

        if (is_callable($debug_callback)) {
            $this->_debug_callback = $debug_callback;
        }

        $this->_debug('construct', 'constructor called', 5);
        
        if (empty($this->host)) {
            $this->_error('construct', 'Host is empty');

            return false;
        }

        $response = $this->Connect();
        if (!$response) {
            $this->_error('construct', 'Could not connect');

            return false;
        }

        $version           = sscanf($response, self::RESPONSE_OK . " MPD %s\n");
        $this->mpd_version = $version[0];

        if ($password) {
            if (!$this->SendCommand(self::COMMAND_PASSWORD, $password, false)) {
                $this->connected = false;
                $this->_error('construct', 'Password supplied is incorrect or Invalid Command');

                return false;  // bad password or command
            }
        } // if password
        else {
            if (!$this->RefreshInfo()) {
                // no read access, might as well be disconnected
                $this->connected = false;
                if ($password) {
                    $this->_error('construct', 'Password supplied does not have read access');
                } else {
                    $this->_error('construct', 'Password required to access server');
                }

                return false;
            }
        }

        return true;
    } // constructor

    /* Connect
     *
     * Connects to the MPD server.
     *
     * NOTE: This is called automatically upon object instantiation; you
     * should not need to call this directly.
     */
    public function Connect()
    {
        $this->_debug('Connect', "host: " . $this->host . ", port: " . $this->port, 5);
        $this->_mpd_sock = fsockopen($this->host, $this->port, $err, $err_str, 6);
        // Vollmerize this bizatch
        /* Set the timeout on the connection */
        stream_set_timeout($this->_mpd_sock, 6);

        /* We want blocking, cause otherwise it doesn't
         * timeout, and feof just keeps on spinning
         */
        stream_set_blocking($this->_mpd_sock, true);
        $status = socket_get_status($this->_mpd_sock);

        if (!$this->_mpd_sock) {
            $this->_error('Connect', "Socket Error: $err_str ($err)");

            return false;
        } else {
            while (!feof($this->_mpd_sock) && !$status['timed_out']) {
                $response =  fgets($this->_mpd_sock, 1024);
                if (function_exists('socket_get_status')) {
                    $status = socket_get_status($this->_mpd_sock);
                }
                if (strncmp(self::RESPONSE_OK, $response, strlen(self::RESPONSE_OK)) == 0) {
                    $this->connected = true;

                    return $response;
                    break;
                }
                if (strncmp(self::RESPONSE_ERR, $response, strlen(self::RESPONSE_ERR)) == 0) {
                    $this->_error('Connect', "Server responded with: $response");

                    return false;
                }
            } // end while
            // Generic response
            $this->_error('Connect', "Connection not available");

            return false;
        }
    } // connect

    /* SendCommand
     *
     * Sends a generic command to the MPD server. Several command constants
     * are pre-defined for use (see self::COMMAND_* constant definitions
     * above).
     */
    public function SendCommand($command, $arguments = null, $refresh_info = true)
    {
        $this->_debug('SendCommand', "cmd: $command, args: " . json_encode($arguments), 5);
        if (! $this->connected) {
            $this->_error('SendCommand', 'Not connected', 1);

            return false;
        } else {
            $response_string = '';

            // Check the command compatibility:
            if (! self::_checkCompatibility($command, $this->mpd_version)) {
                return false;
            }

            if (isset($arguments)) {
                if (is_array($arguments)) {
                    foreach ($arguments as $arg) {
                        $command .= ' "' . $arg . '"';
                    }
                } else {
                    $command .= ' "' . $arguments . '"';
                }
            }

            fputs($this->_mpd_sock, "$command\n");
            while (!feof($this->_mpd_sock)) {
                $response = fgets($this->_mpd_sock, 1024);

                // An OK signals the end of transmission
                if (strncmp(self::RESPONSE_OK, $response, strlen(self::RESPONSE_OK)) == 0) {
                    break;
                }

                // An ERR signals an error!
                if (strncmp(self::RESPONSE_ERR, $response, strlen(self::RESPONSE_ERR)) == 0) {
                    $this->_error('SendCommand', "MPD Error: $response");

                    return false;
                }

                // Build the response string
                $response_string .= $response;
            }
            $this->_debug('SendCommand', "response: $response_string", 5);
        }

        if ($refresh_info) {
            $this->RefreshInfo();
        }

        return $response_string ? $response_string : true;
    }

    /* QueueCommand
     *
     * Queues a generic command for later sending to the MPD server. The
     * CommandQueue can hold as many commands as needed, and are sent all
     * at once, in the order they were queued, using the SendCommandQueue
     * method. The syntax for queueing commands is identical to SendCommand.
     */
    public function QueueCommand($command, $arguments = '')
    {
        $this->_debug('QueueCommand', "start; cmd: $command args: " . json_encode($arguments), 5);
        if (! $this->connected) {
            $this->_error('QueueCommand', 'Not connected');

            return false;
        }

        if (!$this->_command_queue) {
            $this->_command_queue = self::COMMAND_START_BULK . "\n";
        }

        if ($arguments) {
            if (is_array($arguments)) {
                foreach ($arguments as $arg) {
                    $command .= ' "' . $arg . '"';
                }
            } else {
                $command .= ' "' . $arguments . '"';
            }
        }

        $this->_command_queue .= $command . "\n";

        $this->_debug('QueueCommand', 'return', 5);

        return true;
    }

    /* SendCommandQueue
     *
     * Sends all commands in the Command Queue to the MPD server.
     */
    public function SendCommandQueue()
    {
        $this->_debug('SendCommandQueue', 'start', 5);
        if (! $this->connected) {
            _error('SendCommandQueue', 'Not connected');

            return false;
        }

        $this->_command_queue .= self::COMMAND_END_BULK . "\n";
        $response = $this->SendCommand($this->_command_queue);

        if ($response) {
            $this->_command_queue = null;
        }

        $this->_debug('SendCommandQueue', "response: $response", 5);

        return $response;
    }

    /* RefreshInfo
     *
     * Updates all class properties with the values from the MPD server.
     * NOTE: This function is automatically called on Connect()
     */
    public function RefreshInfo()
    {
        $stats  = $this->SendCommand(self::COMMAND_STATISTICS, null, false);
        $status = $this->SendCommand(self::COMMAND_STATUS, null, false);


        if (!$stats || !$status) {
            return false;
        }

        $stats  = self::_parseResponse($stats);
        $status = self::_parseResponse($status);

        $this->stats  = $stats;
        $this->status = $status;

        // Get the Playlist
        $playlist       = $this->SendCommand(self::COMMAND_PLINFO, null, false);
        $this->playlist = self::_parseFileListResponse($playlist);

        return true;
    }

    /* AdjustVolume
     *
     * Adjusts the mixer volume on the MPD by <value>, which can be a
     * positive (volume increase) or negative (volume decrease) value.
     */
    public function AdjustVolume($value)
    {
        $this->_debug('AdjustVolume', 'start', 5);
        if (! is_numeric($value)) {
            $this->_error('AdjustVolume', "argument must be numeric: $value");

            return false;
        }

        $this->RefreshInfo();
        $value    = $this->status['volume'] + $value;
        $response = $this->SetVolume($value);

        $this->_debug('AdjustVolume', "return $response", 5);

        return $response;
    }

    /* SetVolume
     *
     * Sets the mixer volume to <value>, which should be between 1 - 100.
     */
    public function SetVolume($value)
    {
        $this->_debug('SetVolume', 'start', 5);
        if (!is_numeric($value)) {
            $this->_error('SetVolume', "argument must be numeric: $value");

            return false;
        }

        // Forcibly prevent out of range errors
        $value = $value > 0   ? $value : 0;
        $value = $value < 100 ? $value : 100;

        // If we're not compatible with SETVOL, we'll try adjusting
        // using VOLUME
        if (self::_checkCompatibility(self::COMMAND_SETVOL, $this->mpd_version)) {
            $command = self::COMMAND_SETVOL;
        } else {
            $this->RefreshInfo(); // Get the latest volume
            if (is_null($this->status['volume'])) {
                return false;
            } else {
                $command = self::COMMAND_VOLUME;
                $value   = $value - $this->status['volume'];
            }
        }

        $response = $this->SendCommand($command, $value);

        $this->_debug('SetVolume', "return: $response", 5);

        return $response;
    }

    /* GetDir
     *
     * Retrieves a database directory listing of the <dir> directory and
     * places the results into a multidimensional array. If no directory is
     * specified the directory listing is at the base of the MPD music path.
     */
    public function GetDir($dir = '')
    {
        $this->_debug('GetDir', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_LSDIR, $dir, false);
        $dirlist  = self::_parseFileListResponse($response);
        $this->_debug('GetDir', 'return: ' . json_encode($dirlist), 5);

        return $dirlist;
    }

    /* PLAdd
     *
     * Adds each track listed in a single-dimensional <trackArray>, which
     * contains filenames of tracks to add to the end of the playlist. This
     * is used to add many, many tracks to the playlist in one swoop.
     */
    public function PLAddBulk($trackArray)
    {
        $this->_debug('PLAddBulk', 'start', 5);
        $num_files = count($trackArray);
        for ($i = 0; $i < $num_files; $i++) {
            $this->QueueCommand(self::COMMAND_ADD, $trackArray[$i]);
        }
        $response = $this->SendCommandQueue();
        $this->_debug('PLAddBulk', "return: $response", 5);

        return $response;
    }

    /* PLAdd
     *
     * Adds the file <file> to the end of the playlist. <file> must be a
     * track in the MPD database.
     */
    public function PLAdd($filename)
    {
        $this->_debug('PLAdd', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_ADD, $filename);
        $this->_debug('PLAdd', "return: $response", 5);

        return $response;
    }

    /* PLMoveTrack
     *
     * Moves track number <current_position> to position <new_position> in
     * the playlist. This is used to reorder the songs in the playlist.
     */
    public function PLMoveTrack($current_position, $new_position)
    {
        $this->_debug('PLMoveTrack', 'start', 5);
        if (!is_numeric($current_position)) {
            $this->_error('PLMoveTrack', "current_position must be numeric: $current_position");

            return false;
        }
        if ($current_position < 0 || $current_position > count($this->playlist)) {
            $this->_error('PLMoveTrack', "current_position out of range");

            return false;
        }
        $new_position = $new_position > 0 ? $new_position : 0;
        $new_position = $new_position < count($this->playlist)
            ? $new_position
            : count($this->playlist);

        $response = $this->SendCommand(self::COMMAND_MOVETRACK, array($current_position, $new_position));

        $this->_debug('PLMoveTrack', "return: $response", 5);

        return $response;
    }

    /* PLShuffle
     *
     * Randomly reorders the songs in the playlist.
     */
    public function PLShuffle()
    {
        $this->_debug('PLShuffle', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_PLSHUFFLE);
        $this->_debug('PLShuffle', "return: $response", 5);

        return $response;
    }

    /* PLLoad
     *
     * Retrieves the playlist from <file>.m3u and loads it into the current
     * playlist.
     */
    public function PLLoad($file)
    {
        $this->_debug('PLLoad', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_PLLOAD, $file);
        $this->_debug('PLLoad', "return: $response", 5);

        return $response;
    }

    /* PLSave
     *
     * Saves the playlist to <file>.m3u for later retrieval. The file is
     * saved in the MPD playlist directory.
     */
    public function PLSave($file)
    {
        $this->_debug('PLSave', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_PLSAVE, $file, false);
        $this->_debug('PLSave', "return: $response", 5);

        return $response;
    }

    /* PLClear
     *
     * Empties the playlist.
     */
    public function PLClear()
    {
        $this->_debug('PLClear', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_CLEAR);
        $this->_debug('PLClear', "return: $response", 5);

        return $response;
    }

    /* PLRemove
     *
     * Removes track <id> from the playlist.
     */
    public function PLRemove($id)
    {
        if (! is_numeric($id)) {
            $this->_error('PLRemove', "id must be numeric: $id");

            return false;
        }
        $response = $this->SendCommand(self::COMMAND_DELETE, $id);
        $this->_debug('PLRemove', "return: $response", 5);

        return $response;
    } // PLRemove

    /* SetRepeat
     *
     * Enables 'loop' mode -- tells MPD continually loop the playlist. The
     * <repVal> parameter is either 1 (on) or 0 (off).
     */
    public function SetRepeat($value)
    {
        $this->_debug('SetRepeat', 'start', 5);
        $value    = $value ? 1 : 0;
        $response = $this->SendCommand(self::COMMAND_REPEAT, $value);
        $this->_debug('SetRepeat', "return: $response", 5);

        return $response;
    }

    /* SetRandom
     *
     * Enables 'randomize' mode -- tells MPD to play songs in the playlist
     * in random order. The parameter is either 1 (on) or 0 (off).
     */
    public function SetRandom($value)
    {
        $this->_debug('SetRandom', 'start', 5);
        $value    = $value ? 1 : 0;
        $response = $this->SendCommand(self::COMMAND_RANDOM, $value);
        $this->_debug('SetRandom', "return: $response", 5);

        return $response;
    }

    /* Shutdown
     *
     * Shuts down the MPD server (aka sends the KILL command). This closes
     * the current connection and prevents future communication with the
     * server.
     */
    public function Shutdown()
    {
        $this->_debug('Shutdown', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_SHUTDOWN);

        $this->connected = false;
        unset($this->mpd_version);
        unset($this->err_str);
        unset($this->_mpd_sock);

        $this->_debug('Shutdown', "return: $response", 5);

        return $response;
    }

    /* DBRefresh
     *
     * Tells MPD to rescan the music directory for new tracks and refresh
     * the Database. Tracks cannot be played unless they are in the MPD
     * database.
     */
    public function DBRefresh()
    {
        $this->_debug('DBRefresh', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_REFRESH);
        $this->_debug('DBRefresh', "return: $response", 5);

        return $response;
    }

    /* Play
     *
     * Begins playing the songs in the MPD playlist.
     */
    public function Play()
    {
        $this->_debug('Play', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_PLAY);
        $this->_debug('Play', "return: $response", 5);

        return $response;
    }

    /* Stop
     *
     * Stops playback.
     */
    public function Stop()
    {
        $this->_debug('Stop', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_STOP);
        $this->_debug('Stop', "return: $response", 5);

        return $response;
    }

    /* Pause
     *
     * Toggles pausing.
     */
    public function Pause()
    {
        $this->_debug('Pause', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_PAUSE);
        $this->_debug('Pause', "return: $response", 5);

        return $response;
    }

    /* SeekTo
     *
     * Skips directly to the <idx> song in the MPD playlist.
     */
    public function SkipTo($idx)
    {
        $this->_debug('SkipTo', 'start', 5);
        if (! is_numeric($idx)) {
            $this->_error('SkipTo', "argument must be numeric: $idx");

            return false;
        }
        $response = $this->SendCommand(self::COMMAND_PLAY, $idx);
        $this->_debug('SkipTo', "return: $idx", 5);

        return $idx;
    }

    /* SeekTo
     *
     * Skips directly to a given position within a track in the MPD
     * playlist. The <pos> argument, given in seconds, is the track position
     * to locate. The <track> argument, if supplied, is the track number in
     * the playlist. If <track> is not specified, the current track is
     * assumed.
     */
    public function SeekTo($pos, $track = -1)
    {
        $this->_debug('SeekTo', 'start', 5);
        if (! is_numeric($pos)) {
            $this->_error('SeekTo', "pos must be numeric: $pos");

            return false;
        }
        if (! is_numeric($track)) {
            $this->_error('SeekTo', "track must be numeric: $track");

            return false;
        }
        if ($track == -1) {
            $track = $this->current_track_id;
        }

        $response = $this->SendCommand(self::COMMAND_SEEK, array($track, $pos));
        $this->_debug('SeekTo', "return: $pos", 5);

        return $pos;
    }

    /* Next
     *
     * Skips to the next song in the MPD playlist. If not playing, returns
     * an error.
     */
    public function Next()
    {
        $this->_debug('Next', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_NEXT);
        $this->_debug('Next', "return: $response", 5);

        return $response;
    }

    /* Previous
     *
     * Skips to the previous song in the MPD playlist. If not playing,
     * returns an error.
     */
    public function Previous()
    {
        $this->_debug('Previous', 'start', 5);
        $response = $this->SendCommand(self::COMMAND_PREVIOUS);
        $this->_debug('Previous', "return: $response", 5);

        return $response;
    }

    /* Search
     *
     * Searches the MPD database. The search <type> should be one of the
     * following:
     *     self::SEARCH_ARTIST, self::SEARCH_TITLE, self::SEARCH_ALBUM
     * The search <string> is a case-insensitive locator string. Anything
     * that contains <string> will be returned in the results.
     */
    public function Search($type, $string)
    {
        $this->_debug('Search', 'start', 5);

        if ($type != self::SEARCH_ARTIST &&
            $type != self::SEARCH_ALBUM &&
            $type != self::SEARCH_TITLE) {
            $this->_error('Search', 'invalid search type');

            return false;
        }

        $response = $this->SendCommand(self::COMMAND_SEARCH, array($type, $string), false);

        $results = false;

        if ($response) {
            $results = self::_parseFileListResponse($response);
        }
        $this->_debug('Search', 'return: ' . json_encode($results), 5);

        return $results;
    }

    /* Find
     *
     * Find looks for exact matches in the MPD database. The find <type>
     * should be one of the following:
     *    self::SEARCH_ARTIST, self::SEARCH_TITLE, self::SEARCH_ALBUM
     * The find <string> is a case-insensitive locator string. Anything that
     * exactly matches <string> will be returned in the results.
     */
    public function Find($type, $string)
    {
        $this->_debug('Find', 'start', 5);
        if ($type != self::SEARCH_ARTIST &&
            $type != self::SEARCH_ALBUM &&
            $type != self::SEARCH_TITLE) {
            $this->_error('Find', 'invalid find type');

            return false;
        }

        $response = $this->SendCommand(self::COMMAND_FIND, array($type, $string), false);

        $results = false;

        if ($response) {
            $results = self::_parseFileListResponse($response);
        }

        $this->_debug('Find', 'return: ' . json_encode($results), 5);

        return $results;
    }

    /* Disconnect
     *
     * Closes the connection to the MPD server.
     */
    public function Disconnect()
    {
        $this->_debug('Disconnect', 'start', 5);
        fclose($this->_mpd_sock);

        $this->connected = false;
        unset($this->mpd_version);
        unset($this->err_str);
        unset($this->_mpd_sock);
    }

    /* GetArtists
     *
     * Returns the list of artists in the database in an associative array.
     */
    public function GetArtists()
    {
        $this->_debug('GetArtists', 'start', 5);
        if (!$response = $this->SendCommand(self::COMMAND_TABLE, self::TABLE_ARTIST, false)) {
            return false;
        }
        $results = array();

        $parsed = self::_parseResponse($response);

        foreach ($parsed as $key => $value) {
            if ($key == 'Artist') {
                $results[] = $value;
            }
        }

        $this->_debug('GetArtists', 'return: ' . json_encode($results), 5);

        return $results;
    }

    /* GetAlbums
     *
     * Returns the list of albums in the database in an associative array.
     * Optional parameter is an artist Name which will list all albums by a
     * particular artist.
     */
    public function GetAlbums($artist = null)
    {
        $this->_debug('GetAlbums', 'start', 5);

        $params[] = self::TABLE_ALBUM;
        if (!is_null($artist)) {
            $params[] = $artist;
        }

        if (!$response = $this->SendCommand(self::COMMAND_TABLE, $params, false)) {
            return false;
        }

        $results = array();
        $parsed  = self::_parseResponse($response);

        foreach ($parsed as $key => $value) {
            if ($key == 'Album') {
                $results[] = $value;
            }
        }

        $this->_debug('GetAlbums', 'return: ' . json_encode($results), 5);

        return $results;
    }

    /* _computeVersionValue
     *
     * Computes numeric value from a version string
     *
     */
    private static function _computeVersionValue($string)
    {
        $parts = explode('.', $string);

        return (100 * $parts[0]) + (10 * $parts[1]) + $parts[2];
    }

    /* _checkCompatibility
     *
     * Check MPD command compatibility against our internal table of
     * incompatibilities.
     */
    private static function _checkCompatibility($cmd, $mpd_version)
    {
        $mpd = self::_computeVersionValue($mpd_version);

        if (isset(self::$_COMPATIBILITY_TABLE[$cmd])) {
            $min_version = self::$_COMPATIBILITY_TABLE[$cmd]['min'];
            $max_version = self::$_COMPATIBILITY_TABLE[$cmd]['max'];

            if ($min_version) {
                $min = self::_computeVersionValue($min_version);
                if ($mpd < $min) {
                    $this->_error('compatibility', "Command '$cmd' is not compatible with this version of MPD, version $min_version required");

                    return false;
                }
            }

            if ($max_version) {
                $max = self::_computeVersionValue($max_version);

                if ($mpd >= $max) {
                    $this->_error('compatibility', "Command '$cmd' has been deprecated in this version of MPD.  Last compatible version: $max_version");

                    return false;
                }
            }
        }

        return true;
    }

    /* _parseFileListResponse
     *
     * Builds a multidimensional array with MPD response lists.
     */
    private static function _parseFileListResponse($response)
    {
        if (!$response) {
            return false;
        }

        $results = array();
        $counter = -1;
        $lines   = explode("\n", $response);
        foreach ($lines as $line) {
            if (preg_match('/(\w+): (.+)/', $line, $matches)) {
                if ($matches[1] == 'file') {
                    $counter++;
                }
                $results[$counter][$matches[1]] = $matches[2];
            }
        }

        return $results;
    }

    /* _parseResponse
     * Turns a response into an array
     */
    private static function _parseResponse($response)
    {
        if (!$response) {
            return false;
        }

        $results = array();
        $lines   = explode("\n", $response);
        foreach ($lines as $line) {
            if (preg_match('/(\w+): (.+)/', $line, $matches)) {
                $results[$matches[1]] = $matches[2];
            }
        }

        return $results;
    }

    /* _error
     *
     * Set error state
     */
    private function _error($source, $message)
    {
        $this->err_str = "$source: $message";
        $this->_debug($source, $message, 1);
    }

    /* _debug
     *
     * Do the debugging boogaloo
     */
    private function _debug($source, $message, $level)
    {
        if ($this->debugging) {
            echo "$source / $message\n";
        }

        if (!is_null($this->_debug_callback)) {
            call_user_func($this->_debug_callback, 'MPD', "$source / $message", $level);
        }
    }
}   // end class mpd
;
