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