Passed
Pull Request — master (#124)
by
unknown
01:29
created

index.js (10 issues)

1
const fs = require('fs')
2
const ID3Definitions = require("./src/ID3Definitions")
3
const ID3Frames = require('./src/ID3Frames')
4
const ID3Util = require('./src/ID3Util')
5
const zlib = require('zlib')
6
7
/*
8
**  Used specification: http://id3.org/id3v2.3.0
9
*/
10
11
/**
12
 * Write passed tags to a file/buffer
13
 * @param tags - Object containing tags to be written
14
 * @param filebuffer - Can contain a filepath string or buffer
15
 * @param fn - (optional) Function for async version
16
 * @returns {boolean|Buffer|Error}
17
 */
18
module.exports.write = function(tags, filebuffer, fn) {
19
    let completeTag = this.create(tags)
20
    if(filebuffer instanceof Buffer) {
21
        filebuffer = this.removeTagsFromBuffer(filebuffer) || filebuffer
22
        let completeBuffer = Buffer.concat([completeTag, filebuffer])
23
        if(fn && typeof fn === 'function') {
24
            fn(null, completeBuffer)
25
            return undefined
26
        } else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
27
            return completeBuffer
28
        }
29
    }
30
31
    if(fn && typeof fn === 'function') {
32
        try {
33
            fs.readFile(filebuffer, function(err, data) {
34
                if(err) {
35
                    fn(err)
36
                    return
37
                }
38
                data = this.removeTagsFromBuffer(data) || data
39
                let rewriteFile = Buffer.concat([completeTag, data])
40
                fs.writeFile(filebuffer, rewriteFile, 'binary', (err) => {
41
                    fn(err)
42
                })
43
            }.bind(this))
44
        } catch(err) {
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
45
            fn(err)
46
        }
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
47
    } else {
48
        try {
49
            let data = fs.readFileSync(filebuffer)
50
            data = this.removeTagsFromBuffer(data) || data
51
            let rewriteFile = Buffer.concat([completeTag, data])
52
            fs.writeFileSync(filebuffer, rewriteFile, 'binary')
53
            return true
54
        } catch(err) {
55
            return err
56
        }
57
    }
58
}
59
60
/**
61
 * Creates a buffer containing the ID3 Tag
62
 * @param tags - Object containing tags to be written
63
 * @param fn fn - (optional) Function for async version
64
 * @returns {Buffer}
65
 */
66
module.exports.create = function(tags, fn) {
67
    let frames = []
68
69
    //  Create & push a header for the ID3-Frame
70
    const header = Buffer.alloc(10)
71
    header.fill(0)
72
    header.write("ID3", 0)              //File identifier
73
    header.writeUInt16BE(0x0300, 3)     //Version 2.3.0  --  03 00
74
    header.writeUInt16BE(0x0000, 5)     //Flags 00
75
76
    //Last 4 bytes are used for header size, but have to be inserted later, because at this point, its size is not clear.
77
    frames.push(header)
78
79
    frames = frames.concat(this.createBuffersFromTags(tags))
80
81
    //  Calculate frame size of ID3 body to insert into header
82
83
    let totalSize = 0
84
    frames.forEach((frame) => {
85
        totalSize += frame.length
86
    })
87
88
    //  Don't count ID3 header itself
89
    totalSize -= 10
90
    //  ID3 header size uses only 7 bits of a byte, bit shift is needed
91
    let size = ID3Util.encodeSize(totalSize)
92
93
    //  Write bytes to ID3 frame header, which is the first frame
94
    frames[0].writeUInt8(size[0], 6)
95
    frames[0].writeUInt8(size[1], 7)
96
    frames[0].writeUInt8(size[2], 8)
97
    frames[0].writeUInt8(size[3], 9)
98
99
    if(fn && typeof fn === 'function') {
100
        fn(Buffer.concat(frames))
101
    } else {
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
102
        return Buffer.concat(frames)
103
    }
104
}
105
106
/**
107
 * Returns array of buffers created by tags specified in the tags argument
108
 * @param tags - Object containing tags to be written
109
 * @returns {Array}
110
 */
111
module.exports.createBuffersFromTags = function(tags) {
112
    let frames = []
113
    if(!tags) {
114
        return frames
115
    }
116
    const rawObject = Object.keys(tags).reduce((acc, val) => {
117
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
118
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
119
        } else {
120
            acc[val] = tags[val]
121
        }
122
        return acc
123
    }, {})
124
125
    Object.keys(rawObject).forEach((specName) => {
126
        let frame
127
        // Check if invalid specName
128
        if(specName.length !== 4) {
129
            return
130
        }
131
        if(ID3Frames[specName] !== undefined) {
132
            frame = ID3Frames[specName].create(rawObject[specName], 3, this)
133
        } else if(specName.startsWith('T')) {
134
            frame = ID3Frames.GENERIC_TEXT.create(specName, rawObject[specName], 3)
135
        } else if(specName.startsWith('W')) {
136
            if(ID3Util.getSpecOptions(specName, 3).multiple && rawObject[specName] instanceof Array && rawObject[specName].length > 0) {
137
                frame = Buffer.alloc(0)
138
                // deduplicate array
139
                for(let url of [...new Set(rawObject[specName])]) {
140
                    frame = Buffer.concat([frame, ID3Frames.GENERIC_URL.create(specName, url, 3)])
141
                }
142
            } else {
143
                frame = ID3Frames.GENERIC_URL.create(specName, rawObject[specName], 3)
144
            }
145
        }
146
147
        if (frame && frame instanceof Buffer) {
148
            frames.push(frame)
149
        }
150
    })
151
152
    return frames
153
}
154
155
/**
156
 * Read ID3-Tags from passed buffer/filepath
157
 * @param filebuffer - Can contain a filepath string or buffer
158
 * @param options - (optional) Object containing options
159
 * @param fn - (optional) Function for async version
160
 * @returns {boolean}
161
 */
162
module.exports.read = function(filebuffer, options, fn) {
163
    if(!options || typeof options === 'function') {
164
        fn = fn || options
165
        options = {}
166
    }
167
    if(!fn || typeof fn !== 'function') {
168
        if(typeof filebuffer === "string" || filebuffer instanceof String) {
169
            filebuffer = fs.readFileSync(filebuffer)
170
        }
171
        return this.getTagsFromBuffer(filebuffer, options)
172
    } else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
173
        if(typeof filebuffer === "string" || filebuffer instanceof String) {
174
            fs.readFile(filebuffer, function(err, data) {
175
                if(err) {
176
                    fn(err, null)
177
                } else {
178
                    fn(null, this.getTagsFromBuffer(data, options))
179
                }
180
            }.bind(this))
181
        } else {
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
182
            fn(null, this.getTagsFromBuffer(filebuffer, options))
183
        }
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
184
    }
185
}
186
187
/**
188
 * Update ID3-Tags from passed buffer/filepath
189
 * @param tags - Object containing tags to be written
190
 * @param filebuffer - Can contain a filepath string or buffer
191
 * @param options - (optional) Object containing options
192
 * @param fn - (optional) Function for async version
193
 * @returns {boolean|Buffer|Error}
194
 */
195
module.exports.update = function(tags, filebuffer, options, fn) {
196
    if(!options || typeof options === 'function') {
197
        fn = fn || options
198
        options = {}
199
    }
200
201
    const rawTags = Object.keys(tags).reduce((acc, val) => {
202
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
203
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
204
        } else {
205
            acc[val] = tags[val]
206
        }
207
        return acc
208
    }, {})
209
210
    const updateFn = (currentTags) => {
211
        currentTags = currentTags.raw || {}
212
        Object.keys(rawTags).map((specName) => {
213
            const options = ID3Util.getSpecOptions(specName, 3)
214
            const cCompare = {}
215
            if(options.multiple && currentTags[specName] && rawTags[specName]) {
216
                if(options.updateCompareKey) {
217
                    currentTags[specName].forEach((cTag, index) => {
218
                        cCompare[cTag[options.updateCompareKey]] = index
219
                    })
220
221
                }
222
                if (!(rawTags[specName] instanceof Array)) {
223
                    rawTags[specName] = [rawTags[specName]]
224
                }
225
                rawTags[specName].forEach((rTag) => {
226
                    const comparison = cCompare[rTag[options.updateCompareKey]]
227
                    if (comparison !== undefined) {
228
                        currentTags[specName][comparison] = rTag
229
                    } else {
230
                        currentTags[specName].push(rTag)
231
                    }
232
                })
233
            } else {
234
                currentTags[specName] = rawTags[specName]
235
            }
236
        })
237
238
        return currentTags
239
    }
240
241
    if(!fn || typeof fn !== 'function') {
242
        return this.write(updateFn(this.read(filebuffer, options)), filebuffer)
243
    }
244
245
    this.write(updateFn(this.read(filebuffer, options)), filebuffer, fn)
246
}
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
247
248
module.exports.getTagsFromBuffer = function(filebuffer, options) {
249
    let framePosition = ID3Util.getFramePosition(filebuffer)
250
    if(framePosition === -1) {
251
        return this.getTagsFromFrames([], 3, options)
252
    }
253
    const frameSize = ID3Util.decodeSize(filebuffer.slice(framePosition + 6, framePosition + 10)) + 10
254
    let ID3Frame = Buffer.alloc(frameSize + 1)
255
    filebuffer.copy(ID3Frame, 0, framePosition)
256
    //ID3 version e.g. 3 if ID3v2.3.0
257
    let ID3Version = ID3Frame[3]
258
    const tagFlags = ID3Util.parseTagHeaderFlags(ID3Frame)
259
    let extendedHeaderOffset = 0
260
    if(tagFlags.extendedHeader) {
261
        if(ID3Version === 3) {
262
            extendedHeaderOffset = 4 + filebuffer.readUInt32BE(10)
263
        } else if(ID3Version === 4) {
264
            extendedHeaderOffset = ID3Util.decodeSize(filebuffer.slice(10, 14))
265
        }
266
    }
267
    let ID3FrameBody = Buffer.alloc(frameSize - 10 - extendedHeaderOffset)
268
    filebuffer.copy(ID3FrameBody, 0, framePosition + 10 + extendedHeaderOffset)
269
270
    let frames = this.getFramesFromID3Body(ID3FrameBody, ID3Version, options)
271
272
    return this.getTagsFromFrames(frames, ID3Version, options)
273
}
274
275
module.exports.getFramesFromID3Body = function(ID3FrameBody, ID3Version, options = {}) {
276
    let currentPosition = 0
277
    let frames = []
278
    if(!ID3FrameBody || !(ID3FrameBody instanceof Buffer)) {
279
        return frames
280
    }
281
282
    let identifierSize = 4
283
    let textframeHeaderSize = 10
284
    if(ID3Version === 2) {
285
        identifierSize = 3
286
        textframeHeaderSize = 6
287
    }
288
289
    while(currentPosition < ID3FrameBody.length && ID3FrameBody[currentPosition] !== 0x00) {
290
        let bodyFrameHeader = Buffer.alloc(textframeHeaderSize)
291
        ID3FrameBody.copy(bodyFrameHeader, 0, currentPosition)
292
293
        let decodeSize = false
294
        if(ID3Version === 4) {
295
            decodeSize = true
296
        }
297
        let bodyFrameSize = ID3Util.getFrameSize(bodyFrameHeader, decodeSize, ID3Version)
298
        if(bodyFrameSize + 10 > (ID3FrameBody.length - currentPosition)) {
299
            break
300
        }
301
        const specName = bodyFrameHeader.toString('utf8', 0, identifierSize)
302
        if(options.exclude instanceof Array && options.exclude.includes(specName) || options.include instanceof Array && !options.include.includes(specName)) {
303
            currentPosition += bodyFrameSize + textframeHeaderSize
304
            continue
305
        }
306
        const frameHeaderFlags = ID3Util.parseFrameHeaderFlags(bodyFrameHeader, ID3Version)
307
        let bodyFrameBuffer = Buffer.alloc(bodyFrameSize)
308
        ID3FrameBody.copy(bodyFrameBuffer, 0, currentPosition + textframeHeaderSize + (frameHeaderFlags.dataLengthIndicator ? 4 : 0))
309
        //  Size of sub frame + its header
310
        currentPosition += bodyFrameSize + textframeHeaderSize
311
        frames.push({
312
            name: specName,
313
            flags: frameHeaderFlags,
314
            body: frameHeaderFlags.unsynchronisation ? ID3Util.processUnsynchronisedBuffer(bodyFrameBuffer) : bodyFrameBuffer
315
        })
316
    }
317
318
    return frames
319
}
320
321
module.exports.getTagsFromFrames = function(frames, ID3Version, options = {}) {
322
    let tags = { }
323
    let raw = { }
324
325
    frames.forEach((frame) => {
326
        const specName = ID3Version === 2 ? ID3Definitions.FRAME_IDENTIFIERS.v3[ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]] : frame.name
327
        const identifier = ID3Version === 2 ? ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name] : ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v3[frame.name]
328
329
        if(!specName || !identifier || frame.flags.encryption) {
330
            return
331
        }
332
333
        if(frame.flags.compression) {
334
            if(frame.body.length < 5) {
335
                return
336
            }
337
            const inflatedSize = frame.body.readInt32BE()
338
            /*
339
            * ID3 spec defines that compression is stored in ZLIB format, but doesn't specify if header is present or not.
340
            * ZLIB has a 2-byte header.
341
            * 1. try if header + body decompression
342
            * 2. else try if header is not stored (assume that all content is deflated "body")
343
            * 3. else try if inflation works if the header is omitted (implementation dependent)
344
            * */
345
            try {
346
                frame.body = zlib.inflateSync(frame.body.slice(4))
347
            } catch (e) {
348
                try {
349
                    frame.body = zlib.inflateRawSync(frame.body.slice(4))
350
                } catch (e) {
351
                    try {
352
                        frame.body = zlib.inflateRawSync(frame.body.slice(6))
353
                    } catch (e) {
354
                        return
355
                    }
356
                }
357
            }
358
            if(frame.body.length !== inflatedSize) {
359
                return
360
            }
361
        }
362
363
        let decoded
364
        if(ID3Frames[specName]) {
365
            decoded = ID3Frames[specName].read(frame.body, ID3Version, this)
366
        } else if(specName.startsWith('T')) {
367
            decoded = ID3Frames.GENERIC_TEXT.read(frame.body, ID3Version)
368
        } else if(specName.startsWith('W')) {
369
            decoded = ID3Frames.GENERIC_URL.read(frame.body, ID3Version)
370
        }
371
372
        if(decoded) {
373
            if(ID3Util.getSpecOptions(specName, ID3Version).multiple) {
374
                if(!options.onlyRaw) {
375
                    if(!tags[identifier]) {
376
                        tags[identifier] = []
377
                    }
378
                    tags[identifier].push(decoded)
379
                }
380
                if(!options.noRaw) {
381
                    if(!raw[specName]) {
382
                        raw[specName] = []
383
                    }
384
                    raw[specName].push(decoded)
385
                }
386
            } else {
387
                if(!options.onlyRaw) {
388
                    tags[identifier] = decoded
389
                }
390
                if(!options.noRaw) {
391
                    raw[specName] = decoded
392
                }
393
            }
394
        }
395
    })
396
397
    if(options.onlyRaw) {
398
        return raw
399
    }
400
    if(options.noRaw) {
401
        return tags
402
    }
403
404
    tags.raw = raw
405
    return tags
406
}
407
408
/**
409
 * Checks and removes already written ID3-Frames from a buffer
410
 * @param data - Buffer
411
 * @returns {boolean|Buffer}
412
 */
413
module.exports.removeTagsFromBuffer = function(data) {
414
    let framePosition = ID3Util.getFramePosition(data)
415
416
    if (framePosition === -1) {
417
        return data
418
    }
419
420
    let hSize = Buffer.from([data[framePosition + 6], data[framePosition + 7], data[framePosition + 8], data[framePosition + 9]])
421
422
    if ((hSize[0] | hSize[1] | hSize[2] | hSize[3]) & 0x80) {
0 ignored issues
show
You have used a bitwise operator & in a condition. Did you maybe want to use the logical operator &&
Loading history...
423
        //  Invalid tag size (msb not 0)
424
        return false
425
    }
426
427
    if (data.length >= framePosition + 10) {
428
        const size = ID3Util.decodeSize(data.slice(framePosition + 6, framePosition + 10))
429
        return Buffer.concat([data.slice(0, framePosition), data.slice(framePosition + size + 10)])
430
    }
431
432
    return data
433
}
434
435
/**
436
 * Checks and removes already written ID3-Frames from a file
437
 * @param filepath - Filepath to file
438
 * @param fn - (optional) Function for async usage
439
 * @returns {boolean|Error}
440
 */
441
module.exports.removeTags = function(filepath, fn) {
442
    if(!fn || typeof fn !== 'function') {
443
        let data
444
        try {
445
            data = fs.readFileSync(filepath)
446
        } catch(e) {
447
            return e
448
        }
449
450
        let newData = this.removeTagsFromBuffer(data)
451
        if(!newData) {
452
            return false
453
        }
454
455
        try {
456
            fs.writeFileSync(filepath, newData, 'binary')
457
        } catch(e) {
458
            return e
459
        }
460
461
        return true
462
    }
463
464
    fs.readFile(filepath, function(err, data) {
465
        if(err) {
466
            fn(err)
467
        }
468
469
        let newData = this.removeTagsFromBuffer(data)
470
        if(!newData) {
471
            fn(err)
472
            return
473
        }
474
475
        fs.writeFile(filepath, newData, 'binary', function(err) {
476
            if(err) {
477
                fn(err)
478
            } else {
479
                fn(false)
480
            }
481
        })
482
    }.bind(this))
483
}
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
484
485
module.exports.Promise = {
486
    write: (tags, file) => {
487
        return new Promise((resolve, reject) => {
488
            this.write(tags, file, (err, ret) => {
489
                if(err) {
490
                    reject(err)
491
                } else {
492
                    resolve(ret)
493
                }
494
            })
495
        })
496
    },
497
    update: (tags, file) => {
498
        return new Promise((resolve, reject) => {
499
            this.update(tags, file, (err, ret) => {
500
                if(err) {
501
                    reject(err)
502
                } else {
503
                    resolve(ret)
504
                }
505
            })
506
        })
507
    },
508
    create: (tags) => {
509
        return new Promise((resolve) => {
510
            this.create(tags, (buffer) => {
511
                resolve(buffer)
512
            })
513
        })
514
    },
515
    read: (file, options) => {
516
        return new Promise((resolve, reject) => {
517
            this.read(file, options, (err, ret) => {
518
                if(err) {
519
                    reject(err)
520
                } else {
521
                    resolve(ret)
522
                }
523
            })
524
        })
525
    },
526
    removeTags: (filepath) => {
527
        return new Promise((resolve, reject) => {
528
            this.removeTags(filepath, (err) => {
529
                if(err) {
530
                    reject(err)
531
                } else {
532
                    resolve()
533
                }
534
            })
535
        })
536
    }
537
}
538