Passed
Pull Request — master (#127)
by Jan
01:49
created

index.js ➔ readSync   F

Complexity

Conditions 21

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 0
c 0
b 0
f 0
cc 21

How to fix   Complexity   

Complexity

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