jsmediatags
Version:
Media Tags Reader (ID3, MP4)
382 lines (330 loc) • 17.9 kB
JavaScript
;
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
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 _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
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 MediaTagReader = require('./MediaTagReader');
/* The first 4 bytes of a FLAC file describes the header for the file. If these
* bytes respectively read "fLaC", we can determine it is a FLAC file.
*/
var FLAC_HEADER_SIZE = 4;
/* FLAC metadata is stored in blocks containing data ranging from STREAMINFO to
* VORBIS_COMMENT, which is what we want to work with.
*
* Each metadata header is 4 bytes long, with the first byte determining whether
* it is the last metadata block before the audio data and what the block type is.
* This first byte can further be split into 8 bits, with the first bit being the
* last-metadata-block flag, and the last three bits being the block type.
*
* Since the specification states that the decimal value for a VORBIS_COMMENT block
* type is 4, the two possibilities for the comment block header values are:
* - 00000100 (Not a last metadata comment block, value of 4)
* - 10000100 (A last metadata comment block, value of 132)
*
* Similarly, the picture block header values are 6 and 128.
*
* All values for METADATA_BLOCK_HEADER can be found here.
* https://xiph.org/flac/format.html#metadata_block_header
*/
var COMMENT_HEADERS = [4, 132];
var PICTURE_HEADERS = [6, 134]; // These are the possible image types as defined by the FLAC specification.
var IMAGE_TYPES = ["Other", "32x32 pixels 'file icon' (PNG only)", "Other file icon", "Cover (front)", "Cover (back)", "Leaflet page", "Media (e.g. label side of CD)", "Lead artist/lead performer/soloist", "Artist/performer", "Conductor", "Band/Orchestra", "Composer", "Lyricist/text writer", "Recording Location", "During recording", "During performance", "Movie/video screen capture", "A bright coloured fish", "Illustration", "Band/artist logotype", "Publisher/Studio logotype"];
/**
* Class representing a MediaTagReader that parses FLAC tags.
*/
var FLACTagReader = /*#__PURE__*/function (_MediaTagReader) {
_inherits(FLACTagReader, _MediaTagReader);
var _super = _createSuper(FLACTagReader);
function FLACTagReader() {
var _this;
_classCallCheck(this, FLACTagReader);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _super.call.apply(_super, [this].concat(args));
_defineProperty(_assertThisInitialized(_this), "_commentOffset", void 0);
_defineProperty(_assertThisInitialized(_this), "_pictureOffset", void 0);
return _this;
}
_createClass(FLACTagReader, [{
key: "_loadData",
value:
/**
* Function called to load the data from the file.
*
* To begin processing the blocks, the next 4 bytes after the initial 4 bytes
* (bytes 4 through 7) are loaded. From there, the rest of the loading process
* is passed on to the _loadBlock function, which will handle the rest of the
* parsing for the metadata blocks.
*
* @param {MediaFileReader} mediaFileReader - The MediaFileReader used to parse the file.
* @param {LoadCallbackType} callbacks - The callback to call once _loadData is completed.
*/
function _loadData(mediaFileReader, callbacks) {
var self = this;
mediaFileReader.loadRange([4, 7], {
onSuccess: function onSuccess() {
self._loadBlock(mediaFileReader, 4, callbacks);
}
});
}
/**
* Special internal function used to parse the different FLAC blocks.
*
* The FLAC specification doesn't specify a specific location for metadata to resign, but
* dictates that it may be in one of various blocks located throughout the file. To load the
* metadata, we must locate the header first. This can be done by reading the first byte of
* each block to determine the block type. After the block type comes a 24 bit integer that stores
* the length of the block as big endian. Using this, we locate the block and store the offset for
* parsing later.
*
* After each block has been parsed, the _nextBlock function is called in order
* to parse the information of the next block. All blocks need to be parsed in order to find
* all of the picture and comment blocks.
*
* More info on the FLAC specification may be found here:
* https://xiph.org/flac/format.html
* @param {MediaFileReader} mediaFileReader - The MediaFileReader used to parse the file.
* @param {number} offset - The offset to start checking the header from.
* @param {LoadCallbackType} callbacks - The callback to call once the header has been found.
*/
}, {
key: "_loadBlock",
value: function _loadBlock(mediaFileReader, offset, callbacks) {
var self = this;
/* As mentioned above, this first byte is loaded to see what metadata type
* this block represents.
*/
var blockHeader = mediaFileReader.getByteAt(offset);
/* The last three bytes (integer 24) contain a value representing the length
* of the following metadata block. The 1 is added in order to shift the offset
* by one to get the last three bytes in the block header.
*/
var blockSize = mediaFileReader.getInteger24At(offset + 1, true);
/* This conditional checks if blockHeader (the byte retrieved representing the
* type of the header) is one the headers we are looking for.
*
* If that is not true, the block is skipped over and the next range is loaded:
* - offset + 4 + blockSize adds 4 to skip over the initial metadata header and
* blockSize to skip over the block overall, placing it at the head of the next
* metadata header.
* - offset + 4 + 4 + blockSize does the same thing as the previous block with
* the exception of adding another 4 bytes to move it to the end of the new metadata
* header.
*/
if (COMMENT_HEADERS.indexOf(blockHeader) !== -1) {
/* 4 is added to offset to move it to the head of the actual metadata.
* The range starting from offsetMatadata (the beginning of the block)
* and offsetMetadata + blockSize (the end of the block) is loaded.
*/
var offsetMetadata = offset + 4;
mediaFileReader.loadRange([offsetMetadata, offsetMetadata + blockSize], {
onSuccess: function onSuccess() {
self._commentOffset = offsetMetadata;
self._nextBlock(mediaFileReader, offset, blockHeader, blockSize, callbacks);
}
});
} else if (PICTURE_HEADERS.indexOf(blockHeader) !== -1) {
var offsetMetadata = offset + 4;
mediaFileReader.loadRange([offsetMetadata, offsetMetadata + blockSize], {
onSuccess: function onSuccess() {
self._pictureOffset = offsetMetadata;
self._nextBlock(mediaFileReader, offset, blockHeader, blockSize, callbacks);
}
});
} else {
self._nextBlock(mediaFileReader, offset, blockHeader, blockSize, callbacks);
}
}
/**
* Internal function used to load the next range and respective block.
*
* If the metadata block that was identified is not the last block before the
* audio blocks, the function will continue loading the next blocks. If it is
* the last block (identified by any values greater than 127, see FLAC spec.),
* the function will determine whether a comment block had been identified.
*
* If the block does not exist, the error callback is called. Otherwise, the function
* will call the success callback, allowing data parsing to begin.
* @param {MediaFileReader} mediaFileReader - The MediaFileReader used to parse the file.
* @param {number} offset - The offset that the existing header was located at.
* @param {number} blockHeader - An integer reflecting the header type of the block.
* @param {number} blockSize - The size of the previously processed header.
* @param {LoadCallbackType} callbacks - The callback functions to be called.
*/
}, {
key: "_nextBlock",
value: function _nextBlock(mediaFileReader, offset, blockHeader, blockSize, callbacks) {
var self = this;
if (blockHeader > 127) {
if (!self._commentOffset) {
callbacks.onError({
"type": "loadData",
"info": "Comment block could not be found."
});
} else {
callbacks.onSuccess();
}
} else {
mediaFileReader.loadRange([offset + 4 + blockSize, offset + 4 + 4 + blockSize], {
onSuccess: function onSuccess() {
self._loadBlock(mediaFileReader, offset + 4 + blockSize, callbacks);
}
});
}
}
/**
* Parses the data and returns the tags.
*
* This is an overview of the VorbisComment format and what this function attempts to
* retrieve:
* - First 4 bytes: a long that contains the length of the vendor string.
* - Next n bytes: the vendor string encoded in UTF-8.
* - Next 4 bytes: a long representing how many comments are in this block
* For each comment that exists:
* - First 4 bytes: a long representing the length of the comment
* - Next n bytes: the comment encoded in UTF-8.
* The comment string will usually appear in a format similar to:
* ARTIST=me
*
* Note that the longs and integers in this block are encoded in little endian
* as opposed to big endian for the rest of the FLAC spec.
* @param {MediaFileReader} data - The MediaFileReader to parse the file with.
* @param {Array<string>} [tags] - Optional tags to also be retrieved from the file.
* @return {TagType} - An object containing the tag information for the file.
*/
}, {
key: "_parseData",
value: function _parseData(data, tags) {
var vendorLength = data.getLongAt(this._commentOffset, false);
var offsetVendor = this._commentOffset + 4;
/* This line is able to retrieve the vendor string that the VorbisComment block
* contains. However, it is not part of the tags that JSMediaTags normally retrieves,
* and is therefore commented out.
*/
// var vendor = data.getStringWithCharsetAt(offsetVendor, vendorLength, "utf-8").toString();
var offsetList = vendorLength + offsetVendor;
/* To get the metadata from the block, we first get the long that contains the
* number of actual comment values that are existent within the block.
*
* As we loop through all of the comment blocks, we get the data length in order to
* get the right size string, and then determine which category that string falls under.
* The dataOffset variable is constantly updated so that it is at the beginning of the
* comment that is currently being parsed.
*
* Additions of 4 here are used to move the offset past the first 4 bytes which only contain
* the length of the comment.
*/
var numComments = data.getLongAt(offsetList, false);
var dataOffset = offsetList + 4;
var title, artist, album, track, genre, picture;
for (var i = 0; i < numComments; i++) {
var _dataLength = data.getLongAt(dataOffset, false);
var s = data.getStringWithCharsetAt(dataOffset + 4, _dataLength, "utf-8").toString();
var d = s.indexOf("=");
var split = [s.slice(0, d), s.slice(d + 1)];
switch (split[0].toUpperCase()) {
case "TITLE":
title = split[1];
break;
case "ARTIST":
artist = split[1];
break;
case "ALBUM":
album = split[1];
break;
case "TRACKNUMBER":
track = split[1];
break;
case "GENRE":
genre = split[1];
break;
}
dataOffset += 4 + _dataLength;
}
/* If a picture offset was found and assigned, then the reader will start processing
* the picture block from that point.
*
* All the lengths for the picture data can be found online here:
* https://xiph.org/flac/format.html#metadata_block_picture
*/
if (this._pictureOffset) {
var imageType = data.getLongAt(this._pictureOffset, true);
var offsetMimeLength = this._pictureOffset + 4;
var mimeLength = data.getLongAt(offsetMimeLength, true);
var offsetMime = offsetMimeLength + 4;
var mime = data.getStringAt(offsetMime, mimeLength);
var offsetDescriptionLength = offsetMime + mimeLength;
var descriptionLength = data.getLongAt(offsetDescriptionLength, true);
var offsetDescription = offsetDescriptionLength + 4;
var description = data.getStringWithCharsetAt(offsetDescription, descriptionLength, "utf-8").toString();
var offsetDataLength = offsetDescription + descriptionLength + 16;
var dataLength = data.getLongAt(offsetDataLength, true);
var offsetData = offsetDataLength + 4;
var imageData = data.getBytesAt(offsetData, dataLength, true);
picture = {
format: mime,
type: IMAGE_TYPES[imageType],
description: description,
data: imageData
};
}
var tag = {
type: "FLAC",
version: "1",
tags: {
"title": title,
"artist": artist,
"album": album,
"track": track,
"genre": genre,
"picture": picture
}
};
return tag;
}
}], [{
key: "getTagIdentifierByteRange",
value:
/**
* Gets the byte range for the tag identifier.
*
* Because the Vorbis comment block is not guaranteed to be in a specified
* location, we can only load the first 4 bytes of the file to confirm it
* is a FLAC first.
*
* @return {ByteRange} The byte range that identifies the tag for a FLAC.
*/
function getTagIdentifierByteRange() {
return {
offset: 0,
length: FLAC_HEADER_SIZE
};
}
/**
* Determines whether or not this reader can read a certain tag format.
*
* This checks that the first 4 characters in the file are fLaC, which
* according to the FLAC file specification should be the characters that
* indicate a FLAC file.
*
* @return {boolean} True if the header is fLaC, false otherwise.
*/
}, {
key: "canReadTagFormat",
value: function canReadTagFormat(tagIdentifier) {
var id = String.fromCharCode.apply(String, tagIdentifier.slice(0, 4));
return id === 'fLaC';
}
}]);
return FLACTagReader;
}(MediaTagReader);
module.exports = FLACTagReader;