"Fossies" - the Fresh Open Source Software Archive

Member "ampache-5.0.0/src/Repository/Model/Catalog.php" (31 Aug 2021, 138707 Bytes) of package /linux/www/ampache-5.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Catalog.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 /*
    3  * vim:set softtabstop=4 shiftwidth=4 expandtab:
    4  *
    5  * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
    6  * Copyright 2001 - 2020 Ampache.org
    7  *
    8  * This program is free software: you can redistribute it and/or modify
    9  * it under the terms of the GNU Affero General Public License as published by
   10  * the Free Software Foundation, either version 3 of the License, or
   11  * (at your option) any later version.
   12  *
   13  * This program is distributed in the hope that it will be useful,
   14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
   15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   16  * GNU Affero General Public License for more details.
   17  *
   18  * You should have received a copy of the GNU Affero General Public License
   19  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
   20  */
   21 
   22 declare(strict_types=0);
   23 
   24 namespace Ampache\Repository\Model;
   25 
   26 use Ampache\Config\AmpConfig;
   27 use Ampache\Config\ConfigContainerInterface;
   28 use Ampache\Config\ConfigurationKeyEnum;
   29 use Ampache\Module\Art\Collector\ArtCollectorInterface;
   30 use Ampache\Module\Authorization\Access;
   31 use Ampache\Module\Catalog\Catalog_beets;
   32 use Ampache\Module\Catalog\Catalog_beetsremote;
   33 use Ampache\Module\Catalog\Catalog_dropbox;
   34 use Ampache\Module\Catalog\Catalog_local;
   35 use Ampache\Module\Catalog\Catalog_remote;
   36 use Ampache\Module\Catalog\Catalog_Seafile;
   37 use Ampache\Module\Catalog\Catalog_soundcloud;
   38 use Ampache\Module\Catalog\Catalog_subsonic;
   39 use Ampache\Module\Catalog\GarbageCollector\CatalogGarbageCollectorInterface;
   40 use Ampache\Module\Playback\Stream_Url;
   41 use Ampache\Module\Song\Tag\SongTagWriterInterface;
   42 use Ampache\Module\Statistics\Stats;
   43 use Ampache\Module\System\AmpError;
   44 use Ampache\Module\System\Core;
   45 use Ampache\Module\System\Dba;
   46 use Ampache\Module\Util\ObjectTypeToClassNameMapper;
   47 use Ampache\Module\Util\Recommendation;
   48 use Ampache\Module\Util\Ui;
   49 use Ampache\Module\Util\UtilityFactoryInterface;
   50 use Ampache\Module\Util\VaInfo;
   51 use Ampache\Repository\AlbumRepositoryInterface;
   52 use Ampache\Repository\LabelRepositoryInterface;
   53 use Ampache\Repository\LicenseRepositoryInterface;
   54 use Ampache\Repository\Model\Metadata\Repository\Metadata;
   55 use Ampache\Repository\SongRepositoryInterface;
   56 use Ampache\Repository\UserRepositoryInterface;
   57 use Exception;
   58 use PDOStatement;
   59 use 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  */
   65 abstract 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 }