UNPKG

jsmediatags

Version:
374 lines (322 loc) 14.2 kB
/** * Support for iTunes-style m4a tags * See: * http://atomicparsley.sourceforge.net/mpeg-4files.html * http://developer.apple.com/mac/library/documentation/QuickTime/QTFF/Metadata/Metadata.html * Authored by Joshua Kifer <joshua.kifer gmail.com> * */ '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); } var MediaTagReader = require('./MediaTagReader'); var MediaFileReader = require('./MediaFileReader'); var MP4TagReader = /*#__PURE__*/function (_MediaTagReader) { _inherits(MP4TagReader, _MediaTagReader); var _super = _createSuper(MP4TagReader); function MP4TagReader() { _classCallCheck(this, MP4TagReader); return _super.apply(this, arguments); } _createClass(MP4TagReader, [{ key: "_loadData", value: function _loadData(mediaFileReader, callbacks) { // MP4 metadata isn't located in a specific location of the file. Roughly // speaking, it's composed of blocks chained together like a linked list. // These blocks are called atoms (or boxes). // Each atom of the list can have its own child linked list. Atoms in this // situation do not possess any data and are called "container" as they only // contain other atoms. // Other atoms represent a particular set of data, like audio, video or // metadata. In order to find and load all the interesting atoms we need // to traverse the entire linked list of atoms and only load the ones // associated with metadata. // The metadata atoms can be find under the "moov.udta.meta.ilst" hierarchy. var self = this; // Load the header of the first atom mediaFileReader.loadRange([0, 16], { onSuccess: function onSuccess() { self._loadAtom(mediaFileReader, 0, "", callbacks); }, onError: callbacks.onError }); } }, { key: "_loadAtom", value: function _loadAtom(mediaFileReader, offset, parentAtomFullName, callbacks) { if (offset >= mediaFileReader.getSize()) { callbacks.onSuccess(); return; } var self = this; // 8 is the size of the atomSize and atomName fields. // When reading the current block we always read 8 more bytes in order // to also read the header of the next block. var atomSize = mediaFileReader.getLongAt(offset, true); if (atomSize == 0 || isNaN(atomSize)) { callbacks.onSuccess(); return; } var atomName = mediaFileReader.getStringAt(offset + 4, 4); // console.log(parentAtomFullName, atomName, atomSize); // Container atoms (no actual data) if (this._isContainerAtom(atomName)) { if (atomName == "meta") { // The "meta" atom breaks convention and is a container with data. offset += 4; // next_item_id (uint32) } var atomFullName = (parentAtomFullName ? parentAtomFullName + "." : "") + atomName; if (atomFullName === "moov.udta.meta.ilst") { mediaFileReader.loadRange([offset, offset + atomSize], callbacks); } else { mediaFileReader.loadRange([offset + 8, offset + 8 + 8], { onSuccess: function onSuccess() { self._loadAtom(mediaFileReader, offset + 8, atomFullName, callbacks); }, onError: callbacks.onError }); } } else { mediaFileReader.loadRange([offset + atomSize, offset + atomSize + 8], { onSuccess: function onSuccess() { self._loadAtom(mediaFileReader, offset + atomSize, parentAtomFullName, callbacks); }, onError: callbacks.onError }); } } }, { key: "_isContainerAtom", value: function _isContainerAtom(atomName) { return ["moov", "udta", "meta", "ilst"].indexOf(atomName) >= 0; } }, { key: "_canReadAtom", value: function _canReadAtom(atomName) { return atomName !== "----"; } }, { key: "_parseData", value: function _parseData(data, tagsToRead) { var tags = {}; tagsToRead = this._expandShortcutTags(tagsToRead); this._readAtom(tags, data, 0, data.getSize(), tagsToRead); // create shortcuts for most common data. for (var name in SHORTCUTS) { if (SHORTCUTS.hasOwnProperty(name)) { var tag = tags[SHORTCUTS[name]]; if (tag) { if (name === "track") { tags[name] = tag.data.track; } else { tags[name] = tag.data; } } } } return { "type": "MP4", "ftyp": data.getStringAt(8, 4), "version": data.getLongAt(12, true), "tags": tags }; } }, { key: "_readAtom", value: function _readAtom(tags, data, offset, length, tagsToRead, parentAtomFullName, indent) { indent = indent === undefined ? "" : indent + " "; var seek = offset; while (seek < offset + length) { var atomSize = data.getLongAt(seek, true); if (atomSize == 0) { return; } var atomName = data.getStringAt(seek + 4, 4); // console.log(seek, parentAtomFullName, atomName, atomSize); if (this._isContainerAtom(atomName)) { if (atomName == "meta") { seek += 4; // next_item_id (uint32) } var atomFullName = (parentAtomFullName ? parentAtomFullName + "." : "") + atomName; this._readAtom(tags, data, seek + 8, atomSize - 8, tagsToRead, atomFullName, indent); return; } // Value atoms if ((!tagsToRead || tagsToRead.indexOf(atomName) >= 0) && parentAtomFullName === "moov.udta.meta.ilst" && this._canReadAtom(atomName)) { tags[atomName] = this._readMetadataAtom(data, seek); } seek += atomSize; } } }, { key: "_readMetadataAtom", value: function _readMetadataAtom(data, offset) { // 16: size + name + size + "data" (4 bytes each) // 8: 1 byte atom version & 3 bytes atom flags + 4 bytes NULL space // 8: 4 bytes track + 4 bytes total var METADATA_HEADER = 16; var atomSize = data.getLongAt(offset, true); var atomName = data.getStringAt(offset + 4, 4); var klass = data.getInteger24At(offset + METADATA_HEADER + 1, true); var type = TYPES[klass]; var atomData; var bigEndian = true; if (atomName == "trkn") { atomData = { "track": data.getShortAt(offset + METADATA_HEADER + 10, bigEndian), "total": data.getShortAt(offset + METADATA_HEADER + 14, bigEndian) }; } else if (atomName == "disk") { atomData = { "disk": data.getShortAt(offset + METADATA_HEADER + 10, bigEndian), "total": data.getShortAt(offset + METADATA_HEADER + 14, bigEndian) }; } else { // 4: atom version (1 byte) + atom flags (3 bytes) // 4: NULL (usually locale indicator) var atomHeader = METADATA_HEADER + 4 + 4; var dataStart = offset + atomHeader; var dataLength = atomSize - atomHeader; var atomData; // Workaround for covers being parsed as 'uint8' type despite being an 'covr' atom if (atomName === 'covr' && type === 'uint8') { type = 'jpeg'; } switch (type) { case "text": atomData = data.getStringWithCharsetAt(dataStart, dataLength, "utf-8").toString(); break; case "uint8": atomData = data.getShortAt(dataStart, false); break; case "int": case "uint": // Though the QuickTime spec doesn't state it, there are 64-bit values // such as plID (Playlist/Collection ID). With its single 64-bit floating // point number type, these are hard to parse and pass in JavaScript. // The high word of plID seems to always be zero, so, as this is the // only current 64-bit atom handled, it is parsed from its 32-bit // low word as an unsigned long. // var intReader = type == 'int' ? dataLength == 1 ? data.getSByteAt : dataLength == 2 ? data.getSShortAt : dataLength == 4 ? data.getSLongAt : data.getLongAt : dataLength == 1 ? data.getByteAt : dataLength == 2 ? data.getShortAt : data.getLongAt; // $FlowFixMe - getByteAt doesn't receive a second argument atomData = intReader.call(data, dataStart + (dataLength == 8 ? 4 : 0), true); break; case "jpeg": case "png": atomData = { "format": "image/" + type, "data": data.getBytesAt(dataStart, dataLength) }; break; } } return { id: atomName, size: atomSize, description: ATOM_DESCRIPTIONS[atomName] || "Unknown", data: atomData }; } }, { key: "getShortcuts", value: function getShortcuts() { return SHORTCUTS; } }], [{ key: "getTagIdentifierByteRange", value: function getTagIdentifierByteRange() { // The tag identifier is located in [4, 8] but since we'll need to reader // the header of the first block anyway, we load it instead to avoid // making two requests. return { offset: 0, length: 16 }; } }, { key: "canReadTagFormat", value: function canReadTagFormat(tagIdentifier) { var id = String.fromCharCode.apply(String, tagIdentifier.slice(4, 8)); return id === "ftyp"; } }]); return MP4TagReader; }(MediaTagReader); /* * https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35 */ var TYPES = { "0": "uint8", "1": "text", "13": "jpeg", "14": "png", "21": "int", "22": "uint" }; var ATOM_DESCRIPTIONS = { "©alb": "Album", "©ART": "Artist", "aART": "Album Artist", "©day": "Release Date", "©nam": "Title", "©gen": "Genre", "gnre": "Genre", "trkn": "Track Number", "©wrt": "Composer", "©too": "Encoding Tool", "©enc": "Encoded By", "cprt": "Copyright", "covr": "Cover Art", "©grp": "Grouping", "keyw": "Keywords", "©lyr": "Lyrics", "©cmt": "Comment", "tmpo": "Tempo", "cpil": "Compilation", "disk": "Disc Number", "tvsh": "TV Show Name", "tven": "TV Episode ID", "tvsn": "TV Season", "tves": "TV Episode", "tvnn": "TV Network", "desc": "Description", "ldes": "Long Description", "sonm": "Sort Name", "soar": "Sort Artist", "soaa": "Sort Album", "soco": "Sort Composer", "sosn": "Sort Show", "purd": "Purchase Date", "pcst": "Podcast", "purl": "Podcast URL", "catg": "Category", "hdvd": "HD Video", "stik": "Media Type", "rtng": "Content Rating", "pgap": "Gapless Playback", "apID": "Purchase Account", "sfID": "Country Code", "atID": "Artist ID", "cnID": "Catalog ID", "plID": "Collection ID", "geID": "Genre ID", "xid ": "Vendor Information", "flvr": "Codec Flavor" }; var UNSUPPORTED_ATOMS = { "----": 1 }; var SHORTCUTS = { "title": "©nam", "artist": "©ART", "album": "©alb", "year": "©day", "comment": "©cmt", "track": "trkn", "genre": "©gen", "picture": "covr", "lyrics": "©lyr" }; module.exports = MP4TagReader;