Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

node-id3

Package Overview
Dependencies
Maintainers
1
Versions
38
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

node-id3 - npm Package Compare versions

Comparing version 0.1.21 to 0.2.0

CHANGELOG.md

6

index.d.ts

@@ -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"
}
}
# 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:

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc