Initial working version
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
||||
var utils = require('./utils');
|
||||
|
||||
function checkMagicId3v1(view) {
|
||||
var id3Magic = utils.readBytes(view, view.byteLength - 128, 3);
|
||||
//"TAG"
|
||||
return id3Magic[0] === 84 && id3Magic[1] === 65 && id3Magic[2] === 71;
|
||||
}
|
||||
|
||||
module.exports = function(buffer) {
|
||||
//read last 128 bytes
|
||||
var view = utils.createView(buffer);
|
||||
if (!checkMagicId3v1(view)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function trim(value) {
|
||||
return value.replace(/[\s\u0000]+$/, '');
|
||||
}
|
||||
|
||||
try {
|
||||
var offset = view.byteLength - 128 + 3,
|
||||
readAscii = utils.readAscii;
|
||||
var title = readAscii(view, offset, 30),
|
||||
artist = readAscii(view, offset + 30, 30),
|
||||
album = readAscii(view, offset + 60, 30),
|
||||
year = readAscii(view, offset + 90, 4);
|
||||
|
||||
offset += 94;
|
||||
|
||||
var comment = readAscii(view, offset, 28),
|
||||
track = null;
|
||||
offset += 28;
|
||||
if (view.getUint8(offset) === 0) {
|
||||
//next byte is the track
|
||||
track = view.getUint8(offset + 1);
|
||||
} else {
|
||||
comment += readAscii(view, offset, 2);
|
||||
}
|
||||
|
||||
offset += 2;
|
||||
var genre = view.getUint8(offset);
|
||||
return {
|
||||
title: trim(title),
|
||||
artist: trim(artist),
|
||||
album: trim(album),
|
||||
year: trim(year),
|
||||
comment: trim(comment),
|
||||
track: track,
|
||||
genre: genre
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
var utils = require('./utils');
|
||||
|
||||
function checkMagicId3(view, offset) {
|
||||
var id3Magic = utils.readBytes(view, offset, 3);
|
||||
//"ID3"
|
||||
return id3Magic[0] === 73 && id3Magic[1] === 68 && id3Magic[2] === 51;
|
||||
}
|
||||
|
||||
function getUint28(view, offset) {
|
||||
var sizeBytes = utils.readBytes(view, offset, 4);
|
||||
var mask = 0xfffffff;
|
||||
return ((sizeBytes[0] & mask) << 21) |
|
||||
((sizeBytes[1] & mask) << 14) |
|
||||
((sizeBytes[2] & mask) << 7) |
|
||||
(sizeBytes[3] & mask);
|
||||
}
|
||||
|
||||
//http://id3.org/id3v2.3.0
|
||||
//http://id3.org/id3v2.4.0-structure
|
||||
//http://id3.org/id3v2.4.0-frames
|
||||
module.exports = function(buffer) {
|
||||
var view = utils.createView(buffer);
|
||||
if (!checkMagicId3(view, 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var offset = 3;
|
||||
//var majorVersion = view.getUint8(offset);
|
||||
offset += 2;
|
||||
var flags = view.getUint8(offset);
|
||||
offset++;
|
||||
var size = getUint28(view, offset);
|
||||
offset += 4;
|
||||
|
||||
var extendedHeader = (flags & 128) > 0;
|
||||
|
||||
if (extendedHeader) {
|
||||
offset += getUint28(view, offset);
|
||||
}
|
||||
|
||||
function readFrame(offset) {
|
||||
try {
|
||||
var id = utils.readAscii(view, offset, 4);
|
||||
var size = getUint28(view, offset + 4);
|
||||
offset += 10; //+2 more for flags we don't care about
|
||||
|
||||
if (id[0] !== 'T') {
|
||||
return {
|
||||
id: id,
|
||||
size: size + 10
|
||||
};
|
||||
}
|
||||
|
||||
var encoding = view.getUint8(offset),
|
||||
data = '';
|
||||
|
||||
if (encoding <= 3) {
|
||||
offset++;
|
||||
if (encoding === 3) {
|
||||
//UTF8 - null terminated
|
||||
data = utils.readUtf8(view, offset, size - 1);
|
||||
} else {
|
||||
//ISO-8859-1, UTF-16, UTF-16BE
|
||||
//UTF-16 and UTF-16BE are $FF $00 terminated
|
||||
//ISO is null terminated
|
||||
|
||||
//screw these encodings, read it as ascii
|
||||
data = utils.readAscii(view, offset, size - 1);
|
||||
}
|
||||
} else {
|
||||
//no encoding info, read it as ascii
|
||||
data = utils.readAscii(view, offset, size);
|
||||
}
|
||||
|
||||
//id3v2.4 is supposed to have encoding terminations, but sometimes
|
||||
//they don't? meh.
|
||||
data = utils.trimNull(data);
|
||||
|
||||
return {
|
||||
id: id,
|
||||
size: size + 10,
|
||||
content: data
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var idMap = {
|
||||
TALB: 'album',
|
||||
TCOM: 'composer',
|
||||
TIT1: 'title',
|
||||
TIT2: 'title',
|
||||
TPE1: 'artist',
|
||||
TRCK: 'track',
|
||||
TSSE: 'encoder',
|
||||
TDRC: 'year',
|
||||
TCON: 'genre'
|
||||
};
|
||||
|
||||
var endOfTags = offset + size,
|
||||
frames = {};
|
||||
while (offset < endOfTags) {
|
||||
var frame = readFrame(offset);
|
||||
if (!frame) {
|
||||
break;
|
||||
}
|
||||
|
||||
offset += frame.size;
|
||||
if (!frame.content) {
|
||||
continue;
|
||||
}
|
||||
var id = idMap[frame.id] || frame.id;
|
||||
if (id === 'TXXX') {
|
||||
var nullByte = frame.content.indexOf('\u0000');
|
||||
id = frame.content.substring(0, nullByte);
|
||||
frames[id] = frame.content.substring(nullByte + 1);
|
||||
} else {
|
||||
frames[id] = frames[frame.id] = frame.content;
|
||||
}
|
||||
}
|
||||
|
||||
return frames;
|
||||
};
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
var utils = require('./utils');
|
||||
|
||||
/**
|
||||
* See http://www.ietf.org/rfc/rfc3533.txt
|
||||
* @param {Buffer|ArrayBuffer} buffer
|
||||
*/
|
||||
module.exports = function(buffer) {
|
||||
var view = utils.createView(buffer);
|
||||
|
||||
function parsePage(offset, withPacket) {
|
||||
if (view.byteLength < offset + 27) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var numPageSegments = view.getUint8(offset + 26),
|
||||
segmentTable = utils.readBytes(view, offset + 27, numPageSegments),
|
||||
headerSize = 27 + numPageSegments;
|
||||
|
||||
if (!segmentTable.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var
|
||||
pageSize = headerSize + segmentTable.reduce(function(cur, next) {
|
||||
return cur + next;
|
||||
}),
|
||||
length = headerSize + 1 + 'vorbis'.length,
|
||||
packetView = null;
|
||||
|
||||
if (withPacket) {
|
||||
packetView = utils.createView(new ArrayBuffer(pageSize - length));
|
||||
utils.readBytes(view, offset + length, pageSize - length, packetView);
|
||||
}
|
||||
|
||||
return {
|
||||
pageSize: pageSize,
|
||||
packet: packetView
|
||||
};
|
||||
}
|
||||
|
||||
function parseComments(packet) {
|
||||
try {
|
||||
var vendorLength = packet.getUint32(0, true),
|
||||
commentListLength = packet.getUint32(4 + vendorLength, true),
|
||||
comments = {},
|
||||
offset = 8 + vendorLength,
|
||||
map = {
|
||||
tracknumber: 'track'
|
||||
};
|
||||
|
||||
for (var i = 0; i < commentListLength; i++) {
|
||||
var commentLength = packet.getUint32(offset, true),
|
||||
comment = utils.readUtf8(packet, offset + 4, commentLength),
|
||||
equals = comment.indexOf('='),
|
||||
key = comment.substring(0, equals).toLowerCase();
|
||||
|
||||
comments[map[key] || key] = comments[key] = utils.trimNull(comment.substring(equals + 1));
|
||||
offset += 4 + commentLength;
|
||||
}
|
||||
|
||||
return comments;
|
||||
} catch (e) {
|
||||
//all exceptions are just malformed/truncated data, so we just ignore them
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var id = parsePage(0);
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var commentHeader = parsePage(id.pageSize, true);
|
||||
if (!commentHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseComments(commentHeader.packet);
|
||||
};
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
function toArrayBuffer(buffer) {
|
||||
var arrayBuffer = new ArrayBuffer(buffer.length);
|
||||
var view = new Uint8Array(arrayBuffer);
|
||||
for (var i = 0; i < buffer.length; ++i) {
|
||||
view[i] = buffer[i];
|
||||
}
|
||||
return arrayBuffer;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
trimNull: function(s) {
|
||||
return s.replace(/\u0000+$/, '');
|
||||
},
|
||||
|
||||
createView: function(buffer) {
|
||||
if (typeof(Buffer) !== 'undefined' && buffer instanceof Buffer) {
|
||||
//convert nodejs buffers to ArrayBuffer
|
||||
buffer = toArrayBuffer(buffer);
|
||||
}
|
||||
|
||||
if (!(buffer instanceof ArrayBuffer)) {
|
||||
throw new Error('Expected instance of Buffer or ArrayBuffer');
|
||||
}
|
||||
|
||||
return new DataView(buffer);
|
||||
},
|
||||
|
||||
readBytes: function(view, offset, length, target) {
|
||||
if (offset + length < 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var bytes = [];
|
||||
var max = Math.min(offset + length, view.byteLength);
|
||||
for (var i = offset; i < max; i++) {
|
||||
var value = view.getUint8(i);
|
||||
bytes.push(value);
|
||||
if (target) {
|
||||
target.setUint8(i - offset, value);
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
},
|
||||
|
||||
readAscii: function(view, offset, length) {
|
||||
if (view.byteLength < offset + length) {
|
||||
return '';
|
||||
}
|
||||
var s = '';
|
||||
for (var i = 0; i < length; i++) {
|
||||
s += String.fromCharCode(view.getUint8(offset + i));
|
||||
}
|
||||
|
||||
return s;
|
||||
},
|
||||
|
||||
readUtf8: function(view, offset, length) {
|
||||
if (view.byteLength < offset + length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var buffer = view.buffer.slice(offset, offset + length);
|
||||
|
||||
//http://stackoverflow.com/a/17192845 - convert byte array to UTF8 string
|
||||
var encodedString = String.fromCharCode.apply(null, new Uint8Array(buffer));
|
||||
return decodeURIComponent(escape(encodedString));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user