diff --git a/.github/workflows/phpstan-php-code.yml b/.github/workflows/phpstan-php-code.yml new file mode 100644 index 0000000..a222dfc --- /dev/null +++ b/.github/workflows/phpstan-php-code.yml @@ -0,0 +1,14 @@ +name: PHPStan PHP code issues + +on: [push] + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: php-actions/composer@v6 + - uses: php-actions/phpstan@v3 + with: + path: src/ diff --git a/.gitignore b/.gitignore index 7c784c0..9ee9c98 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ psalm.xml vendor .php-cs-fixer.cache .DS_Store +.phpunit.cache +version-draft.md diff --git a/README.md b/README.md index 2951778..0db74de 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Audio files can use different formats, this package aims to provide a simple way ## Requirements -- PHP >= 8.1 +- PHP `8.1` minimum - Optional for update - `FLAC`: `flac` (with `apt`, `brew` or `scoop`) - `OGG`: `vorbis-tools` (with `apt` or `brew`) / `extras/icecast` (with `scoop`) @@ -32,7 +32,7 @@ Audio files can use different formats, this package aims to provide a simple way ## Installation -You can install the package via composer: +You can install the package via [composer](https://getcomposer.org/): ```bash composer require kiwilan/php-audio @@ -45,7 +45,7 @@ Core metadata: ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); $audio->getTitle(); // `?string` to get title $audio->getArtist(); // `?string` to get artist @@ -58,15 +58,15 @@ $audio->getAlbumArtist(); // `?string` to get album artist $audio->getComposer(); // `?string` to get composer $audio->getDiscNumber(); // `?string` to get disc number $audio->isCompilation(); // `bool` to know if is compilation -$audio->getCreationDate(); // `?string` to get creation date (audiobook) -$audio->getCopyright(); // `?string` to get copyright (audiobook) +$audio->getCreationDate(); // `?string` to get creation date +$audio->getCopyright(); // `?string` to get copyright $audio->getEncoding(); // `?string` to get encoding -$audio->getDescription(); // `?string` to get description (audiobook) -$audio->getPodcastDescription(); // `?string` to get podcast description (audiobook) +$audio->getDescription(); // `?string` to get description +$audio->getSynopsis(); // `?string` to get synopsis $audio->getLanguage(); // `?string` to get language -$audio->getLyrics(); // `?string` (audiobook) -$audio->getStik(); // `?string` (audiobook) +$audio->getLyrics(); // `?string` $audio->getDuration(); // `?float` to get duration in seconds +$audio->getDurationHuman(); // `?string` to get duration in human readable format ``` Raw tags: @@ -74,15 +74,14 @@ Raw tags: ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); -$audio->getTags(); // `array` with all tags -$title = $audio->getTag('title'); // `?string` to get title same as `$audio->getTitle()` +$raw_all = $audio->getRawAll(); // `array` with all tags +$raw = $audio->getRaw(); // `array` with main tag +$title = $audio->getRawKey('title'); // `?string` to get title same as `$audio->getTitle()` -$formats = $audio->getAudioFormats(); // `array` with all audio formats - -$format = $audio->getTags('id3v2'); // `?array` with all tags with format `id3v2` -$title = $audio->getTag('title', 'id3v2'); // `?string` to get title with format `id3v2` +$format = $audio->getRaw('id3v2'); // `?array` with all tags with format `id3v2` +$title = $audio->getRawKey('title', 'id3v2'); // `?string` to get title with format `id3v2` ``` Additional metadata: @@ -90,26 +89,23 @@ Additional metadata: ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); $audio->getPath(); // `string` to get path +$audio->getExtension(); // `string` to get extension $audio->hasCover(); // `bool` to know if has cover $audio->isValid(); // `bool` to know if file is valid audio file +$audio->isWritable(); // `bool` to know if file is writable $audio->getFormat(); // `AudioFormatEnum` to get format (mp3, m4a, ...) $audio->getType(); // `?AudioTypeEnum` ID3 type (id3, riff, asf, quicktime, matroska, ape, vorbiscomment) -$audio->getExtras(); // `array` with raw metadata (could contains some metadata not parsed) ``` -Raw audio: - -> [!NOTE] -> -> Cover is removed from `toArray()` method, you can use `getCover()` method to get cover metadata. +You can use `toArray()` method to get raw info: ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); $audio->toArray(); // `array` with all metadata ``` @@ -119,12 +115,10 @@ Advanced properties: ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); -$audio->getReader(); // `?Id3Reader` reader based on `getID3` -$audio->getWriter(); // `?Id3Writer` writer based on `getid3_writetags` -$audio->getStat(); // `AudioStat` (from `stat` function) -$audio->getAudio(); // `?AudioMetadata` with audio metadata +$audio->getId3Reader(); // `?Id3Reader` reader based on `getID3` +$audio->getMetadata(); // `?AudioMetadata` with audio metadata $audio->getCover(); // `?AudioCover` with cover metadata ``` @@ -139,10 +133,10 @@ You can update audio files metadata with `Audio::class`, but not all formats are ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); $audio->getTitle(); // `Title` -$tag = $audio->update() +$tag = $audio->write() ->title('New Title') ->artist('New Artist') ->album('New Album') @@ -154,16 +148,16 @@ $tag = $audio->update() ->composer('New Composer') ->creationDate('2021-01-01') ->description('New Description') + ->synopsis('New Synopsis') ->discNumber('2/2') ->encodingBy('New Encoding By') ->encoding('New Encoding') ->isCompilation() ->lyrics('New Lyrics') - ->stik('New Stik') ->cover('path/to/cover.jpg') // you can use file content `file_get_contents('path/to/cover.jpg')` ->save(); -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); $audio->getTitle(); // `New Title` $audio->getCreationDate(); // `null` because `creationDate` is not supported by `MP3` ``` @@ -172,29 +166,31 @@ Some properties are not supported by all formats, for example `MP3` can't handle #### 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. > -> 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; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); $audio->getAlbumArtist(); // `Band` -$tag = $audio->update() - ->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 = $audio->write() + ->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::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); $audio->getAlbumArtist(); // `New Band` ``` @@ -203,139 +199,119 @@ $audio->getAlbumArtist(); // `New Band` ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); $audio->getAlbumArtist(); // `Band` -$tag = $audio->update() +$tag = $audio->write() ->title('New Title') ->albumArtist('New Band') // `albumArtist` will set `band` for `id3v2`, exception safe ->save(); -$audio = Audio::get('path/to/audio.mp3'); +$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; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); -$tag = $audio->update() +$tag = $audio->write() ->tags([ 'title' => 'New Title', 'title2' => 'New title', // not supported by `id3v2`, will throw an exception ]) - ->preventFailOnError() // will prevent exception + ->skipErrors() // will prevent exception ->save(); ``` -Arrow functions are exception safe for properties but not for unsupported formats. - -```php -use Kiwilan\Audio\Audio; - -$audio = Audio::get('path/to/audio.mp3'); +> [!NOTE] +> +> Arrow functions are exception safe for properties but not for unsupported formats. -$tag = $audio->update() - ->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 - ->save(); -``` +### Raw tags -#### Tags and cover +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`... -Of course you can add cover with `tags` method. +If you want to extract specific field which can be skipped by `Audio::class`, you can use `extras` property. ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); -$cover = 'path/to/cover.jpg'; - -$image = getimagesize($cover); -$coverData = file_get_contents($cover); -$coverPicturetypeid = $image[2]; -$coverDescription = 'cover'; -$coverMime = $image['mime']; - -$tag = $audio->update() - ->tags([ - 'title' => 'New Title', - 'band' => 'New Band', - 'attached_picture' => [ - [ - 'data' => $coverData, - 'picturetypeid' => $coverPicturetypeid, - 'description' => $coverDescription, - 'mime' => $coverMime, - ], - ], - ]) - ->save(); +$audio = Audio::read('path/to/audio.mp3'); +$raw_all = $audio->getRawAll(); // all formats +$raw = $audio->getRaw(); // main format ``` -#### Merge tags - -Merge `tags` with arrow functions. +### AudioMetadata ```php use Kiwilan\Audio\Audio; -$audio = Audio::get($path); - -$tag = $audio->update() - ->title('New Title') // will be merged with `tags` and override `title` key - ->tags([ - 'title' => 'New Title tag', - 'band' => 'New Band', - ]); - -$tag->save(); - -$audio = Audio::get($path); -expect($audio->getTitle())->toBe('New Title'); -expect($audio->getAlbumArtist())->toBe('New Band'); +$audio = Audio::read('path/to/audio.mp3'); +$metadata = $audio->getMetadata(); + +$metadata->getFileSize(); // `?int` in bytes +$metadata->getSizeHuman(); // `?string` (1.2 MB, 1.2 GB, ...) +$metadata->getExtension(); // `?string` (mp3, m4a, ...) +$metadata->getEncoding(); // `?string` (UTF-8...) +$metadata->getMimeType(); // `?string` (audio/mpeg, audio/mp4, ...) +$metadata->getDurationSeconds(); // `?float` in seconds +$metadata->getDurationReadable(); // `?string` (00:00:00) +$metadata->getBitrate(); // `?int` in kbps +$metadata->getBitrateMode(); // `?string` (cbr, vbr, ...) +$metadata->getSampleRate(); // `?int` in Hz +$metadata->getChannels(); // `?int` (1, 2, ...) +$metadata->getChannelMode(); // `?string` (mono, stereo, ...) +$metadata->isLossless(); // `bool` to know if is lossless +$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` +$metadata->getAvDataOffset(); // `?int` in bytes +$metadata->getAvDataEnd(); // `?int` in bytes +$metadata->getFilePath(); // `?string` +$metadata->getFilename(); // `?string` +$metadata->getLastAccessAt(); // `?DateTime` +$metadata->getCreatedAt(); // `?DateTime` +$metadata->getModifiedAt(); // `?DateTime` +$metadata->toArray(); ``` -### Extras +### Quicktime -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`... - -If you want to extract specific field which can be skipped by `Audio::class`, you can use `extras` property. - -```php -use Kiwilan\Audio\Audio; - -$audio = Audio::get('path/to/audio.mp3'); -$extras = $audio->getExtras(); - -$id3v2 = $extras['id3v2'] ?? []; -``` - -### AudioMetadata +For `quicktime` type, like for M4B audiobook, you can use `Id3TagQuicktime` to get more informations. ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); - -$audio->getAudio()->getFilesize(); // `?int` in bytes -$audio->getAudio()->getExtension(); // `?string` (mp3, m4a, ...) -$audio->getAudio()->getEncoding(); // `?string` (UTF-8...) -$audio->getAudio()->getMimeType(); // `?string` (audio/mpeg, audio/mp4, ...) -$audio->getAudio()->getDurationSeconds(); // `?float` in seconds -$audio->getAudio()->getDurationReadable(); // `?string` (00:00:00) -$audio->getAudio()->getBitrate(); // `?int` in kbps -$audio->getAudio()->getBitrateMode(); // `?string` (cbr, vbr, ...) -$audio->getAudio()->getSampleRate(); // `?int` in Hz -$audio->getAudio()->getChannels(); // `?int` (1, 2, ...) -$audio->getAudio()->getChannelMode(); // `?string` (mono, stereo, ...) -$audio->getAudio()->getLossless(); // `bool` to know if is lossless -$audio->getAudio()->getCompressionRatio(); // `?float` +$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 @@ -343,12 +319,13 @@ $audio->getAudio()->getCompressionRatio(); // `?float` ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); +$cover = $audio->getCover(); -$audio->getCover()->getContents(); // `?string` raw file -$audio->getCover()->getMimeType(); // `?string` (image/jpeg, image/png, ...) -$audio->getCover()->getWidth(); // `?int` in pixels -$audio->getCover()->getHeight(); // `?int` in pixels +$cover->getContents(); // `?string` raw file +$cover->getMimeType(); // `?string` (image/jpeg, image/png, ...) +$cover->getWidth(); // `?int` in pixels +$cover->getHeight(); // `?int` in pixels ``` ## Supported formats @@ -413,41 +390,43 @@ You want to add a format? [See FAQ](#faq) `Audio::class` convert some properties to be more readable. -| ID3 type | Original | New property | -| :-------------: | :---------------------: | :------------------: | -| `id3v2` | `band` | `albumArtist` | -| `id3v2` | `track_number` | `trackNumber` | -| `id3v2` | `part_of_a_set` | `discNumber` | -| `id3v2` | `part_of_a_compilation` | `isCompilation` | -| `quicktime` | `track_number` | `trackNumber` | -| `quicktime` | `disc_number` | `discNumber` | -| `quicktime` | `compilation` | `isCompilation` | -| `quicktime` | `creation_date` | `creationDate` | -| `quicktime` | `album_artist` | `albumArtist` | -| `quicktime` | `encoded_by` | `encodingBy` | -| `quicktime` | `encoding_tool` | `encoding` | -| `quicktime` | `description_long` | `podcastDescription` | -| `asf` | `albumartist` | `albumArtist` | -| `asf` | `partofset` | `discNumber` | -| `asf` | `track_number` | `trackNumber` | -| `asf` | `encodingsettings` | `encoding` | -| `vorbiscomment` | `encoder` | `encoding` | -| `vorbiscomment` | `albumartist` | `albumArtist` | -| `vorbiscomment` | `discnumber` | `discNumber` | -| `vorbiscomment` | `compilation` | `isCompilation` | -| `vorbiscomment` | `tracknumber` | `trackNumber` | -| `matroska` | `album_artist` | `albumArtist` | -| `matroska` | `disc` | `discNumber` | -| `matroska` | `part_number` | `trackNumber` | -| `matroska` | `date` | `year` | -| `matroska` | `compilation` | `isCompilation` | -| `matroska` | `encoder` | `encoding` | -| `ape` | `album_artist` | `albumArtist` | -| `ape` | `disc` | `discNumber` | -| `ape` | `compilation` | `isCompilation` | -| `ape` | `track` | `trackNumber` | -| `ape` | `date` | `year` | -| `ape` | `encoder` | `encoding` | +- `ape` format: [`Id3TagApe`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagApe.php) +- `asf` format: [`Id3TagAsf`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagAsf.php) +- `id3v1` format: [`Id3TagAudioV1`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagAudioV1.php) +- `id3v2` format: [`Id3TagAudioV2`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagAudioV2.php) +- `matroska` format: [`Id3TagMatroska`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagMatroska.php) +- `quicktime` format: [`Id3TagQuicktime`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagQuicktime.php) +- `vorbiscomment` format: [`Id3TagVorbisComment`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagVorbisComment.php) +- `riff` format: [`Id3TagRiff`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagRiff.php) +- `unknown` format: [`Id3TagVorbisComment`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagVorbisComment.php) + +| ID3 type | Original | New property | +| :-------------: | :---------------------: | :--------------: | +| `id3v2` | `band` | `album_artist` | +| `id3v2` | `part_of_a_set` | `disc_number` | +| `id3v2` | `part_of_a_compilation` | `is_compilation` | +| `quicktime` | `compilation` | `is_compilation` | +| `quicktime` | `encoded_by` | `encoding_by` | +| `quicktime` | `encoding_tool` | `encoding` | +| `quicktime` | `description_long` | `synopsis` | +| `asf` | `albumartist` | `album_artist` | +| `asf` | `partofset` | `disc_number` | +| `asf` | `encodingsettings` | `encoding` | +| `vorbiscomment` | `encoder` | `encoding` | +| `vorbiscomment` | `albumartist` | `album_artist` | +| `vorbiscomment` | `discnumber` | `disc_number` | +| `vorbiscomment` | `compilation` | `is_compilation` | +| `vorbiscomment` | `tracknumber` | `track_number` | +| `matroska` | `disc` | `disc_number` | +| `matroska` | `part_number` | `track_number` | +| `matroska` | `date` | `year` | +| `matroska` | `compilation` | `is_compilation` | +| `matroska` | `encoder` | `encoding` | +| `ape` | `disc` | `disc_number` | +| `ape` | `compilation` | `is_compilation` | +| `ape` | `track` | `track_number` | +| `ape` | `date` | `year` | +| `ape` | `encoder` | `encoding` | ## Testing @@ -472,11 +451,11 @@ In `Audio::class`, you have a property `extras` which contains all raw metadata, ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); -$extras = $audio->getExtras(); +$audio = Audio::read('path/to/audio.mp3'); +$raw_all = $audio->getRawAll()); $custom = null; -$id3v2 = $extras['id3v2'] ?? []; +$id3v2 = $raw_all['id3v2'] ?? []; if ($id3v2) { $custom = $id3v2['custom'] ?? null; @@ -492,9 +471,9 @@ You can check `extras` property to know if some metadata are available. ```php use Kiwilan\Audio\Audio; -$audio = Audio::get('path/to/audio.mp3'); +$audio = Audio::read('path/to/audio.mp3'); -$extras = $audio->getExtras(); +$extras = $audio->getRawAll(); var_dump($extras); ``` @@ -520,10 +499,9 @@ Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed re ## Credits -- [Ewilan Rivière](https://github.com/ewilan-riviere): package author -- [JamesHeinrich/getID3](https://github.com/JamesHeinrich/getID3): parser used to read audio files -- [spatie/package-skeleton-php](https://github.com/spatie/package-skeleton-php): package skeleton used to create this package -- Tests files from [p1pdd.com](https://p1pdd.com/) (episode 00) +- [`ewilan-riviere`](https://github.com/ewilan-riviere): package author +- [`JamesHeinrich/getID3`](https://github.com/JamesHeinrich/getID3): parser used to read audio files +- [`spatie/package-skeleton-php`](https://github.com/spatie/package-skeleton-php): package skeleton used to create this package ## License diff --git a/composer.json b/composer.json index 5033e57..346ddc6 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "kiwilan/php-audio", "description": "PHP package to parse and update audio files metadata, with `JamesHeinrich/getID3`.", - "version": "3.0.08", + "version": "4.0.0", "keywords": [ "audio", "php", @@ -28,7 +28,8 @@ "wma", "wv", "wav", - "webm" + "webm", + "music" ], "homepage": "https://github.com/kiwilan/php-audio", "license": "MIT", @@ -41,10 +42,11 @@ ], "require": { "php": "^8.1", - "james-heinrich/getid3": "^1.9" + "james-heinrich/getid3": "^v1.9.22" }, "require-dev": { - "pestphp/pest": "^1.20", + "pestphp/pest": "^2.0", + "phpstan/phpstan": "^1.12", "laravel/pint": "^1.2", "spatie/ray": "^1.28" }, diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..e5b7e20 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +parameters: + tmpDir: public/build/.phpstan + + paths: + - src + + # The level 9 is the highest level + level: 5 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a7e9c48..966c25d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,10 @@ @@ -24,16 +21,18 @@ - - ./src - - - - + + + - + - + + + ./src + + + \ No newline at end of file diff --git a/src/Audio.php b/src/Audio.php index 4bf8e65..b3525fe 100755 --- a/src/Audio.php +++ b/src/Audio.php @@ -2,81 +2,55 @@ namespace Kiwilan\Audio; +use Kiwilan\Audio\Core\AudioCore; use Kiwilan\Audio\Enums\AudioFormatEnum; use Kiwilan\Audio\Enums\AudioTypeEnum; -use Kiwilan\Audio\Models\AudioCore; +use Kiwilan\Audio\Id3\Id3Reader; +use Kiwilan\Audio\Id3\Id3Writer; use Kiwilan\Audio\Models\AudioCover; use Kiwilan\Audio\Models\AudioMetadata; -use Kiwilan\Audio\Models\AudioStat; -use Kiwilan\Audio\Models\Id3Reader; -use Kiwilan\Audio\Models\Id3Writer; class Audio { - protected ?string $title = null; - - protected ?string $artist = null; - - protected ?string $album = null; - - protected ?string $genre = null; - - protected ?int $year = null; - - protected ?string $trackNumber = null; - - protected ?string $comment = null; - - protected ?string $albumArtist = null; - - protected ?string $composer = null; - - protected ?string $discNumber = null; - - protected bool $isCompilation = false; - - protected ?string $creationDate = null; - - protected ?string $copyright = null; - - protected ?string $encodingBy = null; - - protected ?string $encoding = null; - - protected ?string $description = null; - - protected ?string $podcastDescription = null; - - protected ?string $language = null; - - protected ?string $lyrics = null; - - protected ?string $stik = null; - - protected ?float $duration = null; - - protected array $extras = []; - - protected ?AudioMetadata $audio = null; - - protected bool $hasCover = false; - - protected ?AudioCover $cover = null; - - protected bool $isValid = false; - - protected ?AudioTypeEnum $type = null; - + /** + * @param array $raw_tags_all + */ protected function __construct( protected string $path, protected string $extension, protected AudioFormatEnum $format, - protected AudioStat $stat, - protected Id3Reader $reader, - protected ?Id3Writer $writer = null, + protected ?AudioTypeEnum $type = null, + protected ?AudioMetadata $metadata = null, + protected ?AudioCover $cover = null, + protected ?float $duration = null, + protected bool $is_writable = false, + protected bool $is_valid = false, + protected bool $has_cover = false, + // + protected ?string $title = null, + protected ?string $artist = null, + protected ?string $album = null, + protected ?string $genre = null, + protected ?int $year = null, + protected ?string $track_number = null, + protected ?string $comment = null, + protected ?string $album_artist = null, + protected ?string $composer = null, + protected ?string $disc_number = null, + protected bool $is_compilation = false, + protected ?string $creation_date = null, + protected ?string $copyright = null, + protected ?string $encoding_by = null, + protected ?string $encoding = null, + protected ?string $description = null, + protected ?string $synopsis = null, + protected ?string $language = null, + protected ?string $lyrics = null, + + protected array $raw_tags_all = [], ) {} - public static function get(string $path): self + public static function read(string $path): self { $fileExists = file_exists($path); if (! $fileExists) { @@ -91,303 +65,432 @@ public static function get(string $path): self path: $path, extension: $extension, format: $format ? $format : AudioFormatEnum::unknown, - stat: AudioStat::make($path), - reader: Id3Reader::make($path), ); - if ($self->reader->isWritable()) { - $self->writer = Id3Writer::make($self); + + try { + $id3_reader = Id3Reader::make($path); + + $self->metadata = AudioMetadata::make($self, $id3_reader); + $self->duration = (float) number_format((float) $self->metadata->getDurationSeconds(), 2, '.', ''); + $self->is_writable = $id3_reader->isWritable(); + + $self->parseTags($id3_reader); + } catch (\Throwable $th) { + error_log($th->getMessage()); } - $self->audio = AudioMetadata::make($self); - $self->parse(); return $self; } /** - * Update metadata of audio file. + * @deprecated Use `read()` method instead. + * + * Get audio file from path. */ - public function update(): Id3Writer + public static function get(string $path): self { - return $this->writer; + return self::read($path); } /** - * Get the value of `stat` method. + * Get audio file path, like `/path/to/audio.mp3`. */ - public function getStat(): ?AudioStat + public function getPath(): string { - return $this->stat; + return $this->path; } /** - * `Id3Reader` with metadata. + * Get audio file extension, like `mp3`. */ - public function getReader(): Id3Reader + public function getExtension(): string { - return $this->reader; + return $this->extension; } /** - * `Id3Writer` to update metadata. + * Get audio format if recognized, like `AudioFormatEnum::mp3`. */ - public function getWriter(): ?Id3Writer + public function getFormat(): AudioFormatEnum { - return $this->writer; + return $this->format; } /** - * Get `title` metadata. + * Get audio type if recognized, like `AudioTypeEnum::id3`. */ - public function getTitle(): ?string + public function getType(): ?AudioTypeEnum { - return $this->title; + return $this->type; } /** - * Get `artist` metadata. + * Get audio metadata. */ - public function getArtist(): ?string + public function getMetadata(): ?AudioMetadata { - return $this->artist; + return $this->metadata; } /** - * Get `album` metadata. + * Get audio cover. */ - public function getAlbum(): ?string + public function getCover(): ?AudioCover { - return $this->album; + return $this->cover; + } + + public function getId3Reader(): ?Id3Reader + { + return Id3Reader::make($this->path); + } + + public function write(): Id3Writer + { + return Id3Writer::make($this); } /** - * Get `genre` metadata. + * @deprecated Use `write()` method instead. + * + * Update audio file. */ - public function getGenre(): ?string + public function update(): Id3Writer { - return $this->genre; + return $this->write(); } /** - * Get `year` metadata. + * Get duration of the audio file in seconds, limited to 2 decimals, like `180.66` + * + * To get exact duration, use `getMetadata()->getDurationSeconds()` instead. */ - public function getYear(): ?int + public function getDuration(): ?float { - return $this->year; + return $this->duration; } /** - * Get `trackNumber` metadata. + * Get duration of the audio file in human readable format, like `00:03:00` */ - public function getTrackNumber(): ?string + public function getDurationHuman(): ?string { - return $this->trackNumber; + return gmdate('H:i:s', intval($this->duration)); } /** - * Get `comment` metadata. + * To know if the audio file is writable. */ - public function getComment(): ?string + public function isWritable(): bool { - return $this->comment; + return $this->is_writable; } /** - * Get `albumArtist` metadata. + * To know if the audio file is valid. */ - public function getAlbumArtist(): ?string + public function isValid(): bool { - return $this->albumArtist; + return $this->is_valid; } /** - * Get `composer` metadata. + * To know if the audio file has cover. */ - public function getComposer(): ?string + public function hasCover(): bool { - return $this->composer; + return $this->has_cover; } /** - * Get `discNumber` metadata. + * Get `title` tag, like `Another Brick In The Wall`. */ - public function getDiscNumber(): ?string + public function getTitle(): ?string { - return $this->discNumber; + return $this->title; } /** - * Know if audio file is a compilation. + * Get `artist` tag, like `Pink Floyd`. */ - public function isCompilation(): bool + public function getArtist(): ?string { - return $this->isCompilation; + return $this->artist; } /** - * Get `creationDate` metadata for audiobook. + * Get `album` tag, like `The Wall`. */ - public function getCreationDate(): ?string + public function getAlbum(): ?string { - return $this->creationDate; + return $this->album; } /** - * Get `encodingBy` metadata for audiobook. + * Get `genre` tag, like `Rock`. */ - public function getEncodingBy(): ?string + public function getGenre(): ?string { - return $this->encodingBy; + return $this->genre; } /** - * Get `encoding` metadata for audiobook. + * Get `year` tag, like `1979`. + * + * - For `matroska` format: `date` tag. + * - For `ape` format: `date` tag. */ - public function getEncoding(): ?string + public function getYear(): ?int { - return $this->encoding; + return $this->year; } /** - * Get `encoding` metadata for audiobook. + * Get `track_number` tag, like `1`. + * + * - For `vorbiscomment` format: `track_number` tag. + * - For `matroska` format: `part_number` tag. + * - For `ape` format: `track` tag. */ - public function getCopyright(): ?string + public function getTrackNumber(): ?string { - return $this->copyright; + return $this->track_number; } /** - * Get `description` metadata for audiobook. + * Get `track_number` tag as integer, like `1`. */ - public function getDescription(): ?string + public function getTrackNumberInt(): ?int { - return $this->description; + return $this->track_number ? intval($this->track_number) : null; + } + + /** + * Get `comment` tag, like `Recorded at Abbey Road Studios`. + */ + public function getComment(): ?string + { + return $this->comment; } /** - * Get `podcastDescription` metadata for audiobook. + * Get `album_artist` tag, like `Pink Floyd`. + * + * - For `id3v2` format: `band` tag. + * - For `asf` format: `albumartist` tag. + * - For `vorbiscomment` format: `albumartist` tag. */ - public function getPodcastDescription(): ?string + public function getAlbumArtist(): ?string { - return $this->podcastDescription; + return $this->album_artist; } /** - * Get `language` metadata for audiobook. + * Get `composer` tag, like `Roger Waters`. */ - public function getLanguage(): ?string + public function getComposer(): ?string { - return $this->language; + return $this->composer; } /** - * Get `lyrics` metadata for audiobook. + * Get `disc_number` tag, like `1`. + * + * - For `id3v2` format: `part_of_a_set` tag. + * - For `asf` format: `partofset` tag. + * - For `vorbiscomment` format: `discnumber` tag. + * - For `matroska` format: `disc` tag. + * - For `ape` format: `disc` tag. */ - public function getLyrics(): ?string + public function getDiscNumber(): ?string { - return $this->lyrics; + return $this->disc_number; } /** - * Get `stik` metadata for audiobook. + * Get `disc_number` tag as integer, like `1`. */ - public function getStik(): ?string + public function getDiscNumberInt(): ?int { - return $this->stik; + if (str_contains($this->disc_number, '/')) { + $disc_number = explode('/', $this->disc_number); + + return intval($disc_number[0]); + } + + return $this->disc_number ? intval($this->disc_number) : null; } /** - * Get `duration` in seconds. + * To know if the audio file is a compilation. + * + * - For `id3v2` format: `part_of_a_compilation` tag. + * - For `quicktime` format: `compilation` tag. + * - For `vorbiscomment` format: `compilation` tag. + * - For `matroska` format: `compilation` tag. + * - For `ape` format: `compilation` tag. */ - public function getDuration(): ?float + public function isCompilation(): bool { - return $this->duration; + return $this->is_compilation; } /** - * Get `duration` in human readable format: `00:00:00`. + * Get `creation_date` tag, like `1979-11-30`. + * + * - For `matroska` format: `date` tag. + * - For `ape` format: `date` tag. */ - public function getDurationHumanReadable(): ?string + public function getCreationDate(): ?string { - return gmdate('H:i:s', intval($this->duration)); + return $this->creation_date; } /** - * Know if audio file is valid. + * Get `encoding_by` tag, like `EAC`. */ - public function isValid(): bool + public function getEncodingBy(): ?string { - return $this->isValid; + return $this->encoding_by; } /** - * Get `extras` with raw metadata. + * Get `encoding` tag, like `LAME`. */ - public function getExtras(): array + public function getEncoding(): ?string { - return $this->extras; + return $this->encoding; } /** - * Get `audio` metadata with some audio information. + * Get `copyright` tag, like `© 1979 Pink Floyd`. */ - public function getAudio(): ?AudioMetadata + public function getCopyright(): ?string { - return $this->audio; + return $this->copyright; } /** - * Know if audio file has cover. + * Get `description` tag, like `The Wall is the eleventh studio album by the English rock band Pink Floyd`. */ - public function hasCover(): bool + public function getDescription(): ?string { - return $this->hasCover; + return $this->description; } /** - * Get `cover` metadata with some cover information. + * Get `synopsis` tag, like `The Wall is the eleventh studio album by the English rock band Pink Floyd`. + * + * `description` and `synopsis` are not the same tag, but for many formats, they are the same. */ - public function getCover(): ?AudioCover + public function getSynopsis(): ?string { - return $this->cover; + return $this->synopsis; } /** - * Get `path` of audio file. + * Get `language` tag, like `en`. */ - public function getPath(): string + public function getLanguage(): ?string { - return $this->path; + return $this->language; } /** - * Get `extension` of audio file. + * Get `lyrics` tag, like `We don't need no education`. */ - public function getgetExtension(): string + public function getLyrics(): ?string { - return $this->extension; + return $this->lyrics; } /** - * Get `format` of audio file. + * Get raw tags as array with all formats. + * + * For example, for `mp3` format: `['id3v1' => [...], 'id3v2' => [...]]`. */ - public function getFormat(): AudioFormatEnum + public function getRawAll(): array { - return $this->format; + return $this->raw_tags_all; } /** - * Get `type` of audio file. + * Get raw tags as array with main format. + * + * For example, for `mp3` format, `id3v2` entry will be returned. + * + * @param string|null $format If not provided, main format will be returned. + * @return string[] */ - public function getType(): ?AudioTypeEnum + public function getRaw(?string $format = null): ?array { - return $this->type; + if ($format) { + return $this->raw_tags_all[$format] ?? null; + } + + $tags = match ($this->type) { + AudioTypeEnum::id3 => $this->raw_tags_all['id3v2'] ?? [], + AudioTypeEnum::vorbiscomment => $this->raw_tags_all['vorbiscomment'] ?? [], + AudioTypeEnum::quicktime => $this->raw_tags_all['quicktime'] ?? [], + AudioTypeEnum::matroska => $this->raw_tags_all['matroska'] ?? [], + AudioTypeEnum::ape => $this->raw_tags_all['ape'] ?? [], + AudioTypeEnum::asf => $this->raw_tags_all['asf'] ?? [], + default => [], + }; + + return $tags; } - private function parse(): self + /** + * Get raw tags key from main format. + * + * @param string $key Key name. + * @param string|null $format If not provided, main format will be used. + */ + public function getRawKey(string $key, ?string $format = null): string|int|bool|null { - $raw = $this->getReader()->getRaw(); - $reader = $this->getReader(); + $tags = $this->getRaw($format); + return $tags[$key] ?? null; + } + + public function toArray(): array + { + return [ + 'path' => $this->path, + 'extension' => $this->extension, + 'format' => $this->format, + 'type' => $this->type, + 'metadata' => $this->metadata?->toArray(), + 'cover' => $this->cover?->toArray(), + 'duration' => $this->duration, + 'is_writable' => $this->is_writable, + 'is_valid' => $this->is_valid, + 'has_cover' => $this->has_cover, + 'title' => $this->title, + 'artist' => $this->artist, + 'album' => $this->album, + 'genre' => $this->genre, + 'year' => $this->year, + 'track_number' => $this->track_number, + 'comment' => $this->comment, + 'album_artist' => $this->album_artist, + 'composer' => $this->composer, + 'disc_number' => $this->disc_number, + 'is_compilation' => $this->is_compilation, + 'creation_date' => $this->creation_date, + 'encoding_by' => $this->encoding_by, + 'encoding' => $this->encoding, + 'description' => $this->description, + 'synopsis' => $this->synopsis, + 'language' => $this->language, + 'lyrics' => $this->lyrics, + 'raw_tags_all' => $this->raw_tags_all, + ]; + } + + private function parseTags(?\Kiwilan\Audio\Id3\Id3Reader $id3_reader): self + { $this->type = match ($this->format) { AudioFormatEnum::aac => null, AudioFormatEnum::aif => AudioTypeEnum::id3, @@ -412,128 +515,67 @@ private function parse(): self default => null, }; - $tags = $reader->getTags(); - if (! $tags) { + $tags = $id3_reader->getTags(); + if (! $tags || $tags->is_empty) { return $this; } - $core = null; - if ($this->type === AudioTypeEnum::id3) { - $core = AudioCore::fromId3($tags->id3v1(), $tags->id3v2()); - $this->isValid = true; - } - - if ($this->type === AudioTypeEnum::quicktime) { - $core = AudioCore::fromQuicktime($tags->quicktime()); - $this->isValid = true; + $raw_tags = $id3_reader->getRaw()['tags'] ?? []; + foreach ($raw_tags as $name => $raw_tag) { + $this->raw_tags_all[$name] = Id3Reader::cleanTags($raw_tag); } - if ($this->type === AudioTypeEnum::vorbiscomment) { - $core = AudioCore::fromVorbisComment($tags->vorbiscomment()); - $this->isValid = true; - } - - if ($this->type === AudioTypeEnum::asf) { - $core = AudioCore::fromAsf($tags->asf()); - $this->isValid = true; - } - - if ($this->type === AudioTypeEnum::matroska) { - $core = AudioCore::fromMatroska($tags->matroska()); - $this->isValid = true; - } + $core = match ($this->type) { + AudioTypeEnum::id3 => AudioCore::fromId3($tags->id3v1, $tags->id3v2), + AudioTypeEnum::vorbiscomment => AudioCore::fromVorbisComment($tags->vorbiscomment), + AudioTypeEnum::quicktime => AudioCore::fromQuicktime($tags->quicktime), + AudioTypeEnum::matroska => AudioCore::fromMatroska($tags->matroska), + AudioTypeEnum::ape => AudioCore::fromApe($tags->ape), + AudioTypeEnum::asf => AudioCore::fromAsf($tags->asf), + default => null, + }; - if ($this->type === AudioTypeEnum::ape) { - $core = AudioCore::fromApe($tags->ape()); - $this->isValid = true; + if (! $core) { + return $this; } - $this->coreToProperties($core); - $this->extras = $raw['tags'] ?? []; - - $this->audio = AudioMetadata::make($this); - $this->cover = AudioCover::make($reader->getComments()); + $this->convertCore($core); + $this->is_valid = true; + $this->cover = AudioCover::make($id3_reader->getComments()); if ($this->cover?->getContents()) { - $this->hasCover = true; + $this->has_cover = true; } - $this->duration = number_format((float) $this->audio->getDurationSeconds(), 2, '.', ''); - return $this; } - private function coreToProperties(?AudioCore $core): self + private function convertCore(?AudioCore $core): self { if (! $core) { return $this; } - $this->title = $core->getTitle(); - $this->artist = $core->getArtist(); - $this->album = $core->getAlbum(); - $this->genre = $core->getGenre(); - $this->year = $core->getYear(); - $this->trackNumber = $core->getTrackNumber(); - $this->comment = $core->getComment(); - $this->albumArtist = $core->getAlbumArtist(); - $this->composer = $core->getComposer(); - $this->discNumber = $core->getDiscNumber(); - $this->isCompilation = $core->isCompilation(); - $this->creationDate = $core->getCreationDate(); - $this->encodingBy = $core->getEncodingBy(); - $this->encoding = $core->getEncoding(); - $this->copyright = $core->getCopyright(); - $this->description = $core->getDescription(); - $this->podcastDescription = $core->getPodcastDescription(); - $this->language = $core->getLanguage(); - $this->lyrics = $core->getLyrics(); - $this->stik = $core->getStik(); + $this->title = $core->title; + $this->artist = $core->artist; + $this->album = $core->album; + $this->genre = $core->genre; + $this->year = $core->year; + $this->track_number = $core->track_number; + $this->comment = $core->comment; + $this->album_artist = $core->album_artist; + $this->composer = $core->composer; + $this->disc_number = $core->disc_number; + $this->is_compilation = $core->is_compilation; + $this->creation_date = $core->creation_date; + $this->encoding_by = $core->encoding_by; + $this->encoding = $core->encoding; + $this->copyright = $core->copyright; + $this->description = $core->description; + $this->synopsis = $core->synopsis; + $this->language = $core->language; + $this->lyrics = $core->lyrics; return $this; } - - /** - * Get a specific tag. - * - * @param string $tag Tag name. - * @param string|null $audioFormat Get a specific format, default format is format with maximum tags. - */ - public function getTag(string $tag, ?string $audioFormat = null): ?string - { - $tags = $this->reader->toTags($audioFormat); - - return $tags[$tag] ?? null; - } - - /** - * Get all tags as array. - * - * @param string|null $tag Get a specific format, default format is format with maximum tags. - * @return array - */ - public function getTags(?string $audioFormat = null): array - { - return $this->reader->toTags($audioFormat); - } - - /** - * Get all audio formats as array, with tags. - * - * @return array> - */ - public function getAudioFormats(): array - { - return $this->reader->toAudioFormats(); - } - - /** - * Get all raw metadata as array. - * - * @return array - */ - public function toArray(): array - { - return $this->reader->toArray(); - } } diff --git a/src/Core/AudioCore.php b/src/Core/AudioCore.php new file mode 100644 index 0000000..51858e1 --- /dev/null +++ b/src/Core/AudioCore.php @@ -0,0 +1,366 @@ + $value !== null); + $properties = array_filter($properties, fn ($value) => $value !== ''); + + return $properties; + } + + private function parseCompilation(AudioCore $core): ?string + { + if ($core->is_compilation === null) { + return null; + } + + return $core->is_compilation ? '1' : '0'; + } + + public static function toId3v2(AudioCore $core): Tag\Id3TagAudioV2 + { + return new Tag\Id3TagAudioV2( + album: $core->album, + artist: $core->artist, + band: $core->album_artist, + comment: $core->comment, + composer: $core->composer, + part_of_a_set: $core->disc_number, + genre: $core->genre, + part_of_a_compilation: $core->parseCompilation($core), + title: $core->title, + track_number: $core->track_number, + year: (string) $core->year, + copyright: $core->copyright, + unsynchronised_lyric: $core->lyrics, + language: $core->language, + ); + } + + public static function toId3v1(AudioCore $core): Tag\Id3TagAudioV1 + { + return new Tag\Id3TagAudioV1( + album: $core->album, + artist: $core->artist, + comment: $core->comment, + genre: $core->genre, + title: $core->title, + track_number: $core->track_number, + year: (string) $core->year, + ); + } + + public static function toVorbisComment(AudioCore $core): Tag\Id3TagVorbisComment + { + return new Tag\Id3TagVorbisComment( + album: $core->album, + artist: $core->artist, + albumartist: $core->album_artist, + comment: $core->comment, + composer: $core->composer, + compilation: $core->parseCompilation($core), + discnumber: $core->disc_number, + genre: $core->genre, + title: $core->title, + tracknumber: $core->track_number, + date: (string) $core->year, + encoder: $core->encoding, + description: $core->description, + ); + } + + public static function toQuicktime(AudioCore $core): Tag\Id3TagQuicktime + { + return new Tag\Id3TagQuicktime( + title: $core->title, + track_number: $core->track_number, + disc_number: $core->disc_number, + compilation: $core->parseCompilation($core), + album: $core->album, + genre: $core->genre, + composer: $core->composer, + creation_date: $core->creation_date, + copyright: $core->copyright, + artist: $core->artist, + album_artist: $core->album_artist, + encoded_by: $core->encoding, + encoding_tool: $core->encoding, + description: $core->description, + description_long: $core->synopsis, + lyrics: $core->lyrics, + comment: $core->comment, + ); + } + + public static function toMatroska(AudioCore $core): Tag\Id3TagMatroska + { + return new Tag\Id3TagMatroska( + title: $core->title, + album: $core->album, + artist: $core->artist, + album_artist: $core->album_artist, + comment: $core->comment, + composer: $core->composer, + disc: $core->disc_number, + compilation: $core->parseCompilation($core), + genre: $core->genre, + part_number: $core->track_number, + date: (string) $core->year, + encoder: $core->encoding, + ); + } + + public static function toApe(AudioCore $core): Tag\Id3TagApe + { + return new Tag\Id3TagApe( + album: $core->album, + artist: $core->artist, + album_artist: $core->album_artist, + comment: $core->comment, + composer: $core->composer, + disc: $core->disc_number, + compilation: $core->parseCompilation($core), + genre: $core->genre, + title: $core->title, + track: $core->track_number, + date: (string) $core->year, + encoder: $core->encoding, + ); + } + + public static function toAsf(AudioCore $core): Tag\Id3TagAsf + { + return new Tag\Id3TagAsf( + album: $core->album, + artist: $core->artist, + albumartist: $core->album_artist, + composer: $core->composer, + partofset: $core->disc_number, + genre: $core->genre, + track_number: $core->track_number, + year: (string) $core->year, + encodingsettings: $core->encoding, + ); + } + + public static function fromId3(?Tag\Id3TagAudioV1 $v1, ?Tag\Id3TagAudioV2 $v2): AudioCore + { + if (! $v1) { + $v1 = new Tag\Id3TagAudioV1; + } + + if (! $v2) { + $v2 = new Tag\Id3TagAudioV2; + } + + return new AudioCore( + album: $v2->album ?? $v1->album, + artist: $v2->artist ?? $v1->artist, + album_artist: $v2->band ?? null, + comment: $v2->comment ?? $v1->comment, + composer: $v2->composer ?? null, + disc_number: $v2->part_of_a_set ?? null, + genre: $v2->genre ?? $v1->genre, + is_compilation: $v2->part_of_a_compilation === '1', + title: $v2->title ?? $v1->title, + track_number: $v2->track_number ?? $v1->track_number, + year: (int) ($v2->year ?? $v1->year), + copyright: $v2->copyright ?? null, + description: $v2->text ?? null, + lyrics: $v2->unsynchronised_lyric ?? null, + language: $v2->language ?? null, + ); + } + + public static function fromId3v2(Tag\Id3TagAudioV2 $tag): AudioCore + { + return new AudioCore( + album: $tag->album, + artist: $tag->artist, + album_artist: $tag->band, + comment: $tag->comment, + composer: $tag->composer, + disc_number: $tag->part_of_a_set, + genre: $tag->genre, + is_compilation: $tag->part_of_a_compilation === '1', + title: $tag->title, + track_number: $tag->track_number, + year: (int) $tag->year, + ); + } + + public static function fromId3v1(Tag\Id3TagAudioV1 $tag): AudioCore + { + return new AudioCore( + album: $tag->album, + artist: $tag->artist, + comment: $tag->comment, + genre: $tag->genre, + title: $tag->title, + track_number: $tag->track_number, + year: (int) $tag->year, + ); + } + + public static function fromQuicktime(Tag\Id3TagQuicktime $tag): AudioCore + { + $date = $tag->creation_date; + $description = $tag->description; + $description_long = $tag->description_long; + + $creation_date = null; + $year = null; + + if ($date) { + if (strlen($date) === 4) { + $year = (int) $date; + } else { + try { + $parsedCreationDate = new \DateTimeImmutable($date); + } catch (\Exception $e) { + // ignore the issue so the rest of the data will be available + } + + if (! empty($parsedCreationDate)) { + $creation_date = $parsedCreationDate->format('Y-m-d\TH:i:s\Z'); + $year = (int) $parsedCreationDate->format('Y'); + } + } + } + + $core = new AudioCore( + title: $tag->title, + artist: $tag->artist, + album: $tag->album, + genre: $tag->genre, + track_number: $tag->track_number, + disc_number: $tag->disc_number, + composer: $tag->composer, + is_compilation: $tag->compilation === '1', + comment: $tag->comment, + album_artist: $tag->album_artist, + encoding_by: $tag->encoded_by, + encoding: $tag->encoding_tool, + language: $tag->language, + copyright: $tag->copyright, + description: $description, + synopsis: $description_long, + lyrics: $tag->lyrics, + creation_date: $creation_date, + year: $year, + ); + + return $core; + } + + public static function fromVorbisComment(Tag\Id3TagVorbisComment $tag): AudioCore + { + return new AudioCore( + title: $tag->title, + artist: $tag->artist, + album: $tag->album, + genre: $tag->genre, + track_number: $tag->tracknumber, + comment: $tag->comment, + album_artist: $tag->albumartist, + composer: $tag->composer, + disc_number: $tag->discnumber, + is_compilation: $tag->compilation === '1', + year: (int) $tag->date, + encoding: $tag->encoder, + description: $tag->description, + ); + } + + public static function fromAsf(Tag\Id3TagAsf $tag): AudioCore + { + return new AudioCore( + title: $tag->title, + artist: $tag->artist, + album: $tag->album, + album_artist: $tag->albumartist, + composer: $tag->composer, + disc_number: $tag->partofset, + genre: $tag->genre, + track_number: $tag->track_number, + year: (int) $tag->year, + encoding: $tag->encodingsettings, + ); + } + + public static function fromMatroska(Tag\Id3TagMatroska $tag): AudioCore + { + return new AudioCore( + title: $tag->title, + album: $tag->album, + artist: $tag->artist, + album_artist: $tag->album_artist, + comment: $tag->comment, + composer: $tag->composer, + disc_number: $tag->disc, + genre: $tag->genre, + is_compilation: $tag->compilation === 'true', + track_number: $tag->part_number, + year: (int) $tag->date, + encoding: $tag->encoder, + ); + } + + public static function fromApe(Tag\Id3TagApe $tag): AudioCore + { + return new AudioCore( + album: $tag->album, + artist: $tag->artist, + album_artist: $tag->album_artist, + comment: $tag->comment, + composer: $tag->composer, + disc_number: $tag->disc, + genre: $tag->genre, + is_compilation: $tag->compilation === '1', + title: $tag->title, + track_number: $tag->track, + creation_date: $tag->date, + year: $tag->year ?? (int) $tag->date, + encoding: $tag->encoder, + description: $tag->description, + copyright: $tag->copyright, + lyrics: $tag->lyrics, + synopsis: $tag->podcastdesc, + language: $tag->language, + ); + } +} diff --git a/src/Core/AudioCoreCover.php b/src/Core/AudioCoreCover.php new file mode 100644 index 0000000..68799cc --- /dev/null +++ b/src/Core/AudioCoreCover.php @@ -0,0 +1,30 @@ +data = file_exists($pathOrData) + ? base64_encode(file_get_contents($pathOrData)) + : base64_encode($pathOrData); + $self->picture_type_id = $image[2]; + $self->description = 'cover'; + $self->mime = $image['mime']; + + return $self; + } +} diff --git a/src/Id3/Id3Reader.php b/src/Id3/Id3Reader.php new file mode 100644 index 0000000..09e104e --- /dev/null +++ b/src/Id3/Id3Reader.php @@ -0,0 +1,261 @@ +raw = $self->instance->analyze($path); + $self->is_writable = $self->instance->is_writable($path); + $metadata = $self->raw; + + $audio = Id3Audio::make($metadata['audio'] ?? null); + $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) { + $bitrate = intval($bitrate); + } + + $self->version = $metadata['GETID3_VERSION'] ?? null; + $self->file_size = $metadata['filesize'] ?? null; + $self->file_path = $metadata['filepath'] ?? null; + $self->filename = $metadata['filename'] ?? null; + $self->filename_path = $metadata['filenamepath'] ?? null; + $self->av_data_offset = $metadata['avdataoffset'] ?? null; + $self->av_data_end = $metadata['avdataend'] ?? null; + $self->file_format = $metadata['fileformat'] ?? null; + $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; + $self->playtime_seconds = $metadata['playtime_seconds'] ?? null; + $self->bitrate = $bitrate; + $self->playtime_string = $metadata['playtime_string'] ?? null; + + return $self; + } + + public function getInstance(): getID3 + { + return $this->instance; + } + + public function getVersion(): ?string + { + return $this->version; + } + + public function getFileSize(): ?int + { + return $this->file_size; + } + + public function getFilePath(): ?string + { + return $this->file_path; + } + + public function getFilename(): ?string + { + return $this->filename; + } + + public function getFilenamePath(): ?string + { + return $this->filename_path; + } + + public function getAvDataOffset(): ?int + { + return $this->av_data_offset; + } + + public function getAvDataEnd(): ?int + { + return $this->av_data_end; + } + + public function getFileFormat(): ?string + { + return $this->file_format; + } + + public function getAudio(): ?Id3Audio + { + return $this->audio; + } + + public function getTags(): ?Id3AudioTag + { + return $this->tags; + } + + 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; + } + + public function getMimeType(): ?string + { + return $this->mime_type; + } + + public function getMpeg(): mixed + { + return $this->mpeg; + } + + public function getPlaytimeSeconds(): ?float + { + return $this->playtime_seconds; + } + + public function getBitrate(): ?float + { + return $this->bitrate; + } + + public function getPlaytimeString(): ?string + { + return $this->playtime_string; + } + + public function isWritable(): bool + { + return $this->is_writable; + } + + public function getRaw(): array + { + return $this->raw; + } + + public function toTags(?string $audioFormat = null): array + { + $rawTags = $this->raw['tags_html'] ?? []; + + if (count($rawTags) === 0) { + return []; + } + + $tagsItems = []; + if ($audioFormat) { + $tagsItems = $rawTags[$audioFormat] ?? []; + } else { + if (count($rawTags) > 1) { + $entries = []; + foreach ($rawTags as $key => $keyTags) { + $entries[$key] = count($keyTags); + } + $maxKey = array_search(max($entries), $entries); + $tagsItems = $rawTags[$maxKey] ?? []; + } else { + $tagsItems = reset($rawTags); + } + } + + return Id3Reader::cleanTags($tagsItems); + } + + public static function cleanTags(?array $tagsItems): array + { + if (! $tagsItems) { + return []; + } + + $temp = []; + foreach ($tagsItems as $k => $v) { + $temp[$k] = $v[0] ?? null; + } + + $items = []; + foreach ($temp as $k => $v) { + $k = strtolower($k); + $k = str_replace(' ', '_', $k); + $items[$k] = $v; + } + + return $items; + } + + public function toAudioFormats(): array + { + return $this->raw['tags_html'] ?? []; + } + + public function toArray(): array + { + $raw = $this->raw; + $raw['id3v2']['APIC'] = null; + $raw['ape']['items']['cover art (front)'] = null; + $raw['comments'] = null; + + return $raw; + } +} diff --git a/src/Id3/Id3Writer.php b/src/Id3/Id3Writer.php new file mode 100644 index 0000000..15d3d81 --- /dev/null +++ b/src/Id3/Id3Writer.php @@ -0,0 +1,642 @@ + $tags Array for `Id3Writer` format. + * @param string[] $tags_core Tags from dedicated methods. + * @param string[] $tags_custom Tags from `tag()` method. + * @param string[] $tags_custom_bulk Tags from `tags()` method. + * @param string[] $warnings + * @param string[] $errors + * @param string[] $formats Formats to write tags. + */ + protected function __construct( + protected Audio $audio, + protected getid3_writetags $writer, + protected AudioCore $core, + protected array $tags = [], + protected array $tags_core = [], + protected array $tags_current = [], + protected array $tags_custom = [], + protected array $tags_custom_bulk = [], + protected array $warnings = [], + protected array $errors = [], + protected bool $cover_deleted = false, + protected bool $skip_errors = false, + protected array $formats = [], + protected bool $success = false, + ) {} + + public static function make(Audio $audio): self + { + $self = new self( + audio: $audio, + writer: new getid3_writetags, + core: new AudioCore, + ); + + $self->writer->filename = $audio->getPath(); + + return $self; + } + + public function getCore(): AudioCore + { + return $this->core; + } + + /** + * Allow to remove other tags when writing tags. + */ + public function removeOtherTags(): self + { + $this->writer->remove_other_tags = true; + + return $this; + } + + public function title(?string $title): self + { + $this->core->title = $title; + + return $this; + } + + public function artist(?string $artist): self + { + $this->core->artist = $artist; + + return $this; + } + + public function album(?string $album): self + { + $this->core->album = $album; + + return $this; + } + + public function year(string|int|null $year): self + { + if (! $year) { + $this->core->year = null; + + return $this; + } + + $this->core->year = intval($year); + + return $this; + } + + public function genre(?string $genre): self + { + $this->core->genre = $genre; + + return $this; + } + + public function trackNumber(string|int|null $track_number): self + { + if (! $track_number) { + $this->core->track_number = null; + + return $this; + } + + if (is_int($track_number)) { + $track_number = (string) $track_number; + } + + $this->core->track_number = $track_number; + + return $this; + } + + public function discNumber(string|int|null $disc_number): self + { + if (! $disc_number) { + $this->core->disc_number = null; + + return $this; + } + + if (is_int($disc_number)) { + $disc_number = (string) $disc_number; + } + + $this->core->disc_number = $disc_number; + + return $this; + } + + public function composer(?string $composer): self + { + $this->core->composer = $composer; + + return $this; + } + + public function comment(?string $comment): self + { + $this->core->comment = $comment; + + return $this; + } + + public function lyrics(?string $lyrics): self + { + $this->core->lyrics = $lyrics; + + return $this; + } + + public function isCompilation(): self + { + $this->core->is_compilation = true; + + return $this; + } + + public function isNotCompilation(): self + { + $this->core->is_compilation = false; + + return $this; + } + + /** + * Not supported by `id3`. + */ + public function creationDate(?string $creation_date): self + { + $this->core->creation_date = $creation_date; + + return $this; + } + + public function copyright(?string $copyright): self + { + $this->core->copyright = $copyright; + + return $this; + } + + /** + * Not supported by `id3`. + */ + public function encodingBy(?string $encoding_by): self + { + $this->core->encoding_by = $encoding_by; + + return $this; + } + + /** + * Not supported by `id3`. + */ + public function encoding(?string $encoding): self + { + $this->core->encoding = $encoding; + + return $this; + } + + /** + * Not supported by `id3`. + */ + public function description(?string $description): self + { + $this->core->description = $description; + + return $this; + } + + /** + * Not supported by `id3`. + */ + public function synopsis(?string $synopsis): self + { + $this->core->synopsis = $synopsis; + + return $this; + } + + public function language(?string $language): self + { + $this->core->language = $language; + + return $this; + } + + /** + * Set new album artist. + */ + public function albumArtist(?string $album_artist): self + { + $this->core->album_artist = $album_artist; + + return $this; + } + + /** + * To create a copy of the audio file with new tags. + */ + public function path(string $path): self + { + if (file_exists($path)) { + unlink($path); + } + copy($this->audio->getPath(), $path); + + $this->writer->filename = $path; + + return $this; + } + + /** + * Advanced usage only to set tags formats. + * + * @param string[] $tag_formats + */ + public function tagFormats(array $tag_formats): self + { + $this->formats = $tag_formats; + + return $this; + } + + /** + * Remove cover from tags. + */ + public function removeCover(): self + { + $this->cover_deleted = true; + + return $this; + } + + /** + * Update cover is only supported by `id3` format. + * + * @param string $pathOrData Path to cover image or binary data + */ + public function cover(string $pathOrData): self + { + $this->core->cover = AudioCoreCover::make($pathOrData); + $this->core->has_cover = true; + + return $this; + } + + /** + * Add custom tags without dedicated method (can be use multiple times). + * + * To know which key use for each format, see documentation. + * For example, album artist for `id3` encoded files, is `band` key. + * + * @docs https://github.com/kiwilan/php-audio#convert-properties + * + * Example: + * + * ```php + * $audio->write() + * ->tag('series-part', '1') + * ->tag('series', 'The Lord of the Rings'); + * ``` + */ + public function tag(string $key, string|int|bool|null $value): self + { + $this->tags_custom[$key] = $value; + + return $this; + } + + /** + * Alternative to `tag()` method, with a full array of tags. + * + * To know which key use for each format, see documentation. + * For example, album artist for `id3` encoded files, is `band` key. + * + * @docs https://github.com/kiwilan/php-audio#convert-properties + * + * @param array $tags + * + * Example: + * + * ```php + * $audio->write() + * ->tags([ + * 'series-part' => '1', + * 'series' => 'The Lord of the Rings', + * ]); + * ``` + */ + public function tags(array $tags): self + { + $this->tags_custom_bulk = $tags; + + return $this; + } + + /** + * Skip errors when writing tags. + */ + public function skipErrors(): self + { + $this->skip_errors = true; + + return $this; + } + + /** + * Write new tags on file. + */ + public function save(): bool + { + $this->assignFormats(); + $this->assignTagsCurrent(); + $this->assignCoverCurrent(); + $this->assignTagsCore(); + $this->assignTagsCustom(); + + $this->convertToWriter(); + $this->convertCoverToWriter(); + + $this->writer->tagformats = $this->formats; + $this->writer->tag_data = $this->tags; + + $this->success = $this->writer->WriteTags(); + $this->errors = $this->writer->errors; + $this->warnings = $this->writer->warnings; + + $this->handleErrors(); + + return $this->success; + } + + private function handleErrors(): void + { + $errors = implode(', ', $this->errors); + $warnings = implode(', ', $this->warnings); + $errors = strip_tags($errors); + $warnings = strip_tags($warnings); + + $supported = match ($this->audio->getFormat()) { + AudioFormatEnum::flac => true, + AudioFormatEnum::mp3 => true, + AudioFormatEnum::ogg => true, + AudioFormatEnum::m4b => true, + default => false + }; + + if (! $supported && ! $this->skip_errors) { + throw new \Exception("php-audio: format {$this->audio->getFormat()->value} is not supported."); + } + + if (! empty($this->errors)) { + error_log("php-audio: {$errors}"); + } + + if (! empty($this->warnings)) { + error_log("php-audio: {$warnings}"); + } + + if (empty($this->errors) && empty($this->warnings)) { + return; + } + + $msg = 'php-audio: Save tags failed.'; + if ($errors) { + $msg .= " Errors: {$errors}."; + } + if ($warnings) { + $msg .= " Warnings: {$warnings}."; + } + $isSuccess = $this->success ? 'true' : 'false'; + $msg .= " Success: {$isSuccess}."; + error_log($msg); + + if (! $this->skip_errors) { + throw new \Exception($msg); + } + } + + /** + * Parse all tags to convert it to writer format. + */ + private function convertToWriter(): self + { + $tags = []; + + // set current tags + foreach ($this->tags_current as $key => $value) { + $tags[$key] = $value; + } + + // set custom tags + foreach ($this->tags_custom as $key => $value) { + $tags[$key] = $value; + } + + // set custom bulk tags + foreach ($this->tags_custom_bulk as $key => $value) { + $tags[$key] = $value; + } + + // set core tags + foreach ($this->tags_core as $key => $value) { + $tags[$key] = $value; + } + + $this->tags = $this->formatTags($tags); + + $forbiddenKeys = ['totaltracks']; + foreach ($forbiddenKeys as $key) { + if (isset($this->tags[$key])) { + unset($this->tags[$key]); + } + } + + return $this; + } + + private function assignTagsCustom(): self + { + if (empty($this->tags_custom) || empty($this->tags_custom_bulk)) { + return $this; + } + + foreach ($this->tags_custom as $key => $value) { + $this->tags_current[$key] = $value; + } + + foreach ($this->tags_custom_bulk as $key => $value) { + $this->tags_current[$key] = $value; + } + + return $this; + } + + /** + * Assign current cover. + */ + private function assignCoverCurrent(): self + { + // cover deleted + if ($this->cover_deleted) { + $this->core->cover = null; + + return $this; + } + + // skip if no current cover + if (! $this->audio->hasCover()) { + return $this; + } + + // skip if new cover already assigned + if ($this->core->cover !== null) { + return $this; + } + + // get current cover + $this->core->cover = new AudioCoreCover( + data: $this->audio->getCover()->getContents(base64: true), + mime: $this->audio->getCover()->getMimeType(), + ); + + return $this; + } + + /** + * Add cover to writer. + */ + private function convertCoverToWriter(): self + { + if (! in_array($this->audio->getType(), self::ALLOWED_COVER_TYPE)) { + return $this; + } + + // skip if cover not exists + if (! $this->core->cover) { + return $this; + } + + if (! $this->core->cover->data) { + return $this; + } + + // 'CTOC' => $old_tags['id3v2']['CTOC'], + // 'CHAP' => $old_tags['id3v2']['CHAP'], + // 'chapters' => $old_tags['id3v2']['chapters'], + $this->tags['attached_picture'] = [ + [ + 'data' => base64_decode($this->core->cover->data), + 'picturetypeid' => $this->core->cover->picture_type_id ?? 1, + 'description' => $this->core->cover->description ?? 'cover', + 'mime' => $this->core->cover->mime, + ], + ]; + + return $this; + } + + /** + * Assign current tags. + */ + private function assignTagsCurrent(): self + { + $currentTags = []; + if (! $this->writer->remove_other_tags) { + $currentTags = $this->audio->getRaw(); + } + + $this->tags_current = $currentTags; + + return $this; + } + + /** + * Assign new tags from core to array. + */ + private function assignTagsCore(): self + { + $tagFormat = match ($this->audio->getType()) { + AudioTypeEnum::id3 => AudioCore::toId3v2($this->core), + AudioTypeEnum::vorbiscomment => AudioCore::toVorbisComment($this->core), + AudioTypeEnum::quicktime => AudioCore::toQuicktime($this->core), + AudioTypeEnum::matroska => AudioCore::toMatroska($this->core), + AudioTypeEnum::ape => AudioCore::toApe($this->core), + AudioTypeEnum::asf => AudioCore::toAsf($this->core), + default => null, + }; + + if (! $tagFormat) { + return $this; + } + + $this->tags_core = $tagFormat->toArray(); + + return $this; + } + + /** + * Assign formats to know how to write tags. + * + * - ID3v1 (v1 & v1.1) + * - ID3v2 (v2.3, v2.4) + * - APE (v2) + * - Ogg Vorbis comments (need `vorbis-tools`) + * - FLAC comments + * + * Options: `id3v1`, `id3v2.2`, `id2v2.3`, `id3v2.4`, `ape`, `vorbiscomment`, `metaflac`, `real` + */ + private function assignFormats(): self + { + if (! empty($this->formats)) { + return $this; + } + + $this->formats = match ($this->audio->getFormat()) { + AudioFormatEnum::flac => ['metaflac'], + AudioFormatEnum::mp3 => ['id3v1', 'id3v2.4'], + AudioFormatEnum::ogg => ['vorbiscomment'], + default => [], + }; + + return $this; + } + + /** + * Format tags to writer format. + * + * @param array $tags + * @return array + */ + private function formatTags(array $tags): array + { + $items = []; + if (! empty($tags)) { + foreach ($tags as $key => $tag) { + if (gettype($tag) === 'string') { + $items[$key] = [$tag]; + } + } + } + + return $items; + } +} diff --git a/src/Id3/Reader/Id3Audio.php b/src/Id3/Reader/Id3Audio.php new file mode 100644 index 0000000..c476d95 --- /dev/null +++ b/src/Id3/Reader/Id3Audio.php @@ -0,0 +1,60 @@ +streams[0] ?? null; + } +} 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/Id3/Reader/Id3AudioTag.php b/src/Id3/Reader/Id3AudioTag.php new file mode 100644 index 0000000..7b508e1 --- /dev/null +++ b/src/Id3/Reader/Id3AudioTag.php @@ -0,0 +1,56 @@ + $value !== null); + $properties = array_filter($properties, fn ($value) => $value !== ''); + + return $properties; + } +} diff --git a/src/Id3/Tag/Id3TagApe.php b/src/Id3/Tag/Id3TagApe.php new file mode 100644 index 0000000..2c3eb46 --- /dev/null +++ b/src/Id3/Tag/Id3TagApe.php @@ -0,0 +1,60 @@ +title; - } - - public function getArtist(): ?string - { - return $this->artist; - } - - public function getAlbum(): ?string - { - return $this->album; - } - - public function getGenre(): ?string - { - return $this->genre; - } - - public function getYear(): ?int - { - return $this->year; - } - - public function getTrackNumber(): ?string - { - return $this->trackNumber; - } - - public function getComment(): ?string - { - return $this->comment; - } - - public function getAlbumArtist(): ?string - { - return $this->albumArtist; - } - - public function getComposer(): ?string - { - return $this->composer; - } - - public function getDiscNumber(): ?string - { - return $this->discNumber; - } - - public function isCompilation(): bool - { - if ($this->isCompilation === null) { - return false; - } - - return $this->isCompilation; - } - - public function getCreationDate(): ?string - { - return $this->creationDate; - } - - public function getCopyright(): ?string - { - return $this->copyright; - } - - public function getEncodingBy(): ?string - { - return $this->encodingBy; - } - - public function getEncoding(): ?string - { - return $this->encoding; - } - - public function getDescription(): ?string - { - return $this->description; - } - - public function getPodcastDescription(): ?string - { - return $this->podcastDescription; - } - - public function getLanguage(): ?string - { - return $this->language; - } - - public function getLyrics(): ?string - { - return $this->lyrics; - } - - public function getStik(): ?string - { - return $this->stik; - } - - public function hasCover(): bool - { - return $this->hasCover; - } - - public function getCover(): ?AudioCoreCover - { - return $this->cover; - } - - public function setTitle(?string $title): self - { - $this->title = $title; - - return $this; - } - - public function setArtist(?string $artist): self - { - $this->artist = $artist; - - return $this; - } - - public function setAlbum(?string $album): self - { - $this->album = $album; - - return $this; - } - - public function setGenre(?string $genre): self - { - $this->genre = $genre; - - return $this; - } - - public function setYear(int $year): self - { - $this->year = $year; - - return $this; - } - - public function setTrackNumber(?string $trackNumber): self - { - $this->trackNumber = $trackNumber; - - return $this; - } - - public function setComment(?string $comment): self - { - $this->comment = $comment; - - return $this; - } - - public function setAlbumArtist(?string $albumArtist): self - { - $this->albumArtist = $albumArtist; - - return $this; - } - - public function setComposer(?string $composer): self - { - $this->composer = $composer; - - return $this; - } - - public function setDiscNumber(?string $discNumber): self - { - $this->discNumber = $discNumber; - - return $this; - } - - public function setIsCompilation(bool $isCompilation): self - { - $this->isCompilation = $isCompilation; - - return $this; - } - - public function setCreationDate(?string $creationDate): self - { - $this->creationDate = $creationDate; - - return $this; - } - - public function setCopyright(?string $copyright): self - { - $this->copyright = $copyright; - - return $this; - } - - public function setEncodingBy(?string $encodingBy): self - { - $this->encodingBy = $encodingBy; - - return $this; - } - - public function setEncoding(?string $encoding): self - { - $this->encoding = $encoding; - - return $this; - } - - public function setDescription(?string $description): self - { - $this->description = $description; - - return $this; - } - - public function setPodcastDescription(?string $podcastDescription): self - { - $this->podcastDescription = $podcastDescription; - - return $this; - } - - public function setLanguage(?string $language): self - { - $this->language = $language; - - return $this; - } - - public function setLyrics(?string $lyrics): self - { - $this->lyrics = $lyrics; - - return $this; - } - - public function setStik(?string $stik): self - { - $this->stik = $stik; - - return $this; - } - - public function setHasCover(bool $hasCover): self - { - $this->hasCover = $hasCover; - - return $this; - } - - public function setCover(string $pathOrData): self - { - $this->cover = AudioCoreCover::make($pathOrData); - - return $this; - } - - public function toArray(): array - { - return [ - 'title' => $this->title, - 'artist' => $this->artist, - 'album' => $this->album, - 'genre' => $this->genre, - 'year' => $this->year, - 'trackNumber' => $this->trackNumber, - 'comment' => $this->comment, - 'albumArtist' => $this->albumArtist, - 'composer' => $this->composer, - 'discNumber' => $this->discNumber, - 'isCompilation' => $this->isCompilation, - 'creationDate' => $this->creationDate, - 'encodingBy' => $this->encodingBy, - 'encoding' => $this->encoding, - 'description' => $this->description, - 'podcastDescription' => $this->podcastDescription, - 'language' => $this->language, - 'lyrics' => $this->lyrics, - 'stik' => $this->stik, - 'hasCover' => $this->hasCover, - 'cover' => $this->cover?->toArray(), - ]; - } - - public static function toId3v2(AudioCore $core): Id3AudioTagV2 - { - return new Id3AudioTagV2( - album: $core->getAlbum(), - artist: $core->getArtist(), - band: $core->getAlbumArtist(), - comment: $core->getComment(), - composer: $core->getComposer(), - part_of_a_set: $core->getDiscNumber(), - genre: $core->getGenre(), - part_of_a_compilation: $core->isCompilation() ? '1' : '0', - title: $core->getTitle(), - track_number: $core->getTrackNumber(), - year: $core->getYear(), - copyright: $core->getCopyright(), - text: $core->getPodcastDescription(), - unsynchronised_lyric: $core->getLyrics(), - language: $core->getLanguage(), - ); - } - - public static function toId3v1(AudioCore $core): Id3AudioTagV1 - { - return new Id3AudioTagV1( - album: $core->getAlbum(), - artist: $core->getArtist(), - comment: $core->getComment(), - genre: $core->getGenre(), - title: $core->getTitle(), - track_number: $core->getTrackNumber(), - year: $core->getYear(), - ); - } - - public static function toVorbisComment(AudioCore $core): Id3TagVorbisComment - { - return new Id3TagVorbisComment( - album: $core->getAlbum(), - artist: $core->getArtist(), - albumartist: $core->getAlbumArtist(), - comment: $core->getComment(), - composer: $core->getComposer(), - compilation: $core->isCompilation() ? '1' : '0', - discnumber: $core->getDiscNumber(), - genre: $core->getGenre(), - title: $core->getTitle(), - tracknumber: $core->getTrackNumber(), - date: $core->getYear(), - encoder: $core->getEncoding(), - description: $core->getDescription(), - ); - } - - public static function toQuicktime(AudioCore $core): Id3TagQuicktime - { - return new Id3TagQuicktime( - title: $core->getTitle(), - track_number: $core->getTrackNumber(), - disc_number: $core->getDiscNumber(), - compilation: $core->isCompilation() ? '1' : '0', - album: $core->getAlbum(), - genre: $core->getGenre(), - composer: $core->getComposer(), - creation_date: $core->getCreationDate(), - copyright: $core->getCopyright(), - artist: $core->getArtist(), - album_artist: $core->getAlbumArtist(), - encoded_by: $core->getEncoding(), - encoding_tool: $core->getEncoding(), - description: $core->getDescription(), - description_long: $core->getPodcastDescription(), - lyrics: $core->getLyrics(), - comment: $core->getComment(), - stik: $core->getStik(), - ); - } - - public static function toMatroska(AudioCore $core): Id3TagMatroska - { - return new Id3TagMatroska( - title: $core->getTitle(), - album: $core->getAlbum(), - artist: $core->getArtist(), - album_artist: $core->getAlbumArtist(), - comment: $core->getComment(), - composer: $core->getComposer(), - disc: $core->getDiscNumber(), - compilation: $core->isCompilation() ? '1' : '0', - genre: $core->getGenre(), - part_number: $core->getTrackNumber(), - date: $core->getYear(), - encoder: $core->getEncoding(), - ); - } - - public static function toApe(AudioCore $core): Id3TagApe - { - return new Id3TagApe( - album: $core->getAlbum(), - artist: $core->getArtist(), - album_artist: $core->getAlbumArtist(), - comment: $core->getComment(), - composer: $core->getComposer(), - disc: $core->getDiscNumber(), - compilation: $core->isCompilation() ? '1' : '0', - genre: $core->getGenre(), - title: $core->getTitle(), - track: $core->getTrackNumber(), - date: $core->getYear(), - encoder: $core->getEncoding(), - ); - } - - public static function toAsf(AudioCore $core): Id3TagAsf - { - return new Id3TagAsf( - album: $core->getAlbum(), - artist: $core->getArtist(), - albumartist: $core->getAlbumArtist(), - composer: $core->getComposer(), - partofset: $core->getDiscNumber(), - genre: $core->getGenre(), - track_number: $core->getTrackNumber(), - year: $core->getYear(), - encodingsettings: $core->getEncoding(), - ); - } - - public static function fromId3(?Id3AudioTagV1 $v1, ?Id3AudioTagV2 $v2): AudioCore - { - if (! $v1) { - $v1 = new Id3AudioTagV1; - } - - if (! $v2) { - $v2 = new Id3AudioTagV2; - } - - return new AudioCore( - album: $v2->album() ?? $v1->album(), - artist: $v2->artist() ?? $v1->artist(), - albumArtist: $v2->band() ?? null, - comment: $v2->comment() ?? $v1->comment(), - composer: $v2->composer() ?? null, - discNumber: $v2->part_of_a_set() ?? null, - genre: $v2->genre() ?? $v1->genre(), - isCompilation: $v2->part_of_a_compilation() === '1', - title: $v2->title() ?? $v1->title(), - trackNumber: $v2->track_number() ?? $v1->track_number(), - year: $v2->year() ?? $v1->year(), - copyright: $v2->copyright() ?? null, - description: $v2->text() ?? null, - lyrics: $v2->unsynchronised_lyric() ?? null, - language: $v2->language() ?? null, - ); - } - - public static function fromId3v2(Id3AudioTagV2 $tag): AudioCore - { - return new AudioCore( - album: $tag->album(), - artist: $tag->artist(), - albumArtist: $tag->band(), - comment: $tag->comment(), - composer: $tag->composer(), - discNumber: $tag->part_of_a_set(), - genre: $tag->genre(), - isCompilation: $tag->part_of_a_compilation() === '1', - title: $tag->title(), - trackNumber: $tag->track_number(), - year: $tag->year(), - ); - } - - public static function fromId3v1(Id3AudioTagV1 $tag): AudioCore - { - return new AudioCore( - album: $tag->album(), - artist: $tag->artist(), - comment: $tag->comment(), - genre: $tag->genre(), - title: $tag->title(), - trackNumber: $tag->track_number(), - year: $tag->year(), - ); - } - - public static function fromQuicktime(Id3TagQuicktime $tag): AudioCore - { - - $creation_date = $tag->creation_date(); - $description = $tag->description(); - $description_long = $tag->description_long(); - - $core = new AudioCore( - title: $tag->title(), - artist: $tag->artist(), - album: $tag->album(), - genre: $tag->genre(), - trackNumber: $tag->track_number(), - discNumber: $tag->disc_number(), - composer: $tag->composer(), - isCompilation: $tag->compilation() === '1', - comment: $tag->comment(), - albumArtist: $tag->album_artist(), - encodingBy: $tag->encoded_by(), - encoding: $tag->encoding_tool(), - language: $tag->language(), - ); - - if ($creation_date) { - if (strlen($creation_date) === 4) { - $core->setYear((int) $creation_date); - } else { - try { - $parsedCreationDate = new \DateTimeImmutable($creation_date); - } catch (\Exception $e) { - // ignore the issue so the rest of the data will be available - } - - if (! empty($parsedCreationDate)) { - $core->setCreationDate($parsedCreationDate->format('Y-m-d\TH:i:s\Z')); - $core->setYear((int) $parsedCreationDate->format('Y')); - } - } - } - - $core->setCopyright($tag->copyright()); - $core->setDescription($description); - $core->setPodcastDescription($description_long); - $core->setLyrics($tag->lyrics()); - $core->setStik($tag->stik()); - - return $core; - } - - public static function fromVorbisComment(Id3TagVorbisComment $tag): AudioCore - { - return new AudioCore( - title: $tag->title(), - artist: $tag->artist(), - album: $tag->album(), - genre: $tag->genre(), - trackNumber: $tag->trackNumber(), - comment: $tag->comment(), - albumArtist: $tag->albumartist(), - composer: $tag->composer(), - discNumber: $tag->discNumber(), - isCompilation: $tag->compilation() === '1', - year: (int) $tag->date(), - encoding: $tag->encoder(), - description: $tag->description(), - ); - } - - public static function fromAsf(Id3TagAsf $tag): AudioCore - { - return new AudioCore( - title: $tag->title(), - artist: $tag->artist(), - album: $tag->album(), - albumArtist: $tag->albumartist(), - composer: $tag->composer(), - discNumber: $tag->partofset(), - genre: $tag->genre(), - trackNumber: $tag->track_number(), - year: (int) $tag->year(), - encoding: $tag->encodingsettings(), - ); - } - - public static function fromMatroska(Id3TagMatroska $tag): AudioCore - { - return new AudioCore( - title: $tag->title(), - album: $tag->album(), - artist: $tag->artist(), - albumArtist: $tag->album_artist(), - comment: $tag->comment(), - composer: $tag->composer(), - discNumber: $tag->disc(), - genre: $tag->genre(), - isCompilation: $tag->compilation(), - trackNumber: $tag->part_number(), - year: (int) $tag->date(), - encoding: $tag->encoder(), - ); - } - - public static function fromApe(Id3TagApe $tag): AudioCore - { - return new AudioCore( - album: $tag->album(), - artist: $tag->artist(), - albumArtist: $tag->album_artist(), - comment: $tag->comment(), - composer: $tag->composer(), - discNumber: $tag->disc(), - genre: $tag->genre(), - isCompilation: $tag->compilation() === '1', - title: $tag->title(), - trackNumber: $tag->track(), - creationDate: $tag->date(), - year: $tag->year() ?? (int) $tag->date(), - encoding: $tag->encoder(), - description: $tag->description(), - copyright: $tag->copyright(), - lyrics: $tag->lyrics(), - podcastDescription: $tag->podcastdesc(), - language: $tag->language(), - ); - } -} - -class AudioCoreCover -{ - public function __construct( - protected ?string $data = null, - protected ?string $picturetypeid = null, - protected ?string $description = null, - protected ?string $mime = null, - ) {} - - public static function make(string $pathOrData): self - { - $self = new self; - - if (file_exists($pathOrData)) { - $image = getimagesize($pathOrData); - $self->data = base64_encode(file_get_contents($pathOrData)); - $self->picturetypeid = $image[2]; - $self->description = 'cover'; - $self->mime = $image['mime']; - - return $self; - } - - $image = getimagesizefromstring($pathOrData); - $self->data = base64_encode($pathOrData); - $self->picturetypeid = $image[2]; - $self->mime = $image['mime']; - $self->description = 'cover'; - - return $self; - } - - public function data(): ?string - { - return $this->data; - } - - public function picturetypeid(): ?string - { - return $this->picturetypeid; - } - - public function description(): ?string - { - return $this->description; - } - - public function mime(): ?string - { - return $this->mime; - } - - public function toArray(): array - { - return [ - 'data' => $this->data, - 'picturetypeid' => $this->picturetypeid, - 'description' => $this->description, - 'mime' => $this->mime, - ]; - } -} diff --git a/src/Models/AudioCover.php b/src/Models/AudioCover.php index 201c119..fabe1b7 100644 --- a/src/Models/AudioCover.php +++ b/src/Models/AudioCover.php @@ -2,57 +2,86 @@ namespace Kiwilan\Audio\Models; +use Kiwilan\Audio\Id3\Reader\Id3Comments; + class AudioCover { - protected ?string $contents = null; - - protected ?string $mimeType = null; - - protected ?int $width = null; - - protected ?int $height = null; + protected function __construct( + protected ?string $contents = null, + protected ?string $mime_type = null, + protected ?int $width = null, + protected ?int $height = null, + ) {} public static function make(?Id3Comments $comments): ?self { - if (! $comments) { + if (! $comments || ! $comments->picture) { return null; } $self = new self; - $self->contents = $comments->picture()->data(); - $self->mimeType = $comments->picture()->image_mime(); - $self->width = $comments->picture()->image_width(); - $self->height = $comments->picture()->image_height(); + $self->contents = base64_encode($comments->picture->data); + $self->mime_type = $comments->picture->image_mime; + $self->width = $comments->picture->image_width; + $self->height = $comments->picture->image_height; return $self; } /** - * @deprecated Use `getContents()` instead. + * Get the contents of the cover + * + * By default, the contents are decoded from base64, but you can get the raw contents by passing `true` as the first argument. */ - public function getContent(): ?string + public function getContents(bool $base64 = false): ?string { - return $this->contents; - } + if (! $this->contents) { + return null; + } - public function getContents(): ?string - { - return $this->contents; + return $base64 ? $this->contents : base64_decode($this->contents); } + /** + * Get the MIME type of the cover + */ public function getMimeType(): ?string { - return $this->mimeType; + return $this->mime_type; } + /** + * Get the width of the cover + */ public function getWidth(): ?int { return $this->width; } + /** + * Get the height of the cover + */ public function getHeight(): ?int { return $this->height; } + + /** + * Extract the cover to a file. + */ + public function extractCover(string $path): void + { + file_put_contents($path, $this->getContents()); + } + + public function toArray(): array + { + return [ + 'contents' => $this->contents, + 'mime_type' => $this->mime_type, + 'width' => $this->width, + 'height' => $this->height, + ]; + } } diff --git a/src/Models/AudioMetadata.php b/src/Models/AudioMetadata.php index 0e04738..6900d6a 100644 --- a/src/Models/AudioMetadata.php +++ b/src/Models/AudioMetadata.php @@ -2,125 +2,315 @@ namespace Kiwilan\Audio\Models; +use DateTime; use Kiwilan\Audio\Audio; +use Kiwilan\Audio\Id3\Id3Reader; +use Kiwilan\Audio\Id3\Reader\Id3AudioQuicktime; class AudioMetadata { protected function __construct( - protected ?string $path = null, - protected ?int $filesize = null, - protected ?string $extension = null, - protected ?string $dataformat = null, + protected ?int $file_size = null, + protected ?string $data_format = null, + protected ?array $warning = [], protected ?string $encoding = null, - protected ?string $mimeType = null, - protected ?float $durationSeconds = null, - protected ?string $durationReadable = null, + protected ?string $mime_type = null, + protected ?Id3AudioQuicktime $quicktime = null, + protected ?float $duration_seconds = null, protected ?int $bitrate = null, - protected ?string $bitrateMode = null, - protected ?int $sampleRate = null, + protected ?string $bitrate_mode = null, + protected ?int $sample_rate = null, protected ?int $channels = null, - protected ?string $channelMode = null, - protected bool $lossless = false, - protected ?float $compressionRatio = null, + protected ?string $channel_mode = null, + protected bool $is_lossless = false, + protected ?float $compression_ratio = null, + protected ?string $codec = null, + protected ?string $encoder_options = null, + protected ?string $version = null, + protected ?int $av_data_offset = null, + protected ?int $av_data_end = null, + protected ?string $file_path = null, + protected ?string $filename = null, + protected ?DateTime $last_access_at = null, + protected ?DateTime $created_at = null, + protected ?DateTime $modified_at = null, ) {} - public static function make(Audio $audio): self + public static function make(Audio $audio, Id3Reader $id3_reader): self { $path = $audio->getPath(); - $reader = $audio->getReader(); - $audio = $reader->getAudio(); + $audio = $id3_reader->getAudio(); + $stat = stat($path); return new self( - path: $path, - filesize: $reader->getFilesize(), - extension: pathinfo($path, PATHINFO_EXTENSION), - dataformat: $audio?->dataformat(), - encoding: $reader->getEncoding(), - mimeType: $reader->getMimeType(), - durationSeconds: $reader->getPlaytimeSeconds(), - durationReadable: $reader->getPlaytimeString(), - bitrate: $reader->getBitrate(), - bitrateMode: $audio?->bitrate_mode(), - sampleRate: $audio?->sample_rate(), - channels: $audio?->channels(), - channelMode: $audio?->channelmode(), - lossless: $audio?->lossless() ?? false, - compressionRatio: $audio?->compression_ratio(), + 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, + sample_rate: $audio?->sample_rate, + channels: $audio?->channels, + channel_mode: $audio?->channel_mode, + is_lossless: $audio?->lossless ?? false, + compression_ratio: $audio?->compression_ratio, + codec: $audio?->codec, + encoder_options: $audio?->encoder_options, + version: $id3_reader->getVersion(), + av_data_offset: $id3_reader->getAvDataOffset(), + av_data_end: $id3_reader->getAvDataEnd(), + file_path: $id3_reader->getFilePath(), + filename: $id3_reader->getFilename(), + last_access_at: $stat['atime'] ? new DateTime('@'.$stat['atime']) : null, + created_at: $stat['ctime'] ? new DateTime('@'.$stat['ctime']) : null, + modified_at: $stat['mtime'] ? new DateTime('@'.$stat['mtime']) : null, ); } - public function getPath(): ?string + /** + * Get size of the audio file in bytes, like `180664` + */ + public function getFileSize(): ?int { - return $this->path; + return $this->file_size; } - public function getFilesize(): ?int + /** + * Get size of the audio file in human readable format, like `175.99 KB` + */ + public function getSizeHuman(int $decimals = 2): ?string { - return $this->filesize; + $file_size = (string) $this->file_size; + $size = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + $factor = floor((strlen($file_size) - 1) / 3); + + return sprintf("%.{$decimals}f", $file_size / pow(1024, $factor)).' '.$size[$factor]; } - public function getExtension(): ?string + /** + * Get data format of the audio file, like `mp3`, `wav`, `etc` + */ + public function getDataFormat(): ?string { - return $this->extension; + return $this->data_format; } - public function getDataformat(): ?string + /** + * Get warning of the audio file + * + * @return string[] + */ + public function getWarning(): array { - return $this->dataformat; + return $this->warning; } + /** + * Get encoding of the audio file, like `UTF-8`, `ISO-8859-1`, `etc` + */ public function getEncoding(): ?string { return $this->encoding; } + /** + * Get mime type of the audio file, like `audio/x-matroska`, `audio/mpeg`, `etc` + */ public function getMimeType(): ?string { - return $this->mimeType; + return $this->mime_type; } - public function getDurationSeconds(): ?float + /** + * Get quicktime data of the audio file, if available + */ + public function getQuicktime(): ?Id3AudioQuicktime { - return $this->durationSeconds; + return $this->quicktime; } - public function getDurationReadable(): ?string + /** + * Get duration of the audio file in seconds, like `11.05` + */ + public function getDurationSeconds(?int $decimals = null): ?float { - return $this->durationReadable; + if ($decimals !== null) { + return round($this->duration_seconds, $decimals); + } + + return $this->duration_seconds; } + /** + * Get bitrate of the audio file in bits per second, like `128000` + */ public function getBitrate(): ?int { return $this->bitrate; } + /** + * Get bitrate mode of the audio file, like `cbr`, `vbr`, `etc` + */ public function getBitrateMode(): ?string { - return $this->bitrateMode; + return $this->bitrate_mode; } + /** + * Get sample rate of the audio file in hertz, like `44100` + */ public function getSampleRate(): ?int { - return $this->sampleRate; + return $this->sample_rate; } + /** + * Get channels of the audio file, like `2` + */ public function getChannels(): ?int { return $this->channels; } + /** + * Get channel mode of the audio file, like `joint stereo`, `stereo`, `etc` + */ public function getChannelMode(): ?string { - return $this->channelMode; + return $this->channel_mode; + } + + /** + * Get lossless status of the audio file, like `false` + */ + public function isLossless(): bool + { + return $this->is_lossless; + } + + /** + * Get compression ratio of the audio file, like `0.1` + */ + public function getCompressionRatio(?int $decimals = null): ?float + { + if ($decimals !== null) { + return round($this->compression_ratio, $decimals); + } + + return $this->compression_ratio; + } + + /** + * Get codec of the audio file, like `LAME` + */ + public function getCodec(): ?string + { + return $this->codec; + } + + /** + * Get encoder options of the audio file, like `CBR`, `VBR`, `etc` + */ + public function getEncoderOptions(): ?string + { + return $this->encoder_options; + } + + /** + * Get version of `JamesHeinrich/getID3`, like `1.9.23-202310190849` + * + * @docs https://github.com/JamesHeinrich/getID3 + */ + public function getVersion(): ?string + { + return $this->version; + } + + /** + * Get audio/video data offset of the audio file, like `25808` + */ + public function getAvDataOffset(): ?int + { + return $this->av_data_offset; + } + + /** + * Get audio/video data end of the audio file, like `1214046` + */ + public function getAvDataEnd(): ?int + { + return $this->av_data_end; + } + + /** + * Get path of audio file directory, like `/path/to` + */ + public function getFilePath(): ?string + { + return $this->file_path; + } + + /** + * Get filename of the audio file, like `audio.mp3` + */ + public function getFilename(): ?string + { + return $this->filename; + } + + /** + * Get last access time of the audio file, like `2021-09-01 00:00:00` + */ + public function getLastAccessAt(): ?DateTime + { + return $this->last_access_at; + } + + /** + * Get created time of the audio file, like `2021-09-01 00:00:00` + */ + public function getCreatedAt(): ?DateTime + { + return $this->created_at; } - public function getLossless(): bool + /** + * Get modified time of the audio file, like `2021-09-01 00:00:00` + */ + public function getModifiedAt(): ?DateTime { - return $this->lossless; + return $this->modified_at; } - public function getCompressionRatio(): ?float + public function toArray(): array { - return $this->compressionRatio; + return [ + 'file_size' => $this->file_size, + 'data_format' => $this->data_format, + 'encoding' => $this->encoding, + 'mime_type' => $this->mime_type, + 'duration_seconds' => $this->duration_seconds, + 'bitrate' => $this->bitrate, + 'bitrate_mode' => $this->bitrate_mode, + 'sample_rate' => $this->sample_rate, + 'channels' => $this->channels, + 'channel_mode' => $this->channel_mode, + 'is_lossless' => $this->is_lossless, + 'compression_ratio' => $this->compression_ratio, + 'codec' => $this->codec, + 'encoder_options' => $this->encoder_options, + 'version' => $this->version, + 'av_data_offset' => $this->av_data_offset, + 'av_data_end' => $this->av_data_end, + 'file_path' => $this->file_path, + 'filename' => $this->filename, + 'last_access_at' => $this->last_access_at?->format('Y-m-d H:i:s'), + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'modified_at' => $this->modified_at?->format('Y-m-d H:i:s'), + ]; } } diff --git a/src/Models/AudioStat.php b/src/Models/AudioStat.php deleted file mode 100644 index 3e70105..0000000 --- a/src/Models/AudioStat.php +++ /dev/null @@ -1,147 +0,0 @@ -deviceNumber = $stat['dev'] ?? null; - $self->inodeNumber = $stat['ino'] ?? null; - $self->inodeProtectionMode = $stat['mode'] ?? null; - $self->numberOfLinks = $stat['nlink'] ?? null; - $self->userId = $stat['uid'] ?? null; - $self->groupId = $stat['gid'] ?? null; - $self->deviceType = $stat['rdev'] ?? null; - $self->size = $stat['size'] ?? null; - $self->lastAccessAt = $stat['atime'] ? new DateTime('@'.$stat['atime']) : null; - $self->createdAt = $stat['ctime'] ? new DateTime('@'.$stat['ctime']) : null; - $self->modifiedAt = $stat['mtime'] ? new DateTime('@'.$stat['mtime']) : null; - $self->blockSize = $stat['blksize'] ?? null; - $self->numberOfBlocks = $stat['blocks'] ?? null; - - return $self; - } - - public function getPath(): string - { - return $this->path; - } - - public function getDeviceNumber(): ?int - { - return $this->deviceNumber; - } - - public function getInodeNumber(): ?int - { - return $this->inodeNumber; - } - - public function getInodeProtectionMode(): ?int - { - return $this->inodeProtectionMode; - } - - public function getNumberOfLinks(): ?int - { - return $this->numberOfLinks; - } - - public function getUserId(): ?int - { - return $this->userId; - } - - public function getGroupId(): ?int - { - return $this->groupId; - } - - public function getDeviceType(): ?int - { - return $this->deviceType; - } - - public function getSize(): ?int - { - return $this->size; - } - - public function getLastAccessAt(): ?DateTime - { - return $this->lastAccessAt; - } - - public function getCreatedAt(): ?DateTime - { - return $this->createdAt; - } - - public function getModifiedAt(): ?DateTime - { - return $this->modifiedAt; - } - - public function getBlockSize(): ?int - { - return $this->blockSize; - } - - public function getNumberOfBlocks(): ?int - { - return $this->numberOfBlocks; - } - - public function toArray(): array - { - return [ - 'path' => $this->path, - 'deviceNumber' => $this->deviceNumber, - 'inodeNumber' => $this->inodeNumber, - 'inodeProtectionMode' => $this->inodeProtectionMode, - 'numberOfLinks' => $this->numberOfLinks, - 'userId' => $this->userId, - 'groupId' => $this->groupId, - 'deviceType' => $this->deviceType, - 'size' => $this->size, - 'lastAccessAt' => $this->lastAccessAt?->format('Y-m-d H:i:s'), - 'createdAt' => $this->createdAt?->format('Y-m-d H:i:s'), - 'modifiedAt' => $this->modifiedAt?->format('Y-m-d H:i:s'), - 'blockSize' => $this->blockSize, - 'numberOfBlocks' => $this->numberOfBlocks, - ]; - } - - public function toJson(): string - { - return json_encode($this->toArray()); - } - - public function __toString(): string - { - return $this->toJson(); - } -} diff --git a/src/Models/Id3Reader.php b/src/Models/Id3Reader.php deleted file mode 100644 index ed23da6..0000000 --- a/src/Models/Id3Reader.php +++ /dev/null @@ -1,1792 +0,0 @@ -raw = $self->instance->analyze($path); - $self->is_writable = $self->instance->is_writable($path); - $metadata = $self->raw; - - $audio = Id3Audio::make($metadata['audio'] ?? null); - $video = Id3Video::make($metadata['video'] ?? null); - $tags = Id3AudioTag::make($metadata['tags'] ?? null); - $comments = Id3Comments::make($metadata['comments'] ?? null); - $tags_html = Id3TagsHtml::make($metadata['tags_html'] ?? null); - $bitrate = $metadata['bitrate'] ?? null; - if ($bitrate) { - $bitrate = intval($bitrate); - } - - $self->version = $metadata['GETID3_VERSION'] ?? null; - $self->filesize = $metadata['filesize'] ?? null; - $self->filepath = $metadata['filepath'] ?? null; - $self->filename = $metadata['filename'] ?? null; - $self->filenamepath = $metadata['filenamepath'] ?? null; - $self->avdataoffset = $metadata['avdataoffset'] ?? null; - $self->avdataend = $metadata['avdataend'] ?? null; - $self->fileformat = $metadata['fileformat'] ?? null; - $self->audio = $audio; - $self->video = $video; - $self->tags = $tags; - $self->comments = $comments; - $self->encoding = $metadata['encoding'] ?? null; - $self->mime_type = $metadata['mime_type'] ?? null; - $self->mpeg = $metadata['mpeg'] ?? null; - $self->playtime_seconds = $metadata['playtime_seconds'] ?? null; - $self->tags_html = $tags_html; - $self->bitrate = $bitrate; - $self->playtime_string = $metadata['playtime_string'] ?? null; - - return $self; - } - - public function getInstance(): getID3 - { - return $this->instance; - } - - public function getVersion(): ?string - { - return $this->version; - } - - public function getFilesize(): ?int - { - return $this->filesize; - } - - public function getFilepath(): ?string - { - return $this->filepath; - } - - public function getFilename(): ?string - { - return $this->filename; - } - - public function getFilenamepath(): ?string - { - return $this->filenamepath; - } - - public function getAvdataoffset(): ?int - { - return $this->avdataoffset; - } - - public function getAvdataend(): ?int - { - return $this->avdataend; - } - - public function getFileformat(): ?string - { - return $this->fileformat; - } - - public function getAudio(): ?Id3Audio - { - return $this->audio; - } - - public function getTags(): ?Id3AudioTag - { - return $this->tags; - } - - public function getComments(): ?Id3Comments - { - return $this->comments; - } - - public function getEncoding(): ?string - { - return $this->encoding; - } - - public function getMimeType(): ?string - { - return $this->mime_type; - } - - public function getMpeg(): mixed - { - return $this->mpeg; - } - - public function getPlaytimeSeconds(): ?float - { - return $this->playtime_seconds; - } - - public function getTagsHtml(): ?Id3TagsHtml - { - return $this->tags_html; - } - - public function getBitrate(): ?float - { - return $this->bitrate; - } - - public function getPlaytimeString(): ?string - { - return $this->playtime_string; - } - - public function isWritable(): bool - { - return $this->is_writable; - } - - public function getRaw(): array - { - return $this->raw; - } - - public function toTags(?string $audioFormat = null): array - { - $rawTags = $this->raw['tags_html'] ?? []; - - if (count($rawTags) === 0) { - return []; - } - - $tagsItems = []; - if ($audioFormat) { - $tagsItems = $rawTags[$audioFormat] ?? []; - } else { - if (count($rawTags) > 1) { - $entries = []; - foreach ($rawTags as $key => $keyTags) { - $entries[$key] = count($keyTags); - } - $maxKey = array_search(max($entries), $entries); - $tagsItems = $rawTags[$maxKey] ?? []; - } else { - $tagsItems = reset($rawTags); - } - } - - return Id3Reader::cleanTags($tagsItems); - } - - public static function cleanTags(?array $tagsItems): array - { - if (! $tagsItems) { - return []; - } - - $temp = []; - foreach ($tagsItems as $k => $v) { - $temp[$k] = $v[0] ?? null; - } - - $items = []; - foreach ($temp as $k => $v) { - $k = strtolower($k); - $k = str_replace(' ', '_', $k); - $items[$k] = $v; - } - - return $items; - } - - public function toAudioFormats(): array - { - return $this->raw['tags_html'] ?? []; - } - - public function toArray(): array - { - $raw = $this->raw; - $raw['id3v2']['APIC'] = null; - $raw['ape']['items']['cover art (front)'] = null; - $raw['comments'] = null; - - return $raw; - } -} - -class Id3Audio -{ - /** @var Id3Stream[] */ - protected array $streams = []; - - protected function __construct( - protected ?string $dataformat = null, - protected ?int $channels = null, - protected ?int $sample_rate = null, - protected ?float $bitrate = null, - protected ?string $channelmode = null, - protected ?string $bitrate_mode = null, - protected ?string $codec = null, - protected ?string $encoder = null, - protected bool $lossless = false, - protected ?string $encoder_options = null, - protected ?float $compression_ratio = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $streams = []; - if (array_key_exists('streams', $metadata)) { - foreach ($metadata['streams'] as $stream) { - $streams[] = Id3Stream::make($stream); - } - } - - $self = new self( - dataformat: $metadata['dataformat'] ?? null, - channels: $metadata['channels'] ?? null, - sample_rate: $metadata['sample_rate'] ?? null, - bitrate: $metadata['bitrate'] ?? null, - channelmode: $metadata['channelmode'] ?? null, - bitrate_mode: $metadata['bitrate_mode'] ?? null, - codec: $metadata['codec'] ?? null, - encoder: $metadata['encoder'] ?? null, - lossless: $metadata['lossless'] ?? false, - encoder_options: $metadata['encoder_options'] ?? null, - compression_ratio: $metadata['compression_ratio'] ?? null, - ); - $self->streams = $streams; - - return $self; - } - - /** @return Id3Stream[] */ - public function streams(): array - { - return $this->streams; - } - - public function dataformat(): ?string - { - return $this->dataformat; - } - - public function channels(): ?int - { - return $this->channels; - } - - public function sample_rate(): ?int - { - return $this->sample_rate; - } - - public function bitrate(): ?float - { - return $this->bitrate; - } - - public function channelmode(): ?string - { - return $this->channelmode; - } - - public function bitrate_mode(): ?string - { - return $this->bitrate_mode; - } - - public function codec(): ?string - { - return $this->codec; - } - - public function encoder(): ?string - { - return $this->encoder; - } - - public function lossless(): bool - { - return $this->lossless; - } - - public function encoder_options(): ?string - { - return $this->encoder_options; - } - - public function compression_ratio(): ?float - { - return $this->compression_ratio; - } - - public function stream(): ?Id3Stream - { - return $this->streams[0] ?? null; - } -} - -class Id3Video -{ - protected function __construct( - protected ?string $dataformat = null, - protected ?int $rotate = null, - protected ?float $resolution_x = null, - protected ?float $resolution_y = null, - protected ?float $frame_rate = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $self = new self( - dataformat: $metadata['dataformat'] ?? null, - rotate: $metadata['rotate'] ?? null, - resolution_x: $metadata['resolution_x'] ?? null, - resolution_y: $metadata['resolution_y'] ?? null, - frame_rate: $metadata['frame_rate'] ?? null, - ); - - return $self; - } - - public function dataformat(): ?string - { - return $this->dataformat; - } - - public function rotate(): ?int - { - return $this->rotate; - } - - public function resolution_x(): ?float - { - return $this->resolution_x; - } - - public function resolution_y(): ?float - { - return $this->resolution_y; - } - - public function frame_rate(): ?float - { - return $this->frame_rate; - } -} - -class Id3Stream -{ - protected function __construct( - protected ?string $dataformat = null, - protected ?int $channels = null, - protected ?int $sample_rate = null, - protected ?float $bitrate = null, - protected ?string $channelmode = null, - protected ?string $bitrate_mode = null, - protected ?string $codec = null, - protected ?string $encoder = null, - protected bool $lossless = false, - protected ?string $encoder_options = null, - protected ?float $compression_ratio = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $self = new self( - dataformat: $metadata['dataformat'] ?? null, - channels: $metadata['channels'] ?? null, - sample_rate: $metadata['sample_rate'] ?? null, - bitrate: $metadata['bitrate'] ?? null, - channelmode: $metadata['channelmode'] ?? null, - bitrate_mode: $metadata['bitrate_mode'] ?? null, - codec: $metadata['codec'] ?? null, - encoder: $metadata['encoder'] ?? null, - lossless: $metadata['lossless'] ?? false, - encoder_options: $metadata['encoder_options'] ?? null, - compression_ratio: $metadata['compression_ratio'] ?? null, - ); - - return $self; - } - - public function dataformat(): ?string - { - return $this->dataformat; - } - - public function channels(): ?int - { - return $this->channels; - } - - public function sample_rate(): ?int - { - return $this->sample_rate; - } - - public function bitrate(): ?float - { - return $this->bitrate; - } - - public function channelmode(): ?string - { - return $this->channelmode; - } - - public function bitrate_mode(): ?string - { - return $this->bitrate_mode; - } - - public function codec(): ?string - { - return $this->codec; - } - - public function encoder(): ?string - { - return $this->encoder; - } - - public function lossless(): bool - { - return $this->lossless; - } - - public function encoder_options(): ?string - { - return $this->encoder_options; - } - - public function compression_ratio(): ?float - { - return $this->compression_ratio; - } -} - -class Id3AudioTag -{ - protected function __construct( - protected ?Id3AudioTagV1 $id3v1 = null, - protected ?Id3AudioTagV2 $id3v2 = null, - protected ?Id3TagQuicktime $quicktime = null, - protected ?Id3TagAsf $asf = null, - protected ?Id3TagVorbisComment $vorbiscomment = null, - protected ?Id3TagRiff $riff = null, - protected ?Id3TagMatroska $matroska = null, - protected ?Id3TagApe $ape = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $id3v1 = Id3Reader::cleanTags($metadata['id3v1'] ?? null); - $id3v2 = Id3Reader::cleanTags($metadata['id3v2'] ?? null); - $quicktime = Id3Reader::cleanTags($metadata['quicktime'] ?? null); - $asf = Id3Reader::cleanTags($metadata['asf'] ?? null); - $vorbiscomment = Id3Reader::cleanTags($metadata['vorbiscomment'] ?? null); - $riff = Id3Reader::cleanTags($metadata['riff'] ?? null); - $matroska = Id3Reader::cleanTags($metadata['matroska'] ?? null); - $ape = Id3Reader::cleanTags($metadata['ape'] ?? null); - - $self = new self( - id3v1: Id3AudioTagV1::make($id3v1), - id3v2: Id3AudioTagV2::make($id3v2), - quicktime: Id3TagQuicktime::make($quicktime), - asf: Id3TagAsf::make($asf), - vorbiscomment: Id3TagVorbisComment::make($vorbiscomment), - riff: Id3TagRiff::make($riff), - matroska: Id3TagMatroska::make($matroska), - ape: Id3TagApe::make($ape), - ); - - return $self; - } - - public function id3v1(): ?Id3AudioTagV1 - { - return $this->id3v1; - } - - public function id3v2(): ?Id3AudioTagV2 - { - return $this->id3v2; - } - - public function quicktime(): ?Id3TagQuicktime - { - return $this->quicktime; - } - - public function asf(): ?Id3TagAsf - { - return $this->asf; - } - - public function vorbiscomment(): ?Id3TagVorbisComment - { - return $this->vorbiscomment; - } - - public function riff(): ?Id3TagRiff - { - return $this->riff; - } - - public function matroska(): ?Id3TagMatroska - { - return $this->matroska; - } - - public function ape(): ?Id3TagApe - { - return $this->ape; - } -} - -class Id3AudioTagV1 -{ - public function __construct( - protected ?string $title = null, - protected ?string $artist = null, - protected ?string $album = null, - protected ?string $year = null, - protected ?string $genre = null, - protected ?string $comment = null, - protected ?string $track_number = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $self = new self( - title: $metadata['title'] ?? null, - artist: $metadata['artist'] ?? null, - album: $metadata['album'] ?? null, - year: $metadata['year'] ?? null, - genre: $metadata['genre'] ?? null, - comment: $metadata['comment'] ?? null, - track_number: $metadata['track_number'] ?? null, - ); - - return $self; - } - - public function title(): ?string - { - return $this->title; - } - - public function artist(): ?string - { - return $this->artist; - } - - public function album(): ?string - { - return $this->album; - } - - public function year(): ?string - { - return $this->year; - } - - public function genre(): ?string - { - return $this->genre; - } - - public function comment(): ?string - { - return $this->comment; - } - - public function track_number(): ?string - { - return $this->track_number; - } - - public function toArray(): array - { - return [ - 'title' => $this->title, - 'artist' => $this->artist, - 'album' => $this->album, - 'year' => $this->year, - 'genre' => $this->genre, - 'comment' => $this->comment, - 'track_number' => $this->track_number, - ]; - } -} - -class Id3AudioTagV2 -{ - public function __construct( - protected ?string $album = null, - protected ?string $artist = null, - protected ?string $band = null, - protected ?string $comment = null, - protected ?string $composer = null, - protected ?string $part_of_a_set = null, - protected ?string $genre = null, - protected ?string $part_of_a_compilation = null, - protected ?string $title = null, - protected ?string $track_number = null, - protected ?string $year = null, - protected ?string $copyright = null, - protected ?string $text = null, - protected ?string $unsynchronised_lyric = null, - protected ?string $language = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $self = new self( - album: $metadata['album'] ?? null, - artist: $metadata['artist'] ?? null, - band: $metadata['band'] ?? null, - comment: $metadata['comment'] ?? null, - composer: $metadata['composer'] ?? null, - part_of_a_set: $metadata['part_of_a_set'] ?? null, - genre: $metadata['genre'] ?? null, - part_of_a_compilation: $metadata['part_of_a_compilation'] ?? null, - title: $metadata['title'] ?? null, - track_number: $metadata['track_number'] ?? null, - year: $metadata['year'] ?? null, - copyright: $metadata['copyright_message'] ?? null, - text: $metadata['text'] ?? null, - unsynchronised_lyric: $metadata['unsynchronised_lyric'] ?? null, - language: $metadata['language'] ?? null, - ); - - return $self; - } - - public function album(): ?string - { - return $this->album; - } - - public function artist(): ?string - { - return $this->artist; - } - - public function band(): ?string - { - return $this->band; - } - - public function comment(): ?string - { - return $this->comment; - } - - public function composer(): ?string - { - return $this->composer; - } - - public function part_of_a_set(): ?string - { - return $this->part_of_a_set; - } - - public function genre(): ?string - { - return $this->genre; - } - - public function part_of_a_compilation(): ?string - { - return $this->part_of_a_compilation; - } - - public function title(): ?string - { - return $this->title; - } - - public function track_number(): ?string - { - return $this->track_number; - } - - public function year(): ?string - { - return $this->year; - } - - public function copyright(): ?string - { - return $this->copyright; - } - - public function text(): ?string - { - return $this->text; - } - - public function unsynchronised_lyric(): ?string - { - return $this->unsynchronised_lyric; - } - - public function language(): ?string - { - return $this->language; - } - - public function toArray(): array - { - return [ - 'album' => $this->album, - 'artist' => $this->artist, - 'band' => $this->band, - 'comment' => $this->comment, - 'composer' => $this->composer, - 'part_of_a_set' => $this->part_of_a_set, - 'genre' => $this->genre, - 'part_of_a_compilation' => $this->part_of_a_compilation, - 'title' => $this->title, - 'track_number' => $this->track_number, - 'year' => $this->year, - 'copyright' => $this->copyright, - 'text' => $this->text, - 'unsynchronised_lyric' => $this->unsynchronised_lyric, - 'language' => $this->language, - ]; - } -} - -class Id3Comments -{ - protected function __construct( - protected ?string $language = null, - protected ?Id3CommentsPicture $picture = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $language = $metadata['language'][0] ?? null; - $picture = Id3CommentsPicture::make($metadata['picture'][0] ?? null); - - $self = new self( - language: $language, - picture: $picture, - ); - - return $self; - } - - public function picture(): ?Id3CommentsPicture - { - return $this->picture; - } -} - -class Id3CommentsPicture -{ - protected function __construct( - protected ?string $data = null, - protected ?string $image_mime = null, - protected ?int $image_width = null, - protected ?int $image_height = null, - protected ?string $picturetype = null, - protected ?string $description = null, - protected ?int $datalength = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $self = new self( - data: $metadata['data'] ?? null, - image_mime: $metadata['image_mime'] ?? null, - image_width: $metadata['image_width'] ?? null, - image_height: $metadata['image_height'] ?? null, - picturetype: $metadata['picturetype'] ?? null, - description: $metadata['description'] ?? null, - datalength: $metadata['datalength'] ?? null, - ); - - return $self; - } - - public function data(): ?string - { - return $this->data; - } - - public function image_mime(): ?string - { - return $this->image_mime; - } - - public function image_width(): ?int - { - return $this->image_width; - } - - public function image_height(): ?int - { - return $this->image_height; - } - - public function picturetype(): ?string - { - return $this->picturetype; - } - - public function description(): ?string - { - return $this->description; - } - - public function datalength(): ?int - { - return $this->datalength; - } -} - -class Id3TagQuicktime -{ - public function __construct( - protected ?string $title = null, - protected ?string $track_number = null, - protected ?string $disc_number = null, - protected ?string $compilation = null, - protected ?string $album = null, - protected ?string $genre = null, - protected ?string $composer = null, - protected ?string $creation_date = null, - protected ?string $copyright = null, - protected ?string $artist = null, - protected ?string $album_artist = null, - protected ?string $encoded_by = null, - protected ?string $encoding_tool = null, - protected ?string $description = null, - protected ?string $description_long = null, - protected ?string $language = null, - protected ?string $lyrics = null, - protected ?string $comment = null, - protected ?string $stik = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $self = new self( - title: $metadata['title'] ?? null, - track_number: $metadata['track_number'] ?? null, - disc_number: $metadata['disc_number'] ?? null, - compilation: $metadata['compilation'] ?? null, - album: $metadata['album'] ?? null, - genre: $metadata['genre'] ?? null, - composer: $metadata['composer'] ?? null, - creation_date: $metadata['creation_date'] ?? null, - copyright: $metadata['copyright'] ?? null, - artist: $metadata['artist'] ?? null, - album_artist: $metadata['album_artist'] ?? null, - encoded_by: $metadata['encoded_by'] ?? null, - encoding_tool: $metadata['encoding_tool'] ?? null, - description: $metadata['description'] ?? null, - description_long: $metadata['description_long'] ?? null, - language: $metadata['language'] ?? null, - lyrics: $metadata['lyrics'] ?? null, - comment: $metadata['comment'] ?? null, - stik: $metadata['stik'] ?? null, - ); - - return $self; - } - - public function title(): ?string - { - return $this->title; - } - - public function track_number(): ?string - { - return $this->track_number; - } - - public function disc_number(): ?string - { - return $this->disc_number; - } - - public function compilation(): ?string - { - return $this->compilation; - } - - public function album(): ?string - { - return $this->album; - } - - public function genre(): ?string - { - return $this->genre; - } - - public function composer(): ?string - { - return $this->composer; - } - - public function creation_date(): ?string - { - return $this->creation_date; - } - - public function copyright(): ?string - { - return $this->copyright; - } - - public function artist(): ?string - { - return $this->artist; - } - - public function album_artist(): ?string - { - return $this->album_artist; - } - - public function encoded_by(): ?string - { - return $this->encoded_by; - } - - public function encoding_tool(): ?string - { - return $this->encoding_tool; - } - - public function description(): ?string - { - return $this->description; - } - - public function description_long(): ?string - { - return $this->description_long; - } - - public function language(): ?string - { - return $this->language; - } - - public function lyrics(): ?string - { - return $this->lyrics; - } - - public function comment(): ?string - { - return $this->comment; - } - - public function stik(): ?string - { - return $this->stik; - } - - public function toArray(): array - { - return [ - 'title' => $this->title, - 'track_number' => $this->track_number, - 'disc_number' => $this->disc_number, - 'compilation' => $this->compilation, - 'album' => $this->album, - 'genre' => $this->genre, - 'composer' => $this->composer, - 'creation_date' => $this->creation_date, - 'copyright' => $this->copyright, - 'artist' => $this->artist, - 'album_artist' => $this->album_artist, - 'encoded_by' => $this->encoded_by, - 'encoding_tool' => $this->encoding_tool, - 'description' => $this->description, - 'description_long' => $this->description_long, - 'language' => $this->language, - 'lyrics' => $this->lyrics, - 'comment' => $this->comment, - 'stik' => $this->stik, - ]; - } -} - -class Id3TagAsf -{ - public function __construct( - protected ?string $title = null, - protected ?string $artist = null, - protected ?string $album = null, - protected ?string $albumartist = null, - protected ?string $composer = null, - protected ?string $partofset = null, - protected ?string $genre = null, - protected ?string $track_number = null, - protected ?string $year = null, - protected ?string $encodingsettings = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - $self = new self( - title: $metadata['title'] ?? null, - artist: $metadata['artist'] ?? null, - album: $metadata['album'] ?? null, - albumartist: $metadata['albumartist'] ?? null, - composer: $metadata['composer'] ?? null, - partofset: $metadata['partofset'] ?? null, - genre: $metadata['genre'] ?? null, - track_number: $metadata['track_number'] ?? null, - year: $metadata['year'] ?? null, - encodingsettings: $metadata['encodingsettings'] ?? null, - ); - - return $self; - } - - public function title(): ?string - { - return $this->title; - } - - public function artist(): ?string - { - return $this->artist; - } - - public function album(): ?string - { - return $this->album; - } - - public function albumartist(): ?string - { - return $this->albumartist; - } - - public function composer(): ?string - { - return $this->composer; - } - - public function partofset(): ?string - { - return $this->partofset; - } - - public function genre(): ?string - { - return $this->genre; - } - - public function track_number(): ?string - { - return $this->track_number; - } - - public function year(): ?string - { - return $this->year; - } - - public function encodingsettings(): ?string - { - return $this->encodingsettings; - } - - public function toArray(): array - { - return [ - 'title' => $this->title, - 'artist' => $this->artist, - 'album' => $this->album, - 'albumartist' => $this->albumartist, - 'composer' => $this->composer, - 'partofset' => $this->partofset, - 'genre' => $this->genre, - 'track_number' => $this->track_number, - 'year' => $this->year, - 'encodingsettings' => $this->encodingsettings, - ]; - } -} - -class Id3TagVorbisComment -{ - public function __construct( - protected ?string $description = null, - protected ?string $encoder = null, - protected ?string $title = null, - protected ?string $artist = null, - protected ?string $album = null, - protected ?string $genre = null, - protected ?string $comment = null, - protected ?string $albumartist = null, - protected ?string $composer = null, - protected ?string $discnumber = null, - protected ?string $compilation = null, - protected ?string $date = null, - protected ?string $tracknumber = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - $self = new self( - description: $metadata['description'] ?? null, - encoder: $metadata['encoder'] ?? null, - title: $metadata['title'] ?? null, - artist: $metadata['artist'] ?? null, - album: $metadata['album'] ?? null, - genre: $metadata['genre'] ?? null, - comment: $metadata['comment'] ?? null, - albumartist: $metadata['albumartist'] ?? null, - composer: $metadata['composer'] ?? null, - discnumber: $metadata['discnumber'] ?? null, - compilation: $metadata['compilation'] ?? null, - date: $metadata['date'] ?? null, - tracknumber: $metadata['tracknumber'] ?? null, - ); - - return $self; - } - - public function description(): ?string - { - return $this->description; - } - - public function title(): ?string - { - return $this->title; - } - - public function artist(): ?string - { - return $this->artist; - } - - public function album(): ?string - { - return $this->album; - } - - public function genre(): ?string - { - return $this->genre; - } - - public function comment(): ?string - { - return $this->comment; - } - - public function albumartist(): ?string - { - return $this->albumartist; - } - - public function composer(): ?string - { - return $this->composer; - } - - public function discnumber(): ?string - { - return $this->discnumber; - } - - public function compilation(): ?string - { - return $this->compilation; - } - - public function date(): ?string - { - return $this->date; - } - - public function tracknumber(): ?string - { - return $this->tracknumber; - } - - public function encoder(): ?string - { - return $this->encoder; - } - - public function toArray(): array - { - return [ - 'description' => $this->description, - 'encoder' => $this->encoder, - 'title' => $this->title, - 'artist' => $this->artist, - 'album' => $this->album, - 'genre' => $this->genre, - 'comment' => $this->comment, - 'albumartist' => $this->albumartist, - 'composer' => $this->composer, - 'discnumber' => $this->discnumber, - 'compilation' => $this->compilation, - 'date' => $this->date, - 'tracknumber' => $this->tracknumber, - ]; - } -} - -class Id3TagRiff -{ - public function __construct( - protected ?string $artist = null, - protected ?string $comment = null, - protected ?string $creationdate = null, - protected ?string $genre = null, - protected ?string $title = null, - protected ?string $product = null, - protected ?string $software = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - $self = new self( - artist: $metadata['artist'] ?? null, - comment: $metadata['comment'] ?? null, - creationdate: $metadata['creationdate'] ?? null, - genre: $metadata['genre'] ?? null, - title: $metadata['title'] ?? null, - product: $metadata['product'] ?? null, - software: $metadata['software'] ?? null, - ); - - return $self; - } - - public function artist(): ?string - { - return $this->artist; - } - - public function comment(): ?string - { - return $this->comment; - } - - public function creationdate(): ?string - { - return $this->creationdate; - } - - public function genre(): ?string - { - return $this->genre; - } - - public function title(): ?string - { - return $this->title; - } - - public function product(): ?string - { - return $this->product; - } - - public function software(): ?string - { - return $this->software; - } - - public function toArray(): array - { - return [ - 'artist' => $this->artist, - 'comment' => $this->comment, - 'creationdate' => $this->creationdate, - 'genre' => $this->genre, - 'title' => $this->title, - 'product' => $this->product, - 'software' => $this->software, - ]; - } -} - -class Id3TagMatroska -{ - public function __construct( - protected ?string $title = null, - protected ?string $muxingapp = null, - protected ?string $writingapp = null, - protected ?string $album = null, - protected ?string $artist = null, - protected ?string $album_artist = null, - protected ?string $comment = null, - protected ?string $composer = null, - protected ?string $disc = null, - protected ?string $genre = null, - protected ?string $compilation = null, - protected ?string $part_number = null, - protected ?string $date = null, - protected ?string $encoder = null, - protected ?string $duration = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $self = new self( - title: $metadata['title'] ?? null, - muxingapp: $metadata['muxingapp'] ?? null, - writingapp: $metadata['writingapp'] ?? null, - album: $metadata['album'] ?? null, - artist: $metadata['artist'] ?? null, - album_artist: $metadata['album_artist'] ?? null, - comment: $metadata['comment'] ?? null, - composer: $metadata['composer'] ?? null, - disc: $metadata['disc'] ?? null, - genre: $metadata['genre'] ?? null, - compilation: $metadata['compilation'] ?? null, - part_number: $metadata['part_number'] ?? null, - date: $metadata['date'] ?? null, - encoder: $metadata['encoder'] ?? null, - duration: $metadata['duration'] ?? null, - ); - - return $self; - } - - public function title(): ?string - { - return $this->title; - } - - public function muxingapp(): ?string - { - return $this->muxingapp; - } - - public function writingapp(): ?string - { - return $this->writingapp; - } - - public function album(): ?string - { - return $this->album; - } - - public function artist(): ?string - { - return $this->artist; - } - - public function album_artist(): ?string - { - return $this->album_artist; - } - - public function comment(): ?string - { - return $this->comment; - } - - public function composer(): ?string - { - return $this->composer; - } - - public function disc(): ?string - { - return $this->disc; - } - - public function genre(): ?string - { - return $this->genre; - } - - public function compilation(): ?string - { - return $this->compilation; - } - - public function part_number(): ?string - { - return $this->part_number; - } - - public function date(): ?string - { - return $this->date; - } - - public function encoder(): ?string - { - return $this->encoder; - } - - public function duration(): ?string - { - return $this->duration; - } - - public function toArray(): array - { - return [ - 'title' => $this->title, - 'muxingapp' => $this->muxingapp, - 'writingapp' => $this->writingapp, - 'album' => $this->album, - 'artist' => $this->artist, - 'album_artist' => $this->album_artist, - 'comment' => $this->comment, - 'composer' => $this->composer, - 'disc' => $this->disc, - 'genre' => $this->genre, - 'compilation' => $this->compilation, - 'part_number' => $this->part_number, - 'date' => $this->date, - 'encoder' => $this->encoder, - 'duration' => $this->duration, - ]; - } -} - -class Id3TagApe -{ - public function __construct( - protected ?string $title = null, - protected ?string $artist = null, - protected ?string $album = null, - protected ?string $album_artist = null, - protected ?string $composer = null, - protected ?string $comment = null, - protected ?string $genre = null, - protected ?string $disc = null, - protected ?string $compilation = null, - protected ?string $track = null, - protected ?string $date = null, - protected ?string $encoder = null, - protected ?string $description = null, - protected ?string $copyright = null, - protected ?string $lyrics = null, - protected ?string $podcastdesc = null, - protected ?string $language = null, - protected ?string $year = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $self = new self( - title: $metadata['title'] ?? null, - artist: $metadata['artist'] ?? null, - album: $metadata['album'] ?? null, - album_artist: $metadata['album_artist'] ?? $metadata['albumartist'] ?? null, - composer: $metadata['composer'] ?? null, - comment: $metadata['comment'] ?? null, - genre: $metadata['genre'] ?? null, - disc: $metadata['disc'] ?? $metadata['discnumber'] ?? null, - compilation: $metadata['compilation'] ?? null, - track: $metadata['track'] ?? null, - date: $metadata['date'] ?? null, - encoder: $metadata['encoder'] ?? null, - description: $metadata['description'] ?? null, - copyright: $metadata['copyright'] ?? null, - lyrics: $metadata['unsyncedlyrics'] ?? null, - podcastdesc: $metadata['podcastdesc'] ?? null, - language: $metadata['language'] ?? null, - year: $metadata['year'] ?? null, - ); - - return $self; - } - - public function title(): ?string - { - return $this->title; - } - - public function artist(): ?string - { - return $this->artist; - } - - public function album(): ?string - { - return $this->album; - } - - public function album_artist(): ?string - { - return $this->album_artist; - } - - public function composer(): ?string - { - return $this->composer; - } - - public function comment(): ?string - { - return $this->comment; - } - - public function genre(): ?string - { - return $this->genre; - } - - public function disc(): ?string - { - return $this->disc; - } - - public function compilation(): ?string - { - return $this->compilation; - } - - public function track(): ?string - { - return $this->track; - } - - public function date(): ?string - { - return $this->date; - } - - public function encoder(): ?string - { - return $this->encoder; - } - - public function description(): ?string - { - return $this->description; - } - - public function copyright(): ?string - { - return $this->copyright; - } - - public function lyrics(): ?string - { - return $this->lyrics; - } - - public function podcastdesc(): ?string - { - return $this->podcastdesc; - } - - public function language(): ?string - { - return $this->language; - } - - public function year(): ?string - { - return $this->year; - } - - public function toArray(): array - { - return [ - 'title' => $this->title, - 'artist' => $this->artist, - 'album' => $this->album, - 'album_artist' => $this->album_artist, - 'composer' => $this->composer, - 'comment' => $this->comment, - 'genre' => $this->genre, - 'disc' => $this->disc, - 'compilation' => $this->compilation, - 'track' => $this->track, - 'date' => $this->date, - 'encoder' => $this->encoder, - 'description' => $this->description, - 'copyright' => $this->copyright, - 'lyrics' => $this->lyrics, - 'podcastdesc' => $this->podcastdesc, - 'language' => $this->language, - 'year' => $this->year, - ]; - } -} - -class Id3TagsHtml -{ - protected function __construct( - protected ?Id3AudioTagV1 $id3v1 = null, - protected ?Id3AudioTagV2 $id3v2 = null, - protected ?Id3TagQuicktime $quicktime = null, - protected ?Id3TagAsf $asf = null, - protected ?Id3TagVorbisComment $vorbiscomment = null, - protected ?Id3TagRiff $riff = null, - protected ?Id3TagMatroska $matroska = null, - protected ?Id3TagApe $ape = null, - ) {} - - public static function make(?array $metadata): ?self - { - if (! $metadata) { - return null; - } - - $id3v1 = Id3Reader::cleanTags($metadata['id3v1'] ?? null); - $id3v2 = Id3Reader::cleanTags($metadata['id3v2'] ?? null); - $quicktime = Id3Reader::cleanTags($metadata['quicktime'] ?? null); - $asf = Id3Reader::cleanTags($metadata['asf'] ?? null); - $vorbiscomment = Id3Reader::cleanTags($metadata['vorbiscomment'] ?? null); - $riff = Id3Reader::cleanTags($metadata['riff'] ?? null); - $matroska = Id3Reader::cleanTags($metadata['matroska'] ?? null); - $ape = Id3Reader::cleanTags($metadata['ape'] ?? null); - - $self = new self( - id3v1: Id3AudioTagV1::make($id3v1), - id3v2: Id3AudioTagV2::make($id3v2), - quicktime: Id3TagQuicktime::make($quicktime), - asf: Id3TagAsf::make($asf), - vorbiscomment: Id3TagVorbisComment::make($vorbiscomment), - riff: Id3TagRiff::make($riff), - matroska: Id3TagMatroska::make($matroska), - ape: Id3TagApe::make($ape), - ); - - return $self; - } - - public function id3v1(): ?Id3AudioTagV1 - { - return $this->id3v1; - } - - public function id3v2(): ?Id3AudioTagV2 - { - return $this->id3v2; - } - - public function quicktime(): ?Id3TagQuicktime - { - return $this->quicktime; - } - - public function asf(): ?Id3TagAsf - { - return $this->asf; - } - - public function vorbiscomment(): ?Id3TagVorbisComment - { - return $this->vorbiscomment; - } - - public function riff(): ?Id3TagRiff - { - return $this->riff; - } - - public function matroska(): ?Id3TagMatroska - { - return $this->matroska; - } - - public function ape(): ?Id3TagApe - { - return $this->ape; - } -} diff --git a/src/Models/Id3Writer.php b/src/Models/Id3Writer.php deleted file mode 100644 index d403cdb..0000000 --- a/src/Models/Id3Writer.php +++ /dev/null @@ -1,505 +0,0 @@ - - */ - protected array $options = [ - 'encoding' => 'UTF-8', - ]; - - /** - * @var array - */ - protected array $tags = []; - - /** - * @var string[] - */ - protected array $warnings = []; - - /** - * @var string[] - */ - protected array $errors = []; - - protected bool $overrideTags = true; - - protected bool $removeOldTags = false; - - protected bool $failOnError = true; - - /** - * @var string[] - */ - protected array $tagFormats = []; - - protected ?string $path = null; - - protected bool $success = false; - - protected function __construct( - protected Audio $audio, - protected getid3_writetags $instance, - protected AudioCore $core, - ) {} - - public static function make(Audio $audio): self - { - $self = new self( - audio: $audio, - instance: new getid3_writetags, - core: new AudioCore - ); - - return $self; - } - - public function getCore(): AudioCore - { - return $this->core; - } - - public function title(?string $title): self - { - $this->core->setTitle($title); - - return $this; - } - - public function artist(?string $artist): self - { - $this->core->setArtist($artist); - - return $this; - } - - public function album(?string $album): self - { - $this->core->setAlbum($album); - - return $this; - } - - public function genre(?string $genre): self - { - $this->core->setGenre($genre); - - return $this; - } - - public function year(int $year): self - { - $this->core->setYear($year); - - return $this; - } - - public function trackNumber(string|int|null $trackNumber): self - { - if (is_int($trackNumber)) { - $trackNumber = (string) $trackNumber; - } - - $this->core->setTrackNumber($trackNumber); - - return $this; - } - - public function comment(?string $comment): self - { - $this->core->setComment($comment); - - return $this; - } - - public function albumArtist(?string $albumArtist): self - { - $this->core->setAlbumArtist($albumArtist); - - return $this; - } - - public function composer(?string $composer): self - { - $this->core->setComposer($composer); - - return $this; - } - - public function discNumber(string|int|null $discNumber): self - { - if (is_int($discNumber)) { - $discNumber = (string) $discNumber; - } - - $this->core->setDiscNumber($discNumber); - - return $this; - } - - public function isCompilation(): self - { - $this->core->setIsCompilation(true); - - return $this; - } - - public function isNotCompilation(): self - { - $this->core->setIsCompilation(false); - - return $this; - } - - public function creationDate(string|DateTime|null $creationDate): self - { - if ($creationDate instanceof DateTime) { - $creationDate = $creationDate->format('Y-m-d'); - } - - $this->core->setCreationDate($creationDate); - - return $this; - } - - public function copyright(?string $copyright): self - { - $this->core->setCopyright($copyright); - - return $this; - } - - public function encodingBy(?string $encodingBy): self - { - $this->core->setEncodingBy($encodingBy); - - return $this; - } - - public function encoding(?string $encoding): self - { - $this->core->setEncoding($encoding); - - return $this; - } - - public function description(?string $description): self - { - $this->core->setDescription($description); - - return $this; - } - - public function podcastDescription(?string $podcastDescription): self - { - $this->core->setPodcastDescription($podcastDescription); - - return $this; - } - - public function language(?string $language): self - { - $this->core->setLanguage($language); - - return $this; - } - - public function lyrics(?string $lyrics): self - { - $this->core->setLyrics($lyrics); - - return $this; - } - - public function stik(?string $stik): self - { - $this->core->setStik($stik); - - return $this; - } - - /** - * @param string $pathOrData Path to cover image or binary data - */ - public function cover(string $pathOrData): self - { - $this->core->setCover($pathOrData); - - return $this; - } - - public function options(array $options = ['encoding' => 'UTF-8']): self - { - $this->options = $options; - - return $this; - } - - public function path(string $path): self - { - $this->path = $path; - - if (file_exists($this->audio->getPath())) { - copy($this->audio->getPath(), $this->path); - } - - return $this; - } - - /** - * Prevent fail on error. - */ - public function preventFailOnError(): self - { - $this->failOnError = false; - - return $this; - } - - /** - * Override existing tags, default is true. - */ - // public function notOverrideTags(): self - // { - // $this->overrideTags = false; - - // return $this; - // } - - /** - * Remove other tags, default is false. - */ - public function removeOldTags(): self - { - $this->removeOldTags = true; - - return $this; - } - - /** - * Set manually tags. - * - * @param array $tags - */ - public function tags(array $tags): self - { - $this->tags = $this->convertTags($tags); - - return $this; - } - - /** - * Set tag format. - * - * @param string[] $tags Options are `id3v1`, `id3v2.2`, `id2v2.3`, `id3v2.4`, `ape`, `vorbiscomment`, `metaflac`, `real` - */ - public function tagFormats(array $tags): self - { - $this->tagFormats = $tags; - - return $this; - } - - /** - * Save tags. - * - * @throws \Exception - */ - public function save(): bool - { - if (! $this->path) { - $this->path = $this->audio->getPath(); - } - - $this->instance->filename = $this->path; - - $this->convertTagFormats(); - $this->automaticConvert(); - - $this->instance->overwrite_tags = $this->overrideTags; - $this->instance->remove_other_tags = $this->removeOldTags; - $this->instance->tagformats = $this->tagFormats; - $this->instance->tag_data = $this->tags; - - $this->success = $this->instance->WriteTags(); - - $this->errors = $this->instance->errors; - $this->warnings = $this->instance->warnings; - - $errors = implode(', ', $this->errors); - $warnings = implode(', ', $this->warnings); - $supported = match ($this->audio->getFormat()) { - AudioFormatEnum::flac => true, - AudioFormatEnum::mp3 => true, - AudioFormatEnum::ogg => true, - default => false - }; - - if (! empty($this->errors)) { - $msg = 'Save tags failed.'; - - $errors = strip_tags($errors); - $errors = "Errors: {$errors}."; - if (! empty($this->errors)) { - $msg .= " {$errors}"; - } - - $warnings = "Warnings: {$warnings}."; - if (! empty($this->warnings)) { - $msg .= " {$warnings}"; - } - - $isSuccess = $this->success ? 'true' : 'false'; - $success = "Success: {$isSuccess}"; - $msg .= " {$success}"; - - error_log($msg); - - if ($this->failOnError) { - throw new \Exception($msg); - } - } - - if (! $supported && $this->failOnError) { - throw new \Exception("Format {$this->audio->getFormat()?->value} is not supported."); - } - - if (! empty($this->warnings)) { - error_log($warnings); - } - - return $this->success; - } - - private function automaticConvert(): self - { - $this->convertTagFormats(); - - $convert = match ($this->audio->getType()) { - AudioTypeEnum::id3 => AudioCore::toId3v2($this->core), - AudioTypeEnum::vorbiscomment => AudioCore::toVorbisComment($this->core), - AudioTypeEnum::quicktime => AudioCore::toQuicktime($this->core), - AudioTypeEnum::matroska => AudioCore::toMatroska($this->core), - AudioTypeEnum::ape => AudioCore::toApe($this->core), - AudioTypeEnum::asf => AudioCore::toAsf($this->core), - default => null, - }; - - $tags = []; - if ($convert) { - $tags = $convert->toArray(); - } - - $tags = $this->convertTags($tags); - $this->attachCover($tags); - - $this->tags = [ - ...$this->tags, - ...$tags, - ]; - - return $this; - } - - /** - * @param array $tags - * @return array - */ - private function convertTags(array $tags): array - { - $attached = $tags['attached_picture'] ?? null; - $items = []; - if (! empty($tags)) { - foreach ($tags as $key => $tag) { - if ($tag && gettype($tag) === 'string') { - $items[$key] = [$tag]; - } - } - } - - if ($attached) { - $items['attached_picture'] = $attached; - } - - return $items; - } - - /** - * - ID3v1 (v1 & v1.1) - * - ID3v2 (v2.3, v2.4) - * - APE (v2) - * - Ogg Vorbis comments (need `vorbis-tools`) - * - FLAC comments - * - * Options: `id3v1`, `id3v2.2`, `id2v2.3`, `id3v2.4`, `ape`, `vorbiscomment`, `metaflac`, `real` - */ - private function convertTagFormats(): self - { - if (! empty($this->tagFormats)) { - return $this; - } - - $formats = match ($this->audio->getFormat()) { - AudioFormatEnum::aac => [], - AudioFormatEnum::aif => [], - AudioFormatEnum::aifc => [], - AudioFormatEnum::aiff => [], - AudioFormatEnum::dsf => [], - AudioFormatEnum::flac => ['metaflac'], - AudioFormatEnum::m4a => [], - AudioFormatEnum::m4b => [], - AudioFormatEnum::m4v => [], - AudioFormatEnum::mpc => [], - AudioFormatEnum::mka => [], - AudioFormatEnum::mkv => [], - AudioFormatEnum::ape => [], - AudioFormatEnum::mp3 => ['id3v1', 'id3v2.4'], - AudioFormatEnum::mp4 => [], - AudioFormatEnum::ogg => ['vorbiscomment'], - AudioFormatEnum::opus => [], - AudioFormatEnum::ofr => [], - AudioFormatEnum::ofs => [], - AudioFormatEnum::spx => [], - AudioFormatEnum::tak => [], - AudioFormatEnum::tta => [], - AudioFormatEnum::wav => [], - AudioFormatEnum::webm => [], - AudioFormatEnum::wma => [], - AudioFormatEnum::wv => [], - default => null, - }; - $this->tagFormats = $formats; - - return $this; - } - - private function attachCover(array &$tags): void - { - $coverFormatsAllowed = [AudioFormatEnum::mp3]; - if ($this->core->getCover() && in_array($this->audio->getFormat(), $coverFormatsAllowed)) { - // $tags = [ - // ...$tags, - // 'CTOC' => $old_tags['id3v2']['CTOC'], - // 'CHAP' => $old_tags['id3v2']['CHAP'], - // 'chapters' => $old_tags['id3v2']['chapters'], - // ]; - $tags['attached_picture'][0] = [ - 'data' => base64_decode($this->core->getCover()->data()), - 'picturetypeid' => $this->core->getCover()->picturetypeid(), - 'description' => $this->core->getCover()->description(), - 'mime' => $this->core->getCover()->mime(), - ]; - $this->core->setHasCover(true); - } - } -} diff --git a/test.php b/test.php deleted file mode 100644 index a645ff2..0000000 --- a/test.php +++ /dev/null @@ -1 +0,0 @@ -1.9.23-202310190849366480/Users/ewilan/Workspace/php-audio/tests/mediaaudiobook.mp3/Users/ewilan/Workspace/php-audio/tests/media/audiobook.mp395396272609mp3ArrayArrayArrayUTF-8ArrayArrayArrayaudio/mpegArray11.0496875Array1280000:11 \ No newline at end of file diff --git a/tests/ArchTest.php b/tests/ArchTest.php new file mode 100644 index 0000000..fd6a1e4 --- /dev/null +++ b/tests/ArchTest.php @@ -0,0 +1,5 @@ +expect(['dd', 'dump', 'ray']) + ->not->toBeUsed(); diff --git a/tests/AudioCoreTest.php b/tests/AudioCoreTest.php index f146f9e..9fab03a 100644 --- a/tests/AudioCoreTest.php +++ b/tests/AudioCoreTest.php @@ -1,43 +1,42 @@ getTitle(), artist: $audio->getArtist(), album: $audio->getAlbum(), genre: $audio->getGenre(), year: $audio->getYear(), - trackNumber: $audio->getTrackNumber(), + track_number: $audio->getTrackNumber(), comment: $audio->getComment(), - albumArtist: $audio->getAlbumArtist(), + album_artist: $audio->getAlbumArtist(), composer: $audio->getComposer(), - discNumber: $audio->getDiscNumber(), - isCompilation: $audio->isCompilation(), - creationDate: $audio->getCreationDate(), + disc_number: $audio->getDiscNumber(), + is_compilation: $audio->isCompilation(), + creation_date: $audio->getCreationDate(), copyright: $audio->getCopyright(), - encodingBy: $audio->getEncodingBy(), + encoding_by: $audio->getEncodingBy(), encoding: $audio->getEncoding(), description: $audio->getDescription(), lyrics: $audio->getLyrics(), - stik: $audio->getStik(), ); - expect($core->getTitle())->toBe('Introduction'); - expect($core->getArtist())->toBe('Mr Piouf'); - expect($core->getAlbum())->toBe('P1PDD Le conclave de Troie'); - expect($core->getGenre())->toBe('Roleplaying game'); - expect($core->getYear())->toBe(2016); - expect($core->getTrackNumber())->toBe('1'); - expect($core->getComment())->toBe('http://www.p1pdd.com'); - expect($core->getAlbumArtist())->toBe('P1PDD & Mr Piouf'); - expect($core->getComposer())->toBe('P1PDD & Piouf'); - expect($core->getDiscNumber())->toBe('1'); - expect($core->isCompilation())->toBe(true); + expect($core->title)->toBe('Introduction'); + expect($core->artist)->toBe('Mr Piouf'); + expect($core->album)->toBe('P1PDD Le conclave de Troie'); + expect($core->genre)->toBe('Roleplaying game'); + expect($core->year)->toBe(2016); + expect($core->track_number)->toBe('1'); + expect($core->comment)->toBe('http://www.p1pdd.com'); + expect($core->album_artist)->toBe('P1PDD & Mr Piouf'); + expect($core->composer)->toBe('P1PDD & Piouf'); + expect($core->disc_number)->toBe('1'); + expect($core->is_compilation)->toBe(true); $id3v1 = AudioCore::toId3v1($core); $id3v2 = AudioCore::toId3v2($core); @@ -52,5 +51,9 @@ $cover = AudioCoreCover::make(FOLDER); expect($core->toArray())->toBeArray(); - expect($cover->toArray())->toBeArray(); + + expect($cover->data)->toBeString(); + expect($cover->picture_type_id)->toBeInt(); + expect($cover->description)->toBeString(); + expect($cover->mime)->toBeString(); }); diff --git a/tests/AudioCoverTest.php b/tests/AudioCoverTest.php new file mode 100644 index 0000000..3d7e375 --- /dev/null +++ b/tests/AudioCoverTest.php @@ -0,0 +1,45 @@ +getCover(); + + if ($audio->hasCover()) { + expect($cover)->toBeInstanceOf(AudioCover::class); + expect($cover->getContents())->toBeString(); + expect($cover->getContents())->toBeString(); + expect($cover->getMimeType())->toBeString(); + if ($cover->getWidth()) { + expect($cover->getWidth())->toBeInt(); + } + if ($cover->getHeight()) { + expect($cover->getHeight())->toBeInt(); + } + + $path = "tests/output/cover-{$ext}.jpg"; + file_put_contents($path, $cover->getContents()); + expect(file_exists($path))->toBeTrue(); + expect($path)->toBeReadableFile(); + } else { + expect($cover)->toBeNull(); + } +})->with([...AUDIO]); + +it('can read as array', function (string $path) { + $audio = Audio::read($path); + $cover = $audio->getCover(); + + if ($cover) { + expect($cover->toArray())->toBeArray(); + } else { + expect($cover)->toBeNull(); + } +})->with([...AUDIO]); diff --git a/tests/AudioMetadataTest.php b/tests/AudioMetadataTest.php new file mode 100644 index 0000000..005c930 --- /dev/null +++ b/tests/AudioMetadataTest.php @@ -0,0 +1,142 @@ +getMetadata(); + expect($metadata->getFileSize())->toBe(272737); + expect($metadata->getDataFormat())->toBe('mp3'); + expect($metadata->getEncoding())->toBe('UTF-8'); + expect($metadata->getMimeType())->toBe('audio/mpeg'); + expect($metadata->getDurationSeconds())->toBe(11.0496875); + expect($metadata->getBitrate())->toBe(128000); + expect($metadata->getBitrateMode())->toBe('cbr'); + expect($metadata->getSampleRate())->toBe(44100); + expect($metadata->getChannels())->toBe(2); + expect($metadata->getChannelMode())->toBe('joint stereo'); + expect($metadata->isLossless())->toBeFalse(); + expect($metadata->getCompressionRatio())->toBe(0.09070294784580499); + expect($metadata->getCodec())->toBe('LAME'); + expect($metadata->getEncoderOptions())->toBe('CBR128'); + expect($metadata->getVersion())->toContain('1.9'); + expect($metadata->getAvDataOffset())->toBe(95396); + expect($metadata->getAvDataEnd())->toBe(272609); + expect($metadata->getFilePath())->toContain('tests/media'); + expect($metadata->getFilename())->toBe('test.mp3'); + expect($metadata->getLastAccessAt())->toBeInstanceOf(DateTime::class); + expect($metadata->getCreatedAt())->toBeInstanceOf(DateTime::class); + expect($metadata->getModifiedAt())->toBeInstanceOf(DateTime::class); +}); + +it('can read basic info', function (string $path) { + $audio = Audio::read($path); + $metadata = $audio->getMetadata(); + + expect($metadata)->toBeInstanceOf(AudioMetadata::class); + expect($metadata->getFileSize())->toBeInt(); + expect($metadata->getSizeHuman())->toBeString(); + expect($metadata->getMimeType())->toBeString(); + expect($metadata->isLossless())->toBeBool(); + expect($metadata->getLastAccessAt())->toBeInstanceOf(DateTime::class); + expect($metadata->getCreatedAt())->toBeInstanceOf(DateTime::class); + expect($metadata->getModifiedAt())->toBeInstanceOf(DateTime::class); + expect($metadata->getVersion())->toContain('1.9'); + expect($metadata->getAvDataOffset())->toBeInt(); + expect($metadata->getAvDataEnd())->toBeInt(); + expect($metadata->getFilePath())->toBeString(); + expect($metadata->getFilename())->toBeString(); + + if ($metadata->getChannels()) { + expect($metadata->getChannels())->toBeInt(); + } + if ($metadata->getBitrate()) { + expect($metadata->getBitrate())->toBeInt(); + } + if ($metadata->getChannelMode()) { + expect($metadata->getChannelMode())->toBeString(); + } + if ($metadata->getDataFormat()) { + expect($metadata->getDataFormat())->toBeString(); + } + if ($metadata->getEncoding()) { + expect($metadata->getEncoding())->toBeString(); + } + if ($metadata->getDurationSeconds()) { + expect($metadata->getDurationSeconds())->toBeFloat(); + expect($metadata->getDurationSeconds(2))->toBeFloat(); + } + if ($metadata->getSampleRate()) { + expect($metadata->getSampleRate())->toBeInt(); + } + if ($metadata->getBitrateMode()) { + expect($metadata->getBitrateMode())->toBeString(); + } + if ($metadata->getCompressionRatio()) { + expect($metadata->getCompressionRatio())->toBeFloat(); + expect($metadata->getCompressionRatio(2))->toBeFloat(); + } + if ($metadata->getCodec()) { + expect($metadata->getCodec())->toBeString(); + } + if ($metadata->getEncoderOptions()) { + expect($metadata->getEncoderOptions())->toBeString(); + } +})->with([...AUDIO]); + +it('can read as array', function (string $path) { + $audio = Audio::read($path); + $metadata = $audio->getMetadata(); + + expect($metadata->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/AudioMp3Test.php b/tests/AudioMp3Test.php new file mode 100644 index 0000000..36d8bb5 --- /dev/null +++ b/tests/AudioMp3Test.php @@ -0,0 +1,116 @@ +toBeInstanceOf(Audio::class); + expect($audio->getPath())->toBe(MP3); + expect($audio->getExtension())->toBe('mp3'); + expect($audio->getFormat())->toBe(AudioFormatEnum::mp3); + expect($audio->getType())->toBe(AudioTypeEnum::id3); + expect($audio->getMetadata())->toBeInstanceOf(AudioMetadata::class); + expect($audio->getId3Reader())->toBeInstanceOf(Id3Reader::class); + expect($audio->getDuration())->toBe(11.05); + expect($audio->getDurationHuman())->toBe('00:00:11'); + + expect($audio->isWritable())->toBeTrue(); + expect($audio->isValid())->toBeTrue(); + expect($audio->hasCover())->toBeTrue(); + + expect($audio->getTitle())->toBe('Introduction'); + expect($audio->getArtist())->toBe('Mr Piouf'); + expect($audio->getAlbum())->toBe('P1PDD Le conclave de Troie'); + expect($audio->getGenre())->toBe('Roleplaying game'); + expect($audio->getYear())->toBe(2016); + expect($audio->getTrackNumber())->toBe('1'); + expect($audio->getTrackNumberInt())->toBe(1); + expect($audio->getAlbumArtist())->toBe('P1PDD & Mr Piouf'); + expect($audio->getComposer())->toBe('P1PDD & Piouf'); + expect($audio->getDiscNumber())->toBe('1'); + expect($audio->getDiscNumberInt())->toBe(1); + expect($audio->isCompilation())->toBeTrue(); + expect($audio->getCreationDate())->toBeNull(); + expect($audio->getEncodingBy())->toBeNull(); + expect($audio->getEncoding())->toBeNull(); + expect($audio->getCopyright())->toBeNull(); + expect($audio->getDescription())->toBeNull(); + expect($audio->getSynopsis())->toBeNull(); + expect($audio->getLanguage())->toBeNull(); + expect($audio->getLyrics())->toBeNull(); + expect($audio->getComment())->toBe('http://www.p1pdd.com'); + + expect($audio->getRawAll())->toBeArray(); + expect($audio->getRawAll()['id3v1'])->toBeArray(); + expect($audio->getRawAll()['id3v1'])->toHaveCount(6); + expect($audio->getRawAll()['id3v2'])->toBeArray(); + expect($audio->getRawAll()['id3v2'])->toHaveCount(11); + expect($audio->getRaw())->toHaveCount(11); + expect($audio->getRaw('id3v2'))->toHaveCount(11); + expect($audio->getRawKey('title'))->toBe('Introduction'); + + $cover = $audio->getCover(); + expect($cover)->toBeInstanceOf(AudioCover::class); + expect($cover->getContents())->toBeString(); + expect($cover->getContents(base64: true))->toBeString(); + expect($cover->getMimeType())->toBe('image/jpeg'); + expect($cover->getWidth())->toBe(640); + expect($cover->getHeight())->toBe(640); + + $metadata = $audio->getMetadata(); + expect($metadata->getFileSize())->toBe(272737); + expect($metadata->getEncoding())->toBe('UTF-8'); + expect($metadata->getMimeType())->toBe('audio/mpeg'); + expect($metadata->getDurationSeconds())->toBe(11.0496875); + expect($metadata->getBitrate())->toBe(128000); + expect($metadata->getBitrateMode())->toBe('cbr'); + expect($metadata->getSampleRate())->toBe(44100); + expect($metadata->getChannels())->toBe(2); + expect($metadata->getChannelMode())->toBe('joint stereo'); + expect($metadata->isLossless())->toBeFalse(); + expect($metadata->getCompressionRatio())->toBe(0.09070294784580499); +}); + +it('can extract cover mp3', function () { + $audio = Audio::read(MP3); + $cover = $audio->getCover(); + + expect($cover)->toBeInstanceOf(AudioCover::class); + expect($cover->getContents())->toBeString(); + expect($cover->getMimeType())->toBe('image/jpeg'); + expect($cover->getWidth())->toBe(640); + expect($cover->getHeight())->toBe(640); + + $path = 'tests/output/cover.jpg'; + file_put_contents($path, $cover->getContents()); + expect(file_exists($path))->toBeTrue(); + expect($path)->toBeReadableFile(); +}); + +it('can read file mp3 no meta', function () { + $audio = Audio::read(MP3_NO_META); + + expect($audio)->toBeInstanceOf(Audio::class); + expect($audio->getTitle())->toBeNull(); + expect($audio->getArtist())->toBeNull(); + expect($audio->getAlbum())->toBeNull(); + expect($audio->getGenre())->toBeNull(); + expect($audio->getYear())->toBeNull(); + expect($audio->getTrackNumber())->toBeNull(); + expect($audio->getComment())->toBeNull(); + expect($audio->getAlbumArtist())->toBeNull(); + expect($audio->getComposer())->toBeNull(); + expect($audio->getDiscNumber())->toBeNull(); + expect($audio->isCompilation())->toBeFalse(); + expect($audio->getPath())->toBe(MP3_NO_META); +}); + +it("can fail if file didn't exists", function () { + expect(fn () => Audio::read('tests/media/unknown.mp3'))->toThrow(Exception::class); +}); diff --git a/tests/AudioTest.php b/tests/AudioTest.php index 2bd29d2..414fde7 100644 --- a/tests/AudioTest.php +++ b/tests/AudioTest.php @@ -2,144 +2,70 @@ use Kiwilan\Audio\Audio; use Kiwilan\Audio\Enums\AudioFormatEnum; -use Kiwilan\Audio\Models\AudioCover; +use Kiwilan\Audio\Id3\Id3Reader; +use Kiwilan\Audio\Models\AudioMetadata; -it('can read file', function (string $path) { - $audio = Audio::get($path); +it('can use get method', function () { + $audio = Audio::get(MP3); + expect($audio)->toBeInstanceOf(Audio::class); +}); + +it('can read basic info', function (string $path) { + $audio = Audio::read($path); $extension = pathinfo($path, PATHINFO_EXTENSION); $format = AudioFormatEnum::tryFrom($extension); expect($audio)->toBeInstanceOf(Audio::class); + expect($audio->getPath())->toBe($path); + expect($audio->getExtension())->toBe($extension); + expect($audio->getFormat())->toBe($format); + + expect($audio->getMetadata())->toBeInstanceOf(AudioMetadata::class); + expect($audio->getId3Reader())->toBeInstanceOf(Id3Reader::class); + expect($audio->getDuration())->toBeFloat(); + expect($audio->getDurationHuman())->toBeString(); + + expect($audio->isWritable())->toBeBool(); + expect($audio->isValid())->toBeBool(); + expect($audio->hasCover())->toBeBool(); + expect($audio->getTitle())->toBe('Introduction'); expect($audio->getArtist())->toBe('Mr Piouf'); expect($audio->getAlbum())->toBe('P1PDD Le conclave de Troie'); expect($audio->getGenre())->toBe('Roleplaying game'); expect($audio->getYear())->toBe(2016); expect($audio->getTrackNumber())->toBe('1'); - if ($audio->getComment()) { - expect($audio->getComment())->toBe('http://www.p1pdd.com'); - } - expect($audio->getAlbumArtist())->toBe('P1PDD & Mr Piouf'); - expect($audio->getComposer())->toBe('P1PDD & Piouf'); - expect($audio->getDiscNumber())->toBeString(); - expect($audio->isCompilation())->toBeBool(); - expect($audio->getPath())->toBe($path); - expect($audio->getgetExtension())->toBe($extension); + expect($audio->getTrackNumberInt())->toBe(1); expect($audio->getFormat())->toBe($format); - expect($audio->getDuration())->toBeFloat(); - expect($audio->getExtras())->toBeArray(); - expect($audio->getTags())->toBeArray(); - expect($audio->toArray())->toBeArray(); - - $metadata = $audio->getAudio(); - expect($metadata->getPath())->toBeString(); - expect($metadata->getFilesize())->toBeInt(); - expect($metadata->getExtension())->toBeString(); - expect($metadata->getEncoding())->toBeString(); - expect($metadata->getMimeType())->toBeString(); - if ($metadata->getDurationSeconds()) { - expect($metadata->getDurationSeconds())->toBeFloat(); - } - if ($metadata->getDurationReadable()) { - expect($metadata->getDurationReadable())->toBeString(); - } - if ($metadata->getBitrate()) { - expect($metadata->getBitrate())->toBeInt(); - } - if ($metadata->getBitrateMode()) { - expect($metadata->getBitrateMode())->toBeString(); - } - if ($metadata->getSampleRate()) { - expect($metadata->getSampleRate())->toBeInt(); - } - if ($metadata->getChannels()) { - expect($metadata->getChannels())->toBeInt(); - } - if ($metadata->getChannelMode()) { - expect($metadata->getChannelMode())->toBeString(); - } - expect($metadata->getLossless())->toBeBool(); - if ($metadata->getCompressionRatio()) { - expect($metadata->getCompressionRatio())->toBeFloat(); - } - expect($audio->isValid())->toBeTrue(); })->with([...AUDIO]); -it('can extract cover', function (string $path) { - $audio = Audio::get($path); - $ext = pathinfo($path, PATHINFO_EXTENSION); - $cover = $audio->getCover(); - - if ($audio->hasCover()) { - expect($cover)->toBeInstanceOf(AudioCover::class); - expect($cover->getContents())->toBeString(); - expect($cover->getContents())->toBeString(); - expect($cover->getMimeType())->toBeString(); - if ($cover->getWidth()) { - expect($cover->getWidth())->toBeInt(); - } - if ($cover->getHeight()) { - expect($cover->getHeight())->toBeInt(); - } - - $path = "tests/output/cover-{$ext}.jpg"; - file_put_contents($path, $cover->getContents()); - expect(file_exists($path))->toBeTrue(); - expect($path)->toBeReadableFile(); - } else { - expect($cover)->toBeNull(); - } -})->with([...AUDIO]); +it('can read disc number', function () { + $audio = Audio::read(M4A); -it('can use stat data', function (string $path) { - $audio = Audio::get($path); - $stat = $audio->getStat(); - - expect($stat->getPath())->toBe($path); - expect($stat->getDeviceNumber())->toBeInt(); - expect($stat->getInodeNumber())->toBeInt(); - expect($stat->getInodeProtectionMode())->toBeInt(); - expect($stat->getNumberOfLinks())->toBeInt(); - expect($stat->getUserId())->toBeInt(); - expect($stat->getGroupId())->toBeInt(); - expect($stat->getDeviceType())->toBeInt(); - expect($stat->getLastAccessAt())->toBeInstanceOf(DateTime::class); - expect($stat->getCreatedAt())->toBeInstanceOf(DateTime::class); - expect($stat->getModifiedAt())->toBeInstanceOf(DateTime::class); - expect($stat->getBlockSize())->toBeInt(); - expect($stat->getNumberOfBlocks())->toBeInt(); - expect($stat->toArray())->toBeArray(); - expect($stat->toJson())->toBeString(); - expect($stat->__toString())->toBeString(); -})->with([...AUDIO]); + expect($audio->getDiscNumber())->toBe('1/2'); + expect($audio->getDiscNumberInt())->toBe(1); +}); -it('can read mp3 stream', function () { - $audio = Audio::get(MP3); - $streams = $audio->getReader()->getAudio()->streams(); - - expect($streams)->toBeArray(); - expect($streams)->toHaveCount(1); - expect($streams[0]->dataformat())->toBe('mp3'); - expect($streams[0]->channels())->toBe(2); - expect($streams[0]->sample_rate())->toBe(44100); - expect($streams[0]->bitrate())->toBe(128000.0); - expect($streams[0]->channelmode())->toBe('joint stereo'); - expect($streams[0]->bitrate_mode())->toBe('cbr'); - expect($streams[0]->codec())->toBe('LAME'); - expect($streams[0]->encoder())->toBe('LAME3.100'); - expect($streams[0]->lossless())->toBeFalse(); - expect($streams[0]->encoder_options())->toBe('CBR128'); - expect($streams[0]->compression_ratio())->toBe(0.09070294784580499); +it('can read encoding', function () { + $audio = Audio::read(M4V); + + expect($audio->getEncoding())->toBe('Lavf60.3.100'); }); -it('can read wrong audio file', function () { - $audio = Audio::get(MD); +it('can read description', function () { + $audio = Audio::read(FLAC); - expect($audio->isValid())->toBeFalse(); + expect($audio->getDescription())->toBe('http://www.p1pdd.com'); +}); + +it('can read creation date', function () { + $audio = Audio::read(WV); + + expect($audio->getCreationDate())->toBe('2016'); }); it('can read file id3v1', function (string $path) { - $audio = Audio::get($path); + $audio = Audio::read($path); $extension = pathinfo($path, PATHINFO_EXTENSION); $format = AudioFormatEnum::tryFrom($extension); @@ -150,11 +76,22 @@ expect($audio->getAlbumArtist())->toBeString(); expect($audio->getComposer())->toBeNull(); - expect($audio->getgetExtension())->toBe($extension); + expect($audio->getExtension())->toBe($extension); expect($audio->getFormat())->toBe($format); expect($audio->getDuration())->toBeFloat(); - expect($audio->getDurationHumanReadable())->toBe('00:00:11'); - expect($audio->getExtras())->toBeArray(); + expect($audio->getDurationHuman())->toBe('00:00:11'); expect($audio)->toBeInstanceOf(Audio::class); })->with([...AUDIO_ID3_V1]); + +it('can read wrong audio file', function () { + $audio = Audio::read(MD); + + expect($audio->isValid())->toBeFalse(); +}); + +it('can read as array', function (string $path) { + $audio = Audio::read($path); + + expect($audio->toArray())->toBeArray(); +})->with([...AUDIO]); diff --git a/tests/AudiobookTest.php b/tests/AudiobookTest.php index 3bcc230..b91796a 100644 --- a/tests/AudiobookTest.php +++ b/tests/AudiobookTest.php @@ -2,9 +2,104 @@ 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 () { + $audiobook = Audio::read(AUDIOBOOK_RH); + + expect($audiobook->getPath())->toContain('tests/media/audiobook_rh.m4b'); + expect($audiobook->getExtension())->toBe('m4b'); + expect($audiobook->getFormat())->toBe(AudioFormatEnum::m4b); + expect($audiobook->getType())->toBe(AudioTypeEnum::quicktime); + expect($audiobook->getMetadata())->toBeInstanceOf(AudioMetadata::class); + expect($audiobook->isWritable())->toBeTrue(); + expect($audiobook->isValid())->toBeTrue(); + expect($audiobook->hasCover())->toBeTrue(); + + expect($audiobook->getTitle())->toBe('Assassin’s Apprentice'); + expect($audiobook->getArtist())->toBe('Robin Hobb'); + expect($audiobook->getAlbum())->toBe('Assassin’s Apprentice'); + expect($audiobook->getGenre())->toBe('Animals/Political/Epic/Military'); + expect($audiobook->getYear())->toBe(2024); + expect($audiobook->getTrackNumber())->toBe('1/1'); + expect($audiobook->getTrackNumberInt())->toBe(1); + expect($audiobook->getComment())->toBe('English'); + expect($audiobook->getAlbumArtist())->toBe('Robin Hobb'); + expect($audiobook->getComposer())->toBe('Paul Boehmer'); + expect($audiobook->getDiscNumber())->toBe('1'); + expect($audiobook->getDiscNumberInt())->toBe(1); + expect($audiobook->isCompilation())->toBeTrue(); + expect($audiobook->getCreationDate())->toBe('2024-09-30T12:00:00Z'); + expect($audiobook->getCopyright())->toBe('HarperCollins'); + expect($audiobook->getEncodingBy())->toBe('©2012 Robin Hobb (P)2012 HarperCollins Publishers Limited'); + expect($audiobook->getEncoding())->toBe('Audiobook Builder 2.2.9 (www.splasm.com), macOS 15.0'); + expect($audiobook->getDescription())->toBeString(); + expect($audiobook->getSynopsis())->toBeString(); + expect($audiobook->getLanguage())->toBe('English'); + expect($audiobook->getLyrics())->toBe('The Farseer #01'); +}); + +it('can read audiobook raw', function () { + $audiobook = Audio::read(AUDIOBOOK_RH); + + $raw = $audiobook->getRaw(); + expect($raw['title'])->toBe('Assassin’s Apprentice'); + expect($raw['artist'])->toBe('Robin Hobb'); + expect($raw['album'])->toBe('Assassin’s Apprentice'); + expect($raw['genre'])->toBe('Animals/Political/Epic/Military'); + expect($raw['origyear'])->toBe('2024/09/30'); + expect($raw['track_number'])->toBe('1/1'); + expect($raw['disc_number'])->toBe('1'); + expect($raw['compilation'])->toBe(1); + expect($raw['creation_date'])->toBe('2024-9-30T12:00:00Z'); + expect($raw['encoding_tool'])->toBe('Audiobook Builder 2.2.9 (www.splasm.com), macOS 15.0'); + expect($raw['subtitle'])->toBe('Subtitle'); + expect($raw['description_long'])->toBeString(); + expect($raw['language'])->toBe('English'); + expect($raw['lyrics'])->toBe('The Farseer #01'); + expect($raw['stik'])->toBe('Audiobook'); + expect($raw['encoded_by'])->toBe('©2012 Robin Hobb (P)2012 HarperCollins Publishers Limited'); + expect($raw['description'])->toBeString(); + expect($raw['copyright'])->toBe('HarperCollins'); + expect($raw['isbn'])->toBe('ISBN'); + expect($raw['composer'])->toBe('Paul Boehmer'); + expect($raw['comment'])->toBe('English'); + expect($raw['asin'])->toBe('ASIN'); + expect($raw['album_artist'])->toBe('Robin Hobb'); + expect($raw['series-part'])->toBe('1'); + expect($raw['series'])->toBe('The Farseer'); + + expect($audiobook->getRawKey('title'))->toBe('Assassin’s Apprentice'); + expect($audiobook->getRawKey('artist'))->toBe('Robin Hobb'); + expect($audiobook->getRawKey('album'))->toBe('Assassin’s Apprentice'); + expect($audiobook->getRawKey('genre'))->toBe('Animals/Political/Epic/Military'); + expect($audiobook->getRawKey('origyear'))->toBe('2024/09/30'); + expect($audiobook->getRawKey('track_number'))->toBe('1/1'); + expect($audiobook->getRawKey('disc_number'))->toBe('1'); + expect($audiobook->getRawKey('compilation'))->toBe(1); + expect($audiobook->getRawKey('creation_date'))->toBe('2024-9-30T12:00:00Z'); + expect($audiobook->getRawKey('encoding_tool'))->toBe('Audiobook Builder 2.2.9 (www.splasm.com), macOS 15.0'); + expect($audiobook->getRawKey('subtitle'))->toBe('Subtitle'); + expect($audiobook->getRawKey('description_long'))->toBeString(); + expect($audiobook->getRawKey('language'))->toBe('English'); + expect($audiobook->getRawKey('lyrics'))->toBe('The Farseer #01'); + expect($audiobook->getRawKey('stik'))->toBe('Audiobook'); + expect($audiobook->getRawKey('encoded_by'))->toBe('©2012 Robin Hobb (P)2012 HarperCollins Publishers Limited'); + expect($audiobook->getRawKey('description'))->toBeString(); + expect($audiobook->getRawKey('copyright'))->toBe('HarperCollins'); + expect($audiobook->getRawKey('isbn'))->toBe('ISBN'); + expect($audiobook->getRawKey('composer'))->toBe('Paul Boehmer'); + expect($audiobook->getRawKey('comment'))->toBe('English'); + expect($audiobook->getRawKey('asin'))->toBe('ASIN'); + expect($audiobook->getRawKey('album_artist'))->toBe('Robin Hobb'); + expect($audiobook->getRawKey('series-part'))->toBe('1'); + expect($audiobook->getRawKey('series'))->toBe('The Farseer'); +}); it('can read audiobook file m4b', function (string $file) { - $audio = Audio::get($file); + $audio = Audio::read($file); expect($audio->getTitle())->toBe('P1PDD Saison 1'); expect($audio->getArtist())->toBe('Mr Piouf'); @@ -24,31 +119,36 @@ expect($audio->getEncoding())->toBe('Audiobook Builder 2.2.6 (www.splasm.com), macOS 13.4'); expect($audio->getCopyright())->toBe('Copyright'); expect($audio->getDescription())->toBe('Description'); - expect($audio->getPodcastDescription())->toBe('Synopsis'); + expect($audio->getSynopsis())->toBe('Synopsis'); expect($audio->getLanguage())->toBe('Language'); expect($audio->getLyrics())->toBe('Lyrics'); - expect($audio->getStik())->toBe('Audiobook'); expect($audio->getDuration())->toBe(11.00); - expect($audio->getDurationHumanReadable())->toBe('00:00:11'); - expect($audio->getExtras())->toBeArray(); - expect($audio->toArray())->toBeArray(); - - expect($audio->getTags())->toBeArray(); - expect($audio->getTag('title'))->toBe('P1PDD Saison 1'); - expect($audio->getTag('artist'))->toBe('Mr Piouf'); - expect($audio->getTag('album'))->toBe('P1PDD Saison 1'); - expect($audio->getTag('genre'))->toBe('Audiobooks'); - expect($audio->getTag('track_number'))->toBe('1/1'); - expect($audio->getTag('comment'))->toBe('P1PDD team'); + expect($audio->getDurationHuman())->toBe('00:00:11'); + + expect($audio->getRaw())->toBeArray(); + expect($audio->getRawKey('title'))->toBe('P1PDD Saison 1'); + expect($audio->getRawKey('artist'))->toBe('Mr Piouf'); + expect($audio->getRawKey('album'))->toBe('P1PDD Saison 1'); + expect($audio->getRawKey('genre'))->toBe('Audiobooks'); + expect($audio->getRawKey('track_number'))->toBe('1/1'); + expect($audio->getRawKey('comment'))->toBe('P1PDD team'); })->with([AUDIOBOOK]); it('can read audiobook file mp3', function (string $file) { - $audio = Audio::get($file); - - expect(count($audio->getTags()))->toBe(16); - expect(count($audio->getTags('id3v2')))->toBe(15); + $audio = Audio::read($file); - expect(count($audio->getAudioFormats()))->toBe(3); - expect($audio->getAudioFormats())->toBeArray(); - expect($audio->toArray())->toBeArray(); + 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'); +}); diff --git a/tests/Mp3Test.php b/tests/Mp3Test.php deleted file mode 100644 index 7598f3c..0000000 --- a/tests/Mp3Test.php +++ /dev/null @@ -1,80 +0,0 @@ -toBeInstanceOf(Audio::class); - expect($audio->getTitle())->toBe('Introduction'); - expect($audio->getArtist())->toBe('Mr Piouf'); - expect($audio->getAlbum())->toBe('P1PDD Le conclave de Troie'); - expect($audio->getGenre())->toBe('Roleplaying game'); - expect($audio->getYear())->toBe(2016); - expect($audio->getTrackNumber())->toBe('1'); - expect($audio->getComment())->toBe('http://www.p1pdd.com'); - expect($audio->getAlbumArtist())->toBe('P1PDD & Mr Piouf'); - expect($audio->getComposer())->toBe('P1PDD & Piouf'); - expect($audio->getDiscNumber())->toBe('1'); - expect($audio->isCompilation())->toBe(true); - expect($audio->getPath())->toBe(MP3); - expect($audio->getFormat())->toBe(AudioFormatEnum::mp3); - expect($audio->getDuration())->toBe(11.05); - expect($audio->getDurationHumanReadable())->toBe('00:00:11'); - expect($audio->getExtras())->toBeArray(); - - $audio = $audio->getAudio(); - expect($audio->getFilesize())->toBe(272737); - expect($audio->getExtension())->toBe('mp3'); - expect($audio->getEncoding())->toBe('UTF-8'); - expect($audio->getMimeType())->toBe('audio/mpeg'); - expect($audio->getDurationSeconds())->toBe(11.0496875); - expect($audio->getDurationReadable())->toBe('0:11'); - expect($audio->getBitrate())->toBe(128000); - expect($audio->getBitrateMode())->toBe('cbr'); - expect($audio->getSampleRate())->toBe(44100); - expect($audio->getChannels())->toBe(2); - expect($audio->getChannelMode())->toBe('joint stereo'); - expect($audio->getLossless())->toBe(false); - expect($audio->getCompressionRatio())->toBe(0.09070294784580499); -}); - -it('can extract cover mp3', function () { - $audio = Audio::get(MP3); - $cover = $audio->getCover(); - - expect($cover)->toBeInstanceOf(AudioCover::class); - expect($cover->getContents())->toBeString(); - expect($cover->getMimeType())->toBe('image/jpeg'); - expect($cover->getWidth())->toBe(640); - expect($cover->getHeight())->toBe(640); - - $path = 'tests/output/cover.jpg'; - file_put_contents($path, $cover->getContents()); - expect(file_exists($path))->toBeTrue(); - expect($path)->toBeReadableFile(); -}); - -it('can read file mp3 no meta', function () { - $audio = Audio::get(MP3_NO_META); - - expect($audio)->toBeInstanceOf(Audio::class); - expect($audio->getTitle())->toBeNull(); - expect($audio->getArtist())->toBeNull(); - expect($audio->getAlbum())->toBeNull(); - expect($audio->getGenre())->toBeNull(); - expect($audio->getYear())->toBeNull(); - expect($audio->getTrackNumber())->toBeNull(); - expect($audio->getComment())->toBeNull(); - expect($audio->getAlbumArtist())->toBeNull(); - expect($audio->getComposer())->toBeNull(); - expect($audio->getDiscNumber())->toBeNull(); - expect($audio->isCompilation())->toBeFalse(); - expect($audio->getPath())->toBe(MP3_NO_META); -}); - -it("can fail if file didn't exists", function () { - expect(fn () => Audio::get('tests/media/unknown.mp3'))->toThrow(Exception::class); -}); diff --git a/tests/Pest.php b/tests/Pest.php index a265cc3..4bd0d54 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,9 +1,12 @@ getTitle())->toBe('Introduction'); + expect($audio->getArtist())->toBe('Mr Piouf'); + expect($audio->getAlbum())->toBe('P1PDD Le conclave de Troie'); + expect($audio->getGenre())->toBe('Roleplaying game'); + expect($audio->getYear())->toBe(2016); + expect($audio->getTrackNumber())->toBe('1'); + expect($audio->getComment())->toBe('http://www.p1pdd.com'); + expect($audio->getAlbumArtist())->toBe('P1PDD & Mr Piouf'); + expect($audio->getComposer())->toBe('P1PDD & Piouf'); + expect($audio->getDiscNumber())->toBe('1'); + expect($audio->isCompilation())->toBeTrue(); +} + +function testMp3Writed(Audio $audio) +{ + expect($audio->getTitle())->toBe('New Title'); + expect($audio->getArtist())->toBe('New Artist'); + expect($audio->getAlbum())->toBe('New Album'); + expect($audio->getGenre())->toBe('New Genre'); + expect($audio->getYear())->toBe(2022); + expect($audio->getAlbumArtist())->toBe('New Album Artist'); + expect($audio->getComment())->toBe('New Comment'); + expect($audio->getComposer())->toBe('New Composer'); + expect($audio->getDiscNumber())->toBe('2/2'); + expect($audio->isCompilation())->toBeFalse(); +} + +function pathTo(string $filename, string $subDirectory = 'output'): string +{ + return __DIR__.'/'.$subDirectory.'/'.$filename; +} + +function resetMp3Writer() +{ + $audio = Audio::read(MP3_WRITER); + + $audio->write() + ->title('Introduction') + ->artist('Mr Piouf') + ->album('P1PDD Le conclave de Troie') + ->genre('Roleplaying game') + ->year(2016) + ->trackNumber('1') + ->comment('http://www.p1pdd.com') + ->albumArtist('P1PDD & Mr Piouf') + ->composer('P1PDD & Piouf') + ->discNumber('1') + ->isCompilation() + ->cover(DEFAULT_FOLDER) + ->save(); +} diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index dda2a98..edd926d 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -1,16 +1,34 @@ getId3Reader()->getAudio()->streams; + + expect($streams)->toBeArray(); + expect($streams)->toHaveCount(1); + expect($streams[0]->data_format)->toBe('mp3'); + expect($streams[0]->channels)->toBe(2); + expect($streams[0]->sample_rate)->toBe(44100); + expect($streams[0]->bitrate)->toBe(128000.0); + expect($streams[0]->channel_mode)->toBe('joint stereo'); + expect($streams[0]->bitrate_mode)->toBe('cbr'); + expect($streams[0]->codec)->toBe('LAME'); + expect($streams[0]->encoder)->toBe('LAME3.100'); + expect($streams[0]->lossless)->toBeFalse(); + expect($streams[0]->encoder_options)->toBe('CBR128'); + expect($streams[0]->compression_ratio)->toBe(0.09070294784580499); +}); it('can parse ID3 reader', function (string $path) { - $audio = Audio::get($path); + $audio = Audio::read($path); - $reader = $audio->getReader(); + $reader = $audio->getId3Reader(); $raw = $reader->getRaw(); expect($reader->getInstance())->toBeInstanceOf(getID3::class); @@ -41,9 +59,6 @@ expect($reader->getPlaytimeSeconds())->toBeFloat(); } - if ($reader->getTagsHtml()) { - expect($reader->getTagsHtml())->toBeInstanceOf(Id3TagsHtml::class); - } if ($reader->getBitrate()) { expect($reader->getBitrate())->toBeFloat(); } @@ -51,16 +66,3 @@ expect($reader->getPlaytimeString())->toBeString(); } })->with([...AUDIO]); - -it('can parse with ID3 methods', function (string $path) { - $audio = Audio::get($path); - $type = $audio->getType()->value; - $tags = $audio->getReader()->getTags(); - - if ($type === 'id3') { - $type = 'id3v2'; - } - - $metadata = $tags->{$type}(); - expect($metadata->toArray())->toBeArray(); -})->with([...AUDIO]); diff --git a/tests/WriterCoverTest.php b/tests/WriterCoverTest.php new file mode 100644 index 0000000..97cc1e1 --- /dev/null +++ b/tests/WriterCoverTest.php @@ -0,0 +1,109 @@ +write() + ->cover(FOLDER) + ->save(); + + $audio = Audio::read($path); + expect($audio->getTitle())->toBe('Introduction'); + + $content = base64_encode(file_get_contents(FOLDER)); + expect($audio->getCover()->getContents(true))->toBe($content); +})->with([MP3_WRITER]); + +it('can read use file content as cover', function (string $path) { + $audio = Audio::read($path); + + $tag = $audio->write() + ->cover(file_get_contents(FOLDER)); + + $tag->save(); + + $audio = Audio::read($path); + + $content = file_get_contents(FOLDER); + expect($audio->getCover()->getContents(true))->toBe(base64_encode($content)); +})->with([MP3_WRITER]); + +it('can read use tags', function (string $path) { + $audio = Audio::read($path); + + $random = (string) rand(1, 1000); + $coverData = file_get_contents(FOLDER); + $tag = $audio->write() + ->tags([ + 'title' => $random, + ]) + ->cover($coverData); + + $tag->save(); + + $audio = Audio::read($path); + expect($audio->getTitle())->toBe($random); + + $content = file_get_contents(FOLDER); + expect($audio->getCover()->getContents())->toBe($content); +})->with([MP3_WRITER]); + +it('can use tags with cover', function (string $path) { + $audio = Audio::read($path); + + $tag = $audio->write() + ->tags([ + 'title' => 'New Title', + ]) + ->cover(FOLDER); + + $tag->save(); + + $audio = Audio::read($path); + + $content = file_get_contents(FOLDER); + expect($audio->getTitle())->toBe('New Title'); + expect($tag->getCore()->cover->data)->toBe(base64_encode($content)); +})->with([MP3_WRITER]); + +it('can update cover with path', function () { + $audio = Audio::read(MP3_WRITER); + + $path = pathTo('cover.jpg', 'media'); + + if (file_exists($path)) { + unlink($path); + } + $audio->getCover()->extractCover($path); + expect(file_exists($path))->toBeTrue(); + + $audio->write() + ->cover(FOLDER) + ->save(); + + $audio = Audio::read(MP3_WRITER); + unlink($path); + $audio->getCover()->extractCover($path); + expect(file_exists($path))->toBeTrue(); + + $content = file_get_contents(FOLDER); + expect($audio->getCover()->getContents(true))->toBe(base64_encode($content)); +}); + +it('can remove cover', function () { + $audio = Audio::read(MP3_WRITER); + + $audio->write() + ->removeCover() + ->save(); + + $audio = Audio::read(MP3_WRITER); + + expect($audio->hasCover())->toBeFalse(); + expect($audio->getCover())->toBeNull(); +}); diff --git a/tests/WriterMp3Test.php b/tests/WriterMp3Test.php new file mode 100644 index 0000000..9a04363 --- /dev/null +++ b/tests/WriterMp3Test.php @@ -0,0 +1,118 @@ +write() + ->title('New Title') + ->artist('New Artist') + ->album('New Album') + ->genre('New Genre') + ->year(2022) + ->trackNumber('2/10') + ->albumArtist('New Album Artist') + ->comment('New Comment') + ->composer('New Composer') + ->discNumber('2/2') + ->isNotCompilation() + ->lyrics('New Lyrics') + ->creationDate('2021-01-01') + ->copyright('New Copyright') + ->encodingBy('New Encoding By') + ->encoding('New Encoding') + ->description('New Description') + ->synopsis('New Synopsis') + ->language('en') + ->copyright('New Copyright') + ->save(); + + $audio = Audio::read(MP3_WRITER); + testMp3Writed($audio); + expect($audio->getLanguage())->toBe('en'); + expect($audio->getCopyright())->toBe('New Copyright'); +}); + +it('can update only one tag', function () { + $audio = Audio::read(MP3_WRITER); + testMp3Writer($audio); + + $audio->write() + ->title('New Title') + ->save(); + + $audio = Audio::read(MP3_WRITER); + expect($audio->getTitle())->toBe('New Title'); + expect($audio->getArtist())->toBe('Mr Piouf'); + expect($audio->getAlbum())->toBe('P1PDD Le conclave de Troie'); + expect($audio->getGenre())->toBe('Roleplaying game'); + expect($audio->getYear())->toBe(2016); + expect($audio->getTrackNumber())->toBe('1'); + expect($audio->getComment())->toBe('http://www.p1pdd.com'); + expect($audio->getAlbumArtist())->toBe('P1PDD & Mr Piouf'); + expect($audio->getComposer())->toBe('P1PDD & Piouf'); + expect($audio->getDiscNumber())->toBe('1'); + expect($audio->isCompilation())->toBeTrue(); +}); + +it('can update tags manually', function () { + $audio = Audio::read(MP3_WRITER); + testMp3Writer($audio); + + $audio->write() + ->tags([ + 'title' => 'New Title', + 'artist' => 'New Artist', + 'album' => 'New Album', + 'genre' => 'New Genre', + 'year' => '2022', + 'track_number' => '2/10', + 'band' => 'New Album Artist', + 'comment' => 'New Comment', + 'composer' => 'New Composer', + 'part_of_a_set' => '2/2', + 'part_of_a_compilation' => false, + 'unsynchronised_lyric' => 'New Lyrics', + 'language' => 'en', + 'copyright' => 'New Copyright', + ]) + ->save(); + + $audio = Audio::read(MP3_WRITER); + testMp3Writed($audio); + expect($audio->getLanguage())->toBe('en'); + expect($audio->getCopyright())->toBe('New Copyright'); +}); + +it('can update tag', function () { + $audio = Audio::read(MP3_WRITER); + testMp3Writer($audio); + + $audio->write() + ->tag('title', 'New Title') + ->tag('artist', 'New Artist') + ->tags([ + 'album' => 'New Album', + 'part_of_a_compilation' => false, + ]) + ->albumArtist('New Album Artist') + ->cover(FOLDER) + ->save(); + + $audio = Audio::read(MP3_WRITER); + expect($audio->getTitle())->toBe('New Title'); + expect($audio->getArtist())->toBe('New Artist'); + expect($audio->getAlbum())->toBe('New Album'); + expect($audio->getAlbumArtist())->toBe('New Album Artist'); + expect($audio->isCompilation())->toBeFalse(); + expect($audio->getGenre())->toBe('Roleplaying game'); + + $content = base64_encode(file_get_contents(FOLDER)); + expect($audio->getCover()->getContents(true))->toBe($content); +}); diff --git a/tests/WriterTest.php b/tests/WriterTest.php index a1c3c60..0ea3720 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -1,13 +1,17 @@ update() + $audio->write() ->title($random) ->artist('New Artist') ->album('New Album') @@ -24,13 +28,9 @@ ->encoding('New Encoding') ->isNotCompilation() ->lyrics('New Lyrics') - ->stik('New Stik') - ->cover(FOLDER); - - $core = $tag->getCore(); - $tag->save(); + ->save(); - $audio = Audio::get($path); + $audio = Audio::read($path); expect($audio->getTitle())->toBe($random); expect($audio->getArtist())->toBe('New Artist'); @@ -52,70 +52,19 @@ if ($audio->getLyrics()) { expect($audio->getLyrics())->toBe('New Lyrics'); } - expect($audio->getStik())->toBeNull(); if ($audio->getFormat() !== AudioFormatEnum::mp3) { expect($audio->getTrackNumber())->toBe('2/10'); } else { expect($audio->getTrackNumber())->toBe('2'); } - - if ($tag->getCore()->hasCover()) { - $content = file_get_contents(FOLDER); - expect($tag->getCore()->getCover()->data())->toBe(base64_encode($content)); - } })->with(AUDIO_WRITER); -it('can read use file content as cover', function (string $path) { - $audio = Audio::get($path); - - $tag = $audio->update() - ->cover(file_get_contents(FOLDER)); - - $tag->save(); - - $audio = Audio::get($path); - - $content = file_get_contents(FOLDER); - expect($tag->getCore()->getCover()->data())->toBe(base64_encode($content)); -})->with([MP3_WRITER]); - -it('can read use tags', function (string $path) { - $audio = Audio::get($path); - - $random = (string) rand(1, 1000); - $image = getimagesize(FOLDER); - $coverData = file_get_contents(FOLDER); - $coverPicturetypeid = $image[2]; - $coverDescription = 'cover'; - $coverMime = $image['mime']; - $tag = $audio->update() - ->tags([ - 'title' => $random, - 'attached_picture' => [ - [ - 'data' => $coverData, - 'picturetypeid' => $coverPicturetypeid, - 'description' => $coverDescription, - 'mime' => $coverMime, - ], - ], - ]); - - $tag->save(); - - $audio = Audio::get($path); - expect($audio->getTitle())->toBe($random); - - $content = file_get_contents(FOLDER); - expect($audio->getCover()->getContents())->toBe($content); -})->with([MP3_WRITER]); - it('can update use tags with tag formats', function (string $path) { - $audio = Audio::get($path); + $audio = Audio::read($path); $random = (string) rand(1, 1000); - $tag = $audio->update() + $tag = $audio->write() ->tags([ 'title' => $random, ]) @@ -123,14 +72,14 @@ $tag->save(); - $audio = Audio::get($path); + $audio = Audio::read($path); expect($audio->getTitle())->toBe($random); })->with([MP3_WRITER]); it('can update with tags and handle native metadata', function (string $path) { - $audio = Audio::get($path); + $audio = Audio::read($path); - $tag = $audio->update() + $tag = $audio->write() ->isCompilation() ->tags([ 'title' => 'New Title', @@ -140,160 +89,117 @@ $tag->save(); - $audio = Audio::get($path); + $audio = Audio::read($path); expect($audio->getTitle())->toBe('New Title'); expect($audio->getAlbumArtist())->toBe('New Band'); expect($audio->isCompilation())->toBeTrue(); })->with([MP3_WRITER]); -it('can update with new path', function (string $path) { - $audio = Audio::get($path); - $newPath = 'tests/output/new.mp3'; - - $tag = $audio->update() - ->title('New Title') - ->path($newPath); - - $tag->save(); - - $audio = Audio::get($newPath); - expect($audio->getTitle())->toBe('New Title'); -})->with([MP3_WRITER]); - -it('can update with merged tags and core methods', function (string $path) { - $audio = Audio::get($path); - - $tag = $audio->update() - ->title('New Title') - ->tags([ - 'title' => 'New Title tag', - 'band' => 'New Band', - ]); - - $tag->save(); - - $audio = Audio::get($path); - expect($audio->getTitle())->toBe('New Title'); - expect($audio->getAlbumArtist())->toBe('New Band'); -})->with([MP3_WRITER]); - it('can use arrow function safe with unsupported tags', function (string $path) { - $audio = Audio::get($path); + $audio = Audio::read($path); - $tag = $audio->update() + $tag = $audio->write() ->title('New Title') ->encoding('New encoding'); expect(fn () => $tag->save())->not()->toThrow(Exception::class); - $audio = Audio::get($path); + $audio = Audio::read($path); expect($audio->getTitle())->toBe('New Title'); })->with([MP3_WRITER]); -it('can use arrow function safe with unsupported formats', function (string $path) { - $audio = Audio::get($path); - - $tag = $audio->update() - ->title('New Title Alac'); - - expect(fn () => $tag->save())->toThrow(Exception::class); -})->with([ALAC_WRITER]); - it('can get core before save', function (string $path) { - $audio = Audio::get($path); + $audio = Audio::read($path); - $tag = $audio->update() + $writer = $audio->write() ->title('New Title') ->tags([ 'title' => 'New Title tag', 'band' => 'New Band', ]); - expect($tag->getCore())->toBeInstanceOf(AudioCore::class); + expect($writer->getCore())->toBeInstanceOf(AudioCore::class); })->with([MP3_WRITER]); it('can handle exceptions', function (string $path) { - $audio = Audio::get($path); + $audio = Audio::read($path); - $tag = $audio->update() + $tag = $audio->write() ->tags([ 'title' => 'New Title', 'albumArtist' => 'New Album Artist', - ]) - ->options(['encoding' => 'UTF-8']); + ]); expect(fn () => $tag->save())->toThrow(Exception::class); })->with([MP3_WRITER]); it('can skip exceptions', function (string $path) { - $audio = Audio::get($path); + $audio = Audio::read($path); - $tag = $audio->update() + $tag = $audio->write() ->tags([ 'title' => 'New Title', 'albumArtist' => 'New Album Artist', ]) - ->preventFailOnError(); + ->skipErrors(); $tag->save(); - $audio = Audio::get($path); + $audio = Audio::read($path); expect($audio->getTitle())->toBe('New Title'); - expect($audio->getAlbumArtist())->toBeNull(); + expect($audio->getAlbumArtist())->toBe('P1PDD & Mr Piouf'); })->with([MP3_WRITER]); -it('can remove old tags', function (string $path) { - $audio = Audio::get($path); +it('can update with new path', function (string $path) { + $audio = Audio::read($path); + $newPath = 'tests/output/new.mp3'; - $tag = $audio->update() + $tag = $audio->write() ->title('New Title') - ->removeOldTags() - ->path('tests/output/new.mp3'); + ->path($newPath); $tag->save(); - $audio = Audio::get('tests/output/new.mp3'); + $audio = Audio::read($newPath); expect($audio->getTitle())->toBe('New Title'); - expect($audio->getAlbumArtist())->toBeNull(); -})->with([MP3]); +})->with([MP3_WRITER]); -it('can use tags with cover', function (string $path) { - $audio = Audio::get($path); +it('can update with merged tags and core methods', function (string $path) { + $audio = Audio::read($path); - $tag = $audio->update() + $tag = $audio->write() ->tags([ - 'title' => 'New Title', - ]) - ->cover(FOLDER); - + 'title' => 'New Title tag', + 'band' => 'New Band', + ]); $tag->save(); - $audio = Audio::get($path); - - $content = file_get_contents(FOLDER); - expect($audio->getTitle())->toBe('New Title'); - expect($tag->getCore()->getCover()->data())->toBe(base64_encode($content)); + $audio = Audio::read($path); + expect($audio->getTitle())->toBe('New Title tag'); + expect($audio->getAlbumArtist())->toBe('New Band'); })->with([MP3_WRITER]); -// it('can change podcast description and language', function () { -// $audio = Audio::get(AUDIOBOOK); -// $tag = $audio->update() -// ->title('New Title') -// ->podcastDescription('New Podcast Description') -// ->language('New Language') -// ->save(); -// }); +it('can use arrow function safe with unsupported formats', function (string $path) { + $audio = Audio::read($path); + + $tag = $audio->write() + ->title('New Title Alac'); + + expect(fn () => $tag->save())->toThrow(Exception::class); +})->with([ALAC_WRITER]); -// it('can not override tags', function (string $path) { -// $audio = Audio::get($path); +it('can remove old tags', function (string $path) { + $audio = Audio::read($path); + $newPath = 'tests/output/new.mp3'; -// $tag = $audio->update() -// ->getTitle('New Title') -// ->notOverrideTags() -// ->path('tests/output/new.mp3'); + $tag = $audio->write() + ->title('New Title') + ->removeOtherTags() + ->path($newPath); -// $tag->save(); + $tag->save(); -// $audio = Audio::get('tests/output/new.mp3'); -// expect($audio->getTitle())->toBe('Introduction'); -// })->with([MP3]); + $audio = Audio::read($newPath); + expect($audio->getTitle())->toBe('New Title'); + expect($audio->getAlbumArtist())->toBeNull(); +})->with([MP3]); diff --git a/tests/media/audiobook_rh-nocover.m4b b/tests/media/audiobook_rh-nocover.m4b new file mode 100644 index 0000000..4017a1b Binary files /dev/null and b/tests/media/audiobook_rh-nocover.m4b differ diff --git a/tests/media/audiobook_rh.m4b b/tests/media/audiobook_rh.m4b new file mode 100644 index 0000000..f5e4e88 Binary files /dev/null and b/tests/media/audiobook_rh.m4b differ diff --git a/tests/media/cover.jpg b/tests/media/cover.jpg new file mode 100644 index 0000000..4eb8fa2 Binary files /dev/null and b/tests/media/cover.jpg differ diff --git a/tests/media/default-folder.jpg b/tests/media/default-folder.jpg new file mode 100644 index 0000000..2a2de69 Binary files /dev/null and b/tests/media/default-folder.jpg differ