Passed
Pull Request — master (#136)
by
unknown
01:52
created

index.js (5 issues)

1
const fs = require('fs')
2
const ID3Definitions = require("./src/ID3Definitions")
3
const ID3Util = require('./src/ID3Util')
4
const ID3Helpers = require('./src/ID3Helpers')
5
const { isFunction, isString } = require('./src/util')
6
7
/*
8
**  Used specification: http://id3.org/id3v2.3.0
9
*/
10
11
/**
12
 * Checks and removes already written ID3-Frames from a buffer
13
 * @param {Buffer} data
14
 * @returns {boolean|Buffer}
15
 */
16
function removeTagsFromBuffer(data) {
17
    const framePosition = ID3Util.getFramePosition(data)
18
19
    if (framePosition === -1) {
20
        return data
21
    }
22
23
    const encodedSize = data.slice(framePosition + 6, framePosition + 10)
24
    if (!ID3Util.isValidEncodedSize(encodedSize)) {
25
        return false
26
    }
27
28
    if (data.length >= framePosition + 10) {
29
        const size = ID3Util.decodeSize(encodedSize)
30
        return Buffer.concat([
31
            data.slice(0, framePosition),
32
            data.slice(framePosition + size + 10)
33
        ])
34
    }
35
36
    return data
37
}
38
39
function writeInBuffer(tags, buffer) {
40
    buffer = removeTagsFromBuffer(buffer) || buffer
41
    return Buffer.concat([tags, buffer])
42
}
43
44
function writeAsync(tags, filebuffer, fn) {
45
    if(isString(filebuffer)) {
46
        try {
47
            fs.readFile(filebuffer, (error, data) => {
48
                if(error) {
49
                    fn(error)
50
                    return
51
                }
52
                const newData = writeInBuffer(tags, data)
53
                fs.writeFile(filebuffer, newData, 'binary', (error) => {
54
                    fn(error)
55
                })
56
            })
57
        } catch(error) {
58
            fn(error)
59
        }
60
    } else {
61
        fn(null, writeInBuffer(tags, filebuffer))
62
    }
63
}
64
65
function writeSync(tags, filebuffer) {
66
    if(isString(filebuffer)) {
67
        try {
68
            const data = fs.readFileSync(filebuffer)
69
            const newData = writeInBuffer(tags, data)
70
            fs.writeFileSync(filebuffer, newData, 'binary')
71
            return true
72
        } catch(error) {
73
            return error
74
        }
75
    }
76
77
    return writeInBuffer(tags, filebuffer)
78
}
79
80
/**
81
 * Write passed tags to a file/buffer
82
 * @param tags - Object containing tags to be written
83
 * @param filebuffer - Can contain a filepath string or buffer
84
 * @param fn - (optional) Function for async version
85
 * @returns {boolean|Buffer|Error}
86
 */
87
function write(tags, filebuffer, fn) {
88
    const completeTags = create(tags)
89
90
    if(isFunction(fn)) {
91
        return writeAsync(completeTags, filebuffer, fn)
92
    }
93
    return writeSync(completeTags, filebuffer)
94
}
95
96
/**
97
 * Creates a buffer containing the ID3 Tag
98
 * @param tags - Object containing tags to be written
99
 * @param fn fn - (optional) Function for async version
100
 * @returns {Buffer}
101
 */
102
function create(tags, fn) {
103
    const frames = ID3Helpers.createBufferFromTags(tags)
104
105
    //  Create ID3 header
106
    const header = Buffer.alloc(10)
107
    header.fill(0)
108
    header.write("ID3", 0)              //File identifier
109
    header.writeUInt16BE(0x0300, 3)     //Version 2.3.0  --  03 00
110
    header.writeUInt16BE(0x0000, 5)     //Flags 00
111
    ID3Util.encodeSize(frames.length).copy(header, 6)
112
113
    const id3Data = Buffer.concat([header, frames])
114
115
    if(isFunction(fn)) {
116
        fn(id3Data)
117
        return undefined
118
    }
119
    return id3Data
120
}
121
122
function readSync(filebuffer, options) {
123
    if(isString(filebuffer)) {
124
        filebuffer = fs.readFileSync(filebuffer)
125
    }
126
    return ID3Helpers.getTagsFromBuffer(filebuffer, options)
127
}
128
129
function readAsync(filebuffer, options, fn) {
130
    if(isString(filebuffer)) {
131
        fs.readFile(filebuffer, (error, data) => {
132
            if(error) {
133
                fn(error, null)
134
            } else {
135
                fn(null, ID3Helpers.getTagsFromBuffer(data, options))
136
            }
137
        })
138
    } else {
139
        fn(null, ID3Helpers.getTagsFromBuffer(filebuffer, options))
140
    }
141
}
142
143
/**
144
 * Read ID3-Tags from passed buffer/filepath
145
 * @param filebuffer - Can contain a filepath string or buffer
146
 * @param options - (optional) Object containing options
147
 * @param fn - (optional) Function for async version
148
 * @returns {boolean}
149
 */
150
function read(filebuffer, options, fn) {
151
    if(!options || typeof options === 'function') {
152
        fn = fn || options
153
        options = {}
154
    }
155
    if(isFunction(fn)) {
156
        return readAsync(filebuffer, options, fn)
157
    }
158
    return readSync(filebuffer, options)
159
}
160
161
/**
162
 * Update ID3-Tags from passed buffer/filepath
163
 * @param tags - Object containing tags to be written
164
 * @param filebuffer - Can contain a filepath string or buffer
165
 * @param options - (optional) Object containing options
166
 * @param fn - (optional) Function for async version
167
 * @returns {boolean|Buffer|Error}
168
 */
169
function update(tags, filebuffer, options, fn) {
170
    if(!options || typeof options === 'function') {
171
        fn = fn || options
172
        options = {}
173
    }
174
175
    const rawTags = Object.keys(tags).reduce((acc, val) => {
176
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
177
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
178
        } else {
179
            acc[val] = tags[val]
180
        }
181
        return acc
182
    }, {})
183
184
    const updateFn = (currentTags) => {
185
        currentTags = currentTags.raw || {}
186
        Object.keys(rawTags).map((frameIdentifier) => {
187
            const options = ID3Util.getSpecOptions(frameIdentifier, 3)
188
            const cCompare = {}
189
            if(options.multiple && currentTags[frameIdentifier] && rawTags[frameIdentifier]) {
190
                if(options.updateCompareKey) {
191
                    currentTags[frameIdentifier].forEach((cTag, index) => {
192
                        cCompare[cTag[options.updateCompareKey]] = index
193
                    })
194
195
                }
196
                if (!(rawTags[frameIdentifier] instanceof Array)) {
197
                    rawTags[frameIdentifier] = [rawTags[frameIdentifier]]
198
                }
199
                rawTags[frameIdentifier].forEach((rTag) => {
200
                    const comparison = cCompare[rTag[options.updateCompareKey]]
201
                    if (comparison !== undefined) {
202
                        currentTags[frameIdentifier][comparison] = rTag
203
                    } else {
204
                        currentTags[frameIdentifier].push(rTag)
205
                    }
206
                })
207
            } else {
208
                currentTags[frameIdentifier] = rawTags[frameIdentifier]
209
            }
210
        })
211
        return currentTags
212
    }
213
214
    if(isFunction(fn)) {
215
        return write(updateFn(read(filebuffer, options)), filebuffer, fn)
216
    }
217
    return write(updateFn(read(filebuffer, options)), filebuffer)
218
}
219
220
/**
221
 * @param {string} filepath - Filepath to file
222
 * @returns {boolean|Error}
223
 */
224
function removeTagsSync(filepath) {
225
    let data
226
    try {
227
        data = fs.readFileSync(filepath)
228
    } catch(error) {
229
        return error
230
    }
231
232
    const newData = removeTagsFromBuffer(data)
233
    if(!newData) {
234
        return false
235
    }
236
237
    try {
238
        fs.writeFileSync(filepath, newData, 'binary')
239
    } catch(error) {
240
        return error
241
    }
242
243
    return true
244
}
245
246
/**
247
 * @param {string} filepath - Filepath to file
248
 * @param {(error: Error) => void} fn - Function for async usage
249
 * @returns {void}
250
 */
251
function removeTagsAsync(filepath, fn) {
252
    fs.readFile(filepath, (error, data) => {
253
        if(error) {
254
            fn(error)
255
            return
256
        }
257
258
        const newData = removeTagsFromBuffer(data)
259
        if(!newData) {
260
            fn(error)
261
            return
262
        }
263
264
        fs.writeFile(filepath, newData, 'binary', (error) => {
265
            if(error) {
266
                fn(error)
267
            } else {
268
                fn(null)
269
            }
270
        })
271
    })
272
}
273
274
/**
275
 * Checks and removes already written ID3-Frames from a file
276
 * @param {string} filepath - Filepath to file
277
 * @param fn - (optional) Function for async usage
278
 * @returns {boolean|Error}
279
 */
280
function removeTags(filepath, fn) {
281
    if(isFunction(fn)) {
282
        return removeTagsAsync(filepath, fn)
283
    }
284
    return removeTagsSync(filepath)
285
}
286
287
function makeSwapParameters(fn) {
288
    return (a, b) => fn(b, a)
289
}
290
291
// The reorderParameter is a workaround because the callback function
292
// does not have a consistent interface between all the API functions.
293
// Ideally, all the functions should align with the promise style and
294
// always have the result first and the error second.
295
// Changing this would break the current public API.
296
// This could be changed internally and swap the parameter in a light
297
// wrapper when creating the public interface and then remove in a
298
// version 1.0 later with an API breaking change.
299
function makePromise(
300
    fn,
301
    reorderParameters = fn => (a, b) => fn(a, b)
302
) {
303
    return new Promise((resolve, reject) => {
304
        fn(reorderParameters((error, result) => {
305
            if(error) {
306
                reject(error)
307
            } else {
308
                resolve(result)
309
            }
310
        }))
311
    })
312
}
313
314
const PromiseExport = {
315
    create: (tags) => makePromise(create.bind(null, tags), makeSwapParameters),
0 ignored issues
show
The call to bind does not seem necessary since the function create declared on line 102 does not use this.
Loading history...
316
    write: (tags, file) => makePromise(write.bind(null, tags, file)),
0 ignored issues
show
The call to bind does not seem necessary since the function write declared on line 87 does not use this.
Loading history...
317
    update: (tags, file, options) => makePromise(update.bind(null, tags, file, options)),
0 ignored issues
show
The call to bind does not seem necessary since the function update declared on line 169 does not use this.
Loading history...
318
    read: (file, options) => makePromise(read.bind(null, file, options)),
0 ignored issues
show
The call to bind does not seem necessary since the function read declared on line 150 does not use this.
Loading history...
319
    removeTags: (filepath) => makePromise(removeTags.bind(null, filepath))
0 ignored issues
show
The call to bind does not seem necessary since the function removeTags declared on line 280 does not use this.
Loading history...
320
}
321
322
module.exports = {
323
    TagConstants: ID3Definitions.TagConstants,
324
    create,
325
    write,
326
    update,
327
    read,
328
    removeTags,
329
    removeTagsFromBuffer,
330
    Promise: PromiseExport
331
}
332