UNPKG

30.5 kBJavaScriptView Raw
1const fs = require('fs')
2const iconv = require("iconv-lite")
3
4module.exports = new NodeID3
5
6/*
7** Used specification: http://id3.org/id3v2.3.0
8*/
9
10/*
11** List of official text information frames
12** LibraryName: "T***"
13** Value is the ID of the text frame specified in the link above, the object's keys are just for simplicity, you can also use the ID directly.
14*/
15const TFrames = {
16 album: "TALB",
17 bpm: "TBPM",
18 composer: "TCOM",
19 genre: "TCON",
20 copyright: "TCOP",
21 date: "TDAT",
22 playlistDelay: "TDLY",
23 encodedBy: "TENC",
24 textWriter: "TEXT",
25 fileType: "TFLT",
26 time: "TIME",
27 contentGroup: "TIT1",
28 title: "TIT2",
29 subtitle: "TIT3",
30 initialKey: "TKEY",
31 language: "TLAN",
32 length: "TLEN",
33 mediaType: "TMED",
34 originalTitle: "TOAL",
35 originalFilename: "TOFN",
36 originalTextwriter: "TOLY",
37 originalArtist: "TOPE",
38 originalYear: "TORY",
39 fileOwner: "TOWN",
40 artist: "TPE1",
41 performerInfo: "TPE2",
42 conductor: "TPE3",
43 remixArtist: "TPE4",
44 partOfSet: "TPOS",
45 publisher: "TPUB",
46 trackNumber: "TRCK",
47 recordingDates: "TRDA",
48 internetRadioName: "TRSN",
49 internetRadioOwner: "TRSO",
50 size: "TSIZ",
51 ISRC: "TSRC",
52 encodingTechnology: "TSSE",
53 year: "TYER"
54}
55
56/*
57** List of non-text frames which follow their specific specification
58** name => Frame ID
59** create => function to create the frame
60** read => function to read the frame
61*/
62const SFrames = {
63 comment: {
64 create: "createCommentFrame",
65 read: "readCommentFrame",
66 name: "COMM"
67 },
68 image: {
69 create: "createPictureFrame",
70 read: "readPictureFrame",
71 name: "APIC"
72 },
73 unsynchronisedLyrics: {
74 create: "createUnsynchronisedLyricsFrame",
75 read: "readUnsynchronisedLyricsFrame",
76 name: "USLT"
77 },
78 userDefinedText: {
79 create: "createUserDefinedText",
80 read: "readUserDefinedText",
81 name: "TXXX",
82 multiple: true,
83 updateCompareKey: "description"
84 }
85}
86
87/*
88** Officially available types of the picture frame
89*/
90const APICTypes = [
91 "other",
92 "file icon",
93 "other file icon",
94 "front cover",
95 "back cover",
96 "leaflet page",
97 "media",
98 "lead artist",
99 "artist",
100 "conductor",
101 "band",
102 "composer",
103 "lyricist",
104 "recording location",
105 "during recording",
106 "during performance",
107 "video screen capture",
108 "a bright coloured fish",
109 "illustration",
110 "band logotype",
111 "publisher logotype"
112]
113
114function NodeID3() {
115}
116
117/*
118** Write passed tags to a file/buffer @ filebuffer
119** tags => Object
120** filebuffer => String || Buffer
121** fn => Function (for asynchronous usage)
122*/
123NodeID3.prototype.write = function(tags, filebuffer, fn) {
124 let completeTag = this.create(tags)
125 if(filebuffer instanceof Buffer) {
126 filebuffer = this.removeTagsFromBuffer(filebuffer) || filebuffer
127 let completeBuffer = Buffer.concat([completeTag, filebuffer])
128 if(fn && typeof fn === 'function') {
129 fn(null, completeBuffer)
130 return
131 } else {
132 return completeBuffer
133 }
134 }
135
136 if(fn && typeof fn === 'function') {
137 try {
138 fs.readFile(filebuffer, function(err, data) {
139 if(err) {
140 fn(err)
141 return
142 }
143 data = this.removeTagsFromBuffer(data) || data
144 let rewriteFile = Buffer.concat([completeTag, data])
145 fs.writeFile(filebuffer, rewriteFile, 'binary', (err) => {
146 fn(err)
147 })
148 }.bind(this))
149 } catch(err) {
150 fn(err)
151 }
152 } else {
153 try {
154 let data = fs.readFileSync(filebuffer)
155 data = this.removeTagsFromBuffer(data) || data
156 let rewriteFile = Buffer.concat([completeTag, data])
157 fs.writeFileSync(filebuffer, rewriteFile, 'binary')
158 return true
159 } catch(err) {
160 return err
161 }
162 }
163}
164
165NodeID3.prototype.create = function(tags, fn) {
166 let frames = []
167
168 // Push a header for the ID3-Frame
169 frames.push(this.createTagHeader())
170
171 let tagNames = Object.keys(tags)
172
173 tagNames.forEach(function (tag, index) {
174 // Check if passed tag is text frame (Alias or ID)
175 let frame;
176 if (TFrames[tag] || Object.keys(TFrames).map(i => TFrames[i]).indexOf(tag) != -1) {
177 let specName = TFrames[tag] || tag
178 frame = this.createTextFrame(specName, tags[tag])
179 } else if (SFrames[tag]) { // Check if Alias of special frame
180 let createFrameFunction = SFrames[tag].create
181 frame = this[createFrameFunction](tags[tag])
182 } else if (Object.keys(SFrames).map(i => SFrames[i]).map(x => x.name).indexOf(tag) != -1) { // Check if ID of special frame
183 // get create function from special frames where tag ID is found at SFrame[index].name
184 let createFrameFunction = SFrames[Object.keys(SFrames)[Object.keys(SFrames).map(i => SFrames[i]).map(x => x.name).indexOf(tag)]].create
185 frame = this[createFrameFunction](tags[tag])
186 }
187
188 if (frame instanceof Buffer) {
189 frames.push(frame)
190 }
191 }.bind(this))
192
193 // Calculate frame size of ID3 body to insert into header
194
195 let totalSize = 0
196 frames.forEach((frame) => {
197 totalSize += frame.length
198 })
199
200 // Don't count ID3 header itself
201 totalSize -= 10
202 // ID3 header size uses only 7 bits of a byte, bit shift is needed
203 let size = this.encodeSize(totalSize)
204
205 // Write bytes to ID3 frame header, which is the first frame
206 frames[0].writeUInt8(size[0], 6)
207 frames[0].writeUInt8(size[1], 7)
208 frames[0].writeUInt8(size[2], 8)
209 frames[0].writeUInt8(size[3], 9)
210
211 if(fn && typeof fn === 'function') {
212 fn(Buffer.concat(frames))
213 } else {
214 return Buffer.concat(frames)
215 }
216}
217
218/*
219** Read ID3-Tags from passed buffer/filepath
220** filebuffer => Buffer || String
221** options => Object
222** fn => function (for asynchronous usage)
223*/
224NodeID3.prototype.read = function(filebuffer, options, fn) {
225 if(!options || typeof options === 'function') {
226 fn = fn || options
227 options = {}
228 }
229 if(!fn || typeof fn !== 'function') {
230 if(typeof filebuffer === "string" || filebuffer instanceof String) {
231 filebuffer = fs.readFileSync(filebuffer)
232 }
233 let tags = this.getTagsFromBuffer(filebuffer, options)
234 return tags
235 } else {
236 if(typeof filebuffer === "string" || filebuffer instanceof String) {
237 fs.readFile(filebuffer, function(err, data) {
238 if(err) {
239 fn(err, null)
240 } else {
241 let tags = this.getTagsFromBuffer(data, options)
242 fn(null, tags)
243 }
244 }.bind(this))
245 }
246 }
247}
248
249/*
250** Update ID3-Tags from passed buffer/filepath
251** filebuffer => Buffer || String
252** tags => Object
253** fn => function (for asynchronous usage)
254*/
255NodeID3.prototype.update = function(tags, filebuffer, fn) {
256 let rawTags = {}
257 let SRawToNameMap = {}
258 Object.keys(SFrames).map((key, index) => {
259 SRawToNameMap[SFrames[key].name] = key
260 })
261 Object.keys(tags).map(function(tagKey) {
262 // if js name passed (TF)
263 if(TFrames[tagKey]) {
264 rawTags[TFrames[tagKey]] = tags[tagKey]
265
266 // if js name passed (SF)
267 } else if(SFrames[tagKey]) {
268 rawTags[SFrames[tagKey].name] = tags[tagKey]
269
270 // if raw name passed (TF)
271 } else if(Object.keys(TFrames).map(i => TFrames[i]).indexOf(tagKey) !== -1) {
272 rawTags[tagKey] = tags[tagKey]
273
274 // if raw name passed (SF)
275 } else if(Object.keys(SFrames).map(i => SFrames[i]).map(x => x.name).indexOf(tagKey) !== -1) {
276 rawTags[tagKey] = tags[tagKey]
277 }
278 })
279 if(!fn || typeof fn !== 'function') {
280 let currentTags = this.read(filebuffer)
281 currentTags = currentTags.raw || {}
282 // update current tags with new or keep them
283 Object.keys(rawTags).map(function(tag) {
284 if(SFrames[SRawToNameMap[tag]] && SFrames[SRawToNameMap[tag]].multiple && currentTags[tag] && rawTags[tag]) {
285 cCompare = {}
286 currentTags[tag].forEach((cTag, index) => {
287 cCompare[cTag[SFrames[SRawToNameMap[tag]].updateCompareKey]] = index
288 })
289 if(!(rawTags[tag] instanceof Array)) rawTags[tag] = [rawTags[tag]]
290 rawTags[tag].forEach((rTag, index) => {
291 let comparison = cCompare[rTag[SFrames[SRawToNameMap[tag]].updateCompareKey]]
292 if(comparison !== undefined) {
293 currentTags[tag][comparison] = rTag
294 } else {
295 currentTags[tag].push(rTag)
296 }
297 })
298 } else {
299 currentTags[tag] = rawTags[tag]
300 }
301 })
302 return this.write(currentTags, filebuffer)
303 } else {
304 this.read(filebuffer, function(err, currentTags) {
305 if(err) {
306 fn(err)
307 return
308 }
309 currentTags = currentTags.raw || {}
310 // update current tags with new or keep them
311 Object.keys(rawTags).map(function(tag) {
312 if(SFrames[SRawToNameMap[tag]] && SFrames[SRawToNameMap[tag]].multiple && currentTags[tag] && rawTags[tag]) {
313 cCompare = {}
314 currentTags[tag].forEach((cTag, index) => {
315 cCompare[cTag[SFrames[SRawToNameMap[tag]].updateCompareKey]] = index
316 })
317 if(!(rawTags[tag] instanceof Array)) rawTags[tag] = [rawTags[tag]]
318 rawTags[tag].forEach((rTag, index) => {
319 let comparison = cCompare[rTag[SFrames[SRawToNameMap[tag]].updateCompareKey]]
320 if(comparison !== undefined) {
321 currentTags[tag][comparison] = rTag
322 } else {
323 currentTags[tag].push(rTag)
324 }
325 })
326 } else {
327 currentTags[tag] = rawTags[tag]
328 }
329 })
330 this.write(currentTags, filebuffer, fn)
331 }.bind(this))
332 }
333}
334
335/*
336** Read ID3-Tags from passed buffer
337** filebuffer => Buffer
338** options => Object
339*/
340NodeID3.prototype.getTagsFromBuffer = function(filebuffer, options) {
341 let framePosition = this.getFramePosition(filebuffer)
342 if(framePosition === -1) {
343 return false
344 }
345 let frameSize = this.getTagSize(Buffer.from(filebuffer.toString('hex', framePosition, framePosition + 10), "hex")) + 10
346 let ID3Frame = Buffer.alloc(frameSize + 1)
347 let ID3FrameBody = Buffer.alloc(frameSize - 10 + 1)
348 filebuffer.copy(ID3Frame, 0, framePosition)
349 filebuffer.copy(ID3FrameBody, 0, framePosition + 10)
350
351 //ID3 version e.g. 3 if ID3v2.3.0
352 let ID3Version = ID3Frame[3]
353
354 // Now, get frame for frame by given size to support unkown tags etc.
355 let frames = []
356 let tags = { raw: {} }
357 let currentPosition = 0
358 while(currentPosition < frameSize - 10 && ID3FrameBody[currentPosition] !== 0x00) {
359 let bodyFrameHeader = Buffer.alloc(10)
360 ID3FrameBody.copy(bodyFrameHeader, 0, currentPosition)
361
362 let decodeSize = false
363 if(ID3Version == 4) {
364 decodeSize = true
365 }
366 let bodyFrameSize = this.getFrameSize(bodyFrameHeader, decodeSize)
367 let bodyFrameBuffer = Buffer.alloc(bodyFrameSize)
368 ID3FrameBody.copy(bodyFrameBuffer, 0, currentPosition + 10)
369 // Size of sub frame + its header
370 currentPosition += bodyFrameSize + 10
371 frames.push({
372 name: bodyFrameHeader.toString('utf8', 0, 4),
373 body: bodyFrameBuffer
374 })
375 }
376
377 frames.forEach(function(frame, index) {
378 // Check first character if frame is text frame
379 if(frame.name[0] === "T" && frame.name !== "TXXX") {
380 // Decode body
381 let decoded
382 if(frame.body[0] === 0x01) {
383 decoded = iconv.decode(frame.body.slice(1), "utf16").replace(/\0/g, "")
384 } else {
385 decoded = iconv.decode(frame.body.slice(1), "ISO-8859-1").replace(/\0/g, "")
386 }
387 tags.raw[frame.name] = decoded
388 Object.keys(TFrames).map(function(key) {
389 if(TFrames[key] === frame.name) {
390 tags[key] = decoded
391 }
392 })
393 } else {
394 // Check if non-text frame is supported
395 Object.keys(SFrames).map(function(key) {
396 if(SFrames[key].name === frame.name) {
397 let decoded = this[SFrames[key].read](frame.body)
398 if(SFrames[key].multiple) {
399 if(!tags[key]) tags[key] = []
400 if(!tags.raw[frame.name]) tags.raw[frame.name] = []
401 tags.raw[frame.name].push(decoded)
402 tags[key].push(decoded)
403 } else {
404 tags.raw[frame.name] = decoded
405 tags[key] = decoded
406 }
407 }
408 }.bind(this))
409 }
410 }.bind(this))
411
412 return tags
413}
414
415/*
416** Get position of ID3-Frame, returns -1 if not found
417** buffer => Buffer
418*/
419NodeID3.prototype.getFramePosition = function(buffer) {
420 let framePosition = buffer.indexOf("ID3")
421 if(framePosition == -1 || framePosition > 20) {
422 return -1
423 } else {
424 return framePosition
425 }
426}
427
428/*
429** Get size of tag from header
430** buffer => Buffer/Array (header)
431*/
432NodeID3.prototype.getTagSize = function(buffer) {
433 return this.decodeSize(Buffer.from([buffer[6], buffer[7], buffer[8], buffer[9]]))
434}
435
436/*
437** Get size of frame from header
438** buffer => Buffer/Array (header)
439** decode => Boolean
440*/
441NodeID3.prototype.getFrameSize = function(buffer, decode) {
442 if(decode) {
443 return this.decodeSize(Buffer.from([buffer[4], buffer[5], buffer[6], buffer[7]]))
444 } else {
445 return Buffer.from([buffer[4], buffer[5], buffer[6], buffer[7]]).readUIntBE(0, 4)
446 }
447}
448
449/*
450** Checks and removes already written ID3-Frames from a buffer
451** data => buffer
452*/
453NodeID3.prototype.removeTagsFromBuffer = function(data) {
454 let framePosition = this.getFramePosition(data)
455
456 if(framePosition == -1) {
457 return data
458 }
459
460 let hSize = Buffer.from([data[framePosition + 6], data[framePosition + 7], data[framePosition + 8], data[framePosition + 9]])
461
462 if ((hSize[0] | hSize[1] | hSize[2] | hSize[3]) & 0x80) {
463 // Invalid tag size (msb not 0)
464 return false;
465 }
466
467 let size = this.decodeSize(hSize)
468 return data.slice(framePosition + size + 10);
469}
470
471/*
472** Checks and removes already written ID3-Frames from a file
473** data => buffer
474*/
475NodeID3.prototype.removeTags = function(filepath, fn) {
476 if(!fn || typeof fn !== 'function') {
477 let data;
478 try {
479 data = fs.readFileSync(filepath)
480 } catch(e) {
481 return e
482 }
483
484 let newData = this.removeTagsFromBuffer(data)
485 if(!newData) {
486 return false
487 }
488
489 try {
490 fs.writeFileSync(filepath, newData, 'binary')
491 } catch(e) {
492 return e
493 }
494
495 return true
496 } else {
497 fs.readFile(filepath, function(err, data) {
498 if(err) {
499 fn(err)
500 }
501
502 let newData = this.removeTagsFromBuffer(data)
503 if(!newData) {
504 fn(err)
505 return
506 }
507
508 fs.writeFile(filepath, newData, 'binary', function(err) {
509 if(err) {
510 fn(err)
511 } else {
512 fn(false)
513 }
514 })
515 }.bind(this))
516 }
517}
518
519/*
520** This function ensures that the msb of each byte is 0
521** totalSize => int
522*/
523NodeID3.prototype.encodeSize = function(totalSize) {
524 let byte_3 = totalSize & 0x7F
525 let byte_2 = (totalSize >> 7) & 0x7F
526 let byte_1 = (totalSize >> 14) & 0x7F
527 let byte_0 = (totalSize >> 21) & 0x7F
528 return ([byte_0, byte_1, byte_2, byte_3])
529}
530
531
532/*
533** This function decodes the 7-bit size structure
534** hSize => int
535*/
536NodeID3.prototype.decodeSize = function(hSize) {
537 return ((hSize[0] << 21) + (hSize[1] << 14) + (hSize[2] << 7) + (hSize[3]))
538}
539
540/*
541** Create header for ID3-Frame v2.3.0
542*/
543NodeID3.prototype.createTagHeader = function() {
544 let header = Buffer.alloc(10)
545 header.fill(0)
546 header.write("ID3", 0) //File identifier
547 header.writeUInt16BE(0x0300, 3) //Version 2.3.0 -- 03 00
548 header.writeUInt16BE(0x0000, 5) //Flags 00
549
550 //Last 4 bytes are used for header size, but have to be inserted later, because at this point, its size is not clear.
551
552 return header;
553}
554
555/*
556** Create text frame
557** specName => string (ID)
558** text => string (body)
559*/
560NodeID3.prototype.createTextFrame = function(specName, text) {
561 if(!specName || !text) {
562 return null
563 }
564
565 let encoded = iconv.encode(text, "utf16")
566
567 let buffer = Buffer.alloc(10)
568 buffer.fill(0)
569 buffer.write(specName, 0) // ID of the specified frame
570 buffer.writeUInt32BE((encoded).length + 1, 4) // Size of frame (string length + encoding byte)
571 let encBuffer = Buffer.alloc(1) // Encoding (now using UTF-16 encoded w/ BOM)
572 encBuffer.fill(1) // UTF-16
573
574 var contentBuffer = Buffer.from(encoded, 'binary') // Text -> Binary encoding for UTF-16 w/ BOM
575 return Buffer.concat([buffer, encBuffer, contentBuffer])
576}
577
578/*
579** data => string || buffer
580*/
581NodeID3.prototype.createPictureFrame = function(data) {
582 try {
583 if(data && data.imageBuffer && data.imageBuffer instanceof Buffer === true) {
584 data = data.imageBuffer
585 }
586 let apicData = (data instanceof Buffer == true) ? Buffer.from(data) : Buffer.from(fs.readFileSync(data, 'binary'), 'binary')
587 let bHeader = Buffer.alloc(10)
588 bHeader.fill(0)
589 bHeader.write("APIC", 0)
590
591 let mime_type = "image/png"
592
593 if(apicData[0] == 0xff && apicData[1] == 0xd8 && apicData[2] == 0xff) {
594 mime_type = "image/jpeg"
595 }
596
597 let bContent = Buffer.alloc(mime_type.length + 4)
598 bContent.fill(0)
599 bContent[mime_type.length + 2] = 0x03 // Front cover
600 bContent.write(mime_type, 1)
601
602 bHeader.writeUInt32BE(apicData.length + bContent.length, 4) // Size of frame
603
604 return Buffer.concat([bHeader, bContent, apicData])
605 } catch(e) {
606 return e
607 }
608}
609
610/*
611** data => buffer
612*/
613NodeID3.prototype.readPictureFrame = function(APICFrame) {
614 let picture = {}
615 let APICMimeType = APICFrame.toString('ascii').substring(1, APICFrame.indexOf(0x00, 1))
616 if(APICMimeType == "image/jpeg") {
617 picture.mime = "jpeg"
618 } else if(APICMimeType == "image/png") {
619 picture.mime = "png"
620 }
621 picture.type = {
622 id: APICFrame[APICFrame.indexOf(0x00, 1) + 1],
623 name: APICTypes[APICFrame[APICFrame.indexOf(0x00, 1) + 1]]
624 }
625 let descEnd;
626 if(APICFrame[0] == 0x00) {
627 picture.description = iconv.decode(APICFrame.slice(APICFrame.indexOf(0x00, 1) + 2, APICFrame.indexOf(0x00, APICFrame.indexOf(0x00, 1) + 2)), "ISO-8859-1") || undefined
628 descEnd = APICFrame.indexOf(0x00, APICFrame.indexOf(0x00, 1) + 2)
629 } else if (APICFrame[0] == 0x01) {
630 let descOffset = APICFrame.indexOf(0x00, 1) + 2
631 let desc = APICFrame.slice(descOffset)
632 let descFound = desc.indexOf("0000", 0, 'hex')
633 descEnd = descOffset + descFound + 2
634
635 if(descFound != -1) {
636 picture.description = iconv.decode(desc.slice(0, descFound + 2), 'utf16') || undefined
637 }
638 }
639 if(descEnd) {
640 picture.imageBuffer = APICFrame.slice(descEnd + 1)
641 } else {
642 picture.imageBuffer = APICFrame.slice(APICFrame.indexOf(0x00, 1) + 2)
643 }
644
645 return picture
646}
647
648NodeID3.prototype.getEncodingByte = function(encoding) {
649 if(!encoding || encoding === 0x00 || encoding === "ISO-8859-1") {
650 return 0x00
651 } else {
652 return 0x01
653 }
654}
655
656NodeID3.prototype.getEncodingName = function(encoding) {
657 if(this.getEncodingByte(encoding) === 0x00) {
658 return "ISO-8859-1"
659 } else {
660 return "utf16"
661 }
662}
663
664NodeID3.prototype.getTerminationCount = function(encoding) {
665 if(encoding === 0x00) {
666 return 1
667 } else {
668 return 2
669 }
670}
671
672NodeID3.prototype.createTextEncoding = function(encoding) {
673 let buffer = Buffer.alloc(1)
674 buffer[0] = this.getEncodingByte(encoding)
675 return buffer
676}
677
678NodeID3.prototype.createLanguage = function(language) {
679 if(!language) {
680 language = "eng"
681 } else if(language.length > 3) {
682 language = language.substring(0, 3)
683 }
684
685 return Buffer.from(language)
686}
687
688NodeID3.prototype.createContentDescriptor = function(description, encoding, terminated) {
689 if(!description) {
690 description = terminated ? iconv.encode("\0", this.getEncodingName(encoding)) : Buffer.alloc(0)
691 return description
692 }
693
694 description = iconv.encode(description, this.getEncodingName(encoding))
695
696 return terminated ? Buffer.concat([description, Buffer.alloc(this.getTerminationCount(encoding)).fill(0x00)]) : description
697}
698
699NodeID3.prototype.createText = function(text, encoding, terminated) {
700 if(!text) {
701 text = ""
702 }
703
704 text = iconv.encode(text, this.getEncodingName(encoding))
705
706 return terminated ? Buffer.concat([text, Buffer.from(this.getTerminationCount(encoding)).fill(0x00)]) : text
707}
708
709/*
710** comment => object {
711** language: string (3 characters),
712** text: string
713** shortText: string
714** }
715**/
716NodeID3.prototype.createCommentFrame = function(comment) {
717 comment = comment || {}
718 if(!comment.text) {
719 return null
720 }
721
722 // Create frame header
723 let buffer = Buffer.alloc(10)
724 buffer.fill(0)
725 buffer.write("COMM", 0) // Write header ID
726
727 let encodingBuffer = this.createTextEncoding(0x01)
728 let languageBuffer = this.createLanguage(comment.language)
729 let descriptorBuffer = this.createContentDescriptor(comment.shortText, 0x01, true)
730 let textBuffer = this.createText(comment.text, 0x01, false)
731
732 buffer.writeUInt32BE(encodingBuffer.length + languageBuffer.length + descriptorBuffer.length + textBuffer.length, 4)
733 return Buffer.concat([buffer, encodingBuffer, languageBuffer, descriptorBuffer, textBuffer])
734}
735
736/*
737** frame => Buffer
738*/
739NodeID3.prototype.readCommentFrame = function(frame) {
740 let tags = {}
741
742 if(!frame) {
743 return tags
744 }
745 if(frame[0] == 0x00) {
746 tags = {
747 language: iconv.decode(frame, "ISO-8859-1").substring(1, 4).replace(/\0/g, ""),
748 shortText: iconv.decode(frame, "ISO-8859-1").substring(4, frame.indexOf(0x00, 1)).replace(/\0/g, ""),
749 text: iconv.decode(frame, "ISO-8859-1").substring(frame.indexOf(0x00, 1) + 1).replace(/\0/g, "")
750 }
751 } else if(frame[0] == 0x01) {
752 let descriptorEscape = 0
753 while(frame[descriptorEscape] !== undefined && frame[descriptorEscape] !== 0x00 || frame[descriptorEscape + 1] !== 0x00 || frame[descriptorEscape + 2] === 0x00) {
754 descriptorEscape++
755 }
756 if(frame[descriptorEscape] === undefined) {
757 return tags
758 }
759 let shortText = frame.slice(4, descriptorEscape)
760 let text = frame.slice(descriptorEscape + 2)
761
762 tags = {
763 language: frame.toString().substring(1, 4).replace(/\0/g, ""),
764 shortText: iconv.decode(shortText, "utf16").replace(/\0/g, ""),
765 text: iconv.decode(text, "utf16").replace(/\0/g, "")
766 }
767 }
768
769 return tags
770}
771
772/*
773** unsynchronisedLyrics => object {
774** language: string (3 characters),
775** text: string
776** shortText: string
777** }
778**/
779NodeID3.prototype.createUnsynchronisedLyricsFrame = function(unsynchronisedLyrics) {
780 unsynchronisedLyrics = unsynchronisedLyrics || {}
781 if(typeof unsynchronisedLyrics === 'string' || unsynchronisedLyrics instanceof String) {
782 unsynchronisedLyrics = {
783 text: unsynchronisedLyrics
784 }
785 }
786 if(!unsynchronisedLyrics.text) {
787 return null
788 }
789
790 // Create frame header
791 let buffer = Buffer.alloc(10)
792 buffer.fill(0)
793 buffer.write("USLT", 0) // Write header ID
794
795 let encodingBuffer = this.createTextEncoding(0x01)
796 let languageBuffer = this.createLanguage(unsynchronisedLyrics.language)
797 let descriptorBuffer = this.createContentDescriptor(unsynchronisedLyrics.shortText, 0x01, true)
798 let textBuffer = this.createText(unsynchronisedLyrics.text, 0x01, false)
799
800 buffer.writeUInt32BE(encodingBuffer.length + languageBuffer.length + descriptorBuffer.length + textBuffer.length, 4)
801 return Buffer.concat([buffer, encodingBuffer, languageBuffer, descriptorBuffer, textBuffer])
802}
803
804/*
805** frame => Buffer
806*/
807NodeID3.prototype.readUnsynchronisedLyricsFrame = function(frame) {
808 let tags = {}
809
810 if(!frame) {
811 return tags
812 }
813 if(frame[0] == 0x00) {
814 tags = {
815 language: iconv.decode(frame, "ISO-8859-1").substring(1, 4).replace(/\0/g, ""),
816 shortText: iconv.decode(frame, "ISO-8859-1").substring(4, frame.indexOf(0x00, 1)).replace(/\0/g, ""),
817 text: iconv.decode(frame, "ISO-8859-1").substring(frame.indexOf(0x00, 1) + 1).replace(/\0/g, "")
818 }
819 } else if(frame[0] == 0x01) {
820 let descriptorEscape = 0
821 while(frame[descriptorEscape] !== undefined && frame[descriptorEscape] !== 0x00 || frame[descriptorEscape + 1] !== 0x00 || frame[descriptorEscape + 2] === 0x00) {
822 descriptorEscape++
823 }
824 if(frame[descriptorEscape] === undefined) {
825 return tags
826 }
827 let shortText = frame.slice(4, descriptorEscape)
828 let text = frame.slice(descriptorEscape + 2)
829
830 tags = {
831 language: frame.toString().substring(1, 4).replace(/\0/g, ""),
832 shortText: iconv.decode(shortText, "utf16").replace(/\0/g, ""),
833 text: iconv.decode(text, "utf16").replace(/\0/g, "")
834 }
835 }
836
837 return tags
838}
839
840/*
841** comment => object / array of objects {
842** description: string
843** value: string
844** }
845**/
846NodeID3.prototype.createUserDefinedText = function(userDefinedText, recursiveBuffer) {
847 udt = userDefinedText || {}
848 if(udt instanceof Array && udt.length > 0) {
849 if(!recursiveBuffer) {
850 // Don't alter passed array value!
851 userDefinedText = userDefinedText.slice(0)
852 }
853 udt = userDefinedText.pop()
854 }
855
856 if(udt && udt.description) {
857 // Create frame header
858 let buffer = Buffer.alloc(10)
859 buffer.fill(0)
860 buffer.write("TXXX", 0) // Write header ID
861
862 let encodingBuffer = this.createTextEncoding(0x01)
863 let descriptorBuffer = this.createContentDescriptor(udt.description, 0x01, true)
864 let valueBuffer = this.createText(udt.value, 0x01, false)
865
866 buffer.writeUInt32BE(encodingBuffer.length + descriptorBuffer.length + valueBuffer.length, 4)
867 if(!recursiveBuffer) {
868 recursiveBuffer = Buffer.concat([buffer, encodingBuffer, descriptorBuffer, valueBuffer])
869 } else {
870 recursiveBuffer = Buffer.concat([recursiveBuffer, buffer, encodingBuffer, descriptorBuffer, valueBuffer])
871 }
872 }
873 if(userDefinedText instanceof Array && userDefinedText.length > 0) {
874 return this.createUserDefinedText(userDefinedText, recursiveBuffer)
875 } else {
876 return recursiveBuffer
877 }
878}
879
880/*
881** frame => Buffer
882*/
883NodeID3.prototype.readUserDefinedText = function(frame) {
884 let tags = {}
885
886 if(!frame) {
887 return tags
888 }
889 if(frame[0] == 0x00) {
890 tags = {
891 description: iconv.decode(frame, "ISO-8859-1").substring(1, frame.indexOf(0x00, 1)).replace(/\0/g, ""),
892 value: iconv.decode(frame, "ISO-8859-1").substring(frame.indexOf(0x00, 1) + 1).replace(/\0/g, "")
893 }
894 } else if(frame[0] == 0x01) {
895 let descriptorEscape = 0
896 while(frame[descriptorEscape] !== undefined && frame[descriptorEscape] !== 0x00 || frame[descriptorEscape + 1] !== 0x00 || frame[descriptorEscape + 2] === 0x00) {
897 descriptorEscape++
898 }
899 if(frame[descriptorEscape] === undefined) {
900 return tags
901 }
902 let description = frame.slice(1, descriptorEscape)
903 let value = frame.slice(descriptorEscape + 2)
904
905 tags = {
906 description: iconv.decode(description, "utf16").replace(/\0/g, ""),
907 value: iconv.decode(value, "utf16").replace(/\0/g, "")
908 }
909 }
910
911 return tags
912}
\No newline at end of file