@bililive-tools/manager
Version:
Batch scheduling recorders
252 lines (251 loc) • 7.97 kB
JavaScript
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,
};