jsmediatags
Version:
Media Tags Reader (ID3, MP4)
419 lines (345 loc) • 13.3 kB
JavaScript
/**
* This is only used for testing, but could be used for other purposes as
* writing.
*
* http://id3.org/id3v2-00
* http://id3.org/id3v2.3.0
* http://id3.org/id3v2.4.0-structure
*
* TODO: Padding and Footer
*
*/
'use strict';
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var ByteArrayUtils = require('./ByteArrayUtils');
var bin = ByteArrayUtils.bin;
var getSynchsafeInteger32 = ByteArrayUtils.getSynchsafeInteger32;
var getInteger32 = ByteArrayUtils.getInteger32;
var getInteger24 = ByteArrayUtils.getInteger24; // Offsets
var FLAGS = 5;
var SIZE = 6;
var EXTENDED_HEADER = 10;
var EXTENDED_FLAGS_V3 = 14;
var EXTENDED_FLAGS_V4 = 15;
var START_EXTENDED_DATA_V3 = 20;
var START_EXTENDED_DATA_V4 = 16; // Sizes
var HEADER_SIZE = 10;
var ID3v2TagContents = /*#__PURE__*/function () {
function ID3v2TagContents(major, revision) {
_classCallCheck(this, ID3v2TagContents);
_defineProperty(this, "_size", void 0);
_defineProperty(this, "_major", void 0);
_defineProperty(this, "_revision", void 0);
_defineProperty(this, "_contents", void 0);
_defineProperty(this, "_frames", void 0);
_defineProperty(this, "_extendedHeader", void 0);
_defineProperty(this, "_hasExtendedHeader", void 0);
_defineProperty(this, "_nextFrameOffset", void 0);
if (major < 2 || major > 4) {
throw new Error('Major version not supported');
}
this._major = major;
this._revision = revision;
this._contents = [].concat(bin("ID3"), [major, revision], [0], // flags
[0, 0, 0, 0] // size
);
this._frames = {};
this._updateSize();
this._extendedHeader = {
// key: length
'UPDATE': 0,
'CRC': 0,
'RESTRICTIONS': 0
};
}
_createClass(ID3v2TagContents, [{
key: "toArray",
value: function toArray() {
return this._contents.slice(0);
}
}, {
key: "setFlags",
value: function setFlags(flags) {
return this._updateFlags(flags, 0);
}
}, {
key: "_updateFlags",
value: function _updateFlags(flags, binaryFlags) {
if (typeof binaryFlags !== 'number') {
binaryFlags = this._contents[FLAGS] || 0;
}
function setOrUnsetBit(shouldSet, bitmap, bit) {
if (shouldSet) {
return bitmap |= 1 << bit;
} else {
return bitmap &= ~(1 << bit);
}
}
if (flags.hasOwnProperty('unsynchronisation')) {
binaryFlags = setOrUnsetBit(!!flags.unsynchronisation, binaryFlags, 7);
}
if (flags.hasOwnProperty('extended_header')) {
binaryFlags = setOrUnsetBit(!!flags.extended_header, binaryFlags, 6);
}
if (flags.hasOwnProperty('experimental_indicator')) {
binaryFlags = setOrUnsetBit(!!flags.experimental_indicator, binaryFlags, 5);
}
if (flags.hasOwnProperty('footer_present')) {
binaryFlags = setOrUnsetBit(!!flags.footer_present, binaryFlags, 4);
}
this._contents[FLAGS] = binaryFlags;
return this;
}
}, {
key: "setCrc",
value: function setCrc(crc) {
if (!this._hasExtendedHeader) {
this._initExtendedHeader();
}
if (this._major === 3) {
this._setBitAtOffset(EXTENDED_FLAGS_V3, 7);
this._setData(START_EXTENDED_DATA_V3, crc);
this._extendedHeader['CRC'] = crc.length; // Update extended header size.
this._setData(EXTENDED_HEADER, getInteger32(10));
} else if (this._major === 4) {
this._setBitAtOffset(EXTENDED_FLAGS_V4, 5);
this._addExtendedHeaderData('CRC', crc);
}
this._updateSize();
return this;
}
}, {
key: "setTagIsUpdate",
value: function setTagIsUpdate() {
if (!this._hasExtendedHeader) {
this._initExtendedHeader();
}
if (this._major === 4) {
this._setBitAtOffset(EXTENDED_FLAGS_V4, 6);
}
this._updateSize();
return this;
}
/**
* For some applications it might be desired to restrict a tag in more
* ways than imposed by the ID3v2 specification. Note that the
* presence of these restrictions does not affect how the tag is
* decoded, merely how it was restricted before encoding. If this flag
* is set the tag is restricted as follows:
*
* Flag data length $01
* Restrictions %ppqrrstt
*
* p - Tag size restrictions
* 00 No more than 128 frames and 1 MB total tag size.
* 01 No more than 64 frames and 128 KB total tag size.
* 10 No more than 32 frames and 40 KB total tag size.
* 11 No more than 32 frames and 4 KB total tag size.
*
* q - Text encoding restrictions
*
* 0 No restrictions
* 1 Strings are only encoded with ISO-8859-1 [ISO-8859-1] or
* UTF-8 [UTF-8].
*
* r - Text fields size restrictions
*
* 00 No restrictions
* 01 No string is longer than 1024 characters.
* 10 No string is longer than 128 characters.
* 11 No string is longer than 30 characters.
*
* Note that nothing is said about how many bytes is used to
* represent those characters, since it is encoding dependent. If a
* text frame consists of more than one string, the sum of the
* strungs is restricted as stated.
*
* s - Image encoding restrictions
*
* 0 No restrictions
* 1 Images are encoded only with PNG [PNG] or JPEG [JFIF].
*
* t - Image size restrictions
*
* 00 No restrictions
* 01 All images are 256x256 pixels or smaller.
* 10 All images are 64x64 pixels or smaller.
* 11 All images are exactly 64x64 pixels, unless required
* otherwise.
*/
}, {
key: "setTagRestrictions",
value: function setTagRestrictions(size, textEncoding, textSize, imageEncoding, imageSize) {
if (!this._hasExtendedHeader) {
this._initExtendedHeader();
}
if (this._major === 4) {
this._setBitAtOffset(EXTENDED_FLAGS_V4, 4); // 0x03 = 0b11
this._addExtendedHeaderData('RESTRICTIONS', [(size & 0x3) << 6 | (textEncoding & 0x1) << 5 | (textSize & 0x3) << 3 | (imageEncoding & 0x1) << 2 | (imageSize & 0x3) << 1]);
}
this._updateSize();
return this;
}
/**
* noFlagsDataLength - The data length if all flags were set to 0,
* for instance, the length before compression and unsynchronisation.
* This field is only needed when data_length_indicator flag is set.
*/
}, {
key: "addFrame",
value: function addFrame(id, data, flags, noFlagsDataLength) {
var size = 0;
var frameFlags = [0, 0];
if (flags) {
flags.message = flags.message || {};
flags.format = flags.format || {};
}
data = data || [];
var dataLength = data.length;
var isTagUnsynchronised = this._contents[FLAGS] & 1 << 7;
if (isTagUnsynchronised) {
var unsynchronisedByteCount = 0;
for (var i = 0; i < data.length - 1; i++) {
if (data[i] === 0xff && data[i + 1] === 0x00) {
unsynchronisedByteCount++;
}
}
dataLength -= unsynchronisedByteCount;
}
if (this._major === 2) {
size = getInteger24(dataLength);
} else if (this._major === 3) {
size = getInteger32(dataLength);
if (flags) {
frameFlags[0] |= (flags.message.tag_alter_preservation ? 1 : 0) << 7;
frameFlags[0] |= (flags.message.file_alter_preservation ? 1 : 0) << 6;
frameFlags[0] |= (flags.message.read_only ? 1 : 0) << 5;
frameFlags[1] |= (flags.format.compression ? 1 : 0) << 7;
frameFlags[1] |= (flags.format.encryption ? 1 : 0) << 6;
frameFlags[1] |= (flags.format.grouping_identify ? 1 : 0) << 5;
}
} else if (this._major === 4) {
if (flags) {
frameFlags[0] |= (flags.message.tag_alter_preservation ? 1 : 0) << 6;
frameFlags[0] |= (flags.message.file_alter_preservation ? 1 : 0) << 5;
frameFlags[0] |= (flags.message.read_only ? 1 : 0) << 4;
frameFlags[1] |= (flags.format.grouping_identify ? 1 : 0) << 6;
frameFlags[1] |= (flags.format.compression ? 1 : 0) << 3;
frameFlags[1] |= (flags.format.encryption ? 1 : 0) << 2;
frameFlags[1] |= (flags.format.unsynchronisation ? 1 : 0) << 1;
frameFlags[1] |= flags.format.data_length_indicator ? 1 : 0;
if (flags.format.data_length_indicator) {
dataLength += 4;
}
}
size = getSynchsafeInteger32(dataLength);
} else {
throw Error("Major version not supported");
}
var frame = [].concat(bin(id), size, frameFlags, flags && flags.format.data_length_indicator && noFlagsDataLength ? getSynchsafeInteger32(noFlagsDataLength) : [], data);
if (!this._frames[id]) {
this._frames[id] = [];
}
this._frames[id].push(frame);
this._addData(this._nextFrameOffset, frame);
this._updateSize();
return this;
}
}, {
key: "_addExtendedHeaderData",
value: function _addExtendedHeaderData(tagKey, tagData) {
var offset = START_EXTENDED_DATA_V4; // Each flag that is set in the extended header has data attached, which
// comes in the order in which the flags are encountered (i.e. the data
// for flag 'b' comes before the data for flag 'c').
// _extendedHeader keeps track of which tag data we have by storing the
// size of the data. To know where to add a particular tag data we just need
// to sum all the data lengths of the tags that come before this tagKey
// because the keys in the map are in order.
for (var key in this._extendedHeader) {
if (this._extendedHeader.hasOwnProperty(key)) {
if (key === tagKey) {
break;
} else {
offset += this._extendedHeader[key];
}
}
}
var data = [tagData.length].concat(tagData);
this._extendedHeader[tagKey] = data.length;
this._addData(offset, data);
}
}, {
key: "_initExtendedHeader",
value: function _initExtendedHeader() {
this._hasExtendedHeader = true;
this._updateFlags({
extended_header: true
});
if (this._major === 3) {
this._addData(EXTENDED_HEADER, [0, 0, 0, 6, // size
0, 0, // flags
0, 0, 0, 0 // padding
]);
} else if (this._major === 4) {
this._addData(EXTENDED_HEADER, [].concat(getSynchsafeInteger32(6), // size
[1], // number of flag bytes
[0] // extended flags
));
} else {
throw new Error("Version doesn't support extended header.");
}
}
}, {
key: "_updateSize",
value: function _updateSize() {
// Header (10 bytes) is not included in the size.
var size = 0;
if (this._hasExtendedHeader) {
// Extended header size
size += this._major === 4 ? 6 : 10; // Extended header data size
for (var key in this._extendedHeader) {
if (this._extendedHeader.hasOwnProperty(key)) {
size += this._extendedHeader[key];
}
}
}
for (var frameId in this._frames) {
if (this._frames.hasOwnProperty(frameId)) {
for (var i = 0, frame; frame = this._frames[frameId][i]; i++) {
size += frame.length;
}
}
}
this._nextFrameOffset = size + HEADER_SIZE;
this._size = size;
this._setData(SIZE, getSynchsafeInteger32(size));
}
}, {
key: "_setBitAtOffset",
value: function _setBitAtOffset(offset, bit) {
var data = this._getData(offset, 1);
data[0] |= 1 << bit;
this._setData(offset, data);
}
}, {
key: "_getData",
value: function _getData(offset, length) {
return this._contents.slice(offset, offset + length);
}
}, {
key: "_setData",
value: function _setData(offset, data) {
this._contents.splice.apply(this._contents, [offset, data.length].concat(data));
}
}, {
key: "_addData",
value: function _addData(offset, data) {
this._contents.splice.apply(this._contents, [offset, 0].concat(data));
}
}]);
return ID3v2TagContents;
}();
module.exports = ID3v2TagContents;