Passed
Pull Request — master (#135)
by
unknown
01:52
created
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
 * Check and remove already written ID3-Frames from a buffer
13
 * @param {Buffer} data
14
 * @returns {false|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 {object} tags - Object containing tags to be written
83
 * @param {string|Buffer} filebuffer - Filepath or buffer
84
 * @param {WriteCallback} [fn] - Function for async version
85
 * @returns {boolean|Buffer|Error|void}
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
 * Create a buffer containing the ID3 Tag
98
 * @param {object} tags - Object containing tags to be written
99
 * @param {CreateCallback} [fn] - Function for async version
100
 * @returns {Buffer|undefined}
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
/**
123
 * Read ID3-Tags from passed buffer/filepath
124
 * @param {string|Buffer} filebuffer - Filepath or buffer
125
 * @param {Options} options - Object containing options
126
 * @returns {object} - Return the read tags
127
 */
128
function readSync(filebuffer, options) {
129
    if(isString(filebuffer)) {
130
        filebuffer = fs.readFileSync(filebuffer)
131
    }
132
    return ID3Helpers.getTagsFromBuffer(filebuffer, options)
133
}
134
135
/**
136
 * Read ID3-Tags from passed buffer/filepath
137
 * @param {string|Buffer} filebuffer - Filepath or buffer
138
 * @param {Options} options - Object containing options
139
 * @param {ReadCallback} fn - Function for async version
140
 * @returns {void} - Return the read tags
141
 */
142
 function readAsync(filebuffer, options, fn) {
143
    if(isString(filebuffer)) {
144
        fs.readFile(filebuffer, (error, data) => {
145
            if(error) {
146
                fn(error, null)
147
            } else {
148
                fn(null, ID3Helpers.getTagsFromBuffer(data, options))
149
            }
150
        })
151
    } else {
152
        fn(null, ID3Helpers.getTagsFromBuffer(filebuffer, options))
153
    }
154
}
155
156
/**
157
 * Read ID3-Tags from passed buffer/filepath
158
 * @param {string|Buffer} filebuffer - Filepath or buffer
159
 * @param {Options} [options] - Object containing options
160
 * @param {ReadCallback} [fn] - Function for async version
161
 * @returns {boolean} - Return the read tags
162
 */
163
function read(filebuffer, options, fn) {
164
    if(!options || typeof options === 'function') {
165
        fn = fn || options
166
        options = {}
167
    }
168
    if(isFunction(fn)) {
169
        return readAsync(filebuffer, options, fn)
170
    }
171
    return readSync(filebuffer, options)
172
}
173
174
/**
175
 * Update ID3-Tags from passed buffer/filepath
176
 * @param {object} tags - Object containing tags to be written
177
 * @param {string|Buffer} filebuffer - A filepath string or buffer
178
 * @param {Options} [options] - Object containing options
179
 * @param {WriteCallback} [fn] - Function for async version
180
 * @returns {boolean|Buffer|Error}
181
 */
182
function update(tags, filebuffer, options, fn) {
183
    if(!options || typeof options === 'function') {
184
        fn = fn || options
185
        options = {}
186
    }
187
188
    const rawTags = Object.keys(tags).reduce((acc, val) => {
189
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
190
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
191
        } else {
192
            acc[val] = tags[val]
193
        }
194
        return acc
195
    }, {})
196
197
    const updateFn = (currentTags) => {
198
        currentTags = currentTags.raw || {}
199
        Object.keys(rawTags).map((frameIdentifier) => {
200
            const options = ID3Util.getSpecOptions(frameIdentifier, 3)
201
            const cCompare = {}
202
            if(options.multiple && currentTags[frameIdentifier] && rawTags[frameIdentifier]) {
203
                if(options.updateCompareKey) {
204
                    currentTags[frameIdentifier].forEach((cTag, index) => {
205
                        cCompare[cTag[options.updateCompareKey]] = index
206
                    })
207
208
                }
209
                if (!(rawTags[frameIdentifier] instanceof Array)) {
210
                    rawTags[frameIdentifier] = [rawTags[frameIdentifier]]
211
                }
212
                rawTags[frameIdentifier].forEach((rTag) => {
213
                    const comparison = cCompare[rTag[options.updateCompareKey]]
214
                    if (comparison !== undefined) {
215
                        currentTags[frameIdentifier][comparison] = rTag
216
                    } else {
217
                        currentTags[frameIdentifier].push(rTag)
218
                    }
219
                })
220
            } else {
221
                currentTags[frameIdentifier] = rawTags[frameIdentifier]
222
            }
223
        })
224
        return currentTags
225
    }
226
227
    if(isFunction(fn)) {
228
        return write(updateFn(read(filebuffer, options)), filebuffer, fn)
229
    }
230
    return write(updateFn(read(filebuffer, options)), filebuffer)
231
}
232
233
/**
234
 * @param {string} filepath - Filepath to file
235
 * @returns {boolean|Error}
236
 */
237
function removeTagsSync(filepath) {
238
    let data
239
    try {
240
        data = fs.readFileSync(filepath)
241
    } catch(error) {
242
        return error
243
    }
244
245
    const newData = removeTagsFromBuffer(data)
246
    if(!newData) {
247
        return false
248
    }
249
250
    try {
251
        fs.writeFileSync(filepath, newData, 'binary')
252
    } catch(error) {
253
        return error
254
    }
255
256
    return true
257
}
258
259
/**
260
 * @param {string} filepath - Filepath to file
261
 * @param {WriteCallback} fn - Function for async usage
262
 * @returns {void}
263
 */
264
function removeTagsAsync(filepath, fn) {
265
    fs.readFile(filepath, (error, data) => {
266
        if(error) {
267
            fn(error)
268
            return
269
        }
270
271
        const newData = removeTagsFromBuffer(data)
272
        if(!newData) {
273
            fn(error)
274
            return
275
        }
276
277
        fs.writeFile(filepath, newData, 'binary', (error) => {
278
            if(error) {
279
                fn(error)
280
            } else {
281
                fn(null)
282
            }
283
        })
284
    })
285
}
286
287
/**
288
 * Check and remove already written ID3-Frames from a file
289
 * @param {string} filepath - Filepath to file
290
 * @param {WriteCallback} [fn] - Function for async usage
291
 * @returns {boolean|Error|void}
292
 */
293
function removeTags(filepath, fn) {
294
    if(isFunction(fn)) {
295
        return removeTagsAsync(filepath, fn)
296
    }
297
    return removeTagsSync(filepath)
298
}
299
300
function makeSwapParameters(fn) {
301
    return (a, b) => fn(b, a)
302
}
303
304
// The reorderParameter is a workaround because the callback function
305
// does not have a consistent interface between all the API functions.
306
// Ideally, all the functions should align with the promise style and
307
// always have the result first and the error second.
308
// Changing this would break the current public API.
309
// This could be changed internally and swap the parameter in a light
310
// wrapper when creating the public interface and then remove in a
311
// version 1.0 later with an API breaking change.
312
function makePromise(
313
    fn,
314
    reorderParameters = fn => (a, b) => fn(a, b)
315
) {
316
    return new Promise((resolve, reject) => {
317
        fn(reorderParameters((error, result) => {
318
            if(error) {
319
                reject(error)
320
            } else {
321
                resolve(result)
322
            }
323
        }))
324
    })
325
}
326
327
const PromiseExport = {
328
    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...
329
    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...
330
    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 182 does not use this.
Loading history...
331
    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 163 does not use this.
Loading history...
332
    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 293 does not use this.
Loading history...
333
}
334
335
module.exports = {
336
    TagConstants: ID3Definitions.TagConstants,
337
    create,
338
    write,
339
    update,
340
    read,
341
    removeTags,
342
    removeTagsFromBuffer,
343
    Promise: PromiseExport
344
}
345