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