Completed
Push — master ( d25395...d83c14 )
by Jan
15s queued 14s
created

src/Frames.ts   B

Complexity

Total Complexity 48
Complexity/F 0

Size

Lines of Code 578
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 48
eloc 439
mnd 48
bc 48
fnc 0
dl 0
loc 578
rs 8.5599
bpm 0
cpm 0
noi 0
c 0
b 0
f 0

How to fix   Complexity   

Complexity

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