ip = $ip; } else { $this->ip = $prefs['multihosts']->{$prefs['currenthost']}->host; } if ($port !== null) { $this->port = $port; } else { $this->port = $prefs['multihosts']->{$prefs['currenthost']}->port; } if ($socket !== null) { $this->socket = $socket; } else { $this->socket = $prefs['multihosts']->{$prefs['currenthost']}->socket; } if ($password !== null) { $this->password = $password; } else { $this->password = $prefs['multihosts']->{$prefs['currenthost']}->password; } if ($is_slave !== null) { $this->is_slave = $is_slave; } else { if (property_exists($prefs['multihosts']->{$prefs['currenthost']}, 'mopidy_slave')) { $this->is_slave = $prefs['multihosts']->{$prefs['currenthost']}->mopidy_slave; } else { // Catch the case where we haven't yet upgraded the player defs $this->is_slave = false; } } logger::trace("MPDPLAYER", "Creating Player for",$this->ip.':'.$this->port); $this->open_mpd_connection(); if ($player_type !== null) { $this->player_type = $player_type; } else { if (array_key_exists('player_backend', $prefs) && $prefs['player_backend'] !== 'none') { $this->player_type = $prefs['player_backend']; } else { $this->player_type = $this->probe_player_type(); } } } public function __destruct() { if ($this->is_connected()) { $this->close_mpd_connection(); } } public function open_mpd_connection() { if ($this->is_connected()) { return true; } if ($this->socket != "") { $this->connection = @stream_socket_client('unix://'.$this->socket); } else { $this->connection = @stream_socket_client('tcp://'.$this->ip.':'.$this->port); } if($this->is_connected()) { stream_set_timeout($this->connection, 65535); stream_set_blocking($this->connection, true); while(!feof($this->connection)) { $gt = fgets($this->connection, 1024); if ($this->parse_mpd_var($gt)) break; } } if ($this->password != "" && $this->is_connected()) { fputs($this->connection, "password ".$this->password."\n"); while(!feof($this->connection)) { $gt = fgets($this->connection, 1024); $a = $this->parse_mpd_var($gt); if($a === true) { $is_connected = true; break; } else if ($a == null) { } else { $this->close_mpd_connection(); return false; } } } return true; } public function close_mpd_connection() { if ($this->is_connected()) { stream_socket_shutdown($this->connection, STREAM_SHUT_RDWR); } } public function is_connected() { return (isset($this->connection) && is_resource($this->connection)); } private function parse_mpd_var($in_str) { $got = trim($in_str); if(!isset($got)) return null; if(strncmp("OK", $got, 2) == 0) return true; if(strncmp("ACK", $got, 3) == 0) { return array(0 => false, 1 => $got); } $key = trim(strtok($got, ":")); $val = trim(strtok("\0")); return array(0 => $key, 1 => $val); } protected function getline() { $got = fgets($this->connection, 2048); $key = trim(strtok($got, ":")); $val = trim(strtok("\0")); if ($val != '') { return array($key, $val); } else if (strpos($got, "OK") === 0 || strpos($got, "ACK") === 0) { return false; } else { return true; } } protected function send_command($command) { $retries = 5; $l = strlen($command."\n"); do { $b = @fputs($this->connection, $command."\n"); if (!$b || $b < $l) { logger::warn("MPD", "Socket Write Error for",$command,"- Retrying"); $this->close_mpd_connection(); usleep(500000); $this->open_mpd_connection(); $retries--; } else { return true; } } while ($retries > 0); return false; } protected function do_mpd_command($command, $return_array = false, $force_array_results = false) { $retarr = array(); if ($this->is_connected()) { logger::debug("MPD", "MPD Command",$command); $success = true; if ($command != '') { $success = $this->send_command($command); } if ($success) { while(!feof($this->connection)) { $var = $this->parse_mpd_var(fgets($this->connection, 1024)); if(isset($var)){ if($var === true && count($retarr) == 0) { // Got an OK or ACK but - no results or return_array is false return true; } if ($var === true) { break; } if ($var[0] == false) { $sdata = stream_get_meta_data($this->connection); if (array_key_exists('timed_out', $sdata) && $sdata['timed_out']) { $var[1] = 'Timed Out'; } logger::warn("MPD", "Error for'",$command,"':",$var[1]); if ($return_array == true) { $retarr['error'] = $var[1]; } else { return false; } break; } if ($return_array == true) { if(array_key_exists($var[0], $retarr)) { if(is_array($retarr[($var[0])])) { $retarr[($var[0])][] = $var[1]; } else { $tmp = $retarr[($var[0])]; $retarr[($var[0])] = array($tmp, $var[1]); } } else { if ($force_array_results) { $retarr[($var[0])] = array($var[1]); } else { $retarr[($var[0])] = $var[1]; } } } } } } else { logger::error("MPD", "Failure to fput command",$command); $retarr['error'] = "There was an error communicating with ".ucfirst($this->player_type)."! (could not write to socket)"; } } return $retarr; } public function do_command_list($cmds) { global $prefs; $done = 0; $cmd_status = null; if ($this->is_slave) { $this->translate_commands_for_slave($cmds); } else if ($this->player_type != $prefs['collection_player']) { $this->translate_player_types($cmds); } if (count($cmds) > 1) { $this->send_command("command_list_begin"); foreach ($cmds as $c) { logger::trace("POSTCOMMAND", "Command List:",$c); // Note. We don't use send_command because that closes and re-opens the connection // if it fails to fputs, and that loses our command list status. Also if this fputs // fails it means the connection has dropped anyway, so we're screwed whatever happens. fputs($this->connection, $c."\n"); $done++; // Command lists have a maximum length, 50 seems to be the default if ($done == 50) { $this->do_mpd_command("command_list_end", true); $this->send_command("command_list_begin"); $done = 0; } } $cmd_status = $this->do_mpd_command("command_list_end", true, false); } else if (count($cmds) == 1) { logger::trace("POSTCOMMAND", "Command :",$cmds[0]); $cmd_status = $this->do_mpd_command($cmds[0], true, false); } return $cmd_status; } public function parse_list_output($command, &$dirs, $domains) { // Generator Function for parsing MPD output for 'list...info', 'search ...' etc type commands // Returns MPD_FILE_MODEL logger::trace("MPD", "MPD Parse",$command); $success = $this->send_command($command); $filedata = MPD_FILE_MODEL; $parts = true; if (is_array($domains) && count($domains) == 0) { $domains = false; } while( $this->is_connected() && !feof($this->connection) && $parts) { $parts = $this->getline(); if (is_array($parts)) { switch ($parts[0]) { case "directory": $dirs[] = trim($parts[1]); break; case "Last-Modified": if ($filedata['file'] != null) { // We don't want the Last-Modified stamps of the directories // to be used for the files. $filedata[$parts[0]] = $parts[1]; } break; case 'file': if ($filedata['file'] !== null) { $filedata['domain'] = getDomain($filedata['file']); if ($domains === false || in_array(getDomain($filedata['file']),$domains)) { if ($this->sanitize_data($filedata)) { yield $filedata; } } } $filedata = MPD_FILE_MODEL; $filedata[$parts[0]] = $parts[1]; break; case 'X-AlbumUri': // Mopidy-beets is using SEMICOLONS in its URI schemes. // Surely a typo, but we need to work around it by not splitting the string // Same applies to file. $filedata[$parts[0]] = $parts[1]; break; default: if (in_array($parts[0], MPD_ARRAY_PARAMS)) { $filedata[$parts[0]] = array_unique(explode(';',$parts[1])); } else { $filedata[$parts[0]] = explode(';',$parts[1])[0]; } break; } } } if ($filedata['file'] !== null) { $filedata['domain'] = getDomain($filedata['file']); if ($domains === false || in_array(getDomain($filedata['file']),$domains)) { if ($this->sanitize_data($filedata)) { yield $filedata; } } } } protected function sanitize_data(&$filedata) { global $dbterms, $numtracks, $totaltime; if ($dbterms['tags'] !== null || $dbterms['rating'] !== null) { // If this is a search and we have tags or ratings to search for, check them here. if (check_url_against_database($filedata['file'], $dbterms['tags'], $dbterms['rating']) == false) { return false; } } if (strpos($filedata['Title'], "[unplayable]") === 0) { logger::log("COLLECTION", "Ignoring unplayable track ".$filedata['file']); return false; } if (strpos($filedata['Title'], "[loading]") === 0) { logger::log("COLLECTION", "Ignoring unloaded track ".$filedata['file']); return false; } $filedata['unmopfile'] = $this->unmopify_file($filedata); if ($filedata['Track'] == 0) { $filedata['Track'] = format_tracknum(basename(rawurldecode($filedata['file']))); } else { $filedata['Track'] = format_tracknum(ltrim($filedata['Track'], '0')); } // cue sheet link (mpd only). We're only doing CUE sheets, not M3U if ($filedata['X-AlbumUri'] === null && strtolower(pathinfo($filedata['playlist'], PATHINFO_EXTENSION)) == "cue") { $filedata['X-AlbumUri'] = $filedata['playlist']; logger::mark("COLLECTION", "Found CUE sheet for album ".$filedata['Album']); } // Disc Number if ($filedata['Disc'] != null) { $filedata['Disc'] = format_tracknum(ltrim($filedata['Disc'], '0')); } $filedata['year'] = getYear($filedata['Date']); $this->player_specific_fixups($filedata); $numtracks++; $totaltime += $filedata['Time']; return true; } private function unmopify_file(&$filedata) { global $prefs; if ($filedata['Pos'] !== null) { // Convert URIs for different player types to be appropriate for the collection // but only when we're getting the playlist if ($this->is_slave && $filedata['domain'] == 'file') { $filedata['file'] = $this->swap_file_for_local($filedata['file']); $filedata['domain'] = 'local'; } if ($prefs['collection_player'] == 'mopidy' && $this->player_type == 'mpd') { $filedata['file'] = $this->mpd_to_mopidy($filedata['file']); } if ($prefs['collection_player'] == 'mpd' && $this->player_type == 'mopidy') { $filedata['file'] = $this->mopidy_to_mpd($filedata['file']); } } // eg local:track:some/uri/of/a/file // We want the path, not the domain or type // This is much faster than using a regexp $cock = explode(':', $filedata['file']); if (count($cock) > 1) { $file = array_pop($cock); } else { $file = $filedata['file']; } return $file; } private function album_from_path($p) { $a = rawurldecode(basename(dirname($p))); if ($a == ".") { $a = ''; } return $a; } private function artist_from_path($p, $f) { $a = rawurldecode(basename(dirname(dirname($p)))); if ($a == "." || $a == "" || $a == " & ") { $a = ucfirst(getDomain(urldecode($f))); } return $a; } protected function check_undefined_tags(&$filedata) { if ($filedata['Title'] == null) $filedata['Title'] = rawurldecode(basename($filedata['file'])); if ($filedata['Album'] == null) $filedata['Album'] = $this->album_from_path($filedata['unmopfile']); if ($filedata['Artist'] == null) $filedata['Artist'] = array($this->artist_from_path($filedata['unmopfile'], $filedata['file'])); } public function get_status() { return $this->do_mpd_command('status', true, false); } public function wait_for_state($expected_state) { if ($expected_state !== null) { $status = $this->get_status(); $retries = 20; while ($retries > 0 && array_key_exists('state', $status) && $status['state'] != $expected_state) { usleep(500000); $retries--; $status = $this->get_status(); } } } public function clear_error() { $this->send_command('clearerror'); } public function get_current_song() { return $this->do_mpd_command('currentsong', true, false); } public function get_config() { if ($this->socket != '' && $this->player_type == 'mpd') { return $this->do_mpd_command('config', true, false); } else { return array(); } } public function get_tagtypes() { return $this->do_mpd_command('tagtypes', true, false); } public function get_commands() { return $this->do_mpd_command('commands', true, false); } public function get_notcommands() { return $this->do_mpd_command('notcommands', true, false); } public function get_decoders() { return $this->do_mpd_command('decoders', true, false); } public function cancel_single_quietly() { $this->send_command('single 0'); } public function get_idle_status() { return $this->do_mpd_command('idle player', true, false); } public function dummy_command() { return $this->do_mpd_command('', true, false); } public function get_playlist(&$collection) { $dirs = array(); foreach ($this->parse_list_output('playlistinfo', $dirs, false) as $filedata) { // Check the database for extra track info $filedata = array_replace($filedata, get_extra_track_info($filedata)); yield $collection->doNewPlaylistFile($filedata); } } public function get_currentsong_as_playlist(&$collection) { $dirs = array(); $retval = array(); foreach ($this->parse_list_output('currentsong', $dirs, false) as $filedata) { // Check the database for extra track info $filedata = array_replace($filedata, get_extra_track_info($filedata)); $retval = $collection->doNewPlaylistFile($filedata); } return $retval; } public function populate_collection($cmd, $domains, &$collection) { $dirs = array(); foreach ($this->parse_list_output($cmd, $dirs, $domains) as $filedata) { $collection->newTrack($filedata); } } public function get_uris_for_directory($path) { logger::log("PLAYER", "Getting Directory Items For",$path); $items = array(); $parts = true; $lines = array(); $this->send_command('lsinfo "'.format_for_mpd($path).'"'); // We have to read in the entire response then go through it // because we only have the one connection to mpd so this function // is not strictly re-entrant and recursing doesn't work unless we do this. while(!feof($connection) && $parts) { $parts = $this->getline($connection); if ($parts === false) { logger::debug("PLAYER", "Got OK or ACK from MPD"); } else { $lines[] = $parts; } } foreach ($lines as $parts) { if (is_array($parts)) { $s = trim($parts[1]); if (substr($s,0,1) != ".") { switch ($parts[0]) { case "file": $items[] = $s; break; case "directory": $items = array_merge($items, $this->get_uris_for_directory($s)); break; } } } } return $items; } public function get_uri_handlers() { $handlers = $this->do_mpd_command('urlhandlers', true); if (is_array($handlers) && array_key_exists('handler', $handlers)) { return $handlers['handler']; } else { return array(); } } public function get_outputs() { return $this->do_mpd_command('outputs', true); } public function get_stored_playlists($only_personal = false) { global $PLAYER_TYPE; $this->playlist_error = false; $retval = array(); $playlists = $this->do_mpd_command('listplaylists', true, true); if (is_array($playlists) && array_key_exists('playlist', $playlists)) { $retval = $playlists['playlist']; usort($retval, 'sort_playlists'); if ($only_personal) { $retval = array_filter($retval, $PLAYER_TYPE.'::is_personal_playlist'); } } else if (is_array($playlists) && array_key_exists('error', $playlists)) { // We frequently get an error getting stored playlists - especially from mopidy // This flag is set so that loadplaylists.php doesn't remove all our stored playlist // images in the event of that happening. $this->playlist_error = true; } return $retval; } public function get_stored_playlist_tracks($playlistname, $startpos) { $dirs = array(); $count = 0; foreach ($this->parse_list_output('listplaylistinfo "'.$playlistname.'"', $dirs, false) as $filedata) { if ($count >= $startpos) { list($class, $url) = $this->get_checked_url($filedata['file']); yield array($class, $url, $filedata); } $count++; } } public function get_tracks_for_spotify_artist($artist) { $dirs = array(); $collection = new musicCollection(); foreach ($this->parse_list_output('find "artist" "'.format_for_mpd($artist).'"', $dirs, array("spotify")) as $filedata) { $collection->newTrack($filedata); } return $collection->getAllTracks('add'); } private function translate_commands_for_slave(&$cmds) { // // Re-check all add and playlistadd commands if we're using a Mopidy File Backend Slave // foreach ($cmds as $key => $cmd) { // add "local:track: // playlistadd "local:track: if (substr($cmd, 0, 17) == 'add "local:track:' || substr($cmd, 0,25) == 'playlistadd "local:track:') { logger::log("MOPIDY", "Translating tracks for Mopidy Slave"); $cmds[$key] = $this->swap_local_for_file($cmd); } } } private function translate_player_types(&$cmds) { // // Experimental translation to and from MPD/Mopidy Local URIs // global $prefs; foreach ($cmds as $key => $cmd) { if (substr($cmd, 0, 4) == 'add ') { logger::log("PLAYER", "Translating Track Uris from",$prefs['collection_player'],'to',$this->player_type); if ($prefs['collection_player']== 'mopidy') { $cmds[$key] = $this->mopidy_to_mpd($cmd); } else if ($prefs['collection_player']== 'mpd'){ $file = trim(substr($cmd, 4), '" '); $cmds[$key] = 'add '.$this->mpd_to_mopidy($file); } } } } private function mopidy_to_mpd($file) { return rawurldecode(preg_replace('#local:track:#', '', $file)); } private function mpd_to_mopidy($file) { if (substr($file, 0, 5) != 'http:' && substr($file, 0, 6) != 'https:') { return 'local:track:'.implode("/", array_map("rawurlencode", explode("/", $file))); } else { return $file; } } private function swap_local_for_file($string) { // url encode the album art directory global $prefs; $path = implode("/", array_map("rawurlencode", explode("/", $prefs['music_directory_albumart']))); logger::log("MOPIDYSLAVE", "Replacing with",$path); return preg_replace('#local:track:#', 'file://'.$path.'/', $string); } private function swap_file_for_local($string) { global $prefs; $path = 'file://'.implode("/", array_map("rawurlencode", explode("/", $prefs['music_directory_albumart']))).'/'; return preg_replace('#'.$path.'#', 'local:track:', $string); } private function probe_player_type() { global $prefs; $retval = false; if ($this->is_connected()) { logger::shout("MPDPLAYER", "Probing Player Type...."); $r = $this->do_mpd_command('tagtypes', true, true); if (is_array($r) && array_key_exists('tagtype', $r)) { if (in_array('X-AlbumUri', $r['tagtype'])) { logger::mark("MPDPLAYER", " ....tagtypes test says we're running Mopidy. Setting cookie"); $retval = "mopidy"; } else { logger::mark("MPDPLAYER", " ....tagtypes test says we're running MPD. Setting cookie"); $retval = "mpd"; } } else { logger::mark("MPDPLAYER", "WARNING! No output for 'tagtypes' - probably an old version of Mopidy. RompЯ may not function correctly"); $retval = "mopidy"; } setcookie('player_backend',$retval,time()+365*24*60*60*10,'/'); $prefs['player_backend'] = $retval; } return $retval; } } ?>