OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 importScripts('function_sequence.js'); | |
6 importScripts('function_parallel.js'); | |
7 importScripts('util.js'); | |
8 | |
9 function Id3Parser(parent) { | |
10 MetadataParser.call(this, parent, 'id3', /\.(mp3)$/i); | |
11 } | |
12 | |
13 Id3Parser.prototype = {__proto__: MetadataParser.prototype}; | |
14 | |
15 /** | |
16 * Reads synchsafe integer. | |
17 * 'SynchSafe' term is taken from id3 documentation. | |
18 * | |
19 * @param {ByteReader} reader - reader to use | |
20 * @param {int} length - bytes to read | |
21 * @return {int} | |
22 */ | |
23 Id3Parser.readSynchSafe_ = function(reader, length) { | |
24 var rv = 0; | |
25 | |
26 switch (length) { | |
27 case 4: | |
28 rv = reader.readScalar(1, false) << 21; | |
29 case 3: | |
30 rv |= reader.readScalar(1, false) << 14; | |
31 case 2: | |
32 rv |= reader.readScalar(1, false) << 7; | |
33 case 1: | |
34 rv |= reader.readScalar(1, false); | |
35 } | |
36 | |
37 return rv; | |
38 }; | |
39 | |
40 /** | |
41 * Reads 3bytes integer. | |
42 * | |
43 * @param {ByteReader} reader - reader to use | |
44 * @return {int} | |
45 */ | |
46 Id3Parser.readUInt24_ = function(reader) { | |
47 return reader.readScalar(2, false) << 16 | reader.readScalar(1, false); | |
48 }; | |
49 | |
50 /** | |
51 * Reads string from reader with specified encoding | |
52 * | |
53 * @param {ByteReader} reader reader to use | |
54 * @param {int} encoding string encoding. | |
55 * @param {int} size maximum string size. Actual result may be shorter. | |
56 * | |
57 */ | |
58 Id3Parser.prototype.readString_ = function(reader, encoding, size) { | |
59 switch (encoding) { | |
60 case Id3Parser.v2.ENCODING.ISO_8859_1: | |
61 return reader.readNullTerminatedString(size); | |
62 | |
63 case Id3Parser.v2.ENCODING.UTF_16: | |
64 return reader.readNullTerminatedStringUTF16(true, size); | |
65 | |
66 case Id3Parser.v2.ENCODING.UTF_16BE: | |
67 return reader.readNullTerminatedStringUTF16(false, size); | |
68 | |
69 case Id3Parser.v2.ENCODING.UTF_8: | |
70 // TODO: implement UTF_8. | |
71 this.log('UTF8 encoding not supported, used ISO_8859_1 instead'); | |
72 return reader.readNullTerminatedString(size); | |
73 | |
74 default: { | |
75 this.log('Unsupported encoding in ID3 tag: ' + encoding); | |
76 return ''; | |
77 } | |
78 } | |
79 }; | |
80 | |
81 /** | |
82 * Reads text frame from reader. | |
83 * | |
84 * @param {ByteReader} reader reader to use | |
85 * @param {int} majorVersion major id3 version to use | |
86 * @param {Object} frame frame so store data at | |
87 * @param {int} end frame end position in reader | |
88 */ | |
89 Id3Parser.prototype.readTextFrame_ = function(reader, | |
90 majorVersion, | |
91 frame, | |
92 end) { | |
93 frame.encoding = reader.readScalar(1, false, end); | |
94 frame.value = this.readString_(reader, frame.encoding, end - reader.tell()); | |
95 }; | |
96 | |
97 /** | |
98 * Reads user defined text frame from reader. | |
99 * | |
100 * @param {ByteReader} reader reader to use | |
101 * @param {int} majorVersion major id3 version to use | |
102 * @param {Object} frame frame so store data at | |
103 * @param {int} end frame end position in reader | |
104 */ | |
105 Id3Parser.prototype.readUserDefinedTextFrame_ = function(reader, | |
106 majorVersion, | |
107 frame, | |
108 end) { | |
109 frame.encoding = reader.readScalar(1, false, end); | |
110 | |
111 frame.description = this.readString_( | |
112 reader, | |
113 frame.encoding, | |
114 end - reader.tell()); | |
115 | |
116 frame.value = this.readString_( | |
117 reader, | |
118 frame.encoding, | |
119 end - reader.tell()); | |
120 }; | |
121 | |
122 Id3Parser.prototype.readPIC_ = function(reader, majorVersion, frame, end) { | |
123 frame.encoding = reader.readScalar(1, false, end); | |
124 frame.format = reader.readNullTerminatedString(3, end - reader.tell()); | |
125 frame.pictureType = reader.readScalar(1, false, end); | |
126 frame.description = this.readString_(reader, | |
127 frame.encoding, | |
128 end - reader.tell()); | |
129 | |
130 | |
131 if (frame.format == '-->') { | |
132 frame.imageUrl = reader.readNullTerminatedString(end - reader.tell()); | |
133 } else { | |
134 frame.imageUrl = reader.readImage(end - reader.tell()); | |
135 } | |
136 }; | |
137 | |
138 Id3Parser.prototype.readAPIC_ = function(reader, majorVersion, frame, end) { | |
139 this.vlog('Extracting picture'); | |
140 frame.encoding = reader.readScalar(1, false, end); | |
141 frame.mime = reader.readNullTerminatedString(end - reader.tell()); | |
142 frame.pictureType = reader.readScalar(1, false, end); | |
143 frame.description = this.readString_( | |
144 reader, | |
145 frame.encoding, | |
146 end - reader.tell()); | |
147 | |
148 if (frame.mime == '-->') { | |
149 frame.imageUrl = reader.readNullTerminatedString(end - reader.tell()); | |
150 } else { | |
151 frame.imageUrl = reader.readImage(end - reader.tell()); | |
152 } | |
153 }; | |
154 | |
155 /** | |
156 * Reads string from reader with specified encoding | |
157 * | |
158 * @param {ByteReader} reader reader to use | |
159 * @return {Object} frame read | |
160 */ | |
161 Id3Parser.prototype.readFrame_ = function(reader, majorVersion) { | |
162 if (reader.eof()) | |
163 return null; | |
164 | |
165 var frame = {}; | |
166 | |
167 reader.pushSeek(reader.tell(), ByteReader.SEEK_BEG); | |
168 | |
169 var position = reader.tell(); | |
170 | |
171 frame.name = (majorVersion == 2) | |
172 ? reader.readNullTerminatedString(3) | |
173 : reader.readNullTerminatedString(4); | |
174 | |
175 if (frame.name == '') | |
176 return null; | |
177 | |
178 this.vlog('Found frame ' + (frame.name) + ' at position ' + position ); | |
179 | |
180 switch (majorVersion) { | |
181 case 2: | |
182 frame.size = Id3Parser.readUInt24_(reader); | |
183 frame.headerSize = 6; | |
184 break; | |
185 case 3: | |
186 frame.size = reader.readScalar(4, false); | |
187 frame.headerSize = 10; | |
188 frame.flags = reader.readScalar(2, false); | |
189 break; | |
190 case 4: | |
191 frame.size = Id3Parser.readSynchSafe_(reader, 4); | |
192 frame.headerSize = 10; | |
193 frame.flags = reader.readScalar(2, false); | |
194 break; | |
195 } | |
196 | |
197 this.vlog('Found frame [' + frame.name + '] with size ['+frame.size+']'); | |
198 | |
199 if (Id3Parser.v2.HANDLERS[frame.name]) { | |
200 Id3Parser.v2.HANDLERS[frame.name].call( | |
201 this, | |
202 reader, | |
203 majorVersion, | |
204 frame, | |
205 reader.tell() + frame.size); | |
206 } else if (frame.name.charAt(0) == 'T' || frame.name.charAt(0) == 'W') { | |
207 this.readTextFrame_( | |
208 reader, | |
209 majorVersion, | |
210 frame, | |
211 reader.tell() + frame.size); | |
212 } | |
213 | |
214 reader.popSeek(); | |
215 | |
216 reader.seek(frame.size + frame.headerSize, ByteReader.SEEK_CUR); | |
217 | |
218 return frame; | |
219 }; | |
220 | |
221 Id3Parser.prototype.parse = function (file, metadata, callback, onError) { | |
222 var self = this; | |
223 | |
224 this.log('Starting id3 parser for ' + file.name); | |
225 | |
226 var id3v1Parser = new FunctionSequence( | |
227 'id3v1parser', | |
228 [ | |
229 /** | |
230 * Reads last 128 bytes of file in bytebuffer, | |
231 * which passes further. | |
232 * In last 128 bytes should be placed ID3v1 tag if available. | |
233 * @param file - file which bytes to read. | |
234 */ | |
235 function readTail(file) { | |
236 util.readFileBytes(file, file.size - 128, file.size, | |
237 this.nextStep, this.onError, this); | |
238 }, | |
239 | |
240 /** | |
241 * Attempts to extract ID3v1 tag from 128 bytes long ByteBuffer | |
242 * @param file file which tags are being extracted. | |
243 * Could be used for logging purposes. | |
244 * @param {ByteReader} reader ByteReader of 128 bytes. | |
245 */ | |
246 function extractId3v1(file, reader) { | |
247 if ( reader.readString(3) == 'TAG') { | |
248 this.logger.vlog('id3v1 found'); | |
249 var id3v1 = metadata.id3v1 = {}; | |
250 | |
251 var title = reader.readNullTerminatedString(30).trim(); | |
252 | |
253 if (title.length > 0) { | |
254 metadata.title = title; | |
255 } | |
256 | |
257 reader.seek(3 + 30, ByteReader.SEEK_BEG); | |
258 | |
259 var artist = reader.readNullTerminatedString(30).trim(); | |
260 if (artist.length > 0) { | |
261 metadata.artist = artist; | |
262 } | |
263 | |
264 reader.seek(3 + 30 + 30, ByteReader.SEEK_BEG); | |
265 | |
266 var album = reader.readNullTerminatedString(30).trim(); | |
267 if (album.length > 0) { | |
268 metadata.album = album; | |
269 } | |
270 } | |
271 this.nextStep(); | |
272 } | |
273 ], | |
274 this | |
275 ); | |
276 | |
277 var id3v2Parser = new FunctionSequence( | |
278 'id3v2parser', | |
279 [ | |
280 function readHead(file) { | |
281 util.readFileBytes(file, 0, 10, this.nextStep, this.onError, | |
282 this); | |
283 }, | |
284 | |
285 /** | |
286 * Check if passed array of 10 bytes contains ID3 header. | |
287 * @param file to check and continue reading if ID3 metadata found | |
288 * @param {ByteReader} reader reader to fill with stream bytes. | |
289 */ | |
290 function checkId3v2(file, reader) { | |
291 if (reader.readString(3) == 'ID3') { | |
292 this.logger.vlog('id3v2 found'); | |
293 var id3v2 = metadata.id3v2 = {}; | |
294 id3v2.major = reader.readScalar(1, false); | |
295 id3v2.minor = reader.readScalar(1, false); | |
296 id3v2.flags = reader.readScalar(1, false); | |
297 id3v2.size = Id3Parser.readSynchSafe_(reader, 4); | |
298 | |
299 util.readFileBytes(file, 10, 10 + id3v2.size, this.nextStep, | |
300 this.onError, this); | |
301 } else { | |
302 this.finish(); | |
303 } | |
304 }, | |
305 | |
306 /** | |
307 * Extracts all ID3v2 frames from given bytebuffer. | |
308 * @param file being parsed. | |
309 * @param {ByteReader} reader to use for metadata extraction. | |
310 */ | |
311 function extractFrames(file, reader) { | |
312 var id3v2 = metadata.id3v2; | |
313 | |
314 if ((id3v2.major > 2) | |
315 && (id3v2.flags & Id3Parser.v2.FLAG_EXTENDED_HEADER != 0)) { | |
316 // Skip extended header if found | |
317 if (id3v2.major == 3) { | |
318 reader.seek(reader.readScalar(4, false) - 4); | |
319 } else if (id3v2.major == 4) { | |
320 reader.seek(Id3Parser.readSynchSafe_(reader, 4) - 4); | |
321 } | |
322 } | |
323 | |
324 var frame; | |
325 | |
326 while (frame = self.readFrame_(reader, id3v2.major)) { | |
327 metadata.id3v2[frame.name] = frame; | |
328 } | |
329 | |
330 this.nextStep(); | |
331 }, | |
332 | |
333 /** | |
334 * Adds 'description' object to metadata. | |
335 * 'description' used to unify different parsers and make | |
336 * metadata parser-aware. | |
337 * Description is array if value-type pairs. Type should be used | |
338 * to properly format value before displaying to user. | |
339 */ | |
340 function prepareDescription() { | |
341 var id3v2 = metadata.id3v2; | |
342 | |
343 if (id3v2['APIC']) | |
344 metadata.thumbnailURL = id3v2['APIC'].imageUrl; | |
345 else if (id3v2['PIC']) | |
346 metadata.thumbnailURL = id3v2['PIC'].imageUrl; | |
347 | |
348 metadata.description = []; | |
349 | |
350 for (var key in id3v2) { | |
351 if (typeof(Id3Parser.v2.MAPPERS[key]) != 'undefined' && | |
352 id3v2[key].value.trim().length > 0) { | |
353 metadata.description.push({ | |
354 key: Id3Parser.v2.MAPPERS[key], | |
355 value: id3v2[key].value.trim() | |
356 }); | |
357 } | |
358 } | |
359 | |
360 function extract(propName, tags) { | |
361 for (var i = 1; i != arguments.length; i++) { | |
362 var tag = id3v2[arguments[i]]; | |
363 if (tag && tag.value) { | |
364 metadata[propName] = tag.value; | |
365 break; | |
366 } | |
367 } | |
368 } | |
369 | |
370 extract('album', 'TALB', 'TAL'); | |
371 extract('title', 'TIT2', 'TT2'); | |
372 extract('artist', 'TPE1', 'TP1'); | |
373 | |
374 metadata.description.sort(function(a, b) { | |
375 return Id3Parser.METADATA_ORDER.indexOf(a.key)- | |
376 Id3Parser.METADATA_ORDER.indexOf(b.key); | |
377 }); | |
378 this.nextStep(); | |
379 } | |
380 ], | |
381 this | |
382 ); | |
383 | |
384 var metadataParser = new FunctionParallel( | |
385 'mp3metadataParser', | |
386 [id3v1Parser, id3v2Parser], | |
387 this, | |
388 function() { | |
389 callback.call(null, metadata); | |
390 }, | |
391 onError | |
392 ); | |
393 | |
394 id3v1Parser.setCallback(metadataParser.nextStep); | |
395 id3v2Parser.setCallback(metadataParser.nextStep); | |
396 | |
397 id3v1Parser.setFailureCallback(metadataParser.onError); | |
398 id3v2Parser.setFailureCallback(metadataParser.onError); | |
399 | |
400 this.vlog('Passed argument : ' + file); | |
401 | |
402 metadataParser.start(file); | |
403 }; | |
404 | |
405 | |
406 /** | |
407 * Metadata order to use for metadata generation | |
408 */ | |
409 Id3Parser.METADATA_ORDER = [ | |
410 'ID3_TITLE', | |
411 'ID3_LEAD_PERFORMER', | |
412 'ID3_YEAR', | |
413 'ID3_ALBUM', | |
414 'ID3_TRACK_NUMBER', | |
415 'ID3_BPM', | |
416 'ID3_COMPOSER', | |
417 'ID3_DATE', | |
418 'ID3_PLAYLIST_DELAY', | |
419 'ID3_LYRICIST', | |
420 'ID3_FILE_TYPE', | |
421 'ID3_TIME', | |
422 'ID3_LENGTH', | |
423 'ID3_FILE_OWNER', | |
424 'ID3_BAND', | |
425 'ID3_COPYRIGHT', | |
426 'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE', | |
427 'ID3_OFFICIAL_ARTIST', | |
428 'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE', | |
429 'ID3_PUBLISHERS_OFFICIAL_WEBPAGE' | |
430 ]; | |
431 | |
432 | |
433 /** | |
434 * id3v1 constants | |
435 */ | |
436 Id3Parser.v1 = { | |
437 /** | |
438 * Genres list as described in id3 documentation. We aren't going to | |
439 * localize this list, because at least in Russian (and I think most | |
440 * other languages), translation exists at least fo 10% and most time | |
441 * translation would degrade to transliteration. | |
442 */ | |
443 GENRES : [ | |
444 'Blues', | |
445 'Classic Rock', | |
446 'Country', | |
447 'Dance', | |
448 'Disco', | |
449 'Funk', | |
450 'Grunge', | |
451 'Hip-Hop', | |
452 'Jazz', | |
453 'Metal', | |
454 'New Age', | |
455 'Oldies', | |
456 'Other', | |
457 'Pop', | |
458 'R&B', | |
459 'Rap', | |
460 'Reggae', | |
461 'Rock', | |
462 'Techno', | |
463 'Industrial', | |
464 'Alternative', | |
465 'Ska', | |
466 'Death Metal', | |
467 'Pranks', | |
468 'Soundtrack', | |
469 'Euro-Techno', | |
470 'Ambient', | |
471 'Trip-Hop', | |
472 'Vocal', | |
473 'Jazz+Funk', | |
474 'Fusion', | |
475 'Trance', | |
476 'Classical', | |
477 'Instrumental', | |
478 'Acid', | |
479 'House', | |
480 'Game', | |
481 'Sound Clip', | |
482 'Gospel', | |
483 'Noise', | |
484 'AlternRock', | |
485 'Bass', | |
486 'Soul', | |
487 'Punk', | |
488 'Space', | |
489 'Meditative', | |
490 'Instrumental Pop', | |
491 'Instrumental Rock', | |
492 'Ethnic', | |
493 'Gothic', | |
494 'Darkwave', | |
495 'Techno-Industrial', | |
496 'Electronic', | |
497 'Pop-Folk', | |
498 'Eurodance', | |
499 'Dream', | |
500 'Southern Rock', | |
501 'Comedy', | |
502 'Cult', | |
503 'Gangsta', | |
504 'Top 40', | |
505 'Christian Rap', | |
506 'Pop/Funk', | |
507 'Jungle', | |
508 'Native American', | |
509 'Cabaret', | |
510 'New Wave', | |
511 'Psychadelic', | |
512 'Rave', | |
513 'Showtunes', | |
514 'Trailer', | |
515 'Lo-Fi', | |
516 'Tribal', | |
517 'Acid Punk', | |
518 'Acid Jazz', | |
519 'Polka', | |
520 'Retro', | |
521 'Musical', | |
522 'Rock & Roll', | |
523 'Hard Rock', | |
524 'Folk', | |
525 'Folk-Rock', | |
526 'National Folk', | |
527 'Swing', | |
528 'Fast Fusion', | |
529 'Bebob', | |
530 'Latin', | |
531 'Revival', | |
532 'Celtic', | |
533 'Bluegrass', | |
534 'Avantgarde', | |
535 'Gothic Rock', | |
536 'Progressive Rock', | |
537 'Psychedelic Rock', | |
538 'Symphonic Rock', | |
539 'Slow Rock', | |
540 'Big Band', | |
541 'Chorus', | |
542 'Easy Listening', | |
543 'Acoustic', | |
544 'Humour', | |
545 'Speech', | |
546 'Chanson', | |
547 'Opera', | |
548 'Chamber Music', | |
549 'Sonata', | |
550 'Symphony', | |
551 'Booty Bass', | |
552 'Primus', | |
553 'Porn Groove', | |
554 'Satire', | |
555 'Slow Jam', | |
556 'Club', | |
557 'Tango', | |
558 'Samba', | |
559 'Folklore', | |
560 'Ballad', | |
561 'Power Ballad', | |
562 'Rhythmic Soul', | |
563 'Freestyle', | |
564 'Duet', | |
565 'Punk Rock', | |
566 'Drum Solo', | |
567 'A capella', | |
568 'Euro-House', | |
569 'Dance Hall', | |
570 'Goa', | |
571 'Drum & Bass', | |
572 'Club-House', | |
573 'Hardcore', | |
574 'Terror', | |
575 'Indie', | |
576 'BritPop', | |
577 'Negerpunk', | |
578 'Polsk Punk', | |
579 'Beat', | |
580 'Christian Gangsta Rap', | |
581 'Heavy Metal', | |
582 'Black Metal', | |
583 'Crossover', | |
584 'Contemporary Christian', | |
585 'Christian Rock', | |
586 'Merengue', | |
587 'Salsa', | |
588 'Thrash Metal', | |
589 'Anime', | |
590 'Jpop', | |
591 'Synthpop' | |
592 ] | |
593 }; | |
594 | |
595 /** | |
596 * id3v2 constants | |
597 */ | |
598 Id3Parser.v2 = { | |
599 FLAG_EXTENDED_HEADER: 1 << 5, | |
600 | |
601 ENCODING: { | |
602 /** | |
603 * ISO-8859-1 [ISO-8859-1]. Terminated with $00. | |
604 * | |
605 * @const | |
606 * @type {int} | |
607 */ | |
608 ISO_8859_1 : 0, | |
609 | |
610 | |
611 /** | |
612 * [UTF-16] encoded Unicode [UNICODE] with BOM. All | |
613 * strings in the same frame SHALL have the same byteorder. | |
614 * Terminated with $00 00. | |
615 * | |
616 * @const | |
617 * @type {int} | |
618 */ | |
619 UTF_16 : 1, | |
620 | |
621 /** | |
622 * UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM. | |
623 * Terminated with $00 00. | |
624 * | |
625 * @const | |
626 * @type {int} | |
627 */ | |
628 UTF_16BE : 2, | |
629 | |
630 /** | |
631 * UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00. | |
632 * | |
633 * @const | |
634 * @type {int} | |
635 */ | |
636 UTF_8 : 3 | |
637 }, | |
638 HANDLERS: { | |
639 //User defined text information frame | |
640 TXX: Id3Parser.prototype.readUserDefinedTextFrame_, | |
641 //User defined URL link frame | |
642 WXX: Id3Parser.prototype.readUserDefinedTextFrame_, | |
643 | |
644 //User defined text information frame | |
645 TXXX: Id3Parser.prototype.readUserDefinedTextFrame_, | |
646 | |
647 //User defined URL link frame | |
648 WXXX: Id3Parser.prototype.readUserDefinedTextFrame_, | |
649 | |
650 //User attached image | |
651 PIC: Id3Parser.prototype.readPIC_, | |
652 | |
653 //User attached image | |
654 APIC: Id3Parser.prototype.readAPIC_ | |
655 }, | |
656 MAPPERS: { | |
657 TALB: 'ID3_ALBUM', | |
658 TBPM: 'ID3_BPM', | |
659 TCOM: 'ID3_COMPOSER', | |
660 TDAT: 'ID3_DATE', | |
661 TDLY: 'ID3_PLAYLIST_DELAY', | |
662 TEXT: 'ID3_LYRICIST', | |
663 TFLT: 'ID3_FILE_TYPE', | |
664 TIME: 'ID3_TIME', | |
665 TIT2: 'ID3_TITLE', | |
666 TLEN: 'ID3_LENGTH', | |
667 TOWN: 'ID3_FILE_OWNER', | |
668 TPE1: 'ID3_LEAD_PERFORMER', | |
669 TPE2: 'ID3_BAND', | |
670 TRCK: 'ID3_TRACK_NUMBER', | |
671 TYER: 'ID3_YEAR', | |
672 WCOP: 'ID3_COPYRIGHT', | |
673 WOAF: 'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE', | |
674 WOAR: 'ID3_OFFICIAL_ARTIST', | |
675 WOAS: 'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE', | |
676 WPUB: 'ID3_PUBLISHERS_OFFICIAL_WEBPAGE' | |
677 } | |
678 }; | |
679 | |
680 MetadataDispatcher.registerParserClass(Id3Parser); | |
OLD | NEW |