├── .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 |
--------------------------------------------------------------------------------