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.
7 importScripts('function_sequence.js');
8 importScripts('function_parallel.js');
10 function Id3Parser(parent) {
11 MetadataParser.call(this, parent, 'id3', /\.(mp3)$/i);
14 Id3Parser.prototype = {__proto__: MetadataParser.prototype};
17 * Reads synchsafe integer.
18 * 'SynchSafe' term is taken from id3 documentation.
20 * @param {ByteReader} reader - reader to use.
21 * @param {number} length - bytes to read.
22 * @return {number} // TODO(JSDOC).
25 Id3Parser.readSynchSafe_ = function(reader, length) {
30 rv = reader.readScalar(1, false) << 21;
32 rv |= reader.readScalar(1, false) << 14;
34 rv |= reader.readScalar(1, false) << 7;
36 rv |= reader.readScalar(1, false);
43 * Reads 3bytes integer.
45 * @param {ByteReader} reader - reader to use.
46 * @return {number} // TODO(JSDOC).
49 Id3Parser.readUInt24_ = function(reader) {
50 return reader.readScalar(2, false) << 16 | reader.readScalar(1, false);
54 * Reads string from reader with specified encoding
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).
62 Id3Parser.prototype.readString_ = function(reader, encoding, size) {
64 case Id3Parser.v2.ENCODING.ISO_8859_1:
65 return reader.readNullTerminatedString(size);
67 case Id3Parser.v2.ENCODING.UTF_16:
68 return reader.readNullTerminatedStringUTF16(true, size);
70 case Id3Parser.v2.ENCODING.UTF_16BE:
71 return reader.readNullTerminatedStringUTF16(false, size);
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);
79 this.log('Unsupported encoding in ID3 tag: ' + encoding);
86 * Reads text frame from reader.
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.
94 Id3Parser.prototype.readTextFrame_ = function(reader,
98 frame.encoding = reader.readScalar(1, false, end);
99 frame.value = this.readString_(reader, frame.encoding, end - reader.tell());
103 * Reads user defined text frame from reader.
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.
111 Id3Parser.prototype.readUserDefinedTextFrame_ = function(reader,
115 frame.encoding = reader.readScalar(1, false, end);
117 frame.description = this.readString_(
120 end - reader.tell());
122 frame.value = this.readString_(
125 end - reader.tell());
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.
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,
141 end - reader.tell());
144 if (frame.format == '-->') {
145 frame.imageUrl = reader.readNullTerminatedString(end - reader.tell());
147 frame.imageUrl = reader.readImage(end - reader.tell());
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.
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_(
166 end - reader.tell());
168 if (frame.mime == '-->') {
169 frame.imageUrl = reader.readNullTerminatedString(end - reader.tell());
171 frame.imageUrl = reader.readImage(end - reader.tell());
176 * Reads string from reader with specified encoding
178 * @param {ByteReader} reader reader to use.
179 * @param {number} majorVersion // TODO(JSDOC).
180 * @return {Object} frame read.
183 Id3Parser.prototype.readFrame_ = function(reader, majorVersion) {
189 reader.pushSeek(reader.tell(), ByteReader.SEEK_BEG);
191 var position = reader.tell();
193 frame.name = (majorVersion == 2) ? reader.readNullTerminatedString(3) :
194 reader.readNullTerminatedString(4);
196 if (frame.name == '')
199 this.vlog('Found frame ' + (frame.name) + ' at position ' + position);
201 switch (majorVersion) {
203 frame.size = Id3Parser.readUInt24_(reader);
204 frame.headerSize = 6;
207 frame.size = reader.readScalar(4, false);
208 frame.headerSize = 10;
209 frame.flags = reader.readScalar(2, false);
212 frame.size = Id3Parser.readSynchSafe_(reader, 4);
213 frame.headerSize = 10;
214 frame.flags = reader.readScalar(2, false);
218 this.vlog('Found frame [' + frame.name + '] with size [' + frame.size + ']');
220 if (Id3Parser.v2.HANDLERS[frame.name]) {
221 Id3Parser.v2.HANDLERS[frame.name].call(
226 reader.tell() + frame.size);
227 } else if (frame.name.charAt(0) == 'T' || frame.name.charAt(0) == 'W') {
232 reader.tell() + frame.size);
237 reader.seek(frame.size + frame.headerSize, ByteReader.SEEK_CUR);
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).
248 Id3Parser.prototype.parse = function(file, metadata, callback, onError) {
251 this.log('Starting id3 parser for ' + file.name);
253 var id3v1Parser = new FunctionSequence(
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.
262 function readTail(file) {
263 util.readFileBytes(file, file.size - 128, file.size,
264 this.nextStep, this.onError, this);
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.
273 function extractId3v1(file, reader) {
274 if (reader.readString(3) == 'TAG') {
275 this.logger.vlog('id3v1 found');
276 var id3v1 = metadata.id3v1 = {};
278 var title = reader.readNullTerminatedString(30).trim();
280 if (title.length > 0) {
281 metadata.title = title;
284 reader.seek(3 + 30, ByteReader.SEEK_BEG);
286 var artist = reader.readNullTerminatedString(30).trim();
287 if (artist.length > 0) {
288 metadata.artist = artist;
291 reader.seek(3 + 30 + 30, ByteReader.SEEK_BEG);
293 var album = reader.readNullTerminatedString(30).trim();
294 if (album.length > 0) {
295 metadata.album = album;
304 var id3v2Parser = new FunctionSequence(
307 function readHead(file) {
308 util.readFileBytes(file, 0, 10, this.nextStep, this.onError,
313 * Check if passed array of 10 bytes contains ID3 header.
314 * @param {File} file File to check and continue reading if ID3
316 * @param {ByteReader} reader Reader to fill with stream bytes.
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);
327 util.readFileBytes(file, 10, 10 + id3v2.size, this.nextStep,
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.
339 function extractFrames(file, reader) {
340 var id3v2 = metadata.id3v2;
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);
354 while (frame = self.readFrame_(reader, id3v2.major)) {
355 metadata.id3v2[frame.name] = frame;
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.
368 function prepareDescription() {
369 var id3v2 = metadata.id3v2;
372 metadata.thumbnailURL = id3v2['APIC'].imageUrl;
373 else if (id3v2['PIC'])
374 metadata.thumbnailURL = id3v2['PIC'].imageUrl;
376 metadata.description = [];
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()
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;
398 extract('album', 'TALB', 'TAL');
399 extract('title', 'TIT2', 'TT2');
400 extract('artist', 'TPE1', 'TP1');
402 metadata.description.sort(function(a, b) {
403 return Id3Parser.METADATA_ORDER.indexOf(a.key) -
404 Id3Parser.METADATA_ORDER.indexOf(b.key);
412 var metadataParser = new FunctionParallel(
414 [id3v1Parser, id3v2Parser],
417 callback.call(null, metadata);
422 id3v1Parser.setCallback(metadataParser.nextStep);
423 id3v2Parser.setCallback(metadataParser.nextStep);
425 id3v1Parser.setFailureCallback(metadataParser.onError);
426 id3v2Parser.setFailureCallback(metadataParser.onError);
428 this.vlog('Passed argument : ' + file);
430 metadataParser.start(file);
435 * Metadata order to use for metadata generation
437 Id3Parser.METADATA_ORDER = [
439 'ID3_LEAD_PERFORMER',
446 'ID3_PLAYLIST_DELAY',
454 'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE',
455 'ID3_OFFICIAL_ARTIST',
456 'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE',
457 'ID3_PUBLISHERS_OFFICIAL_WEBPAGE'
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 for 10% and most time
469 * translation would degrade to transliteration.
608 'Christian Gangsta Rap',
612 'Contemporary Christian',
627 FLAG_EXTENDED_HEADER: 1 << 5,
631 * ISO-8859-1 [ISO-8859-1]. Terminated with $00.
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.
650 * UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM.
651 * Terminated with $00 00.
659 * UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00.
667 //User defined text information frame
668 TXX: Id3Parser.prototype.readUserDefinedTextFrame_,
669 //User defined URL link frame
670 WXX: Id3Parser.prototype.readUserDefinedTextFrame_,
672 //User defined text information frame
673 TXXX: Id3Parser.prototype.readUserDefinedTextFrame_,
675 //User defined URL link frame
676 WXXX: Id3Parser.prototype.readUserDefinedTextFrame_,
678 //User attached image
679 PIC: Id3Parser.prototype.readPIC_,
681 //User attached image
682 APIC: Id3Parser.prototype.readAPIC_
687 TCOM: 'ID3_COMPOSER',
689 TDLY: 'ID3_PLAYLIST_DELAY',
690 TEXT: 'ID3_LYRICIST',
691 TFLT: 'ID3_FILE_TYPE',
695 TOWN: 'ID3_FILE_OWNER',
696 TPE1: 'ID3_LEAD_PERFORMER',
698 TRCK: 'ID3_TRACK_NUMBER',
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'
708 MetadataDispatcher.registerParserClass(Id3Parser);