Comparing version 0.1.21 to 0.2.0
@@ -0,1 +1,3 @@ | ||
import {Tags} from "node-id3"; | ||
declare module "node-id3" { | ||
@@ -263,3 +265,3 @@ namespace NodeID3 { | ||
}] | ||
image?: { | ||
image?: string | { | ||
mime: string | ||
@@ -347,3 +349,5 @@ /** | ||
export function read(filebuffer: string | Buffer): Tags | ||
export function read(filebuffer: string | Buffer, options: Object): Tags | ||
export function read(filebuffer: string | Buffer, fn: (err: NodeJS.ErrnoException | null, tags: Tags | null) => void): void | ||
export function read(filebuffer: string | Buffer, options: Object, fn: (err: NodeJS.ErrnoException | null, tags: Tags | null) => void): void | ||
export function update(tags: Tags, filebuffer: Buffer): Buffer | ||
@@ -350,0 +354,0 @@ export function update(tags: Tags, filepath: string): true | Error |
1588
index.js
const fs = require('fs') | ||
const iconv = require("iconv-lite") | ||
const ID3Definitions = require("./src/ID3Definitions") | ||
const ID3Frames = require('./src/ID3Frames') | ||
const ID3Util = require('./src/ID3Util') | ||
module.exports = new NodeID3 | ||
/* | ||
@@ -10,264 +10,10 @@ ** Used specification: http://id3.org/id3v2.3.0 | ||
/* | ||
** List of official text information frames | ||
** LibraryName: "T***" | ||
** Value is the ID of the text frame specified in the link above, the object's keys are just for simplicity, you can also use the ID directly. | ||
*/ | ||
const TFrames = { | ||
album: "TALB", | ||
bpm: "TBPM", | ||
composer: "TCOM", | ||
genre: "TCON", | ||
copyright: "TCOP", | ||
date: "TDAT", | ||
playlistDelay: "TDLY", | ||
encodedBy: "TENC", | ||
textWriter: "TEXT", | ||
fileType: "TFLT", | ||
time: "TIME", | ||
contentGroup: "TIT1", | ||
title: "TIT2", | ||
subtitle: "TIT3", | ||
initialKey: "TKEY", | ||
language: "TLAN", | ||
length: "TLEN", | ||
mediaType: "TMED", | ||
originalTitle: "TOAL", | ||
originalFilename: "TOFN", | ||
originalTextwriter: "TOLY", | ||
originalArtist: "TOPE", | ||
originalYear: "TORY", | ||
fileOwner: "TOWN", | ||
artist: "TPE1", | ||
performerInfo: "TPE2", | ||
conductor: "TPE3", | ||
remixArtist: "TPE4", | ||
partOfSet: "TPOS", | ||
publisher: "TPUB", | ||
trackNumber: "TRCK", | ||
recordingDates: "TRDA", | ||
internetRadioName: "TRSN", | ||
internetRadioOwner: "TRSO", | ||
size: "TSIZ", | ||
ISRC: "TSRC", | ||
encodingTechnology: "TSSE", | ||
year: "TYER" | ||
} | ||
const TFramesV220 = { | ||
album: "TAL", | ||
bpm: "TBP", | ||
composer: "TCM", | ||
genre: "TCO", | ||
copyright: "TCR", | ||
date: "TDA", | ||
playlistDelay: "TDY", | ||
encodedBy: "TEN", | ||
textWriter: "TEXT", | ||
fileType: "TFT", | ||
time: "TIM", | ||
contentGroup: "TT1", | ||
title: "TT2", | ||
subtitle: "TT3", | ||
initialKey: "TKE", | ||
language: "TLA", | ||
length: "TLE", | ||
mediaType: "TMT", | ||
originalTitle: "TOT", | ||
originalFilename: "TOF", | ||
originalTextwriter: "TOL", | ||
originalArtist: "TOA", | ||
originalYear: "TOR", | ||
artist: "TP1", | ||
performerInfo: "TP2", | ||
conductor: "TP3", | ||
remixArtist: "TP4", | ||
partOfSet: "TPA", | ||
publisher: "TPB", | ||
trackNumber: "TRK", | ||
recordingDates: "TRD", | ||
size: "TSI", | ||
ISRC: "TRC", | ||
encodingTechnology: "TSS", | ||
year: "TYE" | ||
} | ||
/* | ||
** List of non-text frames which follow their specific specification | ||
** name => Frame ID | ||
** create => function to create the frame | ||
** read => function to read the frame | ||
*/ | ||
const SFrames = { | ||
comment: { | ||
create: "createCommentFrame", | ||
read: "readCommentFrame", | ||
name: "COMM" | ||
}, | ||
image: { | ||
create: "createPictureFrame", | ||
read: "readPictureFrame", | ||
name: "APIC" | ||
}, | ||
unsynchronisedLyrics: { | ||
create: "createUnsynchronisedLyricsFrame", | ||
read: "readUnsynchronisedLyricsFrame", | ||
name: "USLT" | ||
}, | ||
userDefinedText: { | ||
create: "createUserDefinedText", | ||
read: "readUserDefinedText", | ||
name: "TXXX", | ||
multiple: true, | ||
updateCompareKey: "description" | ||
}, | ||
popularimeter: { | ||
create: "createPopularimeterFrame", | ||
read: "readPopularimeterFrame", | ||
name: "POPM" | ||
}, | ||
private: { | ||
create: "createPrivateFrame", | ||
read: "readPrivateFrame", | ||
name: "PRIV", | ||
multiple: true | ||
}, | ||
chapter: { | ||
create: "createChapterFrame", | ||
read: "readChapterFrame", | ||
name: "CHAP", | ||
multiple: true | ||
}, | ||
tableOfContents: { | ||
create: "createTableOfContentsFrame", | ||
read: "readTableOfContentsFrame", | ||
name: "CTOC", | ||
multiple: true | ||
}, | ||
userDefinedUrl: { | ||
create: "createUserDefinedUrl", | ||
read: "readUserDefinedUrl", | ||
name: "WXXX", | ||
multiple: true, | ||
updateCompareKey: "description" | ||
} | ||
} | ||
const SFramesV220 = { | ||
image: { | ||
create: "createPictureFrame", | ||
read: "readPictureFrame", | ||
name: "PIC" | ||
} | ||
} | ||
/* | ||
** List of URL frames. | ||
** name => Frame ID | ||
** multiple => Whether multiple of this frame can exist | ||
** hasDescription => Whether this frame may include a description | ||
*/ | ||
const WFrames = { | ||
commercialUrl: { | ||
name: "WCOM", | ||
multiple: true | ||
}, | ||
copyrightUrl: { | ||
name: "WCOP" | ||
}, | ||
fileUrl: { | ||
name: "WOAF" | ||
}, | ||
artistUrl: { | ||
name: "WOAR", | ||
multiple: true | ||
}, | ||
audioSourceUrl: { | ||
name: "WOAS" | ||
}, | ||
radioStationUrl: { | ||
name: "WORS" | ||
}, | ||
paymentUrl: { | ||
name: "WPAY" | ||
}, | ||
publisherUrl: { | ||
name: "WPUB" | ||
} | ||
} | ||
/* | ||
4.3.1 WAF Official audio file webpage | ||
4.3.1 WAR Official artist/performer webpage | ||
4.3.1 WAS Official audio source webpage | ||
4.3.1 WCM Commercial information | ||
4.3.1 WCP Copyright/Legal information | ||
4.3.1 WPB Publishers official webpage | ||
4.3.2 WXX User defined URL link frame | ||
*/ | ||
const WFramesV220 = { | ||
commercialUrl: { | ||
name: "WCM", | ||
multiple: true | ||
}, | ||
copyrightUrl: { | ||
name: "WCP" | ||
}, | ||
fileUrl: { | ||
name: "WAF" | ||
}, | ||
artistUrl: { | ||
name: "WAR", | ||
multiple: true | ||
}, | ||
audioSourceUrl: { | ||
name: "WAS" | ||
}, | ||
publisherUrl: { | ||
name: "WPB" | ||
}, | ||
userDefinedUrl: { | ||
name: "WXX", | ||
multiple: true, | ||
hasDescription: true | ||
} | ||
} | ||
/* | ||
** Officially available types of the picture frame | ||
*/ | ||
const APICTypes = [ | ||
"other", | ||
"file icon", | ||
"other file icon", | ||
"front cover", | ||
"back cover", | ||
"leaflet page", | ||
"media", | ||
"lead artist", | ||
"artist", | ||
"conductor", | ||
"band", | ||
"composer", | ||
"lyricist", | ||
"recording location", | ||
"during recording", | ||
"during performance", | ||
"video screen capture", | ||
"a bright coloured fish", | ||
"illustration", | ||
"band logotype", | ||
"publisher logotype" | ||
] | ||
function NodeID3() { | ||
} | ||
/* | ||
** Write passed tags to a file/buffer @ filebuffer | ||
** tags => Object | ||
** filebuffer => String || Buffer | ||
** fn => Function (for asynchronous usage) | ||
*/ | ||
NodeID3.prototype.write = function(tags, filebuffer, fn) { | ||
/** | ||
* Write passed tags to a file/buffer | ||
* @param tags - Object containing tags to be written | ||
* @param filebuffer - Can contain a filepath string or buffer | ||
* @param fn - (optional) Function for async version | ||
* @returns {boolean|Buffer|Error} | ||
*/ | ||
module.exports.write = function(tags, filebuffer, fn) { | ||
let completeTag = this.create(tags) | ||
@@ -314,8 +60,21 @@ if(filebuffer instanceof Buffer) { | ||
NodeID3.prototype.create = function(tags, fn) { | ||
/** | ||
* Creates a buffer containing the ID3 Tag | ||
* @param tags - Object containing tags to be written | ||
* @param fn fn - (optional) Function for async version | ||
* @returns {Buffer} | ||
*/ | ||
module.exports.create = function(tags, fn) { | ||
let frames = [] | ||
// Push a header for the ID3-Frame | ||
frames.push(this.createTagHeader()) | ||
// Create & push a header for the ID3-Frame | ||
const header = Buffer.alloc(10) | ||
header.fill(0) | ||
header.write("ID3", 0) //File identifier | ||
header.writeUInt16BE(0x0300, 3) //Version 2.3.0 -- 03 00 | ||
header.writeUInt16BE(0x0000, 5) //Flags 00 | ||
//Last 4 bytes are used for header size, but have to be inserted later, because at this point, its size is not clear. | ||
frames.push(header) | ||
frames = frames.concat(this.createBuffersFromTags(tags)) | ||
@@ -333,3 +92,3 @@ | ||
// ID3 header size uses only 7 bits of a byte, bit shift is needed | ||
let size = this.encodeSize(totalSize) | ||
let size = ID3Util.encodeSize(totalSize) | ||
@@ -349,31 +108,39 @@ // Write bytes to ID3 frame header, which is the first frame | ||
NodeID3.prototype.createBuffersFromTags = function(tags) { | ||
/** | ||
* Returns array of buffers created by tags specified in the tags argument | ||
* @param tags - Object containing tags to be written | ||
* @returns {Array} | ||
*/ | ||
module.exports.createBuffersFromTags = function(tags) { | ||
let frames = [] | ||
let tagNames = Object.keys(tags) | ||
if(!tags) return frames | ||
const rawObject = Object.keys(tags).reduce((acc, val) => { | ||
if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) { | ||
acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val] | ||
} else { | ||
acc[val] = tags[val] | ||
} | ||
return acc | ||
}, {}) | ||
tagNames.forEach(function (tag, index) { | ||
// Check if passed tag is text frame (Alias or ID) | ||
Object.keys(rawObject).forEach((specName, index) => { | ||
let frame | ||
if (TFrames[tag] || Object.keys(TFrames).map(i => TFrames[i]).indexOf(tag) != -1) { | ||
let specName = TFrames[tag] || tag | ||
frame = this.createTextFrame(specName, tags[tag]) | ||
} else if (WFrames[tag] || Object.keys(WFrames).map(i => WFrames[i]).map(x => x.name).indexOf(tag) !== -1) { | ||
let specName = WFrames[tag] ? WFrames[tag].name : tag | ||
let multiple = WFrames[Object.keys(WFrames)[Object.keys(WFrames).map(i => WFrames[i]).map(x => x.name).indexOf(specName)]].multiple | ||
if(multiple && tags[tag] instanceof Array && tags[tag].length > 0) { | ||
frame = Buffer.alloc(0); | ||
// Check if invalid specName | ||
if(specName.length !== 4) { | ||
return | ||
} | ||
if(ID3Frames[specName] !== undefined) { | ||
frame = ID3Frames[specName].create(rawObject[specName], 3, this) | ||
} else if(specName.startsWith('T')) { | ||
frame = ID3Frames.GENERIC_TEXT.create(specName, rawObject[specName], 3) | ||
} else if(specName.startsWith('W')) { | ||
if(ID3Util.getSpecOptions(specName, 3).multiple && rawObject[specName] instanceof Array && rawObject[specName].length > 0) { | ||
frame = Buffer.alloc(0) | ||
// deduplicate array | ||
for(var url of [...new Set(tags[tag])]) { | ||
frame = Buffer.concat([frame, this.createUrlFrame(specName, url)]) | ||
for(let url of [...new Set(rawObject[specName])]) { | ||
frame = Buffer.concat([frame, ID3Frames.GENERIC_URL.create(specName, url, 3)]) | ||
} | ||
} else { | ||
frame = this.createUrlFrame(specName, tags[tag]) | ||
frame = ID3Frames.GENERIC_URL.create(specName, rawObject[specName], 3) | ||
} | ||
} else if (SFrames[tag]) { // Check if Alias of special frame | ||
let createFrameFunction = SFrames[tag].create | ||
frame = this[createFrameFunction](tags[tag]) | ||
} else if (Object.keys(SFrames).map(i => SFrames[i]).map(x => x.name).indexOf(tag) != -1) { // Check if ID of special frame | ||
// get create function from special frames where tag ID is found at SFrame[index].name | ||
let createFrameFunction = SFrames[Object.keys(SFrames)[Object.keys(SFrames).map(i => SFrames[i]).map(x => x.name).indexOf(tag)]].create | ||
frame = this[createFrameFunction](tags[tag]) | ||
} | ||
@@ -384,3 +151,3 @@ | ||
} | ||
}.bind(this)) | ||
}) | ||
@@ -390,9 +157,10 @@ return frames | ||
/* | ||
** Read ID3-Tags from passed buffer/filepath | ||
** filebuffer => Buffer || String | ||
** options => Object | ||
** fn => function (for asynchronous usage) | ||
*/ | ||
NodeID3.prototype.read = function(filebuffer, options, fn) { | ||
/** | ||
* Read ID3-Tags from passed buffer/filepath | ||
* @param filebuffer - Can contain a filepath string or buffer | ||
* @param options - (optional) Object containing options | ||
* @param fn - (optional) Function for async version | ||
* @returns {boolean} | ||
*/ | ||
module.exports.read = function(filebuffer, options, fn) { | ||
if(!options || typeof options === 'function') { | ||
@@ -406,4 +174,3 @@ fn = fn || options | ||
} | ||
let tags = this.getTagsFromBuffer(filebuffer, options) | ||
return tags | ||
return this.getTagsFromBuffer(filebuffer, options) | ||
} else { | ||
@@ -415,6 +182,7 @@ if(typeof filebuffer === "string" || filebuffer instanceof String) { | ||
} else { | ||
let tags = this.getTagsFromBuffer(data, options) | ||
fn(null, tags) | ||
fn(null, this.getTagsFromBuffer(data, options)) | ||
} | ||
}.bind(this)) | ||
} else { | ||
fn(null, this.getTagsFromBuffer(filebuffer, options)) | ||
} | ||
@@ -424,129 +192,98 @@ } | ||
/* | ||
** Update ID3-Tags from passed buffer/filepath | ||
** filebuffer => Buffer || String | ||
** tags => Object | ||
** fn => function (for asynchronous usage) | ||
*/ | ||
NodeID3.prototype.update = function(tags, filebuffer, fn) { | ||
let rawTags = {} | ||
let SRawToNameMap = {} | ||
Object.keys(SFrames).map((key, index) => { | ||
SRawToNameMap[SFrames[key].name] = key | ||
}) | ||
Object.keys(tags).map(function(tagKey) { | ||
// if js name passed (TF) | ||
if(TFrames[tagKey]) { | ||
rawTags[TFrames[tagKey]] = tags[tagKey] | ||
/** | ||
* Update ID3-Tags from passed buffer/filepath | ||
* @param tags - Object containing tags to be written | ||
* @param filebuffer - Can contain a filepath string or buffer | ||
* @param fn - (optional) Function for async version | ||
* @returns {boolean|Buffer|Error} | ||
*/ | ||
module.exports.update = function(tags, filebuffer, fn) { | ||
const rawTags = Object.keys(tags).reduce((acc, val) => { | ||
if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) { | ||
acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val] | ||
} else { | ||
acc[val] = tags[val] | ||
} | ||
return acc | ||
}, {}) | ||
// if js name passed (WF) | ||
} else if(WFrames[tagKey]) { | ||
rawTags[WFrames[tagKey].name] = tags[tagKey] | ||
const updateFn = (currentTags) => { | ||
currentTags = currentTags.raw || {} | ||
Object.keys(rawTags).map((specName) => { | ||
const options = ID3Util.getSpecOptions(specName, 3) | ||
const cCompare = {} | ||
if(options.multiple && currentTags[specName] && rawTags[specName]) { | ||
if(options.updateCompareKey) { | ||
currentTags[specName].forEach((cTag, index) => { | ||
cCompare[cTag[options.updateCompareKey]] = index | ||
}) | ||
// if js name passed (SF) | ||
} else if(SFrames[tagKey]) { | ||
rawTags[SFrames[tagKey].name] = tags[tagKey] | ||
// if raw name passed (TF) | ||
} else if(Object.keys(TFrames).map(i => TFrames[i]).indexOf(tagKey) !== -1) { | ||
rawTags[tagKey] = tags[tagKey] | ||
// if raw name passed (WF) | ||
} else if(Object.keys(WFrames).map(i => WFrames[i]).map(x => x.name).indexOf(tagKey) !== -1) { | ||
rawTags[tagKey] = tags[tagKey] | ||
// if raw name passed (SF) | ||
} else if(Object.keys(SFrames).map(i => SFrames[i]).map(x => x.name).indexOf(tagKey) !== -1) { | ||
rawTags[tagKey] = tags[tagKey] | ||
} | ||
}) | ||
if(!fn || typeof fn !== 'function') { | ||
let currentTags = this.read(filebuffer) | ||
currentTags = currentTags.raw || {} | ||
// update current tags with new or keep them | ||
Object.keys(rawTags).map(function(tag) { | ||
if(SFrames[SRawToNameMap[tag]] && SFrames[SRawToNameMap[tag]].multiple && currentTags[tag] && rawTags[tag]) { | ||
const cCompare = {} | ||
currentTags[tag].forEach((cTag, index) => { | ||
cCompare[cTag[SFrames[SRawToNameMap[tag]].updateCompareKey]] = index | ||
}) | ||
if(!(rawTags[tag] instanceof Array)) rawTags[tag] = [rawTags[tag]] | ||
rawTags[tag].forEach((rTag, index) => { | ||
let comparison = cCompare[rTag[SFrames[SRawToNameMap[tag]].updateCompareKey]] | ||
if(comparison !== undefined) { | ||
currentTags[tag][comparison] = rTag | ||
} | ||
if (!(rawTags[specName] instanceof Array)) rawTags[specName] = [rawTags[specName]] | ||
rawTags[specName].forEach((rTag, index) => { | ||
const comparison = cCompare[rTag[options.updateCompareKey]] | ||
if (comparison !== undefined) { | ||
currentTags[specName][comparison] = rTag | ||
} else { | ||
currentTags[tag].push(rTag) | ||
currentTags[specName].push(rTag) | ||
} | ||
}) | ||
} else { | ||
currentTags[tag] = rawTags[tag] | ||
currentTags[specName] = rawTags[specName] | ||
} | ||
}) | ||
return this.write(currentTags, filebuffer) | ||
return currentTags | ||
} | ||
if(!fn || typeof fn !== 'function') { | ||
return this.write(updateFn(this.read(filebuffer)), filebuffer) | ||
} else { | ||
this.read(filebuffer, function(err, currentTags) { | ||
if(err) { | ||
fn(err) | ||
return | ||
} | ||
currentTags = currentTags.raw || {} | ||
// update current tags with new or keep them | ||
Object.keys(rawTags).map(function(tag) { | ||
if(SFrames[SRawToNameMap[tag]] && SFrames[SRawToNameMap[tag]].multiple && currentTags[tag] && rawTags[tag]) { | ||
const cCompare = {} | ||
currentTags[tag].forEach((cTag, index) => { | ||
cCompare[cTag[SFrames[SRawToNameMap[tag]].updateCompareKey]] = index | ||
}) | ||
if(!(rawTags[tag] instanceof Array)) rawTags[tag] = [rawTags[tag]] | ||
rawTags[tag].forEach((rTag, index) => { | ||
let comparison = cCompare[rTag[SFrames[SRawToNameMap[tag]].updateCompareKey]] | ||
if(comparison !== undefined) { | ||
currentTags[tag][comparison] = rTag | ||
} else { | ||
currentTags[tag].push(rTag) | ||
} | ||
}) | ||
} else { | ||
currentTags[tag] = rawTags[tag] | ||
} | ||
}) | ||
this.write(currentTags, filebuffer, fn) | ||
}.bind(this)) | ||
this.read(filebuffer, (err, currentTags) => { | ||
this.write(updateFn(this.read(filebuffer)), filebuffer, fn) | ||
}) | ||
} | ||
} | ||
/* | ||
** Read ID3-Tags from passed buffer | ||
** filebuffer => Buffer | ||
** options => Object | ||
*/ | ||
NodeID3.prototype.getTagsFromBuffer = function(filebuffer, options) { | ||
let framePosition = this.getFramePosition(filebuffer) | ||
module.exports.getTagsFromBuffer = function(filebuffer, options) { | ||
let framePosition = ID3Util.getFramePosition(filebuffer) | ||
if(framePosition === -1) { | ||
return false | ||
return this.getTagsFromFrames([], 3, options) | ||
} | ||
let frameSize = this.getTagSize(Buffer.from(filebuffer.toString('hex', framePosition, framePosition + 10), "hex")) + 10 | ||
const frameSize = ID3Util.decodeSize(filebuffer.slice(framePosition + 6, framePosition + 10)) + 10 | ||
let ID3Frame = Buffer.alloc(frameSize + 1) | ||
let ID3FrameBody = Buffer.alloc(frameSize - 10 + 1) | ||
filebuffer.copy(ID3Frame, 0, framePosition) | ||
filebuffer.copy(ID3FrameBody, 0, framePosition + 10) | ||
//ID3 version e.g. 3 if ID3v2.3.0 | ||
let ID3Version = ID3Frame[3] | ||
let identifierSize = 4 | ||
let textframeHeaderSize = 10 | ||
if(ID3Version == 2) { | ||
identifierSize = 3 | ||
textframeHeaderSize = 6 | ||
const tagFlags = ID3Util.parseTagHeaderFlags(ID3Frame) | ||
let extendedHeaderOffset = 0 | ||
if(tagFlags.extendedHeader) { | ||
if(ID3Version === 3) { | ||
extendedHeaderOffset = 4 + filebuffer.readUInt32BE(10) | ||
} else if(ID3Version === 4) { | ||
extendedHeaderOffset = ID3Util.decodeSize(filebuffer.slice(10, 14)) | ||
} | ||
} | ||
let ID3FrameBody = Buffer.alloc(frameSize - 10 - extendedHeaderOffset) | ||
filebuffer.copy(ID3FrameBody, 0, framePosition + 10 + extendedHeaderOffset) | ||
let frames = this.getFramesFromID3Body(ID3FrameBody, ID3Version, identifierSize, textframeHeaderSize) | ||
let frames = this.getFramesFromID3Body(ID3FrameBody, ID3Version, options) | ||
return this.getTagsFromFrames(frames, ID3Version) | ||
return this.getTagsFromFrames(frames, ID3Version, options) | ||
} | ||
NodeID3.prototype.getFramesFromID3Body = function(ID3FrameBody, ID3Version, identifierSize, textframeHeaderSize) { | ||
module.exports.getFramesFromID3Body = function(ID3FrameBody, ID3Version, options = {}) { | ||
let currentPosition = 0 | ||
let frames = [] | ||
if(!ID3FrameBody || !(ID3FrameBody instanceof Buffer)) { | ||
return frames | ||
} | ||
let identifierSize = 4 | ||
let textframeHeaderSize = 10 | ||
if(ID3Version === 2) { | ||
identifierSize = 3 | ||
textframeHeaderSize = 6 | ||
} | ||
while(currentPosition < ID3FrameBody.length && ID3FrameBody[currentPosition] !== 0x00) { | ||
@@ -557,16 +294,23 @@ let bodyFrameHeader = Buffer.alloc(textframeHeaderSize) | ||
let decodeSize = false | ||
if(ID3Version == 4) { | ||
if(ID3Version === 4) { | ||
decodeSize = true | ||
} | ||
let bodyFrameSize = this.getFrameSize(bodyFrameHeader, decodeSize, ID3Version) | ||
let bodyFrameSize = ID3Util.getFrameSize(bodyFrameHeader, decodeSize, ID3Version) | ||
if(bodyFrameSize + 10 > (ID3FrameBody.length - currentPosition)) { | ||
break | ||
} | ||
const specName = bodyFrameHeader.toString('utf8', 0, identifierSize) | ||
if(options.exclude instanceof Array && options.exclude.includes(specName) || options.include instanceof Array && !options.include.includes(specName)) { | ||
currentPosition += bodyFrameSize + textframeHeaderSize | ||
continue | ||
} | ||
const frameHeaderFlags = ID3Util.parseFrameHeaderFlags(bodyFrameHeader, ID3Version) | ||
let bodyFrameBuffer = Buffer.alloc(bodyFrameSize) | ||
ID3FrameBody.copy(bodyFrameBuffer, 0, currentPosition + textframeHeaderSize) | ||
ID3FrameBody.copy(bodyFrameBuffer, 0, currentPosition + textframeHeaderSize + (frameHeaderFlags.dataLengthIndicator ? 4 : 0)) | ||
// Size of sub frame + its header | ||
currentPosition += bodyFrameSize + textframeHeaderSize | ||
frames.push({ | ||
name: bodyFrameHeader.toString('utf8', 0, identifierSize), | ||
body: bodyFrameBuffer | ||
name: specName, | ||
flags: frameHeaderFlags, | ||
body: frameHeaderFlags.unsynchronisation ? ID3Util.processUnsynchronisedBuffer(bodyFrameBuffer) : bodyFrameBuffer | ||
}) | ||
@@ -578,118 +322,59 @@ } | ||
NodeID3.prototype.getTagsFromFrames = function(frames, ID3Version) { | ||
let tags = { raw: {} } | ||
module.exports.getTagsFromFrames = function(frames, ID3Version, options = {}) { | ||
let tags = { } | ||
let raw = { } | ||
frames.forEach(function(frame, index) { | ||
// Check first character if frame is text frame | ||
if(frame.name[0] === "T" && frame.name !== "TXXX") { | ||
// Decode body | ||
let decoded | ||
if(frame.body[0] === 0x01) { | ||
decoded = iconv.decode(frame.body.slice(1), "utf16").replace(/\0/g, "") | ||
frames.forEach((frame, index) => { | ||
const specName = ID3Version === 2 ? ID3Definitions.FRAME_IDENTIFIERS.v3[ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]] : frame.name | ||
const identifier = ID3Version === 2 ? ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name] : ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v3[frame.name] | ||
if(!specName || !identifier) { | ||
return | ||
} | ||
let decoded | ||
if(ID3Frames[specName]) { | ||
decoded = ID3Frames[specName].read(frame.body, ID3Version, this) | ||
} else if(specName.startsWith('T')) { | ||
decoded = ID3Frames.GENERIC_TEXT.read(frame.body, ID3Version) | ||
} else if(specName.startsWith('W')) { | ||
decoded = ID3Frames.GENERIC_URL.read(frame.body, ID3Version) | ||
} | ||
if(decoded) { | ||
if(ID3Util.getSpecOptions(specName, ID3Version).multiple) { | ||
if(!options.onlyRaw) { | ||
if(!tags[identifier]) tags[identifier] = [] | ||
tags[identifier].push(decoded) | ||
} | ||
if(!options.noRaw) { | ||
if(!raw[specName]) raw[specName] = [] | ||
raw[specName].push(decoded) | ||
} | ||
} else { | ||
decoded = iconv.decode(frame.body.slice(1), "ISO-8859-1").replace(/\0/g, "") | ||
} | ||
tags.raw[frame.name] = decoded | ||
let versionFrames = TFrames | ||
if(ID3Version == 2) { | ||
versionFrames = TFramesV220 | ||
} | ||
Object.keys(versionFrames).map(function(key) { | ||
if(versionFrames[key] === frame.name) { | ||
tags[key] = decoded | ||
if(!options.onlyRaw) { | ||
tags[identifier] = decoded | ||
} | ||
}) | ||
} else if (frame.name[0] === "W" && frame.name !== "WXXX") { | ||
let versionFrames = WFrames | ||
if(ID3Version == 2) { | ||
versionFrames = WFramesV220 | ||
} | ||
Object.keys(versionFrames).map(function(key) { | ||
if(versionFrames[key].name === frame.name) { | ||
// URL fields contain no encoding byte and are always ISO-8859-1 as per spec | ||
let decoded = iconv.decode(frame.body, "ISO-8859-1").replace(/\0/g, "") | ||
if(versionFrames[key].multiple) { | ||
if(!tags[key]) tags[key] = [] | ||
if(!tags.raw[frame.name]) tags.raw[frame.name] = [] | ||
tags.raw[frame.name].push(decoded) | ||
tags[key].push(decoded) | ||
} else { | ||
tags.raw[frame.name] = decoded | ||
tags[key] = decoded | ||
} | ||
if(!options.noRaw) { | ||
raw[specName] = decoded | ||
} | ||
}) | ||
} else { | ||
let versionFrames = SFrames | ||
if(ID3Version == 2) { | ||
versionFrames = SFramesV220 | ||
} | ||
// Check if non-text frame is supported | ||
Object.keys(versionFrames).map(function(key) { | ||
if(versionFrames[key].name === frame.name) { | ||
let decoded = this[versionFrames[key].read](frame.body, ID3Version) | ||
if(versionFrames[key].multiple) { | ||
if(!tags[key]) tags[key] = [] | ||
if(!tags.raw[frame.name]) tags.raw[frame.name] = [] | ||
tags.raw[frame.name].push(decoded) | ||
tags[key].push(decoded) | ||
} else { | ||
tags.raw[frame.name] = decoded | ||
tags[key] = decoded | ||
} | ||
} | ||
}.bind(this)) | ||
} | ||
}.bind(this)) | ||
}) | ||
if(options.onlyRaw) return raw | ||
if(options.noRaw) return tags | ||
tags.raw = raw | ||
return tags | ||
} | ||
/* | ||
** Get position of ID3-Frame, returns -1 if not found | ||
** buffer => Buffer | ||
*/ | ||
NodeID3.prototype.getFramePosition = function(buffer) { | ||
let framePosition = buffer.indexOf("ID3") | ||
if(framePosition == -1 || framePosition > 20) { | ||
return -1 | ||
} else { | ||
return framePosition | ||
} | ||
} | ||
/** | ||
* Checks and removes already written ID3-Frames from a buffer | ||
* @param data - Buffer | ||
* @returns {boolean|Buffer} | ||
*/ | ||
module.exports.removeTagsFromBuffer = function(data) { | ||
let framePosition = ID3Util.getFramePosition(data) | ||
/* | ||
** Get size of tag from header | ||
** buffer => Buffer/Array (header) | ||
*/ | ||
NodeID3.prototype.getTagSize = function(buffer) { | ||
return this.decodeSize(Buffer.from([buffer[6], buffer[7], buffer[8], buffer[9]])) | ||
} | ||
/* | ||
** Get size of frame from header | ||
** buffer => Buffer/Array (header) | ||
** decode => Boolean | ||
*/ | ||
NodeID3.prototype.getFrameSize = function(buffer, decode, ID3Version) { | ||
let decodeBytes | ||
if(ID3Version > 2) { | ||
decodeBytes = [buffer[4], buffer[5], buffer[6], buffer[7]] | ||
} else { | ||
decodeBytes = [buffer[3], buffer[4], buffer[5]] | ||
} | ||
if(decode) { | ||
return this.decodeSize(Buffer.from(decodeBytes)) | ||
} else { | ||
return Buffer.from(decodeBytes).readUIntBE(0, decodeBytes.length) | ||
} | ||
} | ||
/* | ||
** Checks and removes already written ID3-Frames from a buffer | ||
** data => buffer | ||
*/ | ||
NodeID3.prototype.removeTagsFromBuffer = function(data) { | ||
let framePosition = this.getFramePosition(data) | ||
if(framePosition === -1) { | ||
@@ -707,14 +392,16 @@ return data | ||
if(data.length >= framePosition + 10) { | ||
const size = this.decodeSize(data.slice(framePosition + 6, framePosition + 10)); | ||
const size = ID3Util.decodeSize(data.slice(framePosition + 6, framePosition + 10)) | ||
return Buffer.concat([data.slice(0, framePosition), data.slice(framePosition + size + 10)]) | ||
} else { | ||
return data; | ||
return data | ||
} | ||
} | ||
/* | ||
** Checks and removes already written ID3-Frames from a file | ||
** data => buffer | ||
*/ | ||
NodeID3.prototype.removeTags = function(filepath, fn) { | ||
/** | ||
* Checks and removes already written ID3-Frames from a file | ||
* @param filepath - Filepath to file | ||
* @param fn - (optional) Function for async usage | ||
* @returns {boolean|Error} | ||
*/ | ||
module.exports.removeTags = function(filepath, fn) { | ||
if(!fn || typeof fn !== 'function') { | ||
@@ -763,827 +450,42 @@ let data | ||
/* | ||
** This function ensures that the msb of each byte is 0 | ||
** totalSize => int | ||
*/ | ||
NodeID3.prototype.encodeSize = function(totalSize) { | ||
let byte_3 = totalSize & 0x7F | ||
let byte_2 = (totalSize >> 7) & 0x7F | ||
let byte_1 = (totalSize >> 14) & 0x7F | ||
let byte_0 = (totalSize >> 21) & 0x7F | ||
return ([byte_0, byte_1, byte_2, byte_3]) | ||
} | ||
/* | ||
** This function decodes the 7-bit size structure | ||
** hSize => int | ||
*/ | ||
NodeID3.prototype.decodeSize = function(hSize) { | ||
return ((hSize[0] << 21) + (hSize[1] << 14) + (hSize[2] << 7) + (hSize[3])) | ||
} | ||
/* | ||
** Create header for ID3-Frame v2.3.0 | ||
*/ | ||
NodeID3.prototype.createTagHeader = function() { | ||
let header = Buffer.alloc(10) | ||
header.fill(0) | ||
header.write("ID3", 0) //File identifier | ||
header.writeUInt16BE(0x0300, 3) //Version 2.3.0 -- 03 00 | ||
header.writeUInt16BE(0x0000, 5) //Flags 00 | ||
//Last 4 bytes are used for header size, but have to be inserted later, because at this point, its size is not clear. | ||
return header | ||
} | ||
/* | ||
** Create text frame | ||
** specName => string (ID) | ||
** text => string (body) | ||
*/ | ||
NodeID3.prototype.createTextFrame = function(specName, text) { | ||
if(!specName || !text) { | ||
return null | ||
} | ||
let encoded = iconv.encode(text, "utf16") | ||
let buffer = Buffer.alloc(10) | ||
buffer.fill(0) | ||
buffer.write(specName, 0) // ID of the specified frame | ||
buffer.writeUInt32BE((encoded).length + 1, 4) // Size of frame (string length + encoding byte) | ||
let encBuffer = Buffer.alloc(1) // Encoding (now using UTF-16 encoded w/ BOM) | ||
encBuffer.fill(1) // UTF-16 | ||
var contentBuffer = Buffer.from(encoded, 'binary') // Text -> Binary encoding for UTF-16 w/ BOM | ||
return Buffer.concat([buffer, encBuffer, contentBuffer]) | ||
} | ||
/* | ||
** Create URL frame | ||
** specName => string (ID) | ||
** text => string (body) | ||
*/ | ||
NodeID3.prototype.createUrlFrame = function(specName, text) { | ||
if(!specName || !text) { | ||
return null | ||
} | ||
let encoded = iconv.encode(text, "ISO-8859-1") | ||
let buffer = Buffer.alloc(10) | ||
buffer.fill(0) | ||
buffer.write(specName, 0) // ID of the specified frame | ||
buffer.writeUInt32BE((encoded).length + 1, 4) // Size of frame (string length + encoding byte) | ||
let encBuffer = Buffer.alloc(1) // Encoding (URLs are always ISO-8859-1) | ||
encBuffer.fill(0) // ISO-8859-1 | ||
var contentBuffer = Buffer.from(encoded, 'binary') // Text -> Binary encoding for ISO-8859-1 | ||
return Buffer.concat([buffer, encBuffer, contentBuffer]) | ||
} | ||
/* | ||
** data => string || buffer | ||
*/ | ||
NodeID3.prototype.createPictureFrame = function(data) { | ||
try { | ||
if (data instanceof Buffer) { | ||
data = { | ||
imageBuffer: Buffer.from(data) | ||
} | ||
} else if (typeof data === 'string' || data instanceof String) { | ||
data = { | ||
imageBuffer: Buffer.from(fs.readFileSync(data, 'binary'), 'binary') | ||
} | ||
} else if (!data.imageBuffer) { | ||
return Buffer.alloc(0); | ||
} | ||
let bHeader = Buffer.alloc(10) | ||
bHeader.fill(0) | ||
bHeader.write("APIC", 0) | ||
let mime_type = data.mime | ||
if(!data.mime) { | ||
if (data.imageBuffer[0] === 0xff && data.imageBuffer[1] === 0xd8 && data.imageBuffer[2] === 0xff) { | ||
mime_type = "image/jpeg" | ||
} else { | ||
mime_type = "image/png" | ||
} | ||
} | ||
let bContent = Buffer.alloc(mime_type.length + 3) | ||
bContent.fill(0) | ||
bContent[mime_type.length + 2] = 0x03 // Front cover | ||
bContent.write(mime_type, 1) | ||
bContent[0] = 0x01 | ||
let bDescription = this.createContentDescriptor(data.description, 0x01, true) | ||
bHeader.writeUInt32BE(data.imageBuffer.length + bContent.length + bDescription.length, 4) // Size of frame | ||
return Buffer.concat([bHeader, bContent, bDescription, data.imageBuffer]) | ||
} catch(e) { | ||
return e | ||
} | ||
} | ||
/* | ||
** data => buffer | ||
*/ | ||
NodeID3.prototype.readPictureFrame = function(APICFrame, ID3Version) { | ||
let picture = {} | ||
let APICMimeType | ||
if(ID3Version == 2) { | ||
APICMimeType = APICFrame.toString('ascii').substring(1, 4) | ||
} else { | ||
APICMimeType = APICFrame.toString('ascii').substring(1, APICFrame.indexOf(0x00, 1)) | ||
} | ||
if(APICMimeType == "image/jpeg") { | ||
picture.mime = "jpeg" | ||
} else if(APICMimeType == "image/png") { | ||
picture.mime = "png" | ||
} else { | ||
picture.mime = APICMimeType | ||
} | ||
picture.type = {} | ||
if(ID3Version == 2 && APICTypes.length < APICFrame[4]) { | ||
picture.type = { | ||
id: APICFrame[4], | ||
name: APICTypes[APICFrame[4]] | ||
} | ||
} else { | ||
picture.type = { | ||
id: APICFrame[APICFrame.indexOf(0x00, 1) + 1], | ||
name: APICTypes[APICFrame[APICFrame.indexOf(0x00, 1) + 1]] | ||
} | ||
} | ||
let descEnd | ||
if(APICFrame[0] == 0x00) { | ||
if(ID3Version == 2) { | ||
picture.description = iconv.decode(APICFrame.slice(5, APICFrame.indexOf(0x00, 5)), "ISO-8859-1") || undefined | ||
descEnd = APICFrame.indexOf(0x00, 5) | ||
} else { | ||
picture.description = iconv.decode(APICFrame.slice(APICFrame.indexOf(0x00, 1) + 2, APICFrame.indexOf(0x00, APICFrame.indexOf(0x00, 1) + 2)), "ISO-8859-1") || undefined | ||
descEnd = APICFrame.indexOf(0x00, APICFrame.indexOf(0x00, 1) + 2) | ||
} | ||
} else if (APICFrame[0] == 0x01) { | ||
if(ID3Version == 2) { | ||
let descOffset = 5 | ||
let desc = APICFrame.slice(descOffset) | ||
let descFound = desc.indexOf("0000", 0, 'hex') | ||
descEnd = descOffset + descFound + 2 | ||
if(descFound != -1) { | ||
picture.description = iconv.decode(desc.slice(0, descFound + 2), 'utf16') || undefined | ||
} | ||
} else { | ||
descEnd = 0 | ||
while(APICFrame[descEnd] !== undefined && APICFrame[descEnd] !== 0x00 || APICFrame[descEnd + 1] !== 0x00 || APICFrame[descEnd + 2] === 0x00) { | ||
descEnd++ | ||
} | ||
if(APICFrame[descEnd] !== undefined) { | ||
picture.description = iconv.decode(APICFrame.slice(APICFrame.indexOf(0x00, 1) + 2, descEnd), 'utf16').replace(/\0/g, "") || undefined | ||
descEnd += 1 | ||
} | ||
} | ||
} | ||
if(descEnd) { | ||
picture.imageBuffer = APICFrame.slice(descEnd + 1) | ||
} else { | ||
picture.imageBuffer = APICFrame.slice(5) | ||
} | ||
return picture | ||
} | ||
NodeID3.prototype.getEncodingByte = function(encoding) { | ||
if(!encoding || encoding === 0x00 || encoding === "ISO-8859-1") { | ||
return 0x00 | ||
} else { | ||
return 0x01 | ||
} | ||
} | ||
NodeID3.prototype.getEncodingName = function(encoding) { | ||
if(this.getEncodingByte(encoding) === 0x00) { | ||
return "ISO-8859-1" | ||
} else { | ||
return "utf16" | ||
} | ||
} | ||
NodeID3.prototype.getTerminationCount = function(encoding) { | ||
if(encoding === 0x00) { | ||
return 1 | ||
} else { | ||
return 2 | ||
} | ||
} | ||
NodeID3.prototype.createTextEncoding = function(encoding) { | ||
let buffer = Buffer.alloc(1) | ||
buffer[0] = this.getEncodingByte(encoding) | ||
return buffer | ||
} | ||
NodeID3.prototype.createLanguage = function(language) { | ||
if(!language) { | ||
language = "eng" | ||
} else if(language.length > 3) { | ||
language = language.substring(0, 3) | ||
} | ||
return Buffer.from(language) | ||
} | ||
NodeID3.prototype.createContentDescriptor = function(description, encoding, terminated) { | ||
if(!description) { | ||
description = terminated ? iconv.encode("\0", this.getEncodingName(encoding)) : Buffer.alloc(0) | ||
return description | ||
} | ||
description = iconv.encode(description, this.getEncodingName(encoding)) | ||
return terminated ? Buffer.concat([description, Buffer.alloc(this.getTerminationCount(encoding)).fill(0x00)]) : description | ||
} | ||
NodeID3.prototype.createText = function(text, encoding, terminated) { | ||
if(!text) { | ||
text = "" | ||
} | ||
text = iconv.encode(text, this.getEncodingName(encoding)) | ||
return terminated ? Buffer.concat([text, Buffer.from(this.getTerminationCount(encoding)).fill(0x00)]) : text | ||
} | ||
/* | ||
** comment => object { | ||
** language: string (3 characters), | ||
** text: string | ||
** shortText: string | ||
** } | ||
**/ | ||
NodeID3.prototype.createCommentFrame = function(comment) { | ||
comment = comment || {} | ||
if(!comment.text) { | ||
return null | ||
} | ||
// Create frame header | ||
let buffer = Buffer.alloc(10) | ||
buffer.fill(0) | ||
buffer.write("COMM", 0) // Write header ID | ||
let encodingBuffer = this.createTextEncoding(0x01) | ||
let languageBuffer = this.createLanguage(comment.language) | ||
let descriptorBuffer = this.createContentDescriptor(comment.shortText, 0x01, true) | ||
let textBuffer = this.createText(comment.text, 0x01, false) | ||
buffer.writeUInt32BE(encodingBuffer.length + languageBuffer.length + descriptorBuffer.length + textBuffer.length, 4) | ||
return Buffer.concat([buffer, encodingBuffer, languageBuffer, descriptorBuffer, textBuffer]) | ||
} | ||
/* | ||
** frame => Buffer | ||
*/ | ||
NodeID3.prototype.readCommentFrame = function(frame) { | ||
let tags = {} | ||
if(!frame) { | ||
return tags | ||
} | ||
if(frame[0] == 0x00) { | ||
tags = { | ||
language: iconv.decode(frame, "ISO-8859-1").substring(1, 4).replace(/\0/g, ""), | ||
shortText: iconv.decode(frame, "ISO-8859-1").substring(4, frame.indexOf(0x00, 1)).replace(/\0/g, ""), | ||
text: iconv.decode(frame, "ISO-8859-1").substring(frame.indexOf(0x00, 1) + 1).replace(/\0/g, "") | ||
} | ||
} else if(frame[0] == 0x01) { | ||
let descriptorEscape = 0 | ||
while(frame[descriptorEscape] !== undefined && frame[descriptorEscape] !== 0x00 || frame[descriptorEscape + 1] !== 0x00 || frame[descriptorEscape + 2] === 0x00) { | ||
descriptorEscape++ | ||
} | ||
if(frame[descriptorEscape] === undefined) { | ||
return tags | ||
} | ||
let shortText = frame.slice(4, descriptorEscape) | ||
let text = frame.slice(descriptorEscape + 2) | ||
tags = { | ||
language: frame.toString().substring(1, 4).replace(/\0/g, ""), | ||
shortText: iconv.decode(shortText, "utf16").replace(/\0/g, ""), | ||
text: iconv.decode(text, "utf16").replace(/\0/g, "") | ||
} | ||
} | ||
return tags | ||
} | ||
/* | ||
** unsynchronisedLyrics => object { | ||
** language: string (3 characters), | ||
** text: string | ||
** shortText: string | ||
** } | ||
**/ | ||
NodeID3.prototype.createUnsynchronisedLyricsFrame = function(unsynchronisedLyrics) { | ||
unsynchronisedLyrics = unsynchronisedLyrics || {} | ||
if(typeof unsynchronisedLyrics === 'string' || unsynchronisedLyrics instanceof String) { | ||
unsynchronisedLyrics = { | ||
text: unsynchronisedLyrics | ||
} | ||
} | ||
if(!unsynchronisedLyrics.text) { | ||
return null | ||
} | ||
// Create frame header | ||
let buffer = Buffer.alloc(10) | ||
buffer.fill(0) | ||
buffer.write("USLT", 0) // Write header ID | ||
let encodingBuffer = this.createTextEncoding(0x01) | ||
let languageBuffer = this.createLanguage(unsynchronisedLyrics.language) | ||
let descriptorBuffer = this.createContentDescriptor(unsynchronisedLyrics.shortText, 0x01, true) | ||
let textBuffer = this.createText(unsynchronisedLyrics.text, 0x01, false) | ||
buffer.writeUInt32BE(encodingBuffer.length + languageBuffer.length + descriptorBuffer.length + textBuffer.length, 4) | ||
return Buffer.concat([buffer, encodingBuffer, languageBuffer, descriptorBuffer, textBuffer]) | ||
} | ||
/* | ||
** frame => Buffer | ||
*/ | ||
NodeID3.prototype.readUnsynchronisedLyricsFrame = function(frame) { | ||
let tags = {} | ||
if(!frame) { | ||
return tags | ||
} | ||
if(frame[0] == 0x00) { | ||
tags = { | ||
language: iconv.decode(frame, "ISO-8859-1").substring(1, 4).replace(/\0/g, ""), | ||
shortText: iconv.decode(frame, "ISO-8859-1").substring(4, frame.indexOf(0x00, 1)).replace(/\0/g, ""), | ||
text: iconv.decode(frame, "ISO-8859-1").substring(frame.indexOf(0x00, 1) + 1).replace(/\0/g, "") | ||
} | ||
} else if(frame[0] == 0x01) { | ||
let descriptorEscape = 0 | ||
while(frame[descriptorEscape] !== undefined && frame[descriptorEscape] !== 0x00 || frame[descriptorEscape + 1] !== 0x00 || frame[descriptorEscape + 2] === 0x00) { | ||
descriptorEscape++ | ||
} | ||
if(frame[descriptorEscape] === undefined) { | ||
return tags | ||
} | ||
let shortText = frame.slice(4, descriptorEscape) | ||
let text = frame.slice(descriptorEscape + 2) | ||
tags = { | ||
language: frame.toString().substring(1, 4).replace(/\0/g, ""), | ||
shortText: iconv.decode(shortText, "utf16").replace(/\0/g, ""), | ||
text: iconv.decode(text, "utf16").replace(/\0/g, "") | ||
} | ||
} | ||
return tags | ||
} | ||
/* | ||
** comment => object / array of objects { | ||
** description: string | ||
** value: string | ||
** } | ||
**/ | ||
NodeID3.prototype.createUserDefinedText = function(userDefinedText, recursiveBuffer) { | ||
let udt = userDefinedText || {} | ||
if(udt instanceof Array && udt.length > 0) { | ||
if(!recursiveBuffer) { | ||
// Don't alter passed array value! | ||
userDefinedText = userDefinedText.slice(0) | ||
} | ||
udt = userDefinedText.pop() | ||
} | ||
if(udt && udt.description) { | ||
// Create frame header | ||
let buffer = Buffer.alloc(10) | ||
buffer.fill(0) | ||
buffer.write("TXXX", 0) // Write header ID | ||
let encodingBuffer = this.createTextEncoding(0x01) | ||
let descriptorBuffer = this.createContentDescriptor(udt.description, 0x01, true) | ||
let valueBuffer = this.createText(udt.value, 0x01, false) | ||
buffer.writeUInt32BE(encodingBuffer.length + descriptorBuffer.length + valueBuffer.length, 4) | ||
if(!recursiveBuffer) { | ||
recursiveBuffer = Buffer.concat([buffer, encodingBuffer, descriptorBuffer, valueBuffer]) | ||
} else { | ||
recursiveBuffer = Buffer.concat([recursiveBuffer, buffer, encodingBuffer, descriptorBuffer, valueBuffer]) | ||
} | ||
} | ||
if(userDefinedText instanceof Array && userDefinedText.length > 0) { | ||
return this.createUserDefinedText(userDefinedText, recursiveBuffer) | ||
} else { | ||
return recursiveBuffer | ||
} | ||
} | ||
/* | ||
** frame => Buffer | ||
*/ | ||
NodeID3.prototype.readUserDefinedText = function(frame) { | ||
let tags = {} | ||
if(!frame) { | ||
return tags | ||
} | ||
if(frame[0] == 0x00) { | ||
tags = { | ||
description: iconv.decode(frame, "ISO-8859-1").substring(1, frame.indexOf(0x00, 1)).replace(/\0/g, ""), | ||
value: iconv.decode(frame, "ISO-8859-1").substring(frame.indexOf(0x00, 1) + 1).replace(/\0/g, "") | ||
} | ||
} else if(frame[0] == 0x01) { | ||
let descriptorEscape = 0 | ||
while(frame[descriptorEscape] !== undefined && frame[descriptorEscape] !== 0x00 || frame[descriptorEscape + 1] !== 0x00 || frame[descriptorEscape + 2] === 0x00) { | ||
descriptorEscape++ | ||
} | ||
if(frame[descriptorEscape] === undefined) { | ||
return tags | ||
} | ||
let description = frame.slice(1, descriptorEscape) | ||
let value = frame.slice(descriptorEscape + 2) | ||
tags = { | ||
description: iconv.decode(description, "utf16").replace(/\0/g, ""), | ||
value: iconv.decode(value, "utf16").replace(/\0/g, "") | ||
} | ||
} | ||
return tags | ||
} | ||
/* | ||
** popularimeter => object { | ||
** email: string, | ||
** rating: int | ||
** counter: int | ||
** } | ||
**/ | ||
NodeID3.prototype.createPopularimeterFrame = function(popularimeter) { | ||
popularimeter = popularimeter || {} | ||
let email = popularimeter.email | ||
let rating = Math.trunc(popularimeter.rating) | ||
let counter = Math.trunc(popularimeter.counter) | ||
if(!email) { | ||
return null | ||
} | ||
if(isNaN(rating) || rating < 0 || rating > 255) { | ||
rating = 0 | ||
} | ||
if(isNaN(counter) || counter < 0) { | ||
counter = 0 | ||
} | ||
// Create frame header | ||
let buffer = Buffer.alloc(10, 0) | ||
buffer.write("POPM", 0) // Write header ID | ||
let emailBuffer = this.createText(email, 0x01, false) | ||
emailBuffer = Buffer.from(email + '\0', 'utf8') | ||
let ratingBuffer = Buffer.alloc(1, rating) | ||
let counterBuffer = Buffer.alloc(4, 0) | ||
counterBuffer.writeUInt32BE(counter, 0) | ||
buffer.writeUInt32BE(emailBuffer.length + ratingBuffer.length + counterBuffer.length, 4) | ||
var frame = Buffer.concat([buffer, emailBuffer, ratingBuffer, counterBuffer]) | ||
return frame | ||
} | ||
/* | ||
** frame => Buffer | ||
*/ | ||
NodeID3.prototype.readPopularimeterFrame = function(frame) { | ||
let tags = {} | ||
if(!frame) { | ||
return tags | ||
} | ||
let endEmailIndex = frame.indexOf(0x00, 1) | ||
if(endEmailIndex > -1) { | ||
tags.email = iconv.decode(frame.slice(0, endEmailIndex), "ISO-8859-1") | ||
let ratingIndex = endEmailIndex + 1 | ||
if(ratingIndex < frame.length) { | ||
tags.rating = frame[ratingIndex] | ||
let counterIndex = ratingIndex + 1 | ||
if(counterIndex < frame.length) { | ||
let value = frame.slice(counterIndex, frame.length) | ||
if(value.length >= 4) { | ||
tags.counter = value.readUInt32BE(0) | ||
} | ||
} | ||
} | ||
} | ||
return tags | ||
} | ||
/* | ||
** _private => object|array { | ||
** ownerIdentifier: string, | ||
** data: buffer|string | ||
** } | ||
**/ | ||
NodeID3.prototype.createPrivateFrame = function(_private) { | ||
if(_private instanceof Array && _private.length > 0) { | ||
let frames = [] | ||
_private.forEach(tag => { | ||
let frame = this.createPrivateFrameHelper(tag) | ||
if(frame) { | ||
frames.push(frame) | ||
} | ||
module.exports.Promise = { | ||
write: (tags, file) => { | ||
return new Promise((resolve, reject) => { | ||
this.write(tags, file, (err, ret) => { | ||
if(err) reject(err) | ||
else resolve(ret) | ||
}) | ||
}) | ||
return frames.length ? Buffer.concat(frames) : null | ||
} else { | ||
return this.createPrivateFrameHelper(_private) | ||
} | ||
} | ||
NodeID3.prototype.createPrivateFrameHelper = function(_private) { | ||
if(!_private || !_private.ownerIdentifier || !_private.data) { | ||
return null; | ||
} | ||
let header = Buffer.alloc(10, 0) | ||
header.write("PRIV") | ||
let ownerIdentifier = Buffer.from(_private.ownerIdentifier + "\0", "utf8") | ||
let data | ||
if(typeof(_private.data) == "string") { | ||
data = Buffer.from(_private.data, "utf8") | ||
} else { | ||
data = _private.data | ||
} | ||
header.writeUInt32BE(ownerIdentifier.length + data.length, 4) | ||
return Buffer.concat([header, ownerIdentifier, data]) | ||
} | ||
/* | ||
** frame => Buffer | ||
*/ | ||
NodeID3.prototype.readPrivateFrame = function(frame) { | ||
let tags = {} | ||
if(!frame) { | ||
return tags | ||
} | ||
let endOfOwnerIdentification = frame.indexOf(0x00) | ||
if(endOfOwnerIdentification == -1) { | ||
return tags | ||
} | ||
tags.ownerIdentifier = iconv.decode(frame.slice(0, endOfOwnerIdentification), "ISO-8859-1") | ||
if(frame.length <= endOfOwnerIdentification + 1) { | ||
return tags | ||
} | ||
tags.data = frame.slice(endOfOwnerIdentification + 1) | ||
return tags | ||
} | ||
/** | ||
* @typedef {Object} Chapter | ||
* @property {string} elementID | ||
* @property {number} startTimeMs | ||
* @property {number} endTimeMs | ||
* @property {number} [startOffsetBytes] | ||
* @property {number} [endOffsetBytes] | ||
* @property {object} [tags] | ||
*/ | ||
NodeID3.prototype.createChapterFrame = function (/** @type Chapter[] | Chapter */chapter) { | ||
if(chapter instanceof Array && chapter.length > 0) { | ||
let frames = [] | ||
chapter.forEach((tag, index) => { | ||
let frame = this.createChapterFrameHelper(tag, index + 1) | ||
if(frame) { | ||
frames.push(frame) | ||
} | ||
}, | ||
update: (tags, file) => { | ||
return new Promise((resolve, reject) => { | ||
this.update(tags, file, (err, ret) => { | ||
if(err) reject(err) | ||
else resolve(ret) | ||
}) | ||
}) | ||
return frames.length ? Buffer.concat(frames) : null | ||
} else { | ||
return this.createChapterFrameHelper(chapter, 1) | ||
} | ||
} | ||
NodeID3.prototype.createChapterFrameHelper = function(chapter, id) { | ||
if(!chapter || !chapter.elementID || typeof chapter.startTimeMs === "undefined" || !chapter.endTimeMs) { | ||
return null | ||
} | ||
let header = Buffer.alloc(10, 0) | ||
header.write("CHAP") | ||
let elementIDBuffer = Buffer.from(chapter.elementID + "\0") | ||
let startTimeBuffer = Buffer.alloc(4) | ||
startTimeBuffer.writeUInt32BE(chapter.startTimeMs, 0) | ||
let endTimeBuffer = Buffer.alloc(4) | ||
endTimeBuffer.writeUInt32BE(chapter.endTimeMs, 0) | ||
let startOffsetBytesBuffer = Buffer.alloc(4, 0xFF) | ||
if(chapter.startOffsetBytes) { | ||
startOffsetBytesBuffer.writeUInt32BE(chapter.startOffsetBytes, 0) | ||
} | ||
let endOffsetBytesBuffer = Buffer.alloc(4, 0xFF) | ||
if(chapter.endOffsetBytes) { | ||
endOffsetBytesBuffer.writeUInt32BE(chapter.endOffsetBytes, 0) | ||
} | ||
let frames | ||
if(chapter.tags) { | ||
frames = this.createBuffersFromTags(chapter.tags) | ||
} | ||
const framesBuffer = frames ? Buffer.concat(frames) : Buffer.alloc(0) | ||
header.writeUInt32BE(elementIDBuffer.length + 16 + framesBuffer.length, 4) | ||
return Buffer.concat([header, elementIDBuffer, startTimeBuffer, endTimeBuffer, startOffsetBytesBuffer, endOffsetBytesBuffer, framesBuffer]) | ||
} | ||
/* | ||
** frame => Buffer | ||
*/ | ||
NodeID3.prototype.readChapterFrame = function(frame) { | ||
let tags = {} | ||
if(!frame) { | ||
return tags | ||
} | ||
let endOfElementIDString = frame.indexOf(0x00) | ||
if(endOfElementIDString == -1 || frame.length - endOfElementIDString - 1 < 16) { | ||
return tags | ||
} | ||
tags.elementID = iconv.decode(frame.slice(0, endOfElementIDString), "ISO-8859-1") | ||
tags.startTimeMs = frame.readUInt32BE(endOfElementIDString + 1) | ||
tags.endTimeMs = frame.readUInt32BE(endOfElementIDString + 5) | ||
if(frame.readUInt32BE(endOfElementIDString + 9) != Buffer.alloc(4, 0xff).readUInt32BE(0)) { | ||
tags.startOffsetBytes = frame.readUInt32BE(endOfElementIDString + 9) | ||
} | ||
if(frame.readUInt32BE(endOfElementIDString + 13) != Buffer.alloc(4, 0xff).readUInt32BE(0)) { | ||
tags.endOffsetBytes = frame.readUInt32BE(endOfElementIDString + 13) | ||
} | ||
if(frame.length - endOfElementIDString - 17 > 0) { | ||
let framesBuffer = frame.slice(endOfElementIDString + 17) | ||
tags.tags = this.getTagsFromFrames(this.getFramesFromID3Body(framesBuffer, 3, 4, 10), 3) | ||
} | ||
return tags | ||
} | ||
NodeID3.prototype.createTableOfContentsFrame = function (tableOfContents) { | ||
if(tableOfContents instanceof Array && tableOfContents.length > 0) { | ||
let frames = [] | ||
tableOfContents.forEach((tag, index) => { | ||
let frame = this.createTableOfContentsFrameHelper(tag, index + 1) | ||
if(frame) { | ||
frames.push(frame) | ||
} | ||
}, | ||
create: (tags) => { | ||
return new Promise((resolve) => { | ||
this.create(tags, (buffer) => { | ||
resolve(buffer) | ||
}) | ||
}) | ||
return frames.length ? Buffer.concat(frames) : null | ||
} else { | ||
return this.createTableOfContentsFrameHelper(tableOfContents, 1) | ||
}, | ||
read: (file, options) => { | ||
return new Promise((resolve, reject) => { | ||
this.read(file, options, (err, ret) => { | ||
if(err) reject(err) | ||
else resolve(ret) | ||
}) | ||
}) | ||
}, | ||
removeTags: (filepath) => { | ||
return new Promise((resolve, reject) => { | ||
this.removeTags(filepath, (err) => { | ||
if(err) reject(err) | ||
else resolve() | ||
}) | ||
}) | ||
} | ||
} | ||
NodeID3.prototype.createTableOfContentsFrameHelper = function(tableOfContents, id) { | ||
if(!tableOfContents || !tableOfContents.elementID) { | ||
return null | ||
} | ||
if(!(tableOfContents.elements instanceof Array)) { | ||
tableOfContents.elements = []; | ||
} | ||
let header = Buffer.alloc(10, 0) | ||
header.write("CTOC") | ||
let elementIDBuffer = Buffer.from(tableOfContents.elementID + "\0") | ||
let ctocFlags = Buffer.alloc(1, 0); | ||
if(id === 1) { | ||
ctocFlags[0] += 2; | ||
} | ||
if(tableOfContents.isOrdered) { | ||
ctocFlags[0] += 1; | ||
} | ||
let entryCountBuffer = Buffer.alloc(1, tableOfContents.elements.length); | ||
let entries = Buffer.from(tableOfContents.elements.map(element => element += "\0").join("")); | ||
let frames | ||
if(tableOfContents.tags) { | ||
frames = this.createBuffersFromTags(tableOfContents.tags) | ||
} | ||
const framesBuffer = frames ? Buffer.concat(frames) : Buffer.alloc(0) | ||
const finalFrame = Buffer.concat([header, elementIDBuffer, ctocFlags, entryCountBuffer, entries, framesBuffer]); | ||
finalFrame.writeUInt32BE(finalFrame.length - 10, 4) | ||
return finalFrame | ||
} | ||
NodeID3.prototype.readTableOfContentsFrame = function(frame) { | ||
let toc = {} | ||
if(!frame) { | ||
return toc | ||
} | ||
let endOfElementIDString = frame.indexOf(0x00) | ||
if(endOfElementIDString === -1 || frame.length - endOfElementIDString - 1 < 16) { | ||
return toc | ||
} | ||
toc.elementID = iconv.decode(frame.slice(0, endOfElementIDString), "ISO-8859-1") | ||
/* %000000ab where a is top level and b is ordered */ | ||
toc.isOrdered = !!(frame.readUInt8(endOfElementIDString + 1) & 0x01 === 0x01) | ||
toc.elements = frame.slice(endOfElementIDString + 3).toString('latin1').split("\0", frame.readUInt8(endOfElementIDString + 2)) | ||
let framesPosition = endOfElementIDString + 3 + toc.elements.map(element => element.length).reduce((a, b) => a + b + 1, 0) | ||
if(frame.length > framesPosition) { | ||
let framesBuffer = frame.slice(framesPosition) | ||
toc.tags = this.getTagsFromFrames(this.getFramesFromID3Body(framesBuffer, 3, 4, 10), 3) | ||
} | ||
return toc | ||
} | ||
NodeID3.prototype.createUserDefinedUrl = function(userDefinedUrl, recursiveBuffer) { | ||
let udu = userDefinedUrl || {} | ||
if(udu instanceof Array && udu.length > 0) { | ||
if(!recursiveBuffer) { | ||
// Don't alter passed array value! | ||
userDefinedUrl = userDefinedUrl.slice(0) | ||
} | ||
udu = userDefinedUrl.pop() | ||
} | ||
if(udu && udu.description) { | ||
// Create frame header | ||
let buffer = Buffer.alloc(10) | ||
buffer.fill(0) | ||
buffer.write("WXXX", 0) // Write header ID | ||
let encodingBuffer = this.createTextEncoding(0x01) | ||
let descriptorBuffer = this.createContentDescriptor(udu.description, 0x01, true) | ||
let urlBuffer = this.createText(udu.url, 0x00, false) | ||
buffer.writeUInt32BE(encodingBuffer.length + descriptorBuffer.length + urlBuffer.length, 4) | ||
if(!recursiveBuffer) { | ||
recursiveBuffer = Buffer.concat([buffer, encodingBuffer, descriptorBuffer, urlBuffer]) | ||
} else { | ||
recursiveBuffer = Buffer.concat([recursiveBuffer, buffer, encodingBuffer, descriptorBuffer, urlBuffer]) | ||
} | ||
} | ||
if(userDefinedUrl instanceof Array && userDefinedUrl.length > 0) { | ||
return this.createUserDefinedUrl(userDefinedUrl, recursiveBuffer) | ||
} else { | ||
return recursiveBuffer | ||
} | ||
} | ||
NodeID3.prototype.readUserDefinedUrl = function(frame) { | ||
let tags = {} | ||
if(!frame) { | ||
return tags | ||
} | ||
if(frame[0] == 0x00) { | ||
tags = { | ||
description: iconv.decode(frame, "ISO-8859-1").substring(1, frame.indexOf(0x00, 1)).replace(/\0/g, ""), | ||
url: iconv.decode(frame, "ISO-8859-1").substring(frame.indexOf(0x00, 1) + 1).replace(/\0/g, "") | ||
} | ||
} else if(frame[0] == 0x01) { | ||
let descriptorEscape = 0 | ||
while(frame[descriptorEscape] !== undefined && frame[descriptorEscape] !== 0x00 || frame[descriptorEscape + 1] !== 0x00 || frame[descriptorEscape + 2] === 0x00) { | ||
descriptorEscape++ | ||
} | ||
if(frame[descriptorEscape] === undefined) { | ||
return tags | ||
} | ||
let description = frame.slice(1, descriptorEscape) | ||
let value = frame.slice(descriptorEscape + 2) | ||
tags = { | ||
description: iconv.decode(description, "utf16").replace(/\0/g, ""), | ||
url: iconv.decode(value, "ISO-8859-1").replace(/\0/g, "") | ||
} | ||
} | ||
return tags | ||
} |
{ | ||
"name": "node-id3", | ||
"version": "0.1.21", | ||
"description": "Pure JavaScript ID3 Tag writer/reader", | ||
"version": "0.2.0", | ||
"description": "Pure JavaScript ID3v2 Tag writer and reader", | ||
"author": "Jan Metzger <jan.metzger@gmx.net>", | ||
@@ -15,5 +15,8 @@ "main": "index.js", | ||
"keywords": [ | ||
"id3", | ||
"ID3", | ||
"ID3v2", | ||
"metadata", | ||
"tags", | ||
"mp3", | ||
"tags", | ||
"audio", | ||
"music" | ||
@@ -29,2 +32,3 @@ ], | ||
"directories": { | ||
"test": "test", | ||
"example": "example" | ||
@@ -37,7 +41,9 @@ }, | ||
"dependencies": { | ||
"iconv-lite": "0.5.1" | ||
"iconv-lite": "0.6.2" | ||
}, | ||
"devDependencies": { | ||
"chai": "^4.2.0", | ||
"jsmediatags": "^3.9.3", | ||
"mocha": "8.1.3" | ||
} | ||
} |
131
README.md
# node-id3 | ||
node-id3 is a ID3-Tag library written in JavaScript without other dependencies. | ||
node-id3 is an ID3-Tag library written in JavaScript. | ||
@@ -17,33 +17,42 @@ ## Installation | ||
// file can be a buffer or string with the path to a file | ||
let file = './path/to/(mp3)file' || new Buffer("Some Buffer of a (mp3) file") | ||
let filebuffer = new Buffer("Some Buffer of a (mp3) file") | ||
let filepath = './path/to/(mp3)file' | ||
const filebuffer = new Buffer("Some Buffer of a (mp3) file") | ||
const filepath = './path/to/(mp3)file' | ||
const tags = { | ||
title: "Tomorrow", | ||
artist: "Kevin Penkin", | ||
album: "TVアニメ「メイドインアビス」オリジナルサウンドトラック", | ||
APIC: "./example/mia_cover.jpg", | ||
TRCK: "27" | ||
} | ||
``` | ||
### Creating/Writing tags | ||
### Write tags to file | ||
```javascript | ||
const success = NodeID3.write(tags, filepath) // Returns true/Error | ||
// async version | ||
NodeID3.write(tags, file, function(err) { }) | ||
``` | ||
### Write tags to filebuffer | ||
```javascript | ||
// Define the tags for your file using the ID (e.g. APIC) or the alias (see at bottom) | ||
let tags = { | ||
title: "Tomorrow", | ||
artist: "Kevin Penkin", | ||
album: "TVアニメ「メイドインアビス」オリジナルサウンドトラック", | ||
APIC: "./example/mia_cover.jpg", | ||
TRCK: "27" | ||
} | ||
const success = NodeID3.write(tags, filebuffer) // Returns Buffer | ||
// async version | ||
NodeID3.write(tags, file, function(err, buffer) { }) | ||
``` | ||
// Create a ID3-Frame buffer from passed tags | ||
// Synchronous | ||
let ID3FrameBuffer = NodeID3.create(tags) // Returns ID3-Frame buffer | ||
// Asynchronous | ||
NodeID3.create(tags, function(frame) { }) | ||
### Update existing tags of file or buffer | ||
This will write new/changed values but keep all others | ||
```javascript | ||
const success = NodeID3.update(tags, filepath) // Returns true/Error | ||
const success = NodeID3.update(tags, filebuffer) // Returns Buffer | ||
NodeID3.update(tags, filepath, function(err, buffer) { }) | ||
NodeID3.update(tags, filebuffer, function(err, buffer) { }) | ||
``` | ||
// Write ID3-Frame into (.mp3) file | ||
let success = NodeID3.write(tags, file) // Returns true/false or, if buffer passed as file, the tagged buffer | ||
NodeID3.write(tags, file, function(err, buffer) { }) // Buffer is only returned if a buffer was passed as file | ||
// Update existing ID3-Frame with new/edited tags | ||
let success = NodeID3.update(tags, file) // Returns true/false or, if buffer passed as file, the tagged buffer | ||
NodeID3.update(tags, file, function(err, buffer) { }) // Buffer is only returned if a buffer was passed as file | ||
### Create tags as buffer | ||
```javascript | ||
const success = NodeID3.create(tags) // Returns ID3-Tag Buffer | ||
// async version | ||
NodeID3.create(tags, function(buffer) { }) | ||
``` | ||
@@ -54,25 +63,33 @@ | ||
```javascript | ||
let tags = NodeID3.read(file) | ||
NodeID3.read(file, function(err, tags) { | ||
/* | ||
tags: { | ||
title: "Tomorrow", | ||
artist: "Kevin Penkin", | ||
image: { | ||
mime: "jpeg", | ||
type: { | ||
id: 3, | ||
name: "front cover" | ||
}, | ||
description: String, | ||
imageBuffer: Buffer | ||
}, | ||
raw: { | ||
TIT2: "Tomorrow", | ||
TPE1: "Kevin Penkin", | ||
APIC: Object (See above) | ||
} | ||
} | ||
*/ | ||
}) | ||
const tags = NodeID3.read(file) | ||
NodeID3.read(file, function(err, tags) {}) | ||
/* | ||
tags: { | ||
title: "Tomorrow", | ||
artist: "Kevin Penkin", | ||
image: { | ||
mime: "jpeg", | ||
type: { | ||
id: 3, | ||
name: "front cover" | ||
}, | ||
description: String, | ||
imageBuffer: Buffer | ||
}, | ||
raw: { | ||
TIT2: "Tomorrow", | ||
TPE1: "Kevin Penkin", | ||
APIC: Object (See above) | ||
} | ||
} | ||
*/ | ||
// Possible options | ||
const options = { | ||
include: ['TALB', 'TIT2'], // only read the specified tags (default: all) | ||
exclude: ['APIC'], // don't read the specified tags (default: []) | ||
onlyRaw: false, // only return raw object (default: false) | ||
noRaw: false // don't generate raw object (default: false) | ||
} | ||
const tags = NodeID3.read(file, options) | ||
``` | ||
@@ -83,3 +100,3 @@ | ||
```javascript | ||
let success = NodeID3.removeTags(filepath) // returns true/false | ||
const success = NodeID3.removeTags(filepath) // returns true/Error | ||
NodeID3.removeTags(filepath, function(err) { }) | ||
@@ -90,4 +107,16 @@ | ||
## Supported aliases | ||
### Using Promises (available starting with v0.2) | ||
```javascript | ||
const NodeID3Promise = require('node-id3').Promise | ||
NodeID3.write(tags, fileOrBuffer) | ||
NodeID3.update(tags, fileOrBuffer) | ||
NodeID3.create(tags) | ||
NodeID3.read(filepath) | ||
NodeID3.removeTags(filepath) | ||
``` | ||
## Supported aliases/fields | ||
``` | ||
album: | ||
@@ -94,0 +123,0 @@ bpm: |
777
test/test.js
@@ -1,5 +0,9 @@ | ||
const NodeID3 = require('../index.js'); | ||
const assert = require('assert'); | ||
const iconv = require('iconv-lite'); | ||
const fs = require('fs'); | ||
const NodeID3 = require('../index.js') | ||
const jsmediatags = require("jsmediatags") | ||
const assert = require('assert') | ||
const chai = require('chai') | ||
const expect = chai.expect | ||
const iconv = require('iconv-lite') | ||
const fs = require('fs') | ||
const ID3Util = require('../src/ID3Util') | ||
@@ -9,4 +13,4 @@ describe('NodeID3', function () { | ||
it('empty tags', function () { | ||
assert.equal(NodeID3.create({}).compare(Buffer.from([0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])), 0); | ||
}); | ||
assert.strictEqual(NodeID3.create({}).compare(Buffer.from([0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])), 0) | ||
}) | ||
it('text frames', function () { | ||
@@ -16,12 +20,15 @@ let tags = { | ||
album: "nasÖÄkdnasd", | ||
notfound: "notfound" | ||
}; | ||
let buffer = NodeID3.create(tags); | ||
let titleSize = 10 + 1 + iconv.encode(tags.TIT2, 'utf16').length; | ||
let albumSize = 10 + 1 + iconv.encode(tags.album, 'utf16').length; | ||
assert.equal(buffer.length, | ||
notfound: "notfound", | ||
year: 1990 | ||
} | ||
let buffer = NodeID3.create(tags) | ||
let titleSize = 10 + 1 + iconv.encode(tags.TIT2, 'utf16').length | ||
let albumSize = 10 + 1 + iconv.encode(tags.album, 'utf16').length | ||
let yearSize = 10 + 1 + iconv.encode(tags.year, 'utf16').length | ||
assert.strictEqual(buffer.length, | ||
10 + // ID3 frame header | ||
titleSize + // TIT2 header + encoding byte + utf16 bytes + utf16 string | ||
albumSize // same as above for album | ||
); | ||
albumSize +// same as above for album, | ||
yearSize | ||
) | ||
// Check ID3 header | ||
@@ -31,5 +38,5 @@ assert.ok(buffer.includes( | ||
Buffer.from([0x49, 0x44, 0x33, 0x03, 0x00, 0x00]), | ||
Buffer.from(NodeID3.encodeSize(titleSize + albumSize)) | ||
Buffer.from(ID3Util.encodeSize(titleSize + albumSize + yearSize)) | ||
]) | ||
)); | ||
)) | ||
@@ -45,3 +52,3 @@ // Check TIT2 frame | ||
]) | ||
)); | ||
)) | ||
// Check album frame | ||
@@ -56,4 +63,13 @@ assert.ok(buffer.includes( | ||
]) | ||
)); | ||
}); | ||
)) | ||
assert.ok(buffer.includes( | ||
Buffer.concat([ | ||
Buffer.from([0x54, 0x59, 0x45, 0x52]), | ||
sizeToBuffer(yearSize - 10), | ||
Buffer.from([0x00, 0x00]), | ||
Buffer.from([0x01]), | ||
iconv.encode(tags.year, 'utf16') | ||
]) | ||
)) | ||
}) | ||
@@ -66,8 +82,8 @@ it('user defined text frames', function() { | ||
} | ||
}; | ||
let buffer = NodeID3.create(tags).slice(10); | ||
let descEncoded = iconv.encode(tags.userDefinedText.description + "\0", "UTF-16"); | ||
let valueEncoded = iconv.encode(tags.userDefinedText.value, "UTF-16"); | ||
} | ||
let buffer = NodeID3.create(tags).slice(10) | ||
let descEncoded = iconv.encode(tags.userDefinedText.description + "\0", "UTF-16") | ||
let valueEncoded = iconv.encode(tags.userDefinedText.value, "UTF-16") | ||
assert.equal(Buffer.compare( | ||
assert.strictEqual(Buffer.compare( | ||
buffer, | ||
@@ -82,3 +98,3 @@ Buffer.concat([ | ||
]) | ||
), 0); | ||
), 0) | ||
@@ -93,27 +109,27 @@ tags = { | ||
}] | ||
}; | ||
buffer = NodeID3.create(tags).slice(10); | ||
let desc1Encoded = iconv.encode(tags.userDefinedText[0].description + "\0", "UTF-16"); | ||
let value1Encoded = iconv.encode(tags.userDefinedText[0].value, "UTF-16"); | ||
let desc2Encoded = iconv.encode(tags.userDefinedText[1].description + "\0", "UTF-16"); | ||
let value2Encoded = iconv.encode(tags.userDefinedText[1].value, "UTF-16"); | ||
} | ||
buffer = NodeID3.create(tags).slice(10) | ||
let desc1Encoded = iconv.encode(tags.userDefinedText[0].description + "\0", "UTF-16") | ||
let value1Encoded = iconv.encode(tags.userDefinedText[0].value, "UTF-16") | ||
let desc2Encoded = iconv.encode(tags.userDefinedText[1].description + "\0", "UTF-16") | ||
let value2Encoded = iconv.encode(tags.userDefinedText[1].value, "UTF-16") | ||
assert.equal(Buffer.compare( | ||
assert.strictEqual(Buffer.compare( | ||
buffer, | ||
Buffer.concat([ | ||
Buffer.from([0x54, 0x58, 0x58, 0x58]), | ||
sizeToBuffer(1 + desc2Encoded.length + value2Encoded.length), | ||
sizeToBuffer(1 + desc1Encoded.length + value1Encoded.length), | ||
Buffer.from([0x00, 0x00]), | ||
Buffer.from([0x01]), | ||
desc2Encoded, | ||
value2Encoded, | ||
desc1Encoded, | ||
value1Encoded, | ||
Buffer.from([0x54, 0x58, 0x58, 0x58]), | ||
sizeToBuffer(1 + desc1Encoded.length + value1Encoded.length), | ||
sizeToBuffer(1 + desc2Encoded.length + value2Encoded.length), | ||
Buffer.from([0x00, 0x00]), | ||
Buffer.from([0x01]), | ||
desc1Encoded, | ||
value1Encoded | ||
desc2Encoded, | ||
value2Encoded | ||
]) | ||
), 0); | ||
}); | ||
), 0) | ||
}) | ||
@@ -128,10 +144,10 @@ it('create APIC frame', function() { | ||
} | ||
}; | ||
} | ||
assert.equal(Buffer.compare( | ||
assert.strictEqual(Buffer.compare( | ||
NodeID3.create(tags), | ||
Buffer.from('4944330300000000003B4150494300000031000001696D6167652F6A7065670003FFFE610073006400660000005B307836312C20307836322C20307836332C20307836345D', 'hex') | ||
), 0); | ||
), 0) | ||
assert.equal(Buffer.compare( | ||
assert.strictEqual(Buffer.compare( | ||
NodeID3.create({ | ||
@@ -147,3 +163,3 @@ image: __dirname + '/smallimg' | ||
assert.equal(Buffer.compare( | ||
assert.strictEqual(Buffer.compare( | ||
NodeID3.create({ | ||
@@ -158,3 +174,3 @@ image: fs.readFileSync(__dirname + '/smallimg') | ||
), 0) | ||
}); | ||
}) | ||
@@ -168,9 +184,9 @@ it('create USLT frame', function() { | ||
} | ||
}; | ||
} | ||
assert.equal(Buffer.compare( | ||
assert.strictEqual(Buffer.compare( | ||
NodeID3.create(tags), | ||
Buffer.from('4944330300000000006E55534C5400000064000001646575FFFE48006100690077007300E400E40023000000FFFE610073006B00640068002000610073006800640020006F006C00610068007300200065006C006F0077007A00200064006C006F0075006100690073006800200064006B0061006A006800', 'hex') | ||
), 0); | ||
}); | ||
), 0) | ||
}) | ||
@@ -184,13 +200,13 @@ it('create COMM frame', function() { | ||
} | ||
}; | ||
let frameBuf = Buffer.from('4944330300000000006E434F4D4D00000064000001646575FFFE48006100690077007300E400E40023000000FFFE610073006B00640068002000610073006800640020006F006C00610068007300200065006C006F0077007A00200064006C006F0075006100690073006800200064006B0061006A006800', 'hex'); | ||
} | ||
let frameBuf = Buffer.from('4944330300000000006E434F4D4D00000064000001646575FFFE48006100690077007300E400E40023000000FFFE610073006B00640068002000610073006800640020006F006C00610068007300200065006C006F0077007A00200064006C006F0075006100690073006800200064006B0061006A006800', 'hex') | ||
assert.equal(Buffer.compare( | ||
assert.strictEqual(Buffer.compare( | ||
NodeID3.create(tags), | ||
frameBuf | ||
), 0); | ||
}); | ||
), 0) | ||
}) | ||
it('create POPM frame', function() { | ||
let frameBuf = Buffer.from('49443303000000000020504F504D0000001600006D61696C406578616D706C652E636F6D00C00000000C', 'hex'); | ||
let frameBuf = Buffer.from('49443303000000000020504F504D0000001600006D61696C406578616D706C652E636F6D00C00000000C', 'hex') | ||
const tags = { | ||
@@ -202,12 +218,12 @@ popularimeter: { | ||
} | ||
}; | ||
} | ||
assert.equal(Buffer.compare( | ||
assert.strictEqual(Buffer.compare( | ||
NodeID3.create(tags), | ||
frameBuf | ||
), 0); | ||
}); | ||
), 0) | ||
}) | ||
it('create PRIV frame', function() { | ||
let frameBuf = Buffer.from('4944330300000000003250524956000000140000416243006173646F61687764696F686177646177505249560000000A000041624353535300010205', 'hex'); | ||
let frameBuf = Buffer.from('4944330300000000003250524956000000140000416243006173646F61687764696F686177646177505249560000000A000041624353535300010205', 'hex') | ||
const tags = { PRIV: [{ | ||
@@ -220,12 +236,12 @@ ownerIdentifier: "AbC", | ||
}] | ||
}; | ||
} | ||
assert.deepEqual( | ||
assert.deepStrictEqual( | ||
NodeID3.create(tags), | ||
frameBuf | ||
); | ||
}); | ||
) | ||
}) | ||
it('create CHAP frame', function() { | ||
let frameBuf = Buffer.from('494433030000000000534348415000000049000048657921000000138800001F400000007B000001C8544954320000000F000001FFFE6100620063006400650066005450453100000011000001FFFE61006B0073006800640061007300', 'hex'); | ||
let frameBuf = Buffer.from('494433030000000000534348415000000049000048657921000000138800001F400000007B000001C8544954320000000F000001FFFE6100620063006400650066005450453100000011000001FFFE61006B0073006800640061007300', 'hex') | ||
const tags = { CHAP: [{ | ||
@@ -241,42 +257,70 @@ elementID: "Hey!", //THIS MUST BE UNIQUE! | ||
} | ||
}]}; | ||
}]} | ||
assert.deepEqual( | ||
assert.deepStrictEqual( | ||
NodeID3.create(tags), | ||
frameBuf | ||
); | ||
}); | ||
}); | ||
) | ||
}) | ||
it('create WXXX frame', function() { | ||
const frameBuf = Buffer.from('4944330300000000002c5758585800000022000001fffe61006200630064002300000068747470733a2f2f6578616d706c652e636f6d', 'hex') | ||
const tags = { | ||
userDefinedUrl: [{ | ||
description: 'abcd#', | ||
url: 'https://example.com' | ||
}] | ||
} | ||
assert.deepStrictEqual( | ||
NodeID3.create(tags), | ||
frameBuf | ||
) | ||
}) | ||
it('create URL frame', function() { | ||
const frameBuf = Buffer.from('4944330300000000003b57434f4d00000013000068747470733a2f2f6578616d706c652e636f6d574f414600000014000068747470733a2f2f6578616d706c65322e636f6d', 'hex') | ||
const tags = { | ||
commercialUrl: ['https://example.com'], | ||
fileUrl: 'https://example2.com' | ||
} | ||
assert.deepStrictEqual( | ||
NodeID3.create(tags), | ||
frameBuf | ||
) | ||
}) | ||
}) | ||
describe('#write()', function() { | ||
it('sync not existing filepath', function() { | ||
assert.throws(NodeID3.write.bind({}, './hopefullydoesnotexist.mp3'), Error); | ||
}); | ||
assert.throws(NodeID3.write.bind({}, './hopefullydoesnotexist.mp3'), Error) | ||
}) | ||
it('async not existing filepath', function() { | ||
NodeID3.write({}, './hopefullydoesnotexist.mp3', function(err) { | ||
if(!(err instanceof Error)) { | ||
assert.fail("No error thrown on non-existing filepath"); | ||
assert.fail("No error thrown on non-existing filepath") | ||
} | ||
}); | ||
}); | ||
}) | ||
}) | ||
let buffer = Buffer.from([0x02, 0x06, 0x12, 0x22]); | ||
let tags = {title: "abc"}; | ||
let filepath = './testfile.mp3'; | ||
let buffer = Buffer.from([0x02, 0x06, 0x12, 0x22]) | ||
let tags = {title: "abc"} | ||
let filepath = './testfile.mp3' | ||
it('sync write file without id3 tag', function() { | ||
fs.writeFileSync(filepath, buffer, 'binary'); | ||
NodeID3.write(tags, filepath); | ||
let newFileBuffer = fs.readFileSync(filepath); | ||
fs.unlinkSync(filepath); | ||
assert.equal(Buffer.compare( | ||
fs.writeFileSync(filepath, buffer, 'binary') | ||
NodeID3.write(tags, filepath) | ||
let newFileBuffer = fs.readFileSync(filepath) | ||
fs.unlinkSync(filepath) | ||
assert.strictEqual(Buffer.compare( | ||
newFileBuffer, | ||
Buffer.concat([NodeID3.create(tags), buffer]) | ||
), 0); | ||
}); | ||
), 0) | ||
}) | ||
it('async write file without id3 tag', function(done) { | ||
fs.writeFileSync(filepath, buffer, 'binary'); | ||
fs.writeFileSync(filepath, buffer, 'binary') | ||
NodeID3.write(tags, filepath, function() { | ||
let newFileBuffer = fs.readFileSync(filepath); | ||
fs.unlinkSync(filepath); | ||
let newFileBuffer = fs.readFileSync(filepath) | ||
fs.unlinkSync(filepath) | ||
if(Buffer.compare( | ||
@@ -286,27 +330,27 @@ newFileBuffer, | ||
) === 0) { | ||
done(); | ||
done() | ||
} else { | ||
done(new Error("buffer not the same")) | ||
} | ||
}); | ||
}); | ||
}) | ||
}) | ||
let bufferWithTag = Buffer.concat([NodeID3.create(tags), buffer]); | ||
tags = {album: "ix123"}; | ||
let bufferWithTag = Buffer.concat([NodeID3.create(tags), buffer]) | ||
tags = {album: "ix123"} | ||
it('sync write file with id3 tag', function() { | ||
fs.writeFileSync(filepath, bufferWithTag, 'binary'); | ||
NodeID3.write(tags, filepath); | ||
let newFileBuffer = fs.readFileSync(filepath); | ||
fs.unlinkSync(filepath); | ||
assert.equal(Buffer.compare( | ||
fs.writeFileSync(filepath, bufferWithTag, 'binary') | ||
NodeID3.write(tags, filepath) | ||
let newFileBuffer = fs.readFileSync(filepath) | ||
fs.unlinkSync(filepath) | ||
assert.strictEqual(Buffer.compare( | ||
newFileBuffer, | ||
Buffer.concat([NodeID3.create(tags), buffer]) | ||
), 0); | ||
}); | ||
), 0) | ||
}) | ||
it('async write file with id3 tag', function(done) { | ||
fs.writeFileSync(filepath, bufferWithTag, 'binary'); | ||
fs.writeFileSync(filepath, bufferWithTag, 'binary') | ||
NodeID3.write(tags, filepath, function() { | ||
let newFileBuffer = fs.readFileSync(filepath); | ||
fs.unlinkSync(filepath); | ||
let newFileBuffer = fs.readFileSync(filepath) | ||
fs.unlinkSync(filepath) | ||
if(Buffer.compare( | ||
@@ -316,67 +360,67 @@ newFileBuffer, | ||
) === 0) { | ||
done(); | ||
done() | ||
} else { | ||
done(new Error("file written incorrectly")); | ||
done(new Error("file written incorrectly")) | ||
} | ||
}); | ||
}); | ||
}); | ||
}) | ||
}) | ||
}) | ||
describe('#read()', function() { | ||
it('read empty id3 tag', function() { | ||
let frame = NodeID3.create({}); | ||
assert.deepEqual( | ||
let frame = NodeID3.create({}) | ||
assert.deepStrictEqual( | ||
NodeID3.read(frame), | ||
{raw: {}} | ||
); | ||
}); | ||
) | ||
}) | ||
it('read text frames id3 tag', function() { | ||
let frame = NodeID3.create({ title: "asdfghjÄÖP", album: "naBGZwssg" }); | ||
assert.deepEqual( | ||
let frame = NodeID3.create({ title: "asdfghjÄÖP", album: "naBGZwssg" }) | ||
assert.deepStrictEqual( | ||
NodeID3.read(frame), | ||
{ title: "asdfghjÄÖP", album: "naBGZwssg", raw: { TIT2: "asdfghjÄÖP", TALB: "naBGZwssg" }} | ||
); | ||
}); | ||
) | ||
}) | ||
it('read tag with broken frame', function() { | ||
let frame = NodeID3.create({ title: "asdfghjÄÖP", album: "naBGZwssg" }); | ||
frame[10] = 0x99; | ||
assert.deepEqual( | ||
let frame = NodeID3.create({ title: "asdfghjÄÖP", album: "naBGZwssg" }) | ||
frame[10] = 0x99 | ||
assert.deepStrictEqual( | ||
NodeID3.read(frame), | ||
{ album: "naBGZwssg", raw: { TALB: "naBGZwssg" }} | ||
); | ||
}); | ||
) | ||
}) | ||
/*it('read tag with broken tag', function() { | ||
let frame = NodeID3.create({ title: "asdfghjÄÖP", album: "naBGZwssg" }); | ||
frame[3] = 0x99; | ||
assert.deepEqual( | ||
let frame = NodeID3.create({ title: "asdfghjÄÖP", album: "naBGZwssg" }) | ||
frame[3] = 0x99 | ||
assert.deepStrictEqual( | ||
NodeID3.read(frame), | ||
{ raw: { }} | ||
); | ||
});*/ | ||
) | ||
})*/ | ||
it('read tag with bigger size', function() { | ||
let frame = NodeID3.create({ title: "asdfghjÄÖP", album: "naBGZwssg" }); | ||
frame[9] += 100; | ||
assert.deepEqual( | ||
let frame = NodeID3.create({ title: "asdfghjÄÖP", album: "naBGZwssg" }) | ||
frame[9] += 100 | ||
assert.deepStrictEqual( | ||
NodeID3.read(frame), | ||
{ title: "asdfghjÄÖP", album: "naBGZwssg", raw: { TIT2: "asdfghjÄÖP", TALB: "naBGZwssg" }} | ||
); | ||
}); | ||
) | ||
}) | ||
it('read tag with smaller size', function() { | ||
let frame = NodeID3.create({ title: "asdfghjÄÖP", album: "naBGZwssg" }); | ||
frame[9] -= 25; | ||
assert.deepEqual( | ||
let frame = NodeID3.create({ title: "asdfghjÄÖP", album: "naBGZwssg" }) | ||
frame[9] -= 25 | ||
assert.deepStrictEqual( | ||
NodeID3.read(frame), | ||
{ title: "asdfghjÄÖP", raw: { TIT2: "asdfghjÄÖP" }} | ||
); | ||
}); | ||
) | ||
}) | ||
it('read TXXX frame', function() { | ||
let tags = { userDefinedText: {description: "abc", value: "deg"} }; | ||
let frame = NodeID3.create(tags); | ||
assert.deepEqual( | ||
let tags = { userDefinedText: {description: "abc", value: "deg"} } | ||
let frame = NodeID3.create(tags) | ||
assert.deepStrictEqual( | ||
NodeID3.read(frame), | ||
@@ -389,22 +433,22 @@ { | ||
} | ||
); | ||
}); | ||
) | ||
}) | ||
it('read TXXX array frame', function() { | ||
let tags = { userDefinedText: [{description: "abc", value: "deg"}, {description: "abcd", value: "efgh"}] }; | ||
let frame = NodeID3.create(tags); | ||
assert.deepEqual( | ||
let tags = { userDefinedText: [{description: "abc", value: "deg"}, {description: "abcd", value: "efgh"}] } | ||
let frame = NodeID3.create(tags) | ||
assert.deepStrictEqual( | ||
NodeID3.read(frame), | ||
{ | ||
userDefinedText: [tags.userDefinedText[1], tags.userDefinedText[0]], | ||
userDefinedText: tags.userDefinedText, | ||
raw: { | ||
TXXX: [tags.userDefinedText[1], tags.userDefinedText[0]] | ||
TXXX: tags.userDefinedText | ||
} | ||
} | ||
); | ||
}); | ||
) | ||
}) | ||
it('read APIC frame', function() { | ||
let withAll = Buffer.from("4944330300000000101C4150494300000016000000696D6167652F6A7065670003617364660061626364", "hex"); | ||
let noDesc = Buffer.from("494433030000000000264150494300000012000000696D6167652F6A70656700030061626364", "hex"); | ||
let withAll = Buffer.from("4944330300000000101C4150494300000016000000696D6167652F6A7065670003617364660061626364", "hex") | ||
let noDesc = Buffer.from("494433030000000000264150494300000012000000696D6167652F6A70656700030061626364", "hex") | ||
let obj = { | ||
@@ -415,18 +459,18 @@ description: "asdf", | ||
type: { id: 3, name: "front cover" } | ||
}; | ||
} | ||
assert.deepEqual( | ||
assert.deepStrictEqual( | ||
NodeID3.read(withAll).image, | ||
obj | ||
); | ||
) | ||
obj.description = undefined; | ||
assert.deepEqual( | ||
obj.description = undefined | ||
assert.deepStrictEqual( | ||
NodeID3.read(noDesc).image, | ||
obj | ||
); | ||
}); | ||
) | ||
}) | ||
it('read USLT frame', function() { | ||
let frameBuf = Buffer.from('4944330300000000006E55534C5400000064000001646575FFFE48006100690077007300E400E40023000000FFFE610073006B00640068002000610073006800640020006F006C00610068007300200065006C006F0077007A00200064006C006F0075006100690073006800200064006B0061006A006800', 'hex'); | ||
let frameBuf = Buffer.from('4944330300000000006E55534C5400000064000001646575FFFE48006100690077007300E400E40023000000FFFE610073006B00640068002000610073006800640020006F006C00610068007300200065006C006F0077007A00200064006C006F0075006100690073006800200064006B0061006A006800', 'hex') | ||
const unsynchronisedLyrics = { | ||
@@ -436,12 +480,12 @@ language: "deu", | ||
text: "askdh ashd olahs elowz dlouaish dkajh" | ||
}; | ||
} | ||
assert.deepEqual( | ||
assert.deepStrictEqual( | ||
NodeID3.read(frameBuf).unsynchronisedLyrics, | ||
unsynchronisedLyrics | ||
); | ||
}); | ||
) | ||
}) | ||
it('read COMM frame', function() { | ||
let frameBuf = Buffer.from('4944330300000000006E434F4D4D00000064000001646575FFFE48006100690077007300E400E40023000000FFFE610073006B00640068002000610073006800640020006F006C00610068007300200065006C006F0077007A00200064006C006F0075006100690073006800200064006B0061006A006800', 'hex'); | ||
let frameBuf = Buffer.from('4944330300000000006E434F4D4D00000064000001646575FFFE48006100690077007300E400E40023000000FFFE610073006B00640068002000610073006800640020006F006C00610068007300200065006C006F0077007A00200064006C006F0075006100690073006800200064006B0061006A006800', 'hex') | ||
const comment = { | ||
@@ -451,12 +495,12 @@ language: "deu", | ||
text: "askdh ashd olahs elowz dlouaish dkajh" | ||
}; | ||
} | ||
assert.deepEqual( | ||
assert.deepStrictEqual( | ||
NodeID3.read(frameBuf).comment, | ||
comment | ||
); | ||
}); | ||
) | ||
}) | ||
it('read POPM frame', function() { | ||
let frameBuf = Buffer.from('49443303000000000020504F504D0000001600006D61696C406578616D706C652E636F6D00C00000000C', 'hex'); | ||
let frameBuf = Buffer.from('49443303000000000020504F504D0000001600006D61696C406578616D706C652E636F6D00C00000000C', 'hex') | ||
const popularimeter = { | ||
@@ -466,12 +510,12 @@ email: "mail@example.com", | ||
counter: 12 | ||
}; | ||
} | ||
assert.deepEqual( | ||
assert.deepStrictEqual( | ||
NodeID3.read(frameBuf).popularimeter, | ||
popularimeter | ||
); | ||
}); | ||
) | ||
}) | ||
it('read PRIV frame', function() { | ||
let frameBuf = Buffer.from('4944330300000000003250524956000000140000416243006173646F61687764696F686177646177505249560000000A000041624353535300010205', 'hex'); | ||
let frameBuf = Buffer.from('4944330300000000003250524956000000140000416243006173646F61687764696F686177646177505249560000000A000041624353535300010205', 'hex') | ||
const priv = [{ | ||
@@ -483,12 +527,12 @@ ownerIdentifier: "AbC", | ||
data: Buffer.from([0x01, 0x02, 0x05]) | ||
}]; | ||
}] | ||
assert.deepEqual( | ||
assert.deepStrictEqual( | ||
NodeID3.read(frameBuf).private, | ||
priv | ||
); | ||
}); | ||
) | ||
}) | ||
it('read CHAP frame', function() { | ||
let frameBuf = Buffer.from('494433030000000000534348415000000049000048657921000000138800001F400000007B000001C8544954320000000F000001FFFE6100620063006400650066005450453100000011000001FFFE61006B0073006800640061007300', 'hex'); | ||
let frameBuf = Buffer.from('494433030000000000534348415000000049000048657921000000138800001F400000007B000001C8544954320000000F000001FFFE6100620063006400650066005450453100000011000001FFFE61006B0073006800640061007300', 'hex') | ||
const chap = [{ | ||
@@ -508,36 +552,126 @@ elementID: "Hey!", //THIS MUST BE UNIQUE! | ||
} | ||
}]; | ||
}] | ||
assert.deepEqual( | ||
assert.deepStrictEqual( | ||
NodeID3.read(frameBuf).chapter, | ||
chap | ||
); | ||
}); | ||
}); | ||
}); | ||
) | ||
}) | ||
it('read WXXX frame', function() { | ||
const frameBuf = Buffer.from('4944330300000000002c5758585800000022000001fffe61006200630064002300000068747470733a2f2f6578616d706c652e636f6d', 'hex') | ||
const userDefinedUrl = [{ | ||
description: 'abcd#', | ||
url: 'https://example.com' | ||
}] | ||
assert.deepStrictEqual( | ||
NodeID3.read(frameBuf).userDefinedUrl, | ||
userDefinedUrl | ||
) | ||
}) | ||
it('read URL frame', function() { | ||
const frameBuf = Buffer.from('4944330300000000003d57434f4d0000001400000068747470733a2f2f6578616d706c652e636f6d574f41460000001500000068747470733a2f2f6578616d706c65322e636f6d', 'hex') | ||
const commercialUrl = ['https://example.com'] | ||
const fileUrl = 'https://example2.com' | ||
assert.deepStrictEqual( | ||
NodeID3.read(frameBuf).commercialUrl, | ||
commercialUrl | ||
) | ||
assert.deepStrictEqual( | ||
NodeID3.read(frameBuf).fileUrl, | ||
fileUrl | ||
) | ||
}) | ||
it('read exclude', function() { | ||
let tags = { | ||
TIT2: "abcdeÜ看板かんばん", | ||
album: "nasÖÄkdnasd", | ||
year: "1990" | ||
} | ||
const buffer = NodeID3.create(tags) | ||
let read = NodeID3.read(buffer, { exclude: ['TIT2'] }) | ||
delete read.raw | ||
delete tags.TIT2 | ||
assert.deepStrictEqual( | ||
read, | ||
tags | ||
) | ||
}) | ||
it('read include', function() { | ||
let tags = { | ||
title: "abcdeÜ看板かんばん", | ||
album: "nasÖÄkdnasd", | ||
year: "1990" | ||
} | ||
const buffer = NodeID3.create(tags) | ||
let read = NodeID3.read(buffer, { include: ['TALB', 'TIT2'] }) | ||
delete read.raw | ||
delete tags.year | ||
assert.deepStrictEqual( | ||
read, | ||
tags | ||
) | ||
}) | ||
it('onlyRaw', function() { | ||
let tags = { | ||
TIT2: "abcdeÜ看板かんばん", | ||
TALB: "nasÖÄkdnasd" | ||
} | ||
const buffer = NodeID3.create(tags) | ||
let read = NodeID3.read(buffer, { onlyRaw: true }) | ||
assert.deepStrictEqual( | ||
read, | ||
tags | ||
) | ||
}) | ||
it('noRaw', function() { | ||
let tags = { | ||
title: "abcdeÜ看板かんばん", | ||
album: "nasÖÄkdnasd" | ||
} | ||
const buffer = NodeID3.create(tags) | ||
let read = NodeID3.read(buffer, { noRaw: true }) | ||
assert.deepStrictEqual( | ||
read, | ||
tags | ||
) | ||
}) | ||
}) | ||
}) | ||
describe('ID3 helper functions', function () { | ||
describe('#removeTagsFromBuffer()', function () { | ||
it('no tags in buffer', function () { | ||
let emptyBuffer = Buffer.from([0x12, 0x04, 0x05, 0x01, 0x76, 0x27, 0x76, 0x27, 0x76, 0x27, 0x76, 0x27]); | ||
assert.equal(Buffer.compare( | ||
let emptyBuffer = Buffer.from([0x12, 0x04, 0x05, 0x01, 0x76, 0x27, 0x76, 0x27, 0x76, 0x27, 0x76, 0x27]) | ||
assert.strictEqual(Buffer.compare( | ||
emptyBuffer, | ||
NodeID3.removeTagsFromBuffer(emptyBuffer) | ||
), 0); | ||
}); | ||
), 0) | ||
}) | ||
it('tags at start', function () { | ||
let buffer = Buffer.from([0x22, 0x73, 0x72]); | ||
let buffer = Buffer.from([0x22, 0x73, 0x72]) | ||
let bufferWithID3 = Buffer.concat([ | ||
NodeID3.create({title: "abc"}), | ||
buffer | ||
]); | ||
assert.equal(Buffer.compare( | ||
]) | ||
assert.strictEqual(Buffer.compare( | ||
NodeID3.removeTagsFromBuffer(bufferWithID3), | ||
buffer | ||
), 0); | ||
}); | ||
), 0) | ||
}) | ||
it('tags in middle/end', function () { | ||
let buffer = Buffer.from([0x22, 0x73, 0x72]); | ||
let buffer = Buffer.from([0x22, 0x73, 0x72]) | ||
let bufferWithID3 = Buffer.concat([ | ||
@@ -547,15 +681,250 @@ buffer, | ||
buffer | ||
]); | ||
assert.equal(Buffer.compare( | ||
]) | ||
assert.strictEqual(Buffer.compare( | ||
NodeID3.removeTagsFromBuffer(bufferWithID3), | ||
Buffer.concat([buffer, buffer]) | ||
), 0); | ||
}); | ||
}); | ||
}); | ||
), 0) | ||
}) | ||
}) | ||
}) | ||
const nodeTagsFull = { | ||
title: 'abc', | ||
album: '人物asfjas as das \\', | ||
comment: { | ||
language: 'en3', | ||
shortText: 'asd物f', | ||
text: 1337 | ||
}, | ||
unsynchronisedLyrics: { | ||
language: 'e33', | ||
text: 'asd物f asd物f asd物f' | ||
}, | ||
userDefinedText: [ | ||
{ | ||
description: "txxx name物", | ||
value: "TXXX value text" | ||
}, { | ||
description: "txxx name 2", | ||
value: "TXXX value text 2" | ||
} | ||
], | ||
image: { | ||
mime: "jpeg", | ||
description: 'asd物f asd物f asd物f', | ||
imageBuffer: Buffer.from([0x02, 0x27, 0x17, 0x99]) | ||
}, | ||
popularimeter: { | ||
email: 'test@example.com', | ||
rating: 192, | ||
counter: 12 | ||
}, | ||
private: [{ | ||
ownerIdentifier: "AbC", | ||
data: "asdoahwdiohawdaw" | ||
}, { | ||
ownerIdentifier: "AbCSSS", | ||
data: Buffer.from([0x01, 0x02, 0x05]) | ||
}], | ||
chapter: [{ | ||
elementID: "Hey!", // THIS MUST BE UNIQUE! | ||
startTimeMs: 5000, | ||
endTimeMs: 8000, | ||
startOffsetBytes: 123, // OPTIONAL! | ||
endOffsetBytes: 456, // OPTIONAL! | ||
tags: { // OPTIONAL | ||
title: "abcdef", | ||
artist: "akshdas" | ||
} | ||
}], | ||
tableOfContents: [{ | ||
elementID: "toc1", // THIS MUST BE UNIQUE! | ||
isOrdered: false, // OPTIONAL, tells a player etc. if elements are in a specific order | ||
elements: ['chap1'], // OPTIONAL but most likely needed, contains the chapter/tableOfContents elementIDs | ||
tags: { // OPTIONAL | ||
title: "abcdef" | ||
} | ||
}], | ||
commercialUrl: ["commercialurl.com"], | ||
userDefinedUrl: [{ | ||
description: "URL description物", | ||
url: "https://example.com/" | ||
}] | ||
} | ||
const nodeTagsMissingValues = { | ||
comment: { | ||
language: 'en3', | ||
text: 1337 | ||
}, | ||
userDefinedText: [ | ||
{ | ||
value: "TXXX value text" | ||
}, { | ||
value: "TXXX value text 2" | ||
} | ||
], | ||
image: { | ||
mime: "jpeg", | ||
imageBuffer: Buffer.from([0x02, 0x27, 0x17, 0x99]) | ||
}, | ||
popularimeter: { | ||
email: 'test@example.com', | ||
counter: 12 | ||
}, | ||
private: [{ | ||
data: "asdoahwdiohawdaw" | ||
}, { | ||
data: Buffer.from([0x01, 0x02, 0x05]) | ||
}], | ||
chapter: [{ | ||
elementID: "Hey!", // THIS MUST BE UNIQUE! | ||
startTimeMs: 5000, | ||
endTimeMs: 8000 | ||
}], | ||
tableOfContents: [{ | ||
elementID: "toc1", // THIS MUST BE UNIQUE! | ||
elements: ['chap1'] | ||
}], | ||
} | ||
describe('Cross tests jsmediatags', function() { | ||
it('write full', function() { | ||
jsmediatags.read(NodeID3.create(nodeTagsFull), { | ||
onSuccess: (tag) => { | ||
const tags = tag.tags | ||
assert.strictEqual(tags.TIT2.data, nodeTagsFull.title) | ||
assert.strictEqual(tags.TALB.data, nodeTagsFull.album) | ||
assert.deepStrictEqual({ language: tags.COMM.data.language, shortText: tags.COMM.data.short_description, text: parseInt(tags.COMM.data.text) }, nodeTagsFull.comment) | ||
assert.deepStrictEqual({ language: tags.USLT.data.language, text: tags.USLT.data.lyrics }, nodeTagsFull.unsynchronisedLyrics) | ||
expect(tags.TXXX.map((t) => { | ||
return { | ||
description: t.data.user_description, | ||
value: t.data.data | ||
} | ||
})).to.have.deep.members(nodeTagsFull.userDefinedText) | ||
assert.deepStrictEqual({ | ||
mime: tags.APIC.data.format, | ||
description: tags.APIC.data.description, | ||
imageBuffer: Buffer.from(tags.APIC.data.data) | ||
}, nodeTagsFull.image) | ||
/* POPM seems broken in jsmediatags, data is null but tag looks correct */ | ||
/* PRIV seems broken in jsmediatags, data is null but tag looks correct */ | ||
assert.deepStrictEqual({ | ||
elementID: nodeTagsFull.chapter[0].elementID, | ||
startTimeMs: tags.CHAP.data.startTime, | ||
endTimeMs: tags.CHAP.data.endTime, | ||
startOffsetBytes: tags.CHAP.data.startOffset, | ||
endOffsetBytes: tags.CHAP.data.endOffset, | ||
tags: { | ||
title: tags.CHAP.data.subFrames.TIT2.data, | ||
artist: tags.CHAP.data.subFrames.TPE1.data | ||
} | ||
}, nodeTagsFull.chapter[0]) | ||
assert.deepStrictEqual({ | ||
elementID:nodeTagsFull.tableOfContents[0].elementID, | ||
isOrdered: false, | ||
elements: tags.CTOC.data.childElementIds, | ||
tags: { | ||
title: tags.CTOC.data.subFrames.TIT2.data | ||
} | ||
}, nodeTagsFull.tableOfContents[0]) | ||
assert.strictEqual(tags.WCOM.data, nodeTagsFull.commercialUrl[0]) | ||
assert.deepStrictEqual({ | ||
description: tags.WXXX.data.user_description, | ||
url: nodeTagsFull.userDefinedUrl[0].url /* The URL is always encoded with ISO-8859-1 => jsmediatags reads as UTF-16, can't use here*/ | ||
}, nodeTagsFull.userDefinedUrl[0]) | ||
}, | ||
onError: function(error) { | ||
throw error | ||
} | ||
}) | ||
}) | ||
it('write with missing values', function() { | ||
jsmediatags.read(NodeID3.create(nodeTagsMissingValues), { | ||
onSuccess: (tag) => { | ||
const tags = tag.tags | ||
assert.deepStrictEqual({ language: tags.COMM.data.language, text: parseInt(tags.COMM.data.text) }, nodeTagsMissingValues.comment) | ||
assert.strictEqual(tags.COMM.data.short_description, '') | ||
expect(tags.TXXX.map((t) => { | ||
return { | ||
value: t.data.data | ||
} | ||
})).to.have.deep.members(nodeTagsMissingValues.userDefinedText) | ||
tags.TXXX.forEach((t) => { | ||
assert.strictEqual(t.data.user_description, '') | ||
}) | ||
assert.deepStrictEqual({ | ||
mime: tags.APIC.data.format, | ||
imageBuffer: Buffer.from(tags.APIC.data.data) | ||
}, nodeTagsMissingValues.image) | ||
assert.strictEqual(tags.APIC.data.description, '') | ||
/* POPM seems broken in jsmediatags, data is null but tag looks correct */ | ||
/* PRIV seems broken in jsmediatags, data is null but tag looks correct */ | ||
assert.deepStrictEqual({ | ||
elementID: nodeTagsMissingValues.chapter[0].elementID, | ||
startTimeMs: tags.CHAP.data.startTime, | ||
endTimeMs: tags.CHAP.data.endTime | ||
}, nodeTagsMissingValues.chapter[0]) | ||
assert.deepStrictEqual(tags.CHAP.data.subFrames, {}) | ||
assert.strictEqual(tags.CHAP.data.startOffset, 0xFFFFFFFF) | ||
assert.strictEqual(tags.CHAP.data.endOffset, 0xFFFFFFFF) | ||
assert.deepStrictEqual({ | ||
elementID: nodeTagsMissingValues.tableOfContents[0].elementID, | ||
elements: tags.CTOC.data.childElementIds, | ||
}, nodeTagsMissingValues.tableOfContents[0]) | ||
assert.deepStrictEqual(tags.CTOC.data.subFrames, {}) | ||
assert.strictEqual(tags.CTOC.data.ordered, false) | ||
}, | ||
onError: function(error) { | ||
throw error | ||
} | ||
}) | ||
}) | ||
it('read from full self-created tags', function() { | ||
const tagsBuffer = NodeID3.create(nodeTagsFull) | ||
let read = NodeID3.read(tagsBuffer) | ||
delete read.raw | ||
delete read.chapter[0].tags.raw | ||
delete read.tableOfContents[0].tags.raw | ||
read.comment.text = parseInt(read.comment.text) | ||
delete read.image.type | ||
read.private[0].data = read.private[0].data.toString() | ||
if(read.unsynchronisedLyrics.shortText === undefined) delete read.unsynchronisedLyrics.shortText | ||
assert.deepStrictEqual(nodeTagsFull, read) | ||
}) | ||
it('read from missing values self-created tags', function() { | ||
const tagsBuffer = NodeID3.create(nodeTagsMissingValues) | ||
let read = NodeID3.read(tagsBuffer) | ||
delete read.raw | ||
assert.deepStrictEqual(read.chapter[0].tags.raw, {}) | ||
delete read.chapter[0].tags | ||
read.comment.text = parseInt(read.comment.text) | ||
if(read.comment.shortText === undefined) delete read.comment.shortText | ||
if(read.image.description === undefined) delete read.image.description | ||
delete read.image.type | ||
assert.strictEqual(read.popularimeter.rating, 0) | ||
delete read.popularimeter.rating | ||
read.private[0].data = read.private[0].data.toString() | ||
if(read.private[0].ownerIdentifier === undefined) delete read.private[0].ownerIdentifier | ||
if(read.private[1].ownerIdentifier === undefined) delete read.private[1].ownerIdentifier | ||
assert.strictEqual(read.tableOfContents[0].isOrdered, false) | ||
assert.deepStrictEqual(read.tableOfContents[0].tags.raw, {}) | ||
delete read.tableOfContents[0].tags | ||
delete read.tableOfContents[0].isOrdered | ||
if(read.userDefinedText[0].description === undefined) delete read.userDefinedText[0].description | ||
if(read.userDefinedText[1].description === undefined) delete read.userDefinedText[1].description | ||
assert.deepStrictEqual(nodeTagsMissingValues, read) | ||
}) | ||
}) | ||
function sizeToBuffer(totalSize) { | ||
let buffer = Buffer.alloc(4); | ||
buffer.writeUInt32BE(totalSize); | ||
return buffer; | ||
let buffer = Buffer.alloc(4) | ||
buffer.writeUInt32BE(totalSize) | ||
return buffer | ||
} |
Sorry, the diff of this file is not supported yet
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
118012
16
2543
284
3
4
1
+ Addediconv-lite@0.6.2(transitive)
- Removediconv-lite@0.5.1(transitive)
Updatediconv-lite@0.6.2