1<?php 2/* 3 * vim:set softtabstop=4 shiftwidth=4 expandtab: 4 * 5 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later) 6 * Copyright 2001 - 2020 Ampache.org 7 * 8 * This program is free software: you can redistribute it and/or modify 9 * it under the terms of the GNU Affero General Public License as published by 10 * the Free Software Foundation, either version 3 of the License, or 11 * (at your option) any later version. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * GNU Affero General Public License for more details. 17 * 18 * You should have received a copy of the GNU Affero General Public License 19 * along with this program. If not, see <https://www.gnu.org/licenses/>. 20 * 21 */ 22 23declare(strict_types=0); 24/* vim:set softtabstop=4 shiftwidth=4 expandtab: */ 25 26namespace Ampache\Module\Playback\Localplay\Mpd; 27 28/** 29 * mpd.class.php - PHP Object Interface to the MPD Music Player Daemon 30 * Version 1.3 31 * 32 * Copyright (C) 2003-2004 Benjamin Carlisle (bcarlisle@24oz.com) 33 * Copyright 2010 Paul Arthur MacIain 34 * 35 * This program is free software; you can redistribute it and/or modify 36 * it under the terms of the GNU General Public License as published by 37 * the Free Software Foundation; either version 2 of the License, or 38 * (at your option) any later version. 39 * 40 * This program is distributed in the hope that it will be useful, 41 * but WITHOUT ANY WARRANTY; without even the implied warranty of 42 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 43 * GNU General Public License for more details. 44 * 45 * You should have received a copy of the GNU General Public License 46 * along with this program; if not, write to the Free Software 47 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 48 * Class mpd 49 */ 50class mpd 51{ 52 // Command names 53 // Status queries 54 const COMMAND_CLEARERROR = 'clearerror'; 55 const COMMAND_CURRENTSONG = 'currentsong'; 56 const COMMAND_IDLE = 'idle'; 57 const COMMAND_STATUS = 'status'; 58 const COMMAND_STATISTICS = 'stats'; 59 60 // Playback options 61 const COMMAND_CONSUME = 'consume'; 62 const COMMAND_CROSSFADE = 'crossfade'; 63 const COMMAND_RANDOM = 'random'; 64 const COMMAND_REPEAT = 'repeat'; 65 const COMMAND_SETVOL = 'setvol'; 66 const COMMAND_SINGLE = 'single'; 67 const COMMAND_REPLAY_GAIN_MODE = 'replay_gain_mode'; 68 const COMMAND_REPLAY_GAIN_STATUS = 'replay_gain_status'; 69 70 // Playback control 71 const COMMAND_NEXT = 'next'; 72 const COMMAND_PAUSE = 'pause'; 73 const COMMAND_PLAY = 'play'; 74 const COMMAND_PLAYID = 'playid'; 75 const COMMAND_PREVIOUS = 'previous'; 76 const COMMAND_SEEK = 'seek'; 77 const COMMAND_SEEKID = 'seekid'; 78 const COMMAND_STOP = 'stop'; 79 80 // Current playlist control 81 const COMMAND_ADD = 'add'; 82 const COMMAND_ADDID = 'addid'; 83 const COMMAND_CLEAR = 'clear'; 84 const COMMAND_DELETE = 'delete'; 85 const COMMAND_DELETEID = 'deleteid'; 86 const COMMAND_MOVETRACK = 'move'; 87 const COMMAND_MOVEID = 'moveid'; 88 const COMMAND_PLFIND = 'playlistfind'; 89 const COMMAND_PLID = 'playlistid'; 90 const COMMAND_PLINFO = 'playlistinfo'; 91 const COMMAND_PLSEARCH = 'playlistsearch'; 92 const COMMAND_PLCHANGES = 'plchanges'; 93 const COMMAND_PLCHANGESPOSID = 'plchangesposid'; 94 const COMMAND_PLSHUFFLE = 'shuffle'; 95 const COMMAND_PLSWAPTRACK = 'swap'; 96 const COMMAND_PLSWAPID = 'swapid'; 97 98 // Stored playlists 99 const COMMAND_LISTPL = 'listplaylist'; 100 const COMMAND_LISTPLINFO = 'listplaylistinfo'; 101 const COMMAND_LISTPLAYLISTS = 'listplaylists'; 102 const COMMAND_PLLOAD = 'load'; 103 const COMMAND_PLADD = 'playlistadd'; 104 const COMMAND_PLCLEAR = 'playlistclear'; 105 const COMMAND_PLDELETE = 'playlistdelete'; 106 const COMMAND_PLMOVE = 'playlistmove'; 107 const COMMAND_RENAME = 'rename'; 108 const COMMAND_RM = 'rm'; 109 const COMMAND_PLSAVE = 'save'; 110 111 // Music database 112 const COMMAND_COUNT = 'count'; 113 const COMMAND_FIND = 'find'; 114 const COMMAND_FINDADD = 'findadd'; 115 const COMMAND_TABLE = 'list'; 116 const COMMAND_LISTALL = 'listall'; 117 const COMMAND_LISTALLINFO = 'listallinfo'; 118 const COMMAND_LSDIR = 'lsinfo'; 119 const COMMAND_SEARCH = 'search'; 120 const COMMAND_REFRESH = 'update'; 121 const COMMAND_RESCAN = 'rescan'; 122 123 // Stickers 124 const COMMAND_STICKER = 'sticker'; 125 const STICKER_GET = 'get'; 126 const STICKER_SET = 'set'; 127 const STICKER_DELETE = 'delete'; 128 const STICKER_LIST = 'list'; 129 const STICKER_FIND = 'find'; 130 131 // Connection 132 const COMMAND_CLOSE = 'close'; 133 const COMMAND_KILL = 'kill'; 134 const COMMAND_PASSWORD = 'password'; 135 const COMMAND_PING = 'ping'; 136 const COMMAND_SHUTDOWN = 'shutdown'; 137 138 // Deprecated commands 139 const COMMAND_VOLUME = 'volume'; 140 141 // Bulk commands 142 const COMMAND_START_BULK = 'command_list_begin'; 143 const COMMAND_END_BULK = 'command_list_end'; 144 145 // Predefined MPD Response messages 146 const RESPONSE_ERR = 'ACK'; 147 const RESPONSE_OK = 'OK'; 148 149 // MPD State Constants 150 const STATE_PLAYING = 'play'; 151 const STATE_STOPPED = 'stop'; 152 const STATE_PAUSED = 'pause'; 153 154 // MPD Searching Constants 155 const SEARCH_ARTIST = 'artist'; 156 const SEARCH_TITLE = 'title'; 157 const SEARCH_ALBUM = 'album'; 158 159 // MPD Cache Tables 160 const TABLE_ARTIST = 'artist'; 161 const TABLE_ALBUM = 'album'; 162 163 // Table holding version compatibility information 164 private static $_COMPATIBILITY_TABLE = array( 165 self::COMMAND_CONSUME => array('min' => '0.15.0', 'max' => false), 166 self::COMMAND_IDLE => array('min' => '0.14.0', 'max' => false), 167 self::COMMAND_PASSWORD => array('min' => '0.10.0', 'max' => false), 168 self::COMMAND_MOVETRACK => array('min' => '0.9.1', 'max' => false), 169 self::COMMAND_PLSWAPTRACK => array('min' => '0.9.1', 'max' => false), 170 self::COMMAND_RANDOM => array('min' => '0.9.1', 'max' => false), 171 self::COMMAND_SEEK => array('min' => '0.9.1', 'max' => false), 172 self::COMMAND_SETVOL => array('min' => '0.10.0', 'max' => false), 173 self::COMMAND_SINGLE => array('min' => '0.15.0', 'max' => false), 174 self::COMMAND_STICKER => array('min' => '0.15.0', 'max' => false), 175 self::COMMAND_VOLUME => array('min' => false, 'max' => '0.10.0') 176 ); 177 178 // TCP/Connection variables 179 private $host; 180 private $port; 181 private $password; 182 183 private $_mpd_sock = null; 184 public $connected = false; 185 186 // MPD Status variables 187 public $mpd_version = "(unknown)"; 188 189 public $stats; 190 public $status; 191 public $playlist; 192 193 // Misc Other Vars 194 public $mpd_class_version = '1.3'; 195 196 public $err_str = ''; // Stores the latest error message 197 private $_command_queue; // The list of commands for bulk command sending 198 199 private $_debug_callback = null; // Optional callback to be run on debug 200 public $debugging = false; 201 202 /** 203 * Constructor 204 * Builds the MPD object, connects to the server, and refreshes all 205 * local object properties. 206 * mpd constructor. 207 * @param $server 208 * @param $port 209 * @param $password 210 * @param $debug_callback 211 */ 212 public function __construct($server, $port, $password = null, $debug_callback = null) 213 { 214 $this->host = $server; 215 $this->port = $port; 216 $this->password = $password; 217 debug_event(self::class, "Connecting to: " . $server . ":" . $port, 5); 218 219 if (is_callable($debug_callback)) { 220 $this->_debug_callback = $debug_callback; 221 } 222 223 $this->_debug('construct', 'constructor called', 5); 224 225 if (empty($this->host)) { 226 $this->_error('construct', 'Host is empty'); 227 228 return false; 229 } 230 231 $response = $this->Connect(); 232 if (!$response) { 233 $this->_error('construct', 'Could not connect'); 234 235 return false; 236 } 237 238 $version = sscanf($response, self::RESPONSE_OK . " MPD %s\n"); 239 $this->mpd_version = $version[0]; 240 241 if ($password) { 242 if (!$this->SendCommand(self::COMMAND_PASSWORD, $password, false)) { 243 // bad password or command 244 $this->connected = false; 245 $this->_error('construct', 'Password supplied is incorrect or Invalid Command'); 246 247 return false; 248 } 249 } else { 250 if (!$this->RefreshInfo()) { 251 // no read access, might as well be disconnected 252 $this->connected = false; 253 $this->_error('construct', 'Password required to access server'); 254 255 return false; 256 } 257 } 258 259 return true; 260 } // constructor 261 262 /** 263 * Connect 264 * 265 * Connects to the MPD server. 266 * 267 * NOTE: This is called automatically upon object instantiation; you 268 * should not need to call this directly. 269 * @return false|string 270 */ 271 public function connect() 272 { 273 $this->_debug(self::class, "host: " . $this->host . ", port: " . $this->port, 5); 274 $this->_mpd_sock = fsockopen($this->host, (int) $this->port, $err, $err_str, 6); 275 276 if (!$this->_mpd_sock) { 277 $this->_error('Connect', "Socket Error: $err_str ($err)"); 278 279 return false; 280 } 281 // Set the timeout on the connection */ 282 stream_set_timeout($this->_mpd_sock, 6); 283 284 // We want blocking, cause otherwise it doesn't timeout, and feof just keeps on spinning 285 stream_set_blocking($this->_mpd_sock, true); 286 $status = socket_get_status($this->_mpd_sock); 287 while (!feof($this->_mpd_sock) && !$status['timed_out']) { 288 $response = fgets($this->_mpd_sock, 1024); 289 if (function_exists('socket_get_status')) { 290 $status = socket_get_status($this->_mpd_sock); 291 } 292 if (strncmp(self::RESPONSE_OK, $response, strlen(self::RESPONSE_OK)) == 0) { 293 $this->connected = true; 294 295 return $response; 296 } 297 if (strncmp(self::RESPONSE_ERR, $response, strlen(self::RESPONSE_ERR)) == 0) { 298 $this->_error('Connect', "Server responded with: $response"); 299 300 return false; 301 } 302 } // end while 303 // Generic response 304 $this->_error('Connect', "Connection not available"); 305 306 return false; 307 } // connect 308 309 /** 310 * SendCommand 311 * 312 * Sends a generic command to the MPD server. Several command constants 313 * are pre-defined for use (see self::COMMAND_* constant definitions 314 * above). 315 * @param $command 316 * @param $arguments 317 * @param boolean $refresh_info 318 * @return boolean|string 319 */ 320 public function SendCommand($command, $arguments = null, $refresh_info = true) 321 { 322 $this->_debug('SendCommand', "cmd: $command, args: " . json_encode($arguments), 5); 323 if (!$this->connected) { 324 $this->_error('SendCommand', 'Not connected'); 325 326 return false; 327 } else { 328 $response_string = ''; 329 330 // Check the command compatibility: 331 if (!$this->_checkCompatibility($command, $this->mpd_version)) { 332 return false; 333 } 334 335 if (isset($arguments)) { 336 if (is_array($arguments)) { 337 foreach ($arguments as $arg) { 338 $command .= ' "' . $arg . '"'; 339 } 340 } else { 341 $command .= ' "' . $arguments . '"'; 342 } 343 } 344 345 fputs($this->_mpd_sock, "$command\n"); 346 while (!feof($this->_mpd_sock)) { 347 $response = fgets($this->_mpd_sock, 1024); 348 349 // An OK signals the end of transmission 350 if (strncmp(self::RESPONSE_OK, $response, strlen(self::RESPONSE_OK)) == 0) { 351 break; 352 } 353 354 // An ERR signals an error! 355 if (strncmp(self::RESPONSE_ERR, $response, strlen(self::RESPONSE_ERR)) == 0) { 356 $this->_error('SendCommand', "MPD Error: $response"); 357 358 return false; 359 } 360 361 // Build the response string 362 $response_string .= $response; 363 } 364 $this->_debug('SendCommand', "response: $response_string", 5); 365 } 366 367 if ($refresh_info) { 368 $this->RefreshInfo(); 369 } 370 371 return $response_string ? $response_string : true; 372 } 373 374 /** 375 * QueueCommand 376 * 377 * Queues a generic command for later sending to the MPD server. The 378 * CommandQueue can hold as many commands as needed, and are sent all 379 * at once, in the order they were queued, using the SendCommandQueue 380 * method. The syntax for queueing commands is identical to SendCommand. 381 * @param $command 382 * @param string $arguments 383 * @return boolean 384 */ 385 public function QueueCommand($command, $arguments = '') 386 { 387 $this->_debug('QueueCommand', "start; cmd: $command args: " . json_encode($arguments), 5); 388 if (!$this->connected) { 389 $this->_error('QueueCommand', 'Not connected'); 390 391 return false; 392 } 393 394 if (!$this->_command_queue) { 395 $this->_command_queue = self::COMMAND_START_BULK . "\n"; 396 } 397 398 if ($arguments) { 399 if (is_array($arguments)) { 400 foreach ($arguments as $arg) { 401 $command .= ' "' . $arg . '"'; 402 } 403 } else { 404 $command .= ' "' . $arguments . '"'; 405 } 406 } 407 408 $this->_command_queue .= $command . "\n"; 409 410 $this->_debug('QueueCommand', 'return', 5); 411 412 return true; 413 } 414 415 /** 416 * SendCommandQueue 417 * 418 * Sends all commands in the Command Queue to the MPD server. 419 * @return boolean|string 420 */ 421 public function SendCommandQueue() 422 { 423 $this->_debug('SendCommandQueue', 'start', 5); 424 if (!$this->connected) { 425 $this->_error('SendCommandQueue', 'Not connected'); 426 427 return false; 428 } 429 430 $this->_command_queue .= self::COMMAND_END_BULK . "\n"; 431 $response = $this->SendCommand($this->_command_queue); 432 433 if ($response) { 434 $this->_command_queue = null; 435 } 436 437 $this->_debug('SendCommandQueue', "response: $response", 5); 438 439 return $response; 440 } 441 442 /** 443 * RefreshInfo 444 * 445 * Updates all class properties with the values from the MPD server. 446 * NOTE: This function is automatically called on Connect() 447 * @return boolean 448 */ 449 public function RefreshInfo() 450 { 451 $stats = $this->SendCommand(self::COMMAND_STATISTICS, null, false); 452 $status = $this->SendCommand(self::COMMAND_STATUS, null, false); 453 454 455 if (!$stats || !$status) { 456 return false; 457 } 458 459 $stats = self::_parseResponse($stats); 460 $status = self::_parseResponse($status); 461 462 $this->stats = $stats; 463 $this->status = $status; 464 465 // Get the Playlist 466 $playlist = $this->SendCommand(self::COMMAND_PLINFO, null, false); 467 $this->playlist = self::_parseFileListResponse($playlist); 468 469 return true; 470 } 471 472 /** 473 * AdjustVolume 474 * 475 * Adjusts the mixer volume on the MPD by <value>, which can be a 476 * positive (volume increase) or negative (volume decrease) value. 477 * @param $value 478 * @return boolean|string 479 */ 480 public function AdjustVolume($value) 481 { 482 $this->_debug('AdjustVolume', 'start', 5); 483 if (!is_numeric($value)) { 484 $this->_error('AdjustVolume', "argument must be numeric: $value"); 485 486 return false; 487 } 488 489 $this->RefreshInfo(); 490 $value = $this->status['volume'] + $value; 491 $response = $this->SetVolume($value); 492 493 $this->_debug('AdjustVolume', "return $response", 5); 494 495 return $response; 496 } 497 498 /** 499 * SetVolume 500 * 501 * Sets the mixer volume to <value>, which should be between 1 - 100. 502 * @param $value 503 * @return boolean|string 504 */ 505 public function SetVolume($value) 506 { 507 $this->_debug('SetVolume', 'start', 5); 508 if (!is_numeric($value)) { 509 $this->_error('SetVolume', "argument must be numeric: $value"); 510 511 return false; 512 } 513 514 // Forcibly prevent out of range errors 515 $value = $value > 0 ? $value : 0; 516 $value = $value < 100 ? $value : 100; 517 518 // If we're not compatible with SETVOL, we'll try adjusting 519 // using VOLUME 520 if ($this->_checkCompatibility(self::COMMAND_SETVOL, $this->mpd_version)) { 521 $command = self::COMMAND_SETVOL; 522 } else { 523 $this->RefreshInfo(); // Get the latest volume 524 if ($this->status['volume'] === null) { 525 return false; 526 } else { 527 $command = self::COMMAND_VOLUME; 528 $value = $value - $this->status['volume']; 529 } 530 } 531 532 $response = $this->SendCommand($command, $value); 533 534 $this->_debug('SetVolume', "return: $response", 5); 535 536 return $response; 537 } 538 539 /** 540 * GetDir 541 * 542 * Retrieves a database directory listing of the <dir> directory and 543 * places the results into a multidimensional array. If no directory is 544 * specified the directory listing is at the base of the MPD music path. 545 * @param string $dir 546 * @return array|boolean 547 */ 548 public function GetDir($dir = '') 549 { 550 $this->_debug('GetDir', 'start', 5); 551 $response = $this->SendCommand(self::COMMAND_LSDIR, $dir, false); 552 $dirlist = self::_parseFileListResponse($response); 553 $this->_debug('GetDir', 'return: ' . json_encode($dirlist), 5); 554 555 return $dirlist; 556 } 557 558 /** 559 * PLAdd 560 * 561 * Adds each track listed in a single-dimensional <trackArray>, which 562 * contains filenames of tracks to add to the end of the playlist. This 563 * is used to add many, many tracks to the playlist in one swoop. 564 * @param $trackArray 565 * @return boolean|string 566 */ 567 public function PLAddBulk($trackArray) 568 { 569 $this->_debug('PLAddBulk', 'start', 5); 570 $num_files = count($trackArray); 571 for ($count = 0; $count < $num_files; $count++) { 572 $this->QueueCommand(self::COMMAND_ADD, $trackArray[$count]); 573 } 574 $response = $this->SendCommandQueue(); 575 $this->_debug('PLAddBulk', "return: $response", 5); 576 577 return $response; 578 } 579 580 /** 581 * PLAdd 582 * 583 * Adds the file <file> to the end of the playlist. <file> must be a 584 * track in the MPD database. 585 * @param string $filename 586 * @return boolean|string 587 */ 588 public function PLAdd($filename) 589 { 590 $this->_debug('PLAdd', 'start', 5); 591 $response = $this->SendCommand(self::COMMAND_ADD, $filename); 592 $this->_debug('PLAdd', "return: $response", 5); 593 594 return $response; 595 } 596 597 /**PLMoveTrack 598 * 599 * Moves track number <current_position> to position <new_position> in 600 * the playlist. This is used to reorder the songs in the playlist. 601 * @param $current_position 602 * @param $new_position 603 * @return boolean|string 604 */ 605 public function PLMoveTrack($current_position, $new_position) 606 { 607 $this->_debug('PLMoveTrack', 'start', 5); 608 if (!is_numeric($current_position)) { 609 $this->_error('PLMoveTrack', "current_position must be numeric: $current_position"); 610 611 return false; 612 } 613 if ($current_position < 0 || $current_position > count($this->playlist)) { 614 $this->_error('PLMoveTrack', "current_position out of range"); 615 616 return false; 617 } 618 $new_position = $new_position > 0 ? $new_position : 0; 619 $new_position = ($new_position < count($this->playlist)) ? $new_position : count($this->playlist); 620 621 $response = $this->SendCommand(self::COMMAND_MOVETRACK, array($current_position, $new_position)); 622 623 $this->_debug('PLMoveTrack', "return: $response", 5); 624 625 return $response; 626 } 627 628 /**PLShuffle 629 * 630 * Randomly reorders the songs in the playlist. 631 * @return boolean|string 632 */ 633 public function PLShuffle() 634 { 635 $this->_debug('PLShuffle', 'start', 5); 636 $response = $this->SendCommand(self::COMMAND_PLSHUFFLE); 637 $this->_debug('PLShuffle', "return: $response", 5); 638 639 return $response; 640 } 641 642 /**PLLoad 643 * 644 * Retrieves the playlist from <file>.m3u and loads it into the current 645 * playlist. 646 * @param $file 647 * @return boolean|string 648 */ 649 public function PLLoad($file) 650 { 651 $this->_debug('PLLoad', 'start', 5); 652 $response = $this->SendCommand(self::COMMAND_PLLOAD, $file); 653 $this->_debug('PLLoad', "return: $response", 5); 654 655 return $response; 656 } 657 658 /**PLSave 659 * 660 * Saves the playlist to <file>.m3u for later retrieval. The file is 661 * saved in the MPD playlist directory. 662 * @param $file 663 * @return boolean|string 664 */ 665 public function PLSave($file) 666 { 667 $this->_debug('PLSave', 'start', 5); 668 $response = $this->SendCommand(self::COMMAND_PLSAVE, $file, false); 669 $this->_debug('PLSave', "return: $response", 5); 670 671 return $response; 672 } 673 674 /** 675 * PLClear 676 * 677 * Empties the playlist. 678 * @return boolean|string 679 */ 680 public function PLClear() 681 { 682 $this->_debug('PLClear', 'start', 5); 683 $response = $this->SendCommand(self::COMMAND_CLEAR); 684 $this->_debug('PLClear', "return: $response", 5); 685 686 return $response; 687 } 688 689 /** 690 * PLRemove 691 * 692 * Removes track <id> from the playlist. 693 * @param $id 694 * @return boolean|string 695 */ 696 public function PLRemove($id) 697 { 698 if (!is_numeric($id)) { 699 $this->_error('PLRemove', "id must be numeric: $id"); 700 701 return false; 702 } 703 $response = $this->SendCommand(self::COMMAND_DELETE, $id); 704 $this->_debug('PLRemove', "return: $response", 5); 705 706 return $response; 707 } // PLRemove 708 709 /** 710 * SetRepeat 711 * 712 * Enables 'loop' mode -- tells MPD continually loop the playlist. The 713 * <repVal> parameter is either 1 (on) or 0 (off). 714 * @param $value 715 * @return boolean|string 716 */ 717 public function SetRepeat($value) 718 { 719 $this->_debug('SetRepeat', 'start', 5); 720 $value = $value ? 1 : 0; 721 $response = $this->SendCommand(self::COMMAND_REPEAT, $value); 722 $this->_debug('SetRepeat', "return: $response", 5); 723 724 return $response; 725 } 726 727 /** 728 * SetRandom 729 * 730 * Enables 'randomize' mode -- tells MPD to play songs in the playlist 731 * in random order. The parameter is either 1 (on) or 0 (off). 732 * @param $value 733 * @return boolean|string 734 */ 735 public function SetRandom($value) 736 { 737 $this->_debug('SetRandom', 'start', 5); 738 $value = $value ? 1 : 0; 739 $response = $this->SendCommand(self::COMMAND_RANDOM, $value); 740 $this->_debug('SetRandom', "return: $response", 5); 741 742 return $response; 743 } 744 745 /** 746 * Shutdown 747 * 748 * Shuts down the MPD server (aka sends the KILL command). This closes 749 * the current connection and prevents future communication with the 750 * server. 751 * @return boolean|string 752 */ 753 public function Shutdown() 754 { 755 $this->_debug('Shutdown', 'start', 5); 756 $response = $this->SendCommand(self::COMMAND_SHUTDOWN); 757 758 $this->connected = false; 759 unset($this->mpd_version); 760 unset($this->err_str); 761 unset($this->_mpd_sock); 762 763 $this->_debug('Shutdown', "return: $response", 5); 764 765 return $response; 766 } 767 768 /** 769 * DBRefresh 770 * 771 * Tells MPD to rescan the music directory for new tracks and refresh 772 * the Database. Tracks cannot be played unless they are in the MPD 773 * database. 774 * @return boolean|string 775 */ 776 public function DBRefresh() 777 { 778 $this->_debug('DBRefresh', 'start', 5); 779 $response = $this->SendCommand(self::COMMAND_REFRESH); 780 $this->_debug('DBRefresh', "return: $response", 5); 781 782 return $response; 783 } 784 785 /** 786 * Play 787 * 788 * Begins playing the songs in the MPD playlist. 789 * @return boolean|string 790 */ 791 public function Play() 792 { 793 $this->_debug('Play', 'start', 5); 794 $response = $this->SendCommand(self::COMMAND_PLAY); 795 $this->_debug('Play', "return: $response", 5); 796 797 return $response; 798 } 799 800 /** 801 * Stop 802 * 803 * Stops playback. 804 * @return boolean|string 805 */ 806 public function Stop() 807 { 808 $this->_debug('Stop', 'start', 5); 809 $response = $this->SendCommand(self::COMMAND_STOP); 810 $this->_debug('Stop', "return: $response", 5); 811 812 return $response; 813 } 814 815 /** 816 * Pause 817 * 818 * Toggles pausing. 819 * @return boolean|string 820 */ 821 public function Pause() 822 { 823 $this->_debug('Pause', 'start', 5); 824 $response = $this->SendCommand(self::COMMAND_PAUSE); 825 $this->_debug('Pause', "return: $response", 5); 826 827 return $response; 828 } 829 830 /** 831 * SeekTo 832 * 833 * Skips directly to the <idx> song in the MPD playlist. 834 * @param $idx 835 * @return boolean 836 */ 837 public function SkipTo($idx) 838 { 839 $this->_debug('SkipTo', 'start', 5); 840 if (!is_numeric($idx)) { 841 $this->_error('SkipTo', "argument must be numeric: $idx"); 842 843 return false; 844 } 845 $response = $this->SendCommand(self::COMMAND_PLAY, $idx); 846 $this->_debug('SkipTo', "return: $idx", 5); 847 848 return $idx; 849 } 850 851 /** 852 * SeekTo 853 * 854 * Skips directly to a given position within a track in the MPD 855 * playlist. The <pos> argument, given in seconds, is the track position 856 * to locate. The <track> argument, if supplied, is the track number in 857 * the playlist. If <track> is not specified, the current track is 858 * assumed. 859 * @param $pos 860 * @param integer $track 861 * @return boolean 862 */ 863 public function SeekTo($pos, $track = -1) 864 { 865 $this->_debug('SeekTo', 'start', 5); 866 if (!is_numeric($pos)) { 867 $this->_error('SeekTo', "pos must be numeric: $pos"); 868 869 return false; 870 } 871 if (!is_numeric($track)) { 872 $this->_error('SeekTo', "track must be numeric: $track"); 873 874 return false; 875 } 876 if ($track == -1) { 877 $track = $this->current_track_id; 878 } 879 880 $response = $this->SendCommand(self::COMMAND_SEEK, array($track, $pos)); 881 $this->_debug('SeekTo', "return: $pos", 5); 882 883 return $pos; 884 } 885 886 /** 887 * Next 888 * 889 * Skips to the next song in the MPD playlist. If not playing, returns 890 * an error. 891 * @return boolean|string 892 */ 893 public function Next() 894 { 895 $this->_debug('Next', 'start', 5); 896 $response = $this->SendCommand(self::COMMAND_NEXT); 897 $this->_debug('Next', "return: $response", 5); 898 899 return $response; 900 } 901 902 /** 903 * Previous 904 * 905 * Skips to the previous song in the MPD playlist. If not playing, 906 * returns an error. 907 * @return boolean|string 908 */ 909 public function Previous() 910 { 911 $this->_debug('Previous', 'start', 5); 912 $response = $this->SendCommand(self::COMMAND_PREVIOUS); 913 $this->_debug('Previous', "return: $response", 5); 914 915 return $response; 916 } 917 918 /** 919 * Search 920 * 921 * Searches the MPD database. The search <type> should be one of the 922 * following: 923 * self::SEARCH_ARTIST, self::SEARCH_TITLE, self::SEARCH_ALBUM 924 * The search <string> is a case-insensitive locator string. Anything 925 * that contains <string> will be returned in the results. 926 * @param string $type 927 * @param string $string 928 * @return array|boolean 929 */ 930 public function Search($type, $string) 931 { 932 $this->_debug('Search', 'start', 5); 933 934 if ($type != self::SEARCH_ARTIST && $type != self::SEARCH_ALBUM && $type != self::SEARCH_TITLE) { 935 $this->_error('Search', 'invalid search type'); 936 937 return false; 938 } 939 940 $response = $this->SendCommand(self::COMMAND_SEARCH, array($type, $string), false); 941 942 $results = false; 943 944 if ($response) { 945 $results = self::_parseFileListResponse($response); 946 } 947 $this->_debug('Search', 'return: ' . json_encode($results), 5); 948 949 return $results; 950 } 951 952 /** 953 * Find 954 * 955 * Find looks for exact matches in the MPD database. The find <type> 956 * should be one of the following: 957 * self::SEARCH_ARTIST, self::SEARCH_TITLE, self::SEARCH_ALBUM 958 * The find <string> is a case-insensitive locator string. Anything that 959 * exactly matches <string> will be returned in the results. 960 * @param string $type 961 * @param string $string 962 * @return array|boolean 963 */ 964 public function Find($type, $string) 965 { 966 $this->_debug('Find', 'start', 5); 967 if ($type != self::SEARCH_ARTIST && $type != self::SEARCH_ALBUM && $type != self::SEARCH_TITLE) { 968 $this->_error('Find', 'invalid find type'); 969 970 return false; 971 } 972 973 $response = $this->SendCommand(self::COMMAND_FIND, array($type, $string), false); 974 975 $results = false; 976 977 if ($response) { 978 $results = self::_parseFileListResponse($response); 979 } 980 981 $this->_debug('Find', 'return: ' . json_encode($results), 5); 982 983 return $results; 984 } 985 986 /** 987 * Disconnect 988 * 989 * Closes the connection to the MPD server. 990 */ 991 public function Disconnect() 992 { 993 $this->_debug('Disconnect', 'start', 5); 994 fclose($this->_mpd_sock); 995 996 $this->connected = false; 997 unset($this->mpd_version); 998 unset($this->err_str); 999 unset($this->_mpd_sock); 1000 } 1001 1002 /** 1003 * GetArtists 1004 * 1005 * Returns the list of artists in the database in an associative array. 1006 * @return array|boolean 1007 */ 1008 public function GetArtists() 1009 { 1010 $this->_debug('GetArtists', 'start', 5); 1011 if (!$response = $this->SendCommand(self::COMMAND_TABLE, self::TABLE_ARTIST, false)) { 1012 return false; 1013 } 1014 $results = array(); 1015 1016 $parsed = self::_parseResponse($response); 1017 1018 foreach ($parsed as $key => $value) { 1019 if ($key == 'Artist') { 1020 $results[] = $value; 1021 } 1022 } 1023 1024 $this->_debug('GetArtists', 'return: ' . json_encode($results), 5); 1025 1026 return $results; 1027 } 1028 1029 /** 1030 * GetAlbums 1031 * 1032 * Returns the list of albums in the database in an associative array. 1033 * Optional parameter is an artist Name which will list all albums by a 1034 * particular artist. 1035 * @param $artist 1036 * @return array|boolean 1037 */ 1038 public function GetAlbums($artist = null) 1039 { 1040 $this->_debug('GetAlbums', 'start', 5); 1041 1042 $params[] = self::TABLE_ALBUM; 1043 if ($artist === null) { 1044 $params[] = $artist; 1045 } 1046 1047 if (!$response = $this->SendCommand(self::COMMAND_TABLE, $params, false)) { 1048 return false; 1049 } 1050 1051 $results = array(); 1052 $parsed = self::_parseResponse($response); 1053 1054 foreach ($parsed as $key => $value) { 1055 if ($key == 'Album') { 1056 $results[] = $value; 1057 } 1058 } 1059 1060 $this->_debug('GetAlbums', 'return: ' . json_encode($results), 5); 1061 1062 return $results; 1063 } 1064 1065 /** 1066 * _computeVersionValue 1067 * 1068 * Computes numeric value from a version string 1069 * 1070 * @param string $string 1071 * @return float|integer|mixed 1072 */ 1073 private static function _computeVersionValue($string) 1074 { 1075 $parts = explode('.', $string); 1076 1077 return (100 * $parts[0]) + (10 * $parts[1]) + $parts[2]; 1078 } 1079 1080 /** 1081 * _checkCompatibility 1082 * 1083 * Check MPD command compatibility against our internal table of 1084 * incompatibilities. 1085 * @param $cmd 1086 * @param $mpd_version 1087 * @return boolean 1088 */ 1089 private function _checkCompatibility($cmd, $mpd_version) 1090 { 1091 $mpd = self::_computeVersionValue($mpd_version); 1092 1093 if (isset(self::$_COMPATIBILITY_TABLE[$cmd])) { 1094 $min_version = self::$_COMPATIBILITY_TABLE[$cmd]['min']; 1095 $max_version = self::$_COMPATIBILITY_TABLE[$cmd]['max']; 1096 1097 if ($min_version) { 1098 $min = self::_computeVersionValue($min_version); 1099 if ($mpd < $min) { 1100 $this->_error('compatibility', 1101 "Command '$cmd' is not compatible with this version of MPD, version $min_version required"); 1102 1103 return false; 1104 } 1105 } 1106 1107 if ($max_version) { 1108 $max = self::_computeVersionValue($max_version); 1109 1110 if ($mpd >= $max) { 1111 $this->_error('compatibility', 1112 "Command '$cmd' has been deprecated in this version of MPD. Last compatible version: $max_version"); 1113 1114 return false; 1115 } 1116 } 1117 } 1118 1119 return true; 1120 } 1121 1122 /** 1123 * _parseFileListResponse 1124 * 1125 * Builds a multidimensional array with MPD response lists. 1126 * @param $response 1127 * @return array|boolean 1128 */ 1129 private static function _parseFileListResponse($response) 1130 { 1131 if (is_bool($response)) { 1132 return false; 1133 } 1134 1135 $results = array(); 1136 $counter = -1; 1137 $lines = explode("\n", $response); 1138 foreach ($lines as $line) { 1139 if (preg_match('/(\w+): (.+)/', $line, $matches)) { 1140 if ($matches[1] == 'file') { 1141 $counter++; 1142 } 1143 $results[$counter][$matches[1]] = $matches[2]; 1144 } 1145 } 1146 1147 return $results; 1148 } 1149 1150 /** 1151 * _parseResponse 1152 * Turns a response into an array 1153 * @param $response 1154 * @return array|boolean 1155 */ 1156 private static function _parseResponse($response) 1157 { 1158 if (!$response) { 1159 return false; 1160 } 1161 1162 $results = array(); 1163 $lines = explode("\n", $response); 1164 foreach ($lines as $line) { 1165 if (preg_match('/(\w+): (.+)/', $line, $matches)) { 1166 $results[$matches[1]] = $matches[2]; 1167 } 1168 } 1169 1170 return $results; 1171 } 1172 1173 /** 1174 * _error 1175 * 1176 * Set error state 1177 * @param string $source 1178 * @param string $message 1179 * @param integer $level 1180 */ 1181 private function _error($source, $message, $level = 1) 1182 { 1183 $this->err_str = "$source: $message"; 1184 $this->_debug($source, $message, $level); 1185 } 1186 1187 /** 1188 * _debug 1189 * 1190 * Do the debugging boogaloo 1191 * @param $source 1192 * @param $message 1193 * @param $level 1194 */ 1195 private function _debug($source, $message, $level) 1196 { 1197 if ($this->debugging) { 1198 echo "$source / $message\n"; 1199 } 1200 1201 if ($this->_debug_callback === null) { 1202 call_user_func($this->_debug_callback, 'MPD', "$source / $message", $level); 1203 } 1204 } 1205} // end class mpd 1206