node-id3
Version:
Pure JavaScript ID3v2 Tag writer and reader
332 lines (295 loc) • 9.83 kB
JavaScript
const fs = require('fs')
const ID3Definitions = require("./src/ID3Definitions")
const ID3Util = require('./src/ID3Util')
const ID3Helpers = require('./src/ID3Helpers')
const { isFunction, isString } = require('./src/util')
/*
** Used specification: http://id3.org/id3v2.3.0
*/
/**
* Checks and removes already written ID3-Frames from a buffer
* @param {Buffer} data
* @returns {boolean|Buffer}
*/
function removeTagsFromBuffer(data) {
const framePosition = ID3Util.getFramePosition(data)
if (framePosition === -1) {
return data
}
const encodedSize = data.slice(framePosition + 6, framePosition + 10)
if (!ID3Util.isValidEncodedSize(encodedSize)) {
return false
}
if (data.length >= framePosition + 10) {
const size = ID3Util.decodeSize(encodedSize)
return Buffer.concat([
data.slice(0, framePosition),
data.slice(framePosition + size + 10)
])
}
return data
}
function writeInBuffer(tags, buffer) {
buffer = removeTagsFromBuffer(buffer) || buffer
return Buffer.concat([tags, buffer])
}
function writeAsync(tags, filebuffer, fn) {
if(isString(filebuffer)) {
try {
fs.readFile(filebuffer, (error, data) => {
if(error) {
fn(error)
return
}
const newData = writeInBuffer(tags, data)
fs.writeFile(filebuffer, newData, 'binary', (error) => {
fn(error)
})
})
} catch(error) {
fn(error)
}
} else {
fn(null, writeInBuffer(tags, filebuffer))
}
}
function writeSync(tags, filebuffer) {
if(isString(filebuffer)) {
try {
const data = fs.readFileSync(filebuffer)
const newData = writeInBuffer(tags, data)
fs.writeFileSync(filebuffer, newData, 'binary')
return true
} catch(error) {
return error
}
}
return writeInBuffer(tags, filebuffer)
}
/**
* 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}
*/
function write(tags, filebuffer, fn) {
const completeTags = create(tags)
if(isFunction(fn)) {
return writeAsync(completeTags, filebuffer, fn)
}
return writeSync(completeTags, filebuffer)
}
/**
* 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}
*/
function create(tags, fn) {
const frames = ID3Helpers.createBufferFromTags(tags)
// Create ID3 header
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
ID3Util.encodeSize(frames.length).copy(header, 6)
const id3Data = Buffer.concat([header, frames])
if(isFunction(fn)) {
fn(id3Data)
return undefined
}
return id3Data
}
function readSync(filebuffer, options) {
if(isString(filebuffer)) {
filebuffer = fs.readFileSync(filebuffer)
}
return ID3Helpers.getTagsFromBuffer(filebuffer, options)
}
function readAsync(filebuffer, options, fn) {
if(isString(filebuffer)) {
fs.readFile(filebuffer, (error, data) => {
if(error) {
fn(error, null)
} else {
fn(null, ID3Helpers.getTagsFromBuffer(data, options))
}
})
} else {
fn(null, ID3Helpers.getTagsFromBuffer(filebuffer, options))
}
}
/**
* 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}
*/
function read(filebuffer, options, fn) {
if(!options || typeof options === 'function') {
fn = fn || options
options = {}
}
if(isFunction(fn)) {
return readAsync(filebuffer, options, fn)
}
return readSync(filebuffer, options)
}
/**
* 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 options - (optional) Object containing options
* @param fn - (optional) Function for async version
* @returns {boolean|Buffer|Error}
*/
function update(tags, filebuffer, options, fn) {
if(!options || typeof options === 'function') {
fn = fn || options
options = {}
}
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
}, {})
const updateFn = (currentTags) => {
currentTags = currentTags.raw || {}
Object.keys(rawTags).map((frameIdentifier) => {
const options = ID3Util.getSpecOptions(frameIdentifier, 3)
const cCompare = {}
if(options.multiple && currentTags[frameIdentifier] && rawTags[frameIdentifier]) {
if(options.updateCompareKey) {
currentTags[frameIdentifier].forEach((cTag, index) => {
cCompare[cTag[options.updateCompareKey]] = index
})
}
if (!(rawTags[frameIdentifier] instanceof Array)) {
rawTags[frameIdentifier] = [rawTags[frameIdentifier]]
}
rawTags[frameIdentifier].forEach((rTag) => {
const comparison = cCompare[rTag[options.updateCompareKey]]
if (comparison !== undefined) {
currentTags[frameIdentifier][comparison] = rTag
} else {
currentTags[frameIdentifier].push(rTag)
}
})
} else {
currentTags[frameIdentifier] = rawTags[frameIdentifier]
}
})
return currentTags
}
if(isFunction(fn)) {
return write(updateFn(read(filebuffer, options)), filebuffer, fn)
}
return write(updateFn(read(filebuffer, options)), filebuffer)
}
/**
* @param {string} filepath - Filepath to file
* @returns {boolean|Error}
*/
function removeTagsSync(filepath) {
let data
try {
data = fs.readFileSync(filepath)
} catch(error) {
return error
}
const newData = removeTagsFromBuffer(data)
if(!newData) {
return false
}
try {
fs.writeFileSync(filepath, newData, 'binary')
} catch(error) {
return error
}
return true
}
/**
* @param {string} filepath - Filepath to file
* @param {(error: Error) => void} fn - Function for async usage
* @returns {void}
*/
function removeTagsAsync(filepath, fn) {
fs.readFile(filepath, (error, data) => {
if(error) {
fn(error)
return
}
const newData = removeTagsFromBuffer(data)
if(!newData) {
fn(error)
return
}
fs.writeFile(filepath, newData, 'binary', (error) => {
if(error) {
fn(error)
} else {
fn(null)
}
})
})
}
/**
* Checks and removes already written ID3-Frames from a file
* @param {string} filepath - Filepath to file
* @param fn - (optional) Function for async usage
* @returns {boolean|Error}
*/
function removeTags(filepath, fn) {
if(isFunction(fn)) {
return removeTagsAsync(filepath, fn)
}
return removeTagsSync(filepath)
}
function makeSwapParameters(fn) {
return (a, b) => fn(b, a)
}
// The reorderParameter is a workaround because the callback function
// does not have a consistent interface between all the API functions.
// Ideally, all the functions should align with the promise style and
// always have the result first and the error second.
// Changing this would break the current public API.
// This could be changed internally and swap the parameter in a light
// wrapper when creating the public interface and then remove in a
// version 1.0 later with an API breaking change.
function makePromise(
fn,
reorderParameters = fn => (a, b) => fn(a, b)
) {
return new Promise((resolve, reject) => {
fn(reorderParameters((error, result) => {
if(error) {
reject(error)
} else {
resolve(result)
}
}))
})
}
const PromiseExport = {
create: (tags) => makePromise(create.bind(null, tags), makeSwapParameters),
write: (tags, file) => makePromise(write.bind(null, tags, file)),
update: (tags, file, options) => makePromise(update.bind(null, tags, file, options)),
read: (file, options) => makePromise(read.bind(null, file, options)),
removeTags: (filepath) => makePromise(removeTags.bind(null, filepath))
}
module.exports = {
TagConstants: ID3Definitions.TagConstants,
create,
write,
update,
read,
removeTags,
removeTagsFromBuffer,
Promise: PromiseExport
}