diff --git a/bin/metadata b/bin/metadata deleted file mode 100755 index 288e588a0..000000000 --- a/bin/metadata +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env php - dirname(__DIR__), - ] -); - -$console = new Silly\Application('AzuraCast Metadata Processor', App\Version::FALLBACK_VERSION); - -$console->command( - 'read path json-output [--art-output=]', - new App\MediaProcessor\Command\ReadCommand -); - -$console->command( - 'write path json-input [--art-input=]', - new App\MediaProcessor\Command\WriteCommand -); - -$console->run(); diff --git a/composer.json b/composer.json index 3099d2256..448149703 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "ext-xmlwriter": "*", "azuracast/azuraforms": "dev-main", "azuracast/flysystem-v2-extensions": "dev-main", + "azuracast/metadata-manager": "dev-main", "azuracast/nowplaying": "dev-main", "azuracast/slim-callable-eventdispatcher": "dev-main", "bacon/bacon-qr-code": "^2.0", @@ -35,7 +36,6 @@ "guzzlehttp/oauth-subscriber": "^0.6.0", "http-interop/http-factory-guzzle": "^1.0", "intervention/image": "^2.6", - "james-heinrich/getid3": "dev-master#0bc9aca", "laminas/laminas-config": "^3.3", "league/csv": "^9.6", "league/flysystem-aws-s3-v3": "^2.0", @@ -81,7 +81,6 @@ "symfony/yaml": "^5.3", "theiconic/php-ga-measurement-protocol": "^2.9", "vlucas/phpdotenv": "^5.3", - "voku/portable-utf8": "^5.4", "wikimedia/composer-merge-plugin": "dev-master", "zircote/swagger-php": "^3" }, diff --git a/composer.lock b/composer.lock index b8a990d8c..6610f80b7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "20b7638fe0d73d395369811b1b582368", + "content-hash": "47b9920342b23a86135e11644f663a6c", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.191.1", + "version": "3.191.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "221eb3790a2f5cc73067ec1c13d4c788a0296351" + "reference": "d659144bf8618c891fd01f0f375f1f0b4db21b05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/221eb3790a2f5cc73067ec1c13d4c788a0296351", - "reference": "221eb3790a2f5cc73067ec1c13d4c788a0296351", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d659144bf8618c891fd01f0f375f1f0b4db21b05", + "reference": "d659144bf8618c891fd01f0f375f1f0b4db21b05", "shasum": "" }, "require": { @@ -92,9 +92,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.191.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.191.2" }, - "time": "2021-08-20T18:43:14+00:00" + "time": "2021-08-23T18:17:20+00:00" }, { "name": "azuracast/azuraforms", @@ -247,6 +247,76 @@ ], "time": "2021-06-21T02:10:40+00:00" }, + { + "name": "azuracast/metadata-manager", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/AzuraCast/metadata-manager.git", + "reference": "36638b7dfe52e3d2a502e9269b02b4d885820ddd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/AzuraCast/metadata-manager/zipball/36638b7dfe52e3d2a502e9269b02b4d885820ddd", + "reference": "36638b7dfe52e3d2a502e9269b02b4d885820ddd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "james-heinrich/getid3": "dev-master#0bc9aca", + "php": ">=7.4", + "symfony/console": ">5.0", + "voku/portable-utf8": "^5.4" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^0.5.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^0.12", + "roave/security-advisories": "dev-latest" + }, + "default-branch": true, + "bin": [ + "bin/metadata-manager" + ], + "type": "library", + "autoload": { + "psr-4": { + "Azura\\MetadataManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Buster 'Silver Eagle' Neece", + "email": "buster@busterneece.com", + "homepage": "https://dashdev.net/", + "role": "Lead Developer" + } + ], + "description": "A command-line wrapper around the PHP GetId3 library.", + "homepage": "https://github.com/AzuraCast/metadata-manager", + "support": { + "source": "https://github.com/AzuraCast/metadata-manager/tree/main" + }, + "funding": [ + { + "url": "https://github.com/AzuraCast", + "type": "github" + }, + { + "url": "https://opencollective.com/azuracast", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/AzuraCast", + "type": "patreon" + } + ], + "time": "2021-08-24T03:19:30+00:00" + }, { "name": "azuracast/nowplaying", "version": "dev-main", @@ -1882,16 +1952,16 @@ }, { "name": "doctrine/orm", - "version": "2.9.4", + "version": "2.9.5", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "b19a13f4edfaa5806109cd899f5912a7df1547b5" + "reference": "77cc86ed880e3f1f6c9c5819e131a8aaeeeee0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/b19a13f4edfaa5806109cd899f5912a7df1547b5", - "reference": "b19a13f4edfaa5806109cd899f5912a7df1547b5", + "url": "https://api.github.com/repos/doctrine/orm/zipball/77cc86ed880e3f1f6c9c5819e131a8aaeeeee0da", + "reference": "77cc86ed880e3f1f6c9c5819e131a8aaeeeee0da", "shasum": "" }, "require": { @@ -1970,9 +2040,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.9.4" + "source": "https://github.com/doctrine/orm/tree/2.9.5" }, - "time": "2021-08-11T20:53:03+00:00" + "time": "2021-08-23T10:20:22+00:00" }, { "name": "doctrine/persistence", @@ -2935,12 +3005,12 @@ "source": { "type": "git", "url": "https://github.com/JamesHeinrich/getID3.git", - "reference": "0bc9aca" + "reference": "cb831b64d21b2b2361e7011853d0fc26e323e11c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JamesHeinrich/getID3/zipball/0bc9aca", - "reference": "0bc9aca", + "url": "https://api.github.com/repos/JamesHeinrich/getID3/zipball/cb831b64d21b2b2361e7011853d0fc26e323e11c", + "reference": "cb831b64d21b2b2361e7011853d0fc26e323e11c", "shasum": "" }, "require": { @@ -11822,12 +11892,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "3c3cc12a9f163e589a12b9ea756c5a2dae9c59dd" + "reference": "cd0a994884c7323cdc591f02b6027e00c1d88e74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/3c3cc12a9f163e589a12b9ea756c5a2dae9c59dd", - "reference": "3c3cc12a9f163e589a12b9ea756c5a2dae9c59dd", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/cd0a994884c7323cdc591f02b6027e00c1d88e74", + "reference": "cd0a994884c7323cdc591f02b6027e00c1d88e74", "shasum": "" }, "conflict": { @@ -12005,7 +12075,7 @@ "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", "phpwhois/phpwhois": "<=4.2.5", "phpxmlrpc/extras": "<0.6.1", - "pimcore/pimcore": "<10.0.7", + "pimcore/pimcore": "<10.1.1", "pocketmine/pocketmine-mp": "<3.15.4", "pressbooks/pressbooks": "<5.18", "prestashop/autoupgrade": ">=4,<4.10.1", @@ -12028,8 +12098,8 @@ "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", "sensiolabs/connect": "<4.2.3", "serluck/phpwhois": "<=4.2.6", - "shopware/core": "<=6.4.1", - "shopware/platform": "<=6.4.1", + "shopware/core": "<=6.4.3", + "shopware/platform": "<=6.4.3", "shopware/production": "<=6.3.5.2", "shopware/shopware": "<=5.6.9", "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1", @@ -12197,7 +12267,7 @@ "type": "tidelift" } ], - "time": "2021-08-19T16:07:21+00:00" + "time": "2021-08-23T20:03:09+00:00" }, { "name": "sebastian/cli-parser", @@ -13760,9 +13830,9 @@ "stability-flags": { "azuracast/azuraforms": 20, "azuracast/flysystem-v2-extensions": 20, + "azuracast/metadata-manager": 20, "azuracast/nowplaying": 20, "azuracast/slim-callable-eventdispatcher": 20, - "james-heinrich/getid3": 20, "lstrojny/fxmlrpc": 20, "rlanvin/php-ip": 20, "supervisorphp/supervisor": 20, diff --git a/src/Entity/Metadata.php b/src/Entity/Metadata.php deleted file mode 100644 index 5816fba3a..000000000 --- a/src/Entity/Metadata.php +++ /dev/null @@ -1,89 +0,0 @@ - */ - protected array $tags = []; - - protected float $duration = 0.0; - - protected ?string $artwork = null; - - protected string $mimeType = ''; - - public function getTags(): array - { - return $this->tags; - } - - public function setTags(array $tags): void - { - $this->tags = $tags; - } - - public function addTag(string $key, mixed $value): void - { - $this->tags[$key] = $value; - } - - public function getDuration(): float - { - return $this->duration; - } - - public function setDuration(float $duration): void - { - $this->duration = $duration; - } - - public function getArtwork(): ?string - { - return $this->artwork; - } - - public function setArtwork(?string $artwork): void - { - $this->artwork = $artwork; - } - - public function getMimeType(): string - { - return $this->mimeType; - } - - public function setMimeType(string $mimeType): void - { - $this->mimeType = $mimeType; - } - - public function jsonSerialize() - { - // Artwork is not included in this JSON feed. - return [ - 'tags' => $this->tags, - 'duration' => $this->duration, - 'mimeType' => $this->mimeType, - ]; - } - - public static function fromJson(array $data): self - { - $metadata = new self(); - - if (isset($data['tags'])) { - $metadata->setTags((array)$data['tags']); - } - if (isset($data['duration'])) { - $metadata->setDuration((float)$data['duration']); - } - if (isset($data['mimeType'])) { - $metadata->setMimeType((string)$data['mimeType']); - } - - return $metadata; - } -} diff --git a/src/Entity/StationMedia.php b/src/Entity/StationMedia.php index 2142031d0..4850ff2a1 100644 --- a/src/Entity/StationMedia.php +++ b/src/Entity/StationMedia.php @@ -10,6 +10,8 @@ use App\Entity\Interfaces\ProcessableMediaInterface; use App\Entity\Interfaces\SongInterface; use App\Normalizer\Attributes\DeepNormalize; use App\Utilities\Time; +use Azura\MetadataManager\Metadata; +use Azura\MetadataManager\MetadataInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -460,7 +462,7 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar return $this->playlists; } - public function fromMetadata(Metadata $metadata): void + public function fromMetadata(MetadataInterface $metadata): void { $this->setLength((int)$metadata->getDuration()); @@ -488,7 +490,7 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar $this->updateSongId(); } - public function toMetadata(): Metadata + public function toMetadata(): MetadataInterface { $metadata = new Metadata(); $metadata->setDuration($this->getLength() ?? 0.0); diff --git a/src/Event/Media/ReadMetadata.php b/src/Event/Media/ReadMetadata.php index be0dd02b8..759b650ec 100644 --- a/src/Event/Media/ReadMetadata.php +++ b/src/Event/Media/ReadMetadata.php @@ -4,12 +4,13 @@ declare(strict_types=1); namespace App\Event\Media; -use App\Entity; +use Azura\MetadataManager\Metadata; +use Azura\MetadataManager\MetadataInterface; use Symfony\Contracts\EventDispatcher\Event; class ReadMetadata extends Event { - protected ?Entity\Metadata $metadata = null; + protected ?MetadataInterface $metadata = null; public function __construct( protected string $path @@ -21,13 +22,13 @@ class ReadMetadata extends Event return $this->path; } - public function setMetadata(Entity\Metadata $metadata): void + public function setMetadata(MetadataInterface $metadata): void { $this->metadata = $metadata; } - public function getMetadata(): Entity\Metadata + public function getMetadata(): MetadataInterface { - return $this->metadata ?? new Entity\Metadata(); + return $this->metadata ?? new Metadata(); } } diff --git a/src/Event/Media/WriteMetadata.php b/src/Event/Media/WriteMetadata.php index 1becaa533..6796581d9 100644 --- a/src/Event/Media/WriteMetadata.php +++ b/src/Event/Media/WriteMetadata.php @@ -4,18 +4,18 @@ declare(strict_types=1); namespace App\Event\Media; -use App\Entity; +use Azura\MetadataManager\MetadataInterface; use Symfony\Contracts\EventDispatcher\Event; class WriteMetadata extends Event { public function __construct( - protected Entity\Metadata $metadata, + protected MetadataInterface $metadata, protected string $path ) { } - public function getMetadata(): ?Entity\Metadata + public function getMetadata(): ?MetadataInterface { return $this->metadata; } diff --git a/src/Media/MetadataManager.php b/src/Media/MetadataManager.php index b6df78a99..cb154a691 100644 --- a/src/Media/MetadataManager.php +++ b/src/Media/MetadataManager.php @@ -4,13 +4,14 @@ declare(strict_types=1); namespace App\Media; -use App\Entity; use App\Environment; use App\Event\Media\ReadMetadata; use App\Event\Media\WriteMetadata; use App\Exception\CannotProcessMediaException; use App\Utilities\File; use App\Utilities\Json; +use Azura\MetadataManager\Metadata; +use Azura\MetadataManager\MetadataInterface; use GuzzleHttp\Client; use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -38,7 +39,7 @@ class MetadataManager implements EventSubscriberInterface ]; } - public function read(string $filePath): Entity\Metadata + public function read(string $filePath): MetadataInterface { if (!MimeType::isFileProcessable($filePath)) { $mimeType = MimeType::getMimeTypeFromFile($filePath); @@ -67,7 +68,7 @@ class MetadataManager implements EventSubscriberInterface throw new \RuntimeException('Could not find PHP executable path.'); } - $scriptPath = $this->environment->getBaseDirectory() . '/bin/metadata'; + $scriptPath = $this->environment->getBaseDirectory() . '/vendor/bin/metadata-manager'; $process = new Process( [ @@ -76,14 +77,14 @@ class MetadataManager implements EventSubscriberInterface 'read', $sourceFilePath, $jsonOutput, - '--art-output=' . $artOutput, + $artOutput, ] ); $process->mustRun(); $metadataJson = Json::loadFromFile($jsonOutput); - $metadata = Entity\Metadata::fromJson($metadataJson); + $metadata = Metadata::fromJson($metadataJson); if (is_file($artOutput)) { $artwork = file_get_contents($artOutput) ?: null; @@ -97,7 +98,7 @@ class MetadataManager implements EventSubscriberInterface } } - public function write(Entity\Metadata $metadata, string $filePath): void + public function write(MetadataInterface $metadata, string $filePath): void { $event = new WriteMetadata($metadata, $filePath); $this->eventDispatcher->dispatch($event); @@ -136,7 +137,7 @@ class MetadataManager implements EventSubscriberInterface throw new \RuntimeException('Could not find PHP executable path.'); } - $scriptPath = $this->environment->getBaseDirectory() . '/bin/metadata'; + $scriptPath = $this->environment->getBaseDirectory() . '/vendor/bin/metadata-manager'; $processCommand = [ $phpBinaryPath, @@ -147,7 +148,7 @@ class MetadataManager implements EventSubscriberInterface ]; if (null !== $artwork) { - $processCommand[] = '--art-input=' . $artInput; + $processCommand[] = $artInput; } $process = new Process($processCommand); diff --git a/src/MediaProcessor/Command/ReadCommand.php b/src/MediaProcessor/Command/ReadCommand.php deleted file mode 100644 index 19ccba49e..000000000 --- a/src/MediaProcessor/Command/ReadCommand.php +++ /dev/null @@ -1,133 +0,0 @@ -error(sprintf('File not readable: %s', $path)); - return 1; - } - - $id3 = new \getID3(); - - $id3->option_md5_data = true; - $id3->option_md5_data_source = true; - $id3->encoding = 'UTF-8'; - - $info = $id3->analyze($path); - $id3->CopyTagsToComments($info); - - if (!empty($info['error'])) { - $io->error( - sprintf( - 'Cannot process media at path %s: %s', - pathinfo($path, PATHINFO_FILENAME), - json_encode($info['error'], JSON_THROW_ON_ERROR) - ) - ); - return 1; - } - - $metadata = new Entity\Metadata(); - - if (is_numeric($info['playtime_seconds'])) { - $metadata->setDuration( - Time::displayTimeToSeconds($info['playtime_seconds']) ?? 0.0 - ); - } - - $metaTags = []; - - $toProcess = [ - $info['comments'] ?? null, - $info['tags'] ?? null, - ]; - - foreach ($toProcess as $tagSet) { - if (empty($tagSet)) { - continue; - } - - foreach ($tagSet as $tagName => $tagContents) { - if (!empty($tagContents[0]) && !isset($metaTags[$tagName])) { - $tagValue = $tagContents[0]; - if (is_array($tagValue)) { - // Skip pictures - if (isset($tagValue['data'])) { - continue; - } - $flatValue = Arrays::flattenArray($tagValue); - $tagValue = implode(', ', $flatValue); - } - - $metaTags[$tagName] = $this->cleanUpString((string)$tagValue); - } - } - } - - $metadata->setTags($metaTags); - $metadata->setMimeType($info['mime_type']); - - file_put_contents( - $jsonOutput, - json_encode($metadata, JSON_THROW_ON_ERROR), - ); - - if (null !== $artOutput) { - $artwork = null; - if (!empty($info['attached_picture'][0])) { - $artwork = $info['attached_picture'][0]['data']; - } elseif (!empty($info['comments']['picture'][0])) { - $artwork = $info['comments']['picture'][0]['data']; - } elseif (!empty($info['id3v2']['APIC'][0]['data'])) { - $artwork = $info['id3v2']['APIC'][0]['data']; - } elseif (!empty($info['id3v2']['PIC'][0]['data'])) { - $artwork = $info['id3v2']['PIC'][0]['data']; - } - - if (!empty($artwork)) { - file_put_contents( - $artOutput, - $artwork - ); - } - } - - return 0; - } - - protected function cleanUpString(?string $original): string - { - $original ??= ''; - - $string = UTF8::encode('UTF-8', $original); - $string = UTF8::fix_simple_utf8($string); - return UTF8::clean( - $string, - true, - true, - true, - true, - true - ); - } -} diff --git a/src/MediaProcessor/Command/WriteCommand.php b/src/MediaProcessor/Command/WriteCommand.php deleted file mode 100644 index 020da5890..000000000 --- a/src/MediaProcessor/Command/WriteCommand.php +++ /dev/null @@ -1,90 +0,0 @@ -setOption(['encoding' => 'UTF8']); - - $tagwriter = new getid3_writetags(); - $tagwriter->filename = $path; - - $pathExt = strtolower(pathinfo($path, PATHINFO_EXTENSION)); - - $tagFormats = match ($pathExt) { - 'mp3', 'mp2', 'mp1', 'riff' => ['id3v1', 'id3v2.3'], - 'mpc' => ['ape'], - 'flac' => ['metaflac'], - 'real' => ['real'], - 'ogg' => ['vorbiscomment'], - default => null - }; - - if (null === $tagFormats) { - $io->error('Cannot write tag formats based on file type.'); - return 1; - } - - $tagwriter->tagformats = $tagFormats; - $tagwriter->overwrite_tags = true; - $tagwriter->tag_encoding = 'UTF8'; - $tagwriter->remove_other_tags = true; - - $json = Json::loadFromFile($jsonInput); - $writeTags = Metadata::fromJson($json)->getTags(); - - if ($artInput && is_file($artInput)) { - $artContents = file_get_contents($artInput); - if (false !== $artContents) { - $writeTags['attached_picture'] = [ - 'encodingid' => 0, // ISO-8859-1; 3=UTF8 but only allowed in ID3v2.4 - 'description' => 'cover art', - 'data' => $artContents, - 'picturetypeid' => 0x03, - 'mime' => 'image/jpeg', - ]; - } - } - - // All ID3 tags have to be written as ['key' => ['value']] (i.e. with "value" at position 0). - $tagData = []; - foreach ($writeTags as $tagKey => $tagValue) { - $tagData[$tagKey] = [$tagValue]; - } - - $tagwriter->tag_data = $tagData; - $tagwriter->WriteTags(); - - if (!empty($tagwriter->errors) || !empty($tagwriter->warnings)) { - $messages = array_merge($tagwriter->errors, $tagwriter->warnings); - - $io->error( - sprintf( - 'Cannot process media file %s: %s', - $path, - implode(', ', $messages) - ) - ); - return 1; - } - - return 0; - } -}