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