Passed
Pull Request — master (#133)
by Jan
01:31
created

src/ID3Helpers.js   A

Complexity

Total Complexity 38
Complexity/F 3.8

Size

Lines of Code 183
Function Count 10

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 38
eloc 106
mnd 28
bc 28
fnc 10
dl 0
loc 183
rs 9.36
bpm 2.8
cpm 3.8
noi 0
c 0
b 0
f 0

4 Functions

Rating   Name   Duplication   Size   Complexity  
A ID3Helpers.js ➔ getFramesFromID3Body 0 30 5
B ID3Helpers.js ➔ isFrameDiscardedByOptions 0 7 8
D ID3Helpers.js ➔ createBuffersFromTags 0 51 13
C ID3Helpers.js ➔ getTagsFromFrames 0 41 11
1
const ID3Definitions = require("./ID3Definitions")
2
const ID3Frames = require('./ID3Frames')
3
const ID3Util = require('./ID3Util')
4
const ID3Frame = require('./ID3Frame')
5
const ID3FrameHeader = require('./ID3FrameHeader')
6
7
/**
8
 * Returns array of buffers created by tags specified in the tags argument
9
 * @param tags - Object containing tags to be written
10
 * @returns {Array}
11
 */
12
function createBuffersFromTags(tags) {
13
    const frames = []
14
    if(!tags) {
15
        return frames
16
    }
17
    const rawObject = Object.keys(tags).reduce((acc, val) => {
18
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
19
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
20
        } else if(ID3Definitions.FRAME_IDENTIFIERS.v4[val] !== undefined) {
21
            /**
22
             * Currently, node-id3 always writes ID3 version 3.
23
             * However, version 3 and 4 are very similar, and node-id3 can also read version 4 frames.
24
             * Until version 4 is fully supported, as a workaround, allow writing version 4 frames into a version 3 tag.
25
             * If a reader does not support a v4 frame, it's (per spec) supposed to skip it, so it should not be a problem.
26
             */
27
            acc[ID3Definitions.FRAME_IDENTIFIERS.v4[val]] = tags[val]
28
        } else {
29
            acc[val] = tags[val]
30
        }
31
        return acc
32
    }, {})
33
34
    Object.keys(rawObject).forEach((frameIdentifier) => {
35
        let frame
36
        // Check if invalid frameIdentifier
37
        if(frameIdentifier.length !== 4) {
38
            return
39
        }
40
        if(ID3Frames[frameIdentifier] !== undefined) {
41
            frame = ID3Frames[frameIdentifier].create(rawObject[frameIdentifier], 3)
42
        } else if(frameIdentifier.startsWith('T')) {
43
            frame = ID3Frames.GENERIC_TEXT.create(frameIdentifier, rawObject[frameIdentifier], 3)
44
        } else if(frameIdentifier.startsWith('W')) {
45
            if(ID3Util.getSpecOptions(frameIdentifier, 3).multiple && rawObject[frameIdentifier] instanceof Array && rawObject[frameIdentifier].length > 0) {
46
                frame = Buffer.alloc(0)
47
                // deduplicate array
48
                for(const url of [...new Set(rawObject[frameIdentifier])]) {
49
                    frame = Buffer.concat([frame, ID3Frames.GENERIC_URL.create(frameIdentifier, url, 3)])
50
                }
51
            } else {
52
                frame = ID3Frames.GENERIC_URL.create(frameIdentifier, rawObject[frameIdentifier], 3)
53
            }
54
        }
55
56
        if (frame && frame instanceof Buffer) {
57
            frames.push(frame)
58
        }
59
    })
60
61
    return frames
62
}
63
64
/**
65
 * Return a buffer with the frames for the specified tags
66
 * @param tags - Object containing tags to be written
67
 * @returns {Buffer}
68
 */
69
module.exports.createBufferFromTags = function(tags) {
70
    return Buffer.concat(createBuffersFromTags(tags))
71
}
72
73
module.exports.getTagsFromBuffer = function(filebuffer, options) {
74
    const framePosition = ID3Util.getFramePosition(filebuffer)
75
    if(framePosition === -1) {
76
        return getTagsFromFrames([], 3, options)
77
    }
78
    const frameSize = ID3Util.decodeSize(filebuffer.slice(framePosition + 6, framePosition + 10)) + 10
79
    const ID3Frame = Buffer.alloc(frameSize + 1)
80
    filebuffer.copy(ID3Frame, 0, framePosition)
81
    //ID3 version e.g. 3 if ID3v2.3.0
82
    const ID3Version = ID3Frame[3]
83
    const tagFlags = ID3Util.parseTagHeaderFlags(ID3Frame)
84
    let extendedHeaderOffset = 0
85
    if(tagFlags.extendedHeader) {
86
        if(ID3Version === 3) {
87
            extendedHeaderOffset = 4 + filebuffer.readUInt32BE(10)
88
        } else if(ID3Version === 4) {
89
            extendedHeaderOffset = ID3Util.decodeSize(filebuffer.slice(10, 14))
90
        }
91
    }
92
    const ID3FrameBody = Buffer.alloc(frameSize - 10 - extendedHeaderOffset)
93
    filebuffer.copy(ID3FrameBody, 0, framePosition + 10 + extendedHeaderOffset)
94
95
    const frames = getFramesFromID3Body(ID3FrameBody, ID3Version, options)
96
97
    return getTagsFromFrames(frames, ID3Version, options)
98
}
99
100
function isFrameDiscardedByOptions(frameIdentifier, options) {
101
    if(options.exclude instanceof Array && options.exclude.includes(frameIdentifier)) {
102
        return true
103
    }
104
105
    return options.include instanceof Array && !options.include.includes(frameIdentifier)
106
}
107
108
function getFramesFromID3Body(ID3TagBody, ID3Version, options = {}) {
109
    let currentPosition = 0
110
    const frames = []
111
    if(!ID3TagBody || !(ID3TagBody instanceof Buffer)) {
112
        return frames
113
    }
114
115
    while(currentPosition < ID3TagBody.length && ID3TagBody[currentPosition] !== 0x00) {
116
        const frameSize = ID3FrameHeader.getFrameSizeFromBuffer(ID3TagBody.subarray(currentPosition)) + ID3FrameHeader.getSizeByVersion(ID3Version)
117
        const frameBuffer = ID3TagBody.subarray(currentPosition, currentPosition + frameSize)
118
        const frame = ID3Frame.createFromBuffer(frameBuffer, ID3Version)
119
        // It's possible to discard frames via options.exclude/options.include
120
        // If that is the case, skip this frame and continue with the next
121
        if(!frame || isFrameDiscardedByOptions(frame.identifier, options)) {
122
            currentPosition += frameSize
123
            continue
124
        }
125
        // Prevent errors when the current frame's size exceeds the remaining tags size (e.g. due to broken size bytes).
126
        if(frameSize > (ID3TagBody.length - currentPosition)) {
127
            break
128
        }
129
130
        frames.push(frame)
131
132
        //  Size of frame body + its header
133
        currentPosition += frameSize
134
    }
135
136
    return frames
137
}
138
139
function getTagsFromFrames(frames, ID3Version, options = {}) {
140
    const tags = { }
141
    const raw = { }
142
143
    frames.forEach((frame) => {
144
        const frameValue = frame.getValue()
145
        const frameAlias = ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v3[frame.identifier] || ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v4[frame.identifier]
146
147
        if(ID3Util.getSpecOptions(frame.identifier, ID3Version).multiple) {
148
            if(!options.onlyRaw) {
149
                if(!tags[frameAlias]) {
150
                    tags[frameAlias] = []
151
                }
152
                tags[frameAlias].push(frameValue)
153
            }
154
            if(!options.noRaw) {
155
                if(!raw[frame.identifier]) {
156
                    raw[frame.identifier] = []
157
                }
158
                raw[frame.identifier].push(frameValue)
159
            }
160
        } else {
161
            if(!options.onlyRaw) {
162
                tags[frameAlias] = frameValue
163
            }
164
            if(!options.noRaw) {
165
                raw[frame.identifier] = frameValue
166
            }
167
        }
168
    })
169
170
    if(options.onlyRaw) {
171
        return raw
172
    }
173
    if(options.noRaw) {
174
        return tags
175
    }
176
177
    tags.raw = raw
178
    return tags
179
}
180
181
module.exports.getTagsFromID3Body = function(body) {
182
    return getTagsFromFrames(getFramesFromID3Body(body, 3), 3)
183
}
184