Passed
Pull Request — master (#127)
by
unknown
01:31
created
Severity
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 {
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) {
45
            fn(err)
46
        }
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 {
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 {
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 {
182
            fn(null, this.getTagsFromBuffer(filebuffer, options))
183
        }
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
}
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
}
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