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 Rytes to read.
     31  * @return {number} Synchsafe value.
     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} Uint24 value.
     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} String value.
     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 Major id3 version to use.
    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 File object to parse.
    253  * @param {Object} metadata Metadata object of the file.
    254  * @param {function(Object)} callback Success callback.
    255  * @param {function(etring)} onError Error callback.
    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   var id3v2Parser = new FunctionSequence(
    313       'id3v2parser',
    314       [
    315         function readHead(file) {
    316           util.readFileBytes(file, 0, 10, this.nextStep, this.onError,
    317               this);
    318         },
    319 
    320         /**
    321          * Check if passed array of 10 bytes contains ID3 header.
    322          * @param {File} file File to check and continue reading if ID3
    323          *     metadata found.
    324          * @param {ByteReader} reader Reader to fill with stream bytes.
    325          */
    326         function checkId3v2(file, reader) {
    327           if (reader.readString(3) == 'ID3') {
    328             this.logger.vlog('id3v2 found');
    329             var id3v2 = metadata.id3v2 = {};
    330             id3v2.major = reader.readScalar(1, false);
    331             id3v2.minor = reader.readScalar(1, false);
    332             id3v2.flags = reader.readScalar(1, false);
    333             id3v2.size = Id3Parser.readSynchSafe_(reader, 4);
    334 
    335             util.readFileBytes(file, 10, 10 + id3v2.size, this.nextStep,
    336                 this.onError, this);
    337           } else {
    338             this.finish();
    339           }
    340         },
    341 
    342         /**
    343          * Extracts all ID3v2 frames from given bytebuffer.
    344          * @param {File} file File being parsed.
    345          * @param {ByteReader} reader Reader to use for metadata extraction.
    346          */
    347         function extractFrames(file, reader) {
    348           var id3v2 = metadata.id3v2;
    349 
    350           if ((id3v2.major > 2) &&
    351               (id3v2.flags & Id3Parser.v2.FLAG_EXTENDED_HEADER != 0)) {
    352             // Skip extended header if found
    353             if (id3v2.major == 3) {
    354               reader.seek(reader.readScalar(4, false) - 4);
    355             } else if (id3v2.major == 4) {
    356               reader.seek(Id3Parser.readSynchSafe_(reader, 4) - 4);
    357             }
    358           }
    359 
    360           var frame;
    361 
    362           while (frame = self.readFrame_(reader, id3v2.major)) {
    363             metadata.id3v2[frame.name] = frame;
    364           }
    365 
    366           this.nextStep();
    367         },
    368 
    369         /**
    370          * Adds 'description' object to metadata.
    371          * 'description' used to unify different parsers and make
    372          * metadata parser-aware.
    373          * Description is array if value-type pairs. Type should be used
    374          * to properly format value before displaying to user.
    375          */
    376         function prepareDescription() {
    377           var id3v2 = metadata.id3v2;
    378 
    379           if (id3v2['APIC'])
    380             metadata.thumbnailURL = id3v2['APIC'].imageUrl;
    381           else if (id3v2['PIC'])
    382             metadata.thumbnailURL = id3v2['PIC'].imageUrl;
    383 
    384           metadata.description = [];
    385 
    386           for (var key in id3v2) {
    387             if (typeof(Id3Parser.v2.MAPPERS[key]) != 'undefined' &&
    388                 id3v2[key].value.trim().length > 0) {
    389               metadata.description.push({
    390                 key: Id3Parser.v2.MAPPERS[key],
    391                 value: id3v2[key].value.trim()
    392               });
    393             }
    394           }
    395 
    396           function extract(propName, tags) {
    397             for (var i = 1; i != arguments.length; i++) {
    398               var tag = id3v2[arguments[i]];
    399               if (tag && tag.value) {
    400                 metadata[propName] = tag.value;
    401                 break;
    402               }
    403             }
    404           }
    405 
    406           extract('album', 'TALB', 'TAL');
    407           extract('title', 'TIT2', 'TT2');
    408           extract('artist', 'TPE1', 'TP1');
    409 
    410           metadata.description.sort(function(a, b) {
    411             return Id3Parser.METADATA_ORDER.indexOf(a.key) -
    412                    Id3Parser.METADATA_ORDER.indexOf(b.key);
    413           });
    414           this.nextStep();
    415         }
    416       ],
    417       this);
    418 
    419   var metadataParser = new FunctionParallel(
    420       'mp3metadataParser',
    421       [id3v1Parser, id3v2Parser],
    422       this,
    423       function() {
    424         callback.call(null, metadata);
    425       },
    426       onError);
    427 
    428   id3v1Parser.setCallback(metadataParser.nextStep);
    429   id3v2Parser.setCallback(metadataParser.nextStep);
    430 
    431   id3v1Parser.setFailureCallback(metadataParser.onError);
    432   id3v2Parser.setFailureCallback(metadataParser.onError);
    433 
    434   this.vlog('Passed argument : ' + file);
    435 
    436   metadataParser.start(file);
    437 };
    438 
    439 
    440 /**
    441  * Metadata order to use for metadata generation
    442  * @type {Array.<string>}
    443  * @const
    444  */
    445 Id3Parser.METADATA_ORDER = [
    446   'ID3_TITLE',
    447   'ID3_LEAD_PERFORMER',
    448   'ID3_YEAR',
    449   'ID3_ALBUM',
    450   'ID3_TRACK_NUMBER',
    451   'ID3_BPM',
    452   'ID3_COMPOSER',
    453   'ID3_DATE',
    454   'ID3_PLAYLIST_DELAY',
    455   'ID3_LYRICIST',
    456   'ID3_FILE_TYPE',
    457   'ID3_TIME',
    458   'ID3_LENGTH',
    459   'ID3_FILE_OWNER',
    460   'ID3_BAND',
    461   'ID3_COPYRIGHT',
    462   'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE',
    463   'ID3_OFFICIAL_ARTIST',
    464   'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE',
    465   'ID3_PUBLISHERS_OFFICIAL_WEBPAGE'
    466 ];
    467 
    468 
    469 /**
    470  * Id3v1 constants.
    471  * @type {Object.<*>}
    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  * @type {Object.<*>}
    635  */
    636 Id3Parser.v2 = {
    637   FLAG_EXTENDED_HEADER: 1 << 5,
    638 
    639   ENCODING: {
    640     /**
    641      * ISO-8859-1 [ISO-8859-1]. Terminated with $00.
    642      *
    643      * @const
    644      * @type {number}
    645      */
    646     ISO_8859_1: 0,
    647 
    648 
    649     /**
    650      * [UTF-16] encoded Unicode [UNICODE] with BOM. All
    651      * strings in the same frame SHALL have the same byteorder.
    652      * Terminated with $00 00.
    653      *
    654      * @const
    655      * @type {number}
    656      */
    657     UTF_16: 1,
    658 
    659     /**
    660      * UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM.
    661      * Terminated with $00 00.
    662      *
    663      * @const
    664      * @type {number}
    665      */
    666     UTF_16BE: 2,
    667 
    668     /**
    669      * UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00.
    670      *
    671      * @const
    672      * @type {number}
    673      */
    674     UTF_8: 3
    675   },
    676   HANDLERS: {
    677     //User defined text information frame
    678     TXX: Id3Parser.prototype.readUserDefinedTextFrame_,
    679     //User defined URL link frame
    680     WXX: Id3Parser.prototype.readUserDefinedTextFrame_,
    681 
    682     //User defined text information frame
    683     TXXX: Id3Parser.prototype.readUserDefinedTextFrame_,
    684 
    685     //User defined URL link frame
    686     WXXX: Id3Parser.prototype.readUserDefinedTextFrame_,
    687 
    688     //User attached image
    689     PIC: Id3Parser.prototype.readPIC_,
    690 
    691     //User attached image
    692     APIC: Id3Parser.prototype.readAPIC_
    693   },
    694   MAPPERS: {
    695     TALB: 'ID3_ALBUM',
    696     TBPM: 'ID3_BPM',
    697     TCOM: 'ID3_COMPOSER',
    698     TDAT: 'ID3_DATE',
    699     TDLY: 'ID3_PLAYLIST_DELAY',
    700     TEXT: 'ID3_LYRICIST',
    701     TFLT: 'ID3_FILE_TYPE',
    702     TIME: 'ID3_TIME',
    703     TIT2: 'ID3_TITLE',
    704     TLEN: 'ID3_LENGTH',
    705     TOWN: 'ID3_FILE_OWNER',
    706     TPE1: 'ID3_LEAD_PERFORMER',
    707     TPE2: 'ID3_BAND',
    708     TRCK: 'ID3_TRACK_NUMBER',
    709     TYER: 'ID3_YEAR',
    710     WCOP: 'ID3_COPYRIGHT',
    711     WOAF: 'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE',
    712     WOAR: 'ID3_OFFICIAL_ARTIST',
    713     WOAS: 'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE',
    714     WPUB: 'ID3_PUBLISHERS_OFFICIAL_WEBPAGE'
    715   }
    716 };
    717 
    718 MetadataDispatcher.registerParserClass(Id3Parser);
    719