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