├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── browser-id3-writer.d.ts ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── src ├── ID3Writer.mjs ├── encoder.mjs ├── signatures.mjs ├── sizes.mjs └── transform.mjs ├── test ├── arrayOfStrings.mjs ├── encoder.mjs ├── index.mjs ├── integer.mjs ├── object │ ├── APIC.mjs │ ├── COMM.mjs │ ├── IPLS.mjs │ ├── PRIV.mjs │ ├── SYLT.mjs │ ├── TXXX.mjs │ └── USLT.mjs ├── string.mjs ├── transform.mjs └── utils.mjs └── tools ├── distSize.mjs └── id3v2.3.0.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: 22 13 | - run: npm ci 14 | - run: npm test 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v6.2.0 4 | 5 | - Add `TCMP` frame support 6 | 7 | ## v6.1.0 8 | 9 | - Add TypeScript declaration file 10 | 11 | ## v6.0.0 12 | 13 | - **Breaking**: Now this library exports only as JS native module (not UMD) and use named export (not default export) 14 | 15 | Migration on Nodejs: 16 | 17 | ```js 18 | // v5 common js 19 | const ID3Writer = require('browser-id3-writer'); 20 | 21 | // v5 esm interop 22 | import ID3Writer from 'browser-id3-writer'; 23 | 24 | // v6 25 | import { ID3Writer } from 'browser-id3-writer'; 26 | ``` 27 | 28 | Migration on browsers: 29 | 30 | ```html 31 | 32 | 33 | 36 | 37 | 38 | 42 | ``` 43 | 44 | ## v5.0.0 45 | 46 | - **Breaking**: Change `TDAT` frame type from number to string as some values is not possible to represent as number in JS (like 0212), so this change fixes ability to properly encode this frame in some situations: 47 | 48 | ```js 49 | // v4 50 | writer.setFrame('TDAT', 1234); 51 | 52 | // v5 53 | writer.setFrame('TDAT', '1234'); 54 | ``` 55 | 56 | - **Breaking**: Drop Babel, so now this library requires ES6 native support (IE isn't supported anymore) 57 | - Add `IPLS` and `SYLT` frames support 58 | 59 | ## v4.4.0 60 | 61 | - Add language support for `COMM` and `USLT` frames: 62 | 63 | ```js 64 | writer.setFrame('USLT', { 65 | language: 'jpn', 66 | description: '例えば', 67 | lyrics: 'サマータイム', 68 | }); 69 | ``` 70 | 71 | ## v4.3.0 72 | 73 | - Add `TLAN`, `TIT1`, `TIT3` frames 74 | 75 | ## v4.2.0 76 | 77 | - Remove `TKEY` frame validation 78 | - Support `TEXT` and `PRIV` frames 79 | 80 | ## v4.1.0 81 | 82 | - Add support for `TCOP`, `TSRC` and `TDAT` frames 83 | 84 | ## v4.0.0 85 | 86 | - **Breaking**: Now description of `APIC` frame is encoded in Western encoding by-default. That's because of a problem with iTunes and Finder on macOS. You can still encode it in Unicode encoding by specifying it: 87 | 88 | ```js 89 | // v3 90 | writer.setFrame('APIC', { 91 | type: 3, 92 | data: coverArrayBuffer, 93 | description: 'Продам гараж', 94 | }); 95 | 96 | // v4 97 | writer.setFrame('APIC', { 98 | type: 3, 99 | data: coverArrayBuffer, 100 | description: 'Продам гараж', 101 | useUnicodeEncoding: true, // that's dangerous 102 | }); 103 | ``` 104 | 105 | ## v3.0.3 106 | 107 | - Decrease library size from `8.68 kB` to `7.3 kB` in result of using rollup instead of webpack 108 | 109 | ## v3.0.2 110 | 111 | - Now this library works in `IE10`. Just replaced `ArrayBuffer.prototype.slice` to `TypedArray.prototype.subarray`. 112 | 113 | ## v3.0.1 114 | 115 | - No new features / bug fixes, but now readme in both Github and npm will contain exact library version and integrity to include it from CDN. 116 | 117 | ## v3.0.0 118 | 119 | - **Breaking**: now only minified version of the lib is distributed and without maps. If you are using v2 browser-id3-writer.js from CDN update the link: 120 | 121 | ```html 122 | 123 | 124 | 125 | 126 | 127 | 128 | ``` 129 | 130 | - **Breaking**: no more "Unknown Artist" is added when you set `TPE1` or `TCOM` frames with empty array 131 | - **Breaking**: `USLT` frame now accepts an object with keys description and lyrics: 132 | 133 | ```js 134 | // v2 135 | writer.setFrame('USLT', 'This is unsychronised lyrics'); 136 | 137 | // v3 138 | writer.setFrame('USLT', { 139 | description: '', 140 | lyrics: 'This is unsychronised lyrics', 141 | }); 142 | ``` 143 | 144 | - **Breaking**: `APIC` frame now accepts an object with keys type (see `APIC` picture types), data and description: 145 | 146 | ```js 147 | // v2 148 | writer.setFrame('APIC', coverArrayBuffer); 149 | 150 | // v3 151 | writer.setFrame('APIC', { 152 | type: 3, 153 | data: coverArrayBuffer, 154 | description: '', 155 | }); 156 | ``` 157 | 158 | - Add support for next frames: `COMM`, `TXXX`, `WCOM`, `WCOP`, `WOAF`, `WOAR`, `WOAS`, `WORS`, `WPAY`, `WPUB`, `TKEY`, `TMED`, `TPE4`, `TPE3` and `TBPM`. See readme for usage info. 159 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright Artyom Egorov 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browser ID3 Writer 2 | 3 | [![npm package][npm-badge]][npm] 4 | 5 | [npm-badge]: https://img.shields.io/npm/v/browser-id3-writer.svg?style=flat-square 6 | [npm]: https://www.npmjs.com/package/browser-id3-writer 7 | 8 | JavaScript library for writing [ID3 (v2.3)](https://egoroof.github.io/browser-id3-writer/spec/) tag to MP3 files in browsers and Node.js. 9 | It can't read the tag so use another lib to do it. 10 | 11 | **Note**: the library removes existing ID3 tag (v2.2, v2.3 and v2.4). 12 | 13 | Here is an online demonstration: [egoroof.github.io/browser-id3-writer/](https://egoroof.github.io/browser-id3-writer/) 14 | 15 | Find the changelog in [CHANGELOG.md](https://github.com/egoroof/browser-id3-writer/blob/master/CHANGELOG.md) 16 | 17 | ## Table of Contents 18 | 19 | - [Installation](#installation) 20 | - [JS modules](#js-modules) 21 | - [Usage](#usage) 22 | - [Browser](#browser) 23 | 1. [Get ArrayBuffer of song](#get-arraybuffer-of-song) 24 | 2. [Add a tag](#add-a-tag) 25 | 3. [Save file](#save-file) 26 | 4. [Memory control](#memory-control) 27 | - [Node.js](#nodejs) 28 | - [Supported frames](#supported-frames) 29 | - [APIC picture types](#apic-picture-types) 30 | - [SYLT content types](#sylt-content-types) 31 | - [SYLT timestamp formats](#sylt-timestamp-formats) 32 | 33 | ## Installation 34 | 35 | Take latest version [here](https://unpkg.com/browser-id3-writer) or with npm: 36 | 37 | ``` 38 | npm install browser-id3-writer --save 39 | ``` 40 | 41 | ### JS modules 42 | 43 | The library is only deployed in [native JS modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), so in browsers you have to use `script` with type `module`: 44 | 45 | ```html 46 | 50 | ``` 51 | 52 | Or bundle the library to your code. 53 | 54 | In Nodejs it imports easily: 55 | 56 | ```js 57 | import { ID3Writer } from 'browser-id3-writer'; 58 | ``` 59 | 60 | ## Usage 61 | 62 | ### Browser 63 | 64 | #### Get ArrayBuffer of song 65 | 66 | In browsers you should first get 67 | [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) 68 | of the song you would like to add ID3 tag. 69 | 70 | ##### FileReader 71 | 72 | For example you can create file input and use 73 | [FileReader](https://developer.mozilla.org/en-US/docs/Web/API/FileReader): 74 | 75 | ```html 76 | 77 | 96 | ``` 97 | 98 | ##### Fetch 99 | 100 | To get arrayBuffer from a remote server you can use 101 | [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API): 102 | 103 | ```js 104 | const request = await fetch(urlToSongFile); 105 | if (!request.ok) { 106 | // handle error 107 | console.error(`Unable to fetch ${urlToSongFile}`); 108 | } 109 | const arrayBuffer = await request.arrayBuffer(); 110 | // go next 111 | ``` 112 | 113 | #### Add a tag 114 | 115 | Create a new `ID3Writer` instance with arrayBuffer of your song, set frames and add a tag: 116 | 117 | ```js 118 | // arrayBuffer of song or empty arrayBuffer if you just want only id3 tag without song 119 | const writer = new ID3Writer(arrayBuffer); 120 | writer 121 | .setFrame('TIT2', 'Home') 122 | .setFrame('TPE1', ['Eminem', '50 Cent']) 123 | .setFrame('TALB', 'Friday Night Lights') 124 | .setFrame('TYER', 2004) 125 | .setFrame('TRCK', '6/8') 126 | .setFrame('TCON', ['Soundtrack']) 127 | .setFrame('TBPM', 128) 128 | .setFrame('WPAY', 'https://google.com') 129 | .setFrame('TKEY', 'Fbm') 130 | .setFrame('APIC', { 131 | type: 3, 132 | data: coverArrayBuffer, 133 | description: 'Super picture', 134 | }); 135 | writer.addTag(); 136 | ``` 137 | 138 | #### Save file 139 | 140 | Now you can save it to file as you want: 141 | 142 | ```js 143 | const taggedSongBuffer = writer.arrayBuffer; 144 | const blob = writer.getBlob(); 145 | const url = writer.getURL(); 146 | ``` 147 | 148 | For example you can save file using [FileSaver.js](https://github.com/eligrey/FileSaver.js/): 149 | 150 | ```js 151 | saveAs(blob, 'song with tags.mp3'); 152 | ``` 153 | 154 | If you are writing chromium extension you can save file using 155 | [Downloads API](https://developer.chrome.com/docs/extensions/reference/api/downloads): 156 | 157 | ```js 158 | chrome.downloads.download({ 159 | url: url, 160 | filename: 'song with tags.mp3', 161 | }); 162 | ``` 163 | 164 | #### Memory control 165 | 166 | When you generate URLs via `writer.getURL()` you should know 167 | that whole file is kept in memory until you close the page or move to another one. 168 | So if you generate lots of URLs in a single page you should manually free memory 169 | after you finish downloading file: 170 | 171 | ```js 172 | URL.revokeObjectURL(url); // if you know url or 173 | writer.revokeURL(); // if you have access to writer 174 | ``` 175 | 176 | ### Node.js 177 | 178 | Simple example with blocking IO: 179 | 180 | ```js 181 | import { ID3Writer } from 'browser-id3-writer'; 182 | import { readFileSync, writeFileSync } from 'fs'; 183 | 184 | const songBuffer = readFileSync('path_to_song.mp3'); 185 | const coverBuffer = readFileSync('path_to_cover.jpg'); 186 | 187 | const writer = new ID3Writer(songBuffer); 188 | writer 189 | .setFrame('TIT2', 'Home') 190 | .setFrame('TPE1', ['Eminem', '50 Cent']) 191 | .setFrame('TALB', 'Friday Night Lights') 192 | .setFrame('TYER', 2004) 193 | .setFrame('APIC', { 194 | type: 3, 195 | data: coverBuffer, 196 | description: 'Super picture', 197 | }); 198 | writer.addTag(); 199 | 200 | const taggedSongBuffer = Buffer.from(writer.arrayBuffer); 201 | writeFileSync('song_with_tags.mp3', taggedSongBuffer); 202 | ``` 203 | 204 | You can also create only ID3 tag without song and use it as you want: 205 | 206 | ```js 207 | const writer = new ID3Writer(Buffer.alloc(0)); 208 | writer.padding = 0; // default 4096 209 | writer.setFrame('TIT2', 'Home'); 210 | writer.addTag(); 211 | const id3Buffer = Buffer.from(writer.arrayBuffer); 212 | ``` 213 | 214 | ## Supported frames 215 | 216 | **array of strings:** 217 | 218 | - TPE1 (song artists) 219 | - TCOM (song composers) 220 | - TCON (song genres) 221 | 222 | **string** 223 | 224 | - TLAN (language) 225 | - TIT1 (content group description) 226 | - TIT2 (song title) 227 | - TIT3 (song subtitle) 228 | - TALB (album title) 229 | - TPE2 (album artist) 230 | - TPE3 (conductor/performer refinement) 231 | - TPE4 (interpreted, remixed, or otherwise modified by) 232 | - TRCK (song number in album): '5' or '5/10' 233 | - TPOS (album disc number): '1' or '1/3' 234 | - TPUB (label name) 235 | - TKEY (initial key) 236 | - TMED (media type) 237 | - TDAT (album release date expressed as 'DDMM') 238 | - TSRC (isrc - international standard recording code) 239 | - TCOP (copyright message) 240 | - TCMP (iTunes compilation flag) 241 | - TEXT (lyricist / text writer) 242 | - WCOM (commercial information) 243 | - WCOP (copyright/Legal information) 244 | - WOAF (official audio file webpage) 245 | - WOAR (official artist/performer webpage) 246 | - WOAS (official audio source webpage) 247 | - WORS (official internet radio station homepage) 248 | - WPAY (payment) 249 | - WPUB (publishers official webpage) 250 | 251 | **integer** 252 | 253 | - TLEN (song duration in milliseconds) 254 | - TYER (album release year) 255 | - TBPM (beats per minute) 256 | 257 | **object** 258 | 259 | - COMM (comments): 260 | 261 | ```js 262 | writer.setFrame('COMM', { 263 | description: 'description here', 264 | text: 'text here', 265 | language: 'eng', 266 | }); 267 | ``` 268 | 269 | - USLT (unsychronised lyrics): 270 | 271 | ```js 272 | writer.setFrame('USLT', { 273 | description: 'description here', 274 | lyrics: 'lyrics here', 275 | language: 'eng', 276 | }); 277 | ``` 278 | 279 | - IPLS (involved people list): 280 | 281 | ```js 282 | writer.setFrame('IPLS', [ 283 | ['role', 'name'], 284 | ['role', 'name'], 285 | // ... 286 | ]); 287 | ``` 288 | 289 | - SYLT (synchronised lyrics): 290 | 291 | ```js 292 | writer.setFrame('SYLT', { 293 | type: 1, 294 | text: [ 295 | ['lyrics here', 0], 296 | ['lyrics here', 3500], 297 | // ... 298 | ], 299 | timestampFormat: 2, 300 | language: 'eng', 301 | description: 'description', 302 | }); 303 | ``` 304 | 305 | `text` is an array of arrays of string and integer. 306 | 307 | - TXXX (user defined text): 308 | 309 | ```js 310 | writer.setFrame('TXXX', { 311 | description: 'description here', 312 | value: 'value here', 313 | }); 314 | ``` 315 | 316 | - PRIV (private frame): 317 | 318 | ```js 319 | writer.setFrame('PRIV', { 320 | id: 'identifier', 321 | data: dataArrayBuffer, 322 | }); 323 | ``` 324 | 325 | - APIC (attached picture): 326 | 327 | ```js 328 | writer.setFrame('APIC', { 329 | type: 3, 330 | data: coverArrayBuffer, 331 | description: 'description here', 332 | useUnicodeEncoding: false, 333 | }); 334 | ``` 335 | 336 | `useUnicodeEncoding` should only be `true` when description contains non-Western characters. 337 | When it's set to `true` some program might not be able to read the picture correctly. 338 | See [#42](https://github.com/egoroof/browser-id3-writer/issues/42). 339 | 340 | ## APIC picture types 341 | 342 | | Type | Name | 343 | | ---- | ----------------------------------- | 344 | | 0 | Other | 345 | | 1 | 32x32 pixels 'file icon' (PNG only) | 346 | | 2 | Other file icon | 347 | | 3 | Cover (front) | 348 | | 4 | Cover (back) | 349 | | 5 | Leaflet page | 350 | | 6 | Media (e.g. label side of CD) | 351 | | 7 | Lead artist/lead performer/soloist | 352 | | 8 | Artist/performer | 353 | | 9 | Conductor | 354 | | 10 | Band/Orchestra | 355 | | 11 | Composer | 356 | | 12 | Lyricist/text writer | 357 | | 13 | Recording location | 358 | | 14 | During recording | 359 | | 15 | During performance | 360 | | 16 | Movie/video screen capture | 361 | | 17 | A bright coloured fish | 362 | | 18 | Illustration | 363 | | 19 | Band/artist logotype | 364 | | 20 | Publisher/Studio logotype | 365 | 366 | ## SYLT content types 367 | 368 | | Type | Name | 369 | | ---- | -------------------------------------------- | 370 | | 0 | Other | 371 | | 1 | Lyrics | 372 | | 2 | Text transcription | 373 | | 3 | Movement/part name (e.g. "Adagio") | 374 | | 4 | Events (e.g. "Don Quijote enters the stage") | 375 | | 5 | Chord (e.g. "Bb F Fsus") | 376 | | 6 | Trivia/'pop up' information | 377 | 378 | ## SYLT timestamp formats 379 | 380 | | Type | Name | 381 | | ---- | ------------------------------------------------------- | 382 | | 1 | Absolute time, 32 bit sized, using MPEG frames as unit | 383 | | 2 | Absolute time, 32 bit sized, using milliseconds as unit | 384 | -------------------------------------------------------------------------------- /browser-id3-writer.d.ts: -------------------------------------------------------------------------------- 1 | // Hexidecimal values used to maintain consistency with the ID3v2.3 2 | // documentation. Refer to /tools/id3v2.3.0.txt. 3 | declare module 'browser-id3-writer' { 4 | export const enum SynchronizedLyricsType { 5 | Other = 0x00, 6 | Lyrics = 0x01, 7 | 8 | /** 9 | * Text transcription 10 | */ 11 | TextTranscription = 0x02, 12 | 13 | /** 14 | * Movement/part name (e.g. "Adagio") 15 | */ 16 | MovementPartName = 0x03, 17 | 18 | /** 19 | * Events (e.g. "Don Quijote enters the stage") 20 | */ 21 | Events = 0x04, 22 | 23 | /** 24 | * Chord (e.g. "Bb F Fsus") 25 | */ 26 | Chord = 0x05, 27 | 28 | /** 29 | * Trivia/'pop up' information 30 | */ 31 | Trivia = 0x06, 32 | } 33 | 34 | export const enum SynchronizedLyricsTimestampFormat { 35 | /** 36 | * Absolute time, 32 bit sized, using MPEG frames as unit 37 | */ 38 | Frames = 0x01, 39 | 40 | /** 41 | * Absolute time, 32 bit sized, using milliseconds as unit 42 | */ 43 | Milliseconds = 0x02, 44 | } 45 | 46 | export const enum ImageType { 47 | Other = 0x00, 48 | /** 49 | * 32x32 pixels 'file icon' (PNG only) 50 | */ 51 | Icon = 0x01, 52 | 53 | /** 54 | * Other file icon 55 | */ 56 | OtherIcon = 0x02, 57 | 58 | /** 59 | * Cover (front) 60 | */ 61 | CoverFront = 0x03, 62 | 63 | /** 64 | * Cover (back) 65 | */ 66 | CoverBack = 0x04, 67 | 68 | /** 69 | * Leaflet page 70 | */ 71 | Leaflet = 0x05, 72 | 73 | /** 74 | * Media (e.g. label side of CD) 75 | */ 76 | Media = 0x06, 77 | 78 | /** 79 | * Lead artist/lead performer/soloist 80 | */ 81 | LeadArtist = 0x07, 82 | 83 | /** 84 | * Artist/performer 85 | */ 86 | Artist = 0x08, 87 | 88 | Conductor = 0x09, 89 | 90 | /** 91 | * Band/Orchestra 92 | */ 93 | Band = 0x0a, 94 | 95 | Composer = 0x0b, 96 | 97 | /** 98 | * Lyricist/text writer 99 | */ 100 | Lyricist = 0x0c, 101 | 102 | /** 103 | * Recording location 104 | */ 105 | RecordingLocation = 0x0d, 106 | 107 | /** 108 | * During recording 109 | */ 110 | DuringRecording = 0x0e, 111 | 112 | /** 113 | * During performance 114 | */ 115 | DuringPerformance = 0x0f, 116 | 117 | /** 118 | * Movie/video screen capture 119 | */ 120 | MovieScreenCapture = 0x10, 121 | 122 | /** 123 | * A brightly coloured fish 124 | */ 125 | BrightColouredFish = 0x11, 126 | 127 | Illustration = 0x12, 128 | 129 | /** 130 | * Band/artist logotype 131 | */ 132 | BandLogotype = 0x13, 133 | 134 | /** 135 | * Publisher/Studio logotype 136 | */ 137 | PublisherLogotype = 0x14, 138 | } 139 | 140 | export class ID3Writer { 141 | constructor(buffer: ArrayBufferLike); 142 | 143 | setFrame(id: 'TBPM' | 'TLEN' | 'TYER', value: number): this; 144 | 145 | setFrame( 146 | id: 147 | | 'TALB' 148 | | 'TCOP' 149 | | 'TCMP' 150 | | 'TDAT' 151 | | 'TEXT' 152 | | 'TIT1' 153 | | 'TIT2' 154 | | 'TIT3' 155 | | 'TKEY' 156 | | 'TLAN' 157 | | 'TMED' 158 | | 'TPE2' 159 | | 'TPE3' 160 | | 'TPE4' 161 | | 'TPOS' 162 | | 'TPUB' 163 | | 'TRCK' 164 | | 'TSRC' 165 | | 'WCOM' 166 | | 'WCOP' 167 | | 'WOAF' 168 | | 'WOAR' 169 | | 'WOAS' 170 | | 'WORS' 171 | | 'WPAY' 172 | | 'WPUB', 173 | value: string, 174 | ): this; 175 | 176 | setFrame(id: 'TCOM' | 'TCON' | 'TPE1', value: readonly string[]): this; 177 | 178 | setFrame( 179 | id: 'USLT', 180 | value: { 181 | readonly description: string; 182 | readonly language?: string; 183 | readonly lyrics: string; 184 | }, 185 | ): this; 186 | 187 | setFrame( 188 | id: 'APIC', 189 | value: { 190 | readonly description: string; 191 | readonly data: ArrayBufferLike; 192 | readonly type: ImageType; 193 | readonly useUnicodeEncoding?: boolean; 194 | }, 195 | ): this; 196 | 197 | setFrame( 198 | id: 'TXXX', 199 | value: { 200 | readonly description: string; 201 | readonly value: string; 202 | }, 203 | ): this; 204 | 205 | setFrame( 206 | id: 'COMM', 207 | value: { 208 | readonly language?: string; 209 | readonly description: string; 210 | readonly text: string; 211 | }, 212 | ): this; 213 | 214 | setFrame( 215 | id: 'PRIV', 216 | value: { 217 | readonly id: string; 218 | readonly data: ArrayBufferLike; 219 | }, 220 | ): this; 221 | 222 | setFrame(id: 'IPLS', value: readonly (readonly [string, string])[]): this; 223 | 224 | setFrame( 225 | id: 'SYLT', 226 | value: { 227 | readonly type: SynchronizedLyricsType; 228 | readonly text: readonly (readonly [string, number])[]; 229 | readonly timestampFormat: SynchronizedLyricsTimestampFormat; 230 | readonly language?: string; 231 | readonly description?: string; 232 | }, 233 | ): this; 234 | 235 | removeTag(): void; 236 | 237 | addTag(): ArrayBuffer; 238 | 239 | getBlob(): Blob; 240 | 241 | getURL(): string; 242 | 243 | revokeURL(): void; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | 3 | export default [ 4 | js.configs.recommended, 5 | { 6 | languageOptions: { 7 | globals: { 8 | URL: 'readonly', 9 | Blob: 'readonly', 10 | console: 'readonly', 11 | }, 12 | }, 13 | rules: { 14 | 'no-var': 2, // require let or const instead of var 15 | 'prefer-arrow-callback': 2, // suggest using arrow functions as callbacks 16 | 'prefer-const': 2, // suggest using const declaration for variables that are never modified after declared 17 | 'prefer-rest-params': 2, // suggest using the rest parameters instead of arguments 18 | 'prefer-spread': 2, // suggest using the spread operator instead of .apply(). 19 | 'prefer-template': 2, // suggest using template literals instead of strings concatenation 20 | }, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-id3-writer", 3 | "version": "6.2.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "browser-id3-writer", 9 | "version": "6.2.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@eslint/js": "^9.19.0", 13 | "eslint": "^9.19.0", 14 | "prettier": "^3.4.2", 15 | "rollup": "^4.32.0", 16 | "terser": "^5.37.0" 17 | } 18 | }, 19 | "node_modules/@aashutoshrathi/word-wrap": { 20 | "version": "1.2.6", 21 | "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", 22 | "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", 23 | "dev": true, 24 | "engines": { 25 | "node": ">=0.10.0" 26 | } 27 | }, 28 | "node_modules/@eslint-community/eslint-utils": { 29 | "version": "4.4.0", 30 | "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", 31 | "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", 32 | "dev": true, 33 | "dependencies": { 34 | "eslint-visitor-keys": "^3.3.0" 35 | }, 36 | "engines": { 37 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 38 | }, 39 | "peerDependencies": { 40 | "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 41 | } 42 | }, 43 | "node_modules/@eslint-community/regexpp": { 44 | "version": "4.12.1", 45 | "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", 46 | "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", 47 | "dev": true, 48 | "license": "MIT", 49 | "engines": { 50 | "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 51 | } 52 | }, 53 | "node_modules/@eslint/config-array": { 54 | "version": "0.19.1", 55 | "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", 56 | "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", 57 | "dev": true, 58 | "license": "Apache-2.0", 59 | "dependencies": { 60 | "@eslint/object-schema": "^2.1.5", 61 | "debug": "^4.3.1", 62 | "minimatch": "^3.1.2" 63 | }, 64 | "engines": { 65 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 66 | } 67 | }, 68 | "node_modules/@eslint/core": { 69 | "version": "0.10.0", 70 | "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", 71 | "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", 72 | "dev": true, 73 | "license": "Apache-2.0", 74 | "dependencies": { 75 | "@types/json-schema": "^7.0.15" 76 | }, 77 | "engines": { 78 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 79 | } 80 | }, 81 | "node_modules/@eslint/eslintrc": { 82 | "version": "3.2.0", 83 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", 84 | "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", 85 | "dev": true, 86 | "license": "MIT", 87 | "dependencies": { 88 | "ajv": "^6.12.4", 89 | "debug": "^4.3.2", 90 | "espree": "^10.0.1", 91 | "globals": "^14.0.0", 92 | "ignore": "^5.2.0", 93 | "import-fresh": "^3.2.1", 94 | "js-yaml": "^4.1.0", 95 | "minimatch": "^3.1.2", 96 | "strip-json-comments": "^3.1.1" 97 | }, 98 | "engines": { 99 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 100 | }, 101 | "funding": { 102 | "url": "https://opencollective.com/eslint" 103 | } 104 | }, 105 | "node_modules/@eslint/js": { 106 | "version": "9.19.0", 107 | "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", 108 | "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", 109 | "dev": true, 110 | "license": "MIT", 111 | "engines": { 112 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 113 | } 114 | }, 115 | "node_modules/@eslint/object-schema": { 116 | "version": "2.1.5", 117 | "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", 118 | "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", 119 | "dev": true, 120 | "license": "Apache-2.0", 121 | "engines": { 122 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 123 | } 124 | }, 125 | "node_modules/@eslint/plugin-kit": { 126 | "version": "0.2.5", 127 | "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", 128 | "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", 129 | "dev": true, 130 | "license": "Apache-2.0", 131 | "dependencies": { 132 | "@eslint/core": "^0.10.0", 133 | "levn": "^0.4.1" 134 | }, 135 | "engines": { 136 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 137 | } 138 | }, 139 | "node_modules/@humanfs/core": { 140 | "version": "0.19.1", 141 | "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 142 | "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 143 | "dev": true, 144 | "license": "Apache-2.0", 145 | "engines": { 146 | "node": ">=18.18.0" 147 | } 148 | }, 149 | "node_modules/@humanfs/node": { 150 | "version": "0.16.6", 151 | "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", 152 | "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", 153 | "dev": true, 154 | "license": "Apache-2.0", 155 | "dependencies": { 156 | "@humanfs/core": "^0.19.1", 157 | "@humanwhocodes/retry": "^0.3.0" 158 | }, 159 | "engines": { 160 | "node": ">=18.18.0" 161 | } 162 | }, 163 | "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { 164 | "version": "0.3.1", 165 | "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", 166 | "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", 167 | "dev": true, 168 | "license": "Apache-2.0", 169 | "engines": { 170 | "node": ">=18.18" 171 | }, 172 | "funding": { 173 | "type": "github", 174 | "url": "https://github.com/sponsors/nzakas" 175 | } 176 | }, 177 | "node_modules/@humanwhocodes/module-importer": { 178 | "version": "1.0.1", 179 | "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 180 | "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 181 | "dev": true, 182 | "engines": { 183 | "node": ">=12.22" 184 | }, 185 | "funding": { 186 | "type": "github", 187 | "url": "https://github.com/sponsors/nzakas" 188 | } 189 | }, 190 | "node_modules/@humanwhocodes/retry": { 191 | "version": "0.4.1", 192 | "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", 193 | "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", 194 | "dev": true, 195 | "license": "Apache-2.0", 196 | "engines": { 197 | "node": ">=18.18" 198 | }, 199 | "funding": { 200 | "type": "github", 201 | "url": "https://github.com/sponsors/nzakas" 202 | } 203 | }, 204 | "node_modules/@jridgewell/gen-mapping": { 205 | "version": "0.3.3", 206 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", 207 | "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", 208 | "dev": true, 209 | "dependencies": { 210 | "@jridgewell/set-array": "^1.0.1", 211 | "@jridgewell/sourcemap-codec": "^1.4.10", 212 | "@jridgewell/trace-mapping": "^0.3.9" 213 | }, 214 | "engines": { 215 | "node": ">=6.0.0" 216 | } 217 | }, 218 | "node_modules/@jridgewell/resolve-uri": { 219 | "version": "3.1.0", 220 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", 221 | "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", 222 | "dev": true, 223 | "engines": { 224 | "node": ">=6.0.0" 225 | } 226 | }, 227 | "node_modules/@jridgewell/set-array": { 228 | "version": "1.1.2", 229 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", 230 | "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", 231 | "dev": true, 232 | "engines": { 233 | "node": ">=6.0.0" 234 | } 235 | }, 236 | "node_modules/@jridgewell/source-map": { 237 | "version": "0.3.3", 238 | "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", 239 | "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", 240 | "dev": true, 241 | "dependencies": { 242 | "@jridgewell/gen-mapping": "^0.3.0", 243 | "@jridgewell/trace-mapping": "^0.3.9" 244 | } 245 | }, 246 | "node_modules/@jridgewell/sourcemap-codec": { 247 | "version": "1.4.15", 248 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 249 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", 250 | "dev": true 251 | }, 252 | "node_modules/@jridgewell/trace-mapping": { 253 | "version": "0.3.18", 254 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", 255 | "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", 256 | "dev": true, 257 | "dependencies": { 258 | "@jridgewell/resolve-uri": "3.1.0", 259 | "@jridgewell/sourcemap-codec": "1.4.14" 260 | } 261 | }, 262 | "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { 263 | "version": "1.4.14", 264 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", 265 | "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", 266 | "dev": true 267 | }, 268 | "node_modules/@rollup/rollup-android-arm-eabi": { 269 | "version": "4.32.0", 270 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz", 271 | "integrity": "sha512-G2fUQQANtBPsNwiVFg4zKiPQyjVKZCUdQUol53R8E71J7AsheRMV/Yv/nB8giOcOVqP7//eB5xPqieBYZe9bGg==", 272 | "cpu": [ 273 | "arm" 274 | ], 275 | "dev": true, 276 | "license": "MIT", 277 | "optional": true, 278 | "os": [ 279 | "android" 280 | ] 281 | }, 282 | "node_modules/@rollup/rollup-android-arm64": { 283 | "version": "4.32.0", 284 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.0.tgz", 285 | "integrity": "sha512-qhFwQ+ljoymC+j5lXRv8DlaJYY/+8vyvYmVx074zrLsu5ZGWYsJNLjPPVJJjhZQpyAKUGPydOq9hRLLNvh1s3A==", 286 | "cpu": [ 287 | "arm64" 288 | ], 289 | "dev": true, 290 | "license": "MIT", 291 | "optional": true, 292 | "os": [ 293 | "android" 294 | ] 295 | }, 296 | "node_modules/@rollup/rollup-darwin-arm64": { 297 | "version": "4.32.0", 298 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.0.tgz", 299 | "integrity": "sha512-44n/X3lAlWsEY6vF8CzgCx+LQaoqWGN7TzUfbJDiTIOjJm4+L2Yq+r5a8ytQRGyPqgJDs3Rgyo8eVL7n9iW6AQ==", 300 | "cpu": [ 301 | "arm64" 302 | ], 303 | "dev": true, 304 | "license": "MIT", 305 | "optional": true, 306 | "os": [ 307 | "darwin" 308 | ] 309 | }, 310 | "node_modules/@rollup/rollup-darwin-x64": { 311 | "version": "4.32.0", 312 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.0.tgz", 313 | "integrity": "sha512-F9ct0+ZX5Np6+ZDztxiGCIvlCaW87HBdHcozUfsHnj1WCUTBUubAoanhHUfnUHZABlElyRikI0mgcw/qdEm2VQ==", 314 | "cpu": [ 315 | "x64" 316 | ], 317 | "dev": true, 318 | "license": "MIT", 319 | "optional": true, 320 | "os": [ 321 | "darwin" 322 | ] 323 | }, 324 | "node_modules/@rollup/rollup-freebsd-arm64": { 325 | "version": "4.32.0", 326 | "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.0.tgz", 327 | "integrity": "sha512-JpsGxLBB2EFXBsTLHfkZDsXSpSmKD3VxXCgBQtlPcuAqB8TlqtLcbeMhxXQkCDv1avgwNjF8uEIbq5p+Cee0PA==", 328 | "cpu": [ 329 | "arm64" 330 | ], 331 | "dev": true, 332 | "license": "MIT", 333 | "optional": true, 334 | "os": [ 335 | "freebsd" 336 | ] 337 | }, 338 | "node_modules/@rollup/rollup-freebsd-x64": { 339 | "version": "4.32.0", 340 | "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.0.tgz", 341 | "integrity": "sha512-wegiyBT6rawdpvnD9lmbOpx5Sph+yVZKHbhnSP9MqUEDX08G4UzMU+D87jrazGE7lRSyTRs6NEYHtzfkJ3FjjQ==", 342 | "cpu": [ 343 | "x64" 344 | ], 345 | "dev": true, 346 | "license": "MIT", 347 | "optional": true, 348 | "os": [ 349 | "freebsd" 350 | ] 351 | }, 352 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 353 | "version": "4.32.0", 354 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.0.tgz", 355 | "integrity": "sha512-3pA7xecItbgOs1A5H58dDvOUEboG5UfpTq3WzAdF54acBbUM+olDJAPkgj1GRJ4ZqE12DZ9/hNS2QZk166v92A==", 356 | "cpu": [ 357 | "arm" 358 | ], 359 | "dev": true, 360 | "license": "MIT", 361 | "optional": true, 362 | "os": [ 363 | "linux" 364 | ] 365 | }, 366 | "node_modules/@rollup/rollup-linux-arm-musleabihf": { 367 | "version": "4.32.0", 368 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.0.tgz", 369 | "integrity": "sha512-Y7XUZEVISGyge51QbYyYAEHwpGgmRrAxQXO3siyYo2kmaj72USSG8LtlQQgAtlGfxYiOwu+2BdbPjzEpcOpRmQ==", 370 | "cpu": [ 371 | "arm" 372 | ], 373 | "dev": true, 374 | "license": "MIT", 375 | "optional": true, 376 | "os": [ 377 | "linux" 378 | ] 379 | }, 380 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 381 | "version": "4.32.0", 382 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.0.tgz", 383 | "integrity": "sha512-r7/OTF5MqeBrZo5omPXcTnjvv1GsrdH8a8RerARvDFiDwFpDVDnJyByYM/nX+mvks8XXsgPUxkwe/ltaX2VH7w==", 384 | "cpu": [ 385 | "arm64" 386 | ], 387 | "dev": true, 388 | "license": "MIT", 389 | "optional": true, 390 | "os": [ 391 | "linux" 392 | ] 393 | }, 394 | "node_modules/@rollup/rollup-linux-arm64-musl": { 395 | "version": "4.32.0", 396 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.0.tgz", 397 | "integrity": "sha512-HJbifC9vex9NqnlodV2BHVFNuzKL5OnsV2dvTw6e1dpZKkNjPG6WUq+nhEYV6Hv2Bv++BXkwcyoGlXnPrjAKXw==", 398 | "cpu": [ 399 | "arm64" 400 | ], 401 | "dev": true, 402 | "license": "MIT", 403 | "optional": true, 404 | "os": [ 405 | "linux" 406 | ] 407 | }, 408 | "node_modules/@rollup/rollup-linux-loongarch64-gnu": { 409 | "version": "4.32.0", 410 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.0.tgz", 411 | "integrity": "sha512-VAEzZTD63YglFlWwRj3taofmkV1V3xhebDXffon7msNz4b14xKsz7utO6F8F4cqt8K/ktTl9rm88yryvDpsfOw==", 412 | "cpu": [ 413 | "loong64" 414 | ], 415 | "dev": true, 416 | "license": "MIT", 417 | "optional": true, 418 | "os": [ 419 | "linux" 420 | ] 421 | }, 422 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 423 | "version": "4.32.0", 424 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.0.tgz", 425 | "integrity": "sha512-Sts5DST1jXAc9YH/iik1C9QRsLcCoOScf3dfbY5i4kH9RJpKxiTBXqm7qU5O6zTXBTEZry69bGszr3SMgYmMcQ==", 426 | "cpu": [ 427 | "ppc64" 428 | ], 429 | "dev": true, 430 | "license": "MIT", 431 | "optional": true, 432 | "os": [ 433 | "linux" 434 | ] 435 | }, 436 | "node_modules/@rollup/rollup-linux-riscv64-gnu": { 437 | "version": "4.32.0", 438 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.0.tgz", 439 | "integrity": "sha512-qhlXeV9AqxIyY9/R1h1hBD6eMvQCO34ZmdYvry/K+/MBs6d1nRFLm6BOiITLVI+nFAAB9kUB6sdJRKyVHXnqZw==", 440 | "cpu": [ 441 | "riscv64" 442 | ], 443 | "dev": true, 444 | "license": "MIT", 445 | "optional": true, 446 | "os": [ 447 | "linux" 448 | ] 449 | }, 450 | "node_modules/@rollup/rollup-linux-s390x-gnu": { 451 | "version": "4.32.0", 452 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.0.tgz", 453 | "integrity": "sha512-8ZGN7ExnV0qjXa155Rsfi6H8M4iBBwNLBM9lcVS+4NcSzOFaNqmt7djlox8pN1lWrRPMRRQ8NeDlozIGx3Omsw==", 454 | "cpu": [ 455 | "s390x" 456 | ], 457 | "dev": true, 458 | "license": "MIT", 459 | "optional": true, 460 | "os": [ 461 | "linux" 462 | ] 463 | }, 464 | "node_modules/@rollup/rollup-linux-x64-gnu": { 465 | "version": "4.32.0", 466 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.0.tgz", 467 | "integrity": "sha512-VDzNHtLLI5s7xd/VubyS10mq6TxvZBp+4NRWoW+Hi3tgV05RtVm4qK99+dClwTN1McA6PHwob6DEJ6PlXbY83A==", 468 | "cpu": [ 469 | "x64" 470 | ], 471 | "dev": true, 472 | "license": "MIT", 473 | "optional": true, 474 | "os": [ 475 | "linux" 476 | ] 477 | }, 478 | "node_modules/@rollup/rollup-linux-x64-musl": { 479 | "version": "4.32.0", 480 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.0.tgz", 481 | "integrity": "sha512-qcb9qYDlkxz9DxJo7SDhWxTWV1gFuwznjbTiov289pASxlfGbaOD54mgbs9+z94VwrXtKTu+2RqwlSTbiOqxGg==", 482 | "cpu": [ 483 | "x64" 484 | ], 485 | "dev": true, 486 | "license": "MIT", 487 | "optional": true, 488 | "os": [ 489 | "linux" 490 | ] 491 | }, 492 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 493 | "version": "4.32.0", 494 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.0.tgz", 495 | "integrity": "sha512-pFDdotFDMXW2AXVbfdUEfidPAk/OtwE/Hd4eYMTNVVaCQ6Yl8et0meDaKNL63L44Haxv4UExpv9ydSf3aSayDg==", 496 | "cpu": [ 497 | "arm64" 498 | ], 499 | "dev": true, 500 | "license": "MIT", 501 | "optional": true, 502 | "os": [ 503 | "win32" 504 | ] 505 | }, 506 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 507 | "version": "4.32.0", 508 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.0.tgz", 509 | "integrity": "sha512-/TG7WfrCAjeRNDvI4+0AAMoHxea/USWhAzf9PVDFHbcqrQ7hMMKp4jZIy4VEjk72AAfN5k4TiSMRXRKf/0akSw==", 510 | "cpu": [ 511 | "ia32" 512 | ], 513 | "dev": true, 514 | "license": "MIT", 515 | "optional": true, 516 | "os": [ 517 | "win32" 518 | ] 519 | }, 520 | "node_modules/@rollup/rollup-win32-x64-msvc": { 521 | "version": "4.32.0", 522 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.0.tgz", 523 | "integrity": "sha512-5hqO5S3PTEO2E5VjCePxv40gIgyS2KvO7E7/vvC/NbIW4SIRamkMr1hqj+5Y67fbBWv/bQLB6KelBQmXlyCjWA==", 524 | "cpu": [ 525 | "x64" 526 | ], 527 | "dev": true, 528 | "license": "MIT", 529 | "optional": true, 530 | "os": [ 531 | "win32" 532 | ] 533 | }, 534 | "node_modules/@types/estree": { 535 | "version": "1.0.6", 536 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", 537 | "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", 538 | "dev": true, 539 | "license": "MIT" 540 | }, 541 | "node_modules/@types/json-schema": { 542 | "version": "7.0.15", 543 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 544 | "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 545 | "dev": true, 546 | "license": "MIT" 547 | }, 548 | "node_modules/acorn": { 549 | "version": "8.14.0", 550 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 551 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 552 | "dev": true, 553 | "license": "MIT", 554 | "bin": { 555 | "acorn": "bin/acorn" 556 | }, 557 | "engines": { 558 | "node": ">=0.4.0" 559 | } 560 | }, 561 | "node_modules/acorn-jsx": { 562 | "version": "5.3.2", 563 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 564 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 565 | "dev": true, 566 | "license": "MIT", 567 | "peerDependencies": { 568 | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 569 | } 570 | }, 571 | "node_modules/ajv": { 572 | "version": "6.12.6", 573 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 574 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 575 | "dev": true, 576 | "license": "MIT", 577 | "dependencies": { 578 | "fast-deep-equal": "^3.1.1", 579 | "fast-json-stable-stringify": "^2.0.0", 580 | "json-schema-traverse": "^0.4.1", 581 | "uri-js": "^4.2.2" 582 | }, 583 | "funding": { 584 | "type": "github", 585 | "url": "https://github.com/sponsors/epoberezkin" 586 | } 587 | }, 588 | "node_modules/ansi-styles": { 589 | "version": "4.3.0", 590 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 591 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 592 | "dev": true, 593 | "dependencies": { 594 | "color-convert": "^2.0.1" 595 | }, 596 | "engines": { 597 | "node": ">=8" 598 | }, 599 | "funding": { 600 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 601 | } 602 | }, 603 | "node_modules/argparse": { 604 | "version": "2.0.1", 605 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 606 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 607 | "dev": true, 608 | "license": "Python-2.0" 609 | }, 610 | "node_modules/balanced-match": { 611 | "version": "1.0.2", 612 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 613 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 614 | "dev": true, 615 | "license": "MIT" 616 | }, 617 | "node_modules/brace-expansion": { 618 | "version": "1.1.11", 619 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 620 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 621 | "dev": true, 622 | "license": "MIT", 623 | "dependencies": { 624 | "balanced-match": "^1.0.0", 625 | "concat-map": "0.0.1" 626 | } 627 | }, 628 | "node_modules/buffer-from": { 629 | "version": "1.1.2", 630 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 631 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 632 | "dev": true 633 | }, 634 | "node_modules/callsites": { 635 | "version": "3.1.0", 636 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 637 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 638 | "dev": true, 639 | "license": "MIT", 640 | "engines": { 641 | "node": ">=6" 642 | } 643 | }, 644 | "node_modules/chalk": { 645 | "version": "4.1.2", 646 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 647 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 648 | "dev": true, 649 | "dependencies": { 650 | "ansi-styles": "^4.1.0", 651 | "supports-color": "^7.1.0" 652 | }, 653 | "engines": { 654 | "node": ">=10" 655 | }, 656 | "funding": { 657 | "url": "https://github.com/chalk/chalk?sponsor=1" 658 | } 659 | }, 660 | "node_modules/color-convert": { 661 | "version": "2.0.1", 662 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 663 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 664 | "dev": true, 665 | "dependencies": { 666 | "color-name": "~1.1.4" 667 | }, 668 | "engines": { 669 | "node": ">=7.0.0" 670 | } 671 | }, 672 | "node_modules/color-name": { 673 | "version": "1.1.4", 674 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 675 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 676 | "dev": true 677 | }, 678 | "node_modules/commander": { 679 | "version": "2.20.3", 680 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 681 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 682 | "dev": true 683 | }, 684 | "node_modules/concat-map": { 685 | "version": "0.0.1", 686 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 687 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 688 | "dev": true, 689 | "license": "MIT" 690 | }, 691 | "node_modules/cross-spawn": { 692 | "version": "7.0.6", 693 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 694 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 695 | "dev": true, 696 | "license": "MIT", 697 | "dependencies": { 698 | "path-key": "^3.1.0", 699 | "shebang-command": "^2.0.0", 700 | "which": "^2.0.1" 701 | }, 702 | "engines": { 703 | "node": ">= 8" 704 | } 705 | }, 706 | "node_modules/debug": { 707 | "version": "4.4.0", 708 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 709 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 710 | "dev": true, 711 | "license": "MIT", 712 | "dependencies": { 713 | "ms": "^2.1.3" 714 | }, 715 | "engines": { 716 | "node": ">=6.0" 717 | }, 718 | "peerDependenciesMeta": { 719 | "supports-color": { 720 | "optional": true 721 | } 722 | } 723 | }, 724 | "node_modules/deep-is": { 725 | "version": "0.1.4", 726 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 727 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 728 | "dev": true 729 | }, 730 | "node_modules/escape-string-regexp": { 731 | "version": "4.0.0", 732 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 733 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 734 | "dev": true, 735 | "engines": { 736 | "node": ">=10" 737 | }, 738 | "funding": { 739 | "url": "https://github.com/sponsors/sindresorhus" 740 | } 741 | }, 742 | "node_modules/eslint": { 743 | "version": "9.19.0", 744 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", 745 | "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", 746 | "dev": true, 747 | "license": "MIT", 748 | "dependencies": { 749 | "@eslint-community/eslint-utils": "^4.2.0", 750 | "@eslint-community/regexpp": "^4.12.1", 751 | "@eslint/config-array": "^0.19.0", 752 | "@eslint/core": "^0.10.0", 753 | "@eslint/eslintrc": "^3.2.0", 754 | "@eslint/js": "9.19.0", 755 | "@eslint/plugin-kit": "^0.2.5", 756 | "@humanfs/node": "^0.16.6", 757 | "@humanwhocodes/module-importer": "^1.0.1", 758 | "@humanwhocodes/retry": "^0.4.1", 759 | "@types/estree": "^1.0.6", 760 | "@types/json-schema": "^7.0.15", 761 | "ajv": "^6.12.4", 762 | "chalk": "^4.0.0", 763 | "cross-spawn": "^7.0.6", 764 | "debug": "^4.3.2", 765 | "escape-string-regexp": "^4.0.0", 766 | "eslint-scope": "^8.2.0", 767 | "eslint-visitor-keys": "^4.2.0", 768 | "espree": "^10.3.0", 769 | "esquery": "^1.5.0", 770 | "esutils": "^2.0.2", 771 | "fast-deep-equal": "^3.1.3", 772 | "file-entry-cache": "^8.0.0", 773 | "find-up": "^5.0.0", 774 | "glob-parent": "^6.0.2", 775 | "ignore": "^5.2.0", 776 | "imurmurhash": "^0.1.4", 777 | "is-glob": "^4.0.0", 778 | "json-stable-stringify-without-jsonify": "^1.0.1", 779 | "lodash.merge": "^4.6.2", 780 | "minimatch": "^3.1.2", 781 | "natural-compare": "^1.4.0", 782 | "optionator": "^0.9.3" 783 | }, 784 | "bin": { 785 | "eslint": "bin/eslint.js" 786 | }, 787 | "engines": { 788 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 789 | }, 790 | "funding": { 791 | "url": "https://eslint.org/donate" 792 | }, 793 | "peerDependencies": { 794 | "jiti": "*" 795 | }, 796 | "peerDependenciesMeta": { 797 | "jiti": { 798 | "optional": true 799 | } 800 | } 801 | }, 802 | "node_modules/eslint-scope": { 803 | "version": "8.2.0", 804 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", 805 | "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", 806 | "dev": true, 807 | "license": "BSD-2-Clause", 808 | "dependencies": { 809 | "esrecurse": "^4.3.0", 810 | "estraverse": "^5.2.0" 811 | }, 812 | "engines": { 813 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 814 | }, 815 | "funding": { 816 | "url": "https://opencollective.com/eslint" 817 | } 818 | }, 819 | "node_modules/eslint-visitor-keys": { 820 | "version": "3.4.3", 821 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 822 | "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 823 | "dev": true, 824 | "engines": { 825 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 826 | }, 827 | "funding": { 828 | "url": "https://opencollective.com/eslint" 829 | } 830 | }, 831 | "node_modules/eslint/node_modules/eslint-visitor-keys": { 832 | "version": "4.2.0", 833 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", 834 | "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", 835 | "dev": true, 836 | "license": "Apache-2.0", 837 | "engines": { 838 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 839 | }, 840 | "funding": { 841 | "url": "https://opencollective.com/eslint" 842 | } 843 | }, 844 | "node_modules/espree": { 845 | "version": "10.3.0", 846 | "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", 847 | "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", 848 | "dev": true, 849 | "license": "BSD-2-Clause", 850 | "dependencies": { 851 | "acorn": "^8.14.0", 852 | "acorn-jsx": "^5.3.2", 853 | "eslint-visitor-keys": "^4.2.0" 854 | }, 855 | "engines": { 856 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 857 | }, 858 | "funding": { 859 | "url": "https://opencollective.com/eslint" 860 | } 861 | }, 862 | "node_modules/espree/node_modules/eslint-visitor-keys": { 863 | "version": "4.2.0", 864 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", 865 | "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", 866 | "dev": true, 867 | "license": "Apache-2.0", 868 | "engines": { 869 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 870 | }, 871 | "funding": { 872 | "url": "https://opencollective.com/eslint" 873 | } 874 | }, 875 | "node_modules/esquery": { 876 | "version": "1.5.0", 877 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", 878 | "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", 879 | "dev": true, 880 | "dependencies": { 881 | "estraverse": "^5.1.0" 882 | }, 883 | "engines": { 884 | "node": ">=0.10" 885 | } 886 | }, 887 | "node_modules/esrecurse": { 888 | "version": "4.3.0", 889 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 890 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 891 | "dev": true, 892 | "license": "BSD-2-Clause", 893 | "dependencies": { 894 | "estraverse": "^5.2.0" 895 | }, 896 | "engines": { 897 | "node": ">=4.0" 898 | } 899 | }, 900 | "node_modules/estraverse": { 901 | "version": "5.3.0", 902 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 903 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 904 | "dev": true, 905 | "engines": { 906 | "node": ">=4.0" 907 | } 908 | }, 909 | "node_modules/esutils": { 910 | "version": "2.0.3", 911 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 912 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 913 | "dev": true, 914 | "engines": { 915 | "node": ">=0.10.0" 916 | } 917 | }, 918 | "node_modules/fast-deep-equal": { 919 | "version": "3.1.3", 920 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 921 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 922 | "dev": true, 923 | "license": "MIT" 924 | }, 925 | "node_modules/fast-json-stable-stringify": { 926 | "version": "2.1.0", 927 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 928 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 929 | "dev": true, 930 | "license": "MIT" 931 | }, 932 | "node_modules/fast-levenshtein": { 933 | "version": "2.0.6", 934 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 935 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 936 | "dev": true 937 | }, 938 | "node_modules/file-entry-cache": { 939 | "version": "8.0.0", 940 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 941 | "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 942 | "dev": true, 943 | "dependencies": { 944 | "flat-cache": "^4.0.0" 945 | }, 946 | "engines": { 947 | "node": ">=16.0.0" 948 | } 949 | }, 950 | "node_modules/find-up": { 951 | "version": "5.0.0", 952 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 953 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 954 | "dev": true, 955 | "dependencies": { 956 | "locate-path": "^6.0.0", 957 | "path-exists": "^4.0.0" 958 | }, 959 | "engines": { 960 | "node": ">=10" 961 | }, 962 | "funding": { 963 | "url": "https://github.com/sponsors/sindresorhus" 964 | } 965 | }, 966 | "node_modules/flat-cache": { 967 | "version": "4.0.1", 968 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 969 | "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 970 | "dev": true, 971 | "dependencies": { 972 | "flatted": "^3.2.9", 973 | "keyv": "^4.5.4" 974 | }, 975 | "engines": { 976 | "node": ">=16" 977 | } 978 | }, 979 | "node_modules/flatted": { 980 | "version": "3.3.1", 981 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", 982 | "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", 983 | "dev": true 984 | }, 985 | "node_modules/fsevents": { 986 | "version": "2.3.2", 987 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 988 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 989 | "dev": true, 990 | "hasInstallScript": true, 991 | "optional": true, 992 | "os": [ 993 | "darwin" 994 | ], 995 | "engines": { 996 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 997 | } 998 | }, 999 | "node_modules/glob-parent": { 1000 | "version": "6.0.2", 1001 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 1002 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 1003 | "dev": true, 1004 | "dependencies": { 1005 | "is-glob": "^4.0.3" 1006 | }, 1007 | "engines": { 1008 | "node": ">=10.13.0" 1009 | } 1010 | }, 1011 | "node_modules/globals": { 1012 | "version": "14.0.0", 1013 | "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 1014 | "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 1015 | "dev": true, 1016 | "license": "MIT", 1017 | "engines": { 1018 | "node": ">=18" 1019 | }, 1020 | "funding": { 1021 | "url": "https://github.com/sponsors/sindresorhus" 1022 | } 1023 | }, 1024 | "node_modules/has-flag": { 1025 | "version": "4.0.0", 1026 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1027 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1028 | "dev": true, 1029 | "engines": { 1030 | "node": ">=8" 1031 | } 1032 | }, 1033 | "node_modules/ignore": { 1034 | "version": "5.3.2", 1035 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 1036 | "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 1037 | "dev": true, 1038 | "license": "MIT", 1039 | "engines": { 1040 | "node": ">= 4" 1041 | } 1042 | }, 1043 | "node_modules/import-fresh": { 1044 | "version": "3.3.0", 1045 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 1046 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 1047 | "dev": true, 1048 | "license": "MIT", 1049 | "dependencies": { 1050 | "parent-module": "^1.0.0", 1051 | "resolve-from": "^4.0.0" 1052 | }, 1053 | "engines": { 1054 | "node": ">=6" 1055 | }, 1056 | "funding": { 1057 | "url": "https://github.com/sponsors/sindresorhus" 1058 | } 1059 | }, 1060 | "node_modules/imurmurhash": { 1061 | "version": "0.1.4", 1062 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 1063 | "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 1064 | "dev": true, 1065 | "engines": { 1066 | "node": ">=0.8.19" 1067 | } 1068 | }, 1069 | "node_modules/is-extglob": { 1070 | "version": "2.1.1", 1071 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1072 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1073 | "dev": true, 1074 | "engines": { 1075 | "node": ">=0.10.0" 1076 | } 1077 | }, 1078 | "node_modules/is-glob": { 1079 | "version": "4.0.3", 1080 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1081 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1082 | "dev": true, 1083 | "dependencies": { 1084 | "is-extglob": "^2.1.1" 1085 | }, 1086 | "engines": { 1087 | "node": ">=0.10.0" 1088 | } 1089 | }, 1090 | "node_modules/isexe": { 1091 | "version": "2.0.0", 1092 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1093 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1094 | "dev": true, 1095 | "license": "ISC" 1096 | }, 1097 | "node_modules/js-yaml": { 1098 | "version": "4.1.0", 1099 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 1100 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 1101 | "dev": true, 1102 | "license": "MIT", 1103 | "dependencies": { 1104 | "argparse": "^2.0.1" 1105 | }, 1106 | "bin": { 1107 | "js-yaml": "bin/js-yaml.js" 1108 | } 1109 | }, 1110 | "node_modules/json-buffer": { 1111 | "version": "3.0.1", 1112 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 1113 | "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 1114 | "dev": true 1115 | }, 1116 | "node_modules/json-schema-traverse": { 1117 | "version": "0.4.1", 1118 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1119 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 1120 | "dev": true, 1121 | "license": "MIT" 1122 | }, 1123 | "node_modules/json-stable-stringify-without-jsonify": { 1124 | "version": "1.0.1", 1125 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 1126 | "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 1127 | "dev": true 1128 | }, 1129 | "node_modules/keyv": { 1130 | "version": "4.5.4", 1131 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 1132 | "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 1133 | "dev": true, 1134 | "dependencies": { 1135 | "json-buffer": "3.0.1" 1136 | } 1137 | }, 1138 | "node_modules/levn": { 1139 | "version": "0.4.1", 1140 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 1141 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 1142 | "dev": true, 1143 | "dependencies": { 1144 | "prelude-ls": "^1.2.1", 1145 | "type-check": "~0.4.0" 1146 | }, 1147 | "engines": { 1148 | "node": ">= 0.8.0" 1149 | } 1150 | }, 1151 | "node_modules/locate-path": { 1152 | "version": "6.0.0", 1153 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 1154 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 1155 | "dev": true, 1156 | "dependencies": { 1157 | "p-locate": "^5.0.0" 1158 | }, 1159 | "engines": { 1160 | "node": ">=10" 1161 | }, 1162 | "funding": { 1163 | "url": "https://github.com/sponsors/sindresorhus" 1164 | } 1165 | }, 1166 | "node_modules/lodash.merge": { 1167 | "version": "4.6.2", 1168 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 1169 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 1170 | "dev": true 1171 | }, 1172 | "node_modules/minimatch": { 1173 | "version": "3.1.2", 1174 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1175 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1176 | "dev": true, 1177 | "license": "ISC", 1178 | "dependencies": { 1179 | "brace-expansion": "^1.1.7" 1180 | }, 1181 | "engines": { 1182 | "node": "*" 1183 | } 1184 | }, 1185 | "node_modules/ms": { 1186 | "version": "2.1.3", 1187 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1188 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1189 | "dev": true, 1190 | "license": "MIT" 1191 | }, 1192 | "node_modules/natural-compare": { 1193 | "version": "1.4.0", 1194 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 1195 | "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 1196 | "dev": true 1197 | }, 1198 | "node_modules/optionator": { 1199 | "version": "0.9.3", 1200 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", 1201 | "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", 1202 | "dev": true, 1203 | "dependencies": { 1204 | "@aashutoshrathi/word-wrap": "^1.2.3", 1205 | "deep-is": "^0.1.3", 1206 | "fast-levenshtein": "^2.0.6", 1207 | "levn": "^0.4.1", 1208 | "prelude-ls": "^1.2.1", 1209 | "type-check": "^0.4.0" 1210 | }, 1211 | "engines": { 1212 | "node": ">= 0.8.0" 1213 | } 1214 | }, 1215 | "node_modules/p-limit": { 1216 | "version": "3.1.0", 1217 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 1218 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 1219 | "dev": true, 1220 | "dependencies": { 1221 | "yocto-queue": "^0.1.0" 1222 | }, 1223 | "engines": { 1224 | "node": ">=10" 1225 | }, 1226 | "funding": { 1227 | "url": "https://github.com/sponsors/sindresorhus" 1228 | } 1229 | }, 1230 | "node_modules/p-locate": { 1231 | "version": "5.0.0", 1232 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 1233 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 1234 | "dev": true, 1235 | "dependencies": { 1236 | "p-limit": "^3.0.2" 1237 | }, 1238 | "engines": { 1239 | "node": ">=10" 1240 | }, 1241 | "funding": { 1242 | "url": "https://github.com/sponsors/sindresorhus" 1243 | } 1244 | }, 1245 | "node_modules/parent-module": { 1246 | "version": "1.0.1", 1247 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 1248 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 1249 | "dev": true, 1250 | "license": "MIT", 1251 | "dependencies": { 1252 | "callsites": "^3.0.0" 1253 | }, 1254 | "engines": { 1255 | "node": ">=6" 1256 | } 1257 | }, 1258 | "node_modules/path-exists": { 1259 | "version": "4.0.0", 1260 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1261 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1262 | "dev": true, 1263 | "engines": { 1264 | "node": ">=8" 1265 | } 1266 | }, 1267 | "node_modules/path-key": { 1268 | "version": "3.1.1", 1269 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1270 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1271 | "dev": true, 1272 | "license": "MIT", 1273 | "engines": { 1274 | "node": ">=8" 1275 | } 1276 | }, 1277 | "node_modules/prelude-ls": { 1278 | "version": "1.2.1", 1279 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 1280 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 1281 | "dev": true, 1282 | "engines": { 1283 | "node": ">= 0.8.0" 1284 | } 1285 | }, 1286 | "node_modules/prettier": { 1287 | "version": "3.4.2", 1288 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", 1289 | "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", 1290 | "dev": true, 1291 | "license": "MIT", 1292 | "bin": { 1293 | "prettier": "bin/prettier.cjs" 1294 | }, 1295 | "engines": { 1296 | "node": ">=14" 1297 | }, 1298 | "funding": { 1299 | "url": "https://github.com/prettier/prettier?sponsor=1" 1300 | } 1301 | }, 1302 | "node_modules/punycode": { 1303 | "version": "2.3.1", 1304 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 1305 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 1306 | "dev": true, 1307 | "license": "MIT", 1308 | "engines": { 1309 | "node": ">=6" 1310 | } 1311 | }, 1312 | "node_modules/resolve-from": { 1313 | "version": "4.0.0", 1314 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 1315 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 1316 | "dev": true, 1317 | "license": "MIT", 1318 | "engines": { 1319 | "node": ">=4" 1320 | } 1321 | }, 1322 | "node_modules/rollup": { 1323 | "version": "4.32.0", 1324 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.0.tgz", 1325 | "integrity": "sha512-JmrhfQR31Q4AuNBjjAX4s+a/Pu/Q8Q9iwjWBsjRH1q52SPFE2NqRMK6fUZKKnvKO6id+h7JIRf0oYsph53eATg==", 1326 | "dev": true, 1327 | "license": "MIT", 1328 | "dependencies": { 1329 | "@types/estree": "1.0.6" 1330 | }, 1331 | "bin": { 1332 | "rollup": "dist/bin/rollup" 1333 | }, 1334 | "engines": { 1335 | "node": ">=18.0.0", 1336 | "npm": ">=8.0.0" 1337 | }, 1338 | "optionalDependencies": { 1339 | "@rollup/rollup-android-arm-eabi": "4.32.0", 1340 | "@rollup/rollup-android-arm64": "4.32.0", 1341 | "@rollup/rollup-darwin-arm64": "4.32.0", 1342 | "@rollup/rollup-darwin-x64": "4.32.0", 1343 | "@rollup/rollup-freebsd-arm64": "4.32.0", 1344 | "@rollup/rollup-freebsd-x64": "4.32.0", 1345 | "@rollup/rollup-linux-arm-gnueabihf": "4.32.0", 1346 | "@rollup/rollup-linux-arm-musleabihf": "4.32.0", 1347 | "@rollup/rollup-linux-arm64-gnu": "4.32.0", 1348 | "@rollup/rollup-linux-arm64-musl": "4.32.0", 1349 | "@rollup/rollup-linux-loongarch64-gnu": "4.32.0", 1350 | "@rollup/rollup-linux-powerpc64le-gnu": "4.32.0", 1351 | "@rollup/rollup-linux-riscv64-gnu": "4.32.0", 1352 | "@rollup/rollup-linux-s390x-gnu": "4.32.0", 1353 | "@rollup/rollup-linux-x64-gnu": "4.32.0", 1354 | "@rollup/rollup-linux-x64-musl": "4.32.0", 1355 | "@rollup/rollup-win32-arm64-msvc": "4.32.0", 1356 | "@rollup/rollup-win32-ia32-msvc": "4.32.0", 1357 | "@rollup/rollup-win32-x64-msvc": "4.32.0", 1358 | "fsevents": "~2.3.2" 1359 | } 1360 | }, 1361 | "node_modules/shebang-command": { 1362 | "version": "2.0.0", 1363 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1364 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1365 | "dev": true, 1366 | "license": "MIT", 1367 | "dependencies": { 1368 | "shebang-regex": "^3.0.0" 1369 | }, 1370 | "engines": { 1371 | "node": ">=8" 1372 | } 1373 | }, 1374 | "node_modules/shebang-regex": { 1375 | "version": "3.0.0", 1376 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1377 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1378 | "dev": true, 1379 | "license": "MIT", 1380 | "engines": { 1381 | "node": ">=8" 1382 | } 1383 | }, 1384 | "node_modules/source-map": { 1385 | "version": "0.6.1", 1386 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1387 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1388 | "dev": true, 1389 | "engines": { 1390 | "node": ">=0.10.0" 1391 | } 1392 | }, 1393 | "node_modules/source-map-support": { 1394 | "version": "0.5.21", 1395 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 1396 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 1397 | "dev": true, 1398 | "dependencies": { 1399 | "buffer-from": "^1.0.0", 1400 | "source-map": "^0.6.0" 1401 | } 1402 | }, 1403 | "node_modules/strip-json-comments": { 1404 | "version": "3.1.1", 1405 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1406 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1407 | "dev": true, 1408 | "license": "MIT", 1409 | "engines": { 1410 | "node": ">=8" 1411 | }, 1412 | "funding": { 1413 | "url": "https://github.com/sponsors/sindresorhus" 1414 | } 1415 | }, 1416 | "node_modules/supports-color": { 1417 | "version": "7.2.0", 1418 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1419 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1420 | "dev": true, 1421 | "dependencies": { 1422 | "has-flag": "^4.0.0" 1423 | }, 1424 | "engines": { 1425 | "node": ">=8" 1426 | } 1427 | }, 1428 | "node_modules/terser": { 1429 | "version": "5.37.0", 1430 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", 1431 | "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", 1432 | "dev": true, 1433 | "license": "BSD-2-Clause", 1434 | "dependencies": { 1435 | "@jridgewell/source-map": "^0.3.3", 1436 | "acorn": "^8.8.2", 1437 | "commander": "^2.20.0", 1438 | "source-map-support": "~0.5.20" 1439 | }, 1440 | "bin": { 1441 | "terser": "bin/terser" 1442 | }, 1443 | "engines": { 1444 | "node": ">=10" 1445 | } 1446 | }, 1447 | "node_modules/type-check": { 1448 | "version": "0.4.0", 1449 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 1450 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 1451 | "dev": true, 1452 | "dependencies": { 1453 | "prelude-ls": "^1.2.1" 1454 | }, 1455 | "engines": { 1456 | "node": ">= 0.8.0" 1457 | } 1458 | }, 1459 | "node_modules/uri-js": { 1460 | "version": "4.4.1", 1461 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1462 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1463 | "dev": true, 1464 | "license": "BSD-2-Clause", 1465 | "dependencies": { 1466 | "punycode": "^2.1.0" 1467 | } 1468 | }, 1469 | "node_modules/which": { 1470 | "version": "2.0.2", 1471 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1472 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1473 | "dev": true, 1474 | "license": "ISC", 1475 | "dependencies": { 1476 | "isexe": "^2.0.0" 1477 | }, 1478 | "bin": { 1479 | "node-which": "bin/node-which" 1480 | }, 1481 | "engines": { 1482 | "node": ">= 8" 1483 | } 1484 | }, 1485 | "node_modules/yocto-queue": { 1486 | "version": "0.1.0", 1487 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1488 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1489 | "dev": true, 1490 | "engines": { 1491 | "node": ">=10" 1492 | }, 1493 | "funding": { 1494 | "url": "https://github.com/sponsors/sindresorhus" 1495 | } 1496 | } 1497 | } 1498 | } 1499 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-id3-writer", 3 | "version": "6.2.0", 4 | "description": "JavaScript library for writing ID3 tag to MP3 files in browsers and Node.js", 5 | "main": "dist/browser-id3-writer.mjs", 6 | "types": "./browser-id3-writer.d.ts", 7 | "scripts": { 8 | "lint": "eslint src tools test", 9 | "build": "npm run build:bundle && npm run build:compress && node tools/distSize.mjs", 10 | "build:bundle": "rollup -i src/ID3Writer.mjs -o dist/browser-id3-writer.mjs", 11 | "build:compress": "terser dist/browser-id3-writer.mjs -o dist/browser-id3-writer.mjs -m -c --module", 12 | "test": "npm run prettier:check && npm run lint && npm run build && npm run mocha", 13 | "mocha": "node --test --test-reporter spec", 14 | "preversion": "npm test", 15 | "version": "git add package.json package-lock.json", 16 | "postversion": "git push && git push --tags && npm publish", 17 | "prettier:write": "prettier --write .", 18 | "prettier:check": "prettier --check ." 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/egoroof/browser-id3-writer.git" 23 | }, 24 | "keywords": [ 25 | "browser", 26 | "nodejs", 27 | "writer", 28 | "id3", 29 | "mp3", 30 | "audio", 31 | "tag", 32 | "library" 33 | ], 34 | "author": "egoroof", 35 | "files": [ 36 | "LICENSE.md", 37 | "README.md", 38 | "browser-id3-writer.d.ts", 39 | "dist/browser-id3-writer.mjs" 40 | ], 41 | "license": "MIT", 42 | "devDependencies": { 43 | "@eslint/js": "^9.19.0", 44 | "eslint": "^9.19.0", 45 | "prettier": "^3.4.2", 46 | "rollup": "^4.32.0", 47 | "terser": "^5.37.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ID3Writer.mjs: -------------------------------------------------------------------------------- 1 | import { encodeWindows1252, encodeUtf16le } from './encoder.mjs'; 2 | import { getMimeType, isId3v2 } from './signatures.mjs'; 3 | import { 4 | uint7ArrayToUint28, 5 | uint28ToUint7Array, 6 | uint32ToUint8Array, 7 | } from './transform.mjs'; 8 | import { 9 | getNumericFrameSize, 10 | getStringFrameSize, 11 | getPictureFrameSize, 12 | getLyricsFrameSize, 13 | getCommentFrameSize, 14 | getUserStringFrameSize, 15 | getUrlLinkFrameSize, 16 | getPrivateFrameSize, 17 | getPairedTextFrameSize, 18 | getSynchronisedLyricsFrameSize, 19 | } from './sizes.mjs'; 20 | 21 | export class ID3Writer { 22 | _setIntegerFrame(name, value) { 23 | const integer = parseInt(value, 10); 24 | 25 | this.frames.push({ 26 | name, 27 | value: integer, 28 | size: getNumericFrameSize(integer.toString().length), 29 | }); 30 | } 31 | 32 | _setStringFrame(name, value) { 33 | const stringValue = value.toString(); 34 | let size = getStringFrameSize(stringValue.length); 35 | 36 | if (name === 'TDAT') { 37 | size = getNumericFrameSize(stringValue.length); 38 | } 39 | 40 | this.frames.push({ 41 | name, 42 | value: stringValue, 43 | size, 44 | }); 45 | } 46 | 47 | _setPictureFrame(pictureType, data, description, useUnicodeEncoding) { 48 | const mimeType = getMimeType(new Uint8Array(data)); 49 | const descriptionString = description.toString(); 50 | 51 | if (!mimeType) { 52 | throw new Error('Unknown picture MIME type'); 53 | } 54 | if (!description) { 55 | useUnicodeEncoding = false; 56 | } 57 | this.frames.push({ 58 | name: 'APIC', 59 | value: data, 60 | pictureType, 61 | mimeType, 62 | useUnicodeEncoding, 63 | description: descriptionString, 64 | size: getPictureFrameSize( 65 | data.byteLength, 66 | mimeType.length, 67 | descriptionString.length, 68 | useUnicodeEncoding, 69 | ), 70 | }); 71 | } 72 | 73 | _setLyricsFrame(language, description, lyrics) { 74 | const languageCode = language.split('').map((c) => c.charCodeAt(0)); 75 | const descriptionString = description.toString(); 76 | const lyricsString = lyrics.toString(); 77 | 78 | this.frames.push({ 79 | name: 'USLT', 80 | value: lyricsString, 81 | language: languageCode, 82 | description: descriptionString, 83 | size: getLyricsFrameSize(descriptionString.length, lyricsString.length), 84 | }); 85 | } 86 | 87 | _setCommentFrame(language, description, text) { 88 | const languageCode = language.split('').map((c) => c.charCodeAt(0)); 89 | const descriptionString = description.toString(); 90 | const textString = text.toString(); 91 | 92 | this.frames.push({ 93 | name: 'COMM', 94 | value: textString, 95 | language: languageCode, 96 | description: descriptionString, 97 | size: getCommentFrameSize(descriptionString.length, textString.length), 98 | }); 99 | } 100 | 101 | _setPrivateFrame(id, data) { 102 | const identifier = id.toString(); 103 | 104 | this.frames.push({ 105 | name: 'PRIV', 106 | value: data, 107 | id: identifier, 108 | size: getPrivateFrameSize(identifier.length, data.byteLength), 109 | }); 110 | } 111 | 112 | _setUserStringFrame(description, value) { 113 | const descriptionString = description.toString(); 114 | const valueString = value.toString(); 115 | 116 | this.frames.push({ 117 | name: 'TXXX', 118 | description: descriptionString, 119 | value: valueString, 120 | size: getUserStringFrameSize( 121 | descriptionString.length, 122 | valueString.length, 123 | ), 124 | }); 125 | } 126 | 127 | _setUrlLinkFrame(name, url) { 128 | const urlString = url.toString(); 129 | 130 | this.frames.push({ 131 | name, 132 | value: urlString, 133 | size: getUrlLinkFrameSize(urlString.length), 134 | }); 135 | } 136 | 137 | _setPairedTextFrame(name, list) { 138 | this.frames.push({ 139 | name, 140 | value: list, 141 | size: getPairedTextFrameSize(list), 142 | }); 143 | } 144 | 145 | _setSynchronisedLyricsFrame( 146 | type, 147 | text, 148 | timestampFormat, 149 | language, 150 | description, 151 | ) { 152 | const descriptionString = description.toString(); 153 | const languageCode = language.split('').map((c) => c.charCodeAt(0)); 154 | 155 | this.frames.push({ 156 | name: 'SYLT', 157 | value: text, 158 | language: languageCode, 159 | description: descriptionString, 160 | type, 161 | timestampFormat, 162 | size: getSynchronisedLyricsFrameSize(text, descriptionString.length), 163 | }); 164 | } 165 | 166 | constructor(buffer) { 167 | if (!buffer || typeof buffer !== 'object' || !('byteLength' in buffer)) { 168 | throw new Error( 169 | 'First argument should be an instance of ArrayBuffer or Buffer', 170 | ); 171 | } 172 | 173 | this.arrayBuffer = buffer; 174 | this.padding = 4096; 175 | this.frames = []; 176 | this.url = ''; 177 | } 178 | 179 | setFrame(frameName, frameValue) { 180 | switch (frameName) { 181 | case 'TPE1': // song artists 182 | case 'TCOM': // song composers 183 | case 'TCON': { 184 | // song genres 185 | if (!Array.isArray(frameValue)) { 186 | throw new Error( 187 | `${frameName} frame value should be an array of strings`, 188 | ); 189 | } 190 | const delemiter = frameName === 'TCON' ? ';' : '/'; 191 | const value = frameValue.join(delemiter); 192 | 193 | this._setStringFrame(frameName, value); 194 | break; 195 | } 196 | case 'TLAN': // language 197 | case 'TIT1': // content group description 198 | case 'TIT2': // song title 199 | case 'TIT3': // song subtitle 200 | case 'TALB': // album title 201 | case 'TPE2': // album artist // spec doesn't say anything about separator, so it is a string, not array 202 | case 'TPE3': // conductor/performer refinement 203 | case 'TPE4': // interpreted, remixed, or otherwise modified by 204 | case 'TRCK': // song number in album: 5 or 5/10 205 | case 'TPOS': // album disc number: 1 or 1/3 206 | case 'TMED': // media type 207 | case 'TPUB': // label name 208 | case 'TCOP': // copyright 209 | case 'TKEY': // musical key in which the sound starts 210 | case 'TEXT': // lyricist / text writer 211 | case 'TDAT': // album release date expressed as DDMM 212 | case 'TCMP': // compilation flag ("1" stored as a string) 213 | case 'TSRC': { 214 | // isrc 215 | this._setStringFrame(frameName, frameValue); 216 | break; 217 | } 218 | case 'TBPM': // beats per minute 219 | case 'TLEN': // song duration 220 | case 'TYER': { 221 | // album release year 222 | this._setIntegerFrame(frameName, frameValue); 223 | break; 224 | } 225 | case 'USLT': { 226 | // unsychronised lyrics 227 | frameValue.language = frameValue.language || 'eng'; 228 | if ( 229 | typeof frameValue !== 'object' || 230 | !('description' in frameValue) || 231 | !('lyrics' in frameValue) 232 | ) { 233 | throw new Error( 234 | 'USLT frame value should be an object with keys description and lyrics', 235 | ); 236 | } 237 | if (frameValue.language && !frameValue.language.match(/[a-z]{3}/i)) { 238 | throw new Error( 239 | 'Language must be coded following the ISO 639-2 standards', 240 | ); 241 | } 242 | this._setLyricsFrame( 243 | frameValue.language, 244 | frameValue.description, 245 | frameValue.lyrics, 246 | ); 247 | break; 248 | } 249 | case 'APIC': { 250 | // song cover 251 | if ( 252 | typeof frameValue !== 'object' || 253 | !('type' in frameValue) || 254 | !('data' in frameValue) || 255 | !('description' in frameValue) 256 | ) { 257 | throw new Error( 258 | 'APIC frame value should be an object with keys type, data and description', 259 | ); 260 | } 261 | if (frameValue.type < 0 || frameValue.type > 20) { 262 | throw new Error('Incorrect APIC frame picture type'); 263 | } 264 | this._setPictureFrame( 265 | frameValue.type, 266 | frameValue.data, 267 | frameValue.description, 268 | !!frameValue.useUnicodeEncoding, 269 | ); 270 | break; 271 | } 272 | case 'TXXX': { 273 | // user defined text information 274 | if ( 275 | typeof frameValue !== 'object' || 276 | !('description' in frameValue) || 277 | !('value' in frameValue) 278 | ) { 279 | throw new Error( 280 | 'TXXX frame value should be an object with keys description and value', 281 | ); 282 | } 283 | this._setUserStringFrame(frameValue.description, frameValue.value); 284 | break; 285 | } 286 | case 'WCOM': // Commercial information 287 | case 'WCOP': // Copyright/Legal information 288 | case 'WOAF': // Official audio file webpage 289 | case 'WOAR': // Official artist/performer webpage 290 | case 'WOAS': // Official audio source webpage 291 | case 'WORS': // Official internet radio station homepage 292 | case 'WPAY': // Payment 293 | case 'WPUB': { 294 | // Publishers official webpage 295 | this._setUrlLinkFrame(frameName, frameValue); 296 | break; 297 | } 298 | case 'COMM': { 299 | // Comments 300 | frameValue.language = frameValue.language || 'eng'; 301 | if ( 302 | typeof frameValue !== 'object' || 303 | !('description' in frameValue) || 304 | !('text' in frameValue) 305 | ) { 306 | throw new Error( 307 | 'COMM frame value should be an object with keys description and text', 308 | ); 309 | } 310 | if (frameValue.language && !frameValue.language.match(/[a-z]{3}/i)) { 311 | throw new Error( 312 | 'Language must be coded following the ISO 639-2 standards', 313 | ); 314 | } 315 | this._setCommentFrame( 316 | frameValue.language, 317 | frameValue.description, 318 | frameValue.text, 319 | ); 320 | break; 321 | } 322 | case 'PRIV': { 323 | // Private frame 324 | if ( 325 | typeof frameValue !== 'object' || 326 | !('id' in frameValue) || 327 | !('data' in frameValue) 328 | ) { 329 | throw new Error( 330 | 'PRIV frame value should be an object with keys id and data', 331 | ); 332 | } 333 | this._setPrivateFrame(frameValue.id, frameValue.data); 334 | break; 335 | } 336 | case 'IPLS': { 337 | // Involved people 338 | if (!Array.isArray(frameValue) || !Array.isArray(frameValue[0])) { 339 | throw new Error('IPLS frame value should be an array of pairs'); 340 | } 341 | 342 | this._setPairedTextFrame(frameName, frameValue); 343 | break; 344 | } 345 | case 'SYLT': { 346 | // Synchronised Lyrics 347 | if ( 348 | typeof frameValue !== 'object' || 349 | !('type' in frameValue) || 350 | !('text' in frameValue) || 351 | !('timestampFormat' in frameValue) 352 | ) { 353 | throw new Error( 354 | 'SYLT frame value should be an object with keys type, text and timestampFormat', 355 | ); 356 | } 357 | if ( 358 | !Array.isArray(frameValue.text) || 359 | !Array.isArray(frameValue.text[0]) 360 | ) { 361 | throw new Error('SYLT frame text value should be an array of pairs'); 362 | } 363 | if (frameValue.type < 0 || frameValue.type > 6) { 364 | throw new Error('Incorrect SYLT frame content type'); 365 | } 366 | if (frameValue.timestampFormat < 1 || frameValue.timestampFormat > 2) { 367 | throw new Error('Incorrect SYLT frame time stamp format'); 368 | } 369 | frameValue.language = frameValue.language || 'eng'; 370 | frameValue.description = frameValue.description || ''; 371 | 372 | this._setSynchronisedLyricsFrame( 373 | frameValue.type, 374 | frameValue.text, 375 | frameValue.timestampFormat, 376 | frameValue.language, 377 | frameValue.description, 378 | ); 379 | break; 380 | } 381 | default: { 382 | throw new Error(`Unsupported frame ${frameName}`); 383 | } 384 | } 385 | return this; 386 | } 387 | 388 | removeTag() { 389 | const headerLength = 10; 390 | 391 | if (this.arrayBuffer.byteLength < headerLength) { 392 | return; 393 | } 394 | const bytes = new Uint8Array(this.arrayBuffer); 395 | const version = bytes[3]; 396 | const tagSize = 397 | uint7ArrayToUint28([bytes[6], bytes[7], bytes[8], bytes[9]]) + 398 | headerLength; 399 | 400 | if (!isId3v2(bytes) || version < 2 || version > 4) { 401 | return; 402 | } 403 | this.arrayBuffer = new Uint8Array(bytes.subarray(tagSize)).buffer; 404 | } 405 | 406 | addTag() { 407 | this.removeTag(); 408 | 409 | const BOM = [0xff, 0xfe]; 410 | const headerSize = 10; 411 | const totalFrameSize = this.frames.reduce( 412 | (sum, frame) => sum + frame.size, 413 | 0, 414 | ); 415 | const totalTagSize = headerSize + totalFrameSize + this.padding; 416 | const buffer = new ArrayBuffer(this.arrayBuffer.byteLength + totalTagSize); 417 | const bufferWriter = new Uint8Array(buffer); 418 | 419 | let offset = 0; 420 | let writeBytes = []; 421 | 422 | writeBytes = [0x49, 0x44, 0x33, 3]; // ID3 tag and version 423 | bufferWriter.set(writeBytes, offset); 424 | offset += writeBytes.length; 425 | 426 | offset++; // version revision 427 | offset++; // flags 428 | 429 | writeBytes = uint28ToUint7Array(totalTagSize - headerSize); // tag size (without header) 430 | bufferWriter.set(writeBytes, offset); 431 | offset += writeBytes.length; 432 | 433 | this.frames.forEach((frame) => { 434 | writeBytes = encodeWindows1252(frame.name); // frame name 435 | bufferWriter.set(writeBytes, offset); 436 | offset += writeBytes.length; 437 | 438 | writeBytes = uint32ToUint8Array(frame.size - headerSize); // frame size (without header) 439 | bufferWriter.set(writeBytes, offset); 440 | offset += writeBytes.length; 441 | 442 | offset += 2; // flags 443 | 444 | switch (frame.name) { 445 | case 'WCOM': 446 | case 'WCOP': 447 | case 'WOAF': 448 | case 'WOAR': 449 | case 'WOAS': 450 | case 'WORS': 451 | case 'WPAY': 452 | case 'WPUB': { 453 | writeBytes = encodeWindows1252(frame.value); // URL 454 | bufferWriter.set(writeBytes, offset); 455 | offset += writeBytes.length; 456 | break; 457 | } 458 | case 'TPE1': 459 | case 'TCOM': 460 | case 'TCON': 461 | case 'TLAN': 462 | case 'TIT1': 463 | case 'TIT2': 464 | case 'TIT3': 465 | case 'TALB': 466 | case 'TPE2': 467 | case 'TPE3': 468 | case 'TPE4': 469 | case 'TRCK': 470 | case 'TPOS': 471 | case 'TKEY': 472 | case 'TMED': 473 | case 'TPUB': 474 | case 'TCOP': 475 | case 'TEXT': 476 | case 'TSRC': { 477 | writeBytes = [1].concat(BOM); // encoding, BOM 478 | bufferWriter.set(writeBytes, offset); 479 | offset += writeBytes.length; 480 | 481 | writeBytes = encodeUtf16le(frame.value); // frame value 482 | bufferWriter.set(writeBytes, offset); 483 | offset += writeBytes.length; 484 | break; 485 | } 486 | case 'TXXX': 487 | case 'USLT': 488 | case 'COMM': { 489 | writeBytes = [1]; // encoding 490 | if (frame.name === 'USLT' || frame.name === 'COMM') { 491 | writeBytes = writeBytes.concat(frame.language); // language 492 | } 493 | writeBytes = writeBytes.concat(BOM); // BOM for content descriptor 494 | bufferWriter.set(writeBytes, offset); 495 | offset += writeBytes.length; 496 | 497 | writeBytes = encodeUtf16le(frame.description); // content descriptor 498 | bufferWriter.set(writeBytes, offset); 499 | offset += writeBytes.length; 500 | 501 | writeBytes = [0, 0].concat(BOM); // separator, BOM for frame value 502 | bufferWriter.set(writeBytes, offset); 503 | offset += writeBytes.length; 504 | 505 | writeBytes = encodeUtf16le(frame.value); // frame value 506 | bufferWriter.set(writeBytes, offset); 507 | offset += writeBytes.length; 508 | break; 509 | } 510 | case 'TBPM': 511 | case 'TLEN': 512 | case 'TDAT': 513 | case 'TYER': { 514 | offset++; // encoding 515 | 516 | writeBytes = encodeWindows1252(frame.value); // frame value 517 | bufferWriter.set(writeBytes, offset); 518 | offset += writeBytes.length; 519 | break; 520 | } 521 | case 'PRIV': { 522 | writeBytes = encodeWindows1252(frame.id); // identifier 523 | bufferWriter.set(writeBytes, offset); 524 | offset += writeBytes.length; 525 | 526 | offset++; // separator 527 | 528 | bufferWriter.set(new Uint8Array(frame.value), offset); // frame data 529 | offset += frame.value.byteLength; 530 | break; 531 | } 532 | case 'APIC': { 533 | writeBytes = [frame.useUnicodeEncoding ? 1 : 0]; // encoding 534 | bufferWriter.set(writeBytes, offset); 535 | offset += writeBytes.length; 536 | 537 | writeBytes = encodeWindows1252(frame.mimeType); // MIME type 538 | bufferWriter.set(writeBytes, offset); 539 | offset += writeBytes.length; 540 | 541 | writeBytes = [0, frame.pictureType]; // separator, pic type 542 | bufferWriter.set(writeBytes, offset); 543 | offset += writeBytes.length; 544 | 545 | if (frame.useUnicodeEncoding) { 546 | writeBytes = [].concat(BOM); // BOM 547 | bufferWriter.set(writeBytes, offset); 548 | offset += writeBytes.length; 549 | 550 | writeBytes = encodeUtf16le(frame.description); // description 551 | bufferWriter.set(writeBytes, offset); 552 | offset += writeBytes.length; 553 | 554 | offset += 2; // separator 555 | } else { 556 | writeBytes = encodeWindows1252(frame.description); // description 557 | bufferWriter.set(writeBytes, offset); 558 | offset += writeBytes.length; 559 | 560 | offset++; // separator 561 | } 562 | 563 | bufferWriter.set(new Uint8Array(frame.value), offset); // picture content 564 | offset += frame.value.byteLength; 565 | break; 566 | } 567 | case 'IPLS': { 568 | writeBytes = [1]; // encoding 569 | bufferWriter.set(writeBytes, offset); 570 | offset += writeBytes.length; 571 | 572 | frame.value.forEach((pair) => { 573 | writeBytes = [].concat(BOM); // BOM 574 | bufferWriter.set(writeBytes, offset); 575 | offset += writeBytes.length; 576 | 577 | writeBytes = encodeUtf16le(pair[0].toString()); // role 578 | bufferWriter.set(writeBytes, offset); 579 | offset += writeBytes.length; 580 | 581 | writeBytes = [0, 0].concat(BOM); // separator + BOM 582 | bufferWriter.set(writeBytes, offset); 583 | offset += writeBytes.length; 584 | 585 | writeBytes = encodeUtf16le(pair[1].toString()); // name 586 | bufferWriter.set(writeBytes, offset); 587 | offset += writeBytes.length; 588 | 589 | writeBytes = [0, 0]; // separator 590 | bufferWriter.set(writeBytes, offset); 591 | offset += writeBytes.length; 592 | }); 593 | break; 594 | } 595 | case 'SYLT': { 596 | writeBytes = [1] // encoding 597 | .concat(frame.language) // language 598 | .concat(frame.timestampFormat) // time stamp format 599 | .concat(frame.type); // content type 600 | bufferWriter.set(writeBytes, offset); 601 | offset += writeBytes.length; 602 | 603 | writeBytes = [].concat(BOM); // BOM 604 | bufferWriter.set(writeBytes, offset); 605 | offset += writeBytes.length; 606 | 607 | writeBytes = encodeUtf16le(frame.description); // description 608 | bufferWriter.set(writeBytes, offset); 609 | offset += writeBytes.length; 610 | 611 | offset += 2; // separator 612 | 613 | frame.value.forEach((line) => { 614 | writeBytes = [].concat(BOM); // BOM 615 | bufferWriter.set(writeBytes, offset); 616 | offset += writeBytes.length; 617 | 618 | writeBytes = encodeUtf16le(line[0].toString()); // lyric line 619 | bufferWriter.set(writeBytes, offset); 620 | offset += writeBytes.length; 621 | 622 | writeBytes = [0, 0]; // separator 623 | bufferWriter.set(writeBytes, offset); 624 | offset += writeBytes.length; 625 | 626 | writeBytes = uint32ToUint8Array(line[1]); // timestamp 627 | bufferWriter.set(writeBytes, offset); 628 | offset += writeBytes.length; 629 | }); 630 | break; 631 | } 632 | } 633 | }); 634 | 635 | offset += this.padding; // free space for rewriting 636 | bufferWriter.set(new Uint8Array(this.arrayBuffer), offset); 637 | this.arrayBuffer = buffer; 638 | return buffer; 639 | } 640 | 641 | getBlob() { 642 | return new Blob([this.arrayBuffer], { type: 'audio/mpeg' }); 643 | } 644 | 645 | getURL() { 646 | if (!this.url) { 647 | this.url = URL.createObjectURL(this.getBlob()); 648 | } 649 | return this.url; 650 | } 651 | 652 | revokeURL() { 653 | URL.revokeObjectURL(this.url); 654 | } 655 | } 656 | -------------------------------------------------------------------------------- /src/encoder.mjs: -------------------------------------------------------------------------------- 1 | // https://encoding.spec.whatwg.org/ 2 | 3 | export function strToCodePoints(str) { 4 | return String(str) 5 | .split('') 6 | .map((c) => c.charCodeAt(0)); 7 | } 8 | 9 | export function encodeWindows1252(str) { 10 | return new Uint8Array(strToCodePoints(str)); 11 | } 12 | 13 | export function encodeUtf16le(str) { 14 | const buffer = new ArrayBuffer(str.length * 2); 15 | const u8 = new Uint8Array(buffer); 16 | const u16 = new Uint16Array(buffer); 17 | 18 | u16.set(strToCodePoints(str)); 19 | return u8; 20 | } 21 | -------------------------------------------------------------------------------- /src/signatures.mjs: -------------------------------------------------------------------------------- 1 | export function isId3v2(buf) { 2 | return buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33; 3 | } 4 | 5 | export function getMimeType(buf) { 6 | // https://github.com/sindresorhus/file-type 7 | if (!buf || !buf.length) { 8 | return null; 9 | } 10 | if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) { 11 | return 'image/jpeg'; 12 | } 13 | if ( 14 | buf[0] === 0x89 && 15 | buf[1] === 0x50 && 16 | buf[2] === 0x4e && 17 | buf[3] === 0x47 18 | ) { 19 | return 'image/png'; 20 | } 21 | if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) { 22 | return 'image/gif'; 23 | } 24 | if ( 25 | buf[8] === 0x57 && 26 | buf[9] === 0x45 && 27 | buf[10] === 0x42 && 28 | buf[11] === 0x50 29 | ) { 30 | return 'image/webp'; 31 | } 32 | const isLeTiff = 33 | buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0x2a && buf[3] === 0; 34 | const isBeTiff = 35 | buf[0] === 0x4d && buf[1] === 0x4d && buf[2] === 0 && buf[3] === 0x2a; 36 | 37 | if (isLeTiff || isBeTiff) { 38 | return 'image/tiff'; 39 | } 40 | if (buf[0] === 0x42 && buf[1] === 0x4d) { 41 | return 'image/bmp'; 42 | } 43 | if (buf[0] === 0 && buf[1] === 0 && buf[2] === 1 && buf[3] === 0) { 44 | return 'image/x-icon'; 45 | } 46 | return null; 47 | } 48 | -------------------------------------------------------------------------------- /src/sizes.mjs: -------------------------------------------------------------------------------- 1 | export function getNumericFrameSize(frameSize) { 2 | const headerSize = 10; 3 | const encodingSize = 1; 4 | 5 | return headerSize + encodingSize + frameSize; 6 | } 7 | 8 | export function getStringFrameSize(frameSize) { 9 | const headerSize = 10; 10 | const encodingSize = 1; 11 | const bomSize = 2; 12 | const frameUtf16Size = frameSize * 2; 13 | 14 | return headerSize + encodingSize + bomSize + frameUtf16Size; 15 | } 16 | 17 | export function getLyricsFrameSize(descriptionSize, lyricsSize) { 18 | const headerSize = 10; 19 | const encodingSize = 1; 20 | const languageSize = 3; 21 | const bomSize = 2; 22 | const descriptionUtf16Size = descriptionSize * 2; 23 | const separatorSize = 2; 24 | const lyricsUtf16Size = lyricsSize * 2; 25 | 26 | return ( 27 | headerSize + 28 | encodingSize + 29 | languageSize + 30 | bomSize + 31 | descriptionUtf16Size + 32 | separatorSize + 33 | bomSize + 34 | lyricsUtf16Size 35 | ); 36 | } 37 | 38 | export function getPictureFrameSize( 39 | pictureSize, 40 | mimeTypeSize, 41 | descriptionSize, 42 | useUnicodeEncoding, 43 | ) { 44 | const headerSize = 10; 45 | const encodingSize = 1; 46 | const separatorSize = 1; 47 | const pictureTypeSize = 1; 48 | const bomSize = 2; 49 | const encodedDescriptionSize = useUnicodeEncoding 50 | ? bomSize + (descriptionSize + separatorSize) * 2 51 | : descriptionSize + separatorSize; 52 | 53 | return ( 54 | headerSize + 55 | encodingSize + 56 | mimeTypeSize + 57 | separatorSize + 58 | pictureTypeSize + 59 | encodedDescriptionSize + 60 | pictureSize 61 | ); 62 | } 63 | 64 | export function getCommentFrameSize(descriptionSize, textSize) { 65 | const headerSize = 10; 66 | const encodingSize = 1; 67 | const languageSize = 3; 68 | const bomSize = 2; 69 | const descriptionUtf16Size = descriptionSize * 2; 70 | const separatorSize = 2; 71 | const textUtf16Size = textSize * 2; 72 | 73 | return ( 74 | headerSize + 75 | encodingSize + 76 | languageSize + 77 | bomSize + 78 | descriptionUtf16Size + 79 | separatorSize + 80 | bomSize + 81 | textUtf16Size 82 | ); 83 | } 84 | 85 | export function getPrivateFrameSize(idSize, dataSize) { 86 | const headerSize = 10; 87 | const separatorSize = 1; 88 | 89 | return headerSize + idSize + separatorSize + dataSize; 90 | } 91 | 92 | export function getUserStringFrameSize(descriptionSize, valueSize) { 93 | const headerSize = 10; 94 | const encodingSize = 1; 95 | const bomSize = 2; 96 | const descriptionUtf16Size = descriptionSize * 2; 97 | const separatorSize = 2; 98 | const valueUtf16Size = valueSize * 2; 99 | 100 | return ( 101 | headerSize + 102 | encodingSize + 103 | bomSize + 104 | descriptionUtf16Size + 105 | separatorSize + 106 | bomSize + 107 | valueUtf16Size 108 | ); 109 | } 110 | 111 | export function getUrlLinkFrameSize(urlSize) { 112 | const headerSize = 10; 113 | 114 | return headerSize + urlSize; 115 | } 116 | 117 | export function getPairedTextFrameSize(list) { 118 | const headerSize = 10; 119 | const encodingSize = 1; 120 | const bomSize = 2; 121 | const separatorSize = 2; 122 | let encodedListSize = 0; 123 | list.forEach((pair) => { 124 | encodedListSize += 125 | bomSize + 126 | pair[0].length * 2 + 127 | separatorSize + 128 | bomSize + 129 | pair[1].length * 2 + 130 | separatorSize; 131 | }); 132 | 133 | return headerSize + encodingSize + encodedListSize; 134 | } 135 | 136 | export function getSynchronisedLyricsFrameSize(lyrics, descriptionSize) { 137 | const headerSize = 10; 138 | const encodingSize = 1; 139 | const languageSize = 3; 140 | const timestampFormatSize = 1; 141 | const contentTypeSize = 1; 142 | const bomSize = 2; 143 | const descriptionUtf16Size = descriptionSize * 2; 144 | const separatorSize = 2; 145 | const timestampSize = 4; 146 | let encodedLyricsSize = 0; 147 | lyrics.forEach((line) => { 148 | encodedLyricsSize += 149 | bomSize + line[0].length * 2 + separatorSize + timestampSize; 150 | }); 151 | 152 | return ( 153 | headerSize + 154 | encodingSize + 155 | languageSize + 156 | timestampFormatSize + 157 | contentTypeSize + 158 | bomSize + 159 | descriptionUtf16Size + 160 | separatorSize + 161 | encodedLyricsSize 162 | ); 163 | } 164 | -------------------------------------------------------------------------------- /src/transform.mjs: -------------------------------------------------------------------------------- 1 | export function uint32ToUint8Array(uint32) { 2 | const eightBitMask = 0xff; 3 | 4 | return [ 5 | (uint32 >>> 24) & eightBitMask, 6 | (uint32 >>> 16) & eightBitMask, 7 | (uint32 >>> 8) & eightBitMask, 8 | uint32 & eightBitMask, 9 | ]; 10 | } 11 | 12 | export function uint28ToUint7Array(uint28) { 13 | const sevenBitMask = 0x7f; 14 | 15 | return [ 16 | (uint28 >>> 21) & sevenBitMask, 17 | (uint28 >>> 14) & sevenBitMask, 18 | (uint28 >>> 7) & sevenBitMask, 19 | uint28 & sevenBitMask, 20 | ]; 21 | } 22 | 23 | export function uint7ArrayToUint28(uint7Array) { 24 | return ( 25 | (uint7Array[0] << 21) + 26 | (uint7Array[1] << 14) + 27 | (uint7Array[2] << 7) + 28 | uint7Array[3] 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /test/arrayOfStrings.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from './utils.mjs'; 4 | import { encodeUtf16le, encodeWindows1252 } from '../src/encoder.mjs'; 5 | import { uint28ToUint7Array, uint32ToUint8Array } from '../src/transform.mjs'; 6 | import { ID3Writer } from '../dist/browser-id3-writer.mjs'; 7 | 8 | const frames = ['TPE1', 'TCOM', 'TCON']; 9 | 10 | describe('Frames: array of strings', () => { 11 | frames.forEach((frameName) => { 12 | it(frameName, () => { 13 | const delemiter = frameName === 'TCON' ? ';' : '/'; 14 | const writer = new ID3Writer(getEmptyBuffer()); 15 | writer.padding = 0; 16 | writer.setFrame(frameName, ['Eminem', '50 Cent']); 17 | writer.addTag(); 18 | const actual = new Uint8Array(writer.arrayBuffer); 19 | const expected = new Uint8Array([ 20 | ...id3Header, 21 | ...uint28ToUint7Array(41), // tag size without header 22 | ...encodeWindows1252(frameName), 23 | ...uint32ToUint8Array(31), // frame size without header 24 | 0, 25 | 0, // flags 26 | 1, // encoding 27 | 0xff, 28 | 0xfe, // BOM 29 | ...encodeUtf16le(`Eminem${delemiter}50 Cent`), 30 | ]); 31 | deepStrictEqual(actual, expected); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/encoder.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual } from 'assert'; 3 | import { 4 | strToCodePoints, 5 | encodeWindows1252, 6 | encodeUtf16le, 7 | } from '../src/encoder.mjs'; 8 | 9 | describe('encoder', () => { 10 | describe('strToCodePoints', () => { 11 | it('latin', () => { 12 | const actual = strToCodePoints('Hello'); 13 | const expected = [72, 101, 108, 108, 111]; 14 | deepStrictEqual(actual, expected); 15 | }); 16 | it('cyrillic', () => { 17 | const actual = strToCodePoints('Привет'); 18 | const expected = [1055, 1088, 1080, 1074, 1077, 1090]; 19 | deepStrictEqual(actual, expected); 20 | }); 21 | }); 22 | 23 | describe('encodeWindows1252', () => { 24 | it('encodes latin', () => { 25 | const actual = encodeWindows1252('Hello'); 26 | const expected = new Uint8Array([72, 101, 108, 108, 111]); 27 | deepStrictEqual(actual, expected); 28 | }); 29 | it('loses cyrillic', () => { 30 | const actual = encodeWindows1252('Привет'); 31 | const expected = new Uint8Array([31, 64, 56, 50, 53, 66]); 32 | deepStrictEqual(actual, expected); 33 | }); 34 | }); 35 | 36 | describe('encodeUtf16le', () => { 37 | it('encodes latin', () => { 38 | const actual = encodeUtf16le('Hello'); 39 | const expected = new Uint8Array([72, 0, 101, 0, 108, 0, 108, 0, 111, 0]); 40 | deepStrictEqual(actual, expected); 41 | }); 42 | it('encodes cyrillic', () => { 43 | const actual = encodeUtf16le('Привет'); 44 | const expected = new Uint8Array([ 45 | 31, 4, 64, 4, 56, 4, 50, 4, 53, 4, 66, 4, 46 | ]); 47 | deepStrictEqual(actual, expected); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/index.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual, throws } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from './utils.mjs'; 4 | import { encodeUtf16le, encodeWindows1252 } from '../src/encoder.mjs'; 5 | import { uint28ToUint7Array, uint32ToUint8Array } from '../src/transform.mjs'; 6 | import { ID3Writer } from '../dist/browser-id3-writer.mjs'; 7 | 8 | describe('Commom usage', () => { 9 | it('Copy data from buffer, add padding and tags', () => { 10 | const payload = new Uint8Array([1, 2, 3]); 11 | const writer = new ID3Writer(payload.buffer); 12 | writer.padding = 5; 13 | writer.setFrame('TIT2', 'Home').setFrame('TPE1', ['Eminem']); 14 | writer.addTag(); 15 | const actual = new Uint8Array(writer.arrayBuffer); 16 | const expected = new Uint8Array([ 17 | ...id3Header, 18 | ...uint28ToUint7Array(51), // tag size without header 19 | ...encodeWindows1252('TIT2'), 20 | ...uint32ToUint8Array(11), // size of tit2 without header 21 | 0, 22 | 0, // flags 23 | 1, // encoding 24 | 0xff, 25 | 0xfe, // BOM 26 | ...encodeUtf16le('Home'), 27 | ...encodeWindows1252('TPE1'), 28 | ...uint32ToUint8Array(15), // size of tpe1 without header 29 | 0, 30 | 0, // flags 31 | 1, // encoding 32 | 0xff, 33 | 0xfe, // BOM 34 | ...encodeUtf16le('Eminem'), 35 | 0, 36 | 0, 37 | 0, 38 | 0, 39 | 0, // padding 40 | 1, 41 | 2, 42 | 3, // payload 43 | ]); 44 | deepStrictEqual(actual, expected); 45 | }); 46 | it('Throw with wrong frame name', () => { 47 | const writer = new ID3Writer(getEmptyBuffer()); 48 | throws(() => { 49 | writer.setFrame('yoyo', 'hey'); 50 | }, /Unsupported frame/); 51 | }); 52 | it('Throw if no argument passed to constructor', () => { 53 | throws(() => { 54 | new ID3Writer(); 55 | }, /First argument should be an instance of ArrayBuffer or Buffer/); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/integer.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from './utils.mjs'; 4 | import { encodeWindows1252 } from '../src/encoder.mjs'; 5 | import { uint28ToUint7Array, uint32ToUint8Array } from '../src/transform.mjs'; 6 | import { ID3Writer } from '../dist/browser-id3-writer.mjs'; 7 | 8 | const frames = ['TLEN', 'TYER', 'TBPM']; 9 | 10 | describe('Frames: integer', () => { 11 | frames.forEach((frameName) => { 12 | it(frameName, () => { 13 | const writer = new ID3Writer(getEmptyBuffer()); 14 | writer.padding = 0; 15 | writer.setFrame(frameName, 2023); 16 | writer.addTag(); 17 | const actual = new Uint8Array(writer.arrayBuffer); 18 | const expected = new Uint8Array([ 19 | ...id3Header, 20 | ...uint28ToUint7Array(15), // tag size without header 21 | ...encodeWindows1252(frameName), 22 | ...uint32ToUint8Array(5), // frame size without header 23 | 0, 24 | 0, // flags 25 | 0, // encoding 26 | ...encodeWindows1252('2023'), 27 | ]); 28 | deepStrictEqual(actual, expected); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/object/APIC.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual, throws } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from '../utils.mjs'; 4 | import { encodeUtf16le, encodeWindows1252 } from '../../src/encoder.mjs'; 5 | import { 6 | uint28ToUint7Array, 7 | uint32ToUint8Array, 8 | } from '../../src/transform.mjs'; 9 | import { ID3Writer } from '../../dist/browser-id3-writer.mjs'; 10 | 11 | const imageContent = [4, 8, 15, 16, 23, 42]; 12 | 13 | describe('APIC', () => { 14 | it('jpeg', () => { 15 | const signature = [0xff, 0xd8, 0xff]; 16 | const image = new Uint8Array(signature.concat(imageContent)); 17 | const writer = new ID3Writer(getEmptyBuffer()); 18 | writer.padding = 0; 19 | writer.setFrame('APIC', { 20 | type: 3, 21 | data: image.buffer, 22 | description: 'yo', 23 | }); 24 | writer.addTag(); 25 | const actual = new Uint8Array(writer.arrayBuffer); 26 | const expected = new Uint8Array([ 27 | ...id3Header, 28 | ...uint28ToUint7Array(35), // tag size without header 29 | ...encodeWindows1252('APIC'), 30 | ...uint32ToUint8Array(25), // frame size without header 31 | 0, 32 | 0, // flags 33 | 0, // encoding 34 | ...encodeWindows1252('image/jpeg'), 35 | 0, // separator 36 | 3, // pic type 37 | ...encodeWindows1252('yo'), 38 | 0, // separator 39 | ...signature, 40 | ...imageContent, 41 | ]); 42 | deepStrictEqual(actual, expected); 43 | }); 44 | it('jpeg with useUnicodeEncoding', () => { 45 | const signature = [0xff, 0xd8, 0xff]; 46 | const image = new Uint8Array(signature.concat(imageContent)); 47 | const writer = new ID3Writer(getEmptyBuffer()); 48 | writer.padding = 0; 49 | writer.setFrame('APIC', { 50 | type: 3, 51 | data: image.buffer, 52 | description: 'yo', 53 | useUnicodeEncoding: true, 54 | }); 55 | writer.addTag(); 56 | const actual = new Uint8Array(writer.arrayBuffer); 57 | const expected = new Uint8Array([ 58 | ...id3Header, 59 | ...uint28ToUint7Array(40), // tag size without header 60 | ...encodeWindows1252('APIC'), 61 | ...uint32ToUint8Array(30), // frame size without header 62 | 0, 63 | 0, // flags 64 | 1, // encoding 65 | ...encodeWindows1252('image/jpeg'), 66 | 0, // separator 67 | 3, // pic type 68 | 0xff, 69 | 0xfe, // BOM 70 | ...encodeUtf16le('yo'), 71 | 0, 72 | 0, // separator 73 | ...signature, 74 | ...imageContent, 75 | ]); 76 | deepStrictEqual(actual, expected); 77 | }); 78 | it('png', () => { 79 | const signature = [0x89, 0x50, 0x4e, 0x47]; 80 | const image = new Uint8Array(signature.concat(imageContent)); 81 | const writer = new ID3Writer(getEmptyBuffer()); 82 | writer.padding = 0; 83 | writer.setFrame('APIC', { 84 | type: 3, 85 | data: image.buffer, 86 | description: 'yo', 87 | }); 88 | writer.addTag(); 89 | const actual = new Uint8Array(writer.arrayBuffer); 90 | const expected = new Uint8Array([ 91 | ...id3Header, 92 | ...uint28ToUint7Array(35), // tag size without header 93 | ...encodeWindows1252('APIC'), 94 | ...uint32ToUint8Array(25), // frame size without header 95 | 0, 96 | 0, // flags 97 | 0, // encoding 98 | ...encodeWindows1252('image/png'), 99 | 0, // separator 100 | 3, // pic type 101 | ...encodeWindows1252('yo'), 102 | 0, // separator 103 | ...signature, 104 | ...imageContent, 105 | ]); 106 | deepStrictEqual(actual, expected); 107 | }); 108 | it('gif', () => { 109 | const signature = [0x47, 0x49, 0x46]; 110 | const image = new Uint8Array(signature.concat(imageContent)); 111 | const writer = new ID3Writer(getEmptyBuffer()); 112 | writer.padding = 0; 113 | writer.setFrame('APIC', { 114 | type: 3, 115 | data: image.buffer, 116 | description: 'yo', 117 | }); 118 | writer.addTag(); 119 | const actual = new Uint8Array(writer.arrayBuffer); 120 | const expected = new Uint8Array([ 121 | ...id3Header, 122 | ...uint28ToUint7Array(34), // tag size without header 123 | ...encodeWindows1252('APIC'), 124 | ...uint32ToUint8Array(24), // frame size without header 125 | 0, 126 | 0, // flags 127 | 0, // encoding 128 | ...encodeWindows1252('image/gif'), 129 | 0, // separator 130 | 3, // pic type 131 | ...encodeWindows1252('yo'), 132 | 0, // separator 133 | ...signature, 134 | ...imageContent, 135 | ]); 136 | deepStrictEqual(actual, expected); 137 | }); 138 | it('webp', () => { 139 | const signature = [0, 0, 0, 0, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50]; 140 | const image = new Uint8Array(signature.concat(imageContent)); 141 | const writer = new ID3Writer(getEmptyBuffer()); 142 | writer.padding = 0; 143 | writer.setFrame('APIC', { 144 | type: 3, 145 | data: image.buffer, 146 | description: 'yo', 147 | }); 148 | writer.addTag(); 149 | const actual = new Uint8Array(writer.arrayBuffer); 150 | const expected = new Uint8Array([ 151 | ...id3Header, 152 | ...uint28ToUint7Array(44), // tag size without header 153 | ...encodeWindows1252('APIC'), 154 | ...uint32ToUint8Array(34), // frame size without header 155 | 0, 156 | 0, // flags 157 | 0, // encoding 158 | ...encodeWindows1252('image/webp'), 159 | 0, // separator 160 | 3, // pic type 161 | ...encodeWindows1252('yo'), 162 | 0, // separator 163 | ...signature, 164 | ...imageContent, 165 | ]); 166 | deepStrictEqual(actual, expected); 167 | }); 168 | it('tiff', () => { 169 | const signature = [0x49, 0x49, 0x2a, 0]; 170 | const image = new Uint8Array(signature.concat(imageContent)); 171 | const writer = new ID3Writer(getEmptyBuffer()); 172 | writer.padding = 0; 173 | writer.setFrame('APIC', { 174 | type: 3, 175 | data: image.buffer, 176 | description: 'yo', 177 | }); 178 | writer.addTag(); 179 | const actual = new Uint8Array(writer.arrayBuffer); 180 | const expected = new Uint8Array([ 181 | ...id3Header, 182 | ...uint28ToUint7Array(36), // tag size without header 183 | ...encodeWindows1252('APIC'), 184 | ...uint32ToUint8Array(26), // frame size without header 185 | 0, 186 | 0, // flags 187 | 0, // encoding 188 | ...encodeWindows1252('image/tiff'), 189 | 0, // separator 190 | 3, // pic type 191 | ...encodeWindows1252('yo'), 192 | 0, // separator 193 | ...signature, 194 | ...imageContent, 195 | ]); 196 | deepStrictEqual(actual, expected); 197 | }); 198 | it('tiff 2', () => { 199 | const signature = [0x4d, 0x4d, 0, 0x2a]; 200 | const image = new Uint8Array(signature.concat(imageContent)); 201 | const writer = new ID3Writer(getEmptyBuffer()); 202 | writer.padding = 0; 203 | writer.setFrame('APIC', { 204 | type: 3, 205 | data: image.buffer, 206 | description: 'yo', 207 | }); 208 | writer.addTag(); 209 | const actual = new Uint8Array(writer.arrayBuffer); 210 | const expected = new Uint8Array([ 211 | ...id3Header, 212 | ...uint28ToUint7Array(36), // tag size without header 213 | ...encodeWindows1252('APIC'), 214 | ...uint32ToUint8Array(26), // frame size without header 215 | 0, 216 | 0, // flags 217 | 0, // encoding 218 | ...encodeWindows1252('image/tiff'), 219 | 0, // separator 220 | 3, // pic type 221 | ...encodeWindows1252('yo'), 222 | 0, // separator 223 | ...signature, 224 | ...imageContent, 225 | ]); 226 | deepStrictEqual(actual, expected); 227 | }); 228 | it('bmp', () => { 229 | const signature = [0x42, 0x4d]; 230 | const image = new Uint8Array(signature.concat(imageContent)); 231 | const writer = new ID3Writer(getEmptyBuffer()); 232 | writer.padding = 0; 233 | writer.setFrame('APIC', { 234 | type: 3, 235 | data: image.buffer, 236 | description: 'yo', 237 | }); 238 | writer.addTag(); 239 | const actual = new Uint8Array(writer.arrayBuffer); 240 | const expected = new Uint8Array([ 241 | ...id3Header, 242 | ...uint28ToUint7Array(33), // tag size without header 243 | ...encodeWindows1252('APIC'), 244 | ...uint32ToUint8Array(23), // frame size without header 245 | 0, 246 | 0, // flags 247 | 0, // encoding 248 | ...encodeWindows1252('image/bmp'), 249 | 0, // separator 250 | 3, // pic type 251 | ...encodeWindows1252('yo'), 252 | 0, // separator 253 | ...signature, 254 | ...imageContent, 255 | ]); 256 | deepStrictEqual(actual, expected); 257 | }); 258 | it('icon', () => { 259 | const signature = [0, 0, 1, 0]; 260 | const image = new Uint8Array(signature.concat(imageContent)); 261 | const writer = new ID3Writer(getEmptyBuffer()); 262 | writer.padding = 0; 263 | writer.setFrame('APIC', { 264 | type: 3, 265 | data: image.buffer, 266 | description: 'yo', 267 | }); 268 | writer.addTag(); 269 | const actual = new Uint8Array(writer.arrayBuffer); 270 | const expected = new Uint8Array([ 271 | ...id3Header, 272 | ...uint28ToUint7Array(38), // tag size without header 273 | ...encodeWindows1252('APIC'), 274 | ...uint32ToUint8Array(28), // frame size without header 275 | 0, 276 | 0, // flags 277 | 0, // encoding 278 | ...encodeWindows1252('image/x-icon'), 279 | 0, // separator 280 | 3, // pic type 281 | ...encodeWindows1252('yo'), 282 | 0, // separator 283 | ...signature, 284 | ...imageContent, 285 | ]); 286 | deepStrictEqual(actual, expected); 287 | }); 288 | it('Force Western encoding when description is empty', () => { 289 | const signature = [0, 0, 1, 0]; 290 | const image = new Uint8Array(signature.concat(imageContent)); 291 | const writer = new ID3Writer(getEmptyBuffer()); 292 | writer.padding = 0; 293 | writer.setFrame('APIC', { 294 | type: 3, 295 | data: image.buffer, 296 | description: '', 297 | useUnicodeEncoding: true, 298 | }); 299 | writer.addTag(); 300 | const actual = new Uint8Array(writer.arrayBuffer); 301 | const expected = new Uint8Array([ 302 | ...id3Header, 303 | ...uint28ToUint7Array(36), // tag size without header 304 | ...encodeWindows1252('APIC'), 305 | ...uint32ToUint8Array(26), // frame size without header 306 | 0, 307 | 0, // flags 308 | 0, // encoding 309 | ...encodeWindows1252('image/x-icon'), 310 | 0, // separator 311 | 3, // pic type 312 | 0, // separator 313 | ...signature, 314 | ...imageContent, 315 | ]); 316 | deepStrictEqual(actual, expected); 317 | }); 318 | it('Throw when value is not an object', () => { 319 | const writer = new ID3Writer(getEmptyBuffer()); 320 | throws(() => { 321 | writer.setFrame('APIC', 4512); 322 | }, /APIC frame value should be an object with keys type, data and description/); 323 | }); 324 | it('Throw when picture type is out of allowed range', () => { 325 | const writer = new ID3Writer(getEmptyBuffer()); 326 | throws(() => { 327 | writer.setFrame('APIC', { 328 | type: 43, 329 | data: new ArrayBuffer(20), 330 | description: '', 331 | }); 332 | }, /Incorrect APIC frame picture type/); 333 | }); 334 | it('Throw when mime type is not detected', () => { 335 | const writer = new ID3Writer(getEmptyBuffer()); 336 | throws(() => { 337 | writer.setFrame('APIC', { 338 | type: 0, 339 | data: getEmptyBuffer(), 340 | description: '', 341 | }); 342 | }, /Unknown picture MIME type/); 343 | }); 344 | }); 345 | -------------------------------------------------------------------------------- /test/object/COMM.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from '../utils.mjs'; 4 | import { encodeUtf16le, encodeWindows1252 } from '../../src/encoder.mjs'; 5 | import { 6 | uint28ToUint7Array, 7 | uint32ToUint8Array, 8 | } from '../../src/transform.mjs'; 9 | import { ID3Writer } from '../../dist/browser-id3-writer.mjs'; 10 | 11 | describe('COMM', () => { 12 | it('COMM', () => { 13 | const writer = new ID3Writer(getEmptyBuffer()); 14 | writer.padding = 0; 15 | writer.setFrame('COMM', { 16 | description: 'advert', 17 | text: 'free hugs', 18 | }); 19 | writer.addTag(); 20 | const actual = new Uint8Array(writer.arrayBuffer); 21 | const expected = new Uint8Array([ 22 | ...id3Header, 23 | ...uint28ToUint7Array(50), // tag size without header 24 | ...encodeWindows1252('COMM'), 25 | ...uint32ToUint8Array(40), // frame size without header 26 | 0, 27 | 0, // flags 28 | 1, // encoding 29 | ...encodeWindows1252('eng'), 30 | 0xff, 31 | 0xfe, // BOM 32 | ...encodeUtf16le('advert'), 33 | 0, 34 | 0, 35 | 0xff, 36 | 0xfe, // separator, BOM 37 | ...encodeUtf16le('free hugs'), 38 | ]); 39 | deepStrictEqual(actual, expected); 40 | }); 41 | it('Change language', () => { 42 | const writer = new ID3Writer(getEmptyBuffer()); 43 | writer.padding = 0; 44 | writer.setFrame('COMM', { 45 | language: 'jpn', 46 | description: 'この世界', 47 | text: '俺の名前', 48 | }); 49 | writer.addTag(); 50 | const actual = new Uint8Array(writer.arrayBuffer); 51 | const expected = new Uint8Array([ 52 | ...id3Header, 53 | ...uint28ToUint7Array(36), // tag size without header 54 | ...encodeWindows1252('COMM'), 55 | ...uint32ToUint8Array(26), // frame size without header 56 | 0, 57 | 0, // flags 58 | 1, // encoding 59 | ...encodeWindows1252('jpn'), 60 | 0xff, 61 | 0xfe, // BOM 62 | ...encodeUtf16le('この世界'), 63 | 0, 64 | 0, // separator 65 | 0xff, 66 | 0xfe, // BOM 67 | ...encodeUtf16le('俺の名前'), 68 | ]); 69 | deepStrictEqual(actual, expected); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/object/IPLS.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from '../utils.mjs'; 4 | import { encodeUtf16le, encodeWindows1252 } from '../../src/encoder.mjs'; 5 | import { 6 | uint28ToUint7Array, 7 | uint32ToUint8Array, 8 | } from '../../src/transform.mjs'; 9 | import { ID3Writer } from '../../dist/browser-id3-writer.mjs'; 10 | 11 | describe('IPLS', () => { 12 | it('IPLS', () => { 13 | const writer = new ID3Writer(getEmptyBuffer()); 14 | writer.padding = 0; 15 | writer.setFrame('IPLS', [ 16 | ['author', 'Thomas Bangalter'], 17 | ['author', 'Guy-Manuel de Homem-Christo'], 18 | ['mixer', 'DJ Falcon'], 19 | ]); 20 | writer.addTag(); 21 | const actual = new Uint8Array(writer.arrayBuffer); 22 | const expected = new Uint8Array([ 23 | ...id3Header, 24 | ...uint28ToUint7Array(173), // tag size without header 25 | ...encodeWindows1252('IPLS'), 26 | ...uint32ToUint8Array(163), // frame size without header 27 | 0, 28 | 0, // flags 29 | 1, // encoding 30 | 0xff, 31 | 0xfe, // BOM 32 | ...encodeUtf16le('author'), 33 | 0, 34 | 0, 35 | 0xff, 36 | 0xfe, // separator, BOM 37 | ...encodeUtf16le('Thomas Bangalter'), 38 | 0, 39 | 0, 40 | 0xff, 41 | 0xfe, // separator, BOM 42 | ...encodeUtf16le('author'), 43 | 0, 44 | 0, 45 | 0xff, 46 | 0xfe, // separator, BOM 47 | ...encodeUtf16le('Guy-Manuel de Homem-Christo'), 48 | 0, 49 | 0, 50 | 0xff, 51 | 0xfe, // separator, BOM 52 | ...encodeUtf16le('mixer'), 53 | 0, 54 | 0, 55 | 0xff, 56 | 0xfe, // separator, BOM 57 | ...encodeUtf16le('DJ Falcon'), 58 | 0, 59 | 0, // separator (EOF) 60 | ]); 61 | deepStrictEqual(actual, expected); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/object/PRIV.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from '../utils.mjs'; 4 | import { encodeWindows1252 } from '../../src/encoder.mjs'; 5 | import { 6 | uint28ToUint7Array, 7 | uint32ToUint8Array, 8 | } from '../../src/transform.mjs'; 9 | import { ID3Writer } from '../../dist/browser-id3-writer.mjs'; 10 | 11 | describe('PRIV', () => { 12 | it('PRIV', () => { 13 | const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]); 14 | const writer = new ID3Writer(getEmptyBuffer()); 15 | writer.padding = 0; 16 | writer.setFrame('PRIV', { 17 | id: 'site.com', 18 | data, 19 | }); 20 | writer.addTag(); 21 | const actual = new Uint8Array(writer.arrayBuffer); 22 | const expected = new Uint8Array([ 23 | ...id3Header, 24 | ...uint28ToUint7Array(28), // tag size without header 25 | ...encodeWindows1252('PRIV'), 26 | ...uint32ToUint8Array(18), // frame size without header 27 | 0, 28 | 0, // flags 29 | ...encodeWindows1252('site.com'), 30 | 0, // separator 31 | ...data, 32 | ]); 33 | deepStrictEqual(actual, expected); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/object/SYLT.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from '../utils.mjs'; 4 | import { encodeWindows1252, encodeUtf16le } from '../../src/encoder.mjs'; 5 | import { 6 | uint28ToUint7Array, 7 | uint32ToUint8Array, 8 | } from '../../src/transform.mjs'; 9 | import { ID3Writer } from '../../dist/browser-id3-writer.mjs'; 10 | 11 | describe('SYLT', () => { 12 | it('SYLT', () => { 13 | const writer = new ID3Writer(getEmptyBuffer()); 14 | writer.padding = 0; 15 | writer.setFrame('SYLT', { 16 | text: [ 17 | ["She's up all night 'til the sun", 1], 18 | ["I'm up all night to get some", 2], 19 | ["She's up all night for good fun", 3], 20 | ["I'm up all night to get lucky", 4], 21 | ], 22 | type: 1, 23 | timestampFormat: 2, 24 | language: 'eng', 25 | description: 'Description', 26 | }); 27 | writer.addTag(); 28 | const actual = new Uint8Array(writer.arrayBuffer); 29 | const expected = new Uint8Array([ 30 | ...id3Header, 31 | ...uint28ToUint7Array(312), // tag size without header 32 | ...encodeWindows1252('SYLT'), 33 | ...uint32ToUint8Array(302), // frame size without header 34 | 0, 35 | 0, // flags 36 | 1, // text encoding 37 | ...encodeWindows1252('eng'), // language 38 | 2, // Time stamp format 39 | 1, // Content type 40 | 0xff, 41 | 0xfe, // BOM 42 | ...encodeUtf16le('Description'), // description 43 | 0, 44 | 0, // separator 45 | 0xff, 46 | 0xfe, // BOM 47 | ...encodeUtf16le("She's up all night 'til the sun"), 48 | 0, 49 | 0, // separator 50 | 0, 51 | 0, 52 | 0, 53 | 1, // timestamp 54 | 0xff, 55 | 0xfe, // BOM 56 | ...encodeUtf16le("I'm up all night to get some"), 57 | 0, 58 | 0, // separator 59 | 0, 60 | 0, 61 | 0, 62 | 2, // timestamp 63 | 0xff, 64 | 0xfe, // BOM 65 | ...encodeUtf16le("She's up all night for good fun"), 66 | 0, 67 | 0, // separator 68 | 0, 69 | 0, 70 | 0, 71 | 3, // timestamp 72 | 0xff, 73 | 0xfe, // BOM 74 | ...encodeUtf16le("I'm up all night to get lucky"), 75 | 0, 76 | 0, // separator 77 | 0, 78 | 0, 79 | 0, 80 | 4, // timestamp 81 | ]); 82 | deepStrictEqual(actual, expected); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/object/TXXX.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual, throws } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from '../utils.mjs'; 4 | import { encodeUtf16le, encodeWindows1252 } from '../../src/encoder.mjs'; 5 | import { 6 | uint28ToUint7Array, 7 | uint32ToUint8Array, 8 | } from '../../src/transform.mjs'; 9 | import { ID3Writer } from '../../dist/browser-id3-writer.mjs'; 10 | 11 | describe('TXXX', () => { 12 | it('TXXX', () => { 13 | const writer = new ID3Writer(getEmptyBuffer()); 14 | writer.padding = 0; 15 | writer.setFrame('TXXX', { 16 | description: 'foo', 17 | value: 'bar', 18 | }); 19 | writer.addTag(); 20 | const actual = new Uint8Array(writer.arrayBuffer); 21 | const expected = new Uint8Array([ 22 | ...id3Header, 23 | ...uint28ToUint7Array(29), // tag size without header 24 | ...encodeWindows1252('TXXX'), 25 | ...uint32ToUint8Array(19), // frame size without header 26 | 0, 27 | 0, // flags 28 | 1, // encoding 29 | 0xff, 30 | 0xfe, // BOM 31 | ...encodeUtf16le('foo'), 32 | 0, 33 | 0, // separator 34 | 0xff, 35 | 0xfe, // BOM 36 | ...encodeUtf16le('bar'), 37 | ]); 38 | deepStrictEqual(actual, expected); 39 | }); 40 | it('Throw with simple string', () => { 41 | const writer = new ID3Writer(getEmptyBuffer()); 42 | throws(() => { 43 | writer.setFrame('TXXX', 'foobar'); 44 | }, /TXXX frame value should be an object with keys description and value/); 45 | }); 46 | it('Throw when no description provided', () => { 47 | const writer = new ID3Writer(getEmptyBuffer()); 48 | throws(() => { 49 | writer.setFrame('TXXX', { 50 | value: 'foobar', 51 | }); 52 | }, /TXXX frame value should be an object with keys description and value/); 53 | }); 54 | it('Throw when no value provided', () => { 55 | const writer = new ID3Writer(getEmptyBuffer()); 56 | throws(() => { 57 | writer.setFrame('TXXX', { 58 | description: 'foobar', 59 | }); 60 | }, /TXXX frame value should be an object with keys description and value/); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/object/USLT.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from '../utils.mjs'; 4 | import { encodeUtf16le, encodeWindows1252 } from '../../src/encoder.mjs'; 5 | import { 6 | uint28ToUint7Array, 7 | uint32ToUint8Array, 8 | } from '../../src/transform.mjs'; 9 | import { ID3Writer } from '../../dist/browser-id3-writer.mjs'; 10 | 11 | describe('USLT', () => { 12 | it('USLT', () => { 13 | const writer = new ID3Writer(getEmptyBuffer()); 14 | writer.padding = 0; 15 | writer.setFrame('USLT', { 16 | description: 'Ярл', 17 | lyrics: 'Лирика', 18 | }); 19 | writer.addTag(); 20 | const actual = new Uint8Array(writer.arrayBuffer); 21 | const expected = new Uint8Array([ 22 | ...id3Header, 23 | ...uint28ToUint7Array(38), // tag size without header 24 | ...encodeWindows1252('USLT'), 25 | ...uint32ToUint8Array(28), // frame size without header 26 | 0, 27 | 0, // flags 28 | 1, // encoding 29 | ...encodeWindows1252('eng'), 30 | 0xff, 31 | 0xfe, // BOM 32 | ...encodeUtf16le('Ярл'), 33 | 0, 34 | 0, // separator 35 | 0xff, 36 | 0xfe, // BOM 37 | ...encodeUtf16le('Лирика'), 38 | ]); 39 | deepStrictEqual(actual, expected); 40 | }); 41 | it('Change language', () => { 42 | const writer = new ID3Writer(getEmptyBuffer()); 43 | writer.padding = 0; 44 | writer.setFrame('USLT', { 45 | language: 'rus', 46 | description: 'Ярл', 47 | lyrics: 'Лирика', 48 | }); 49 | writer.addTag(); 50 | const actual = new Uint8Array(writer.arrayBuffer); 51 | const expected = new Uint8Array([ 52 | ...id3Header, 53 | ...uint28ToUint7Array(38), // tag size without header 54 | ...encodeWindows1252('USLT'), 55 | ...uint32ToUint8Array(28), // frame size without header 56 | 0, 57 | 0, // flags 58 | 1, // encoding 59 | ...encodeWindows1252('rus'), 60 | 0xff, 61 | 0xfe, // BOM 62 | ...encodeUtf16le('Ярл'), 63 | 0, 64 | 0, // separator 65 | 0xff, 66 | 0xfe, // BOM 67 | ...encodeUtf16le('Лирика'), 68 | ]); 69 | deepStrictEqual(actual, expected); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/string.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual } from 'assert'; 3 | import { getEmptyBuffer, id3Header } from './utils.mjs'; 4 | import { encodeUtf16le, encodeWindows1252 } from '../src/encoder.mjs'; 5 | import { uint28ToUint7Array, uint32ToUint8Array } from '../src/transform.mjs'; 6 | import { ID3Writer } from '../dist/browser-id3-writer.mjs'; 7 | 8 | const oneByteEncodedFrames = ['TDAT']; 9 | const twoByteEncodedFrames = [ 10 | 'TLAN', 11 | 'TIT1', 12 | 'TIT2', 13 | 'TIT3', 14 | 'TALB', 15 | 'TPE2', 16 | 'TPE3', 17 | 'TPE4', 18 | 'TRCK', 19 | 'TPOS', 20 | 'TPUB', 21 | 'TKEY', 22 | 'TMED', 23 | 'TSRC', 24 | 'TCOP', 25 | 'TEXT', 26 | ]; 27 | const urlLinkFrames = [ 28 | 'WCOM', 29 | 'WCOP', 30 | 'WOAF', 31 | 'WOAR', 32 | 'WOAS', 33 | 'WORS', 34 | 'WPAY', 35 | 'WPUB', 36 | ]; 37 | 38 | describe('Frames: URL link', () => { 39 | urlLinkFrames.forEach((frameName) => { 40 | it(frameName, () => { 41 | const writer = new ID3Writer(getEmptyBuffer()); 42 | writer.padding = 0; 43 | writer.setFrame(frameName, 'https://google.com'); 44 | writer.addTag(); 45 | const actual = new Uint8Array(writer.arrayBuffer); 46 | const expected = new Uint8Array([ 47 | ...id3Header, 48 | ...uint28ToUint7Array(28), // tag size without header 49 | ...encodeWindows1252(frameName), 50 | ...uint32ToUint8Array(18), // frame size without header 51 | 0, 52 | 0, // flags 53 | ...encodeWindows1252('https://google.com'), 54 | ]); 55 | deepStrictEqual(actual, expected); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('Frames: one byte encoded string', () => { 61 | oneByteEncodedFrames.forEach((frameName) => { 62 | it(frameName, () => { 63 | const writer = new ID3Writer(getEmptyBuffer()); 64 | writer.padding = 0; 65 | writer.setFrame(frameName, 'Lyricist/Text writer'); 66 | writer.addTag(); 67 | const actual = new Uint8Array(writer.arrayBuffer); 68 | const expected = new Uint8Array([ 69 | ...id3Header, 70 | ...uint28ToUint7Array(31), // tag size without header 71 | ...encodeWindows1252(frameName), 72 | ...uint32ToUint8Array(21), // frame size without header 73 | 0, 74 | 0, // flags 75 | 0, // encoding 76 | ...encodeWindows1252('Lyricist/Text writer'), 77 | ]); 78 | deepStrictEqual(actual, expected); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('Frames: two byte encoded string', () => { 84 | twoByteEncodedFrames.forEach((frameName) => { 85 | it(frameName, () => { 86 | const writer = new ID3Writer(getEmptyBuffer()); 87 | writer.padding = 0; 88 | writer.setFrame(frameName, 'Lyricist/Text writer'); 89 | writer.addTag(); 90 | const actual = new Uint8Array(writer.arrayBuffer); 91 | const expected = new Uint8Array([ 92 | ...id3Header, 93 | ...uint28ToUint7Array(53), // tag size without header 94 | ...encodeWindows1252(frameName), 95 | ...uint32ToUint8Array(43), // frame size without header 96 | 0, 97 | 0, // flags 98 | 1, // encoding 99 | 0xff, 100 | 0xfe, // BOM 101 | ...encodeUtf16le('Lyricist/Text writer'), 102 | ]); 103 | deepStrictEqual(actual, expected); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/transform.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { deepStrictEqual } from 'assert'; 3 | import { 4 | uint32ToUint8Array, 5 | uint28ToUint7Array, 6 | uint7ArrayToUint28, 7 | } from '../src/transform.mjs'; 8 | 9 | describe('transform', () => { 10 | describe('uint32ToUint8Array', () => { 11 | it('255 - only first byte', () => { 12 | const actual = uint32ToUint8Array(255); 13 | const expected = [0, 0, 0, 255]; 14 | deepStrictEqual(actual, expected); 15 | }); 16 | it('256 - second byte', () => { 17 | const actual = uint32ToUint8Array(256); 18 | const expected = [0, 0, 1, 0]; 19 | deepStrictEqual(actual, expected); 20 | }); 21 | it('257 - second + first bytes', () => { 22 | const actual = uint32ToUint8Array(257); 23 | const expected = [0, 0, 1, 1]; 24 | deepStrictEqual(actual, expected); 25 | }); 26 | it('2 ** 16', () => { 27 | const actual = uint32ToUint8Array(2 ** 16); 28 | const expected = [0, 1, 0, 0]; 29 | deepStrictEqual(actual, expected); 30 | }); 31 | it('2 ** 24', () => { 32 | const actual = uint32ToUint8Array(2 ** 24); 33 | const expected = [1, 0, 0, 0]; 34 | deepStrictEqual(actual, expected); 35 | }); 36 | it('max (2**32)-1', () => { 37 | const actual = uint32ToUint8Array(2 ** 32 - 1); 38 | const expected = [255, 255, 255, 255]; 39 | deepStrictEqual(actual, expected); 40 | }); 41 | it('overflow on 2**32', () => { 42 | const actual = uint32ToUint8Array(2 ** 32); 43 | const expected = [0, 0, 0, 0]; 44 | deepStrictEqual(actual, expected); 45 | }); 46 | }); 47 | 48 | describe('uint28ToUint7Array', () => { 49 | it('127 - only first byte', () => { 50 | const actual = uint28ToUint7Array(127); 51 | const expected = [0, 0, 0, 127]; 52 | deepStrictEqual(actual, expected); 53 | }); 54 | it('128 - second byte', () => { 55 | const actual = uint28ToUint7Array(128); 56 | const expected = [0, 0, 1, 0]; 57 | deepStrictEqual(actual, expected); 58 | }); 59 | it('129 - second + first bytes', () => { 60 | const actual = uint28ToUint7Array(129); 61 | const expected = [0, 0, 1, 1]; 62 | deepStrictEqual(actual, expected); 63 | }); 64 | it('2**14', () => { 65 | const actual = uint28ToUint7Array(2 ** 14); 66 | const expected = [0, 1, 0, 0]; 67 | deepStrictEqual(actual, expected); 68 | }); 69 | it('2**21', () => { 70 | const actual = uint28ToUint7Array(2 ** 21); 71 | const expected = [1, 0, 0, 0]; 72 | deepStrictEqual(actual, expected); 73 | }); 74 | it('max (2**28)-1', () => { 75 | const actual = uint28ToUint7Array(2 ** 28 - 1); 76 | const expected = [127, 127, 127, 127]; 77 | deepStrictEqual(actual, expected); 78 | }); 79 | it('overflow on 2**28', () => { 80 | const actual = uint28ToUint7Array(2 ** 28); 81 | const expected = [0, 0, 0, 0]; 82 | deepStrictEqual(actual, expected); 83 | }); 84 | }); 85 | 86 | describe('uint7ArrayToUint28', () => { 87 | it('127', () => { 88 | const actual = uint7ArrayToUint28([0, 0, 0, 127]); 89 | const expected = 127; 90 | deepStrictEqual(actual, expected); 91 | }); 92 | it('128', () => { 93 | const actual = uint7ArrayToUint28([0, 0, 1, 0]); 94 | const expected = 128; 95 | deepStrictEqual(actual, expected); 96 | }); 97 | it('129', () => { 98 | const actual = uint7ArrayToUint28([0, 0, 1, 1]); 99 | const expected = 129; 100 | deepStrictEqual(actual, expected); 101 | }); 102 | it('2 ** 14', () => { 103 | const actual = uint7ArrayToUint28([0, 1, 0, 0]); 104 | const expected = 2 ** 14; 105 | deepStrictEqual(actual, expected); 106 | }); 107 | it('2 ** 21', () => { 108 | const actual = uint7ArrayToUint28([1, 0, 0, 0]); 109 | const expected = 2 ** 21; 110 | deepStrictEqual(actual, expected); 111 | }); 112 | it('max (2**28)-1', () => { 113 | const actual = uint7ArrayToUint28([127, 127, 127, 127]); 114 | const expected = 2 ** 28 - 1; 115 | deepStrictEqual(actual, expected); 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /test/utils.mjs: -------------------------------------------------------------------------------- 1 | export function getEmptyBuffer() { 2 | return new ArrayBuffer(0); 3 | } 4 | 5 | export const id3Header = [ 6 | 73, 7 | 68, 8 | 51, // ID3 magic nubmer 9 | 3, 10 | 0, // version 11 | 0, // flags 12 | ]; 13 | -------------------------------------------------------------------------------- /tools/distSize.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { gzipSync } from 'zlib'; 3 | 4 | const bytesToKiB = (bytes) => (bytes / 1024).toFixed(2); 5 | 6 | const filePath = 'dist/browser-id3-writer.mjs'; 7 | const distFile = readFileSync(filePath); 8 | const size = bytesToKiB(distFile.byteLength); 9 | const gzipSize = bytesToKiB(gzipSync(distFile).byteLength); 10 | 11 | console.log(`Size of "${filePath}" is ${size} KiB (gzip ${gzipSize} KiB)`); 12 | --------------------------------------------------------------------------------