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 23/* vim:set softtabstop=4 shiftwidth=4 expandtab: */ 24 25namespace Ampache\Module\Playback\Localplay\Xbmc; 26 27use Ampache\Config\AmpConfig; 28use Ampache\Module\Playback\Localplay\localplay_controller; 29use Ampache\Repository\Model\Preference; 30use Ampache\Repository\Model\Song; 31use Ampache\Module\Playback\Stream_Url; 32use Ampache\Module\System\Core; 33use Ampache\Module\System\Dba; 34use PDOStatement; 35use XBMC_RPC_ConnectionException; 36use XBMC_RPC_Exception; 37use XBMC_RPC_HTTPClient; 38 39/** 40 * This is the class for the XBMC Localplay method to remote control 41 * a XBMC Instance 42 */ 43class AmpacheXbmc extends localplay_controller 44{ 45 /* Variables */ 46 private $version = '000001'; 47 private $description = 'Controls a XBMC instance'; 48 49 /* Constructed variables */ 50 private $_xbmc; 51 // Always use player 0 for now 52 private $_playerId = 0; 53 // Always use playlist 0 for now 54 private $_playlistId = 0; 55 56 /** 57 * get_description 58 * This returns the description of this Localplay method 59 */ 60 public function get_description() 61 { 62 return $this->description; 63 } // get_description 64 65 /** 66 * get_version 67 * This returns the current version 68 */ 69 public function get_version() 70 { 71 return $this->version; 72 } // get_version 73 74 /** 75 * is_installed 76 * This returns true or false if xbmc controller is installed 77 */ 78 public function is_installed() 79 { 80 $sql = "SHOW TABLES LIKE 'localplay_xbmc'"; 81 $db_results = Dba::query($sql); 82 83 return (Dba::num_rows($db_results) > 0); 84 } // is_installed 85 86 /** 87 * install 88 * This function installs the XBMC Localplay controller 89 */ 90 public function install() 91 { 92 $collation = (AmpConfig::get('database_collation', 'utf8mb4_unicode_ci')); 93 $charset = (AmpConfig::get('database_charset', 'utf8mb4')); 94 $engine = ($charset == 'utf8mb4') ? 'InnoDB' : 'MYISAM'; 95 96 $sql = "CREATE TABLE `localplay_xbmc` (`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(128) COLLATE $collation NOT NULL, `owner` INT(11) NOT NULL, `host` VARCHAR(255) COLLATE $collation NOT NULL, `port` INT(11) UNSIGNED NOT NULL, `user` VARCHAR(255) COLLATE $collation NOT NULL, `pass` VARCHAR(255) COLLATE $collation NOT NULL) ENGINE = $engine DEFAULT CHARSET=$charset COLLATE=$collation"; 97 Dba::query($sql); 98 99 // Add an internal preference for the users current active instance 100 Preference::insert('xbmc_active', T_('XBMC Active Instance'), 0, 25, 'integer', 'internal', 'xbmc'); 101 102 return true; 103 } // install 104 105 /** 106 * uninstall 107 * This removes the Localplay controller 108 */ 109 public function uninstall() 110 { 111 $sql = "DROP TABLE `localplay_xbmc`"; 112 Dba::query($sql); 113 114 // Remove the pref we added for this 115 Preference::delete('xbmc_active'); 116 117 return true; 118 } // uninstall 119 120 /** 121 * add_instance 122 * This takes key'd data and inserts a new xbmc instance 123 * @param array $data 124 * @return PDOStatement|boolean 125 */ 126 public function add_instance($data) 127 { 128 $sql = "INSERT INTO `localplay_xbmc` (`name`, `host`, `port`, `user`, `pass`, `owner`) VALUES (?, ?, ?, ?, ?, ?)"; 129 130 return Dba::query($sql, array( 131 $data['name'], 132 $data['host'], 133 $data['port'], 134 $data['user'], 135 $data['pass'], 136 Core::get_global('user')->id 137 )); 138 } // add_instance 139 140 /** 141 * delete_instance 142 * This takes a UID and deletes the instance in question 143 * @param $uid 144 * @return boolean 145 */ 146 public function delete_instance($uid) 147 { 148 $sql = "DELETE FROM `localplay_xbmc` WHERE `id` = ?"; 149 Dba::query($sql, array($uid)); 150 151 return true; 152 } // delete_instance 153 154 /** 155 * get_instances 156 * This returns a key'd array of the instance information with 157 * [UID]=>[NAME] 158 */ 159 public function get_instances() 160 { 161 $sql = "SELECT * FROM `localplay_xbmc` ORDER BY `name`"; 162 $db_results = Dba::query($sql); 163 $results = array(); 164 165 while ($row = Dba::fetch_assoc($db_results)) { 166 $results[$row['id']] = $row['name']; 167 } 168 169 return $results; 170 } // get_instances 171 172 /** 173 * update_instance 174 * This takes an ID and an array of data and updates the instance specified 175 * @param $uid 176 * @param array $data 177 * @return boolean 178 */ 179 public function update_instance($uid, $data) 180 { 181 $sql = "UPDATE `localplay_xbmc` SET `host` = ?, `port` = ?, `name` = ?, `user` = ?, `pass` = ? WHERE `id` = ?"; 182 Dba::query($sql, array($data['host'], $data['port'], $data['name'], $data['user'], $data['pass'], $uid)); 183 184 return true; 185 } // update_instance 186 187 /** 188 * instance_fields 189 * This returns a key'd array of [NAME]=>array([DESCRIPTION]=>VALUE,[TYPE]=>VALUE) for the 190 * fields so that we can on-the-fly generate a form 191 */ 192 public function instance_fields() 193 { 194 $fields['name'] = array('description' => T_('Instance Name'), 'type' => 'text'); 195 $fields['host'] = array('description' => T_('Hostname'), 'type' => 'text'); 196 $fields['port'] = array('description' => T_('Port'), 'type' => 'number'); 197 $fields['user'] = array('description' => T_('Username'), 'type' => 'text'); 198 $fields['pass'] = array('description' => T_('Password'), 'type' => 'password'); 199 200 return $fields; 201 } // instance_fields 202 203 /** 204 * get_instance 205 * This returns a single instance and all it's variables 206 * @param string $instance 207 * @return array 208 */ 209 public function get_instance($instance = '') 210 { 211 $instance = is_numeric($instance) ? (int) $instance : (int) AmpConfig::get('xbmc_active', 0); 212 $sql = ($instance > 1) ? "SELECT * FROM `localplay_xbmc` WHERE `id` = ?" : "SELECT * FROM `localplay_xbmc`"; 213 $db_results = Dba::query($sql, array($instance)); 214 215 return Dba::fetch_assoc($db_results); 216 } // get_instance 217 218 /** 219 * set_active_instance 220 * This sets the specified instance as the 'active' one 221 * @param $uid 222 * @param string $user_id 223 * @return boolean 224 */ 225 public function set_active_instance($uid, $user_id = '') 226 { 227 // Not an admin? bubkiss! 228 if (!Core::get_global('user')->has_access('100')) { 229 $user_id = Core::get_global('user')->id; 230 } 231 232 $user_id = $user_id ?: Core::get_global('user')->id; 233 234 Preference::update('xbmc_active', $user_id, $uid); 235 AmpConfig::set('xbmc_active', $uid, true); 236 237 return true; 238 } // set_active_instance 239 240 /** 241 * get_active_instance 242 * This returns the UID of the current active instance 243 * false if none are active 244 */ 245 public function get_active_instance() 246 { 247 } // get_active_instance 248 249 /** 250 * @param Stream_Url $url 251 * @return boolean 252 */ 253 public function add_url(Stream_Url $url) 254 { 255 if (!$this->_xbmc) { 256 return false; 257 } 258 259 try { 260 $this->_xbmc->Playlist->Add(array( 261 'playlistid' => $this->_playlistId, 262 'item' => array('file' => $url->url) 263 )); 264 265 return true; 266 } catch (XBMC_RPC_Exception $ex) { 267 debug_event(self::class, 'add_url failed: ' . $ex->getMessage(), 1); 268 269 return false; 270 } 271 } 272 273 /** 274 * delete_track 275 * Delete a track from the xbmc playlist 276 * @param $object_id 277 * @return boolean 278 */ 279 public function delete_track($object_id) 280 { 281 if (!$this->_xbmc) { 282 return false; 283 } 284 285 try { 286 $this->_xbmc->Playlist->Remove(array( 287 'playlistid' => $this->_playlistId, 288 'position' => $object_id 289 )); 290 291 return true; 292 } catch (XBMC_RPC_Exception $ex) { 293 debug_event(self::class, 'delete_track failed: ' . $ex->getMessage(), 1); 294 295 return false; 296 } 297 } // delete_track 298 299 /** 300 * clear_playlist 301 * This deletes the entire xbmc playlist. 302 */ 303 public function clear_playlist() 304 { 305 if (!$this->_xbmc) { 306 return false; 307 } 308 309 try { 310 $this->_xbmc->Playlist->Clear(array( 311 'playlistid' => $this->_playlistId 312 )); 313 314 return true; 315 } catch (XBMC_RPC_Exception $ex) { 316 debug_event(self::class, 'clear_playlist failed: ' . $ex->getMessage(), 1); 317 318 return false; 319 } 320 } // clear_playlist 321 322 /** 323 * play 324 * This just tells xbmc to start playing, it does not 325 * take any arguments 326 */ 327 public function play() 328 { 329 if (!$this->_xbmc) { 330 return false; 331 } 332 333 try { 334 // XBMC requires to load a playlist to play. We don't know if this play is after a new playlist or after pause 335 // So we get current status 336 $status = $this->status(); 337 if ($status['state'] == 'stop') { 338 $this->_xbmc->Player->Open(array( 339 'item' => array('playlistid' => $this->_playlistId) 340 )); 341 } else { 342 $this->_xbmc->Player->PlayPause(array( 343 'playerid' => $this->_playlistId, 344 'play' => true 345 )); 346 } 347 348 return true; 349 } catch (XBMC_RPC_Exception $ex) { 350 debug_event(self::class, 'play failed: ' . $ex->getMessage(), 1); 351 352 return false; 353 } 354 } // play 355 356 /** 357 * pause 358 * This tells XBMC to pause the current song 359 */ 360 public function pause() 361 { 362 if (!$this->_xbmc) { 363 return false; 364 } 365 366 try { 367 $this->_xbmc->Player->PlayPause(array( 368 'playerid' => $this->_playerId, 369 'play' => false 370 )); 371 372 return true; 373 } catch (XBMC_RPC_Exception $ex) { 374 debug_event(self::class, 'pause failed, is the player started? ' . $ex->getMessage(), 1); 375 376 return false; 377 } 378 } // pause 379 380 /** 381 * stop 382 * This just tells XBMC to stop playing, it does not take 383 * any arguments 384 */ 385 public function stop() 386 { 387 if (!$this->_xbmc) { 388 return false; 389 } 390 391 try { 392 $this->_xbmc->Player->Stop(array( 393 'playerid' => $this->_playerId 394 )); 395 396 return true; 397 } catch (XBMC_RPC_Exception $ex) { 398 debug_event(self::class, 'stop failed, is the player started? ' . $ex->getMessage(), 1); 399 400 return false; 401 } 402 } // stop 403 404 /** 405 * skip 406 * This tells XBMC to skip to the specified song 407 * @param $song 408 * @return boolean 409 */ 410 public function skip($song) 411 { 412 if (!$this->_xbmc) { 413 return false; 414 } 415 416 try { 417 $this->_xbmc->Player->GoTo(array( 418 'playerid' => $this->_playerId, 419 'to' => $song 420 )); 421 422 return true; 423 } catch (XBMC_RPC_Exception $ex) { 424 debug_event(self::class, 'skip failed, is the player started?: ' . $ex->getMessage(), 1); 425 426 return false; 427 } 428 } // skip 429 430 /** 431 * This tells XBMC to increase the volume 432 */ 433 public function volume_up() 434 { 435 if (!$this->_xbmc) { 436 return false; 437 } 438 439 try { 440 $this->_xbmc->Application->SetVolume(array( 441 'volume' => 'increment' 442 )); 443 444 return true; 445 } catch (XBMC_RPC_Exception $ex) { 446 debug_event(self::class, 'volume_up failed: ' . $ex->getMessage(), 1); 447 448 return false; 449 } 450 } // volume_up 451 452 /** 453 * This tells XBMC to decrease the volume 454 */ 455 public function volume_down() 456 { 457 if (!$this->_xbmc) { 458 return false; 459 } 460 461 try { 462 $this->_xbmc->Application->SetVolume(array( 463 'volume' => 'decrement' 464 )); 465 466 return true; 467 } catch (XBMC_RPC_Exception $ex) { 468 debug_event(self::class, 'volume_down failed: ' . $ex->getMessage(), 1); 469 470 return false; 471 } 472 } // volume_down 473 474 /** 475 * next 476 * This just tells xbmc to skip to the next song 477 */ 478 public function next() 479 { 480 if (!$this->_xbmc) { 481 return false; 482 } 483 484 try { 485 $this->_xbmc->Player->GoTo(array( 486 'playerid' => $this->_playerId, 487 'to' => 'next' 488 )); 489 490 return true; 491 } catch (XBMC_RPC_Exception $ex) { 492 debug_event(self::class, 'next failed, is the player started? ' . $ex->getMessage(), 1); 493 494 return false; 495 } 496 } // next 497 498 /** 499 * prev 500 * This just tells xbmc to skip to the prev song 501 */ 502 public function prev() 503 { 504 if (!$this->_xbmc) { 505 return false; 506 } 507 508 try { 509 $this->_xbmc->Player->GoTo(array( 510 'playerid' => $this->_playerId, 511 'to' => 'previous' 512 )); 513 514 return true; 515 } catch (XBMC_RPC_Exception $ex) { 516 debug_event(self::class, 'prev failed, is the player started? ' . $ex->getMessage(), 1); 517 518 return false; 519 } 520 } // prev 521 522 /** 523 * volume 524 * This tells XBMC to set the volume to the specified amount 525 * @param $volume 526 * @return boolean 527 */ 528 public function volume($volume) 529 { 530 if (!$this->_xbmc) { 531 return false; 532 } 533 534 try { 535 $this->_xbmc->Application->SetVolume(array( 536 'volume' => $volume 537 )); 538 539 return true; 540 } catch (XBMC_RPC_Exception $ex) { 541 debug_event(self::class, 'volume failed: ' . $ex->getMessage(), 1); 542 543 return false; 544 } 545 } // volume 546 547 /** 548 * repeat 549 * This tells XBMC to set the repeating the playlist (i.e. loop) to either on or off 550 * @param $state 551 * @return boolean 552 */ 553 public function repeat($state) 554 { 555 if (!$this->_xbmc) { 556 return false; 557 } 558 559 try { 560 $this->_xbmc->Player->SetRepeat(array( 561 'playerid' => $this->_playerId, 562 'repeat' => ($state ? 'all' : 'off') 563 )); 564 565 return true; 566 } catch (XBMC_RPC_Exception $ex) { 567 debug_event(self::class, 'repeat failed, is the player started? ' . $ex->getMessage(), 1); 568 569 return false; 570 } 571 } // repeat 572 573 /** 574 * random 575 * This tells XBMC to turn on or off the playing of songs from the playlist in random order 576 * @param $onoff 577 * @return boolean 578 */ 579 public function random($onoff) 580 { 581 if (!$this->_xbmc) { 582 return false; 583 } 584 585 try { 586 $this->_xbmc->Player->SetShuffle(array( 587 'playerid' => $this->_playerId, 588 'shuffle' => $onoff 589 )); 590 591 return true; 592 } catch (XBMC_RPC_Exception $ex) { 593 debug_event(self::class, 'random failed, is the player started? ' . $ex->getMessage(), 1); 594 595 return false; 596 } 597 } // random 598 599 /** 600 * get 601 * This functions returns an array containing information about 602 * The songs that XBMC currently has in it's playlist. This must be 603 * done in a standardized fashion 604 */ 605 public function get() 606 { 607 if (!$this->_xbmc) { 608 return false; 609 } 610 611 $results = array(); 612 613 try { 614 $playlist = $this->_xbmc->Playlist->GetItems(array( 615 'playlistid' => $this->_playlistId, 616 'properties' => array('file') 617 )); 618 619 for ($i = $playlist['limits']['start']; $i < $playlist['limits']['end']; ++$i) { 620 $item = $playlist['items'][$i]; 621 622 $data = array(); 623 $data['link'] = $item['file']; 624 $data['id'] = $i; 625 $data['track'] = $i + 1; 626 627 $url_data = $this->parse_url($data['link']); 628 if ($url_data != null) { 629 $data['oid'] = $url_data['oid']; 630 $song = new Song($data['oid']); 631 if ($song != null) { 632 $data['name'] = $song->get_artist_name() . ' - ' . $song->title; 633 } 634 } 635 if (!$data['name']) { 636 $data['name'] = $item['label']; 637 } 638 $results[] = $data; 639 } 640 } catch (XBMC_RPC_Exception $ex) { 641 debug_event(self::class, 'get failed: ' . $ex->getMessage(), 1); 642 } 643 644 return $results; 645 } // get 646 647 /** 648 * status 649 * This returns bool/int values for features, loop, repeat and any other features 650 * that this Localplay method supports. 651 * This works as in requesting the xbmc properties 652 * @return array 653 */ 654 public function status() 655 { 656 $array = array(); 657 if (!$this->_xbmc) { 658 return $array; 659 } 660 661 try { 662 $appprop = $this->_xbmc->Application->GetProperties(array( 663 'properties' => array('volume') 664 )); 665 $array['volume'] = (int)($appprop['volume']); 666 667 try { 668 $currentplay = $this->_xbmc->Player->GetItem(array( 669 'playerid' => $this->_playerId, 670 'properties' => array('file') 671 )); 672 // We assume it's playing. No pause detection support. 673 $array['state'] = 'play'; 674 675 $playprop = $this->_xbmc->Player->GetProperties(array( 676 'playerid' => $this->_playerId, 677 'properties' => array('repeat', 'shuffled') 678 )); 679 $array['repeat'] = ($playprop['repeat'] != "off"); 680 $array['random'] = (strtolower($playprop['shuffled']) == 1); 681 $array['track'] = $currentplay['file']; 682 683 $url_data = $this->parse_url($array['track']); 684 $song = new Song($url_data['oid']); 685 if ($song->title || $song->get_artist_name() || $song->get_album_name()) { 686 $array['track_title'] = $song->title; 687 $array['track_artist'] = $song->get_artist_name(); 688 $array['track_album'] = $song->get_album_name(); 689 } 690 } catch (XBMC_RPC_Exception $ex) { 691 debug_event(self::class, 'get current item failed, player probably stopped. ' . $ex->getMessage(), 1); 692 $array['state'] = 'stop'; 693 } 694 } catch (XBMC_RPC_Exception $ex) { 695 debug_event(self::class, 'status failed: ' . $ex->getMessage(), 1); 696 } 697 698 return $array; 699 } // status 700 701 /** 702 * connect 703 * This functions creates the connection to XBMC and returns 704 * a boolean value for the status, to save time this handle 705 * is stored in this class 706 */ 707 public function connect() 708 { 709 $options = self::get_instance(); 710 try { 711 debug_event(self::class, 'Trying to connect xbmc instance ' . $options['host'] . ':' . $options['port'] . '.', 5); 712 $this->_xbmc = new XBMC_RPC_HTTPClient($options); 713 debug_event(self::class, 'Connected.', 5); 714 715 return true; 716 } catch (XBMC_RPC_ConnectionException $ex) { 717 debug_event(self::class, 'xbmc connection failed: ' . $ex->getMessage(), 1); 718 719 return false; 720 } 721 } // connect 722} 723