node-id3
Version:
Pure JavaScript ID3v2 Tag writer and reader
236 lines (214 loc) • 7.69 kB
JavaScript
const iconv = require('iconv-lite')
const ID3Definitions = require('./ID3Definitions')
const ENCODINGS = [
'ISO-8859-1', 'UTF-16', 'UTF-16BE', 'UTF-8'
]
module.exports.SplitBuffer = class SplitBuffer {
constructor(value = null, remainder = null) {
this.value = value
this.remainder = remainder
}
}
/**
* Expects a buffer containing a string at the beginning that is terminated by a \0 character.
* Returns a split buffer containing the bytes before and after null termination.
*/
module.exports.splitNullTerminatedBuffer = function(buffer, encodingByte = 0x00) {
// UTF-16/BE always uses two bytes per character.
// \0 is therefore encoded as [0x00, 0x00] instead of just [0x00].
// We'll do a sliding window search, window size depends on encoding.
const charSize = [0x01, 0x02].includes(encodingByte) ? 2 : 1
for(let pos = 0; pos + charSize - 1 < buffer.length; pos += charSize) {
if(buffer.readUIntBE(pos, charSize) === 0) {
return new this.SplitBuffer(
buffer.subarray(0, pos),
buffer.subarray(pos + charSize)
)
}
}
return new this.SplitBuffer(null, buffer.subarray(0))
}
module.exports.terminationBuffer = function(encodingByte = 0x00) {
if(encodingByte === 0x01 || encodingByte === 0x02) {
return Buffer.alloc(2, 0x00)
}
return Buffer.alloc(1, 0x00)
}
module.exports.encodingFromStringOrByte = function(encoding) {
if(ENCODINGS.indexOf(encoding) !== -1) {
return encoding
} else if(encoding > -1 && encoding < ENCODINGS.length) {
encoding = ENCODINGS[encoding]
} else {
encoding = ENCODINGS[0]
}
return encoding
}
module.exports.stringToEncodedBuffer = function(str, encodingByte) {
return iconv.encode(str, this.encodingFromStringOrByte(encodingByte))
}
module.exports.bufferToDecodedString = function(buffer, encodingByte) {
return iconv.decode(buffer, this.encodingFromStringOrByte(encodingByte)).replace(/\0/g, '')
}
module.exports.getSpecOptions = function(frameIdentifier) {
if(ID3Definitions.ID3_FRAME_OPTIONS[frameIdentifier]) {
return ID3Definitions.ID3_FRAME_OPTIONS[frameIdentifier]
}
return {}
}
module.exports.isValidID3Header = function(buffer) {
if(buffer.length < 10) {
return false
}
if(buffer.readUIntBE(0, 3) !== 0x494433) {
return false
}
if([0x02, 0x03, 0x04].indexOf(buffer[3]) === -1 || buffer[4] !== 0x00) {
return false
}
return this.isValidEncodedSize(buffer.slice(6, 10))
}
module.exports.getFramePosition = function(buffer) {
/* Search Buffer for valid ID3 frame */
let framePosition = -1
let frameHeaderValid = false
do {
framePosition = buffer.indexOf("ID3", framePosition + 1)
if(framePosition !== -1) {
/* It's possible that there is a "ID3" sequence without being an ID3 Frame,
* so we need to check for validity of the next 10 bytes
*/
frameHeaderValid = this.isValidID3Header(buffer.slice(framePosition, framePosition + 10))
}
} while (framePosition !== -1 && !frameHeaderValid)
if(!frameHeaderValid) {
return -1
}
return framePosition
}
/**
* @param {Buffer} encodedSize
* @return {boolean} Return if the header contains a valid encoded size
*/
module.exports.isValidEncodedSize = function(encodedSize) {
// The size must not have the bit 7 set
return ((
encodedSize[0] |
encodedSize[1] |
encodedSize[2] |
encodedSize[3]
) & 128) === 0
}
/**
* ID3 header size uses only 7 bits of a byte, bit shift is needed
* @param {number} size
* @return {Buffer} Return a Buffer of 4 bytes with the encoded size
*/
module.exports.encodeSize = function(size) {
const byte_3 = size & 0x7F
const byte_2 = (size >> 7) & 0x7F
const byte_1 = (size >> 14) & 0x7F
const byte_0 = (size >> 21) & 0x7F
return Buffer.from([byte_0, byte_1, byte_2, byte_3])
}
/**
* Decode the size encoded in the ID3 header
* @param {Buffer} encodedSize
* @return {number} Return the decoded size
*/
module.exports.decodeSize = function(encodedSize) {
return (
(encodedSize[0] << 21) +
(encodedSize[1] << 14) +
(encodedSize[2] << 7) +
encodedSize[3]
)
}
module.exports.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)
}
}
module.exports.parseTagHeaderFlags = function(header) {
if(!(header instanceof Buffer && header.length >= 10)) {
return {}
}
const version = header[3]
const flagsByte = header[5]
if(version === 3) {
return {
unsynchronisation: !!(flagsByte & 128),
extendedHeader: !!(flagsByte & 64),
experimentalIndicator: !!(flagsByte & 32)
}
}
if(version === 4) {
return {
unsynchronisation: !!(flagsByte & 128),
extendedHeader: !!(flagsByte & 64),
experimentalIndicator: !!(flagsByte & 32),
footerPresent: !!(flagsByte & 16)
}
}
return {}
}
module.exports.parseFrameHeaderFlags = function(header, ID3Version) {
if(!(header instanceof Buffer && header.length === 10)) {
return {}
}
const flagsFirstByte = header[8]
const flagsSecondByte = header[9]
if(ID3Version === 3) {
return {
tagAlterPreservation: !!(flagsFirstByte & 128),
fileAlterPreservation: !!(flagsFirstByte & 64),
readOnly: !!(flagsFirstByte & 32),
compression: !!(flagsSecondByte & 128),
encryption: !!(flagsSecondByte & 64),
groupingIdentity: !!(flagsSecondByte & 32),
dataLengthIndicator: !!(flagsSecondByte & 128)
}
}
if(ID3Version === 4) {
return {
tagAlterPreservation: !!(flagsFirstByte & 64),
fileAlterPreservation: !!(flagsFirstByte & 32),
readOnly: !!(flagsFirstByte & 16),
groupingIdentity: !!(flagsSecondByte & 64),
compression: !!(flagsSecondByte & 8),
encryption: !!(flagsSecondByte & 4),
unsynchronisation: !!(flagsSecondByte & 2),
dataLengthIndicator: !!(flagsSecondByte & 1)
}
}
return {}
}
module.exports.processUnsynchronisedBuffer = function(buffer) {
const newDataArr = []
if(buffer.length > 0) {
newDataArr.push(buffer[0])
}
for(let i = 1; i < buffer.length; i++) {
if(buffer[i - 1] === 0xFF && buffer[i] === 0x00)
continue
newDataArr.push(buffer[i])
}
return Buffer.from(newDataArr)
}
module.exports.getPictureMimeTypeFromBuffer = function(pictureBuffer) {
if (pictureBuffer.length > 3 && pictureBuffer.compare(Buffer.from([0xff, 0xd8, 0xff]), 0, 3, 0, 3) === 0) {
return "image/jpeg"
} else if (pictureBuffer > 8 && pictureBuffer.compare(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), 0, 8, 0, 8) === 0) {
return "image/png"
} else {
return null
}
}