ampache/ampache

View on GitHub
src/Module/Song/Tag/SongTagWriter.php

Summary

Maintainability
F
4 days
Test Coverage
<?php

/**
 * 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\Module\Song\Tag;

use Ampache\Config\ConfigContainerInterface;
use Ampache\Config\ConfigurationKeyEnum;
use Ampache\Repository\Model\Album;
use Ampache\Repository\Model\Art;
use Ampache\Repository\Model\Catalog;
use Ampache\Repository\Model\Rating;
use Ampache\Repository\Model\Song;
use Ampache\Repository\Model\User;
use Ampache\Module\System\Core;
use Ampache\Module\System\LegacyLogger;
use Ampache\Module\Util\UtilityFactoryInterface;
use Psr\Log\LoggerInterface;

final class SongTagWriter implements SongTagWriterInterface
{
    private ConfigContainerInterface $configContainer;

    private UtilityFactoryInterface $utilityFactory;

    private LoggerInterface $logger;

    public function __construct(
        ConfigContainerInterface $configContainer,
        UtilityFactoryInterface $utilityFactory,
        LoggerInterface $logger
    ) {
        $this->configContainer = $configContainer;
        $this->utilityFactory  = $utilityFactory;
        $this->logger          = $logger;
    }

    /**
     * Write the current song id3 metadata to the file
     */
    public function write(
        Song $song
    ): void {
        if ($this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::WRITE_TAGS) === false) {
            return;
        }

        $catalog = Catalog::create_from_id($song->getCatalogId());
        if ($catalog === null) {
            return;
        }
        if ($catalog->get_type() == 'local') {
            $this->logger->debug(
                sprintf('Writing metadata to file %s', $song->file),
                [LegacyLogger::CONTEXT_TYPE => __CLASS__]
            );

            $ndata = array();
            if ($this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::ENABLE_CUSTOM_METADATA) === true) {
                foreach ($song->getMetadata() as $metadata) {
                    $field = $metadata->getField();

                    if ($field !== null) {
                        $ndata[$field->getName()] = $metadata->getData();
                    }
                }
            }
            $vainfo = $this->utilityFactory->createVaInfo(
                (string) $song->file
            );

            $result     = $vainfo->read_id3();
            $fileformat = $result['fileformat'];

            $song->format();
            if ($fileformat == 'mp3') {
                $songMeta  = $this->getId3Metadata($song);
                $txxxData  = $result['id3v2']['comments']['text'] ?? array();
                $id3v2Data = $result['tags']['id3v2'] ?? array();
                $apics     = $result['id3v2']['APIC'] ?? null;
                // Update existing file frames.
                if (!empty($txxxData)) {
                    foreach ($txxxData as $key => $value) {
                        $idx = $this->search_txxx($key, $songMeta['text']);
                        if ($idx) {
                            $ndata['text'][] = array(
                                'data' => $songMeta['text'][$idx]['data'],
                                'description' => $key,
                                'encodingid' => 0
                            );
                        } else {
                            $ndata['text'][] = array(
                                'data' => $value,
                                'description' => $key,
                                'encodingid' => 0
                            );
                        }
                    }
                } else {
                    // Assumes file originally had no TXXX frames
                    $metatext = $songMeta['text'];
                    if (!empty($metatext)) {
                        foreach ($metatext as $key => $value) {
                            $ndata['text'][$key] = $value;
                        }
                    }
                }
                if (!empty($id3v2Data)) {
                    unset($id3v2Data['text']);
                    foreach ($id3v2Data as $key => $value) {
                        if (isset($songMeta[$key]) && $value[0] !== $songMeta[$key]) {
                            $ndata[$key][] = $songMeta[$key];
                        } else {
                            $ndata[$key][] = $value[0];
                        }
                    }
                } else {
                    unset($songMeta['text']);
                    foreach ($songMeta as $key => $value) {
                        $ndata[$key][] = $songMeta;
                    }
                }
                if (isset($songMeta['unique_file_identifier'])) {
                    $ndata['unique_file_identifier'] = $songMeta['unique_file_identifier'];
                }
                if (isset($songMeta['Popularimeter'])) {
                    $ndata['Popularimeter'] = $songMeta['Popularimeter'];
                }
            } else {
                $songMeta       = $this->getVorbisMetadata($song);
                $vorbiscomments = $result['tags']['vorbiscomment'] ?? array();
                $apics          = $result['flac']['PICTURE'] ?? null;
                //  Update existing vorbiscomments
                if (!empty($vorbiscomments)) {
                    foreach ($vorbiscomments as $key => $value) {
                        if (isset($songMeta[$key])) {
                            if ($key == 'releasetype' || $key == 'releasestatus') {
                                $ndata[$key] = $songMeta[$key];
                            } else {
                                $ndata[$key][] = $songMeta[$key];
                            }
                        } else {
                            $ndata[$key] = $value;
                        }
                    }
                }
                // Insert vorbiscomments that might not be in file.
                foreach ($songMeta as $key => $value) {
                    if (!isset($vorbiscomments[$key]) && isset($value)) {
                        if ($key == 'releasetype' || $key == 'releasestatus') {
                            $ndata[$key] = $value;
                        } else {
                            $ndata[$key][] = $value;
                        }
                    }
                }
            }
            $apic_typeid = ($fileformat == 'flac' || $fileformat == 'ogg')
                ? 'typeid'
                : 'picturetypeid';
            $apic_mimetype = ($fileformat == 'flac' || $fileformat == 'ogg')
                ? 'image_mime'
                : 'mime';
            $file_has_pics = isset($apics) && is_array($apics);
            if ($file_has_pics) {
                foreach ($apics as $apic) {
                    $ndata['attached_picture'][] = array(
                        'data' => $apic['data'],
                        'mime' => $apic[$apic_mimetype],
                        'picturetypeid' => $apic[$apic_typeid],
                        'description' => $apic['description'],
                        'encodingid' => $apic['encodingid']
                    );
                }
            }

            $art = new Art($song->artist, 'artist');
            if ($art->has_db_info()) {
                $image   = $art->get(true);
                $new_pic = array(
                    'data' => $image,
                    'mime' => $art->raw_mime,
                    'picturetypeid' => 8,
                    'description' => $song->get_artist_fullname(),
                    'encodingid' => 0
                );
                if ($file_has_pics) {
                    $idx = $this->check_for_duplicate($apics, $new_pic, $ndata, $apic_typeid);
                    if (is_null($idx)) {
                        $ndata['attached_picture'][] = $new_pic;
                    }
                } else {
                    $ndata['attached_picture'][] = $new_pic;
                }
            }
            $art = new Art($song->album, 'album');
            if ($art->has_db_info()) {
                $image   = $art->get(true);
                $new_pic = array(
                    'data' => $image,
                    'mime' => $art->raw_mime,
                    'picturetypeid' => 3,
                    'description' => $song->f_album,
                    'encodingid' => 0
                );
                if ($file_has_pics) {
                    $idx = $this->check_for_duplicate($apics, $new_pic, $ndata, $apic_typeid);
                    if (is_null($idx)) {
                        $ndata['attached_picture'][] = $new_pic;
                    }
                } else {
                    $ndata['attached_picture'][] = $new_pic;
                }
            }
            $vainfo->write_id3($ndata);
        } // catalog type = local
    }

    /**
     * Write the song rating to the file and include existing tags
     */
    public function writeRating(
        Song $song,
        User $user,
        Rating $rating
    ): void {
        if ($this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::WRITE_TAGS) === false) {
            return;
        }

        $catalog = Catalog::create_from_id($song->getCatalogId());
        if ($catalog === null) {
            return;
        }
        if ($catalog->get_type() == 'local') {
            $this->logger->debug(
                sprintf('Writing rating to file %s', $song->file),
                [LegacyLogger::CONTEXT_TYPE => __CLASS__]
            );

            $vainfo = $this->utilityFactory->createVaInfo(
                (string) $song->file
            );

            $ndata      = array();
            $result     = $vainfo->read_id3();
            $fileformat = $result['fileformat'];
            $my_rating  = $rating->get_user_rating($user->id);

            if ($fileformat == 'mp3') {
                $txxxData  = $result['id3v2']['comments']['text'] ?? array();
                $id3v2Data = $result['tags']['id3v2'] ?? array();
                $apics     = $result['id3v2']['APIC'] ?? null;
                // Update existing file frames.
                if (!empty($txxxData)) {
                    foreach ($txxxData as $key => $value) {
                        $ndata['text'][] = array(
                            'data' => $value,
                            'description' => $key,
                            'encodingid' => 0
                        );
                    }
                }
                if (!empty($id3v2Data)) {
                    unset($id3v2Data['text']);
                    foreach ($id3v2Data as $key => $value) {
                        $ndata[$key][] = $value[0];
                    }
                }
                if (!empty($user->email)) {
                    $ndata['Popularimeter'] = [
                        "email" => $user->email,
                        "rating" => ($my_rating > 0) ? $my_rating * (255 / 5) : 0,
                        "data" => $song->get_totalcount()
                    ];
                    $this->logger->debug(
                        print_r($ndata['Popularimeter'], true),
                        [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                    );
                } else {
                    $this->logger->debug(
                        'Rating user must have an email address on record.',
                        [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                    );
                }
            } else {
                $vorbiscomments = $result['tags']['vorbiscomment'] ?? array();
                $apics          = $result['flac']['PICTURE'] ?? null;
                if (!empty($vorbiscomments)) {
                    // Fill existing tags
                    foreach ($vorbiscomments as $key => $value) {
                        $ndata[$key] = $value;
                    }
                }
                if (!empty($user->email)) {
                    // set a rating and per-user rating
                    $tag_rating                      = array(($my_rating > 0) ? $my_rating * (100 / 5) : 0);
                    $ndata['rating']                 = $tag_rating;
                    $ndata['rating:' . $user->email] = $tag_rating;
                } else {
                    $this->logger->debug(
                        'Rating user must have an email address on record.',
                        [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                    );
                }
            }
            $apic_typeid = ($fileformat == 'flac' || $fileformat == 'ogg')
                ? 'typeid'
                : 'picturetypeid';
            $apic_mimetype = ($fileformat == 'flac' || $fileformat == 'ogg')
                ? 'image_mime'
                : 'mime';
            $file_has_pics = isset($apics) && is_array($apics);
            if ($file_has_pics) {
                foreach ($apics as $apic) {
                    $ndata['attached_picture'][] = array(
                        'data' => $apic['data'],
                        'picturetypeid' => $apic[$apic_typeid],
                        'description' => $apic['description'] ?? '',
                        'mime' => $apic[$apic_mimetype],
                        'encodingid' => $apic['encodingid'] ?? 0
                    );
                }
            }
            $art = new Art($song->artist, 'artist');
            if ($art->has_db_info()) {
                $image   = $art->get(true);
                $new_pic = array(
                    'data' => $image,
                    'picturetypeid' => 8,
                    'description' => $song->get_artist_fullname(),
                    'mime' => $art->raw_mime,
                    'encodingid' => 0
                );
                if ($file_has_pics) {
                    $idx = $this->check_for_duplicate($apics, $new_pic, $ndata, $apic_typeid);
                    if (is_null($idx)) {
                        $ndata['attached_picture'][] = $new_pic;
                    }
                }
            }
            $art = new Art($song->album, 'album');
            if ($art->has_db_info()) {
                $image   = $art->get(true);
                $new_pic = array(
                    'data' => $image,
                    'picturetypeid' => 3,
                    'description' => $song->f_album,
                    'mime' => $art->raw_mime,
                    'encodingid' => 0
                );
                if ($file_has_pics) {
                    $idx = $this->check_for_duplicate($apics, $new_pic, $ndata, $apic_typeid);
                    if (is_null($idx)) {
                        $ndata['attached_picture'][] = $new_pic;
                    }
                }
            }
            $vainfo->write_id3($ndata);
        } // catalog type = local
    }

    /**
     * @param int|string $description
     * @param $ndata
     * @return int|null
     */
    private function search_txxx($description, $ndata): ?int
    {
        $cnt = count($ndata);
        for ($i = 0; $i < $cnt; $i++) {
            if (strtolower($ndata[$i]['description']) == strtolower((string)$description)) {
                return $i;
            }
        }

        return null;
    }

    public function check_for_duplicate($apics, $new_pic, &$ndata, $apic_typeid): ?int
    {
        $idx = null;
        $cnt = count($apics);
        for ($i = 0; $i < $cnt; $i++) {
            if ($new_pic['picturetypeid'] == $apics[$i][$apic_typeid]) {
                $ndata['attached_picture'][$i]['description']   = $new_pic['description'];
                $ndata['attached_picture'][$i]['data']          = $new_pic['data'];
                $ndata['attached_picture'][$i]['mime']          = $new_pic['mime'];
                $ndata['attached_picture'][$i]['picturetypeid'] = $new_pic['picturetypeid'];
                $ndata['attached_picture'][$i]['encodingid']    = $new_pic['encodingid'];
                $idx                                            = $i;
            }
        }

        return $idx;
    }

    private function getVorbisMetadata(
        Song $song
    ): array {
        $song->format();
        $meta = [];

        $meta['date']                = $song->year;
        $meta['title']               = $song->title;
        $meta['comment']             = $song->comment;
        $meta['album']               = $song->f_album_full;
        $meta['artist']              = $song->get_artist_fullname();
        $meta['albumartist']         = $song->f_albumartist_full;
        $meta['composer']            = $song->composer;
        $meta['label']               = $song->f_publisher;
        $meta['tracknumber']         = $song->f_track;
        $meta['discnumber']          = $song->disk;
        $meta['musicbrainz_trackid'] = $song->mbid;
        $meta['musicbrainz_albumid'] = $song->album_mbid;
        $meta['license']             = $song->license;
        $meta['genre']               = [];

        if (!empty($song->tags)) {
            foreach ($song->tags as $tag) {
                $meta['genre'][] = $tag['name'];
            }
        }
        $meta['genre'] = implode(', ', $meta['genre']);

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

        $meta['musicbrainz_albumartistid']  = $song->albumartist_mbid;
        $meta['musicbrainz_releasegroupid'] = $album->mbid_group;

        if (isset($album->release_type)) {
            $release_type = explode(',', $album->release_type);
            if (count($release_type) == 2) {
                $release_type[1] = trim($release_type[1]);
            }
            $meta['releasetype'] = $release_type;
        }

        if (isset($album->release_status)) {
            $release_status = explode(',', $album->release_status);
            if (count($release_status) == 2) {
                $release_status[1] = trim($release_status[1]);
            }
            $meta['releasestatus'] = $release_status;
        }
        $meta['barcode']       = $album->barcode;
        $meta['catalognumber'] = $album->catalog_number;
        $meta['original_year'] = $album->original_year;

        if ($this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::RATINGS)) {
            $user      = Core::get_global('user');
            $rating    = new Rating($song->id, 'song');
            $my_rating = $rating->get_user_rating($user->id);
            if (!empty($user->email)) {
                $meta['rating:' . $user->email] = array(($my_rating > 0) ? $my_rating * (100 / 5) : 0);
            } else {
                $this->logger->debug(
                    'Rating user must have an email address on record.',
                    [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                );
            }
        }

        return $meta;
    }

    /**
     * Get an array of metadata for writing id3 file tags.
     */
    private function getId3Metadata(
        Song $song
    ): array {
        $meta = [];

        $meta['year']          = $song->year;
        $meta['time']          = $song->time;
        $meta['title']         = $song->title;
        $meta['comment']       = $song->comment;
        $meta['album']         = $song->get_album_fullname();
        $meta['artist']        = $song->get_artist_fullname();
        $meta['band']          = $song->f_albumartist_full;
        $meta['composer']      = $song->composer;
        $meta['publisher']     = $song->f_publisher;
        $meta['track_number']  = $song->f_track;
        $meta['part_of_a_set'] = $song->disk;
        if (isset($song->mbid)) {
            $meta['unique_file_identifier'] = [
                'data' => $song->mbid,
                'ownerid' => "http://musicbrainz.org"
            ];
        }
        if ($this->configContainer->isFeatureEnabled(ConfigurationKeyEnum::RATINGS)) {
            $user      = Core::get_global('user');
            $rating    = new Rating($song->id, 'song');
            $my_rating = $rating->get_user_rating($user->id);
            if (!empty($user->email)) {
                $meta['Popularimeter'] = [
                    "email" => $user->email,
                    "rating" => ($my_rating > 0) ? $my_rating * (255 / 5) : 0,
                    "data" => $song->get_totalcount()
                ];
            } else {
                $this->logger->debug(
                    'Rating user must have an email address on record.',
                    [LegacyLogger::CONTEXT_TYPE => __CLASS__]
                );
            }
        }

        $meta['genre'] = [];

        if (!empty($song->tags)) {
            foreach ($song->tags as $tag) {
                $meta['genre'][] = $tag['name'];
            }
        }
        $meta['genre'] = implode(', ', $meta['genre']);

        $album = new Album($song->album);
        $album->format();
        $meta['original_year'] = $album->original_year;  //TORY

        $meta['text'] = array();
        if ($song->album_mbid) {
            $meta['text'][] = [
                'data' => $song->album_mbid,
                'description' => 'MusicBrainz Album Id',
                'encodingid' => 0
            ];
        }
        if ($song->albumartist_mbid) {
            $meta['text'][] = [
                'data' => $song->albumartist_mbid,
                'description' => 'MusicBrainz Album Artist Id',
                'encodingid' => 0
            ];
        }
        if ($song->albumartist_mbid) {
            $meta['text'][] = [
                'data' => $song->albumartist_mbid,
                'description' => 'MusicBrainz Album Artist Id',
                'encodingid' => 0
            ];
        }
        if ($album->release_status) {
            $meta['text'][] = [
                'data' => $album->release_status,
                'description' => 'MusicBrainz Album Status',
                'encodingid' => 0];
        }
        if ($album->release_type) {
            $meta['text'][] = [
                'data' => $album->release_type,
                'description' => 'MusicBrainz Album Type',
                'encodingid' => 0
            ];
        }
        if ($album->barcode) {
            $meta['text'][] = [
                'data' => $album->barcode,
                'description' => 'BARCODE',
                'encodingid' => 0
            ];
        }
        if ($album->catalog_number) {
            $meta['text'][] = [
                'data' => $album->catalog_number,
                'description' => 'CATALOGNUMBER',
                'encodingid' => 0
            ];
        }

        return $meta;
    }
}