Fix bug in MPD/Common where playlist version numbers would not be properly returned...
[patchfork.git] / inc / Net / MPD / Common.php
CommitLineData
964dd0bc
JW
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 */
35class 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':
83b0af90 361 if ($output['status']) {
964dd0bc
JW
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.055864 seconds and 4 git commands to generate.