├── .gitignore ├── LICENSE.md ├── src ├── filereader.js ├── id3v1.js ├── id3.js ├── stringutils.js ├── binaryfile.js ├── id4.js ├── id3v2frames.js ├── bufferedbinaryajax.js └── id3v2.js ├── Makefile ├── package.json ├── example └── index.html ├── README.md └── dist └── id3-minimized.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules 4 | dist/id3-debug.js 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Jacob Seidelin, http://blog.nihilogic.dk/ 2 | [BSD License](http://opensource.org/licenses/BSD-3-Clause) 3 | 4 | Copyright (c) 2009 Opera Software ASA 5 | [BSD License](http://dev.opera.com/licenses/bsd/) 6 | 7 | Copyright (c) 2010 António Afonso 8 | [BSD License](http://opensource.org/licenses/BSD-3-Clause) 9 | 10 | Copyright (c) 2010 Joshua Kifer 11 | [BSD License](http://opensource.org/licenses/BSD-3-Clause) -------------------------------------------------------------------------------- /src/filereader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010 António Afonso, antonio.afonso gmail, http://www.aadsm.net/ 3 | * MIT License [http://www.opensource.org/licenses/mit-license.php] 4 | * 5 | */ 6 | 7 | var BinaryFile = require('./binaryfile') 8 | 9 | var FileAPIReader = function(file, opt_reader) { 10 | return function(url, fncCallback, fncError) { 11 | var reader = opt_reader || new FileReader(); 12 | 13 | reader.onload = function(event) { 14 | var result = event.target.result; 15 | fncCallback(new BinaryFile(result)); 16 | }; 17 | reader.onerror = fncError; 18 | reader.readAsBinaryString(file); 19 | } 20 | }; 21 | 22 | module.exports = FileAPIReader; 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Binaries 2 | BROWSERIFY = ./node_modules/.bin/browserify 3 | CLOSURE_COMPILER=./node_modules/google-closure-compiler/compiler.jar 4 | 5 | .PHONY: dist debug 6 | 7 | dist: 8 | $(BROWSERIFY) ./src/id3.js --standalone ID3 \ 9 | --exclude xmlhttprequest \ 10 | --exclude btoa \ 11 | --exclude atob \ 12 | --no-builtins > output.js 13 | sed -i.bak "s/\.ID3/['ID3']/g" output.js 14 | java -jar $(CLOSURE_COMPILER) \ 15 | --compilation_level ADVANCED_OPTIMIZATIONS \ 16 | --js output.js \ 17 | > dist/id3-minimized.js 18 | #--formatting PRETTY_PRINT 19 | rm output.js* 20 | 21 | debug: 22 | $(BROWSERIFY) ./src/id3.js --standalone ID3 \ 23 | --exclude xmlhttprequest \ 24 | --exclude btoa \ 25 | --exclude atob \ 26 | --no-builtins --debug > dist/id3-debug.js 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "id3-reader", 3 | "version": "1.0.0", 4 | "description": "ID3 tags reader in JavaScript (ID3v1, ID3v2 and AAC)", 5 | "main": "src/id3.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/aadsm/JavaScript-ID3-Reader.git" 9 | }, 10 | "keywords": [ 11 | "ID3", 12 | "tags", 13 | "AAC", 14 | "mp3", 15 | "m4a", 16 | "audio" 17 | ], 18 | "author": "António Afonso ", 19 | "contributors": [ 20 | { 21 | "name": "Jacob Seidelin" 22 | }, 23 | { 24 | "name": "Joshua Kifer" 25 | }, 26 | { 27 | "name": "Jesse Ditson", 28 | "email": "jesse.ditson@gmail.com" 29 | } 30 | ], 31 | "license": "BSD-3-Clause", 32 | "bugs": { 33 | "url": "https://github.com/aadsm/JavaScript-ID3-Reader/issues" 34 | }, 35 | "homepage": "https://github.com/aadsm/JavaScript-ID3-Reader#readme", 36 | "dependencies": { 37 | "atob": "^1.1.2", 38 | "btoa": "^1.1.2", 39 | "xmlhttprequest": "^1.7.0" 40 | }, 41 | "devDependencies": { 42 | "browserify": "^10.2.4", 43 | "google-closure-compiler": "20151015.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Javascript ID3 Reader 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |

Title:

20 |

Artist:

21 |

Album:

22 | picture extracted from ID3 23 |
24 | 25 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/id3v1.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript ID3 Tag Reader 1.0.0 3 | * Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com, http://blog.nihilogic.dk/ 4 | * MIT License [http://www.opensource.org/licenses/mit-license.php] 5 | * 6 | * Extended by António Afonso (antonio.afonso@opera.com), Opera Software ASA 7 | * Modified by António Afonso (antonio.afonso gmail.com) 8 | */ 9 | 10 | 11 | var ID3v1 = {}; 12 | 13 | var genres = [ 14 | "Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge", 15 | "Hip-Hop","Jazz","Metal","New Age","Oldies","Other","Pop","R&B", 16 | "Rap","Reggae","Rock","Techno","Industrial","Alternative","Ska", 17 | "Death Metal","Pranks","Soundtrack","Euro-Techno","Ambient", 18 | "Trip-Hop","Vocal","Jazz+Funk","Fusion","Trance","Classical", 19 | "Instrumental","Acid","House","Game","Sound Clip","Gospel", 20 | "Noise","AlternRock","Bass","Soul","Punk","Space","Meditative", 21 | "Instrumental Pop","Instrumental Rock","Ethnic","Gothic", 22 | "Darkwave","Techno-Industrial","Electronic","Pop-Folk", 23 | "Eurodance","Dream","Southern Rock","Comedy","Cult","Gangsta", 24 | "Top 40","Christian Rap","Pop/Funk","Jungle","Native American", 25 | "Cabaret","New Wave","Psychadelic","Rave","Showtunes","Trailer", 26 | "Lo-Fi","Tribal","Acid Punk","Acid Jazz","Polka","Retro", 27 | "Musical","Rock & Roll","Hard Rock","Folk","Folk-Rock", 28 | "National Folk","Swing","Fast Fusion","Bebob","Latin","Revival", 29 | "Celtic","Bluegrass","Avantgarde","Gothic Rock","Progressive Rock", 30 | "Psychedelic Rock","Symphonic Rock","Slow Rock","Big Band", 31 | "Chorus","Easy Listening","Acoustic","Humour","Speech","Chanson", 32 | "Opera","Chamber Music","Sonata","Symphony","Booty Bass","Primus", 33 | "Porn Groove","Satire","Slow Jam","Club","Tango","Samba", 34 | "Folklore","Ballad","Power Ballad","Rhythmic Soul","Freestyle", 35 | "Duet","Punk Rock","Drum Solo","Acapella","Euro-House","Dance Hall" 36 | ]; 37 | 38 | ID3v1.loadData = function(data, callback) { 39 | var length = data.getLength(); 40 | data.loadRange([length-128-1, length], callback); 41 | } 42 | 43 | ID3v1.readTagsFromData = function(data) { 44 | var offset = data.getLength() - 128; 45 | var header = data.getStringAt(offset, 3); 46 | if (header == "TAG") { 47 | var title = data.getStringAt(offset + 3, 30).replace(/\0/g, ""); 48 | var artist = data.getStringAt(offset + 33, 30).replace(/\0/g, ""); 49 | var album = data.getStringAt(offset + 63, 30).replace(/\0/g, ""); 50 | var year = data.getStringAt(offset + 93, 4).replace(/\0/g, ""); 51 | 52 | var trackFlag = data.getByteAt(offset + 97 + 28); 53 | if (trackFlag == 0) { 54 | var comment = data.getStringAt(offset + 97, 28).replace(/\0/g, ""); 55 | var track = data.getByteAt(offset + 97 + 29); 56 | } else { 57 | var comment = ""; 58 | var track = 0; 59 | } 60 | 61 | var genreIdx = data.getByteAt(offset + 97 + 30); 62 | if (genreIdx < 255) { 63 | var genre = genres[genreIdx]; 64 | } else { 65 | var genre = ""; 66 | } 67 | 68 | return { 69 | "version" : '1.1', 70 | "title" : title, 71 | "artist" : artist, 72 | "album" : album, 73 | "year" : year, 74 | "comment" : comment, 75 | "track" : track, 76 | "genre" : genre 77 | } 78 | } else { 79 | return {}; 80 | } 81 | }; 82 | 83 | module.exports = ID3v1; 84 | -------------------------------------------------------------------------------- /src/id3.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript ID3 Tag Reader 1.0.0 3 | * Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com, http://blog.nihilogic.dk/ 4 | * MIT License [http://www.opensource.org/licenses/mit-license.php] 5 | * 6 | * Extended by António Afonso (antonio.afonso@opera.com), Opera Software ASA 7 | * Modified by António Afonso 8 | */ 9 | 10 | var ID4 = require('./id4'); 11 | var ID3v1 = require('./id3v1'); 12 | var ID3v2 = require('./id3v2'); 13 | var BufferedBinaryAjax = require('./bufferedbinaryajax'); 14 | var FileAPIReader = require('./filereader'); 15 | 16 | if (typeof window !== 'undefined') { 17 | window['FileAPIReader'] = FileAPIReader; 18 | } 19 | 20 | var ID3 = {}; 21 | 22 | var _files = {}; 23 | // location of the format identifier 24 | var _formatIDRange = [0, 7]; 25 | 26 | /** 27 | * Finds out the tag format of this data and returns the appropriate 28 | * reader. 29 | */ 30 | function getTagReader(data) { 31 | // FIXME: improve this detection according to the spec 32 | return data.getStringAt(4, 7) == "ftypM4A" ? ID4 : 33 | (data.getStringAt(0, 3) == "ID3" ? ID3v2 : ID3v1); 34 | } 35 | 36 | function readTags(reader, data, url, tags) { 37 | var tagsFound = reader.readTagsFromData(data, tags); 38 | //console.log("Downloaded data: " + data.getDownloadedBytesCount() + "bytes"); 39 | var tags = _files[url] || {}; 40 | for( var tag in tagsFound ) if( tagsFound.hasOwnProperty(tag) ) { 41 | tags[tag] = tagsFound[tag]; 42 | } 43 | _files[url] = tags; 44 | } 45 | 46 | ID3.clearTags = function(url) { 47 | delete _files[url]; 48 | }; 49 | 50 | ID3.clearAll = function() { 51 | _files = {}; 52 | }; 53 | 54 | /** 55 | * @param {string} url The location of the sound file to read. 56 | * @param {function()} cb The callback function to be invoked when all tags have been read. 57 | * @param {{tags: Array., dataReader: function(string, function(BinaryReader))}} options The set of options that can specify the tags to be read and the dataReader to use in order to read the file located at url. 58 | */ 59 | ID3.loadTags = function(url, cb, options) { 60 | options = options || {}; 61 | var dataReader = options["dataReader"] || BufferedBinaryAjax; 62 | var onError = options["onError"]; 63 | 64 | dataReader(url, function(data) { 65 | // preload the format identifier 66 | data.loadRange(_formatIDRange, function() { 67 | var reader = getTagReader(data); 68 | reader.loadData(data, function() { 69 | try { 70 | readTags(reader, data, url, options["tags"]); 71 | } catch (err) { 72 | if (onError) onError(err); 73 | } 74 | if( cb ) cb(); 75 | }); 76 | }); 77 | }, onError); 78 | }; 79 | 80 | ID3.getAllTags = function(url) { 81 | if (!_files[url]) return null; 82 | 83 | var tags = {}; 84 | for (var a in _files[url]) { 85 | if (_files[url].hasOwnProperty(a)) 86 | tags[a] = _files[url][a]; 87 | } 88 | return tags; 89 | }; 90 | 91 | ID3.getTag = function(url, tag) { 92 | if (!_files[url]) return null; 93 | 94 | return _files[url][tag]; 95 | }; 96 | 97 | ID3["FileAPIReader"] = FileAPIReader; 98 | ID3["loadTags"] = ID3.loadTags; 99 | ID3["getAllTags"] = ID3.getAllTags; 100 | ID3["getTag"] = ID3.getTag; 101 | ID3["clearTags"] = ID3.clearTags; 102 | ID3["clearAll"] = ID3.clearAll; 103 | 104 | module.exports = ID3; 105 | -------------------------------------------------------------------------------- /src/stringutils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010, António Afonso . All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | * of conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY António Afonso ``AS IS'' AND ANY EXPRESS OR IMPLIED 15 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR 17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | */ 24 | var StringUtils = { 25 | readUTF16String: function(bytes, bigEndian, maxBytes) { 26 | var ix = 0; 27 | var offset1 = 1, offset2 = 0; 28 | maxBytes = Math.min(maxBytes||bytes.length, bytes.length); 29 | 30 | if( bytes[0] == 0xFE && bytes[1] == 0xFF ) { 31 | bigEndian = true; 32 | ix = 2; 33 | } else if( bytes[0] == 0xFF && bytes[1] == 0xFE ) { 34 | bigEndian = false; 35 | ix = 2; 36 | } 37 | if( bigEndian ) { 38 | offset1 = 0; 39 | offset2 = 1; 40 | } 41 | 42 | var arr = []; 43 | for( var j = 0; ix < maxBytes; j++ ) { 44 | var byte1 = bytes[ix+offset1]; 45 | var byte2 = bytes[ix+offset2]; 46 | var word1 = (byte1<<8)+byte2; 47 | ix += 2; 48 | if( word1 == 0x0000 ) { 49 | break; 50 | } else if( byte1 < 0xD8 || byte1 >= 0xE0 ) { 51 | arr[j] = String.fromCharCode(word1); 52 | } else { 53 | var byte3 = bytes[ix+offset1]; 54 | var byte4 = bytes[ix+offset2]; 55 | var word2 = (byte3<<8)+byte4; 56 | ix += 2; 57 | arr[j] = String.fromCharCode(word1, word2); 58 | } 59 | } 60 | var string = new String(arr.join("")); 61 | string.bytesReadCount = ix; 62 | return string; 63 | }, 64 | readUTF8String: function(bytes, maxBytes) { 65 | var ix = 0; 66 | maxBytes = Math.min(maxBytes||bytes.length, bytes.length); 67 | 68 | if( bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF ) { 69 | ix = 3; 70 | } 71 | 72 | var arr = []; 73 | for( var j = 0; ix < maxBytes; j++ ) { 74 | var byte1 = bytes[ix++]; 75 | if( byte1 == 0x00 ) { 76 | break; 77 | } else if( byte1 < 0x80 ) { 78 | arr[j] = String.fromCharCode(byte1); 79 | } else if( byte1 >= 0xC2 && byte1 < 0xE0 ) { 80 | var byte2 = bytes[ix++]; 81 | arr[j] = String.fromCharCode(((byte1&0x1F)<<6) + (byte2&0x3F)); 82 | } else if( byte1 >= 0xE0 && byte1 < 0xF0 ) { 83 | var byte2 = bytes[ix++]; 84 | var byte3 = bytes[ix++]; 85 | arr[j] = String.fromCharCode(((byte1&0xFF)<<12) + ((byte2&0x3F)<<6) + (byte3&0x3F)); 86 | } else if( byte1 >= 0xF0 && byte1 < 0xF5) { 87 | var byte2 = bytes[ix++]; 88 | var byte3 = bytes[ix++]; 89 | var byte4 = bytes[ix++]; 90 | var codepoint = ((byte1&0x07)<<18) + ((byte2&0x3F)<<12)+ ((byte3&0x3F)<<6) + (byte4&0x3F) - 0x10000; 91 | arr[j] = String.fromCharCode( 92 | (codepoint>>10) + 0xD800, 93 | (codepoint&0x3FF) + 0xDC00 94 | ); 95 | } 96 | } 97 | var string = new String(arr.join("")); 98 | string.bytesReadCount = ix; 99 | return string; 100 | }, 101 | readNullTerminatedString: function(bytes, maxBytes) { 102 | var arr = []; 103 | maxBytes = maxBytes || bytes.length; 104 | for ( var i = 0; i < maxBytes; ) { 105 | var byte1 = bytes[i++]; 106 | if( byte1 == 0x00 ) break; 107 | arr[i-1] = String.fromCharCode(byte1); 108 | } 109 | var string = new String(arr.join("")); 110 | string.bytesReadCount = i; 111 | return string; 112 | } 113 | }; 114 | 115 | module.exports = StringUtils; 116 | -------------------------------------------------------------------------------- /src/binaryfile.js: -------------------------------------------------------------------------------- 1 | var StringUtils = require('./stringutils'); 2 | 3 | /** 4 | * @constructor 5 | */ 6 | function BinaryFile(strData, iDataOffset, iDataLength) { 7 | var data = strData; 8 | var dataOffset = iDataOffset || 0; 9 | var dataLength = 0; 10 | 11 | this.getRawData = function() { 12 | return data; 13 | }; 14 | 15 | if (typeof strData == "string") { 16 | dataLength = iDataLength || data.length; 17 | 18 | this.getByteAt = function(iOffset) { 19 | return data.charCodeAt(iOffset + dataOffset) & 0xFF; 20 | }; 21 | } else if (typeof strData == "unknown") { 22 | dataLength = iDataLength || IEBinary_getLength(data); 23 | 24 | this.getByteAt = function(iOffset) { 25 | return IEBinary_getByteAt(data, iOffset + dataOffset); 26 | }; 27 | } 28 | // @aadsm 29 | this.getBytesAt = function(iOffset, iLength) { 30 | var bytes = new Array(iLength); 31 | for( var i = 0; i < iLength; i++ ) { 32 | bytes[i] = this.getByteAt(iOffset+i); 33 | } 34 | return bytes; 35 | }; 36 | 37 | this.getLength = function() { 38 | return dataLength; 39 | }; 40 | 41 | // @aadsm 42 | this.isBitSetAt = function(iOffset, iBit) { 43 | var iByte = this.getByteAt(iOffset); 44 | return (iByte & (1 << iBit)) != 0; 45 | }; 46 | 47 | this.getSByteAt = function(iOffset) { 48 | var iByte = this.getByteAt(iOffset); 49 | if (iByte > 127) 50 | return iByte - 256; 51 | else 52 | return iByte; 53 | }; 54 | 55 | this.getShortAt = function(iOffset, bBigEndian) { 56 | var iShort = bBigEndian ? 57 | (this.getByteAt(iOffset) << 8) + this.getByteAt(iOffset + 1) 58 | : (this.getByteAt(iOffset + 1) << 8) + this.getByteAt(iOffset); 59 | if (iShort < 0) iShort += 65536; 60 | return iShort; 61 | }; 62 | this.getSShortAt = function(iOffset, bBigEndian) { 63 | var iUShort = this.getShortAt(iOffset, bBigEndian); 64 | if (iUShort > 32767) 65 | return iUShort - 65536; 66 | else 67 | return iUShort; 68 | }; 69 | this.getLongAt = function(iOffset, bBigEndian) { 70 | var iByte1 = this.getByteAt(iOffset), 71 | iByte2 = this.getByteAt(iOffset + 1), 72 | iByte3 = this.getByteAt(iOffset + 2), 73 | iByte4 = this.getByteAt(iOffset + 3); 74 | 75 | var iLong = bBigEndian ? 76 | (((((iByte1 << 8) + iByte2) << 8) + iByte3) << 8) + iByte4 77 | : (((((iByte4 << 8) + iByte3) << 8) + iByte2) << 8) + iByte1; 78 | if (iLong < 0) iLong += 4294967296; 79 | return iLong; 80 | }; 81 | this.getSLongAt = function(iOffset, bBigEndian) { 82 | var iULong = this.getLongAt(iOffset, bBigEndian); 83 | if (iULong > 2147483647) 84 | return iULong - 4294967296; 85 | else 86 | return iULong; 87 | }; 88 | // @aadsm 89 | this.getInteger24At = function(iOffset, bBigEndian) { 90 | var iByte1 = this.getByteAt(iOffset), 91 | iByte2 = this.getByteAt(iOffset + 1), 92 | iByte3 = this.getByteAt(iOffset + 2); 93 | 94 | var iInteger = bBigEndian ? 95 | ((((iByte1 << 8) + iByte2) << 8) + iByte3) 96 | : ((((iByte3 << 8) + iByte2) << 8) + iByte1); 97 | if (iInteger < 0) iInteger += 16777216; 98 | return iInteger; 99 | }; 100 | this.getStringAt = function(iOffset, iLength) { 101 | var aStr = []; 102 | for (var i=iOffset,j=0;i 7 | * MIT License [http://www.opensource.org/licenses/mit-license.php] 8 | */ 9 | 10 | 11 | var ID4 = {}; 12 | 13 | ID4.types = { 14 | '0' : 'uint8', 15 | '1' : 'text', 16 | '13' : 'jpeg', 17 | '14' : 'png', 18 | '21' : 'uint8' 19 | }; 20 | ID4.atom = { 21 | '©alb': ['album'], 22 | '©art': ['artist'], 23 | '©ART': ['artist'], 24 | 'aART': ['artist'], 25 | '©day': ['year'], 26 | '©nam': ['title'], 27 | '©gen': ['genre'], 28 | 'trkn': ['track'], 29 | '©wrt': ['composer'], 30 | '©too': ['encoder'], 31 | 'cprt': ['copyright'], 32 | 'covr': ['picture'], 33 | '©grp': ['grouping'], 34 | 'keyw': ['keyword'], 35 | '©lyr': ['lyrics'], 36 | '©cmt': ['comment'], 37 | 'tmpo': ['tempo'], 38 | 'cpil': ['compilation'], 39 | 'disk': ['disc'] 40 | }; 41 | 42 | ID4.loadData = function(data, callback) { 43 | // load the header of the first block 44 | data.loadRange([0, 7], function () { 45 | loadAtom(data, 0, data.getLength(), callback); 46 | }); 47 | }; 48 | 49 | /** 50 | * Make sure that the [offset, offset+7] bytes (the block header) are 51 | * already loaded before calling this function. 52 | */ 53 | function loadAtom(data, offset, length, callback) { 54 | // 8 is the size of the atomSize and atomName fields. 55 | // When reading the current block we always read 8 more bytes in order 56 | // to also read the header of the next block. 57 | var atomSize = data.getLongAt(offset, true); 58 | if (atomSize == 0) return callback(); 59 | var atomName = data.getStringAt(offset + 4, 4); 60 | 61 | // Container atoms 62 | if (['moov', 'udta', 'meta', 'ilst'].indexOf(atomName) > -1) 63 | { 64 | if (atomName == 'meta') offset += 4; // next_item_id (uint32) 65 | data.loadRange([offset+8, offset+8 + 8], function() { 66 | loadAtom(data, offset + 8, atomSize - 8, callback); 67 | }); 68 | } else { 69 | // Value atoms 70 | var readAtom = atomName in ID4.atom; 71 | data.loadRange([offset+(readAtom?0:atomSize), offset+atomSize + 8], function() { 72 | loadAtom(data, offset+atomSize, length, callback); 73 | }); 74 | } 75 | }; 76 | 77 | ID4.readTagsFromData = function(data) { 78 | var tag = {}; 79 | readAtom(tag, data, 0, data.getLength()); 80 | return tag; 81 | }; 82 | 83 | function readAtom(tag, data, offset, length, indent) 84 | { 85 | indent = indent === undefined ? "" : indent + " "; 86 | var seek = offset; 87 | while (seek < offset + length) 88 | { 89 | var atomSize = data.getLongAt(seek, true); 90 | if (atomSize == 0) return; 91 | var atomName = data.getStringAt(seek + 4, 4); 92 | // Container atoms 93 | if (['moov', 'udta', 'meta', 'ilst'].indexOf(atomName) > -1) 94 | { 95 | if (atomName == 'meta') seek += 4; // next_item_id (uint32) 96 | readAtom(tag, data, seek + 8, atomSize - 8, indent); 97 | return; 98 | } 99 | // Value atoms 100 | if (ID4.atom[atomName]) 101 | { 102 | var klass = data.getInteger24At(seek + 16 + 1, true); 103 | var atom = ID4.atom[atomName]; 104 | var type = ID4.types[klass]; 105 | if (atomName == 'trkn') 106 | { 107 | tag[atom[0]] = data.getByteAt(seek + 16 + 11); 108 | tag['count'] = data.getByteAt(seek + 16 + 13); 109 | } 110 | else 111 | { 112 | // 16: name + size + "data" + size (4 bytes each) 113 | // 4: atom version (1 byte) + atom flags (3 bytes) 114 | // 4: NULL (usually locale indicator) 115 | var dataStart = seek + 16 + 4 + 4; 116 | var dataEnd = atomSize - 16 - 4 - 4; 117 | var atomData; 118 | switch( type ) { 119 | case 'text': 120 | atomData = data.getStringWithCharsetAt(dataStart, dataEnd, "UTF-8"); 121 | break; 122 | 123 | case 'uint8': 124 | atomData = data.getShortAt(dataStart); 125 | break; 126 | 127 | case 'jpeg': 128 | case 'png': 129 | atomData = { 130 | format : "image/" + type, 131 | data : data.getBytesAt(dataStart, dataEnd) 132 | }; 133 | break; 134 | } 135 | 136 | if (atom[0] === "comment") { 137 | tag[atom[0]] = { 138 | "text": atomData 139 | }; 140 | } else { 141 | tag[atom[0]] = atomData; 142 | } 143 | } 144 | } 145 | seek += atomSize; 146 | } 147 | } 148 | 149 | module.exports = ID4; 150 | -------------------------------------------------------------------------------- /src/id3v2frames.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript ID3 Tag Reader 1.0.0 3 | * Copyright (c) 2009 Opera Software ASA, António Afonso (antonio.afonso@opera.com) 4 | * Modified by António Afonso 5 | */ 6 | 7 | var ID3v2 = { 8 | readFrameData: {} 9 | }; 10 | 11 | var pictureType = [ 12 | "32x32 pixels 'file icon' (PNG only)", 13 | "Other file icon", 14 | "Cover (front)", 15 | "Cover (back)", 16 | "Leaflet page", 17 | "Media (e.g. lable side of CD)", 18 | "Lead artist/lead performer/soloist", 19 | "Artist/performer", 20 | "Conductor", 21 | "Band/Orchestra", 22 | "Composer", 23 | "Lyricist/text writer", 24 | "Recording Location", 25 | "During recording", 26 | "During performance", 27 | "Movie/video screen capture", 28 | "A bright coloured fish", 29 | "Illustration", 30 | "Band/artist logotype", 31 | "Publisher/Studio logotype" 32 | ]; 33 | 34 | function getTextEncoding( bite ) { 35 | var charset; 36 | switch( bite ) 37 | { 38 | case 0x00: 39 | charset = 'iso-8859-1'; 40 | break; 41 | 42 | case 0x01: 43 | charset = 'utf-16'; 44 | break; 45 | 46 | case 0x02: 47 | charset = 'utf-16be'; 48 | break; 49 | 50 | case 0x03: 51 | charset = 'utf-8'; 52 | break; 53 | } 54 | 55 | return charset; 56 | } 57 | 58 | function getTime( duration ) 59 | { 60 | var duration = duration/1000, 61 | seconds = Math.floor( duration ) % 60, 62 | minutes = Math.floor( duration/60 ) % 60, 63 | hours = Math.floor( duration/3600 ); 64 | 65 | return { 66 | seconds : seconds, 67 | minutes : minutes, 68 | hours : hours 69 | }; 70 | } 71 | 72 | function formatTime( time ) 73 | { 74 | var seconds = time.seconds < 10 ? '0'+time.seconds : time.seconds; 75 | var minutes = (time.hours > 0 && time.minutes < 10) ? '0'+time.minutes : time.minutes; 76 | 77 | return (time.hours>0?time.hours+':':'') + minutes + ':' + seconds; 78 | } 79 | 80 | ID3v2.readFrameData['APIC'] = function readPictureFrame(offset, length, data, flags, v) { 81 | v = v || '3'; 82 | 83 | var start = offset; 84 | var charset = getTextEncoding( data.getByteAt(offset) ); 85 | switch( v ) { 86 | case '2': 87 | var format = data.getStringAt(offset+1, 3); 88 | offset += 4; 89 | break; 90 | 91 | case '3': 92 | case '4': 93 | var format = data.getStringWithCharsetAt(offset+1, length - (offset-start), ''); 94 | offset += 1 + format.bytesReadCount; 95 | break; 96 | } 97 | var bite = data.getByteAt(offset, 1); 98 | var type = pictureType[bite]; 99 | var desc = data.getStringWithCharsetAt(offset+1, length - (offset-start), charset); 100 | 101 | offset += 1 + desc.bytesReadCount; 102 | 103 | return { 104 | "format" : format.toString(), 105 | "type" : type, 106 | "description" : desc.toString(), 107 | "data" : data.getBytesAt(offset, (start+length) - offset) 108 | }; 109 | }; 110 | 111 | ID3v2.readFrameData['COMM'] = function readCommentsFrame(offset, length, data) { 112 | var start = offset; 113 | var charset = getTextEncoding( data.getByteAt(offset) ); 114 | var language = data.getStringAt( offset+1, 3 ); 115 | var shortdesc = data.getStringWithCharsetAt(offset+4, length-4, charset); 116 | 117 | offset += 4 + shortdesc.bytesReadCount; 118 | var text = data.getStringWithCharsetAt( offset, (start+length) - offset, charset ); 119 | 120 | return { 121 | language : language, 122 | short_description : shortdesc.toString(), 123 | text : text.toString() 124 | }; 125 | }; 126 | 127 | ID3v2.readFrameData['COM'] = ID3v2.readFrameData['COMM']; 128 | 129 | ID3v2.readFrameData['PIC'] = function(offset, length, data, flags) { 130 | return ID3v2.readFrameData['APIC'](offset, length, data, flags, '2'); 131 | }; 132 | 133 | ID3v2.readFrameData['PCNT'] = function readCounterFrame(offset, length, data) { 134 | // FIXME: implement the rest of the spec 135 | return data.getInteger32At(offset); 136 | }; 137 | 138 | ID3v2.readFrameData['CNT'] = ID3v2.readFrameData['PCNT']; 139 | 140 | ID3v2.readFrameData['T*'] = function readTextFrame(offset, length, data) { 141 | var charset = getTextEncoding( data.getByteAt(offset) ); 142 | 143 | return data.getStringWithCharsetAt(offset+1, length-1, charset).toString(); 144 | }; 145 | 146 | ID3v2.readFrameData['TCON'] = function readGenreFrame(offset, length, data) { 147 | var text = ID3v2.readFrameData['T*'].apply( this, arguments ); 148 | return text.replace(/^\(\d+\)/, ''); 149 | }; 150 | 151 | ID3v2.readFrameData['TCO'] = ID3v2.readFrameData['TCON']; 152 | 153 | //ID3v2.readFrameData['TLEN'] = function readLengthFrame(offset, length, data) { 154 | // var text = ID3v2.readFrameData['T*'].apply( this, arguments ); 155 | // 156 | // return { 157 | // text : text, 158 | // parsed : formatTime( getTime(parseInt(text)) ) 159 | // }; 160 | //}; 161 | 162 | ID3v2.readFrameData['USLT'] = function readLyricsFrame(offset, length, data) { 163 | var start = offset; 164 | var charset = getTextEncoding( data.getByteAt(offset) ); 165 | var language = data.getStringAt( offset+1, 3 ); 166 | var descriptor = data.getStringWithCharsetAt( offset+4, length-4, charset ); 167 | 168 | offset += 4 + descriptor.bytesReadCount; 169 | var lyrics = data.getStringWithCharsetAt( offset, (start+length) - offset, charset ); 170 | 171 | return { 172 | language : language, 173 | descriptor : descriptor.toString(), 174 | lyrics : lyrics.toString() 175 | }; 176 | }; 177 | 178 | ID3v2.readFrameData['ULT'] = ID3v2.readFrameData['USLT']; 179 | 180 | module.exports = ID3v2; 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: This project has been superseded by [jsmediatags](https://github.com/aadsm/jsmediatags) and is no longer under maintainance, the reasons: 2 | * Browser and node support ([find it on npm](https://www.npmjs.com/package/jsmediatags)). 3 | * Better code structure 4 | * Extensible 5 | * Unit tests 6 | 7 | Use this [guide](https://github.com/aadsm/jsmediatags#javascript-id3-reader) to migrate to `jsmediatags`. 8 | 9 | JavaScript ID3 Reader 10 | ===================== 11 | 12 | This library was originally made by Jacob Seidelin using ID3v1 for demo'ing his BinaryAjax library [http://www.nihilogic.dk/labs/id3/]. 13 | It was then extended by me (António Afonso) to include the ID3v2 tag specification [http://www.id3.org/id3v2.4.0-structure], while I was working at Opera Software, in the context of the Unite Media Player application which was developed using server side JavaScript. 14 | Joshua Kifer implemented the tag reader for the QuickTime metadata information found in aac files. 15 | A new BufferedBinaryFile was created that extends BinaryFile in a way that only required data will be downloaded from the server. This makes it possible to read tag structures such as the Quicktime metadata without having to download the entire file as it was happening in previous versions of this library. 16 | 17 | Demo: http://www.aadsm.net/libraries/id3/#demo 18 | 19 | Technical Information 20 | --------------------- 21 | 22 | This library will only download the relevant data from the mp3 file whenever the webserver supports the HTTP Range feature, otherwise the entire file will be downloaded at the cost of degrading the performance of the library. 23 | Another caveat is on the Opera browser, since it lacks support for setting the Range header, the entire file will be downloaded. 24 | This library is not complete and there is still some features missing and/or going on: 25 | 26 | * Unsynchronisation support 27 | * CommonJS support 28 | * Support for other types of charsets, at the moment only latin1, UTF-8 and UTF-16 are supported, these are the ones defined in the specification. However, the usage of local charsets has been common for a long time specially in Russia, Japan and China. This support can be achieved using [chardet](http://github.com/aadsm/jschardet) and a proper string reader function. 29 | 30 | How To Use It 31 | ------------- 32 | 33 | In its simplest form: 34 | ```javascript 35 | ID3.loadTags("filename.mp3", function() { 36 | var tags = ID3.getAllTags("filename.mp3"); 37 | alert(tags.artist + " - " + tags.title + ", " + tags.album); 38 | }); 39 | ``` 40 | 41 | by specifying specific tags: 42 | ```javascript 43 | ID3.loadTags("filename.mp3", function() { 44 | var tags = ID3.getAllTags("filename.mp3"); 45 | alert(tags.COMM.data + " - " + tags.TCON.data + ", " + tags.WXXX.data); 46 | }, 47 | {tags: ["COMM", "TCON", "WXXX"]}); 48 | ``` 49 | 50 | or even by specifying shortcuts instead of cryptic tags: 51 | ```javascript 52 | ID3.loadTags("filename.mp3", function() { 53 | var tags = ID3.getAllTags("filename.mp3"); 54 | alert(tags.comment + " - " + tags.track + ", " + tags.lyrics); 55 | }, 56 | {tags: ["comment", "track", "lyrics"]}); 57 | ``` 58 | 59 | handling errors: 60 | ```javascript 61 | ID3.loadTags("http://localhost/filename.mp3", function() { 62 | var tags = ID3.getAllTags("http://localhost/filename.mp3"); 63 | alert(tags.comment + " - " + tags.track + ", " + tags.lyrics); 64 | }, 65 | { 66 | tags: ["comment", "track", "lyrics"], 67 | onError: function(reason) { 68 | if (reason.error === "xhr") { 69 | console.log("There was a network error: ", reason.xhr); 70 | } 71 | } 72 | }); 73 | ``` 74 | 75 | ### Cordova / PhoneGap 76 | 77 | [Raymond Camden](https://github.com/cfjedimaster) wrote a pretty nice blog post on this topic: http://www.raymondcamden.com/2015/04/30/working-with-mp3s-id3-and-phonegapcordova-2 78 | 79 | File API 80 | -------- 81 | Reading a music file through the File API can be done by using the `FileAPIReader` data reader packaged with ID3: 82 | 83 | ```javascript 84 | ID3.loadTags("filename.mp3", function() { 85 | var tags = ID3.getAllTags("filename.mp3"); 86 | alert(tags.comment + " - " + tags.track + ", " + tags.lyrics); 87 | }, { 88 | dataReader: ID3.FileAPIReader(file) 89 | }); 90 | ``` 91 | `file` is a `File` object as defined by the [File API](http://www.w3.org/TR/FileAPI/). 92 | 93 | Example 94 | ------- 95 | See `/example` for additional information. 96 | Besides open http://www.aadsm.net/libraries/id3/ for a live example. 97 | 98 | Documentation 99 | ------------- 100 | 101 | `ID3.loadTags(url, cb, [options])` 102 | `url` - The URL of the mp3 file to read, this must reside on the same domain (document.domain). 103 | `cb` - The callback function to invoke when the tags are loaded. 104 | `options` - Optional parameters. 105 | `options.tags` - The array of tags and/or shortcuts to read from the ID3 block. Default value is: `["title", "artist", "album", "track"]` 106 | `options.dataReader` - The function used to create the data reader out of a url. It receives (`url`, `success`: callback function that returns the data reader, `fail`: callback function to inform an error setting up the reader). By default it will be BufferedBinaryAjax. 107 | `options.onError` - The function that will be called when an error occurs 108 | . It receives one argument with an error object. The object has an `error` 109 | property indicating the type of error. In the case the error type is 110 | `"xhr"` then an aditional `xhr` property is available with the XHR 111 | object for inspection. 112 | 113 | `ID3.getAllTags(url)` 114 | `url` - The URL of the mp3 file to read, this must be the same value given to `ID3.loadTags()`. 115 | `return value` - This function will return the following object structure, for IDv1: 116 | 117 | { 118 | version: "1.1", 119 | title: string, 120 | artist: string, 121 | album: string, 122 | year: string, 123 | comment: string, 124 | track: string, 125 | genre: string 126 | } 127 | for ID3v2: 128 | 129 | { 130 | version: "2..", 131 | major: integer, 132 | revision: integer, 133 | flags: { 134 | unsynchronisation: boolean, 135 | extended_header: boolean, 136 | experimental_indicator: boolean 137 | }, 138 | size: integer, 139 | *: { 140 | id: integer, 141 | size: integer, 142 | description: string, 143 | data: 144 | }, 145 | *: pointer to .data 146 | } 147 | 148 | for AAC: 149 | 150 | { 151 | album: string, 152 | artist: string, 153 | year: integer, 154 | title: string, 155 | genre: string, 156 | track: integer, 157 | composer': string, 158 | encoder: string, 159 | copyright: string, 160 | picture: { 161 | format: string, 162 | data: bytes[] 163 | }, 164 | grouping: string, 165 | keyword: string, 166 | lyrics: string, 167 | genre: string 168 | } 169 | 170 | ### How to show the cover art from the byte array: 171 | 172 | You can do this by using a `data:` url. 173 | 174 | ```javascript 175 | var base64String = ""; 176 | for (var i = 0; i < image.data.length; i++) { 177 | base64String += String.fromCharCode(image.data[i]); 178 | } 179 | var dataUrl = "data:" + image.format + ";base64," + window.btoa(base64String); 180 | ``` 181 | 182 | ### Currently supported frames on ID3: 183 | 184 | * APIC/PIC: Attached picture 185 | * COMM/COM: Comments 186 | * PCNT/CNT: Play counter 187 | * T*: Text frames 188 | * USLT/ULT: Unsychronized lyric/text transcription 189 | 190 | ### Shortcuts: 191 | 192 | * title: TIT2/TT2 193 | * artist: TPE1/TP1 194 | * album: TALB/TAL 195 | * year: TYER/TYE 196 | * comment: COMM/COM 197 | * track: TRCK/TRK 198 | * genre: TCON/TCO 199 | * picture: APIC/PIC 200 | * lyrics: USLT/ULT 201 | 202 | A comprehensive list of all tags defined in the specification can be found [here](http://www.id3.org/id3v2.3.0#head-e4b3c63f836c3eb26a39be082065c21fba4e0acc) 203 | 204 | ### Cross-Domain Requests (CORS) 205 | When doing CORS requests the browser is not able to read all response HTTP headers unless the response explicitly allows it to. 206 | You need to add the following headers to the response: 207 | ``` 208 | Access-Control-Allow-Origin: 209 | Access-Control-Allow-Headers: If-Modified-Since,Range 210 | Access-Control-Expose-Headers: Accept-Ranges,Content-Encoding,Content-Length,Content-Range 211 | ``` 212 | Otherwise you could get the error `TypeError: block is undefined @ id3/bufferedbinaryajax.js:215` 213 | 214 | ### Module Loaders 215 | This package is packaged with browserify `--standalone`, so it can be used with your favorite flavor of module loaders: 216 | 217 | - requirejs: 218 | 219 | ``` 220 | require('ID3', function (ID3) { 221 | // you may now use ID3 methods on the ID3 object. 222 | }); 223 | ``` 224 | 225 | - CommonJS: 226 | 227 | ``` 228 | var ID3 = require('ID3'); 229 | // do stuff with ID3 230 | ``` 231 | 232 | - SES (Secure Ecma Script) 233 | 234 | ``` 235 | var ID3 = ses.ID3(); 236 | // ID3 is available now. 237 | ``` 238 | 239 | - No module loader: 240 | 241 | ``` 242 | var ID3 = window.ID3 243 | // ID3 is exposed as a global, so you can just use it directly or pull it off the window to be explicit. 244 | ``` 245 | 246 | ### Node.js 247 | 248 | This library is also npm compatible, so it can be required. As of this writing it is not published to the npm repository, but that should be remedied soon. 249 | 250 | You can use ID3 either via browserify or directly on the server: 251 | 252 | ``` 253 | var ID3 = require('id3-reader') 254 | 255 | var fileurl = "https://example.com/path/to/music.mp3" 256 | 257 | ID3.loadTags(fileurl, function() { 258 | var tags = ID3.getAllTags(fileurl); 259 | console.log(tags); 260 | // tags are now available. 261 | }, { 262 | onError: function(reason) { 263 | if (reason.error === "xhr") { 264 | console.log("There was a network error: ", reason.xhr); 265 | } 266 | } 267 | }); 268 | ``` 269 | 270 | Authors 271 | ------- 272 | * Jacob Seidelin 273 | * António Afonso 274 | * Joshua Kifer 275 | * Jesse Ditson 276 | -------------------------------------------------------------------------------- /src/bufferedbinaryajax.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Buffered Binary Ajax 0.2.1 3 | * Copyright (c) 2010 António Afonso, antonio.afonso gmail, http://www.aadsm.net/ 4 | * MIT License [http://www.opensource.org/licenses/mit-license.php] 5 | * 6 | * Adapted from Binary Ajax 0.1.5 7 | */ 8 | 9 | /** 10 | * This function prepares a BufferedBinaryFile object for reading the file pointed by the URL given. 11 | * 12 | * @param {String} strUrl The URL with the location of the file to be read. 13 | * @param {function(BufferedBinaryFile)} fncCallback The function that will be invoked when the BufferedBinaryFile is ready to be used. 14 | * @param {function()} fncError The function that will be invoked when an error occrus, for instance, the file pointed by the URL is doesn't exist. 15 | */ 16 | 17 | var BinaryFile = require('./binaryfile'); 18 | 19 | var BufferedBinaryAjax = function(strUrl, fncCallback, fncError) { 20 | 21 | function sendRequest(strURL, fncCallback, fncError, aRange, bAcceptRanges, iFileSize, bAsync) { 22 | var oHTTP = createRequest(); 23 | if (oHTTP) { 24 | var iDataOffset = 0; 25 | if (aRange && !bAcceptRanges) { 26 | iDataOffset = aRange[0]; 27 | } 28 | var iDataLen = 0; 29 | if (aRange) { 30 | iDataLen = aRange[1]-aRange[0]+1; 31 | } 32 | if( typeof bAsync === "undefined" ) bAsync = true; 33 | 34 | if (fncCallback) { 35 | if (typeof(oHTTP.onload) != "undefined") { 36 | oHTTP.onload = function() { 37 | 38 | if (oHTTP.status == "200" || oHTTP.status == "206") { 39 | oHTTP.fileSize = iFileSize || oHTTP.getResponseHeader("Content-Length"); 40 | fncCallback(oHTTP); 41 | } else { 42 | if (fncError) { 43 | fncError({error: "xhr", "xhr": oHTTP}); 44 | } 45 | } 46 | oHTTP = null; 47 | }; 48 | if (fncError) { 49 | oHTTP.onerror = function() { 50 | fncError({error: "xhr", "xhr": oHTTP}); 51 | oHTTP = null; 52 | }; 53 | } 54 | } else { 55 | oHTTP.onreadystatechange = function() { 56 | if (oHTTP.readyState == 4) { 57 | if (oHTTP.status == "200" || oHTTP.status == "206") { 58 | oHTTP.fileSize = iFileSize || oHTTP.getResponseHeader("Content-Length"); 59 | fncCallback(oHTTP); 60 | } else { 61 | if (fncError) { 62 | fncError({error: "xhr", "xhr": oHTTP}); 63 | } 64 | } 65 | oHTTP = null; 66 | } 67 | }; 68 | } 69 | } 70 | oHTTP.open("GET", strURL, bAsync); 71 | 72 | if (oHTTP.overrideMimeType) oHTTP.overrideMimeType('text/plain; charset=x-user-defined'); 73 | 74 | if (aRange && bAcceptRanges) { 75 | oHTTP.setRequestHeader("Range", "bytes=" + aRange[0] + "-" + aRange[1]); 76 | } 77 | 78 | oHTTP.setRequestHeader("If-Modified-Since", "Sat, 01 Jan 1970 00:00:00 GMT"); 79 | 80 | oHTTP.send(null); 81 | } else { 82 | if (fncError) { 83 | fncError({error: "Unable to create XHR object"}); 84 | } 85 | } 86 | } 87 | function createRequest() { 88 | var oHTTP = null; 89 | if (typeof window === 'undefined') { 90 | oHTTP = new (require("xmlhttprequest").XMLHttpRequest)(); 91 | } else if (window.XMLHttpRequest) { 92 | oHTTP = new window.XMLHttpRequest(); 93 | } else if (window.ActiveXObject) { 94 | oHTTP = new window.ActiveXObject("Microsoft.XMLHTTP"); 95 | } 96 | return oHTTP; 97 | } 98 | 99 | function getHead(strURL, fncCallback, fncError) { 100 | var oHTTP = createRequest(); 101 | if (oHTTP) { 102 | if (fncCallback) { 103 | if (typeof(oHTTP.onload) != "undefined") { 104 | oHTTP.onload = function() { 105 | if (oHTTP.status == "200" || oHTTP.status == "206") { 106 | fncCallback(this); 107 | } else { 108 | if (fncError) { 109 | fncError({error: "xhr", "xhr": oHTTP}); 110 | } 111 | } 112 | oHTTP = null; 113 | }; 114 | if (fncError) { 115 | oHTTP.onerror = function() { 116 | fncError({error: "xhr", "xhr": oHTTP}); 117 | oHTTP = null; 118 | }; 119 | } 120 | } else { 121 | oHTTP.onreadystatechange = function() { 122 | if (oHTTP.readyState == 4) { 123 | if (oHTTP.status == "200" || oHTTP.status == "206") { 124 | fncCallback(this); 125 | } else { 126 | if (fncError) { 127 | fncError({error: "xhr", "xhr": oHTTP}); 128 | } 129 | } 130 | oHTTP = null; 131 | } 132 | }; 133 | } 134 | } 135 | oHTTP.open("HEAD", strURL, true); 136 | oHTTP.send(null); 137 | } else { 138 | if (fncError) { 139 | fncError({error: "Unable to create XHR object"}); 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * @class Reads a remote file without having to download it all. 146 | * 147 | * Creates a new BufferedBinaryFile that will download chunks of the file pointed by the URL given only on a per need basis. 148 | * 149 | * @param {string} strUrl The URL with the location of the file to be read. 150 | * @param {number} iLength The size of the file. 151 | * @param {number} [blockSize=2048] The size of the chunk that will be downloaded when data is read. 152 | * @param {number} [blockRadius=0] The number of chunks, immediately after and before the chunk needed, that will also be downloaded. 153 | * 154 | * @constructor 155 | * @augments BinaryFile 156 | */ 157 | function BufferedBinaryFile(strUrl, iLength, blockSize, blockRadius) { 158 | var undefined; 159 | var downloadedBytesCount = 0; 160 | var binaryFile = new BinaryFile("", 0, iLength); 161 | var blocks = []; 162 | 163 | blockSize = blockSize || 1024*2; 164 | blockRadius = (typeof blockRadius === "undefined") ? 0 : blockRadius; 165 | blockTotal = ~~((iLength-1)/blockSize) + 1; 166 | 167 | function getBlockRangeForByteRange(range) { 168 | var blockStart = ~~(range[0]/blockSize) - blockRadius; 169 | var blockEnd = ~~(range[1]/blockSize)+1 + blockRadius; 170 | 171 | if( blockStart < 0 ) blockStart = 0; 172 | if( blockEnd >= blockTotal ) blockEnd = blockTotal-1; 173 | 174 | return [blockStart, blockEnd]; 175 | } 176 | 177 | // TODO: wondering if a "recently used block" could help things around 178 | // here. 179 | function getBlockAtOffset(offset) { 180 | var blockRange = getBlockRangeForByteRange([offset, offset]); 181 | waitForBlocks(blockRange); 182 | return blocks[~~(offset/blockSize)]; 183 | } 184 | 185 | /** 186 | * @param {?function()} callback If a function is passed then this function will be asynchronous and the callback invoked when the blocks have been loaded, otherwise it blocks script execution until the request is completed. 187 | */ 188 | function waitForBlocks(blockRange, callback) { 189 | // Filter out already downloaded blocks or return if found out that 190 | // the entire block range has already been downloaded. 191 | while( blocks[blockRange[0]] ) { 192 | blockRange[0]++; 193 | if( blockRange[0] > blockRange[1] ) return callback ? callback() : undefined; 194 | } 195 | while( blocks[blockRange[1]] ) { 196 | blockRange[1]--; 197 | if( blockRange[0] > blockRange[1] ) return callback ? callback() : undefined; 198 | } 199 | var range = [blockRange[0]*blockSize, (blockRange[1]+1)*blockSize-1]; 200 | //console.log("Getting: " + range[0] + " to " + range[1]); 201 | sendRequest( 202 | strUrl, 203 | function(http) { 204 | var size = parseInt(http.getResponseHeader("Content-Length"), 10); 205 | // Range header not supported 206 | if( size == iLength ) { 207 | blockRange[0] = 0; 208 | blockRange[1] = blockTotal-1; 209 | range[0] = 0; 210 | range[1] = iLength-1; 211 | } 212 | var block = { 213 | data: http.responseBody || http.responseText, 214 | offset: range[0] 215 | }; 216 | 217 | for( var i = blockRange[0]; i <= blockRange[1]; i++ ) { 218 | blocks[i] = block; 219 | } 220 | downloadedBytesCount += range[1] - range[0] + 1; 221 | if (callback) callback(); 222 | }, 223 | fncError, 224 | range, 225 | "bytes", 226 | undefined, 227 | !!callback 228 | ); 229 | } 230 | 231 | // Mixin all BinaryFile's methods. 232 | // Not using prototype linking since the constructor needs to know 233 | // the length of the file. 234 | for( var key in binaryFile ) { 235 | if( binaryFile.hasOwnProperty(key) && 236 | typeof binaryFile[key] === "function") { 237 | this[key] = binaryFile[key]; 238 | } 239 | } 240 | /** 241 | * @override 242 | */ 243 | this.getByteAt = function(iOffset) { 244 | var block = getBlockAtOffset(iOffset); 245 | if( block && typeof block.data == "string" ) { 246 | return block.data.charCodeAt(iOffset - block.offset) & 0xFF; 247 | } else if( block && typeof block.data == "unknown" ) { 248 | return IEBinary_getByteAt(block.data, iOffset - block.offset); 249 | } else { 250 | return "" 251 | } 252 | }; 253 | 254 | /** 255 | * Gets the number of total bytes that have been downloaded. 256 | * 257 | * @returns The number of total bytes that have been downloaded. 258 | */ 259 | this.getDownloadedBytesCount = function() { 260 | return downloadedBytesCount; 261 | }; 262 | 263 | /** 264 | * Downloads the byte range given. Useful for preloading. 265 | * 266 | * @param {Array} range Two element array that denotes the first byte to be read on the first position and the last byte to be read on the last position. A range of [2, 5] will download bytes 2,3,4 and 5. 267 | * @param {?function()} callback The function to invoke when the blocks have been downloaded, this makes this call asynchronous. 268 | */ 269 | this.loadRange = function(range, callback) { 270 | var blockRange = getBlockRangeForByteRange(range); 271 | waitForBlocks(blockRange, callback); 272 | }; 273 | } 274 | 275 | function init() { 276 | getHead( 277 | strUrl, 278 | function(oHTTP) { 279 | var iLength = parseInt(oHTTP.getResponseHeader("Content-Length"),10) || -1; 280 | fncCallback(new BufferedBinaryFile(strUrl, iLength)); 281 | }, 282 | fncError 283 | ); 284 | } 285 | 286 | init(); 287 | }; 288 | 289 | module.exports = BufferedBinaryAjax; 290 | -------------------------------------------------------------------------------- /src/id3v2.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript ID3 Tag Reader 1.0.0 3 | * Copyright (c) 2009 Opera Software ASA, António Afonso (antonio.afonso@opera.com) 4 | * Modified by António Afonso 5 | */ 6 | 7 | 8 | var ID3v2 = require('./id3v2frames'); 9 | 10 | ID3v2.frames = { 11 | // v2.2 12 | "BUF" : "Recommended buffer size", 13 | "CNT" : "Play counter", 14 | "COM" : "Comments", 15 | "CRA" : "Audio encryption", 16 | "CRM" : "Encrypted meta frame", 17 | "ETC" : "Event timing codes", 18 | "EQU" : "Equalization", 19 | "GEO" : "General encapsulated object", 20 | "IPL" : "Involved people list", 21 | "LNK" : "Linked information", 22 | "MCI" : "Music CD Identifier", 23 | "MLL" : "MPEG location lookup table", 24 | "PIC" : "Attached picture", 25 | "POP" : "Popularimeter", 26 | "REV" : "Reverb", 27 | "RVA" : "Relative volume adjustment", 28 | "SLT" : "Synchronized lyric/text", 29 | "STC" : "Synced tempo codes", 30 | "TAL" : "Album/Movie/Show title", 31 | "TBP" : "BPM (Beats Per Minute)", 32 | "TCM" : "Composer", 33 | "TCO" : "Content type", 34 | "TCR" : "Copyright message", 35 | "TDA" : "Date", 36 | "TDY" : "Playlist delay", 37 | "TEN" : "Encoded by", 38 | "TFT" : "File type", 39 | "TIM" : "Time", 40 | "TKE" : "Initial key", 41 | "TLA" : "Language(s)", 42 | "TLE" : "Length", 43 | "TMT" : "Media type", 44 | "TOA" : "Original artist(s)/performer(s)", 45 | "TOF" : "Original filename", 46 | "TOL" : "Original Lyricist(s)/text writer(s)", 47 | "TOR" : "Original release year", 48 | "TOT" : "Original album/Movie/Show title", 49 | "TP1" : "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group", 50 | "TP2" : "Band/Orchestra/Accompaniment", 51 | "TP3" : "Conductor/Performer refinement", 52 | "TP4" : "Interpreted, remixed, or otherwise modified by", 53 | "TPA" : "Part of a set", 54 | "TPB" : "Publisher", 55 | "TRC" : "ISRC (International Standard Recording Code)", 56 | "TRD" : "Recording dates", 57 | "TRK" : "Track number/Position in set", 58 | "TSI" : "Size", 59 | "TSS" : "Software/hardware and settings used for encoding", 60 | "TT1" : "Content group description", 61 | "TT2" : "Title/Songname/Content description", 62 | "TT3" : "Subtitle/Description refinement", 63 | "TXT" : "Lyricist/text writer", 64 | "TXX" : "User defined text information frame", 65 | "TYE" : "Year", 66 | "UFI" : "Unique file identifier", 67 | "ULT" : "Unsychronized lyric/text transcription", 68 | "WAF" : "Official audio file webpage", 69 | "WAR" : "Official artist/performer webpage", 70 | "WAS" : "Official audio source webpage", 71 | "WCM" : "Commercial information", 72 | "WCP" : "Copyright/Legal information", 73 | "WPB" : "Publishers official webpage", 74 | "WXX" : "User defined URL link frame", 75 | // v2.3 76 | "AENC" : "Audio encryption", 77 | "APIC" : "Attached picture", 78 | "COMM" : "Comments", 79 | "COMR" : "Commercial frame", 80 | "ENCR" : "Encryption method registration", 81 | "EQUA" : "Equalization", 82 | "ETCO" : "Event timing codes", 83 | "GEOB" : "General encapsulated object", 84 | "GRID" : "Group identification registration", 85 | "IPLS" : "Involved people list", 86 | "LINK" : "Linked information", 87 | "MCDI" : "Music CD identifier", 88 | "MLLT" : "MPEG location lookup table", 89 | "OWNE" : "Ownership frame", 90 | "PRIV" : "Private frame", 91 | "PCNT" : "Play counter", 92 | "POPM" : "Popularimeter", 93 | "POSS" : "Position synchronisation frame", 94 | "RBUF" : "Recommended buffer size", 95 | "RVAD" : "Relative volume adjustment", 96 | "RVRB" : "Reverb", 97 | "SYLT" : "Synchronized lyric/text", 98 | "SYTC" : "Synchronized tempo codes", 99 | "TALB" : "Album/Movie/Show title", 100 | "TBPM" : "BPM (beats per minute)", 101 | "TCOM" : "Composer", 102 | "TCON" : "Content type", 103 | "TCOP" : "Copyright message", 104 | "TDAT" : "Date", 105 | "TDLY" : "Playlist delay", 106 | "TENC" : "Encoded by", 107 | "TEXT" : "Lyricist/Text writer", 108 | "TFLT" : "File type", 109 | "TIME" : "Time", 110 | "TIT1" : "Content group description", 111 | "TIT2" : "Title/songname/content description", 112 | "TIT3" : "Subtitle/Description refinement", 113 | "TKEY" : "Initial key", 114 | "TLAN" : "Language(s)", 115 | "TLEN" : "Length", 116 | "TMED" : "Media type", 117 | "TOAL" : "Original album/movie/show title", 118 | "TOFN" : "Original filename", 119 | "TOLY" : "Original lyricist(s)/text writer(s)", 120 | "TOPE" : "Original artist(s)/performer(s)", 121 | "TORY" : "Original release year", 122 | "TOWN" : "File owner/licensee", 123 | "TPE1" : "Lead performer(s)/Soloist(s)", 124 | "TPE2" : "Band/orchestra/accompaniment", 125 | "TPE3" : "Conductor/performer refinement", 126 | "TPE4" : "Interpreted, remixed, or otherwise modified by", 127 | "TPOS" : "Part of a set", 128 | "TPUB" : "Publisher", 129 | "TRCK" : "Track number/Position in set", 130 | "TRDA" : "Recording dates", 131 | "TRSN" : "Internet radio station name", 132 | "TRSO" : "Internet radio station owner", 133 | "TSIZ" : "Size", 134 | "TSRC" : "ISRC (international standard recording code)", 135 | "TSSE" : "Software/Hardware and settings used for encoding", 136 | "TYER" : "Year", 137 | "TXXX" : "User defined text information frame", 138 | "UFID" : "Unique file identifier", 139 | "USER" : "Terms of use", 140 | "USLT" : "Unsychronized lyric/text transcription", 141 | "WCOM" : "Commercial information", 142 | "WCOP" : "Copyright/Legal information", 143 | "WOAF" : "Official audio file webpage", 144 | "WOAR" : "Official artist/performer webpage", 145 | "WOAS" : "Official audio source webpage", 146 | "WORS" : "Official internet radio station homepage", 147 | "WPAY" : "Payment", 148 | "WPUB" : "Publishers official webpage", 149 | "WXXX" : "User defined URL link frame" 150 | }; 151 | 152 | var _shortcuts = { 153 | "title" : ["TIT2", "TT2"], 154 | "artist" : ["TPE1", "TP1"], 155 | "album" : ["TALB", "TAL"], 156 | "year" : ["TYER", "TYE"], 157 | "comment" : ["COMM", "COM"], 158 | "track" : ["TRCK", "TRK"], 159 | "genre" : ["TCON", "TCO"], 160 | "picture" : ["APIC", "PIC"], 161 | "lyrics" : ["USLT", "ULT"] 162 | }; 163 | var _defaultShortcuts = ["title", "artist", "album", "track"]; 164 | 165 | function getTagsFromShortcuts(shortcuts) { 166 | var tags = []; 167 | for( var i = 0, shortcut; shortcut = shortcuts[i]; i++ ) { 168 | tags = tags.concat(_shortcuts[shortcut]||[shortcut]); 169 | } 170 | return tags; 171 | } 172 | 173 | // The ID3v2 tag/frame size is encoded with four bytes where the most significant bit (bit 7) is set to zero in every byte, making a total of 28 bits. The zeroed bits are ignored, so a 257 bytes long tag is represented as $00 00 02 01. 174 | function readSynchsafeInteger32At(offset, data) { 175 | var size1 = data.getByteAt(offset); 176 | var size2 = data.getByteAt(offset+1); 177 | var size3 = data.getByteAt(offset+2); 178 | var size4 = data.getByteAt(offset+3); 179 | // 0x7f = 0b01111111 180 | var size = size4 & 0x7f 181 | | ((size3 & 0x7f) << 7) 182 | | ((size2 & 0x7f) << 14) 183 | | ((size1 & 0x7f) << 21); 184 | 185 | return size; 186 | } 187 | 188 | function readFrameFlags(data, offset) 189 | { 190 | var flags = 191 | { 192 | message: 193 | { 194 | tag_alter_preservation : data.isBitSetAt( offset, 6), 195 | file_alter_preservation : data.isBitSetAt( offset, 5), 196 | read_only : data.isBitSetAt( offset, 4) 197 | }, 198 | format: 199 | { 200 | grouping_identity : data.isBitSetAt( offset+1, 7), 201 | compression : data.isBitSetAt( offset+1, 3), 202 | encription : data.isBitSetAt( offset+1, 2), 203 | unsynchronisation : data.isBitSetAt( offset+1, 1), 204 | data_length_indicator : data.isBitSetAt( offset+1, 0) 205 | } 206 | }; 207 | 208 | return flags; 209 | } 210 | 211 | /** All the frames consists of a frame header followed by one or more fields containing the actual information. 212 | * The frame ID made out of the characters capital A-Z and 0-9. Identifiers beginning with "X", "Y" and "Z" are for experimental use and free for everyone to use, without the need to set the experimental bit in the tag header. Have in mind that someone else might have used the same identifier as you. All other identifiers are either used or reserved for future use. 213 | * The frame ID is followed by a size descriptor, making a total header size of ten bytes in every frame. The size is calculated as frame size excluding frame header (frame size - 10). 214 | */ 215 | function readFrames(offset, end, data, id3header, tags) 216 | { 217 | var frames = {}; 218 | var frameDataSize; 219 | var major = id3header["major"]; 220 | 221 | tags = getTagsFromShortcuts(tags || _defaultShortcuts); 222 | 223 | while( offset < end ) { 224 | var readFrameFunc = null; 225 | var frameData = data; 226 | var frameDataOffset = offset; 227 | var flags = null; 228 | 229 | switch( major ) { 230 | case 2: 231 | var frameID = frameData.getStringAt(frameDataOffset, 3); 232 | var frameSize = frameData.getInteger24At(frameDataOffset+3, true); 233 | var frameHeaderSize = 6; 234 | break; 235 | 236 | case 3: 237 | var frameID = frameData.getStringAt(frameDataOffset, 4); 238 | var frameSize = frameData.getLongAt(frameDataOffset+4, true); 239 | var frameHeaderSize = 10; 240 | break; 241 | 242 | case 4: 243 | var frameID = frameData.getStringAt(frameDataOffset, 4); 244 | var frameSize = readSynchsafeInteger32At(frameDataOffset+4, frameData); 245 | var frameHeaderSize = 10; 246 | break; 247 | } 248 | // if last frame GTFO 249 | if( frameID == "" ) { break; } 250 | 251 | // advance data offset to the next frame data 252 | offset += frameHeaderSize + frameSize; 253 | // skip unwanted tags 254 | if( tags.indexOf( frameID ) < 0 ) { continue; } 255 | 256 | // read frame message and format flags 257 | if( major > 2 ) 258 | { 259 | flags = readFrameFlags(frameData, frameDataOffset+8); 260 | } 261 | 262 | frameDataOffset += frameHeaderSize; 263 | 264 | // the first 4 bytes are the real data size 265 | // (after unsynchronisation && encryption) 266 | if( flags && flags.format.data_length_indicator ) 267 | { 268 | frameDataSize = readSynchsafeInteger32At(frameDataOffset, frameData); 269 | frameDataOffset += 4; 270 | frameSize -= 4; 271 | } 272 | 273 | // TODO: support unsynchronisation 274 | if( flags && flags.format.unsynchronisation ) 275 | { 276 | //frameData = removeUnsynchronisation(frameData, frameSize); 277 | continue; 278 | } 279 | 280 | // find frame parsing function 281 | if( frameID in ID3v2.readFrameData ) { 282 | readFrameFunc = ID3v2.readFrameData[frameID]; 283 | } else if( frameID[0] == "T" ) { 284 | readFrameFunc = ID3v2.readFrameData["T*"]; 285 | } 286 | 287 | var parsedData = readFrameFunc ? readFrameFunc(frameDataOffset, frameSize, frameData, flags) : undefined; 288 | var desc = frameID in ID3v2.frames ? ID3v2.frames[frameID] : 'Unknown'; 289 | 290 | var frame = { 291 | id : frameID, 292 | size : frameSize, 293 | description : desc, 294 | data : parsedData 295 | }; 296 | 297 | if( frameID in frames ) { 298 | if( frames[frameID].id ) { 299 | frames[frameID] = [frames[frameID]]; 300 | } 301 | frames[frameID].push(frame); 302 | } else { 303 | frames[frameID] = frame; 304 | } 305 | } 306 | 307 | return frames; 308 | } 309 | 310 | //function removeUnsynchronisation(data, size) 311 | //{ 312 | // return data; 313 | //} 314 | 315 | function getFrameData( frames, ids ) { 316 | if( typeof ids == 'string' ) { ids = [ids]; } 317 | 318 | for( var i = 0, id; id = ids[i]; i++ ) { 319 | if( id in frames ) { return frames[id].data; } 320 | } 321 | } 322 | 323 | ID3v2.loadData = function(data, callback) { 324 | data.loadRange([0, readSynchsafeInteger32At(6, data)], callback); 325 | }; 326 | 327 | // http://www.id3.org/id3v2.3.0 328 | ID3v2.readTagsFromData = function(data, tags) { 329 | var offset = 0; 330 | var major = data.getByteAt(offset+3); 331 | if( major > 4 ) { return {version: '>2.4'}; } 332 | var revision = data.getByteAt(offset+4); 333 | var unsynch = data.isBitSetAt(offset+5, 7); 334 | var xheader = data.isBitSetAt(offset+5, 6); 335 | var xindicator = data.isBitSetAt(offset+5, 5); 336 | var size = readSynchsafeInteger32At(offset+6, data); 337 | offset += 10; 338 | 339 | if( xheader ) { 340 | var xheadersize = data.getLongAt(offset, true); 341 | // The 'Extended header size', currently 6 or 10 bytes, excludes itself. 342 | offset += xheadersize + 4; 343 | } 344 | 345 | var id3 = { 346 | "version" : '2.' + major + '.' + revision, 347 | "major" : major, 348 | "revision" : revision, 349 | "flags" : { 350 | "unsynchronisation" : unsynch, 351 | "extended_header" : xheader, 352 | "experimental_indicator" : xindicator 353 | }, 354 | "size" : size 355 | }; 356 | var frames = unsynch ? {} : readFrames(offset, size-10, data, id3, tags); 357 | // create shortcuts for most common data 358 | for( var name in _shortcuts ) if(_shortcuts.hasOwnProperty(name)) { 359 | var data = getFrameData( frames, _shortcuts[name] ); 360 | if( data ) id3[name] = data; 361 | } 362 | 363 | for( var frame in frames ) { 364 | if( frames.hasOwnProperty(frame) ) { 365 | id3[frame] = frames[frame]; 366 | } 367 | } 368 | 369 | return id3; 370 | }; 371 | 372 | module.exports = ID3v2; 373 | -------------------------------------------------------------------------------- /dist/id3-minimized.js: -------------------------------------------------------------------------------- 1 | (function(A){if("object"===typeof exports&&"undefined"!==typeof module)module.f=A();else if("function"===typeof define&&define.M)define([],A);else{var g;"undefined"!==typeof window?g=window:"undefined"!==typeof global?g=global:"undefined"!==typeof self?g=self:g=this;g.ID3=A()}})(function(){return function g(l,h,f){function c(b,d){if(!h[b]){if(!l[b]){var e="function"==typeof require&&require;if(!d&&e)return e(b,!0);if(a)return a(b,!0);e=Error("Cannot find module '"+b+"'");throw e.code="MODULE_NOT_FOUND", 2 | e;}e=h[b]={f:{}};l[b][0].call(e.f,function(a){var e=l[b][1][a];return c(e?e:a)},e,e.f,g,l,h,f)}return h[b].f}for(var a="function"==typeof require&&require,b=0;ba&&(a+=65536);return a};this.m=function(a){var b=this.a(a),d=this.a(a+1),m=this.a(a+2);a=this.a(a+3);b=(((b<<8)+d<<8)+m<<8)+a;0>b&&(b+=4294967296);return b};this.w=function(a){var b=this.a(a),d=this.a(a+1);a=this.a(a+2);b=((b<<8)+d<<8)+a;0>b&&(b+=16777216);return b};this.c=function(a,b){for(var d=[],m=a,c=0;mb&&(b=0);a>=blockTotal&&(a=blockTotal-1);return[b,a]}function g(c,f){for(;n[c[0]];)if(c[0]++,c[0]>c[1]){f&&f();return}for(;n[c[1]];)if(c[1]--,c[0]>c[1]){f&&f();return}var h=[c[0]*m,(c[1]+1)*m-1];b(d,function(a){parseInt(a.getResponseHeader("Content-Length"),10)==e&&(c[0]=0,c[1]=blockTotal-1,h[0]=0,h[1]=e-1);a={data:a.W||a.responseText,offset:h[0]};for(var b=c[0];b<=c[1];b++)n[b]= 9 | a;f&&f()},a,h,k,!!f)}var k,l=new h("",0,e),n=[];m=m||2048;c="undefined"===typeof c?0:c;blockTotal=~~((e-1)/m)+1;for(var p in l)l.hasOwnProperty(p)&&"function"===typeof l[p]&&(this[p]=l[p]);this.a=function(a){var b;g(f([a,a]));return(b=n[~~(a/m)])&&"string"==typeof b.data?b.data.charCodeAt(a-b.offset)&255:b&&"unknown"==typeof b.data?IEBinary_getByteAt(b.data,a-b.offset):""};this.i=function(a,b){g(f(a),b)}}(function(){d(f,function(a){a=parseInt(a.getResponseHeader("Content-Length"),10)||-1;c(new e(f, 10 | a))},a)})()}},{"./binaryfile":1,xmlhttprequest:void 0}],3:[function(g,l){var h=g("./binaryfile");l.f=function(f,c){return function(a,b){var m=c||new FileReader;m.onload=function(a){b(new h(a.target.result))};m.readAsBinaryString(f)}}},{"./binaryfile":1}],4:[function(g,l){function h(b){return"ftypM4A"==b.c(4,7)?f:"ID3"==b.c(0,3)?a:c}var f=g("./id4"),c=g("./id3v1"),a=g("./id3v2"),b=g("./bufferedbinaryajax"),m=g("./filereader");"undefined"!==typeof window&&(window.FileAPIReader=m);var d={},e={},r=[0, 11 | 7];d.B=function(a){delete e[a]};d.A=function(){e={}};d.H=function(a,d,c){c=c||{};(c.dataReader||b)(a,function(b){b.i(r,function(){var m=h(b);m.u(b,function(){var f=c.tags,h=m.v(b,f),f=e[a]||{},r;for(r in h)h.hasOwnProperty(r)&&(f[r]=h[r]);e[a]=f;d&&d()})})},c.onError)};d.D=function(a){if(!e[a])return null;var b={},d;for(d in e[a])e[a].hasOwnProperty(d)&&(b[d]=e[a][d]);return b};d.G=function(a,b){return e[a]?e[a][b]:null};d.FileAPIReader=m;d.loadTags=d.H;d.getAllTags=d.D;d.getTag=d.G;d.clearTags=d.B; 12 | d.clearAll=d.A;l.f=d},{"./bufferedbinaryajax":2,"./filereader":3,"./id3v1":5,"./id3v2":6,"./id4":8}],5:[function(g,l){var h={},f="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;Top 40;Christian Rap;Pop/Funk;Jungle;Native American;Cabaret;New Wave;Psychadelic;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;Acapella;Euro-House;Dance Hall".split(";"); 13 | h.u=function(c,a){var b=c.l();c.i([b-128-1,b],a)};h.v=function(c){var a=c.l()-128;if("TAG"==c.c(a,3)){var b=c.c(a+3,30).replace(/\0/g,""),m=c.c(a+33,30).replace(/\0/g,""),d=c.c(a+63,30).replace(/\0/g,""),e=c.c(a+93,4).replace(/\0/g,"");if(0==c.a(a+97+28))var h=c.c(a+97,28).replace(/\0/g,""),g=c.a(a+97+29);else h="",g=0;c=c.a(a+97+30);return{version:"1.1",title:b,artist:m,album:d,year:e,comment:h,track:g,genre:255>c?f[c]:""}}return{}};l.f=h},{}],6:[function(g,l){function h(a,c){var d=c.a(a),e=c.a(a+ 14 | 1),f=c.a(a+2);return c.a(a+3)&127|(f&127)<<7|(e&127)<<14|(d&127)<<21}var f=g("./id3v2frames");f.frames={BUF:"Recommended buffer size",CNT:"Play counter",COM:"Comments",CRA:"Audio encryption",CRM:"Encrypted meta frame",ETC:"Event timing codes",EQU:"Equalization",GEO:"General encapsulated object",IPL:"Involved people list",LNK:"Linked information",MCI:"Music CD Identifier",MLL:"MPEG location lookup table",PIC:"Attached picture",POP:"Popularimeter",REV:"Reverb",RVA:"Relative volume adjustment",SLT:"Synchronized lyric/text", 15 | STC:"Synced tempo codes",TAL:"Album/Movie/Show title",TBP:"BPM (Beats Per Minute)",TCM:"Composer",TCO:"Content type",TCR:"Copyright message",TDA:"Date",TDY:"Playlist delay",TEN:"Encoded by",TFT:"File type",TIM:"Time",TKE:"Initial key",TLA:"Language(s)",TLE:"Length",TMT:"Media type",TOA:"Original artist(s)/performer(s)",TOF:"Original filename",TOL:"Original Lyricist(s)/text writer(s)",TOR:"Original release year",TOT:"Original album/Movie/Show title",TP1:"Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group", 16 | TP2:"Band/Orchestra/Accompaniment",TP3:"Conductor/Performer refinement",TP4:"Interpreted, remixed, or otherwise modified by",TPA:"Part of a set",TPB:"Publisher",TRC:"ISRC (International Standard Recording Code)",TRD:"Recording dates",TRK:"Track number/Position in set",TSI:"Size",TSS:"Software/hardware and settings used for encoding",TT1:"Content group description",TT2:"Title/Songname/Content description",TT3:"Subtitle/Description refinement",TXT:"Lyricist/text writer",TXX:"User defined text information frame", 17 | TYE:"Year",UFI:"Unique file identifier",ULT:"Unsychronized lyric/text transcription",WAF:"Official audio file webpage",WAR:"Official artist/performer webpage",WAS:"Official audio source webpage",WCM:"Commercial information",WCP:"Copyright/Legal information",WPB:"Publishers official webpage",WXX:"User defined URL link frame",AENC:"Audio encryption",APIC:"Attached picture",COMM:"Comments",COMR:"Commercial frame",ENCR:"Encryption method registration",EQUA:"Equalization",ETCO:"Event timing codes",GEOB:"General encapsulated object", 18 | GRID:"Group identification registration",IPLS:"Involved people list",LINK:"Linked information",MCDI:"Music CD identifier",MLLT:"MPEG location lookup table",OWNE:"Ownership frame",PRIV:"Private frame",PCNT:"Play counter",POPM:"Popularimeter",POSS:"Position synchronisation frame",RBUF:"Recommended buffer size",RVAD:"Relative volume adjustment",RVRB:"Reverb",SYLT:"Synchronized lyric/text",SYTC:"Synchronized tempo codes",TALB:"Album/Movie/Show title",TBPM:"BPM (beats per minute)",TCOM:"Composer",TCON:"Content type", 19 | TCOP:"Copyright message",TDAT:"Date",TDLY:"Playlist delay",TENC:"Encoded by",TEXT:"Lyricist/Text writer",TFLT:"File type",TIME:"Time",TIT1:"Content group description",TIT2:"Title/songname/content description",TIT3:"Subtitle/Description refinement",TKEY:"Initial key",TLAN:"Language(s)",TLEN:"Length",TMED:"Media type",TOAL:"Original album/movie/show title",TOFN:"Original filename",TOLY:"Original lyricist(s)/text writer(s)",TOPE:"Original artist(s)/performer(s)",TORY:"Original release year",TOWN:"File owner/licensee", 20 | TPE1:"Lead performer(s)/Soloist(s)",TPE2:"Band/orchestra/accompaniment",TPE3:"Conductor/performer refinement",TPE4:"Interpreted, remixed, or otherwise modified by",TPOS:"Part of a set",TPUB:"Publisher",TRCK:"Track number/Position in set",TRDA:"Recording dates",TRSN:"Internet radio station name",TRSO:"Internet radio station owner",TSIZ:"Size",TSRC:"ISRC (international standard recording code)",TSSE:"Software/Hardware and settings used for encoding",TYER:"Year",TXXX:"User defined text information frame", 21 | UFID:"Unique file identifier",USER:"Terms of use",USLT:"Unsychronized lyric/text transcription",WCOM:"Commercial information",WCOP:"Copyright/Legal information",WOAF:"Official audio file webpage",WOAR:"Official artist/performer webpage",WOAS:"Official audio source webpage",WORS:"Official internet radio station homepage",WPAY:"Payment",WPUB:"Publishers official webpage",WXXX:"User defined URL link frame"};var c={title:["TIT2","TT2"],artist:["TPE1","TP1"],album:["TALB","TAL"],year:["TYER","TYE"],comment:["COMM", 22 | "COM"],track:["TRCK","TRK"],genre:["TCON","TCO"],picture:["APIC","PIC"],lyrics:["USLT","ULT"]},a=["title","artist","album","track"];f.u=function(a,c){a.i([0,h(6,a)],c)};f.v=function(b,m){var d=0,e=b.a(d+3);if(42.4"};var r=b.a(d+4),g=b.g(d+5,7),l=b.g(d+5,6),w=b.g(d+5,5),x=h(d+6,b),d=d+10;if(l)var q=b.m(d),d=d+(q+4);var e={version:"2."+e+"."+r,major:e,revision:r,flags:{unsynchronisation:g,extended_header:l,experimental_indicator:w},size:x},k;if(g)k={};else{for(var x=x-10,g=b,r=m, 23 | l={},w=e.major,q=[],u=0,n;n=(r||a)[u];u++)q=q.concat(c[n]||[n]);for(r=q;dr.indexOf(k))){if(2e||224<=e?f[d]=String.fromCharCode(l): 31 | (e=(h[a+b]<<8)+h[a+g],a+=2,f[d]=String.fromCharCode(l,e))}h=new String(f.join(""));h.j=a;return h},K:function(h,f){var c=0;f=Math.min(f||h.length,h.length);239==h[0]&&187==h[1]&&191==h[2]&&(c=3);for(var a=[],b=0;cg)a[b]=String.fromCharCode(g);else if(194<=g&&224>g){var d=h[c++];a[b]=String.fromCharCode(((g&31)<<6)+(d&63))}else if(224<=g&&240>g){var d=h[c++],e=h[c++];a[b]=String.fromCharCode(((g&255)<<12)+((d&63)<<6)+(e&63))}else if(240<=g&&245>g){var d= 32 | h[c++],e=h[c++],l=h[c++],g=((g&7)<<18)+((d&63)<<12)+((e&63)<<6)+(l&63)-65536;a[b]=String.fromCharCode((g>>10)+55296,(g&1023)+56320)}}a=new String(a.join(""));a.j=c;return a},I:function(g,f){var c=[];f=f||g.length;for(var a=0;a