UNPKG

jsmediatags

Version:
382 lines (330 loc) 17.9 kB
"use strict"; 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;