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 22declare(strict_types=0); 23 24namespace Ampache\Repository\Model; 25 26use Ampache\Module\Playback\Stream; 27use Ampache\Module\Playback\Stream_Url; 28use Ampache\Module\Statistics\Stats; 29use Ampache\Module\System\Dba; 30use Ampache\Module\Util\Ui; 31use Ampache\Module\Util\UtilityFactoryInterface; 32use Ampache\Module\Util\VaInfo; 33use Ampache\Module\Authorization\Access; 34use Ampache\Config\AmpConfig; 35use Ampache\Module\System\Core; 36use PDOStatement; 37 38class Podcast_Episode extends database_object implements Media, library_item, GarbageCollectibleInterface 39{ 40 protected const DB_TABLENAME = 'podcast_episode'; 41 42 public $id; 43 public $title; 44 public $guid; 45 public $podcast; 46 public $state; 47 public $file; 48 public $source; 49 public $size; 50 public $time; 51 public $played; 52 public $type; 53 public $mime; 54 public $website; 55 public $description; 56 public $author; 57 public $category; 58 public $pubdate; 59 public $enabled; 60 public $object_cnt; 61 public $catalog; 62 public $f_title; 63 public $f_file; 64 public $f_size; 65 public $f_time; 66 public $f_time_h; 67 public $f_description; 68 public $f_author; 69 public $f_artist_full; 70 public $f_category; 71 public $f_website; 72 public $f_pubdate; 73 public $f_state; 74 public $link; 75 public $f_link; 76 public $f_podcast; 77 public $f_podcast_link; 78 private $total_count; 79 80 /** 81 * Constructor 82 * 83 * Podcast Episode class 84 * @param integer $episode_id 85 */ 86 public function __construct($episode_id = null) 87 { 88 if ($episode_id === null) { 89 return false; 90 } 91 92 $this->id = (int)$episode_id; 93 94 if ($info = $this->get_info($this->id)) { 95 foreach ($info as $key => $value) { 96 $this->$key = $value; 97 } 98 if (!empty($this->file)) { 99 $data = pathinfo($this->file); 100 $this->type = strtolower((string)$data['extension']); 101 $this->mime = Song::type_to_mime($this->type); 102 $this->enabled = true; 103 } 104 } else { 105 $this->id = null; 106 107 return false; 108 } 109 110 return true; 111 } // constructor 112 113 public function getId(): int 114 { 115 return (int) $this->id; 116 } 117 118 /** 119 * garbage_collection 120 * 121 * Cleans up the podcast_episode table 122 */ 123 public static function garbage_collection() 124 { 125 Dba::write("DELETE FROM `podcast_episode` USING `podcast_episode` LEFT JOIN `podcast` ON `podcast`.`id` = `podcast_episode`.`podcast` WHERE `podcast`.`id` IS NULL;"); 126 } 127 128 /** 129 * get_catalogs 130 * 131 * Get all catalog ids related to this item. 132 * @return integer[] 133 */ 134 public function get_catalogs() 135 { 136 return array($this->catalog); 137 } 138 139 /** 140 * format 141 * this function takes the object and formats some values 142 * @param boolean $details 143 * @return boolean 144 */ 145 public function format($details = true) 146 { 147 $this->f_title = scrub_out($this->title); 148 $this->f_description = scrub_out($this->description); 149 $this->f_category = scrub_out($this->category); 150 $this->f_author = scrub_out($this->author); 151 $this->f_artist_full = $this->f_author; 152 $this->f_website = scrub_out($this->website); 153 $this->f_pubdate = date("c", (int)$this->pubdate); 154 $this->f_state = ucfirst($this->state); 155 156 // Format the Time 157 $min = floor($this->time / 60); 158 $sec = sprintf("%02d", ($this->time % 60)); 159 $this->f_time = $min . ":" . $sec; 160 $hour = sprintf("%02d", floor($min / 60)); 161 $min_h = sprintf("%02d", ($min % 60)); 162 $this->f_time_h = $hour . ":" . $min_h . ":" . $sec; 163 // Format the Size 164 $this->f_size = Ui::format_bytes($this->size); 165 $this->f_file = $this->f_title . '.' . $this->type; 166 167 $this->link = AmpConfig::get('web_path') . '/podcast_episode.php?action=show&podcast_episode=' . $this->id; 168 $this->f_link = '<a href="' . $this->link . '" title="' . scrub_out($this->f_title) . '">' . scrub_out($this->f_title) . '</a>'; 169 170 if ($details) { 171 $podcast = new Podcast($this->podcast); 172 $podcast->format(); 173 $this->f_podcast = $podcast->f_title; 174 $this->f_podcast_link = $podcast->f_link; 175 $this->f_file = $this->f_podcast . ' - ' . $this->f_file; 176 } 177 if (AmpConfig::get('show_played_times')) { 178 $this->object_cnt = (int) $this->total_count; 179 } 180 181 return true; 182 } 183 184 /** 185 * @return array|mixed 186 */ 187 public function get_keywords() 188 { 189 $keywords = array(); 190 $keywords['podcast'] = array( 191 'important' => true, 192 'label' => T_('Podcast'), 193 'value' => $this->f_podcast 194 ); 195 $keywords['title'] = array( 196 'important' => true, 197 'label' => T_('Title'), 198 'value' => $this->f_title 199 ); 200 201 return $keywords; 202 } 203 204 /** 205 * @return string 206 */ 207 public function get_fullname() 208 { 209 return $this->f_title; 210 } 211 212 /** 213 * @return array 214 */ 215 public function get_parent() 216 { 217 return array('object_type' => 'podcast', 'object_id' => $this->podcast); 218 } 219 220 /** 221 * @return array 222 */ 223 public function get_childrens() 224 { 225 return array(); 226 } 227 228 /** 229 * @param string $name 230 * @return array 231 */ 232 public function search_childrens($name) 233 { 234 debug_event(self::class, 'search_childrens ' . $name, 5); 235 236 return array(); 237 } 238 239 /** 240 * @param string $filter_type 241 * @return array 242 */ 243 public function get_medias($filter_type = null) 244 { 245 $medias = array(); 246 if ($filter_type === null || $filter_type == 'podcast_episode') { 247 $medias[] = array( 248 'object_type' => 'podcast_episode', 249 'object_id' => $this->id 250 ); 251 } 252 253 return $medias; 254 } 255 256 /** 257 * @return mixed|null 258 */ 259 public function get_user_owner() 260 { 261 return null; 262 } 263 264 /** 265 * @return string 266 */ 267 public function get_default_art_kind() 268 { 269 return 'default'; 270 } 271 272 /** 273 * @return string 274 */ 275 public function get_description() 276 { 277 return $this->f_description; 278 } 279 280 /** 281 * display_art 282 * @param integer $thumb 283 * @param boolean $force 284 */ 285 public function display_art($thumb = 2, $force = false) 286 { 287 $episode_id = null; 288 $type = null; 289 290 if (Art::has_db($this->id, 'podcast_episode')) { 291 $episode_id = $this->id; 292 $type = 'podcast_episode'; 293 } else { 294 if (Art::has_db($this->podcast, 'podcast') || $force) { 295 $episode_id = $this->podcast; 296 $type = 'podcast'; 297 } 298 } 299 300 if ($episode_id !== null && $type !== null) { 301 Art::display($type, $episode_id, $this->get_fullname(), $thumb, $this->link); 302 } 303 } 304 305 /** 306 * update 307 * This takes a key'd array of data and updates the current podcast episode 308 * @param array $data 309 * @return integer 310 */ 311 public function update(array $data) 312 { 313 $title = isset($data['title']) ? $data['title'] : $this->title; 314 $website = isset($data['website']) ? $data['website'] : $this->website; 315 $description = isset($data['description']) ? $data['description'] : $this->description; 316 $author = isset($data['author']) ? $data['author'] : $this->author; 317 $category = isset($data['category']) ? $data['category'] : $this->category; 318 319 $sql = 'UPDATE `podcast_episode` SET `title` = ?, `website` = ?, `description` = ?, `author` = ?, `category` = ? WHERE `id` = ?'; 320 Dba::write($sql, array($title, $website, $description, $author, $category, $this->id)); 321 322 $this->title = $title; 323 $this->website = $website; 324 $this->description = $description; 325 $this->author = $author; 326 $this->category = $category; 327 328 return $this->id; 329 } 330 331 /** 332 * set_played 333 * this checks to see if the current object has been played 334 * if not then it sets it to played. In any case it updates stats. 335 * @param integer $user 336 * @param string $agent 337 * @param array $location 338 * @param integer $date 339 * @return boolean 340 */ 341 public function set_played($user, $agent, $location, $date = null) 342 { 343 // ignore duplicates or skip the last track 344 if (!$this->check_play_history($user, $agent, $date)) { 345 return false; 346 } 347 Stats::insert('podcast_episode', $this->id, $user, $agent, $location, 'stream', $date); 348 349 if (!$this->played) { 350 self::update_played(true, $this->id); 351 } 352 353 return true; 354 } // set_played 355 356 /** 357 * @param integer $user 358 * @param string $agent 359 * @param integer $date 360 * @return boolean 361 */ 362 public function check_play_history($user, $agent, $date) 363 { 364 return Stats::has_played_history('podcast_episode', $this, $user, $agent, $date); 365 } 366 367 /** 368 * update_played 369 * sets the played flag 370 * @param boolean $new_played 371 * @param integer $id 372 */ 373 public static function update_played($new_played, $id) 374 { 375 self::_update_item('played', ($new_played ? 1 : 0), $id, '25'); 376 } // update_played 377 378 /** 379 * _update_item 380 * This is a private function that should only be called from within the podcast episode class. 381 * It takes a field, value song_id and level. first and foremost it checks the level 382 * against Core::get_global('user') to make sure they are allowed to update this record 383 * it then updates it and sets $this->{$field} to the new value 384 * @param string $field 385 * @param integer $value 386 * @param integer $song_id 387 * @param integer $level 388 * @return boolean 389 */ 390 private static function _update_item($field, $value, $song_id, $level) 391 { 392 /* Check them Rights! */ 393 if (!Access::check('interface', $level)) { 394 return false; 395 } 396 397 /* Can't update to blank */ 398 if (!strlen(trim((string)$value))) { 399 return false; 400 } 401 402 $sql = "UPDATE `podcast_episode` SET `$field` = ? WHERE `id` = ?"; 403 Dba::write($sql, array($value, $song_id)); 404 405 return true; 406 } // _update_item 407 408 /** 409 * Get stream name. 410 * @return string 411 */ 412 public function get_stream_name() 413 { 414 return $this->f_podcast . " - " . $this->f_title; 415 } 416 417 /** 418 * Get transcode settings. 419 * @param string $target 420 * @param string $player 421 * @param array $options 422 * @return array 423 */ 424 public function get_transcode_settings($target = null, $player = null, $options = array()) 425 { 426 return Song::get_transcode_settings_for_media($this->type, $target, $player, 'song', $options); 427 } 428 429 /** 430 * play_url 431 * This function takes all the song information and correctly formats a 432 * a stream URL taking into account the downsmapling mojo and everything 433 * else, this is the true function 434 * @param string $additional_params 435 * @param string $player 436 * @param boolean $local 437 * @param int|string $uid 438 * @return string 439 */ 440 public function play_url($additional_params = '', $player = '', $local = false, $uid = false) 441 { 442 if (!$this->id) { 443 return ''; 444 } 445 if (!$uid) { 446 // No user in the case of upnp. Set to 0 instead. required to fix database insertion errors 447 $uid = Core::get_global('user')->id ?: 0; 448 } 449 // set no use when using auth 450 if (!AmpConfig::get('use_auth') && !AmpConfig::get('require_session')) { 451 $uid = -1; 452 } 453 454 $type = $this->type; 455 456 $this->format(); 457 $media_name = $this->get_stream_name() . "." . $type; 458 $media_name = preg_replace("/[^a-zA-Z0-9\. ]+/", "-", $media_name); 459 $media_name = rawurlencode($media_name); 460 461 $url = Stream::get_base_url($local) . "type=podcast_episode&oid=" . $this->id . "&uid=" . (string) $uid . '&format=raw' . $additional_params; 462 if ($player !== '') { 463 $url .= "&player=" . $player; 464 } 465 $url .= "&name=" . $media_name; 466 467 return Stream_Url::format($url); 468 } 469 470 /** 471 * Get stream types. 472 * @param string $player 473 * @return array 474 */ 475 public function get_stream_types($player = null) 476 { 477 return Song::get_stream_types_for_type($this->type, $player); 478 } 479 480 /** 481 * remove 482 * @return PDOStatement|boolean 483 */ 484 public function remove() 485 { 486 debug_event(self::class, 'Removing podcast episode ' . $this->id, 5); 487 488 if (AmpConfig::get('delete_from_disk') && !empty($this->file)) { 489 if (!unlink($this->file)) { 490 debug_event(self::class, 'Cannot delete file ' . $this->file, 3); 491 } 492 } 493 494 // keep details about deletions 495 $params = array($this->id); 496 $sql = "REPLACE INTO `deleted_podcast_episode` (`id`, `addition_time`, `delete_time`, `title`, `file`, `catalog`, `total_count`, `total_skip`, `podcast`) SELECT `id`, `addition_time`, UNIX_TIMESTAMP(), `title`, `file`, `catalog`, `total_count`, `total_skip`, `podcast` FROM `podcast_episode` WHERE `id` = ?;"; 497 Dba::write($sql, $params); 498 499 $sql = "DELETE FROM `podcast_episode` WHERE `id` = ?"; 500 501 return Dba::write($sql, $params); 502 } 503 504 /** 505 * change_state 506 * @param string $state 507 * @return PDOStatement|boolean 508 */ 509 public function change_state($state) 510 { 511 $sql = "UPDATE `podcast_episode` SET `state` = ? WHERE `id` = ?"; 512 513 return Dba::write($sql, array($state, $this->id)); 514 } 515 516 /** 517 * gather 518 * download the podcast episode to your catalog 519 */ 520 public function gather() 521 { 522 if (!empty($this->source)) { 523 $podcast = new Podcast($this->podcast); 524 $file = $podcast->get_root_path(); 525 if (!empty($file)) { 526 $pinfo = pathinfo($this->source); 527 528 $file .= DIRECTORY_SEPARATOR . $this->pubdate . '-' . str_replace(array('?', '<', '>', '\\', '/'), '_', $this->title) . '-' . strtok($pinfo['basename'], '?'); 529 debug_event(self::class, 'Downloading ' . $this->source . ' to ' . $file . ' ...', 4); 530 if (file_put_contents($file, fopen($this->source, 'r')) !== false) { 531 debug_event(self::class, 'Download completed.', 4); 532 $this->file = $file; 533 534 $vainfo = $this->getUtilityFactory()->createVaInfo($this->file); 535 $vainfo->get_info(); 536 $key = VaInfo::get_tag_type($vainfo->tags); 537 $infos = VaInfo::clean_tag_info($vainfo->tags, $key, $file); 538 // No time information, get it from file 539 if ($this->time < 1) { 540 $this->time = $infos['time']; 541 } 542 $this->size = $infos['size']; 543 544 $sql = "UPDATE `podcast_episode` SET `file` = ?, `size` = ?, `time` = ?, `state` = 'completed' WHERE `id` = ?"; 545 Dba::write($sql, array($this->file, $this->size, $this->time, $this->id)); 546 } else { 547 debug_event(self::class, 'Error when downloading podcast episode.', 1); 548 } 549 } 550 } else { 551 debug_event(self::class, 'Cannot download podcast episode ' . $this->id . ', empty source.', 3); 552 } 553 } 554 555 /** 556 * get_deleted 557 * get items from the deleted_podcast_episodes table 558 * @return int[] 559 */ 560 public static function get_deleted() 561 { 562 $deleted = array(); 563 $sql = "SELECT * FROM `deleted_podcast_episode`"; 564 $db_results = Dba::read($sql); 565 while ($row = Dba::fetch_assoc($db_results)) { 566 $deleted[] = $row; 567 } 568 569 return $deleted; 570 } // get_deleted 571 572 /** 573 * type_to_mime 574 * 575 * Returns the mime type for the specified file extension/type 576 * @param string $type 577 * @return string 578 */ 579 public static function type_to_mime($type) 580 { 581 return Song::type_to_mime($type); 582 } 583 584 /** 585 * @deprecated Inject by constructor 586 */ 587 private function getUtilityFactory(): UtilityFactoryInterface 588 { 589 global $dic; 590 591 return $dic->get(UtilityFactoryInterface::class); 592 } 593} 594