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 23namespace Ampache\Module\Playback\Localplay\HttpQ; 24 25use Ampache\Config\AmpConfig; 26use Ampache\Repository\Model\Democratic; 27use Ampache\Module\Playback\Localplay\localplay_controller; 28use Ampache\Repository\Model\Preference; 29use Ampache\Repository\Model\Song; 30use Ampache\Module\Playback\Stream_Url; 31use Ampache\Module\System\Core; 32use Ampache\Module\System\Dba; 33use Ampache\Module\Util\ObjectTypeToClassNameMapper; 34use PDOStatement; 35 36/** 37 * AmpacheHttpq Class 38 * 39 * This is the class for the httpQ Localplay method to remotely control 40 * a WinAmp Instance 41 * 42 */ 43class AmpacheHttpq extends localplay_controller 44{ 45 /* Variables */ 46 private $version = '000002'; 47 private $description = "Controls an httpQ instance, requires Ampache's httpQ version"; 48 49 /* Constructed variables */ 50 private $_httpq; 51 52 /** 53 * get_description 54 * This returns the description of this Localplay method 55 */ 56 public function get_description() 57 { 58 return $this->description; 59 } // get_description 60 61 /** 62 * get_version 63 * This returns the current version 64 */ 65 public function get_version() 66 { 67 return $this->version; 68 } // get_version 69 70 /** 71 * is_installed 72 * This returns true or false if this controller is installed 73 */ 74 public function is_installed() 75 { 76 $sql = "SHOW TABLES LIKE 'localplay_httpq'"; 77 $db_results = Dba::read($sql); 78 79 return (Dba::num_rows($db_results) > 0); 80 } // is_installed 81 82 /** 83 * install 84 * This function installs the controller 85 */ 86 public function install() 87 { 88 $collation = (AmpConfig::get('database_collation', 'utf8mb4_unicode_ci')); 89 $charset = (AmpConfig::get('database_charset', 'utf8mb4')); 90 $engine = ($charset == 'utf8mb4') ? 'InnoDB' : 'MYISAM'; 91 92 $sql = "CREATE TABLE `localplay_httpq` (`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, `password` VARCHAR(255) COLLATE $collation NOT NULL, `access` SMALLINT(4) UNSIGNED NOT NULL DEFAULT '0') ENGINE = $engine DEFAULT CHARSET=$charset COLLATE=$collation"; 93 Dba::query($sql); 94 95 // Add an internal preference for the users current active instance 96 Preference::insert('httpq_active', T_('HTTPQ Active Instance'), 0, 25, 'integer', 'internal', 'httpq'); 97 98 return true; 99 } // install 100 101 /** 102 * uninstall 103 * This removes the Localplay controller 104 */ 105 public function uninstall() 106 { 107 $sql = "DROP TABLE `localplay_httpq`"; 108 Dba::write($sql); 109 110 // Remove the pref we added for this 111 Preference::delete('httpq_active'); 112 113 return true; 114 } // uninstall 115 116 /** 117 * add_instance 118 * This takes keyed data and inserts a new httpQ instance 119 * @param array $data 120 * @return PDOStatement|boolean 121 */ 122 public function add_instance($data) 123 { 124 $name = Dba::escape($data['name']); 125 $host = Dba::escape($data['host']); 126 $port = Dba::escape($data['port']); 127 $password = Dba::escape($data['password']); 128 $user_id = Dba::escape(Core::get_global('user')->id); 129 130 $sql = "INSERT INTO `localplay_httpq` (`name`, `host`, `port`, `password`, `owner`) VALUES ('$name', '$host', '$port', '$password', '$user_id')"; 131 132 return Dba::write($sql); 133 } // add_instance 134 135 /** 136 * delete_instance 137 * This takes a UID and deletes the instance in question 138 * @param $uid 139 * @return boolean 140 */ 141 public function delete_instance($uid) 142 { 143 $uid = Dba::escape($uid); 144 $sql = "DELETE FROM `localplay_httpq` WHERE `id`='$uid'"; 145 Dba::write($sql); 146 147 return true; 148 } // delete_instance 149 150 /** 151 * get_instances 152 * This returns a keyed array of the instance information with 153 * [UID]=>[NAME] 154 */ 155 public function get_instances() 156 { 157 $sql = "SELECT * FROM `localplay_httpq` ORDER BY `name`"; 158 159 $db_results = Dba::read($sql); 160 $results = array(); 161 while ($row = Dba::fetch_assoc($db_results)) { 162 $results[$row['id']] = $row['name']; 163 } 164 165 return $results; 166 } // get_instances 167 168 /** 169 * update_instance 170 * This takes an ID and an array of data and updates the instance specified 171 * @param $uid 172 * @param array $data 173 * @return boolean 174 */ 175 public function update_instance($uid, $data) 176 { 177 $uid = Dba::escape($uid); 178 $port = Dba::escape($data['port']); 179 $host = Dba::escape($data['host']); 180 $name = Dba::escape($data['name']); 181 $pass = Dba::escape($data['password']); 182 183 $sql = "UPDATE `localplay_httpq` SET `host`='$host', `port`='$port', `name`='$name', `password`='$pass' WHERE `id`='$uid'"; 184 Dba::write($sql); 185 186 return true; 187 } // update_instance 188 189 /** 190 * instance_fields 191 * This returns a keyed array of [NAME]=>array([DESCRIPTION]=>VALUE,[TYPE]=>VALUE) for the 192 * fields so that we can on-the-fly generate a form 193 */ 194 public function instance_fields() 195 { 196 $fields['name'] = array('description' => T_('Instance Name'), 'type' => 'text'); 197 $fields['host'] = array('description' => T_('Hostname'), 'type' => 'text'); 198 $fields['port'] = array('description' => T_('Port'), 'type' => 'number'); 199 $fields['password'] = array('description' => T_('Password'), 'type' => 'password'); 200 201 return $fields; 202 } // instance_fields 203 204 /** 205 * get_instance 206 * This returns a single instance and all its variables 207 * @param string $instance 208 * @return array 209 */ 210 public function get_instance($instance = '') 211 { 212 $instance = is_numeric($instance) ? (int) $instance : (int) AmpConfig::get('httpq_active', 0); 213 $sql = ($instance > 1) ? "SELECT * FROM `localplay_httpq` WHERE `id` = ?" : "SELECT * FROM `localplay_httpq`"; 214 $db_results = Dba::query($sql, array($instance)); 215 216 return Dba::fetch_assoc($db_results); 217 } // get_instance 218 219 /** 220 * set_active_instance 221 * This sets the specified instance as the 'active' one 222 * @param $uid 223 * @param string $user_id 224 * @return boolean 225 */ 226 public function set_active_instance($uid, $user_id = '') 227 { 228 // Not an admin? bubkiss! 229 if (!Core::get_global('user')->has_access('100')) { 230 $user_id = Core::get_global('user')->id; 231 } 232 233 $user_id = $user_id ?: Core::get_global('user')->id; 234 235 Preference::update('httpq_active', $user_id, $uid); 236 AmpConfig::set('httpq_active', $uid, true); 237 238 return true; 239 } // set_active_instance 240 241 /** 242 * get_active_instance 243 * This returns the UID of the current active instance 244 * false if none are active 245 */ 246 public function get_active_instance() 247 { 248 } // get_active_instance 249 250 /** 251 * add_url 252 * This is the new hotness 253 * @param Stream_Url $url 254 * @return boolean 255 */ 256 public function add_url(Stream_Url $url) 257 { 258 if ($this->_httpq->add($url->title, $url->url) === null) { 259 debug_event('httpq.controller', 'add_url failed to add ' . (string)$url->url, 1); 260 261 return false; 262 } 263 264 return true; 265 } 266 267 /** 268 * delete_track 269 * This must take an ID (as returned by our get function) 270 * and delete it from httpQ 271 * @param integer $object_id 272 * @return boolean 273 */ 274 public function delete_track($object_id) 275 { 276 if ($this->_httpq->delete_pos($object_id) === null) { 277 debug_event('httpq.controller', 'Unable to delete ' . $object_id . ' from httpQ', 1); 278 279 return false; 280 } 281 282 return true; 283 } // delete_track 284 285 /** 286 * clear_playlist 287 */ 288 public function clear_playlist() 289 { 290 if ($this->_httpq->clear() === null) { 291 return false; 292 } 293 294 // If the clear worked we should stop it! 295 $this->stop(); 296 297 return true; 298 } // clear_playlist 299 300 /** 301 * play 302 * This just tells httpQ to start playing, it does not 303 * take any arguments 304 */ 305 public function play() 306 { 307 // A play when it's already playing causes a track restart, so double check its state 308 if ($this->_httpq->state() == 'play') { 309 return true; 310 } 311 312 if ($this->_httpq->play() === null) { 313 return false; 314 } 315 316 return true; 317 } // play 318 319 /** 320 * stop 321 * This just tells httpQ to stop playing, it does not take 322 * any arguments 323 */ 324 public function stop() 325 { 326 if ($this->_httpq->stop() === null) { 327 return false; 328 } 329 330 return true; 331 } // stop 332 333 /** 334 * skip 335 * This tells httpQ to skip to the specified song 336 * @param $song 337 * @return boolean 338 */ 339 public function skip($song) 340 { 341 if ($this->_httpq->skip($song) === null) { 342 return false; 343 } 344 345 return true; 346 } // skip 347 348 /** 349 * This tells httpQ to increase the volume by WinAmps default amount 350 */ 351 public function volume_up() 352 { 353 return $this->_httpq->volume_up(); 354 } // volume_up 355 356 /** 357 * This tells httpQ to decrease the volume by Winamp's default amount 358 */ 359 public function volume_down() 360 { 361 return $this->_httpq->volume_down(); 362 } // volume_down 363 364 /** 365 * next 366 * This just tells httpQ to skip to the next song 367 */ 368 public function next() 369 { 370 if ($this->_httpq->next() === null) { 371 return false; 372 } 373 374 return true; 375 } // next 376 377 /** 378 * prev 379 * This just tells httpQ to skip to the prev song 380 */ 381 public function prev() 382 { 383 if ($this->_httpq->prev() === null) { 384 return false; 385 } 386 387 return true; 388 } // prev 389 390 /** 391 * pause 392 * This tells httpQ to pause the current song 393 */ 394 public function pause() 395 { 396 if ($this->_httpq->pause() === null) { 397 return false; 398 } 399 400 return true; 401 } // pause 402 403 /** 404 * volume 405 * This tells httpQ to set the volume to the specified amount this 406 * is 0-100 407 * @param $volume 408 * @return boolean 409 */ 410 public function volume($volume) 411 { 412 return $this->_httpq->set_volume($volume); 413 } // volume 414 415 /** 416 * repeat 417 * This tells httpQ to set the repeating the playlist (i.e. loop) to 418 * either on or off 419 * @param $state 420 * @return boolean 421 */ 422 public function repeat($state) 423 { 424 if ($this->_httpq->repeat($state) === null) { 425 return false; 426 } 427 428 return true; 429 } // repeat 430 431 /** 432 * random 433 * This tells httpQ to turn on or off the playing of songs from the 434 * playlist in random order 435 * @param $onoff 436 * @return boolean 437 */ 438 public function random($onoff) 439 { 440 if ($this->_httpq->random($onoff) === null) { 441 return false; 442 } 443 444 return true; 445 } // random 446 447 /** 448 * get 449 * This functions returns an array containing information about 450 * The songs that httpQ currently has in its playlist. This must be 451 * done in a standardized fashion 452 */ 453 public function get() 454 { 455 /* Get the Current Playlist */ 456 $list = $this->_httpq->get_tracks(); 457 458 if (!$list) { 459 return array(); 460 } 461 462 $songs = explode("::", $list); 463 $results = array(); 464 465 foreach ($songs as $key => $entry) { 466 $data = array(); 467 468 /* Required Elements */ 469 $data['id'] = $key; 470 $data['raw'] = $entry; 471 472 $url_data = $this->parse_url($entry); 473 switch ($url_data['primary_key']) { 474 case 'oid': 475 $data['oid'] = $url_data['oid']; 476 $song = new Song($data['oid']); 477 $song->format(); 478 $data['name'] = $song->f_title . ' - ' . $song->f_album . ' - ' . $song->f_artist; 479 $data['link'] = $song->f_link; 480 break; 481 case 'demo_id': 482 $democratic = new Democratic($url_data['demo_id']); 483 $data['name'] = T_('Democratic') . ' - ' . $democratic->name; 484 $data['link'] = ''; 485 break; 486 case 'random': 487 $data['name'] = T_('Random') . ' - ' . scrub_out(ucfirst($url_data['type'])); 488 $data['link'] = ''; 489 break; 490 default: 491 // If we don't know it, look up by filename 492 $filename = Dba::escape($entry['file']); 493 $sql = "SELECT `id`, 'song' AS `type` FROM `song` WHERE `file` LIKE '%$filename' UNION ALL SELECT `id`, 'live_stream' AS `type` FROM `live_stream` WHERE `url`='$filename' "; 494 495 $db_results = Dba::read($sql); 496 if ($row = Dba::fetch_assoc($db_results)) { 497 $class_name = ObjectTypeToClassNameMapper::map($row['type']); 498 $media = new $class_name($row['id']); 499 $media->format(); 500 switch ($row['type']) { 501 case 'song': 502 $data['name'] = $media->f_title . ' - ' . $media->f_album . ' - ' . $media->f_artist; 503 $data['link'] = $media->f_link; 504 break; 505 case 'live_stream': 506 $frequency = $media->frequency ? '[' . $media->frequency . ']' : ''; 507 $site_url = $media->site_url ? '(' . $media->site_url . ')' : ''; 508 $data['name'] = "$media->name $frequency $site_url"; 509 $data['link'] = $media->site_url; 510 break; 511 } // end switch on type 512 } else { 513 $data['name'] = basename($data['raw']); 514 $data['link'] = basename($data['raw']); 515 } 516 517 break; 518 } // end switch on primary key type 519 520 $data['track'] = $key + 1; 521 522 $results[] = $data; 523 } // foreach playlist items 524 525 return $results; 526 } // get 527 528 /** 529 * status 530 * This returns bool/int values for features, loop, repeat and any other features 531 * That this Localplay method supports. required function 532 * @return array 533 */ 534 public function status() 535 { 536 /* Construct the Array */ 537 $array['state'] = $this->_httpq->state(); 538 $array['volume'] = $this->_httpq->get_volume(); 539 $array['repeat'] = $this->_httpq->get_repeat(); 540 $array['random'] = $this->_httpq->get_random(); 541 $array['track'] = $this->_httpq->get_now_playing(); 542 $url_data = $this->parse_url($array['track']); 543 544 if (isset($url_data['oid'])) { 545 $song = new Song($url_data['oid']); 546 $array['track_title'] = $song->title; 547 $array['track_artist'] = $song->get_artist_name(); 548 $array['track_album'] = $song->get_album_name(); 549 } else { 550 $array['track_title'] = basename($array['track']); 551 } 552 553 return $array; 554 } // status 555 556 /** 557 * connect 558 * This functions creates the connection to httpQ and returns 559 * a boolean value for the status, to save time this handle 560 * is stored in this class 561 */ 562 public function connect() 563 { 564 $options = self::get_instance(); 565 $this->_httpq = new HttpQPlayer($options['host'], $options['password'], $options['port']); 566 567 // Test our connection by retrieving the version 568 if ($this->_httpq->version() !== null) { 569 return true; 570 } 571 572 return false; 573 } // connect 574} 575