UNPKG

@bililive-tools/manager

Version:
252 lines (251 loc) 7.97 kB
import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { Readable } from "node:stream"; import { finished } from "node:stream/promises"; import { throttle, range } from "lodash-es"; import filenamify from "filenamify"; export function asyncThrottle(fn, time, opts = {}) { let savingPromise = null; let hasDeferred = false; const wrappedWithAllowDefer = () => { if (savingPromise != null) { hasDeferred = true; return; } savingPromise = fn().finally(() => { savingPromise = null; if (hasDeferred) { hasDeferred = false; if (opts.immediateRunWhenEndOfDefer) { wrappedWithAllowDefer(); } else { throttled(); } } }); }; const throttled = throttle(wrappedWithAllowDefer, time); return throttled; } export function replaceExtName(filePath, newExtName) { return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)) + newExtName); } /** * 接收 fn ,返回一个和 fn 签名一致的函数 fn'。当已经有一个 fn' 在运行时,再调用 * fn' 会直接返回运行中 fn' 的 Promise,直到 Promise 结束 pending 状态 */ export function singleton(fn) { let latestPromise = null; return function (...args) { if (latestPromise) return latestPromise; // @ts-ignore const promise = fn.apply(this, args).finally(() => { if (promise === latestPromise) { latestPromise = null; } }); latestPromise = promise; return promise; }; } /** * 从数组中按照特定算法提取一些值(允许同个索引重复提取)。 * 算法的行为类似 flex 的 space-between。 * * examples: * ``` * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 1)) * // [1] * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 3)) * // [1, 4, 7] * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 4)) * // [1, 3, 5, 7] * console.log(getValuesFromArrayLikeFlexSpaceBetween([1, 2, 3, 4, 5, 6, 7], 11)) * // [1, 1, 2, 3, 3, 4, 5, 5, 6, 7, 7] * ``` */ export function getValuesFromArrayLikeFlexSpaceBetween(array, columnCount) { if (columnCount < 1) return []; if (columnCount === 1) return [array[0]]; const spacingCount = columnCount - 1; const spacingLength = array.length / spacingCount; const columns = range(1, columnCount + 1); const columnValues = columns.map((column, idx, columns) => { // 首个和最后的列是特殊的,因为它们不在范围内,而是在两端 if (idx === 0) { return array[0]; } else if (idx === columns.length - 1) { return array[array.length - 1]; } const beforeSpacingCount = column - 1; const colPos = beforeSpacingCount * spacingLength; return array[Math.floor(colPos)]; }); return columnValues; } export function ensureFolderExist(fileOrFolderPath) { const folder = path.dirname(fileOrFolderPath); if (!fs.existsSync(folder)) { fs.mkdirSync(folder, { recursive: true }); } } export function assert(assertion, msg) { if (!assertion) { throw new Error(msg); } } export function assertStringType(data, msg) { assert(typeof data === "string", msg); } export function assertNumberType(data, msg) { assert(typeof data === "number", msg); } export function assertObjectType(data, msg) { assert(typeof data === "object", msg); } export function formatDate(date, format) { const map = { yyyy: date.getFullYear().toString(), MM: (date.getMonth() + 1).toString().padStart(2, "0"), dd: date.getDate().toString().padStart(2, "0"), HH: date.getHours().toString().padStart(2, "0"), mm: date.getMinutes().toString().padStart(2, "0"), ss: date.getSeconds().toString().padStart(2, "0"), }; return format.replace(/yyyy|MM|dd|HH|mm|ss/g, (matched) => map[matched]); } export function removeSystemReservedChars(filename) { return filenamify(filename, { replacement: "_" }); } export function isFfmpegStartSegment(line) { return line.includes("Opening ") && line.includes("for writing"); } export function isFfmpegStart(line) { return line.includes("frame=") && line.includes("fps="); } export const formatTemplate = function template(string, ...args) { const nargs = /\{([0-9a-zA-Z_]+)\}/g; let params; if (args.length === 1 && typeof args[0] === "object") { params = args[0]; } else { params = args; } if (!params || !params.hasOwnProperty) { params = {}; } return string.replace(nargs, function replaceArg(match, i, index) { let result; if (string[index - 1] === "{" && string[index + match.length] === "}") { return i; } else { result = Object.hasOwn(params, i) ? params[i] : null; if (result === null || result === undefined) { return ""; } return result; } }); }; export function createInvalidStreamChecker(count = 15) { let prevFrame = 0; let frameUnchangedCount = 0; return (ffmpegLogLine) => { const streamInfo = ffmpegLogLine.match(/frame=\s*(\d+) fps=.*? q=.*? size=.*? time=.*? bitrate=.*? speed=.*?/); if (streamInfo != null) { const [, frameText] = streamInfo; const frame = Number(frameText); if (frame === prevFrame) { if (++frameUnchangedCount >= count) { return true; } } else { prevFrame = frame; frameUnchangedCount = 0; } return false; } return false; }; } export function createTimeoutChecker(onTimeout, time) { let timer = null; let stopped = false; const update = () => { if (stopped) return; if (timer != null) clearTimeout(timer); timer = setTimeout(() => { timer = null; onTimeout(); }, time); }; update(); return { update, stop() { stopped = true; if (timer != null) clearTimeout(timer); timer = null; }, }; } export async function downloadImage(imageUrl, savePath) { const res = await fetch(imageUrl); if (!res.body) { throw new Error("No body in response"); } const fileStream = fs.createWriteStream(savePath, { flags: "wx" }); // @ts-ignore await finished(Readable.fromWeb(res.body).pipe(fileStream)); } const md5 = (str) => { return crypto.createHash("md5").update(str).digest("hex"); }; const uuid = () => { return crypto.randomUUID(); }; /** * 根据指定的顺序对对象数组进行排序 * @param objects 要排序的对象数组 * @param order 指定的顺序 * @param key 用于排序的键 * @returns 排序后的对象数组 */ export function sortByKeyOrder(objects, order, key) { const orderMap = new Map(order.map((value, index) => [value, index])); return [...objects].sort((a, b) => { const indexA = orderMap.get(a[key]) ?? Number.MAX_VALUE; const indexB = orderMap.get(b[key]) ?? Number.MAX_VALUE; return indexA - indexB; }); } export default { replaceExtName, singleton, getValuesFromArrayLikeFlexSpaceBetween, ensureFolderExist, assert, assertStringType, assertNumberType, assertObjectType, asyncThrottle, isFfmpegStartSegment, createInvalidStreamChecker, createTimeoutChecker, downloadImage, md5, uuid, sortByKeyOrder, };