Home | History | Annotate | Download | only in metadata
      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  */
     12 var FILE_MANAGER_HOST = 'chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj';
     13 
     14 importScripts(
     15     FILE_MANAGER_HOST + '/foreground/js/metadata/function_sequence.js');
     16 importScripts(
     17     FILE_MANAGER_HOST + '/foreground/js/metadata/function_parallel.js');
     18 
     19 function Id3Parser(parent) {
     20   MetadataParser.call(this, parent, 'id3', /\.(mp3)$/i);
     21 }
     22 
     23 Id3Parser.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  */
     34 Id3Parser.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  */
     58 Id3Parser.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  */
     71 Id3Parser.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  */
    103 Id3Parser.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  */
    120 Id3Parser.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  */
    144 Id3Parser.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  */
    167 Id3Parser.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  */
    192 Id3Parser.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  */
    257 Id3Parser.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  */
    446 Id3Parser.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  */
    473 Id3Parser.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  */
    635 Id3Parser.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 
    717 MetadataDispatcher.registerParserClass(Id3Parser);
    718