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\Config\AmpConfig; 27use Ampache\Config\ConfigContainerInterface; 28use Ampache\Config\ConfigurationKeyEnum; 29use Ampache\Module\Art\Collector\ArtCollectorInterface; 30use Ampache\Module\Authorization\Access; 31use Ampache\Module\Catalog\Catalog_beets; 32use Ampache\Module\Catalog\Catalog_beetsremote; 33use Ampache\Module\Catalog\Catalog_dropbox; 34use Ampache\Module\Catalog\Catalog_local; 35use Ampache\Module\Catalog\Catalog_remote; 36use Ampache\Module\Catalog\Catalog_Seafile; 37use Ampache\Module\Catalog\Catalog_soundcloud; 38use Ampache\Module\Catalog\Catalog_subsonic; 39use Ampache\Module\Catalog\GarbageCollector\CatalogGarbageCollectorInterface; 40use Ampache\Module\Playback\Stream_Url; 41use Ampache\Module\Song\Tag\SongTagWriterInterface; 42use Ampache\Module\Statistics\Stats; 43use Ampache\Module\System\AmpError; 44use Ampache\Module\System\Core; 45use Ampache\Module\System\Dba; 46use Ampache\Module\Util\ObjectTypeToClassNameMapper; 47use Ampache\Module\Util\Recommendation; 48use Ampache\Module\Util\Ui; 49use Ampache\Module\Util\UtilityFactoryInterface; 50use Ampache\Module\Util\VaInfo; 51use Ampache\Repository\AlbumRepositoryInterface; 52use Ampache\Repository\LabelRepositoryInterface; 53use Ampache\Repository\LicenseRepositoryInterface; 54use Ampache\Repository\Model\Metadata\Repository\Metadata; 55use Ampache\Repository\SongRepositoryInterface; 56use Ampache\Repository\UserRepositoryInterface; 57use Exception; 58use PDOStatement; 59use ReflectionException; 60 61/** 62 * This class handles all actual work in regards to the catalog, 63 * it contains functions for creating/listing/updated the catalogs. 64 */ 65abstract class Catalog extends database_object 66{ 67 protected const DB_TABLENAME = 'catalog'; 68 69 private const CATALOG_TYPES = [ 70 'beets' => Catalog_beets::class, 71 'beetsremote' => Catalog_beetsremote::class, 72 'dropbox' => Catalog_dropbox::class, 73 'local' => Catalog_local::class, 74 'remote' => Catalog_remote::class, 75 'seafile' => Catalog_Seafile::class, 76 'soundcloud' => Catalog_soundcloud::class, 77 'subsonic' => Catalog_subsonic::class, 78 ]; 79 80 /** 81 * @var integer $id 82 */ 83 public $id; 84 /** 85 * @var string $name 86 */ 87 public $name; 88 /** 89 * @var integer $last_update 90 */ 91 public $last_update; 92 /** 93 * @var integer $last_add 94 */ 95 public $last_add; 96 /** 97 * @var integer $last_clean 98 */ 99 public $last_clean; 100 /** 101 * @var string $key 102 */ 103 public $key; 104 /** 105 * @var string $rename_pattern 106 */ 107 public $rename_pattern; 108 /** 109 * @var string $sort_pattern 110 */ 111 public $sort_pattern; 112 /** 113 * @var string $catalog_type 114 */ 115 public $catalog_type; 116 /** 117 * @var string $gather_types 118 */ 119 public $gather_types; 120 /** 121 * @var integer $filter_user 122 */ 123 public $filter_user; 124 125 /** 126 * @var string $f_name 127 */ 128 public $f_name; 129 /** 130 * @var string $link 131 */ 132 public $link; 133 /** 134 * @var string $f_link 135 */ 136 public $f_link; 137 /** 138 * @var string $f_update 139 */ 140 public $f_update; 141 /** 142 * @var string $f_add 143 */ 144 public $f_add; 145 /** 146 * @var string $f_clean 147 */ 148 public $f_clean; 149 /** 150 * alias for catalog paths, urls, etc etc 151 * @var string $f_full_info 152 */ 153 public $f_full_info; 154 /** 155 * alias for catalog paths, urls, etc etc 156 * @var string $f_info 157 */ 158 public $f_info; 159 /** 160 * @var integer $enabled 161 */ 162 public $enabled; 163 /** 164 * @var string $f_filter_user 165 */ 166 public $f_filter_user; 167 168 /** 169 * This is a private var that's used during catalog builds 170 * @var array $_playlists 171 */ 172 protected $_playlists = array(); 173 174 /** 175 * Cache all files in catalog for quick lookup during add 176 * @var array $_filecache 177 */ 178 protected $_filecache = array(); 179 180 // Used in functions 181 /** 182 * @var array $albums 183 */ 184 protected static $albums = array(); 185 /** 186 * @var array $artists 187 */ 188 protected static $artists = array(); 189 /** 190 * @var array $tags 191 */ 192 protected static $tags = array(); 193 194 /** 195 * @return string 196 */ 197 abstract public function get_type(); 198 199 /** 200 * @return string 201 */ 202 abstract public function get_description(); 203 204 /** 205 * @return string 206 */ 207 abstract public function get_version(); 208 209 /** 210 * @return string 211 */ 212 abstract public function get_create_help(); 213 214 /** 215 * @return boolean 216 */ 217 abstract public function is_installed(); 218 219 /** 220 * @return boolean 221 */ 222 abstract public function install(); 223 224 /** 225 * @param array $options 226 * @return mixed 227 */ 228 abstract public function add_to_catalog($options = null); 229 230 /** 231 * @return mixed 232 */ 233 abstract public function verify_catalog_proc(); 234 235 /** 236 * @return int 237 */ 238 abstract public function clean_catalog_proc(); 239 240 /** 241 * @param string $new_path 242 * @return boolean 243 */ 244 abstract public function move_catalog_proc($new_path); 245 246 /** 247 * @return boolean 248 */ 249 abstract public function cache_catalog_proc(); 250 251 /** 252 * @return array 253 */ 254 abstract public function catalog_fields(); 255 256 /** 257 * @param string $file_path 258 * @return string 259 */ 260 abstract public function get_rel_path($file_path); 261 262 /** 263 * @param Song|Podcast_Episode|Song_Preview|Video $media 264 * @return Media|null 265 */ 266 abstract public function prepare_media($media); 267 268 public function getId(): int 269 { 270 return (int) $this->id; 271 } 272 273 /** 274 * Check if the catalog is ready to perform actions (configuration completed, ...) 275 * @return boolean 276 */ 277 public function isReady() 278 { 279 return true; 280 } 281 282 /** 283 * Show a message to make the catalog ready. 284 */ 285 public function show_ready_process() 286 { 287 // Do nothing. 288 } 289 290 /** 291 * Perform the last step process to make the catalog ready. 292 */ 293 public function perform_ready() 294 { 295 // Do nothing. 296 } 297 298 /** 299 * uninstall 300 * This removes the remote catalog 301 * @return boolean 302 */ 303 public function uninstall() 304 { 305 $sql = "DELETE FROM `catalog` WHERE `catalog_type` = ?"; 306 Dba::query($sql, array($this->get_type())); 307 308 $sql = "DROP TABLE `catalog_" . $this->get_type() . "`"; 309 Dba::query($sql); 310 311 return true; 312 } // uninstall 313 314 /** 315 * Create a catalog from its id. 316 * @param integer $catalog_id 317 * @return Catalog|null 318 */ 319 public static function create_from_id($catalog_id) 320 { 321 $sql = 'SELECT `catalog_type` FROM `catalog` WHERE `id` = ?'; 322 $db_results = Dba::read($sql, array($catalog_id)); 323 $results = Dba::fetch_assoc($db_results); 324 325 return self::create_catalog_type($results['catalog_type'], $catalog_id); 326 } 327 328 /** 329 * create_catalog_type 330 * This function attempts to create a catalog type 331 * @param string $type 332 * @param integer $catalog_id 333 * @return Catalog|null 334 */ 335 public static function create_catalog_type($type, $catalog_id = 0) 336 { 337 if (!$type) { 338 return null; 339 } 340 341 $controller = self::CATALOG_TYPES[$type] ?? null; 342 343 if ($controller === null) { 344 /* Throw Error Here */ 345 debug_event(self::class, 'Unable to load ' . $type . ' catalog type', 2); 346 347 return null; 348 } // include 349 if ($catalog_id > 0) { 350 $catalog = new $controller($catalog_id); 351 } else { 352 $catalog = new $controller(); 353 } 354 if (!($catalog instanceof Catalog)) { 355 debug_event(__CLASS__, $type . ' not an instance of Catalog abstract, unable to load', 1); 356 357 return null; 358 } 359 // identify if it's actually enabled 360 $sql = 'SELECT `enabled` FROM `catalog` WHERE `id` = ?'; 361 $db_results = Dba::read($sql, array($catalog->id)); 362 363 while ($results = Dba::fetch_assoc($db_results)) { 364 $catalog->enabled = $results['enabled']; 365 } 366 367 return $catalog; 368 } 369 370 /** 371 * Show dropdown catalog types. 372 * @param string $divback 373 */ 374 public static function show_catalog_types($divback = 'catalog_type_fields') 375 { 376 echo '<script>' . "var type_fields = new Array();type_fields['none'] = '';"; 377 $seltypes = '<option value="none">[' . T_("Select") . ']</option>'; 378 $types = self::get_catalog_types(); 379 foreach ($types as $type) { 380 $catalog = self::create_catalog_type($type); 381 if ($catalog->is_installed()) { 382 $seltypes .= '<option value="' . $type . '">' . $type . '</option>'; 383 echo "type_fields['" . $type . "'] = \""; 384 $fields = $catalog->catalog_fields(); 385 $help = $catalog->get_create_help(); 386 if (!empty($help)) { 387 echo "<tr><td></td><td>" . $help . "</td></tr>"; 388 } 389 foreach ($fields as $key => $field) { 390 echo "<tr><td style='width: 25%;'>" . $field['description'] . ":</td><td>"; 391 392 switch ($field['type']) { 393 case 'checkbox': 394 echo "<input type='checkbox' name='" . $key . "' value='1' " . (($field['value']) ? 'checked' : '') . "/>"; 395 break; 396 default: 397 echo "<input type='" . $field['type'] . "' name='" . $key . "' value='" . $field['value'] . "' />"; 398 break; 399 } 400 echo "</td></tr>"; 401 } 402 echo "\";"; 403 } 404 } 405 406 echo "function catalogTypeChanged() {var sel = document.getElementById('catalog_type');var seltype = sel.options[sel.selectedIndex].value;var ftbl = document.getElementById('" . $divback . "');ftbl.innerHTML = '<table class=\"tabledata\">' + type_fields[seltype] + '</table>';} </script><select name=\"type\" id=\"catalog_type\" onChange=\"catalogTypeChanged();\">" . $seltypes . "</select>"; 407 } 408 409 /** 410 * get_catalog_types 411 * This returns the catalog types that are available 412 * @return string[] 413 */ 414 public static function get_catalog_types() 415 { 416 return array_keys(self::CATALOG_TYPES); 417 } 418 419 /** 420 * Check if a file is an audio. 421 * @param string $file 422 * @return boolean 423 */ 424 public static function is_audio_file($file) 425 { 426 $ignore_pattern = AmpConfig::get('catalog_ignore_pattern'); 427 $ignore_check = !($ignore_pattern) || preg_match("/(" . $ignore_pattern . ")/i", $file) === 0; 428 $file_pattern = AmpConfig::get('catalog_file_pattern'); 429 $pattern = "/\.(" . $file_pattern . ")$/i"; 430 431 return ($ignore_check && preg_match($pattern, $file)); 432 } 433 434 /** 435 * Check if a file is a video. 436 * @param string $file 437 * @return boolean 438 */ 439 public static function is_video_file($file) 440 { 441 $ignore_pattern = AmpConfig::get('catalog_ignore_pattern'); 442 $ignore_check = !($ignore_pattern) || preg_match("/(" . $ignore_pattern . ")/i", $file) === 0; 443 $video_pattern = "/\.(" . AmpConfig::get('catalog_video_pattern') . ")$/i"; 444 445 return ($ignore_check && preg_match($video_pattern, $file)); 446 } 447 448 /** 449 * Check if a file is a playlist. 450 * @param string $file 451 * @return integer 452 */ 453 public static function is_playlist_file($file) 454 { 455 $ignore_pattern = AmpConfig::get('catalog_ignore_pattern'); 456 $ignore_check = !($ignore_pattern) || preg_match("/(" . $ignore_pattern . ")/i", $file) === 0; 457 $playlist_pattern = "/\.(" . AmpConfig::get('catalog_playlist_pattern') . ")$/i"; 458 459 return ($ignore_check && preg_match($playlist_pattern, $file)); 460 } 461 462 /** 463 * Get catalog info from table. 464 * @param integer $object_id 465 * @param string $table_name 466 * @return array 467 */ 468 public function get_info($object_id, $table_name = 'catalog') 469 { 470 $info = parent::get_info($object_id, $table_name); 471 472 $table = 'catalog_' . $this->get_type(); 473 $sql = "SELECT `id` FROM `$table` WHERE `catalog_id` = ?"; 474 $db_results = Dba::read($sql, array($object_id)); 475 476 if ($results = Dba::fetch_assoc($db_results)) { 477 $info_type = parent::get_info($results['id'], $table); 478 foreach ($info_type as $key => $value) { 479 if (!$info[$key]) { 480 $info[$key] = $value; 481 } 482 } 483 } 484 485 return $info; 486 } 487 488 /** 489 * Get enable sql filter; 490 * @param string $type 491 * @param string $catalog_id 492 * @return string 493 */ 494 public static function get_enable_filter($type, $catalog_id) 495 { 496 $sql = ""; 497 if ($type == "song" || $type == "album" || $type == "artist") { 498 if ($type == "song") { 499 $type = "id"; 500 } 501 $sql = "(SELECT COUNT(`song_dis`.`id`) FROM `song` AS `song_dis` LEFT JOIN `catalog` AS `catalog_dis` ON `catalog_dis`.`id` = `song_dis`.`catalog` WHERE `song_dis`.`" . $type . "`=" . $catalog_id . " AND `catalog_dis`.`enabled` = '1' GROUP BY `song_dis`.`" . $type . "`) > 0"; 502 } elseif ($type == "video") { 503 $sql = "(SELECT COUNT(`video_dis`.`id`) FROM `video` AS `video_dis` LEFT JOIN `catalog` AS `catalog_dis` ON `catalog_dis`.`id` = `video_dis`.`catalog` WHERE `video_dis`.`id`=" . $catalog_id . " AND `catalog_dis`.`enabled` = '1' GROUP BY `video_dis`.`id`) > 0"; 504 } 505 506 return $sql; 507 } 508 509 /** 510 * Get filter_user sql filter; 511 * @param string $type 512 * @param integer $user_id 513 * @return string 514 */ 515 public static function get_user_filter($type, $user_id) 516 { 517 switch ($type) { 518 case "video": 519 case "artist": 520 case "album": 521 case "song": 522 case "podcast": 523 case "podcast_episode": 524 case "live_stream": 525 $sql = " `$type`.`id` IN (SELECT `object_id` FROM `catalog_map` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog_map`.`object_type` = '$type' AND `catalog`.`filter_user` IN (0, $user_id) GROUP BY `catalog_map`.`object_id`) "; 526 break; 527 case "song_artist": 528 case "song_album": 529 $type = str_replace('song_', '', (string) $type); 530 $sql = " `song`.`$type` IN (SELECT `object_id` FROM `catalog_map` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog_map`.`object_type` = '$type' AND `catalog`.`filter_user` IN (0, $user_id) GROUP BY `catalog_map`.`object_id`) "; 531 break; 532 case "album_artist": 533 $sql = " `song`.`$type` IN (SELECT `object_id` FROM `catalog_map` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog_map`.`object_type` = '$type' AND `catalog`.`filter_user` IN (0, $user_id) GROUP BY `catalog_map`.`object_id`) "; 534 break; 535 case "label": 536 $sql = " `label`.`id` IN (SELECT `label` FROM `label_asso` LEFT JOIN `artist` ON `label_asso`.`artist` = `artist`.`id` LEFT JOIN `catalog_map` ON `catalog_map`.`object_type` = 'artist' AND `catalog_map`.`object_id` = `artist`.`id` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog_map`.`object_type` = 'artist' AND `catalog`.`filter_user` IN (0, $user_id) GROUP BY `label_asso`.`label`) "; 537 break; 538 case "playlist": 539 $sql = " `playlist`.`id` IN (SELECT `playlist` FROM `playlist_data` LEFT JOIN `song` ON `playlist_data`.`object_id` = `song`.`id` AND `playlist_data`.`object_type` = 'song' LEFT JOIN `catalog_map` ON `catalog_map`.`object_type` = 'song' AND `catalog_map`.`object_id` = `song`.`id` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog_map`.`object_type` = 'song' AND `catalog`.`filter_user` IN (0, $user_id) GROUP BY `playlist_data`.`playlist`) "; 540 break; 541 case "share": 542 $sql = " `share`.`object_id` IN (SELECT `share`.`object_id` FROM `share` LEFT JOIN `catalog_map` ON `share`.`object_type` = `catalog_map`.`object_type` AND `share`.`object_id` = `catalog_map`.`object_id` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog`.`filter_user` IN (0, $user_id) GROUP BY `share`.`object_id`, `share`.`object_type`) "; 543 break; 544 case "tag": 545 $sql = " `tag`.`id` IN (SELECT `tag_id` FROM `tag_map` LEFT JOIN `catalog_map` ON `catalog_map`.`object_type` = `tag_map`.`object_type` AND `catalog_map`.`object_id` = `tag_map`.`object_id` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog`.`filter_user` IN (0, $user_id) GROUP BY `tag_map`.`tag_id`) "; 546 break; 547 case 'tvshow': 548 $sql = " `tvshow`.`id` IN (SELECT `tvshow` FROM `tvshow_season` LEFT JOIN `tvshow_episode` ON `tvshow_episode`.`season` = `tvshow_season`.`id` LEFT JOIN `video` ON `tvshow_episode`.`id` = `video`.`id` LEFT JOIN `catalog_map` ON `catalog_map`.`object_type` = 'video' AND `catalog_map`.`object_id` = `video`.`id` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog`.`filter_user` IN (0, $user_id) GROUP BY `tvshow_season`.`tvshow`) "; 549 break; 550 case 'tvshow_season': 551 $sql = " `tvshow_season`.`tvshow` IN (SELECT `season` FROM `tvshow_episode` LEFT JOIN `video` ON `tvshow_episode`.`id` = `video`.`id` LEFT JOIN `catalog_map` ON `catalog_map`.`object_type` = 'video' AND `catalog_map`.`object_id` = `video`.`id` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog`.`filter_user` IN (0, $user_id) GROUP BY `tvshow_episode`.`season`) "; 552 break; 553 case 'tvshow_episode': 554 case 'movie': 555 case 'personal_video': 556 case 'clip': 557 $sql = " `$type`.`id` IN (SELECT `video`.`id` FROM `video` LEFT JOIN `catalog_map` ON `catalog_map`.`object_type` = 'video' AND `catalog_map`.`object_id` = `video`.`id` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog`.`filter_user` IN (0, $user_id) GROUP BY `video`.`id`) "; 558 break; 559 case "object_count_artist": 560 case "object_count_album": 561 case "object_count_song": 562 case "object_count_podcast_episode": 563 case "object_count_video": 564 $type = str_replace('object_count_', '', (string) $type); 565 $sql = " `object_count`.`object_id` IN (SELECT `catalog_map`.`object_id` FROM `catalog_map` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog_map`.`object_type` = '$type' AND `catalog`.`filter_user` IN (0, $user_id) GROUP BY `catalog_map`.`object_id`) "; 566 break; 567 case "rating_artist": 568 case "rating_album": 569 case "rating_song": 570 case "rating_video": 571 case "rating_podcast_episode": 572 $type = str_replace('rating_', '', (string) $type); 573 $sql = " `rating`.`object_id` IN (SELECT `catalog_map`.`object_id` FROM `catalog_map` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog_map`.`object_type` = '$type' AND `catalog`.`filter_user` IN (0, $user_id) GROUP BY `catalog_map`.`object_id`) "; 574 break; 575 case "user_flag_artist": 576 case "user_flag_album": 577 case "user_flag_song": 578 case "user_flag_video": 579 case "user_flag_podcast_episode": 580 $type = str_replace('user_flag_', '', (string) $type); 581 $sql = " `user_flag`.`object_id` IN (SELECT `catalog_map`.`object_id` FROM `catalog_map` LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog_map`.`object_type` = '$type' AND `catalog`.`filter_user` IN (0, $user_id) GROUP BY `catalog_map`.`object_id`) "; 582 break; 583 case "rating_playlist": 584 $sql = " `rating`.`object_id` IN (SELECT DISTINCT(`playlist`.`id`) FROM `playlist` LEFT JOIN `playlist_data` ON `playlist_data`.`playlist` = `playlist`.`id` LEFT JOIN `catalog_map` ON `playlist_data`.`object_id` = `catalog_map`.`object_id` AND `playlist_data`.`object_type` = 'song' LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog`.`filter_user` IN (0, $user_id) GROUP BY `playlist`.`id`) "; 585 break; 586 case "user_flag_playlist": 587 $sql = " `user_flag`.`object_id` IN (SELECT DISTINCT(`playlist`.`id`) FROM `playlist` LEFT JOIN `playlist_data` ON `playlist_data`.`playlist` = `playlist`.`id` LEFT JOIN `catalog_map` ON `playlist_data`.`object_id` = `catalog_map`.`object_id` AND `playlist_data`.`object_type` = 'song' LEFT JOIN `catalog` ON `catalog_map`.`catalog_id` = `catalog`.`id` WHERE `catalog`.`filter_user` IN (0, $user_id) GROUP BY `playlist`.`id`) "; 588 break; 589 case "catalog": 590 $sql = " `catalog`.`filter_user` IN (0, $user_id) "; 591 break; 592 default: 593 $sql = ""; 594 } 595 596 return $sql; 597 } 598 599 /** 600 * _create_filecache 601 * 602 * This populates an array which is used to speed up the add process. 603 * @return boolean 604 */ 605 protected function _create_filecache() 606 { 607 if (count($this->_filecache) == 0) { 608 // Get _EVERYTHING_ 609 $sql = 'SELECT `id`, `file` FROM `song` WHERE `catalog` = ?'; 610 $db_results = Dba::read($sql, array($this->id)); 611 612 // Populate the filecache 613 while ($results = Dba::fetch_assoc($db_results)) { 614 $this->_filecache[strtolower((string)$results['file'])] = $results['id']; 615 } 616 617 $sql = 'SELECT `id`, `file` FROM `video` WHERE `catalog` = ?'; 618 $db_results = Dba::read($sql, array($this->id)); 619 620 while ($results = Dba::fetch_assoc($db_results)) { 621 $this->_filecache[strtolower((string)$results['file'])] = 'v_' . $results['id']; 622 } 623 } 624 625 return true; 626 } 627 628 /** 629 * get_count 630 * 631 * return the counts from update info to speed up responses 632 * @param string $table 633 * @return integer 634 */ 635 public static function get_count(string $table) 636 { 637 if ($table == 'playlist' || $table == 'search') { 638 $sql = "SELECT 'playlist' AS `key`, SUM(value) AS `value` FROM `update_info` WHERE `key` IN ('playlist', 'search')"; 639 $db_results = Dba::read($sql); 640 } else { 641 $sql = "SELECT * FROM `update_info` WHERE `key` = ?"; 642 $db_results = Dba::read($sql, array($table)); 643 } 644 $results = Dba::fetch_assoc($db_results); 645 646 return (int) $results['value']; 647 } // get_count 648 649 /** 650 * set_count 651 * 652 * write the total_counts to update_info 653 * @param string $table 654 * @param int $value 655 */ 656 public static function set_count(string $table, int $value) 657 { 658 Dba::write("REPLACE INTO `update_info` SET `key`= ?, `value`= ?;", array($table, $value)); 659 } // set_count 660 661 /** 662 * update_enabled 663 * sets the enabled flag 664 * @param string $new_enabled 665 * @param integer $catalog_id 666 */ 667 public static function update_enabled($new_enabled, $catalog_id) 668 { 669 self::_update_item('enabled', make_bool($new_enabled), $catalog_id, '75'); 670 } // update_enabled 671 672 /** 673 * _update_item 674 * This is a private function that should only be called from within the catalog class. 675 * It takes a field, value, catalog id and level. first and foremost it checks the level 676 * against Core::get_global('user') to make sure they are allowed to update this record 677 * it then updates it and sets $this->{$field} to the new value 678 * @param string $field 679 * @param boolean $value 680 * @param integer $catalog_id 681 * @param integer $level 682 * @return PDOStatement|boolean 683 */ 684 private static function _update_item($field, $value, $catalog_id, $level) 685 { 686 /* Check them Rights! */ 687 if (!Access::check('interface', $level)) { 688 return false; 689 } 690 691 /* Can't update to blank */ 692 if (!strlen(trim((string)$value))) { 693 return false; 694 } 695 696 $value = Dba::escape($value); 697 698 $sql = "UPDATE `catalog` SET `$field`='$value' WHERE `id`='$catalog_id'"; 699 700 return Dba::write($sql); 701 } // _update_item 702 703 /** 704 * format 705 * 706 * This makes the object human-readable. 707 */ 708 public function format() 709 { 710 $this->f_name = filter_var($this->name, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES); 711 $this->link = AmpConfig::get('web_path') . '/admin/catalog.php?action=show_customize_catalog&catalog_id=' . $this->id; 712 $this->f_link = '<a href="' . $this->link . '" title="' . $this->f_name . '">' . $this->f_name . '</a>'; 713 $this->f_update = $this->last_update ? get_datetime((int)$this->last_update) : T_('Never'); 714 $this->f_add = $this->last_add ? get_datetime((int)$this->last_add) : T_('Never'); 715 $this->f_clean = $this->last_clean ? get_datetime((int)$this->last_clean) : T_('Never'); 716 $this->f_filter_user = ($this->filter_user == 0) 717 ? T_('Public Catalog') 718 : User::get_username($this->filter_user); 719 } 720 721 /** 722 * get_catalogs 723 * 724 * Pull all the current catalogs and return an array of ids 725 * of what you find 726 * @param string $filter_type 727 * @param int $user_id 728 * @return integer[] 729 */ 730 public static function get_catalogs($filter_type = '', $user_id = null) 731 { 732 $params = array(); 733 $sql = "SELECT `id` FROM `catalog` "; 734 $join = "WHERE"; 735 if (!empty($filter_type)) { 736 $sql .= "$join `gather_types` = ? "; 737 $params[] = $filter_type; 738 $join = "AND"; 739 } 740 if (AmpConfig::get('catalog_filter') && $user_id > 0) { 741 $sql .= $join . Catalog::get_user_filter('catalog', $user_id); 742 } 743 $sql .= "ORDER BY `name`"; 744 745 $db_results = Dba::read($sql, $params); 746 $results = array(); 747 while ($row = Dba::fetch_assoc($db_results)) { 748 $results[] = (int)$row['id']; 749 } 750 751 return $results; 752 } 753 754 /** 755 * Run the cache_catalog_proc() on music catalogs. 756 * @param integer[]|null $catalogs 757 * @return integer 758 */ 759 public static function cache_catalogs() 760 { 761 $catalogs = self::get_catalogs('music'); 762 foreach ($catalogs as $catalogid) { 763 debug_event(__CLASS__, 'cache_catalogs: ' . $catalogid, 5); 764 $catalog = self::create_from_id($catalogid); 765 $catalog->cache_catalog_proc(); 766 } 767 } 768 769 /** 770 * Get last catalogs update. 771 * @param integer[]|null $catalogs 772 * @return integer 773 */ 774 public static function getLastUpdate($catalogs = null) 775 { 776 $last_update = 0; 777 if ($catalogs == null || !is_array($catalogs)) { 778 $catalogs = self::get_catalogs(); 779 } 780 foreach ($catalogs as $catalogid) { 781 $catalog = self::create_from_id($catalogid); 782 if ($catalog->last_add > $last_update) { 783 $last_update = $catalog->last_add; 784 } 785 if ($catalog->last_update > $last_update) { 786 $last_update = $catalog->last_update; 787 } 788 if ($catalog->last_clean > $last_update) { 789 $last_update = $catalog->last_clean; 790 } 791 } 792 793 return $last_update; 794 } 795 796 /** 797 * get_stats 798 * 799 * This returns an hash with the #'s for the different 800 * objects that are associated with this catalog. This is used 801 * to build the stats box, it also calculates time. 802 * @param integer|null $catalog_id 803 * @return array 804 */ 805 public static function get_stats($catalog_id = null) 806 { 807 $counts = ($catalog_id) ? self::count_catalog($catalog_id) : self::get_server_counts(0); 808 $counts = array_merge(User::count(), $counts); 809 $counts['tags'] = self::count_tags(); 810 811 $counts['formatted_size'] = Ui::format_bytes($counts['size']); 812 813 $hours = floor($counts['time'] / 3600); 814 $days = floor($hours / 24); 815 $hours = $hours % 24; 816 817 $time_text = "$days "; 818 $time_text .= nT_('day', 'days', $days); 819 $time_text .= ", $hours "; 820 $time_text .= nT_('hour', 'hours', $hours); 821 822 $counts['time_text'] = $time_text; 823 824 return $counts; 825 } 826 827 /** 828 * create 829 * 830 * This creates a new catalog entry and associate it to current instance 831 * @param array $data 832 * @return integer 833 */ 834 public static function create($data) 835 { 836 $name = $data['name']; 837 $type = $data['type']; 838 $rename_pattern = $data['rename_pattern']; 839 $sort_pattern = $data['sort_pattern']; 840 $gather_types = $data['gather_media']; 841 842 // Should it be an array? Not now. 843 if (!in_array($gather_types, 844 array('music', 'clip', 'tvshow', 'movie', 'personal_video', 'podcast'))) { 845 return 0; 846 } 847 848 $insert_id = 0; 849 850 $classname = self::CATALOG_TYPES[$type] ?? null; 851 852 if ($classname === null) { 853 return $insert_id; 854 } 855 856 $sql = 'INSERT INTO `catalog` (`name`, `catalog_type`, ' . '`rename_pattern`, `sort_pattern`, `gather_types`) VALUES (?, ?, ?, ?, ?)'; 857 Dba::write($sql, array( 858 $name, 859 $type, 860 $rename_pattern, 861 $sort_pattern, 862 $gather_types 863 )); 864 865 $insert_id = Dba::insert_id(); 866 867 if (!$insert_id) { 868 AmpError::add('general', T_('Failed to create the catalog, check the debug logs')); 869 debug_event(__CLASS__, 'Insert failed: ' . json_encode($data), 2); 870 871 return 0; 872 } 873 874 if (!$classname::create_type($insert_id, $data)) { 875 $sql = 'DELETE FROM `catalog` WHERE `id` = ?'; 876 Dba::write($sql, array($insert_id)); 877 $insert_id = 0; 878 } 879 880 return (int)$insert_id; 881 } 882 883 /** 884 * count_tags 885 * 886 * This returns the current number of unique tags in the database. 887 * @return integer 888 */ 889 public static function count_tags() 890 { 891 // FIXME: Ignores catalog_id 892 $sql = "SELECT COUNT(`id`) FROM `tag`"; 893 $db_results = Dba::read($sql); 894 895 $row = Dba::fetch_row($db_results); 896 897 return $row[0]; 898 } 899 900 /** 901 * has_access 902 * 903 * When filtering catalogs you shouldn't be able to play the files 904 * @param int $catalog_id 905 * @param int $user_id 906 * @return bool 907 */ 908 public static function has_access($catalog_id, $user_id) 909 { 910 if (!AmpConfig::get('catalog_filter')) { 911 return true; 912 } 913 $params = array($catalog_id); 914 $sql = "SELECT `filter_user` FROM `catalog` WHERE `id` = ?"; 915 916 $db_results = Dba::read($sql, $params); 917 while ($row = Dba::fetch_assoc($db_results)) { 918 if ((int)$row['filter_user'] == 0 || (int)$row['filter_user'] == $user_id) { 919 return true; 920 } 921 } 922 923 return false; 924 } // has_access 925 926 /** 927 * get_server_counts 928 * 929 * This returns the current number of songs, videos, albums, artists, items, etc across all catalogs on the server 930 * @param int $user_id 931 * @return array 932 */ 933 public static function get_server_counts($user_id) 934 { 935 $results = array(); 936 if ($user_id > 0) { 937 $sql = "SELECT `key`, `value` FROM `user_data` WHERE `user` = ?;"; 938 $db_results = Dba::read($sql, array($user_id)); 939 } else { 940 $sql = "SELECT `key`, `value` FROM `update_info`;"; 941 $db_results = Dba::read($sql); 942 } 943 944 while ($row = Dba::fetch_assoc($db_results)) { 945 $results[$row['key']] = (int)$row['value']; 946 } 947 948 return $results; 949 } // get_server_counts 950 951 /** 952 * count_table 953 * 954 * Update a specific table count when adding/removing from the server 955 * @param string $table 956 * @return array 957 */ 958 public static function count_table($table) 959 { 960 $sql = "SELECT COUNT(`id`) FROM `$table`"; 961 $db_results = Dba::read($sql); 962 $data = Dba::fetch_row($db_results); 963 964 self::set_count($table, (int)$data[0]); 965 966 return $data; 967 } // count_table 968 969 /** 970 * count_catalog 971 * 972 * This returns the current number of songs, videos, podcast_episodes in this catalog. 973 * @param integer $catalog_id 974 * @return array 975 */ 976 public static function count_catalog($catalog_id) 977 { 978 $where_sql = $catalog_id ? 'WHERE `catalog` = ?' : ''; 979 $params = $catalog_id ? array($catalog_id) : array(); 980 $results = array(); 981 $catalog = self::create_from_id($catalog_id); 982 983 if ($catalog->id) { 984 $table = self::get_table_from_type($catalog->gather_types); 985 if ($table == 'podcast_episode' && $catalog_id) { 986 $where_sql = "WHERE `podcast` IN (SELECT `id` FROM `podcast` WHERE `catalog` = ?)"; 987 } 988 $sql = "SELECT COUNT(`id`), IFNULL(SUM(`time`), 0), IFNULL(SUM(`size`), 0) FROM `" . $table . "` " . $where_sql; 989 $db_results = Dba::read($sql, $params); 990 $data = Dba::fetch_row($db_results); 991 $results['items'] = $data[0]; 992 $results['time'] = $data[1]; 993 $results['size'] = $data[2]; 994 } 995 996 return $results; 997 } // count_catalog 998 999 /** 1000 * get_uploads_sql 1001 * 1002 * @param string $type 1003 * @param integer|null $user_id 1004 * @return string 1005 */ 1006 public static function get_uploads_sql($type, $user_id = null) 1007 { 1008 if ($user_id === null) { 1009 $user_id = Core::get_global('user')->id; 1010 } 1011 $user_id = (int)($user_id); 1012 1013 switch ($type) { 1014 case 'song': 1015 $sql = "SELECT `song`.`id` as `id` FROM `song` WHERE `song`.`user_upload` = '" . $user_id . "'"; 1016 break; 1017 case 'album': 1018 $sql = "SELECT `album`.`id` as `id` FROM `album` JOIN `song` ON `song`.`album` = `album`.`id` WHERE `song`.`user_upload` = '" . $user_id . "' GROUP BY `album`.`id`"; 1019 break; 1020 case 'artist': 1021 default: 1022 $sql = "SELECT `artist`.`id` as `id` FROM `artist` JOIN `song` ON `song`.`artist` = `artist`.`id` WHERE `song`.`user_upload` = '" . $user_id . "' GROUP BY `artist`.`id`"; 1023 break; 1024 } 1025 1026 return $sql; 1027 } // get_uploads_sql 1028 1029 /** 1030 * get_album_ids 1031 * 1032 * This returns an array of ids of albums that have songs in this 1033 * catalog's 1034 * @param string $filter 1035 * @return integer[] 1036 */ 1037 public function get_album_ids($filter = '') 1038 { 1039 $results = array(); 1040 1041 $sql = 'SELECT `album`.`id` FROM `album` WHERE `album`.`catalog` = ?'; 1042 if ($filter === 'art') { 1043 $sql = "SELECT `album`.`id` FROM `album` LEFT JOIN `image` ON `album`.`id` = `image`.`object_id` AND `object_type` = 'album'WHERE `album`.`catalog` = ? AND `image`.`object_id` IS NULL"; 1044 } 1045 $db_results = Dba::read($sql, array($this->id)); 1046 1047 while ($row = Dba::fetch_assoc($db_results)) { 1048 $results[] = (int)$row['id']; 1049 } 1050 1051 return array_reverse($results); 1052 } 1053 1054 /** 1055 * get_video_ids 1056 * 1057 * This returns an array of ids of videos in this catalog 1058 * @param string $type 1059 * @return integer[] 1060 */ 1061 public function get_video_ids($type = '') 1062 { 1063 $results = array(); 1064 1065 $sql = 'SELECT DISTINCT(`video`.`id`) AS `id` FROM `video` '; 1066 if (!empty($type)) { 1067 $sql .= 'JOIN `' . $type . '` ON `' . $type . '`.`id` = `video`.`id`'; 1068 } 1069 $sql .= 'WHERE `video`.`catalog` = ?'; 1070 $db_results = Dba::read($sql, array($this->id)); 1071 1072 while ($row = Dba::fetch_assoc($db_results)) { 1073 $results[] = (int)$row['id']; 1074 } 1075 1076 return $results; 1077 } 1078 1079 /** 1080 * 1081 * @param integer[]|null $catalogs 1082 * @param string $type 1083 * @return Video[] 1084 */ 1085 public static function get_videos($catalogs = null, $type = '') 1086 { 1087 if (!$catalogs) { 1088 $catalogs = self::get_catalogs(); 1089 } 1090 1091 $results = array(); 1092 foreach ($catalogs as $catalog_id) { 1093 $catalog = self::create_from_id($catalog_id); 1094 $video_ids = $catalog->get_video_ids($type); 1095 foreach ($video_ids as $video_id) { 1096 $results[] = Video::create_from_id($video_id); 1097 } 1098 } 1099 1100 return $results; 1101 } 1102 1103 /** 1104 * 1105 * @param integer|null $catalog_id 1106 * @param string $type 1107 * @return integer 1108 */ 1109 public static function get_videos_count($catalog_id = null, $type = '') 1110 { 1111 $sql = "SELECT COUNT(`video`.`id`) AS `video_cnt` FROM `video` "; 1112 if (!empty($type)) { 1113 $sql .= "JOIN `" . $type . "` ON `" . $type . "`.`id` = `video`.`id` "; 1114 } 1115 if ($catalog_id) { 1116 $sql .= "WHERE `video`.`catalog` = `" . (string)($catalog_id) . "`"; 1117 } 1118 $db_results = Dba::read($sql); 1119 $video_cnt = 0; 1120 if ($row = Dba::fetch_row($db_results)) { 1121 $video_cnt = $row[0]; 1122 } 1123 1124 return $video_cnt; 1125 } 1126 1127 /** 1128 * get_tvshow_ids 1129 * 1130 * This returns an array of ids of tvshows in this catalog 1131 * @return integer[] 1132 */ 1133 public function get_tvshow_ids() 1134 { 1135 $results = array(); 1136 1137 $sql = 'SELECT DISTINCT(`tvshow`.`id`) AS `id` FROM `tvshow` '; 1138 $sql .= 'JOIN `tvshow_season` ON `tvshow_season`.`tvshow` = `tvshow`.`id` '; 1139 $sql .= 'JOIN `tvshow_episode` ON `tvshow_episode`.`season` = `tvshow_season`.`id` '; 1140 $sql .= 'JOIN `video` ON `video`.`id` = `tvshow_episode`.`id` '; 1141 $sql .= 'WHERE `video`.`catalog` = ?'; 1142 1143 $db_results = Dba::read($sql, array($this->id)); 1144 while ($row = Dba::fetch_assoc($db_results)) { 1145 $results[] = (int)$row['id']; 1146 } 1147 1148 return $results; 1149 } 1150 1151 /** 1152 * get_tvshows 1153 * @param integer[]|null $catalogs 1154 * @return TvShow[] 1155 */ 1156 public static function get_tvshows($catalogs = null) 1157 { 1158 if (!$catalogs) { 1159 $catalogs = self::get_catalogs(); 1160 } 1161 1162 $results = array(); 1163 foreach ($catalogs as $catalog_id) { 1164 $catalog = self::create_from_id($catalog_id); 1165 $tvshow_ids = $catalog->get_tvshow_ids(); 1166 foreach ($tvshow_ids as $tvshow_id) { 1167 $results[] = new TvShow($tvshow_id); 1168 } 1169 } 1170 1171 return $results; 1172 } 1173 1174 /** 1175 * get_artist_arrays 1176 * 1177 * Get each array of [id, full_name, name] for artists in an array of catalog id's 1178 * @param array $catalogs 1179 * @return array 1180 */ 1181 public static function get_artist_arrays($catalogs) 1182 { 1183 $list = Dba::escape(implode(',', $catalogs)); 1184 $sql = "SELECT DISTINCT `artist`.`id`, LTRIM(CONCAT(COALESCE(`artist`.`prefix`, ''), ' ', `artist`.`name`)) AS `f_name`, `artist`.`name`, MIN(`catalog_map`.`catalog_id`) FROM `artist` LEFT JOIN `catalog_map` ON `catalog_map`.`object_type` = 'artist' AND `catalog_map`.`object_id` = `artist`.`id` WHERE `catalog_map`.`catalog_id` IN ($list) GROUP BY `artist`.`id` ORDER BY `f_name`"; 1185 1186 $db_results = Dba::read($sql); 1187 $results = array(); 1188 while ($row = Dba::fetch_assoc($db_results, false)) { 1189 $results[] = $row; 1190 } 1191 1192 return $results; 1193 } 1194 1195 /** 1196 * get_artist_ids 1197 * 1198 * This returns an array of ids of artist that have songs in this catalog 1199 * @param string $filter 1200 * @return integer[] 1201 */ 1202 public function get_artist_ids($filter = '') 1203 { 1204 $results = array(); 1205 1206 $sql = 'SELECT DISTINCT(`song`.`artist`) AS `artist` FROM `song` WHERE `song`.`catalog` = ?'; 1207 if ($filter === 'art') { 1208 $sql = "SELECT DISTINCT(`song`.`artist`) AS `artist` FROM `song` LEFT JOIN `image` ON `song`.`artist` = `image`.`object_id` AND `object_type` = 'artist'WHERE `song`.`catalog` = ? AND `image`.`object_id` IS NULL"; 1209 } 1210 if ($filter === 'info') { 1211 // only update info when you haven't done it for 6 months 1212 $sql = "SELECT DISTINCT(`artist`.`id`) AS `artist` FROM `artist` WHERE `artist`.`last_update` < (UNIX_TIMESTAMP() - 15768000)"; 1213 } 1214 if ($filter === 'count') { 1215 // Update for things added in the last run or empty ones 1216 $sql = "SELECT DISTINCT(`artist`.`id`) AS `artist` FROM `artist` WHERE `artist`.`id` IN (SELECT DISTINCT `song`.`artist` FROM `song` WHERE `song`.`catalog` = ? AND `addition_time` > " . $this->last_add . ") OR (`album_count` = 0 AND `song_count` = 0) "; 1217 } 1218 $db_results = Dba::read($sql, array($this->id)); 1219 1220 while ($row = Dba::fetch_assoc($db_results)) { 1221 $results[] = (int) $row['artist']; 1222 } 1223 1224 return array_reverse($results); 1225 } 1226 1227 /** 1228 * get_artists 1229 * 1230 * This returns an array of artists that have songs in the catalogs parameter 1231 * @param array|null $catalogs 1232 * @param integer $size 1233 * @param integer $offset 1234 * @return Artist[] 1235 */ 1236 public static function get_artists($catalogs = null, $size = 0, $offset = 0) 1237 { 1238 $sql_where = ""; 1239 if (is_array($catalogs) && count($catalogs)) { 1240 $catlist = '(' . implode(',', $catalogs) . ')'; 1241 $sql_where = "WHERE `song`.`catalog` IN $catlist"; 1242 } 1243 1244 $sql_limit = ""; 1245 if ($offset > 0 && $size > 0) { 1246 $sql_limit = "LIMIT " . $offset . ", " . $size; 1247 } elseif ($size > 0) { 1248 $sql_limit = "LIMIT " . $size; 1249 } elseif ($offset > 0) { 1250 // MySQL doesn't have notation for last row, so we have to use the largest possible BIGINT value 1251 // https://dev.mysql.com/doc/refman/5.0/en/select.html // TODO mysql8 test 1252 $sql_limit = "LIMIT " . $offset . ", 18446744073709551615"; 1253 } 1254 $album_type = (AmpConfig::get('album_group')) ? '`artist`.`album_group_count`' : '`artist`.`album_count`'; 1255 1256 $sql = "SELECT `artist`.`id`, `artist`.`name`, `artist`.`prefix`, `artist`.`summary`, $album_type AS `albums` FROM `song` LEFT JOIN `artist` ON `artist`.`id` = `song`.`artist` $sql_where GROUP BY `artist`.`id`, `artist`.`name`, `artist`.`prefix`, `artist`.`summary`, `song`.`artist`, $album_type ORDER BY `artist`.`name` " . $sql_limit; 1257 1258 $results = array(); 1259 $db_results = Dba::read($sql); 1260 1261 while ($row = Dba::fetch_assoc($db_results)) { 1262 $results[] = Artist::construct_from_array($row); 1263 } 1264 1265 return $results; 1266 } 1267 1268 /** 1269 * get_catalog_map 1270 * 1271 * This returns an id of artist that have songs in this catalog 1272 * @param string $object_type 1273 * @param string $object_id 1274 * @return integer 1275 */ 1276 public static function get_catalog_map($object_type, $object_id) 1277 { 1278 $sql = "SELECT MIN(`catalog_map`.`catalog_id`) AS `catalog_id` FROM `catalog_map` WHERE `object_type` = ? AND `object_id` = ?"; 1279 1280 $db_results = Dba::read($sql, array($object_type, $object_id)); 1281 if ($row = Dba::fetch_assoc($db_results)) { 1282 return (int) $row['catalog_id']; 1283 } 1284 1285 return 0; 1286 } 1287 1288 /** 1289 * get_id_from_file 1290 * 1291 * Get media id from the file path. 1292 * 1293 * @param string $file_path 1294 * @param string $media_type 1295 * @return integer 1296 */ 1297 public static function get_id_from_file($file_path, $media_type) 1298 { 1299 $sql = "SELECT `id` FROM `$media_type` WHERE `file` = ?;"; 1300 $db_results = Dba::read($sql, array($file_path)); 1301 1302 if ($results = Dba::fetch_assoc($db_results)) { 1303 return (int)$results['id']; 1304 } 1305 1306 return 0; 1307 } 1308 1309 /** 1310 * get_label_ids 1311 * 1312 * This returns an array of ids of labels 1313 * @param string $filter 1314 * @return integer[] 1315 */ 1316 public function get_label_ids($filter) 1317 { 1318 $results = array(); 1319 1320 $sql = 'SELECT `id` FROM `label` WHERE `category` = ? OR `mbid` IS NULL'; 1321 $db_results = Dba::read($sql, array($filter)); 1322 1323 while ($row = Dba::fetch_assoc($db_results)) { 1324 $results[] = (int)$row['id']; 1325 } 1326 1327 return $results; 1328 } 1329 1330 /** 1331 * @param string $name 1332 * @param integer $catalog_id 1333 * @return array 1334 */ 1335 public static function search_childrens($name, $catalog_id = 0) 1336 { 1337 $search = array(); 1338 $search['type'] = "artist"; 1339 $search['rule_0_input'] = $name; 1340 $search['rule_0_operator'] = 4; 1341 $search['rule_0'] = "name"; 1342 if ($catalog_id > 0) { 1343 $search['rule_1_input'] = $catalog_id; 1344 $search['rule_1_operator'] = 0; 1345 $search['rule_1'] = "catalog"; 1346 } 1347 $artists = Search::run($search); 1348 1349 $childrens = array(); 1350 foreach ($artists as $artist_id) { 1351 $childrens[] = array( 1352 'object_type' => 'artist', 1353 'object_id' => $artist_id 1354 ); 1355 } 1356 1357 return $childrens; 1358 } 1359 1360 /** 1361 * get_albums 1362 * 1363 * Returns an array of ids of albums that have songs in the catalogs parameter 1364 * @param integer $size 1365 * @param integer $offset 1366 * @param integer[]|null $catalogs 1367 * @return integer[] 1368 */ 1369 public static function get_albums($size = 0, $offset = 0, $catalogs = null) 1370 { 1371 $sql = "SELECT `album`.`id` FROM `album` "; 1372 if (is_array($catalogs) && count($catalogs)) { 1373 $catlist = '(' . implode(',', $catalogs) . ')'; 1374 $sql = "SELECT `album`.`id` FROM `song` LEFT JOIN `album` ON `album`.`id` = `song`.`album` WHERE `song`.`catalog` IN $catlist "; 1375 } 1376 1377 $sql_limit = ""; 1378 if ($offset > 0 && $size > 0) { 1379 $sql_limit = "LIMIT $offset, $size"; 1380 } elseif ($size > 0) { 1381 $sql_limit = "LIMIT $size"; 1382 } elseif ($offset > 0) { 1383 // MySQL doesn't have notation for last row, so we have to use the largest possible BIGINT value 1384 // https://dev.mysql.com/doc/refman/5.0/en/select.html 1385 $sql_limit = "LIMIT $offset, 18446744073709551615"; 1386 } 1387 1388 $sql .= "GROUP BY `album`.`id` ORDER BY `album`.`name` $sql_limit"; 1389 1390 $db_results = Dba::read($sql); 1391 $results = array(); 1392 while ($row = Dba::fetch_assoc($db_results)) { 1393 $results[] = (int)$row['id']; 1394 } 1395 1396 return $results; 1397 } 1398 1399 /** 1400 * get_albums_by_artist 1401 * 1402 * Returns an array of ids of albums that have songs in the catalogs parameter, grouped by artist 1403 * @param integer $size 1404 * @param integer $offset 1405 * @param integer[]|null $catalogs 1406 * @return integer[] 1407 * @oaram int $offset 1408 */ 1409 public static function get_albums_by_artist($size = 0, $offset = 0, $catalogs = null) 1410 { 1411 $sql = "SELECT `album`.`id` FROM `album` "; 1412 $sql_where = ""; 1413 $sql_group = "GROUP BY `album`.`id`, `artist`.`name`, `artist`.`id`, `album`.`name`, `album`.`mbid`"; 1414 if (is_array($catalogs) && count($catalogs)) { 1415 $catlist = '(' . implode(',', $catalogs) . ')'; 1416 $sql = "SELECT `song`.`album` as 'id' FROM `song` LEFT JOIN `album` ON `album`.`id` = `song`.`album` "; 1417 $sql_where = "WHERE `song`.`catalog` IN $catlist"; 1418 $sql_group = "GROUP BY `song`.`album`, `artist`.`name`, `artist`.`id`, `album`.`name`, `album`.`mbid`"; 1419 } 1420 1421 $sql_limit = ""; 1422 if ($offset > 0 && $size > 0) { 1423 $sql_limit = "LIMIT $offset, $size"; 1424 } elseif ($size > 0) { 1425 $sql_limit = "LIMIT $size"; 1426 } elseif ($offset > 0) { 1427 // MySQL doesn't have notation for last row, so we have to use the largest possible BIGINT value 1428 // https://dev.mysql.com/doc/refman/5.0/en/select.html // TODO mysql8 test 1429 $sql_limit = "LIMIT $offset, 18446744073709551615"; 1430 } 1431 1432 $sql .= "LEFT JOIN `artist` ON `artist`.`id` = `album`.`album_artist` $sql_where $sql_group ORDER BY `artist`.`name`, `artist`.`id`, `album`.`name` $sql_limit"; 1433 1434 $db_results = Dba::read($sql); 1435 $results = array(); 1436 while ($row = Dba::fetch_assoc($db_results)) { 1437 $results[] = (int)$row['id']; 1438 } 1439 1440 return $results; 1441 } 1442 1443 /** 1444 * get_podcast_ids 1445 * 1446 * This returns an array of ids of podcasts in this catalog 1447 * @return integer[] 1448 */ 1449 public function get_podcast_ids() 1450 { 1451 $results = array(); 1452 1453 $sql = 'SELECT `podcast`.`id` FROM `podcast` '; 1454 $sql .= 'WHERE `podcast`.`catalog` = ?'; 1455 $db_results = Dba::read($sql, array($this->id)); 1456 while ($row = Dba::fetch_assoc($db_results)) { 1457 $results[] = (int)$row['id']; 1458 } 1459 1460 return $results; 1461 } 1462 1463 /** 1464 * 1465 * @param integer[]|null $catalogs 1466 * @return Podcast[] 1467 */ 1468 public static function get_podcasts($catalogs = null) 1469 { 1470 if (!$catalogs) { 1471 $catalogs = self::get_catalogs('podcast'); 1472 } 1473 1474 $results = array(); 1475 foreach ($catalogs as $catalog_id) { 1476 $catalog = self::create_from_id($catalog_id); 1477 $podcast_ids = $catalog->get_podcast_ids(); 1478 foreach ($podcast_ids as $podcast_id) { 1479 $results[] = new Podcast($podcast_id); 1480 } 1481 } 1482 1483 return $results; 1484 } 1485 1486 /** 1487 * get_newest_podcasts_ids 1488 * 1489 * This returns an array of ids of latest podcast episodes in this catalog 1490 * @param integer $count 1491 * @return integer[] 1492 */ 1493 public function get_newest_podcasts_ids($count) 1494 { 1495 $results = array(); 1496 1497 $sql = 'SELECT `podcast_episode`.`id` FROM `podcast_episode` INNER JOIN `podcast` ON `podcast`.`id` = `podcast_episode`.`podcast` WHERE `podcast`.`catalog` = ? ORDER BY `podcast_episode`.`pubdate` DESC'; 1498 if ($count > 0) { 1499 $sql .= ' LIMIT ' . (string)$count; 1500 } 1501 $db_results = Dba::read($sql, array($this->id)); 1502 while ($row = Dba::fetch_assoc($db_results)) { 1503 $results[] = (int)$row['id']; 1504 } 1505 1506 return $results; 1507 } 1508 1509 /** 1510 * 1511 * @param integer $count 1512 * @return Podcast_Episode[] 1513 */ 1514 public static function get_newest_podcasts($count) 1515 { 1516 $catalogs = self::get_catalogs('podcast'); 1517 $results = array(); 1518 1519 foreach ($catalogs as $catalog_id) { 1520 $catalog = self::create_from_id($catalog_id); 1521 $episode_ids = $catalog->get_newest_podcasts_ids($count); 1522 foreach ($episode_ids as $episode_id) { 1523 $results[] = new Podcast_Episode($episode_id); 1524 } 1525 } 1526 1527 return $results; 1528 } 1529 1530 /** 1531 * gather_art_item 1532 * @param string $type 1533 * @param integer $object_id 1534 * @param boolean $db_art_first 1535 * @param boolean $api 1536 * @return boolean 1537 */ 1538 public static function gather_art_item($type, $object_id, $db_art_first = false, $api = false) 1539 { 1540 // Should be more generic ! 1541 if ($type == 'video') { 1542 $libitem = Video::create_from_id($object_id); 1543 } else { 1544 $class_name = ObjectTypeToClassNameMapper::map($type); 1545 $libitem = new $class_name($object_id); 1546 } 1547 $inserted = false; 1548 $options = array(); 1549 $libitem->format(); 1550 if ($libitem->id) { 1551 // Only search on items with default art kind as `default`. 1552 if ($libitem->get_default_art_kind() == 'default') { 1553 $keywords = $libitem->get_keywords(); 1554 $keyword = ''; 1555 foreach ($keywords as $key => $word) { 1556 $options[$key] = $word['value']; 1557 if ($word['important'] && !empty($word['value'])) { 1558 $keyword .= ' ' . $word['value']; 1559 } 1560 } 1561 $options['keyword'] = $keyword; 1562 } 1563 1564 $parent = $libitem->get_parent(); 1565 if (!empty($parent)) { 1566 self::gather_art_item($parent['object_type'], $parent['object_id'], $db_art_first, $api); 1567 } 1568 } 1569 1570 $art = new Art($object_id, $type); 1571 // don't search for art when you already have it 1572 if ($art->has_db_info() && $db_art_first) { 1573 debug_event(self::class, "gather_art_item $type: {{$object_id}} blocked", 5); 1574 $results = array(); 1575 } else { 1576 debug_event(__CLASS__, "gather_art_item $type: {{$object_id}} searching", 4); 1577 1578 global $dic; 1579 $results = $dic->get(ArtCollectorInterface::class)->collect( 1580 $art, 1581 $options 1582 ); 1583 } 1584 1585 foreach ($results as $result) { 1586 // Pull the string representation from the source 1587 $image = Art::get_from_source($result, $type); 1588 if (strlen((string)$image) > '5') { 1589 $inserted = $art->insert($image, $result['mime']); 1590 // If they've enabled resizing of images generate a thumbnail 1591 if (AmpConfig::get('resize_images')) { 1592 $size = array('width' => 275, 'height' => 275); 1593 $thumb = $art->generate_thumb($image, $size, $result['mime']); 1594 if (!empty($thumb)) { 1595 $art->save_thumb($thumb['thumb'], $thumb['thumb_mime'], $size); 1596 } 1597 } 1598 if ($inserted) { 1599 break; 1600 } 1601 } elseif ($result === true) { 1602 debug_event(self::class, 'Database already has image.', 3); 1603 } else { 1604 debug_event(self::class, 'Image less than 5 chars, not inserting', 3); 1605 } 1606 } 1607 1608 if ($type == 'video' && AmpConfig::get('generate_video_preview')) { 1609 Video::generate_preview($object_id); 1610 } 1611 1612 if (Ui::check_ticker() && !$api) { 1613 Ui::update_text('read_art_' . $object_id, $libitem->get_fullname()); 1614 } 1615 if ($inserted) { 1616 return true; 1617 } 1618 1619 return false; 1620 } 1621 1622 /** 1623 * gather_art 1624 * 1625 * This runs through all of the albums and finds art for them 1626 * This runs through all of the needs art albums and tries 1627 * to find the art for them from the mp3s 1628 * @param integer[]|null $songs 1629 * @param integer[]|null $videos 1630 * @return boolean 1631 */ 1632 public function gather_art($songs = null, $videos = null) 1633 { 1634 // Make sure they've actually got methods 1635 $art_order = AmpConfig::get('art_order'); 1636 $gather_song_art = AmpConfig::get('gather_song_art', false); 1637 $db_art_first = ($art_order[0] == 'db'); 1638 if (!count($art_order)) { 1639 debug_event(self::class, 'art_order not set, self::gather_art aborting', 3); 1640 1641 return false; 1642 } 1643 1644 // Prevent the script from timing out 1645 set_time_limit(0); 1646 1647 $search_count = 0; 1648 $searches = array(); 1649 if ($songs == null) { 1650 $searches['album'] = $this->get_album_ids('art'); 1651 $searches['artist'] = $this->get_artist_ids('art'); 1652 if ($gather_song_art) { 1653 $searches['song'] = $this->get_songs(); 1654 } 1655 } else { 1656 $searches['album'] = array(); 1657 $searches['artist'] = array(); 1658 if ($gather_song_art) { 1659 $searches['song'] = array(); 1660 } 1661 foreach ($songs as $song_id) { 1662 $song = new Song($song_id); 1663 if ($song->id) { 1664 if (!in_array($song->album, $searches['album'])) { 1665 $searches['album'][] = $song->album; 1666 } 1667 if (!in_array($song->artist, $searches['artist'])) { 1668 $searches['artist'][] = $song->artist; 1669 } 1670 if ($gather_song_art) { 1671 $searches['song'][] = $song->id; 1672 } 1673 } 1674 } 1675 } 1676 if ($videos == null) { 1677 $searches['video'] = $this->get_video_ids(); 1678 } else { 1679 $searches['video'] = $videos; 1680 } 1681 1682 debug_event(self::class, 'gather_art found ' . (string) count($searches) . ' items missing art', 4); 1683 // Run through items and get the art! 1684 foreach ($searches as $key => $values) { 1685 foreach ($values as $object_id) { 1686 self::gather_art_item($key, $object_id, $db_art_first); 1687 1688 // Stupid little cutesie thing 1689 $search_count++; 1690 if (Ui::check_ticker()) { 1691 Ui::update_text('count_art_' . $this->id, $search_count); 1692 } 1693 } 1694 } 1695 // One last time for good measure 1696 Ui::update_text('count_art_' . $this->id, $search_count); 1697 1698 return true; 1699 } 1700 1701 /** 1702 * gather_artist_info 1703 * 1704 * This runs through all of the artists and refreshes last.fm information 1705 * including similar artists that exist in your catalog. 1706 * @param array $artist_list 1707 */ 1708 public function gather_artist_info($artist_list = array()) 1709 { 1710 // Prevent the script from timing out 1711 set_time_limit(0); 1712 1713 $search_count = 0; 1714 debug_event(self::class, 'gather_artist_info found ' . (string) count($artist_list) . ' items to check', 4); 1715 // Run through items and refresh info 1716 foreach ($artist_list as $object_id) { 1717 Recommendation::get_artist_info($object_id); 1718 Recommendation::get_artists_like($object_id); 1719 Artist::set_last_update($object_id); 1720 1721 // Stupid little cutesie thing 1722 $search_count++; 1723 if (Ui::check_ticker()) { 1724 Ui::update_text('count_artist_' . $object_id, $search_count); 1725 } 1726 } 1727 1728 // One last time for good measure 1729 Ui::update_text('count_artist_complete', $search_count); 1730 } 1731 1732 /** 1733 * update_from_external 1734 * 1735 * This runs through all of the labels and refreshes information from musicbrainz 1736 * @param array $object_list 1737 */ 1738 public function update_from_external($object_list = array()) 1739 { 1740 // Prevent the script from timing out 1741 set_time_limit(0); 1742 1743 debug_event(self::class, 'update_from_external found ' . (string) count($object_list) . ' items to check', 4); 1744 $plugin = new Plugin('musicbrainz'); 1745 if ($plugin->load(new User(-1))) { 1746 // Run through items and refresh info 1747 foreach ($object_list as $label_id) { 1748 $label = new Label($label_id); 1749 $plugin->_plugin->get_external_metadata($label, 'label'); 1750 } 1751 } 1752 } 1753 1754 /** 1755 * get_songs 1756 * 1757 * Returns an array of song objects. 1758 * @return Song[] 1759 */ 1760 public function get_songs() 1761 { 1762 $songs = array(); 1763 $results = array(); 1764 1765 $sql = "SELECT `id` FROM `song` WHERE `catalog` = ? AND `enabled`='1'"; 1766 $db_results = Dba::read($sql, array($this->id)); 1767 1768 while ($row = Dba::fetch_assoc($db_results)) { 1769 $songs[] = (int)$row['id']; 1770 } 1771 1772 if (AmpConfig::get('memory_cache')) { 1773 Song::build_cache($songs); 1774 } 1775 1776 foreach ($songs as $song_id) { 1777 $results[] = new Song($song_id); 1778 } 1779 1780 return $results; 1781 } 1782 1783 /** 1784 * get_song_ids 1785 * 1786 * Returns an array of song ids. 1787 * @return integer[] 1788 */ 1789 public function get_song_ids() 1790 { 1791 $songs = array(); 1792 1793 $sql = "SELECT `id` FROM `song` WHERE `catalog` = ? AND `enabled`='1'"; 1794 $db_results = Dba::read($sql, array($this->id)); 1795 1796 while ($row = Dba::fetch_assoc($db_results)) { 1797 $songs[] = (int)$row['id']; 1798 } 1799 1800 return $songs; 1801 } 1802 1803 /** 1804 * update_last_update 1805 * updates the last_update of the catalog 1806 */ 1807 protected function update_last_update() 1808 { 1809 $date = time(); 1810 $sql = "UPDATE `catalog` SET `last_update` = ? WHERE `id` = ?"; 1811 Dba::write($sql, array($date, $this->id)); 1812 } // update_last_update 1813 1814 /** 1815 * update_last_add 1816 * updates the last_add of the catalog 1817 */ 1818 public function update_last_add() 1819 { 1820 $date = time(); 1821 $sql = "UPDATE `catalog` SET `last_add` = ? WHERE `id` = ?"; 1822 Dba::write($sql, array($date, $this->id)); 1823 } // update_last_add 1824 1825 /** 1826 * update_last_clean 1827 * This updates the last clean information 1828 */ 1829 public function update_last_clean() 1830 { 1831 $date = time(); 1832 $sql = "UPDATE `catalog` SET `last_clean` = ? WHERE `id` = ?"; 1833 Dba::write($sql, array($date, $this->id)); 1834 } // update_last_clean 1835 1836 /** 1837 * update_settings 1838 * This function updates the basic setting of the catalog 1839 * @param array $data 1840 * @return boolean 1841 */ 1842 public static function update_settings($data) 1843 { 1844 $sql = "UPDATE `catalog` SET `name` = ?, `rename_pattern` = ?, `sort_pattern` = ?, `filter_user` = ? WHERE `id` = ?"; 1845 $params = array($data['name'], $data['rename_pattern'], $data['sort_pattern'], $data['filter_user'], $data['catalog_id']); 1846 Dba::write($sql, $params); 1847 1848 if ($data['filter_user']) { 1849 User::update_counts(); 1850 } 1851 1852 return true; 1853 } // update_settings 1854 1855 /** 1856 * update_single_item 1857 * updates a single album,artist,song from the tag data 1858 * this can be done by 75+ 1859 * @param string $type 1860 * @param integer $object_id 1861 * @param boolean $api 1862 * @return integer 1863 */ 1864 public static function update_single_item($type, $object_id, $api = false) 1865 { 1866 // Because single items are large numbers of things too 1867 set_time_limit(0); 1868 1869 $songs = array(); 1870 $result = $object_id; 1871 $libitem = 0; 1872 1873 switch ($type) { 1874 case 'album': 1875 $libitem = new Album($object_id); 1876 $songs = static::getSongRepository()->getByAlbum($object_id); 1877 break; 1878 case 'artist': 1879 $libitem = new Artist($object_id); 1880 $songs = static::getSongRepository()->getAllByArtist($object_id); 1881 break; 1882 case 'song': 1883 $songs[] = $object_id; 1884 break; 1885 } // end switch type 1886 1887 if (!$api) { 1888 echo '<table class="tabledata striped-rows">' . "\n"; 1889 echo '<thead><tr class="th-top">' . "\n"; 1890 echo "<th>" . T_("Song") . "</th><th>" . T_("Status") . "</th>\n"; 1891 echo "<tbody>\n"; 1892 } 1893 foreach ($songs as $song_id) { 1894 $song = new Song($song_id); 1895 $info = self::update_media_from_tags($song); 1896 // don't echo useless info when using api 1897 if (($info['change']) && (!$api)) { 1898 if ($info['element'][$type]) { 1899 $change = explode(' --> ', (string)$info['element'][$type]); 1900 $result = (int)$change[1]; 1901 } 1902 $file = scrub_out($song->file); 1903 echo '<tr>' . "\n"; 1904 echo "<td>$file</td><td>" . T_('Updated') . "</td>\n"; 1905 echo $info['text']; 1906 echo "</td>\n</tr>\n"; 1907 } else { 1908 if (!$api) { 1909 echo '<tr><td>' . scrub_out($song->file) . "</td><td>" . T_('No Update Needed') . "</td></tr>\n"; 1910 } 1911 } 1912 flush(); 1913 } // foreach songs 1914 if (!$api) { 1915 echo "</tbody></table>\n"; 1916 } 1917 // Update the tags for 1918 switch ($type) { 1919 case 'album': 1920 $tags = self::getSongTags('album', $libitem->id); 1921 Tag::update_tag_list(implode(',', $tags), 'album', $libitem->id, false); 1922 Album::update_album_counts($libitem->id); 1923 break; 1924 case 'artist': 1925 foreach ($libitem->get_child_ids() as $album_id) { 1926 $album_tags = self::getSongTags('album', $album_id); 1927 Tag::update_tag_list(implode(',', $album_tags), 'album', $album_id, false); 1928 Album::update_album_counts($album_id); 1929 } 1930 $tags = self::getSongTags('artist', $libitem->id); 1931 Tag::update_tag_list(implode(',', $tags), 'artist', $libitem->id, false); 1932 Artist::update_artist_counts($libitem->id); 1933 break; 1934 } // end switch type 1935 1936 static::getAlbumRepository()->collectGarbage(); 1937 Artist::garbage_collection(); 1938 1939 return $result; 1940 } // update_single_item 1941 1942 /** 1943 * update_media_from_tags 1944 * This is a 'wrapper' function calls the update function for the media 1945 * type in question 1946 * @param Song|Video|Podcast_Episode $media 1947 * @param array $gather_types 1948 * @param string $sort_pattern 1949 * @param string $rename_pattern 1950 * @return array 1951 */ 1952 public static function update_media_from_tags( 1953 $media, 1954 $gather_types = array('music'), 1955 $sort_pattern = '', 1956 $rename_pattern = '' 1957 ) { 1958 $catalog = self::create_from_id($media->catalog); 1959 if ($catalog === null) { 1960 debug_event(self::class, 'update_media_from_tags: Error loading catalog ' . $media->catalog, 2); 1961 1962 return array(); 1963 } 1964 if (Core::get_filesize(Core::conv_lc_file($media->file)) == 0) { 1965 debug_event(self::class, 'update_media_from_tags: Error loading file ' . $media->file, 2); 1966 1967 return array(); 1968 } 1969 1970 $type = ObjectTypeToClassNameMapper::reverseMap(get_class($media)); 1971 // Figure out what type of object this is and call the right function 1972 $name = ($type == 'song') ? 'song' : 'video'; 1973 1974 $functions = [ 1975 'song' => static function ($results, $media) { 1976 return self::update_song_from_tags($results, $media); 1977 }, 1978 'video' => static function ($results, $media) { 1979 return self::update_video_from_tags($results, $media); 1980 }, 1981 ]; 1982 1983 $callable = $functions[$name]; 1984 1985 // try and get the tags from your file 1986 $extension = strtolower(pathinfo($media->file, PATHINFO_EXTENSION)); 1987 $results = $catalog->get_media_tags($media, $gather_types, $sort_pattern, $rename_pattern); 1988 // for files without tags try to update from their file name instead 1989 if ($media->id && in_array($extension, array('wav', 'shn'))) { 1990 debug_event(self::class, 'update_media_from_tags: ' . $extension . ' extension: parse_pattern', 2); 1991 // match against your catalog 'Filename Pattern' and 'Folder Pattern' 1992 $patres = vainfo::parse_pattern($media->file, $catalog->sort_pattern, $catalog->rename_pattern); 1993 $results = array_merge($results, $patres); 1994 1995 return $callable($results, $media); 1996 } 1997 debug_event(self::class, 'Reading tags from ' . $media->file, 4); 1998 1999 return $callable($results, $media); 2000 } // update_media_from_tags 2001 2002 /** 2003 * update_song_from_tags 2004 * Updates the song info based on tags; this is called from a bunch of 2005 * different places and passes in a full fledged song object, so it's a 2006 * static function. 2007 * FIXME: This is an ugly mess, this really needs to be consolidated and 2008 * cleaned up. 2009 * @param array $results 2010 * @param Song $song 2011 * @return array 2012 * @throws ReflectionException 2013 */ 2014 public static function update_song_from_tags($results, Song $song) 2015 { 2016 // info for the song table. This is all the primary file data that is song related 2017 $new_song = new Song(); 2018 $new_song->file = $results['file']; 2019 $new_song->year = (strlen((string)$results['year']) > 4) 2020 ? (int)substr($results['year'], -4, 4) 2021 : (int)($results['year']); 2022 $new_song->title = self::check_length(self::check_title($results['title'], $new_song->file)); 2023 $new_song->bitrate = $results['bitrate']; 2024 $new_song->rate = $results['rate']; 2025 $new_song->mode = ($results['mode'] == 'cbr') ? 'cbr' : 'vbr'; 2026 $new_song->size = $results['size']; 2027 $new_song->time = (strlen((string)$results['time']) > 5) 2028 ? (int)substr($results['time'], -5, 5) 2029 : (int)($results['time']); 2030 if ($new_song->time < 0) { 2031 // fall back to last time if you fail to scan correctly 2032 $new_song->time = $song->time; 2033 } 2034 $new_song->track = self::check_track((string)$results['track']); 2035 $new_song->mbid = $results['mb_trackid']; 2036 $new_song->composer = self::check_length($results['composer']); 2037 $new_song->mime = $results['mime']; 2038 2039 // info for the song_data table. used in Song::update_song 2040 $new_song->comment = $results['comment']; 2041 $new_song->lyrics = str_replace( 2042 ["\r\n", "\r", "\n"], 2043 '<br />', 2044 strip_tags($results['lyrics']) 2045 ); 2046 if (isset($results['license'])) { 2047 $licenseRepository = static::getLicenseRepository(); 2048 $licenseName = (string) $results['license']; 2049 $licenseId = $licenseRepository->find($licenseName); 2050 2051 $new_song->license = $licenseId === 0 ? $licenseRepository->create($licenseName, '', '') : $licenseId; 2052 } else { 2053 $new_song->license = null; 2054 } 2055 $new_song->label = isset($results['publisher']) ? self::check_length($results['publisher'], 128) : null; 2056 if ($song->label && AmpConfig::get('label')) { 2057 // create the label if missing 2058 foreach (array_map('trim', explode(';', $new_song->label)) as $label_name) { 2059 Label::helper($label_name); 2060 } 2061 } 2062 $new_song->language = self::check_length($results['language'], 128); 2063 $new_song->replaygain_track_gain = !is_null($results['replaygain_track_gain']) ? (float) $results['replaygain_track_gain'] : null; 2064 $new_song->replaygain_track_peak = !is_null($results['replaygain_track_peak']) ? (float) $results['replaygain_track_peak'] : null; 2065 $new_song->replaygain_album_gain = !is_null($results['replaygain_album_gain']) ? (float) $results['replaygain_album_gain'] : null; 2066 $new_song->replaygain_album_peak = !is_null($results['replaygain_album_peak']) ? (float) $results['replaygain_album_peak'] : null; 2067 $new_song->r128_track_gain = !is_null($results['r128_track_gain']) ? (int) $results['r128_track_gain'] : null; 2068 $new_song->r128_album_gain = !is_null($results['r128_album_gain']) ? (int) $results['r128_album_gain'] : null; 2069 2070 // genre is used in the tag and tag_map tables 2071 $new_song->tags = $results['genre']; 2072 $tags = Tag::get_object_tags('song', $song->id); 2073 if ($tags) { 2074 foreach ($tags as $tag) { 2075 $song->tags[] = $tag['name']; 2076 } 2077 } 2078 // info for the artist table. 2079 $artist = self::check_length($results['artist']); 2080 $artist_mbid = $results['mb_artistid']; 2081 $albumartist_mbid = $results['mb_albumartistid']; 2082 // info for the album table. 2083 $album = self::check_length($results['album']); 2084 $album_mbid = $results['mb_albumid']; 2085 $disk = $results['disk']; 2086 // year is also included in album 2087 $album_mbid_group = $results['mb_albumid_group']; 2088 $release_type = self::check_length($results['release_type'], 32); 2089 $release_status = $results['release_status']; 2090 $albumartist = self::check_length($results['albumartist']); 2091 $albumartist = $albumartist ?: null; 2092 $original_year = $results['original_year']; 2093 $barcode = self::check_length($results['barcode'], 64); 2094 $catalog_number = self::check_length($results['catalog_number'], 64); 2095 2096 // check whether this artist exists (and the album_artist) 2097 $new_song->artist = Artist::check($artist, $artist_mbid); 2098 if ($albumartist) { 2099 $new_song->albumartist = Artist::check($albumartist, $albumartist_mbid); 2100 if (!$new_song->albumartist) { 2101 $new_song->albumartist = $song->albumartist; 2102 } 2103 } 2104 if (!$new_song->artist) { 2105 $new_song->artist = $song->artist; 2106 } 2107 2108 // check whether this album exists 2109 $new_song->album = Album::check($song->catalog, $album, $new_song->year, $disk, $album_mbid, $album_mbid_group, $new_song->albumartist, $release_type, $release_status, $original_year, $barcode, $catalog_number); 2110 if (!$new_song->album) { 2111 $new_song->album = $song->album; 2112 } 2113 2114 if ($artist_mbid) { 2115 $new_song->artist_mbid = $artist_mbid; 2116 } 2117 if ($album_mbid) { 2118 $new_song->album_mbid = $album_mbid; 2119 } 2120 if ($albumartist_mbid) { 2121 $new_song->albumartist_mbid = $albumartist_mbid; 2122 } 2123 2124 /* Since we're doing a full compare make sure we fill the extended information */ 2125 $song->fill_ext_info(); 2126 2127 if (Song::isCustomMetadataEnabled()) { 2128 $ctags = self::get_clean_metadata($song, $results); 2129 if (method_exists($song, 'updateOrInsertMetadata') && $song::isCustomMetadataEnabled()) { 2130 $ctags = array_diff_key($ctags, array_flip($song->getDisabledMetadataFields())); 2131 foreach ($ctags as $tag => $value) { 2132 $field = $song->getField($tag); 2133 $song->updateOrInsertMetadata($field, $value); 2134 } 2135 } 2136 } 2137 2138 // Duplicate arts if required 2139 if (($song->artist && $new_song->artist) && $song->artist != $new_song->artist) { 2140 if (!Art::has_db($new_song->artist, 'artist')) { 2141 Art::duplicate('artist', $song->artist, $new_song->artist); 2142 } 2143 } 2144 if (($song->albumartist && $new_song->albumartist) && $song->albumartist != $new_song->albumartist) { 2145 if (!Art::has_db($new_song->albumartist, 'artist')) { 2146 Art::duplicate('artist', $song->albumartist, $new_song->albumartist); 2147 } 2148 } 2149 if (($song->album && $new_song->album) && $song->album != $new_song->album) { 2150 if (!Art::has_db($new_song->album, 'album')) { 2151 Art::duplicate('album', $song->album, $new_song->album); 2152 } 2153 } 2154 if ($song->label && AmpConfig::get('label')) { 2155 $labelRepository = static::getLabelRepository(); 2156 2157 foreach (array_map('trim', explode(';', $song->label)) as $label_name) { 2158 $label_id = Label::helper($label_name) 2159 ?: $labelRepository->lookup($label_name); 2160 if ($label_id > 0) { 2161 $label = new Label($label_id); 2162 $artists = $label->get_artists(); 2163 if (!in_array($song->artist, $artists)) { 2164 debug_event(__CLASS__, "$song->artist: adding association to $label->name", 4); 2165 $labelRepository->addArtistAssoc($label->id, $song->artist); 2166 } 2167 } 2168 } 2169 } 2170 2171 $info = Song::compare_song_information($song, $new_song); 2172 if ($info['change']) { 2173 debug_event(self::class, "$song->file : differences found, updating database", 4); 2174 2175 // Update the song and song_data table 2176 Song::update_song($song->id, $new_song); 2177 2178 // If you've migrated the album/artist you need to migrate their data here 2179 self::migrate('artist', $song->artist, $new_song->artist); 2180 self::migrate('album', $song->album, $new_song->album); 2181 2182 if ($song->tags != $new_song->tags) { 2183 // we do still care if there are no tags on your object 2184 $tag_comma = (!empty($new_song->tags)) 2185 ? implode(',', $new_song->tags) 2186 : ''; 2187 Tag::update_tag_list($tag_comma, 'song', $song->id, true); 2188 self::updateAlbumTags($song); 2189 self::updateArtistTags($song); 2190 } 2191 if ($song->license != $new_song->license) { 2192 Song::update_license($new_song->license, $song->id); 2193 } 2194 $update_time = time(); 2195 Song::update_utime($song->id, $update_time); 2196 } else { 2197 debug_event(self::class, "$song->file : no differences found", 5); 2198 } 2199 2200 // If song rating tag exists and is well formed (array user=>rating), update it 2201 if ($song->id && array_key_exists('rating', $results) && is_array($results['rating'])) { 2202 // For each user's ratings, call the function 2203 foreach ($results['rating'] as $user => $rating) { 2204 debug_event(self::class, "Updating rating for Song " . $song->id . " to $rating for user $user", 5); 2205 $o_rating = new Rating($song->id, 'song'); 2206 $o_rating->set_rating($rating, $user); 2207 } 2208 } 2209 2210 return $info; 2211 } // update_song_from_tags 2212 2213 /** 2214 * @param $results 2215 * @param Video $video 2216 * @return array 2217 */ 2218 public static function update_video_from_tags($results, Video $video) 2219 { 2220 /* Setup the vars */ 2221 $new_video = new Video(); 2222 $new_video->file = $results['file']; 2223 $new_video->title = $results['title']; 2224 $new_video->size = $results['size']; 2225 $new_video->video_codec = $results['video_codec']; 2226 $new_video->audio_codec = $results['audio_codec']; 2227 $new_video->resolution_x = $results['resolution_x']; 2228 $new_video->resolution_y = $results['resolution_y']; 2229 $new_video->time = $results['time']; 2230 $new_video->release_date = $results['release_date'] ?: 0; 2231 $new_video->bitrate = $results['bitrate']; 2232 $new_video->mode = $results['mode']; 2233 $new_video->channels = $results['channels']; 2234 $new_video->display_x = $results['display_x']; 2235 $new_video->display_y = $results['display_y']; 2236 $new_video->frame_rate = $results['frame_rate']; 2237 $new_video->video_bitrate = (int) self::check_int($results['video_bitrate'], 4294967294, 0); 2238 $tags = Tag::get_object_tags('video', $video->id); 2239 if ($tags) { 2240 foreach ($tags as $tag) { 2241 $video->tags[] = $tag['name']; 2242 } 2243 } 2244 $new_video->tags = $results['genre']; 2245 2246 $info = Video::compare_video_information($video, $new_video); 2247 if ($info['change']) { 2248 debug_event(self::class, $video->file . " : differences found, updating database", 5); 2249 2250 Video::update_video($video->id, $new_video); 2251 2252 if ($video->tags != $new_video->tags) { 2253 Tag::update_tag_list(implode(',', $new_video->tags), 'video', $video->id, true); 2254 } 2255 Video::update_video_counts($video->id); 2256 } else { 2257 debug_event(self::class, $video->file . " : no differences found", 5); 2258 } 2259 2260 return $info; 2261 } 2262 2263 /** 2264 * Get rid of all tags found in the libraryItem 2265 * @param library_item $libraryItem 2266 * @param array $metadata 2267 * @return array 2268 */ 2269 private static function get_clean_metadata(library_item $libraryItem, $metadata) 2270 { 2271 $tags = array_diff_key($metadata, get_object_vars($libraryItem), array_flip($libraryItem::$aliases ?: array())); 2272 2273 return array_filter($tags); 2274 } 2275 2276 /** 2277 * update the artist or album counts on catalog changes 2278 */ 2279 public static function update_counts() 2280 { 2281 debug_event(self::class, 'update_counts after catalog changes', 5); 2282 // fix object_count table missing artist row 2283 $sql = "SELECT `object_id`, `date`, `user`, `agent`, `geo_latitude`, `geo_longitude`, `geo_name`, `count_type` FROM `object_count` WHERE `object_type` = 'song' AND `count_type` = 'stream' AND `date` NOT IN (SELECT `date` from `object_count` WHERE `count_type` = 'stream' AND `object_type` = 'artist') LIMIT 100;"; 2284 $db_results = Dba::read($sql); 2285 while ($row = Dba::fetch_assoc($db_results)) { 2286 $song = new Song($row['object_id']); 2287 $sql = "INSERT INTO `object_count` (`object_type`, `object_id`, `count_type`, `date`, `user`, `agent`, `geo_latitude`, `geo_longitude`, `geo_name`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; 2288 Dba::write($sql, array('artist', $song->artist, $row['count_type'], $row['date'], $row['user'], $row['agent'], $row['geo_latitude'], $row['geo_longitude'], $row['geo_name'])); 2289 } 2290 // fix object_count table missing album row 2291 $sql = "SELECT `object_id`, `date`, `user`, `agent`, `geo_latitude`, `geo_longitude`, `geo_name`, `count_type` FROM `object_count` WHERE `object_type` = 'song' AND `count_type` = 'stream' AND `date` NOT IN (SELECT `date` from `object_count` WHERE `count_type` = 'stream' AND `object_type` = 'album') LIMIT 100;"; 2292 $db_results = Dba::read($sql); 2293 while ($row = Dba::fetch_assoc($db_results)) { 2294 $song = new Song($row['object_id']); 2295 $sql = "INSERT INTO `object_count` (`object_type`, `object_id`, `count_type`, `date`, `user`, `agent`, `geo_latitude`, `geo_longitude`, `geo_name`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; 2296 Dba::write($sql, array('album', $song->album, $row['count_type'], $row['date'], $row['user'], $row['agent'], $row['geo_latitude'], $row['geo_longitude'], $row['geo_name'])); 2297 } 2298 // object_count.album 2299 $sql = "UPDATE `object_count`, (SELECT `song_count`.`date`, `song`.`id` as `songid`, `song`.`album`, `album_count`.`object_id` as `albumid`, `album_count`.`user`, `album_count`.`agent`, `album_count`.`count_type` FROM `song` LEFT JOIN `object_count` as `song_count` on `song_count`.`object_type` = 'song' and `song_count`.`count_type` = 'stream' and `song_count`.`object_id` = `song`.`id` LEFT JOIN `object_count` as `album_count` on `album_count`.`object_type` = 'album' and `album_count`.`count_type` = 'stream' and `album_count`.`date` = `song_count`.`date` WHERE `song_count`.`date` IS NOT NULL AND `song`.`album` != `album_count`.`object_id` AND `album_count`.`count_type` = 'stream') AS `album_check` SET `object_count`.`object_id` = `album_check`.`album` WHERE `object_count`.`object_id` != `album_check`.`album` AND `object_count`.`object_type` = 'album' AND `object_count`.`date` = `album_check`.`date` AND `object_count`.`user` = `album_check`.`user` AND `object_count`.`agent` = `album_check`.`agent` AND `object_count`.`count_type` = `album_check`.`count_type`;"; 2300 Dba::write($sql); 2301 // object_count.artist 2302 $sql = "UPDATE `object_count`, (SELECT `song_count`.`date`, MIN(`song`.`id`) as `songid`, MIN(`song`.`artist`) AS `artist`, `artist_count`.`object_id` as `artistid`, `artist_count`.`user`, `artist_count`.`agent`, `artist_count`.`count_type` FROM `song` LEFT JOIN `object_count` as `song_count` on `song_count`.`object_type` = 'song' and `song_count`.`count_type` = 'stream' and `song_count`.`object_id` = `song`.`id` LEFT JOIN `object_count` as `artist_count` on `artist_count`.`object_type` = 'artist' and `artist_count`.`count_type` = 'stream' and `artist_count`.`date` = `song_count`.`date` WHERE `song_count`.`date` IS NOT NULL AND `song`.`artist` != `artist_count`.`object_id` AND `artist_count`.`count_type` = 'stream' GROUP BY `artist_count`.`object_id`, `date`,`user`,`agent`,`count_type`) AS `artist_check` SET `object_count`.`object_id` = `artist_check`.`artist` WHERE `object_count`.`object_id` != `artist_check`.`artist` AND `object_count`.`object_type` = 'artist' AND `object_count`.`date` = `artist_check`.`date` AND `object_count`.`user` = `artist_check`.`user` AND `object_count`.`agent` = `artist_check`.`agent` AND `object_count`.`count_type` = `artist_check`.`count_type`;"; 2303 Dba::write($sql); 2304 // song.played might have had issues 2305 $sql = "UPDATE `song` SET `song`.`played` = 0 WHERE `song`.`played` = 1 AND `song`.`id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_type` = 'song' AND `count_type` = 'stream');"; 2306 Dba::write($sql); 2307 $sql = "UPDATE `song` SET `song`.`played` = 1 WHERE `song`.`played` = 0 AND `song`.`id` IN (SELECT `object_id` FROM `object_count` WHERE `object_type` = 'song' AND `count_type` = 'stream');"; 2308 Dba::write($sql); 2309 // fix up incorrect total_count values too 2310 $sql = "UPDATE `artist` SET `total_count` = 0 WHERE `total_count` > 0 AND `id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'artist' AND `object_count`.`count_type` = 'stream');"; 2311 Dba::write($sql); 2312 $sql = "UPDATE `album` SET `total_count` = 0 WHERE `total_count` > 0 AND `id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'album' AND `object_count`.`count_type` = 'stream');"; 2313 Dba::write($sql); 2314 $sql = "UPDATE `song` SET `total_count` = 0 WHERE `total_count` > 0 AND `id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'song' AND `object_count`.`count_type` = 'stream');"; 2315 Dba::write($sql); 2316 $sql = "UPDATE `song` SET `total_skip` = 0 WHERE `total_skip` > 0 AND `id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'song' AND `object_count`.`count_type` = 'stream');"; 2317 Dba::write($sql); 2318 if (AmpConfig::get('podcast')) { 2319 $sql = "UPDATE `podcast_episode` SET `total_count` = 0 WHERE `total_count` > 0 AND `id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'podcast_episode' AND `object_count`.`count_type` = 'stream');"; 2320 Dba::write($sql); 2321 $sql = "UPDATE `podcast_episode` SET `total_skip` = 0 WHERE `total_skip` > 0 AND `id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'podcast_episode' AND `object_count`.`count_type` = 'stream');"; 2322 Dba::write($sql); 2323 $sql = "UPDATE `podcast_episode` SET `podcast_episode`.`played` = 0 WHERE `podcast_episode`.`played` = 1 AND `podcast_episode`.`id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_type` = 'podcast_episode' AND `count_type` = 'stream');"; 2324 Dba::write($sql); 2325 $sql = "UPDATE `podcast_episode` SET `podcast_episode`.`played` = 1 WHERE `podcast_episode`.`played` = 0 AND `podcast_episode`.`id` IN (SELECT `object_id` FROM `object_count` WHERE `object_type` = 'podcast_episode' AND `count_type` = 'stream');"; 2326 Dba::write($sql); 2327 // podcast_episode.total_count 2328 $sql = "UPDATE `podcast_episode`, (SELECT COUNT(`object_count`.`object_id`) AS `total_count`, `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'podcast_episode' AND `object_count`.`count_type` = 'stream' GROUP BY `object_count`.`object_id`) AS `object_count` SET `podcast_episode`.`total_count` = `object_count`.`total_count` WHERE `podcast_episode`.`total_count` != `object_count`.`total_count` AND `podcast_episode`.`id` = `object_count`.`object_id`;"; 2329 Dba::write($sql); 2330 // podcast.total_count 2331 $sql = "UPDATE `podcast`, (SELECT SUM(`podcast_episode`.`total_count`) AS `total_count`, `podcast` FROM `podcast_episode` GROUP BY `podcast_episode`.`podcast`) AS `object_count` SET `podcast`.`total_count` = `object_count`.`total_count` WHERE `podcast`.`total_count` != `object_count`.`total_count` AND `podcast`.`id` = `object_count`.`podcast`;"; 2332 Dba::write($sql); 2333 } 2334 if (AmpConfig::get('allow_video')) { 2335 $sql = "UPDATE `video` SET `total_count` = 0 WHERE `total_count` > 0 AND `id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'video' AND `object_count`.`count_type` = 'stream');"; 2336 Dba::write($sql); 2337 $sql = "UPDATE `video` SET `total_skip` = 0 WHERE `total_skip` > 0 AND `id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'video' AND `object_count`.`count_type` = 'stream');"; 2338 Dba::write($sql); 2339 $sql = "UPDATE `video` SET `video`.`played` = 0 WHERE `video`.`played` = 1 AND `video`.`id` NOT IN (SELECT `object_id` FROM `object_count` WHERE `object_type` = 'video' AND `count_type` = 'stream');"; 2340 Dba::write($sql); 2341 $sql = "UPDATE `video` SET `video`.`played` = 1 WHERE `video`.`played` = 0 AND `video`.`id` IN (SELECT `object_id` FROM `object_count` WHERE `object_type` = 'video' AND `count_type` = 'stream');"; 2342 Dba::write($sql); 2343 // video.total_count 2344 $sql = "UPDATE `video`, (SELECT COUNT(`object_count`.`object_id`) AS `total_count`, `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'video' AND `object_count`.`count_type` = 'stream' GROUP BY `object_count`.`object_id`) AS `object_count` SET `video`.`total_count` = `object_count`.`total_count` WHERE `video`.`total_count` != `object_count`.`total_count` AND `video`.`id` = `object_count`.`object_id`;"; 2345 Dba::write($sql); 2346 } 2347 // artist.album_count 2348 $sql = "UPDATE `artist`, (SELECT COUNT(DISTINCT `album`.`id`) AS `album_count`, `album_artist` FROM `album` LEFT JOIN `catalog` ON `catalog`.`id` = `album`.`catalog` WHERE `catalog`.`enabled` = '1' GROUP BY `album_artist`) AS `album` SET `artist`.`album_count` = `album`.`album_count` WHERE `artist`.`album_count` != `album`.`album_count` AND `artist`.`id` = `album`.`album_artist`;"; 2349 Dba::write($sql); 2350 // artist.album_group_count 2351 $sql = "UPDATE `artist`, (SELECT COUNT(DISTINCT CONCAT(COALESCE(`album`.`prefix`, ''), `album`.`name`, COALESCE(`album`.`album_artist`, ''), COALESCE(`album`.`mbid`, ''), COALESCE(`album`.`year`, ''))) AS `album_group_count`, `album_artist` FROM `album` LEFT JOIN `catalog` ON `catalog`.`id` = `album`.`catalog` WHERE `catalog`.`enabled` = '1' GROUP BY `album_artist`) AS `album` SET `artist`.`album_group_count` = `album`.`album_group_count` WHERE `artist`.`album_group_count` != `album`.`album_group_count` AND `artist`.`id` = `album`.`album_artist`;"; 2352 Dba::write($sql); 2353 // artist.song_count 2354 $sql = "UPDATE `artist`, (SELECT COUNT(`song`.`id`) AS `song_count`, `artist` FROM `song` LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` WHERE `catalog`.`enabled` = '1' GROUP BY `artist`) AS `song` SET `artist`.`song_count` = `song`.`song_count` WHERE `artist`.`song_count` != `song`.`song_count` AND `artist`.`id` = `song`.`artist`;"; 2355 Dba::write($sql); 2356 // artist.total_count 2357 $sql = "UPDATE `artist`, (SELECT COUNT(`object_count`.`object_id`) AS `total_count`, `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'artist' AND `object_count`.`count_type` = 'stream' GROUP BY `object_count`.`object_id`) AS `object_count` SET `artist`.`total_count` = `object_count`.`total_count` WHERE `artist`.`total_count` != `object_count`.`total_count` AND `artist`.`id` = `object_count`.`object_id`;"; 2358 Dba::write($sql); 2359 // artist.time 2360 $sql = "UPDATE `artist`, (SELECT sum(`song`.`time`) as `time`, `song`.`artist` FROM `song` GROUP BY `song`.`artist`) AS `song` SET `artist`.`time` = `song`.`time` WHERE `artist`.`id` = `song`.`artist` AND (`artist`.`time` != `song`.`time` OR `artist`.`time` IS NULL);"; 2361 Dba::write($sql); 2362 // album.time 2363 $sql = "UPDATE `album`, (SELECT sum(`song`.`time`) as `time`, `song`.`album` FROM `song` GROUP BY `song`.`album`) AS `song` SET `album`.`time` = `song`.`time` WHERE `album`.`id` = `song`.`album` AND (`album`.`time` != `song`.`time` OR `album`.`time` IS NULL);"; 2364 Dba::write($sql); 2365 // album.addition_time 2366 $sql = "UPDATE `album`, (SELECT MIN(`song`.`addition_time`) AS `addition_time`, `song`.`album` FROM `song` GROUP BY `song`.`album`) AS `song` SET `album`.`addition_time` = `song`.`addition_time` WHERE `album`.`addition_time` != `song`.`addition_time` AND `song`.`album` = `album`.`id`;"; 2367 Dba::write($sql); 2368 // album.total_count 2369 $sql = "UPDATE `album`, (SELECT COUNT(`object_count`.`object_id`) AS `total_count`, `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'album' AND `object_count`.`count_type` = 'stream' GROUP BY `object_count`.`object_id`) AS `object_count` SET `album`.`total_count` = `object_count`.`total_count` WHERE `album`.`total_count` != `object_count`.`total_count` AND `album`.`id` = `object_count`.`object_id`;"; 2370 Dba::write($sql); 2371 // album.song_count 2372 $sql = "UPDATE `album`, (SELECT COUNT(`song`.`id`) AS `song_count`, `album` FROM `song` LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` WHERE `catalog`.`enabled` = '1' GROUP BY `album`) AS `song` SET `album`.`song_count` = `song`.`song_count` WHERE `album`.`song_count` != `song`.`song_count` AND `album`.`id` = `song`.`album`;"; 2373 Dba::write($sql); 2374 // album.artist_count 2375 $sql = "UPDATE `album`, (SELECT COUNT(DISTINCT(`song`.`artist`)) AS `artist_count`, `album` FROM `song` LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` WHERE `catalog`.`enabled` = '1' GROUP BY `album`) AS `song` SET `album`.`artist_count` = `song`.`artist_count` WHERE `album`.`artist_count` != `song`.`artist_count` AND `album`.`id` = `song`.`album`;"; 2376 Dba::write($sql); 2377 // song.total_count 2378 $sql = "UPDATE `song`, (SELECT COUNT(`object_count`.`object_id`) AS `total_count`, `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'song' AND `object_count`.`count_type` = 'stream' GROUP BY `object_count`.`object_id`) AS `object_count` SET `song`.`total_count` = `object_count`.`total_count` WHERE `song`.`total_count` != `object_count`.`total_count` AND `song`.`id` = `object_count`.`object_id`;"; 2379 Dba::write($sql); 2380 // song.total_skip 2381 $sql = "UPDATE `song`, (SELECT COUNT(`object_count`.`object_id`) AS `total_skip`, `object_id` FROM `object_count` WHERE `object_count`.`object_type` = 'song' AND `object_count`.`count_type` = 'skip' GROUP BY `object_count`.`object_id`) AS `object_count` SET `song`.`total_skip` = `object_count`.`total_skip` WHERE `song`.`total_skip` != `object_count`.`total_skip` AND `song`.`id` = `object_count`.`object_id`;"; 2382 Dba::write($sql); 2383 2384 // update server total counts 2385 $catalog_disable = AmpConfig::get('catalog_disable'); 2386 // tables with media items to count, song-related tables and the rest 2387 $media_tables = array('song', 'video', 'podcast_episode'); 2388 $items = 0; 2389 $time = 0; 2390 $size = 0; 2391 foreach ($media_tables as $table) { 2392 $enabled_sql = ($catalog_disable && $table !== 'podcast_episode') ? " WHERE `$table`.`enabled`='1'" : ''; 2393 $sql = "SELECT COUNT(`id`), IFNULL(SUM(`time`), 0), IFNULL(SUM(`size`), 0) FROM `$table`" . $enabled_sql; 2394 $db_results = Dba::read($sql); 2395 $data = Dba::fetch_row($db_results); 2396 // save the object and add to the current size 2397 $items += (int)$data[0]; 2398 $time += (int)$data[1]; 2399 $size += (int)$data[2]; 2400 self::set_count($table, (int)$data[0]); 2401 } 2402 self::set_count('items', $items); 2403 self::set_count('time', $time); 2404 self::set_count('size', $size); 2405 2406 $song_tables = array('artist', 'album'); 2407 foreach ($song_tables as $table) { 2408 $sql = "SELECT COUNT(DISTINCT(`$table`)) FROM `song`"; 2409 $db_results = Dba::read($sql); 2410 $data = Dba::fetch_row($db_results); 2411 self::set_count($table, (int)$data[0]); 2412 } 2413 // grouped album counts 2414 $sql = "SELECT COUNT(DISTINCT(`album`.`id`)) AS `count` FROM `album` WHERE `id` in (SELECT MIN(`id`) from `album` GROUP BY `album`.`prefix`, `album`.`name`, `album`.`album_artist`, `album`.`release_type`, `album`.`release_status`, `album`.`mbid`, `album`.`year`, `album`.`original_year`);"; 2415 $db_results = Dba::read($sql); 2416 $data = Dba::fetch_row($db_results); 2417 self::set_count('album_group', (int)$data[0]); 2418 2419 $list_tables = array('search', 'playlist', 'live_stream', 'podcast', 'user', 'catalog', 'label', 'tag', 'share', 'license'); 2420 foreach ($list_tables as $table) { 2421 $sql = "SELECT COUNT(`id`) FROM `$table`"; 2422 $db_results = Dba::read($sql); 2423 $data = Dba::fetch_row($db_results); 2424 self::set_count($table, (int)$data[0]); 2425 } 2426 // user accounts may have different items to return based on catalog_filter so lets set those too 2427 User::update_counts(); 2428 debug_event(self::class, 'update_counts completed', 5); 2429 } 2430 2431 /** 2432 * 2433 * @param library_item $libraryItem 2434 * @param array $metadata 2435 */ 2436 public static function add_metadata(library_item $libraryItem, $metadata) 2437 { 2438 $tags = self::get_clean_metadata($libraryItem, $metadata); 2439 2440 foreach ($tags as $tag => $value) { 2441 $field = $libraryItem->getField($tag); 2442 $libraryItem->addMetadata($field, $value); 2443 } 2444 } 2445 2446 /** 2447 * get_media_tags 2448 * @param Song|Video|Podcast_Episode $media 2449 * @param array $gather_types 2450 * @param string $sort_pattern 2451 * @param string $rename_pattern 2452 * @return array 2453 */ 2454 public function get_media_tags($media, $gather_types, $sort_pattern, $rename_pattern) 2455 { 2456 // Check for patterns 2457 if (!$sort_pattern || !$rename_pattern) { 2458 $sort_pattern = $this->sort_pattern; 2459 $rename_pattern = $this->rename_pattern; 2460 } 2461 2462 $vainfo = $this->getUtilityFactory()->createVaInfo( 2463 $media->file, 2464 $gather_types, 2465 '', 2466 '', 2467 $sort_pattern, 2468 $rename_pattern 2469 ); 2470 try { 2471 $vainfo->get_info(); 2472 } catch (Exception $error) { 2473 debug_event(self::class, 'Error ' . $error->getMessage(), 1); 2474 2475 return array(); 2476 } 2477 2478 $key = VaInfo::get_tag_type($vainfo->tags); 2479 2480 return VaInfo::clean_tag_info($vainfo->tags, $key, $media->file); 2481 } 2482 2483 /** 2484 * get_gather_types 2485 * @param string $media_type 2486 * @return array 2487 */ 2488 public function get_gather_types($media_type = '') 2489 { 2490 $gtypes = $this->gather_types; 2491 if (empty($gtypes)) { 2492 $gtypes = "music"; 2493 } 2494 $types = explode(',', $gtypes); 2495 2496 if ($media_type == "video") { 2497 $types = array_diff($types, array('music')); 2498 } 2499 2500 if ($media_type == "music") { 2501 $types = array_diff($types, array('personal_video', 'movie', 'tvshow', 'clip')); 2502 } 2503 2504 return $types; 2505 } 2506 2507 /** 2508 * get_table_from_type 2509 * @param string $gather_type 2510 * @return string 2511 */ 2512 public static function get_table_from_type($gather_type) 2513 { 2514 switch ($gather_type) { 2515 case 'clip': 2516 case 'tvshow': 2517 case 'movie': 2518 case 'personal_video': 2519 $table = 'video'; 2520 break; 2521 case 'podcast': 2522 $table = 'podcast_episode'; 2523 break; 2524 case 'music': 2525 default: 2526 $table = 'song'; 2527 break; 2528 } 2529 2530 return $table; 2531 } 2532 2533 /** 2534 * clean_empty_albums 2535 */ 2536 public static function clean_empty_albums() 2537 { 2538 $sql = "SELECT `id`, `album_artist` FROM `album` WHERE NOT EXISTS (SELECT `id` FROM `song` WHERE `song`.`album` = `album`.`id`)"; 2539 $db_results = Dba::read($sql); 2540 $artists = array(); 2541 while ($album = Dba::fetch_assoc($db_results)) { 2542 $object_id = $album['id']; 2543 $sql = "DELETE FROM `album` WHERE `id` = ?"; 2544 $db_results = Dba::write($sql, array($object_id)); 2545 $artists[] = (int) $album['album_artist']; 2546 } 2547 } 2548 2549 /** 2550 * clean_catalog 2551 * 2552 * Cleans the catalog of files that no longer exist. 2553 */ 2554 public function clean_catalog() 2555 { 2556 // We don't want to run out of time 2557 set_time_limit(0); 2558 2559 debug_event(self::class, 'Starting clean on ' . $this->name, 5); 2560 2561 if (!defined('SSE_OUTPUT') && !defined('CLI')) { 2562 require Ui::find_template('show_clean_catalog.inc.php'); 2563 ob_flush(); 2564 flush(); 2565 } 2566 2567 $dead_total = $this->clean_catalog_proc(); 2568 if ($dead_total > 0) { 2569 self::clean_empty_albums(); 2570 } 2571 2572 debug_event(self::class, 'clean finished, ' . $dead_total . ' removed from ' . $this->name, 4); 2573 2574 if (!defined('SSE_OUTPUT') && !defined('CLI')) { 2575 Ui::show_box_top(); 2576 } 2577 Ui::update_text(T_("Catalog Cleaned"), 2578 sprintf(nT_("%d file removed.", "%d files removed.", $dead_total), $dead_total)); 2579 if (!defined('SSE_OUTPUT') && !defined('CLI')) { 2580 Ui::show_box_bottom(); 2581 } 2582 2583 $this->update_last_clean(); 2584 } // clean_catalog 2585 2586 /** 2587 * verify_catalog 2588 * This function verify the catalog 2589 */ 2590 public function verify_catalog() 2591 { 2592 if (!defined('SSE_OUTPUT') && !defined('CLI')) { 2593 require Ui::find_template('show_verify_catalog.inc.php'); 2594 ob_flush(); 2595 flush(); 2596 } 2597 2598 $verified = $this->verify_catalog_proc(); 2599 2600 if (!defined('SSE_OUTPUT') && !defined('CLI')) { 2601 Ui::show_box_top(); 2602 } 2603 Ui::update_text(T_("Catalog Verified"), 2604 sprintf(nT_('%d file updated.', '%d files updated.', $verified['updated']), $verified['updated'])); 2605 if (!defined('SSE_OUTPUT') && !defined('CLI')) { 2606 Ui::show_box_bottom(); 2607 } 2608 2609 return true; 2610 } // verify_catalog 2611 2612 /** 2613 * trim_prefix 2614 * Splits the prefix from the string 2615 * @param string $string 2616 * @return array 2617 */ 2618 public static function trim_prefix($string) 2619 { 2620 $prefix_pattern = '/^(' . implode('\\s|', 2621 explode('|', AmpConfig::get('catalog_prefix_pattern'))) . '\\s)(.*)/i'; 2622 preg_match($prefix_pattern, $string, $matches); 2623 2624 if (count($matches)) { 2625 $string = trim((string)$matches[2]); 2626 $prefix = trim((string)$matches[1]); 2627 } else { 2628 $prefix = null; 2629 } 2630 2631 return array('string' => $string, 'prefix' => $prefix); 2632 } // trim_prefix 2633 2634 /** 2635 * @param $year 2636 * @return integer 2637 */ 2638 public static function normalize_year($year) 2639 { 2640 if (empty($year)) { 2641 return 0; 2642 } 2643 2644 $year = (int)($year); 2645 if ($year < 0 || $year > 9999) { 2646 return 0; 2647 } 2648 2649 return $year; 2650 } 2651 2652 /** 2653 * trim_slashed_list 2654 * Split items by configurable delimiter 2655 * Return first item as string = default 2656 * Return all items as array if doTrim = false passed as optional parameter 2657 * @param string $string 2658 * @param bool $doTrim 2659 * @return string|array 2660 */ 2661 public static function trim_slashed_list($string, $doTrim = true) 2662 { 2663 $delimiters = static::getConfigContainer()->get(ConfigurationKeyEnum::ADDITIONAL_DELIMITERS); 2664 $pattern = '~[\s]?(' . $delimiters . ')[\s]?~'; 2665 $items = preg_split($pattern, $string); 2666 $items = array_map('trim', $items); 2667 2668 if ((isset($items) && isset($items[0])) && $doTrim) { 2669 return $items[0]; 2670 } 2671 2672 return $items; 2673 } // trim_slashed_list 2674 2675 /** 2676 * trim_featuring 2677 * Splits artists featuring from the string 2678 * @param string $string 2679 * @return array 2680 */ 2681 public static function trim_featuring($string) 2682 { 2683 return array_map('trim', explode(' feat. ', $string)); 2684 } // trim_featuring 2685 2686 /** 2687 * check_title 2688 * this checks to make sure something is 2689 * set on the title, if it isn't it looks at the 2690 * filename and tries to set the title based on that 2691 * @param string $title 2692 * @param string $file 2693 * @return string 2694 */ 2695 public static function check_title($title, $file = '') 2696 { 2697 if (strlen(trim((string)$title)) < 1) { 2698 $title = Dba::escape($file); 2699 } 2700 2701 return $title; 2702 } // check_title 2703 2704 /** 2705 * check_length 2706 * Check to make sure the string fits into the database 2707 * max_length is the maximum number of characters that the (varchar) column can hold 2708 * @param string $string 2709 * @param integer $max_length 2710 * @return string 2711 */ 2712 public static function check_length($string, $max_length = 255) 2713 { 2714 $string = (string)$string; 2715 if (false !== $encoding = mb_detect_encoding($string, null, true)) { 2716 $string = trim(mb_substr($string, 0, $max_length, $encoding)); 2717 } else { 2718 $string = trim(substr($string, 0, $max_length)); 2719 } 2720 2721 return $string; 2722 } 2723 2724 /** 2725 * check_track 2726 * Check to make sure the track number fits into the database: max 32767, min -32767 2727 * 2728 * @param string $track 2729 * @return integer 2730 */ 2731 public static function check_track($track) 2732 { 2733 $retval = ((int)$track > 32767 || (int)$track < -32767) ? (int)substr($track, -4, 4) : (int)$track; 2734 if ((int)$track !== $retval) { 2735 debug_event(__CLASS__, "check_track: '{$track}' out of range. Changed into '{$retval}'", 4); 2736 } 2737 2738 return $retval; 2739 } 2740 2741 /** 2742 * check_int 2743 * Check to make sure a number fits into the database 2744 * 2745 * @param integer $track 2746 * @param integer $max 2747 * @param integer $min 2748 * @return integer 2749 */ 2750 public static function check_int($track, $max, $min) 2751 { 2752 if ($track > $max) { 2753 return $max; 2754 } 2755 if ($track < $min) { 2756 return $min; 2757 } 2758 2759 return $track; 2760 } 2761 2762 /** 2763 * get_unique_string 2764 * Check to make sure the string doesn't have duplicate strings ({)e.g. "Enough Records; Enough Records") 2765 * 2766 * @param string $str_array 2767 * @return string 2768 */ 2769 public static function get_unique_string($str_array) 2770 { 2771 $array = array_unique(array_map('trim', explode(';', $str_array))); 2772 2773 return implode($array); 2774 } 2775 2776 /** 2777 * playlist_import 2778 * Attempts to create a Public Playlist based on the playlist file 2779 * @param string $playlist_file 2780 * @param int $user_id 2781 * @param string $playlist_type (public|private) 2782 * @return array 2783 */ 2784 public static function import_playlist($playlist_file, $user_id, $playlist_type) 2785 { 2786 $data = file_get_contents($playlist_file); 2787 if (substr($playlist_file, -3, 3) == 'm3u' || substr($playlist_file, -4, 4) == 'm3u8') { 2788 $files = self::parse_m3u($data); 2789 } elseif (substr($playlist_file, -3, 3) == 'pls') { 2790 $files = self::parse_pls($data); 2791 } elseif (substr($playlist_file, -3, 3) == 'asx') { 2792 $files = self::parse_asx($data); 2793 } elseif (substr($playlist_file, -4, 4) == 'xspf') { 2794 $files = self::parse_xspf($data); 2795 } 2796 2797 $songs = array(); 2798 $import = array(); 2799 $pinfo = pathinfo($playlist_file); 2800 $track = 1; 2801 $web_path = AmpConfig::get('web_path'); 2802 if (isset($files)) { 2803 foreach ($files as $file) { 2804 $found = false; 2805 $file = trim((string)$file); 2806 $orig = $file; 2807 // Check to see if it's a url from this ampache instance 2808 if (!empty($web_path) && substr($file, 0, strlen($web_path)) == $web_path) { 2809 $data = Stream_Url::parse($file); 2810 $sql = 'SELECT COUNT(*) FROM `song` WHERE `id` = ?'; 2811 $db_results = Dba::read($sql, array($data['id'])); 2812 if (Dba::num_rows($db_results) && (int)$data['id'] > 0) { 2813 debug_event(self::class, "import_playlist identified: {" . $data['id'] . "}", 5); 2814 $songs[$track] = $data['id']; 2815 $track++; 2816 $found = true; 2817 } 2818 } else { 2819 // Remove file:// prefix if any 2820 if (strpos($file, "file://") !== false) { 2821 $file = urldecode(substr($file, 7)); 2822 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 2823 // Removing starting / on Windows OS. 2824 if (substr($file, 0, 1) == '/') { 2825 $file = substr($file, 1); 2826 } 2827 // Restore real directory separator 2828 $file = str_replace("/", DIRECTORY_SEPARATOR, $file); 2829 } 2830 } 2831 2832 // First, try to find the file as absolute path 2833 $sql = "SELECT `id` FROM `song` WHERE `file` = ?"; 2834 $db_results = Dba::read($sql, array($file)); 2835 $results = Dba::fetch_assoc($db_results); 2836 2837 if ((int)$results['id'] > 0) { 2838 debug_event(self::class, "import_playlist identified: {" . (int)$results['id'] . "}", 5); 2839 $songs[$track] = (int)$results['id']; 2840 $track++; 2841 $found = true; 2842 } else { 2843 // Not found in absolute path, create it from relative path 2844 $file = $pinfo['dirname'] . DIRECTORY_SEPARATOR . $file; 2845 // Normalize the file path. realpath requires the files to exists. 2846 $file = realpath($file); 2847 if ($file) { 2848 $sql = "SELECT `id` FROM `song` WHERE `file` = ?"; 2849 $db_results = Dba::read($sql, array($file)); 2850 $results = Dba::fetch_assoc($db_results); 2851 2852 if ((int)$results['id'] > 0) { 2853 debug_event(self::class, "import_playlist identified: {" . (int)$results['id'] . "}", 5); 2854 $songs[$track] = (int)$results['id']; 2855 $track++; 2856 $found = true; 2857 } 2858 } 2859 } 2860 } // if it's a file 2861 if (!$found) { 2862 debug_event(self::class, "import_playlist skipped: {{$orig}}", 5); 2863 } 2864 // add the results to an array to display after 2865 $import[] = array( 2866 'track' => $track - 1, 2867 'file' => $orig, 2868 'found' => (int)$found 2869 ); 2870 } 2871 } 2872 2873 debug_event(self::class, "import_playlist Parsed " . $playlist_file . ", found " . count($songs) . " songs", 5); 2874 2875 if (count($songs)) { 2876 $name = $pinfo['filename']; 2877 $playlist_id = (int)Playlist::create($name, $playlist_type, $user_id); 2878 2879 if ($playlist_id < 1) { 2880 return array( 2881 'success' => false, 2882 'error' => T_('Failed to create playlist'), 2883 ); 2884 } 2885 2886 $playlist = new Playlist($playlist_id); 2887 $playlist->delete_all(); 2888 $playlist->add_songs($songs); 2889 2890 return array( 2891 'success' => true, 2892 'id' => $playlist_id, 2893 'count' => count($songs), 2894 'results' => $import 2895 ); 2896 } 2897 2898 return array( 2899 'success' => false, 2900 'error' => T_('No valid songs found in playlist file'), 2901 'results' => $import 2902 ); 2903 } 2904 2905 /** 2906 * parse_m3u 2907 * this takes m3u filename and then attempts to found song filenames listed in the m3u 2908 * @param string $data 2909 * @return array 2910 */ 2911 public static function parse_m3u($data) 2912 { 2913 $files = array(); 2914 $results = explode("\n", $data); 2915 2916 foreach ($results as $value) { 2917 $value = trim((string)$value); 2918 if (!empty($value) && substr($value, 0, 1) != '#') { 2919 $files[] = $value; 2920 } 2921 } 2922 2923 return $files; 2924 } // parse_m3u 2925 2926 /** 2927 * parse_pls 2928 * this takes pls filename and then attempts to found song filenames listed in the pls 2929 * @param string $data 2930 * @return array 2931 */ 2932 public static function parse_pls($data) 2933 { 2934 $files = array(); 2935 $results = explode("\n", $data); 2936 2937 foreach ($results as $value) { 2938 $value = trim((string)$value); 2939 if (preg_match("/file[0-9]+[\s]*\=(.*)/i", $value, $matches)) { 2940 $file = trim((string)$matches[1]); 2941 if (!empty($file)) { 2942 $files[] = $file; 2943 } 2944 } 2945 } 2946 2947 return $files; 2948 } // parse_pls 2949 2950 /** 2951 * parse_asx 2952 * this takes asx filename and then attempts to found song filenames listed in the asx 2953 * @param string $data 2954 * @return array 2955 */ 2956 public static function parse_asx($data) 2957 { 2958 $files = array(); 2959 $xml = simplexml_load_string($data); 2960 2961 if ($xml) { 2962 foreach ($xml->entry as $entry) { 2963 $file = trim((string)$entry->ref['href']); 2964 if (!empty($file)) { 2965 $files[] = $file; 2966 } 2967 } 2968 } 2969 2970 return $files; 2971 } // parse_asx 2972 2973 /** 2974 * parse_xspf 2975 * this takes xspf filename and then attempts to found song filenames listed in the xspf 2976 * @param string $data 2977 * @return array 2978 */ 2979 public static function parse_xspf($data) 2980 { 2981 $files = array(); 2982 $xml = simplexml_load_string($data); 2983 if ($xml) { 2984 foreach ($xml->trackList->track as $track) { 2985 $file = trim((string)$track->location); 2986 if (!empty($file)) { 2987 $files[] = $file; 2988 } 2989 } 2990 } 2991 2992 return $files; 2993 } // parse_xspf 2994 2995 /** 2996 * delete 2997 * Deletes the catalog and everything associated with it 2998 * it takes the catalog id 2999 * @param integer $catalog_id 3000 * @return boolean 3001 */ 3002 public static function delete($catalog_id) 3003 { 3004 // Large catalog deletion can take time 3005 set_time_limit(0); 3006 $params = array($catalog_id); 3007 3008 // First remove the songs in this catalog 3009 $sql = "DELETE FROM `song` WHERE `catalog` = ?"; 3010 $db_results = Dba::write($sql, $params); 3011 3012 // Only if the previous one works do we go on 3013 if (!$db_results) { 3014 return false; 3015 } 3016 self::clean_empty_albums(); 3017 3018 $sql = "DELETE FROM `video` WHERE `catalog` = ?"; 3019 $db_results = Dba::write($sql, $params); 3020 3021 if (!$db_results) { 3022 return false; 3023 } 3024 3025 $sql = "DELETE FROM `podcast` WHERE `catalog` = ?"; 3026 $db_results = Dba::write($sql, $params); 3027 3028 if (!$db_results) { 3029 return false; 3030 } 3031 3032 $sql = "DELETE FROM `live_stream` WHERE `catalog` = ?"; 3033 $db_results = Dba::write($sql, $params); 3034 3035 if (!$db_results) { 3036 return false; 3037 } 3038 3039 $catalog = self::create_from_id($catalog_id); 3040 3041 if (!$catalog->id) { 3042 return false; 3043 } 3044 3045 $sql = 'DELETE FROM `catalog_' . $catalog->get_type() . '` WHERE catalog_id = ?'; 3046 $db_results = Dba::write($sql, $params); 3047 3048 if (!$db_results) { 3049 return false; 3050 } 3051 3052 // Next Remove the Catalog Entry it's self 3053 $sql = "DELETE FROM `catalog` WHERE `id` = ?"; 3054 Dba::write($sql, $params); 3055 3056 // run garbage collection 3057 static::getCatalogGarbageCollector()->collect(); 3058 3059 return true; 3060 } // delete 3061 3062 /** 3063 * exports the catalog 3064 * it exports all songs in the database to the given export type. 3065 * @param string $type 3066 * @param integer|null $catalog_id 3067 */ 3068 public static function export($type, $catalog_id = null) 3069 { 3070 // Select all songs in catalog 3071 $params = array(); 3072 if ($catalog_id) { 3073 $sql = "SELECT `id` FROM `song` WHERE `catalog`= ? ORDER BY `album`, `track`"; 3074 $params[] = $catalog_id; 3075 } else { 3076 $sql = 'SELECT `id` FROM `song` ORDER BY `album`, `track`'; 3077 } 3078 $db_results = Dba::read($sql, $params); 3079 3080 switch ($type) { 3081 case 'itunes': 3082 echo static::xml_get_header('itunes'); 3083 while ($results = Dba::fetch_assoc($db_results)) { 3084 $song = new Song($results['id']); 3085 $song->format(); 3086 3087 $xml = array(); 3088 $xml['key'] = $results['id']; 3089 $xml['dict']['Track ID'] = (int)($results['id']); 3090 $xml['dict']['Name'] = $song->title; 3091 $xml['dict']['Artist'] = $song->f_artist_full; 3092 $xml['dict']['Album'] = $song->f_album_full; 3093 $xml['dict']['Total Time'] = (int) ($song->time) * 1000; // iTunes uses milliseconds 3094 $xml['dict']['Track Number'] = (int) ($song->track); 3095 $xml['dict']['Year'] = (int) ($song->year); 3096 $xml['dict']['Date Added'] = get_datetime((int) $song->addition_time, 'short', 'short', "Y-m-d\TH:i:s\Z"); 3097 $xml['dict']['Bit Rate'] = (int) ($song->bitrate / 1000); 3098 $xml['dict']['Sample Rate'] = (int) ($song->rate); 3099 $xml['dict']['Play Count'] = (int) ($song->played); 3100 $xml['dict']['Track Type'] = "URL"; 3101 $xml['dict']['Location'] = $song->play_url(); 3102 echo (string) xoutput_from_array($xml, true, 'itunes'); 3103 // flush output buffer 3104 } // while result 3105 echo static::xml_get_footer('itunes'); 3106 break; 3107 case 'csv': 3108 echo "ID,Title,Artist,Album,Length,Track,Year,Date Added,Bitrate,Played,File\n"; 3109 while ($results = Dba::fetch_assoc($db_results)) { 3110 $song = new Song($results['id']); 3111 $song->format(); 3112 echo '"' . $song->id . '","' . $song->title . '","' . $song->f_artist_full . '","' . $song->f_album_full . '","' . $song->f_time . '","' . $song->f_track . '","' . $song->year . '","' . get_datetime((int)$song->addition_time) . '","' . $song->f_bitrate . '","' . $song->played . '","' . $song->file . '"' . "\n"; 3113 } 3114 break; 3115 } // end switch 3116 } // export 3117 3118 /** 3119 * Update the catalog mapping for various types 3120 * @param string $table 3121 */ 3122 public static function update_mapping($table) 3123 { 3124 // fill the data 3125 debug_event(self::class, 'Update mapping for table: ' . $table, 5); 3126 $sql = "REPLACE INTO `catalog_map` (`catalog_id`, `object_type`, `object_id`) SELECT `$table`.`catalog`, '$table', `$table`.`id` FROM `$table` WHERE `$table`.`catalog` > 0;"; 3127 Dba::write($sql); 3128 } 3129 3130 /** 3131 * Update the catalog mapping for various types 3132 */ 3133 public static function garbage_collect_mapping() 3134 { 3135 // delete non-existent maps 3136 $tables = ['album', 'artist', 'song', 'video', 'podcast', 'podcast_episode', 'live_stream']; 3137 foreach ($tables as $type) { 3138 $sql = "DELETE FROM `catalog_map` USING `catalog_map` LEFT JOIN `$type` ON `$type`.`id`=`catalog_map`.`object_id` WHERE `catalog_map`.`object_type`='$type' AND `$type`.`id` IS NULL;"; 3139 Dba::write($sql); 3140 } 3141 $sql = "DELETE FROM `catalog_map` WHERE `catalog_id` = 0"; 3142 Dba::write($sql); 3143 } 3144 3145 /** 3146 * Update the catalog map for a single item 3147 */ 3148 public static function update_map($catalog, $object_type, $object_id) 3149 { 3150 debug_event(self::class, "update_map $object_type: {{$object_id}}", 5); 3151 if ($object_type == 'artist') { 3152 $sql = "REPLACE INTO `catalog_map` (`catalog_id`, `object_type`, `object_id`) SELECT `song`.`catalog`, 'artist', `artist`.`id` FROM `artist` LEFT JOIN `song` ON `song`.`artist` = `artist`.`id` WHERE `artist`.`id` = ? AND `song`.`catalog` > 0;"; 3153 Dba::write($sql, array($object_id)); 3154 $sql = "REPLACE INTO `catalog_map` (`catalog_id`, `object_type`, `object_id`) SELECT `album`.`catalog`, 'artist', `artist`.`id` FROM `artist` LEFT JOIN `album` ON `album`.`album_artist` = `artist`.`id` WHERE `artist`.`id` = ? AND `album`.`catalog` > 0;"; 3155 Dba::write($sql, array($object_id)); 3156 } elseif ($catalog > 0) { 3157 $sql = "REPLACE INTO `catalog_map` (`catalog_id`, `object_type`, `object_id`) VALUES (?, ?, ?);"; 3158 Dba::write($sql, array($catalog, $object_type, $object_id)); 3159 } 3160 } 3161 3162 /** 3163 * Migrate an object associated catalog to a new object 3164 * @param string $object_type 3165 * @param integer $old_object_id 3166 * @param integer $new_object_id 3167 * @return PDOStatement|boolean 3168 */ 3169 public static function migrate_map($object_type, $old_object_id, $new_object_id) 3170 { 3171 $sql = "UPDATE IGNORE `catalog_map` SET `object_id` = ? WHERE `object_type` = ? AND `object_id` = ?"; 3172 $params = array($new_object_id, $object_type, $old_object_id); 3173 3174 return Dba::write($sql, $params); 3175 } 3176 3177 /** 3178 * Updates album tags from given song 3179 * @param Song $song 3180 */ 3181 protected static function updateAlbumTags(Song $song) 3182 { 3183 $tags = self::getSongTags('album', $song->album); 3184 Tag::update_tag_list(implode(',', $tags), 'album', $song->album, true); 3185 } 3186 3187 /** 3188 * Updates artist tags from given song 3189 * @param Song $song 3190 */ 3191 protected static function updateArtistTags(Song $song) 3192 { 3193 $tags = self::getSongTags('artist', $song->artist); 3194 Tag::update_tag_list(implode(',', $tags), 'artist', $song->artist, true); 3195 } 3196 3197 /** 3198 * Get all tags from all Songs from [type] (artist, album, ...) 3199 * @param string $type 3200 * @param integer $object_id 3201 * @return array 3202 */ 3203 protected static function getSongTags($type, $object_id) 3204 { 3205 $tags = array(); 3206 $db_results = Dba::read("SELECT `tag`.`name` FROM `tag` JOIN `tag_map` ON `tag`.`id` = `tag_map`.`tag_id` JOIN `song` ON `tag_map`.`object_id` = `song`.`id` WHERE `song`.`$type` = ? AND `tag_map`.`object_type` = 'song' GROUP BY `tag`.`id`, `tag`.`name`", 3207 array($object_id)); 3208 while ($row = Dba::fetch_assoc($db_results)) { 3209 $tags[] = $row['name']; 3210 } 3211 3212 return $tags; 3213 } 3214 3215 /** 3216 * @param Artist|Album|Song|Video|Podcast_Episode|TvShow|TVShow_Episode|Label|TVShow_Season $libitem 3217 * @param integer|null $user_id 3218 * @return boolean 3219 */ 3220 public static function can_remove($libitem, $user_id = null) 3221 { 3222 if (!$user_id) { 3223 $user_id = Core::get_global('user')->id; 3224 } 3225 3226 if (!$user_id) { 3227 return false; 3228 } 3229 3230 if (!AmpConfig::get('delete_from_disk')) { 3231 return false; 3232 } 3233 3234 return ( 3235 Access::check('interface', 75) || 3236 ($libitem->get_user_owner() == $user_id && AmpConfig::get('upload_allow_remove')) 3237 ); 3238 } 3239 3240 /** 3241 * process_action 3242 * @param string $action 3243 * @param $catalogs 3244 * @param array $options 3245 * @noinspection PhpMissingBreakStatementInspection 3246 */ 3247 public static function process_action($action, $catalogs, $options = null) 3248 { 3249 if (!$options || !is_array($options)) { 3250 $options = array(); 3251 } 3252 3253 switch ($action) { 3254 case 'add_to_all_catalogs': 3255 $catalogs = self::get_catalogs(); 3256 // Intentional break fall-through 3257 case 'add_to_catalog': 3258 case 'import_to_catalog': 3259 $options = ($action == 'import_to_catalog') 3260 ? array('gather_art' => false, 'parse_playlist' => true) 3261 : $options; 3262 if ($catalogs) { 3263 foreach ($catalogs as $catalog_id) { 3264 $catalog = self::create_from_id($catalog_id); 3265 if ($catalog !== null) { 3266 $catalog->add_to_catalog($options); 3267 } 3268 } 3269 3270 if (!defined('SSE_OUTPUT') && !defined('CLI')) { 3271 echo AmpError::display('catalog_add'); 3272 } 3273 } 3274 break; 3275 case 'update_all_catalogs': 3276 $catalogs = self::get_catalogs(); 3277 // Intentional break fall-through 3278 case 'update_catalog': 3279 if ($catalogs) { 3280 foreach ($catalogs as $catalog_id) { 3281 $catalog = self::create_from_id($catalog_id); 3282 if ($catalog !== null) { 3283 $catalog->verify_catalog(); 3284 } 3285 } 3286 } 3287 break; 3288 case 'full_service': 3289 if (!$catalogs) { 3290 $catalogs = self::get_catalogs(); 3291 } 3292 3293 /* This runs the clean/verify/add in that order */ 3294 foreach ($catalogs as $catalog_id) { 3295 $catalog = self::create_from_id($catalog_id); 3296 if ($catalog !== null) { 3297 $catalog->clean_catalog(); 3298 $catalog->verify_catalog(); 3299 $catalog->add_to_catalog(); 3300 } 3301 } 3302 Dba::optimize_tables(); 3303 break; 3304 case 'clean_all_catalogs': 3305 $catalogs = self::get_catalogs(); 3306 // Intentional break fall-through 3307 case 'clean_catalog': 3308 if ($catalogs) { 3309 foreach ($catalogs as $catalog_id) { 3310 $catalog = self::create_from_id($catalog_id); 3311 if ($catalog !== null) { 3312 $catalog->clean_catalog(); 3313 } 3314 } // end foreach catalogs 3315 Dba::optimize_tables(); 3316 } 3317 break; 3318 case 'update_from': 3319 $catalog_id = 0; 3320 // First see if we need to do an add 3321 if ($options['add_path'] != '/' && strlen((string)$options['add_path'])) { 3322 if ($catalog_id = Catalog_local::get_from_path($options['add_path'])) { 3323 $catalog = self::create_from_id($catalog_id); 3324 if ($catalog !== null) { 3325 $catalog->add_to_catalog(array('subdirectory' => $options['add_path'])); 3326 } 3327 } 3328 } // end if add 3329 3330 // Now check for an update 3331 if ($options['update_path'] != '/' && strlen((string)$options['update_path'])) { 3332 if ($catalog_id = Catalog_local::get_from_path($options['update_path'])) { 3333 $songs = Song::get_from_path($options['update_path']); 3334 foreach ($songs as $song_id) { 3335 self::update_single_item('song', $song_id); 3336 } 3337 } 3338 } // end if update 3339 3340 if ($catalog_id < 1) { 3341 AmpError::add('general', 3342 T_("This subdirectory is not inside an existing Catalog. The update can not be processed.")); 3343 } 3344 break; 3345 case 'gather_media_art': 3346 if (!$catalogs) { 3347 $catalogs = self::get_catalogs(); 3348 } 3349 3350 // Iterate throughout the catalogs and gather as needed 3351 foreach ($catalogs as $catalog_id) { 3352 $catalog = self::create_from_id($catalog_id); 3353 if ($catalog !== null) { 3354 require Ui::find_template('show_gather_art.inc.php'); 3355 flush(); 3356 $catalog->gather_art(); 3357 } 3358 } 3359 break; 3360 case 'update_all_file_tags': 3361 $catalogs = self::get_catalogs(); 3362 // Intentional break fall-through 3363 case 'update_file_tags': 3364 $write_tags = AmpConfig::get('write_tags', false); 3365 AmpConfig::set_by_array(['write_tags' => 'true'], true); 3366 3367 $id3Writer = static::getSongTagWriter(); 3368 set_time_limit(0); 3369 foreach ($catalogs as $catalog_id) { 3370 $catalog = self::create_from_id($catalog_id); 3371 if ($catalog !== null) { 3372 $song_ids = $catalog->get_song_ids(); 3373 foreach ($song_ids as $song_id) { 3374 $song = new Song($song_id); 3375 $song->format(); 3376 3377 $id3Writer->write($song); 3378 } 3379 } 3380 } 3381 AmpConfig::set_by_array(['write_tags' => $write_tags], true); 3382 } 3383 3384 // Remove any orphaned artists/albums/etc. 3385 debug_event(self::class, 'Run Garbage collection', 5); 3386 static::getCatalogGarbageCollector()->collect(); 3387 self::clean_empty_albums(); 3388 Album::update_album_artist(); 3389 self::update_counts(); 3390 } 3391 3392 /** 3393 * Migrate an object associate images to a new object 3394 * @param string $object_type 3395 * @param integer $old_object_id 3396 * @param integer $new_object_id 3397 * @return boolean 3398 */ 3399 public static function migrate($object_type, $old_object_id, $new_object_id) 3400 { 3401 if ($old_object_id != $new_object_id) { 3402 debug_event(__CLASS__, "migrate $object_type: {{$old_object_id}} to {{$new_object_id}}", 4); 3403 3404 Stats::migrate($object_type, $old_object_id, $new_object_id); 3405 Useractivity::migrate($object_type, $old_object_id, $new_object_id); 3406 Recommendation::migrate($object_type, $old_object_id, $new_object_id); 3407 Share::migrate($object_type, $old_object_id, $new_object_id); 3408 Shoutbox::migrate($object_type, $old_object_id, $new_object_id); 3409 Tag::migrate($object_type, $old_object_id, $new_object_id); 3410 Userflag::migrate($object_type, $old_object_id, $new_object_id); 3411 Rating::migrate($object_type, $old_object_id, $new_object_id); 3412 Art::duplicate($object_type, $old_object_id, $new_object_id); 3413 Playlist::migrate($object_type, $old_object_id, $new_object_id); 3414 Label::migrate($object_type, $old_object_id, $new_object_id); 3415 Wanted::migrate($object_type, $old_object_id, $new_object_id); 3416 Metadata::migrate($object_type, $old_object_id, $new_object_id); 3417 Bookmark::migrate($object_type, $old_object_id, $new_object_id); 3418 self::migrate_map($object_type, $old_object_id, $new_object_id); 3419 switch ($object_type) { 3420 case 'artist': 3421 Artist::update_artist_counts($new_object_id); 3422 break; 3423 case 'album': 3424 Album::update_album_counts($new_object_id); 3425 break; 3426 } 3427 3428 return true; 3429 } 3430 3431 return false; 3432 } 3433 3434 /** 3435 * xml_get_footer 3436 * This takes the type and returns the correct xml footer 3437 * @param string $type 3438 * @return string 3439 */ 3440 private static function xml_get_footer($type) 3441 { 3442 switch ($type) { 3443 case 'itunes': 3444 return " </dict>\n" . 3445 "</dict>\n" . 3446 "</plist>\n"; 3447 case 'xspf': 3448 return " </trackList>\n" . 3449 "</playlist>\n"; 3450 default: 3451 return ''; 3452 } 3453 } // xml_get_footer 3454 3455 /** 3456 * xml_get_header 3457 * This takes the type and returns the correct xml header 3458 * @param string $type 3459 * @return string 3460 */ 3461 private static function xml_get_header($type) 3462 { 3463 switch ($type) { 3464 case 'itunes': 3465 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" . 3466 "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\"\n" . 3467 "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" . 3468 "<plist version=\"1.0\">\n" . 3469 "<dict>\n" . 3470 " <key>Major Version</key><integer>1</integer>\n" . 3471 " <key>Minor Version</key><integer>1</integer>\n" . 3472 " <key>Application Version</key><string>7.0.2</string>\n" . 3473 " <key>Features</key><integer>1</integer>\n" . 3474 " <key>Show Content Ratings</key><true/>\n" . 3475 " <key>Tracks</key>\n" . 3476 " <dict>\n"; 3477 case 'xspf': 3478 return "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" . 3479 "<!-- XML Generated by Ampache v." . AmpConfig::get('version') . " -->"; 3480 default: 3481 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; 3482 } 3483 } // xml_get_header 3484 3485 /** 3486 * @deprecated 3487 */ 3488 private static function getSongRepository(): SongRepositoryInterface 3489 { 3490 global $dic; 3491 3492 return $dic->get(SongRepositoryInterface::class); 3493 } 3494 3495 /** 3496 * @deprecated 3497 */ 3498 private static function getAlbumRepository(): AlbumRepositoryInterface 3499 { 3500 global $dic; 3501 3502 return $dic->get(AlbumRepositoryInterface::class); 3503 } 3504 3505 /** 3506 * @deprecated 3507 */ 3508 private static function getCatalogGarbageCollector(): CatalogGarbageCollectorInterface 3509 { 3510 global $dic; 3511 3512 return $dic->get(CatalogGarbageCollectorInterface::class); 3513 } 3514 3515 /** 3516 * @deprecated 3517 */ 3518 private static function getSongTagWriter(): SongTagWriterInterface 3519 { 3520 global $dic; 3521 3522 return $dic->get(SongTagWriterInterface::class); 3523 } 3524 3525 /** 3526 * @deprecated 3527 */ 3528 private static function getLabelRepository(): LabelRepositoryInterface 3529 { 3530 global $dic; 3531 3532 return $dic->get(LabelRepositoryInterface::class); 3533 } 3534 3535 /** 3536 * @deprecated 3537 */ 3538 private static function getLicenseRepository(): LicenseRepositoryInterface 3539 { 3540 global $dic; 3541 3542 return $dic->get(LicenseRepositoryInterface::class); 3543 } 3544 3545 /** 3546 * @deprecated inject by constructor 3547 */ 3548 private static function getConfigContainer(): ConfigContainerInterface 3549 { 3550 global $dic; 3551 3552 return $dic->get(ConfigContainerInterface::class); 3553 } 3554 3555 /** 3556 * @deprecated Inject by constructor 3557 */ 3558 private function getUtilityFactory(): UtilityFactoryInterface 3559 { 3560 global $dic; 3561 3562 return $dic->get(UtilityFactoryInterface::class); 3563 } 3564} 3565