From f992d1e1b515714242499ed091910a97390a1625 Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Mon, 17 Jun 2019 23:12:49 +0100
Subject: [PATCH 01/12] the big 'n
---
.editorconfig | 7 +
.eslintrc.json | 25 +
.gitignore | 3 +-
.prettierrc.json | 10 +
examples/test-nodejs.js | 8 -
examples/test.html | 23 -
lib/dataview-extra.js | 103 ----
lib/genres.js | 151 ------
lib/id3frame.js | 314 -----------
lib/id3tag.js | 148 ------
lib/reader.js | 158 ------
package-lock.json | 1057 ++++++++++++++++++++++++++++++++++++++
package.json | 23 +-
src/browserFileReader.ts | 41 ++
src/genres.ts | 153 ++++++
src/id3.ts | 59 +++
src/id3Frame.ts | 410 +++++++++++++++
src/id3Tag.ts | 178 +++++++
src/index.ts | 1 +
src/localReader.ts | 95 ++++
src/reader.ts | 50 ++
src/remoteReader.ts | 40 ++
src/util.ts | 174 +++++++
tsconfig.json | 21 +
24 files changed, 2344 insertions(+), 908 deletions(-)
create mode 100644 .editorconfig
create mode 100644 .eslintrc.json
create mode 100644 .prettierrc.json
delete mode 100644 examples/test-nodejs.js
delete mode 100644 examples/test.html
delete mode 100644 lib/dataview-extra.js
delete mode 100644 lib/genres.js
delete mode 100644 lib/id3frame.js
delete mode 100644 lib/id3tag.js
delete mode 100644 lib/reader.js
create mode 100644 package-lock.json
create mode 100644 src/browserFileReader.ts
create mode 100644 src/genres.ts
create mode 100644 src/id3.ts
create mode 100644 src/id3Frame.ts
create mode 100644 src/id3Tag.ts
create mode 100644 src/index.ts
create mode 100644 src/localReader.ts
create mode 100644 src/reader.ts
create mode 100644 src/remoteReader.ts
create mode 100644 src/util.ts
create mode 100644 tsconfig.json
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..a18586b
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*]
+end_of_line = lf
+indent_size = 2
+indent_style = space
+trim_trailing_whitespace = true
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..210c831
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": [
+ "google",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": 2017,
+ "sourceType": "module"
+ },
+ "plugins": [
+ "@typescript-eslint"
+ ],
+ "rules": {
+ "no-unused-vars": "off",
+ "indent": "off",
+ "comma-dangle": ["error", "never"],
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/indent": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/explicit-function-return-type": ["error", {
+ "allowExpressions": true
+ }]
+ }
+}
diff --git a/.gitignore b/.gitignore
index bbbf928..bf634b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
-build/
+lib/
+node_modules/
*.swp
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..fba4260
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,10 @@
+{
+ "bracketSpacing": false,
+ "printWidth": 80,
+ "semi": true,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "trailingComma": "none",
+ "useTabs": false,
+ "arrowParens": "always"
+}
diff --git a/examples/test-nodejs.js b/examples/test-nodejs.js
deleted file mode 100644
index 1a8e2a6..0000000
--- a/examples/test-nodejs.js
+++ /dev/null
@@ -1,8 +0,0 @@
-var id3 = require('id3js');
-
-id3({ file:'./track.mp3', type: 'local' }, function(err, tags) {
- /*
- * 'local' type causes the file to be read from the local file-system
- */
- console.log(tags);
-});
diff --git a/examples/test.html b/examples/test.html
deleted file mode 100644
index 81d2ad8..0000000
--- a/examples/test.html
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
- Test
-
-
-
-
-
-
-
-
-
diff --git a/lib/dataview-extra.js b/lib/dataview-extra.js
deleted file mode 100644
index 4c9d8f7..0000000
--- a/lib/dataview-extra.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * dataview-extra.js
- * 43081j
- * License: MIT, see LICENSE
- */
-DataView.prototype.getString = function(length, offset, raw) {
- offset = offset || 0;
- length = length || (this.byteLength - offset);
- if(length < 0) {
- length += this.byteLength;
- }
- var str = '';
- if(typeof Buffer !== 'undefined') {
- var data = [];
- for(var i = offset; i < (offset + length); i++) {
- data.push(this.getUint8(i));
- }
- return (new Buffer(data)).toString();
- } else {
- for(var i = offset; i < (offset + length); i++) {
- str += String.fromCharCode(this.getUint8(i));
- }
- if(raw) {
- return str;
- }
- return decodeURIComponent(escape(str));
- }
-};
-
-DataView.prototype.getStringUtf16 = function(length, offset, bom) {
- offset = offset || 0;
- length = length || (this.byteLength - offset);
- var littleEndian = false,
- str = '',
- useBuffer = false;
- if(typeof Buffer !== 'undefined') {
- str = [];
- useBuffer = true;
- }
- if(length < 0) {
- length += this.byteLength;
- }
- if(bom) {
- var bomInt = this.getUint16(offset);
- if(bomInt === 0xFFFE) {
- littleEndian = true;
- }
- offset += 2;
- length -= 2;
- }
- for(var i = offset; i < (offset + length); i += 2) {
- var ch = this.getUint16(i, littleEndian);
- if((ch >= 0 && ch <= 0xD7FF) || (ch >= 0xE000 && ch <= 0xFFFF)) {
- if(useBuffer) {
- str.push(ch);
- } else {
- str += String.fromCharCode(ch);
- }
- } else if(ch >= 0x10000 && ch <= 0x10FFFF) {
- ch -= 0x10000;
- if(useBuffer) {
- str.push(((0xFFC00 & ch) >> 10) + 0xD800);
- str.push((0x3FF & ch) + 0xDC00);
- } else {
- str += String.fromCharCode(((0xFFC00 & ch) >> 10) + 0xD800) + String.fromCharCode((0x3FF & ch) + 0xDC00);
- }
- }
- }
- if(useBuffer) {
- return str.toString();
- } else {
- return decodeURIComponent(escape(str));
- }
-};
-
-DataView.prototype.getSynch = function(num) {
- var out = 0,
- mask = 0x7f000000;
- while(mask) {
- out >>= 1;
- out |= num & mask;
- mask >>= 8;
- }
- return out;
-};
-
-DataView.prototype.getUint8Synch = function(offset) {
- return this.getSynch(this.getUint8(offset));
-};
-
-DataView.prototype.getUint32Synch = function(offset) {
- return this.getSynch(this.getUint32(offset));
-};
-
-/*
- * Not really an int as such, but named for consistency
- */
-DataView.prototype.getUint24 = function(offset, littleEndian) {
- if(littleEndian) {
- return this.getUint8(offset) + (this.getUint8(offset + 1) << 8) + (this.getUint8(offset + 2) << 16);
- }
- return this.getUint8(offset + 2) + (this.getUint8(offset + 1) << 8) + (this.getUint8(offset) << 16);
-};
diff --git a/lib/genres.js b/lib/genres.js
deleted file mode 100644
index 417a753..0000000
--- a/lib/genres.js
+++ /dev/null
@@ -1,151 +0,0 @@
-var Genres = [
- 'Blues',
- 'Classic Rock',
- 'Country',
- 'Dance',
- 'Disco',
- 'Funk',
- 'Grunge',
- 'Hip-Hop',
- 'Jazz',
- 'Metal',
- 'New Age',
- 'Oldies',
- 'Other',
- 'Pop',
- 'R&B',
- 'Rap',
- 'Reggae',
- 'Rock',
- 'Techno',
- 'Industrial',
- 'Alternative',
- 'Ska',
- 'Death Metal',
- 'Pranks',
- 'Soundtrack',
- 'Euro-Techno',
- 'Ambient',
- 'Trip-Hop',
- 'Vocal',
- 'Jazz+Funk',
- 'Fusion',
- 'Trance',
- 'Classical',
- 'Instrumental',
- 'Acid',
- 'House',
- 'Game',
- 'Sound Clip',
- 'Gospel',
- 'Noise',
- 'AlternRock',
- 'Bass',
- 'Soul',
- 'Punk',
- 'Space',
- 'Meditative',
- 'Instrumental Pop',
- 'Instrumental Rock',
- 'Ethnic',
- 'Gothic',
- 'Darkwave',
- 'Techno-Industrial',
- 'Electronic',
- 'Pop-Folk',
- 'Eurodance',
- 'Dream',
- 'Southern Rock',
- 'Comedy',
- 'Cult',
- 'Gangsta Rap',
- 'Top 40',
- 'Christian Rap',
- 'Pop / Funk',
- 'Jungle',
- 'Native American',
- 'Cabaret',
- 'New Wave',
- 'Psychedelic',
- 'Rave',
- 'Showtunes',
- 'Trailer',
- 'Lo-Fi',
- 'Tribal',
- 'Acid Punk',
- 'Acid Jazz',
- 'Polka',
- 'Retro',
- 'Musical',
- 'Rock & Roll',
- 'Hard Rock',
- 'Folk',
- 'Folk-Rock',
- 'National Folk',
- 'Swing',
- 'Fast Fusion',
- 'Bebob',
- 'Latin',
- 'Revival',
- 'Celtic',
- 'Bluegrass',
- 'Avantgarde',
- 'Gothic Rock',
- 'Progressive Rock',
- 'Psychedelic Rock',
- 'Symphonic Rock',
- 'Slow Rock',
- 'Big Band',
- 'Chorus',
- 'Easy Listening',
- 'Acoustic',
- 'Humour',
- 'Speech',
- 'Chanson',
- 'Opera',
- 'Chamber Music',
- 'Sonata',
- 'Symphony',
- 'Booty Bass',
- 'Primus',
- 'Porn Groove',
- 'Satire',
- 'Slow Jam',
- 'Club',
- 'Tango',
- 'Samba',
- 'Folklore',
- 'Ballad',
- 'Power Ballad',
- 'Rhythmic Soul',
- 'Freestyle',
- 'Duet',
- 'Punk Rock',
- 'Drum Solo',
- 'A Cappella',
- 'Euro-House',
- 'Dance Hall',
- 'Goa',
- 'Drum & Bass',
- 'Club-House',
- 'Hardcore',
- 'Terror',
- 'Indie',
- 'BritPop',
- 'Negerpunk',
- 'Polsk Punk',
- 'Beat',
- 'Christian Gangsta Rap',
- 'Heavy Metal',
- 'Black Metal',
- 'Crossover',
- 'Contemporary Christian',
- 'Christian Rock',
- 'Merengue',
- 'Salsa',
- 'Thrash Metal',
- 'Anime',
- 'JPop',
- 'Synthpop',
- 'Rock/Pop'
-];
diff --git a/lib/id3frame.js b/lib/id3frame.js
deleted file mode 100644
index 548ffcf..0000000
--- a/lib/id3frame.js
+++ /dev/null
@@ -1,314 +0,0 @@
-var ID3Frame = {};
-
-/*
- * ID3v2.3 and later frame types
- */
-ID3Frame.types = {
- /*
- * Textual frames
- */
- 'TALB': 'album',
- 'TBPM': 'bpm',
- 'TCOM': 'composer',
- 'TCON': 'genre',
- 'TCOP': 'copyright',
- 'TDEN': 'encoding-time',
- 'TDLY': 'playlist-delay',
- 'TDOR': 'original-release-time',
- 'TDRC': 'recording-time',
- 'TDRL': 'release-time',
- 'TDTG': 'tagging-time',
- 'TENC': 'encoder',
- 'TEXT': 'writer',
- 'TFLT': 'file-type',
- 'TIPL': 'involved-people',
- 'TIT1': 'content-group',
- 'TIT2': 'title',
- 'TIT3': 'subtitle',
- 'TKEY': 'initial-key',
- 'TLAN': 'language',
- 'TLEN': 'length',
- 'TMCL': 'credits',
- 'TMED': 'media-type',
- 'TMOO': 'mood',
- 'TOAL': 'original-album',
- 'TOFN': 'original-filename',
- 'TOLY': 'original-writer',
- 'TOPE': 'original-artist',
- 'TOWN': 'owner',
- 'TPE1': 'artist',
- 'TPE2': 'band',
- 'TPE3': 'conductor',
- 'TPE4': 'remixer',
- 'TPOS': 'set-part',
- 'TPRO': 'produced-notice',
- 'TPUB': 'publisher',
- 'TRCK': 'track',
- 'TRSN': 'radio-name',
- 'TRSO': 'radio-owner',
- 'TSOA': 'album-sort',
- 'TSOP': 'performer-sort',
- 'TSOT': 'title-sort',
- 'TSRC': 'isrc',
- 'TSSE': 'encoder-settings',
- 'TSST': 'set-subtitle',
- /*
- * Textual frames (<=2.2)
- */
- 'TAL': 'album',
- 'TBP': 'bpm',
- 'TCM': 'composer',
- 'TCO': 'genre',
- 'TCR': 'copyright',
- 'TDY': 'playlist-delay',
- 'TEN': 'encoder',
- 'TFT': 'file-type',
- 'TKE': 'initial-key',
- 'TLA': 'language',
- 'TLE': 'length',
- 'TMT': 'media-type',
- 'TOA': 'original-artist',
- 'TOF': 'original-filename',
- 'TOL': 'original-writer',
- 'TOT': 'original-album',
- 'TP1': 'artist',
- 'TP2': 'band',
- 'TP3': 'conductor',
- 'TP4': 'remixer',
- 'TPA': 'set-part',
- 'TPB': 'publisher',
- 'TRC': 'isrc',
- 'TRK': 'track',
- 'TSS': 'encoder-settings',
- 'TT1': 'content-group',
- 'TT2': 'title',
- 'TT3': 'subtitle',
- 'TXT': 'writer',
- /*
- * URL frames
- */
- 'WCOM': 'url-commercial',
- 'WCOP': 'url-legal',
- 'WOAF': 'url-file',
- 'WOAR': 'url-artist',
- 'WOAS': 'url-source',
- 'WORS': 'url-radio',
- 'WPAY': 'url-payment',
- 'WPUB': 'url-publisher',
- /*
- * URL frames (<=2.2)
- */
- 'WAF': 'url-file',
- 'WAR': 'url-artist',
- 'WAS': 'url-source',
- 'WCM': 'url-commercial',
- 'WCP': 'url-copyright',
- 'WPB': 'url-publisher',
- /*
- * Comment frame
- */
- 'COMM': 'comments',
- /*
- * Image frame
- */
- 'APIC': 'image',
- 'PIC': 'image'
-};
-
-/*
- * ID3 image types
- */
-ID3Frame.imageTypes = [
- 'other',
- 'file-icon',
- 'icon',
- 'cover-front',
- 'cover-back',
- 'leaflet',
- 'media',
- 'artist-lead',
- 'artist',
- 'conductor',
- 'band',
- 'composer',
- 'writer',
- 'location',
- 'during-recording',
- 'during-performance',
- 'screen',
- 'fish',
- 'illustration',
- 'logo-band',
- 'logo-publisher'
-];
-
-/*
- * ID3v2.3 and later
- */
-ID3Frame.parse = function(buffer, major, minor) {
- minor = minor || 0;
- major = major || 4;
- var result = {tag: null, value: null},
- dv = new DataView(buffer);
- if(major < 3) {
- return ID3Frame.parseLegacy(buffer);
- }
- var header = {
- id: dv.getString(4),
- type: dv.getString(1),
- size: dv.getUint32Synch(4),
- flags: [
- dv.getUint8(8),
- dv.getUint8(9)
- ]
- };
- /*
- * No support for compressed, unsychronised, etc frames
- */
- if(header.flags[1] !== 0) {
- return false;
- }
- if(!header.id in ID3Frame.types) {
- return false;
- }
- result.tag = ID3Frame.types[header.id];
- if(header.type === 'T') {
- var encoding = dv.getUint8(10);
- /*
- * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
- */
- if(encoding === 0 || encoding === 3) {
- result.value = dv.getString(-11, 11);
- } else if(encoding === 1) {
- result.value = dv.getStringUtf16(-11, 11, true);
- } else if(encoding === 2) {
- result.value = dv.getStringUtf16(-11, 11);
- } else {
- return false;
- }
- if(header.id === 'TCON' && !!parseInt(result.value)) {
- result.value = Genres[parseInt(result.value)];
- }
- } else if(header.type === 'W') {
- result.value = dv.getString(-10, 10);
- } else if(header.id === 'COMM') {
- /*
- * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
- */
- var encoding = dv.getUint8(10),
- variableStart = 14, variableLength = 0;
- /*
- * Skip the comment description and retrieve only the comment its self
- */
- for(var i = variableStart;; i++) {
- if(encoding === 1 || encoding === 2) {
- if(dv.getUint16(i) === 0x0000) {
- variableStart = i + 2;
- break;
- }
- i++;
- } else {
- if(dv.getUint8(i) === 0x00) {
- variableStart = i + 1;
- break;
- }
- }
- }
- if(encoding === 0 || encoding === 3) {
- result.value = dv.getString(-1 * variableStart, variableStart);
- } else if(encoding === 1) {
- result.value = dv.getStringUtf16(-1 * variableStart, variableStart, true);
- } else if(encoding === 2) {
- result.value = dv.getStringUtf16(-1 * variableStart, variableStart);
- } else {
- return false;
- }
- } else if(header.id === 'APIC') {
- var encoding = dv.getUint8(10),
- image = {
- type: null,
- mime: null,
- description: null,
- data: null
- };
- var variableStart = 11, variableLength = 0;
- for(var i = variableStart;;i++) {
- if(dv.getUint8(i) === 0x00) {
- variableLength = i - variableStart;
- break;
- }
- }
- image.mime = dv.getString(variableLength, variableStart);
- image.type = ID3Frame.imageTypes[dv.getUint8(variableStart + variableLength + 1)] || 'other';
- variableStart += variableLength + 2;
- variableLength = 0;
- for(var i = variableStart;; i++) {
- if(dv.getUint8(i) === 0x00) {
- variableLength = i - variableStart;
- break;
- }
- }
- image.description = (variableLength === 0 ? null : dv.getString(variableLength, variableStart));
- image.data = buffer.slice(variableStart + 1);
- result.value = image;
- }
- return (result.tag ? result : false);
-};
-
-/*
- * ID3v2.2 and earlier
- */
-ID3Frame.parseLegacy = function(buffer) {
- var result = {tag: null, value: null},
- dv = new DataView(buffer),
- header = {
- id: dv.getString(3),
- type: dv.getString(1),
- size: dv.getUint24(3)
- };
- if(!header.id in ID3Frame.types) {
- return false;
- }
- result.tag = ID3Frame.types[header.id];
- if(header.type === 'T') {
- var encoding = dv.getUint8(7);
- /*
- * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
- */
- result.value = dv.getString(-7, 7);
- if(header.id === 'TCO' && !!parseInt(result.value)) {
- result.value = Genres[parseInt(result.value)];
- }
- } else if(header.type === 'W') {
- result.value = dv.getString(-7, 7);
- } else if(header.id === 'COM') {
- /*
- * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
- */
- var encoding = dv.getUint8(6);
- result.value = dv.getString(-10, 10);
- if(result.value.indexOf('\x00') !== -1) {
- result.value = result.value.substr(result.value.indexOf('\x00') + 1);
- }
- } else if(header.id === 'PIC') {
- var encoding = dv.getUint8(6),
- image = {
- type: null,
- mime: 'image/' + dv.getString(3, 7).toLowerCase(),
- description: null,
- data: null
- };
- image.type = ID3Frame.imageTypes[dv.getUint8(11)] || 'other';
- var variableStart = 11, variableLength = 0;
- for(var i = variableStart;; i++) {
- if(dv.getUint8(i) === 0x00) {
- variableLength = i - variableStart;
- break;
- }
- }
- image.description = (variableLength === 0 ? null : dv.getString(variableLength, variableStart));
- image.data = buffer.slice(variableStart + 1);
- result.value = image;
- }
- return (result.tag ? result : false);
-};
diff --git a/lib/id3tag.js b/lib/id3tag.js
deleted file mode 100644
index 1c1f77c..0000000
--- a/lib/id3tag.js
+++ /dev/null
@@ -1,148 +0,0 @@
-var ID3Tag = {};
-
-ID3Tag.parse = function(handle, callback) {
- var tags = {
- title: null,
- album: null,
- artist: null,
- year: null,
- v1: {
- title: null,
- artist: null,
- album: null,
- year: null,
- comment: null,
- track: null,
- version: 1.0
- },
- v2: {
- version: [null, null]
- }
- },
- processed = {
- v1: false,
- v2: false
- },
- process = function() {
- if(processed.v1 && processed.v2) {
- tags.title = tags.v2.title || tags.v1.title;
- tags.album = tags.v2.album || tags.v1.album;
- tags.artist = tags.v2.artist || tags.v1.artist;
- tags.year = tags.v1.year;
- callback(null, tags);
- }
- };
- /*
- * Read the last 128 bytes (ID3v1)
- */
- handle.read(128, handle.size - 128, function(err, buffer) {
- if(err) {
- return callback('Could not read file');
- }
- var dv = new DataView(buffer);
- if(buffer.byteLength !== 128 || dv.getString(3, null, true) !== 'TAG') {
- processed.v1 = true;
- return process();
- }
- tags.v1.title = dv.getString(30, 3).replace(/(^\s+|\s+$)/, '') || null;
- tags.v1.artist = dv.getString(30, 33).replace(/(^\s+|\s+$)/, '') || null;
- tags.v1.album = dv.getString(30, 63).replace(/(^\s+|\s+$)/, '') || null;
- tags.v1.year = dv.getString(4, 93).replace(/(^\s+|\s+$)/, '') || null;
- /*
- * If there is a zero byte at [125], the comment is 28 bytes and the remaining 2 are [0, trackno]
- */
- if(dv.getUint8(125) === 0) {
- tags.v1.comment = dv.getString(28, 97).replace(/(^\s+|\s+$)/, '');
- tags.v1.version = 1.1;
- tags.v1.track = dv.getUint8(126);
- } else {
- tags.v1.comment = dv.getString(30, 97).replace(/(^\s+|\s+$)/, '');
- }
- /*
- * Lookup the genre index in the predefined genres array
- */
- tags.v1.genre = Genres[dv.getUint8(127)] || null;
- processed.v1 = true;
- process();
- });
- /*
- * Read 14 bytes (10 for ID3v2 header, 4 for possible extended header size)
- * Assuming the ID3v2 tag is prepended
- */
- handle.read(14, 0, function(err, buffer) {
- if(err) {
- return callback('Could not read file');
- }
- var dv = new DataView(buffer),
- headerSize = 10,
- tagSize = 0,
- tagFlags;
- /*
- * Be sure that the buffer is at least the size of an id3v2 header
- * Assume incompatibility if a major version of > 4 is used
- */
- if(buffer.byteLength !== 14 || dv.getString(3, null, true) !== 'ID3' || dv.getUint8(3) > 4) {
- processed.v2 = true;
- return process();
- }
- tags.v2.version = [
- dv.getUint8(3),
- dv.getUint8(4)
- ];
- tagFlags = dv.getUint8(5);
- /*
- * Do not support unsynchronisation
- */
- if((tagFlags & 0x80) !== 0) {
- processed.v2 = true;
- return process();
- }
- /*
- * Increment the header size to offset by if an extended header exists
- */
- if((tagFlags & 0x40) !== 0) {
- headerSize += dv.getUint32Synch(11);
- }
- /*
- * Calculate the tag size to be read
- */
- tagSize += dv.getUint32Synch(6);
- handle.read(tagSize, headerSize, function(err, buffer) {
- if(err) {
- processed.v2 = true;
- return process();
- }
- var dv = new DataView(buffer),
- position = 0;
- while(position < buffer.byteLength) {
- var frame,
- slice,
- frameBit,
- isFrame = true;
- for(var i = 0; i < 3; i++) {
- frameBit = dv.getUint8(position + i);
- if((frameBit < 0x41 || frameBit > 0x5A) && (frameBit < 0x30 || frameBit > 0x39)) {
- isFrame = false;
- }
- }
- if(!isFrame) break;
- /*
- * < v2.3, frame ID is 3 chars, size is 3 bytes making a total size of 6 bytes
- * >= v2.3, frame ID is 4 chars, size is 4 bytes, flags are 2 bytes, total 10 bytes
- */
- if(tags.v2.version[0] < 3) {
- slice = buffer.slice(position, position + 6 + dv.getUint24(position + 3));
- } else {
- slice = buffer.slice(position, position + 10 + dv.getUint32Synch(position + 4));
- }
- frame = ID3Frame.parse(slice, tags.v2.version[0]);
- if(frame) {
- tags.v2[frame.tag] = frame.value;
- }
- position += slice.byteLength;
- }
- processed.v2 = true;
- process();
- });
- });
-};
diff --git a/lib/reader.js b/lib/reader.js
deleted file mode 100644
index 19d4b4c..0000000
--- a/lib/reader.js
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Reader.js
- * A unified reader interface for AJAX, local and File API access
- * 43081j
- * License: MIT, see LICENSE
- */
-var Reader = function(type) {
- this.type = type || Reader.OPEN_URI;
- this.size = null;
- this.file = null;
-};
-
-Reader.OPEN_FILE = 1;
-Reader.OPEN_URI = 2;
-Reader.OPEN_LOCAL = 3;
-
-Reader.prototype.open = function(file, callback) {
- this.file = file;
- var self = this;
- switch(this.type) {
- case Reader.OPEN_LOCAL:
- fs.stat(this.file, function(err, stat) {
- if(err) {
- return callback(err);
- }
- self.size = stat.size;
- fs.open(self.file, 'r', function(err, fd) {
- if(err) {
- return callback(err);
- }
- self.fd = fd;
- callback();
- });
- });
- break;
- case Reader.OPEN_FILE:
- this.size = this.file.size;
- callback();
- break;
- default:
- this.ajax(
- {
- uri: this.file,
- type: 'HEAD',
- },
- function(err, resp, xhr) {
- if(err) {
- return callback(err);
- }
- self.size = parseInt(xhr.getResponseHeader('Content-Length'));
- callback();
- }
- );
- break;
- }
-};
-
-Reader.prototype.close = function() {
- if(this.type === Reader.OPEN_LOCAL) {
- fs.close(this.fd);
- }
-};
-
-Reader.prototype.read = function(length, position, callback) {
- if(this.type === Reader.OPEN_LOCAL) {
- this.readLocal(length, position, callback);
- } else if(this.type === Reader.OPEN_FILE) {
- this.readFile(length, position, callback);
- } else {
- this.readUri(length, position, callback);
- }
-};
-
-/*
- * Local reader
- */
-Reader.prototype.readLocal = function(length, position, callback) {
- var buffer = new Buffer(length);
- fs.read(this.fd, buffer, 0, length, position, function(err, bytesRead, buffer) {
- if(err) {
- return callback(err);
- }
- var ab = new ArrayBuffer(buffer.length),
- view = new Uint8Array(ab);
- for(var i = 0; i < buffer.length; i++) {
- view[i] = buffer[i];
- }
- callback(null, ab);
- });
-};
-
-/*
- * URL reader
- */
-Reader.prototype.ajax = function(opts, callback) {
- var options = {
- type: 'GET',
- uri: null,
- responseType: 'text'
- };
- if(typeof opts === 'string') {
- opts = {uri: opts};
- }
- for(var k in opts) {
- options[k] = opts[k];
- }
- var xhr = new XMLHttpRequest();
- xhr.onreadystatechange = function() {
- if(xhr.readyState !== 4) return;
- if(xhr.status !== 200 && xhr.status !== 206) {
- return callback('Received non-200/206 response (' + xhr.status + ')');
- }
- callback(null, xhr.response, xhr);
- };
- xhr.responseType = options.responseType;
- xhr.open(options.type, options.uri, true);
- if(options.range) {
- options.range = [].concat(options.range);
- if(options.range.length === 2) {
- xhr.setRequestHeader('Range', 'bytes=' + options.range[0] + '-' + options.range[1]);
- } else {
- xhr.setRequestHeader('Range', 'bytes=' + options.range[0]);
- }
- }
- xhr.send();
-};
-
-Reader.prototype.readUri = function(length, position, callback) {
- this.ajax(
- {
- uri: this.file,
- type: 'GET',
- responseType: 'arraybuffer',
- range: [position, position+length-1]
- },
- function(err, buffer) {
- if(err) {
- return callback(err);
- }
- return callback(null, buffer);
- }
- );
-};
-
-/*
- * File API reader
- */
-Reader.prototype.readFile = function(length, position, callback) {
- var slice = this.file.slice(position, position+length),
- fr = new FileReader();
- fr.onload = function(e) {
- callback(null, e.target.result);
- };
- fr.onerror = function(e) {
- callback('File read failed');
- };
- fr.readAsArrayBuffer(slice);
-};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..d9d926b
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1057 @@
+{
+ "name": "id3js",
+ "version": "1.1.3",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
+ "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.0.0"
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
+ "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.0.0",
+ "esutils": "^2.0.2",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@types/eslint-visitor-keys": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+ "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "12.0.8",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.8.tgz",
+ "integrity": "sha512-b8bbUOTwzIY3V5vDTY1fIJ+ePKDUBqt2hC2woVGotdQQhG/2Sh62HOKHrT7ab+VerXAcPyAiTEipPu/FsreUtg==",
+ "dev": true
+ },
+ "@typescript-eslint/eslint-plugin": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.10.2.tgz",
+ "integrity": "sha512-7449RhjE1oLFIy5E/5rT4wG5+KsfPzakJuhvpzXJ3C46lq7xywY0/Rjo9ZBcwrfbk0nRZ5xmUHkk7DZ67tSBKw==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/experimental-utils": "1.10.2",
+ "eslint-utils": "^1.3.1",
+ "functional-red-black-tree": "^1.0.1",
+ "regexpp": "^2.0.1",
+ "tsutils": "^3.7.0"
+ }
+ },
+ "@typescript-eslint/experimental-utils": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.10.2.tgz",
+ "integrity": "sha512-Hf5lYcrnTH5Oc67SRrQUA7KuHErMvCf5RlZsyxXPIT6AXa8fKTyfFO6vaEnUmlz48RpbxO4f0fY3QtWkuHZNjg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/typescript-estree": "1.10.2",
+ "eslint-scope": "^4.0.0"
+ }
+ },
+ "@typescript-eslint/parser": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.10.2.tgz",
+ "integrity": "sha512-xWDWPfZfV0ENU17ermIUVEVSseBBJxKfqBcRCMZ8nAjJbfA5R7NWMZmFFHYnars5MjK4fPjhu4gwQv526oZIPQ==",
+ "dev": true,
+ "requires": {
+ "@types/eslint-visitor-keys": "^1.0.0",
+ "@typescript-eslint/experimental-utils": "1.10.2",
+ "@typescript-eslint/typescript-estree": "1.10.2",
+ "eslint-visitor-keys": "^1.0.0"
+ }
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.10.2.tgz",
+ "integrity": "sha512-Kutjz0i69qraOsWeI8ETqYJ07tRLvD9URmdrMoF10bG8y8ucLmPtSxROvVejWvlJUGl2et/plnMiKRDW+rhEhw==",
+ "dev": true,
+ "requires": {
+ "lodash.unescape": "4.0.1",
+ "semver": "5.5.0"
+ }
+ },
+ "acorn": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz",
+ "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==",
+ "dev": true
+ },
+ "acorn-jsx": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz",
+ "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==",
+ "dev": true
+ },
+ "ajv": {
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
+ "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^2.0.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-escapes": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+ "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "astral-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
+ "cli-cursor": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+ "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^2.0.0"
+ }
+ },
+ "cli-width": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+ "dev": true
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "requires": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+ "dev": true
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "eslint": {
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
+ "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "ajv": "^6.9.1",
+ "chalk": "^2.1.0",
+ "cross-spawn": "^6.0.5",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "eslint-scope": "^4.0.3",
+ "eslint-utils": "^1.3.1",
+ "eslint-visitor-keys": "^1.0.0",
+ "espree": "^5.0.1",
+ "esquery": "^1.0.1",
+ "esutils": "^2.0.2",
+ "file-entry-cache": "^5.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob": "^7.1.2",
+ "globals": "^11.7.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "inquirer": "^6.2.2",
+ "js-yaml": "^3.13.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.3.0",
+ "lodash": "^4.17.11",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.1",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.8.2",
+ "path-is-inside": "^1.0.2",
+ "progress": "^2.0.0",
+ "regexpp": "^2.0.1",
+ "semver": "^5.5.1",
+ "strip-ansi": "^4.0.0",
+ "strip-json-comments": "^2.0.1",
+ "table": "^5.2.3",
+ "text-table": "^0.2.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
+ "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-config-google": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.13.0.tgz",
+ "integrity": "sha512-ELgMdOIpn0CFdsQS+FuxO+Ttu4p+aLaXHv9wA9yVnzqlUGV7oN/eRRnJekk7TCur6Cu2FXX0fqfIXRBaM14lpQ==",
+ "dev": true
+ },
+ "eslint-scope": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+ "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.1.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "eslint-utils": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
+ "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
+ "dev": true
+ },
+ "eslint-visitor-keys": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+ "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
+ "dev": true
+ },
+ "espree": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz",
+ "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==",
+ "dev": true,
+ "requires": {
+ "acorn": "^6.0.7",
+ "acorn-jsx": "^5.0.0",
+ "eslint-visitor-keys": "^1.0.0"
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "esquery": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+ "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^4.0.0"
+ }
+ },
+ "esrecurse": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^4.1.0"
+ }
+ },
+ "estraverse": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+ "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+ "dev": true
+ },
+ "external-editor": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz",
+ "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==",
+ "dev": true,
+ "requires": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ }
+ },
+ "fast-deep-equal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+ "dev": true
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "figures": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+ "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "file-entry-cache": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
+ "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^2.0.1"
+ }
+ },
+ "flat-cache": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
+ "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+ "dev": true,
+ "requires": {
+ "flatted": "^2.0.0",
+ "rimraf": "2.6.3",
+ "write": "1.0.3"
+ }
+ },
+ "flatted": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz",
+ "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==",
+ "dev": true
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
+ "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
+ },
+ "import-fresh": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz",
+ "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "inquirer": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz",
+ "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^3.2.0",
+ "chalk": "^2.4.2",
+ "cli-cursor": "^2.1.0",
+ "cli-width": "^2.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^2.0.0",
+ "lodash": "^4.17.11",
+ "mute-stream": "0.0.7",
+ "run-async": "^2.2.0",
+ "rxjs": "^6.4.0",
+ "string-width": "^2.1.0",
+ "strip-ansi": "^5.1.0",
+ "through": "^2.3.6"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "is-promise": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ }
+ },
+ "lodash": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+ "dev": true
+ },
+ "lodash.unescape": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz",
+ "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
+ "dev": true
+ },
+ "mimic-fn": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "mute-stream": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+ "dev": true
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
+ "nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "onetime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+ "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^1.0.0"
+ }
+ },
+ "optionator": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+ "dev": true,
+ "requires": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.4",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "wordwrap": "~1.0.0"
+ }
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+ "dev": true
+ },
+ "prettier": {
+ "version": "1.18.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz",
+ "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
+ "dev": true
+ },
+ "progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true
+ },
+ "regexpp": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+ "dev": true
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
+ },
+ "restore-cursor": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+ "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+ "dev": true,
+ "requires": {
+ "onetime": "^2.0.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "run-async": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+ "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+ "dev": true,
+ "requires": {
+ "is-promise": "^2.1.0"
+ }
+ },
+ "rxjs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz",
+ "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
+ "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
+ "dev": true
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+ "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "astral-regex": "^1.0.0",
+ "is-fullwidth-code-point": "^2.0.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "table": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/table/-/table-5.4.1.tgz",
+ "integrity": "sha512-E6CK1/pZe2N75rGZQotFOdmzWQ1AILtgYbMAbAjvms0S1l5IDB47zG3nCnFGB/w+7nB3vKofbLXCH7HPBo864w==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.9.1",
+ "lodash": "^4.17.11",
+ "slice-ansi": "^2.1.0",
+ "string-width": "^3.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
+ },
+ "tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ },
+ "tslib": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
+ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
+ "dev": true
+ },
+ "tsutils": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.0.tgz",
+ "integrity": "sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ }
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2"
+ }
+ },
+ "typescript": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz",
+ "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==",
+ "dev": true
+ },
+ "uri-js": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "write": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
+ "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
+ "dev": true,
+ "requires": {
+ "mkdirp": "^0.5.1"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index a5adaed..318f624 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,8 @@
"version": "1.1.3",
"author": "43081j",
"description": "A modern ID3 parser written completely in JavaScript, making use of typed arrays and the HTML5 File API",
- "main": "./dist/id3.js",
+ "main": "./lib/index.js",
+ "type": "module",
"repository": {
"type": "git",
"url": "https://github.com/43081j/id3.git"
@@ -13,5 +14,23 @@
"mp3",
"parser"
],
- "license": "MIT"
+ "files": [
+ "lib/**/*.js",
+ "lib/**/*.d.ts"
+ ],
+ "license": "MIT",
+ "devDependencies": {
+ "@types/node": "^12.0.8",
+ "@typescript-eslint/eslint-plugin": "^1.10.2",
+ "@typescript-eslint/parser": "^1.10.2",
+ "eslint": "^5.16.0",
+ "eslint-config-google": "^0.13.0",
+ "prettier": "^1.18.2",
+ "typescript": "^3.5.2"
+ },
+ "scripts": {
+ "lint": "eslint \"src/**/*.ts\"",
+ "build": "npm run lint && tsc",
+ "format": "prettier --write \"src/**/*.ts\""
+ }
}
diff --git a/src/browserFileReader.ts b/src/browserFileReader.ts
new file mode 100644
index 0000000..1885f21
--- /dev/null
+++ b/src/browserFileReader.ts
@@ -0,0 +1,41 @@
+import {Reader} from './reader.js';
+
+/**
+ * Reads a `File` instance
+ */
+export class BrowserFileReader extends Reader {
+ protected _file: File;
+
+ /**
+ * @param {File} file File to read
+ */
+ public constructor(file: File) {
+ super();
+
+ this._file = file;
+ }
+
+ /** @inheritdoc */
+ public async open(): Promise {
+ this.size = this._file.size;
+ }
+
+ /** @inheritdoc */
+ public async read(length: number, position: number): Promise {
+ const slice = this._file.slice(position, position + length);
+
+ return new Promise((resolve, reject) => {
+ const fr = new FileReader();
+
+ fr.onload = () => {
+ resolve(fr.result as ArrayBuffer);
+ };
+
+ fr.onerror = () => {
+ reject(new Error('File read failed'));
+ };
+
+ fr.readAsArrayBuffer(slice);
+ });
+ }
+}
diff --git a/src/genres.ts b/src/genres.ts
new file mode 100644
index 0000000..bc0e1b2
--- /dev/null
+++ b/src/genres.ts
@@ -0,0 +1,153 @@
+const genres = [
+ 'Blues',
+ 'Classic Rock',
+ 'Country',
+ 'Dance',
+ 'Disco',
+ 'Funk',
+ 'Grunge',
+ 'Hip-Hop',
+ 'Jazz',
+ 'Metal',
+ 'New Age',
+ 'Oldies',
+ 'Other',
+ 'Pop',
+ 'R&B',
+ 'Rap',
+ 'Reggae',
+ 'Rock',
+ 'Techno',
+ 'Industrial',
+ 'Alternative',
+ 'Ska',
+ 'Death Metal',
+ 'Pranks',
+ 'Soundtrack',
+ 'Euro-Techno',
+ 'Ambient',
+ 'Trip-Hop',
+ 'Vocal',
+ 'Jazz+Funk',
+ 'Fusion',
+ 'Trance',
+ 'Classical',
+ 'Instrumental',
+ 'Acid',
+ 'House',
+ 'Game',
+ 'Sound Clip',
+ 'Gospel',
+ 'Noise',
+ 'AlternRock',
+ 'Bass',
+ 'Soul',
+ 'Punk',
+ 'Space',
+ 'Meditative',
+ 'Instrumental Pop',
+ 'Instrumental Rock',
+ 'Ethnic',
+ 'Gothic',
+ 'Darkwave',
+ 'Techno-Industrial',
+ 'Electronic',
+ 'Pop-Folk',
+ 'Eurodance',
+ 'Dream',
+ 'Southern Rock',
+ 'Comedy',
+ 'Cult',
+ 'Gangsta Rap',
+ 'Top 40',
+ 'Christian Rap',
+ 'Pop / Funk',
+ 'Jungle',
+ 'Native American',
+ 'Cabaret',
+ 'New Wave',
+ 'Psychedelic',
+ 'Rave',
+ 'Showtunes',
+ 'Trailer',
+ 'Lo-Fi',
+ 'Tribal',
+ 'Acid Punk',
+ 'Acid Jazz',
+ 'Polka',
+ 'Retro',
+ 'Musical',
+ 'Rock & Roll',
+ 'Hard Rock',
+ 'Folk',
+ 'Folk-Rock',
+ 'National Folk',
+ 'Swing',
+ 'Fast Fusion',
+ 'Bebob',
+ 'Latin',
+ 'Revival',
+ 'Celtic',
+ 'Bluegrass',
+ 'Avantgarde',
+ 'Gothic Rock',
+ 'Progressive Rock',
+ 'Psychedelic Rock',
+ 'Symphonic Rock',
+ 'Slow Rock',
+ 'Big Band',
+ 'Chorus',
+ 'Easy Listening',
+ 'Acoustic',
+ 'Humour',
+ 'Speech',
+ 'Chanson',
+ 'Opera',
+ 'Chamber Music',
+ 'Sonata',
+ 'Symphony',
+ 'Booty Bass',
+ 'Primus',
+ 'Porn Groove',
+ 'Satire',
+ 'Slow Jam',
+ 'Club',
+ 'Tango',
+ 'Samba',
+ 'Folklore',
+ 'Ballad',
+ 'Power Ballad',
+ 'Rhythmic Soul',
+ 'Freestyle',
+ 'Duet',
+ 'Punk Rock',
+ 'Drum Solo',
+ 'A Cappella',
+ 'Euro-House',
+ 'Dance Hall',
+ 'Goa',
+ 'Drum & Bass',
+ 'Club-House',
+ 'Hardcore',
+ 'Terror',
+ 'Indie',
+ 'BritPop',
+ 'Negerpunk',
+ 'Polsk Punk',
+ 'Beat',
+ 'Christian Gangsta Rap',
+ 'Heavy Metal',
+ 'Black Metal',
+ 'Crossover',
+ 'Contemporary Christian',
+ 'Christian Rock',
+ 'Merengue',
+ 'Salsa',
+ 'Thrash Metal',
+ 'Anime',
+ 'JPop',
+ 'Synthpop',
+ 'Rock/Pop'
+];
+
+export default genres;
diff --git a/src/id3.ts b/src/id3.ts
new file mode 100644
index 0000000..efa1607
--- /dev/null
+++ b/src/id3.ts
@@ -0,0 +1,59 @@
+import {ID3Tag, parse} from './id3Tag.js';
+import {BrowserFileReader} from './browserFileReader.js';
+import {RemoteReader} from './remoteReader.js';
+import {Reader} from './reader.js';
+
+const SUPPORTS_FILE =
+ typeof window !== 'undefined' &&
+ 'File' in window &&
+ 'FileReader' in window &&
+ typeof ArrayBuffer !== 'undefined';
+
+/**
+ * Parses ID3 tags from a given reader
+ * @param {Reader} reader Reader to use
+ * @return {Promise}
+ */
+export async function fromReader(reader: Reader): Promise {
+ await reader.open();
+
+ const tags = await parse(reader);
+
+ await reader.close();
+
+ return tags;
+}
+
+/**
+ * Parses ID3 tags from a local path
+ * @param {string} path Path to file
+ * @return {Promise}
+ */
+export async function fromPath(path: string): Promise {
+ const mod = await import('./localReader.js');
+ return fromReader(new mod.LocalReader(path));
+}
+
+/**
+ * Parses ID3 tags from a specified URL
+ * @param {string} url URL to retrieve data from
+ * @return {Promise}
+ */
+export function fromUrl(url: string): Promise {
+ return fromReader(new RemoteReader(url));
+}
+
+/**
+ * Parses ID3 tags from a File instance
+ * @param {File} file File to parse
+ * @return {Promise}
+ */
+export function fromFile(file: File): Promise {
+ if (!SUPPORTS_FILE) {
+ throw new Error(
+ 'Browser does not have support for the File API and/or ' + 'ArrayBuffers'
+ );
+ }
+
+ return fromReader(new BrowserFileReader(file));
+}
diff --git a/src/id3Frame.ts b/src/id3Frame.ts
new file mode 100644
index 0000000..2d106b9
--- /dev/null
+++ b/src/id3Frame.ts
@@ -0,0 +1,410 @@
+import genres from './genres.js';
+import {getString, getUint24, getUint32Synch, getStringUtf16} from './util.js';
+
+export interface ID3Frame {
+ tag: string | null;
+ value: unknown | null;
+}
+
+export interface ImageValue {
+ type: null | string;
+ mime: null | string;
+ description: null | string;
+ data: null | ArrayBuffer;
+}
+
+export const types: ReadonlyMap = new Map([
+ /*
+ * Textual frames
+ */
+ ['TALB', 'album'],
+ ['TBPM', 'bpm'],
+ ['TCOM', 'composer'],
+ ['TCON', 'genre'],
+ ['TCOP', 'copyright'],
+ ['TDEN', 'encoding-time'],
+ ['TDLY', 'playlist-delay'],
+ ['TDOR', 'original-release-time'],
+ ['TDRC', 'recording-time'],
+ ['TDRL', 'release-time'],
+ ['TDTG', 'tagging-time'],
+ ['TENC', 'encoder'],
+ ['TEXT', 'writer'],
+ ['TFLT', 'file-type'],
+ ['TIPL', 'involved-people'],
+ ['TIT1', 'content-group'],
+ ['TIT2', 'title'],
+ ['TIT3', 'subtitle'],
+ ['TKEY', 'initial-key'],
+ ['TLAN', 'language'],
+ ['TLEN', 'length'],
+ ['TMCL', 'credits'],
+ ['TMED', 'media-type'],
+ ['TMOO', 'mood'],
+ ['TOAL', 'original-album'],
+ ['TOFN', 'original-filename'],
+ ['TOLY', 'original-writer'],
+ ['TOPE', 'original-artist'],
+ ['TOWN', 'owner'],
+ ['TPE1', 'artist'],
+ ['TPE2', 'band'],
+ ['TPE3', 'conductor'],
+ ['TPE4', 'remixer'],
+ ['TPOS', 'set-part'],
+ ['TPRO', 'produced-notice'],
+ ['TPUB', 'publisher'],
+ ['TRCK', 'track'],
+ ['TRSN', 'radio-name'],
+ ['TRSO', 'radio-owner'],
+ ['TSOA', 'album-sort'],
+ ['TSOP', 'performer-sort'],
+ ['TSOT', 'title-sort'],
+ ['TSRC', 'isrc'],
+ ['TSSE', 'encoder-settings'],
+ ['TSST', 'set-subtitle'],
+ /*
+ * Textual frames (<=2.2)
+ */
+ ['TAL', 'album'],
+ ['TBP', 'bpm'],
+ ['TCM', 'composer'],
+ ['TCO', 'genre'],
+ ['TCR', 'copyright'],
+ ['TDY', 'playlist-delay'],
+ ['TEN', 'encoder'],
+ ['TFT', 'file-type'],
+ ['TKE', 'initial-key'],
+ ['TLA', 'language'],
+ ['TLE', 'length'],
+ ['TMT', 'media-type'],
+ ['TOA', 'original-artist'],
+ ['TOF', 'original-filename'],
+ ['TOL', 'original-writer'],
+ ['TOT', 'original-album'],
+ ['TP1', 'artist'],
+ ['TP2', 'band'],
+ ['TP3', 'conductor'],
+ ['TP4', 'remixer'],
+ ['TPA', 'set-part'],
+ ['TPB', 'publisher'],
+ ['TRC', 'isrc'],
+ ['TRK', 'track'],
+ ['TSS', 'encoder-settings'],
+ ['TT1', 'content-group'],
+ ['TT2', 'title'],
+ ['TT3', 'subtitle'],
+ ['TXT', 'writer'],
+ /*
+ * URL frames
+ */
+ ['WCOM', 'url-commercial'],
+ ['WCOP', 'url-legal'],
+ ['WOAF', 'url-file'],
+ ['WOAR', 'url-artist'],
+ ['WOAS', 'url-source'],
+ ['WORS', 'url-radio'],
+ ['WPAY', 'url-payment'],
+ ['WPUB', 'url-publisher'],
+ /*
+ * URL frames (<=2.2)
+ */
+ ['WAF', 'url-file'],
+ ['WAR', 'url-artist'],
+ ['WAS', 'url-source'],
+ ['WCM', 'url-commercial'],
+ ['WCP', 'url-copyright'],
+ ['WPB', 'url-publisher'],
+ /*
+ * Comment frame
+ */
+ ['COMM', 'comments'],
+ /*
+ * Image frame
+ */
+ ['APIC', 'image'],
+ ['PIC', 'image'],
+ /*
+ * Private frames
+ */
+ ['PRIV', 'private']
+]);
+
+export const imageTypes = [
+ 'other',
+ 'file-icon',
+ 'icon',
+ 'cover-front',
+ 'cover-back',
+ 'leaflet',
+ 'media',
+ 'artist-lead',
+ 'artist',
+ 'conductor',
+ 'band',
+ 'composer',
+ 'writer',
+ 'location',
+ 'during-recording',
+ 'during-performance',
+ 'screen',
+ 'fish',
+ 'illustration',
+ 'logo-band',
+ 'logo-publisher'
+];
+
+/**
+ * Parses legacy frames for ID3 v2.2 and earlier
+ * @param {ArrayBuffer} buffer Buffer to read
+ * @return {ID3Frame|null}
+ */
+export function parseLegacy(buffer: ArrayBuffer): ID3Frame | null {
+ const result: ID3Frame = {
+ tag: null,
+ value: null
+ };
+ const dv = new DataView(buffer);
+ const header = {
+ id: getString(dv, 3),
+ type: getString(dv, 1),
+ size: getUint24(dv, 3)
+ };
+
+ const matchedType = types.get(header.id);
+
+ if (!matchedType) {
+ return null;
+ }
+
+ result.tag = matchedType;
+
+ if (header.type === 'T') {
+ /*
+ * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
+ */
+ let val = getString(dv, -7, 7);
+
+ if (header.id === 'TCO' && !!parseInt(val)) {
+ val = genres[parseInt(val)];
+ }
+
+ result.value = val;
+ } else if (header.type === 'W') {
+ result.value = getString(dv, -7, 7);
+ } else if (header.id === 'COM') {
+ /*
+ * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
+ */
+ let val = getString(dv, -10, 10);
+
+ if (val.indexOf('\x00') !== -1) {
+ val = val.substr(val.indexOf('\x00') + 1);
+ }
+
+ result.value = val;
+ } else if (header.id === 'PIC') {
+ const image: ImageValue = {
+ type: null,
+ mime: 'image/' + getString(dv, 3, 7).toLowerCase(),
+ description: null,
+ data: null
+ };
+
+ image.type = imageTypes[dv.getUint8(11)] || 'other';
+
+ const variableStart = 11;
+ let variableLength = 0;
+
+ for (let i = variableStart; ; i++) {
+ if (dv.getUint8(i) === 0x00) {
+ variableLength = i - variableStart;
+ break;
+ }
+ }
+
+ image.description =
+ variableLength === 0
+ ? null
+ : getString(dv, variableLength, variableStart);
+ image.data = buffer.slice(variableStart + 1);
+
+ result.value = image;
+ }
+
+ return result.tag ? result : null;
+}
+
+/**
+ * Parses a given buffer into an ID3 frame
+ * @param {ArrayBuffer} buffer Buffer to read data from
+ * @param {number} major Major version of ID3
+ * @param {number} minor Minor version of ID3
+ * @return {ID3Frame|null}
+ */
+export function parse(
+ buffer: ArrayBuffer,
+ major: number,
+ minor: number
+): ID3Frame | null {
+ minor = minor || 0;
+ major = major || 4;
+
+ const result: ID3Frame = {tag: null, value: null};
+ const dv = new DataView(buffer);
+
+ if (major < 3) {
+ return parseLegacy(buffer);
+ }
+
+ const header = {
+ id: getString(dv, 4),
+ type: getString(dv, 1),
+ size: getUint32Synch(dv, 4),
+ flags: [dv.getUint8(8), dv.getUint8(9)]
+ };
+
+ /*
+ * No support for compressed, unsychronised, etc frames
+ */
+ if (header.flags[1] !== 0) {
+ return null;
+ }
+
+ const matchedType = types.get(header.id);
+
+ if (!matchedType) {
+ return null;
+ }
+
+ result.tag = matchedType;
+
+ if (header.type === 'T') {
+ const encoding = dv.getUint8(10);
+ let val: string | null = null;
+
+ /*
+ * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
+ */
+ if (encoding === 0 || encoding === 3) {
+ val = getString(dv, -11, 11);
+ } else if (encoding === 1) {
+ val = getStringUtf16(dv, -11, 11, true);
+ } else if (encoding === 2) {
+ val = getStringUtf16(dv, -11, 11);
+ }
+
+ if (header.id === 'TCON' && val !== null && !!parseInt(val)) {
+ val = genres[parseInt(val)];
+ }
+
+ result.value = val;
+ } else if (header.type === 'W') {
+ result.value = getString(dv, -10, 10);
+ } else if (header.id === 'PRIV') {
+ const variableStart = 10;
+ let variableLength = 0;
+
+ for (let i = 0; ; i++) {
+ if (dv.getUint8(i) === 0x00) {
+ variableLength = i - variableStart;
+ break;
+ }
+ }
+
+ result.value = {
+ identifier:
+ variableLength === 0
+ ? null
+ : getString(dv, variableLength, variableStart),
+ data: buffer.slice(variableLength + variableStart + 1)
+ };
+ } else if (header.id === 'COMM') {
+ /*
+ * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
+ */
+ const encoding = dv.getUint8(10);
+ let variableStart = 14;
+
+ /*
+ * Skip the comment description and retrieve only the comment its self
+ */
+ for (let i = variableStart; ; i++) {
+ if (encoding === 1 || encoding === 2) {
+ if (dv.getUint16(i) === 0x0000) {
+ variableStart = i + 2;
+ break;
+ }
+ i++;
+ } else {
+ if (dv.getUint8(i) === 0x00) {
+ variableStart = i + 1;
+ break;
+ }
+ }
+ }
+
+ if (encoding === 0 || encoding === 3) {
+ result.value = getString(dv, -1 * variableStart, variableStart);
+ } else if (encoding === 1) {
+ result.value = getStringUtf16(
+ dv,
+ -1 * variableStart,
+ variableStart,
+ true
+ );
+ } else if (encoding === 2) {
+ result.value = getStringUtf16(dv, -1 * variableStart, variableStart);
+ }
+ } else if (header.id === 'APIC') {
+ const encoding = dv.getUint8(10);
+ const image: ImageValue = {
+ type: null,
+ mime: null,
+ description: null,
+ data: null
+ };
+ let variableStart = 11;
+ let variableLength = 0;
+
+ for (let i = variableStart; ; i++) {
+ if (dv.getUint8(i) === 0x00) {
+ variableLength = i - variableStart;
+ break;
+ }
+ }
+
+ image.mime = getString(dv, variableLength, variableStart);
+ image.type =
+ imageTypes[dv.getUint8(variableStart + variableLength + 1)] || 'other';
+ variableStart += variableLength + 2;
+ variableLength = 0;
+
+ for (let i = variableStart; ; i++) {
+ if (dv.getUint8(i) === 0x00) {
+ variableLength = i - variableStart;
+ break;
+ }
+ }
+
+ if (variableLength !== 0) {
+ if (encoding === 0 || encoding === 3) {
+ image.description = getString(dv, variableLength, variableStart);
+ } else if (encoding === 1) {
+ image.description = getStringUtf16(
+ dv,
+ variableLength,
+ variableStart,
+ true
+ );
+ } else if (encoding === 2) {
+ image.description = getStringUtf16(dv, variableLength, variableStart);
+ }
+ }
+
+ image.data = buffer.slice(variableStart + 1);
+
+ result.value = image;
+ }
+
+ return result.tag ? result : null;
+}
diff --git a/src/id3Tag.ts b/src/id3Tag.ts
new file mode 100644
index 0000000..3902668
--- /dev/null
+++ b/src/id3Tag.ts
@@ -0,0 +1,178 @@
+import {Reader} from './reader.js';
+import {parse as parseFrame} from './id3Frame.js';
+import {getString, getUint32Synch, getUint24} from './util.js';
+import genres from './genres.js';
+
+export interface ID3Tag {
+ title: string | null;
+ album: string | null;
+ artist: string | null;
+ year: string | null;
+ [key: string]: unknown;
+}
+
+export interface ID3TagV1 extends ID3Tag {
+ kind: 'v1';
+ comment: string | null;
+ track: string | null;
+ genre: string | null;
+ version: number;
+}
+
+export interface ID3TagV2 extends ID3Tag {
+ kind: 'v2';
+ version: [number, number];
+}
+
+/**
+ * Parses a given resource into an ID3 tag
+ * @param {Reader} handle Reader to use for reading the resource
+ * @return {Promise}
+ */
+export async function parse(handle: Reader): Promise {
+ let tag: ID3Tag | null = null;
+
+ /*
+ * Read the last 128 bytes (ID3v1)
+ */
+ const v1HeaderBuf = await handle.read(128, handle.size - 128);
+ const v1Header = new DataView(v1HeaderBuf);
+
+ if (
+ v1HeaderBuf.byteLength === 128 &&
+ getString(v1Header, 3, undefined, true) === 'TAG'
+ ) {
+ tag = {
+ kind: 'v1',
+ title: getString(v1Header, 30, 3).replace(/(^\s+|\s+$)/, '') || null,
+ album: getString(v1Header, 30, 63).replace(/(^\s+|\s+$)/, '') || null,
+ artist: getString(v1Header, 30, 33).replace(/(^\s+|\s+$)/, '') || null,
+ year: getString(v1Header, 4, 93).replace(/(^\s+|\s+$)/, '') || null,
+ genre: null,
+ comment: null,
+ track: null
+ };
+
+ /*
+ * If there is a zero byte at [125], the comment is 28 bytes and the
+ * remaining 2 are [0, trackno]
+ */
+ if (v1Header.getUint8(125) === 0) {
+ tag.comment = getString(v1Header, 28, 97).replace(/(^\s+|\s+$)/, '');
+ tag.version = 1.1;
+ tag.track = v1Header.getUint8(126);
+ } else {
+ tag.comment = getString(v1Header, 30, 97).replace(/(^\s+|\s+$)/, '');
+ }
+
+ /*
+ * Lookup the genre index in the predefined genres array
+ */
+ tag.genre = genres[v1Header.getUint8(127)] || null;
+ }
+
+ /*
+ * Read 14 bytes (10 for ID3v2 header, 4 for possible extended header size)
+ * Assuming the ID3v2 tag is prepended
+ */
+ const v2PrefixBuf = await handle.read(14, 0);
+ const v2Prefix = new DataView(v2PrefixBuf);
+
+ /*
+ * Be sure that the buffer is at least the size of an id3v2 header
+ * Assume incompatibility if a major version of > 4 is used
+ */
+ if (
+ v2PrefixBuf.byteLength === 14 &&
+ getString(v2Prefix, 3, undefined, true) === 'ID3' &&
+ v2Prefix.getUint8(3) <= 4
+ ) {
+ let headerSize = 10;
+ let tagSize = 0;
+ const version = [v2Prefix.getUint8(3), v2Prefix.getUint8(4)];
+ const tagFlags = v2Prefix.getUint8(5);
+
+ /*
+ * Do not support unsynchronisation
+ */
+ if ((tagFlags & 0x80) === 0) {
+ tag = {
+ kind: 'v2',
+ title: tag ? tag.title : null,
+ album: tag ? tag.album : null,
+ artist: tag ? tag.artist : null,
+ year: tag ? tag.year : null,
+ version: version
+ };
+
+ /*
+ * Increment the header size to offset by if an extended header exists
+ */
+ if ((tagFlags & 0x40) !== 0) {
+ headerSize += getUint32Synch(v2Prefix, 11);
+ }
+
+ /*
+ * Calculate the tag size to be read
+ */
+ tagSize += getUint32Synch(v2Prefix, 6);
+
+ const v2TagBuf = await handle.read(tagSize, headerSize);
+ const v2Tag = new DataView(v2TagBuf);
+ let position = 0;
+
+ while (position < v2TagBuf.byteLength) {
+ let slice;
+ let isFrame = true;
+
+ for (let i = 0; i < 3; i++) {
+ const frameBit = v2Tag.getUint8(position + i);
+
+ if (
+ (frameBit < 0x41 || frameBit > 0x5a) &&
+ (frameBit < 0x30 || frameBit > 0x39)
+ ) {
+ isFrame = false;
+ }
+ }
+
+ if (!isFrame) {
+ break;
+ }
+
+ /*
+ * < v2.3, frame ID is 3 chars, size is 3 bytes making a total
+ * size of 6 bytes.
+ * >= v2.3, frame ID is 4 chars, size is 4 bytes, flags are 2 bytes,
+ * total 10 bytes.
+ */
+ if (version[0] < 3) {
+ slice = v2TagBuf.slice(
+ position,
+ position + 6 + getUint24(v2Tag, position + 3)
+ );
+ } else if (version[0] === 3) {
+ slice = v2TagBuf.slice(
+ position,
+ position + 10 + v2Tag.getUint32(position + 4)
+ );
+ } else {
+ slice = v2TagBuf.slice(
+ position,
+ position + 10 + getUint32Synch(v2Tag, position + 4)
+ );
+ }
+
+ const frame = await parseFrame(slice, version[0], version[1]);
+
+ if (frame && frame.tag) {
+ tag[frame.tag] = frame.value;
+ }
+
+ position += slice.byteLength;
+ }
+ }
+ }
+
+ return tag;
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..f47819a
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1 @@
+export * from './id3.js';
diff --git a/src/localReader.ts b/src/localReader.ts
new file mode 100644
index 0000000..b936fc9
--- /dev/null
+++ b/src/localReader.ts
@@ -0,0 +1,95 @@
+import {Reader} from './reader.js';
+import * as fs from 'fs';
+
+/**
+ * Provides read access to the local file system
+ */
+export class LocalReader extends Reader {
+ protected _path: string;
+ protected _fd?: number;
+
+ /**
+ * @param {string} path Path of the local file
+ */
+ public constructor(path: string) {
+ super();
+
+ this._path = path;
+ }
+
+ /** @inheritdoc */
+ public async open(): Promise {
+ return new Promise((resolve, reject): void => {
+ fs.stat(this._path, (err, stat): void => {
+ if (err) {
+ reject(err);
+ return;
+ }
+
+ this.size = stat.size;
+
+ fs.open(this._path, 'r', (openErr, fd) => {
+ if (openErr) {
+ reject(err);
+ return;
+ }
+
+ this._fd = fd;
+ resolve();
+ });
+ });
+ });
+ }
+
+ /** @inheritdoc */
+ public async close(): Promise {
+ return new Promise((resolve, reject) => {
+ if (this._fd === undefined) {
+ reject(new Error('Resource not yet open'));
+ return;
+ }
+
+ fs.close(this._fd, (err) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve();
+ }
+ });
+ });
+ }
+
+ /** @inheritdoc */
+ public async read(length: number, position: number): Promise {
+ const buffer = Buffer.alloc(length);
+
+ return new Promise((resolve, reject) => {
+ if (this._fd === undefined) {
+ reject(new Error('Resource not yet open'));
+ return;
+ }
+
+ fs.read(
+ this._fd,
+ buffer,
+ 0,
+ length,
+ position,
+ (err, _bytesRead, buffer) => {
+ if (err) {
+ return reject(err);
+ }
+
+ const ab = new ArrayBuffer(buffer.length);
+ const view = new Uint8Array(ab);
+
+ for (let i = 0; i < buffer.length; i++) {
+ view[i] = buffer[i];
+ }
+
+ resolve(ab);
+ }
+ );
+ });
+ }
+}
diff --git a/src/reader.ts b/src/reader.ts
new file mode 100644
index 0000000..d49b17a
--- /dev/null
+++ b/src/reader.ts
@@ -0,0 +1,50 @@
+/**
+ * Provides read access to a given resource
+ */
+export abstract class Reader {
+ /**
+ * Size of the resource
+ */
+ public size: number = 0;
+
+ /**
+ * Opens the resource for reading
+ * @return {Promise}
+ */
+ public abstract async open(): Promise;
+
+ /**
+ * Closes the resource
+ * @return {Promise}
+ */
+ public async close(): Promise {
+ return;
+ }
+
+ /**
+ * Reads a specified range of the resource
+ * @param {number} length Number of bytes to read
+ * @param {number} position Position to begin from
+ * @return {Promise}
+ */
+ public abstract async read(
+ length: number,
+ position: number
+ ): Promise;
+
+ /**
+ * Reads a specified range into a Blob
+ * @param {number} length Number of bytes to read
+ * @param {number} position Position to begin from
+ * @param {string=} type Type of data to return
+ * @return {Promise}
+ */
+ public async readBlob(
+ length: number,
+ position: number = 0,
+ type: string = 'application/octet-stream'
+ ): Promise {
+ const data = await this.read(length, position);
+ return new Blob([data], {type: type});
+ }
+}
diff --git a/src/remoteReader.ts b/src/remoteReader.ts
new file mode 100644
index 0000000..a56c29e
--- /dev/null
+++ b/src/remoteReader.ts
@@ -0,0 +1,40 @@
+import {Reader} from './reader.js';
+
+/**
+ * Reads a remote URL
+ */
+export class RemoteReader extends Reader {
+ protected _url: string;
+
+ /**
+ * @param {string} url URL to retrieve
+ */
+ public constructor(url: string) {
+ super();
+
+ this._url = url;
+ }
+
+ /** @inheritdoc */
+ public async open(): Promise {
+ const resp = await fetch(this._url, {
+ method: 'HEAD'
+ });
+
+ const contentLength = resp.headers.get('Content-Length');
+
+ this.size = contentLength ? Number(contentLength) : 0;
+ }
+
+ /** @inheritdoc */
+ public async read(length: number, position: number): Promise {
+ const resp = await fetch(this._url, {
+ method: 'GET',
+ headers: {
+ Range: `bytes=${position}-${position + length - 1}`
+ }
+ });
+
+ return await resp.arrayBuffer();
+ }
+}
diff --git a/src/util.ts b/src/util.ts
new file mode 100644
index 0000000..27c2bef
--- /dev/null
+++ b/src/util.ts
@@ -0,0 +1,174 @@
+/**
+ * Retrieves a string from a specific offset of a data view
+ * @param {DataView} view View to retrieve string from
+ * @param {number|null} length Bytes to read
+ * @param {number=} offset Offset to read from
+ * @param {boolean=} raw Whether to return the raw string or not
+ * @return {string}
+ */
+export function getString(
+ view: DataView,
+ length: number | undefined,
+ offset: number = 0,
+ raw?: boolean
+): string {
+ let len = length || view.byteLength - offset;
+ const useBuffer = typeof Buffer !== 'undefined';
+
+ if (len < 0) {
+ len += view.byteLength;
+ }
+
+ const data: number[] = [];
+ const limit = offset + len;
+
+ for (let i = offset; i < limit; i++) {
+ const current = view.getUint8(i);
+ if (current === 0) {
+ break;
+ }
+ data.push(current);
+ }
+
+ if (useBuffer) {
+ return Buffer.from(data).toString();
+ }
+
+ const str = data.map((chr) => String.fromCharCode(chr)).join('');
+
+ if (raw) {
+ return str;
+ }
+
+ return decodeURIComponent(escape(str));
+}
+
+/**
+ * Retrieves a UTF16 string from a specific offset of a data view
+ * @param {DataView} view View to retrieve string from
+ * @param {number|null} length Bytes to read
+ * @param {number=} offset Offset to read from
+ * @param {boolean=} bom Whether to use BOM or not
+ * @return {string}
+ */
+export function getStringUtf16(
+ view: DataView,
+ length: number | null,
+ offset: number = 0,
+ bom?: boolean
+): string {
+ let littleEndian = false;
+ let len = length || view.byteLength - offset;
+ const str: number[] = [];
+ const useBuffer = typeof Buffer !== 'undefined';
+
+ if (len < 0) {
+ len += view.byteLength;
+ }
+
+ if (bom) {
+ const bomInt = view.getUint16(offset);
+
+ if (bomInt === 0xfffe) {
+ littleEndian = true;
+ }
+
+ offset += 2;
+ len -= 2;
+ }
+
+ const limit = offset + len;
+
+ for (let i = offset; i < limit; i += 2) {
+ let ch = view.getUint16(i, littleEndian);
+
+ if (
+ i < limit - 1 &&
+ ch === 0 &&
+ view.getUint16(i + 1, littleEndian) === 0
+ ) {
+ break;
+ }
+
+ if ((ch >= 0 && ch <= 0xd7ff) || (ch >= 0xe000 && ch <= 0xffff)) {
+ str.push(ch);
+ } else if (ch >= 0x10000 && ch <= 0x10ffff) {
+ ch -= 0x10000;
+
+ str.push(((0xffc00 & ch) >> 10) + 0xd800);
+ str.push((0x3ff & ch) + 0xdc00);
+ }
+ }
+
+ if (useBuffer) {
+ return Buffer.from(str).toString();
+ }
+
+ return decodeURIComponent(
+ escape(str.map((chr) => String.fromCharCode(chr)).join(''))
+ );
+}
+
+/**
+ * Gets the "synch" representation of a number
+ * @param {number} num Number to convert
+ * @return {number}
+ */
+export function getSynch(num: number): number {
+ let out = 0;
+ let mask = 0x7f000000;
+
+ while (mask) {
+ out >>= 1;
+ out |= num & mask;
+ mask >>= 8;
+ }
+
+ return out;
+}
+
+/**
+ * Gets a "synch2 uint8 from a view
+ * @param {DataView} view View to read
+ * @param {number=} offset Offset to read from
+ * @return {number}
+ */
+export function getUint8Synch(view: DataView, offset: number = 0): number {
+ return getSynch(view.getUint8(offset));
+}
+
+/**
+ * Gets a "synch2 uint32 from a view
+ * @param {DataView} view View to read
+ * @param {number=} offset Offset to read from
+ * @return {number}
+ */
+export function getUint32Synch(view: DataView, offset: number = 0): number {
+ return getSynch(view.getUint32(offset));
+}
+
+/**
+ * Gets a uint24 from a view
+ * @param {DataView} view View to read
+ * @param {number=} offset Offset to read from
+ * @param {boolean=} littleEndian Whether to use little endian or not
+ * @return {number}
+ */
+export function getUint24(
+ view: DataView,
+ offset: number = 0,
+ littleEndian?: boolean
+): number {
+ if (littleEndian) {
+ return (
+ view.getUint8(offset) +
+ (view.getUint8(offset + 1) << 8) +
+ (view.getUint8(offset + 2) << 16)
+ );
+ }
+ return (
+ view.getUint8(offset + 2) +
+ (view.getUint8(offset + 1) << 8) +
+ (view.getUint8(offset) << 16)
+ );
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..f455aa5
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "es2018",
+ "module": "esnext",
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "./lib",
+ "strict": true,
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "alwaysStrict": true,
+ "moduleResolution": "node"
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+}
From 16dc4c8b74b9d00a41538ed57a466774fc9e30cc Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Mon, 17 Jun 2019 23:23:31 +0100
Subject: [PATCH 02/12] add easier entrypoint
---
id3.js | 1 +
package.json | 3 ++-
src/index.ts | 1 -
3 files changed, 3 insertions(+), 2 deletions(-)
create mode 100644 id3.js
delete mode 100644 src/index.ts
diff --git a/id3.js b/id3.js
new file mode 100644
index 0000000..a6d0305
--- /dev/null
+++ b/id3.js
@@ -0,0 +1 @@
+export * from './lib/id3.js';
diff --git a/package.json b/package.json
index 318f624..47e4d30 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "1.1.3",
"author": "43081j",
"description": "A modern ID3 parser written completely in JavaScript, making use of typed arrays and the HTML5 File API",
- "main": "./lib/index.js",
+ "main": "./id3.js",
"type": "module",
"repository": {
"type": "git",
@@ -15,6 +15,7 @@
"parser"
],
"files": [
+ "id3.js",
"lib/**/*.js",
"lib/**/*.d.ts"
],
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index f47819a..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './id3.js';
From 2afd096e4e6e2d7e3ece447287d713970c54270f Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Mon, 17 Jun 2019 23:37:07 +0100
Subject: [PATCH 03/12] guard against empty strings
---
src/util.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/util.ts b/src/util.ts
index 27c2bef..7717420 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -66,6 +66,10 @@ export function getStringUtf16(
len += view.byteLength;
}
+ if (offset + 1 > view.byteLength) {
+ return '';
+ }
+
if (bom) {
const bomInt = view.getUint16(offset);
From bd3d45e86cbd869496027fce0c0fb18db84a8c4c Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Mon, 17 Jun 2019 23:44:04 +0100
Subject: [PATCH 04/12] write uint16s to buffers
---
src/util.ts | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/util.ts b/src/util.ts
index 7717420..b75b59e 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -105,7 +105,17 @@ export function getStringUtf16(
}
if (useBuffer) {
- return Buffer.from(str).toString();
+ const buf = Buffer.alloc(str.length * 2);
+ for (let i = 0; i < str.length; i++) {
+ const chr = str[i];
+
+ if (littleEndian) {
+ buf.writeUInt16LE(chr, i * 2);
+ } else {
+ buf.writeUInt16BE(chr, i * 2);
+ }
+ }
+ return buf.toString();
}
return decodeURIComponent(
From b597f836b1f48aa4c70e2d6b7b0f17b8a9c91c81 Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Tue, 18 Jun 2019 18:48:36 +0100
Subject: [PATCH 05/12] expose raw frames
---
src/id3Frame.ts | 6 +++++-
src/id3Tag.ts | 15 +++++++++------
2 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/src/id3Frame.ts b/src/id3Frame.ts
index 2d106b9..9b9c233 100644
--- a/src/id3Frame.ts
+++ b/src/id3Frame.ts
@@ -4,6 +4,7 @@ import {getString, getUint24, getUint32Synch, getStringUtf16} from './util.js';
export interface ID3Frame {
tag: string | null;
value: unknown | null;
+ id: string | null;
}
export interface ImageValue {
@@ -160,6 +161,7 @@ export const imageTypes = [
*/
export function parseLegacy(buffer: ArrayBuffer): ID3Frame | null {
const result: ID3Frame = {
+ id: null,
tag: null,
value: null
};
@@ -176,6 +178,7 @@ export function parseLegacy(buffer: ArrayBuffer): ID3Frame | null {
return null;
}
+ result.id = header.id;
result.tag = matchedType;
if (header.type === 'T') {
@@ -249,7 +252,7 @@ export function parse(
minor = minor || 0;
major = major || 4;
- const result: ID3Frame = {tag: null, value: null};
+ const result: ID3Frame = {id: null, tag: null, value: null};
const dv = new DataView(buffer);
if (major < 3) {
@@ -277,6 +280,7 @@ export function parse(
}
result.tag = matchedType;
+ result.id = header.id;
if (header.type === 'T') {
const encoding = dv.getUint8(10);
diff --git a/src/id3Tag.ts b/src/id3Tag.ts
index 3902668..3a9007c 100644
--- a/src/id3Tag.ts
+++ b/src/id3Tag.ts
@@ -1,5 +1,5 @@
import {Reader} from './reader.js';
-import {parse as parseFrame} from './id3Frame.js';
+import {parse as parseFrame, ID3Frame} from './id3Frame.js';
import {getString, getUint32Synch, getUint24} from './util.js';
import genres from './genres.js';
@@ -22,6 +22,7 @@ export interface ID3TagV1 extends ID3Tag {
export interface ID3TagV2 extends ID3Tag {
kind: 'v2';
version: [number, number];
+ frames: ID3Frame[];
}
/**
@@ -44,10 +45,10 @@ export async function parse(handle: Reader): Promise {
) {
tag = {
kind: 'v1',
- title: getString(v1Header, 30, 3).replace(/(^\s+|\s+$)/, '') || null,
- album: getString(v1Header, 30, 63).replace(/(^\s+|\s+$)/, '') || null,
- artist: getString(v1Header, 30, 33).replace(/(^\s+|\s+$)/, '') || null,
- year: getString(v1Header, 4, 93).replace(/(^\s+|\s+$)/, '') || null,
+ title: getString(v1Header, 30, 3) || null,
+ album: getString(v1Header, 30, 63) || null,
+ artist: getString(v1Header, 30, 33) || null,
+ year: getString(v1Header, 4, 93) || null,
genre: null,
comment: null,
track: null
@@ -102,7 +103,8 @@ export async function parse(handle: Reader): Promise {
album: tag ? tag.album : null,
artist: tag ? tag.artist : null,
year: tag ? tag.year : null,
- version: version
+ version: version,
+ frames: []
};
/*
@@ -166,6 +168,7 @@ export async function parse(handle: Reader): Promise {
const frame = await parseFrame(slice, version[0], version[1]);
if (frame && frame.tag) {
+ (tag as ID3TagV2).frames.push(frame);
tag[frame.tag] = frame.value;
}
From 7c867b0f2a50f49d8cde08e1739beec88975e950 Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Tue, 18 Jun 2019 19:20:24 +0100
Subject: [PATCH 06/12] use uint16s to align string encoding
---
src/util.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/util.ts b/src/util.ts
index b75b59e..88e15ec 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -118,9 +118,9 @@ export function getStringUtf16(
return buf.toString();
}
- return decodeURIComponent(
- escape(str.map((chr) => String.fromCharCode(chr)).join(''))
- );
+ return String.fromCharCode.apply(null, (new Uint16Array(
+ str
+ ) as unknown) as number[]);
}
/**
From 6a885d192b9707459bc22507579ccfa5a4ac1a52 Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Tue, 18 Jun 2019 19:36:30 +0100
Subject: [PATCH 07/12] support multiple images
---
src/id3Tag.ts | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/src/id3Tag.ts b/src/id3Tag.ts
index 3a9007c..e7f5157 100644
--- a/src/id3Tag.ts
+++ b/src/id3Tag.ts
@@ -1,5 +1,5 @@
import {Reader} from './reader.js';
-import {parse as parseFrame, ID3Frame} from './id3Frame.js';
+import {parse as parseFrame, ID3Frame, ImageValue} from './id3Frame.js';
import {getString, getUint32Synch, getUint24} from './util.js';
import genres from './genres.js';
@@ -23,6 +23,7 @@ export interface ID3TagV2 extends ID3Tag {
kind: 'v2';
version: [number, number];
frames: ID3Frame[];
+ images: ImageValue[];
}
/**
@@ -59,11 +60,11 @@ export async function parse(handle: Reader): Promise {
* remaining 2 are [0, trackno]
*/
if (v1Header.getUint8(125) === 0) {
- tag.comment = getString(v1Header, 28, 97).replace(/(^\s+|\s+$)/, '');
+ tag.comment = getString(v1Header, 28, 97);
tag.version = 1.1;
tag.track = v1Header.getUint8(126);
} else {
- tag.comment = getString(v1Header, 30, 97).replace(/(^\s+|\s+$)/, '');
+ tag.comment = getString(v1Header, 30, 97);
}
/*
@@ -104,7 +105,8 @@ export async function parse(handle: Reader): Promise {
artist: tag ? tag.artist : null,
year: tag ? tag.year : null,
version: version,
- frames: []
+ frames: [],
+ images: []
};
/*
@@ -168,8 +170,15 @@ export async function parse(handle: Reader): Promise {
const frame = await parseFrame(slice, version[0], version[1]);
if (frame && frame.tag) {
- (tag as ID3TagV2).frames.push(frame);
- tag[frame.tag] = frame.value;
+ const tagAsV2 = tag as ID3TagV2;
+
+ tagAsV2.frames.push(frame);
+
+ if (frame.tag === 'image') {
+ tagAsV2.images.push(frame.value as ImageValue);
+ } else {
+ tag[frame.tag] = frame.value;
+ }
}
position += slice.byteLength;
From 06d71e027c3904cc5d97688a4b79484fbb75fff1 Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Tue, 18 Jun 2019 19:40:52 +0100
Subject: [PATCH 08/12] add clean script
---
package.json | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/package.json b/package.json
index 47e4d30..bc9ea43 100644
--- a/package.json
+++ b/package.json
@@ -27,11 +27,16 @@
"eslint": "^5.16.0",
"eslint-config-google": "^0.13.0",
"prettier": "^1.18.2",
+ "rimraf": "^2.6.3",
"typescript": "^3.5.2"
},
"scripts": {
+ "clean": "rimraf ./lib",
+ "prebuild": "npm run clean",
"lint": "eslint \"src/**/*.ts\"",
"build": "npm run lint && tsc",
+ "prepare": "npm run build",
+ "prepublishOnly": "npm run lint",
"format": "prettier --write \"src/**/*.ts\""
}
}
From d4048a829fbc53beb4c649627b266dc9ac087cf9 Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Tue, 18 Jun 2019 19:52:54 +0100
Subject: [PATCH 09/12] readme
---
README.md | 134 ++++++++++++++++++++++++------------------------------
1 file changed, 59 insertions(+), 75 deletions(-)
diff --git a/README.md b/README.md
index 76f059e..443af35 100644
--- a/README.md
+++ b/README.md
@@ -1,110 +1,94 @@
-id3.js - Javascript ID3 tag parser
-===
+## id3.js - Javascript ID3 tag parser
-**id3.js** is a JavaScript library for reading and parsing ID3 tags of MP3 files. **id3.js** can parse both ID3v1 and ID3v2 tags within a browser or Node environment. It also supports reading from local files (Node-only), same-origin URLs (AJAX) and File instances (HTML5 File API).
+**id3.js** is a JavaScript library for reading and parsing ID3 tags of MP3
+files.
-AJAX
-===
+It can parse both ID3v1 and ID3v2 tags within a browser or within Node.
-```html
-
-
-```
-
-Here the MP3 is being requested by partial AJAX requests, such that only the ID3v1 and ID3v2 tags are read rather than the file as a whole.
+Files can be read from the local disk (Node only), same-origin URLs
+and `File` instances (HTML5 File API).
-Local Files
-===
+## Usage
-First, install **id3.js** using NPM, the Node package manager.
+Install:
```
-npm install id3js
+$ npm i -S id3js
```
-Then use it like so:
+### AJAX
-```javascript
-var id3 = require('id3js');
+You may parse ID3 tags of a remote MP3 by URL:
-id3({ file: './track.mp3', type: id3.OPEN_LOCAL }, function(err, tags) {
- // tags now contains your ID3 tags
+```html
+
```
-Note that here, the type is set to 'local' directly so that **id3.js** will attempt to read from the local file-system using `fs`.
+This works by sending a `HEAD` request for the file and, based on the response,
+sending subsequent `Range` requests for the ID3 tags.
-This will **only work under NodeJS**.
+This is rather efficient as there is no need for the entire file to be
+downloaded.
-File API (HTML5)
-===
+### Local Files
-```html
-
-
+You may parse ID3 tags of a local file in Node:
+
+```ts
+import * as id3 from './node_modules/id3js/id3.js';
+
+id3.fromPath('./test.mp3').then((tags) => {
+ // tags now contains v1, v2 and merged tags
+});
```
-This will read the data from the File instance using slices, so the entire file is not loaded into memory but rather only the tags.
+### File inputs (HTML5)
-Format
-===
+You may parse ID3 tags of a file input:
-Tags are passed as an object of the following format:
+```html
+
-```json
-{
- "artist": "Song artist",
- "title": "Song name",
- "album": "Song album",
- "year": "2013",
- "v1": {
- "title": "ID3v1 title",
- "artist": "ID3v1 artist",
- "album": "ID3v1 album",
- "year": "ID3v1 year",
- "comment": "ID3v1 comment",
- "track": "ID3v1 track (e.g. 02)",
- "version": 1.0
- },
- "v2": {
- "artist": "ID3v2 artist",
- "album": "ID3v2 album",
- "version": [4, 0]
- }
-}
-````
+
+```
-The `v2` object will contain a variable number of fields, depending on what is defined in the file, whereas the `v1` object will always have the same fields (some of which may be null).
+This will read the data from the File instance using slices,
+so the entire file is not loaded into memory but rather only the tags.
-Images
-===
+## Images
-On occasion, an MP3 may have an image embedded in the ID3v2 tag. If this is the case, it will be available through `v2.image`. This has a structure like so:
+An MP3 may have images embedded in the ID3 tags. If this is the case,
+they can be accessed through the `tag.images` property and will
+look like so:
```json
{
- "type": "cover-front",
- "mime": "image/jpeg",
- "description": null,
- "data": ArrayBuffer
+ "type": "cover-front",
+ "mime": "image/jpeg",
+ "description": null,
+ "data": ArrayBuffer
}
```
-As you can see, the data is provided as an `ArrayBuffer`. To access it, you may use a `DataView` or typed array such as `Uint8Array`.
+As you can see, the data is provided as an `ArrayBuffer`.
+To access it, you may use a `DataView` or typed array such
+as `Uint8Array`.
-License
-===
+## License
MIT
From 3d291932b985f521e6c62f01adaefdc9087c8528 Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Tue, 18 Jun 2019 20:06:34 +0100
Subject: [PATCH 10/12] note about esm
---
README.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/README.md b/README.md
index 443af35..4755957 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,9 @@ id3.fromPath('./test.mp3').then((tags) => {
});
```
+**Keep in mind, Node must be run with `--experimental-modules`
+for this to be imported and it cannot be used with `require`.**
+
### File inputs (HTML5)
You may parse ID3 tags of a file input:
From 94a23635609203eeb591ed425517244caba541ec Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Tue, 18 Jun 2019 21:34:38 +0100
Subject: [PATCH 11/12] travis
---
.travis.yml | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .travis.yml
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ca49427
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js: '10'
+cache: npm
+before_script:
+ - npm run build
From d1b9479b3ba27b507c61d5d390d98b223690ec78 Mon Sep 17 00:00:00 2001
From: 43081j <43081j@users.noreply.github.com>
Date: Tue, 18 Jun 2019 21:39:28 +0100
Subject: [PATCH 12/12] remove old dist
---
dist/id3.js | 987 ----------------------------------------------------
1 file changed, 987 deletions(-)
delete mode 100644 dist/id3.js
diff --git a/dist/id3.js b/dist/id3.js
deleted file mode 100644
index 0724ebf..0000000
--- a/dist/id3.js
+++ /dev/null
@@ -1,987 +0,0 @@
-/*
- * ID3 (v1/v2) Parser
- * 43081j
- * License: MIT, see LICENSE
- */
-
-(function() {
- /*
- * lib/reader.js
- * Readers (local, ajax, file)
- */
- var Reader = function(type) {
- this.type = type || Reader.OPEN_URI;
- this.size = null;
- this.file = null;
- };
-
- Reader.OPEN_FILE = 1;
- Reader.OPEN_URI = 2;
- Reader.OPEN_LOCAL = 3;
-
- if(typeof require === 'function') {
- var fs = require('fs');
- }
-
- Reader.prototype.open = function(file, callback) {
- this.file = file;
- var self = this;
- switch(this.type) {
- case Reader.OPEN_LOCAL:
- fs.stat(this.file, function(err, stat) {
- if(err) {
- return callback(err);
- }
- self.size = stat.size;
- fs.open(self.file, 'r', function(err, fd) {
- if(err) {
- return callback(err);
- }
- self.fd = fd;
- callback();
- });
- });
- break;
- case Reader.OPEN_FILE:
- this.size = this.file.size;
- callback();
- break;
- default:
- this.ajax(
- {
- uri: this.file,
- type: 'HEAD',
- },
- function(err, resp, xhr) {
- if(err) {
- return callback(err);
- }
- self.size = parseInt(xhr.getResponseHeader('Content-Length'));
- callback();
- }
- );
- break;
- }
- };
-
- Reader.prototype.close = function() {
- if(this.type === Reader.OPEN_LOCAL) {
- fs.close(this.fd);
- }
- };
-
- Reader.prototype.read = function(length, position, callback) {
- if(typeof position === 'function') {
- callback = position;
- position = 0;
- }
- if(this.type === Reader.OPEN_LOCAL) {
- this.readLocal(length, position, callback);
- } else if(this.type === Reader.OPEN_FILE) {
- this.readFile(length, position, callback);
- } else {
- this.readUri(length, position, callback);
- }
- };
-
- Reader.prototype.readBlob = function(length, position, type, callback) {
- if(typeof position === 'function') {
- callback = position;
- position = 0;
- } else if(typeof type === 'function') {
- callback = type;
- type = 'application/octet-stream';
- }
- this.read(length, position, function(err, data) {
- if(err) {
- callback(err);
- return;
- }
- callback(null, new Blob([data], {type: type}));
- });
- };
-
- /*
- * Local reader
- */
- Reader.prototype.readLocal = function(length, position, callback) {
- var buffer = new Buffer(length);
- fs.read(this.fd, buffer, 0, length, position, function(err, bytesRead, buffer) {
- if(err) {
- return callback(err);
- }
- var ab = new ArrayBuffer(buffer.length),
- view = new Uint8Array(ab);
- for(var i = 0; i < buffer.length; i++) {
- view[i] = buffer[i];
- }
- callback(null, ab);
- });
- };
-
- /*
- * URL reader
- */
- Reader.prototype.ajax = function(opts, callback) {
- var options = {
- type: 'GET',
- uri: null,
- responseType: 'text'
- };
- if(typeof opts === 'string') {
- opts = {uri: opts};
- }
- for(var k in opts) {
- options[k] = opts[k];
- }
- var xhr = new XMLHttpRequest();
- xhr.onreadystatechange = function() {
- if(xhr.readyState !== 4) return;
- if(xhr.status !== 200 && xhr.status !== 206) {
- return callback('Received non-200/206 response (' + xhr.status + ')');
- }
- callback(null, xhr.response, xhr);
- };
- xhr.responseType = options.responseType;
- xhr.open(options.type, options.uri, true);
- if(options.range) {
- options.range = [].concat(options.range);
- if(options.range.length === 2) {
- xhr.setRequestHeader('Range', 'bytes=' + options.range[0] + '-' + options.range[1]);
- } else {
- xhr.setRequestHeader('Range', 'bytes=' + options.range[0]);
- }
- }
- xhr.send();
- };
-
- Reader.prototype.readUri = function(length, position, callback) {
- this.ajax(
- {
- uri: this.file,
- type: 'GET',
- responseType: 'arraybuffer',
- range: [position, position+length-1]
- },
- function(err, buffer) {
- if(err) {
- return callback(err);
- }
- return callback(null, buffer);
- }
- );
- };
-
- /*
- * File API reader
- */
- Reader.prototype.readFile = function(length, position, callback) {
- var slice = this.file.slice(position, position+length),
- fr = new FileReader();
- fr.onload = function(e) {
- callback(null, e.target.result);
- };
- fr.onerror = function(e) {
- callback('File read failed');
- };
- fr.readAsArrayBuffer(slice);
- };
-
- /*
- * lib/dataview-extra.js
- */
- DataView.prototype.getString = function(length, offset, raw) {
- offset = offset || 0;
- length = length || (this.byteLength - offset);
- if(length < 0) {
- length += this.byteLength;
- }
- var str = '';
- if(typeof Buffer !== 'undefined') {
- var data = [];
- for(var i = offset; i < (offset + length); i++) {
- data.push(this.getUint8(i));
- }
- return (new Buffer(data)).toString();
- } else {
- for(var i = offset; i < (offset + length); i++) {
- str += String.fromCharCode(this.getUint8(i));
- }
- if(raw) {
- return str;
- }
- return decodeURIComponent(escape(str));
- }
- };
-
- DataView.prototype.getStringUtf16 = function(length, offset, bom) {
- offset = offset || 0;
- length = length || (this.byteLength - offset);
- var littleEndian = false,
- str = '',
- useBuffer = false;
- if(typeof Buffer !== 'undefined') {
- str = [];
- useBuffer = true;
- }
- if(length < 0) {
- length += this.byteLength;
- }
- if(bom) {
- var bomInt = this.getUint16(offset);
- if(bomInt === 0xFFFE) {
- littleEndian = true;
- }
- offset += 2;
- length -= 2;
- }
- for(var i = offset; i < (offset + length); i += 2) {
- var ch = this.getUint16(i, littleEndian);
- if((ch >= 0 && ch <= 0xD7FF) || (ch >= 0xE000 && ch <= 0xFFFF)) {
- if(useBuffer) {
- str.push(ch);
- } else {
- str += String.fromCharCode(ch);
- }
- } else if(ch >= 0x10000 && ch <= 0x10FFFF) {
- ch -= 0x10000;
- if(useBuffer) {
- str.push(((0xFFC00 & ch) >> 10) + 0xD800);
- str.push((0x3FF & ch) + 0xDC00);
- } else {
- str += String.fromCharCode(((0xFFC00 & ch) >> 10) + 0xD800) + String.fromCharCode((0x3FF & ch) + 0xDC00);
- }
- }
- }
- if(useBuffer) {
- return (new Buffer(str)).toString();
- } else {
- return decodeURIComponent(escape(str));
- }
- };
-
- DataView.prototype.getSynch = function(num) {
- var out = 0,
- mask = 0x7f000000;
- while(mask) {
- out >>= 1;
- out |= num & mask;
- mask >>= 8;
- }
- return out;
- };
-
- DataView.prototype.getUint8Synch = function(offset) {
- return this.getSynch(this.getUint8(offset));
- };
-
- DataView.prototype.getUint32Synch = function(offset) {
- return this.getSynch(this.getUint32(offset));
- };
-
- /*
- * Not really an int as such, but named for consistency
- */
- DataView.prototype.getUint24 = function(offset, littleEndian) {
- if(littleEndian) {
- return this.getUint8(offset) + (this.getUint8(offset + 1) << 8) + (this.getUint8(offset + 2) << 16);
- }
- return this.getUint8(offset + 2) + (this.getUint8(offset + 1) << 8) + (this.getUint8(offset) << 16);
- };
-
- var id3 = function(opts, cb) {
- /*
- * Initialise ID3
- */
- var options = {
- type: id3.OPEN_URI,
- };
- if(typeof opts === 'string') {
- opts = {file: opts, type: id3.OPEN_URI};
- } else if(typeof window !== 'undefined' && window.File && opts instanceof window.File) {
- opts = {file: opts, type: id3.OPEN_FILE};
- }
- for(var k in opts) {
- options[k] = opts[k];
- }
-
- if(!options.file) {
- return cb('No file was set');
- }
-
- if(options.type === id3.OPEN_FILE) {
- if(typeof window === 'undefined' || !window.File || !window.FileReader || typeof ArrayBuffer === 'undefined') {
- return cb('Browser does not have support for the File API and/or ArrayBuffers');
- }
- } else if(options.type === id3.OPEN_LOCAL) {
- if(typeof require !== 'function') {
- return cb('Local paths may not be read within a browser');
- }
- } else {
- }
-
- /*
- * lib/genres.js
- * Genre list
- */
-
- var Genres = [
- 'Blues',
- 'Classic Rock',
- 'Country',
- 'Dance',
- 'Disco',
- 'Funk',
- 'Grunge',
- 'Hip-Hop',
- 'Jazz',
- 'Metal',
- 'New Age',
- 'Oldies',
- 'Other',
- 'Pop',
- 'R&B',
- 'Rap',
- 'Reggae',
- 'Rock',
- 'Techno',
- 'Industrial',
- 'Alternative',
- 'Ska',
- 'Death Metal',
- 'Pranks',
- 'Soundtrack',
- 'Euro-Techno',
- 'Ambient',
- 'Trip-Hop',
- 'Vocal',
- 'Jazz+Funk',
- 'Fusion',
- 'Trance',
- 'Classical',
- 'Instrumental',
- 'Acid',
- 'House',
- 'Game',
- 'Sound Clip',
- 'Gospel',
- 'Noise',
- 'AlternRock',
- 'Bass',
- 'Soul',
- 'Punk',
- 'Space',
- 'Meditative',
- 'Instrumental Pop',
- 'Instrumental Rock',
- 'Ethnic',
- 'Gothic',
- 'Darkwave',
- 'Techno-Industrial',
- 'Electronic',
- 'Pop-Folk',
- 'Eurodance',
- 'Dream',
- 'Southern Rock',
- 'Comedy',
- 'Cult',
- 'Gangsta Rap',
- 'Top 40',
- 'Christian Rap',
- 'Pop / Funk',
- 'Jungle',
- 'Native American',
- 'Cabaret',
- 'New Wave',
- 'Psychedelic',
- 'Rave',
- 'Showtunes',
- 'Trailer',
- 'Lo-Fi',
- 'Tribal',
- 'Acid Punk',
- 'Acid Jazz',
- 'Polka',
- 'Retro',
- 'Musical',
- 'Rock & Roll',
- 'Hard Rock',
- 'Folk',
- 'Folk-Rock',
- 'National Folk',
- 'Swing',
- 'Fast Fusion',
- 'Bebob',
- 'Latin',
- 'Revival',
- 'Celtic',
- 'Bluegrass',
- 'Avantgarde',
- 'Gothic Rock',
- 'Progressive Rock',
- 'Psychedelic Rock',
- 'Symphonic Rock',
- 'Slow Rock',
- 'Big Band',
- 'Chorus',
- 'Easy Listening',
- 'Acoustic',
- 'Humour',
- 'Speech',
- 'Chanson',
- 'Opera',
- 'Chamber Music',
- 'Sonata',
- 'Symphony',
- 'Booty Bass',
- 'Primus',
- 'Porn Groove',
- 'Satire',
- 'Slow Jam',
- 'Club',
- 'Tango',
- 'Samba',
- 'Folklore',
- 'Ballad',
- 'Power Ballad',
- 'Rhythmic Soul',
- 'Freestyle',
- 'Duet',
- 'Punk Rock',
- 'Drum Solo',
- 'A Cappella',
- 'Euro-House',
- 'Dance Hall',
- 'Goa',
- 'Drum & Bass',
- 'Club-House',
- 'Hardcore',
- 'Terror',
- 'Indie',
- 'BritPop',
- 'Negerpunk',
- 'Polsk Punk',
- 'Beat',
- 'Christian Gangsta Rap',
- 'Heavy Metal',
- 'Black Metal',
- 'Crossover',
- 'Contemporary Christian',
- 'Christian Rock',
- 'Merengue',
- 'Salsa',
- 'Thrash Metal',
- 'Anime',
- 'JPop',
- 'Synthpop',
- 'Rock/Pop'
- ];
-
-
- /*
- * lib/id3frame.js
- * ID3Frame
- */
-
- var ID3Frame = {};
-
- /*
- * ID3v2.3 and later frame types
- */
- ID3Frame.types = {
- /*
- * Textual frames
- */
- 'TALB': 'album',
- 'TBPM': 'bpm',
- 'TCOM': 'composer',
- 'TCON': 'genre',
- 'TCOP': 'copyright',
- 'TDEN': 'encoding-time',
- 'TDLY': 'playlist-delay',
- 'TDOR': 'original-release-time',
- 'TDRC': 'recording-time',
- 'TDRL': 'release-time',
- 'TDTG': 'tagging-time',
- 'TENC': 'encoder',
- 'TEXT': 'writer',
- 'TFLT': 'file-type',
- 'TIPL': 'involved-people',
- 'TIT1': 'content-group',
- 'TIT2': 'title',
- 'TIT3': 'subtitle',
- 'TKEY': 'initial-key',
- 'TLAN': 'language',
- 'TLEN': 'length',
- 'TMCL': 'credits',
- 'TMED': 'media-type',
- 'TMOO': 'mood',
- 'TOAL': 'original-album',
- 'TOFN': 'original-filename',
- 'TOLY': 'original-writer',
- 'TOPE': 'original-artist',
- 'TOWN': 'owner',
- 'TPE1': 'artist',
- 'TPE2': 'band',
- 'TPE3': 'conductor',
- 'TPE4': 'remixer',
- 'TPOS': 'set-part',
- 'TPRO': 'produced-notice',
- 'TPUB': 'publisher',
- 'TRCK': 'track',
- 'TRSN': 'radio-name',
- 'TRSO': 'radio-owner',
- 'TSOA': 'album-sort',
- 'TSOP': 'performer-sort',
- 'TSOT': 'title-sort',
- 'TSRC': 'isrc',
- 'TSSE': 'encoder-settings',
- 'TSST': 'set-subtitle',
- /*
- * Textual frames (<=2.2)
- */
- 'TAL': 'album',
- 'TBP': 'bpm',
- 'TCM': 'composer',
- 'TCO': 'genre',
- 'TCR': 'copyright',
- 'TDY': 'playlist-delay',
- 'TEN': 'encoder',
- 'TFT': 'file-type',
- 'TKE': 'initial-key',
- 'TLA': 'language',
- 'TLE': 'length',
- 'TMT': 'media-type',
- 'TOA': 'original-artist',
- 'TOF': 'original-filename',
- 'TOL': 'original-writer',
- 'TOT': 'original-album',
- 'TP1': 'artist',
- 'TP2': 'band',
- 'TP3': 'conductor',
- 'TP4': 'remixer',
- 'TPA': 'set-part',
- 'TPB': 'publisher',
- 'TRC': 'isrc',
- 'TRK': 'track',
- 'TSS': 'encoder-settings',
- 'TT1': 'content-group',
- 'TT2': 'title',
- 'TT3': 'subtitle',
- 'TXT': 'writer',
- /*
- * URL frames
- */
- 'WCOM': 'url-commercial',
- 'WCOP': 'url-legal',
- 'WOAF': 'url-file',
- 'WOAR': 'url-artist',
- 'WOAS': 'url-source',
- 'WORS': 'url-radio',
- 'WPAY': 'url-payment',
- 'WPUB': 'url-publisher',
- /*
- * URL frames (<=2.2)
- */
- 'WAF': 'url-file',
- 'WAR': 'url-artist',
- 'WAS': 'url-source',
- 'WCM': 'url-commercial',
- 'WCP': 'url-copyright',
- 'WPB': 'url-publisher',
- /*
- * Comment frame
- */
- 'COMM': 'comments',
- /*
- * Image frame
- */
- 'APIC': 'image',
- 'PIC': 'image'
- };
-
- /*
- * ID3 image types
- */
- ID3Frame.imageTypes = [
- 'other',
- 'file-icon',
- 'icon',
- 'cover-front',
- 'cover-back',
- 'leaflet',
- 'media',
- 'artist-lead',
- 'artist',
- 'conductor',
- 'band',
- 'composer',
- 'writer',
- 'location',
- 'during-recording',
- 'during-performance',
- 'screen',
- 'fish',
- 'illustration',
- 'logo-band',
- 'logo-publisher'
- ];
-
- /*
- * ID3v2.3 and later
- */
- ID3Frame.parse = function(buffer, major, minor) {
- minor = minor || 0;
- major = major || 4;
- var result = {tag: null, value: null},
- dv = new DataView(buffer);
- if(major < 3) {
- return ID3Frame.parseLegacy(buffer);
- }
- var header = {
- id: dv.getString(4),
- type: dv.getString(1),
- size: dv.getUint32Synch(4),
- flags: [
- dv.getUint8(8),
- dv.getUint8(9)
- ]
- };
- /*
- * No support for compressed, unsychronised, etc frames
- */
- if(header.flags[1] !== 0) {
- return false;
- }
- if(!header.id in ID3Frame.types) {
- return false;
- }
- result.tag = ID3Frame.types[header.id];
- if(header.type === 'T') {
- var encoding = dv.getUint8(10);
- /*
- * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
- */
- if(encoding === 0 || encoding === 3) {
- result.value = dv.getString(-11, 11);
- } else if(encoding === 1) {
- result.value = dv.getStringUtf16(-11, 11, true);
- } else if(encoding === 2) {
- result.value = dv.getStringUtf16(-11, 11);
- } else {
- return false;
- }
- if(header.id === 'TCON' && !!parseInt(result.value)) {
- result.value = Genres[parseInt(result.value)];
- }
- } else if(header.type === 'W') {
- result.value = dv.getString(-10, 10);
- } else if(header.id === 'COMM') {
- /*
- * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
- */
- var encoding = dv.getUint8(10),
- variableStart = 14, variableLength = 0;
- /*
- * Skip the comment description and retrieve only the comment its self
- */
- for(var i = variableStart;; i++) {
- if(encoding === 1 || encoding === 2) {
- if(dv.getUint16(i) === 0x0000) {
- variableStart = i + 2;
- break;
- }
- i++;
- } else {
- if(dv.getUint8(i) === 0x00) {
- variableStart = i + 1;
- break;
- }
- }
- }
- if(encoding === 0 || encoding === 3) {
- result.value = dv.getString(-1 * variableStart, variableStart);
- } else if(encoding === 1) {
- result.value = dv.getStringUtf16(-1 * variableStart, variableStart, true);
- } else if(encoding === 2) {
- result.value = dv.getStringUtf16(-1 * variableStart, variableStart);
- } else {
- return false;
- }
- } else if(header.id === 'APIC') {
- var encoding = dv.getUint8(10),
- image = {
- type: null,
- mime: null,
- description: null,
- data: null
- };
- var variableStart = 11, variableLength = 0;
- for(var i = variableStart;;i++) {
- if(dv.getUint8(i) === 0x00) {
- variableLength = i - variableStart;
- break;
- }
- }
- image.mime = dv.getString(variableLength, variableStart);
- image.type = ID3Frame.imageTypes[dv.getUint8(variableStart + variableLength + 1)] || 'other';
- variableStart += variableLength + 2;
- variableLength = 0;
- for(var i = variableStart;; i++) {
- if(dv.getUint8(i) === 0x00) {
- variableLength = i - variableStart;
- break;
- }
- }
- image.description = (variableLength === 0 ? null : dv.getString(variableLength, variableStart));
- image.data = buffer.slice(variableStart + 1);
- result.value = image;
- }
- return (result.tag ? result : false);
- };
-
- /*
- * ID3v2.2 and earlier
- */
- ID3Frame.parseLegacy = function(buffer) {
- var result = {tag: null, value: null},
- dv = new DataView(buffer),
- header = {
- id: dv.getString(3),
- type: dv.getString(1),
- size: dv.getUint24(3)
- };
- if(!header.id in ID3Frame.types) {
- return false;
- }
- result.tag = ID3Frame.types[header.id];
- if(header.type === 'T') {
- var encoding = dv.getUint8(7);
- /*
- * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
- */
- result.value = dv.getString(-7, 7);
- if(header.id === 'TCO' && !!parseInt(result.value)) {
- result.value = Genres[parseInt(result.value)];
- }
- } else if(header.type === 'W') {
- result.value = dv.getString(-7, 7);
- } else if(header.id === 'COM') {
- /*
- * TODO: Implement UTF-8, UTF-16 and UTF-16 with BOM properly?
- */
- var encoding = dv.getUint8(6);
- result.value = dv.getString(-10, 10);
- if(result.value.indexOf('\x00') !== -1) {
- result.value = result.value.substr(result.value.indexOf('\x00') + 1);
- }
- } else if(header.id === 'PIC') {
- var encoding = dv.getUint8(6),
- image = {
- type: null,
- mime: 'image/' + dv.getString(3, 7).toLowerCase(),
- description: null,
- data: null
- };
- image.type = ID3Frame.imageTypes[dv.getUint8(11)] || 'other';
- var variableStart = 11, variableLength = 0;
- for(var i = variableStart;; i++) {
- if(dv.getUint8(i) === 0x00) {
- variableLength = i - variableStart;
- break;
- }
- }
- image.description = (variableLength === 0 ? null : dv.getString(variableLength, variableStart));
- image.data = buffer.slice(variableStart + 1);
- result.value = image;
- }
- return (result.tag ? result : false);
- };
-
- /*
- * lib/id3tag.js
- * Parse an ID3 tag
- */
-
- var ID3Tag = {};
-
- ID3Tag.parse = function(handle, callback) {
- var tags = {
- title: null,
- album: null,
- artist: null,
- year: null,
- v1: {
- title: null,
- artist: null,
- album: null,
- year: null,
- comment: null,
- track: null,
- version: 1.0
- },
- v2: {
- version: [null, null]
- }
- },
- processed = {
- v1: false,
- v2: false
- },
- process = function(err) {
- if(processed.v1 && processed.v2) {
- tags.title = tags.v2.title || tags.v1.title;
- tags.album = tags.v2.album || tags.v1.album;
- tags.artist = tags.v2.artist || tags.v1.artist;
- tags.year = tags.v1.year;
- callback(err, tags);
- }
- };
- /*
- * Read the last 128 bytes (ID3v1)
- */
- handle.read(128, handle.size - 128, function(err, buffer) {
- if(err) {
- return process('Could not read file');
- }
- var dv = new DataView(buffer);
- if(buffer.byteLength !== 128 || dv.getString(3, null, true) !== 'TAG') {
- processed.v1 = true;
- return process();
- }
- tags.v1.title = dv.getString(30, 3).replace(/(^\s+|\s+$)/, '') || null;
- tags.v1.artist = dv.getString(30, 33).replace(/(^\s+|\s+$)/, '') || null;
- tags.v1.album = dv.getString(30, 63).replace(/(^\s+|\s+$)/, '') || null;
- tags.v1.year = dv.getString(4, 93).replace(/(^\s+|\s+$)/, '') || null;
- /*
- * If there is a zero byte at [125], the comment is 28 bytes and the remaining 2 are [0, trackno]
- */
- if(dv.getUint8(125) === 0) {
- tags.v1.comment = dv.getString(28, 97).replace(/(^\s+|\s+$)/, '');
- tags.v1.version = 1.1;
- tags.v1.track = dv.getUint8(126);
- } else {
- tags.v1.comment = dv.getString(30, 97).replace(/(^\s+|\s+$)/, '');
- }
- /*
- * Lookup the genre index in the predefined genres array
- */
- tags.v1.genre = Genres[dv.getUint8(127)] || null;
- processed.v1 = true;
- process();
- });
- /*
- * Read 14 bytes (10 for ID3v2 header, 4 for possible extended header size)
- * Assuming the ID3v2 tag is prepended
- */
- handle.read(14, 0, function(err, buffer) {
- if(err) {
- return process('Could not read file');
- }
- var dv = new DataView(buffer),
- headerSize = 10,
- tagSize = 0,
- tagFlags;
- /*
- * Be sure that the buffer is at least the size of an id3v2 header
- * Assume incompatibility if a major version of > 4 is used
- */
- if(buffer.byteLength !== 14 || dv.getString(3, null, true) !== 'ID3' || dv.getUint8(3) > 4) {
- processed.v2 = true;
- return process();
- }
- tags.v2.version = [
- dv.getUint8(3),
- dv.getUint8(4)
- ];
- tagFlags = dv.getUint8(5);
- /*
- * Do not support unsynchronisation
- */
- if((tagFlags & 0x80) !== 0) {
- processed.v2 = true;
- return process();
- }
- /*
- * Increment the header size to offset by if an extended header exists
- */
- if((tagFlags & 0x40) !== 0) {
- headerSize += dv.getUint32Synch(11);
- }
- /*
- * Calculate the tag size to be read
- */
- tagSize += dv.getUint32Synch(6);
- handle.read(tagSize, headerSize, function(err, buffer) {
- if(err) {
- processed.v2 = true;
- return process();
- }
- var dv = new DataView(buffer),
- position = 0;
- while(position < buffer.byteLength) {
- var frame,
- slice,
- frameBit,
- isFrame = true;
- for(var i = 0; i < 3; i++) {
- frameBit = dv.getUint8(position + i);
- if((frameBit < 0x41 || frameBit > 0x5A) && (frameBit < 0x30 || frameBit > 0x39)) {
- isFrame = false;
- }
- }
- if(!isFrame) break;
- /*
- * < v2.3, frame ID is 3 chars, size is 3 bytes making a total size of 6 bytes
- * >= v2.3, frame ID is 4 chars, size is 4 bytes, flags are 2 bytes, total 10 bytes
- */
- if(tags.v2.version[0] < 3) {
- slice = buffer.slice(position, position + 6 + dv.getUint24(position + 3));
- } else {
- slice = buffer.slice(position, position + 10 + dv.getUint32Synch(position + 4));
- }
- frame = ID3Frame.parse(slice, tags.v2.version[0]);
- if(frame) {
- tags.v2[frame.tag] = frame.value;
- }
- position += slice.byteLength;
- }
- processed.v2 = true;
- process();
- });
- });
- };
-
- /*
- * Read the file
- */
-
- var handle = new Reader(options.type);
-
- handle.open(options.file, function(err) {
- if(err) {
- return cb('Could not open specified file');
- }
- ID3Tag.parse(handle, function(err, tags) {
- cb(err, tags);
- handle.close()
- });
- });
- };
-
- id3.OPEN_FILE = Reader.OPEN_FILE;
- id3.OPEN_URI = Reader.OPEN_URI;
- id3.OPEN_LOCAL = Reader.OPEN_LOCAL;
-
- if(typeof module !== 'undefined' && module.exports) {
- module.exports = id3;
- } else {
- if(typeof define === 'function' && define.amd) {
- define('id3', [], function() {
- return id3;
- });
- } else {
- window.id3 = id3;
- }
- }
-})();