diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index e7c63ea9..e09d4800 100755
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -286,6 +286,7 @@
"JPath expression",
"CSS selector",
"Extract EXIF",
+ "Extract ID3",
"Extract Files"
]
},
diff --git a/src/core/operations/ExtractID3.mjs b/src/core/operations/ExtractID3.mjs
new file mode 100644
index 00000000..b06bdcfe
--- /dev/null
+++ b/src/core/operations/ExtractID3.mjs
@@ -0,0 +1,324 @@
+/**
+ * @author n1073645 [n1073645@gmail.com]
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2020
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+
+/**
+ * Extract ID3 operation
+ */
+class ExtractID3 extends Operation {
+
+ /**
+ * ExtractID3 constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Extract ID3";
+ this.module = "Default";
+ this.description = "This operation extracts ID3 metadata from an MP3 file.
ID3 is a metadata container most often used in conjunction with the MP3 audio file format. It allows information such as the title, artist, album, track number, and other information about the file to be stored in the file itself.";
+ this.infoURL = "https://wikipedia.org/wiki/ID3";
+ this.inputType = "ArrayBuffer";
+ this.outputType = "JSON";
+ this.presentType = "html";
+ this.args = [];
+ }
+
+ /**
+ * @param {ArrayBuffer} input
+ * @param {Object[]} args
+ * @returns {JSON}
+ */
+ run(input, args) {
+ input = new Uint8Array(input);
+
+ /**
+ * Extracts the ID3 header fields.
+ */
+ function extractHeader() {
+ if (!Array.from(input.slice(0, 3)).equals([0x49, 0x44, 0x33]))
+ throw new OperationError("No valid ID3 header.");
+
+ const header = {
+ "Type": "ID3",
+ // Tag version
+ "Version": input[3].toString() + "." + input[4].toString(),
+ // Header version
+ "Flags": input[5].toString()
+ };
+
+ input = input.slice(6);
+ return header;
+ }
+
+ /**
+ * Converts the size fields to a single integer.
+ *
+ * @param {number} num
+ * @returns {string}
+ */
+ function readSize(num) {
+ let result = 0;
+
+ // The sizes are 7 bit numbers stored in 8 bit locations
+ for (let i = (num) * 7; i; i -= 7) {
+ result = (result << i) | input[0];
+ input = input.slice(1);
+ }
+ return result;
+ }
+
+ /**
+ * Reads frame header based on ID.
+ *
+ * @param {string} id
+ * @returns {number}
+ */
+ function readFrame(id) {
+ const frame = {};
+
+ // Size of frame
+ const size = readSize(4);
+ frame.Size = size.toString();
+ frame.Description = FRAME_DESCRIPTIONS[id];
+ input = input.slice(2);
+
+ // Read data from frame
+ let data = "";
+ for (let i = 1; i < size; i++)
+ data += String.fromCharCode(input[i]);
+ frame.Data = data;
+
+ // Move to next Frame
+ input = input.slice(size);
+
+ return [frame, size];
+ }
+
+ const result = extractHeader();
+
+ const headerTagSize = readSize(4);
+ result.Size = headerTagSize.toString();
+
+ const tags = {};
+ let pos = 10;
+
+ // While the current element is in the header
+ while (pos < headerTagSize) {
+
+ // Frame Identifier of frame
+ let id = String.fromCharCode(input[0]) + String.fromCharCode(input[1]) + String.fromCharCode(input[2]);
+ input = input.slice(3);
+
+ // If the next character is non-zero it is an identifier
+ if (input[0] !== 0) {
+ id += String.fromCharCode(input[0]);
+ }
+ input = input.slice(1);
+
+ if (id in FRAME_DESCRIPTIONS) {
+ const [frame, size] = readFrame(id);
+ tags[id] = frame;
+ pos += 10 + size;
+ } else if (id === "\x00\x00\x00") { // end of header
+ break;
+ } else {
+ throw new OperationError("Unknown Frame Identifier: " + id);
+ }
+ }
+
+ result.Tags = tags;
+
+ return result;
+ }
+
+ /**
+ * Displays the extracted data in a more accessible format for web apps.
+ * @param {JSON} data
+ * @returns {html}
+ */
+ present(data) {
+ if (!data || !Object.prototype.hasOwnProperty.call(data, "Tags"))
+ return JSON.stringify(data, null, 4);
+
+ let output = `
Tag | Description | Data |
---|---|---|
${tagID} | ${Utils.escapeHtml(description)} | ${Utils.escapeHtml(contents)} |