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