Passed
Branch scrut-test (fa348b)
by Jan
01:33
created
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
 * @param {any} value
157
 * @returns {boolean} true if value is a function
158
 */
159
 const isFunction = (value) => value && typeof value === 'function'
160
161
 /**
162
  * @param {any} value
163
  * @returns  {boolean} true if value is a string
164
  */
165
 const isString = (value) => typeof value === 'string' || value instanceof String
166
167
function readSync(filebuffer, options) {
168
    return this.getTagsFromBuffer(filebuffer, options)
169
}
170
171
function readAsync(filebuffer, options, fn) {
172
    if(isString(filebuffer)) {
173
        fs.readFile(filebuffer, (error, data) => {
174
            if(error) {
175
                fn(error, null)
176
            } else {
177
                fn(null, this.getTagsFromBuffer(data, options))
178
            }
179
        })
180
    } else {
181
        fn(null, this.getTagsFromBuffer(filebuffer, options))
182
    }
183
}
184
185
/**
186
 * Read ID3-Tags from passed buffer/filepath
187
 * @param filebuffer - Can contain a filepath string or buffer
188
 * @param options - (optional) Object containing options
189
 * @param fn - (optional) Function for async version
190
 * @returns {boolean}
191
 */
192
module.exports.read = function(filebuffer, options, fn) {
193
    if(!options || typeof options === 'function') {
194
        fn = fn || options
195
        options = {}
196
    }
197
    if(isFunction(fn)) {
198
        return readAsync.bind(this)(filebuffer, options, fn)
199
    }
200
    return readSync.bind(this)(filebuffer, options)
201
}
202
203
/**
204
 * Update ID3-Tags from passed buffer/filepath
205
 * @param tags - Object containing tags to be written
206
 * @param filebuffer - Can contain a filepath string or buffer
207
 * @param options - (optional) Object containing options
208
 * @param fn - (optional) Function for async version
209
 * @returns {boolean|Buffer|Error}
210
 */
211
module.exports.update = function(tags, filebuffer, options, fn) {
212
    if(!options || typeof options === 'function') {
213
        fn = fn || options
214
        options = {}
215
    }
216
217
    const rawTags = Object.keys(tags).reduce((acc, val) => {
218
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
219
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
220
        } else {
221
            acc[val] = tags[val]
222
        }
223
        return acc
224
    }, {})
225
226
    const updateFn = (currentTags) => {
227
        currentTags = currentTags.raw || {}
228
        Object.keys(rawTags).map((specName) => {
229
            const options = ID3Util.getSpecOptions(specName, 3)
230
            const cCompare = {}
231
            if(options.multiple && currentTags[specName] && rawTags[specName]) {
232
                if(options.updateCompareKey) {
233
                    currentTags[specName].forEach((cTag, index) => {
234
                        cCompare[cTag[options.updateCompareKey]] = index
235
                    })
236
237
                }
238
                if (!(rawTags[specName] instanceof Array)) {
239
                    rawTags[specName] = [rawTags[specName]]
240
                }
241
                rawTags[specName].forEach((rTag) => {
242
                    const comparison = cCompare[rTag[options.updateCompareKey]]
243
                    if (comparison !== undefined) {
244
                        currentTags[specName][comparison] = rTag
245
                    } else {
246
                        currentTags[specName].push(rTag)
247
                    }
248
                })
249
            } else {
250
                currentTags[specName] = rawTags[specName]
251
            }
252
        })
253
254
        return currentTags
255
    }
256
257
    if(!fn || typeof fn !== 'function') {
258
        return this.write(updateFn(this.read(filebuffer, options)), filebuffer)
259
    }
260
261
    this.write(updateFn(this.read(filebuffer, options)), filebuffer, fn)
262
}
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...
263
264
module.exports.getTagsFromBuffer = function(filebuffer, options) {
265
    let framePosition = ID3Util.getFramePosition(filebuffer)
266
    if(framePosition === -1) {
267
        return this.getTagsFromFrames([], 3, options)
268
    }
269
    const frameSize = ID3Util.decodeSize(filebuffer.slice(framePosition + 6, framePosition + 10)) + 10
270
    let ID3Frame = Buffer.alloc(frameSize + 1)
271
    filebuffer.copy(ID3Frame, 0, framePosition)
272
    //ID3 version e.g. 3 if ID3v2.3.0
273
    let ID3Version = ID3Frame[3]
274
    const tagFlags = ID3Util.parseTagHeaderFlags(ID3Frame)
275
    let extendedHeaderOffset = 0
276
    if(tagFlags.extendedHeader) {
277
        if(ID3Version === 3) {
278
            extendedHeaderOffset = 4 + filebuffer.readUInt32BE(10)
279
        } else if(ID3Version === 4) {
280
            extendedHeaderOffset = ID3Util.decodeSize(filebuffer.slice(10, 14))
281
        }
282
    }
283
    let ID3FrameBody = Buffer.alloc(frameSize - 10 - extendedHeaderOffset)
284
    filebuffer.copy(ID3FrameBody, 0, framePosition + 10 + extendedHeaderOffset)
285
286
    let frames = this.getFramesFromID3Body(ID3FrameBody, ID3Version, options)
287
288
    return this.getTagsFromFrames(frames, ID3Version, options)
289
}
290
291
module.exports.getFramesFromID3Body = function(ID3FrameBody, ID3Version, options = {}) {
292
    let currentPosition = 0
293
    let frames = []
294
    if(!ID3FrameBody || !(ID3FrameBody instanceof Buffer)) {
295
        return frames
296
    }
297
298
    let identifierSize = 4
299
    let textframeHeaderSize = 10
300
    if(ID3Version === 2) {
301
        identifierSize = 3
302
        textframeHeaderSize = 6
303
    }
304
305
    while(currentPosition < ID3FrameBody.length && ID3FrameBody[currentPosition] !== 0x00) {
306
        let bodyFrameHeader = Buffer.alloc(textframeHeaderSize)
307
        ID3FrameBody.copy(bodyFrameHeader, 0, currentPosition)
308
309
        let decodeSize = false
310
        if(ID3Version === 4) {
311
            decodeSize = true
312
        }
313
        let bodyFrameSize = ID3Util.getFrameSize(bodyFrameHeader, decodeSize, ID3Version)
314
        if(bodyFrameSize + 10 > (ID3FrameBody.length - currentPosition)) {
315
            break
316
        }
317
        const specName = bodyFrameHeader.toString('utf8', 0, identifierSize)
318
        if(options.exclude instanceof Array && options.exclude.includes(specName) || options.include instanceof Array && !options.include.includes(specName)) {
319
            currentPosition += bodyFrameSize + textframeHeaderSize
320
            continue
321
        }
322
        const frameHeaderFlags = ID3Util.parseFrameHeaderFlags(bodyFrameHeader, ID3Version)
323
        let bodyFrameBuffer = Buffer.alloc(bodyFrameSize)
324
        ID3FrameBody.copy(bodyFrameBuffer, 0, currentPosition + textframeHeaderSize + (frameHeaderFlags.dataLengthIndicator ? 4 : 0))
325
        //  Size of sub frame + its header
326
        currentPosition += bodyFrameSize + textframeHeaderSize
327
        frames.push({
328
            name: specName,
329
            flags: frameHeaderFlags,
330
            body: frameHeaderFlags.unsynchronisation ? ID3Util.processUnsynchronisedBuffer(bodyFrameBuffer) : bodyFrameBuffer
331
        })
332
    }
333
334
    return frames
335
}
336
337
module.exports.getTagsFromFrames = function(frames, ID3Version, options = {}) {
338
    let tags = { }
339
    let raw = { }
340
341
    frames.forEach((frame) => {
342
        const specName = ID3Version === 2 ? ID3Definitions.FRAME_IDENTIFIERS.v3[ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]] : frame.name
343
        const identifier = ID3Version === 2 ? ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name] : ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v3[frame.name]
344
345
        if(!specName || !identifier || frame.flags.encryption) {
346
            return
347
        }
348
349
        if(frame.flags.compression) {
350
            if(frame.body.length < 5) {
351
                return
352
            }
353
            const inflatedSize = frame.body.readInt32BE()
354
            /*
355
            * ID3 spec defines that compression is stored in ZLIB format, but doesn't specify if header is present or not.
356
            * ZLIB has a 2-byte header.
357
            * 1. try if header + body decompression
358
            * 2. else try if header is not stored (assume that all content is deflated "body")
359
            * 3. else try if inflation works if the header is omitted (implementation dependent)
360
            * */
361
            try {
362
                frame.body = zlib.inflateSync(frame.body.slice(4))
363
            } catch (e) {
364
                try {
365
                    frame.body = zlib.inflateRawSync(frame.body.slice(4))
366
                } catch (e) {
367
                    try {
368
                        frame.body = zlib.inflateRawSync(frame.body.slice(6))
369
                    } catch (e) {
370
                        return
371
                    }
372
                }
373
            }
374
            if(frame.body.length !== inflatedSize) {
375
                return
376
            }
377
        }
378
379
        let decoded
380
        if(ID3Frames[specName]) {
381
            decoded = ID3Frames[specName].read(frame.body, ID3Version, this)
382
        } else if(specName.startsWith('T')) {
383
            decoded = ID3Frames.GENERIC_TEXT.read(frame.body, ID3Version)
384
        } else if(specName.startsWith('W')) {
385
            decoded = ID3Frames.GENERIC_URL.read(frame.body, ID3Version)
386
        }
387
388
        if(decoded) {
389
            if(ID3Util.getSpecOptions(specName, ID3Version).multiple) {
390
                if(!options.onlyRaw) {
391
                    if(!tags[identifier]) {
392
                        tags[identifier] = []
393
                    }
394
                    tags[identifier].push(decoded)
395
                }
396
                if(!options.noRaw) {
397
                    if(!raw[specName]) {
398
                        raw[specName] = []
399
                    }
400
                    raw[specName].push(decoded)
401
                }
402
            } else {
403
                if(!options.onlyRaw) {
404
                    tags[identifier] = decoded
405
                }
406
                if(!options.noRaw) {
407
                    raw[specName] = decoded
408
                }
409
            }
410
        }
411
    })
412
413
    if(options.onlyRaw) {
414
        return raw
415
    }
416
    if(options.noRaw) {
417
        return tags
418
    }
419
420
    tags.raw = raw
421
    return tags
422
}
423
424
/**
425
 * Checks and removes already written ID3-Frames from a buffer
426
 * @param data - Buffer
427
 * @returns {boolean|Buffer}
428
 */
429
module.exports.removeTagsFromBuffer = function(data) {
430
    let framePosition = ID3Util.getFramePosition(data)
431
432
    if (framePosition === -1) {
433
        return data
434
    }
435
436
    let hSize = Buffer.from([data[framePosition + 6], data[framePosition + 7], data[framePosition + 8], data[framePosition + 9]])
437
438
    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...
439
        //  Invalid tag size (msb not 0)
440
        return false
441
    }
442
443
    if (data.length >= framePosition + 10) {
444
        const size = ID3Util.decodeSize(data.slice(framePosition + 6, framePosition + 10))
445
        return Buffer.concat([data.slice(0, framePosition), data.slice(framePosition + size + 10)])
446
    }
447
448
    return data
449
}
450
451
/**
452
 * Checks and removes already written ID3-Frames from a file
453
 * @param filepath - Filepath to file
454
 * @param fn - (optional) Function for async usage
455
 * @returns {boolean|Error}
456
 */
457
module.exports.removeTags = function(filepath, fn) {
458
    if(!fn || typeof fn !== 'function') {
459
        let data
460
        try {
461
            data = fs.readFileSync(filepath)
462
        } catch(e) {
463
            return e
464
        }
465
466
        let newData = this.removeTagsFromBuffer(data)
467
        if(!newData) {
468
            return false
469
        }
470
471
        try {
472
            fs.writeFileSync(filepath, newData, 'binary')
473
        } catch(e) {
474
            return e
475
        }
476
477
        return true
478
    }
479
480
    fs.readFile(filepath, function(err, data) {
481
        if(err) {
482
            fn(err)
483
        }
484
485
        let newData = this.removeTagsFromBuffer(data)
486
        if(!newData) {
487
            fn(err)
488
            return
489
        }
490
491
        fs.writeFile(filepath, newData, 'binary', function(err) {
492
            if(err) {
493
                fn(err)
494
            } else {
495
                fn(false)
496
            }
497
        })
498
    }.bind(this))
499
}
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...
500
501
module.exports.Promise = {
502
    write: (tags, file) => {
503
        return new Promise((resolve, reject) => {
504
            this.write(tags, file, (err, ret) => {
505
                if(err) {
506
                    reject(err)
507
                } else {
508
                    resolve(ret)
509
                }
510
            })
511
        })
512
    },
513
    update: (tags, file) => {
514
        return new Promise((resolve, reject) => {
515
            this.update(tags, file, (err, ret) => {
516
                if(err) {
517
                    reject(err)
518
                } else {
519
                    resolve(ret)
520
                }
521
            })
522
        })
523
    },
524
    create: (tags) => {
525
        return new Promise((resolve) => {
526
            this.create(tags, (buffer) => {
527
                resolve(buffer)
528
            })
529
        })
530
    },
531
    read: (file, options) => {
532
        return new Promise((resolve, reject) => {
533
            this.read(file, options, (err, ret) => {
534
                if(err) {
535
                    reject(err)
536
                } else {
537
                    resolve(ret)
538
                }
539
            })
540
        })
541
    },
542
    removeTags: (filepath) => {
543
        return new Promise((resolve, reject) => {
544
            this.removeTags(filepath, (err) => {
545
                if(err) {
546
                    reject(err)
547
                } else {
548
                    resolve()
549
                }
550
            })
551
        })
552
    }
553
}
554
555
module.exports.Constants = ID3Definitions.Constants
556