bililive-cli 3.13.0 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11575,7 +11575,7 @@ const APP_DEFAULT_CONFIG = {
11575
11575
  },
11576
11576
  ],
11577
11577
  songRecognizeAsr: {
11578
- modelId: "",
11578
+ modelId: "bcut",
11579
11579
  },
11580
11580
  songRecognizeLlm: {
11581
11581
  modelId: "",
@@ -12136,6 +12136,7 @@ const defaultRecordConfig = {
12136
12136
  sendToWebhook: false,
12137
12137
  noGlobalFollowFields: [],
12138
12138
  saveCover: false,
12139
+ convert2Mp4: false,
12139
12140
  extra: {},
12140
12141
  qualityRetry: 0,
12141
12142
  formatName: "auto",
@@ -54494,7 +54495,7 @@ async function trash(paths, options) {
54494
54495
  } else if (process$2.platform === 'win32') {
54495
54496
  module = await Promise.resolve().then(function () { return require('./windows-OmnJ7a39.cjs'); });
54496
54497
  } else {
54497
- module = await Promise.resolve().then(function () { return require('./linux-CXmRE85w.cjs'); });
54498
+ module = await Promise.resolve().then(function () { return require('./linux-CH6NYi8a.cjs'); });
54498
54499
  }
54499
54500
 
54500
54501
  return module.default(paths);
@@ -61384,9 +61385,6 @@ const genFfmpegParams = (options) => {
61384
61385
  if (options.audioCodec) {
61385
61386
  result.push(`-c:a ${options.audioCodec}`);
61386
61387
  }
61387
- else {
61388
- result.push(`-c:a copy`);
61389
- }
61390
61388
  if (options.extraOptions) {
61391
61389
  options.extraOptions.split(" ").forEach((option) => {
61392
61390
  result.push(option);
@@ -181078,26 +181076,31 @@ const checkMergeVideos = async (inputFiles) => {
181078
181076
  const videoMetas = await Promise.all(inputFiles.map((file) => readVideoMeta(file)));
181079
181077
  const errors = [];
181080
181078
  const warnings = [];
181079
+ const videoStream0 = videoMetas[0].streams.find((stream) => stream.codec_type === "video");
181080
+ const audioStream0 = videoMetas[0].streams.find((stream) => stream.codec_type === "audio");
181081
181081
  for (const meta of videoMetas) {
181082
181082
  if (meta.format.format_name !== videoMetas[0].format.format_name) {
181083
181083
  errors.push("输入视频容器不一致");
181084
181084
  }
181085
181085
  const videoStream = meta.streams.find((stream) => stream.codec_type === "video");
181086
- const videoStream0 = videoMetas[0].streams.find((stream) => stream.codec_type === "video");
181087
181086
  const audioStream = meta.streams.find((stream) => stream.codec_type === "audio");
181088
- const audioStream0 = videoMetas[0].streams.find((stream) => stream.codec_type === "audio");
181089
181087
  if (videoStream?.codec_name !== videoStream0?.codec_name) {
181090
181088
  errors.push("输入视频编码器不一致");
181091
181089
  }
181092
181090
  if (audioStream?.codec_name !== audioStream0?.codec_name) {
181093
181091
  errors.push("输入视频音频编码器不一致");
181094
181092
  }
181095
- // 分辨率不一致警告
181096
181093
  if (videoStream?.width !== videoStream0?.width) {
181097
- warnings.push("输入视频分辨率宽不一致");
181094
+ errors.push("输入视频分辨率宽不一致");
181098
181095
  }
181099
181096
  if (videoStream?.height !== videoStream0?.height) {
181100
- warnings.push("输入视频分辨率高不一致");
181097
+ errors.push("输入视频分辨率高不一致");
181098
+ }
181099
+ if (audioStream?.sample_rate !== audioStream0?.sample_rate) {
181100
+ errors.push("输入视频音频采样率不一致");
181101
+ }
181102
+ if (videoStream?.r_frame_rate !== videoStream0?.r_frame_rate) {
181103
+ warnings.push("输入视频帧率不一致");
181101
181104
  }
181102
181105
  }
181103
181106
  return {
@@ -186234,71 +186237,80 @@ async function getBiliStatusInfoByRoomIds(RoomIds) {
186234
186237
 
186235
186238
  /**
186236
186239
  * XML流式写入控制器,用于实时写入弹幕、礼物等信息到XML文件
186237
- * 相比原有的json方案,这个实现每隔5秒就会写入数据,减少内存占用和数据丢失风险
186240
+ * 相比原有的json方案,这个实现每隔10秒就会写入数据,减少内存占用和数据丢失风险
186238
186241
  */
186242
+ const METADATA_PLACEHOLDER = "<!--METADATA_PLACEHOLDER-->";
186243
+ const XML_FILE_HEADER = `<?xml version="1.0" encoding="utf-8"?>\n<?xml-stylesheet type="text/xsl" href="#s"?>\n<i>\n${METADATA_PLACEHOLDER}\n<RecorderXmlStyle><z:stylesheet version="1.0" id="s" xml:id="s" xmlns:z="http://www.w3.org/1999/XSL/Transform"><z:output method="html"/><z:template match="/"><html><meta name="viewport" content="width=device-width"/><title>弹幕文件 <z:value-of select="/i/metadata/user_name/text()"/></title><style>body{margin:0}h1,h2,p,table{margin-left:5px}table{border-spacing:0}td,th{border:1px solid grey;padding:1px 5px}th{position:sticky;top:0;background:#4098de}tr:hover{background:#d9f4ff}div{overflow:auto;max-height:80vh;max-width:100vw;width:fit-content}</style><h1>弹幕XML文件</h1><p>本文件不支持在 IE 浏览器里预览,请使用 Chrome Firefox Edge 等浏览器。</p><p>文件用法参考文档 <a href="https://rec.danmuji.org/user/danmaku/">https://rec.danmuji.org/user/danmaku/</a></p><table><tr><td>房间号</td><td><z:value-of select="/i/metadata/room_id/text()"/></td></tr><tr><td>主播名</td><td><z:value-of select="/i/metadata/user_name/text()"/></td></tr><tr><td><a href="#d">弹幕</a></td><td>共<z:value-of select="count(/i/d)"/>条记录</td></tr><tr><td><a href="#guard">上船</a></td><td>共<z:value-of select="count(/i/guard)"/>条记录</td></tr><tr><td><a href="#sc">SC</a></td><td>共<z:value-of select="count(/i/sc)"/>条记录</td></tr><tr><td><a href="#gift">礼物</a></td><td>共<z:value-of select="count(/i/gift)"/>条记录</td></tr></table><h2 id="d">弹幕</h2><div id="dm"><table><tr><th>用户名</th><th>出现时间</th><th>用户ID</th><th>弹幕</th><th>参数</th></tr><z:for-each select="/i/d"><tr><td><z:value-of select="@user"/></td><td></td><td></td><td><z:value-of select="."/></td><td><z:value-of select="@p"/></td></tr></z:for-each></table></div><script>Array.from(document.querySelectorAll('#dm tr')).slice(1).map(t=>t.querySelectorAll('td')).forEach(t=>{let p=t[4].textContent.split(','),a=p[0];t[1].textContent=\`\u0024{(Math.floor(a/60/60)+'').padStart(2,0)}:\u0024{(Math.floor(a/60%60)+'').padStart(2,0)}:\u0024{(a%60).toFixed(3).padStart(6,0)}\`;t[2].innerHTML=\`&lt;a target=_blank rel="nofollow noreferrer" "&gt;\u0024{p[6]}&lt;/a&gt;\`})</script><h2 id="guard">舰长购买</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>舰长等级</th><th>购买数量</th><th>出现时间</th></tr><z:for-each select="/i/guard"><tr><td><z:value-of select="@user"/></td><td><a rel="nofollow noreferrer"><z:attribute name="href"><z:text></z:text><z:value-of select="@uid" /></z:attribute><z:value-of select="@uid"/></a></td><td><z:value-of select="@level"/></td><td><z:value-of select="@count"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div><h2 id="sc">SuperChat 醒目留言</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>内容</th><th>显示时长</th><th>价格</th><th>出现时间</th></tr><z:for-each select="/i/sc"><tr><td><z:value-of select="@user"/></td><td><a rel="nofollow noreferrer"><z:attribute name="href"><z:text></z:text><z:value-of select="@uid" /></z:attribute><z:value-of select="@uid"/></a></td><td><z:value-of select="."/></td><td><z:value-of select="@time"/></td><td><z:value-of select="@price"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div><h2 id="gift">礼物</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>礼物名</th><th>礼物数量</th><th>出现时间</th></tr><z:for-each select="/i/gift"><tr><td><z:value-of select="@user"/></td><td><span rel="nofollow noreferrer"><z:attribute name="href"></z:attribute><z:value-of select="@uid"/></span></td><td><z:value-of select="@giftname"/></td><td><z:value-of select="@giftcount"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div></html></z:template></z:stylesheet></RecorderXmlStyle>\n`;
186239
186244
  function createRecordExtraDataController(savePath) {
186240
186245
  const data = {
186246
+ header: XML_FILE_HEADER,
186241
186247
  meta: {
186242
186248
  recordStartTimestamp: Date.now(),
186243
186249
  },
186244
186250
  pendingMessages: [],
186245
186251
  };
186246
186252
  let hasCompleted = false;
186247
- let isWriting = false;
186248
- let isInitialized = false;
186249
- // 初始化文件
186250
- const initializeFile = async () => {
186251
- if (isInitialized)
186252
- return;
186253
- isInitialized = true;
186254
- try {
186255
- // 创建XML文件头,使用占位符预留metadata位置
186256
- const header = `<?xml version="1.0" encoding="utf-8"?>\n<?xml-stylesheet type="text/xsl" href="#s"?>\n<i>\n<!--METADATA_PLACEHOLDER-->\n<RecorderXmlStyle><z:stylesheet version="1.0" id="s" xml:id="s" xmlns:z="http://www.w3.org/1999/XSL/Transform"><z:output method="html"/><z:template match="/"><html><meta name="viewport" content="width=device-width"/><title>弹幕文件 <z:value-of select="/i/metadata/user_name/text()"/></title><style>body{margin:0}h1,h2,p,table{margin-left:5px}table{border-spacing:0}td,th{border:1px solid grey;padding:1px 5px}th{position:sticky;top:0;background:#4098de}tr:hover{background:#d9f4ff}div{overflow:auto;max-height:80vh;max-width:100vw;width:fit-content}</style><h1>弹幕XML文件</h1><p>本文件不支持在 IE 浏览器里预览,请使用 Chrome Firefox Edge 等浏览器。</p><p>文件用法参考文档 <a href="https://rec.danmuji.org/user/danmaku/">https://rec.danmuji.org/user/danmaku/</a></p><table><tr><td>房间号</td><td><z:value-of select="/i/metadata/room_id/text()"/></td></tr><tr><td>主播名</td><td><z:value-of select="/i/metadata/user_name/text()"/></td></tr><tr><td><a href="#d">弹幕</a></td><td>共<z:value-of select="count(/i/d)"/>条记录</td></tr><tr><td><a href="#guard">上船</a></td><td>共<z:value-of select="count(/i/guard)"/>条记录</td></tr><tr><td><a href="#sc">SC</a></td><td>共<z:value-of select="count(/i/sc)"/>条记录</td></tr><tr><td><a href="#gift">礼物</a></td><td>共<z:value-of select="count(/i/gift)"/>条记录</td></tr></table><h2 id="d">弹幕</h2><div id="dm"><table><tr><th>用户名</th><th>出现时间</th><th>用户ID</th><th>弹幕</th><th>参数</th></tr><z:for-each select="/i/d"><tr><td><z:value-of select="@user"/></td><td></td><td></td><td><z:value-of select="."/></td><td><z:value-of select="@p"/></td></tr></z:for-each></table></div><script>Array.from(document.querySelectorAll('#dm tr')).slice(1).map(t=>t.querySelectorAll('td')).forEach(t=>{let p=t[4].textContent.split(','),a=p[0];t[1].textContent=\`\u0024{(Math.floor(a/60/60)+'').padStart(2,0)}:\u0024{(Math.floor(a/60%60)+'').padStart(2,0)}:\u0024{(a%60).toFixed(3).padStart(6,0)}\`;t[2].innerHTML=\`&lt;a target=_blank rel="nofollow noreferrer" "&gt;\u0024{p[6]}&lt;/a&gt;\`})</script><h2 id="guard">舰长购买</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>舰长等级</th><th>购买数量</th><th>出现时间</th></tr><z:for-each select="/i/guard"><tr><td><z:value-of select="@user"/></td><td><a rel="nofollow noreferrer"><z:attribute name="href"><z:text></z:text><z:value-of select="@uid" /></z:attribute><z:value-of select="@uid"/></a></td><td><z:value-of select="@level"/></td><td><z:value-of select="@count"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div><h2 id="sc">SuperChat 醒目留言</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>内容</th><th>显示时长</th><th>价格</th><th>出现时间</th></tr><z:for-each select="/i/sc"><tr><td><z:value-of select="@user"/></td><td><a rel="nofollow noreferrer"><z:attribute name="href"><z:text></z:text><z:value-of select="@uid" /></z:attribute><z:value-of select="@uid"/></a></td><td><z:value-of select="."/></td><td><z:value-of select="@time"/></td><td><z:value-of select="@price"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div><h2 id="gift">礼物</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>礼物名</th><th>礼物数量</th><th>出现时间</th></tr><z:for-each select="/i/gift"><tr><td><z:value-of select="@user"/></td><td><span rel="nofollow noreferrer"><z:attribute name="href"></z:attribute><z:value-of select="@uid"/></span></td><td><z:value-of select="@giftname"/></td><td><z:value-of select="@giftcount"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div></html></z:template></z:stylesheet></RecorderXmlStyle>`;
186257
- await fs$D.promises.writeFile(savePath, header);
186258
- }
186259
- catch (error) {
186260
- console.error("初始化XML文件失败:", error);
186261
- isInitialized = false;
186262
- throw error;
186263
- }
186264
- };
186265
- // 每10秒写入一次数据
186266
- const scheduleWrite = asyncThrottle(() => writeToFile(), 10e3, {
186267
- immediateRunWhenEndOfDefer: true,
186253
+ let hasPersistedHeader = false;
186254
+ let danmaNum = 0;
186255
+ let scNum = 0;
186256
+ let guardNum = 0;
186257
+ const interactedUsers = new Set();
186258
+ const getStats = () => ({
186259
+ danmaNum,
186260
+ uniqMember: interactedUsers.size,
186261
+ scNum,
186262
+ guardNum,
186268
186263
  });
186269
- const writeToFile = async () => {
186270
- if (isWriting || hasCompleted || data.pendingMessages.length === 0) {
186264
+ const trackInteractedUser = (message) => {
186265
+ const userName = message.sender?.name?.trim();
186266
+ if (!userName)
186271
186267
  return;
186268
+ interactedUsers.add(userName);
186269
+ };
186270
+ const initializeFile = async (content) => {
186271
+ // 这里有个假设,那就是第一次保存必然存在metatdata信息
186272
+ const initialContent = data.header.replace(METADATA_PLACEHOLDER, generateMetadataXml(data.meta));
186273
+ await fs$D.promises.writeFile(savePath, initialContent + content);
186274
+ hasPersistedHeader = true;
186275
+ };
186276
+ const writeToFile = async (force = false) => {
186277
+ if (!force && data.pendingMessages.length === 0) {
186278
+ return Promise.resolve();
186272
186279
  }
186273
- // 确保文件已初始化
186274
- await initializeFile();
186275
- isWriting = true;
186280
+ const messagesToWrite = [...data.pendingMessages];
186281
+ data.pendingMessages = [];
186276
186282
  try {
186277
- // 获取待写入的消息
186278
- const messagesToWrite = [...data.pendingMessages];
186279
- data.pendingMessages = [];
186280
- // 生成XML内容
186281
186283
  const xmlContent = generateXmlContent(data.meta, messagesToWrite);
186282
- // 追加写入文件
186283
- await appendToXmlFile(savePath, xmlContent);
186284
+ if (!hasPersistedHeader) {
186285
+ await initializeFile(xmlContent);
186286
+ }
186287
+ else if (xmlContent) {
186288
+ await appendToXmlFile(savePath, xmlContent);
186289
+ }
186284
186290
  }
186285
186291
  catch (error) {
186286
186292
  console.error("写入XML文件失败:", error);
186287
- // 如果写入失败,将消息重新加入队列
186288
- data.pendingMessages = [...data.pendingMessages];
186289
- }
186290
- finally {
186291
- isWriting = false;
186293
+ data.pendingMessages = [...messagesToWrite, ...data.pendingMessages];
186292
186294
  }
186293
186295
  };
186296
+ // 每10秒写入一次数据
186297
+ const writeTimer = setInterval(() => {
186298
+ writeToFile();
186299
+ }, 10e3);
186294
186300
  const addMessage = (message) => {
186295
186301
  if (hasCompleted)
186296
186302
  return;
186297
- // if (!isInitialized) return;
186303
+ if (message.type === "comment") {
186304
+ danmaNum += 1;
186305
+ }
186306
+ else if (message.type === "super_chat") {
186307
+ scNum += 1;
186308
+ }
186309
+ else if (message.type === "guard") {
186310
+ guardNum += 1;
186311
+ }
186312
+ trackInteractedUser(message);
186298
186313
  data.pendingMessages.push(message);
186299
- // 确保文件已初始化
186300
- initializeFile().catch(console.error);
186301
- scheduleWrite();
186302
186314
  };
186303
186315
  const setMeta = async (meta) => {
186304
186316
  if (hasCompleted)
@@ -186307,30 +186319,34 @@ function createRecordExtraDataController(savePath) {
186307
186319
  ...data.meta,
186308
186320
  ...meta,
186309
186321
  };
186310
- // 确保文件已初始化,然后立即更新文件中的metadata
186311
- await initializeFile().catch(console.error);
186312
- await updateMetadataInFile(savePath, data.meta).catch(console.error);
186313
186322
  };
186314
186323
  const flush = async () => {
186315
186324
  if (hasCompleted)
186316
186325
  return;
186317
186326
  hasCompleted = true;
186318
- scheduleWrite.cancel();
186319
- await initializeFile().catch(console.error);
186320
- // 写入剩余的数据
186321
- if (data.pendingMessages.length > 0) {
186322
- await writeToFile();
186323
- }
186324
- // 完成XML文件(添加结束标签等)
186325
- await finalizeXmlFile(savePath);
186326
- // 清理内存
186327
- data.pendingMessages = [];
186327
+ writeTimer && clearInterval(writeTimer);
186328
+ try {
186329
+ await writeToFile(true);
186330
+ await appendToXmlFile(savePath, "</i>");
186331
+ }
186332
+ catch (error) {
186333
+ console.error("完成XML文件写入失败:", error);
186334
+ }
186335
+ finally {
186336
+ // 清理内存
186337
+ data.pendingMessages = [];
186338
+ interactedUsers.clear();
186339
+ danmaNum = 0;
186340
+ scNum = 0;
186341
+ guardNum = 0;
186342
+ }
186328
186343
  };
186329
186344
  return {
186330
186345
  data,
186331
186346
  addMessage,
186332
186347
  setMeta,
186333
186348
  flush,
186349
+ getStats,
186334
186350
  };
186335
186351
  }
186336
186352
  /**
@@ -186346,7 +186362,7 @@ function generateXmlContent(metadata, messages) {
186346
186362
  .filter((item) => item.type === "comment")
186347
186363
  .map((ele) => {
186348
186364
  const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
186349
- const data = {
186365
+ const attrs = {
186350
186366
  "@@p": "",
186351
186367
  "@@progress": progress,
186352
186368
  "@@mode": String(ele.mode ?? 1),
@@ -186361,18 +186377,18 @@ function generateXmlContent(metadata, messages) {
186361
186377
  "@@uid": String(ele?.sender?.uid),
186362
186378
  "@@timestamp": String(ele.timestamp),
186363
186379
  };
186364
- data["@@p"] = [
186365
- data["@@progress"],
186366
- data["@@mode"],
186367
- data["@@fontsize"],
186368
- data["@@color"],
186369
- data["@@ctime"],
186370
- data["@@pool"],
186371
- data["@@midHash"],
186372
- data["@@uid"],
186373
- data["@@weight"],
186380
+ attrs["@@p"] = [
186381
+ attrs["@@progress"],
186382
+ attrs["@@mode"],
186383
+ attrs["@@fontsize"],
186384
+ attrs["@@color"],
186385
+ attrs["@@ctime"],
186386
+ attrs["@@pool"],
186387
+ attrs["@@midHash"],
186388
+ attrs["@@uid"],
186389
+ attrs["@@weight"],
186374
186390
  ].join(",");
186375
- return pick$1(data, ["@@p", "#text", "@@user", "@@uid", "@@timestamp"]);
186391
+ return pick$1(attrs, ["@@p", "#text", "@@user", "@@uid", "@@timestamp"]);
186376
186392
  });
186377
186393
  const gifts = messages
186378
186394
  .filter((item) => item.type === "give_gift")
@@ -186438,55 +186454,22 @@ async function appendToXmlFile(filePath, content) {
186438
186454
  throw error;
186439
186455
  }
186440
186456
  }
186441
- /**
186442
- * 更新XML文件中的metadata
186443
- */
186444
- async function updateMetadataInFile(filePath, metadata) {
186445
- try {
186446
- const builder = new fxp.XMLBuilder({
186447
- ignoreAttributes: false,
186448
- attributeNamePrefix: "@@",
186449
- format: true,
186450
- });
186451
- // 生成metadata XML
186452
- const metadataXml = builder.build({
186453
- metadata: {
186454
- platform: metadata.platform,
186455
- video_start_time: metadata.recordStartTimestamp,
186456
- live_start_time: metadata.liveStartTimestamp,
186457
- room_title: metadata.title,
186458
- user_name: metadata.user_name,
186459
- room_id: metadata.room_id,
186460
- },
186461
- });
186462
- // 读取文件内容
186463
- const content = await fs$D.promises.readFile(filePath, "utf-8");
186464
- // 替换占位符为实际的metadata
186465
- const updatedContent = content.replace("<!--METADATA_PLACEHOLDER-->", metadataXml);
186466
- // 写回文件
186467
- await fs$D.promises.writeFile(filePath, updatedContent);
186468
- }
186469
- catch (error) {
186470
- console.error(`更新XML文件metadata失败: ${filePath}`, error);
186471
- throw error;
186472
- }
186473
- }
186474
- /**
186475
- * 完成XML文件写入
186476
- */
186477
- async function finalizeXmlFile(filePath) {
186478
- try {
186479
- // 读取文件内容
186480
- const content = await fs$D.promises.readFile(filePath, "utf-8");
186481
- // 添加结束标签
186482
- const finalContent = content + "</i>";
186483
- // 写回文件
186484
- await fs$D.promises.writeFile(filePath, finalContent);
186485
- }
186486
- catch (error) {
186487
- console.error(`完成XML文件写入失败: ${filePath}`, error);
186488
- throw error;
186489
- }
186457
+ function generateMetadataXml(metadata) {
186458
+ const builder = new fxp.XMLBuilder({
186459
+ ignoreAttributes: false,
186460
+ attributeNamePrefix: "@@",
186461
+ format: true,
186462
+ });
186463
+ return builder.build({
186464
+ metadata: {
186465
+ platform: metadata.platform,
186466
+ video_start_time: metadata.recordStartTimestamp,
186467
+ live_start_time: metadata.liveStartTimestamp,
186468
+ room_title: metadata.title,
186469
+ user_name: metadata.user_name,
186470
+ room_id: metadata.room_id,
186471
+ },
186472
+ });
186490
186473
  }
186491
186474
 
186492
186475
  class Segment extends EventEmitter$j {
@@ -186499,11 +186482,19 @@ class Segment extends EventEmitter$j {
186499
186482
  outputVideoFilePath;
186500
186483
  disableDanma;
186501
186484
  videoExt;
186502
- constructor(getSavePath, disableDanma, videoExt) {
186485
+ options;
186486
+ constructor(getSavePath, disableDanma, videoExt, options) {
186503
186487
  super();
186504
186488
  this.getSavePath = getSavePath;
186505
186489
  this.disableDanma = disableDanma;
186506
186490
  this.videoExt = videoExt;
186491
+ this.options = options;
186492
+ }
186493
+ getVideoFileCompletedPayload() {
186494
+ return {
186495
+ filename: this.outputFilePath,
186496
+ stats: this.extraDataController?.getStats(),
186497
+ };
186507
186498
  }
186508
186499
  async handleSegmentEnd() {
186509
186500
  if (!this.outputVideoFilePath) {
@@ -186513,6 +186504,7 @@ class Segment extends EventEmitter$j {
186513
186504
  });
186514
186505
  return;
186515
186506
  }
186507
+ const data = this.getVideoFileCompletedPayload();
186516
186508
  try {
186517
186509
  this.emit("DebugLog", {
186518
186510
  type: "info",
@@ -186522,7 +186514,7 @@ class Segment extends EventEmitter$j {
186522
186514
  retry$1(() => fs$E.rename(this.rawRecordingVideoPath, this.outputFilePath), 20, 1000),
186523
186515
  this.extraDataController?.flush(),
186524
186516
  ]);
186525
- this.emit("videoFileCompleted", { filename: this.outputFilePath });
186517
+ this.emit("videoFileCompleted", data);
186526
186518
  }
186527
186519
  catch (err) {
186528
186520
  this.emit("DebugLog", {
@@ -186530,18 +186522,20 @@ class Segment extends EventEmitter$j {
186530
186522
  text: "videoFileCompleted error " + String(err),
186531
186523
  });
186532
186524
  // 虽然重命名失败了,但是也当作完成处理,避免卡住录制流程
186533
- this.emit("videoFileCompleted", { filename: this.outputFilePath });
186525
+ this.emit("videoFileCompleted", data);
186534
186526
  }
186535
186527
  }
186536
186528
  async onSegmentStart(stderrLine, callBack) {
186537
186529
  if (!this.init) {
186538
186530
  await this.handleSegmentEnd();
186539
186531
  }
186532
+ // 首次创建使用上次的时间戳,后续创建使用当前时间戳
186533
+ const startTime = this.init ? (this.options?.firstStartTime ?? Date.now()) : Date.now();
186540
186534
  this.init = false;
186541
- const startTime = Date.now();
186542
186535
  let liveInfo = { title: "", cover: "" };
186543
186536
  if (callBack?.onUpdateLiveInfo) {
186544
186537
  try {
186538
+ // TODO:这里存在bug,当调用onUpdateLiveInfo并在等待时,handleSegmentEnd被调用,那么会造成竞态导致数据错误,后续需要优化,需要保存segment状态
186545
186539
  liveInfo = await callBack.onUpdateLiveInfo();
186546
186540
  }
186547
186541
  catch (err) {
@@ -186622,7 +186616,9 @@ class StreamManager extends EventEmitter$j {
186622
186616
  }
186623
186617
  this.recordSavePath = recordSavePath;
186624
186618
  if (hasSegment) {
186625
- this.segment = new Segment(getSavePath, disableDanma, this.videoExt);
186619
+ this.segment = new Segment(getSavePath, disableDanma, this.videoExt, {
186620
+ firstStartTime: startTime,
186621
+ });
186626
186622
  this.segment.on("DebugLog", (data) => {
186627
186623
  this.emit("DebugLog", data);
186628
186624
  });
@@ -186680,8 +186676,13 @@ class StreamManager extends EventEmitter$j {
186680
186676
  }
186681
186677
  else {
186682
186678
  if (this.recordStartTime) {
186683
- await this.getExtraDataController()?.flush();
186684
- this.emit("videoFileCompleted", { filename: this.videoFilePath });
186679
+ const stats = this.extraDataController?.getStats();
186680
+ const extraDataController = this.getExtraDataController();
186681
+ await extraDataController?.flush();
186682
+ this.emit("videoFileCompleted", {
186683
+ filename: this.videoFilePath,
186684
+ stats: stats,
186685
+ });
186685
186686
  }
186686
186687
  }
186687
186688
  }
@@ -186845,7 +186846,7 @@ function createRecorderManager$1(opts) {
186845
186846
  }
186846
186847
  this.emit("videoFileCreated", { recorder: recorder.toJSON(), filename, rawFilename });
186847
186848
  });
186848
- recorder.on("videoFileCompleted", ({ filename }) => this.emit("videoFileCompleted", { recorder: recorder.toJSON(), filename }));
186849
+ recorder.on("videoFileCompleted", ({ filename, stats }) => this.emit("videoFileCompleted", { recorder: recorder.toJSON(), filename, stats }));
186849
186850
  recorder.on("Message", (message) => this.emit("Message", { recorder: recorder.toJSON(), message }));
186850
186851
  recorder.on("Updated", (keys) => this.emit("RecorderUpdated", { recorder: recorder.toJSON(), keys }));
186851
186852
  recorder.on("DebugLog", (log) => this.emit("RecorderDebugLog", { recorder: recorder, ...log }));
@@ -186990,18 +186991,9 @@ function createRecorderManager$1(opts) {
186990
186991
  }
186991
186992
  }
186992
186993
  else {
186993
- // 检查该 provider 是否还有 recorder
186994
- const hasRecorders = this.recorders.some((r) => r.providerId === providerId);
186995
- if (hasRecorders) {
186996
- // 继续循环
186997
- const timer = setTimeout(checkLoop, providerConfig.autoCheckInterval);
186998
- checkLoopTimers.set(providerId, timer);
186999
- }
187000
- else {
187001
- // 没有 recorder 了,停止该 provider 的检查循环
187002
- // TODO: 也许不需要删除定时器
187003
- checkLoopTimers.delete(providerId);
187004
- }
186994
+ // 即使当前 provider 暂时没有 recorder,也保留轮询,避免后续新增 recorder 时漏掉自动检查。
186995
+ const timer = setTimeout(checkLoop, providerConfig.autoCheckInterval);
186996
+ checkLoopTimers.set(providerId, timer);
187005
186997
  }
187006
186998
  }
187007
186999
  };
@@ -187237,15 +187229,15 @@ class mesioDownloader extends EventEmitter$j {
187237
187229
  this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
187238
187230
  this.emit("videoFileCreated", { filename, cover, rawFilename, title });
187239
187231
  });
187240
- this.streamManager.on("videoFileCompleted", ({ filename }) => {
187241
- this.emit("videoFileCompleted", { filename });
187232
+ this.streamManager.on("videoFileCompleted", (data) => {
187233
+ this.emit("videoFileCompleted", data);
187242
187234
  });
187243
187235
  this.streamManager.on("DebugLog", (data) => {
187244
187236
  this.emit("DebugLog", data);
187245
187237
  });
187246
187238
  }
187247
187239
  createCommand() {
187248
- const inputOptions = [...this.inputOptions, "--fix", "--no-proxy"];
187240
+ const inputOptions = [...this.inputOptions, "--fix", "--no-proxy", "--disable-log-file"];
187249
187241
  if (this.debugLevel === "verbose") {
187250
187242
  inputOptions.push("-v");
187251
187243
  }
@@ -187440,8 +187432,8 @@ class BililiveDownloader extends EventEmitter$j {
187440
187432
  this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
187441
187433
  this.emit("videoFileCreated", { filename, cover, rawFilename, title });
187442
187434
  });
187443
- this.streamManager.on("videoFileCompleted", ({ filename }) => {
187444
- this.emit("videoFileCompleted", { filename });
187435
+ this.streamManager.on("videoFileCompleted", (data) => {
187436
+ this.emit("videoFileCompleted", data);
187445
187437
  });
187446
187438
  this.streamManager.on("DebugLog", (data) => {
187447
187439
  this.emit("DebugLog", data);
@@ -187671,8 +187663,8 @@ class FFmpegDownloader extends EventEmitter$j {
187671
187663
  this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
187672
187664
  this.emit("videoFileCreated", { filename, cover, rawFilename, title });
187673
187665
  });
187674
- this.streamManager.on("videoFileCompleted", ({ filename }) => {
187675
- this.emit("videoFileCompleted", { filename });
187666
+ this.streamManager.on("videoFileCompleted", (data) => {
187667
+ this.emit("videoFileCompleted", data);
187676
187668
  });
187677
187669
  this.streamManager.on("DebugLog", (data) => {
187678
187670
  this.emit("DebugLog", data);
@@ -187821,6 +187813,7 @@ function defaultToJSON(provider, recorder) {
187821
187813
  "segment",
187822
187814
  "saveSCDanma",
187823
187815
  "saveCover",
187816
+ "convert2Mp4",
187824
187817
  "saveGiftDanma",
187825
187818
  "disableProvideCommentsWhenRecording",
187826
187819
  "liveInfo",
@@ -195234,8 +195227,8 @@ const checkLiveStatusAndRecord$4 = async function ({ getSavePath, banLiveId, isM
195234
195227
  });
195235
195228
  };
195236
195229
  downloader.on("videoFileCreated", handleVideoCreated);
195237
- downloader.on("videoFileCompleted", ({ filename }) => {
195238
- this.emit("videoFileCompleted", { filename });
195230
+ downloader.on("videoFileCompleted", (data) => {
195231
+ this.emit("videoFileCompleted", data);
195239
195232
  });
195240
195233
  downloader.on("DebugLog", (data) => {
195241
195234
  this.emit("DebugLog", data);
@@ -196104,14 +196097,14 @@ const getMatchingFiles = async (config, folderPath, startTime) => {
196104
196097
  .filter((result) => {
196105
196098
  // 排除directory,开始时间大于startTime,ctime大于当前时间五分钟,startTimeMs从旧到新排序
196106
196099
  const filename = require$$0$7.basename(result.path);
196107
- const startTimeMs = extractStartTimeFromFilename(filename, config.startTimeAutoMatch, result.value.birthtimeMs);
196100
+ const startTimeMs = extractStartTimeFromFilename(filename, config.startTimeAutoMatch, result.value.birthtimeMs || result.value.ctimeMs);
196108
196101
  return (result.value.isFile() &&
196109
196102
  startTimeMs > startTime &&
196110
196103
  Date.now() - result.value.ctimeMs > 1 * 60 * 1000);
196111
196104
  })
196112
196105
  .map((result) => {
196113
196106
  const filename = require$$0$7.basename(result.path);
196114
- const startTimeMs = extractStartTimeFromFilename(filename, config.startTimeAutoMatch, result.value.birthtimeMs);
196107
+ const startTimeMs = extractStartTimeFromFilename(filename, config.startTimeAutoMatch, result.value.birthtimeMs || result.value.ctimeMs);
196115
196108
  return {
196116
196109
  path: result.path,
196117
196110
  startTimeMs,
@@ -211397,8 +211390,8 @@ const checkLiveStatusAndRecord$3 = async function ({ getSavePath, banLiveId, isM
211397
211390
  });
211398
211391
  };
211399
211392
  downloader.on("videoFileCreated", handleVideoCreated);
211400
- downloader.on("videoFileCompleted", ({ filename }) => {
211401
- this.emit("videoFileCompleted", { filename });
211393
+ downloader.on("videoFileCompleted", (data) => {
211394
+ this.emit("videoFileCompleted", data);
211402
211395
  });
211403
211396
  downloader.on("DebugLog", (data) => {
211404
211397
  this.emit("DebugLog", data);
@@ -213080,8 +213073,8 @@ const checkLiveStatusAndRecord$2 = async function ({ getSavePath, isManualStart,
213080
213073
  });
213081
213074
  };
213082
213075
  downloader.on("videoFileCreated", handleVideoCreated);
213083
- downloader.on("videoFileCompleted", ({ filename }) => {
213084
- this.emit("videoFileCompleted", { filename });
213076
+ downloader.on("videoFileCompleted", (data) => {
213077
+ this.emit("videoFileCompleted", data);
213085
213078
  });
213086
213079
  downloader.on("DebugLog", (data) => {
213087
213080
  this.emit("DebugLog", data);
@@ -213097,34 +213090,47 @@ const checkLiveStatusAndRecord$2 = async function ({ getSavePath, isManualStart,
213097
213090
  uid: Number(this.uid),
213098
213091
  useServerTimestamp: this.useServerTimestamp,
213099
213092
  });
213100
- if (!this.disableProvideCommentsWhenRecording) {
213101
- danmaClient.on("Message", (msg) => {
213102
- const extraDataController = downloader.getExtraDataController();
213103
- if (!extraDataController)
213104
- return;
213105
- if (msg.type === "super_chat" && this.saveSCDanma === false)
213106
- return;
213107
- if ((msg.type === "give_gift" || msg.type === "guard") && this.saveGiftDanma === false)
213108
- return;
213109
- this.emit("Message", msg);
213110
- extraDataController.addMessage(msg);
213111
- });
213112
- danmaClient.on("onRoomInfoChange", (msg) => {
213113
- if (utils$2.shouldCheckTitleKeywords(isManualStart, this.titleKeywords)) {
213114
- const title = msg?.body?.title ?? "";
213115
- const hasTitleKeyword = utils$2.hasBlockedTitleKeywords(title, this.titleKeywords);
213116
- if (hasTitleKeyword) {
213117
- this.state = "title-blocked";
213118
- this.emit("DebugLog", {
213119
- type: "common",
213120
- text: `检测到标题包含关键词,停止录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
213121
- });
213122
- // 停止录制
213123
- this.recordHandle && this.recordHandle.stop("直播间标题包含关键词");
213124
- }
213093
+ // 开启了禁止提供弹幕功能,并且也没有设置标题关键词,才完全禁止连接弹幕服务器,否则都连接弹幕服务器,前者不处理弹幕消息,后者根据标题关键词来判断是否停止录制
213094
+ const enableDanmaListen = !this.disableProvideCommentsWhenRecording ||
213095
+ utils$2.shouldCheckTitleKeywords(isManualStart, this.titleKeywords);
213096
+ danmaClient.on("Message", (msg) => {
213097
+ if (this.disableProvideCommentsWhenRecording)
213098
+ return;
213099
+ const extraDataController = downloader.getExtraDataController();
213100
+ if (!extraDataController)
213101
+ return;
213102
+ if (msg.type === "super_chat" && this.saveSCDanma === false)
213103
+ return;
213104
+ if ((msg.type === "give_gift" || msg.type === "guard") && this.saveGiftDanma === false)
213105
+ return;
213106
+ this.emit("Message", msg);
213107
+ extraDataController.addMessage(msg);
213108
+ });
213109
+ danmaClient.on("onRoomInfoChange", (msg) => {
213110
+ if (utils$2.shouldCheckTitleKeywords(isManualStart, this.titleKeywords)) {
213111
+ const title = msg?.body?.title ?? "";
213112
+ const hasTitleKeyword = utils$2.hasBlockedTitleKeywords(title, this.titleKeywords);
213113
+ if (hasTitleKeyword) {
213114
+ this.state = "title-blocked";
213115
+ this.emit("DebugLog", {
213116
+ type: "common",
213117
+ text: `检测到标题包含关键词,停止录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
213118
+ });
213119
+ // 停止录制
213120
+ this.recordHandle && this.recordHandle.stop("直播间标题包含关键词");
213125
213121
  }
213126
- });
213127
- danmaClient.start();
213122
+ }
213123
+ });
213124
+ if (enableDanmaListen) {
213125
+ try {
213126
+ danmaClient.start();
213127
+ }
213128
+ catch (err) {
213129
+ this.emit("DebugLog", {
213130
+ type: "error",
213131
+ text: `弹幕连接失败,错误信息: ${String(err)}`,
213132
+ });
213133
+ }
213128
213134
  }
213129
213135
  const downloaderArgs = downloader.getArguments();
213130
213136
  downloader.run();
@@ -216690,6 +216696,9 @@ async function getRoomInfoByUserWeb(secUserId, opts = {}) {
216690
216696
  if (res.data.includes("验证码")) {
216691
216697
  throw new Error("需要验证码,请在浏览器中打开链接获取" + url);
216692
216698
  }
216699
+ if (!res.data.includes("抖音号")) {
216700
+ throw new Error("userHTML页面没有正常加载" + String(res.data));
216701
+ }
216693
216702
  if (!res.data.includes("直播中")) {
216694
216703
  return {
216695
216704
  living: false,
@@ -216881,6 +216890,7 @@ async function getRoomInfoByWeb(webRoomId, opts = {}) {
216881
216890
  }
216882
216891
  async function getRoomInfoByMobile(secUserId, opts = {}) {
216883
216892
  if (!secUserId) {
216893
+ console.error(opts);
216884
216894
  throw new Error("Mobile API need secUserId, please set uid field");
216885
216895
  }
216886
216896
  if (typeof secUserId === "number") {
@@ -216897,7 +216907,7 @@ async function getRoomInfoByMobile(secUserId, opts = {}) {
216897
216907
  const res = await requester.get(`https://webcast.amemv.com/webcast/room/reflow/info/`, {
216898
216908
  params,
216899
216909
  headers: {
216900
- cookie: opts.auth,
216910
+ // cookie: opts.auth,
216901
216911
  },
216902
216912
  });
216903
216913
  // @ts-ignore
@@ -249155,8 +249165,8 @@ const checkLiveStatusAndRecord$1 = async function ({ getSavePath, banLiveId, isM
249155
249165
  });
249156
249166
  };
249157
249167
  downloader.on("videoFileCreated", handleVideoCreated);
249158
- downloader.on("videoFileCompleted", ({ filename }) => {
249159
- this.emit("videoFileCompleted", { filename });
249168
+ downloader.on("videoFileCompleted", (data) => {
249169
+ this.emit("videoFileCompleted", data);
249160
249170
  });
249161
249171
  downloader.on("DebugLog", (data) => {
249162
249172
  this.emit("DebugLog", data);
@@ -290470,8 +290480,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
290470
290480
  });
290471
290481
  };
290472
290482
  downloader.on("videoFileCreated", handleVideoCreated);
290473
- downloader.on("videoFileCompleted", ({ filename }) => {
290474
- this.emit("videoFileCompleted", { filename });
290483
+ downloader.on("videoFileCompleted", (data) => {
290484
+ this.emit("videoFileCompleted", data);
290475
290485
  });
290476
290486
  downloader.on("DebugLog", (data) => {
290477
290487
  this.emit("DebugLog", data);
@@ -290897,6 +290907,7 @@ class RecorderConfig {
290897
290907
  saveGiftDanma: getValue("saveGiftDanma") ?? false,
290898
290908
  saveSCDanma: getValue("saveSCDanma") ?? true,
290899
290909
  saveCover: getValue("saveCover") ?? false,
290910
+ convert2Mp4: getValue("convert2Mp4") ?? false,
290900
290911
  segment: getValue("segment") ?? 90,
290901
290912
  uid: uid,
290902
290913
  qualityRetry: getValue("qualityRetry") ?? 0,
@@ -290994,6 +291005,30 @@ async function sendEndLiveNotification(appConfig, recorder, config) {
290994
291005
  // 更新最后通知时间
290995
291006
  endLiveNotificationCache.set(cacheKey, now);
290996
291007
  }
291008
+ async function convert2Mp4(videoFile) {
291009
+ const output = replaceExtName$1(videoFile, ".mp4");
291010
+ if (await fs$k.pathExists(output))
291011
+ return output;
291012
+ const name = path$y.basename(output);
291013
+ return new Promise((resolve, reject) => {
291014
+ transcode(videoFile, name, {
291015
+ encoder: "copy",
291016
+ audioCodec: "copy",
291017
+ }, {
291018
+ saveType: 1,
291019
+ savePath: ".",
291020
+ override: false,
291021
+ removeOrigin: false,
291022
+ }).then((task) => {
291023
+ task.on("task-end", () => {
291024
+ resolve(output);
291025
+ });
291026
+ task.on("task-error", () => {
291027
+ reject();
291028
+ });
291029
+ });
291030
+ });
291031
+ }
290997
291032
  async function createRecorderManager(appConfig) {
290998
291033
  /**
290999
291034
  * 更新录制器
@@ -291119,7 +291154,7 @@ async function createRecorderManager(appConfig) {
291119
291154
  return;
291120
291155
  if (recorder.recordHandle) {
291121
291156
  const logFilePath = utils$2.replaceExtName(`${recorder.recordHandle.savePath}_${recorder.id}`, ".recorder.log");
291122
- fs$k.appendFileSync(logFilePath, log.text + "\n");
291157
+ fs$k.appendFile(logFilePath, log.text + "\n").catch(() => { });
291123
291158
  return;
291124
291159
  }
291125
291160
  else {
@@ -291198,8 +291233,8 @@ async function createRecorderManager(appConfig) {
291198
291233
  platform: recorder.providerId,
291199
291234
  });
291200
291235
  });
291201
- manager.on("videoFileCompleted", async ({ recorder, filename }) => {
291202
- logObj.info("Manager videoFileCompleted", { recorder, filename });
291236
+ manager.on("videoFileCompleted", async ({ recorder, filename, stats }) => {
291237
+ logObj.info("Manager videoFileCompleted", { recorder, filename, stats });
291203
291238
  const endTime = new Date();
291204
291239
  const data = recorderConfig.get(recorder.id);
291205
291240
  const title = recorder?.liveInfo?.title;
@@ -291209,8 +291244,14 @@ async function createRecorderManager(appConfig) {
291209
291244
  const config = appConfig.getAll();
291210
291245
  try {
291211
291246
  const xmlFile = replaceExtName$1(filename, ".xml");
291212
- const videoMeta = await readVideoMeta(filename);
291213
- const duration = videoMeta?.format?.duration ?? 0;
291247
+ let duration = 0;
291248
+ try {
291249
+ const videoMeta = await readVideoMeta(filename);
291250
+ duration = videoMeta?.format?.duration ?? 0;
291251
+ }
291252
+ catch (error) {
291253
+ logObj.error("读取视频元信息失败", { filename, error });
291254
+ }
291214
291255
  // 提取文件名(不含后缀)
291215
291256
  const videoFilename = path$y.basename(filename, path$y.extname(filename));
291216
291257
  // 计算文件快速哈希值
@@ -291230,7 +291271,16 @@ async function createRecorderManager(appConfig) {
291230
291271
  video_filename: videoFilename,
291231
291272
  quick_hash: quickHash,
291232
291273
  });
291233
- if (xmlFile && (await fs$k.pathExists(xmlFile))) {
291274
+ if (stats) {
291275
+ recordHistory.upadteLive({
291276
+ video_file: filename,
291277
+ live_id: liveId,
291278
+ }, {
291279
+ danma_num: stats.danmaNum,
291280
+ interact_num: stats.uniqMember,
291281
+ });
291282
+ }
291283
+ else if (xmlFile && (await fs$k.pathExists(xmlFile))) {
291234
291284
  const { uniqMember, danmaNum } = await danmaReport(xmlFile);
291235
291285
  recordHistory.upadteLive({
291236
291286
  video_file: filename,
@@ -291240,6 +291290,16 @@ async function createRecorderManager(appConfig) {
291240
291290
  interact_num: uniqMember,
291241
291291
  });
291242
291292
  }
291293
+ if (data?.convert2Mp4) {
291294
+ try {
291295
+ await convert2Mp4(filename);
291296
+ await fs$k.unlink(filename);
291297
+ logObj.info("转换 mp4 成功,已删除原文件", { filename });
291298
+ }
291299
+ catch (error) {
291300
+ logObj.error("convert2Mp4 error", error);
291301
+ }
291302
+ }
291243
291303
  }
291244
291304
  catch (error) {
291245
291305
  logObj.error("Update live error", { recorder, filename, error });