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 importScripts('function_sequence.js');
      8 importScripts('function_parallel.js');
      9 
     10 function Id3Parser(parent) {
     11   MetadataParser.call(this, parent, 'id3', /\.(mp3)$/i);
     12 }
     13 
     14 Id3Parser.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  */
     25 Id3Parser.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  */
     49 Id3Parser.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  */
     62 Id3Parser.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  */
     94 Id3Parser.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  */
    111 Id3Parser.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  */
    135 Id3Parser.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  */
    158 Id3Parser.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  */
    183 Id3Parser.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  */
    248 Id3Parser.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  */
    437 Id3Parser.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  */
    464 Id3Parser.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 fo 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  */
    626 Id3Parser.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 
    708 MetadataDispatcher.registerParserClass(Id3Parser);
    709