├── .vscode ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── phpstan.neon └── src ├── Audio.php ├── Core ├── AudioCore.php └── AudioCoreCover.php ├── Enums ├── AudioFormatEnum.php └── AudioTypeEnum.php ├── Id3 ├── Id3Reader.php ├── Id3Writer.php ├── Reader │ ├── Id3Audio.php │ ├── Id3AudioQuicktime.php │ ├── Id3AudioQuicktimeChapter.php │ ├── Id3AudioQuicktimeItem.php │ ├── Id3AudioTag.php │ ├── Id3Comments.php │ ├── Id3CommentsPicture.php │ ├── Id3Stream.php │ └── Id3Video.php └── Tag │ ├── Id3Tag.php │ ├── Id3TagApe.php │ ├── Id3TagAsf.php │ ├── Id3TagAudioV1.php │ ├── Id3TagAudioV2.php │ ├── Id3TagMatroska.php │ ├── Id3TagQuicktime.php │ ├── Id3TagRiff.php │ └── Id3TagVorbisComment.php └── Models ├── AudioCover.php └── AudioMetadata.php /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "intelephense.stubs": [ 3 | "apache", 4 | "bcmath", 5 | "bz2", 6 | "calendar", 7 | "com_dotnet", 8 | "Core", 9 | "ctype", 10 | "curl", 11 | "date", 12 | "dba", 13 | "dom", 14 | "enchant", 15 | "exif", 16 | "FFI", 17 | "fileinfo", 18 | "filter", 19 | "fpm", 20 | "ftp", 21 | "gd", 22 | "gettext", 23 | "gmp", 24 | "hash", 25 | "iconv", 26 | "imap", 27 | "intl", 28 | "json", 29 | "ldap", 30 | "libxml", 31 | "mbstring", 32 | "meta", 33 | "mysqli", 34 | "oci8", 35 | "odbc", 36 | "openssl", 37 | "pcntl", 38 | "pcre", 39 | "PDO", 40 | "pdo_ibm", 41 | "pdo_mysql", 42 | "pdo_pgsql", 43 | "pdo_sqlite", 44 | "pgsql", 45 | "Phar", 46 | "posix", 47 | "pspell", 48 | "random", 49 | "readline", 50 | "Reflection", 51 | "session", 52 | "shmop", 53 | "SimpleXML", 54 | "snmp", 55 | "soap", 56 | "sockets", 57 | "sodium", 58 | "SPL", 59 | "sqlite3", 60 | "standard", 61 | "superglobals", 62 | "sysvmsg", 63 | "sysvsem", 64 | "sysvshm", 65 | "tidy", 66 | "tokenizer", 67 | "xml", 68 | "xmlreader", 69 | "xmlrpc", 70 | "xmlwriter", 71 | "xsl", 72 | "Zend OPcache", 73 | "zip", 74 | "zlib", 75 | "imagick", 76 | "rar" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "install", 8 | "type": "shell", 9 | "command": "composer i", 10 | "problemMatcher": [], 11 | "presentation": { 12 | "revealProblems": "onProblem", 13 | "close": true 14 | } 15 | }, 16 | { 17 | "label": "update", 18 | "type": "shell", 19 | "command": "composer update", 20 | "problemMatcher": [], 21 | "presentation": { 22 | "revealProblems": "onProblem", 23 | "close": true 24 | } 25 | }, 26 | { 27 | "label": "tests", 28 | "type": "shell", 29 | "command": "composer test", 30 | "problemMatcher": [], 31 | "presentation": { 32 | "revealProblems": "onProblem", 33 | "close": true 34 | } 35 | }, 36 | { 37 | "label": "merge-to-main", 38 | "type": "shell", 39 | "command": "git checkout main && git merge develop && git push && git checkout develop", 40 | "problemMatcher": [], 41 | "presentation": { 42 | "revealProblems": "onProblem", 43 | "close": true 44 | } 45 | }, 46 | { 47 | "label": "pull-main", 48 | "type": "shell", 49 | "command": "git checkout main && git pull && git checkout develop && git merge main", 50 | "problemMatcher": [], 51 | "presentation": { 52 | "revealProblems": "onProblem", 53 | "close": true 54 | } 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `php-audio` will be documented in this file. 4 | 5 | ## v4.0.01 - 2024-10-03 6 | 7 | Add `toArray()` to `quicktime` for `AudioMetadata` 8 | 9 | ## v4.0.0 - 2024-10-03 10 | 11 | **BREAKING CHANGES** 12 | 13 | - internal architecture has been totally redesigned 14 | - `get()` static method is now `read()` (old method is still available) 15 | - `update()` method is now `write()` (old method is still available) 16 | - `getAudio()` is now `getMetadata()` 17 | - `getStat()` has been removed, you can find `getLastAccessAt()`, `getCreatedAt()`, `getModifiedAt()` into `getMetadata()` 18 | - `getWriter()` has been removed, only used when `write()` method is called 19 | - `getReader()` is now `getId3Reader()` 20 | - `getPodcastDescription()` is now `getSynopsis()` 21 | - `getStik()` has been removed (you can find it in `getRaw()` method or with `getRawKey('stik')`) 22 | - cover contents is now base64 encoded into `AudioCover` object 23 | - `toArray()` has been revised to return a more structured array 24 | - `getDuration()` is now `float` 25 | - add `getDurationHuman()` to get human readable duration 26 | - add `getTrackNumberInt()` to get track number as integer 27 | - add `getDiscNumberInt()` 28 | - `getTags()` is now `getRawAll()` as multidimensional array 29 | - new method `getRaw()` will return main format as array 30 | - new method `getRawKey('ANY_KEY')` will return specific key from main format 31 | - `getAudioFormats()` has been removed 32 | - `getExtras()` has been removed (duplicate of `getRawAll()`, `getRaw()` or `toArray()`) 33 | - `writer()` can now use `tag('ANY_KEY', 'ANY_VALUE')` to update directly any tag without use `tags()` 34 | - `writer()` method `tags()` has been modified, it's not native method of `getID3` anymore, just an array of tags 35 | - add `getQuicktime()` to `AudioMetadata` to get quicktime tags, with chapters for audiobooks 36 | 37 | *AudioCover* 38 | 39 | - now contents are stored as base64 encoded string 40 | - new `getContents()` method to get contents, default is raw string (binary) and you can get base64 encoded string with `true` parameter 41 | - new `getMimeType()` method to get mime type of the cover 42 | - new `getWidth()` method to get width of the cover 43 | - new `getHeight()` method to get height of the cover 44 | - new `toArray()` method to get cover as array 45 | 46 | *AudioMetadata* 47 | 48 | - `getFilesize()` is now `getFileSize()` 49 | - add `getSizeHuman()` to get human readable size with decimal precision 50 | - add `getDataFormat()` to get data format like `mp3` 51 | - remove `getDurationReadable()` because it's now into `Audio::class` 52 | - `getLossless()` is now `isLossless()` 53 | - add `getCodec()` to get codec of the file, like `LAME` 54 | - add `getEncoderOptions()` to get encoder options of the file, like `CBR` 55 | - add `getVersion()` to get version of `JamesHeinrich/getID3` 56 | - add `getAvDataOffset()` to get offset of audio/video data 57 | - add `getAvDataEnd()` to get end of audio/video data 58 | - add `getFilePath()` to get file path, like `/path/to` 59 | - add `getFilename()` to get filename, like `file.mp3` 60 | - add `getLastAccessAt()`, `getCreatedAt()`, `getModifiedAt()` from `stat()` function 61 | - add `toArray()` method to get metadata as array 62 | 63 | **Fix** 64 | 65 | - now `write()` won't erase other tags #34 66 | 67 | ## v3.0.08 - 2024-07-28 68 | 69 | Add `getDurationHumanReadable()` method to get the duration in human readable format: `HH:MM:SS`. 70 | 71 | ## v3.0.07 - 2024-06-04 72 | 73 | By @panVag 74 | 75 | * use `DateTimeImmutable` super powers to parse date and datetime strings 76 | * catch and ignore exception when instantiating the aforementioned object so the whole script won't fail 77 | * remove the null safe operators when it's not needed 78 | 79 | ## v3.0.06 - 2024-02-04 80 | 81 | - Add `getAudioFormats()` to `Audio::class` to get an array with all audio formats of file. 82 | - Add param to `getTags(?string $audioFormat = null)` to select a specific tag format, default will be maximum tags found. 83 | - Add same param to `getTag(string $tag, ?string $audioFormat = null)` to select a specific tag format, default will be maximum tags found. 84 | - Add `getPodcastDescription()` method to `Audio::class` to get the podcast description. 85 | - Add `getLanguage()` method to `Audio::class` to get the language of the podcast. 86 | 87 | ## v3.0.05 - 2024-02-03 88 | 89 | - Add `toArray()` method to `Audio::class` to get all properties without cover. 90 | - Add `getTags()` method to `Audio::class` to get all tags as `array`. 91 | - Add `getTag(string $tag)` method to `Audio::class` to get a single tag. 92 | 93 | ## v3.0.04 - 2023-11-01 94 | 95 | - `AudioCore`, fix `fromId3()` with `null` check `Id3AudioTagV1` and `Id3AudioTagV2`, issue #18 thanks to @cospin 96 | 97 | ## v3.0.03 - 2023-10-31 98 | 99 | - `AudioCore`, `fromId3()` method comment bug, issue #18 thanks to @cospin 100 | - `AudioMetadata`, add `path`, `dataformat` 101 | 102 | ## v3.0.02 - 2023-09-21 103 | 104 | - Id3Writer: `trackNumber()` and `discNumber()` accept now integers (and strings) 105 | 106 | ## v3.0.01 - 2023-09-20 107 | 108 | - Add `getContents()` to AudioCover 109 | - Old method `getContent()` is deprecated 110 | 111 | ## 3.0.0 - 2023-08-08 112 | 113 | ### BREAKING CHANGES 114 | 115 | - All simple getters have now `get` prefix. For example, `getTitle()` instead of `title()`, `getAlbum()` instead of `album()`, etc. It concerns all simple getters of `AudioCore`, `AudioCover`, `AudioMetadata`, `AudioStat`, `Id3Reader` classes. 116 | 117 | > Why? 118 | All these classes have some methods like setters or actions. To be consistent and clear, all simple getters have now `get` prefix. 119 | 120 | ## 2.0.0 - 2023-06-08 121 | 122 | **BREAKING CHANGES** 123 | 124 | - `update` chained methods are now without `set` prefix 125 | 126 | **Features** 127 | 128 | - `update` have now `tags` to set tags manually 129 | - `update` have now `tagFormats` to set tag formats manually 130 | - `update` have now `preventFailOnError` to prevent fail on error 131 | 132 | ## 1.0.2 - 2023-06-05 133 | 134 | - fix ci 135 | 136 | ## 1.0.1 - 2023-06-05 137 | 138 | - fix Windows bugs 139 | 140 | ## 1.0.0 - 2023-06-05 141 | 142 | Official version 143 | 144 | ### BREAKING CHANGES 145 | 146 | For `Audio::class` 147 | 148 | - to parse audio file `make` static method is now `get` 149 | 150 | ### New features 151 | 152 | - add `update` method to `Audio::class` to update metadata 153 | - `id3` property is now `reader` and `writer` 154 | - new enums `AudioTypeEnum` and `AudioFormatEnum` 155 | 156 | ## 0.3.0 - 2023-06-04 157 | 158 | - Add more formats 159 | 160 | ## 0.2.0 - 2023-06-04 161 | 162 | - add multiple formats 163 | - add some properties 164 | - refactoring 165 | 166 | ## 0.1.0 - 2023-06-04 167 | 168 | init 169 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) kiwilan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Audio 2 | 3 | ![Banner with speaker and PHP Audio title](https://raw.githubusercontent.com/kiwilan/php-audio/main/docs/banner.jpg) 4 | 5 | [![php][php-version-src]][php-version-href] 6 | [![version][version-src]][version-href] 7 | [![downloads][downloads-src]][downloads-href] 8 | [![license][license-src]][license-href] 9 | [![tests][tests-src]][tests-href] 10 | [![codecov][codecov-src]][codecov-href] 11 | 12 | PHP package to parse and update audio files metadata, with [`JamesHeinrich/getID3`](https://github.com/JamesHeinrich/getID3). 13 | 14 | > [!NOTE] 15 | > 16 | > You can check formats supported on [Supported formats](#supported-formats) section. 17 | 18 | ## About 19 | 20 | Audio files can use different formats, this package aims to provide a simple way to read them with [`JamesHeinrich/getID3`](https://github.com/JamesHeinrich/getID3). The `JamesHeinrich/getID3` package is excellent to read metadata from audio files, but output is just an array, current package aims to provide a simple way to read audio files with a beautiful API. 21 | 22 | ## Requirements 23 | 24 | - PHP `8.1` minimum 25 | - Optional for update 26 | - `FLAC`: `flac` (with `apt`, `brew` or `scoop`) 27 | - `OGG`: `vorbis-tools` (with `apt` or `brew`) / `extras/icecast` (with `scoop`) 28 | 29 | ### Roadmap 30 | 31 | - Add support for more formats with [external packages](https://askubuntu.com/questions/226773/how-to-read-mp3-tags-in-shell) 32 | 33 | ## Installation 34 | 35 | You can install the package via [composer](https://getcomposer.org/): 36 | 37 | ```bash 38 | composer require kiwilan/php-audio 39 | ``` 40 | 41 | ## Usage 42 | 43 | Core metadata: 44 | 45 | ```php 46 | use Kiwilan\Audio\Audio; 47 | 48 | $audio = Audio::read('path/to/audio.mp3'); 49 | 50 | $audio->getTitle(); // `?string` to get title 51 | $audio->getArtist(); // `?string` to get artist 52 | $audio->getAlbum(); // `?string` to get album 53 | $audio->getGenre(); // `?string` to get genre 54 | $audio->getYear(); // `?int` to get year 55 | $audio->getTrackNumber(); // `?string` to get track number 56 | $audio->getComment(); // `?string` to get comment 57 | $audio->getAlbumArtist(); // `?string` to get album artist 58 | $audio->getComposer(); // `?string` to get composer 59 | $audio->getDiscNumber(); // `?string` to get disc number 60 | $audio->isCompilation(); // `bool` to know if is compilation 61 | $audio->getCreationDate(); // `?string` to get creation date 62 | $audio->getCopyright(); // `?string` to get copyright 63 | $audio->getEncoding(); // `?string` to get encoding 64 | $audio->getDescription(); // `?string` to get description 65 | $audio->getSynopsis(); // `?string` to get synopsis 66 | $audio->getLanguage(); // `?string` to get language 67 | $audio->getLyrics(); // `?string` 68 | $audio->getDuration(); // `?float` to get duration in seconds 69 | $audio->getDurationHuman(); // `?string` to get duration in human readable format 70 | ``` 71 | 72 | Raw tags: 73 | 74 | ```php 75 | use Kiwilan\Audio\Audio; 76 | 77 | $audio = Audio::read('path/to/audio.mp3'); 78 | 79 | $raw_all = $audio->getRawAll(); // `array` with all tags 80 | $raw = $audio->getRaw(); // `array` with main tag 81 | $title = $audio->getRawKey('title'); // `?string` to get title same as `$audio->getTitle()` 82 | 83 | $format = $audio->getRaw('id3v2'); // `?array` with all tags with format `id3v2` 84 | $title = $audio->getRawKey('title', 'id3v2'); // `?string` to get title with format `id3v2` 85 | ``` 86 | 87 | Additional metadata: 88 | 89 | ```php 90 | use Kiwilan\Audio\Audio; 91 | 92 | $audio = Audio::read('path/to/audio.mp3'); 93 | 94 | $audio->getPath(); // `string` to get path 95 | $audio->getExtension(); // `string` to get extension 96 | $audio->hasCover(); // `bool` to know if has cover 97 | $audio->isValid(); // `bool` to know if file is valid audio file 98 | $audio->isWritable(); // `bool` to know if file is writable 99 | $audio->getFormat(); // `AudioFormatEnum` to get format (mp3, m4a, ...) 100 | $audio->getType(); // `?AudioTypeEnum` ID3 type (id3, riff, asf, quicktime, matroska, ape, vorbiscomment) 101 | ``` 102 | 103 | You can use `toArray()` method to get raw info: 104 | 105 | ```php 106 | use Kiwilan\Audio\Audio; 107 | 108 | $audio = Audio::read('path/to/audio.mp3'); 109 | 110 | $audio->toArray(); // `array` with all metadata 111 | ``` 112 | 113 | Advanced properties: 114 | 115 | ```php 116 | use Kiwilan\Audio\Audio; 117 | 118 | $audio = Audio::read('path/to/audio.mp3'); 119 | 120 | $audio->getId3Reader(); // `?Id3Reader` reader based on `getID3` 121 | $audio->getMetadata(); // `?AudioMetadata` with audio metadata 122 | $audio->getCover(); // `?AudioCover` with cover metadata 123 | ``` 124 | 125 | ### Update 126 | 127 | You can update audio files metadata with `Audio::class`, but not all formats are supported. [See supported formats](#updatable-formats) 128 | 129 | > [!WARNING] 130 | > 131 | > You can use any property of `Audio::class` but if you use a property not supported by the format, it will be ignored. 132 | 133 | ```php 134 | use Kiwilan\Audio\Audio; 135 | 136 | $audio = Audio::read('path/to/audio.mp3'); 137 | $audio->getTitle(); // `Title` 138 | 139 | $tag = $audio->write() 140 | ->title('New Title') 141 | ->artist('New Artist') 142 | ->album('New Album') 143 | ->genre('New Genre') 144 | ->year('2022') 145 | ->trackNumber('2/10') 146 | ->albumArtist('New Album Artist') 147 | ->comment('New Comment') 148 | ->composer('New Composer') 149 | ->creationDate('2021-01-01') 150 | ->description('New Description') 151 | ->synopsis('New Synopsis') 152 | ->discNumber('2/2') 153 | ->encodingBy('New Encoding By') 154 | ->encoding('New Encoding') 155 | ->isCompilation() 156 | ->lyrics('New Lyrics') 157 | ->cover('path/to/cover.jpg') // you can use file content `file_get_contents('path/to/cover.jpg')` 158 | ->save(); 159 | 160 | $audio = Audio::read('path/to/audio.mp3'); 161 | $audio->getTitle(); // `New Title` 162 | $audio->getCreationDate(); // `null` because `creationDate` is not supported by `MP3` 163 | ``` 164 | 165 | Some properties are not supported by all formats, for example `MP3` can't handle some properties like `lyrics` or `stik`, if you try to update these properties, they will be ignored. 166 | 167 | #### Set tags manually 168 | 169 | 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). 170 | 171 | > [!WARNING] 172 | > 173 | > 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. 174 | > 175 | > If your key is not supported, `save` method will throw an exception, unless you use `skipErrors`. 176 | 177 | ```php 178 | use Kiwilan\Audio\Audio; 179 | 180 | $audio = Audio::read('path/to/audio.mp3'); 181 | $audio->getAlbumArtist(); // `Band` 182 | 183 | $tag = $audio->write() 184 | ->tag('composer', 'New Composer') 185 | ->tag('genre', 'New Genre') // can be chained 186 | ->tags([ 187 | 'title' => 'New Title', 188 | 'band' => 'New Band', // `band` is used by `id3v2` to set album artist, method is `albumArtist` but `albumArtist` key will throw an exception with `id3v2` 189 | ]) 190 | ->tagFormats(['id3v1', 'id3v2.4']) // optional 191 | ->save(); 192 | 193 | $audio = Audio::read('path/to/audio.mp3'); 194 | $audio->getAlbumArtist(); // `New Band` 195 | ``` 196 | 197 | #### Arrow functions 198 | 199 | ```php 200 | use Kiwilan\Audio\Audio; 201 | 202 | $audio = Audio::read('path/to/audio.mp3'); 203 | $audio->getAlbumArtist(); // `Band` 204 | 205 | $tag = $audio->write() 206 | ->title('New Title') 207 | ->albumArtist('New Band') // `albumArtist` will set `band` for `id3v2`, exception safe 208 | ->save(); 209 | 210 | $audio = Audio::read('path/to/audio.mp3'); 211 | $audio->getAlbumArtist(); // `New Band` 212 | ``` 213 | 214 | #### Skip errors 215 | 216 | You can use `skipErrors` to prevent exception if you use unsupported format. 217 | 218 | ```php 219 | use Kiwilan\Audio\Audio; 220 | 221 | $audio = Audio::read('path/to/audio.mp3'); 222 | 223 | $tag = $audio->write() 224 | ->tags([ 225 | 'title' => 'New Title', 226 | 'title2' => 'New title', // not supported by `id3v2`, will throw an exception 227 | ]) 228 | ->skipErrors() // will prevent exception 229 | ->save(); 230 | ``` 231 | 232 | > [!NOTE] 233 | > 234 | > Arrow functions are exception safe for properties but not for unsupported formats. 235 | 236 | ### Raw tags 237 | 238 | Audio files format metadata with different methods, `JamesHeinrich/getID3` offer to check these metadatas by different methods. In `raw_all` property of `Audio::class`, you will find raw metadata from `JamesHeinrich/getID3` package, like `id3v2`, `id3v1`, `riff`, `asf`, `quicktime`, `matroska`, `ape`, `vorbiscomment`... 239 | 240 | If you want to extract specific field which can be skipped by `Audio::class`, you can use `raw_all` property. 241 | 242 | ```php 243 | use Kiwilan\Audio\Audio; 244 | 245 | $audio = Audio::read('path/to/audio.mp3'); 246 | $raw_all = $audio->getRawAll(); // all formats 247 | $raw = $audio->getRaw(); // main format 248 | ``` 249 | 250 | ### AudioMetadata 251 | 252 | ```php 253 | use Kiwilan\Audio\Audio; 254 | 255 | $audio = Audio::read('path/to/audio.mp3'); 256 | $metadata = $audio->getMetadata(); 257 | 258 | $metadata->getFileSize(); // `?int` in bytes 259 | $metadata->getSizeHuman(); // `?string` (1.2 MB, 1.2 GB, ...) 260 | $metadata->getExtension(); // `?string` (mp3, m4a, ...) 261 | $metadata->getEncoding(); // `?string` (UTF-8...) 262 | $metadata->getMimeType(); // `?string` (audio/mpeg, audio/mp4, ...) 263 | $metadata->getDurationSeconds(); // `?float` in seconds 264 | $metadata->getDurationReadable(); // `?string` (00:00:00) 265 | $metadata->getBitrate(); // `?int` in kbps 266 | $metadata->getBitrateMode(); // `?string` (cbr, vbr, ...) 267 | $metadata->getSampleRate(); // `?int` in Hz 268 | $metadata->getChannels(); // `?int` (1, 2, ...) 269 | $metadata->getChannelMode(); // `?string` (mono, stereo, ...) 270 | $metadata->isLossless(); // `bool` to know if is lossless 271 | $metadata->getCompressionRatio(); // `?float` 272 | $metadata->getFilesize(); // `?int` in bytes 273 | $metadata->getSizeHuman(); // `?string` (1.2 MB, 1.2 GB, ...) 274 | $metadata->getDataFormat(); // `?string` (mp3, m4a, ...) 275 | $metadata->getWarning(); // `?array` 276 | $metadata->getQuicktime(); // `?Id3AudioQuicktime 277 | $metadata->getCodec(); // `?string` (mp3, aac, ...) 278 | $metadata->getEncoderOptions(); // `?string` 279 | $metadata->getVersion(); // `?string` 280 | $metadata->getAvDataOffset(); // `?int` in bytes 281 | $metadata->getAvDataEnd(); // `?int` in bytes 282 | $metadata->getFilePath(); // `?string` 283 | $metadata->getFilename(); // `?string` 284 | $metadata->getLastAccessAt(); // `?DateTime` 285 | $metadata->getCreatedAt(); // `?DateTime` 286 | $metadata->getModifiedAt(); // `?DateTime` 287 | $metadata->toArray(); 288 | ``` 289 | 290 | ### Quicktime 291 | 292 | For `quicktime` type, like for M4B audiobook, you can use `Id3TagQuicktime` to get more informations. 293 | 294 | ```php 295 | use Kiwilan\Audio\Audio; 296 | 297 | $audio = Audio::read('path/to/audio.m4b'); 298 | $quicktime = $audio->getMetadata()->getQuicktime(); 299 | 300 | $quicktime->getHinting(); 301 | $quicktime->getController(); 302 | $quicktime->getFtyp(); 303 | $quicktime->getTimestampsUnix(); 304 | $quicktime->getTimeScale(); 305 | $quicktime->getDisplayScale(); 306 | $quicktime->getVideo(); 307 | $quicktime->getAudio(); 308 | $quicktime->getSttsFramecount(); 309 | $quicktime->getComments(); 310 | $quicktime->getFree(); 311 | $quicktime->getWide(); 312 | $quicktime->getMdat(); 313 | $quicktime->getEncoding(); 314 | $quicktime->getChapters(); // ?Id3AudioQuicktimeChapter[] 315 | ``` 316 | 317 | ### AudioCover 318 | 319 | ```php 320 | use Kiwilan\Audio\Audio; 321 | 322 | $audio = Audio::read('path/to/audio.mp3'); 323 | $cover = $audio->getCover(); 324 | 325 | $cover->getContents(); // `?string` raw file 326 | $cover->getMimeType(); // `?string` (image/jpeg, image/png, ...) 327 | $cover->getWidth(); // `?int` in pixels 328 | $cover->getHeight(); // `?int` in pixels 329 | ``` 330 | 331 | ## Supported formats 332 | 333 | ### Readable formats 334 | 335 | - `id3v2` will be selected before `id3v1` or `riff` if both are available. 336 | 337 | | Format | Supported | About | ID3 type | Notes | 338 | | :----: | :-------: | :----------------------------------: | :-------------: | :-------------------: | 339 | | AAC | ❌ | Advanced Audio Coding | | | 340 | | ALAC | ✅ | Apple Lossless Audio Codec | `quicktime` | | 341 | | AIF | ✅ | Audio Interchange File Format (aif) | `id3v2`,`riff` | | 342 | | AIFC | ✅ | Audio Interchange File Format (aifc) | `id3v2`,`riff` | | 343 | | AIFF | ✅ | Audio Interchange File Format (aiff) | `id3v2`,`riff` | | 344 | | DSF | ❌ | Direct Stream Digital Audio | | | 345 | | FLAC | ✅ | Free Lossless Audio Codec | `vorbiscomment` | | 346 | | MKA | ✅ | Matroska | `matroska` | _Cover not supported_ | 347 | | MKV | ✅ | Matroska | `matroska` | _Cover not supported_ | 348 | | APE | ❌ | Monkey's Audio | | | 349 | | MP3 | ✅ | MPEG audio layer 3 | `id3v2`,`id3v1` | | 350 | | MP4 | ✅ | Digital multimedia container format | `quicktime` | _Partially supported_ | 351 | | M4A | ✅ | mpeg-4 audio | `quicktime` | | 352 | | M4B | ✅ | Audiobook | `quicktime` | | 353 | | M4V | ✅ | mpeg-4 video | `quicktime` | | 354 | | MPC | ❌ | Musepack | | | 355 | | OGG | ✅ | Open container format | `vorbiscomment` | | 356 | | OPUS | ✅ | IETF Opus audio | `vorbiscomment` | | 357 | | OFR | ❌ | OptimFROG | | | 358 | | OFS | ❌ | OptimFROG | | | 359 | | SPX | ✅ | Speex | `vorbiscomment` | _Cover not supported_ | 360 | | TAK | ❌ | Tom's Audio Kompressor | | | 361 | | TTA | ✅ | True Audio | `ape` | _Cover not supported_ | 362 | | WMA | ✅ | Windows Media Audio | `asf` | _Cover not supported_ | 363 | | WV | ✅ | WavPack | `ape` | | 364 | | WAV | ✅ | Waveform Audio | `id3v2`,`riff` | | 365 | | WEBM | ✅ | WebM | `matroska` | _Cover not supported_ | 366 | 367 | You want to add a format? [See FAQ](#faq) 368 | 369 | ### Updatable formats 370 | 371 | `JamesHeinrich/getID3` can update some formats, but not all. 372 | 373 | > - ID3v1 (v1 & v1.1) 374 | > - ID3v2 (v2.3, v2.4) 375 | > - APE (v2) 376 | > - Ogg Vorbis comments (need `vorbis-tools`) 377 | > - FLAC comments (need `flac`) 378 | 379 | | Format | Notes | Requires | 380 | | :----: | :-------------------: | :------------: | 381 | | FLAC | _Cover not supported_ | `flac` | 382 | | MP3 | | | 383 | | OGG | _Cover not supported_ | `vorbis-tools` | 384 | 385 | - `flac`: with `apt`, `brew` or `scoop` 386 | - `vorbis-tools`: with `apt`, `brew` or `scoop` 387 | - With `scoop`, `vorbis-tools` is not available, you can use `extras/icecast` instead. 388 | 389 | ### Convert properties 390 | 391 | `Audio::class` convert some properties to be more readable. 392 | 393 | - `ape` format: [`Id3TagApe`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagApe.php) 394 | - `asf` format: [`Id3TagAsf`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagAsf.php) 395 | - `id3v1` format: [`Id3TagAudioV1`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagAudioV1.php) 396 | - `id3v2` format: [`Id3TagAudioV2`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagAudioV2.php) 397 | - `matroska` format: [`Id3TagMatroska`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagMatroska.php) 398 | - `quicktime` format: [`Id3TagQuicktime`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagQuicktime.php) 399 | - `vorbiscomment` format: [`Id3TagVorbisComment`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagVorbisComment.php) 400 | - `riff` format: [`Id3TagRiff`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagRiff.php) 401 | - `unknown` format: [`Id3TagVorbisComment`](https://github.com/kiwilan/php-audio/blob/main/src/Id3/Tag/Id3TagVorbisComment.php) 402 | 403 | | ID3 type | Original | New property | 404 | | :-------------: | :---------------------: | :--------------: | 405 | | `id3v2` | `band` | `album_artist` | 406 | | `id3v2` | `part_of_a_set` | `disc_number` | 407 | | `id3v2` | `part_of_a_compilation` | `is_compilation` | 408 | | `quicktime` | `compilation` | `is_compilation` | 409 | | `quicktime` | `encoded_by` | `encoding_by` | 410 | | `quicktime` | `encoding_tool` | `encoding` | 411 | | `quicktime` | `description_long` | `synopsis` | 412 | | `asf` | `albumartist` | `album_artist` | 413 | | `asf` | `partofset` | `disc_number` | 414 | | `asf` | `encodingsettings` | `encoding` | 415 | | `vorbiscomment` | `encoder` | `encoding` | 416 | | `vorbiscomment` | `albumartist` | `album_artist` | 417 | | `vorbiscomment` | `discnumber` | `disc_number` | 418 | | `vorbiscomment` | `compilation` | `is_compilation` | 419 | | `vorbiscomment` | `tracknumber` | `track_number` | 420 | | `matroska` | `disc` | `disc_number` | 421 | | `matroska` | `part_number` | `track_number` | 422 | | `matroska` | `date` | `year` | 423 | | `matroska` | `compilation` | `is_compilation` | 424 | | `matroska` | `encoder` | `encoding` | 425 | | `ape` | `disc` | `disc_number` | 426 | | `ape` | `compilation` | `is_compilation` | 427 | | `ape` | `track` | `track_number` | 428 | | `ape` | `date` | `year` | 429 | | `ape` | `encoder` | `encoding` | 430 | 431 | ## Testing 432 | 433 | ```bash 434 | composer test 435 | ``` 436 | 437 | ## Tools 438 | 439 | - [ffmpeg](https://ffmpeg.org/): free and open-source software project consisting of a suite of libraries and programs for handling video, audio, and other multimedia files and streams. 440 | - [MP3TAG](https://www.mp3tag.de/en/): powerful and easy-to-use tool to edit metadata of audio files (free on Windows). 441 | - [Audiobook Builder](https://www.splasm.com/audiobookbuilder/): makes it easy to turn audio CDs and files into audiobooks (only macOS and paid). 442 | - [Tag Editor](https://github.com/Martchus/tageditor): A tag editor with Qt GUI and command-line interface supporting MP4/M4A/AAC (iTunes), ID3, Vorbis, Opus, FLAC and Matroska. 443 | - [Tag Editor](https://amvidia.com/tag-editor): a spreadsheet application for editing audio metadata in a simple, fast, and flexible way. 444 | 445 | ## FAQ 446 | 447 | ### I have a specific metadata field in my audio files, what can I do? 448 | 449 | In `Audio::class`, you have a property `raw_all` which contains all raw metadata, if `JamesHeinrich/getID3` support this field, you will find it in this property. 450 | 451 | ```php 452 | use Kiwilan\Audio\Audio; 453 | 454 | $audio = Audio::read('path/to/audio.mp3'); 455 | $raw_all = $audio->getRawAll()); 456 | 457 | $custom = null; 458 | $id3v2 = $raw_all['id3v2'] ?? []; 459 | 460 | if ($id3v2) { 461 | $custom = $id3v2['custom'] ?? null; 462 | } 463 | ``` 464 | 465 | If your field could be added to global properties of `Audio::class`, you could create an [an issue](https://github.com/kiwilan/php-audio/issues/new/choose). 466 | 467 | ### Metadata are `null`, what can I do? 468 | 469 | You can check `extras` property to know if some metadata are available. 470 | 471 | ```php 472 | use Kiwilan\Audio\Audio; 473 | 474 | $audio = Audio::read('path/to/audio.mp3'); 475 | 476 | $raw_all = $audio->getRawAll(); 477 | var_dump($raw_all); 478 | ``` 479 | 480 | If you find metadata which are not parsed by `Audio::class`, you can create [an issue](https://github.com/kiwilan/php-audio/issues/new/choose), otherwise `JamesHeinrich/getID3` doesn't support this metadata.z 481 | 482 | ### My favorite format is not supported, what can I do? 483 | 484 | You can create [an issue](https://github.com/kiwilan/php-audio/issues/new/choose) with the format name and a link to the format documentation. If `JamesHeinrich/getID3` support this format, I will add it to this package but if you want to contribute, you can create a pull request with the format implementation. 485 | 486 | **Please give me an example file to test the format.** 487 | 488 | ### I have an issue with a supported format, what can I do? 489 | 490 | You can create [an issue](https://github.com/kiwilan/php-audio/issues/new/choose) with informations. 491 | 492 | ### How to convert audio files? 493 | 494 | This package doesn't provide a way to convert audio files, but you can use [ffmpeg](https://ffmpeg.org/) to convert audio files and [PHP-FFMpeg/PHP-FFMpeg](https://github.com/PHP-FFMpeg/PHP-FFMpeg). 495 | 496 | ## Changelog 497 | 498 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 499 | 500 | ## Credits 501 | 502 | - [`ewilan-riviere`](https://github.com/ewilan-riviere): package author 503 | - [`JamesHeinrich/getID3`](https://github.com/JamesHeinrich/getID3): parser used to read audio files 504 | - [`spatie/package-skeleton-php`](https://github.com/spatie/package-skeleton-php): package skeleton used to create this package 505 | 506 | ## License 507 | 508 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 509 | 510 | [](https://github.com/kiwilan) 511 | 512 | [version-src]: https://img.shields.io/packagist/v/kiwilan/php-audio.svg?style=flat&colorA=18181B&colorB=777BB4 513 | [version-href]: https://packagist.org/packages/kiwilan/php-audio 514 | [php-version-src]: https://img.shields.io/static/v1?style=flat&label=PHP&message=v8.1&color=777BB4&logo=php&logoColor=ffffff&labelColor=18181b 515 | [php-version-href]: https://www.php.net/ 516 | [downloads-src]: https://img.shields.io/packagist/dt/kiwilan/php-audio.svg?style=flat&colorA=18181B&colorB=777BB4 517 | [downloads-href]: https://packagist.org/packages/kiwilan/php-audio 518 | [license-src]: https://img.shields.io/github/license/kiwilan/php-audio.svg?style=flat&colorA=18181B&colorB=777BB4 519 | [license-href]: https://github.com/kiwilan/php-audio/blob/main/README.md 520 | [tests-src]: https://img.shields.io/github/actions/workflow/status/kiwilan/php-audio/run-tests.yml?branch=main&label=tests&style=flat&colorA=18181B 521 | [tests-href]: https://packagist.org/packages/kiwilan/php-audio 522 | [codecov-src]: https://img.shields.io/codecov/c/gh/kiwilan/php-audio/main?style=flat&colorA=18181B&colorB=777BB4 523 | [codecov-href]: https://codecov.io/gh/kiwilan/php-audio 524 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kiwilan/php-audio", 3 | "description": "PHP package to parse and update audio files metadata, with `JamesHeinrich/getID3`.", 4 | "version": "4.0.01", 5 | "keywords": [ 6 | "audio", 7 | "php", 8 | "mp3", 9 | "id3", 10 | "id3v2", 11 | "id3v1", 12 | "audiobook", 13 | "alac", 14 | "aif", 15 | "aifc", 16 | "aiff", 17 | "flac", 18 | "mka", 19 | "mkv", 20 | "mp4", 21 | "m4a", 22 | "m4b", 23 | "m4v", 24 | "ogg", 25 | "opus", 26 | "spx", 27 | "tta", 28 | "wma", 29 | "wv", 30 | "wav", 31 | "webm", 32 | "music" 33 | ], 34 | "homepage": "https://github.com/kiwilan/php-audio", 35 | "license": "MIT", 36 | "authors": [ 37 | { 38 | "name": "Ewilan Rivière", 39 | "email": "ewilan.riviere@gmail.com", 40 | "role": "Developer" 41 | } 42 | ], 43 | "require": { 44 | "php": "^8.1", 45 | "james-heinrich/getid3": "^v1.9.22" 46 | }, 47 | "require-dev": { 48 | "pestphp/pest": "^2.0", 49 | "phpstan/phpstan": "^1.12", 50 | "laravel/pint": "^1.2", 51 | "spatie/ray": "^1.28" 52 | }, 53 | "autoload": { 54 | "psr-4": { 55 | "Kiwilan\\Audio\\": "src" 56 | } 57 | }, 58 | "autoload-dev": { 59 | "psr-4": { 60 | "Kiwilan\\Audio\\Tests\\": "tests" 61 | } 62 | }, 63 | "scripts": { 64 | "test": "vendor/bin/pest", 65 | "test-filter": "vendor/bin/pest --filter", 66 | "test-parallel": "vendor/bin/pest --parallel", 67 | "test-coverage": "vendor/bin/pest --coverage --min=90", 68 | "test-coverage-parallel": "vendor/bin/pest --parallel --coverage", 69 | "analyse": "vendor/bin/phpstan analyse", 70 | "format": "vendor/bin/pint" 71 | }, 72 | "config": { 73 | "sort-packages": true, 74 | "allow-plugins": { 75 | "pestphp/pest-plugin": true, 76 | "phpstan/extension-installer": true 77 | } 78 | }, 79 | "minimum-stability": "dev", 80 | "prefer-stable": true 81 | } 82 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | tmpDir: public/build/.phpstan 3 | 4 | paths: 5 | - src 6 | 7 | # The level 9 is the highest level 8 | level: 5 9 | -------------------------------------------------------------------------------- /src/Audio.php: -------------------------------------------------------------------------------- 1 | $raw_all 17 | */ 18 | protected function __construct( 19 | protected string $path, 20 | protected string $extension, 21 | protected AudioFormatEnum $format, 22 | protected ?AudioTypeEnum $type = null, 23 | protected ?AudioMetadata $metadata = null, 24 | protected ?AudioCover $cover = null, 25 | protected ?float $duration = null, 26 | protected bool $is_writable = false, 27 | protected bool $is_valid = false, 28 | protected bool $has_cover = false, 29 | // 30 | protected ?string $title = null, 31 | protected ?string $artist = null, 32 | protected ?string $album = null, 33 | protected ?string $genre = null, 34 | protected ?int $year = null, 35 | protected ?string $track_number = null, 36 | protected ?string $comment = null, 37 | protected ?string $album_artist = null, 38 | protected ?string $composer = null, 39 | protected ?string $disc_number = null, 40 | protected bool $is_compilation = false, 41 | protected ?string $creation_date = null, 42 | protected ?string $copyright = null, 43 | protected ?string $encoding_by = null, 44 | protected ?string $encoding = null, 45 | protected ?string $description = null, 46 | protected ?string $synopsis = null, 47 | protected ?string $language = null, 48 | protected ?string $lyrics = null, 49 | 50 | protected array $raw_all = [], 51 | ) {} 52 | 53 | public static function read(string $path): self 54 | { 55 | $fileExists = file_exists($path); 56 | if (! $fileExists) { 57 | throw new \Exception("File not found: {$path}"); 58 | } 59 | 60 | $extension = pathinfo($path, PATHINFO_EXTENSION); 61 | $extension = strtolower($extension); 62 | $format = AudioFormatEnum::tryFrom($extension); 63 | 64 | $self = new self( 65 | path: $path, 66 | extension: $extension, 67 | format: $format ? $format : AudioFormatEnum::unknown, 68 | ); 69 | 70 | try { 71 | $id3_reader = Id3Reader::make($path); 72 | 73 | $self->metadata = AudioMetadata::make($self, $id3_reader); 74 | $self->duration = (float) number_format((float) $self->metadata->getDurationSeconds(), 2, '.', ''); 75 | $self->is_writable = $id3_reader->isWritable(); 76 | 77 | $self->parseTags($id3_reader); 78 | } catch (\Throwable $th) { 79 | error_log($th->getMessage()); 80 | } 81 | 82 | return $self; 83 | } 84 | 85 | /** 86 | * @deprecated Use `read()` method instead. 87 | * 88 | * Get audio file from path. 89 | */ 90 | public static function get(string $path): self 91 | { 92 | return self::read($path); 93 | } 94 | 95 | /** 96 | * Get audio file path, like `/path/to/audio.mp3`. 97 | */ 98 | public function getPath(): string 99 | { 100 | return $this->path; 101 | } 102 | 103 | /** 104 | * Get audio file extension, like `mp3`. 105 | */ 106 | public function getExtension(): string 107 | { 108 | return $this->extension; 109 | } 110 | 111 | /** 112 | * Get audio format if recognized, like `AudioFormatEnum::mp3`. 113 | */ 114 | public function getFormat(): AudioFormatEnum 115 | { 116 | return $this->format; 117 | } 118 | 119 | /** 120 | * Get audio type if recognized, like `AudioTypeEnum::id3`. 121 | */ 122 | public function getType(): ?AudioTypeEnum 123 | { 124 | return $this->type; 125 | } 126 | 127 | /** 128 | * Get audio metadata. 129 | */ 130 | public function getMetadata(): ?AudioMetadata 131 | { 132 | return $this->metadata; 133 | } 134 | 135 | /** 136 | * Get audio cover. 137 | */ 138 | public function getCover(): ?AudioCover 139 | { 140 | return $this->cover; 141 | } 142 | 143 | public function getId3Reader(): ?Id3Reader 144 | { 145 | return Id3Reader::make($this->path); 146 | } 147 | 148 | public function write(): Id3Writer 149 | { 150 | return Id3Writer::make($this); 151 | } 152 | 153 | /** 154 | * @deprecated Use `write()` method instead. 155 | * 156 | * Update audio file. 157 | */ 158 | public function update(): Id3Writer 159 | { 160 | return $this->write(); 161 | } 162 | 163 | /** 164 | * Get duration of the audio file in seconds, limited to 2 decimals, like `180.66` 165 | * 166 | * To get exact duration, use `getMetadata()->getDurationSeconds()` instead. 167 | */ 168 | public function getDuration(): ?float 169 | { 170 | return $this->duration; 171 | } 172 | 173 | /** 174 | * Get duration of the audio file in human readable format, like `00:03:00` 175 | */ 176 | public function getDurationHuman(): ?string 177 | { 178 | return gmdate('H:i:s', intval($this->duration)); 179 | } 180 | 181 | /** 182 | * To know if the audio file is writable. 183 | */ 184 | public function isWritable(): bool 185 | { 186 | return $this->is_writable; 187 | } 188 | 189 | /** 190 | * To know if the audio file is valid. 191 | */ 192 | public function isValid(): bool 193 | { 194 | return $this->is_valid; 195 | } 196 | 197 | /** 198 | * To know if the audio file has cover. 199 | */ 200 | public function hasCover(): bool 201 | { 202 | return $this->has_cover; 203 | } 204 | 205 | /** 206 | * Get `title` tag, like `Another Brick In The Wall`. 207 | */ 208 | public function getTitle(): ?string 209 | { 210 | return $this->title; 211 | } 212 | 213 | /** 214 | * Get `artist` tag, like `Pink Floyd`. 215 | */ 216 | public function getArtist(): ?string 217 | { 218 | return $this->artist; 219 | } 220 | 221 | /** 222 | * Get `album` tag, like `The Wall`. 223 | */ 224 | public function getAlbum(): ?string 225 | { 226 | return $this->album; 227 | } 228 | 229 | /** 230 | * Get `genre` tag, like `Rock`. 231 | */ 232 | public function getGenre(): ?string 233 | { 234 | return $this->genre; 235 | } 236 | 237 | /** 238 | * Get `year` tag, like `1979`. 239 | * 240 | * - For `matroska` format: `date` tag. 241 | * - For `ape` format: `date` tag. 242 | */ 243 | public function getYear(): ?int 244 | { 245 | return $this->year; 246 | } 247 | 248 | /** 249 | * Get `track_number` tag, like `1`. 250 | * 251 | * - For `vorbiscomment` format: `track_number` tag. 252 | * - For `matroska` format: `part_number` tag. 253 | * - For `ape` format: `track` tag. 254 | */ 255 | public function getTrackNumber(): ?string 256 | { 257 | return $this->track_number; 258 | } 259 | 260 | /** 261 | * Get `track_number` tag as integer, like `1`. 262 | */ 263 | public function getTrackNumberInt(): ?int 264 | { 265 | return $this->track_number ? intval($this->track_number) : null; 266 | } 267 | 268 | /** 269 | * Get `comment` tag, like `Recorded at Abbey Road Studios`. 270 | */ 271 | public function getComment(): ?string 272 | { 273 | return $this->comment; 274 | } 275 | 276 | /** 277 | * Get `album_artist` tag, like `Pink Floyd`. 278 | * 279 | * - For `id3v2` format: `band` tag. 280 | * - For `asf` format: `albumartist` tag. 281 | * - For `vorbiscomment` format: `albumartist` tag. 282 | */ 283 | public function getAlbumArtist(): ?string 284 | { 285 | return $this->album_artist; 286 | } 287 | 288 | /** 289 | * Get `composer` tag, like `Roger Waters`. 290 | */ 291 | public function getComposer(): ?string 292 | { 293 | return $this->composer; 294 | } 295 | 296 | /** 297 | * Get `disc_number` tag, like `1`. 298 | * 299 | * - For `id3v2` format: `part_of_a_set` tag. 300 | * - For `asf` format: `partofset` tag. 301 | * - For `vorbiscomment` format: `discnumber` tag. 302 | * - For `matroska` format: `disc` tag. 303 | * - For `ape` format: `disc` tag. 304 | */ 305 | public function getDiscNumber(): ?string 306 | { 307 | return $this->disc_number; 308 | } 309 | 310 | /** 311 | * Get `disc_number` tag as integer, like `1`. 312 | */ 313 | public function getDiscNumberInt(): ?int 314 | { 315 | if (str_contains($this->disc_number, '/')) { 316 | $disc_number = explode('/', $this->disc_number); 317 | 318 | return intval($disc_number[0]); 319 | } 320 | 321 | return $this->disc_number ? intval($this->disc_number) : null; 322 | } 323 | 324 | /** 325 | * To know if the audio file is a compilation. 326 | * 327 | * - For `id3v2` format: `part_of_a_compilation` tag. 328 | * - For `quicktime` format: `compilation` tag. 329 | * - For `vorbiscomment` format: `compilation` tag. 330 | * - For `matroska` format: `compilation` tag. 331 | * - For `ape` format: `compilation` tag. 332 | */ 333 | public function isCompilation(): bool 334 | { 335 | return $this->is_compilation; 336 | } 337 | 338 | /** 339 | * Get `creation_date` tag, like `1979-11-30`. 340 | * 341 | * - For `matroska` format: `date` tag. 342 | * - For `ape` format: `date` tag. 343 | */ 344 | public function getCreationDate(): ?string 345 | { 346 | return $this->creation_date; 347 | } 348 | 349 | /** 350 | * Get `encoding_by` tag, like `EAC`. 351 | */ 352 | public function getEncodingBy(): ?string 353 | { 354 | return $this->encoding_by; 355 | } 356 | 357 | /** 358 | * Get `encoding` tag, like `LAME`. 359 | */ 360 | public function getEncoding(): ?string 361 | { 362 | return $this->encoding; 363 | } 364 | 365 | /** 366 | * Get `copyright` tag, like `© 1979 Pink Floyd`. 367 | */ 368 | public function getCopyright(): ?string 369 | { 370 | return $this->copyright; 371 | } 372 | 373 | /** 374 | * Get `description` tag, like `The Wall is the eleventh studio album by the English rock band Pink Floyd`. 375 | */ 376 | public function getDescription(): ?string 377 | { 378 | return $this->description; 379 | } 380 | 381 | /** 382 | * Get `synopsis` tag, like `The Wall is the eleventh studio album by the English rock band Pink Floyd`. 383 | * 384 | * `description` and `synopsis` are not the same tag, but for many formats, they are the same. 385 | */ 386 | public function getSynopsis(): ?string 387 | { 388 | return $this->synopsis; 389 | } 390 | 391 | /** 392 | * Get `language` tag, like `en`. 393 | */ 394 | public function getLanguage(): ?string 395 | { 396 | return $this->language; 397 | } 398 | 399 | /** 400 | * Get `lyrics` tag, like `We don't need no education`. 401 | */ 402 | public function getLyrics(): ?string 403 | { 404 | return $this->lyrics; 405 | } 406 | 407 | /** 408 | * Get raw tags as array with all formats. 409 | * 410 | * For example, for `mp3` format: `['id3v1' => [...], 'id3v2' => [...]]`. 411 | */ 412 | public function getRawAll(): array 413 | { 414 | return $this->raw_all; 415 | } 416 | 417 | /** 418 | * Get raw tags as array with main format. 419 | * 420 | * For example, for `mp3` format, `id3v2` entry will be returned. 421 | * 422 | * @param string|null $format If not provided, main format will be returned. 423 | * @return string[] 424 | */ 425 | public function getRaw(?string $format = null): ?array 426 | { 427 | if ($format) { 428 | return $this->raw_all[$format] ?? null; 429 | } 430 | 431 | $tags = match ($this->type) { 432 | AudioTypeEnum::id3 => $this->raw_all['id3v2'] ?? [], 433 | AudioTypeEnum::vorbiscomment => $this->raw_all['vorbiscomment'] ?? [], 434 | AudioTypeEnum::quicktime => $this->raw_all['quicktime'] ?? [], 435 | AudioTypeEnum::matroska => $this->raw_all['matroska'] ?? [], 436 | AudioTypeEnum::ape => $this->raw_all['ape'] ?? [], 437 | AudioTypeEnum::asf => $this->raw_all['asf'] ?? [], 438 | default => [], 439 | }; 440 | 441 | return $tags; 442 | } 443 | 444 | /** 445 | * Get raw tags key from main format. 446 | * 447 | * @param string $key Key name. 448 | * @param string|null $format If not provided, main format will be used. 449 | */ 450 | public function getRawKey(string $key, ?string $format = null): string|int|bool|null 451 | { 452 | $tags = $this->getRaw($format); 453 | 454 | return $tags[$key] ?? null; 455 | } 456 | 457 | public function toArray(): array 458 | { 459 | return [ 460 | 'path' => $this->path, 461 | 'extension' => $this->extension, 462 | 'format' => $this->format, 463 | 'type' => $this->type, 464 | 'metadata' => $this->metadata?->toArray(), 465 | 'cover' => $this->cover?->toArray(), 466 | 'duration' => $this->duration, 467 | 'is_writable' => $this->is_writable, 468 | 'is_valid' => $this->is_valid, 469 | 'has_cover' => $this->has_cover, 470 | 'title' => $this->title, 471 | 'artist' => $this->artist, 472 | 'album' => $this->album, 473 | 'genre' => $this->genre, 474 | 'year' => $this->year, 475 | 'track_number' => $this->track_number, 476 | 'comment' => $this->comment, 477 | 'album_artist' => $this->album_artist, 478 | 'composer' => $this->composer, 479 | 'disc_number' => $this->disc_number, 480 | 'is_compilation' => $this->is_compilation, 481 | 'creation_date' => $this->creation_date, 482 | 'encoding_by' => $this->encoding_by, 483 | 'encoding' => $this->encoding, 484 | 'description' => $this->description, 485 | 'synopsis' => $this->synopsis, 486 | 'language' => $this->language, 487 | 'lyrics' => $this->lyrics, 488 | 'raw_all' => $this->raw_all, 489 | ]; 490 | } 491 | 492 | private function parseTags(?\Kiwilan\Audio\Id3\Id3Reader $id3_reader): self 493 | { 494 | $this->type = match ($this->format) { 495 | AudioFormatEnum::aac => null, 496 | AudioFormatEnum::aif => AudioTypeEnum::id3, 497 | AudioFormatEnum::aifc => AudioTypeEnum::id3, 498 | AudioFormatEnum::aiff => AudioTypeEnum::id3, 499 | AudioFormatEnum::flac => AudioTypeEnum::vorbiscomment, 500 | AudioFormatEnum::m4a => AudioTypeEnum::quicktime, 501 | AudioFormatEnum::m4b => AudioTypeEnum::quicktime, 502 | AudioFormatEnum::m4v => AudioTypeEnum::quicktime, 503 | AudioFormatEnum::mka => AudioTypeEnum::matroska, 504 | AudioFormatEnum::mkv => AudioTypeEnum::matroska, 505 | AudioFormatEnum::mp3 => AudioTypeEnum::id3, 506 | AudioFormatEnum::mp4 => AudioTypeEnum::quicktime, 507 | AudioFormatEnum::ogg => AudioTypeEnum::vorbiscomment, 508 | AudioFormatEnum::opus => AudioTypeEnum::vorbiscomment, 509 | AudioFormatEnum::spx => AudioTypeEnum::vorbiscomment, 510 | AudioFormatEnum::tta => AudioTypeEnum::ape, 511 | AudioFormatEnum::wav => AudioTypeEnum::id3, 512 | AudioFormatEnum::webm => AudioTypeEnum::matroska, 513 | AudioFormatEnum::wma => AudioTypeEnum::asf, 514 | AudioFormatEnum::wv => AudioTypeEnum::ape, 515 | default => null, 516 | }; 517 | 518 | $tags = $id3_reader->getTags(); 519 | if (! $tags || $tags->is_empty) { 520 | return $this; 521 | } 522 | 523 | $raw_tags = $id3_reader->getRaw()['tags'] ?? []; 524 | foreach ($raw_tags as $name => $raw_tag) { 525 | $this->raw_all[$name] = Id3Reader::cleanTags($raw_tag); 526 | } 527 | 528 | $core = match ($this->type) { 529 | AudioTypeEnum::id3 => AudioCore::fromId3($tags->id3v1, $tags->id3v2), 530 | AudioTypeEnum::vorbiscomment => AudioCore::fromVorbisComment($tags->vorbiscomment), 531 | AudioTypeEnum::quicktime => AudioCore::fromQuicktime($tags->quicktime), 532 | AudioTypeEnum::matroska => AudioCore::fromMatroska($tags->matroska), 533 | AudioTypeEnum::ape => AudioCore::fromApe($tags->ape), 534 | AudioTypeEnum::asf => AudioCore::fromAsf($tags->asf), 535 | default => null, 536 | }; 537 | 538 | if (! $core) { 539 | return $this; 540 | } 541 | 542 | $this->convertCore($core); 543 | $this->is_valid = true; 544 | $this->cover = AudioCover::make($id3_reader->getComments()); 545 | 546 | if ($this->cover?->getContents()) { 547 | $this->has_cover = true; 548 | } 549 | 550 | return $this; 551 | } 552 | 553 | private function convertCore(?AudioCore $core): self 554 | { 555 | if (! $core) { 556 | return $this; 557 | } 558 | 559 | $this->title = $core->title; 560 | $this->artist = $core->artist; 561 | $this->album = $core->album; 562 | $this->genre = $core->genre; 563 | $this->year = $core->year; 564 | $this->track_number = $core->track_number; 565 | $this->comment = $core->comment; 566 | $this->album_artist = $core->album_artist; 567 | $this->composer = $core->composer; 568 | $this->disc_number = $core->disc_number; 569 | $this->is_compilation = $core->is_compilation; 570 | $this->creation_date = $core->creation_date; 571 | $this->encoding_by = $core->encoding_by; 572 | $this->encoding = $core->encoding; 573 | $this->copyright = $core->copyright; 574 | $this->description = $core->description; 575 | $this->synopsis = $core->synopsis; 576 | $this->language = $core->language; 577 | $this->lyrics = $core->lyrics; 578 | 579 | return $this; 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /src/Core/AudioCore.php: -------------------------------------------------------------------------------- 1 | $value !== null); 40 | $properties = array_filter($properties, fn ($value) => $value !== ''); 41 | 42 | return $properties; 43 | } 44 | 45 | private function parseCompilation(AudioCore $core): ?string 46 | { 47 | if ($core->is_compilation === null) { 48 | return null; 49 | } 50 | 51 | return $core->is_compilation ? '1' : '0'; 52 | } 53 | 54 | public static function toId3v2(AudioCore $core): Tag\Id3TagAudioV2 55 | { 56 | return new Tag\Id3TagAudioV2( 57 | album: $core->album, 58 | artist: $core->artist, 59 | band: $core->album_artist, 60 | comment: $core->comment, 61 | composer: $core->composer, 62 | part_of_a_set: $core->disc_number, 63 | genre: $core->genre, 64 | part_of_a_compilation: $core->parseCompilation($core), 65 | title: $core->title, 66 | track_number: $core->track_number, 67 | year: (string) $core->year, 68 | copyright: $core->copyright, 69 | unsynchronised_lyric: $core->lyrics, 70 | language: $core->language, 71 | ); 72 | } 73 | 74 | public static function toId3v1(AudioCore $core): Tag\Id3TagAudioV1 75 | { 76 | return new Tag\Id3TagAudioV1( 77 | album: $core->album, 78 | artist: $core->artist, 79 | comment: $core->comment, 80 | genre: $core->genre, 81 | title: $core->title, 82 | track_number: $core->track_number, 83 | year: (string) $core->year, 84 | ); 85 | } 86 | 87 | public static function toVorbisComment(AudioCore $core): Tag\Id3TagVorbisComment 88 | { 89 | return new Tag\Id3TagVorbisComment( 90 | album: $core->album, 91 | artist: $core->artist, 92 | albumartist: $core->album_artist, 93 | comment: $core->comment, 94 | composer: $core->composer, 95 | compilation: $core->parseCompilation($core), 96 | discnumber: $core->disc_number, 97 | genre: $core->genre, 98 | title: $core->title, 99 | tracknumber: $core->track_number, 100 | date: (string) $core->year, 101 | encoder: $core->encoding, 102 | description: $core->description, 103 | ); 104 | } 105 | 106 | public static function toQuicktime(AudioCore $core): Tag\Id3TagQuicktime 107 | { 108 | return new Tag\Id3TagQuicktime( 109 | title: $core->title, 110 | track_number: $core->track_number, 111 | disc_number: $core->disc_number, 112 | compilation: $core->parseCompilation($core), 113 | album: $core->album, 114 | genre: $core->genre, 115 | composer: $core->composer, 116 | creation_date: $core->creation_date, 117 | copyright: $core->copyright, 118 | artist: $core->artist, 119 | album_artist: $core->album_artist, 120 | encoded_by: $core->encoding, 121 | encoding_tool: $core->encoding, 122 | description: $core->description, 123 | description_long: $core->synopsis, 124 | lyrics: $core->lyrics, 125 | comment: $core->comment, 126 | ); 127 | } 128 | 129 | public static function toMatroska(AudioCore $core): Tag\Id3TagMatroska 130 | { 131 | return new Tag\Id3TagMatroska( 132 | title: $core->title, 133 | album: $core->album, 134 | artist: $core->artist, 135 | album_artist: $core->album_artist, 136 | comment: $core->comment, 137 | composer: $core->composer, 138 | disc: $core->disc_number, 139 | compilation: $core->parseCompilation($core), 140 | genre: $core->genre, 141 | part_number: $core->track_number, 142 | date: (string) $core->year, 143 | encoder: $core->encoding, 144 | ); 145 | } 146 | 147 | public static function toApe(AudioCore $core): Tag\Id3TagApe 148 | { 149 | return new Tag\Id3TagApe( 150 | album: $core->album, 151 | artist: $core->artist, 152 | album_artist: $core->album_artist, 153 | comment: $core->comment, 154 | composer: $core->composer, 155 | disc: $core->disc_number, 156 | compilation: $core->parseCompilation($core), 157 | genre: $core->genre, 158 | title: $core->title, 159 | track: $core->track_number, 160 | date: (string) $core->year, 161 | encoder: $core->encoding, 162 | ); 163 | } 164 | 165 | public static function toAsf(AudioCore $core): Tag\Id3TagAsf 166 | { 167 | return new Tag\Id3TagAsf( 168 | album: $core->album, 169 | artist: $core->artist, 170 | albumartist: $core->album_artist, 171 | composer: $core->composer, 172 | partofset: $core->disc_number, 173 | genre: $core->genre, 174 | track_number: $core->track_number, 175 | year: (string) $core->year, 176 | encodingsettings: $core->encoding, 177 | ); 178 | } 179 | 180 | public static function fromId3(?Tag\Id3TagAudioV1 $v1, ?Tag\Id3TagAudioV2 $v2): AudioCore 181 | { 182 | if (! $v1) { 183 | $v1 = new Tag\Id3TagAudioV1; 184 | } 185 | 186 | if (! $v2) { 187 | $v2 = new Tag\Id3TagAudioV2; 188 | } 189 | 190 | return new AudioCore( 191 | album: $v2->album ?? $v1->album, 192 | artist: $v2->artist ?? $v1->artist, 193 | album_artist: $v2->band ?? null, 194 | comment: $v2->comment ?? $v1->comment, 195 | composer: $v2->composer ?? null, 196 | disc_number: $v2->part_of_a_set ?? null, 197 | genre: $v2->genre ?? $v1->genre, 198 | is_compilation: $v2->part_of_a_compilation === '1', 199 | title: $v2->title ?? $v1->title, 200 | track_number: $v2->track_number ?? $v1->track_number, 201 | year: (int) ($v2->year ?? $v1->year), 202 | copyright: $v2->copyright ?? null, 203 | description: $v2->text ?? null, 204 | lyrics: $v2->unsynchronised_lyric ?? null, 205 | language: $v2->language ?? null, 206 | ); 207 | } 208 | 209 | public static function fromId3v2(Tag\Id3TagAudioV2 $tag): AudioCore 210 | { 211 | return new AudioCore( 212 | album: $tag->album, 213 | artist: $tag->artist, 214 | album_artist: $tag->band, 215 | comment: $tag->comment, 216 | composer: $tag->composer, 217 | disc_number: $tag->part_of_a_set, 218 | genre: $tag->genre, 219 | is_compilation: $tag->part_of_a_compilation === '1', 220 | title: $tag->title, 221 | track_number: $tag->track_number, 222 | year: (int) $tag->year, 223 | ); 224 | } 225 | 226 | public static function fromId3v1(Tag\Id3TagAudioV1 $tag): AudioCore 227 | { 228 | return new AudioCore( 229 | album: $tag->album, 230 | artist: $tag->artist, 231 | comment: $tag->comment, 232 | genre: $tag->genre, 233 | title: $tag->title, 234 | track_number: $tag->track_number, 235 | year: (int) $tag->year, 236 | ); 237 | } 238 | 239 | public static function fromQuicktime(Tag\Id3TagQuicktime $tag): AudioCore 240 | { 241 | $date = $tag->creation_date; 242 | $description = $tag->description; 243 | $description_long = $tag->description_long; 244 | 245 | $creation_date = null; 246 | $year = null; 247 | 248 | if ($date) { 249 | if (strlen($date) === 4) { 250 | $year = (int) $date; 251 | } else { 252 | try { 253 | $parsedCreationDate = new \DateTimeImmutable($date); 254 | } catch (\Exception $e) { 255 | // ignore the issue so the rest of the data will be available 256 | } 257 | 258 | if (! empty($parsedCreationDate)) { 259 | $creation_date = $parsedCreationDate->format('Y-m-d\TH:i:s\Z'); 260 | $year = (int) $parsedCreationDate->format('Y'); 261 | } 262 | } 263 | } 264 | 265 | $core = new AudioCore( 266 | title: $tag->title, 267 | artist: $tag->artist, 268 | album: $tag->album, 269 | genre: $tag->genre, 270 | track_number: $tag->track_number, 271 | disc_number: $tag->disc_number, 272 | composer: $tag->composer, 273 | is_compilation: $tag->compilation === '1', 274 | comment: $tag->comment, 275 | album_artist: $tag->album_artist, 276 | encoding_by: $tag->encoded_by, 277 | encoding: $tag->encoding_tool, 278 | language: $tag->language, 279 | copyright: $tag->copyright, 280 | description: $description, 281 | synopsis: $description_long, 282 | lyrics: $tag->lyrics, 283 | creation_date: $creation_date, 284 | year: $year, 285 | ); 286 | 287 | return $core; 288 | } 289 | 290 | public static function fromVorbisComment(Tag\Id3TagVorbisComment $tag): AudioCore 291 | { 292 | return new AudioCore( 293 | title: $tag->title, 294 | artist: $tag->artist, 295 | album: $tag->album, 296 | genre: $tag->genre, 297 | track_number: $tag->tracknumber, 298 | comment: $tag->comment, 299 | album_artist: $tag->albumartist, 300 | composer: $tag->composer, 301 | disc_number: $tag->discnumber, 302 | is_compilation: $tag->compilation === '1', 303 | year: (int) $tag->date, 304 | encoding: $tag->encoder, 305 | description: $tag->description, 306 | ); 307 | } 308 | 309 | public static function fromAsf(Tag\Id3TagAsf $tag): AudioCore 310 | { 311 | return new AudioCore( 312 | title: $tag->title, 313 | artist: $tag->artist, 314 | album: $tag->album, 315 | album_artist: $tag->albumartist, 316 | composer: $tag->composer, 317 | disc_number: $tag->partofset, 318 | genre: $tag->genre, 319 | track_number: $tag->track_number, 320 | year: (int) $tag->year, 321 | encoding: $tag->encodingsettings, 322 | ); 323 | } 324 | 325 | public static function fromMatroska(Tag\Id3TagMatroska $tag): AudioCore 326 | { 327 | return new AudioCore( 328 | title: $tag->title, 329 | album: $tag->album, 330 | artist: $tag->artist, 331 | album_artist: $tag->album_artist, 332 | comment: $tag->comment, 333 | composer: $tag->composer, 334 | disc_number: $tag->disc, 335 | genre: $tag->genre, 336 | is_compilation: $tag->compilation === 'true', 337 | track_number: $tag->part_number, 338 | year: (int) $tag->date, 339 | encoding: $tag->encoder, 340 | ); 341 | } 342 | 343 | public static function fromApe(Tag\Id3TagApe $tag): AudioCore 344 | { 345 | return new AudioCore( 346 | album: $tag->album, 347 | artist: $tag->artist, 348 | album_artist: $tag->album_artist, 349 | comment: $tag->comment, 350 | composer: $tag->composer, 351 | disc_number: $tag->disc, 352 | genre: $tag->genre, 353 | is_compilation: $tag->compilation === '1', 354 | title: $tag->title, 355 | track_number: $tag->track, 356 | creation_date: $tag->date, 357 | year: $tag->year ?? (int) $tag->date, 358 | encoding: $tag->encoder, 359 | description: $tag->description, 360 | copyright: $tag->copyright, 361 | lyrics: $tag->lyrics, 362 | synopsis: $tag->podcastdesc, 363 | language: $tag->language, 364 | ); 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/Core/AudioCoreCover.php: -------------------------------------------------------------------------------- 1 | data = file_exists($pathOrData) 22 | ? base64_encode(file_get_contents($pathOrData)) 23 | : base64_encode($pathOrData); 24 | $self->picture_type_id = $image[2]; 25 | $self->description = 'cover'; 26 | $self->mime = $image['mime']; 27 | 28 | return $self; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Enums/AudioFormatEnum.php: -------------------------------------------------------------------------------- 1 | raw = $self->instance->analyze($path); 45 | $self->is_writable = $self->instance->is_writable($path); 46 | $metadata = $self->raw; 47 | 48 | $audio = Id3Audio::make($metadata['audio'] ?? null); 49 | $video = Id3Video::make($metadata['video'] ?? null); 50 | $tags = Id3AudioTag::make($metadata['tags'] ?? null); 51 | $comments = Id3Comments::make($metadata['comments'] ?? null); 52 | $quicktime = Id3AudioQuicktime::make($self->raw['quicktime'] ?? null); 53 | $warning = $metadata['warning'] ?? null; 54 | 55 | $bitrate = $metadata['bitrate'] ?? null; 56 | if ($bitrate) { 57 | $bitrate = intval($bitrate); 58 | } 59 | 60 | $self->version = $metadata['GETID3_VERSION'] ?? null; 61 | $self->file_size = $metadata['filesize'] ?? null; 62 | $self->file_path = $metadata['filepath'] ?? null; 63 | $self->filename = $metadata['filename'] ?? null; 64 | $self->filename_path = $metadata['filenamepath'] ?? null; 65 | $self->av_data_offset = $metadata['avdataoffset'] ?? null; 66 | $self->av_data_end = $metadata['avdataend'] ?? null; 67 | $self->file_format = $metadata['fileformat'] ?? null; 68 | $self->audio = $audio; 69 | $self->video = $video; 70 | $self->tags = $tags; 71 | $self->quicktime = $quicktime; 72 | $self->comments = $comments; 73 | $self->warning = $warning; 74 | $self->encoding = $metadata['encoding'] ?? null; 75 | $self->mime_type = $metadata['mime_type'] ?? null; 76 | $self->mpeg = $metadata['mpeg'] ?? null; 77 | $self->playtime_seconds = $metadata['playtime_seconds'] ?? null; 78 | $self->bitrate = $bitrate; 79 | $self->playtime_string = $metadata['playtime_string'] ?? null; 80 | 81 | return $self; 82 | } 83 | 84 | public function getInstance(): getID3 85 | { 86 | return $this->instance; 87 | } 88 | 89 | public function getVersion(): ?string 90 | { 91 | return $this->version; 92 | } 93 | 94 | public function getFileSize(): ?int 95 | { 96 | return $this->file_size; 97 | } 98 | 99 | public function getFilePath(): ?string 100 | { 101 | return $this->file_path; 102 | } 103 | 104 | public function getFilename(): ?string 105 | { 106 | return $this->filename; 107 | } 108 | 109 | public function getFilenamePath(): ?string 110 | { 111 | return $this->filename_path; 112 | } 113 | 114 | public function getAvDataOffset(): ?int 115 | { 116 | return $this->av_data_offset; 117 | } 118 | 119 | public function getAvDataEnd(): ?int 120 | { 121 | return $this->av_data_end; 122 | } 123 | 124 | public function getFileFormat(): ?string 125 | { 126 | return $this->file_format; 127 | } 128 | 129 | public function getAudio(): ?Id3Audio 130 | { 131 | return $this->audio; 132 | } 133 | 134 | public function getTags(): ?Id3AudioTag 135 | { 136 | return $this->tags; 137 | } 138 | 139 | public function getComments(): ?Id3Comments 140 | { 141 | return $this->comments; 142 | } 143 | 144 | public function getVideo(): ?Id3Video 145 | { 146 | return $this->video; 147 | } 148 | 149 | public function getQuicktime(): ?Id3AudioQuicktime 150 | { 151 | return $this->quicktime; 152 | } 153 | 154 | public function getWarning(): ?array 155 | { 156 | return $this->warning; 157 | } 158 | 159 | public function getEncoding(): ?string 160 | { 161 | return $this->encoding; 162 | } 163 | 164 | public function getMimeType(): ?string 165 | { 166 | return $this->mime_type; 167 | } 168 | 169 | public function getMpeg(): mixed 170 | { 171 | return $this->mpeg; 172 | } 173 | 174 | public function getPlaytimeSeconds(): ?float 175 | { 176 | return $this->playtime_seconds; 177 | } 178 | 179 | public function getBitrate(): ?float 180 | { 181 | return $this->bitrate; 182 | } 183 | 184 | public function getPlaytimeString(): ?string 185 | { 186 | return $this->playtime_string; 187 | } 188 | 189 | public function isWritable(): bool 190 | { 191 | return $this->is_writable; 192 | } 193 | 194 | public function getRaw(): array 195 | { 196 | return $this->raw; 197 | } 198 | 199 | public function toTags(?string $audioFormat = null): array 200 | { 201 | $rawTags = $this->raw['tags_html'] ?? []; 202 | 203 | if (count($rawTags) === 0) { 204 | return []; 205 | } 206 | 207 | $tagsItems = []; 208 | if ($audioFormat) { 209 | $tagsItems = $rawTags[$audioFormat] ?? []; 210 | } else { 211 | if (count($rawTags) > 1) { 212 | $entries = []; 213 | foreach ($rawTags as $key => $keyTags) { 214 | $entries[$key] = count($keyTags); 215 | } 216 | $maxKey = array_search(max($entries), $entries); 217 | $tagsItems = $rawTags[$maxKey] ?? []; 218 | } else { 219 | $tagsItems = reset($rawTags); 220 | } 221 | } 222 | 223 | return Id3Reader::cleanTags($tagsItems); 224 | } 225 | 226 | public static function cleanTags(?array $tagsItems): array 227 | { 228 | if (! $tagsItems) { 229 | return []; 230 | } 231 | 232 | $temp = []; 233 | foreach ($tagsItems as $k => $v) { 234 | $temp[$k] = $v[0] ?? null; 235 | } 236 | 237 | $items = []; 238 | foreach ($temp as $k => $v) { 239 | $k = strtolower($k); 240 | $k = str_replace(' ', '_', $k); 241 | $items[$k] = $v; 242 | } 243 | 244 | return $items; 245 | } 246 | 247 | public function toAudioFormats(): array 248 | { 249 | return $this->raw['tags_html'] ?? []; 250 | } 251 | 252 | public function toArray(): array 253 | { 254 | $raw = $this->raw; 255 | $raw['id3v2']['APIC'] = null; 256 | $raw['ape']['items']['cover art (front)'] = null; 257 | $raw['comments'] = null; 258 | 259 | return $raw; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/Id3/Id3Writer.php: -------------------------------------------------------------------------------- 1 | $tags Array for `Id3Writer` format. 18 | * @param string[] $tags_core Tags from dedicated methods. 19 | * @param string[] $tags_custom Tags from `tag()` method. 20 | * @param string[] $tags_custom_bulk Tags from `tags()` method. 21 | * @param string[] $warnings 22 | * @param string[] $errors 23 | * @param string[] $formats Formats to write tags. 24 | */ 25 | protected function __construct( 26 | protected Audio $audio, 27 | protected getid3_writetags $writer, 28 | protected AudioCore $core, 29 | protected array $tags = [], 30 | protected array $tags_core = [], 31 | protected array $tags_current = [], 32 | protected array $tags_custom = [], 33 | protected array $tags_custom_bulk = [], 34 | protected array $warnings = [], 35 | protected array $errors = [], 36 | protected bool $cover_deleted = false, 37 | protected bool $skip_errors = false, 38 | protected array $formats = [], 39 | protected bool $success = false, 40 | ) {} 41 | 42 | public static function make(Audio $audio): self 43 | { 44 | $self = new self( 45 | audio: $audio, 46 | writer: new getid3_writetags, 47 | core: new AudioCore, 48 | ); 49 | 50 | $self->writer->filename = $audio->getPath(); 51 | 52 | return $self; 53 | } 54 | 55 | public function getCore(): AudioCore 56 | { 57 | return $this->core; 58 | } 59 | 60 | /** 61 | * Allow to remove other tags when writing tags. 62 | */ 63 | public function removeOtherTags(): self 64 | { 65 | $this->writer->remove_other_tags = true; 66 | 67 | return $this; 68 | } 69 | 70 | public function title(?string $title): self 71 | { 72 | $this->core->title = $title; 73 | 74 | return $this; 75 | } 76 | 77 | public function artist(?string $artist): self 78 | { 79 | $this->core->artist = $artist; 80 | 81 | return $this; 82 | } 83 | 84 | public function album(?string $album): self 85 | { 86 | $this->core->album = $album; 87 | 88 | return $this; 89 | } 90 | 91 | public function year(string|int|null $year): self 92 | { 93 | if (! $year) { 94 | $this->core->year = null; 95 | 96 | return $this; 97 | } 98 | 99 | $this->core->year = intval($year); 100 | 101 | return $this; 102 | } 103 | 104 | public function genre(?string $genre): self 105 | { 106 | $this->core->genre = $genre; 107 | 108 | return $this; 109 | } 110 | 111 | public function trackNumber(string|int|null $track_number): self 112 | { 113 | if (! $track_number) { 114 | $this->core->track_number = null; 115 | 116 | return $this; 117 | } 118 | 119 | if (is_int($track_number)) { 120 | $track_number = (string) $track_number; 121 | } 122 | 123 | $this->core->track_number = $track_number; 124 | 125 | return $this; 126 | } 127 | 128 | public function discNumber(string|int|null $disc_number): self 129 | { 130 | if (! $disc_number) { 131 | $this->core->disc_number = null; 132 | 133 | return $this; 134 | } 135 | 136 | if (is_int($disc_number)) { 137 | $disc_number = (string) $disc_number; 138 | } 139 | 140 | $this->core->disc_number = $disc_number; 141 | 142 | return $this; 143 | } 144 | 145 | public function composer(?string $composer): self 146 | { 147 | $this->core->composer = $composer; 148 | 149 | return $this; 150 | } 151 | 152 | public function comment(?string $comment): self 153 | { 154 | $this->core->comment = $comment; 155 | 156 | return $this; 157 | } 158 | 159 | public function lyrics(?string $lyrics): self 160 | { 161 | $this->core->lyrics = $lyrics; 162 | 163 | return $this; 164 | } 165 | 166 | public function isCompilation(): self 167 | { 168 | $this->core->is_compilation = true; 169 | 170 | return $this; 171 | } 172 | 173 | public function isNotCompilation(): self 174 | { 175 | $this->core->is_compilation = false; 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * Not supported by `id3`. 182 | */ 183 | public function creationDate(?string $creation_date): self 184 | { 185 | $this->core->creation_date = $creation_date; 186 | 187 | return $this; 188 | } 189 | 190 | public function copyright(?string $copyright): self 191 | { 192 | $this->core->copyright = $copyright; 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * Not supported by `id3`. 199 | */ 200 | public function encodingBy(?string $encoding_by): self 201 | { 202 | $this->core->encoding_by = $encoding_by; 203 | 204 | return $this; 205 | } 206 | 207 | /** 208 | * Not supported by `id3`. 209 | */ 210 | public function encoding(?string $encoding): self 211 | { 212 | $this->core->encoding = $encoding; 213 | 214 | return $this; 215 | } 216 | 217 | /** 218 | * Not supported by `id3`. 219 | */ 220 | public function description(?string $description): self 221 | { 222 | $this->core->description = $description; 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * Not supported by `id3`. 229 | */ 230 | public function synopsis(?string $synopsis): self 231 | { 232 | $this->core->synopsis = $synopsis; 233 | 234 | return $this; 235 | } 236 | 237 | public function language(?string $language): self 238 | { 239 | $this->core->language = $language; 240 | 241 | return $this; 242 | } 243 | 244 | /** 245 | * Set new album artist. 246 | */ 247 | public function albumArtist(?string $album_artist): self 248 | { 249 | $this->core->album_artist = $album_artist; 250 | 251 | return $this; 252 | } 253 | 254 | /** 255 | * To create a copy of the audio file with new tags. 256 | */ 257 | public function path(string $path): self 258 | { 259 | if (file_exists($path)) { 260 | unlink($path); 261 | } 262 | copy($this->audio->getPath(), $path); 263 | 264 | $this->writer->filename = $path; 265 | 266 | return $this; 267 | } 268 | 269 | /** 270 | * Advanced usage only to set tags formats. 271 | * 272 | * @param string[] $tag_formats 273 | */ 274 | public function tagFormats(array $tag_formats): self 275 | { 276 | $this->formats = $tag_formats; 277 | 278 | return $this; 279 | } 280 | 281 | /** 282 | * Remove cover from tags. 283 | */ 284 | public function removeCover(): self 285 | { 286 | $this->cover_deleted = true; 287 | 288 | return $this; 289 | } 290 | 291 | /** 292 | * Update cover is only supported by `id3` format. 293 | * 294 | * @param string $pathOrData Path to cover image or binary data 295 | */ 296 | public function cover(string $pathOrData): self 297 | { 298 | $this->core->cover = AudioCoreCover::make($pathOrData); 299 | $this->core->has_cover = true; 300 | 301 | return $this; 302 | } 303 | 304 | /** 305 | * Add custom tags without dedicated method (can be use multiple times). 306 | * 307 | * To know which key use for each format, see documentation. 308 | * For example, album artist for `id3` encoded files, is `band` key. 309 | * 310 | * @docs https://github.com/kiwilan/php-audio#convert-properties 311 | * 312 | * Example: 313 | * 314 | * ```php 315 | * $audio->write() 316 | * ->tag('series-part', '1') 317 | * ->tag('series', 'The Lord of the Rings'); 318 | * ``` 319 | */ 320 | public function tag(string $key, string|int|bool|null $value): self 321 | { 322 | $this->tags_custom[$key] = $value; 323 | 324 | return $this; 325 | } 326 | 327 | /** 328 | * Alternative to `tag()` method, with a full array of tags. 329 | * 330 | * To know which key use for each format, see documentation. 331 | * For example, album artist for `id3` encoded files, is `band` key. 332 | * 333 | * @docs https://github.com/kiwilan/php-audio#convert-properties 334 | * 335 | * @param array $tags 336 | * 337 | * Example: 338 | * 339 | * ```php 340 | * $audio->write() 341 | * ->tags([ 342 | * 'series-part' => '1', 343 | * 'series' => 'The Lord of the Rings', 344 | * ]); 345 | * ``` 346 | */ 347 | public function tags(array $tags): self 348 | { 349 | $this->tags_custom_bulk = $tags; 350 | 351 | return $this; 352 | } 353 | 354 | /** 355 | * Skip errors when writing tags. 356 | */ 357 | public function skipErrors(): self 358 | { 359 | $this->skip_errors = true; 360 | 361 | return $this; 362 | } 363 | 364 | /** 365 | * Write new tags on file. 366 | */ 367 | public function save(): bool 368 | { 369 | $this->assignFormats(); 370 | $this->assignTagsCurrent(); 371 | $this->assignCoverCurrent(); 372 | $this->assignTagsCore(); 373 | $this->assignTagsCustom(); 374 | 375 | $this->convertToWriter(); 376 | $this->convertCoverToWriter(); 377 | 378 | $this->writer->tagformats = $this->formats; 379 | $this->writer->tag_data = $this->tags; 380 | 381 | $this->success = $this->writer->WriteTags(); 382 | $this->errors = $this->writer->errors; 383 | $this->warnings = $this->writer->warnings; 384 | 385 | $this->handleErrors(); 386 | 387 | return $this->success; 388 | } 389 | 390 | private function handleErrors(): void 391 | { 392 | $errors = implode(', ', $this->errors); 393 | $warnings = implode(', ', $this->warnings); 394 | $errors = strip_tags($errors); 395 | $warnings = strip_tags($warnings); 396 | 397 | $supported = match ($this->audio->getFormat()) { 398 | AudioFormatEnum::flac => true, 399 | AudioFormatEnum::mp3 => true, 400 | AudioFormatEnum::ogg => true, 401 | AudioFormatEnum::m4b => true, 402 | default => false 403 | }; 404 | 405 | if (! $supported && ! $this->skip_errors) { 406 | throw new \Exception("php-audio: format {$this->audio->getFormat()->value} is not supported."); 407 | } 408 | 409 | if (! empty($this->errors)) { 410 | error_log("php-audio: {$errors}"); 411 | } 412 | 413 | if (! empty($this->warnings)) { 414 | error_log("php-audio: {$warnings}"); 415 | } 416 | 417 | if (empty($this->errors) && empty($this->warnings)) { 418 | return; 419 | } 420 | 421 | $msg = 'php-audio: Save tags failed.'; 422 | if ($errors) { 423 | $msg .= " Errors: {$errors}."; 424 | } 425 | if ($warnings) { 426 | $msg .= " Warnings: {$warnings}."; 427 | } 428 | $isSuccess = $this->success ? 'true' : 'false'; 429 | $msg .= " Success: {$isSuccess}."; 430 | error_log($msg); 431 | 432 | if (! $this->skip_errors) { 433 | throw new \Exception($msg); 434 | } 435 | } 436 | 437 | /** 438 | * Parse all tags to convert it to writer format. 439 | */ 440 | private function convertToWriter(): self 441 | { 442 | $tags = []; 443 | 444 | // set current tags 445 | foreach ($this->tags_current as $key => $value) { 446 | $tags[$key] = $value; 447 | } 448 | 449 | // set custom tags 450 | foreach ($this->tags_custom as $key => $value) { 451 | $tags[$key] = $value; 452 | } 453 | 454 | // set custom bulk tags 455 | foreach ($this->tags_custom_bulk as $key => $value) { 456 | $tags[$key] = $value; 457 | } 458 | 459 | // set core tags 460 | foreach ($this->tags_core as $key => $value) { 461 | $tags[$key] = $value; 462 | } 463 | 464 | $this->tags = $this->formatTags($tags); 465 | 466 | $forbiddenKeys = ['totaltracks']; 467 | foreach ($forbiddenKeys as $key) { 468 | if (isset($this->tags[$key])) { 469 | unset($this->tags[$key]); 470 | } 471 | } 472 | 473 | return $this; 474 | } 475 | 476 | private function assignTagsCustom(): self 477 | { 478 | if (empty($this->tags_custom) || empty($this->tags_custom_bulk)) { 479 | return $this; 480 | } 481 | 482 | foreach ($this->tags_custom as $key => $value) { 483 | $this->tags_current[$key] = $value; 484 | } 485 | 486 | foreach ($this->tags_custom_bulk as $key => $value) { 487 | $this->tags_current[$key] = $value; 488 | } 489 | 490 | return $this; 491 | } 492 | 493 | /** 494 | * Assign current cover. 495 | */ 496 | private function assignCoverCurrent(): self 497 | { 498 | // cover deleted 499 | if ($this->cover_deleted) { 500 | $this->core->cover = null; 501 | 502 | return $this; 503 | } 504 | 505 | // skip if no current cover 506 | if (! $this->audio->hasCover()) { 507 | return $this; 508 | } 509 | 510 | // skip if new cover already assigned 511 | if ($this->core->cover !== null) { 512 | return $this; 513 | } 514 | 515 | // get current cover 516 | $this->core->cover = new AudioCoreCover( 517 | data: $this->audio->getCover()->getContents(base64: true), 518 | mime: $this->audio->getCover()->getMimeType(), 519 | ); 520 | 521 | return $this; 522 | } 523 | 524 | /** 525 | * Add cover to writer. 526 | */ 527 | private function convertCoverToWriter(): self 528 | { 529 | if (! in_array($this->audio->getType(), self::ALLOWED_COVER_TYPE)) { 530 | return $this; 531 | } 532 | 533 | // skip if cover not exists 534 | if (! $this->core->cover) { 535 | return $this; 536 | } 537 | 538 | if (! $this->core->cover->data) { 539 | return $this; 540 | } 541 | 542 | // 'CTOC' => $old_tags['id3v2']['CTOC'], 543 | // 'CHAP' => $old_tags['id3v2']['CHAP'], 544 | // 'chapters' => $old_tags['id3v2']['chapters'], 545 | $this->tags['attached_picture'] = [ 546 | [ 547 | 'data' => base64_decode($this->core->cover->data), 548 | 'picturetypeid' => $this->core->cover->picture_type_id ?? 1, 549 | 'description' => $this->core->cover->description ?? 'cover', 550 | 'mime' => $this->core->cover->mime, 551 | ], 552 | ]; 553 | 554 | return $this; 555 | } 556 | 557 | /** 558 | * Assign current tags. 559 | */ 560 | private function assignTagsCurrent(): self 561 | { 562 | $currentTags = []; 563 | if (! $this->writer->remove_other_tags) { 564 | $currentTags = $this->audio->getRaw(); 565 | } 566 | 567 | $this->tags_current = $currentTags; 568 | 569 | return $this; 570 | } 571 | 572 | /** 573 | * Assign new tags from core to array. 574 | */ 575 | private function assignTagsCore(): self 576 | { 577 | $tagFormat = match ($this->audio->getType()) { 578 | AudioTypeEnum::id3 => AudioCore::toId3v2($this->core), 579 | AudioTypeEnum::vorbiscomment => AudioCore::toVorbisComment($this->core), 580 | AudioTypeEnum::quicktime => AudioCore::toQuicktime($this->core), 581 | AudioTypeEnum::matroska => AudioCore::toMatroska($this->core), 582 | AudioTypeEnum::ape => AudioCore::toApe($this->core), 583 | AudioTypeEnum::asf => AudioCore::toAsf($this->core), 584 | default => null, 585 | }; 586 | 587 | if (! $tagFormat) { 588 | return $this; 589 | } 590 | 591 | $this->tags_core = $tagFormat->toArray(); 592 | 593 | return $this; 594 | } 595 | 596 | /** 597 | * Assign formats to know how to write tags. 598 | * 599 | * - ID3v1 (v1 & v1.1) 600 | * - ID3v2 (v2.3, v2.4) 601 | * - APE (v2) 602 | * - Ogg Vorbis comments (need `vorbis-tools`) 603 | * - FLAC comments 604 | * 605 | * Options: `id3v1`, `id3v2.2`, `id2v2.3`, `id3v2.4`, `ape`, `vorbiscomment`, `metaflac`, `real` 606 | */ 607 | private function assignFormats(): self 608 | { 609 | if (! empty($this->formats)) { 610 | return $this; 611 | } 612 | 613 | $this->formats = match ($this->audio->getFormat()) { 614 | AudioFormatEnum::flac => ['metaflac'], 615 | AudioFormatEnum::mp3 => ['id3v1', 'id3v2.4'], 616 | AudioFormatEnum::ogg => ['vorbiscomment'], 617 | default => [], 618 | }; 619 | 620 | return $this; 621 | } 622 | 623 | /** 624 | * Format tags to writer format. 625 | * 626 | * @param array $tags 627 | * @return array 628 | */ 629 | private function formatTags(array $tags): array 630 | { 631 | $items = []; 632 | if (! empty($tags)) { 633 | foreach ($tags as $key => $tag) { 634 | if (gettype($tag) === 'string') { 635 | $items[$key] = [$tag]; 636 | } 637 | } 638 | } 639 | 640 | return $items; 641 | } 642 | } 643 | -------------------------------------------------------------------------------- /src/Id3/Reader/Id3Audio.php: -------------------------------------------------------------------------------- 1 | streams[0] ?? null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Id3/Reader/Id3AudioQuicktime.php: -------------------------------------------------------------------------------- 1 | |null $timestamps_unix 10 | * @param array|null $comments 11 | * @param array|null $video 12 | * @param array|null $audio 13 | * @param Id3AudioQuicktimeChapter[] $chapters 14 | */ 15 | protected function __construct( 16 | protected bool $hinting = false, 17 | protected ?string $controller = null, 18 | protected ?Id3AudioQuicktimeItem $ftyp = null, 19 | protected ?array $timestamps_unix = null, 20 | protected ?int $time_scale = null, 21 | protected ?int $display_scale = null, 22 | protected ?array $video = null, 23 | protected ?array $audio = null, 24 | protected ?array $stts_framecount = null, 25 | protected ?array $comments = [], 26 | protected array $chapters = [], 27 | protected ?Id3AudioQuicktimeItem $free = null, 28 | protected ?Id3AudioQuicktimeItem $wide = null, 29 | protected ?Id3AudioQuicktimeItem $mdat = null, 30 | protected ?string $encoding = null, 31 | ) {} 32 | 33 | public static function make(?array $metadata): ?self 34 | { 35 | if (! $metadata) { 36 | return null; 37 | } 38 | 39 | $hinting = $metadata['hinting'] ?? false; 40 | $controller = $metadata['controller'] ?? null; 41 | $ftyp = Id3AudioQuicktimeItem::make($metadata['ftyp'] ?? null); 42 | $timestamps_unix = $metadata['timestamps_unix'] ?? null; 43 | $time_scale = $metadata['time_scale'] ?? null; 44 | $display_scale = $metadata['display_scale'] ?? null; 45 | $video = $metadata['video'] ?? null; 46 | $audio = $metadata['audio'] ?? null; 47 | $stts_framecount = $metadata['stts_framecount'] ?? null; 48 | $comments = $metadata['comments'] ?? []; 49 | 50 | $chapters = []; 51 | $chaps = $metadata['chapters'] ?? []; 52 | foreach ($chaps as $chapter) { 53 | $chapters[] = Id3AudioQuicktimeChapter::make($chapter); 54 | } 55 | 56 | $free = Id3AudioQuicktimeItem::make($metadata['free'] ?? null); 57 | $wide = Id3AudioQuicktimeItem::make($metadata['wide'] ?? null); 58 | $mdat = Id3AudioQuicktimeItem::make($metadata['mdat'] ?? null); 59 | $encoding = $metadata['encoding'] ?? null; 60 | 61 | $self = new self( 62 | hinting: $hinting, 63 | controller: $controller, 64 | ftyp: $ftyp, 65 | timestamps_unix: $timestamps_unix, 66 | time_scale: $time_scale, 67 | display_scale: $display_scale, 68 | video: $video, 69 | audio: $audio, 70 | stts_framecount: $stts_framecount, 71 | comments: $comments, 72 | chapters: $chapters, 73 | free: $free, 74 | wide: $wide, 75 | mdat: $mdat, 76 | encoding: $encoding, 77 | ); 78 | 79 | return $self; 80 | } 81 | 82 | /** 83 | * @return Id3AudioQuicktimeChapter[] 84 | */ 85 | public function getChapters(): array 86 | { 87 | return $this->chapters; 88 | } 89 | 90 | /** 91 | * @return array|null 92 | */ 93 | public function getComments(): ?array 94 | { 95 | return $this->comments; 96 | } 97 | 98 | /** 99 | * @return array|null 100 | */ 101 | public function getTimestampsUnix(): ?array 102 | { 103 | return $this->timestamps_unix; 104 | } 105 | 106 | /** 107 | * @return array|null 108 | */ 109 | public function getVideo(): ?array 110 | { 111 | return $this->video; 112 | } 113 | 114 | /** 115 | * @return array|null 116 | */ 117 | public function getAudio(): ?array 118 | { 119 | return $this->audio; 120 | } 121 | 122 | public function getEncoding(): ?string 123 | { 124 | return $this->encoding; 125 | } 126 | 127 | public function getHinting(): bool 128 | { 129 | return $this->hinting; 130 | } 131 | 132 | public function getController(): ?string 133 | { 134 | return $this->controller; 135 | } 136 | 137 | public function getFtyp(): ?Id3AudioQuicktimeItem 138 | { 139 | return $this->ftyp; 140 | } 141 | 142 | public function getTimeScale(): ?int 143 | { 144 | return $this->time_scale; 145 | } 146 | 147 | public function getDisplayScale(): ?int 148 | { 149 | return $this->display_scale; 150 | } 151 | 152 | /** 153 | * @return int[]|null 154 | */ 155 | public function getSttsFramecount(): ?array 156 | { 157 | return $this->stts_framecount; 158 | } 159 | 160 | public function getFree(): ?Id3AudioQuicktimeItem 161 | { 162 | return $this->free; 163 | } 164 | 165 | public function getWide(): ?Id3AudioQuicktimeItem 166 | { 167 | return $this->wide; 168 | } 169 | 170 | public function getMdat(): ?Id3AudioQuicktimeItem 171 | { 172 | return $this->mdat; 173 | } 174 | 175 | public function getChapter(int $index): ?Id3AudioQuicktimeChapter 176 | { 177 | return $this->chapters[$index] ?? null; 178 | } 179 | 180 | public function toArray(): array 181 | { 182 | $chapters = []; 183 | foreach ($this->chapters as $chapter) { 184 | $chapters[] = $chapter->toArray(); 185 | } 186 | 187 | return [ 188 | 'hinting' => $this->hinting, 189 | 'controller' => $this->controller, 190 | 'ftyp' => $this->ftyp?->toArray(), 191 | 'timestamps_unix' => $this->timestamps_unix, 192 | 'time_scale' => $this->time_scale, 193 | 'display_scale' => $this->display_scale, 194 | 'video' => $this->video, 195 | 'audio' => $this->audio, 196 | 'stts_framecount' => $this->stts_framecount, 197 | 'comments' => $this->comments, 198 | 'chapters' => $chapters, 199 | 'free' => $this->free?->toArray(), 200 | 'wide' => $this->wide?->toArray(), 201 | 'mdat' => $this->mdat?->toArray(), 202 | 'encoding' => $this->encoding, 203 | ]; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/Id3/Reader/Id3AudioQuicktimeChapter.php: -------------------------------------------------------------------------------- 1 | timestamp; 32 | } 33 | 34 | public function getTitle(): ?string 35 | { 36 | return $this->title; 37 | } 38 | 39 | public function toArray(): array 40 | { 41 | return [ 42 | 'timestamp' => $this->timestamp, 43 | 'title' => $this->title, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Id3/Reader/Id3AudioQuicktimeItem.php: -------------------------------------------------------------------------------- 1 | hierarchy; 47 | } 48 | 49 | public function getName(): ?string 50 | { 51 | return $this->name; 52 | } 53 | 54 | public function getSize(): ?int 55 | { 56 | return $this->size; 57 | } 58 | 59 | public function getOffset(): ?int 60 | { 61 | return $this->offset; 62 | } 63 | 64 | public function getSignature(): ?string 65 | { 66 | return $this->signature; 67 | } 68 | 69 | public function getUnknown1(): ?int 70 | { 71 | return $this->unknown_1; 72 | } 73 | 74 | public function getFourcc(): ?string 75 | { 76 | return $this->fourcc; 77 | } 78 | 79 | public function toArray(): array 80 | { 81 | return [ 82 | 'hierarchy' => $this->hierarchy, 83 | 'name' => $this->name, 84 | 'size' => $this->size, 85 | 'offset' => $this->offset, 86 | 'signature' => $this->signature, 87 | 'unknown_1' => $this->unknown_1, 88 | 'fourcc' => $this->fourcc, 89 | ]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Id3/Reader/Id3AudioTag.php: -------------------------------------------------------------------------------- 1 | $value !== null); 25 | $properties = array_filter($properties, fn ($value) => $value !== ''); 26 | 27 | return $properties; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Id3/Tag/Id3TagApe.php: -------------------------------------------------------------------------------- 1 | picture) { 19 | return null; 20 | } 21 | 22 | $self = new self; 23 | 24 | $self->contents = base64_encode($comments->picture->data); 25 | $self->mime_type = $comments->picture->image_mime; 26 | $self->width = $comments->picture->image_width; 27 | $self->height = $comments->picture->image_height; 28 | 29 | return $self; 30 | } 31 | 32 | /** 33 | * Get the contents of the cover 34 | * 35 | * By default, the contents are decoded from base64, but you can get the raw contents by passing `true` as the first argument. 36 | */ 37 | public function getContents(bool $base64 = false): ?string 38 | { 39 | if (! $this->contents) { 40 | return null; 41 | } 42 | 43 | return $base64 ? $this->contents : base64_decode($this->contents); 44 | } 45 | 46 | /** 47 | * Get the MIME type of the cover 48 | */ 49 | public function getMimeType(): ?string 50 | { 51 | return $this->mime_type; 52 | } 53 | 54 | /** 55 | * Get the width of the cover 56 | */ 57 | public function getWidth(): ?int 58 | { 59 | return $this->width; 60 | } 61 | 62 | /** 63 | * Get the height of the cover 64 | */ 65 | public function getHeight(): ?int 66 | { 67 | return $this->height; 68 | } 69 | 70 | /** 71 | * Extract the cover to a file. 72 | */ 73 | public function extractCover(string $path): void 74 | { 75 | file_put_contents($path, $this->getContents()); 76 | } 77 | 78 | public function toArray(): array 79 | { 80 | return [ 81 | 'contents' => $this->contents, 82 | 'mime_type' => $this->mime_type, 83 | 'width' => $this->width, 84 | 'height' => $this->height, 85 | ]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Models/AudioMetadata.php: -------------------------------------------------------------------------------- 1 | getPath(); 42 | $audio = $id3_reader->getAudio(); 43 | $stat = stat($path); 44 | 45 | return new self( 46 | file_size: $id3_reader->getFileSize(), 47 | data_format: $audio?->data_format, 48 | warning: $id3_reader->getWarning(), 49 | encoding: $id3_reader->getEncoding(), 50 | mime_type: $id3_reader->getMimeType(), 51 | quicktime: $id3_reader->getQuicktime(), 52 | duration_seconds: $id3_reader->getPlaytimeSeconds(), 53 | bitrate: intval($id3_reader->getBitrate()), 54 | bitrate_mode: $audio?->bitrate_mode, 55 | sample_rate: $audio?->sample_rate, 56 | channels: $audio?->channels, 57 | channel_mode: $audio?->channel_mode, 58 | is_lossless: $audio?->lossless ?? false, 59 | compression_ratio: $audio?->compression_ratio, 60 | codec: $audio?->codec, 61 | encoder_options: $audio?->encoder_options, 62 | version: $id3_reader->getVersion(), 63 | av_data_offset: $id3_reader->getAvDataOffset(), 64 | av_data_end: $id3_reader->getAvDataEnd(), 65 | file_path: $id3_reader->getFilePath(), 66 | filename: $id3_reader->getFilename(), 67 | last_access_at: $stat['atime'] ? new DateTime('@'.$stat['atime']) : null, 68 | created_at: $stat['ctime'] ? new DateTime('@'.$stat['ctime']) : null, 69 | modified_at: $stat['mtime'] ? new DateTime('@'.$stat['mtime']) : null, 70 | ); 71 | } 72 | 73 | /** 74 | * Get size of the audio file in bytes, like `180664` 75 | */ 76 | public function getFileSize(): ?int 77 | { 78 | return $this->file_size; 79 | } 80 | 81 | /** 82 | * Get size of the audio file in human readable format, like `175.99 KB` 83 | */ 84 | public function getSizeHuman(int $decimals = 2): ?string 85 | { 86 | $file_size = (string) $this->file_size; 87 | $size = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; 88 | $factor = floor((strlen($file_size) - 1) / 3); 89 | 90 | return sprintf("%.{$decimals}f", $file_size / pow(1024, $factor)).' '.$size[$factor]; 91 | } 92 | 93 | /** 94 | * Get data format of the audio file, like `mp3`, `wav`, `etc` 95 | */ 96 | public function getDataFormat(): ?string 97 | { 98 | return $this->data_format; 99 | } 100 | 101 | /** 102 | * Get warning of the audio file 103 | * 104 | * @return string[] 105 | */ 106 | public function getWarning(): array 107 | { 108 | return $this->warning; 109 | } 110 | 111 | /** 112 | * Get encoding of the audio file, like `UTF-8`, `ISO-8859-1`, `etc` 113 | */ 114 | public function getEncoding(): ?string 115 | { 116 | return $this->encoding; 117 | } 118 | 119 | /** 120 | * Get mime type of the audio file, like `audio/x-matroska`, `audio/mpeg`, `etc` 121 | */ 122 | public function getMimeType(): ?string 123 | { 124 | return $this->mime_type; 125 | } 126 | 127 | /** 128 | * Get quicktime data of the audio file, if available 129 | */ 130 | public function getQuicktime(): ?Id3AudioQuicktime 131 | { 132 | return $this->quicktime; 133 | } 134 | 135 | /** 136 | * Get duration of the audio file in seconds, like `11.05` 137 | */ 138 | public function getDurationSeconds(?int $decimals = null): ?float 139 | { 140 | if ($decimals !== null) { 141 | return round($this->duration_seconds, $decimals); 142 | } 143 | 144 | return $this->duration_seconds; 145 | } 146 | 147 | /** 148 | * Get bitrate of the audio file in bits per second, like `128000` 149 | */ 150 | public function getBitrate(): ?int 151 | { 152 | return $this->bitrate; 153 | } 154 | 155 | /** 156 | * Get bitrate mode of the audio file, like `cbr`, `vbr`, `etc` 157 | */ 158 | public function getBitrateMode(): ?string 159 | { 160 | return $this->bitrate_mode; 161 | } 162 | 163 | /** 164 | * Get sample rate of the audio file in hertz, like `44100` 165 | */ 166 | public function getSampleRate(): ?int 167 | { 168 | return $this->sample_rate; 169 | } 170 | 171 | /** 172 | * Get channels of the audio file, like `2` 173 | */ 174 | public function getChannels(): ?int 175 | { 176 | return $this->channels; 177 | } 178 | 179 | /** 180 | * Get channel mode of the audio file, like `joint stereo`, `stereo`, `etc` 181 | */ 182 | public function getChannelMode(): ?string 183 | { 184 | return $this->channel_mode; 185 | } 186 | 187 | /** 188 | * Get lossless status of the audio file, like `false` 189 | */ 190 | public function isLossless(): bool 191 | { 192 | return $this->is_lossless; 193 | } 194 | 195 | /** 196 | * Get compression ratio of the audio file, like `0.1` 197 | */ 198 | public function getCompressionRatio(?int $decimals = null): ?float 199 | { 200 | if ($decimals !== null) { 201 | return round($this->compression_ratio, $decimals); 202 | } 203 | 204 | return $this->compression_ratio; 205 | } 206 | 207 | /** 208 | * Get codec of the audio file, like `LAME` 209 | */ 210 | public function getCodec(): ?string 211 | { 212 | return $this->codec; 213 | } 214 | 215 | /** 216 | * Get encoder options of the audio file, like `CBR`, `VBR`, `etc` 217 | */ 218 | public function getEncoderOptions(): ?string 219 | { 220 | return $this->encoder_options; 221 | } 222 | 223 | /** 224 | * Get version of `JamesHeinrich/getID3`, like `1.9.23-202310190849` 225 | * 226 | * @docs https://github.com/JamesHeinrich/getID3 227 | */ 228 | public function getVersion(): ?string 229 | { 230 | return $this->version; 231 | } 232 | 233 | /** 234 | * Get audio/video data offset of the audio file, like `25808` 235 | */ 236 | public function getAvDataOffset(): ?int 237 | { 238 | return $this->av_data_offset; 239 | } 240 | 241 | /** 242 | * Get audio/video data end of the audio file, like `1214046` 243 | */ 244 | public function getAvDataEnd(): ?int 245 | { 246 | return $this->av_data_end; 247 | } 248 | 249 | /** 250 | * Get path of audio file directory, like `/path/to` 251 | */ 252 | public function getFilePath(): ?string 253 | { 254 | return $this->file_path; 255 | } 256 | 257 | /** 258 | * Get filename of the audio file, like `audio.mp3` 259 | */ 260 | public function getFilename(): ?string 261 | { 262 | return $this->filename; 263 | } 264 | 265 | /** 266 | * Get last access time of the audio file, like `2021-09-01 00:00:00` 267 | */ 268 | public function getLastAccessAt(): ?DateTime 269 | { 270 | return $this->last_access_at; 271 | } 272 | 273 | /** 274 | * Get created time of the audio file, like `2021-09-01 00:00:00` 275 | */ 276 | public function getCreatedAt(): ?DateTime 277 | { 278 | return $this->created_at; 279 | } 280 | 281 | /** 282 | * Get modified time of the audio file, like `2021-09-01 00:00:00` 283 | */ 284 | public function getModifiedAt(): ?DateTime 285 | { 286 | return $this->modified_at; 287 | } 288 | 289 | public function toArray(): array 290 | { 291 | return [ 292 | 'file_size' => $this->file_size, 293 | 'data_format' => $this->data_format, 294 | 'encoding' => $this->encoding, 295 | 'mime_type' => $this->mime_type, 296 | 'quicktime' => $this->quicktime?->toArray(), 297 | 'warning' => $this->warning, 298 | 'duration_seconds' => $this->duration_seconds, 299 | 'bitrate' => $this->bitrate, 300 | 'bitrate_mode' => $this->bitrate_mode, 301 | 'sample_rate' => $this->sample_rate, 302 | 'channels' => $this->channels, 303 | 'channel_mode' => $this->channel_mode, 304 | 'is_lossless' => $this->is_lossless, 305 | 'compression_ratio' => $this->compression_ratio, 306 | 'codec' => $this->codec, 307 | 'encoder_options' => $this->encoder_options, 308 | 'version' => $this->version, 309 | 'av_data_offset' => $this->av_data_offset, 310 | 'av_data_end' => $this->av_data_end, 311 | 'file_path' => $this->file_path, 312 | 'filename' => $this->filename, 313 | 'last_access_at' => $this->last_access_at?->format('Y-m-d H:i:s'), 314 | 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), 315 | 'modified_at' => $this->modified_at?->format('Y-m-d H:i:s'), 316 | ]; 317 | } 318 | } 319 | --------------------------------------------------------------------------------