music-metadata
Advanced tools
Comparing version 0.1.0 to 0.1.1
@@ -1,2 +0,1 @@ | ||
/* jshint maxlen: 120 */ | ||
'use strict' | ||
@@ -3,0 +2,0 @@ var strtok = require('strtok2') |
'use strict' | ||
var common = require('./common') | ||
var strtok = require('strtok2') | ||
var type = 'APEv2' | ||
var type = 'APEv2' // ToDo: version should be made dynamic, APE may also contain ID3 | ||
var ape = {} | ||
/** | ||
* APETag version history / supported formats | ||
* | ||
* 1.0 (1000) - Original APE tag spec. Fully supported by this code. | ||
* 2.0 (2000) - Refined APE tag spec (better streaming support, UTF encoding). Fully supported by this code. | ||
* | ||
* Notes: | ||
* - also supports reading of ID3v1.1 tags | ||
* - all saving done in the APE Tag format using CURRENT_APE_TAG_VERSION | ||
* | ||
* APE File Format Overview: (pieces in order -- only valid for the latest version APE files) | ||
* | ||
* JUNK - any amount of "junk" before the APE_DESCRIPTOR (so people that put ID3v2 tags on the files aren't hosed) | ||
* APE_DESCRIPTOR - defines the sizes (and offsets) of all the pieces, as well as the MD5 checksum | ||
* APE_HEADER - describes all of the necessary information about the APE file | ||
* SEEK TABLE - the table that represents seek offsets [optional] | ||
* HEADER DATA - the pre-audio data from the original file [optional] | ||
* APE FRAMES - the actual compressed audio (broken into frames for seekability) | ||
* TERMINATING DATA - the post-audio data from the original file [optional] | ||
* TAG - describes all the properties of the file [optional] | ||
*/ | ||
module.exports = function (stream, callback, done) { | ||
var ApeDescriptor = { | ||
len: 44, | ||
strtok.parse(stream, function (v, cb) { | ||
if (v === undefined) { | ||
cb.state = 'descriptor' | ||
return Ape.descriptor | ||
} | ||
switch (cb.state) { | ||
case 'descriptor': | ||
if (v.ID !== 'MAC ') { | ||
throw new Error('Expected MAC on beginning of file') // ToDo: strip/parse JUNK | ||
} | ||
ape.descriptor = v | ||
var lenExp = v.descriptorBytes - ape.descriptor.len | ||
if (lenExp > 0) { | ||
cb.state = 'descriptorExpansion' | ||
return new strtok.IgnoreType(lenExp) | ||
} else { | ||
cb.state = 'header' | ||
return Ape.header | ||
} | ||
cb.state = 'descriptorExpansion' | ||
return new strtok.IgnoreType(lenExp) | ||
case 'descriptorExpansion': | ||
cb.state = 'header' | ||
return Ape.header | ||
case 'header': | ||
ape.header = v | ||
callback('format', 'tagType', type) | ||
callback('format', 'bitsPerSample', v.bitsPerSample) | ||
callback('format', 'sampleRate', v.sampleRate) | ||
callback('format', 'numberOfChannels', v.channel) | ||
callback('format', 'duration', calculateDuration(v)) | ||
var forwardBytes = ape.descriptor.seekTableBytes + ape.descriptor.headerDataBytes + | ||
ape.descriptor.apeFrameDataBytes + ape.descriptor.terminatingDataBytes | ||
cb.state = 'skipData' | ||
return new strtok.IgnoreType(forwardBytes) | ||
case 'skipData': | ||
cb.state = 'tagFooter' | ||
return Ape.tagFooter | ||
case 'tagFooter': | ||
if (v.ID !== 'APETAGEX') { | ||
done(new Error('Expected footer to start with APETAGEX ')) | ||
} | ||
ape.footer = v | ||
cb.state = 'tagField' | ||
return Ape.tagField(v) | ||
case 'tagField': | ||
parseTags(ape.footer, v, callback) | ||
done() | ||
break | ||
default: | ||
done(new Error('Illegal state: ' + cb.state)) | ||
} | ||
return 0 | ||
}) | ||
} | ||
var Ape = { | ||
/** | ||
* APE_DESCRIPTOR: defines the sizes (and offsets) of all the pieces, as well as the MD5 checksum | ||
*/ | ||
descriptor: { | ||
len: 52, | ||
get: function (buf, off) { | ||
return { | ||
// should equal 'MAC ' | ||
ID: new strtok.StringType(4, 'ascii').get(buf, off), | ||
// version number * 1000 (3.81 = 3810) (remember that 4-byte alignment causes this to take 4-bytes) | ||
version: strtok.UINT32_LE.get(buf, off + 4) / 1000, | ||
// the number of descriptor bytes (allows later expansion of this header) | ||
descriptorBytes: strtok.UINT32_LE.get(buf, off + 8), | ||
headerDataBytes: strtok.UINT32_LE.get(buf, off + 12), | ||
APEFrameDataBytes: strtok.UINT32_LE.get(buf, off + 16), | ||
APEFrameDataBytesHigh: strtok.UINT32_LE.get(buf, off + 20), | ||
terminatingDataBytes: strtok.UINT32_LE.get(buf, off + 24), | ||
fileMD5: new strtok.BufferType(16).get(buf, 28) | ||
// the number of header APE_HEADER bytes | ||
headerBytes: strtok.UINT32_LE.get(buf, off + 12), | ||
// the number of header APE_HEADER bytes | ||
seekTableBytes: strtok.UINT32_LE.get(buf, off + 16), | ||
// the number of header data bytes (from original file) | ||
headerDataBytes: strtok.UINT32_LE.get(buf, off + 20), | ||
// the number of bytes of APE frame data | ||
apeFrameDataBytes: strtok.UINT32_LE.get(buf, off + 24), | ||
// the high order number of APE frame data bytes | ||
apeFrameDataBytesHigh: strtok.UINT32_LE.get(buf, off + 28), | ||
// the terminating data of the file (not including tag data) | ||
terminatingDataBytes: strtok.UINT32_LE.get(buf, off + 32), | ||
// the MD5 hash of the file (see notes for usage... it's a littly tricky) | ||
fileMD5: new strtok.BufferType(16).get(buf, off + 36) | ||
} | ||
} | ||
} | ||
}, | ||
// headerDataBytes = 24 | ||
var ApeHeader = { | ||
/** | ||
* APE_HEADER: describes all of the necessary information about the APE file | ||
*/ | ||
header: { | ||
len: 24, | ||
@@ -31,43 +137,47 @@ | ||
return { | ||
// the compression level (see defines I.E. COMPRESSION_LEVEL_FAST) | ||
compressionLevel: strtok.UINT16_LE.get(buf, off), | ||
// any format flags (for future use) | ||
formatFlags: strtok.UINT16_LE.get(buf, off + 2), | ||
// the number of audio blocks in one frame | ||
blocksPerFrame: strtok.UINT32_LE.get(buf, off + 4), | ||
// the number of audio blocks in the final frame | ||
finalFrameBlocks: strtok.UINT32_LE.get(buf, off + 8), | ||
// the total number of frames | ||
totalFrames: strtok.UINT32_LE.get(buf, off + 12), | ||
// the bits per sample (typically 16) | ||
bitsPerSample: strtok.UINT16_LE.get(buf, off + 16), | ||
// the number of channels (1 or 2) | ||
channel: strtok.UINT16_LE.get(buf, off + 18), | ||
// the sample rate (typically 44100) | ||
sampleRate: strtok.UINT32_LE.get(buf, off + 20) | ||
} | ||
} | ||
} | ||
}, | ||
strtok.parse(stream, function (v, cb) { | ||
if (v === undefined) { | ||
cb.state = 0 | ||
return ApeDescriptor | ||
} | ||
/** | ||
* TAG: describes all the properties of the file [optional] | ||
*/ | ||
tagFooter: { | ||
len: 32, | ||
switch (cb.state) { | ||
case 0: | ||
if (v.ID !== 'MAC ') { | ||
throw new Error('Expected MAC on beginning of file') | ||
} | ||
cb.state = 1 | ||
return new strtok.BufferType(v.descriptorBytes - 44) | ||
case 1: | ||
cb.state = 2 | ||
return ApeHeader | ||
case 2: | ||
callback('format', 'tagType', type) | ||
callback('format', 'bitsPerSample', v.bitsPerSample) | ||
callback('format', 'sampleRate', v.sampleRate) | ||
callback('format', 'numberOfChannels', v.channel) | ||
callback('format', 'duration', calculateDuration(v)) | ||
return -1 | ||
get: function (buf, off) { | ||
return { | ||
// should equal 'APETAGEX' | ||
ID: new strtok.StringType(8, 'ascii').get(buf, off), | ||
// equals CURRENT_APE_TAG_VERSION | ||
version: strtok.UINT32_LE.get(buf, off + 8), | ||
// the complete size of the tag, including this footer (excludes header) | ||
size: strtok.UINT32_LE.get(buf, off + 12), | ||
// the number of fields in the tag | ||
fields: strtok.UINT32_LE.get(buf, off + 16), | ||
// reserved for later use (must be zero) | ||
reserved: new strtok.BufferType(12).get(buf, off + 20) // ToDo: what is this??? | ||
} | ||
} | ||
}) | ||
}, | ||
return readMetadata(stream, callback, done) | ||
tagField: function (footer) { | ||
return new strtok.BufferType(footer.size - Ape.tagFooter.len) | ||
} | ||
} | ||
@@ -86,37 +196,15 @@ | ||
function readMetadata (stream, callback, done) { | ||
var bufs = [] | ||
function parseTags (footer, buffer, callback) { | ||
var offset = 0 | ||
// 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) | ||
}) | ||
for (var i = 0; i < footer.fields; i++) { | ||
var size = strtok.UINT32_LE.get(buffer, offset, offset += 4) | ||
var flags = parseTagFlags(strtok.UINT32_LE.get(buffer, offset, offset += 4)) | ||
common.streamOnRealEnd(stream, function () { | ||
var buffer = Buffer.concat(bufs) | ||
var offset = buffer.length - 32 | ||
var zero = common.findZero(buffer, offset, buffer.length) | ||
var key = buffer.toString('ascii', offset, zero) | ||
offset = zero + 1 | ||
if (buffer.toString('utf8', offset, offset += 8) !== 'APETAGEX') { | ||
done(new Error("expected APE header but wasn't found")) | ||
} | ||
var footer = { | ||
version: strtok.UINT32_LE.get(buffer, offset, offset + 4), | ||
size: strtok.UINT32_LE.get(buffer, offset + 4, offset + 8), | ||
count: strtok.UINT32_LE.get(buffer, offset + 8, offset + 12) | ||
} | ||
// go 'back' to where the 'tags' start | ||
offset = buffer.length - footer.size | ||
for (var i = 0; i < footer.count; i++) { | ||
var size = strtok.UINT32_LE.get(buffer, offset, offset += 4) | ||
var flags = strtok.UINT32_LE.get(buffer, offset, offset += 4) | ||
var kind = (flags & 6) >> 1 | ||
var zero = common.findZero(buffer, offset, buffer.length) | ||
var key = buffer.toString('ascii', offset, zero) | ||
offset = zero + 1 | ||
if (kind === 0) { // utf-8 textstring | ||
switch (flags.dataType) { | ||
case 'text_utf8': { // utf-8 textstring | ||
var value = buffer.toString('utf8', offset, offset += size) | ||
@@ -129,3 +217,5 @@ var values = value.split(/\x00/g) | ||
}) | ||
} else if (kind === 1) { // binary (probably artwork) | ||
} | ||
break | ||
case 'binary': { // binary (probably artwork) | ||
if (key === 'Cover Art (Front)' || key === 'Cover Art (Back)') { | ||
@@ -148,5 +238,29 @@ var picData = buffer.slice(offset, offset + size) | ||
} | ||
break | ||
} | ||
return done() | ||
}) | ||
} | ||
} | ||
function parseTagFlags (flags) { | ||
return { | ||
containsHeader: isBitSet(flags, 31), | ||
containsFooter: isBitSet(flags, 30), | ||
isHeader: isBitSet(flags, 31), | ||
readOnly: isBitSet(flags, 0), | ||
dataType: getDataType((flags & 6) >> 1) | ||
} | ||
} | ||
function getDataType (type) { | ||
var types = ['text_utf8', 'binary', 'external_info', 'reserved'] | ||
return types[type] | ||
} | ||
/** | ||
* @param num {number} | ||
* @param bit 0 is least significant bit (LSB) | ||
* @return {boolean} true if bit is 1; otherwise false | ||
*/ | ||
function isBitSet (num, bit) { | ||
return (num & 1 << bit) !== 0 | ||
} |
{ | ||
"name": "music-metadata", | ||
"description": "Streaming music metadata parser for node and the browser.", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"author": "Borewit", | ||
@@ -70,3 +70,3 @@ "dependencies": { | ||
"node": true, | ||
"maxlen": 100, | ||
"maxlen": 120, | ||
"indent": 2 | ||
@@ -73,0 +73,0 @@ }, |
181
README.md
@@ -23,5 +23,4 @@ [![Build Status][travis-image]][travis-url] [![NPM version][npm-image]][npm-url] [![npm downloads][npm-downloads-image]][npm-url] | ||
* asf (wma, wmv) | ||
* MonkeyAudio, APEv2 (ape) | ||
* ape (MonkeyAudio) | ||
API | ||
@@ -44,98 +43,98 @@ ----------------- | ||
{ | ||
"common": { | ||
"title": "Lungs", | ||
"artist": ["I Have A Tribe"], | ||
"albumartist": ["I Have A Tribe"], | ||
"album": "No Countries", | ||
"year": "2015", | ||
"track": {"no": 4, "of": 5}, | ||
"genre": ["Pop Rock"], | ||
"disk": {"no": 1, "of": 1}, | ||
"picture": [ | ||
common: { | ||
title: 'Lungs', | ||
artist: ['I Have A Tribe'], | ||
albumartist: ['I Have A Tribe'], | ||
album: 'No Countries', | ||
year: '2015', | ||
track: {no: 4, of: 5}, | ||
genre: ['Pop Rock'], | ||
disk: {'no: 1, 'of: 1}, | ||
picture: [ | ||
{ | ||
"format": "jpg", | ||
"data": { | ||
"type": "Buffer", | ||
"data": ["..."] | ||
format: 'jpg', | ||
data: { | ||
type: 'Buffer', | ||
data: ['...'] | ||
} | ||
} | ||
], | ||
"grouping": "Rock", | ||
"copyright": "2015 Grönland Records", | ||
"releasecountry": "DE", | ||
"label": "Grönland Records", | ||
"musicbrainz_albumartistid": ["d8e73ae6-9884-4061-a056-c686b3375c9d"], | ||
"date": "2015-10-16", | ||
"musicbrainz_trackid": "ed040a93-1f95-4f91-8c41-359f5a6e7770", | ||
"albumartistsort": ["I Have a Tribe"], | ||
"originaldate": "2015-10-16", | ||
"script": "Latn", | ||
"musicbrainz_albumid": "4f54e938-89b4-4ee8-b282-74964f1e23bb", | ||
"releasestatus": "official", | ||
"acoustid_id": "5c94b20e-be79-4f6d-9800-d4caf8bc2a76", | ||
"catalognumber": "DAGRON153", | ||
"musicbrainz_artistid": ["d8e73ae6-9884-4061-a056-c686b3375c9d"], | ||
"media": "Digital Media", | ||
"releasetype": ["ep"], | ||
"originalyear": "2015", | ||
"musicbrainz_releasegroupid": "9c288627-be99-490e-9d3e-e6b135e9b8dd", | ||
"musicbrainz_recordingid": "a1a9ede1-219b-464c-9520-d9fd1debf933", | ||
"artistsort": ["I Have a Tribe"] | ||
grouping: 'Rock', | ||
copyright: '2015 Grönland Records', | ||
releasecountry: 'DE', | ||
label: 'Grönland Records', | ||
musicbrainz_albumartistid: ['d8e73ae6-9884-4061-a056-c686b3375c9d'], | ||
date: '2015-10-16', | ||
musicbrainz_trackid: 'ed040a93-1f95-4f91-8c41-359f5a6e7770', | ||
albumartistsort: ['I Have a Tribe'], | ||
originaldate: '2015-10-16', | ||
script: 'Latn', | ||
musicbrainz_albumid: '4f54e938-89b4-4ee8-b282-74964f1e23bb', | ||
releasestatus: 'official', | ||
acoustid_id: '5c94b20e-be79-4f6d-9800-d4caf8bc2a76', | ||
catalognumber: 'DAGRON153', | ||
musicbrainz_artistid: ['d8e73ae6-9884-4061-a056-c686b3375c9d'], | ||
media': 'Digital Media', | ||
releasetype: ['ep'], | ||
originalyear: '2015', | ||
musicbrainz_releasegroupid: '9c288627-be99-490e-9d3e-e6b135e9b8dd', | ||
musicbrainz_recordingid: 'a1a9ede1-219b-464c-9520-d9fd1debf933', | ||
artistsort: ['I Have a Tribe'] | ||
}, | ||
"format": { | ||
"duration": 266.56, | ||
"numberOfChannels": 2, | ||
"bitsPerSample": 16, | ||
"tagType": "vorbis", | ||
"sampleRate": 44100 | ||
format: { | ||
duration: 266.56, | ||
numberOfChannels: 2, | ||
bitsPerSample: 16, | ||
tagType: 'vorbis', | ||
sampleRate: 44100 | ||
}, | ||
"vorbis": { | ||
"GROUPING": "Rock", | ||
"COPYRIGHT": "2015 Grönland Records", | ||
"GENRE": ["Pop Rock"], | ||
"DESCRIPTION": ["Interprètes : I Have A Tribe, Main Artist; Patrick O'Laoghaire, Composer, Lyricist; Copyright Control\r\nLabel : Grönland Records - GoodToGo\r\n"], | ||
"TITLE": "Lungs", | ||
"RELEASECOUNTRY": "DE", | ||
"TOTALDISCS": ["1"], | ||
"LABEL": "Grönland Records", | ||
"TOTALTRACKS": ["5"], | ||
"MUSICBRAINZ_ALBUMARTISTID": ["d8e73ae6-9884-4061-a056-c686b3375c9d"], | ||
"DATE": "2015-10-16", | ||
"DISCNUMBER": "1", | ||
"TRACKTOTAL": "5", | ||
"MUSICBRAINZ_RELEASETRACKID": "ed040a93-1f95-4f91-8c41-359f5a6e7770", | ||
"ALBUMARTISTSORT": ["I Have a Tribe"], | ||
"ORIGINALDATE": "2015-10-16", | ||
"SCRIPT": "Latn", | ||
"MUSICBRAINZ_ALBUMID": "4f54e938-89b4-4ee8-b282-74964f1e23bb", | ||
"RELEASESTATUS": "official", | ||
"ALBUMARTIST": ["I Have A Tribe"], | ||
"ACOUSTID_ID": "5c94b20e-be79-4f6d-9800-d4caf8bc2a76", | ||
"CATALOGNUMBER": "DAGRON153", | ||
"ALBUM": "No Countries", | ||
"MUSICBRAINZ_ARTISTID": ["d8e73ae6-9884-4061-a056-c686b3375c9d"], | ||
"MEDIA": "Digital Media", | ||
"RELEASETYPE": ["ep"], | ||
"ORIGINALYEAR": "2015", | ||
"ARTIST": ["I Have A Tribe"], | ||
"DISCTOTAL": "1", | ||
"MUSICBRAINZ_RELEASEGROUPID": "9c288627-be99-490e-9d3e-e6b135e9b8dd", | ||
"MUSICBRAINZ_TRACKID": "a1a9ede1-219b-464c-9520-d9fd1debf933", | ||
"ARTISTSORT": ["I Have a Tribe"], | ||
"ARTISTS": ["I Have A Tribe"], | ||
"TRACKNUMBER": "4", | ||
"METADATA_BLOCK_PICTURE": [ | ||
vorbis: { | ||
GROUPING: 'Rock', | ||
COPYRIGHT: '2015 Grönland Records', | ||
GENRE: ['Pop Rock'], | ||
DESCRIPTION: ['Interprètes : I Have A Tribe, Main Artist; Patrick O'Laoghaire, Composer, Lyricist; Copyright Control\r\nLabel : Grönland Records - GoodToGo\r\n'], | ||
TITLE: 'Lungs', | ||
RELEASECOUNTRY: 'DE', | ||
TOTALDISCS: ['1'], | ||
LABEL: 'Grönland Records', | ||
TOTALTRACKS: ['5'], | ||
MUSICBRAINZ_ALBUMARTISTID: ['d8e73ae6-9884-4061-a056-c686b3375c9d'], | ||
DATE: '2015-10-16', | ||
DISCNUMBER: '1', | ||
TRACKTOTAL: '5', | ||
MUSICBRAINZ_RELEASETRACKID: 'ed040a93-1f95-4f91-8c41-359f5a6e7770', | ||
ALBUMARTISTSORT: ['I Have a Tribe'], | ||
ORIGINALDATE: '2015-10-16', | ||
SCRIPT: 'Latn', | ||
MUSICBRAINZ_ALBUMID: '4f54e938-89b4-4ee8-b282-74964f1e23bb', | ||
RELEASESTATUS: 'official', | ||
ALBUMARTIST: ['I Have A Tribe'], | ||
ACOUSTID_ID: '5c94b20e-be79-4f6d-9800-d4caf8bc2a76', | ||
CATALOGNUMBER: 'DAGRON153', | ||
ALBUM: 'No Countries', | ||
MUSICBRAINZ_ARTISTID: ['d8e73ae6-9884-4061-a056-c686b3375c9d'], | ||
MEDIA: 'Digital Media', | ||
RELEASETYPE: ['ep'], | ||
ORIGINALYEAR: '2015', | ||
ARTIST: ['I Have A Tribe'], | ||
DISCTOTAL: '1', | ||
MUSICBRAINZ_RELEASEGROUPID: '9c288627-be99-490e-9d3e-e6b135e9b8dd', | ||
MUSICBRAINZ_TRACKID: 'a1a9ede1-219b-464c-9520-d9fd1debf933', | ||
ARTISTSORT: ['I Have a Tribe'], | ||
ARTISTS: ['I Have A Tribe'], | ||
TRACKNUMBER: '4', | ||
METADATA_BLOCK_PICTURE: [ | ||
{ | ||
"type": "Cover (front)", | ||
"format": "image/jpeg", | ||
"description": "Official cover included in digital release", | ||
"width": 0, | ||
"height": 0, | ||
"colour_depth": 0, | ||
"indexed_color": 0, | ||
"data": { | ||
"type": "Buffer", | ||
"data": ["..."] | ||
type: 'Cover (front)', | ||
format: 'image/jpeg', | ||
description: 'Official cover included in digital release', | ||
width: 0, | ||
height: 0, | ||
colour_depth: 0, | ||
indexed_color: 0, | ||
data: { | ||
type: 'Buffer', | ||
data: ['...'] | ||
} | ||
@@ -187,3 +186,3 @@ } | ||
Based on [musicmetadata] (https://github.com/leetreveil/musicmetadata/) written by Lee Treveil <leetreveil@gmail.com> and many others. | ||
Based on [musicmetadata](https://github.com/leetreveil/musicmetadata/) written by Lee Treveil <leetreveil@gmail.com> and many others. | ||
@@ -201,2 +200,2 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
[travis-url]: https://travis-ci.org/profile/Borewit/music-metadata | ||
[travis-image]: https://api.travis-ci.org/Borewit/music-metadata.svg?branch=master | ||
[travis-image]: https://travis-ci.org/Borewit/music-metadata.svg?branch=master |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
389511
10917
198