UNPKG

node-id3

Version:

Pure JavaScript ID3v2 Tag writer and reader

332 lines (295 loc) 9.83 kB
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 }