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