UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

655 lines (654 loc) 22.5 kB
import { o as __toESM } from "./chunk-CNf5ZN-e.js"; import { t as require_src } from "./src-6PhJB9jV.js"; import { E as Uint8ArrayType, O as textDecode, T as UINT8, o as makeUnexpectedFileContentError, x as UINT32_BE, y as UINT24_BE } from "./BasicParser-CSYy5xVJ.js"; import { a as getBit, i as findZero, n as decodeString, r as decodeUintBE } from "./Util--vOO_7DL.js"; import { a as SyncTextHeader, l as UINT32SYNCSAFE, n as ExtendedHeader, o as TextEncodingToken, r as ID3v2Header, s as TextHeader, t as AttachedPictureType } from "./ID3v2Token-Vj5Brwlp.js"; import { t as Genres } from "./ID3v1Parser-BDoQ48nd.js"; //#region node_modules/music-metadata/lib/id3v2/ID3v2ChapterToken.js var import_src = /* @__PURE__ */ __toESM(require_src(), 1); /** * Data portion of `CHAP` sub frame */ const ChapterInfo = { len: 16, get: (buf, off) => { const startOffset = UINT32_BE.get(buf, off + 8); const endOffset = UINT32_BE.get(buf, off + 12); return { startTime: UINT32_BE.get(buf, off), endTime: UINT32_BE.get(buf, off + 4), startOffset: startOffset === 4294967295 ? void 0 : startOffset, endOffset: endOffset === 4294967295 ? void 0 : endOffset }; } }; //#endregion //#region node_modules/music-metadata/lib/id3v2/FrameHeader.js /** * Frame header length (bytes) depending on ID3v2 major version. */ function getFrameHeaderLength(majorVer) { switch (majorVer) { case 2: return 6; case 3: case 4: return 10; default: throw makeUnexpectedMajorVersionError$2(majorVer); } } function readFrameFlags(b) { return { status: { tag_alter_preservation: getBit(b, 0, 6), file_alter_preservation: getBit(b, 0, 5), read_only: getBit(b, 0, 4) }, format: { grouping_identity: getBit(b, 1, 7), compression: getBit(b, 1, 3), encryption: getBit(b, 1, 2), unsynchronisation: getBit(b, 1, 1), data_length_indicator: getBit(b, 1, 0) } }; } /** * Factory: parse a frame header from its header bytes (6 for v2.2, 10 for v2.3/v2.4). * * Note: It only *parses* and does light validation. It does not read payload bytes. */ function readFrameHeader(uint8Array, majorVer, warningCollector) { switch (majorVer) { case 2: return parseFrameHeaderV22(uint8Array, majorVer, warningCollector); case 3: case 4: return parseFrameHeaderV23V24(uint8Array, majorVer, warningCollector); default: throw makeUnexpectedMajorVersionError$2(majorVer); } } function parseFrameHeaderV22(uint8Array, majorVer, warningCollector) { const header = { id: textDecode(uint8Array.subarray(0, 3), "ascii"), length: UINT24_BE.get(uint8Array, 3) }; if (!header.id.match(/^[A-Z0-9]{3}$/)) warningCollector.addWarning(`Invalid ID3v2.${majorVer} frame-header-ID: ${header.id}`); return header; } function parseFrameHeaderV23V24(uint8Array, majorVer, warningCollector) { const header = { id: textDecode(uint8Array.subarray(0, 4), "ascii"), length: (majorVer === 4 ? UINT32SYNCSAFE : UINT32_BE).get(uint8Array, 4), flags: readFrameFlags(uint8Array.subarray(8, 10)) }; if (!header.id.match(/^[A-Z0-9]{4}$/)) warningCollector.addWarning(`Invalid ID3v2.${majorVer} frame-header-ID: ${header.id}`); return header; } function makeUnexpectedMajorVersionError$2(majorVer) { throw new Id3v2ContentError(`Unexpected majorVer: ${majorVer}`); } //#endregion //#region node_modules/music-metadata/lib/id3v2/FrameParser.js const debug = (0, import_src.default)("music-metadata:id3v2:frame-parser"); const defaultEnc = "latin1"; const urlEnc = { encoding: defaultEnc, bom: false }; function parseGenre(origVal) { const genres = []; let code; let word = ""; for (const c of origVal) if (typeof code === "string") if (c === "(" && code === "") { word += "("; code = void 0; } else if (c === ")") { if (word !== "") { genres.push(word); word = ""; } const genre = parseGenreCode(code); if (genre) genres.push(genre); code = void 0; } else code += c; else if (c === "(") code = ""; else word += c; if (word) { if (genres.length === 0 && word.match(/^\d*$/)) word = parseGenreCode(word); if (word) genres.push(word); } return genres; } function parseGenreCode(code) { if (code === "RX") return "Remix"; if (code === "CR") return "Cover"; if (code.match(/^\d*$/)) return Genres[Number.parseInt(code, 10)]; } var FrameParser = class FrameParser { /** * Create id3v2 frame parser * @param major - Major version, e.g. (4) for id3v2.4 * @param warningCollector - Used to collect decode issue */ constructor(major, warningCollector) { this.major = major; this.warningCollector = warningCollector; } readData(uint8Array, type, includeCovers) { if (uint8Array.length === 0) { this.warningCollector.addWarning(`id3v2.${this.major} header has empty tag type=${type}`); return; } const { encoding, bom } = TextEncodingToken.get(uint8Array, 0); const length = uint8Array.length; let offset = 0; let output = []; const nullTerminatorLength = FrameParser.getNullTerminatorLength(encoding); let fzero; debug(`Parsing tag type=${type}, encoding=${encoding}, bom=${bom}`); switch (type !== "TXXX" && type[0] === "T" ? "T*" : type) { case "T*": case "GRP1": case "GP1": case "IPLS": case "MVIN": case "MVNM": case "PCS": case "PCST": { let text; try { text = FrameParser.trimNullPadding(decodeString(uint8Array.subarray(1), encoding)); } catch (error) { if (error instanceof Error) { this.warningCollector.addWarning(`id3v2.${this.major} type=${type} header has invalid string value: ${error.message}`); break; } throw error; } switch (type) { case "TMCL": case "TIPL": case "IPLS": output = FrameParser.functionList(this.splitValue(type, text)); break; case "TRK": case "TRCK": case "TPOS": case "TIT1": case "TIT2": case "TIT3": output = text; break; case "TCOM": case "TEXT": case "TOLY": case "TOPE": case "TPE1": case "TSRC": output = this.splitValue(type, text); break; case "TCO": case "TCON": output = this.splitValue(type, text).map((v) => parseGenre(v)).reduce((acc, val) => acc.concat(val), []); break; case "PCS": case "PCST": output = this.major >= 4 ? this.splitValue(type, text) : [text]; output = Array.isArray(output) && output[0] === "" ? 1 : 0; break; default: output = this.major >= 4 ? this.splitValue(type, text) : [text]; } break; } case "TXXX": { const idAndData = FrameParser.readIdentifierAndData(uint8Array.subarray(1), encoding); output = { description: idAndData.id, text: this.splitValue(type, decodeString(idAndData.data, encoding).replace(/\x00+$/, "")) }; break; } case "PIC": case "APIC": if (includeCovers) { const pic = {}; uint8Array = uint8Array.subarray(1); switch (this.major) { case 2: pic.format = decodeString(uint8Array.subarray(0, 3), "latin1"); uint8Array = uint8Array.subarray(3); break; case 3: case 4: fzero = findZero(uint8Array, defaultEnc); pic.format = decodeString(uint8Array.subarray(0, fzero), defaultEnc); uint8Array = uint8Array.subarray(fzero + 1); break; default: throw makeUnexpectedMajorVersionError$1(this.major); } pic.format = FrameParser.fixPictureMimeType(pic.format); pic.type = AttachedPictureType[uint8Array[0]]; uint8Array = uint8Array.subarray(1); fzero = findZero(uint8Array, encoding); pic.description = decodeString(uint8Array.subarray(0, fzero), encoding); uint8Array = uint8Array.subarray(fzero + nullTerminatorLength); pic.data = uint8Array; output = pic; } break; case "CNT": case "PCNT": output = decodeUintBE(uint8Array); break; case "SYLT": { const syltHeader = SyncTextHeader.get(uint8Array, 0); uint8Array = uint8Array.subarray(SyncTextHeader.len); const result = { descriptor: "", language: syltHeader.language, contentType: syltHeader.contentType, timeStampFormat: syltHeader.timeStampFormat, syncText: [] }; let readSyllables = false; while (uint8Array.length > 0) { const nullStr = FrameParser.readNullTerminatedString(uint8Array, syltHeader.encoding); uint8Array = uint8Array.subarray(nullStr.len); if (readSyllables) { const timestamp = UINT32_BE.get(uint8Array, 0); uint8Array = uint8Array.subarray(UINT32_BE.len); result.syncText.push({ text: nullStr.text, timestamp }); } else { result.descriptor = nullStr.text; readSyllables = true; } } output = result; break; } case "ULT": case "USLT": case "COM": case "COMM": { const textHeader = TextHeader.get(uint8Array, offset); offset += TextHeader.len; const descriptorStr = FrameParser.readNullTerminatedString(uint8Array.subarray(offset), textHeader.encoding); offset += descriptorStr.len; const textStr = FrameParser.readNullTerminatedString(uint8Array.subarray(offset), textHeader.encoding); output = { language: textHeader.language, descriptor: descriptorStr.text, text: textStr.text }; break; } case "UFID": { const ufid = FrameParser.readIdentifierAndData(uint8Array, defaultEnc); output = { owner_identifier: ufid.id, identifier: ufid.data }; break; } case "PRIV": { const priv = FrameParser.readIdentifierAndData(uint8Array, defaultEnc); output = { owner_identifier: priv.id, data: priv.data }; break; } case "POPM": { uint8Array = uint8Array.subarray(offset); const emailStr = FrameParser.readNullTerminatedString(uint8Array, urlEnc); const email = emailStr.text; uint8Array = uint8Array.subarray(emailStr.len); if (uint8Array.length === 0) { this.warningCollector.addWarning(`id3v2.${this.major} type=${type} POPM frame missing rating byte`); output = { email, rating: 0, counter: void 0 }; break; } const rating = UINT8.get(uint8Array, 0); const counterBytes = uint8Array.subarray(UINT8.len); output = { email, rating, counter: counterBytes.length > 0 ? decodeUintBE(counterBytes) : void 0 }; break; } case "GEOB": { const encoding = TextEncodingToken.get(uint8Array, 0); uint8Array = uint8Array.subarray(1); const mimeTypeStr = FrameParser.readNullTerminatedString(uint8Array, urlEnc); const mimeType = mimeTypeStr.text; uint8Array = uint8Array.subarray(mimeTypeStr.len); const filenameStr = FrameParser.readNullTerminatedString(uint8Array, encoding); const filename = filenameStr.text; uint8Array = uint8Array.subarray(filenameStr.len); const descriptionStr = FrameParser.readNullTerminatedString(uint8Array, encoding); const description = descriptionStr.text; uint8Array = uint8Array.subarray(descriptionStr.len); output = { type: mimeType, filename, description, data: uint8Array }; break; } case "WCOM": case "WCOP": case "WOAF": case "WOAR": case "WOAS": case "WORS": case "WPAY": case "WPUB": output = FrameParser.readNullTerminatedString(uint8Array, urlEnc).text; break; case "WXXX": { const encoding = TextEncodingToken.get(uint8Array, 0); uint8Array = uint8Array.subarray(1); const descriptionStr = FrameParser.readNullTerminatedString(uint8Array, encoding); const description = descriptionStr.text; uint8Array = uint8Array.subarray(descriptionStr.len); output = { description, url: FrameParser.trimNullPadding(decodeString(uint8Array, defaultEnc)) }; break; } case "WFD": case "WFED": { const encoding = TextEncodingToken.get(uint8Array, 0); uint8Array = uint8Array.subarray(1); output = FrameParser.readNullTerminatedString(uint8Array, encoding).text; break; } case "MCDI": output = uint8Array.subarray(0, length); break; case "CHAP": { debug("Reading CHAP"); fzero = findZero(uint8Array, defaultEnc); const chapter = { label: decodeString(uint8Array.subarray(0, fzero), defaultEnc), info: ChapterInfo.get(uint8Array, fzero + 1), frames: /* @__PURE__ */ new Map() }; offset += fzero + 1 + ChapterInfo.len; while (offset < length) { const subFrame = readFrameHeader(uint8Array.subarray(offset), this.major, this.warningCollector); const headerSize = getFrameHeaderLength(this.major); offset += headerSize; const subOutput = this.readData(uint8Array.subarray(offset, offset + subFrame.length), subFrame.id, includeCovers); offset += subFrame.length; chapter.frames.set(subFrame.id, subOutput); } output = chapter; break; } case "CTOC": { debug("Reading CTOC"); const idEnd = findZero(uint8Array, defaultEnc); const label = decodeString(uint8Array.subarray(0, idEnd), defaultEnc); offset = idEnd + 1; const flags = uint8Array[offset++]; const topLevel = (flags & 2) !== 0; const ordered = (flags & 1) !== 0; const entryCount = uint8Array[offset++]; const childElementIds = []; for (let i = 0; i < entryCount && offset < length; i++) { const end = findZero(uint8Array.subarray(offset), defaultEnc); const childId = decodeString(uint8Array.subarray(offset, offset + end), defaultEnc); childElementIds.push(childId); offset += end + 1; } const toc = { label, flags: { topLevel, ordered }, childElementIds, frames: /* @__PURE__ */ new Map() }; while (offset < length) { const subFrame = readFrameHeader(uint8Array.subarray(offset), this.major, this.warningCollector); const headerSize = getFrameHeaderLength(this.major); offset += headerSize; const subOutput = this.readData(uint8Array.subarray(offset, offset + subFrame.length), subFrame.id, includeCovers); offset += subFrame.length; toc.frames.set(subFrame.id, subOutput); } output = toc; break; } default: debug(`Warning: unsupported id3v2-tag-type: ${type}`); break; } return output; } static readNullTerminatedString(uint8Array, encoding) { const bomSize = encoding.bom ? 2 : 0; const originalLen = uint8Array.length; const valueArray = uint8Array.subarray(bomSize); const zeroIndex = findZero(valueArray, encoding.encoding); if (zeroIndex >= valueArray.length) return { text: decodeString(valueArray, encoding.encoding), len: originalLen }; return { text: decodeString(valueArray.subarray(0, zeroIndex), encoding.encoding), len: bomSize + zeroIndex + FrameParser.getNullTerminatorLength(encoding.encoding) }; } static fixPictureMimeType(pictureType) { pictureType = pictureType.toLocaleLowerCase(); switch (pictureType) { case "jpg": return "image/jpeg"; case "png": return "image/png"; } return pictureType; } /** * Converts TMCL (Musician credits list) or TIPL (Involved people list) * @param entries */ static functionList(entries) { const res = {}; for (let i = 0; i + 1 < entries.length; i += 2) { const names = entries[i + 1].split(","); res[entries[i]] = res[entries[i]] ? res[entries[i]].concat(names) : names; } return res; } /** * id3v2.4 defines that multiple T* values are separated by 0x00 * id3v2.3 defines that TCOM, TEXT, TOLY, TOPE & TPE1 values are separated by / * @param tag - Tag name * @param text - Concatenated tag value * @returns Split tag value */ splitValue(tag, text) { let values; if (this.major < 4) { values = text.split(/\x00/g); if (values.length > 1) this.warningCollector.addWarning(`ID3v2.${this.major} ${tag} uses non standard null-separator.`); else values = text.split(/\//g); } else values = text.split(/\x00/g); return FrameParser.trimArray(values); } static trimArray(values) { return values.map((value) => FrameParser.trimNullPadding(value).trim()); } static trimNullPadding(value) { let end = value.length; while (end > 0 && value.charCodeAt(end - 1) === 0) end--; return end === value.length ? value : value.slice(0, end); } static readIdentifierAndData(uint8Array, encoding) { const idStr = FrameParser.readNullTerminatedString(uint8Array, { encoding, bom: false }); return { id: idStr.text, data: uint8Array.subarray(idStr.len) }; } static getNullTerminatorLength(enc) { return enc.startsWith("utf-16") ? 2 : 1; } }; var Id3v2ContentError = class extends makeUnexpectedFileContentError("id3v2") {}; function makeUnexpectedMajorVersionError$1(majorVer) { throw new Id3v2ContentError(`Unexpected majorVer: ${majorVer}`); } //#endregion //#region node_modules/music-metadata/lib/id3v2/ID3v2Parser.js var ID3v2Parser = class ID3v2Parser { constructor() { this.tokenizer = void 0; this.id3Header = void 0; this.metadata = void 0; this.headerType = void 0; this.options = void 0; } static removeUnsyncBytes(buffer) { let readI = 0; let writeI = 0; while (readI < buffer.length - 1) { if (readI !== writeI) buffer[writeI] = buffer[readI]; readI += buffer[readI] === 255 && buffer[readI + 1] === 0 ? 2 : 1; writeI++; } if (readI < buffer.length) buffer[writeI++] = buffer[readI]; return buffer.subarray(0, writeI); } static readFrameData(uint8Array, frameHeader, majorVer, includeCovers, warningCollector) { const frameParser = new FrameParser(majorVer, warningCollector); switch (majorVer) { case 2: return frameParser.readData(uint8Array, frameHeader.id, includeCovers); case 3: case 4: if (frameHeader.flags?.format.unsynchronisation) uint8Array = ID3v2Parser.removeUnsyncBytes(uint8Array); if (frameHeader.flags?.format.data_length_indicator) uint8Array = uint8Array.subarray(4, uint8Array.length); return frameParser.readData(uint8Array, frameHeader.id, includeCovers); default: throw makeUnexpectedMajorVersionError(majorVer); } } /** * Create a combined tag key, of tag & description * @param tag e.g.: COM * @param description e.g. iTunPGAP * @returns string e.g. COM:iTunPGAP */ static makeDescriptionTagName(tag, description) { return tag + (description ? `:${description}` : ""); } async parse(metadata, tokenizer, options) { this.tokenizer = tokenizer; this.metadata = metadata; this.options = options; const id3Header = await this.tokenizer.readToken(ID3v2Header); if (id3Header.fileIdentifier !== "ID3") throw new Id3v2ContentError("expected ID3-header file-identifier 'ID3' was not found"); this.id3Header = id3Header; this.headerType = `ID3v2.${id3Header.version.major}`; await (id3Header.flags.isExtendedHeader ? this.parseExtendedHeader() : this.parseId3Data(id3Header.size)); const chapters = ID3v2Parser.mapId3v2Chapters(this.metadata.native[this.headerType]); this.metadata.setFormat("chapters", chapters); } async parseExtendedHeader() { const extendedHeader = await this.tokenizer.readToken(ExtendedHeader); const dataRemaining = extendedHeader.size - ExtendedHeader.len; return dataRemaining > 0 ? this.parseExtendedHeaderData(dataRemaining, extendedHeader.size) : this.parseId3Data(this.id3Header.size - extendedHeader.size); } async parseExtendedHeaderData(dataRemaining, extendedHeaderSize) { await this.tokenizer.ignore(dataRemaining); return this.parseId3Data(this.id3Header.size - extendedHeaderSize); } async parseId3Data(dataLen) { const uint8Array = await this.tokenizer.readToken(new Uint8ArrayType(dataLen)); for (const tag of this.parseMetadata(uint8Array)) switch (tag.id) { case "TXXX": if (tag.value) await this.handleTag(tag, tag.value.text, () => tag.value.description); break; default: await (Array.isArray(tag.value) ? Promise.all(tag.value.map((value) => this.addTag(tag.id, value))) : this.addTag(tag.id, tag.value)); } } async handleTag(tag, values, descriptor, resolveValue = (value) => value) { await Promise.all(values.map((value) => this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, descriptor(value)), resolveValue(value)))); } async addTag(id, value) { await this.metadata.addTag(this.headerType, id, value); } parseMetadata(data) { let offset = 0; const tags = []; while (true) { if (offset === data.length) break; const frameHeaderLength = getFrameHeaderLength(this.id3Header.version.major); if (offset + frameHeaderLength > data.length) { this.metadata.addWarning("Illegal ID3v2 tag length"); break; } const frameHeaderBytes = data.subarray(offset, offset + frameHeaderLength); offset += frameHeaderLength; const frameHeader = readFrameHeader(frameHeaderBytes, this.id3Header.version.major, this.metadata); const frameDataBytes = data.subarray(offset, offset + frameHeader.length); offset += frameHeader.length; const values = ID3v2Parser.readFrameData(frameDataBytes, frameHeader, this.id3Header.version.major, !this.options.skipCovers, this.metadata); if (values) tags.push({ id: frameHeader.id, value: values }); } return tags; } /** * Convert parsed ID3v2 chapter frames (CHAP / CTOC) to generic `format.chapters`. * * This function expects the `native` tags already to contain parsed `CHAP` and `CTOC` frame values, * as produced by `FrameParser.readData`. */ static mapId3v2Chapters(id3Tags) { if (!id3Tags) return; const chapFrames = id3Tags.filter((t) => t.id === "CHAP"); if (!chapFrames?.length) return; const topLevelToc = id3Tags.filter((t) => t.id === "CTOC")?.find((t) => t.value.flags?.topLevel); const chapterById = /* @__PURE__ */ new Map(); for (const chap of chapFrames) chapterById.set(chap.value.label, chap.value); const orderedIds = topLevelToc?.value.childElementIds; const chapters = []; const source = orderedIds ?? [...chapterById.keys()]; for (const id of source) { const chap = chapterById.get(id); if (!chap) continue; const frames = chap.frames; const title = frames.get("TIT2"); if (!title) continue; chapters.push({ id, title, url: frames.get("WXXX"), start: chap.info.startTime / 1e3, end: chap.info.endTime / 1e3, image: frames.get("APIC") }); } if (!orderedIds) chapters.sort((a, b) => a.start - b.start); return chapters.length ? chapters : void 0; } }; function makeUnexpectedMajorVersionError(majorVer) { throw new Id3v2ContentError(`Unexpected majorVer: ${majorVer}`); } //#endregion export { ID3v2Parser as t };