UNPKG

mp3tag

Version:

A library for reading/writing mp3 tag data

371 lines (370 loc) 15.4 kB
"use strict"; /** This module defines the decoder/encoder class, which gets set to the tagdata.decoder property and provides * framedecoding and -encoding method based on the tags version. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Decoder = void 0; const _ = __importStar(require("lodash")); const util = __importStar(require("util")); const data_1 = require("./data"); const cp_1 = require("./cp"); const cp = __importStar(require("./cp")); var EncodingIdentifier; (function (EncodingIdentifier) { EncodingIdentifier["ISO_8895_1"] = "ISO-8895-1"; EncodingIdentifier["UTF_16_LE_BOM"] = "UTF-16LE-BOM"; EncodingIdentifier["UTF_16_BE_BOM"] = "UTF-16BE-BOM"; EncodingIdentifier["UTF_8_BOM"] = "UTF-8-BOM"; EncodingIdentifier["UTF_8"] = "UTF-8"; })(EncodingIdentifier || (EncodingIdentifier = {})); //TODO: Change name to extends SupportedEncodings class Encoding { name; bom; dbe; encodingByte; /** * @param name the name of the encoding * @param bom array of BOM bytes * @param dbe true if double-byte encoding (terminated wit 0x00 0x00) * @param encodingByte optional encoding byte used for identification */ constructor(name, bom, dbe, encodingByte) { this.name = name; this.bom = bom; this.dbe = dbe; this.encodingByte = encodingByte; } static [EncodingIdentifier.ISO_8895_1] = new Encoding(cp_1.Encoding.ISO_8895_1, [], false, 0x00); static [EncodingIdentifier.UTF_16_LE_BOM] = new Encoding(cp_1.Encoding.UTF_16LE, [0xFF, 0xFE], true, 0x01); static [EncodingIdentifier.UTF_16_BE_BOM] = new Encoding(cp_1.Encoding.UTF_16BE, [0xFE, 0xFF], true, undefined); static [EncodingIdentifier.UTF_8_BOM] = new Encoding(cp_1.Encoding.UTF_8, [0xEF, 0xBB, 0xBF], false, undefined); static [EncodingIdentifier.UTF_8] = new Encoding(cp_1.Encoding.UTF_8, [], false, 0x03); } /** List of unicode encodings supported as text encoding */ const UCEncodings = [ Encoding[EncodingIdentifier.UTF_16_LE_BOM], Encoding[EncodingIdentifier.UTF_16_BE_BOM], Encoding[EncodingIdentifier.UTF_8_BOM], Encoding[EncodingIdentifier.UTF_8] // Empty bom sets the default codepage ]; /** Defines the default encoding for the encodeString method */ const DEFAULT_ENCODINGS = { 3: Encoding[EncodingIdentifier.UTF_16_LE_BOM], 4: Encoding[EncodingIdentifier.UTF_8] }; /** Decoder class, used to de- and encode frames */ class Decoder { version; // major version of tagData /** Creates a new decoder for the given tag version */ constructor(majorVersion) { this.version = majorVersion; } /** This method decodes the given buffer into a js string. * * @param buffer the buffer to decode (the first byte is the encoding byte) * * @return the decoded string */ decodeString(buffer) { if (!(buffer instanceof Buffer)) { throw new Error(`Expected buffer, got: ${typeof (buffer)} - ${util.inspect(buffer, { showHidden: true })}`); } const data = buffer.subarray(1); try { return internal.decodeString(this, data, buffer[0]); } catch (err) { throw new Error(`Unsupported buffer encoding: ${util.inspect(buffer)}`); } } /** Decodes a comment frame buffer into a structure of {language,short,long} * * @param buffer the buffer of the comment frame to decode * * @return the decoded comment object */ decodeComment(buffer) { if (!(buffer instanceof Buffer)) { throw new Error(`Expected buffer, got: ${typeof (buffer)} - ${util.inspect(buffer, { showHidden: true })}`); } const encodingByte = buffer[0]; const language = buffer.toString('ascii', 1, 4); // the language code has acutally 3 characters! const data = buffer.subarray(4); try { this.getBufferEncoding(data, encodingByte); } catch (e) { throw new Error(`Unsupported buffer encoding: ${util.inspect(buffer)}`); } const cData = internal.decodeCString(this, data, encodingByte); //TODO try catch other error const shortComment = cData.string; const longComment = internal.decodeString(this, data.subarray(cData.pastNullPos)); return { language: language, short: shortComment, long: longComment }; } /** Encodes the given comment object back into a buffer * * @param comment the comment object to encode * * @return the encoded buffer */ encodeComment(comment) { const encoding = DEFAULT_ENCODINGS[this.version]; const shortComment = internal.encodeCString(comment.short, encoding); const longComment = internal.encodeString(comment.long, encoding); const result = Buffer.alloc(4 + shortComment.length + longComment.length); result[0] = encoding.encodingByte; // set appropriate encoding byte result.write(comment.language.substring(0, 3).padEnd(3), 1, 3, 'ascii'); shortComment.copy(result, 4); longComment.copy(result, 4 + shortComment.length); return result; } /** Decodes the popularity object from the given frame buffer. * * @param buffer the buffer to decode the popularity from * * @return the decoded popularity */ decodePopularity(buffer) { //v-- no unicode support for email const email = internal.decodeCString(this, buffer, 0x00); const rating = buffer[email.pastNullPos]; const offset = email.pastNullPos + 1; const played = internal.decodeNumberBE(buffer, offset, buffer.length - offset); return { email: email.string, rating: rating, playCount: played }; } /** * @param buffer the buffer to decode the buffer from * * @return the pictue object */ decodePicture(buffer) { if (!(buffer instanceof Buffer)) { throw new Error(`Expected buffer, got: ${typeof (buffer)} - ${util.inspect(buffer, { showHidden: true })}`); } const encodingByte = buffer[0]; const dataBuffer = buffer.subarray(1); //v-- MIME will always be ISO-8895-1 const mimeData = internal.decodeCString(this, dataBuffer, 0x00); const mimeType = mimeData.string; const pictureType = dataBuffer[mimeData.pastNullPos]; const descriptionBuffer = dataBuffer.subarray(mimeData.pastNullPos + 1); const descData = internal.decodeCString(this, descriptionBuffer, encodingByte); const description = descData.string; const pictureData = descriptionBuffer.subarray(descData.pastNullPos); return { mimeType: mimeType, // String pictureType: pictureType, // Integer description: description, // String pictureData: new data_1.Data(pictureData) // BufferData }; } /** Encodes the given picture data into a Buffer * * @param {Picture} picture the picture information in the same format as returned by decodePicture * * @return {Buffer} encoded picture data */ encodePicture(picture) { const defaultEncoding = DEFAULT_ENCODINGS[this.version]; const frameBuffers = [ Buffer.alloc(1, defaultEncoding.encodingByte), internal.encodeCString(picture.mimeType, Encoding[EncodingIdentifier.ISO_8895_1]), // mime is always encoded in ISO-8895-1 Buffer.alloc(1, picture.pictureType), internal.encodeCString(picture.description, defaultEncoding), picture.pictureData.toBuffer() ]; // Return the resulting buffer return Buffer.concat(frameBuffers); } /** Returns the encoding for the given buffer and encodingByte * If no encodingByte is passed the encoding is being guessed by searching the buffer * for a BOM. * * @param buffer the buffer (not containing the encodingByte) * @param encodingByte optional encoding byte, which specifies the buffer's encoding * * @return an encoding, which describes the detected buffer encoding */ getBufferEncoding(buffer, encodingByte) { if (encodingByte === undefined) { encodingByte = 0x01; // Default is unicode if not passed } if (encodingByte === 0x00) { // ISO-8895-1 encoding return Encoding[EncodingIdentifier.ISO_8895_1]; } if (encodingByte === 0x01) { // UC (search for BOM and look up) return _.find(UCEncodings, (encoding) => { for (let c = 0; c < encoding.bom.length; ++c) { if (buffer[c] !== encoding.bom[c]) { return false; } } return true; }); // ! tells TS that the resull cannot be undefined, because the last UC-encoding has an empty BOM, so it will always match } if (this.version >= 4) { // New encodings added with 2.4 if (encodingByte === 0x02) { return new Encoding(cp_1.Encoding.UTF_16BE, [], true, undefined); // UTF-16BE without BOM } if (encodingByte === 0x03) { return new Encoding(cp_1.Encoding.UTF_8, [], false, undefined); } } throw new Error(`Unknown encoding byte: '${encodingByte}'`); } /** Encodes the given string into a buffer with the default encoding, which is * UTF-16LE for v2.3 and UTF-8 for v2.4. * * @param string the string to encode * * @return the encoded buffer */ encodeString(string) { if (typeof (string) !== "string") { throw new Error(`Expected string, got: ${typeof (string)} - ${util.inspect(string, { showHidden: true })}`); } const encoding = DEFAULT_ENCODINGS[this.version]; const bomBuf = internal.encodeString(string, encoding); const result = Buffer.alloc(bomBuf.length + 1); result[0] = encoding.encodingByte; //set appropriate encoding byte bomBuf.copy(result, 1); return result; } } exports.Decoder = Decoder; const internal = { /** Same as decodeCString, but this string isn't zero terminated. The whole * buffer is treated as string. * * @param decoder the decoder to use for the decoding operation * @param buffer the buffer to read from * @param encodingByte optional byte which specifies, which encoding to use * * @return decoded string */ decodeString: function (decoder, buffer, encodingByte) { const encoding = decoder.getBufferEncoding(buffer, encodingByte); return cp.decodeBuffer(buffer.subarray(encoding.bom.length), encoding.name); }, /** Receives a zero terminated buffer to decode from and the encoding byte * * @param decoder the decoder to use for this operation * @param buffer the buffer to read the string from * @param encodingByte the byte, which specifies encoding * * @return the decoded string object */ decodeCString: function (decoder, buffer, encodingByte) { const encoding = decoder.getBufferEncoding(buffer, encodingByte); const nullPos = internal.getStringEndPos(buffer, encoding.dbe); if (nullPos === -1) { throw new Error(`Expected NULL terminated string, missing NULL byte(s), with encoding: ${encoding.name}`); } const pastNullPos = nullPos + (encoding.dbe ? 2 : 1); const contentSlice = buffer.subarray(encoding.bom.length, nullPos); const string = cp.decodeBuffer(contentSlice, encoding.name); return { string, nullPos, pastNullPos }; }, /** Encodes the string into a buffer with the encoding's BOM. * The encodingByte won't be written into the buffer as this is format specific. * * @param string the string to encode * @param encoding the encoding object, which represents the encoding to use * * @return the buffer with the encoded string */ encodeString: function (string, encoding) { const strBuffer = cp.encodeString(string, encoding.name); const result = Buffer.alloc(strBuffer.length + encoding.bom.length); for (let c = 0; c < encoding.bom.length; ++c) { result[c] = encoding.bom[c]; } strBuffer.copy(result, encoding.bom.length); return result; }, /** Encodes the string into a buffer with the encoding's BOM and terminates the string with a NULL character. * The encodingByte won't be written into the buffer as this is format specific. * * @param string the string to encode * @param encoding the encoding object, which represents the encoding to use * * @return the buffer with the encoded string */ encodeCString: function (string, encoding) { return internal.encodeString(string + '\0', encoding); }, /** Returns the offset of the NULL (double-)byte inside a string buffer * * @param buffer the buffer to search * @param isDoubleNull if true, the function treats the byte buffer like a two byte encoding * * @return returns the endpos */ getStringEndPos: function (buffer, isDoubleNull) { for (let c = 0; c < buffer.length; ++c) { if (buffer[c] == 0x00 && !isDoubleNull) { return c; } else if (buffer[c] == 0x00 && buffer[c + 1] == 0x00 && isDoubleNull) { return c; } if (isDoubleNull) { ++c; // Additional increment if double byte NULL is expected } } return -1; //Not found }, /** * @param buffer the buffer to decode the number from * @param offset the offset to start decoding the number from * @param bytes the number of bytes which encode the number * * @return the decoded number */ decodeNumberBE: function (buffer, offset, bytes) { let result = 0; for (let c = offset; c < offset + bytes; ++c) { result = ((result << 8) | buffer[c]); // Decode big endian number } return result; } };