@bililive-tools/manager
Version:
Batch scheduling recorders
157 lines (156 loc) • 5.34 kB
JavaScript
/**
* 用一个视频之外的独立文件来存储元信息和弹幕、礼物等信息。
* 暂时使用 json 作为存储方案试试效果,猜测可能会有实时写入差、占用空间大的问题。
*/
import path from "node:path";
import fs from "node:fs";
import { XMLBuilder } from "fast-xml-parser";
import { pick } from "lodash-es";
import { asyncThrottle } from "./utils.js";
export function createRecordExtraDataController(savePath) {
const data = {
meta: {
recordStartTimestamp: Date.now(),
},
messages: [],
};
const scheduleSave = asyncThrottle(() => save(), 30e3, {
immediateRunWhenEndOfDefer: true,
});
const save = () => {
return fs.promises.writeFile(savePath, JSON.stringify(data));
};
// TODO: 将所有数据存放在内存中可能存在问题
const addMessage = (comment) => {
data.messages.push(comment);
scheduleSave();
};
const setMeta = (meta) => {
data.meta = {
...data.meta,
...meta,
};
scheduleSave();
};
const flush = async () => {
scheduleSave.flush();
scheduleSave.cancel();
const xmlContent = convert2Xml(data);
const parsedPath = path.parse(savePath);
const xmlPath = path.join(parsedPath.dir, parsedPath.name + ".xml");
await fs.promises.writeFile(xmlPath, xmlContent);
await fs.promises.rm(savePath);
};
return {
data,
addMessage,
setMeta,
flush,
};
}
/**
* 转换弹幕为b站格式xml
* @link: https://socialsisteryi.github.io/bilibili-API-collect/docs/danmaku/danmaku_xml.html#%E5%B1%9E%E6%80%A7-p
*/
export function convert2Xml(data) {
const metadata = data.meta;
const builder = new XMLBuilder({
ignoreAttributes: false,
attributeNamePrefix: "@@",
format: true,
});
const comments = data.messages
.filter((item) => item.type === "comment")
.map((ele) => {
const progress = (ele.timestamp - metadata?.recordStartTimestamp) / 1000;
const data = {
"@@p": "",
"@@progress": progress,
"@@mode": String(ele.mode ?? 1),
"@@fontsize": String(25),
"@@color": String(parseInt((ele.color || "#ffffff").replace("#", ""), 16)),
"@@midHash": String(ele?.sender?.uid),
"#text": String(ele?.text || ""),
"@@ctime": String(ele.timestamp),
"@@pool": String(0),
"@@weight": String(0),
"@@user": String(ele.sender?.name),
"@@uid": String(ele?.sender?.uid),
};
data["@@p"] = [
data["@@progress"],
data["@@mode"],
data["@@fontsize"],
data["@@color"],
data["@@ctime"],
data["@@pool"],
data["@@midHash"],
data["@@uid"],
data["@@weight"],
].join(",");
return pick(data, ["@@p", "#text", "@@user", "@@uid"]);
});
const gifts = data.messages
.filter((item) => item.type === "give_gift")
.map((ele) => {
const progress = (ele.timestamp - metadata?.recordStartTimestamp) / 1000;
const data = {
"@@ts": progress,
"@@giftname": String(ele.name),
"@@giftcount": String(ele.count),
"@@price": String(ele.price * 1000),
"@@user": String(ele.sender?.name),
"@@uid": String(ele?.sender?.uid),
// "@@raw": JSON.stringify(ele),
};
return data;
});
const superChats = data.messages
.filter((item) => item.type === "super_chat")
.map((ele) => {
const progress = (ele.timestamp - metadata?.recordStartTimestamp) / 1000;
const data = {
"@@ts": progress,
"@@price": String(ele.price * 1000),
"#text": String(ele.text),
"@@user": String(ele.sender?.name),
"@@uid": String(ele?.sender?.uid),
// "@@raw": JSON.stringify(ele),
};
return data;
});
const guardGift = data.messages
.filter((item) => item.type === "guard")
.map((ele) => {
const progress = (ele.timestamp - metadata?.recordStartTimestamp) / 1000;
const data = {
"@@ts": progress,
"@@price": String(ele.price * 1000),
"@@giftname": String(ele.name),
"@@giftcount": String(ele.count),
"@@level": String(ele.level),
"@@user": String(ele.sender?.name),
"@@uid": String(ele?.sender?.uid),
// "@@raw": JSON.stringify(ele),
};
return data;
});
const xmlContent = builder.build({
i: {
metadata: {
platform: metadata.platform,
video_start_time: metadata.recordStartTimestamp,
live_start_time: metadata.liveStartTimestamp,
room_title: metadata.title,
user_name: metadata.user_name,
room_id: metadata.room_id,
},
d: comments,
gift: gifts,
sc: superChats,
guard: guardGift,
},
});
return `<?xml version="1.0" encoding="utf-8"?>
${xmlContent}`;
}