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; - } - } -})();