1<?php
2/*
3 * vim:set softtabstop=4 shiftwidth=4 expandtab:
4 *
5 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
6 * Copyright 2001 - 2020 Ampache.org
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU Affero General Public License for more details.
17 *
18 * You should have received a copy of the GNU Affero General Public License
19 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20 */
21
22declare(strict_types=0);
23
24namespace Ampache\Repository\Model;
25
26use Ampache\Module\Playback\Stream;
27use Ampache\Module\Playback\Stream_Url;
28use Ampache\Module\Song\Deletion\SongDeleterInterface;
29use Ampache\Module\Song\Tag\SongTagWriterInterface;
30use Ampache\Module\Statistics\Stats;
31use Ampache\Module\System\Dba;
32use Ampache\Module\User\Activity\UserActivityPosterInterface;
33use Ampache\Module\Util\Recommendation;
34use Ampache\Module\Util\Ui;
35use Ampache\Repository\Model\Metadata\Metadata;
36use Ampache\Module\Authorization\Access;
37use Ampache\Config\AmpConfig;
38use Ampache\Module\System\Core;
39use Ampache\Repository\LicenseRepositoryInterface;
40use PDOStatement;
41
42class Song extends database_object implements Media, library_item, GarbageCollectibleInterface
43{
44    use Metadata;
45
46    protected const DB_TABLENAME = 'song';
47
48    /* Variables from DB */
49
50    /**
51     * @var integer $id
52     */
53    public $id;
54    /**
55     * @var string $file
56     */
57    public $file;
58    /**
59     * @var integer $album
60     */
61    public $album;
62    /**
63     * @var integer $artist
64     */
65    public $artist;
66    /**
67     * @var string $title
68     */
69    public $title;
70    /**
71     * @var integer $year
72     */
73    public $year;
74    /**
75     * @var integer $bitrate
76     */
77    public $bitrate;
78    /**
79     * @var integer $rate
80     */
81    public $rate;
82    /**
83     * @var string $mode
84     */
85    public $mode;
86    /**
87     * @var integer $size
88     */
89    public $size;
90    /**
91     * @var integer $time
92     */
93    public $time;
94    /**
95     * @var integer $track
96     */
97    public $track;
98    /**
99     * @var string $album_mbid
100     */
101    public $album_mbid;
102    /**
103     * @var string $artist_mbid
104     */
105    public $artist_mbid;
106    /**
107     * @var string $albumartist_mbid
108     */
109    public $albumartist_mbid;
110    /**
111     * @var string $type
112     */
113    public $type;
114    /**
115     * @var string $mime
116     */
117    public $mime;
118    /**
119     * @var boolean $played
120     */
121    public $played;
122    /**
123     * @var boolean $enabled
124     */
125    public $enabled;
126    /**
127     * @var integer $addition_time
128     */
129    public $addition_time;
130    /**
131     * @var integer $update_time
132     */
133    public $update_time;
134    /**
135     * MusicBrainz ID
136     * @var string $mbid
137     */
138    public $mbid;
139    /**
140     * @var integer $catalog
141     */
142    public $catalog;
143    /**
144     * @var integer|null $waveform
145     */
146    public $waveform;
147    /**
148     * @var integer|null $user_upload
149     */
150    public $user_upload;
151    /**
152     * @var integer|null $license
153     */
154    public $license;
155    /**
156     * @var string $composer
157     */
158    public $composer;
159    /**
160     * @var string $catalog_number
161     */
162    public $catalog_number;
163    /**
164     * @var integer $channels
165     */
166    public $channels;
167
168    /**
169     * @var array $tags
170     */
171    public $tags;
172    /**
173     * @var string $label
174     */
175    public $label;
176    /**
177     * @var string $language
178     */
179    public $language;
180    /**
181     * @var string $comment
182     */
183    public $comment;
184    /**
185     * @var string $lyrics
186     */
187    public $lyrics;
188    /**
189     * @var float|null $replaygain_track_gain
190     */
191    public $replaygain_track_gain;
192    /**
193     * @var float|null $replaygain_track_peak
194     */
195    public $replaygain_track_peak;
196    /**
197     * @var float|null $replaygain_album_gain
198     */
199    public $replaygain_album_gain;
200    /**
201     * @var float|null $replaygain_album_peak
202     */
203    public $replaygain_album_peak;
204    /**
205     * @var integer|null $r128_album_gain
206     */
207    public $r128_album_gain;
208    /**
209     * @var integer|null $r128_track_gain
210     */
211    public $r128_track_gain;
212    /**
213     * @var string $f_title
214     */
215    public $f_title;
216    /**
217     * @var string $f_artist
218     */
219    public $f_artist;
220    /**
221     * @var string $f_album
222     */
223    public $f_album;
224    /**
225     * @var string $f_artist_full
226     */
227    public $f_artist_full;
228    /**
229     * @var integer $albumartist
230     */
231    public $albumartist;
232    /**
233     * @var string $f_albumartist_full
234     */
235    public $f_albumartist_full;
236    /**
237     * @var string $f_album_full
238     */
239    public $f_album_full;
240    /**
241     * @var string $f_time
242     */
243    public $f_time;
244    /**
245     * @var string $f_time_h
246     */
247    public $f_time_h;
248    /**
249     * @var string $f_track
250     */
251    public $f_track;
252    /**
253     * @var string $disk
254     */
255    public $disk;
256    /**
257     * @var string $f_bitrate
258     */
259    public $f_bitrate;
260    /**
261     * @var string $link
262     */
263    public $link;
264    /**
265     * @var string $f_file
266     */
267    public $f_file;
268    /**
269     * @var string $f_title_full
270     */
271    public $f_title_full;
272    /**
273     * @var string $f_link
274     */
275    public $f_link;
276    /**
277     * @var string $f_album_link
278     */
279    public $f_album_link;
280    /**
281     * @var string $f_artist_link
282     */
283    public $f_artist_link;
284    /**
285     * @var string $f_albumartist_link
286     */
287    public $f_albumartist_link;
288
289    /**
290     * @var string $f_year_link
291     */
292    public $f_year_link;
293
294    /**
295     * @var string $f_tags
296     */
297    public $f_tags;
298    /**
299     * @var string $f_size
300     */
301    public $f_size;
302    /**
303     * @var string $f_lyrics
304     */
305    public $f_lyrics;
306    /**
307     * @var string $f_pattern
308     */
309    public $f_pattern;
310    /**
311     * @var integer $count
312     */
313    public $count;
314    /**
315     * @var string $f_publisher
316     */
317    public $f_publisher;
318    /**
319     * @var string $f_composer
320     */
321    public $f_composer;
322    /**
323     * @var string $f_license
324     */
325    public $f_license;
326
327    /** @var int */
328    public $skip_cnt;
329
330    /** @var int */
331    public $object_cnt;
332
333    /** @var int */
334    private $total_count;
335
336    /* Setting Variables */
337    /**
338     * @var boolean $_fake
339     */
340    public $_fake = false; // If this is a 'construct_from_array' object
341
342    /**
343     * Aliases used in insert function
344     */
345    public static $aliases = array(
346        'mb_trackid',
347        'mbid',
348        'mb_albumid',
349        'mb_albumid_group',
350        'mb_artistid',
351        'mb_albumartistid',
352        'genre',
353        'publisher'
354    );
355
356    /**
357     * Constructor
358     *
359     * Song class, for modifying a song.
360     * @param integer|null $songid
361     * @param string $limit_threshold
362     */
363    public function __construct($songid = null, $limit_threshold = '')
364    {
365        if ($songid === null) {
366            return false;
367        }
368
369        $this->id = (int)($songid);
370
371        if (self::isCustomMetadataEnabled()) {
372            $this->initializeMetadata();
373        }
374
375        $info = $this->has_info($limit_threshold);
376        if ($info !== false && is_array($info)) {
377            foreach ($info as $key => $value) {
378                $this->$key = $value;
379            }
380            $data             = pathinfo($this->file);
381            $this->type       = strtolower((string)$data['extension']);
382            $this->mime       = self::type_to_mime($this->type);
383            $this->object_cnt = (int)$this->total_count;
384        } else {
385            $this->id = null;
386
387            return false;
388        }
389
390        return true;
391    } // constructor
392
393    public function getId(): int
394    {
395        return (int) $this->id;
396    }
397
398    /**
399     * insert
400     *
401     * This inserts the song described by the passed array
402     * @param array $results
403     * @return integer|boolean
404     */
405    public static function insert(array $results)
406    {
407        $check_file = Catalog::get_id_from_file($results['file'], 'song');
408        if ($check_file > 0) {
409            return $check_file;
410        }
411        $catalog          = $results['catalog'];
412        $file             = $results['file'];
413        $title            = Catalog::check_length(Catalog::check_title($results['title'], $file));
414        $artist           = Catalog::check_length($results['artist']);
415        $album            = Catalog::check_length($results['album']);
416        $albumartist      = Catalog::check_length($results['albumartist']);
417        $albumartist      = $albumartist ?: null;
418        $bitrate          = $results['bitrate'] ?: 0;
419        $rate             = $results['rate'] ?: 0;
420        $mode             = $results['mode'];
421        $size             = $results['size'] ?: 0;
422        $time             = $results['time'] ?: 0;
423        $track            = Catalog::check_track((string) $results['track']);
424        $track_mbid       = $results['mb_trackid'] ?: $results['mbid'];
425        $track_mbid       = $track_mbid ?: null;
426        $album_mbid       = $results['mb_albumid'];
427        $album_mbid_group = $results['mb_albumid_group'];
428        $artist_mbid      = $results['mb_artistid'];
429        $albumartist_mbid = $results['mb_albumartistid'];
430        $disk             = (Album::sanitize_disk($results['disk']) > 0) ? Album::sanitize_disk($results['disk']) : 1;
431        $year             = Catalog::normalize_year($results['year'] ?: 0);
432        $comment          = $results['comment'];
433        $tags             = $results['genre']; // multiple genre support makes this an array
434        $lyrics           = $results['lyrics'];
435        $user_upload      = isset($results['user_upload']) ? $results['user_upload'] : null;
436        $composer         = isset($results['composer']) ? Catalog::check_length($results['composer']) : null;
437        $label            = isset($results['publisher']) ? Catalog::get_unique_string(Catalog::check_length($results['publisher'], 128)) : null;
438        if ($label && AmpConfig::get('label')) {
439            // create the label if missing
440            foreach (array_map('trim', explode(';', $label)) as $label_name) {
441                Label::helper($label_name);
442            }
443        }
444
445        if (isset($results['license'])) {
446            $licenseRepository = static::getLicenseRepository();
447            $licenseName       = (string) $results['license'];
448            $licenseId         = $licenseRepository->find($licenseName);
449
450            $license = $licenseId === 0 ? $licenseRepository->create($licenseName, '', '') : $licenseId;
451        } else {
452            $license = null;
453        }
454
455        $catalog_number        = isset($results['catalog_number']) ? Catalog::check_length($results['catalog_number'], 64) : null;
456        $language              = isset($results['language']) ? Catalog::check_length($results['language'], 128) : null;
457        $channels              = $results['channels'] ?: 0;
458        $release_type          = isset($results['release_type']) ? Catalog::check_length($results['release_type'], 32) : null;
459        $release_status        = isset($results['release_status']) ? $results['release_status'] : null;
460        $replaygain_track_gain = isset($results['replaygain_track_gain']) ? $results['replaygain_track_gain'] : null;
461        $replaygain_track_peak = isset($results['replaygain_track_peak']) ? $results['replaygain_track_peak'] : null;
462        $replaygain_album_gain = isset($results['replaygain_album_gain']) ? $results['replaygain_album_gain'] : null;
463        $replaygain_album_peak = isset($results['replaygain_album_peak']) ? $results['replaygain_album_peak'] : null;
464        $r128_track_gain       = isset($results['r128_track_gain']) ? $results['r128_track_gain'] : null;
465        $r128_album_gain       = isset($results['r128_album_gain']) ? $results['r128_album_gain'] : null;
466        $original_year         = Catalog::normalize_year($results['original_year'] ?: 0);
467        $barcode               = Catalog::check_length($results['barcode'], 64);
468
469        if (!in_array($mode, ['vbr', 'cbr', 'abr'])) {
470            debug_event(self::class, 'Error analyzing: ' . $file . ' unknown file bitrate mode: ' . $mode, 2);
471            $mode = null;
472        }
473        if (!isset($results['albumartist_id'])) {
474            $albumartist_id = null;
475            if ($albumartist) {
476                // Multiple artist per songs not supported for now
477                $albumartist_mbid = Catalog::trim_slashed_list($albumartist_mbid);
478                $albumartist_id   = Artist::check($albumartist, $albumartist_mbid);
479            }
480        } else {
481            $albumartist_id = (int)($results['albumartist_id']);
482        }
483        if (!isset($results['artist_id'])) {
484            // Multiple artist per songs not supported for now
485            $artist_mbid = Catalog::trim_slashed_list($artist_mbid);
486            $artist_id   = Artist::check($artist, $artist_mbid);
487        } else {
488            $artist_id = (int)($results['artist_id']);
489        }
490        if (!isset($results['album_id'])) {
491            $album_id = Album::check($catalog, $album, $year, $disk, $album_mbid, $album_mbid_group, $albumartist_id, $release_type, $release_status, $original_year, $barcode, $catalog_number);
492        } else {
493            $album_id = (int)($results['album_id']);
494        }
495        $insert_time = time();
496
497        $sql = "INSERT INTO `song` (`catalog`, `file`, `album`, `artist`, `title`, `bitrate`, `rate`, `mode`, `size`, `time`, `track`, `addition_time`, `update_time`, `year`, `mbid`, `user_upload`, `license`, `composer`, `channels`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
498
499        $db_results = Dba::write($sql, array(
500            $catalog,
501            $file,
502            $album_id,
503            $artist_id,
504            $title,
505            $bitrate,
506            $rate,
507            $mode,
508            $size,
509            $time,
510            $track,
511            $insert_time,
512            $insert_time,
513            $year,
514            $track_mbid,
515            $user_upload,
516            $license,
517            $composer,
518            $channels
519        ));
520
521        if (!$db_results) {
522            debug_event(self::class, 'Unable to insert ' . $file, 2);
523
524            return false;
525        }
526
527        $song_id = (int)Dba::insert_id();
528
529        Catalog::update_map((int)$catalog, 'song', $song_id);
530        Album::update_album_counts($album_id);
531        Artist::update_artist_counts($artist_id);
532
533        if ($user_upload) {
534            static::getUserActivityPoster()->post((int) $user_upload, 'upload', 'song', (int) $song_id, time());
535        }
536
537        // Allow scripts to populate new tags when injecting user uploads
538        if (!defined('NO_SESSION')) {
539            if ($user_upload && !Access::check('interface', 50, $user_upload)) {
540                $tags = Tag::clean_to_existing($tags);
541            }
542        }
543        if (is_array($tags)) {
544            foreach ($tags as $tag) {
545                $tag = trim((string)$tag);
546                if (!empty($tag)) {
547                    Tag::add('song', $song_id, $tag, false);
548                    Tag::add('album', $album_id, $tag, false);
549                    Tag::add('artist', $artist_id, $tag, false);
550                }
551            }
552        }
553
554        $sql = "INSERT INTO `song_data` (`song_id`, `comment`, `lyrics`, `label`, `language`, `replaygain_track_gain`, `replaygain_track_peak`, `replaygain_album_gain`, `replaygain_album_peak`, `r128_track_gain`, `r128_album_gain`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
555        Dba::write($sql, array($song_id, $comment, $lyrics, $label, $language, $replaygain_track_gain, $replaygain_track_peak, $replaygain_album_gain, $replaygain_album_peak, $r128_track_gain, $r128_album_gain));
556
557        return $song_id;
558    }
559
560    /**
561     * garbage_collection
562     *
563     * Cleans up the song_data table
564     */
565    public static function garbage_collection()
566    {
567        // delete files matching catalog_ignore_pattern
568        $ignore_pattern = AmpConfig::get('catalog_ignore_pattern');
569        if ($ignore_pattern) {
570            Dba::write("DELETE FROM `song` WHERE `file` REGEXP ?;", array($ignore_pattern));
571        }
572        // delete duplicates
573        Dba::write("DELETE `dupe` FROM `song` AS `dupe`, `song` AS `orig` WHERE `dupe`.`id` > `orig`.`id` AND `dupe`.`file` <=> `orig`.`file`;");
574        // clean up missing catalogs
575        Dba::write("DELETE FROM `song` WHERE `song`.`catalog` NOT IN (SELECT `id` FROM `catalog`);");
576        // delete the rest
577        Dba::write("DELETE FROM `song_data` WHERE `song_data`.`song_id` NOT IN (SELECT `song`.`id` FROM `song`);");
578    }
579
580    /**
581     * build_cache
582     *
583     * This attempts to reduce queries by asking for everything in the
584     * browse all at once and storing it in the cache, this can help if the
585     * db connection is the slow point.
586     * @param integer[] $song_ids
587     * @param string $limit_threshold
588     * @return boolean
589     */
590    public static function build_cache($song_ids, $limit_threshold = '')
591    {
592        if (empty($song_ids)) {
593            return false;
594        }
595
596        $idlist = '(' . implode(',', $song_ids) . ')';
597        if ($idlist == '()') {
598            return false;
599        }
600        $artists = array();
601        $albums  = array();
602        $tags    = array();
603
604        // Song data cache
605        $sql   = (AmpConfig::get('catalog_disable'))
606            ? "SELECT `song`.`id`, `file`, `catalog`, `album`, `year`, `artist`, `title`, `bitrate`, `rate`, `mode`, `size`, `time`, `track`, `played`, `song`.`enabled`, `update_time`, `tag_map`.`tag_id`, `mbid`, `addition_time`, `license`, `composer`, `user_upload`, `song`.`total_count`, `song`.`total_skip` FROM `song` LEFT JOIN `tag_map` ON `tag_map`.`object_id`=`song`.`id` AND `tag_map`.`object_type`='song' LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` WHERE `song`.`id` IN $idlist AND `catalog`.`enabled` = '1' "
607            : "SELECT `song`.`id`, `file`, `catalog`, `album`, `year`, `artist`, `title`, `bitrate`, `rate`, `mode`, `size`, `time`, `track`, `played`, `song`.`enabled`, `update_time`, `tag_map`.`tag_id`, `mbid`, `addition_time`, `license`, `composer`, `user_upload`, `song`.`total_count`, `song`.`total_skip` FROM `song` LEFT JOIN `tag_map` ON `tag_map`.`object_id`=`song`.`id` AND `tag_map`.`object_type`='song' WHERE `song`.`id` IN $idlist";
608
609        $db_results = Dba::read($sql);
610        while ($row = Dba::fetch_assoc($db_results)) {
611            if (AmpConfig::get('show_played_times')) {
612                $row['object_cnt'] = (!empty($limit_threshold))
613                    ? Stats::get_object_count('song', $row['id'], $limit_threshold)
614                    : $row['total_count'];
615            }
616            if (AmpConfig::get('show_skipped_times')) {
617                $row['skip_cnt'] = (!empty($limit_threshold))
618                    ? Stats::get_object_count('song', $row['id'], $limit_threshold, 'skip')
619                    : $row['total_skip'];
620            }
621            parent::add_to_cache('song', $row['id'], $row);
622            $artists[$row['artist']] = $row['artist'];
623            $albums[$row['album']]   = $row['album'];
624            if ($row['tag_id']) {
625                $tags[$row['tag_id']] = $row['tag_id'];
626            }
627        }
628
629        Artist::build_cache($artists);
630        Album::build_cache($albums);
631        Tag::build_cache($tags);
632        Tag::build_map_cache('song', $song_ids);
633        Art::build_cache($albums);
634
635        // If we're rating this then cache them as well
636        if (AmpConfig::get('ratings')) {
637            Rating::build_cache('song', $song_ids);
638        }
639        if (AmpConfig::get('userflags')) {
640            Userflag::build_cache('song', $song_ids);
641        }
642
643        // Build a cache for the song's extended table
644        $sql        = "SELECT * FROM `song_data` WHERE `song_id` IN $idlist";
645        $db_results = Dba::read($sql);
646
647        while ($row = Dba::fetch_assoc($db_results)) {
648            parent::add_to_cache('song_data', $row['song_id'], $row);
649        }
650
651        return true;
652    } // build_cache
653
654    /**
655     * has_info
656     * @param string $limit_threshold
657     * @return array|boolean
658     */
659    private function has_info($limit_threshold = '')
660    {
661        $song_id = $this->id;
662
663        if (parent::is_cached('song', $song_id)) {
664            return parent::get_from_cache('song', $song_id);
665        }
666
667        $sql        = "SELECT `song`.`id`, `song`.`file`, `song`.`catalog`, `song`.`album`, `song`.`total_count`, `song`.`total_skip`, `album`.`album_artist` AS `albumartist`, `song`.`year`, `song`.`artist`, `song`.`title`, `song`.`bitrate`, `song`.`rate`, `song`.`mode`, `song`.`size`, `song`.`time`, `song`.`track`, `song`.`played`, `song`.`enabled`, `song`.`update_time`, `song`.`mbid`, `song`.`addition_time`, `song`.`license`, `song`.`composer`, `song`.`user_upload`, `album`.`disk`, `album`.`mbid` AS `album_mbid`, `artist`.`mbid` AS `artist_mbid`, `album_artist`.`mbid` AS `albumartist_mbid` FROM `song` LEFT JOIN `album` ON `album`.`id` = `song`.`album` LEFT JOIN `artist` ON `artist`.`id` = `song`.`artist` LEFT JOIN `artist` AS `album_artist` ON `album_artist`.`id` = `album`.`album_artist` WHERE `song`.`id` = ?";
668        $db_results = Dba::read($sql, array($song_id));
669        $results    = Dba::fetch_assoc($db_results);
670        if (isset($results['id'])) {
671            if (AmpConfig::get('show_played_times')) {
672                $results['object_cnt'] = $results['total_count'];
673            }
674            if (AmpConfig::get('show_skipped_times')) {
675                $results['skip_cnt'] = $results['total_skip'];
676            }
677
678            parent::add_to_cache('song', $song_id, $results);
679
680            return $results;
681        }
682
683        return false;
684    }
685
686    /**
687     * can_scrobble
688     *
689     * return a song id based on a last.fm-style search in the database
690     * @param string $song_name
691     * @param string $artist_name
692     * @param string $album_name
693     * @param string $song_mbid
694     * @param string $artist_mbid
695     * @param string $album_mbid
696     * @return string
697     */
698    public static function can_scrobble(
699        $song_name,
700        $artist_name,
701        $album_name,
702        $song_mbid = '',
703        $artist_mbid = '',
704        $album_mbid = ''
705    ) {
706        // by default require song, album, artist for any searches
707        $sql = "SELECT `song`.`id` FROM `song` LEFT JOIN `album` ON `album`.`id` = `song`.`album` LEFT JOIN `artist` ON `artist`.`id` = `song`.`artist` LEFT JOIN `artist` AS `album_artist` ON `album_artist`.`id` = `album`.`album_artist` WHERE `song`.`title` = '" . Dba::escape($song_name) . "' AND (`artist`.`name` = '" . Dba::escape($artist_name) . "' OR LTRIM(CONCAT(COALESCE(`artist`.`prefix`, ''), `artist`.`name`)) = '" . Dba::escape($artist_name) . "') AND (`album`.`name` = '" . Dba::escape($album_name) . "' OR LTRIM(CONCAT(COALESCE(`album`.`prefix`, ''), `album`.`name`)) = '" . Dba::escape($album_name) . "')";
708        if ($song_mbid) {
709            $sql .= " AND `song`.`mbid` = '" . $song_mbid . "'";
710        }
711        if ($artist_mbid) {
712            $sql .= " AND `artist`.`mbid` = '" . $song_mbid . "'";
713        }
714        if ($album_mbid) {
715            $sql .= " AND `album`.`mbid` = '" . $song_mbid . "'";
716        }
717        $db_results = Dba::read($sql);
718        $results    = Dba::fetch_assoc($db_results);
719        if (isset($results['id'])) {
720            return $results['id'];
721        }
722
723        return '';
724    }
725
726    /**
727     * _get_ext_info
728     * This function gathers information from the song_ext_info table and adds it to the
729     * current object
730     * @param string $select
731     * @return array
732     */
733    public function _get_ext_info($select = '')
734    {
735        $song_id = (int) ($this->id);
736        $columns = (!empty($select)) ? Dba::escape($select) : '*';
737
738        if (parent::is_cached('song_data', $song_id)) {
739            return parent::get_from_cache('song_data', $song_id);
740        }
741
742        $sql        = "SELECT $columns FROM `song_data` WHERE `song_id` = ?";
743        $db_results = Dba::read($sql, array($song_id));
744
745        $results = Dba::fetch_assoc($db_results);
746
747        parent::add_to_cache('song_data', $song_id, $results);
748
749        return $results;
750    } // _get_ext_info
751
752    /**
753     * fill_ext_info
754     * This calls the _get_ext_info and then sets the correct vars
755     * @param string $data_filter
756     */
757    public function fill_ext_info($data_filter = '')
758    {
759        $info = $this->_get_ext_info($data_filter);
760
761        if (!empty($info)) {
762            foreach ($info as $key => $value) {
763                if ($key != 'song_id') {
764                    $this->$key = $value;
765                }
766            } // end foreach
767        }
768    } // fill_ext_info
769
770    /**
771     * type_to_mime
772     *
773     * Returns the mime type for the specified file extension/type
774     * @param string $type
775     * @return string
776     */
777    public static function type_to_mime($type)
778    {
779        // FIXME: This should really be done the other way around.
780        // Store the mime type in the database, and provide a function
781        // to make it a human-friendly type.
782        switch ($type) {
783            case 'spx':
784            case 'ogg':
785                return 'application/ogg';
786            case 'opus':
787                return 'audio/ogg; codecs=opus';
788            case 'wma':
789            case 'asf':
790                return 'audio/x-ms-wma';
791            case 'rm':
792            case 'ra':
793                return 'audio/x-realaudio';
794            case 'flac':
795                return 'audio/x-flac';
796            case 'wv':
797                return 'audio/x-wavpack';
798            case 'aac':
799            case 'mp4':
800            case 'm4a':
801            case 'm4b':
802                return 'audio/mp4';
803            case 'aacp':
804                return 'audio/aacp';
805            case 'mpc':
806                return 'audio/x-musepack';
807            case 'mkv':
808                return 'audio/x-matroska';
809            case 'mpeg3':
810            case 'mp3':
811            default:
812                return 'audio/mpeg';
813        }
814    }
815
816    /**
817     * get_disabled
818     *
819     * Gets a list of the disabled songs for and returns an array of Songs
820     * @param integer $count
821     * @return Song[]
822     */
823    public static function get_disabled($count = 0)
824    {
825        $results = array();
826
827        $sql = "SELECT `id` FROM `song` WHERE `enabled`='0'";
828        if ($count) {
829            $sql .= " LIMIT $count";
830        }
831        $db_results = Dba::read($sql);
832
833        while ($row = Dba::fetch_assoc($db_results)) {
834            $results[] = new Song($row['id']);
835        }
836
837        return $results;
838    }
839
840    /**
841     * find
842     * @param array $data
843     * @return boolean
844     */
845    public static function find($data)
846    {
847        $sql_base = "SELECT `song`.`id` FROM `song`";
848        if ($data['mb_trackid']) {
849            $sql        = $sql_base . " WHERE `song`.`mbid` = ? LIMIT 1";
850            $db_results = Dba::read($sql, array($data['mb_trackid']));
851            if ($results = Dba::fetch_assoc($db_results)) {
852                return $results['id'];
853            }
854        }
855        if ($data['file']) {
856            $sql        = $sql_base . " WHERE `song`.`file` = ? LIMIT 1";
857            $db_results = Dba::read($sql, array($data['file']));
858            if ($results = Dba::fetch_assoc($db_results)) {
859                return $results['id'];
860            }
861        }
862
863        $where  = "WHERE `song`.`title` = ?";
864        $sql    = $sql_base;
865        $params = array($data['title']);
866        if ($data['track']) {
867            $where .= " AND `song`.`track` = ?";
868            $params[] = $data['track'];
869        }
870        $sql .= " INNER JOIN `artist` ON `artist`.`id` = `song`.`artist`";
871        $sql .= " INNER JOIN `album` ON `album`.`id` = `song`.`album`";
872
873        if ($data['mb_artistid']) {
874            $where .= " AND `artist`.`mbid` = ?";
875            $params[] = $data['mb_albumid'];
876        } else {
877            $where .= " AND `artist`.`name` = ?";
878            $params[] = $data['artist'];
879        }
880        if ($data['mb_albumid']) {
881            $where .= " AND `album`.`mbid` = ?";
882            $params[] = $data['mb_albumid'];
883        } else {
884            $where .= " AND `album`.`name` = ?";
885            $params[] = $data['album'];
886        }
887
888        $sql .= $where . " LIMIT 1";
889        $db_results = Dba::read($sql, $params);
890        if ($results = Dba::fetch_assoc($db_results)) {
891            return $results['id'];
892        }
893
894        return false;
895    }
896
897    /**
898     * Get duplicate information.
899     * @param array $dupe
900     * @param string $search_type
901     * @return integer[]
902     */
903    public static function get_duplicate_info($dupe, $search_type)
904    {
905        $results = array();
906        if (isset($dupe['id'])) {
907            $results[] = $dupe['id'];
908        } else {
909            $sql = "SELECT `id` FROM `song` WHERE `title`='" . Dba::escape($dupe['title']) . "' ";
910
911            if ($search_type == 'artist_title' || $search_type == 'artist_album_title') {
912                $sql .= "AND `artist`='" . Dba::escape($dupe['artist']) . "' ";
913            }
914            if ($search_type == 'artist_album_title') {
915                $sql .= "AND `album` = '" . Dba::escape($dupe['album']) . "' ";
916            }
917            $sql .= 'ORDER BY `time`, `bitrate`, `size`';
918
919            if ($search_type == 'album') {
920                $sql = "SELECT `id` from `song` LEFT JOIN (SELECT MIN(`id`) AS `dupe_id1`, LTRIM(CONCAT(COALESCE(`album`.`prefix`, ''), ' ', `album`.`name`)) AS `fullname`, COUNT(LTRIM(CONCAT(COALESCE(`album`.`prefix`, ''), ' ', `album`.`name`))) AS `Counting` FROM `album` GROUP BY `album_artist`, LTRIM(CONCAT(COALESCE(`album`.`prefix`, ''), ' ', `album`.`name`)), `disk` HAVING `Counting` > 1) AS `dupe_search` ON `song`.`album` = `dupe_search`.`dupe_id1` LEFT JOIN (SELECT MAX(`id`) AS `dupe_id2`, LTRIM(CONCAT(COALESCE(`album`.`prefix`, ''), ' ', `album`.`name`)) AS `fullname`, COUNT(LTRIM(CONCAT(COALESCE(`album`.`prefix`, ''), ' ', `album`.`name`))) AS `Counting` FROM `album` GROUP BY `album_artist`, LTRIM(CONCAT(COALESCE(`album`.`prefix`, ''), ' ', `album`.`name`)), `disk` HAVING `Counting` > 1) AS `dupe_search2` ON `song`.`album` = `dupe_search2`.`dupe_id2` WHERE `dupe_search`.`dupe_id1` IS NOT NULL OR `dupe_search2`.`dupe_id2` IS NOT NULL ORDER BY `album`, `track`";
921            }
922
923            $db_results = Dba::read($sql);
924
925            while ($item = Dba::fetch_assoc($db_results)) {
926                $results[] = (int)$item['id'];
927            } // end while
928        }
929
930        return $results;
931    }
932
933    /**
934     * get_album_name
935     * gets the name of $this->album, allows passing of id
936     * @param integer $album_id
937     * @return string
938     */
939    public function get_album_name($album_id = 0)
940    {
941        if (!$album_id) {
942            $album_id = $this->album;
943        }
944        $album = new Album($album_id);
945
946        return $album->f_name;
947    } // get_album_name
948
949    /**
950     * get_album_catalog_number
951     * gets the catalog_number of $this->album, allows passing of id
952     * @param integer $album_id
953     * @return string
954     */
955    public function get_album_catalog_number($album_id = null)
956    {
957        if ($album_id === null) {
958            $album_id = $this->album;
959        }
960        $album = new Album($album_id);
961
962        return $album->catalog_number;
963    } // get_album_catalog_number
964
965    /**
966     * get_album_original_year
967     * gets the original_year of $this->album, allows passing of id
968     * @param integer $album_id
969     * @return integer
970     */
971    public function get_album_original_year($album_id = null)
972    {
973        if ($album_id === null) {
974            $album_id = $this->album;
975        }
976        $album = new Album($album_id);
977
978        return $album->original_year;
979    } // get_album_original_year
980
981    /**
982     * get_album_barcode
983     * gets the barcode of $this->album, allows passing of id
984     * @param integer $album_id
985     * @return string
986     */
987    public function get_album_barcode($album_id = null)
988    {
989        if (!$album_id) {
990            $album_id = $this->album;
991        }
992        $album = new Album($album_id);
993
994        return $album->barcode;
995    } // get_album_barcode
996
997    /**
998     * get_artist_name
999     * gets the name of $this->artist, allows passing of id
1000     * @param integer $artist_id
1001     * @return string
1002     */
1003    public function get_artist_name($artist_id = 0)
1004    {
1005        if (!$artist_id) {
1006            $artist_id = $this->artist;
1007        }
1008        $artist = new Artist($artist_id);
1009        if ($artist->id) {
1010            return $artist->f_name;
1011        }
1012
1013        return '';
1014    } // get_artist_name
1015
1016    /**
1017     * get_album_artist_name
1018     * gets the name of $this->albumartist, allows passing of id
1019     * @param integer $album_artist_id
1020     * @return string
1021     */
1022    public function get_album_artist_name($album_artist_id = 0)
1023    {
1024        if (!$album_artist_id) {
1025            $album_artist_id = $this->albumartist;
1026        }
1027        $album_artist = new Artist($album_artist_id);
1028        if ($album_artist->id) {
1029            return (string)$album_artist->f_name;
1030        }
1031
1032        return '';
1033    } // get_album_artist_name
1034
1035    /**
1036     * set_played
1037     * this checks to see if the current object has been played
1038     * if not then it sets it to played. In any case it updates stats.
1039     * @param integer $user
1040     * @param string $agent
1041     * @param array $location
1042     * @param integer $date
1043     * @return boolean
1044     */
1045    public function set_played($user, $agent, $location, $date = null)
1046    {
1047        // ignore duplicates or skip the last track
1048        if (!$this->check_play_history($user, $agent, $date)) {
1049            return false;
1050        }
1051        // insert stats for each object type
1052        if (Stats::insert('song', $this->id, $user, $agent, $location, 'stream', $date)) {
1053            Stats::insert('album', $this->album, $user, $agent, $location, 'stream', $date);
1054            Stats::insert('artist', $this->artist, $user, $agent, $location, 'stream', $date);
1055            // followup on some stats too
1056            $user_data = User::get_user_data($user, 'play_size');
1057            $play_size = (isset($user_data['play_size']))
1058                ? (int)$user_data['play_size']
1059                : 0;
1060            User::set_user_data($user, 'play_size', ($play_size + $this->size));
1061        }
1062        // If it hasn't been played, set it
1063        if (!$this->played) {
1064            self::update_played(true, $this->id);
1065        }
1066
1067        return true;
1068    } // set_played
1069
1070    /**
1071     * check_play_history
1072     * this checks to see if the current object has been played
1073     * if not then it sets it to played. In any case it updates stats.
1074     * @param integer $user
1075     * @param string $agent
1076     * @param integer $date
1077     * @return boolean
1078     */
1079    public function check_play_history($user, $agent, $date)
1080    {
1081        return Stats::has_played_history('song', $this, $user, $agent, $date);
1082    }
1083
1084    /**
1085     * compare_song_information
1086     * this compares the new ID3 tags of a file against
1087     * the ones in the database to see if they have changed
1088     * it returns false if nothing has changes, or the true
1089     * if they have. Static because it doesn't need this
1090     * @param Song $song
1091     * @param Song $new_song
1092     * @return array
1093     */
1094    public static function compare_song_information(Song $song, Song $new_song)
1095    {
1096        // Remove some stuff we don't care about as this function only needs to check song information.
1097        unset($song->catalog, $song->played, $song->enabled, $song->addition_time, $song->update_time, $song->type, $song->disk);
1098        $string_array = array('title', 'comment', 'lyrics', 'composer', 'tags', 'artist', 'album', 'time');
1099        $skip_array   = array(
1100            'id',
1101            'tag_id',
1102            'mime',
1103            'mbid',
1104            'waveform',
1105            'object_cnt',
1106            'skip_cnt',
1107            'albumartist',
1108            'artist_mbid',
1109            'album_mbid',
1110            'albumartist_mbid',
1111            'mb_albumid_group',
1112            'disabledMetadataFields'
1113        );
1114
1115        return self::compare_media_information($song, $new_song, $string_array, $skip_array);
1116    } // compare_song_information
1117
1118    /**
1119     * compare_media_information
1120     * @param $media
1121     * @param $new_media
1122     * @param string[] $string_array
1123     * @param string[] $skip_array
1124     * @return array
1125     */
1126    public static function compare_media_information($media, $new_media, $string_array, $skip_array)
1127    {
1128        $array = array();
1129
1130        // Pull out all the currently set vars
1131        $fields = get_object_vars($media);
1132
1133        // Foreach them
1134        foreach ($fields as $key => $value) {
1135            $key = trim((string)$key);
1136            if (empty($key) || in_array($key, $skip_array)) {
1137                continue;
1138            }
1139
1140            // Represent the value as a string for simpler comparison. For array, ensure to sort similarly old/new values
1141            if (is_array($media->$key)) {
1142                $arr = $media->$key;
1143                sort($arr);
1144                $mediaData = implode(" ", $arr);
1145            } else {
1146                $mediaData = $media->$key;
1147            }
1148
1149            // Skip the item if it is no string nor something we can turn into a string
1150            if (!is_string($mediaData) && !is_numeric($mediaData) && !is_bool($mediaData)) {
1151                if (is_object($mediaData) && !method_exists($mediaData, '__toString')) {
1152                    continue;
1153                }
1154            }
1155
1156            if (is_array($new_media->$key)) {
1157                $arr = $new_media->$key;
1158                sort($arr);
1159                $newMediaData = implode(" ", $arr);
1160            } else {
1161                $newMediaData = $new_media->$key;
1162            }
1163
1164            // If it's a string thing
1165            if (in_array($key, $string_array)) {
1166                $mediaData    = self::clean_string_field_value($mediaData);
1167                $newMediaData = self::clean_string_field_value($newMediaData);
1168                if ($mediaData != $newMediaData) {
1169                    $array['change']        = true;
1170                    $array['element'][$key] = 'OLD: ' . $mediaData . ' --> ' . $newMediaData;
1171                }
1172            } // in array of strings
1173            elseif ($newMediaData !== null) {
1174                if ($media->$key != $new_media->$key) {
1175                    $array['change']        = true;
1176                    $array['element'][$key] = 'OLD:' . $mediaData . ' --> ' . $newMediaData;
1177                }
1178            } // end else
1179        } // end foreach
1180
1181        if ($array['change']) {
1182            debug_event(self::class, 'media-diff ' . json_encode($array['element']), 5);
1183        }
1184
1185        return $array;
1186    }
1187
1188    /**
1189     * clean_string_field_value
1190     * @param string $value
1191     * @return string
1192     */
1193    private static function clean_string_field_value($value)
1194    {
1195        $value = trim(stripslashes(preg_replace('/\s+/', ' ', $value)));
1196
1197        // Strings containing  only UTF-8 BOM = empty string
1198        if (strlen((string)$value) == 2 && (ord($value[0]) == 0xFF || ord($value[0]) == 0xFE)) {
1199            $value = "";
1200        }
1201
1202        return $value;
1203    }
1204
1205    /**
1206     * update
1207     * This takes a key'd array of data does any cleaning it needs to
1208     * do and then calls the helper functions as needed.
1209     * @param array $data
1210     * @return integer
1211     */
1212    public function update(array $data)
1213    {
1214        $changed = array();
1215        foreach ($data as $key => $value) {
1216            debug_event(self::class, $key . '=' . $value, 5);
1217
1218            switch ($key) {
1219                case 'artist_name':
1220                    // Create new artist name and id
1221                    $old_artist_id = $this->artist;
1222                    $new_artist_id = Artist::check($value);
1223                    $this->artist  = $new_artist_id;
1224                    self::update_artist($new_artist_id, $this->id, $old_artist_id);
1225                    $changed[] = (string) $key;
1226                    break;
1227                case 'album_name':
1228                    // Create new album name and id
1229                    $old_album_id = $this->album;
1230                    $new_album_id = Album::check($this->catalog, $value);
1231                    $this->album  = $new_album_id;
1232                    self::update_album($new_album_id, $this->id, $old_album_id);
1233                    $changed[] = (string) $key;
1234                    break;
1235                case 'artist':
1236                    // Change artist the song is assigned to
1237                    if ($value != $this->$key) {
1238                        $old_artist_id = $this->artist;
1239                        $new_artist_id = $value;
1240                        self::update_artist($new_artist_id, $this->id, $old_artist_id);
1241                        $changed[] = (string) $key;
1242                    }
1243                    break;
1244                case 'album':
1245                    // Change album the song is assigned to
1246                    if ($value != $this->$key) {
1247                        $old_album_id = $this->$key;
1248                        $new_album_id = $value;
1249                        self::update_album($new_album_id, $this->id, $old_album_id);
1250                        $changed[] = (string) $key;
1251                    }
1252                    break;
1253                case 'year':
1254                case 'title':
1255                case 'track':
1256                case 'mbid':
1257                case 'license':
1258                case 'composer':
1259                case 'label':
1260                case 'language':
1261                case 'comment':
1262                    // Check to see if it needs to be updated
1263                    if ($value != $this->$key) {
1264                        $function = 'update_' . $key;
1265                        self::$function($value, $this->id);
1266                        $this->$key = $value;
1267                        $changed[]  = (string) $key;
1268                    }
1269                    break;
1270                case 'edit_tags':
1271                    Tag::update_tag_list($value, 'song', $this->id, true);
1272                    $this->tags = Tag::get_top_tags('song', $this->id);
1273                    $changed[]  = (string) $key;
1274                    break;
1275                case 'metadata':
1276                    if (self::isCustomMetadataEnabled()) {
1277                        $this->updateMetadata($value);
1278                    }
1279                    break;
1280                default:
1281                    break;
1282            } // end whitelist
1283        } // end foreach
1284
1285        $this->getSongTagWriter()->write(
1286            $this
1287        );
1288
1289        return $this->id;
1290    } // update
1291
1292    /**
1293     * update_song
1294     * this is the main updater for a song it actually
1295     * calls a whole bunch of mini functions to update
1296     * each little part of the song... lastly it updates
1297     * the "update_time" of the song
1298     * @param integer $song_id
1299     * @param Song $new_song
1300     */
1301    public static function update_song($song_id, Song $new_song)
1302    {
1303        $update_time = time();
1304
1305        $sql = "UPDATE `song` SET `album` = ?, `year` = ?, `artist` = ?, `title` = ?, `composer` = ?, `bitrate` = ?, `rate` = ?, `mode` = ?, `size` = ?, `time` = ?, `track` = ?, `mbid` = ?, `update_time` = ? WHERE `id` = ?";
1306        Dba::write($sql, array($new_song->album, $new_song->year, $new_song->artist,
1307                                $new_song->title, $new_song->composer, (int) $new_song->bitrate, (int) $new_song->rate, $new_song->mode,
1308                                (int) $new_song->size, (int) $new_song->time, $new_song->track, $new_song->mbid,
1309                                $update_time, $song_id));
1310
1311        $sql = "UPDATE `song_data` SET `label` = ?, `lyrics` = ?, `language` = ?, `comment` = ?, `replaygain_track_gain` = ?, `replaygain_track_peak` = ?, `replaygain_album_gain` = ?, `replaygain_album_peak` = ?, `r128_track_gain` = ?, `r128_album_gain` = ? WHERE `song_id` = ?";
1312        Dba::write($sql, array($new_song->label, $new_song->lyrics, $new_song->language, $new_song->comment, $new_song->replaygain_track_gain,
1313            $new_song->replaygain_track_peak, $new_song->replaygain_album_gain, $new_song->replaygain_album_peak, $new_song->r128_track_gain, $new_song->r128_album_gain, $song_id));
1314    } // update_song
1315
1316    /**
1317     * update_year
1318     * update the year tag
1319     * @param integer $new_year
1320     * @param integer $song_id
1321     */
1322    public static function update_year($new_year, $song_id)
1323    {
1324        self::_update_item('year', $new_year, $song_id, 50, true);
1325    } // update_year
1326
1327    /**
1328     * update_label
1329     * This updates the label tag of the song
1330     * @param string $new_value
1331     * @param integer $song_id
1332     */
1333    public static function update_label($new_value, $song_id)
1334    {
1335        self::_update_ext_item('label', $new_value, $song_id, 50, true);
1336    } // update_label
1337
1338    /**
1339     * update_language
1340     * This updates the language tag of the song
1341     * @param string $new_lang
1342     * @param integer $song_id
1343     */
1344    public static function update_language($new_lang, $song_id)
1345    {
1346        self::_update_ext_item('language', $new_lang, $song_id, 50, true);
1347    } // update_language
1348
1349    /**
1350     * update_comment
1351     * updates the comment field
1352     * @param string $new_comment
1353     * @param integer $song_id
1354     */
1355    public static function update_comment($new_comment, $song_id)
1356    {
1357        self::_update_ext_item('comment', $new_comment, $song_id, 50, true);
1358    } // update_comment
1359
1360    /**
1361     * update_lyrics
1362     * updates the lyrics field
1363     * @param string $new_lyrics
1364     * @param integer $song_id
1365     */
1366    public static function update_lyrics($new_lyrics, $song_id)
1367    {
1368        self::_update_ext_item('lyrics', $new_lyrics, $song_id, 50, true);
1369    } // update_lyrics
1370
1371    /**
1372     * update_title
1373     * updates the title field
1374     * @param string $new_title
1375     * @param integer $song_id
1376     */
1377    public static function update_title($new_title, $song_id)
1378    {
1379        self::_update_item('title', $new_title, $song_id, 50, true);
1380    } // update_title
1381
1382    /**
1383     * update_composer
1384     * updates the composer field
1385     * @param string $new_value
1386     * @param integer $song_id
1387     */
1388    public static function update_composer($new_value, $song_id)
1389    {
1390        self::_update_item('composer', $new_value, $song_id, 50, true);
1391    } // update_composer
1392
1393    /**
1394     * update_publisher
1395     * updates the publisher field
1396     * @param string $new_value
1397     * @param integer $song_id
1398     */
1399    public static function update_publisher($new_value, $song_id)
1400    {
1401        self::_update_item('publisher', $new_value, $song_id, 50, true);
1402    } // update_publisher
1403
1404    /**
1405     * update_bitrate
1406     * updates the bitrate field
1407     * @param integer $new_bitrate
1408     * @param integer $song_id
1409     */
1410    public static function update_bitrate($new_bitrate, $song_id)
1411    {
1412        self::_update_item('bitrate', $new_bitrate, $song_id, 50, true);
1413    } // update_bitrate
1414
1415    /**
1416     * update_rate
1417     * updates the rate field
1418     * @param integer $new_rate
1419     * @param integer $song_id
1420     */
1421    public static function update_rate($new_rate, $song_id)
1422    {
1423        self::_update_item('rate', $new_rate, $song_id, 50, true);
1424    } // update_rate
1425
1426    /**
1427     * update_mode
1428     * updates the mode field
1429     * @param string $new_mode
1430     * @param integer $song_id
1431     */
1432    public static function update_mode($new_mode, $song_id)
1433    {
1434        self::_update_item('mode', $new_mode, $song_id, 50, true);
1435    } // update_mode
1436
1437    /**
1438     * update_size
1439     * updates the size field
1440     * @param integer $new_size
1441     * @param integer $song_id
1442     */
1443    public static function update_size($new_size, $song_id)
1444    {
1445        self::_update_item('size', $new_size, $song_id, 50);
1446    } // update_size
1447
1448    /**
1449     * update_time
1450     * updates the time field
1451     * @param integer $new_time
1452     * @param integer $song_id
1453     */
1454    public static function update_time($new_time, $song_id)
1455    {
1456        self::_update_item('time', $new_time, $song_id, 50, true);
1457    } // update_time
1458
1459    /**
1460     * update_track
1461     * this updates the track field
1462     * @param integer $new_track
1463     * @param integer $song_id
1464     */
1465    public static function update_track($new_track, $song_id)
1466    {
1467        self::_update_item('track', $new_track, $song_id, 50, true);
1468    } // update_track
1469
1470    /**
1471     * update_mbid
1472     * updates mbid field
1473     * @param string $new_mbid
1474     * @param integer $song_id
1475     */
1476    public static function update_mbid($new_mbid, $song_id)
1477    {
1478        self::_update_item('mbid', $new_mbid, $song_id, 50);
1479    } // update_mbid
1480
1481    /**
1482     * update_license
1483     * updates license field
1484     * @param string $new_license
1485     * @param integer $song_id
1486     */
1487    public static function update_license($new_license, $song_id)
1488    {
1489        self::_update_item('license', $new_license, $song_id, 50, true);
1490    } // update_license
1491
1492    /**
1493     * update_artist
1494     * updates the artist field
1495     * @param integer $new_artist
1496     * @param integer $song_id
1497     * @param integer $old_artist
1498     */
1499    public static function update_artist($new_artist, $song_id, $old_artist)
1500    {
1501        self::_update_item('artist', $new_artist, $song_id, 50);
1502
1503        // migrate stats for the old artist
1504        Stats::migrate('artist', $old_artist, $new_artist);
1505        Useractivity::migrate('artist', $old_artist, $new_artist);
1506        Recommendation::migrate('artist', $old_artist, $new_artist);
1507        Share::migrate('artist', $old_artist, $new_artist);
1508        Shoutbox::migrate('artist', $old_artist, $new_artist);
1509        Tag::migrate('artist', $old_artist, $new_artist);
1510        Userflag::migrate('artist', $old_artist, $new_artist);
1511        Rating::migrate('artist', $old_artist, $new_artist);
1512        Art::duplicate('artist', $old_artist, $new_artist);
1513        Wanted::migrate('artist', $old_artist, $new_artist);
1514        Catalog::migrate_map('artist', $old_artist, $new_artist);
1515        Artist::update_artist_counts($new_artist);
1516    } // update_artist
1517
1518    /**
1519     * update_album
1520     * updates the album field
1521     * @param integer $new_album
1522     * @param integer $song_id
1523     * @param integer $old_album
1524     */
1525    public static function update_album($new_album, $song_id, $old_album)
1526    {
1527        self::_update_item('album', $new_album, $song_id, 50, true);
1528
1529        // migrate stats for the old album
1530        Stats::migrate('album', $old_album, $new_album);
1531        Useractivity::migrate('album', $old_album, $new_album);
1532        Recommendation::migrate('album', $old_album, $new_album);
1533        Share::migrate('album', $old_album, $new_album);
1534        Shoutbox::migrate('album', $old_album, $new_album);
1535        Tag::migrate('album', $old_album, $new_album);
1536        Userflag::migrate('album', $old_album, $new_album);
1537        Rating::migrate('album', $old_album, $new_album);
1538        Art::duplicate('album', $old_album, $new_album);
1539        Catalog::migrate_map('album', $old_album, $new_album);
1540        Album::update_album_counts($new_album);
1541    } // update_album
1542
1543    /**
1544     * update_utime
1545     * sets a new update time
1546     * @param integer $song_id
1547     * @param integer $time
1548     */
1549    public static function update_utime($song_id, $time = 0)
1550    {
1551        if (!$time) {
1552            $time = time();
1553        }
1554
1555        self::_update_item('update_time', $time, $song_id, 75, true);
1556    } // update_utime
1557
1558    /**
1559     * update_played
1560     * sets the played flag
1561     * @param boolean $new_played
1562     * @param integer $song_id
1563     */
1564    public static function update_played($new_played, $song_id)
1565    {
1566        self::_update_item('played', ($new_played ? 1 : 0), $song_id, 25);
1567    } // update_played
1568
1569    /**
1570     * update_enabled
1571     * sets the enabled flag
1572     * @param boolean $new_enabled
1573     * @param integer $song_id
1574     */
1575    public static function update_enabled($new_enabled, $song_id)
1576    {
1577        self::_update_item('enabled', ($new_enabled ? 1 : 0), $song_id, 75, true);
1578    } // update_enabled
1579
1580    /**
1581     * _update_item
1582     * This is a private function that should only be called from within the song class.
1583     * It takes a field, value song id and level. first and foremost it checks the level
1584     * against Core::get_global('user') to make sure they are allowed to update this record
1585     * it then updates it and sets $this->{$field} to the new value
1586     * @param string $field
1587     * @param mixed $value
1588     * @param integer $song_id
1589     * @param integer $level
1590     * @param boolean $check_owner
1591     * @return PDOStatement|boolean
1592     */
1593    private static function _update_item($field, $value, $song_id, $level, $check_owner = false)
1594    {
1595        if ($check_owner) {
1596            $item = new Song($song_id);
1597            if ($item->id && $item->get_user_owner() == Core::get_global('user')->id) {
1598                $level = 25;
1599            }
1600        }
1601        /* Check them Rights! */
1602        if (!Access::check('interface', $level)) {
1603            return false;
1604        }
1605
1606        /* Can't update to blank */
1607        if (!strlen(trim((string)$value)) && $field != 'comment') {
1608            return false;
1609        }
1610
1611        $sql = "UPDATE `song` SET `$field` = ? WHERE `id` = ?";
1612
1613        return Dba::write($sql, array($value, $song_id));
1614    } // _update_item
1615
1616    /**
1617     * _update_ext_item
1618     * This updates a song record that is housed in the song_ext_info table
1619     * These are items that aren't used normally, and often large/informational only
1620     * @param string $field
1621     * @param string $value
1622     * @param integer $song_id
1623     * @param integer $level
1624     * @param boolean $check_owner
1625     * @return PDOStatement|boolean
1626     */
1627    private static function _update_ext_item($field, $value, $song_id, $level, $check_owner = false)
1628    {
1629        if ($check_owner) {
1630            $item = new Song($song_id);
1631            if ($item->id && $item->get_user_owner() == Core::get_global('user')->id) {
1632                $level = 25;
1633            }
1634        }
1635
1636        /* Check them rights boy! */
1637        if (!Access::check('interface', $level)) {
1638            return false;
1639        }
1640
1641        $sql = "UPDATE `song_data` SET `$field` = ? WHERE `song_id` = ?";
1642
1643        return Dba::write($sql, array($value, $song_id));
1644    } // _update_ext_item
1645
1646    /**
1647     * format
1648     * This takes the current song object
1649     * and does a ton of formatting on it creating f_??? variables on the current
1650     * object
1651     * @param boolean $details
1652     */
1653    public function format($details = true)
1654    {
1655        if ($details) {
1656            $this->fill_ext_info();
1657
1658            // Get the top tags
1659            $this->tags   = Tag::get_top_tags('song', $this->id);
1660            $this->f_tags = Tag::get_display($this->tags, true, 'song');
1661        }
1662        // force the album artist.
1663        $album             = new Album($this->album);
1664        $this->albumartist = (!empty($this->albumartist)) ? $this->albumartist : $album->album_artist;
1665
1666        // fix missing song disk (where is this coming from?)
1667        $this->disk = ($this->disk) ? $this->disk : $album->disk;
1668
1669        // Format the album name
1670        $this->f_album_full = $this->get_album_name();
1671        $this->f_album      = $this->f_album_full;
1672
1673        // Format the artist name
1674        $this->f_artist_full = $this->get_artist_name();
1675        $this->f_artist      = $this->f_artist_full;
1676
1677        // Format the album_artist name
1678        $this->f_albumartist_full = $this->get_album_artist_name();
1679
1680        // Format the title
1681        $this->f_title      = $this->title;
1682        $this->f_title_full = $this->f_title;
1683
1684        // Create Links for the different objects
1685        $this->link          = AmpConfig::get('web_path') . "/song.php?action=show_song&song_id=" . $this->id;
1686        $this->f_link        = "<a href=\"" . scrub_out($this->link) . "\" title=\"" . scrub_out($this->f_artist) . " - " . scrub_out($this->f_title) . "\"> " . scrub_out($this->f_title) . "</a>";
1687        $this->f_album_link  = "<a href=\"" . AmpConfig::get('web_path') . "/albums.php?action=show&amp;album=" . $this->album . "\" title=\"" . scrub_out($this->f_album_full) . "\"> " . scrub_out($this->f_album) . "</a>";
1688        $this->f_artist_link = "<a href=\"" . AmpConfig::get('web_path') . "/artists.php?action=show&amp;artist=" . $this->artist . "\" title=\"" . scrub_out($this->f_artist_full) . "\"> " . scrub_out($this->f_artist) . "</a>";
1689        if (!empty($this->albumartist)) {
1690            $this->f_albumartist_link = "<a href=\"" . AmpConfig::get('web_path') . "/artists.php?action=show&amp;artist=" . $this->albumartist . "\" title=\"" . scrub_out($this->f_albumartist_full) . "\"> " . scrub_out($this->f_albumartist_full) . "</a>";
1691        }
1692
1693        // Format the Bitrate
1694        $this->f_bitrate = (int)($this->bitrate / 1000) . "-" . strtoupper((string)$this->mode);
1695
1696        // Format the Time
1697        $min            = floor($this->time / 60);
1698        $sec            = sprintf("%02d", ($this->time % 60));
1699        $this->f_time   = $min . ":" . $sec;
1700        $hour           = sprintf("%02d", floor($min / 60));
1701        $min_h          = sprintf("%02d", ($min % 60));
1702        $this->f_time_h = $hour . ":" . $min_h . ":" . $sec;
1703
1704        // Format the track (there isn't really anything to do here)
1705        $this->f_track = (string)$this->track;
1706
1707        // Format the size
1708        $this->f_size = Ui::format_bytes($this->size);
1709
1710        $this->f_lyrics = "<a title=\"" . scrub_out($this->title) . "\" href=\"" . AmpConfig::get('web_path') . "/song.php?action=show_lyrics&song_id=" . $this->id . "\">" . T_('Show Lyrics') . "</a>";
1711
1712        $this->f_file = $this->f_artist . ' - ';
1713        if ($this->track) {
1714            $this->f_file .= $this->track . ' - ';
1715        }
1716        $this->f_file .= $this->f_title . '.' . $this->type;
1717
1718        $this->f_publisher = $this->label;
1719        $this->f_composer  = $this->composer;
1720
1721        $year              = (int)$this->year;
1722        $this->f_year_link = "<a href=\"" . AmpConfig::get('web_path') . "/search.php?type=album&action=search&limit=0&rule_1=year&rule_1_operator=2&rule_1_input=" . $year . "\">" . $year . "</a>";
1723
1724        if (AmpConfig::get('licensing') && $this->license !== null) {
1725            $license = new License($this->license);
1726
1727            $this->f_license = $license->getLinkFormatted();
1728        }
1729    } // format
1730
1731    /**
1732     * Get item keywords for metadata searches.
1733     * @return array
1734     */
1735    public function get_keywords()
1736    {
1737        $keywords               = array();
1738        $keywords['mb_trackid'] = array(
1739            'important' => false,
1740            'label' => T_('Track MusicBrainzID'),
1741            'value' => $this->mbid
1742        );
1743        $keywords['artist'] = array(
1744            'important' => true,
1745            'label' => T_('Artist'),
1746            'value' => $this->f_artist
1747        );
1748        $keywords['title'] = array(
1749            'important' => true,
1750            'label' => T_('Title'),
1751            'value' => $this->f_title
1752        );
1753
1754        return $keywords;
1755    }
1756
1757    /**
1758     * Get total count
1759     * @return integer
1760     */
1761    public function get_totalcount()
1762    {
1763        return $this->total_count;
1764    }
1765
1766    /**
1767     * Get item fullname.
1768     * @return string
1769     */
1770    public function get_fullname()
1771    {
1772        return $this->f_title;
1773    }
1774
1775    /**
1776     * Get parent item description.
1777     * @return array|null
1778     */
1779    public function get_parent()
1780    {
1781        return array('object_type' => 'album', 'object_id' => $this->album);
1782    }
1783
1784    /**
1785     * Get item children.
1786     * @return array
1787     */
1788    public function get_childrens()
1789    {
1790        return array();
1791    }
1792
1793    /**
1794     * Search for item children.
1795     * @param string $name
1796     * @return array
1797     */
1798    public function search_childrens($name)
1799    {
1800        debug_event(self::class, 'search_childrens ' . $name, 5);
1801
1802        return array();
1803    }
1804
1805    /**
1806     * Get all childrens and sub-childrens medias.
1807     * @param string $filter_type
1808     * @return array
1809     */
1810    public function get_medias($filter_type = null)
1811    {
1812        $medias = array();
1813        if ($filter_type === null || $filter_type == 'song') {
1814            $medias[] = array(
1815                'object_type' => 'song',
1816                'object_id' => $this->id
1817            );
1818        }
1819
1820        return $medias;
1821    }
1822
1823    /**
1824     * get_catalogs
1825     *
1826     * Get all catalog ids related to this item.
1827     * @return integer[]
1828     */
1829    public function get_catalogs()
1830    {
1831        return array($this->catalog);
1832    }
1833
1834    /**
1835     * Get item's owner.
1836     * @return integer|null
1837     */
1838    public function get_user_owner()
1839    {
1840        if ($this->user_upload !== null) {
1841            return $this->user_upload;
1842        }
1843
1844        return null;
1845    }
1846
1847    /**
1848     * Get default art kind for this item.
1849     * @return string
1850     */
1851    public function get_default_art_kind()
1852    {
1853        return 'default';
1854    }
1855
1856    /**
1857     * get_description
1858     * @return string
1859     */
1860    public function get_description()
1861    {
1862        if (!empty($this->comment)) {
1863            return $this->comment;
1864        }
1865
1866        $album = new Album($this->album);
1867        $album->format();
1868
1869        return $album->get_description();
1870    }
1871
1872    /**
1873     * display_art
1874     * @param integer $thumb
1875     * @param boolean $force
1876     */
1877    public function display_art($thumb = 2, $force = false)
1878    {
1879        $object_id = null;
1880        $type      = null;
1881
1882        if (Art::has_db($this->id, 'song')) {
1883            $object_id = $this->id;
1884            $type      = 'song';
1885        } else {
1886            if (Art::has_db($this->album, 'album')) {
1887                $object_id = $this->album;
1888                $type      = 'album';
1889            } else {
1890                if (Art::has_db($this->artist, 'artist') || $force) {
1891                    $object_id = $this->artist;
1892                    $type      = 'artist';
1893                }
1894            }
1895        }
1896
1897        if ($object_id !== null && $type !== null) {
1898            Art::display($type, $object_id, $this->get_fullname(), $thumb, $this->link);
1899        }
1900    }
1901
1902    /**
1903     * get_fields
1904     * This returns all of the 'data' fields for this object, we need to filter out some that we don't
1905     * want to present to a user, and add some that don't exist directly on the object but are related
1906     * @return array
1907     */
1908    public static function get_fields()
1909    {
1910        $fields = get_class_vars(Song::class);
1911
1912        unset($fields['id'], $fields['_transcoded'], $fields['_fake'], $fields['cache_hit'], $fields['mime'], $fields['type']);
1913
1914        // Some additional fields
1915        $fields['tag']     = true;
1916        $fields['catalog'] = true;
1917        // FIXME: These are here to keep the ideas, don't want to have to worry about them for now
1918        //        $fields['rating'] = true;
1919        //        $fields['recently Played'] = true;
1920
1921        return $fields;
1922    } // get_fields
1923
1924    /**
1925     * get_from_path
1926     * This returns all of the songs that exist under the specified path
1927     * @param string $path
1928     * @return integer[]
1929     */
1930    public static function get_from_path($path)
1931    {
1932        $path = Dba::escape($path);
1933
1934        $sql        = "SELECT * FROM `song` WHERE `file` LIKE '$path%'";
1935        $db_results = Dba::read($sql);
1936
1937        $songs = array();
1938
1939        while ($row = Dba::fetch_assoc($db_results)) {
1940            $songs[] = (int)$row['id'];
1941        }
1942
1943        return $songs;
1944    } // get_from_path
1945
1946    /**
1947     * @function    get_rel_path
1948     * @discussion    returns the path of the song file stripped of the catalog path
1949     *        used for mpd playback
1950     * @param string $file_path
1951     * @param integer $catalog_id
1952     * @return string
1953     */
1954    public function get_rel_path($file_path = null, $catalog_id = 0)
1955    {
1956        $info = null;
1957        if ($file_path === null) {
1958            $info      = $this->has_info();
1959            $file_path = $info['file'];
1960        }
1961        if (!$catalog_id) {
1962            if (!is_array($info)) {
1963                $info = $this->has_info();
1964            }
1965            $catalog_id = $info['catalog'];
1966        }
1967        $catalog = Catalog::create_from_id($catalog_id);
1968
1969        return $catalog->get_rel_path($file_path);
1970    } // get_rel_path
1971
1972    /**
1973     * play_url
1974     * This function takes all the song information and correctly formats a
1975     * a stream URL taking into account the downsampling mojo and everything
1976     * else, this is the true function
1977     * @param string $additional_params
1978     * @param string $player
1979     * @param boolean $local
1980     * @param int|string $uid
1981     * @return string
1982     */
1983    public function play_url($additional_params = '', $player = '', $local = false, $uid = false)
1984    {
1985        if (!$this->id) {
1986            return '';
1987        }
1988        if (!$uid) {
1989            // No user in the case of upnp. Set to 0 instead. required to fix database insertion errors
1990            $uid = Core::get_global('user')->id ?: 0;
1991        }
1992        // set no use when using auth
1993        if (!AmpConfig::get('use_auth') && !AmpConfig::get('require_session')) {
1994            $uid = -1;
1995        }
1996
1997        $type = $this->type;
1998
1999        $this->format();
2000        $media_name = $this->get_stream_name() . "." . $type;
2001        $media_name = preg_replace("/[^a-zA-Z0-9\. ]+/", "-", $media_name);
2002        $media_name = rawurlencode($media_name);
2003
2004        $url = Stream::get_base_url($local) . "type=song&oid=" . $this->id . "&uid=" . (string) $uid . $additional_params;
2005        if ($player !== '') {
2006            $url .= "&player=" . $player;
2007        }
2008        $url .= "&name=" . $media_name;
2009
2010        return Stream_Url::format($url);
2011    }
2012
2013    /**
2014     * Get stream name.
2015     * @return string
2016     */
2017    public function get_stream_name()
2018    {
2019        return $this->get_artist_name() . " - " . $this->title;
2020    }
2021
2022    /**
2023     * get_recently_played
2024     * This function returns the last X songs that have been played
2025     * it uses the popular threshold to figure out how many to pull
2026     * it will only return unique object
2027     * @param integer $user_id
2028     * @return array
2029     */
2030    public static function get_recently_played($user_id = 0)
2031    {
2032        $personal_info_recent = 91;
2033        $personal_info_time   = 92;
2034        $personal_info_agent  = 93;
2035        $catalog_filter       = AmpConfig::get('catalog_filter');
2036        $user_id              = ($catalog_filter)
2037            ? Core::get_global('user')->id
2038            : $user_id;
2039
2040        $results = array();
2041        $valid   = ($user_id > 0);
2042        $limit   = AmpConfig::get('popular_threshold', 10);
2043        $sql     = "SELECT `object_id`, `object_count`.`user`, `object_type`, `date`, `agent`, `geo_latitude`, `geo_longitude`, `geo_name`, `pref_recent`.`value` AS `user_recent`, `pref_time`.`value` AS `user_time`, `pref_agent`.`value` AS `user_agent` FROM `object_count` LEFT JOIN `user_preference` AS `pref_recent` ON `pref_recent`.`preference`='$personal_info_recent' AND `pref_recent`.`user` = `object_count`.`user` LEFT JOIN `user_preference` AS `pref_time` ON `pref_time`.`preference`='$personal_info_time' AND `pref_time`.`user` = `object_count`.`user` LEFT JOIN `user_preference` AS `pref_agent` ON `pref_agent`.`preference`='$personal_info_agent' AND `pref_agent`.`user` = `object_count`.`user` WHERE `object_type` = 'song' AND `count_type` = 'stream' ";
2044        if (AmpConfig::get('catalog_disable')) {
2045            $sql .= "AND " . Catalog::get_enable_filter('song', '`object_id`') . " ";
2046        }
2047        if ($catalog_filter && $valid) {
2048            $sql .= "AND" . Catalog::get_user_filter('object_count_song', $user_id) . " ";
2049        }
2050        if ($valid && !$catalog_filter) {
2051            // If user is not empty, we're looking directly to user personal info (admin view)
2052            $sql .= "AND `object_count`.`user`='$user_id' ";
2053        } else {
2054            if (!Access::check('interface', 100)) {
2055                // If user identifier is empty, we need to retrieve only users which have allowed view of personal info
2056                $current_user = (int) Core::get_global('user')->id;
2057                if ($current_user > 0) {
2058                    $sql .= "AND `object_count`.`user` IN (SELECT `user` FROM `user_preference` WHERE (`preference`='$personal_info_recent' AND `value`='1') OR `user`='$current_user') ";
2059                }
2060            }
2061        }
2062        $sql .= "ORDER BY `date` DESC LIMIT " . (string)$limit . " ";
2063        //debug_event(self::class, 'get_recently_played ' . $sql, 5);
2064
2065        $db_results = Dba::read($sql);
2066        while ($row = Dba::fetch_assoc($db_results)) {
2067            if (empty($row['geo_name']) && $row['latitude'] && $row['longitude']) {
2068                $row['geo_name'] = Stats::get_cached_place_name($row['latitude'], $row['longitude']);
2069            }
2070            $results[] = $row;
2071        }
2072
2073        return $results;
2074    } // get_recently_played
2075
2076    /**
2077     * Get stream types.
2078     * @param string $player
2079     * @return array
2080     */
2081    public function get_stream_types($player = null)
2082    {
2083        return self::get_stream_types_for_type($this->type, $player);
2084    }
2085
2086    /**
2087     * Get stream types for media type.
2088     * @param string $type
2089     * @param string $player
2090     * @return array
2091     */
2092    public static function get_stream_types_for_type($type, $player = '')
2093    {
2094        $types     = array();
2095        $transcode = AmpConfig::get('transcode_' . $type);
2096        if ($player !== '') {
2097            $player_transcode = AmpConfig::get('transcode_player_' . $player . '_' . $type);
2098            if ($player_transcode) {
2099                $transcode = $player_transcode;
2100            }
2101        }
2102
2103        if ($transcode != 'required') {
2104            $types[] = 'native';
2105        }
2106        if (make_bool($transcode)) {
2107            $types[] = 'transcode';
2108        }
2109
2110        return $types;
2111    }
2112
2113    /**
2114     * Get transcode settings for media.
2115     * It can be confusing but when waveforms are enabled
2116     * it will transcode the file twice.
2117     *
2118     * @param string $source
2119     * @param string $target
2120     * @param string $player
2121     * @param string $media_type
2122     * @param array $options
2123     * @return array
2124     */
2125    public static function get_transcode_settings_for_media(
2126        $source,
2127        $target = null,
2128        $player = null,
2129        $media_type = 'song',
2130        $options = array()
2131    ) {
2132        // default target for songs
2133        $setting_target = 'encode_target';
2134        // default target for video
2135        if ($media_type != 'song') {
2136            $setting_target = 'encode_' . $media_type . '_target';
2137        }
2138        // webplayer / api transcode actions
2139        $has_player_target = false;
2140        if ($player) {
2141            // encode target for songs in webplayer/api
2142            $player_setting_target = 'encode_player_' . $player . '_target';
2143            if ($media_type != 'song') {
2144                // encode target for video in webplayer/api
2145                $player_setting_target = 'encode_' . $media_type . '_player_' . $player . '_target';
2146            }
2147            $has_player_target = AmpConfig::get($player_setting_target);
2148        }
2149        $has_default_target = AmpConfig::get($setting_target);
2150        $has_codec_target   = AmpConfig::get('encode_target_' . $source);
2151
2152        // Fall backwards from the specific transcode formats to default
2153        // TARGET > PLAYER > CODEC > DEFAULT
2154        if ($target) {
2155            debug_event(self::class, 'Explicit target requested: {' . $target . '} format for: ' . $source, 5);
2156        } elseif ($has_player_target) {
2157            $target = $has_player_target;
2158            debug_event(self::class, 'Transcoding for ' . $player . ': {' . $target . '} format for: ' . $source, 5);
2159        } elseif ($has_codec_target) {
2160            $target = $has_codec_target;
2161            debug_event(self::class, 'Transcoding for codec: {' . $target . '} format for: ' . $source, 5);
2162        } elseif ($has_default_target) {
2163            $target = $has_default_target;
2164            debug_event(self::class, 'Transcoding to default: {' . $target . '} format for: ' . $source, 5);
2165        }
2166        // fall back to resampling if no default
2167        if (!$target) {
2168            $target = $source;
2169            debug_event(self::class, 'No transcode target for: ' . $source . ', choosing to resample', 5);
2170        }
2171
2172        $cmd  = AmpConfig::get('transcode_cmd_' . $source) ?: AmpConfig::get('transcode_cmd');
2173        if (empty($cmd)) {
2174            debug_event(self::class, 'A valid transcode_cmd is required to transcode', 5);
2175
2176            return array();
2177        }
2178
2179        $args = '';
2180        if (AmpConfig::get('encode_ss_frame') && isset($options['frame'])) {
2181            $args .= ' ' . AmpConfig::get('encode_ss_frame');
2182        }
2183        if (AmpConfig::get('encode_ss_duration') && isset($options['duration'])) {
2184            $args .= ' ' . AmpConfig::get('encode_ss_duration');
2185        }
2186        $args .= ' ' . AmpConfig::get('transcode_input');
2187
2188        if (AmpConfig::get('encode_srt') && $options['subtitle']) {
2189            debug_event(self::class, 'Using subtitle ' . $options['subtitle'], 5);
2190            $args .= ' ' . AmpConfig::get('encode_srt');
2191        }
2192
2193        $argst = AmpConfig::get('encode_args_' . $target);
2194        if (!$args) {
2195            debug_event(self::class, 'Target format ' . $target . ' is not properly configured', 2);
2196
2197            return array();
2198        }
2199        $args .= ' ' . $argst;
2200
2201        debug_event(self::class, 'Command: ' . $cmd . ' Arguments:' . $args, 5);
2202
2203        return array('format' => $target, 'command' => $cmd . $args);
2204    }
2205
2206    /**
2207     * Get transcode settings.
2208     * @param string $target
2209     * @param string $player
2210     * @param array $options
2211     * @return array
2212     */
2213    public function get_transcode_settings($target = null, $player = null, $options = array())
2214    {
2215        return self::get_transcode_settings_for_media($this->type, $target, $player, 'song', $options);
2216    }
2217
2218    /**
2219     * Get lyrics.
2220     * @return array
2221     */
2222    public function get_lyrics()
2223    {
2224        if ($this->lyrics) {
2225            return array('text' => $this->lyrics);
2226        }
2227
2228        foreach (Plugin::get_plugins('get_lyrics') as $plugin_name) {
2229            $plugin = new Plugin($plugin_name);
2230            if ($plugin->load(Core::get_global('user'))) {
2231                $lyrics = $plugin->_plugin->get_lyrics($this);
2232                if ($lyrics != false) {
2233                    return $lyrics;
2234                }
2235            }
2236        }
2237
2238        return null;
2239    }
2240
2241    /**
2242     * Run custom play action.
2243     * @param integer $action_index
2244     * @param string $codec
2245     * @return array
2246     */
2247    public function run_custom_play_action($action_index, $codec = '')
2248    {
2249        $transcoder = array();
2250        $actions    = self::get_custom_play_actions();
2251        if ($action_index <= count($actions)) {
2252            $action = $actions[$action_index - 1];
2253            if (!$codec) {
2254                $codec = $this->type;
2255            }
2256
2257            $run = str_replace("%f", $this->file, $action['run']);
2258            $run = str_replace("%c", $codec, $run);
2259            $run = str_replace("%a", $this->f_artist, $run);
2260            $run = str_replace("%A", $this->f_album, $run);
2261            $run = str_replace("%t", $this->f_title, $run);
2262
2263            debug_event(self::class, "Running custom play action: " . $run, 3);
2264
2265            $descriptors = array(1 => array('pipe', 'w'));
2266            if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
2267                // Windows doesn't like to provide stderr as a pipe
2268                $descriptors[2] = array('pipe', 'w');
2269            }
2270            $process = proc_open($run, $descriptors, $pipes);
2271
2272            $transcoder['process'] = $process;
2273            $transcoder['handle']  = $pipes[1];
2274            $transcoder['stderr']  = $pipes[2];
2275            $transcoder['format']  = $codec;
2276        }
2277
2278        return $transcoder;
2279    }
2280
2281    /**
2282     * Get custom play actions.
2283     * @return array
2284     */
2285    public static function get_custom_play_actions()
2286    {
2287        $actions = array();
2288        $count   = 0;
2289        while (AmpConfig::get('custom_play_action_title_' . $count)) {
2290            $actions[] = array(
2291                'index' => ($count + 1),
2292                'title' => AmpConfig::get('custom_play_action_title_' . $count),
2293                'icon' => AmpConfig::get('custom_play_action_icon_' . $count),
2294                'run' => AmpConfig::get('custom_play_action_run_' . $count),
2295            );
2296            ++$count;
2297        }
2298
2299        return $actions;
2300    }
2301
2302    /**
2303     * Update Metadata from array
2304     * @param array $meta_value
2305     */
2306    public function updateMetadata($meta_value)
2307    {
2308        foreach ($meta_value as $metadataId => $value) {
2309            $metadata = $this->metadataRepository->findById($metadataId);
2310            if (!$metadata || $value != $metadata->getData()) {
2311                $metadata->setData($value);
2312                $this->metadataRepository->update($metadata);
2313            }
2314        }
2315    }
2316
2317    /**
2318     * get_deleted
2319     * get items from the deleted_songs table
2320     * @return int[]
2321     */
2322    public static function get_deleted()
2323    {
2324        $deleted    = array();
2325        $sql        = "SELECT * FROM `deleted_song`";
2326        $db_results = Dba::read($sql);
2327        while ($row = Dba::fetch_assoc($db_results)) {
2328            $deleted[] = $row;
2329        }
2330
2331        return $deleted;
2332    } // get_deleted
2333
2334    /**
2335     * remove
2336     * Remove the song from disk.
2337     * @return bool
2338     */
2339    public function remove(): bool
2340    {
2341        return $this->getSongDeleter()->delete($this);
2342    }
2343
2344    /**
2345     * @deprecated
2346     */
2347    private function getSongTagWriter(): SongTagWriterInterface
2348    {
2349        global $dic;
2350
2351        return $dic->get(SongTagWriterInterface::class);
2352    }
2353
2354    /**
2355     * @deprecated
2356     */
2357    private static function getLicenseRepository(): LicenseRepositoryInterface
2358    {
2359        global $dic;
2360
2361        return $dic->get(LicenseRepositoryInterface::class);
2362    }
2363
2364    /**
2365     * @deprecated
2366     */
2367    private function getSongDeleter(): SongDeleterInterface
2368    {
2369        global $dic;
2370
2371        return $dic->get(SongDeleterInterface::class);
2372    }
2373
2374    /**
2375     * @deprecated inject dependency
2376     */
2377    private static function getUserActivityPoster(): UserActivityPosterInterface
2378    {
2379        global $dic;
2380
2381        return $dic->get(UserActivityPosterInterface::class);
2382    }
2383}
2384