jsmediatags
Version:
Media Tags Reader (ID3, MP4)
374 lines (322 loc) • 14.2 kB
JavaScript
/**
* 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>
*
*/
;
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;