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