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); $response = $this->Connect(); if (!$response) { $this->_error('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 , 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 , 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 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 , 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 to the end of the playlist. 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 to 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 .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 .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 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 * 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 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 argument, given in seconds, is the track position * to locate. The argument, if supplied, is the track number in * the playlist. If 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 should be one of the * following: * self::SEARCH_ARTIST, self::SEARCH_TITLE, self::SEARCH_ALBUM * The search is a case-insensitive locator string. Anything * that contains 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 * should be one of the following: * self::SEARCH_ARTIST, self::SEARCH_TITLE, self::SEARCH_ALBUM * The find is a case-insensitive locator string. Anything that * exactly matches 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 ?>