Fix bug in MPD/Common where playlist version numbers would not be properly returned...
[patchfork.git] / inc / Net / MPD / Common.php
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3 /**
4  * Music Player Daemon API
5  *
6  * PHP Version 5
7  *
8  * LICENSE: This source file is subject to version 3.01 of the PHP license
9  * that is available thorugh the world-wide-web at the following URI:
10  * http://www.php.net/license/3_01.txt. If you did not receive a copy of
11  * the PHP License and are unable to obtain it through the web, please
12  * send a note to license@php.net so we can mail you a copy immediately.
13  *
14  * @category  Networking
15  * @package   Net_MPD
16  * @author    Graham Christensen <graham.christensen@itrebal.com>
17  * @copyright 2006 Graham Christensen
18  * @license   http://www.php.net/license/3_01.txt
19  * @version   CVS: $ID:$
20  */
21
22 /**
23  * API for the common peices of Music Player Daemon commands
24  *
25  * Used for basic interaction and output handeling, as well as
26  * several standard commands.
27  *
28  * @category  Networking
29  * @package   Net_MPD
30  * @author    Graham Christensen <graham.christensen@itrebal.com>
31  * @copyright 2006 Graham Christensen
32  * @license   http://www.php.net/license/3_01.txt
33  * @version   CVS: $ID:$
34  */
35 class Net_MPD_Common
36 {
37     //Connection & Write Errors
38     const CONNECTION_NOT_OPENED   = 100;
39     const CONNECTION_FAILED       = 102;
40     const WRITE_FAILED            = 103;
41
42     //MPD Errors
43     const ACK_NOT_LIST            = 1;
44     const ACK_ARG                 = 2;
45     const ACK_PASSWORD            = 3;
46     const ACK_PERMISSION          = 4;
47     const ACK_UNKOWN              = 5;
48     const ACK_NO_EXIST            = 50;
49     const ACK_PLAYLIST_MAX        = 51;
50     const ACK_SYSTEM              = 52;
51     const ACK_PLAYLIST_LOAD       = 53;
52     const ACK_UPDATE_ALREADY      = 54;
53     const ACK_PLAYER_SYNC         = 55;
54     const ACK_EXIST               = 56;
55     const ACK_COMMAND_FAILED      = -100;
56
57     //MPD Responces
58     const RESPONSE_OK             = 'OK';
59
60     private $_connection          = null;
61     protected $_errors            = array();
62     private $_current_error       = array();
63     protected $_commands          = array();
64     protected $_output            = array();
65     private $connection_params    = array();
66
67
68
69     /**
70      * Set connection params
71      *
72      * @param $host host to connect to, (default: localhost)
73      * @param $port port to connec through, (default: 6600)
74      * @param $password password to send, (default: null)
75      * @return void
76      */
77     function __construct($host = 'localhost', $port = 6600, $password = null)
78     {
79         $this->connection_params['host'] = $host;
80         $this->connection_params['port'] = $port;
81         $this->connection_params['password'] = $password;
82     }
83
84
85
86     /**
87      * Connect to MPD
88      *
89      * @return bool
90      */
91     public function connect()
92     {
93         if ($this->isConnected()) {
94             return true;
95         }
96         $connection = @fsockopen($this->connection_params['host'], $this->connection_params['port'], $errn, $errs, 4);
97         if ($connection) {
98             $this->_connection = $connection;
99             // Read from the source until its ready for commands
100             //$this->read();
101             while (!feof($this->_connection)) {
102                 $line = fgets($this->_connection);
103                 if (trim(substr($line, 0, 2)) == self::RESPONSE_OK) {
104                     break;
105                 }
106             }
107             if (!is_null($this->connection_params['password'])) {
108                 if (!$this->runCommand('password', $this->connection_params['password'])) {
109                     throw new PEAR_Exception('Password invalid.', self::ACK_PASSWORD);
110                 }
111             }
112             return true;
113         }
114         throw new PEAR_Exception('Error connecting: '.$errn.' ; '.$errs, self::CONNECTION_FAILED);
115     }
116
117
118
119     /**
120      * Check connection status
121      *
122      * @return bool
123      */
124     public function isConnected()
125     {
126         if (!is_resource($this->_connection)) {
127             return false;
128         }
129         return true;
130     }
131
132
133
134     /**
135      * Disconnect from MPD
136      *
137      * @return bool
138      */
139     public function disconnect()
140     {
141         $this->runCommand('close');
142         fclose($this->_connection);
143         $this->_connection = null;
144         return true;
145     }
146
147
148
149     /**
150      * Write data to the socket
151      *
152      * @param $command string data to be sent
153      *
154      * @return bool
155      *
156      */
157     function write($data)
158     {
159         //Are we connected?
160         if (!$this->isConnected()) {
161             // Try to connect
162             $this->connect();
163         }
164         //Write the data
165         if (!fwrite($this->_connection, $data."\r\n")) {
166             throw new PEAR_Exception('Write failed', self::WRITE_FAILED);
167         }
168         $this->_commands[] = $data;
169         return true;
170     }
171
172
173
174     /**
175      * Read data from the socket
176      *
177      * @return array of raw output
178      *
179      */
180     function read()
181     {
182         //Are we connected?
183         if (!$this->isConnected()) {
184             throw new PEAR_Exception('Not connected', self::CONNECTION_NOT_OPENED);
185         }
186         //Loop through the connection, putting the data into $line
187         $output = array();
188         while (!feof($this->_connection)) {
189             $line = fgets($this->_connection);
190             if (preg_match('/^ACK \[(.*?)\@(.*?)\] \{(.*?)\} (.*?)$/', $line, $matches)) {
191                 //If the output is an ACK error
192                 //$this->runCommand('clearerror'); //Cleanup the error
193                 $this->_errors[] = $matches;
194                 $this->_current_error = array('ack' => $matches[1], 'func' => $matches[3], 'error' => $matches[4]);
195                 throw new PEAR_Exception('Command Failed', self::ACK_COMMAND_FAILED);
196             } elseif (trim($line) == self::RESPONSE_OK) {
197                 //The last line of output was hit, close the loop
198                 break;
199             } else {
200                 //Output from the server added to the return array
201                 $output[] = $line;
202             }
203         }
204         return $output;
205     }
206
207
208
209     /**
210      * Get the current error data
211      *
212      * @return array of error data
213      */
214     public function getErrorData()
215     {
216         return $this->_current_error;
217     }
218
219
220     public function clearError() {
221          $this->runCommand('clearerror'); //Cleanup the error
222     }
223
224     /**
225      * Run command
226      *
227      * @param $command string a command to be executed through MPD
228      * @param $args mixed string for a single argument, array for multiple
229      * @param $parse mixed false to parse the output, int for parse style
230      *
231      * @return array of server output
232      */
233     public function runCommand($command, $args = array(), $parse = 0)
234     {
235         //Generate the command
236         if (is_array($args)) {
237             foreach($args as $arg) {
238                 $command.= ' "'.str_replace('"', '\"', $arg) .'"';
239             }
240         } elseif (!is_null($args)) {
241             $command.= ' "'.str_replace('"', '\"', $args) .'"';
242         }
243         //Write and then capture the output
244         $this->write($command);
245         $output = $this->read();
246         
247         $this->_output[] = array($command, $output);
248         if ($output === array()) {
249             return true;
250         }
251         if ($parse !== false) {
252             return $this->parseOutput($output, $parse);
253         }
254         return $output;
255     }
256
257
258
259     /**
260      * Parse MPD output on a line-by-line basis
261      * creating output that is easy to work with
262      *
263      * @param $input array of input from MPD
264      * @param $style int style number,'0' for the "intelligent" sorting
265      *
266      * @return array
267      */
268     public function parseOutput($input, $style = 0)
269     {
270         if (!is_array($input)) {
271             $input = array($input);
272         }
273         $count = array('outputs' => 0, 'file' => -1, 'key' => 0);
274         $used_keys = array();
275         $output = array();
276         $prev = array('key' => null, 'value' => null);
277         $dirtoggle = false;
278         foreach($input as $line) {
279             if (is_array($line)) {
280                 $this->_errors[] = 'Server output not expected: '.print_r($line, true);
281                 continue;
282             } else {
283                 $parts = explode(': ', $line, 2);
284                 if (!isset($parts[0], $parts[1])) {
285                     $this->errors[] = 'Server output not expected: '.$line;
286                     continue;
287                 }
288             }
289             $key = trim($parts[0]);
290             $value = trim($parts[1]);
291             if ($style == 0) {
292                 switch ($key) {
293                     //The following has to do strictly
294                     //with files in the output
295                     case 'file':
296                     case 'Artist':
297                     case 'Album':
298                     case 'Title':
299                     case 'Track':
300                     case 'Name':
301                     case 'Genre':
302                     case 'Date':
303                     case 'Composer':
304                     case 'Performer':
305                     case 'Comment':
306                     case 'Disc':
307                     case 'Id':
308                     case 'Pos':
309                     case 'Time':
310                     case 'cpos':
311                         if ($key == 'file'||$key== 'cpos') {
312                             $count['file']++;
313                         }
314                         $output['file'][$count['file']][$key] = $value;
315                         break;
316
317                     //The next section is for a 'stats' call
318                     case 'artists':
319                     case 'albums':
320                     case 'songs':
321                     case 'uptime':
322                     case 'playtime':
323                     case 'db_playtime':
324                     case 'db_update':
325                         $output['stats'][$key] = $value;
326                         break;
327
328                     //Now for a status call:
329                     case 'volume':
330                     case 'repeat':
331                     case 'random':
332                     case 'playlistlength':
333                     case 'xfade':
334                     case 'state':
335                     case 'song':
336                     case 'songid':
337                     case 'time':
338                     case 'bitrate':
339                     case 'audio':
340                     case 'updating_db':
341                     case 'error':
342                         $output['status'][$key] = $value;
343                         break;
344
345                     //Outputs
346                     case 'outputid':
347                     case 'outputname':
348                     case 'outputenabled':
349                         if ($key == 'outputid') {
350                             $count['outputs']++;
351                         }
352                         $output['outputs'][$count['outputs']][$key] = $value;
353                         break;
354
355                     //The 'playlist' case works in 2 scenarios
356                     //1) in a file/directory listing
357                     //2) in a status call
358                     // This is to determine if it was in a status call
359                     // or in a directory call.
360                     case 'playlist':
361                         if ($output['status']) {
362                             $output['status'][$key] = $value;
363                         } else {
364                             $output[$key][] = $value;
365                         }
366                         break;
367
368                     //Now that we've covered most of the weird
369                     //options of output,
370                     //lets cover everything else!
371                     default:
372                         if (isset($used_keys[$key])) {
373                             $used_keys = array();
374                             $count['key']++;
375                         }
376                         $used_keys[$key] = true;
377                         //$output[$count['key']][$key] = $value;//This is rarely useful
378                         $output[$key][] = $value;
379                         break;
380                 }
381             } elseif ($style == 1) {
382                 $output[$key][] = $value;
383             }
384             if ($key == 'directory') {
385                 $dirtoggle = true;
386             }
387             $prev['key'] = $key;
388             $prev['value'] = $value;
389         }
390         return $output;
391     }
392
393
394
395     /**
396      * A method to access errors
397      *
398      * @return array
399      */
400     public function getErrors()
401     {
402         return $this->_errors;
403     }
404
405
406
407     /**
408      * Used to discover commands that are not available
409      *
410      * @return array (null on no functions not being available)
411      */
412     public function getNotCommands()
413     {
414         $cmds = $this->runCommand('notcommands');
415         if (!isset($cmds['command'])) {
416             return array();
417         }
418         return $cmds['command'];
419     }
420
421
422
423     /**
424      * Used to discover which commands are available
425      *
426      * @return array (null on no functions being available
427      */
428     public function getCommands()
429     {
430         $cmds = $this->runCommand('commands');
431         if (!isset($cmds['command'])) {
432             return array();
433         }
434         return $cmds['command'];
435     }
436
437     public function hasCommand($name) {
438         $cmds = $this->getCommands();
439         foreach ($cmds as $cmd)
440             if($cmd==$name)
441                 return true;
442         return false;
443     }
444
445
446
447     /**
448      * Ping the MPD server to keep the connection running
449      *
450      * @return bool
451      */
452     public function ping()
453     {
454         if ($this->runCommand('ping')) {
455             return true;
456         }
457         return false;
458     }
459
460
461
462     /**
463      * Get various statistics about the MPD server
464      *
465      * @return array
466      */
467     public function getStats()
468     {
469         $stats = $this->runCommand('stats');
470         if (!isset($stats['stats'])) {
471             return false;
472         }
473         return $stats['stats'];
474     }
475
476
477
478     /**
479      * Get the status of the MPD server
480      *
481      * @return array
482      */
483     public function getStatus()
484     {
485         $status = $this->runCommand('status');
486         if (!isset($status['status'])) {
487             return false;
488         }
489         return $status['status'];
490     }
491 }
492 ?>
This page took 0.04149 seconds and 4 git commands to generate.