Use standalone MetadataManager library.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-08-23 23:00:29 -05:00
parent 424c28b027
commit 32296cbda6
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
10 changed files with 117 additions and 385 deletions

View File

@ -1,29 +0,0 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT);
ini_set('display_errors', '1');
$autoloader = require dirname(__DIR__) . '/vendor/autoload.php';
$environment = App\AppFactory::buildEnvironment(
[
App\Environment::BASE_DIR => 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();

View File

@ -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"
},

118
composer.lock generated
View File

@ -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,

View File

@ -1,89 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
class Metadata implements \JsonSerializable
{
/** @var array<string, mixed> */
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;
}
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -1,133 +0,0 @@
<?php
declare(strict_types=1);
namespace App\MediaProcessor\Command;
use App\Entity;
use App\Utilities\Arrays;
use App\Utilities\Time;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use voku\helper\UTF8;
use const JSON_THROW_ON_ERROR;
class ReadCommand
{
public function __invoke(
SymfonyStyle $io,
OutputInterface $output,
string $path,
string $jsonOutput,
?string $artOutput
): int {
if (!is_file($path)) {
$io->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
);
}
}

View File

@ -1,90 +0,0 @@
<?php
declare(strict_types=1);
namespace App\MediaProcessor\Command;
use App\Entity\Metadata;
use App\Utilities\Json;
use getID3;
use getid3_writetags;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class WriteCommand
{
public function __invoke(
SymfonyStyle $io,
OutputInterface $output,
string $path,
string $jsonInput,
?string $artInput
): int {
$getID3 = new getID3();
$getID3->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;
}
}