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