# Browser ID3 Writer

JavaScript library for writing [ID3 (v2.3)](https://egoroof.github.io/browser-id3-writer/spec/) tag to MP3 files in browsers and Node.js.
It can't read the tag so use another lib to do it.

**Note**: the library removes existing ID3 tag (v2.2, v2.3 and v2.4).

Here is an online demonstration: [egoroof.github.io/browser-id3-writer/](https://egoroof.github.io/browser-id3-writer/)

Find the changelog in [CHANGELOG.md](https://github.com/egoroof/browser-id3-writer/blob/master/CHANGELOG.md)

## Table of Contents

- [Installation](#installation)
  - [JS modules](#js-modules)
- [Usage](#usage)
  - [Browser](#browser)
    1. [Get ArrayBuffer of song](#get-arraybuffer-of-song)
    2. [Add a tag](#add-a-tag)
    3. [Save file](#save-file)
  - [Node.js](#nodejs)
- [Supported frames](#supported-frames)
- [APIC picture types](#apic-picture-types)
- [SYLT content types](#sylt-content-types)
- [SYLT timestamp formats](#sylt-timestamp-formats)

## Installation

Take latest version [here](https://unpkg.com/browser-id3-writer) or with [npm](https://www.npmjs.com/package/browser-id3-writer):

```
npm install browser-id3-writer --save
```

### JS modules

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`:

```html
<script type="module">
  import { ID3Writer } from 'https://your-host/browser-id3-writer.mjs';
  // your code here..
</script>
```

Or bundle the library to your code.

In Nodejs it imports easily:

```js
import { ID3Writer } from 'browser-id3-writer';
```

## Usage

### Browser

#### Get ArrayBuffer of song

In browsers you should first get
[ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer)
of the song you would like to add ID3 tag.

For example you can create file input and add `change` event listener:

```html
<input type="file" id="file" accept="audio/mpeg" />
<script type="module">
  import { ID3Writer } from 'https://your-host/browser-id3-writer.mjs';

  document.getElementById('file').addEventListener('change', async (e) => {
    for (const file of e.target.files) {
      const arrayBuffer = await file.arrayBuffer();
      // go next
    }
  });
</script>
```

If you want to get arrayBuffer from a remote server you can use
[Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API):

```js
const request = await fetch(urlToSongFile);
if (!request.ok) {
  // handle error
  console.error(`Unable to fetch ${urlToSongFile}`);
}
const arrayBuffer = await request.arrayBuffer();
// go next
```

#### Add a tag

Create a new `ID3Writer` instance with arrayBuffer of your song, set frames and add a tag:

```js
// arrayBuffer of song or empty arrayBuffer if you just want only id3 tag without song
const writer = new ID3Writer(arrayBuffer);
writer
  .setFrame('TIT2', 'Home')
  .setFrame('TPE1', ['Eminem', '50 Cent'])
  .setFrame('TALB', 'Friday Night Lights')
  .setFrame('TYER', 2004)
  .setFrame('TRCK', '6/8')
  .setFrame('TCON', ['Soundtrack'])
  .setFrame('TBPM', 128)
  .setFrame('WPAY', 'https://google.com')
  .setFrame('TKEY', 'Fbm')
  .setFrame('APIC', {
    type: 3,
    data: coverArrayBuffer,
    description: 'Super picture',
  });
writer.addTag();
```

#### Save file

Now you can save it to file as you want:

```js
const taggedSongBuffer = writer.arrayBuffer;
const blob = writer.getBlob();

// or if you need url
const url = URL.createObjectURL(blob);
```

For example you can save file using [FileSaver.js](https://github.com/eligrey/FileSaver.js/):

```js
saveAs(blob, 'song with tags.mp3');
```

If you are writing chromium extension you can save file using
[Downloads API](https://developer.chrome.com/docs/extensions/reference/api/downloads):

```js
chrome.downloads.download({
  url: url,
  filename: 'song with tags.mp3',
});

// when finish downloading - clean memory
URL.revokeObjectURL(url);
```

### Node.js

Simple example with blocking IO:

```js
import { ID3Writer } from 'browser-id3-writer';
import { readFileSync, writeFileSync } from 'fs';

const songBuffer = readFileSync('path_to_song.mp3');
const coverBuffer = readFileSync('path_to_cover.jpg');

const writer = new ID3Writer(songBuffer);
writer
  .setFrame('TIT2', 'Home')
  .setFrame('TPE1', ['Eminem', '50 Cent'])
  .setFrame('TALB', 'Friday Night Lights')
  .setFrame('TYER', 2004)
  .setFrame('APIC', {
    type: 3,
    data: coverBuffer,
    description: 'Super picture',
  });
writer.addTag();

const taggedSongBuffer = Buffer.from(writer.arrayBuffer);
writeFileSync('song_with_tags.mp3', taggedSongBuffer);
```

You can also create only ID3 tag without song and use it as you want:

```js
const writer = new ID3Writer(Buffer.alloc(0));
writer.padding = 0; // default 4096
writer.setFrame('TIT2', 'Home');
writer.addTag();
const id3Buffer = Buffer.from(writer.arrayBuffer);
```

## Supported frames

**array of strings:**

- TPE1 (song artists)
- TCOM (song composers)
- TCON (song genres)

**string**

- TLAN (language)
- TIT1 (content group description)
- TIT2 (song title)
- TIT3 (song subtitle)
- TALB (album title)
- TPE2 (album artist)
- TPE3 (conductor/performer refinement)
- TPE4 (interpreted, remixed, or otherwise modified by)
- TRCK (song number in album): '5' or '5/10'
- TPOS (album disc number): '1' or '1/3'
- TPUB (label name)
- TKEY (initial key)
- TMED (media type)
- TDAT (album release date expressed as 'DDMM')
- TSRC (isrc - international standard recording code)
- TSSE (software/hardware and settings used for encoding)
- TCOP (copyright message)
- TCMP (iTunes compilation flag)
- TEXT (lyricist / text writer)
- WCOM (commercial information)
- WCOP (copyright/Legal information)
- WOAF (official audio file webpage)
- WOAR (official artist/performer webpage)
- WOAS (official audio source webpage)
- WORS (official internet radio station homepage)
- WPAY (payment)
- WPUB (publishers official webpage)

**integer**

- TLEN (song duration in milliseconds)
- TYER (album release year)
- TBPM (beats per minute)

**object**

- COMM (comments):

```js
writer.setFrame('COMM', {
  description: 'description here',
  text: 'text here',
  language: 'eng',
});
```

- USLT (unsychronised lyrics):

```js
writer.setFrame('USLT', {
  description: 'description here',
  lyrics: 'lyrics here',
  language: 'eng',
});
```

- IPLS (involved people list):

```js
writer.setFrame('IPLS', [
  ['role', 'name'],
  ['role', 'name'],
  // ...
]);
```

- SYLT (synchronised lyrics):

```js
writer.setFrame('SYLT', {
  type: 1,
  text: [
    ['lyrics here', 0],
    ['lyrics here', 3500],
    // ...
  ],
  timestampFormat: 2,
  language: 'eng',
  description: 'description',
});
```

`text` is an array of arrays of string and integer.

- TXXX (user defined text):

```js
writer.setFrame('TXXX', {
  description: 'description here',
  value: 'value here',
});
```

- WXXX (user defined URL):

```js
writer.setFrame('WXXX', {
  description: 'description here',
  value: 'https://google.com',
  useUnicodeEncoding: false,
});
```

The URL is always encoded as ISO-8859-1, as the spec requires. Only the
description follows `useUnicodeEncoding`, which behaves like it does on APIC:
set it to `true` only when the description contains non-Western characters,
as some readers apply the encoding to the URL as well and will not read it
back correctly.

- CHAP (chapter) and CTOC (table of contents):

```js
writer
  .setFrame('CHAP', {
    id: 'chp0',
    startTime: 0, // milliseconds
    endTime: 3500,
    startOffset: 0, // bytes, optional
    endOffset: 1024,
    subFrames: {
      TIT2: 'Intro',
      WXXX: { description: 'chapter url', value: 'https://google.com' },
    },
  })
  .setFrame('CTOC', {
    id: 'toc0',
    ordered: true,
    topLevel: true,
    childElementIds: ['chp0'],
    subFrames: { TIT2: 'Table of contents' },
  });
```

Both are defined by the
[ID3v2 Chapter Frame Addendum](https://id3.org/id3v2-chapters-1.0) rather than
the main spec. `subFrames` is optional and keyed by frame id; `TIT2`, `TIT3`,
`TXXX`, `WXXX` and `APIC` may be embedded. A `CTOC` frame can hold at most 255
entries because the entry count is stored in a single byte.

`startOffset` and `endOffset` are optional. Byte offsets are often not known
and stop being correct as soon as the audio is re-encoded, so when they are
omitted they are written as `0xFFFFFFFF`, which the spec defines as "ignore
this and use the time instead".

- PRIV (private frame):

```js
writer.setFrame('PRIV', {
  id: 'identifier',
  data: dataArrayBuffer,
});
```

- APIC (attached picture):

```js
writer.setFrame('APIC', {
  type: 3,
  data: coverArrayBuffer,
  description: 'description here',
  useUnicodeEncoding: false,
});
```

`useUnicodeEncoding` should only be `true` when description contains non-Western characters.
When it's set to `true` some program might not be able to read the picture correctly.
See [#42](https://github.com/egoroof/browser-id3-writer/issues/42).

## APIC picture types

| Type | Name                                |
| ---- | ----------------------------------- |
| 0    | Other                               |
| 1    | 32x32 pixels 'file icon' (PNG only) |
| 2    | Other file icon                     |
| 3    | Cover (front)                       |
| 4    | Cover (back)                        |
| 5    | Leaflet page                        |
| 6    | Media (e.g. label side of CD)       |
| 7    | Lead artist/lead performer/soloist  |
| 8    | Artist/performer                    |
| 9    | Conductor                           |
| 10   | Band/Orchestra                      |
| 11   | Composer                            |
| 12   | Lyricist/text writer                |
| 13   | Recording location                  |
| 14   | During recording                    |
| 15   | During performance                  |
| 16   | Movie/video screen capture          |
| 17   | A bright coloured fish              |
| 18   | Illustration                        |
| 19   | Band/artist logotype                |
| 20   | Publisher/Studio logotype           |

## SYLT content types

| Type | Name                                         |
| ---- | -------------------------------------------- |
| 0    | Other                                        |
| 1    | Lyrics                                       |
| 2    | Text transcription                           |
| 3    | Movement/part name (e.g. "Adagio")           |
| 4    | Events (e.g. "Don Quijote enters the stage") |
| 5    | Chord (e.g. "Bb F Fsus")                     |
| 6    | Trivia/'pop up' information                  |

## SYLT timestamp formats

| Type | Name                                                    |
| ---- | ------------------------------------------------------- |
| 1    | Absolute time, 32 bit sized, using MPEG frames as unit  |
| 2    | Absolute time, 32 bit sized, using milliseconds as unit |
