Zazama /
node-id3
| 1 | const fs = require('fs')
|
||
| 2 | const ID3Definitions = require("./src/ID3Definitions")
|
||
| 3 | const ID3Util = require('./src/ID3Util')
|
||
| 4 | const ID3Helpers = require('./src/ID3Helpers')
|
||
| 5 | const { isFunction, isString } = require('./src/util')
|
||
| 6 | |||
| 7 | /* |
||
| 8 | ** Used specification: http://id3.org/id3v2.3.0 |
||
| 9 | */ |
||
| 10 | |||
| 11 | /** |
||
| 12 | * Checks and removes already written ID3-Frames from a buffer |
||
| 13 | * @param {Buffer} data
|
||
| 14 | * @returns {boolean|Buffer}
|
||
| 15 | */ |
||
| 16 | function removeTagsFromBuffer(data) {
|
||
| 17 | const framePosition = ID3Util.getFramePosition(data) |
||
| 18 | |||
| 19 | if (framePosition === -1) {
|
||
| 20 | return data |
||
| 21 | } |
||
| 22 | |||
| 23 | const encodedSize = data.slice(framePosition + 6, framePosition + 10) |
||
| 24 | if (!ID3Util.isValidEncodedSize(encodedSize)) {
|
||
| 25 | return false |
||
| 26 | } |
||
| 27 | |||
| 28 | if (data.length >= framePosition + 10) {
|
||
| 29 | const size = ID3Util.decodeSize(encodedSize) |
||
| 30 | return Buffer.concat([ |
||
| 31 | data.slice(0, framePosition), |
||
| 32 | data.slice(framePosition + size + 10) |
||
| 33 | ]) |
||
| 34 | } |
||
| 35 | |||
| 36 | return data |
||
| 37 | } |
||
| 38 | |||
| 39 | function writeInBuffer(tags, buffer) {
|
||
| 40 | buffer = removeTagsFromBuffer(buffer) || buffer |
||
| 41 | return Buffer.concat([tags, buffer]) |
||
| 42 | } |
||
| 43 | |||
| 44 | function writeAsync(tags, filebuffer, fn) {
|
||
| 45 | if(isString(filebuffer)) {
|
||
| 46 | try {
|
||
| 47 | fs.readFile(filebuffer, (error, data) => {
|
||
| 48 | if(error) {
|
||
| 49 | fn(error) |
||
| 50 | return |
||
| 51 | } |
||
| 52 | const newData = writeInBuffer(tags, data) |
||
| 53 | fs.writeFile(filebuffer, newData, 'binary', (error) => {
|
||
| 54 | fn(error) |
||
| 55 | }) |
||
| 56 | }) |
||
| 57 | } catch(error) {
|
||
| 58 | fn(error) |
||
| 59 | } |
||
| 60 | } else {
|
||
| 61 | fn(null, writeInBuffer(tags, filebuffer)) |
||
| 62 | } |
||
| 63 | } |
||
| 64 | |||
| 65 | function writeSync(tags, filebuffer) {
|
||
| 66 | if(isString(filebuffer)) {
|
||
| 67 | try {
|
||
| 68 | const data = fs.readFileSync(filebuffer) |
||
| 69 | const newData = writeInBuffer(tags, data) |
||
| 70 | fs.writeFileSync(filebuffer, newData, 'binary') |
||
| 71 | return true |
||
| 72 | } catch(error) {
|
||
| 73 | return error |
||
| 74 | } |
||
| 75 | } |
||
| 76 | |||
| 77 | return writeInBuffer(tags, filebuffer) |
||
| 78 | } |
||
| 79 | |||
| 80 | /** |
||
| 81 | * Write passed tags to a file/buffer |
||
| 82 | * @param tags - Object containing tags to be written |
||
| 83 | * @param filebuffer - Can contain a filepath string or buffer |
||
| 84 | * @param fn - (optional) Function for async version |
||
| 85 | * @returns {boolean|Buffer|Error}
|
||
| 86 | */ |
||
| 87 | function write(tags, filebuffer, fn) {
|
||
| 88 | const completeTags = create(tags) |
||
| 89 | |||
| 90 | if(isFunction(fn)) {
|
||
| 91 | return writeAsync(completeTags, filebuffer, fn) |
||
| 92 | } |
||
| 93 | return writeSync(completeTags, filebuffer) |
||
| 94 | } |
||
| 95 | |||
| 96 | /** |
||
| 97 | * Creates a buffer containing the ID3 Tag |
||
| 98 | * @param tags - Object containing tags to be written |
||
| 99 | * @param fn fn - (optional) Function for async version |
||
| 100 | * @returns {Buffer}
|
||
| 101 | */ |
||
| 102 | function create(tags, fn) {
|
||
| 103 | const frames = ID3Helpers.createBufferFromTags(tags) |
||
| 104 | |||
| 105 | // Create ID3 header |
||
| 106 | const header = Buffer.alloc(10) |
||
| 107 | header.fill(0) |
||
| 108 | header.write("ID3", 0) //File identifier
|
||
| 109 | header.writeUInt16BE(0x0300, 3) //Version 2.3.0 -- 03 00 |
||
| 110 | header.writeUInt16BE(0x0000, 5) //Flags 00 |
||
| 111 | ID3Util.encodeSize(frames.length).copy(header, 6) |
||
| 112 | |||
| 113 | const id3Data = Buffer.concat([header, frames]) |
||
| 114 | |||
| 115 | if(isFunction(fn)) {
|
||
| 116 | fn(id3Data) |
||
| 117 | return undefined |
||
| 118 | } |
||
| 119 | return id3Data |
||
| 120 | } |
||
| 121 | |||
| 122 | function readSync(filebuffer, options) {
|
||
| 123 | if(isString(filebuffer)) {
|
||
| 124 | filebuffer = fs.readFileSync(filebuffer) |
||
| 125 | } |
||
| 126 | return ID3Helpers.getTagsFromBuffer(filebuffer, options) |
||
| 127 | } |
||
| 128 | |||
| 129 | function readAsync(filebuffer, options, fn) {
|
||
| 130 | if(isString(filebuffer)) {
|
||
| 131 | fs.readFile(filebuffer, (error, data) => {
|
||
| 132 | if(error) {
|
||
| 133 | fn(error, null) |
||
| 134 | } else {
|
||
| 135 | fn(null, ID3Helpers.getTagsFromBuffer(data, options)) |
||
| 136 | } |
||
| 137 | }) |
||
| 138 | } else {
|
||
| 139 | fn(null, ID3Helpers.getTagsFromBuffer(filebuffer, options)) |
||
| 140 | } |
||
| 141 | } |
||
| 142 | |||
| 143 | /** |
||
| 144 | * Read ID3-Tags from passed buffer/filepath |
||
| 145 | * @param filebuffer - Can contain a filepath string or buffer |
||
| 146 | * @param options - (optional) Object containing options |
||
| 147 | * @param fn - (optional) Function for async version |
||
| 148 | * @returns {boolean}
|
||
| 149 | */ |
||
| 150 | function read(filebuffer, options, fn) {
|
||
| 151 | if(!options || typeof options === 'function') {
|
||
| 152 | fn = fn || options |
||
| 153 | options = {}
|
||
| 154 | } |
||
| 155 | if(isFunction(fn)) {
|
||
| 156 | return readAsync(filebuffer, options, fn) |
||
| 157 | } |
||
| 158 | return readSync(filebuffer, options) |
||
| 159 | } |
||
| 160 | |||
| 161 | /** |
||
| 162 | * Update ID3-Tags from passed buffer/filepath |
||
| 163 | * @param tags - Object containing tags to be written |
||
| 164 | * @param filebuffer - Can contain a filepath string or buffer |
||
| 165 | * @param options - (optional) Object containing options |
||
| 166 | * @param fn - (optional) Function for async version |
||
| 167 | * @returns {boolean|Buffer|Error}
|
||
| 168 | */ |
||
| 169 | function update(tags, filebuffer, options, fn) {
|
||
| 170 | if(!options || typeof options === 'function') {
|
||
| 171 | fn = fn || options |
||
| 172 | options = {}
|
||
| 173 | } |
||
| 174 | |||
| 175 | const rawTags = Object.keys(tags).reduce((acc, val) => {
|
||
| 176 | if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
|
||
| 177 | acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val] |
||
| 178 | } else {
|
||
| 179 | acc[val] = tags[val] |
||
| 180 | } |
||
| 181 | return acc |
||
| 182 | }, {})
|
||
| 183 | |||
| 184 | const updateFn = (currentTags) => {
|
||
| 185 | currentTags = currentTags.raw || {}
|
||
| 186 | Object.keys(rawTags).map((frameIdentifier) => {
|
||
| 187 | const options = ID3Util.getSpecOptions(frameIdentifier, 3) |
||
| 188 | const cCompare = {}
|
||
| 189 | if(options.multiple && currentTags[frameIdentifier] && rawTags[frameIdentifier]) {
|
||
| 190 | if(options.updateCompareKey) {
|
||
| 191 | currentTags[frameIdentifier].forEach((cTag, index) => {
|
||
| 192 | cCompare[cTag[options.updateCompareKey]] = index |
||
| 193 | }) |
||
| 194 | |||
| 195 | } |
||
| 196 | if (!(rawTags[frameIdentifier] instanceof Array)) {
|
||
| 197 | rawTags[frameIdentifier] = [rawTags[frameIdentifier]] |
||
| 198 | } |
||
| 199 | rawTags[frameIdentifier].forEach((rTag) => {
|
||
| 200 | const comparison = cCompare[rTag[options.updateCompareKey]] |
||
| 201 | if (comparison !== undefined) {
|
||
| 202 | currentTags[frameIdentifier][comparison] = rTag |
||
| 203 | } else {
|
||
| 204 | currentTags[frameIdentifier].push(rTag) |
||
| 205 | } |
||
| 206 | }) |
||
| 207 | } else {
|
||
| 208 | currentTags[frameIdentifier] = rawTags[frameIdentifier] |
||
| 209 | } |
||
| 210 | }) |
||
| 211 | return currentTags |
||
| 212 | } |
||
| 213 | |||
| 214 | if(isFunction(fn)) {
|
||
| 215 | return write(updateFn(read(filebuffer, options)), filebuffer, fn) |
||
| 216 | } |
||
| 217 | return write(updateFn(read(filebuffer, options)), filebuffer) |
||
| 218 | } |
||
| 219 | |||
| 220 | /** |
||
| 221 | * @param {string} filepath - Filepath to file
|
||
| 222 | * @returns {boolean|Error}
|
||
| 223 | */ |
||
| 224 | function removeTagsSync(filepath) {
|
||
| 225 | let data |
||
| 226 | try {
|
||
| 227 | data = fs.readFileSync(filepath) |
||
| 228 | } catch(error) {
|
||
| 229 | return error |
||
| 230 | } |
||
| 231 | |||
| 232 | const newData = removeTagsFromBuffer(data) |
||
| 233 | if(!newData) {
|
||
| 234 | return false |
||
| 235 | } |
||
| 236 | |||
| 237 | try {
|
||
| 238 | fs.writeFileSync(filepath, newData, 'binary') |
||
| 239 | } catch(error) {
|
||
| 240 | return error |
||
| 241 | } |
||
| 242 | |||
| 243 | return true |
||
| 244 | } |
||
| 245 | |||
| 246 | /** |
||
| 247 | * @param {string} filepath - Filepath to file
|
||
| 248 | * @param {(error: Error) => void} fn - Function for async usage
|
||
| 249 | * @returns {void}
|
||
| 250 | */ |
||
| 251 | function removeTagsAsync(filepath, fn) {
|
||
| 252 | fs.readFile(filepath, (error, data) => {
|
||
| 253 | if(error) {
|
||
| 254 | fn(error) |
||
| 255 | return |
||
| 256 | } |
||
| 257 | |||
| 258 | const newData = removeTagsFromBuffer(data) |
||
| 259 | if(!newData) {
|
||
| 260 | fn(error) |
||
| 261 | return |
||
| 262 | } |
||
| 263 | |||
| 264 | fs.writeFile(filepath, newData, 'binary', (error) => {
|
||
| 265 | if(error) {
|
||
| 266 | fn(error) |
||
| 267 | } else {
|
||
| 268 | fn(null) |
||
| 269 | } |
||
| 270 | }) |
||
| 271 | }) |
||
| 272 | } |
||
| 273 | |||
| 274 | /** |
||
| 275 | * Checks and removes already written ID3-Frames from a file |
||
| 276 | * @param {string} filepath - Filepath to file
|
||
| 277 | * @param fn - (optional) Function for async usage |
||
| 278 | * @returns {boolean|Error}
|
||
| 279 | */ |
||
| 280 | function removeTags(filepath, fn) {
|
||
| 281 | if(isFunction(fn)) {
|
||
| 282 | return removeTagsAsync(filepath, fn) |
||
| 283 | } |
||
| 284 | return removeTagsSync(filepath) |
||
| 285 | } |
||
| 286 | |||
| 287 | function makeSwapParameters(fn) {
|
||
| 288 | return (a, b) => fn(b, a) |
||
| 289 | } |
||
| 290 | |||
| 291 | // The reorderParameter is a workaround because the callback function |
||
| 292 | // does not have a consistent interface between all the API functions. |
||
| 293 | // Ideally, all the functions should align with the promise style and |
||
| 294 | // always have the result first and the error second. |
||
| 295 | // Changing this would break the current public API. |
||
| 296 | // This could be changed internally and swap the parameter in a light |
||
| 297 | // wrapper when creating the public interface and then remove in a |
||
| 298 | // version 1.0 later with an API breaking change. |
||
| 299 | function makePromise( |
||
| 300 | fn, |
||
| 301 | reorderParameters = fn => (a, b) => fn(a, b) |
||
| 302 | ) {
|
||
| 303 | return new Promise((resolve, reject) => {
|
||
| 304 | fn(reorderParameters((error, result) => {
|
||
| 305 | if(error) {
|
||
| 306 | reject(error) |
||
| 307 | } else {
|
||
| 308 | resolve(result) |
||
| 309 | } |
||
| 310 | })) |
||
| 311 | }) |
||
| 312 | } |
||
| 313 | |||
| 314 | const PromiseExport = {
|
||
| 315 | create: (tags) => makePromise(create.bind(null, tags), makeSwapParameters), |
||
|
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
| 316 | write: (tags, file) => makePromise(write.bind(null, tags, file)), |
||
|
0 ignored issues
–
show
|
|||
| 317 | update: (tags, file, options) => makePromise(update.bind(null, tags, file, options)), |
||
|
0 ignored issues
–
show
|
|||
| 318 | read: (file, options) => makePromise(read.bind(null, file, options)), |
||
|
0 ignored issues
–
show
|
|||
| 319 | removeTags: (filepath) => makePromise(removeTags.bind(null, filepath)) |
||
|
0 ignored issues
–
show
|
|||
| 320 | } |
||
| 321 | |||
| 322 | module.exports = {
|
||
| 323 | TagConstants: ID3Definitions.TagConstants, |
||
| 324 | create, |
||
| 325 | write, |
||
| 326 | update, |
||
| 327 | read, |
||
| 328 | removeTags, |
||
| 329 | removeTagsFromBuffer, |
||
| 330 | Promise: PromiseExport |
||
| 331 | } |
||
| 332 |