Passed
Push — convert-to-typescript ( 6cf209 )
by Jan
03:57
created

src/ID3Frames.js   F

Complexity

Total Complexity 95
Complexity/F 2.02

Size

Lines of Code 547
Function Count 47

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 95
eloc 365
mnd 48
bc 48
fnc 47
dl 0
loc 547
rs 2
bpm 1.0212
cpm 2.0212
noi 0
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like src/ID3Frames.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 ID3FrameBuilder = require("./ID3FrameBuilder")
3
const ID3FrameReader = require("./ID3FrameReader")
4
import * as ID3Definitions from './ID3Definitions'
5
const ID3Util = require("./ID3Util")
6
const ID3Helpers = require('./ID3Helpers')
7
const { isString } = require('./util')
8
9
module.exports.GENERIC_TEXT = {
10
    create: (frameIdentifier, data) => {
11
        if(!frameIdentifier || !data) {
12
            return null
13
        }
14
15
        return new ID3FrameBuilder(frameIdentifier)
16
            .appendStaticNumber(0x01, 0x01)
17
            .appendStaticValue(data, null, 0x01)
18
            .getBuffer()
19
    },
20
    read: (buffer) => {
21
        const reader = new ID3FrameReader(buffer, 0)
22
23
        return reader.consumeStaticValue('string')
24
    }
25
}
26
27
module.exports.GENERIC_URL = {
28
    create: (frameIdentifier, data) => {
29
        if(!frameIdentifier || !data) {
30
            return null
31
        }
32
33
        return new ID3FrameBuilder(frameIdentifier)
34
            .appendStaticValue(data)
35
            .getBuffer()
36
    },
37
    read: (buffer) => {
38
        const reader = new ID3FrameReader(buffer)
39
40
        return reader.consumeStaticValue('string')
41
    }
42
}
43
44
module.exports.APIC = {
45
    create: (data) => {
46
        try {
47
            if (data instanceof Buffer) {
48
                data = {
49
                    imageBuffer: Buffer.from(data)
50
                }
51
            } else if (isString(data)) {
52
                data = {
53
                    imageBuffer: fs.readFileSync(data)
54
                }
55
            } else if (!data.imageBuffer) {
56
                return Buffer.alloc(0)
57
            }
58
59
            let mime_type = data.mime
60
61
            if(!mime_type) {
62
                mime_type = ID3Util.getPictureMimeTypeFromBuffer(data.imageBuffer)
63
            }
64
65
            const TagConstants = ID3Definitions.TagConstants.AttachedPicture
66
            const pictureType = data.type || {}
67
            const pictureTypeId = pictureType.id === undefined
68
                ? TagConstants.PictureType.FRONT_COVER : pictureType.id
69
70
            /*
71
             * Fix a bug in iTunes where the artwork is not recognized when the description is empty using UTF-16.
72
             * Instead, if the description is empty, use encoding 0x00 (ISO-8859-1).
73
             */
74
            const { description = '' } = data
75
            const encoding = description ? 0x01 : 0x00
76
            return new ID3FrameBuilder('APIC')
77
              .appendStaticNumber(encoding, 1)
78
              .appendNullTerminatedValue(mime_type)
79
              .appendStaticNumber(pictureTypeId, 1)
80
              .appendNullTerminatedValue(description, encoding)
81
              .appendStaticValue(data.imageBuffer)
82
              .getBuffer()
83
        } catch(error) {
84
            return error
85
        }
86
    },
87
    read: (buffer, version) => {
88
        const reader = new ID3FrameReader(buffer, 0)
89
        let mime
90
        if(version === 2) {
91
            mime = reader.consumeStaticValue('string', 3, 0x00)
92
        } else {
93
            mime = reader.consumeNullTerminatedValue('string', 0x00)
94
        }
95
96
        const typeId = reader.consumeStaticValue('number', 1)
97
        const description = reader.consumeNullTerminatedValue('string')
98
        const imageBuffer = reader.consumeStaticValue()
99
100
        return {
101
            mime: mime,
102
            type: {
103
                id: typeId,
104
                name: ID3Definitions.APIC_TYPES[typeId]
105
            },
106
            description: description,
107
            imageBuffer: imageBuffer
108
        }
109
    }
110
}
111
112
module.exports.COMM = {
113
    create: (data) => {
114
        data = data || {}
115
        if(!data.text) {
116
            return null
117
        }
118
119
        return new ID3FrameBuilder("COMM")
120
            .appendStaticNumber(0x01, 1)
121
            .appendStaticValue(data.language)
122
            .appendNullTerminatedValue(data.shortText, 0x01)
123
            .appendStaticValue(data.text, null, 0x01)
124
            .getBuffer()
125
    },
126
    read: (buffer) => {
127
        const reader = new ID3FrameReader(buffer, 0)
128
129
        return {
130
            language: reader.consumeStaticValue('string', 3, 0x00),
131
            shortText: reader.consumeNullTerminatedValue('string'),
132
            text: reader.consumeStaticValue('string', null)
133
        }
134
    }
135
}
136
137
module.exports.USLT = {
138
    create: (data) => {
139
        data = data || {}
140
        if(isString(data)) {
141
            data = {
142
                text: data
143
            }
144
        }
145
        if(!data.text) {
146
            return null
147
        }
148
149
        return new ID3FrameBuilder("USLT")
150
            .appendStaticNumber(0x01, 1)
151
            .appendStaticValue(data.language)
152
            .appendNullTerminatedValue(data.shortText, 0x01)
153
            .appendStaticValue(data.text, null, 0x01)
154
            .getBuffer()
155
    },
156
    read: (buffer) => {
157
        const reader = new ID3FrameReader(buffer, 0)
158
159
        return {
160
            language: reader.consumeStaticValue('string', 3, 0x00),
161
            shortText: reader.consumeNullTerminatedValue('string'),
162
            text: reader.consumeStaticValue('string', null)
163
        }
164
    }
165
}
166
167
module.exports.SYLT = {
168
    create: (data) => {
169
        if(!(data instanceof Array)) {
170
            data = [data]
171
        }
172
173
        const encoding = 1 // 16 bit unicode
174
        return Buffer.concat(data.map(lycics => {
175
            const frameBuilder = new ID3FrameBuilder("SYLT")
176
                .appendStaticNumber(encoding, 1)
177
                .appendStaticValue(lycics.language, 3)
178
                .appendStaticNumber(lycics.timeStampFormat, 1)
179
                .appendStaticNumber(lycics.contentType, 1)
180
                .appendNullTerminatedValue(lycics.shortText, encoding)
181
            lycics.synchronisedText.forEach(part => {
182
                frameBuilder.appendNullTerminatedValue(part.text, encoding)
183
                frameBuilder.appendStaticNumber(part.timeStamp, 4)
184
            })
185
            return frameBuilder.getBuffer()
186
        }))
187
    },
188
    read: (buffer) => {
189
        const reader = new ID3FrameReader(buffer, 0)
190
191
        return {
192
            language: reader.consumeStaticValue('string', 3, 0x00),
193
            timeStampFormat: reader.consumeStaticValue('number', 1),
194
            contentType: reader.consumeStaticValue('number', 1),
195
            shortText: reader.consumeNullTerminatedValue('string'),
196
            synchronisedText: Array.from((function*() {
197
                while(true) {
198
                    const text = reader.consumeNullTerminatedValue('string')
199
                    const timeStamp = reader.consumeStaticValue('number', 4)
200
                    if (text === undefined || timeStamp === undefined) {
201
                        break
202
                    }
203
                    yield {text, timeStamp}
204
                }
205
            })())
206
        }
207
    }
208
}
209
210
module.exports.TXXX = {
211
    create: (data) => {
212
        if(!(data instanceof Array)) {
213
            data = [data]
214
        }
215
216
        return Buffer.concat(data.map(udt => new ID3FrameBuilder("TXXX")
217
            .appendStaticNumber(0x01, 1)
218
            .appendNullTerminatedValue(udt.description, 0x01)
219
            .appendStaticValue(udt.value, null, 0x01)
220
            .getBuffer()))
221
    },
222
    read: (buffer) => {
223
        const reader = new ID3FrameReader(buffer, 0)
224
225
        return {
226
            description: reader.consumeNullTerminatedValue('string'),
227
            value: reader.consumeStaticValue('string')
228
        }
229
    }
230
}
231
232
module.exports.POPM = {
233
    create: (data) => {
234
        const email = data.email
235
        let rating = Math.trunc(data.rating)
236
        let counter = Math.trunc(data.counter)
237
        if(!email) {
238
            return null
239
        }
240
        if(isNaN(rating) || rating < 0 || rating > 255) {
241
            rating = 0
242
        }
243
        if(isNaN(counter) || counter < 0) {
244
            counter = 0
245
        }
246
247
        return new ID3FrameBuilder("POPM")
248
            .appendNullTerminatedValue(email)
249
            .appendStaticNumber(rating, 1)
250
            .appendStaticNumber(counter, 4)
251
            .getBuffer()
252
    },
253
    read: (buffer) => {
254
        const reader = new ID3FrameReader(buffer)
255
        return {
256
            email: reader.consumeNullTerminatedValue('string'),
257
            rating: reader.consumeStaticValue('number', 1),
258
            counter: reader.consumeStaticValue('number')
259
        }
260
    }
261
}
262
263
module.exports.PRIV = {
264
    create: (data) => {
265
        if(!(data instanceof Array)) {
266
            data = [data]
267
        }
268
269
        return Buffer.concat(data.map(priv => new ID3FrameBuilder("PRIV")
270
            .appendNullTerminatedValue(priv.ownerIdentifier)
271
            .appendStaticValue(priv.data instanceof Buffer ? priv.data : Buffer.from(priv.data, "utf8"))
272
            .getBuffer()))
273
    },
274
    read: (buffer) => {
275
        const reader = new ID3FrameReader(buffer)
276
        return {
277
            ownerIdentifier: reader.consumeNullTerminatedValue('string'),
278
            data: reader.consumeStaticValue()
279
        }
280
    }
281
}
282
283
module.exports.UFID = {
284
    create: (data) => {
285
        if (!(data instanceof Array)) {
286
            data = [data]
287
        }
288
289
        return Buffer.concat(data.map(ufid => new ID3FrameBuilder("UFID")
290
            .appendNullTerminatedValue(ufid.ownerIdentifier)
291
            .appendStaticValue(
292
                ufid.identifier instanceof Buffer ?
293
                ufid.identifier : Buffer.from(ufid.identifier, "utf8")
294
            )
295
            .getBuffer()))
296
    },
297
    read: (buffer) => {
298
        const reader = new ID3FrameReader(buffer)
299
        return {
300
            ownerIdentifier: reader.consumeNullTerminatedValue('string'),
301
            identifier: reader.consumeStaticValue()
302
        }
303
    }
304
}
305
306
module.exports.CHAP = {
307
    create: (data) => {
308
        if (!(data instanceof Array)) {
309
            data = [data]
310
        }
311
312
        return Buffer.concat(data.map(chap => {
313
            if (!chap || !chap.elementID || typeof chap.startTimeMs === "undefined" || !chap.endTimeMs) {
314
                return null
315
            }
316
            return new ID3FrameBuilder("CHAP")
317
                .appendNullTerminatedValue(chap.elementID)
318
                .appendStaticNumber(chap.startTimeMs, 4)
319
                .appendStaticNumber(chap.endTimeMs, 4)
320
                .appendStaticNumber(chap.startOffsetBytes ? chap.startOffsetBytes : 0xFFFFFFFF, 4)
321
                .appendStaticNumber(chap.endOffsetBytes ? chap.endOffsetBytes : 0xFFFFFFFF, 4)
322
                .appendStaticValue(ID3Helpers.createBufferFromTags(chap.tags))
323
                .getBuffer()
324
        }).filter(chap => chap instanceof Buffer))
325
    },
326
    read: (buffer) => {
327
        const reader = new ID3FrameReader(buffer)
328
        const chap = {
329
            elementID: reader.consumeNullTerminatedValue('string'),
330
            startTimeMs: reader.consumeStaticValue('number', 4),
331
            endTimeMs: reader.consumeStaticValue('number', 4),
332
            startOffsetBytes: reader.consumeStaticValue('number', 4),
333
            endOffsetBytes: reader.consumeStaticValue('number', 4),
334
            tags: ID3Helpers.getTagsFromID3Body(reader.consumeStaticValue())
335
        }
336
        if(chap.startOffsetBytes === 0xFFFFFFFF) {
337
            delete chap.startOffsetBytes
338
        }
339
        if(chap.endOffsetBytes === 0xFFFFFFFF) {
340
            delete chap.endOffsetBytes
341
        }
342
        return chap
343
    }
344
}
345
346
module.exports.CTOC = {
347
    create: (data) => {
348
        if(!(data instanceof Array)) {
349
            data = [data]
350
        }
351
352
        return Buffer.concat(data.map((toc, index) => {
353
            if(!toc || !toc.elementID) {
354
                return null
355
            }
356
            if(!(toc.elements instanceof Array)) {
357
                toc.elements = []
358
            }
359
360
            const ctocFlags = Buffer.alloc(1, 0)
361
            if(index === 0) {
362
                ctocFlags[0] += 2
363
            }
364
            if(toc.isOrdered) {
365
                ctocFlags[0] += 1
366
            }
367
368
            const builder = new ID3FrameBuilder("CTOC")
369
                .appendNullTerminatedValue(toc.elementID)
370
                .appendStaticValue(ctocFlags, 1)
371
                .appendStaticNumber(toc.elements.length, 1)
372
            toc.elements.forEach((el) => {
373
                builder.appendNullTerminatedValue(el)
374
            })
375
            if(toc.tags) {
376
                builder.appendStaticValue(ID3Helpers.createBufferFromTags(toc.tags))
377
            }
378
            return builder.getBuffer()
379
        }).filter((toc) => toc instanceof Buffer))
380
    },
381
    read: (buffer) => {
382
        const reader = new ID3FrameReader(buffer)
383
        const elementID = reader.consumeNullTerminatedValue('string')
384
        const flags = reader.consumeStaticValue('number', 1)
385
        const entries = reader.consumeStaticValue('number', 1)
386
        const elements = []
387
        for(let i = 0; i < entries; i++) {
388
            elements.push(reader.consumeNullTerminatedValue('string'))
389
        }
390
        const tags = ID3Helpers.getTagsFromID3Body(reader.consumeStaticValue())
391
392
        return {
393
            elementID,
394
            isOrdered: !!(flags & 0x01 === 0x01),
395
            elements,
396
            tags
397
        }
398
    }
399
}
400
401
module.exports.WXXX = {
402
    create: (data) => {
403
        if(!(data instanceof Array)) {
404
            data = [data]
405
        }
406
407
        return Buffer.concat(data.map((udu) => {
408
            return new ID3FrameBuilder("WXXX")
409
                .appendStaticNumber(0x01, 1)
410
                .appendNullTerminatedValue(udu.description, 0x01)
411
                .appendStaticValue(udu.url, null)
412
                .getBuffer()
413
        }))
414
    },
415
    read: (buffer) => {
416
        const reader = new ID3FrameReader(buffer, 0)
417
418
        return {
419
            description: reader.consumeNullTerminatedValue('string'),
420
            url: reader.consumeStaticValue('string', null, 0x00)
421
        }
422
    }
423
}
424
425
module.exports.ETCO = {
426
    create: (data) => {
427
        const builder = new ID3FrameBuilder("ETCO")
428
            .appendStaticNumber(data.timeStampFormat, 1)
429
        data.keyEvents.forEach((keyEvent) => {
430
            builder
431
                .appendStaticNumber(keyEvent.type, 1)
432
                .appendStaticNumber(keyEvent.timeStamp, 4)
433
        })
434
435
        return builder.getBuffer()
436
    },
437
    read: (buffer) => {
438
        const reader = new ID3FrameReader(buffer)
439
440
        return {
441
            timeStampFormat: reader.consumeStaticValue('number', 1),
442
            keyEvents: Array.from((function*() {
443
                while(true) {
444
                    const type = reader.consumeStaticValue('number', 1)
445
                    const timeStamp = reader.consumeStaticValue('number', 4)
446
                    if (type === undefined || timeStamp === undefined) {
447
                        break
448
                    }
449
                    yield {type, timeStamp}
450
                }
451
            })())
452
        }
453
    }
454
}
455
456
module.exports.COMR = {
457
    create: (data) => {
458
        if(!(data instanceof Array)) {
459
            data = [data]
460
        }
461
462
        return Buffer.concat(data.map(comr => {
463
            const prices = comr.prices || {}
464
            const builder = new ID3FrameBuilder("COMR")
465
466
            // Text encoding
467
            builder.appendStaticNumber(0x01, 1)
468
            // Price string
469
            const priceString = Object.entries(prices).map((price) => {
470
                return price[0].substring(0, 3) + price[1].toString()
471
            }).join('/')
472
            builder.appendNullTerminatedValue(priceString, 0x00)
473
            // Valid until
474
            builder.appendStaticValue(
475
                comr.validUntil.year.toString().padStart(4, '0').substring(0, 4) +
476
                comr.validUntil.month.toString().padStart(2, '0').substring(0, 2) +
477
                comr.validUntil.day.toString().padStart(2, '0').substring(0, 2),
478
                8, 0x00
479
            )
480
            // Contact URL
481
            builder.appendNullTerminatedValue(comr.contactUrl, 0x00)
482
            // Received as
483
            builder.appendStaticNumber(comr.receivedAs, 1)
484
            // Name of seller
485
            builder.appendNullTerminatedValue(comr.nameOfSeller, 0x01)
486
            // Description
487
            builder.appendNullTerminatedValue(comr.description, 0x01)
488
            // Seller logo
489
            if(comr.sellerLogo) {
490
                let picture = comr.sellerLogo.picture
491
                if(typeof comr.sellerLogo.picture === 'string' || comr.sellerLogo.picture instanceof String) {
492
                    picture = fs.readFileSync(comr.sellerLogo.picture)
493
                }
494
                let mimeType = comr.sellerLogo.mimeType || ID3Util.getPictureMimeTypeFromBuffer(picture)
495
                // Only image/png and image/jpeg allowed
496
                if(mimeType !== 'image/png' && 'image/jpeg') {
497
                    mimeType = 'image/'
498
                }
499
500
                builder.appendNullTerminatedValue(mimeType ? mimeType : '', 0x00)
501
                builder.appendStaticValue(picture)
502
            }
503
            return builder.getBuffer()
504
        }))
505
    },
506
    read: (buffer) => {
507
        const reader = new ID3FrameReader(buffer, 0)
508
509
        const tag = {}
510
511
        // Price string
512
        const priceStrings = reader.consumeNullTerminatedValue('string', 0x00)
513
            .split('/')
514
            .filter((price) => price.length > 3)
515
        tag.prices = {}
516
        for(const price of priceStrings) {
517
            tag.prices[price.substring(0, 3)] = price.substring(3)
518
        }
519
        // Valid until
520
        const validUntilString = reader.consumeStaticValue('string', 8, 0x00)
521
        tag.validUntil = { year: 0, month: 0, day: 0 }
522
        if(/^\d+$/.test(validUntilString)) {
523
            tag.validUntil.year = parseInt(validUntilString.substring(0, 4))
524
            tag.validUntil.month = parseInt(validUntilString.substring(4, 6))
525
            tag.validUntil.day = parseInt(validUntilString.substring(6))
526
        }
527
        // Contact URL
528
        tag.contactUrl = reader.consumeNullTerminatedValue('string', 0x00)
529
        // Received as
530
        tag.receivedAs = reader.consumeStaticValue('number', 1)
531
        // Name of seller
532
        tag.nameOfSeller = reader.consumeNullTerminatedValue('string')
533
        // Description
534
        tag.description = reader.consumeNullTerminatedValue('string')
535
        // Seller logo
536
        const mimeType = reader.consumeNullTerminatedValue('string', 0x00)
537
        const picture = reader.consumeStaticValue('buffer')
538
        if(picture && picture.length > 0) {
539
            tag.sellerLogo = {
540
                mimeType,
541
                picture
542
            }
543
        }
544
545
        return tag
546
    }
547
}
548