musicmetadata
Advanced tools
Comparing version 0.2.2 to 0.2.3
@@ -5,3 +5,3 @@ var strtok = require('strtok'); | ||
exports.detectMediaType = function (header) { | ||
//default to id3v1.1 if we cannot detect any other tags | ||
// default to id3v1.1 if we cannot detect any other tags | ||
var tag = 'id3v1'; | ||
@@ -24,32 +24,17 @@ if ('ID3' === header.slice(0, 3)) { | ||
stream.Stream.prototype.onRealEnd = function(callback) { | ||
stream.Stream.prototype.onRealEnd = function (callback) { | ||
var called = false; | ||
this.on('end', function() { | ||
if (!called) callback(); | ||
called = true; | ||
}); | ||
}) | ||
this.on('close', function() { | ||
if (!called) callback(); | ||
called = true; | ||
}); | ||
}; | ||
exports.joinBuffers = function(buffers, totalLength) { | ||
var result = new Buffer(totalLength); | ||
var pos = 0 | ||
for (var i=0; i < buffers.length; i++) { | ||
buffers[i].copy(result, pos); | ||
pos += buffers[i].length; | ||
} | ||
return result; | ||
}) | ||
} | ||
exports.readVorbisPicture = function(buffer) { | ||
var picture = {}, | ||
offset = 0; | ||
exports.readVorbisPicture = function (buffer) { | ||
var picture = {}; | ||
var offset = 0; | ||
@@ -75,6 +60,5 @@ picture.type = PICTURE_TYPE[strtok.UINT32_BE.get(buffer, 0)]; | ||
exports.removeUnsyncBytes = function(buffer) { | ||
var readI = 0, | ||
writeI = 0; | ||
exports.removeUnsyncBytes = function (buffer) { | ||
var readI = 0; | ||
var writeI = 0; | ||
while (readI < buffer.length -1) { | ||
@@ -84,22 +68,16 @@ if (readI !== writeI) { | ||
} | ||
readI += (buffer[readI] === 0xFF && buffer[readI + 1] === 0) ? 2 : 1; | ||
writeI++; | ||
} | ||
if (readI < buffer.length) { | ||
buffer[writeI++] = buffer[readI++]; | ||
buffer[writeI++] = buffer[readI++]; | ||
} | ||
return buffer.slice(0, writeI); | ||
}; | ||
} | ||
exports.findZero = function(buffer, start, end, encoding) { | ||
exports.findZero = function (buffer, start, end, encoding) { | ||
var i = start; | ||
if (encoding === 'utf16') { | ||
while (buffer[i] !== 0 || buffer[i+1] !== 0) { | ||
if (i >= end) { | ||
return end; | ||
} | ||
if (i >= end) return end; | ||
i++; | ||
@@ -109,19 +87,15 @@ } | ||
while (buffer[i] !== 0) { | ||
if (i >= end) { | ||
return end; | ||
} | ||
if (i >= end) return end; | ||
i++; | ||
} | ||
} | ||
return i; | ||
}; | ||
} | ||
exports.isBitSetAt = function(b, offset, bit) { | ||
exports.isBitSetAt = function (b, offset, bit) { | ||
return (b[offset] & (1 << bit)) !== 0; | ||
}; | ||
} | ||
var decodeString = exports.decodeString = function(b, encoding, start, end) { | ||
var decodeString = exports.decodeString = function (b, encoding, start, end) { | ||
var text = ''; | ||
if (encoding == 'utf16') { | ||
@@ -133,16 +107,12 @@ text = readUTF16String(b.slice(start, end)); | ||
} | ||
return { | ||
text : text, | ||
length : end - start | ||
}; | ||
}; | ||
return { text : text, length : end - start } | ||
} | ||
exports.parseGenre = function(origVal) { | ||
//match everything inside parentheses | ||
exports.parseGenre = function (origVal) { | ||
// match everything inside parentheses | ||
var split = origVal.trim().split(/\((.*?)\)/g) | ||
.filter(function(val) { return val !== ''; }); | ||
.filter(function (val) { return val !== ''; }); | ||
var array = []; | ||
for (var i=0; i < split.length; i++) { | ||
for (var i = 0; i < split.length; i++) { | ||
var cur = split[i]; | ||
@@ -152,41 +122,48 @@ if (!isNaN(parseInt(cur))) cur = GENRES[cur]; | ||
} | ||
return array.join('/'); | ||
} | ||
var readUTF16String = function readUTF16String(bytes) { | ||
var ix = 0, | ||
offset1 = 1, | ||
offset2 = 0, | ||
maxBytes = bytes.length; | ||
function swapBytes (buffer) { | ||
var l = buffer.length; | ||
if (l & 0x01) { | ||
throw new Error('Buffer length must be even'); | ||
} | ||
for (var i = 0; i < l; i += 2) { | ||
var a = buffer[i]; | ||
buffer[i] = buffer[i+1]; | ||
buffer[i+1] = a; | ||
} | ||
return buffer; | ||
} | ||
var readUTF16String = exports.readUTF16String = function (bytes) { | ||
// bom detection (big endian) | ||
if (bytes[0] === 0xFE && bytes[1] === 0xFF) { | ||
ix = 2; | ||
offset1 = 0; | ||
offset2 = 1; | ||
} else if (bytes[0] === 0xFF && bytes[1] === 0xFE) { | ||
ix = 2; | ||
bytes = swapBytes(bytes); | ||
} | ||
return bytes.toString('utf16le'); | ||
} | ||
var str = ''; | ||
for (var j = 0; ix < maxBytes; j++) { | ||
var byte1 = bytes[ix + offset1], | ||
byte2 = bytes[ix + offset2], | ||
word1 = (byte1 << 8) + byte2; | ||
ix += 2; | ||
strtok.UINT24_BE = { | ||
len: 3, | ||
get: function(buf, off) { | ||
return (((buf[off] << 8) + buf[off + 1]) << 8) + buf[off + 2]; | ||
} | ||
} | ||
if (word1 === 0x0000) { | ||
break; | ||
} else if (byte1 < 0xD8 || byte1 >= 0xE0) { | ||
str += String.fromCharCode(word1); | ||
} else { | ||
var byte3 = bytes[ix+offset1], | ||
byte4 = bytes[ix+offset2], | ||
word2 = (byte3 << 8) + byte4; | ||
ix += 2; | ||
str += String.fromCharCode(word1, word2); | ||
} | ||
strtok.BITSET = { | ||
len: 1, | ||
get: function(buf, off, bit) { | ||
return (buf[off] & (1 << bit)) !== 0; | ||
} | ||
return str; | ||
}; | ||
} | ||
strtok.INT32SYNCSAFE = { | ||
len: 4, | ||
get: function(buf, off) { | ||
return buf[off + 3] & 0x7f | ((buf[off + 2]) << 7) | ((buf[off + 1]) << 14) | ((buf[off]) << 21); | ||
} | ||
} | ||
var PICTURE_TYPE = exports.PICTURE_TYPE = [ | ||
@@ -214,3 +191,3 @@ "Other", | ||
"Publisher/Studio logotype" | ||
]; | ||
] | ||
@@ -239,2 +216,2 @@ var GENRES = exports.GENRES = [ | ||
'Christian Rock','Merengue','Salsa','Thrash Metal','Anime','JPop','Synthpop' | ||
]; | ||
] |
103
lib/flac.js
@@ -1,28 +0,33 @@ | ||
/* jshint node:true, sub:true, globalstrict:true */ | ||
"use strict"; | ||
var util = require('util'); | ||
var events = require('events'); | ||
var strtok = require('strtok'); | ||
var common = require('./common'); | ||
var DataDecoder = function(data) { | ||
module.exports = function (stream, callback) { | ||
var currentState = startState; | ||
strtok.parse(stream, function (v, cb) { | ||
try { | ||
currentState = currentState.parse(callback, v); | ||
} catch (exception) { | ||
currentState = finishedState; | ||
callback('done', exception); | ||
} | ||
return currentState.getExpectedType(); | ||
}) | ||
} | ||
var DataDecoder = function (data) { | ||
this.data = data; | ||
this.offset = 0; | ||
}; | ||
} | ||
DataDecoder.prototype.readInt32 = function() { | ||
DataDecoder.prototype.readInt32 = function () { | ||
var value = strtok.UINT32_LE.get(this.data, this.offset); | ||
this.offset += 4; | ||
return value; | ||
}; | ||
} | ||
DataDecoder.prototype.readStringUtf8 = function() { | ||
DataDecoder.prototype.readStringUtf8 = function () { | ||
var len = this.readInt32(); | ||
var value = this.data.toString('utf8', this.offset, this.offset + len); | ||
this.offset += len; | ||
return value; | ||
@@ -32,17 +37,17 @@ }; | ||
var finishedState = { | ||
parse: function(context) { | ||
parse: function (callback) { | ||
return this; | ||
}, | ||
getExpectedType: function() { | ||
getExpectedType: function () { | ||
return strtok.DONE; | ||
} | ||
}; | ||
} | ||
var BlockDataState = function(type, length, nextStateFactory) { | ||
var BlockDataState = function (type, length, nextStateFactory) { | ||
this.type = type; | ||
this.length = length; | ||
this.nextStateFactory = nextStateFactory; | ||
}; | ||
} | ||
BlockDataState.prototype.parse = function(context, data) { | ||
BlockDataState.prototype.parse = function (callback, data) { | ||
if (this.type === 4) { | ||
@@ -59,18 +64,18 @@ var decoder = new DataDecoder(data); | ||
split = comment.split('='); | ||
context.emit(split[0].toUpperCase(), split[1]); | ||
callback(split[0].toUpperCase(), split[1]); | ||
} | ||
} else if (this.type === 6) { | ||
var picture = common.readVorbisPicture(data); | ||
context.emit('METADATA_BLOCK_PICTURE', picture); | ||
callback('METADATA_BLOCK_PICTURE', picture); | ||
} | ||
return this.nextStateFactory(); | ||
}; | ||
} | ||
BlockDataState.prototype.getExpectedType = function() { | ||
BlockDataState.prototype.getExpectedType = function () { | ||
return new strtok.BufferType(this.length); | ||
}; | ||
} | ||
var blockHeaderState = { | ||
parse: function(context, data) { | ||
parse: function (callback, data) { | ||
var header = { | ||
@@ -80,26 +85,25 @@ lastBlock: (data[0] & 0x80) == 0x80, | ||
length: strtok.UINT24_BE.get(data, 1) | ||
}; | ||
} | ||
var followingStateFactory = header.lastBlock ? function() { | ||
context.emit('done'); | ||
callback('done'); | ||
return finishedState; | ||
} : function() { | ||
return blockHeaderState; | ||
}; | ||
} | ||
return new BlockDataState(header.type, header.length, followingStateFactory); | ||
}, | ||
getExpectedType: function() { | ||
getExpectedType: function () { | ||
return new strtok.BufferType(4); | ||
} | ||
}; | ||
} | ||
var idState = { | ||
parse: function(context, data) { | ||
parse: function (callback, data) { | ||
if (data !== 'fLaC') { | ||
throw new Error('expected flac header but was not found'); | ||
} | ||
return blockHeaderState; | ||
}, | ||
getExpectedType: function() { | ||
getExpectedType: function () { | ||
return new strtok.StringType(4); | ||
@@ -110,33 +114,8 @@ } | ||
var startState = { | ||
parse: function(context) { | ||
parse: function (callback) { | ||
return idState; | ||
}, | ||
getExpectedType: function() { | ||
getExpectedType: function () { | ||
return strtok.DONE; | ||
} | ||
}; | ||
var Flac = module.exports = function(stream) { | ||
events.EventEmitter.call(this); | ||
this.stream = stream; | ||
this.currentState = startState; | ||
this.parse(); | ||
}; | ||
util.inherits(Flac, events.EventEmitter); | ||
Flac.prototype.parse = function() { | ||
var self = this; | ||
strtok.parse(self.stream, function(v, cb) { | ||
try { | ||
self.currentState = self.currentState.parse(self, v); | ||
} catch (exception) { | ||
self.currentState = finishedState; | ||
self.emit('done', exception); | ||
} | ||
return self.currentState.getExpectedType(); | ||
}); | ||
}; | ||
} |
var util = require('util'); | ||
var events = require('events'); | ||
var common = require('./common'); | ||
var Id3v1 = module.exports = function(stream) { | ||
events.EventEmitter.call(this); | ||
this.stream = stream; | ||
this.parse(); | ||
}; | ||
util.inherits(Id3v1, events.EventEmitter); | ||
Id3v1.prototype.parse = function() { | ||
var self = this, | ||
endData = null; | ||
self.stream.on('data', function(data) { | ||
module.exports = function (stream, callback) { | ||
var endData = null; | ||
stream.on('data', function (data) { | ||
endData = data; | ||
}); | ||
self.stream.onRealEnd(function() { | ||
parse(endData); | ||
}); | ||
function parse(data) { | ||
stream.onRealEnd(function () { | ||
try { | ||
var offset = data.length - 128; | ||
var header = data.toString('ascii', offset, offset += 3); | ||
var offset = endData.length - 128; | ||
var header = endData.toString('ascii', offset, offset += 3); | ||
if (header !== 'TAG') { | ||
@@ -35,31 +17,30 @@ throw new Error('Expected id3v1.1 header but was not found.'); | ||
var title = data.toString('ascii', offset, offset += 30); | ||
self.emit('title', title.trim().replace(/\x00/g, '')); | ||
var title = endData.toString('ascii', offset, offset += 30); | ||
callback('title', title.trim().replace(/\x00/g, '')); | ||
var artist = data.toString('ascii', offset, offset += 30); | ||
self.emit('artist', artist.trim().replace(/\x00/g, '')); | ||
var artist = endData.toString('ascii', offset, offset += 30); | ||
callback('artist', artist.trim().replace(/\x00/g, '')); | ||
var album = data.toString('ascii', offset, offset += 30); | ||
self.emit('album', album.trim().replace(/\x00/g, '')); | ||
var album = endData.toString('ascii', offset, offset += 30); | ||
callback('album', album.trim().replace(/\x00/g, '')); | ||
var year = data.toString('ascii', offset, offset += 4); | ||
self.emit('year', year.trim().replace(/\x00/g, '')); | ||
var year = endData.toString('ascii', offset, offset += 4); | ||
callback('year', year.trim().replace(/\x00/g, '')); | ||
var comment = data.toString('ascii', offset, offset += 28); | ||
self.emit('comment', comment.trim().replace(/\x00/g, '')); | ||
var comment = endData.toString('ascii', offset, offset += 28); | ||
callback('comment', comment.trim().replace(/\x00/g, '')); | ||
var track = data[data.length - 2]; | ||
self.emit('track', track); | ||
var track = endData[endData.length - 2]; | ||
callback('track', track); | ||
if (data[data.length - 1] in common.GENRES) { | ||
var genre = common.GENRES[data[data.length - 1]]; | ||
self.emit('genre', genre); | ||
if (endData[endData.length - 1] in common.GENRES) { | ||
var genre = common.GENRES[endData[endData.length - 1]]; | ||
callback('genre', genre); | ||
} | ||
self.emit('done'); | ||
callback('done'); | ||
} catch (exception) { | ||
self.emit('done', exception); | ||
callback('done', exception); | ||
} | ||
} | ||
}); | ||
} |
202
lib/id3v2.js
@@ -1,128 +0,114 @@ | ||
var util = require('util'); | ||
var events = require('events'); | ||
var strtok = require('strtok'); | ||
var fs = require('fs'); | ||
var parser = require('./id3v2_frames'); | ||
var common = require('./common'); | ||
var Id3v2 = module.exports = function(stream) { | ||
events.EventEmitter.call(this); | ||
this.stream = stream; | ||
this.parse(); | ||
}; | ||
util.inherits(Id3v2, events.EventEmitter); | ||
Id3v2.prototype.parse = function() { | ||
var self = this; | ||
strtok.parse(self.stream, function(v, cb) { | ||
module.exports = function (stream, callback) { | ||
strtok.parse(stream, function (v, cb) { | ||
try { | ||
if (!v) { | ||
cb.position = 'header'; | ||
cb.state = 0; | ||
return new strtok.BufferType(10); | ||
} | ||
if (cb.position === 'header') { | ||
if (v.toString('ascii', 0, 3) !== 'ID3') { | ||
throw new Error('expected id3 header but was not found'); | ||
} | ||
switch (cb.state) { | ||
case -1: // skip | ||
cb.state = 2; | ||
return readFrameHeader(cb.header.major); | ||
case 0: // header | ||
if (v.toString('ascii', 0, 3) !== 'ID3') { | ||
throw new Error('expected id3 header but was not found'); | ||
} | ||
cb.header = { | ||
version: '2.' + v[3] + '.' + v[4], | ||
major: v[3], | ||
unsync: strtok.BITSET.get(v, 5, 7), | ||
xheader: strtok.BITSET.get(v, 5, 6), | ||
xindicator: strtok.BITSET.get(v, 5, 5), | ||
footer: strtok.BITSET.get(v, 5, 4), | ||
size: strtok.INT32SYNCSAFE.get(v, 6) | ||
}; | ||
cb.header = { | ||
version: '2.' + v[3] + '.' + v[4], | ||
major: v[3], | ||
unsync: strtok.BITSET.get(v, 5, 7), | ||
xheader: strtok.BITSET.get(v, 5, 6), | ||
xindicator: strtok.BITSET.get(v, 5, 5), | ||
footer: strtok.BITSET.get(v, 5, 4), | ||
size: strtok.INT32SYNCSAFE.get(v, 6) | ||
}; | ||
if (cb.header.xheader) { | ||
cb.position = 'xheader'; | ||
return strtok.UINT32_BE; | ||
} | ||
if (cb.header.xheader) { | ||
cb.state = 1; | ||
return strtok.UINT32_BE; | ||
} | ||
//expect the first frames header next | ||
cb.position = 'frameheader'; | ||
switch (cb.header.major) { | ||
case 2: | ||
return new strtok.BufferType(6); | ||
case 3: | ||
case 4: | ||
return new strtok.BufferType(10); | ||
default: | ||
throw new Error('header version is incorrect'); | ||
} | ||
} | ||
// expect the first frames header next | ||
cb.state = 2; | ||
return readFrameHeader(cb.header.major); | ||
if (cb.position === 'xheader') { | ||
cb.position = 'frameheader'; | ||
//TODO: this will not work because we do not detect raw objects | ||
//our code will fail on mp3's with xheaders | ||
return new strtok.BufferType(v); //skip xheader | ||
} | ||
case 1: // xheader | ||
cb.state = -1; | ||
return new strtok.BufferType(v - 4); | ||
if (cb.position === 'frameheader') { | ||
cb.position = 'framedata'; | ||
var header = cb.frameHeader = {}; | ||
case 2: // frameheader | ||
var header = cb.frameHeader = {}; | ||
switch (cb.header.major) { | ||
case 2: | ||
header.id = v.toString('ascii', 0, 3); | ||
header.length = strtok.UINT24_BE.get(v, 3, 6); | ||
break; | ||
case 3: | ||
header.id = v.toString('ascii', 0, 4); | ||
header.length = strtok.UINT32_BE.get(v, 4, 8); | ||
header.flags = readFrameFlags(v.slice(8, 10)); | ||
break; | ||
case 4: | ||
header.id = v.toString('ascii', 0, 4); | ||
header.length = strtok.INT32SYNCSAFE.get(v, 4, 8); | ||
header.flags = readFrameFlags(v.slice(8, 10)); | ||
break; | ||
} | ||
switch (cb.header.major) { | ||
case 2: | ||
header.id = v.toString('ascii', 0, 3); | ||
header.length = strtok.UINT24_BE.get(v, 3, 6); | ||
break; | ||
case 3: | ||
header.id = v.toString('ascii', 0, 4); | ||
header.length = strtok.UINT32_BE.get(v, 4, 8); | ||
header.flags = readFrameFlags(v.slice(8, 10)); | ||
break; | ||
case 4: | ||
header.id = v.toString('ascii', 0, 4); | ||
header.length = strtok.INT32SYNCSAFE.get(v, 4, 8); | ||
header.flags = readFrameFlags(v.slice(8, 10)); | ||
break; | ||
} | ||
// Last frame. Check first char is a letter, bit of defensive programming | ||
if (header.id === '' || header.id === '\u0000\u0000\u0000\u0000' | ||
|| 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.search(header.id[0]) === -1) { | ||
callback('done'); | ||
return strtok.DONE; | ||
} | ||
cb.state++; | ||
return new strtok.BufferType(header.length); | ||
// Last frame. Check first char is a letter, bit of defensive programming | ||
if (header.id === '' || header.id === '\u0000\u0000\u0000\u0000' || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.search(header.id[0]) === -1) { | ||
self.emit('done'); | ||
return strtok.DONE; | ||
} | ||
return new strtok.BufferType(header.length); | ||
case 3: // framedata | ||
cb.state = 2; // frameheader up next | ||
var frame, encoding; | ||
switch (cb.header.major) { | ||
case 2: | ||
frame = parser.readData(v, cb.frameHeader.id, null, cb.header.major); | ||
callback(cb.frameHeader.id, frame); | ||
return new strtok.BufferType(6); | ||
case 3: | ||
case 4: | ||
if (cb.frameHeader.flags.format.unsync) { | ||
v = common.removeUnsyncBytes(v); | ||
} | ||
if (cb.frameHeader.flags.format.data_length_indicator) { | ||
v = v.slice(4, v.length); | ||
} | ||
frame = parser.readData(v, cb.frameHeader.id, cb.frameHeader.flags, cb.header.major); | ||
callback(cb.frameHeader.id, frame); | ||
return new strtok.BufferType(10); | ||
} | ||
} | ||
if (cb.position === 'framedata') { | ||
cb.position = 'frameheader'; | ||
var frame, encoding; | ||
switch (cb.header.major) { | ||
case 2: | ||
frame = parser.readData(v, cb.frameHeader.id, null, cb.header.major); | ||
self.emit(cb.frameHeader.id, frame); | ||
return new strtok.BufferType(6); | ||
case 3: | ||
case 4: | ||
if (cb.frameHeader.flags.format.unsync) { | ||
v = common.removeUnsyncBytes(v); | ||
} | ||
if (cb.frameHeader.flags.format.data_length_indicator) { | ||
v = v.slice(4, v.length); //TODO: do we need to do something with this? | ||
} | ||
frame = parser.readData(v, cb.frameHeader.id, cb.frameHeader.flags, cb.header.major); | ||
self.emit(cb.frameHeader.id, frame); | ||
return new strtok.BufferType(10); | ||
} | ||
} | ||
} catch (exception) { | ||
self.emit('done', exception); | ||
callback('done', exception); | ||
return strtok.DONE; | ||
} | ||
}); | ||
}; | ||
}) | ||
} | ||
function readFrameFlags(b) { | ||
function readFrameHeader (majorVer) { | ||
switch (majorVer) { | ||
case 2: | ||
return new strtok.BufferType(6); | ||
case 3: | ||
case 4: | ||
return new strtok.BufferType(10); | ||
default: | ||
throw new Error('header version is incorrect'); | ||
} | ||
} | ||
function readFrameFlags (b) { | ||
return { | ||
@@ -141,3 +127,3 @@ status: { | ||
} | ||
}; | ||
}; | ||
} | ||
} |
127
lib/id4.js
@@ -1,95 +0,78 @@ | ||
var util = require('util'); | ||
var events = require('events'); | ||
var strtok = require('strtok'); | ||
var common = require('./common'); | ||
var fs = require('fs'); | ||
var Id4 = module.exports = function(stream) { | ||
events.EventEmitter.call(this); | ||
this.stream = stream; | ||
this.parse(); | ||
}; | ||
module.exports = function (stream, callback) { | ||
strtok.parse(stream, function (v, cb) { | ||
try { | ||
// we can stop processing atoms once we get to the end of the ilst atom | ||
if (cb.metaAtomsTotalLength >= cb.atomContainerLength - 8) { | ||
callback('done'); | ||
return strtok.DONE; | ||
} | ||
util.inherits(Id4, events.EventEmitter); | ||
Id4.prototype.parse = function() { | ||
var self = this; | ||
strtok.parse(self.stream, function(v, cb) { | ||
try { | ||
//the very first thing we expect to see is the first atom's length | ||
// the very first thing we expect to see is the first atom's length | ||
if (!v) { | ||
cb.metaAtomsTotalLength = 0; | ||
cb.position = 'atomlength'; | ||
cb.state = 0; | ||
return strtok.UINT32_BE; | ||
} | ||
if (cb.position === 'skip') { | ||
cb.position = 'atomlength'; | ||
return strtok.UINT32_BE; | ||
} | ||
switch (cb.state) { | ||
case -1: // skip | ||
cb.state = 0; | ||
return strtok.UINT32_BE; | ||
if (cb.position === 'atomlength') { | ||
cb.position = 'atomname'; | ||
cb.atomLength = v; | ||
return new strtok.StringType(4, 'binary'); | ||
} | ||
case 0: // atom length | ||
cb.atomLength = v; | ||
cb.state++; | ||
return new strtok.StringType(4, 'binary'); | ||
if (cb.position === 'atomname') { | ||
cb.atomName = v; | ||
case 1: // atom name | ||
cb.atomName = v; | ||
//meta has 4 bytes padding at the start (skip) | ||
if (v === 'meta') { | ||
cb.position = 'skip'; | ||
return new strtok.BufferType(4); | ||
} | ||
// meta has 4 bytes padding at the start (skip) | ||
if (v === 'meta') { | ||
cb.state = -1; // what to do for skip? | ||
return new strtok.BufferType(4); | ||
} | ||
if (!~CONTAINER_ATOMS.indexOf(v)) { | ||
cb.position = (cb.atomContainer === 'ilst') ? 'ilstatom' : 'skip'; | ||
return new strtok.BufferType(cb.atomLength - 8); | ||
} | ||
if (!~CONTAINER_ATOMS.indexOf(v)) { | ||
// whats the num for ilst? | ||
cb.state = (cb.atomContainer === 'ilst') ? 2 : -1; | ||
return new strtok.BufferType(cb.atomLength - 8); | ||
} | ||
//dig into container atoms | ||
cb.atomContainer = v; | ||
cb.atomContainerLength = cb.atomLength; | ||
cb.position = 'atomlength'; | ||
return strtok.UINT32_BE; | ||
} | ||
// dig into container atoms | ||
cb.atomContainer = v; | ||
cb.atomContainerLength = cb.atomLength; | ||
cb.state--; | ||
return strtok.UINT32_BE; | ||
//we can stop processing atoms once we get to the end of the ilst atom | ||
if (cb.metaAtomsTotalLength >= cb.atomContainerLength - 8) { | ||
self.emit('done'); | ||
return strtok.DONE; | ||
} | ||
//only process atoms that fall under the ilst atom (metadata) | ||
if (cb.position === 'ilstatom') { | ||
cb.metaAtomsTotalLength += cb.atomLength; | ||
var result = processMetaAtom(v, cb.atomName, cb.atomLength - 8); | ||
if (result.length > 0) { | ||
for (var i = 0; i < result.length; i++) { | ||
self.emit(cb.atomName, result[i]); | ||
case 2: // ilst atom | ||
cb.metaAtomsTotalLength += cb.atomLength; | ||
var result = processMetaAtom(v, cb.atomName, cb.atomLength - 8); | ||
if (result.length > 0) { | ||
for (var i = 0; i < result.length; i++) { | ||
callback(cb.atomName, result[i]); | ||
} | ||
} | ||
} | ||
cb.position = 'atomlength'; | ||
return strtok.UINT32_BE; | ||
cb.state = 0; | ||
return strtok.UINT32_BE; | ||
} | ||
//if we ever get this this point something bad has happened | ||
// if we ever get this this point something bad has happened | ||
throw new Error('error parsing'); | ||
} catch (exception) { | ||
self.emit('done', exception); | ||
callback('done', exception); | ||
return strtok.DONE; | ||
} | ||
}); | ||
}; | ||
}) | ||
} | ||
function processMetaAtom(data, atomName, atomLength) { | ||
function processMetaAtom (data, atomName, atomLength) { | ||
var result = []; | ||
var offset = 0; | ||
//ignore proprietary iTunes atoms (for now) | ||
// ignore proprietary iTunes atoms (for now) | ||
if (atomName == '----') return result; | ||
@@ -101,3 +84,3 @@ | ||
var content = (function processMetaDataAtom(data, type, atomName) { | ||
var content = (function processMetaDataAtom (data, type, atomName) { | ||
switch (type) { | ||
@@ -140,8 +123,4 @@ case 'text': | ||
'21': 'uint8' | ||
}; | ||
} | ||
var CONTAINER_ATOMS = [ | ||
'moov', | ||
'udta', | ||
'meta', | ||
'ilst']; | ||
var CONTAINER_ATOMS = ['moov', 'udta', 'meta', 'ilst']; |
182
lib/index.js
@@ -1,1 +0,181 @@ | ||
module.exports = require('./musicmetadata.js'); | ||
var util = require('util'); | ||
var events = require('events'); | ||
var common = require('./common'); | ||
var strtok = require('strtok'); | ||
var MusicMetadata = module.exports = function (stream) { | ||
events.EventEmitter.call(this); | ||
this.stream = stream; | ||
this.parse(); | ||
}; | ||
util.inherits(MusicMetadata, events.EventEmitter); | ||
MusicMetadata.prototype.parse = function () { | ||
this.metadata = { | ||
title: '', | ||
artist: [], | ||
albumartist: [], | ||
album: '', | ||
year: 0, | ||
track: { no: 0, of: 0 }, | ||
genre: [], | ||
disk: { no: 0, of: 0 }, | ||
picture: {} | ||
} | ||
this.aliased = {}; | ||
var self = this; | ||
this.stream.once('data', function (result) { | ||
var tag = common.detectMediaType(result.toString('binary')); | ||
require('./' + tag)(self.stream, self.readEvents.bind(self)); | ||
// re-emitting the first data chunk so the | ||
// parser picks the stream up from the start | ||
self.stream.emit('data', result); | ||
}); | ||
}; | ||
MusicMetadata.prototype.readEvents = function (event, value) { | ||
// We only emit aliased events once the 'done' event has been raised, | ||
// this is because an alias like 'artist' could have values split | ||
// over many data chunks. | ||
if (event === 'done') { | ||
for (var alias in this.aliased) { | ||
if (this.aliased.hasOwnProperty(alias)) { | ||
var val; | ||
if (alias === 'title' || alias === 'album' || alias === 'year') { | ||
val = this.aliased[alias][0]; | ||
} else { | ||
val = this.aliased[alias]; | ||
} | ||
this.emit(alias, val); | ||
if (this.metadata.hasOwnProperty(alias)) { | ||
this.metadata[alias] = val; | ||
} | ||
} | ||
} | ||
this.emit('metadata', this.metadata); | ||
this.emit('done', value); | ||
return; | ||
} | ||
// lookup alias | ||
var alias; | ||
for (var i = 0; i < MAPPINGS.length; i++) { | ||
for (var j = 0; j < MAPPINGS[i].length; j++) { | ||
var cur = MAPPINGS[i][j]; | ||
if (cur.toUpperCase() === event.toUpperCase()) { | ||
alias = MAPPINGS[i][0]; | ||
break; | ||
} | ||
} | ||
} | ||
// emit original event & value | ||
if (event !== alias) { | ||
this.emit(event, value); | ||
} | ||
// we need to do something special for these events | ||
// TODO: parseInt will return NaN for strings | ||
if (event === 'TRACKTOTAL' || event === 'DISCTOTAL') { | ||
var evt; | ||
if (event === 'TRACKTOTAL') evt = 'track'; | ||
if (event === 'DISCTOTAL') evt = 'disk'; | ||
var cleaned = parseInt(value) | ||
if (!this.aliased.hasOwnProperty(evt)) { | ||
this.aliased[evt] = { no: 0, of: cleaned }; | ||
} else { | ||
this.aliased[evt]['of'] = cleaned; | ||
} | ||
} | ||
// if the event has been aliased then we need to clean it before | ||
// it is emitted to the user. e.g. genre (20) -> Electronic | ||
if (alias) { | ||
var cleaned = value; | ||
if (alias === 'genre') cleaned = common.parseGenre(value); | ||
if (alias === 'picture') cleaned = cleanupPicture(value); | ||
if (alias === 'track' || alias === 'disk') { | ||
cleaned = cleanupTrack(value); | ||
if (this.aliased[alias]) { | ||
this.aliased[alias].no = cleaned.no; | ||
return; | ||
} else { | ||
this.aliased[alias] = cleaned; | ||
return; | ||
} | ||
} | ||
// many tagging libraries use forward slashes to separate artists etc | ||
// within a string, this code separates those strings into an array | ||
if (cleaned.constructor === String) { | ||
// limit to these three aliases, we don't want to be splitting anything else | ||
if (alias === 'artist' || alias === 'albumartist' || alias === 'genre') { | ||
cleaned = cleaned.split('/'); | ||
if (cleaned.length === 1) cleaned = cleaned[0]; | ||
} | ||
} | ||
// if we haven't previously seen this tag then | ||
// initialize it to an array, ready for values to be entered | ||
if (!this.aliased.hasOwnProperty(alias)) { | ||
this.aliased[alias] = []; | ||
} | ||
if (cleaned.constructor === Array) { | ||
this.aliased[alias] = cleaned; | ||
} else { | ||
this.aliased[alias].push(cleaned); | ||
} | ||
} | ||
} | ||
function cleanupArtist (origVal) { | ||
return origVal.split('/'); | ||
} | ||
// TODO: a string of 1of1 would fail to be converted | ||
// converts 1/10 to no : 1, of : 10 | ||
// or 1 to no : 1, of : 0 | ||
function cleanupTrack (origVal) { | ||
var split = origVal.toString().split('/'); | ||
var number = parseInt(split[0], 10) || 0; | ||
var total = parseInt(split[1], 10) || 0; | ||
return { no: number, of: total } | ||
} | ||
function cleanupPicture (picture) { | ||
var newFormat; | ||
if (picture.format) { | ||
var split = picture.format.toLowerCase().split('/'); | ||
newFormat = (split.length > 1) ? split[1] : split[0]; | ||
if (newFormat === 'jpeg') newFormat = 'jpg'; | ||
} else { | ||
newFormat = 'jpg'; | ||
} | ||
return { format: newFormat, data: picture.data } | ||
} | ||
// mappings for common metadata types(id3v2.3, id3v2.2, id4, vorbis, APEv2) | ||
var MAPPINGS = [ | ||
['title', 'TIT2', 'TT2', '©nam', 'TITLE'], | ||
['artist', 'TPE1', 'TP1', '©ART', 'ARTIST'], | ||
['albumartist', 'TPE2', 'TP2', 'aART', 'ALBUMARTIST', 'ENSEMBLE'], | ||
['album', 'TALB', 'TAL', '©alb', 'ALBUM'], | ||
['year', 'TDRC', 'TYER', 'TYE', '©day', 'DATE', 'Year'], | ||
['comment', 'COMM', 'COM', '©cmt', 'COMMENT'], | ||
['track', 'TRCK', 'TRK', 'trkn', 'TRACKNUMBER', 'Track'], | ||
['disk', 'TPOS', 'TPA', 'disk', 'DISCNUMBER', 'Disk'], | ||
['genre', 'TCON', 'TCO', '©gen', 'gnre', 'GENRE'], | ||
['picture', 'APIC', 'PIC', 'covr', 'METADATA_BLOCK_PICTURE', | ||
'Cover Art (Front)', 'Cover Art (Back)'], | ||
['composer', 'TCOM', 'TCM', '©wrt', 'COMPOSER'] | ||
]; |
@@ -1,31 +0,15 @@ | ||
var util = require('util'); | ||
var events = require('events'); | ||
var common = require('./common'); | ||
var strtok = require('strtok'); | ||
var MonkeysAudio = module.exports = function(stream) { | ||
events.EventEmitter.call(this); | ||
this.stream = stream; | ||
this.parse(); | ||
}; | ||
module.exports = function (stream, callback) { | ||
var bufs = []; | ||
util.inherits(MonkeysAudio, events.EventEmitter); | ||
MonkeysAudio.prototype.parse = function() { | ||
var self = this, | ||
bufs = [], | ||
dataLen = 0; | ||
//TODO: need to be able to parse the tag if its at the start of the file | ||
this.stream.on('data', function(data) { | ||
// TODO: need to be able to parse the tag if its at the start of the file | ||
stream.on('data', function (data) { | ||
bufs.push(data); | ||
dataLen += data.length; | ||
}); | ||
}) | ||
this.stream.onRealEnd(parse); | ||
function parse() { | ||
stream.onRealEnd(function () { | ||
try { | ||
var buffer = common.joinBuffers(bufs, dataLen); | ||
var buffer = Buffer.concat(bufs); | ||
var offset = buffer.length - 32; | ||
@@ -77,13 +61,13 @@ | ||
} | ||
self.emit(key, value); | ||
callback(key, value); | ||
} | ||
self.emit('done'); | ||
callback('done'); | ||
return strtok.DONE; | ||
} catch (exception) { | ||
self.emit('done', exception); | ||
callback('done', exception); | ||
return strtok.DONE; | ||
} | ||
} | ||
}) | ||
} |
178
lib/ogg.js
@@ -7,122 +7,108 @@ var fs = require('fs'); | ||
var Ogg = module.exports = function (stream) { | ||
events.EventEmitter.call(this); | ||
this.stream = stream; | ||
this.parse(); | ||
}; | ||
util.inherits(Ogg, events.EventEmitter); | ||
Ogg.prototype.parse = function () { | ||
var self = this; | ||
module.exports = function (stream, callback) { | ||
var innerStream = new events.EventEmitter(); | ||
try { | ||
// top level parser that handles the parsing of pages | ||
strtok.parse(self.stream, function (v, cb) { | ||
// top level parser that handles the parsing of pages | ||
strtok.parse(stream, function (v, cb) { | ||
try { | ||
if (!v) { | ||
cb.commentsRead = 0; | ||
cb.position = 'header'; //read first OggS header | ||
cb.state = 0; | ||
return new strtok.BufferType(27); | ||
} | ||
if (cb.position === 'header') { | ||
cb.header = { | ||
type: v.toString('utf-8', 0, 4), | ||
version: v[4], | ||
packet_flag: v[5], | ||
pcm_sample_pos: 'not_implemented', | ||
stream_serial_num: strtok.UINT32_LE.get(v, 14), | ||
page_number: strtok.UINT32_LE.get(v, 18), | ||
check_sum: strtok.UINT32_LE.get(v, 22), | ||
segments: v[26] | ||
}; | ||
switch (cb.state) { | ||
case 0: // header | ||
cb.header = { | ||
type: v.toString(0, 4), | ||
version: v[4], | ||
packet_flag: v[5], | ||
pcm_sample_pos: 'not_implemented', | ||
stream_serial_num: strtok.UINT32_LE.get(v, 14), | ||
page_number: strtok.UINT32_LE.get(v, 18), | ||
check_sum: strtok.UINT32_LE.get(v, 22), | ||
segments: v[26] | ||
} | ||
cb.state++; | ||
return new strtok.BufferType(cb.header.segments); | ||
//read segment table | ||
cb.position = 'segments'; | ||
return new strtok.BufferType(cb.header.segments); | ||
} | ||
case 1: // segments | ||
var pageLen = 0; | ||
for (var i = 0; i < v.length; i++) { | ||
pageLen += v[i]; | ||
} | ||
cb.state++; | ||
return new strtok.BufferType(pageLen); | ||
if (cb.position === 'segments') { | ||
var pageLen = 0; | ||
for (var i = 0; i < v.length; i++) { | ||
pageLen += v[i]; | ||
} | ||
cb.position = 'page_data'; | ||
return new strtok.BufferType(pageLen); | ||
case 2: // page data | ||
if (cb.header.page_number >= 1) { | ||
innerStream.emit('data', new Buffer(v)); | ||
} | ||
cb.state = 0; | ||
return new strtok.BufferType(27); | ||
} | ||
if (cb.position === 'page_data') { | ||
if (cb.header.page_number >= 1) { | ||
innerStream.emit('data', new Buffer(v)); | ||
} | ||
cb.position = 'header'; | ||
return new strtok.BufferType(27); | ||
} | ||
}) | ||
} catch (exception) { | ||
callback('done', exception); | ||
return strtok.DONE; | ||
} | ||
}) | ||
// Second level parser that handles the parsing of metadata. | ||
// The top level parser emits data that this parser should | ||
// handle. | ||
strtok.parse(innerStream, function (v, cb) { | ||
// Second level parser that handles the parsing of metadata. | ||
// The top level parser emits data that this parser should | ||
// handle. | ||
strtok.parse(innerStream, function (v, cb) { | ||
try { | ||
if (!v) { | ||
cb.position = 'type'; //read first OggS header | ||
cb.commentsRead = 0; | ||
cb.state = 0; | ||
return new strtok.BufferType(7); | ||
} | ||
if (cb.position === 'type') { | ||
cb.position = 'vendor_length'; | ||
return strtok.UINT32_LE; | ||
} | ||
switch (cb.state) { | ||
case 0: // type | ||
cb.state++; | ||
return strtok.UINT32_LE; | ||
if (cb.position === 'vendor_length') { | ||
cb.position = 'vendor_string'; | ||
return new strtok.StringType(v); | ||
} | ||
case 1: // vendor length | ||
cb.state++; | ||
return new strtok.StringType(v); | ||
if (cb.position === 'vendor_string') { | ||
cb.position = 'user_comment_list_length'; | ||
return strtok.UINT32_LE; | ||
} | ||
case 2: // vendor string | ||
cb.state++; | ||
return strtok.UINT32_LE; | ||
if (cb.position === 'user_comment_list_length') { | ||
cb.commentsLength = v; | ||
cb.position = 'comment_length'; | ||
return strtok.UINT32_LE; | ||
} | ||
case 3: // user comment list length | ||
cb.commentsLength = v; | ||
cb.state++; | ||
return strtok.UINT32_LE; | ||
if (cb.position === 'comment_length') { | ||
cb.position = 'comment'; | ||
return new strtok.StringType(v); | ||
} | ||
case 4: // comment length | ||
cb.state++; | ||
return new strtok.StringType(v); | ||
if (cb.position === 'comment') { | ||
cb.commentsRead = cb.commentsRead || 0; | ||
cb.commentsRead++; | ||
case 5: // comment | ||
cb.commentsRead++; | ||
var idx = v.indexOf('='); | ||
var key = v.slice(0, idx).toUpperCase(); | ||
var value = v.slice(idx+1); | ||
var idx = v.indexOf('='); | ||
var key = v.slice(0, idx).toUpperCase(); | ||
var value = v.slice(idx+1); | ||
if (key === 'METADATA_BLOCK_PICTURE') { | ||
value = common.readVorbisPicture(new Buffer(value, 'base64')); | ||
} | ||
self.emit(key, value); | ||
if (key === 'METADATA_BLOCK_PICTURE') { | ||
value = common.readVorbisPicture(new Buffer(value, 'base64')); | ||
} | ||
if (cb.commentsRead === cb.commentsLength) { | ||
self.emit('done'); | ||
return strtok.DONE; | ||
} | ||
callback(key, value); | ||
cb.position = 'comment_length'; | ||
return strtok.UINT32_LE; | ||
if (cb.commentsRead === cb.commentsLength) { | ||
callback('done'); | ||
return strtok.DONE; | ||
} | ||
cb.state--; // back to comment length | ||
return strtok.UINT32_LE; | ||
} | ||
}) | ||
} catch (exception) { | ||
self.emit('done', exception); | ||
return strtok.DONE; | ||
} | ||
} catch (exception) { | ||
callback('done', exception); | ||
return strtok.DONE; | ||
} | ||
}) | ||
} |
{ | ||
"name": "musicmetadata", | ||
"description": "Music metadata library for node, using pure Javascript.", | ||
"version": "0.2.2", | ||
"version": "0.2.3", | ||
"author": "Lee Treveil", | ||
@@ -6,0 +6,0 @@ "dependencies": { |
Sorry, the diff of this file is not supported yet
243513
16
945