| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| importScripts('function_sequence.js'); |
| importScripts('function_parallel.js'); |
| |
| function Id3Parser(parent) { |
| MetadataParser.call(this, parent, 'id3', /\.(mp3)$/i); |
| } |
| |
| Id3Parser.prototype = {__proto__: MetadataParser.prototype}; |
| |
| /** |
| * Reads synchsafe integer. |
| * 'SynchSafe' term is taken from id3 documentation. |
| * |
| * @param {ByteReader} reader - reader to use. |
| * @param {number} length - bytes to read. |
| * @return {number} // TODO(JSDOC). |
| * @private |
| */ |
| Id3Parser.readSynchSafe_ = function(reader, length) { |
| var rv = 0; |
| |
| switch (length) { |
| case 4: |
| rv = reader.readScalar(1, false) << 21; |
| case 3: |
| rv |= reader.readScalar(1, false) << 14; |
| case 2: |
| rv |= reader.readScalar(1, false) << 7; |
| case 1: |
| rv |= reader.readScalar(1, false); |
| } |
| |
| return rv; |
| }; |
| |
| /** |
| * Reads 3bytes integer. |
| * |
| * @param {ByteReader} reader - reader to use. |
| * @return {number} // TODO(JSDOC). |
| * @private |
| */ |
| Id3Parser.readUInt24_ = function(reader) { |
| return reader.readScalar(2, false) << 16 | reader.readScalar(1, false); |
| }; |
| |
| /** |
| * Reads string from reader with specified encoding |
| * |
| * @param {ByteReader} reader reader to use. |
| * @param {number} encoding string encoding. |
| * @param {number} size maximum string size. Actual result may be shorter. |
| * @return {string} // TODO(JSDOC). |
| * @private |
| */ |
| Id3Parser.prototype.readString_ = function(reader, encoding, size) { |
| switch (encoding) { |
| case Id3Parser.v2.ENCODING.ISO_8859_1: |
| return reader.readNullTerminatedString(size); |
| |
| case Id3Parser.v2.ENCODING.UTF_16: |
| return reader.readNullTerminatedStringUTF16(true, size); |
| |
| case Id3Parser.v2.ENCODING.UTF_16BE: |
| return reader.readNullTerminatedStringUTF16(false, size); |
| |
| case Id3Parser.v2.ENCODING.UTF_8: |
| // TODO: implement UTF_8. |
| this.log('UTF8 encoding not supported, used ISO_8859_1 instead'); |
| return reader.readNullTerminatedString(size); |
| |
| default: { |
| this.log('Unsupported encoding in ID3 tag: ' + encoding); |
| return ''; |
| } |
| } |
| }; |
| |
| /** |
| * Reads text frame from reader. |
| * |
| * @param {ByteReader} reader reader to use. |
| * @param {number} majorVersion major id3 version to use. |
| * @param {Object} frame frame so store data at. |
| * @param {number} end frame end position in reader. |
| * @private |
| */ |
| Id3Parser.prototype.readTextFrame_ = function(reader, |
| majorVersion, |
| frame, |
| end) { |
| frame.encoding = reader.readScalar(1, false, end); |
| frame.value = this.readString_(reader, frame.encoding, end - reader.tell()); |
| }; |
| |
| /** |
| * Reads user defined text frame from reader. |
| * |
| * @param {ByteReader} reader reader to use. |
| * @param {number} majorVersion major id3 version to use. |
| * @param {Object} frame frame so store data at. |
| * @param {number} end frame end position in reader. |
| * @private |
| */ |
| Id3Parser.prototype.readUserDefinedTextFrame_ = function(reader, |
| majorVersion, |
| frame, |
| end) { |
| frame.encoding = reader.readScalar(1, false, end); |
| |
| frame.description = this.readString_( |
| reader, |
| frame.encoding, |
| end - reader.tell()); |
| |
| frame.value = this.readString_( |
| reader, |
| frame.encoding, |
| end - reader.tell()); |
| }; |
| |
| /** |
| * @param {ByteReader} reader Reader to use. |
| * @param {number} majorVersion Major id3 version to use. |
| * @param {Object} frame Frame so store data at. |
| * @param {number} end Frame end position in reader. |
| * @private |
| */ |
| Id3Parser.prototype.readPIC_ = function(reader, majorVersion, frame, end) { |
| frame.encoding = reader.readScalar(1, false, end); |
| frame.format = reader.readNullTerminatedString(3, end - reader.tell()); |
| frame.pictureType = reader.readScalar(1, false, end); |
| frame.description = this.readString_(reader, |
| frame.encoding, |
| end - reader.tell()); |
| |
| |
| if (frame.format == '-->') { |
| frame.imageUrl = reader.readNullTerminatedString(end - reader.tell()); |
| } else { |
| frame.imageUrl = reader.readImage(end - reader.tell()); |
| } |
| }; |
| |
| /** |
| * @param {ByteReader} reader Reader to use. |
| * @param {number} majorVersion Major id3 version to use. |
| * @param {Object} frame Frame so store data at. |
| * @param {number} end Frame end position in reader. |
| * @private |
| */ |
| Id3Parser.prototype.readAPIC_ = function(reader, majorVersion, frame, end) { |
| this.vlog('Extracting picture'); |
| frame.encoding = reader.readScalar(1, false, end); |
| frame.mime = reader.readNullTerminatedString(end - reader.tell()); |
| frame.pictureType = reader.readScalar(1, false, end); |
| frame.description = this.readString_( |
| reader, |
| frame.encoding, |
| end - reader.tell()); |
| |
| if (frame.mime == '-->') { |
| frame.imageUrl = reader.readNullTerminatedString(end - reader.tell()); |
| } else { |
| frame.imageUrl = reader.readImage(end - reader.tell()); |
| } |
| }; |
| |
| /** |
| * Reads string from reader with specified encoding |
| * |
| * @param {ByteReader} reader reader to use. |
| * @param {number} majorVersion // TODO(JSDOC). |
| * @return {Object} frame read. |
| * @private |
| */ |
| Id3Parser.prototype.readFrame_ = function(reader, majorVersion) { |
| if (reader.eof()) |
| return null; |
| |
| var frame = {}; |
| |
| reader.pushSeek(reader.tell(), ByteReader.SEEK_BEG); |
| |
| var position = reader.tell(); |
| |
| frame.name = (majorVersion == 2) ? reader.readNullTerminatedString(3) : |
| reader.readNullTerminatedString(4); |
| |
| if (frame.name == '') |
| return null; |
| |
| this.vlog('Found frame ' + (frame.name) + ' at position ' + position); |
| |
| switch (majorVersion) { |
| case 2: |
| frame.size = Id3Parser.readUInt24_(reader); |
| frame.headerSize = 6; |
| break; |
| case 3: |
| frame.size = reader.readScalar(4, false); |
| frame.headerSize = 10; |
| frame.flags = reader.readScalar(2, false); |
| break; |
| case 4: |
| frame.size = Id3Parser.readSynchSafe_(reader, 4); |
| frame.headerSize = 10; |
| frame.flags = reader.readScalar(2, false); |
| break; |
| } |
| |
| this.vlog('Found frame [' + frame.name + '] with size [' + frame.size + ']'); |
| |
| if (Id3Parser.v2.HANDLERS[frame.name]) { |
| Id3Parser.v2.HANDLERS[frame.name].call( |
| this, |
| reader, |
| majorVersion, |
| frame, |
| reader.tell() + frame.size); |
| } else if (frame.name.charAt(0) == 'T' || frame.name.charAt(0) == 'W') { |
| this.readTextFrame_( |
| reader, |
| majorVersion, |
| frame, |
| reader.tell() + frame.size); |
| } |
| |
| reader.popSeek(); |
| |
| reader.seek(frame.size + frame.headerSize, ByteReader.SEEK_CUR); |
| |
| return frame; |
| }; |
| |
| /** |
| * @param {File} file // TODO(JSDOC). |
| * @param {Object} metadata // TODO(JSDOC). |
| * @param {function(Object)} callback // TODO(JSDOC). |
| * @param {function(etring)} onError // TODO(JSDOC). |
| */ |
| Id3Parser.prototype.parse = function(file, metadata, callback, onError) { |
| var self = this; |
| |
| this.log('Starting id3 parser for ' + file.name); |
| |
| var id3v1Parser = new FunctionSequence( |
| 'id3v1parser', |
| [ |
| /** |
| * Reads last 128 bytes of file in bytebuffer, |
| * which passes further. |
| * In last 128 bytes should be placed ID3v1 tag if available. |
| * @param {File} file File which bytes to read. |
| */ |
| function readTail(file) { |
| util.readFileBytes(file, file.size - 128, file.size, |
| this.nextStep, this.onError, this); |
| }, |
| |
| /** |
| * Attempts to extract ID3v1 tag from 128 bytes long ByteBuffer |
| * @param {File} file File which tags are being extracted. Could be used |
| * for logging purposes. |
| * @param {ByteReader} reader ByteReader of 128 bytes. |
| */ |
| function extractId3v1(file, reader) { |
| if (reader.readString(3) == 'TAG') { |
| this.logger.vlog('id3v1 found'); |
| var id3v1 = metadata.id3v1 = {}; |
| |
| var title = reader.readNullTerminatedString(30).trim(); |
| |
| if (title.length > 0) { |
| metadata.title = title; |
| } |
| |
| reader.seek(3 + 30, ByteReader.SEEK_BEG); |
| |
| var artist = reader.readNullTerminatedString(30).trim(); |
| if (artist.length > 0) { |
| metadata.artist = artist; |
| } |
| |
| reader.seek(3 + 30 + 30, ByteReader.SEEK_BEG); |
| |
| var album = reader.readNullTerminatedString(30).trim(); |
| if (album.length > 0) { |
| metadata.album = album; |
| } |
| } |
| this.nextStep(); |
| } |
| ], |
| this |
| ); |
| |
| var id3v2Parser = new FunctionSequence( |
| 'id3v2parser', |
| [ |
| function readHead(file) { |
| util.readFileBytes(file, 0, 10, this.nextStep, this.onError, |
| this); |
| }, |
| |
| /** |
| * Check if passed array of 10 bytes contains ID3 header. |
| * @param {File} file File to check and continue reading if ID3 |
| * metadata found. |
| * @param {ByteReader} reader Reader to fill with stream bytes. |
| */ |
| function checkId3v2(file, reader) { |
| if (reader.readString(3) == 'ID3') { |
| this.logger.vlog('id3v2 found'); |
| var id3v2 = metadata.id3v2 = {}; |
| id3v2.major = reader.readScalar(1, false); |
| id3v2.minor = reader.readScalar(1, false); |
| id3v2.flags = reader.readScalar(1, false); |
| id3v2.size = Id3Parser.readSynchSafe_(reader, 4); |
| |
| util.readFileBytes(file, 10, 10 + id3v2.size, this.nextStep, |
| this.onError, this); |
| } else { |
| this.finish(); |
| } |
| }, |
| |
| /** |
| * Extracts all ID3v2 frames from given bytebuffer. |
| * @param {File} file File being parsed. |
| * @param {ByteReader} reader Reader to use for metadata extraction. |
| */ |
| function extractFrames(file, reader) { |
| var id3v2 = metadata.id3v2; |
| |
| if ((id3v2.major > 2) && |
| (id3v2.flags & Id3Parser.v2.FLAG_EXTENDED_HEADER != 0)) { |
| // Skip extended header if found |
| if (id3v2.major == 3) { |
| reader.seek(reader.readScalar(4, false) - 4); |
| } else if (id3v2.major == 4) { |
| reader.seek(Id3Parser.readSynchSafe_(reader, 4) - 4); |
| } |
| } |
| |
| var frame; |
| |
| while (frame = self.readFrame_(reader, id3v2.major)) { |
| metadata.id3v2[frame.name] = frame; |
| } |
| |
| this.nextStep(); |
| }, |
| |
| /** |
| * Adds 'description' object to metadata. |
| * 'description' used to unify different parsers and make |
| * metadata parser-aware. |
| * Description is array if value-type pairs. Type should be used |
| * to properly format value before displaying to user. |
| */ |
| function prepareDescription() { |
| var id3v2 = metadata.id3v2; |
| |
| if (id3v2['APIC']) |
| metadata.thumbnailURL = id3v2['APIC'].imageUrl; |
| else if (id3v2['PIC']) |
| metadata.thumbnailURL = id3v2['PIC'].imageUrl; |
| |
| metadata.description = []; |
| |
| for (var key in id3v2) { |
| if (typeof(Id3Parser.v2.MAPPERS[key]) != 'undefined' && |
| id3v2[key].value.trim().length > 0) { |
| metadata.description.push({ |
| key: Id3Parser.v2.MAPPERS[key], |
| value: id3v2[key].value.trim() |
| }); |
| } |
| } |
| |
| function extract(propName, tags) { |
| for (var i = 1; i != arguments.length; i++) { |
| var tag = id3v2[arguments[i]]; |
| if (tag && tag.value) { |
| metadata[propName] = tag.value; |
| break; |
| } |
| } |
| } |
| |
| extract('album', 'TALB', 'TAL'); |
| extract('title', 'TIT2', 'TT2'); |
| extract('artist', 'TPE1', 'TP1'); |
| |
| metadata.description.sort(function(a, b) { |
| return Id3Parser.METADATA_ORDER.indexOf(a.key) - |
| Id3Parser.METADATA_ORDER.indexOf(b.key); |
| }); |
| this.nextStep(); |
| } |
| ], |
| this |
| ); |
| |
| var metadataParser = new FunctionParallel( |
| 'mp3metadataParser', |
| [id3v1Parser, id3v2Parser], |
| this, |
| function() { |
| callback.call(null, metadata); |
| }, |
| onError |
| ); |
| |
| id3v1Parser.setCallback(metadataParser.nextStep); |
| id3v2Parser.setCallback(metadataParser.nextStep); |
| |
| id3v1Parser.setFailureCallback(metadataParser.onError); |
| id3v2Parser.setFailureCallback(metadataParser.onError); |
| |
| this.vlog('Passed argument : ' + file); |
| |
| metadataParser.start(file); |
| }; |
| |
| |
| /** |
| * Metadata order to use for metadata generation |
| */ |
| Id3Parser.METADATA_ORDER = [ |
| 'ID3_TITLE', |
| 'ID3_LEAD_PERFORMER', |
| 'ID3_YEAR', |
| 'ID3_ALBUM', |
| 'ID3_TRACK_NUMBER', |
| 'ID3_BPM', |
| 'ID3_COMPOSER', |
| 'ID3_DATE', |
| 'ID3_PLAYLIST_DELAY', |
| 'ID3_LYRICIST', |
| 'ID3_FILE_TYPE', |
| 'ID3_TIME', |
| 'ID3_LENGTH', |
| 'ID3_FILE_OWNER', |
| 'ID3_BAND', |
| 'ID3_COPYRIGHT', |
| 'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE', |
| 'ID3_OFFICIAL_ARTIST', |
| 'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE', |
| 'ID3_PUBLISHERS_OFFICIAL_WEBPAGE' |
| ]; |
| |
| |
| /** |
| * id3v1 constants |
| */ |
| Id3Parser.v1 = { |
| /** |
| * Genres list as described in id3 documentation. We aren't going to |
| * localize this list, because at least in Russian (and I think most |
| * other languages), translation exists at least for 10% and most time |
| * translation would degrade to transliteration. |
| */ |
| GENRES: [ |
| 'Blues', |
| 'Classic Rock', |
| 'Country', |
| 'Dance', |
| 'Disco', |
| 'Funk', |
| 'Grunge', |
| 'Hip-Hop', |
| 'Jazz', |
| 'Metal', |
| 'New Age', |
| 'Oldies', |
| 'Other', |
| 'Pop', |
| 'R&B', |
| 'Rap', |
| 'Reggae', |
| 'Rock', |
| 'Techno', |
| 'Industrial', |
| 'Alternative', |
| 'Ska', |
| 'Death Metal', |
| 'Pranks', |
| 'Soundtrack', |
| 'Euro-Techno', |
| 'Ambient', |
| 'Trip-Hop', |
| 'Vocal', |
| 'Jazz+Funk', |
| 'Fusion', |
| 'Trance', |
| 'Classical', |
| 'Instrumental', |
| 'Acid', |
| 'House', |
| 'Game', |
| 'Sound Clip', |
| 'Gospel', |
| 'Noise', |
| 'AlternRock', |
| 'Bass', |
| 'Soul', |
| 'Punk', |
| 'Space', |
| 'Meditative', |
| 'Instrumental Pop', |
| 'Instrumental Rock', |
| 'Ethnic', |
| 'Gothic', |
| 'Darkwave', |
| 'Techno-Industrial', |
| 'Electronic', |
| 'Pop-Folk', |
| 'Eurodance', |
| 'Dream', |
| 'Southern Rock', |
| 'Comedy', |
| 'Cult', |
| 'Gangsta', |
| 'Top 40', |
| 'Christian Rap', |
| 'Pop/Funk', |
| 'Jungle', |
| 'Native American', |
| 'Cabaret', |
| 'New Wave', |
| 'Psychadelic', |
| 'Rave', |
| 'Showtunes', |
| 'Trailer', |
| 'Lo-Fi', |
| 'Tribal', |
| 'Acid Punk', |
| 'Acid Jazz', |
| 'Polka', |
| 'Retro', |
| 'Musical', |
| 'Rock & Roll', |
| 'Hard Rock', |
| 'Folk', |
| 'Folk-Rock', |
| 'National Folk', |
| 'Swing', |
| 'Fast Fusion', |
| 'Bebob', |
| 'Latin', |
| 'Revival', |
| 'Celtic', |
| 'Bluegrass', |
| 'Avantgarde', |
| 'Gothic Rock', |
| 'Progressive Rock', |
| 'Psychedelic Rock', |
| 'Symphonic Rock', |
| 'Slow Rock', |
| 'Big Band', |
| 'Chorus', |
| 'Easy Listening', |
| 'Acoustic', |
| 'Humour', |
| 'Speech', |
| 'Chanson', |
| 'Opera', |
| 'Chamber Music', |
| 'Sonata', |
| 'Symphony', |
| 'Booty Bass', |
| 'Primus', |
| 'Porn Groove', |
| 'Satire', |
| 'Slow Jam', |
| 'Club', |
| 'Tango', |
| 'Samba', |
| 'Folklore', |
| 'Ballad', |
| 'Power Ballad', |
| 'Rhythmic Soul', |
| 'Freestyle', |
| 'Duet', |
| 'Punk Rock', |
| 'Drum Solo', |
| 'A capella', |
| 'Euro-House', |
| 'Dance Hall', |
| 'Goa', |
| 'Drum & Bass', |
| 'Club-House', |
| 'Hardcore', |
| 'Terror', |
| 'Indie', |
| 'BritPop', |
| 'Negerpunk', |
| 'Polsk Punk', |
| 'Beat', |
| 'Christian Gangsta Rap', |
| 'Heavy Metal', |
| 'Black Metal', |
| 'Crossover', |
| 'Contemporary Christian', |
| 'Christian Rock', |
| 'Merengue', |
| 'Salsa', |
| 'Thrash Metal', |
| 'Anime', |
| 'Jpop', |
| 'Synthpop' |
| ] |
| }; |
| |
| /** |
| * id3v2 constants |
| */ |
| Id3Parser.v2 = { |
| FLAG_EXTENDED_HEADER: 1 << 5, |
| |
| ENCODING: { |
| /** |
| * ISO-8859-1 [ISO-8859-1]. Terminated with $00. |
| * |
| * @const |
| * @type {number} |
| */ |
| ISO_8859_1: 0, |
| |
| |
| /** |
| * [UTF-16] encoded Unicode [UNICODE] with BOM. All |
| * strings in the same frame SHALL have the same byteorder. |
| * Terminated with $00 00. |
| * |
| * @const |
| * @type {number} |
| */ |
| UTF_16: 1, |
| |
| /** |
| * UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM. |
| * Terminated with $00 00. |
| * |
| * @const |
| * @type {number} |
| */ |
| UTF_16BE: 2, |
| |
| /** |
| * UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00. |
| * |
| * @const |
| * @type {number} |
| */ |
| UTF_8: 3 |
| }, |
| HANDLERS: { |
| //User defined text information frame |
| TXX: Id3Parser.prototype.readUserDefinedTextFrame_, |
| //User defined URL link frame |
| WXX: Id3Parser.prototype.readUserDefinedTextFrame_, |
| |
| //User defined text information frame |
| TXXX: Id3Parser.prototype.readUserDefinedTextFrame_, |
| |
| //User defined URL link frame |
| WXXX: Id3Parser.prototype.readUserDefinedTextFrame_, |
| |
| //User attached image |
| PIC: Id3Parser.prototype.readPIC_, |
| |
| //User attached image |
| APIC: Id3Parser.prototype.readAPIC_ |
| }, |
| MAPPERS: { |
| TALB: 'ID3_ALBUM', |
| TBPM: 'ID3_BPM', |
| TCOM: 'ID3_COMPOSER', |
| TDAT: 'ID3_DATE', |
| TDLY: 'ID3_PLAYLIST_DELAY', |
| TEXT: 'ID3_LYRICIST', |
| TFLT: 'ID3_FILE_TYPE', |
| TIME: 'ID3_TIME', |
| TIT2: 'ID3_TITLE', |
| TLEN: 'ID3_LENGTH', |
| TOWN: 'ID3_FILE_OWNER', |
| TPE1: 'ID3_LEAD_PERFORMER', |
| TPE2: 'ID3_BAND', |
| TRCK: 'ID3_TRACK_NUMBER', |
| TYER: 'ID3_YEAR', |
| WCOP: 'ID3_COPYRIGHT', |
| WOAF: 'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE', |
| WOAR: 'ID3_OFFICIAL_ARTIST', |
| WOAS: 'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE', |
| WPUB: 'ID3_PUBLISHERS_OFFICIAL_WEBPAGE' |
| } |
| }; |
| |
| MetadataDispatcher.registerParserClass(Id3Parser); |