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