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