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 25namespace Ampache\Repository\Model; 26 27use Ampache\Module\Playback\Stream; 28use Ampache\Module\Playback\Stream_Url; 29use Ampache\Module\Statistics\Stats; 30use Ampache\Module\System\Dba; 31use Ampache\Module\Util\ObjectTypeToClassNameMapper; 32use Ampache\Config\AmpConfig; 33use Ampache\Module\System\Core; 34use PDOStatement; 35 36/** 37 * This class handles democratic play, which is a fancy 38 * name for voting based playback. 39 */ 40class Democratic extends Tmp_Playlist 41{ 42 protected const DB_TABLENAME = 'democratic'; 43 44 public $name; 45 public $cooldown; 46 public $level; 47 public $user; 48 public $primary; 49 public $base_playlist; 50 51 public $f_cooldown; 52 public $f_primary; 53 public $f_level; 54 55 // Build local, buy local 56 public $tmp_playlist; 57 public $object_ids = array(); 58 public $vote_ids = array(); 59 public $user_votes = array(); 60 61 /** 62 * constructor 63 * We need a constructor for this class. It does it's own thing now 64 * @param $democratic_id 65 */ 66 public function __construct($democratic_id) 67 { 68 parent::__construct($democratic_id); 69 70 $info = $this->get_info($democratic_id); 71 72 foreach ($info as $key => $value) { 73 $this->$key = $value; 74 } 75 } // constructor 76 77 public function getId(): int 78 { 79 return (int) $this->id; 80 } 81 82 /** 83 * build_vote_cache 84 * This builds a vote cache of the objects we've got in the playlist 85 * @param $ids 86 * @return boolean 87 */ 88 public static function build_vote_cache($ids) 89 { 90 if (!is_array($ids) || !count($ids)) { 91 return false; 92 } 93 94 $idlist = '(' . implode(',', $ids) . ')'; 95 $sql = "SELECT `object_id`, COUNT(`user`) AS `count` FROM `user_vote` WHERE `object_id` IN $idlist GROUP BY `object_id`"; 96 97 $db_results = Dba::read($sql); 98 99 while ($row = Dba::fetch_assoc($db_results)) { 100 parent::add_to_cache('democratic_vote', $row['object_id'], array($row['count'])); 101 } 102 103 return true; 104 } // build_vote_cache 105 106 /** 107 * is_enabled 108 * This function just returns true / false if the current democratic 109 * playlist is currently enabled / configured 110 */ 111 public function is_enabled() 112 { 113 if ($this->tmp_playlist) { 114 return true; 115 } 116 117 return false; 118 } // is_enabled 119 120 /** 121 * set_parent 122 * This returns the Tmp_Playlist for this democratic play instance 123 */ 124 public function set_parent() 125 { 126 $demo_id = Dba::escape($this->id); 127 128 $sql = "SELECT * FROM `tmp_playlist` WHERE `session`='$demo_id'"; 129 $db_results = Dba::read($sql); 130 131 $row = Dba::fetch_assoc($db_results); 132 133 $this->tmp_playlist = $row['id']; 134 } // set_parent 135 136 /** 137 * set_user_preferences 138 * This sets up a (or all) user(s) to use democratic play. This sets 139 * their play method and playlist method (clear on send) If no user is 140 * passed it does it for everyone and also locks down the ability to 141 * change to admins only 142 * 143 * @SuppressWarnings(PHPMD.UnusedFormalParameter) 144 */ 145 public static function set_user_preferences() 146 { 147 // FIXME: Code in single user stuff 148 $preference_id = Preference::id_from_name('play_type'); 149 Preference::update_level($preference_id, '75'); 150 Preference::update_all($preference_id, 'democratic'); 151 152 $allow_demo = Preference::id_from_name('allow_democratic_playback'); 153 Preference::update_all($allow_demo, '1'); 154 155 $play_method = Preference::id_from_name('playlist_method'); 156 Preference::update_all($play_method, 'clear'); 157 158 return true; 159 } // set_user_preferences 160 161 /** 162 * format 163 * This makes the variables pretty so that they can be displayed 164 */ 165 public function format() 166 { 167 $this->f_cooldown = $this->cooldown . ' ' . T_('minutes'); 168 $this->f_primary = $this->primary ? T_('Primary') : ''; 169 $this->f_level = User::access_level_to_name($this->level); 170 } // format 171 172 /** 173 * get_playlists 174 * This returns all of the current valid 'Democratic' Playlists that have been created. 175 */ 176 public static function get_playlists() 177 { 178 $sql = "SELECT `id` FROM `democratic` ORDER BY `name`"; 179 180 $db_results = Dba::read($sql); 181 $results = array(); 182 while ($row = Dba::fetch_assoc($db_results)) { 183 $results[] = (int)$row['id']; 184 } 185 186 return $results; 187 } // get_playlists 188 189 /** 190 * get_current_playlist 191 * This returns the current users current playlist, or if specified 192 * this current playlist of the user 193 */ 194 public static function get_current_playlist() 195 { 196 $democratic_id = AmpConfig::get('democratic_id'); 197 198 if (!$democratic_id) { 199 $level = Dba::escape(Core::get_global('user')->access); 200 $sql = "SELECT `id` FROM `democratic` WHERE `level` <= '$level' ORDER BY `level` DESC,`primary` DESC"; 201 $db_results = Dba::read($sql); 202 $row = Dba::fetch_assoc($db_results); 203 $democratic_id = $row['id']; 204 } 205 206 return new Democratic($democratic_id); 207 } // get_current_playlist 208 209 /** 210 * get_items 211 * This returns a sorted array of all object_ids in this Tmp_Playlist. 212 * The array is multidimensional; the inner array needs to contain the 213 * keys 'id', 'object_type' and 'object_id'. 214 * 215 * Sorting is highest to lowest vote count, then by oldest to newest 216 * vote activity. 217 * @param integer $limit 218 * @return array 219 */ 220 public function get_items($limit = null) 221 { 222 // Remove 'unconnected' users votes 223 if (AmpConfig::get('demo_clear_sessions')) { 224 $sql = 'DELETE FROM `user_vote` WHERE `user_vote`.`sid` NOT IN (SELECT `session`.`id` FROM `session`)'; 225 Dba::write($sql); 226 } 227 228 $sql = "SELECT `tmp_playlist_data`.`object_type`, `tmp_playlist_data`.`object_id`, `tmp_playlist_data`.`id` FROM `tmp_playlist_data` INNER JOIN `user_vote` ON `user_vote`.`object_id` = `tmp_playlist_data`.`id` WHERE `tmp_playlist_data`.`tmp_playlist` = '" . Dba::escape($this->tmp_playlist) . "' GROUP BY 1, 2, 3 ORDER BY COUNT(*) DESC, MAX(`user_vote`.`date`), MAX(`tmp_playlist_data`.`id`) "; 229 230 if ($limit !== null) { 231 $sql .= 'LIMIT ' . (string)($limit); 232 } 233 234 $db_results = Dba::read($sql); 235 $results = array(); 236 while ($row = Dba::fetch_assoc($db_results)) { 237 if ($row['id']) { 238 $results[] = $row; 239 } 240 } 241 242 return $results; 243 } // get_items 244 245 /** 246 * play_url 247 * This returns the special play URL for democratic play, only open to ADMINs 248 */ 249 public function play_url() 250 { 251 $link = Stream::get_base_url() . 'uid=' . scrub_out(Core::get_global('user')->id) . '&demo_id=' . scrub_out($this->id); 252 253 return Stream_Url::format($link); 254 } // play_url 255 256 /** 257 * get_next_object 258 * This returns the next object in the tmp_playlist. 259 * Most of the time this will just be the top entry, but if there is a 260 * base_playlist and no items in the playlist then it returns a random 261 * entry from the base_playlist 262 * @param integer $offset 263 * @return integer|null 264 */ 265 public function get_next_object($offset = 0) 266 { 267 // FIXME: Shouldn't this return object_type? 268 269 $offset = (int)($offset); 270 271 $items = $this->get_items($offset + 1); 272 273 if (count($items) > $offset) { 274 return $items[$offset]['object_id']; 275 } 276 277 // If nothing was found and this is a voting playlist then get 278 // from base_playlist 279 if ($this->base_playlist) { 280 $base_playlist = new Playlist($this->base_playlist); 281 $data = $base_playlist->get_random_items(1); 282 283 return $data[0]['object_id']; 284 } else { 285 $sql = "SELECT `id` FROM `song` WHERE `enabled`='1' ORDER BY RAND() LIMIT 1"; 286 $db_results = Dba::read($sql); 287 $results = Dba::fetch_assoc($db_results); 288 289 return $results['id']; 290 } 291 } // get_next_object 292 293 /** 294 * get_uid_from_object_id 295 * This takes an object_id and an object type and returns the ID for the row 296 * @param integer $object_id 297 * @param string $object_type 298 * @return mixed 299 */ 300 public function get_uid_from_object_id($object_id, $object_type = 'song') 301 { 302 $object_id = Dba::escape($object_id); 303 $object_type = Dba::escape($object_type); 304 $tmp_id = Dba::escape($this->tmp_playlist); 305 306 $sql = "SELECT `id` FROM `tmp_playlist_data` WHERE `object_type`='$object_type' AND `tmp_playlist`='$tmp_id' AND `object_id`='$object_id'"; 307 $db_results = Dba::read($sql); 308 309 $row = Dba::fetch_assoc($db_results); 310 311 return $row['id']; 312 } // get_uid_from_object_id 313 314 /** 315 * get_cool_songs 316 * This returns all of the song_ids for songs that have happened within 317 * the last 'cooldown' for this user. 318 */ 319 public function get_cool_songs() 320 { 321 // Convert cooldown time to a timestamp in the past 322 $cool_time = time() - ($this->cooldown * 60); 323 324 return Stats::get_object_history(Core::get_global('user')->id, $cool_time); 325 } // get_cool_songs 326 327 /** 328 * vote 329 * This function is called by users to vote on a system wide playlist 330 * This adds the specified objects to the tmp_playlist and adds a 'vote' 331 * by this user, naturally it checks to make sure that the user hasn't 332 * already voted on any of these objects 333 * @param $items 334 */ 335 public function add_vote($items) 336 { 337 /* Iterate through the objects if no vote, add to playlist and vote */ 338 foreach ($items as $element) { 339 $type = array_shift($element); 340 $object_id = array_shift($element); 341 if (!$this->has_vote($object_id, $type)) { 342 $this->_add_vote($object_id, $type); 343 } 344 } // end foreach 345 } // vote 346 347 /** 348 * has_vote 349 * This checks to see if the current user has already voted on this object 350 * @param integer $object_id 351 * @param string $type 352 * @return boolean 353 */ 354 public function has_vote($object_id, $type = 'song') 355 { 356 $params = array($type, $object_id, $this->tmp_playlist); 357 358 /* Query vote table */ 359 $sql = "SELECT `tmp_playlist_data`.`object_id` FROM `user_vote` INNER JOIN `tmp_playlist_data` ON `tmp_playlist_data`.`id`=`user_vote`.`object_id` WHERE `tmp_playlist_data`.`object_type` = ? AND `tmp_playlist_data`.`object_id` = ? AND `tmp_playlist_data`.`tmp_playlist` = ? "; 360 if (Core::get_global('user')->id > 0) { 361 $sql .= "AND `user_vote`.`user` = ? "; 362 $params[] = Core::get_global('user')->id; 363 } else { 364 $sql .= "AND `user_vote`.`sid` = ? "; 365 $params[] = session_id(); 366 } 367 $db_results = Dba::read($sql, $params); 368 369 /* If we find row, they've voted!! */ 370 if (Dba::num_rows($db_results)) { 371 return true; 372 } 373 374 return false; 375 } // has_vote 376 377 /** 378 * _add_vote 379 * This takes a object id and user and actually inserts the row 380 * @param integer $object_id 381 * @param string $object_type 382 * @return boolean 383 */ 384 private function _add_vote($object_id, $object_type = 'song') 385 { 386 if (!$this->tmp_playlist) { 387 return false; 388 } 389 390 $class_name = ObjectTypeToClassNameMapper::map($object_type); 391 $media = new $class_name($object_id); 392 $track = isset($media->track) ? (int)($media->track) : null; 393 394 /* If it's on the playlist just vote */ 395 $sql = "SELECT `id` FROM `tmp_playlist_data` WHERE `tmp_playlist_data`.`object_id` = ? AND `tmp_playlist_data`.`tmp_playlist` = ?"; 396 $db_results = Dba::write($sql, array($object_id, $this->tmp_playlist)); 397 398 /* If it's not there, add it and pull ID */ 399 if (!$results = Dba::fetch_assoc($db_results)) { 400 $sql = "INSERT INTO `tmp_playlist_data` (`tmp_playlist`, `object_id`, `object_type`, `track`) VALUES (?, ?, ?, ?)"; 401 Dba::write($sql, array($this->tmp_playlist, $object_id, $object_type, $track)); 402 $results['id'] = Dba::insert_id(); 403 } 404 405 /* Vote! */ 406 $time = time(); 407 $sql = "INSERT INTO user_vote (`user`, `object_id`, `date`, `sid`) VALUES (?, ?, ?, ?)"; 408 Dba::write($sql, array(Core::get_global('user')->id, $results['id'], $time, session_id())); 409 410 return true; 411 } 412 413 /** 414 * remove_vote 415 * This is called to remove a vote by a user for an object, it uses the object_id 416 * As that's what we'll have most the time, no need to check if they've got an existing 417 * vote for this, just remove anything that is there 418 * @param $row_id 419 * @return boolean 420 */ 421 public function remove_vote($row_id) 422 { 423 $sql = "DELETE FROM `user_vote` WHERE `object_id` = ? "; 424 $params = array($row_id); 425 if (Core::get_global('user')->id > 0) { 426 $sql .= "AND `user` = ?"; 427 $params[] = Core::get_global('user')->id; 428 } else { 429 $sql .= "AND `user_vote`.`sid` = ? "; 430 $params[] = session_id(); 431 } 432 Dba::write($sql, $params); 433 434 /* Clean up anything that has no votes */ 435 self::prune_tracks(); 436 437 return true; 438 } // remove_vote 439 440 /** 441 * delete_votes 442 * This removes the votes for the specified object on the current playlist 443 * @param $row_id 444 * @return boolean 445 */ 446 public function delete_votes($row_id) 447 { 448 $row_id = Dba::escape($row_id); 449 450 $sql = "DELETE FROM `user_vote` WHERE `object_id`='$row_id'"; 451 Dba::write($sql); 452 453 $sql = "DELETE FROM `tmp_playlist_data` WHERE `id`='$row_id'"; 454 Dba::write($sql); 455 456 return true; 457 } // delete_votes 458 459 /** 460 * delete_from_oid 461 * This takes an OID and type and removes the object from the democratic playlist 462 * @param integer $object_id 463 * @param string $object_type 464 * @return boolean 465 */ 466 public function delete_from_oid($object_id, $object_type) 467 { 468 $row_id = $this->get_uid_from_object_id($object_id, $object_type); 469 if ($row_id) { 470 debug_event(self::class, 'Removing Votes for ' . $object_id . ' of type ' . $object_type, 5); 471 $this->delete_votes($row_id); 472 } else { 473 debug_event(self::class, 'Unable to find Votes for ' . $object_id . ' of type ' . $object_type, 3); 474 } 475 476 return true; 477 } // delete_from_oid 478 479 /** 480 * delete 481 * This deletes a democratic playlist 482 * @param integer $democratic_id 483 * @return boolean 484 */ 485 public static function delete($democratic_id) 486 { 487 $democratic_id = Dba::escape($democratic_id); 488 489 $sql = "DELETE FROM `democratic` WHERE `id`='$democratic_id'"; 490 Dba::write($sql); 491 492 $sql = "DELETE FROM `tmp_playlist` WHERE `session`='$democratic_id'"; 493 Dba::write($sql); 494 495 self::prune_tracks(); 496 497 return true; 498 } // delete 499 500 /** 501 * update 502 * This updates an existing democratic playlist item. It takes a key'd array just like the create 503 * @param array $data 504 * @return boolean 505 */ 506 public function update(array $data) 507 { 508 $name = Dba::escape($data['name']); 509 $base = (int)Dba::escape($data['democratic']); 510 $cool = (int)Dba::escape($data['cooldown']); 511 $level = (int)Dba::escape($data['level']); 512 $default = (int)Dba::escape($data['make_default']); 513 $demo_id = (int)Dba::escape($this->id); 514 515 // no negative ints, this also gives you over 2 million days... 516 if ($cool < 0 || $cool > 3000000000) { 517 return false; 518 } 519 520 $sql = "UPDATE `democratic` SET `name` = ?, `base_playlist` = ?,`cooldown` = ?, `primary` = ?, `level` = ? WHERE `id` = ?"; 521 Dba::write($sql, array($name, $base, $cool, $default, $level, $demo_id)); 522 523 return true; 524 } // update 525 526 /** 527 * create 528 * This is the democratic play create function it inserts this into the democratic table 529 * @param array $data 530 * @return PDOStatement|boolean 531 */ 532 public static function create($data) 533 { 534 // Clean up the input 535 $name = Dba::escape($data['name']); 536 $base = (int)Dba::escape($data['democratic']); 537 $cool = (int)Dba::escape($data['cooldown']); 538 $level = (int)Dba::escape($data['level']); 539 $default = (int)Dba::escape($data['make_default']); 540 $user = (int)Dba::escape(Core::get_global('user')->id); 541 542 $sql = "INSERT INTO `democratic` (`name`, `base_playlist`, `cooldown`, `level`, `user`, `primary`) VALUES ('$name', $base, $cool, $level, $user, $default)"; 543 $db_results = Dba::write($sql); 544 545 if ($db_results) { 546 $insert_id = Dba::insert_id(); 547 parent::create(array( 548 'session_id' => $insert_id, 549 'type' => 'vote', 550 'object_type' => 'song' 551 )); 552 } 553 554 return $db_results; 555 } // create 556 557 /** 558 * prune_tracks 559 * This replaces the normal prune tracks and correctly removes the votes 560 * as well 561 */ 562 public static function prune_tracks() 563 { 564 // This deletes data without votes, if it's a voting democratic playlist 565 $sql = "DELETE FROM `tmp_playlist_data` USING `tmp_playlist_data` LEFT JOIN `user_vote` ON `tmp_playlist_data`.`id`=`user_vote`.`object_id` LEFT JOIN `tmp_playlist` ON `tmp_playlist`.`id`=`tmp_playlist_data`.`tmp_playlist` WHERE `user_vote`.`object_id` IS NULL AND `tmp_playlist`.`type` = 'vote'"; 566 Dba::write($sql); 567 568 return true; 569 } // prune_tracks 570 571 /** 572 * clear 573 * This is really just a wrapper function, it clears the entire playlist 574 * including all votes etc. 575 * @return boolean 576 */ 577 public function clear() 578 { 579 $tmp_id = Dba::escape($this->tmp_playlist); 580 581 if ((int)$tmp_id > 0) { 582 /* Clear all votes then prune */ 583 $sql = "DELETE FROM `user_vote` USING `user_vote` LEFT JOIN `tmp_playlist_data` ON `user_vote`.`object_id` = `tmp_playlist_data`.`id` WHERE `tmp_playlist_data`.`tmp_playlist`='$tmp_id'"; 584 Dba::write($sql); 585 } 586 587 // Prune! 588 self::prune_tracks(); 589 590 // Clean the votes 591 $this->clear_votes(); 592 593 return true; 594 } // clear_playlist 595 596 /** 597 * clean_votes 598 * This removes in left over garbage in the votes table 599 * @return boolean 600 */ 601 public function clear_votes() 602 { 603 $sql = "DELETE FROM `user_vote` USING `user_vote` LEFT JOIN `tmp_playlist_data` ON `user_vote`.`object_id`=`tmp_playlist_data`.`id` WHERE `tmp_playlist_data`.`id` IS NULL"; 604 Dba::write($sql); 605 606 return true; 607 } // clear_votes 608 609 /** 610 * get_vote 611 * This returns the current count for a specific song 612 * @param integer $id 613 * @return integer 614 */ 615 public function get_vote($id) 616 { 617 if (parent::is_cached('democratic_vote', $id)) { 618 return (int)(parent::get_from_cache('democratic_vote', $id))[0]; 619 } 620 621 $sql = "SELECT COUNT(`user`) AS `count` FROM `user_vote` WHERE `object_id` = ?"; 622 $db_results = Dba::read($sql, array($id)); 623 624 $results = Dba::fetch_assoc($db_results); 625 parent::add_to_cache('democratic_vote', $id, $results); 626 627 return (int)$results['count']; 628 } // get_vote 629} 630