ampache/ampache

View on GitHub
src/Repository/Model/Song.php

Summary

Maintainability
F
1 wk
Test Coverage
<?php

declare(strict_types=0);

/**
 * vim:set softtabstop=4 shiftwidth=4 expandtab:
 *
 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
 * Copyright Ampache.org, 2001-2023
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

namespace Ampache\Repository\Model;

use Ampache\Config\AmpConfig;
use Ampache\Module\Authorization\Access;
use Ampache\Module\Authorization\AccessLevelEnum;
use Ampache\Module\Authorization\Check\NetworkCheckerInterface;
use Ampache\Module\Metadata\MetadataEnabledInterface;
use Ampache\Module\Metadata\MetadataManagerInterface;
use Ampache\Module\Playback\Stream;
use Ampache\Module\Playback\Stream_Url;
use Ampache\Module\Song\Deletion\SongDeleterInterface;
use Ampache\Module\Song\Tag\SongTagWriterInterface;
use Ampache\Module\Statistics\Stats;
use Ampache\Module\System\Core;
use Ampache\Module\System\Dba;
use Ampache\Module\User\Activity\UserActivityPosterInterface;
use Ampache\Module\Util\Recommendation;
use Ampache\Module\Util\Ui;
use Ampache\Repository\AlbumRepositoryInterface;
use Ampache\Repository\LicenseRepositoryInterface;
use Ampache\Repository\MetadataRepositoryInterface;
use Ampache\Repository\ShareRepositoryInterface;
use Ampache\Repository\ShoutRepositoryInterface;
use Ampache\Repository\WantedRepositoryInterface;
use PDOStatement;
use Traversable;

class Song extends database_object implements
    Media,
    library_item,
    GarbageCollectibleInterface,
    CatalogItemInterface,
    MetadataEnabledInterface
{
    protected const DB_TABLENAME = 'song';

    /* Variables from DB */
    public int $id = 0;
    public ?string $file;
    public int $catalog;
    public int $album;
    public ?int $disk = null;
    public int $year;
    public ?int $artist;
    public ?string $title = null;
    public int $bitrate;
    public int $rate;
    public ?string $mode = null;
    public int $size;
    public int $time;
    public ?int $track   = null;
    public ?string $mbid = null;
    public bool $played;
    public bool $enabled;
    public int $update_time;
    public int $addition_time;
    public ?int $user_upload = null;
    public ?int $license     = null;
    public ?string $composer = null;
    public ?int $channels    = null;
    public int $total_count;
    public int $total_skip;

    /** song_data table */
    public ?string $comment              = null;
    public ?string $lyrics               = null;
    public ?string $label                = null;
    public ?string $language             = null;
    public ?string $waveform             = null;
    public ?float $replaygain_track_gain = null;
    public ?float $replaygain_track_peak = null;
    public ?float $replaygain_album_gain = null;
    public ?float $replaygain_album_peak = null;
    public ?int $r128_album_gain         = null;
    public ?int $r128_track_gain         = null;
    public ?string $disksubtitle         = null;

    public ?string $link = null;
    /** @var string $type */
    public $type;
    /** @var string $mime */
    public $mime;
    /** @var string $catalog_number */
    public $catalog_number;
    /** @var array $artists */
    public array $artists;
    /** @var array $albumartists */
    public array $albumartists;
    /** @var string $artist_mbid */
    public $artist_mbid;
    /** @var string $albumartist_mbid */
    public $albumartist_mbid;
    /** @var string $album_mbid */
    public $album_mbid;
    /** @var int $album_disk */
    public $album_disk;
    /** @var array $tags */
    public $tags;
    /** @var null|string $f_name */
    public $f_name;
    /** @var null|string $f_artist */
    public $f_artist;
    /** @var null|string $f_album */
    public $f_album;

    private ?string $artist_full_name = null;

    /** @var int|null $albumartist */
    public $albumartist;
    /** @var null|string $f_albumartist_full */
    public $f_albumartist_full;
    /** @var null|string $f_album_full */
    public $f_album_full;
    /** @var null|string $f_time */
    public $f_time;
    /** @var null|string $f_time_h */
    public $f_time_h;
    /** @var null|string $f_track */
    public $f_track;
    /** @var null|string $f_bitrate */
    public $f_bitrate;
    /** @var null|string $f_name_full */
    public $f_name_full;
    /** @var null|string $f_link */
    public $f_link;
    /** @var null|string $f_album_link */
    public $f_album_link;
    /** @var null|string $f_album_disk_link */
    public $f_album_disk_link;
    /** @var null|string $f_artist_link */
    public $f_artist_link;
    /** @var null|string $f_albumartist_link */
    public $f_albumartist_link;
    /** @var null|string $f_year_link */
    public $f_year_link;
    /** @var null|string $f_tags */
    public $f_tags;
    /** @var null|string $f_size */
    public $f_size;
    /** @var null|string $f_lyrics */
    public $f_lyrics;

    /** @var int $count */
    public $count;
    /** @var null|string $f_publisher */
    public $f_publisher;
    /** @var null|string $f_composer */
    public $f_composer;

    /** @var int $tag_id */
    public $tag_id;

    private ?bool $has_art = null;

    private ?License $licenseObj = null;

    /* Setting Variables */
    /**
     * @var bool $_fake
     */
    public $_fake = false; // If this is a 'construct_from_array' object

    /**
     * Constructor
     *
     * Song class, for modifying a song.
     * @param int|null $song_id
     */
    public function __construct($song_id = 0)
    {
        if (!$song_id) {
            return;
        }

        $info = $this->has_info($song_id);
        if (empty($info)) {
            return;
        }
        foreach ($info as $key => $value) {
            $this->$key = $value;
        }
        $this->id          = (int)$song_id;
        $this->type        = strtolower(pathinfo((string)$this->file, PATHINFO_EXTENSION));
        $this->mime        = self::type_to_mime($this->type);
        $this->total_count = (int)$this->total_count;
    }

    public function getId(): int
    {
        return (int)($this->id ?? 0);
    }

    public function isNew(): bool
    {
        return $this->getId() === 0;
    }

    /**
     * insert
     *
     * This inserts the song described by the passed array
     * @param array $results
     * @return int|false
     */
    public static function insert(array $results)
    {
        $check_file = Catalog::get_id_from_file($results['file'], 'song');
        if ($check_file > 0) {
            return $check_file;
        }
        $catalog          = $results['catalog'];
        $file             = $results['file'];
        $title            = Catalog::check_length(Catalog::check_title($results['title'] ?? null, $file));
        $artist           = Catalog::check_length($results['artist'] ?? null);
        $album            = Catalog::check_length($results['album'] ?? null);
        $albumartist      = Catalog::check_length($results['albumartist'] ?? null);
        $bitrate          = $results['bitrate'] ?? 0;
        $rate             = $results['rate'] ?? 0;
        $mode             = $results['mode'] ?? null;
        $size             = $results['size'] ?? 0;
        $time             = $results['time'] ?? 0;
        $track            = Catalog::check_track((string) $results['track']);
        $track_mbid       = $results['mb_trackid'] ?? $results['mbid'] ?? null;
        $album_mbid       = $results['mb_albumid'] ?? null;
        $album_mbid_group = $results['mb_albumid_group'] ?? null;
        $artist_mbid      = $results['mb_artistid'] ?? null;
        $albumartist_mbid = $results['mb_albumartistid'] ?? null;
        $disk             = (Album::sanitize_disk($results['disk']) > 0) ? Album::sanitize_disk($results['disk']) : 1;
        $disksubtitle     = $results['disksubtitle'] ?? null;
        $year             = Catalog::normalize_year($results['year'] ?? 0);
        $comment          = $results['comment'] ?? null;
        $tags             = $results['genre'] ?? array(); // multiple genre support makes this an array
        $lyrics           = $results['lyrics'] ?? null;
        $user_upload      = $results['user_upload'] ?? null;
        $composer         = isset($results['composer']) ? Catalog::check_length($results['composer']) : null;
        $label            = isset($results['publisher']) ? Catalog::get_unique_string(Catalog::check_length($results['publisher'], 128)) : null;
        if ($label && AmpConfig::get('label')) {
            // create the label if missing
            foreach (array_map('trim', explode(';', $label)) as $label_name) {
                Label::helper($label_name);
            }
        }
        // info for the artist_map table.
        $artists_array          = $results['artists'] ?? array();
        $artist_mbid_array      = $results['mb_artistid_array'] ?? array();
        $albumartist_mbid_array = $results['mb_albumartistid_array'] ?? array();
        // if you have an artist array this will be named better than what your tags will give you
        if (!empty($artists_array)) {
            if (!empty($artist) && !empty($albumartist) && $artist == $albumartist) {
                $albumartist = (string)$artists_array[0];
            }
            $artist = (string)$artists_array[0];
        }
        $license_id = null;
        if (isset($results['license']) && (int)$results['license'] > 0) {
            $license_id = (int)$results['license'];
        }

        $language              = isset($results['language']) ? Catalog::check_length($results['language'], 128) : null;
        $channels              = $results['channels'] ?? null;
        $release_type          = isset($results['release_type']) ? Catalog::check_length($results['release_type'], 32) : null;
        $release_status        = $results['release_status'] ?? null;
        $replaygain_track_gain = $results['replaygain_track_gain'] ?? null;
        $replaygain_track_peak = $results['replaygain_track_peak'] ?? null;
        $replaygain_album_gain = $results['replaygain_album_gain'] ?? null;
        $replaygain_album_peak = $results['replaygain_album_peak'] ?? null;
        $r128_track_gain       = $results['r128_track_gain'] ?? null;
        $r128_album_gain       = $results['r128_album_gain'] ?? null;
        $original_year         = Catalog::normalize_year($results['original_year'] ?? 0);
        $barcode               = (isset($results['barcode'])) ? Catalog::check_length($results['barcode'], 64) : null;
        $catalog_number        = isset($results['catalog_number']) ? Catalog::check_length($results['catalog_number'], 64) : null;
        $version               = (isset($results['version'])) ? Catalog::check_length($results['version'], 64) : null;

        if (!in_array($mode, ['vbr', 'cbr', 'abr'])) {
            debug_event(self::class, 'Error analyzing: ' . $file . ' unknown file bitrate mode: ' . $mode, 2);
            $mode = null;
        }
        if (!isset($results['albumartist_id'])) {
            $albumartist_id = null;
            if ($albumartist) {
                $albumartist_mbid = Catalog::trim_slashed_list($albumartist_mbid);
                $albumartist_id   = Artist::check($albumartist, $albumartist_mbid);
            }
        } else {
            $albumartist_id = (int)($results['albumartist_id']);
        }
        if (!isset($results['artist_id'])) {
            $artist_mbid = Catalog::trim_slashed_list($artist_mbid);
            $artist_id   = (int)Artist::check($artist, $artist_mbid);
        } else {
            $artist_id = (int)($results['artist_id']);
        }
        if (!isset($results['album_id'])) {
            $album_id = Album::check($catalog, $album, $year, $album_mbid, $album_mbid_group, $albumartist_id, $release_type, $release_status, $original_year, $barcode, $catalog_number, $version);
        } else {
            $album_id = (int)($results['album_id']);
        }
        $insert_time = time();

        $sql = "INSERT INTO `song` (`catalog`, `file`, `album`, `disk`, `artist`, `title`, `bitrate`, `rate`, `mode`, `size`, `time`, `track`, `addition_time`, `update_time`, `year`, `mbid`, `user_upload`, `license`, `composer`, `channels`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";

        $db_results = Dba::write($sql, array(
            $catalog,
            $file,
            $album_id,
            $disk,
            $artist_id,
            $title,
            $bitrate,
            $rate,
            $mode,
            $size,
            $time,
            $track,
            $insert_time,
            $insert_time,
            $year,
            $track_mbid,
            $user_upload,
            $license_id,
            $composer,
            $channels
        ));

        if (!$db_results) {
            debug_event(self::class, 'Unable to insert ' . $file, 2);

            return false;
        }

        $song_id = (int)Dba::insert_id();
        $artists = array($artist_id, (int)$albumartist_id);

        // create the album_disk (if missing)
        AlbumDisk::check($album_id, $disk, $catalog, $disksubtitle);

        // map the song to catalog album and artist maps
        Catalog::update_map((int)$catalog, 'song', $song_id);
        if ($artist_id > 0) {
            Artist::add_artist_map($artist_id, 'song', $song_id);
            Album::add_album_map($album_id, 'song', (int) $artist_id);
        }
        if ((int)$albumartist_id > 0) {
            Artist::add_artist_map($albumartist_id, 'album', $album_id);
            Album::add_album_map($album_id, 'album', (int) $albumartist_id);
        }
        foreach ($artist_mbid_array as $songArtist_mbid) {
            $song_artist_id = Artist::check_mbid($songArtist_mbid);
            if ($song_artist_id > 0) {
                $artists[] = $song_artist_id;
                if ($song_artist_id != $artist_id) {
                    Artist::add_artist_map($song_artist_id, 'song', $song_id);
                    Album::add_album_map($album_id, 'song', $song_artist_id);
                }
            }
        }
        // add song artists found by name to the list (Ignore artist names when we have the same amount of MBID's)
        if (!empty($artists_array) && !count($artists_array) == count($artist_mbid_array)) {
            foreach ($artists_array as $artist_name) {
                $song_artist_id = (int)Artist::check($artist_name);
                if ($song_artist_id > 0) {
                    $artists[] = $song_artist_id;
                    if ($song_artist_id != $artist_id) {
                        Artist::add_artist_map($song_artist_id, 'song', $song_id);
                        Album::add_album_map($album_id, 'song', $song_artist_id);
                    }
                }
            }
        }
        foreach ($albumartist_mbid_array as $albumArtist_mbid) {
            $album_artist_id = Artist::check_mbid($albumArtist_mbid);
            if ($album_artist_id > 0) {
                $artists[] = $album_artist_id;
                if ($album_artist_id != $albumartist_id) {
                    Artist::add_artist_map($album_artist_id, 'album', $album_id);
                    Album::add_album_map($album_id, 'album', $album_artist_id);
                }
            }
        }
        // update the all the counts for the album right away
        Album::update_album_count($album_id);

        if ($user_upload) {
            static::getUserActivityPoster()->post((int) $user_upload, 'upload', 'song', (int) $song_id, time());
        }

        // Allow scripts to populate new tags when injecting user uploads
        if (!defined('NO_SESSION')) {
            if ($user_upload && !Access::check('interface', 50, $user_upload)) {
                $tags = Tag::clean_to_existing($tags);
            }
        }
        if (is_array($tags)) {
            foreach ($tags as $tag) {
                $tag = trim((string)$tag);
                if (!empty($tag)) {
                    Tag::add('song', $song_id, $tag, false);
                    Tag::add('album', $album_id, $tag, false);
                    foreach (array_unique($artists) as $found_artist_id) {
                        if ($found_artist_id > 0) {
                            Tag::add('artist', $found_artist_id, $tag, false);
                        }
                    }
                }
            }
        }

        $sql = "INSERT INTO `song_data` (`song_id`, `disksubtitle`, `comment`, `lyrics`, `label`, `language`, `replaygain_track_gain`, `replaygain_track_peak`, `replaygain_album_gain`, `replaygain_album_peak`, `r128_track_gain`, `r128_album_gain`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
        Dba::write($sql, array($song_id, $disksubtitle, $comment, $lyrics, $label, $language, $replaygain_track_gain, $replaygain_track_peak, $replaygain_album_gain, $replaygain_album_peak, $r128_track_gain, $r128_album_gain));

        return $song_id;
    }

    /**
     * garbage_collection
     *
     * Cleans up the song_data table
     */
    public static function garbage_collection(): void
    {
        // delete files matching catalog_ignore_pattern
        $ignore_pattern = AmpConfig::get('catalog_ignore_pattern');
        if ($ignore_pattern) {
            Dba::write("DELETE FROM `song` WHERE `file` REGEXP ?;", array($ignore_pattern));
        }
        // delete duplicates
        Dba::write("DELETE `dupe` FROM `song` AS `dupe`, `song` AS `orig` WHERE `dupe`.`id` > `orig`.`id` AND `dupe`.`file` <=> `orig`.`file`;");
        // clean up missing catalogs
        Dba::write("DELETE FROM `song` WHERE `song`.`catalog` NOT IN (SELECT `id` FROM `catalog`);");
        // delete the rest
        Dba::write("DELETE FROM `song_data` WHERE `song_data`.`song_id` NOT IN (SELECT `song`.`id` FROM `song`);");
        // also clean up some bad data that might creep in
        Dba::write("UPDATE `song` SET `composer` = NULL WHERE `composer` = '';");
        Dba::write("UPDATE `song` SET `mbid` = NULL WHERE `mbid` = '';");
        Dba::write("UPDATE `song_data` SET `comment` = NULL WHERE `comment` = '';");
        Dba::write("UPDATE `song_data` SET `lyrics` = NULL WHERE `lyrics` = '';");
        Dba::write("UPDATE `song_data` SET `label` = NULL WHERE `label` = '';");
        Dba::write("UPDATE `song_data` SET `language` = NULL WHERE `language` = '';");
        Dba::write("UPDATE `song_data` SET `waveform` = NULL WHERE `waveform` = '';");
    }

    /**
     * build_cache
     *
     * This attempts to reduce queries by asking for everything in the
     * browse all at once and storing it in the cache, this can help if the
     * db connection is the slow point.
     * @param int[] $song_ids
     * @param string $limit_threshold
     */
    public static function build_cache($song_ids, $limit_threshold = ''): bool
    {
        if (empty($song_ids)) {
            return false;
        }

        $idlist = '(' . implode(',', $song_ids) . ')';
        if ($idlist == '()') {
            return false;
        }
        $artists = array();
        $albums  = array();
        $tags    = array();

        // Song data cache
        $sql = (AmpConfig::get('catalog_disable'))
            ? "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' "
            : "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";

        $db_results = Dba::read($sql);
        while ($row = Dba::fetch_assoc($db_results)) {
            if (AmpConfig::get('show_played_times')) {
                $row['total_count'] = (!empty($limit_threshold))
                    ? Stats::get_object_count('song', $row['id'], $limit_threshold)
                    : $row['total_count'];
            }
            if (AmpConfig::get('show_skipped_times')) {
                $row['total_skip'] = (!empty($limit_threshold))
                    ? Stats::get_object_count('song', $row['id'], $limit_threshold, 'skip')
                    : $row['total_skip'];
            }
            parent::add_to_cache('song', $row['id'], $row);
            $artists[$row['artist']] = $row['artist'];

            $albums[] = (int) $row['album'];

            if ($row['tag_id']) {
                $tags[$row['tag_id']] = $row['tag_id'];
            }
        }

        Artist::build_cache($artists);
        Album::build_cache($albums);
        Tag::build_cache($tags);
        Tag::build_map_cache('song', $song_ids);
        Art::build_cache($albums);

        // If we're rating this then cache them as well
        if (AmpConfig::get('ratings')) {
            Rating::build_cache('song', $song_ids);
            Userflag::build_cache('song', $song_ids);
        }

        // Build a cache for the song's extended table
        $sql        = "SELECT * FROM `song_data` WHERE `song_id` IN $idlist";
        $db_results = Dba::read($sql);

        while ($row = Dba::fetch_assoc($db_results)) {
            parent::add_to_cache('song_data', $row['song_id'], $row);
        }

        return true;
    }

    /**
     * has_info
     * @param int $song_id
     * @return array
     */
    private function has_info($song_id): array
    {
        if (parent::is_cached('song', $song_id)) {
            return parent::get_from_cache('song', $song_id);
        }

        $sql        = "SELECT `song`.`id`, `song`.`file`, `song`.`catalog`, `song`.`album`, `song`.`disk`, `song`.`year`, `song`.`artist`, `song`.`title`, `song`.`bitrate`, `song`.`rate`, `song`.`mode`, `song`.`size`, `song`.`time`, `song`.`track`, `song`.`mbid`, `song`.`played`, `song`.`enabled`, `song`.`update_time`, `song`.`addition_time`, `song`.`license`, `song`.`composer`, `song`.`channels`, `song`.`total_count`, `song`.`total_skip`, `album`.`album_artist` AS `albumartist`, `song`.`user_upload`, `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` = ?";
        $db_results = Dba::read($sql, array($song_id));
        $results    = Dba::fetch_assoc($db_results);
        if (isset($results['id'])) {
            parent::add_to_cache('song', $song_id, $results);

            return $results;
        }

        return array();
    }

    /**
     * has_id
     * @param int|string $song_id
     */
    public static function has_id($song_id): bool
    {
        $sql        = "SELECT `song`.`id` FROM `song` WHERE `song`.`id` = ?";
        $db_results = Dba::read($sql, array($song_id));
        $results    = Dba::fetch_assoc($db_results);
        if (isset($results['id'])) {
            return true;
        }

        return false;
    }

    /**
     * can_scrobble
     *
     * return a song id based on a last.fm-style search in the database
     * @param string $song_name
     * @param string $artist_name
     * @param string $album_name
     * @param string $song_mbid
     * @param string $artist_mbid
     * @param string $album_mbid
     * @return string
     */
    public static function can_scrobble(
        $song_name,
        $artist_name,
        $album_name,
        $song_mbid = '',
        $artist_mbid = '',
        $album_mbid = ''
    ): string {
        // by default require song, album, artist for any searches
        $sql    = "SELECT `song`.`id` FROM `song` LEFT JOIN `album` ON `album`.`id` = `song`.`album` LEFT JOIN `artist` ON `artist`.`id` = `song`.`artist` WHERE `song`.`title` = ? AND (`artist`.`name` = ? OR LTRIM(CONCAT(COALESCE(`artist`.`prefix`, ''), ' ', `artist`.`name`)) = ?) AND (`album`.`name` = ? OR LTRIM(CONCAT(COALESCE(`album`.`prefix`, ''), ' ', `album`.`name`)) = ?)";
        $params = array($song_name, $artist_name, $artist_name, $album_name, $album_name);
        if (!empty($song_mbid)) {
            $sql .= " AND `song`.`mbid` = ?";
            $params[] = $song_mbid;
        }
        if (!empty($artist_mbid)) {
            $sql .= " AND `artist`.`mbid` = ?";
            $params[] = $artist_mbid;
        }
        if (!empty($album_mbid)) {
            $sql .= " AND `album`.`mbid` = ?";
            $params[] = $album_mbid;
        }
        $sql .= " LIMIT 1;";
        $db_results = Dba::read($sql, $params);
        $row        = Dba::fetch_assoc($db_results);
        if (empty($row)) {
            debug_event(self::class, 'can_scrobble failed to find: ' . $song_name, 5);

            return '';
        }

        return $row['id'];
    }

    /**
     * _get_ext_info
     * This function gathers information from the song_ext_info table and adds it to the
     * current object
     * @param string $select
     * @return array
     */
    public function _get_ext_info($select = '')
    {
        $song_id = (int) ($this->id);
        $columns = (!empty($select)) ? Dba::escape($select) : '*';

        if (parent::is_cached('song_data', $song_id)) {
            return parent::get_from_cache('song_data', $song_id);
        }

        $sql        = "SELECT $columns FROM `song_data` WHERE `song_id` = ?";
        $db_results = Dba::read($sql, array($song_id));
        if (!$db_results) {
            return array();
        }
        $results = Dba::fetch_assoc($db_results);

        parent::add_to_cache('song_data', $song_id, $results);

        return $results;
    }

    /**
     * fill_ext_info
     * This calls the _get_ext_info and then sets the correct vars
     * @param string $data_filter
     */
    public function fill_ext_info($data_filter = ''): void
    {
        $info = $this->_get_ext_info($data_filter);
        if (empty($info)) {
            return;
        }
        foreach ($info as $key => $value) {
            if ($key != 'song_id') {
                $this->$key = $value;
            }
        } // end foreach
    }

    /**
     * type_to_mime
     *
     * Returns the mime type for the specified file extension/type
     * @param string $type
     */
    public static function type_to_mime($type): string
    {
        // FIXME: This should really be done the other way around.
        // Store the mime type in the database, and provide a function
        // to make it a human-friendly type.
        switch ($type) {
            case 'spx':
            case 'ogg':
                return 'application/ogg';
            case 'opus':
                return 'audio/ogg; codecs=opus';
            case 'wma':
            case 'asf':
                return 'audio/x-ms-wma';
            case 'rm':
            case 'ra':
                return 'audio/x-realaudio';
            case 'flac':
                return 'audio/flac';
            case 'wv':
                return 'audio/x-wavpack';
            case 'aac':
            case 'mp4':
            case 'm4a':
            case 'm4b':
                return 'audio/mp4';
            case 'aacp':
                return 'audio/aacp';
            case 'mpc':
                return 'audio/x-musepack';
            case 'mkv':
                return 'audio/x-matroska';
            case 'wav':
                return 'audio/wav';
            case 'webma':
                return 'audio/webm';
            case 'mpeg3':
            case 'mp3':
            default:
                return 'audio/mpeg';
        }
    }

    /**
     * get_disabled
     *
     * Gets a list of the disabled songs for and returns an array of Songs
     * @param int $count
     * @return Song[]
     */
    public static function get_disabled($count = 0): array
    {
        $results = array();

        $sql = "SELECT `id` FROM `song` WHERE `enabled`='0'";
        if ($count) {
            $sql .= " LIMIT $count";
        }
        $db_results = Dba::read($sql);

        while ($row = Dba::fetch_assoc($db_results)) {
            $results[] = new Song($row['id']);
        }

        return $results;
    }

    /**
     * find
     * @param array $data
     */
    public static function find($data): bool
    {
        $sql_base = "SELECT `song`.`id` FROM `song`";
        if ($data['mb_trackid']) {
            $sql        = $sql_base . " WHERE `song`.`mbid` = ? LIMIT 1";
            $db_results = Dba::read($sql, array($data['mb_trackid']));
            if ($results = Dba::fetch_assoc($db_results)) {
                return $results['id'];
            }
        }
        if ($data['file']) {
            $sql        = $sql_base . " WHERE `song`.`file` = ? LIMIT 1";
            $db_results = Dba::read($sql, array($data['file']));
            if ($results = Dba::fetch_assoc($db_results)) {
                return $results['id'];
            }
        }

        $where  = "WHERE `song`.`title` = ?";
        $sql    = $sql_base;
        $params = array($data['title']);
        if ($data['track']) {
            $where .= " AND `song`.`track` = ?";
            $params[] = $data['track'];
        }
        $sql .= " INNER JOIN `artist` ON `artist`.`id` = `song`.`artist`";
        $sql .= " INNER JOIN `album` ON `album`.`id` = `song`.`album`";

        if ($data['mb_artistid']) {
            $where .= " AND `artist`.`mbid` = ?";
            $params[] = $data['mb_artistid'];
        } else {
            $where .= " AND `artist`.`name` = ?";
            $params[] = $data['artist'];
        }
        if ($data['mb_albumid']) {
            $where .= " AND `album`.`mbid` = ?";
            $params[] = $data['mb_albumid'];
        } else {
            $where .= " AND `album`.`name` = ?";
            $params[] = $data['album'];
        }

        $sql .= $where . " LIMIT 1";
        $db_results = Dba::read($sql, $params);
        if ($results = Dba::fetch_assoc($db_results)) {
            return $results['id'];
        }

        return false;
    }

    /**
     * get_album_fullname
     * gets the name of $this->album, allows passing of id
     * @param int $album_id
     * @param bool $simple
     */
    public function get_album_fullname($album_id = 0, $simple = false): string
    {
        if (isset($this->f_album_full) && $album_id == 0) {
            return $this->f_album_full;
        }
        $album = (!$album_id)
            ? new Album($this->album)
            : new Album($album_id);
        $this->f_album_full = $album->get_fullname($simple);

        return $this->f_album_full;
    }

    /**
     * get_album_disk_fullname
     * gets the name of $this->album, allows passing of id
     */
    public function get_album_disk_fullname(): string
    {
        $albumDisk = new AlbumDisk((int)$this->get_album_disk());

        return $albumDisk->get_fullname();
    }

    /**
     * get_album_catalog_number
     * gets the catalog_number of $this->album, allows passing of id
     * @param int $album_id
     */
    public function get_album_catalog_number($album_id = null): ?string
    {
        if ($album_id === null) {
            $album_id = $this->album;
        }
        $album = new Album($album_id);

        return $album->catalog_number;
    }

    /**
     * get_album_original_year
     * gets the original_year of $this->album, allows passing of id
     * @param int $album_id
     */
    public function get_album_original_year($album_id = null): ?int
    {
        if ($album_id === null) {
            $album_id = $this->album;
        }
        $album = new Album($album_id);

        return $album->original_year;
    }

    /**
     * get_album_barcode
     * gets the barcode of $this->album, allows passing of id
     * @param int $album_id
     */
    public function get_album_barcode($album_id = null): ?string
    {
        if (!$album_id) {
            $album_id = $this->album;
        }
        $album = new Album($album_id);

        return $album->barcode;
    }

    /**
     * get_artist_fullname
     * gets the name of $this->artist, allows passing of id
     */
    public function get_artist_fullname(): string
    {
        if ($this->artist_full_name === null) {
            $this->artist_full_name = Artist::get_fullname_by_id($this->artist);
        }

        return $this->artist_full_name;
    }

    /**
     * get_album_artist_fullname
     * gets the name of $this->albumartist, allows passing of id
     * @param int $album_artist_id
     */
    public function get_album_artist_fullname($album_artist_id = 0): ?string
    {
        if ($album_artist_id) {
            return Artist::get_fullname_by_id($album_artist_id);
        }
        if (!$this->albumartist) {
            return '';
        }
        if (!isset($this->albumartist)) {
            $this->albumartist = $this->getAlbumRepository()->getAlbumArtistId($this->album);
        }

        return Artist::get_fullname_by_id($this->albumartist);
    }

    /**
     * get_album_disk
     * gets album_disk of the object
     * @return int
     */
    public function get_album_disk(): ?int
    {
        if ($this->album_disk) {
            return $this->album_disk;
        }
        $sql        = "SELECT DISTINCT `id` FROM `album_disk` WHERE `album_id` = ? AND `disk` = ?;";
        $db_results = Dba::read($sql, array($this->album, $this->disk));
        $results    = Dba::fetch_assoc($db_results);
        if (empty($results)) {
            return null;
        }
        $this->album_disk = (int)$results['id'];

        return $this->album_disk;
    }

    /**
     * set_played
     * this checks to see if the current object has been played
     * if not then it sets it to played. In any case it updates stats.
     * @param int $user_id
     * @param string $agent
     * @param array $location
     * @param int $date
     */
    public function set_played($user_id, $agent, $location, $date): bool
    {
        // ignore duplicates or skip the last track
        if (!$this->check_play_history($user_id, $agent, $date)) {
            return false;
        }
        // insert stats for each object type
        if (Stats::insert('song', $this->id, $user_id, $agent, $location, 'stream', $date)) {
            // followup on some stats too
            Stats::insert('album', $this->album, $user_id, $agent, $location, 'stream', $date);
            // insert plays for song and album artists
            $artists = array_unique(array_merge(self::get_parent_array($this->id), self::get_parent_array($this->album, 'album')));
            foreach ($artists as $artist_id) {
                Stats::insert('artist', $artist_id, $user_id, $agent, $location, 'stream', $date);
            }
            // running total of the user stream data
            $play_size = User::get_user_data($user_id, 'play_size', 0)['play_size'];
            User::set_user_data($user_id, 'play_size', ($play_size + ($this->size / 1024 / 1024)));
        }
        // If it hasn't been played, set it
        if (!$this->played) {
            self::update_played(true, $this->id);
        }

        return true;
    }

    /**
     * check_play_history
     * this checks to see if the current object has been played
     * if not then it sets it to played. In any case it updates stats.
     * @param int $user
     * @param string $agent
     * @param int $date
     */
    public function check_play_history($user, $agent, $date): bool
    {
        return Stats::has_played_history('song', $this, $user, $agent, $date);
    }

    /**
     * compare_song_information
     * this compares the new ID3 tags of a file against
     * the ones in the database to see if they have changed
     * it returns false if nothing has changes, or the true
     * if they have. Static because it doesn't need this
     * @param Song $song
     * @param Song $new_song
     * @return array
     */
    public static function compare_song_information(Song $song, Song $new_song): array
    {
        // Remove some stuff we don't care about as this function only needs to check song information.
        unset($song->catalog, $song->played, $song->enabled, $song->addition_time, $song->update_time, $song->type);
        $string_array = array('title', 'comment', 'lyrics', 'composer', 'tags', 'artist', 'album', 'album_disk', 'time');
        $skip_array   = array(
            'id',
            'tag_id',
            'mime',
            'mbid',
            'waveform',
            'total_count',
            'total_skip',
            'albumartist',
            'artist_mbid',
            'album_mbid',
            'albumartist_mbid',
            'mb_albumid_group',
            'disabledMetadataFields'
        );

        return self::compare_media_information($song, $new_song, $string_array, $skip_array);
    }

    /**
     * compare_media_information
     * @param Song|Video $media
     * @param Song|Video $new_media
     * @param string[] $string_array
     * @param string[] $skip_array
     * @return array
     */
    public static function compare_media_information($media, $new_media, $string_array, $skip_array): array
    {
        $array            = array();
        $array['change']  = false;
        $array['element'] = array();

        // Pull out all the currently set vars
        $fields = get_object_vars($media);

        // Foreach them
        foreach ($fields as $key => $value) {
            $key = trim((string)$key);
            if (empty($key) || in_array($key, $skip_array)) {
                continue;
            }

            // Represent the value as a string for simpler comparison. For array, ensure to sort similarly old/new values
            if (is_array($media->$key)) {
                $arr = $media->$key;
                sort($arr);
                $mediaData = implode(" ", $arr);
            } else {
                $mediaData = $media->$key;
            }

            // Skip the item if it is no string nor something we can turn into a string
            if (!is_string($mediaData) && !is_numeric($mediaData) && !is_bool($mediaData)) {
                if (is_object($mediaData) && !method_exists($mediaData, '__toString')) {
                    continue;
                }
            }

            if (is_array($new_media->$key)) {
                $arr = $new_media->$key;
                sort($arr);
                $newMediaData = implode(" ", $arr);
            } else {
                $newMediaData = $new_media->$key;
            }

            if (in_array($key, $string_array)) {
                // If it's a string thing
                $mediaData    = self::clean_string_field_value($mediaData);
                $newMediaData = self::clean_string_field_value($newMediaData);
                if ($mediaData != $newMediaData) {
                    $array['change']        = true;
                    $array['element'][$key] = 'OLD: ' . $mediaData . ' --> ' . $newMediaData;
                }
            } elseif ($newMediaData !== null) {
                // in array of strings
                if ($media->$key != $new_media->$key) {
                    $array['change']        = true;
                    $array['element'][$key] = 'OLD:' . $mediaData . ' --> ' . $newMediaData;
                }
            } // end else
        } // end foreach

        if ($array['change']) {
            debug_event(self::class, 'media-diff ' . json_encode($array['element']), 5);
        }

        return $array;
    }

    /**
     * clean_string_field_value
     * @param string $value
     */
    private static function clean_string_field_value($value): string
    {
        if (!$value) {
            return '';
        }
        $value = trim(stripslashes(preg_replace('/\s+/', ' ', $value)));

        // Strings containing  only UTF-8 BOM = empty string
        if (strlen((string)$value) == 2 && (ord($value[0]) == 0xFF || ord($value[0]) == 0xFE)) {
            $value = "";
        }

        return $value;
    }

    /**
     * update
     * This takes a key'd array of data does any cleaning it needs to
     * do and then calls the helper functions as needed.
     * @param array $data
     * @return int
     */
    public function update(array $data): int
    {
        foreach ($data as $key => $value) {
            debug_event(self::class, $key . '=' . $value, 5);

            switch ($key) {
                case 'artist_name':
                    // Create new artist name and id
                    $old_artist_id = $this->artist;
                    $new_artist_id = (int)Artist::check($value);
                    if ($new_artist_id > 0) {
                        $this->artist = $new_artist_id;
                        self::update_artist($new_artist_id, $this->id, $old_artist_id);
                    }
                    break;
                case 'album_name':
                    // Create new album name and id
                    $old_album_id = $this->album;
                    $new_album_id = Album::check($this->catalog, $value);
                    $this->album  = $new_album_id;
                    self::update_album($new_album_id, $this->id, $old_album_id);
                    break;
                case 'artist':
                    // Change artist the song is assigned to
                    if ($value != $this->$key) {
                        $old_artist_id = $this->artist;
                        $new_artist_id = $value;
                        self::update_artist($new_artist_id, $this->id, $old_artist_id);
                    }
                    break;
                case 'album':
                    // Change album the song is assigned to
                    if ($value != $this->$key) {
                        $old_album_id = $this->$key;
                        $new_album_id = $value;
                        self::update_album($new_album_id, $this->id, $old_album_id);
                    }
                    break;
                case 'year':
                case 'title':
                case 'track':
                case 'mbid':
                case 'license':
                case 'composer':
                case 'label':
                case 'language':
                case 'comment':
                    // Check to see if it needs to be updated
                    if ($value != $this->$key) {
                        $function = 'update_' . $key;
                        self::$function($value, $this->id);
                        $this->$key = $value;
                    }
                    break;
                case 'edit_tags':
                    Tag::update_tag_list($value, 'song', $this->id, true);
                    $this->tags = Tag::get_top_tags('song', $this->id);
                    break;
                case 'metadata':
                    $this->updateMetadata($value);
                    break;
            } // end whitelist
        } // end foreach

        $this->getSongTagWriter()->write(
            $this
        );

        return $this->id;
    }

    /**
     * update_song
     * this is the main updater for a song and updates
     * the "update_time" of the song
     * @param int $song_id
     * @param Song $new_song
     */
    public static function update_song($song_id, Song $new_song): void
    {
        $update_time = time();

        $sql = "UPDATE `song` SET `album` = ?, `disk` = ?, `year` = ?, `artist` = ?, `title` = ?, `composer` = ?, `bitrate` = ?, `rate` = ?, `mode` = ?, `channels` = ?, `size` = ?, `time` = ?, `track` = ?, `mbid` = ?, `update_time` = ? WHERE `id` = ?";
        Dba::write($sql, array($new_song->album, $new_song->disk, $new_song->year, $new_song->artist, $new_song->title, $new_song->composer, (int) $new_song->bitrate, (int) $new_song->rate, $new_song->mode, $new_song->channels, (int) $new_song->size, (int) $new_song->time, $new_song->track, $new_song->mbid, $update_time, $song_id));

        $sql = "UPDATE `song_data` SET `label` = ?, `lyrics` = ?, `language` = ?, `disksubtitle` = ?, `comment` = ?, `replaygain_track_gain` = ?, `replaygain_track_peak` = ?, `replaygain_album_gain` = ?, `replaygain_album_peak` = ?, `r128_track_gain` = ?, `r128_album_gain` = ? WHERE `song_id` = ?";
        Dba::write($sql, array($new_song->label, $new_song->lyrics, $new_song->language, $new_song->disksubtitle, $new_song->comment, $new_song->replaygain_track_gain, $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));
    }

    /**
     * update_year
     * update the year tag
     * @param int $new_year
     * @param int $song_id
     */
    public static function update_year($new_year, $song_id): void
    {
        self::_update_item('year', $new_year, $song_id, 50, true);
    }

    /**
     * update_label
     * This updates the label tag of the song
     * @param string $new_value
     * @param int $song_id
     */
    public static function update_label($new_value, $song_id): void
    {
        self::_update_ext_item('label', $new_value, $song_id, 50, true);
    }

    /**
     * update_language
     * This updates the language tag of the song
     * @param string $new_lang
     * @param int $song_id
     */
    public static function update_language($new_lang, $song_id): void
    {
        self::_update_ext_item('language', $new_lang, $song_id, 50, true);
    }

    /**
     * update_comment
     * updates the comment field
     * @param string $new_comment
     * @param int $song_id
     */
    public static function update_comment($new_comment, $song_id): void
    {
        self::_update_ext_item('comment', $new_comment, $song_id, 50, true);
    }

    /**
     * update_lyrics
     * updates the lyrics field
     * @param string $new_lyrics
     * @param int $song_id
     */
    public static function update_lyrics($new_lyrics, $song_id): void
    {
        self::_update_ext_item('lyrics', $new_lyrics, $song_id, 50, true);
    }

    /**
     * update_title
     * updates the title field
     * @param string $new_title
     * @param int $song_id
     */
    public static function update_title($new_title, $song_id): void
    {
        self::_update_item('title', $new_title, $song_id, 50, true);
    }

    /**
     * update_composer
     * updates the composer field
     * @param string $new_composer
     * @param int $song_id
     */
    public static function update_composer($new_composer, $song_id): void
    {
        self::_update_item('composer', $new_composer, $song_id, 50, true);
    }

    /**
     * update_publisher
     * updates the publisher field
     * @param string $new_publisher
     * @param int $song_id
     */
    public static function update_publisher($new_publisher, $song_id): void
    {
        self::_update_item('publisher', $new_publisher, $song_id, 50, true);
    }

    /**
     * update_bitrate
     * updates the bitrate field
     * @param int $new_bitrate
     * @param int $song_id
     */
    public static function update_bitrate($new_bitrate, $song_id): void
    {
        self::_update_item('bitrate', $new_bitrate, $song_id, 50, true);
    }

    /**
     * update_rate
     * updates the rate field
     * @param int $new_rate
     * @param int $song_id
     */
    public static function update_rate($new_rate, $song_id): void
    {
        self::_update_item('rate', $new_rate, $song_id, 50, true);
    }

    /**
     * update_mode
     * updates the mode field
     * @param string $new_mode
     * @param int $song_id
     */
    public static function update_mode($new_mode, $song_id): void
    {
        self::_update_item('mode', $new_mode, $song_id, 50, true);
    }

    /**
     * update_size
     * updates the size field
     * @param int $new_size
     * @param int $song_id
     */
    public static function update_size($new_size, $song_id): void
    {
        self::_update_item('size', $new_size, $song_id, 50);
    }

    /**
     * update_time
     * updates the time field
     * @param int $new_time
     * @param int $song_id
     */
    public static function update_time($new_time, $song_id): void
    {
        self::_update_item('time', $new_time, $song_id, 50, true);
    }

    /**
     * update_track
     * this updates the track field
     * @param int $new_track
     * @param int $song_id
     */
    public static function update_track($new_track, $song_id): void
    {
        self::_update_item('track', $new_track, $song_id, 50, true);
    }

    /**
     * update_mbid
     * updates mbid field
     * @param string $new_mbid
     * @param int $song_id
     */
    public static function update_mbid($new_mbid, $song_id): void
    {
        self::_update_item('mbid', $new_mbid, $song_id, 50);
    }

    /**
     * update_license
     * updates license field
     * @param int|null $new_license
     * @param int $song_id
     */
    public static function update_license($new_license, $song_id): void
    {
        self::_update_item('license', $new_license, $song_id, 50, true);
    }

    /**
     * update_artist
     * updates the artist field
     * @param int $new_artist
     * @param int $song_id
     * @param int|null $old_artist
     * @param bool $update_counts
     */
    public static function update_artist($new_artist, $song_id, $old_artist, $update_counts = true): bool
    {
        if ($old_artist != $new_artist) {
            if (self::_update_item('artist', $new_artist, $song_id, 50) !== false) {
                if ($update_counts && $old_artist) {
                    self::migrate_artist($new_artist, $old_artist);
                    Artist::update_table_counts();
                }

                return true;
            }
        }

        return false;
    }

    /**
     * update_album
     * updates the album field
     * @param int $new_album
     * @param int $song_id
     * @param int $old_album
     * @param bool $update_counts
     */
    public static function update_album($new_album, $song_id, $old_album, $update_counts = true): bool
    {
        if ($old_album != $new_album) {
            if (self::_update_item('album', $new_album, $song_id, 50, true) !== false) {
                self::migrate_album($new_album, $song_id, $old_album);
                if ($update_counts) {
                    Album::update_table_counts();
                }

                return true;
            }
        }

        return false;
    }

    /**
     * update_utime
     * sets a new update time
     * @param int $song_id
     * @param int $time
     */
    public static function update_utime($song_id, $time = 0): void
    {
        if (!$time) {
            $time = time();
        }

        $sql = "UPDATE `song` SET `update_time` = ? WHERE `id` = ?;";
        Dba::write($sql, array($time, $song_id));
    }

    /**
     * update_played
     * sets the played flag
     * @param bool $new_played
     * @param int $song_id
     */
    public static function update_played($new_played, $song_id): void
    {
        self::_update_item('played', ($new_played ? 1 : 0), $song_id, 25);
    }

    /**
     * update_enabled
     * sets the enabled flag
     * @param bool $new_enabled
     * @param int $song_id
     */
    public static function update_enabled($new_enabled, $song_id): void
    {
        self::_update_item('enabled', ($new_enabled ? 1 : 0), $song_id, 75, true);
    }

    /**
     * _update_item
     * This is a private function that should only be called from within the song class.
     * It takes a field, value song id and level. first and foremost it checks the level
     * against Core::get_global('user') to make sure they are allowed to update this record
     * it then updates it and sets $this->{$field} to the new value
     * @param string $field
     * @param string|int|null $value
     * @param int $song_id
     * @param int $level
     * @param bool $check_owner
     * @return PDOStatement|bool
     */
    private static function _update_item($field, $value, $song_id, $level, $check_owner = false)
    {
        if ($check_owner && !empty(Core::get_global('user'))) {
            $item = new Song($song_id);
            if (isset($item->id) && $item->get_user_owner() == Core::get_global('user')->id) {
                $level = 25;
            }
        }
        /* Check them Rights! */
        if (!Access::check('interface', $level)) {
            return false;
        }

        /* Can't update to blank */
        if (!strlen(trim((string)$value)) && $field != 'comment') {
            return false;
        }

        $sql = "UPDATE `song` SET `$field` = ? WHERE `id` = ?";

        return Dba::write($sql, array($value, $song_id));
    }

    /**
     * _update_ext_item
     * This updates a song record that is housed in the song_ext_info table
     * These are items that aren't used normally, and often large/informational only
     * @param string $field
     * @param string $value
     * @param int $song_id
     * @param int $level
     * @param bool $check_owner
     * @return PDOStatement|bool
     */
    private static function _update_ext_item($field, $value, $song_id, $level, $check_owner = false)
    {
        if ($check_owner) {
            $item = new Song($song_id);
            if ($item->id && $item->get_user_owner() == Core::get_global('user')->id) {
                $level = 25;
            }
        }

        /* Check them rights boy! */
        if (!Access::check('interface', $level)) {
            return false;
        }

        $sql = "UPDATE `song_data` SET `$field` = ? WHERE `song_id` = ?";

        return Dba::write($sql, array($value, $song_id));
    }

    /**
     * format
     * This takes the current song object
     * and does a ton of formatting on it creating f_??? variables on the current
     * object
     *
     * @param bool $details
     */
    public function format($details = true): void
    {
        if ($this->isNew()) {
            return;
        }
        if ($details) {
            $this->fill_ext_info();

            // Get the top tags
            $this->tags        = Tag::get_top_tags('song', $this->id);
            $this->f_tags      = Tag::get_display($this->tags, true, 'song');
            $this->f_publisher = $this->label ?? null;
        }

        if (!isset($this->artists)) {
            $this->get_artists();
        }
        if (!isset($this->albumartists)) {
            $this->albumartists = self::get_parent_array($this->album, 'album');
        }
        $this->albumartist = $this->getAlbumRepository()->getAlbumArtistId($this->album);

        // Format the album name
        $this->f_album_full = $this->get_album_fullname();
        $this->f_album      = $this->f_album_full;

        // Format the artist name
        $this->f_artist = $this->get_artist_fullname();

        // Format the album_artist name
        $this->f_albumartist_full = $this->get_album_artist_fullname();

        // Format the title
        $this->f_name_full = $this->get_fullname();

        // Create Links for the different objects
        $this->get_f_link();
        $this->get_f_artist_link();
        $this->get_f_albumartist_link();
        $this->get_f_album_link();

        // Format the Bitrate
        $this->f_bitrate = (int)($this->bitrate / 1024) . "-" . strtoupper((string)$this->mode);

        // Format the Time
        $min            = floor($this->time / 60);
        $sec            = sprintf("%02d", ($this->time % 60));
        $this->f_time   = $min . ":" . $sec;
        $hour           = sprintf("%02d", floor($min / 60));
        $min_h          = sprintf("%02d", ($min % 60));
        $this->f_time_h = $hour . ":" . $min_h . ":" . $sec;

        // Format the track (there isn't really anything to do here)
        $this->f_track = (string)$this->track;

        // Format the size
        $this->f_size = Ui::format_bytes($this->size);

        $web_path       = AmpConfig::get('web_path');
        $this->f_lyrics = "<a title=\"" . scrub_out($this->title) . "\" href=\"" . $web_path . "/song.php?action=show_lyrics&song_id=" . $this->id . "\">" . T_('Show Lyrics') . "</a>";

        $this->f_composer  = $this->composer;

        $year              = (int)$this->year;
        $this->f_year_link = "<a href=\"" . $web_path . "/search.php?type=album&action=search&limit=0&rule_1=year&rule_1_operator=2&rule_1_input=" . $year . "\">" . $year . "</a>";
    }

    /**
     * Returns the filename of the media-item
     */
    public function getFileName(): string
    {
        $value = $this->get_artist_fullname() . ' - ';
        if ($this->track) {
            $value .= $this->track . ' - ';
        }
        $value .= $this->get_fullname() . '.' . $this->type;

        return $value;
    }

    /**
     * does the item have art?
     */
    public function has_art(): bool
    {
        if ($this->has_art === null) {
            $this->has_art = (AmpConfig::get('show_song_art', false) && Art::has_db($this->id, 'song') || Art::has_db($this->album, 'album'));
        }

        return $this->has_art;
    }

    /**
     * Get item keywords for metadata searches.
     * @return array
     */
    public function get_keywords(): array
    {
        $keywords               = array();
        $keywords['mb_trackid'] = array(
            'important' => false,
            'label' => T_('Track MusicBrainzID'),
            'value' => $this->mbid
        );
        $keywords['artist'] = array(
            'important' => true,
            'label' => T_('Artist'),
            'value' => $this->f_artist
        );
        $keywords['title'] = array(
            'important' => true,
            'label' => T_('Title'),
            'value' => $this->get_fullname()
        );

        return $keywords;
    }

    /**
     * Get total count
     */
    public function get_totalcount(): int
    {
        return $this->total_count;
    }

    /**
     * Get item fullname.
     */
    public function get_fullname(): ?string
    {
        if (!isset($this->f_name)) {
            $this->f_name = $this->title;
        }

        return $this->f_name;
    }

    /**
     * Get item link.
     */
    public function get_link(): string
    {
        // don't do anything if it's formatted
        if ($this->link === null) {
            $web_path   = AmpConfig::get('web_path');
            $this->link = $web_path . "/song.php?action=show_song&song_id=" . $this->id;
        }

        return $this->link;
    }

    /**
     * Get item f_link.
     */
    public function get_f_link(): string
    {
        // don't do anything if it's formatted
        if (!isset($this->f_link)) {
            $this->f_link = "<a href=\"" . scrub_out($this->get_link()) . "\" title=\"" . scrub_out($this->get_artist_fullname()) . " - " . scrub_out($this->get_fullname()) . "\"> " . scrub_out($this->get_fullname()) . "</a>";
        }

        return $this->f_link;
    }

    /**
     * Get item album_artists array
     * @return array
     */
    public function get_artists(): array
    {
        if (!isset($this->artists)) {
            $this->artists = self::get_parent_array($this->id);
        }

        return $this->artists;
    }

    /**
     * Get item f_artist_link.
     */
    public function get_f_artist_link(): ?string
    {
        // don't do anything if it's formatted
        if (!isset($this->f_artist_link)) {
            $this->f_artist_link = '';
            $web_path            = AmpConfig::get('web_path');
            if (!isset($this->artists)) {
                $this->get_artists();
            }
            foreach ($this->artists as $artist_id) {
                $artist_fullname = scrub_out(Artist::get_fullname_by_id($artist_id));
                $this->f_artist_link .= "<a href=\"" . $web_path . "/artists.php?action=show&artist=" . $artist_id . "\" title=\"" . $artist_fullname . "\">" . $artist_fullname . "</a>,&nbsp";
            }
            $this->f_artist_link = rtrim($this->f_artist_link, ",&nbsp");
        }

        return $this->f_artist_link;
    }

    /**
     * Get item f_albumartist_link.
     */
    public function get_f_albumartist_link(): string
    {
        // don't do anything if it's formatted
        if (!isset($this->f_albumartist_link)) {
            $this->f_albumartist_link = '';
            $web_path                 = AmpConfig::get('web_path');
            if (!isset($this->albumartists)) {
                $this->albumartists = self::get_parent_array($this->album, 'album');
            }
            foreach ($this->albumartists as $artist_id) {
                $artist_fullname = scrub_out(Artist::get_fullname_by_id($artist_id));
                $this->f_albumartist_link .= "<a href=\"" . $web_path . '/artists.php?action=show&artist=' . $artist_id . "\" title=\"" . $artist_fullname . "\">" . $artist_fullname . "</a>,&nbsp";
            }
            $this->f_albumartist_link = rtrim($this->f_albumartist_link, ",&nbsp");
        }

        return $this->f_albumartist_link;
    }

    /**
     * Get item get_f_album_link.
     */
    public function get_f_album_link(): string
    {
        // don't do anything if it's formatted
        if (!isset($this->f_album_link)) {
            $this->f_album_link = '';
            $web_path           = AmpConfig::get('web_path');
            $this->f_album_link = "<a href=\"" . $web_path . "/albums.php?action=show&album=" . $this->album . "\" title=\"" . scrub_out($this->get_album_fullname()) . "\"> " . scrub_out($this->get_album_fullname()) . "</a>";
        }

        return $this->f_album_link;
    }

    /**
     * Get item get_f_album_disk_link.
     */
    public function get_f_album_disk_link(): string
    {
        // don't do anything if it's formatted
        if (!isset($this->f_album_disk_link)) {
            $this->f_album_disk_link = '';
            $web_path                = AmpConfig::get('web_path');
            $this->f_album_disk_link = "<a href=\"" . $web_path . "/albums.php?action=show_disk&album_disk=" . $this->get_album_disk() . "\" title=\"" . scrub_out($this->get_album_disk_fullname()) . "\"> " . scrub_out($this->get_album_disk_fullname()) . "</a>";
        }

        return $this->f_album_disk_link;
    }

    /**
     * get_parent
     * Return parent `object_type`, `object_id`; null otherwise.
     */
    public function get_parent(): ?array
    {
        return array(
            'object_type' => 'album',
            'object_id' => $this->album
        );
    }

    /**
     * Get parent song artists.
     * @param int $object_id
     * @return int[]
     */
    public static function get_parent_array($object_id, $type = 'artist'): array
    {
        $results = array();
        $sql     = ($type == 'album')
            ? "SELECT DISTINCT `object_id` FROM `album_map` WHERE `object_type` = 'album' AND `album_id` = ?;"
            : "SELECT DISTINCT `artist_id` AS `object_id` FROM `artist_map` WHERE `object_type` = 'song' AND `object_id` = ?;";
        $db_results = Dba::read($sql, array($object_id));

        while ($row = Dba::fetch_assoc($db_results)) {
            $results[] = (int)$row['object_id'];
        }

        return $results;
    }

    /**
     * Get item children.
     * @return array
     */
    public function get_childrens(): array
    {
        return array();
    }

    /**
     * Search for direct children of an object
     * @param string $name
     * @return array
     */
    public function get_children($name): array
    {
        debug_event(self::class, 'get_children ' . $name, 5);

        return array();
    }

    /**
     * Get all childrens and sub-childrens medias.
     *
     * @return list<array{object_type: string, object_id: int}>
     */
    public function get_medias(?string $filter_type = null): array
    {
        $medias = array();
        if ($filter_type === null || $filter_type === 'song') {
            $medias[] = array(
                'object_type' => 'song',
                'object_id' => $this->id
            );
        }

        return $medias;
    }

    /**
     * Returns the id of the catalog the item is associated to
     */
    public function getCatalogId(): int
    {
        return $this->catalog;
    }

    /**
     * Get item's owner.
     * @return int|null
     */
    public function get_user_owner(): ?int
    {
        if ($this->user_upload !== null) {
            return $this->user_upload;
        }

        return null;
    }

    /**
     * Get default art kind for this item.
     */
    public function get_default_art_kind(): string
    {
        return 'default';
    }

    /**
     * get_description
     */
    public function get_description(): string
    {
        if (!empty($this->comment)) {
            return $this->comment;
        }

        $album = new Album($this->album);
        $album->format();

        return $album->get_description();
    }

    /**
     * display_art
     * @param int $thumb
     * @param bool $force
     */
    public function display_art($thumb = 2, $force = false): void
    {
        $object_id = null;
        $type      = null;

        if (Art::has_db($this->id, 'song')) {
            $object_id = $this->id;
            $type      = 'song';
        } else {
            if (Art::has_db($this->album, 'album')) {
                $object_id = $this->album;
                $type      = 'album';
            } else {
                if (($this->artist && Art::has_db($this->artist, 'artist')) || $force) {
                    $object_id = $this->artist;
                    $type      = 'artist';
                }
            }
        }

        if ($object_id !== null && $type !== null) {
            Art::display($type, $object_id, (string)$this->get_fullname(), $thumb, $this->get_link());
        }
    }

    /**
     * get_fields
     * This returns all of the 'data' fields for this object, we need to filter out some that we don't
     * want to present to a user, and add some that don't exist directly on the object but are related
     * @return array
     */
    public static function get_fields(): array
    {
        $fields = get_class_vars(Song::class);

        unset($fields['id'], $fields['_transcoded'], $fields['_fake'], $fields['cache_hit'], $fields['mime'], $fields['type']);

        // Some additional fields
        $fields['tag']     = true;
        $fields['catalog'] = true;
        // FIXME: These are here to keep the ideas, don't want to have to worry about them for now
        // $fields['rating'] = true;
        // $fields['recently Played'] = true;

        return $fields;
    }

    /**
     * play_url
     * This function takes all the song information and correctly formats a
     * stream URL taking into account the downsampling mojo and everything
     * else, this is the true function
     * @param string $additional_params
     * @param string $player
     * @param bool $local
     * @param int|string $uid
     * @param null|string $streamToken
     */
    public function play_url($additional_params = '', $player = '', $local = false, $uid = false, $streamToken = null): string
    {
        if ($this->isNew()) {
            return '';
        }
        if (!$uid) {
            // No user in the case of upnp. Set to 0 instead. required to fix database insertion errors
            $uid = Core::get_global('user')->id ?? 0;
        }
        // set no use when using auth
        if (!AmpConfig::get('use_auth') && !AmpConfig::get('require_session')) {
            $uid = -1;
        }
        $downsample_remote = false;
        // enforce or disable transcoding depending on local network ACL
        if (AmpConfig::get('downsample_remote') && !$this->getNetworkChecker()->check(AccessLevelEnum::TYPE_NETWORK, $uid, AccessLevelEnum::LEVEL_DEFAULT)) {
            $downsample_remote = true;
            debug_event(self::class, "Transcoding due to downsample_remote", 3);
        }

        // if you transcode the media mime will change
        if (
            AmpConfig::get('transcode') != 'never' &&
            (
                $downsample_remote ||
                empty($additional_params) ||
                (
                    !strpos($additional_params, '&bitrate=') &&
                    !strpos($additional_params, '&format=')
                )
            )
        ) {
            $cache_path     = (string)AmpConfig::get('cache_path', '');
            $cache_target   = (string)AmpConfig::get('cache_target', '');
            $file_target    = Catalog::get_cache_path($this->id, $this->catalog, $cache_path, $cache_target);
            $bitrate        = (int)AmpConfig::get('transcode_bitrate', 128) * 1000;
            $transcode_type = ($file_target !== null && is_file($file_target))
                ? $cache_target
                : Stream::get_transcode_format($this->type, null, $player);
            if (
                !empty($transcode_type) &&
                ($this->type !== $transcode_type || $bitrate < $this->bitrate)
            ) {
                $this->type    = $transcode_type;
                $this->mime    = self::type_to_mime($transcode_type);
                $this->bitrate = $bitrate;

                // replace duplicate/incorrect parameters on the additional params
                $patterns = array(
                    '/&format=[a-z]+/',
                    '/&transcode_to=[a-z|0-9]+/',
                    '/&bitrate=[0-9]+/',
                );
                $additional_params = preg_replace($patterns, '', $additional_params);
                $additional_params .= '&transcode_to=' . $transcode_type . '&bitrate=' . $bitrate;
            }
        }

        $media_name = $this->get_stream_name() . "." . $this->type;
        $media_name = (string)preg_replace("/[^a-zA-Z0-9\. ]+/", "-", $media_name);
        $media_name = (AmpConfig::get('stream_beautiful_url'))
            ? urlencode($media_name)
            : rawurlencode($media_name);

        $url = Stream::get_base_url($local, $streamToken) . "type=song&oid=" . $this->id . "&uid=" . (string) $uid . $additional_params;
        if ($player !== '') {
            $url .= "&player=" . $player;
        }
        $url .= "&name=" . $media_name;

        return Stream_Url::format($url);
    }

    /**
     * Get stream name.
     */
    public function get_stream_name(): string
    {
        return (string)($this->get_artist_fullname() . " - " . $this->title);
    }

    /**
     * Get stream types.
     * @param string $player
     * @return array
     */
    public function get_stream_types($player = null): array
    {
        return Stream::get_stream_types_for_type($this->type, $player);
    }

    /**
     * Get transcode settings.
     * @param string $target
     * @param string $player
     * @param array $options
     * @return array
     */
    public function get_transcode_settings($target = null, $player = null, $options = array()): array
    {
        return Stream::get_transcode_settings_for_media($this->type, $target, $player, 'song', $options);
    }

    /**
     * getYear
     */
    public function getYear(): string
    {
        return (string)($this->year ?: '');
    }

    /**
     * Get lyrics.
     * @return array
     */
    public function get_lyrics(): array
    {
        if ($this->lyrics) {
            return array('text' => $this->lyrics);
        }

        foreach (Plugin::get_plugins('get_lyrics') as $plugin_name) {
            $plugin = new Plugin($plugin_name);
            if ($plugin->_plugin !== null && $plugin->load(Core::get_global('user'))) {
                $lyrics = $plugin->_plugin->get_lyrics($this);
                if ($lyrics) {
                    // save the lyrics if not set before
                    if (array_key_exists('text', $lyrics) && !empty($lyrics['text'])) {
                        self::update_lyrics($lyrics['text'], $this->id);
                    }

                    return $lyrics;
                }
            }
        }

        return array();
    }

    /**
     * Run custom play action.
     * @param int $action_index
     * @param string $codec
     * @return array
     */
    public function run_custom_play_action($action_index, $codec = ''): array
    {
        $transcoder = array();
        $actions    = self::get_custom_play_actions();
        if ($action_index <= count($actions)) {
            $action = $actions[$action_index - 1];
            if (!$codec) {
                $codec = $this->type;
            }

            $run = str_replace("%f", $this->file ?? '%f', $action['run']);
            $run = str_replace("%c", $codec, $run);
            $run = str_replace("%a", $this->f_artist ?? '%a', $run);
            $run = str_replace("%A", $this->f_album ?? '%A', $run);
            $run = str_replace("%t", $this->get_fullname() ?? '%t', $run);

            debug_event(self::class, "Running custom play action: " . $run, 3);

            $descriptors = array(1 => array('pipe', 'w'));
            if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
                // Windows doesn't like to provide stderr as a pipe
                $descriptors[2] = array('pipe', 'w');
            }
            $process = proc_open($run, $descriptors, $pipes);

            $transcoder['process'] = $process;
            $transcoder['handle']  = $pipes[1];
            $transcoder['stderr']  = $pipes[2];
            $transcoder['format']  = $codec;
        }

        return $transcoder;
    }

    /**
     * Get custom play actions.
     * @return array
     */
    public static function get_custom_play_actions(): array
    {
        $actions = array();
        $count   = 0;
        while (AmpConfig::get('custom_play_action_title_' . $count)) {
            $actions[] = array(
                'index' => ($count + 1),
                'title' => AmpConfig::get('custom_play_action_title_' . $count),
                'icon' => AmpConfig::get('custom_play_action_icon_' . $count),
                'run' => AmpConfig::get('custom_play_action_run_' . $count),
            );
            ++$count;
        }

        return $actions;
    }

    /**
     * Update Metadata from array
     * @param array<string, scalar> $meta_value
     */
    public function updateMetadata(array $meta_value): void
    {
        if ($this->getMetadataManager()->isCustomMetadataEnabled()) {
            $metadataRepository = $this->getMetadataRepository();

            foreach ($meta_value as $metadataId => $value) {
                $metadata = $metadataRepository->findById((int) $metadataId);
                if ($metadata && $value !== $metadata->getData()) {
                    $metadata->setData((string) $value);
                    $metadata->save();
                }
            }
        }
    }

    /**
     * get_deleted
     * get items from the deleted_songs table
     * @return int[]
     */
    public static function get_deleted(): array
    {
        $deleted    = array();
        $sql        = "SELECT * FROM `deleted_song`";
        $db_results = Dba::read($sql);
        while ($row = Dba::fetch_assoc($db_results)) {
            $deleted[] = $row;
        }

        return $deleted;
    }

    /**
     * Migrate an artist data to a new object
     * @param int $new_artist
     * @param int $old_artist
     */
    public static function migrate_artist($new_artist, $old_artist): bool
    {
        if ($old_artist != $new_artist) {
            // migrate stats for the old artist
            Useractivity::migrate('artist', $old_artist, $new_artist);
            Recommendation::migrate('artist', $old_artist);
            self::getShareRepository()->migrate('artist', $old_artist, $new_artist);
            self::getShoutRepository()->migrate('artist', $old_artist, $new_artist);
            Tag::migrate('artist', $old_artist, $new_artist);
            Userflag::migrate('artist', $old_artist, $new_artist);
            Rating::migrate('artist', $old_artist, $new_artist);
            Art::duplicate('artist', $old_artist, $new_artist);
            self::getWantedRepository()->migrateArtist($old_artist, $new_artist);
            Catalog::migrate_map('artist', $old_artist, $new_artist);
            // update mapping tables
            $sql = "UPDATE IGNORE `album_map` SET `object_id` = ? WHERE `object_id` = ?";
            if (Dba::write($sql, array($new_artist, $old_artist)) === false) {
                return false;
            }
            $sql = "UPDATE IGNORE `artist_map` SET `artist_id` = ? WHERE `artist_id` = ?";
            if (Dba::write($sql, array($new_artist, $old_artist)) === false) {
                return false;
            }
            $sql = "UPDATE IGNORE `catalog_map` SET `object_id` = ? WHERE `object_type` = ? AND `object_id` = ?";
            if (Dba::write($sql, array($new_artist, 'artist', $old_artist)) === false) {
                return false;
            }
            // delete leftovers duplicate maps
            $sql = "DELETE FROM `album_map` WHERE `object_id` = ?";
            Dba::write($sql, array($old_artist));
            $sql = "DELETE FROM `artist_map` WHERE `artist_id` = ?";
            Dba::write($sql, array($old_artist));
            $sql = "DELETE FROM `catalog_map` WHERE `object_type` = ? AND `object_id` = ?";
            Dba::write($sql, array('artist', $old_artist));
        }

        return true;
    }

    /**
     * Migrate an album data to a new object
     * @param int $new_album
     * @param int $song_id
     * @param int $old_album
     */
    public static function migrate_album($new_album, $song_id, $old_album): bool
    {
        // migrate stats for the old album
        Stats::migrate('album', $old_album, $new_album, $song_id);
        Useractivity::migrate('album', $old_album, $new_album);
        //Recommendation::migrate('album', $old_album);
        self::getShareRepository()->migrate('album', $old_album, $new_album);
        self::getShoutRepository()->migrate('album', $old_album, $new_album);
        Tag::migrate('album', $old_album, $new_album);
        Userflag::migrate('album', $old_album, $new_album);
        Rating::migrate('album', $old_album, $new_album);
        Art::duplicate('album', $old_album, $new_album);
        Catalog::migrate_map('album', $old_album, $new_album);

        // update mapping tables
        $sql = "UPDATE IGNORE `album_disk` SET `album_id` = ? WHERE `album_id` = ?";
        if (Dba::write($sql, array($new_album, $old_album)) === false) {
            return false;
        }
        if ($song_id > 0) {
            $sql = "UPDATE IGNORE `album_map` SET `album_id` = ? WHERE `album_id` = ? AND `object_id` = ? AND `object_type` = 'song'";
            if (Dba::write($sql, array($new_album, $old_album, $song_id)) === false) {
                return false;
            }
        } else {
            $sql = "UPDATE IGNORE `album_map` SET `album_id` = ? WHERE `album_id` = ? AND `object_type` = 'song'";
            if (Dba::write($sql, array($new_album, $old_album)) === false) {
                return false;
            }
        }
        $sql = "UPDATE IGNORE `artist_map` SET `object_id` = ? WHERE `object_type` = ? AND `object_id` = ?";
        if (Dba::write($sql, array($new_album, 'album', $old_album)) === false) {
            return false;
        }
        $sql = "UPDATE IGNORE `catalog_map` SET `object_id` = ? WHERE `object_type` = ? AND `object_id` = ?";
        if (Dba::write($sql, array($new_album, 'album', $old_album)) === false) {
            return false;
        }
        // delete leftovers duplicate maps
        $sql = "DELETE FROM `album_disk` WHERE `album_id` = ?";
        Dba::write($sql, array($old_album));
        $sql = "DELETE FROM `album_map` WHERE `album_id` = ?";
        Dba::write($sql, array($old_album));
        $sql = "DELETE FROM `artist_map` WHERE `object_type` = ? AND `object_id` = ?";
        Dba::write($sql, array('album', $old_album));
        $sql = "DELETE FROM `catalog_map` WHERE `object_type` = ? AND `object_id` = ?";
        Dba::write($sql, array('album', $old_album));

        return true;
    }

    /**
     * Returns the available metadata for this object
     *
     * @return Traversable<Metadata>
     */
    public function getMetadata(): Traversable
    {
        return $this->getMetadataManager()->getMetadata($this);
    }

    /**
     * remove
     * Delete the object from disk and/or database where applicable.
     */
    public function remove(): bool
    {
        return $this->getSongDeleter()->delete($this);
    }

    public function getLicense(): ?License
    {
        if (
            AmpConfig::get('licensing') &&
            $this->licenseObj === null &&
            $this->license !== null
        ) {
            $this->licenseObj = $this->getLicenseRepository()->findById($this->license);
        }

        return $this->licenseObj;
    }

    /**
     * Returns the metadata object-type
     */
    public function getMetadataItemType(): string
    {
        return 'song';
    }

    /**
     * @return list<string>
     */
    public function getIgnoredMetadataKeys(): array
    {
        return [
            'mb_trackid',
            'mbid',
            'mb_albumid',
            'mb_albumid_group',
            'mb_artistid',
            'mb_albumartistid',
            'genre',
            'publisher'
        ];
    }

    /**
     * @deprecated
     */
    private function getSongTagWriter(): SongTagWriterInterface
    {
        global $dic;

        return $dic->get(SongTagWriterInterface::class);
    }

    /**
     * @deprecated
     */
    private function getNetworkChecker(): NetworkCheckerInterface
    {
        global $dic;

        return $dic->get(NetworkCheckerInterface::class);
    }

    /**
     * @deprecated
     */
    private function getSongDeleter(): SongDeleterInterface
    {
        global $dic;

        return $dic->get(SongDeleterInterface::class);
    }

    /**
     * @deprecated inject dependency
     */
    private static function getUserActivityPoster(): UserActivityPosterInterface
    {
        global $dic;

        return $dic->get(UserActivityPosterInterface::class);
    }

    /**
     * @deprecated inject dependency
     */
    private static function getShoutRepository(): ShoutRepositoryInterface
    {
        global $dic;

        return $dic->get(ShoutRepositoryInterface::class);
    }

    /**
     * @deprecated inject dependency
     */
    private function getAlbumRepository(): AlbumRepositoryInterface
    {
        global $dic;

        return $dic->get(AlbumRepositoryInterface::class);
    }

    /**
     * @deprecated inject dependency
     */
    private static function getShareRepository(): ShareRepositoryInterface
    {
        global $dic;

        return $dic->get(ShareRepositoryInterface::class);
    }

    /**
     * @deprecated inject dependency
     */
    private function getLicenseRepository(): LicenseRepositoryInterface
    {
        global $dic;

        return $dic->get(LicenseRepositoryInterface::class);
    }

    /**
     * @deprecated inject dependency
     */
    private function getMetadataRepository(): MetadataRepositoryInterface
    {
        global $dic;

        return $dic->get(MetadataRepositoryInterface::class);
    }

    /**
     * @deprecated inject dependency
     */
    private function getMetadataManager(): MetadataManagerInterface
    {
        global $dic;

        return $dic->get(MetadataManagerInterface::class);
    }

    /**
     * @deprecated inject dependency
     */
    private static function getWantedRepository(): WantedRepositoryInterface
    {
        global $dic;

        return $dic->get(WantedRepositoryInterface::class);
    }
}