Passed
Branch scrut-test (372b2a)
by Jan
03:15
created

index.js   F

Complexity

Total Complexity 119
Complexity/F 2.77

Size

Lines of Code 558
Function Count 43

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 119
eloc 320
mnd 76
bc 76
fnc 43
dl 0
loc 558
rs 2
bpm 1.7674
cpm 2.7674
noi 7
c 0
b 0
f 0

2 Functions

Rating   Name   Duplication   Size   Complexity  
A ➔ readAsync 0 13 4
F ➔ readSync 0 6 28

How to fix   Complexity   

Complexity

Complex classes like index.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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