diff --git a/README.md b/README.md index af7e95f..0db74de 100644 --- a/README.md +++ b/README.md @@ -164,34 +164,15 @@ $audio->getCreationDate(); // `null` because `creationDate` is not supported by Some properties are not supported by all formats, for example `MP3` can't handle some properties like `lyrics` or `stik`, if you try to update these properties, they will be ignored. -#### Update one custom tag - -You can update one custom tag with `tag` method. - -```php -use Kiwilan\Audio\Audio; - -$audio = Audio::read('path/to/audio.mp3'); -$audio->write() - ->tag('custom-a', 'New Custom a Tag') - ->tag('custom-b', 'New Custom b Tag') - ->save(); - -$audio = Audio::read('path/to/audio.mp3'); -$audio->getRawKey('custom-a'); // `New Custom a Tag` -$audio->getRawKey('custom-b'); // `New Custom b Tag` -``` - #### Set tags manually -You can set tags manually with `tags` method, but you need to know the format of the tag, you could use `tagFormats` to set formats of tags (if you don't know the format, it will be automatically detected). +You can set tags manually with `tag()` or `tags()` methods, but you need to know the format of the tag, you could use `tagFormats` to set formats of tags (if you don't know the format, it will be automatically detected). > [!WARNING] > -> If you use `tags` method, you have to use key used by metadata container. For example, if you want to set album artist in `id3v2`, you have to use `band` key. If you want to know which key to use check `src/Models/AudioCore.php` file. +> If you use `tags` method, you have to use key used by metadata container. For example, if you want to set album artist in `id3v2`, you have to use `band` key. If you want to know which key to use check [`src/Core/AudioCore.php`](https://github.com/kiwilan/php-audio/blob/main/src/Core/AudioCore.php) file. > -> You can't use other methods with `tags()` method. -> If your key is not supported, `save` method will throw an exception, unless you use `preventFailOnErrors`. +> If your key is not supported, `save` method will throw an exception, unless you use `skipErrors`. ```php use Kiwilan\Audio\Audio; @@ -200,12 +181,14 @@ $audio = Audio::read('path/to/audio.mp3'); $audio->getAlbumArtist(); // `Band` $tag = $audio->write() - ->tags([ - 'title' => 'New Title', - 'band' => 'New Band', // `band` is used by `id3v2` to set album artist, method is `albumArtist` but `albumArtist` key will throw an exception with `id3v2` - ]) - ->tagFormats(['id3v1', 'id3v2.4']) // optional - ->save(); + ->tag('composer', 'New Composer') + ->tag('genre', 'New Genre') // can be chained + ->tags([ + 'title' => 'New Title', + 'band' => 'New Band', // `band` is used by `id3v2` to set album artist, method is `albumArtist` but `albumArtist` key will throw an exception with `id3v2` + ]) + ->tagFormats(['id3v1', 'id3v2.4']) // optional + ->save(); $audio = Audio::read('path/to/audio.mp3'); $audio->getAlbumArtist(); // `New Band` @@ -228,9 +211,9 @@ $audio = Audio::read('path/to/audio.mp3'); $audio->getAlbumArtist(); // `New Band` ``` -#### Prevent fail on errors +#### Skip errors -You can use `preventFailOnError` to prevent exception if you use unsupported format. +You can use `skipErrors` to prevent exception if you use unsupported format. ```php use Kiwilan\Audio\Audio; @@ -242,45 +225,15 @@ $tag = $audio->write() 'title' => 'New Title', 'title2' => 'New title', // not supported by `id3v2`, will throw an exception ]) - ->preventFailOnError() // will prevent exception - ->save(); -``` - -Arrow functions are exception safe for properties but not for unsupported formats. - -```php -use Kiwilan\Audio\Audio; - -$audio = Audio::read('path/to/audio.mp3'); - -$tag = $audio->write() - ->encoding('New encoding') // not supported by `id3v2`, BUT will not throw an exception - ->preventFailOnError() // if you have some errors with unsupported format for example, you can prevent exception + ->skipErrors() // will prevent exception ->save(); ``` -#### Tags and cover - -Of course you can add cover with `tags` method. - -```php -use Kiwilan\Audio\Audio; - -$audio = Audio::read('path/to/audio.mp3'); -$cover = 'path/to/cover.jpg'; - -$coverData = file_get_contents($cover); - -$tag = $audio->write() - ->tags([ - 'title' => 'New Title', - 'band' => 'New Band', - ]) - ->cover($coverData) - ->save(); -``` +> [!NOTE] +> +> Arrow functions are exception safe for properties but not for unsupported formats. -### Extras +### Raw tags Audio files format metadata with different methods, `JamesHeinrich/getID3` offer to check these metadatas by different methods. In `extras` property of `Audio::class`, you will find raw metadata from `JamesHeinrich/getID3` package, like `id3v2`, `id3v1`, `riff`, `asf`, `quicktime`, `matroska`, `ape`, `vorbiscomment`... @@ -290,9 +243,8 @@ If you want to extract specific field which can be skipped by `Audio::class`, yo use Kiwilan\Audio\Audio; $audio = Audio::read('path/to/audio.mp3'); -$extras = $audio->getExtras(); - -$id3v2 = $extras['id3v2'] ?? []; +$raw_all = $audio->getRawAll(); // all formats +$raw = $audio->getRaw(); // main format ``` ### AudioMetadata @@ -320,6 +272,8 @@ $metadata->getCompressionRatio(); // `?float` $metadata->getFilesize(); // `?int` in bytes $metadata->getSizeHuman(); // `?string` (1.2 MB, 1.2 GB, ...) $metadata->getDataFormat(); // `?string` (mp3, m4a, ...) +$metadata->getWarning(); // `?array` +$metadata->getQuicktime(); // `?Id3AudioQuicktime $metadata->getCodec(); // `?string` (mp3, aac, ...) $metadata->getEncoderOptions(); // `?string` $metadata->getVersion(); // `?string` @@ -333,6 +287,33 @@ $metadata->getModifiedAt(); // `?DateTime` $metadata->toArray(); ``` +### Quicktime + +For `quicktime` type, like for M4B audiobook, you can use `Id3TagQuicktime` to get more informations. + +```php +use Kiwilan\Audio\Audio; + +$audio = Audio::read('path/to/audio.m4b'); +$quicktime = $audio->getMetadata()->getQuicktime(); + +$quicktime->getHinting(); +$quicktime->getController(); +$quicktime->getFtyp(); +$quicktime->getTimestampsUnix(); +$quicktime->getTimeScale(); +$quicktime->getDisplayScale(); +$quicktime->getVideo(); +$quicktime->getAudio(); +$quicktime->getSttsFramecount(); +$quicktime->getComments(); +$quicktime->getFree(); +$quicktime->getWide(); +$quicktime->getMdat(); +$quicktime->getEncoding(); +$quicktime->getChapters(); // ?Id3AudioQuicktimeChapter[] +``` + ### AudioCover ```php diff --git a/src/Id3/Id3Reader.php b/src/Id3/Id3Reader.php index d282d06..09e104e 100644 --- a/src/Id3/Id3Reader.php +++ b/src/Id3/Id3Reader.php @@ -4,6 +4,7 @@ use getID3; use Kiwilan\Audio\Id3\Reader\Id3Audio; +use Kiwilan\Audio\Id3\Reader\Id3AudioQuicktime; use Kiwilan\Audio\Id3\Reader\Id3AudioTag; use Kiwilan\Audio\Id3\Reader\Id3Comments; use Kiwilan\Audio\Id3\Reader\Id3Video; @@ -24,7 +25,9 @@ protected function __construct( protected ?Id3Audio $audio = null, protected ?Id3Video $video = null, protected ?Id3AudioTag $tags = null, + protected ?array $warning = null, protected ?Id3Comments $comments = null, + protected ?Id3AudioQuicktime $quicktime = null, protected ?string $encoding = null, protected ?string $mime_type = null, protected ?array $mpeg = null, @@ -46,6 +49,8 @@ public static function make(string $path): self $video = Id3Video::make($metadata['video'] ?? null); $tags = Id3AudioTag::make($metadata['tags'] ?? null); $comments = Id3Comments::make($metadata['comments'] ?? null); + $quicktime = Id3AudioQuicktime::make($self->raw['quicktime'] ?? null); + $warning = $metadata['warning'] ?? null; $bitrate = $metadata['bitrate'] ?? null; if ($bitrate) { @@ -63,7 +68,9 @@ public static function make(string $path): self $self->audio = $audio; $self->video = $video; $self->tags = $tags; + $self->quicktime = $quicktime; $self->comments = $comments; + $self->warning = $warning; $self->encoding = $metadata['encoding'] ?? null; $self->mime_type = $metadata['mime_type'] ?? null; $self->mpeg = $metadata['mpeg'] ?? null; @@ -134,6 +141,21 @@ public function getComments(): ?Id3Comments return $this->comments; } + public function getVideo(): ?Id3Video + { + return $this->video; + } + + public function getQuicktime(): ?Id3AudioQuicktime + { + return $this->quicktime; + } + + public function getWarning(): ?array + { + return $this->warning; + } + public function getEncoding(): ?string { return $this->encoding; diff --git a/src/Id3/Reader/Id3AudioQuicktime.php b/src/Id3/Reader/Id3AudioQuicktime.php new file mode 100644 index 0000000..87ea32e --- /dev/null +++ b/src/Id3/Reader/Id3AudioQuicktime.php @@ -0,0 +1,174 @@ +|null $timestamps_unix + * @param array|null $comments + * @param array|null $video + * @param array|null $audio + * @param Id3AudioQuicktimeChapter[] $chapters + */ + protected function __construct( + protected bool $hinting = false, + protected ?string $controller = null, + protected ?Id3AudioQuicktimeItem $ftyp = null, + protected ?array $timestamps_unix = null, + protected ?int $time_scale = null, + protected ?int $display_scale = null, + protected ?array $video = null, + protected ?array $audio = null, + protected ?array $stts_framecount = null, + protected ?array $comments = [], + protected array $chapters = [], + protected ?Id3AudioQuicktimeItem $free = null, + protected ?Id3AudioQuicktimeItem $wide = null, + protected ?Id3AudioQuicktimeItem $mdat = null, + protected ?string $encoding = null, + ) {} + + public static function make(?array $metadata): ?self + { + if (! $metadata) { + return null; + } + + $hinting = $metadata['hinting'] ?? false; + $controller = $metadata['controller'] ?? null; + $ftyp = Id3AudioQuicktimeItem::make($metadata['ftyp'] ?? null); + $timestamps_unix = $metadata['timestamps_unix'] ?? null; + $time_scale = $metadata['time_scale'] ?? null; + $display_scale = $metadata['display_scale'] ?? null; + $video = $metadata['video'] ?? null; + $audio = $metadata['audio'] ?? null; + $stts_framecount = $metadata['stts_framecount'] ?? null; + $comments = $metadata['comments'] ?? []; + + $chapters = []; + $chaps = $metadata['chapters'] ?? []; + foreach ($chaps as $chapter) { + $chapters[] = Id3AudioQuicktimeChapter::make($chapter); + } + + $free = Id3AudioQuicktimeItem::make($metadata['free'] ?? null); + $wide = Id3AudioQuicktimeItem::make($metadata['wide'] ?? null); + $mdat = Id3AudioQuicktimeItem::make($metadata['mdat'] ?? null); + $encoding = $metadata['encoding'] ?? null; + + $self = new self( + hinting: $hinting, + controller: $controller, + ftyp: $ftyp, + timestamps_unix: $timestamps_unix, + time_scale: $time_scale, + display_scale: $display_scale, + video: $video, + audio: $audio, + stts_framecount: $stts_framecount, + comments: $comments, + chapters: $chapters, + free: $free, + wide: $wide, + mdat: $mdat, + encoding: $encoding, + ); + + return $self; + } + + /** + * @return Id3AudioQuicktimeChapter[] + */ + public function getChapters(): array + { + return $this->chapters; + } + + /** + * @return array|null + */ + public function getComments(): ?array + { + return $this->comments; + } + + /** + * @return array|null + */ + public function getTimestampsUnix(): ?array + { + return $this->timestamps_unix; + } + + /** + * @return array|null + */ + public function getVideo(): ?array + { + return $this->video; + } + + /** + * @return array|null + */ + public function getAudio(): ?array + { + return $this->audio; + } + + public function getEncoding(): ?string + { + return $this->encoding; + } + + public function getHinting(): bool + { + return $this->hinting; + } + + public function getController(): ?string + { + return $this->controller; + } + + public function getFtyp(): ?Id3AudioQuicktimeItem + { + return $this->ftyp; + } + + public function getTimeScale(): ?int + { + return $this->time_scale; + } + + public function getDisplayScale(): ?int + { + return $this->display_scale; + } + + /** + * @return int[]|null + */ + public function getSttsFramecount(): ?array + { + return $this->stts_framecount; + } + + public function getFree(): ?Id3AudioQuicktimeItem + { + return $this->free; + } + + public function getWide(): ?Id3AudioQuicktimeItem + { + return $this->wide; + } + + public function getMdat(): ?Id3AudioQuicktimeItem + { + return $this->mdat; + } +} diff --git a/src/Id3/Reader/Id3AudioQuicktimeChapter.php b/src/Id3/Reader/Id3AudioQuicktimeChapter.php new file mode 100644 index 0000000..be6c790 --- /dev/null +++ b/src/Id3/Reader/Id3AudioQuicktimeChapter.php @@ -0,0 +1,38 @@ +timestamp; + } + + public function getTitle(): ?string + { + return $this->title; + } +} diff --git a/src/Id3/Reader/Id3AudioQuicktimeItem.php b/src/Id3/Reader/Id3AudioQuicktimeItem.php new file mode 100644 index 0000000..4980e98 --- /dev/null +++ b/src/Id3/Reader/Id3AudioQuicktimeItem.php @@ -0,0 +1,78 @@ +hierarchy; + } + + public function getName(): ?string + { + return $this->name; + } + + public function getSize(): ?int + { + return $this->size; + } + + public function getOffset(): ?int + { + return $this->offset; + } + + public function getSignature(): ?string + { + return $this->signature; + } + + public function getUnknown1(): ?int + { + return $this->unknown_1; + } + + public function getFourcc(): ?string + { + return $this->fourcc; + } +} diff --git a/src/Models/AudioMetadata.php b/src/Models/AudioMetadata.php index 4e049dc..6900d6a 100644 --- a/src/Models/AudioMetadata.php +++ b/src/Models/AudioMetadata.php @@ -5,14 +5,17 @@ use DateTime; use Kiwilan\Audio\Audio; use Kiwilan\Audio\Id3\Id3Reader; +use Kiwilan\Audio\Id3\Reader\Id3AudioQuicktime; class AudioMetadata { protected function __construct( protected ?int $file_size = null, protected ?string $data_format = null, + protected ?array $warning = [], protected ?string $encoding = null, protected ?string $mime_type = null, + protected ?Id3AudioQuicktime $quicktime = null, protected ?float $duration_seconds = null, protected ?int $bitrate = null, protected ?string $bitrate_mode = null, @@ -42,8 +45,10 @@ public static function make(Audio $audio, Id3Reader $id3_reader): self return new self( file_size: $id3_reader->getFileSize(), data_format: $audio?->data_format, + warning: $id3_reader->getWarning(), encoding: $id3_reader->getEncoding(), mime_type: $id3_reader->getMimeType(), + quicktime: $id3_reader->getQuicktime(), duration_seconds: $id3_reader->getPlaytimeSeconds(), bitrate: intval($id3_reader->getBitrate()), bitrate_mode: $audio?->bitrate_mode, @@ -93,6 +98,16 @@ public function getDataFormat(): ?string return $this->data_format; } + /** + * Get warning of the audio file + * + * @return string[] + */ + public function getWarning(): array + { + return $this->warning; + } + /** * Get encoding of the audio file, like `UTF-8`, `ISO-8859-1`, `etc` */ @@ -109,6 +124,14 @@ public function getMimeType(): ?string return $this->mime_type; } + /** + * Get quicktime data of the audio file, if available + */ + public function getQuicktime(): ?Id3AudioQuicktime + { + return $this->quicktime; + } + /** * Get duration of the audio file in seconds, like `11.05` */ diff --git a/tests/AudioMetadataTest.php b/tests/AudioMetadataTest.php index ec5a0cc..005c930 100644 --- a/tests/AudioMetadataTest.php +++ b/tests/AudioMetadataTest.php @@ -1,6 +1,9 @@ toArray())->toBeArray(); })->with([...AUDIO]); + +it('can read warning', function () { + $audio = Audio::read(AUDIOBOOK_RH_NOCOVER); + $metadata = $audio->getMetadata(); + + expect($metadata->getWarning())->toBeArray(); +}); + +it('can read quicktime', function () { + $audio = Audio::read(AUDIOBOOK_RH_NOCOVER); + $quicktime = $audio->getMetadata()->getQuicktime(); + + expect($quicktime)->toBeInstanceOf(Id3AudioQuicktime::class); + expect($quicktime->getHinting())->toBeBool(); + expect($quicktime->getController())->toBeString(); + + expect($quicktime->getFtyp())->toBeInstanceOf(Id3AudioQuicktimeItem::class); + expect($quicktime->getFtyp()->getFourcc())->toBeString(); + expect($quicktime->getFtyp()->getHierarchy())->toBeString(); + expect($quicktime->getFtyp()->getName())->toBeString(); + expect($quicktime->getFtyp()->getOffset())->toBeInt(); + expect($quicktime->getFtyp()->getSignature())->toBeString(); + expect($quicktime->getFtyp()->getSize())->toBeInt(); + expect($quicktime->getFtyp()->getUnknown1())->toBeInt(); + + expect($quicktime->getTimestampsUnix())->toBeArray(); + expect($quicktime->getTimeScale())->toBeInt(); + expect($quicktime->getDisplayScale())->toBeInt(); + expect($quicktime->getVideo())->toBeArray(); + expect($quicktime->getAudio())->toBeArray(); + expect($quicktime->getSttsFramecount())->toBeArray(); + + expect($quicktime->getSttsFramecount())->toBeArray(); + expect($quicktime->getSttsFramecount())->each(fn (Pest\Expectation $i) => expect($i->value)->toBeInt()); + + expect($quicktime->getComments())->toBeArray(); + + expect($quicktime->getChapters())->toBeArray(); + expect($quicktime->getChapters())->each(fn (Pest\Expectation $i) => expect($i->value)->toBeInstanceOf(Id3AudioQuicktimeChapter::class)); + + expect($quicktime->getFree())->toBeInstanceOf(Id3AudioQuicktimeItem::class); + expect($quicktime->getWide())->toBeInstanceOf(Id3AudioQuicktimeItem::class); + expect($quicktime->getMdat())->toBeInstanceOf(Id3AudioQuicktimeItem::class); + expect($quicktime->getEncoding())->toBeString(); +}); diff --git a/tests/AudiobookTest.php b/tests/AudiobookTest.php index 53eacfb..b91796a 100644 --- a/tests/AudiobookTest.php +++ b/tests/AudiobookTest.php @@ -3,6 +3,7 @@ use Kiwilan\Audio\Audio; use Kiwilan\Audio\Enums\AudioFormatEnum; use Kiwilan\Audio\Enums\AudioTypeEnum; +use Kiwilan\Audio\Id3\Reader\Id3AudioQuicktimeChapter; use Kiwilan\Audio\Models\AudioMetadata; it('can read audiobook', function () { @@ -139,3 +140,15 @@ expect(count($audio->getRaw()))->toBe(15); expect(count($audio->getRaw('id3v2')))->toBe(15); })->with([AUDIOBOOK_MP3]); + +it('can read chapters', function () { + $audio = Audio::read(AUDIOBOOK_RH_NOCOVER); + $quicktime = $audio->getMetadata()->getQuicktime(); + + expect($quicktime->getChapters())->toBeArray(); + expect($quicktime->getChapters())->each(fn (Pest\Expectation $chapter) => expect($chapter->value)->toBeInstanceOf(Id3AudioQuicktimeChapter::class)); + + $first = $quicktime->getChapters()[0]; + expect($first->getTimestamp())->toBe(0); + expect($first->getTitle())->toBe('Chapter 01'); +});