bililive-cli 3.12.1 → 3.13.1

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.
@@ -11363,12 +11363,12 @@ const APP_DEFAULT_CONFIG = {
11363
11363
  hotProgressColor: "#f9f5f3",
11364
11364
  hotProgressFillColor: "#333333",
11365
11365
  convert2Mp4: false,
11366
- removeSourceAferrConvert2Mp4: true,
11367
11366
  flvRepair: false,
11368
11367
  syncId: undefined,
11369
11368
  uploadHandleTime: ["00:00:00", "23:59:59"],
11370
11369
  limitUploadTime: false,
11371
11370
  uploadNoDanmu: false,
11371
+ uploadToSameMedia: false,
11372
11372
  noDanmuVideoPreset: undefined,
11373
11373
  limitVideoConvertTime: false,
11374
11374
  videoHandleTime: ["00:00:00", "23:59:59"],
@@ -54494,7 +54494,7 @@ async function trash(paths, options) {
54494
54494
  } else if (process$2.platform === 'win32') {
54495
54495
  module = await Promise.resolve().then(function () { return require('./windows-OmnJ7a39.cjs'); });
54496
54496
  } else {
54497
- module = await Promise.resolve().then(function () { return require('./linux-CUI0soP3.cjs'); });
54497
+ module = await Promise.resolve().then(function () { return require('./linux-B6245EX2.cjs'); });
54498
54498
  }
54499
54499
 
54500
54500
  return module.default(paths);
@@ -65707,8 +65707,9 @@ class Platform extends BaseRequest {
65707
65707
  */
65708
65708
  async editMediaWebApi(videos, options, mode) {
65709
65709
  this.auth.authLogin();
65710
+ const { sortByCid, ...mediaOptions } = options;
65710
65711
  const archive = await this.getArchive({
65711
- aid: options.aid,
65712
+ aid: mediaOptions.aid,
65712
65713
  });
65713
65714
  const archiveData = archive.archive;
65714
65715
  for (const key of [
@@ -65725,9 +65726,9 @@ class Platform extends BaseRequest {
65725
65726
  videos: [],
65726
65727
  ...archiveData,
65727
65728
  csrf: csrf,
65728
- ...options,
65729
+ ...mediaOptions,
65729
65730
  watermark: {
65730
- state: options.watermark?.state ?? archive.watermark.state,
65731
+ state: mediaOptions.watermark?.state ?? archive.watermark.state,
65731
65732
  },
65732
65733
  };
65733
65734
  this.checkOptions(data);
@@ -65740,10 +65741,10 @@ class Platform extends BaseRequest {
65740
65741
  data.desc = this.convertDescV2ToDesc(data.desc_v2);
65741
65742
  }
65742
65743
  if (mode === "append") {
65743
- data.videos = [...archive.videos, ...videos];
65744
+ data.videos = this.sortVideosByCid([...archive.videos, ...videos], sortByCid);
65744
65745
  }
65745
65746
  else if (mode === "replace") {
65746
- data.videos = videos;
65747
+ data.videos = this.sortVideosByCid(videos, sortByCid);
65747
65748
  }
65748
65749
  else {
65749
65750
  throw new Error("mode can only be append or replace");
@@ -65872,6 +65873,29 @@ class Platform extends BaseRequest {
65872
65873
  // }
65873
65874
  return true;
65874
65875
  }
65876
+ sortVideosByCid(videos, cidOrder) {
65877
+ if (!cidOrder || cidOrder.length === 0) {
65878
+ return videos;
65879
+ }
65880
+ const orderMap = new Map();
65881
+ cidOrder.forEach((cid, index) => {
65882
+ if (!orderMap.has(cid)) {
65883
+ orderMap.set(cid, index);
65884
+ }
65885
+ });
65886
+ const matchedVideos = [];
65887
+ const unmatchedVideos = [];
65888
+ videos.forEach(video => {
65889
+ const order = orderMap.get(video.cid);
65890
+ if (order === undefined) {
65891
+ unmatchedVideos.push(video);
65892
+ return;
65893
+ }
65894
+ matchedVideos.push({ video, order });
65895
+ });
65896
+ matchedVideos.sort((left, right) => left.order - right.order);
65897
+ return [...matchedVideos.map(item => item.video), ...unmatchedVideos];
65898
+ }
65875
65899
  /**
65876
65900
  * 获取推荐标签
65877
65901
  * subtype,title,description三个参数对结果影响较大
@@ -66923,7 +66947,10 @@ class Video extends BaseRequest {
66923
66947
  const duration = detail.View?.duration;
66924
66948
  if (!duration)
66925
66949
  throw new Error("视频时长获取失败");
66926
- const totalSegment = Math.ceil(duration / 360);
66950
+ const part = detail.View?.pages?.find((p) => p.cid === params.cid);
66951
+ if (!part)
66952
+ throw new Error("未找到对应cid的视频分P");
66953
+ const totalSegment = Math.ceil(part.duration / 360);
66927
66954
  const dmList = [];
66928
66955
  // 从1开始
66929
66956
  for (let i = 0; i < totalSegment; i++) {
@@ -179736,31 +179763,119 @@ async function drawSmoothLineChart(data, width, height) {
179736
179763
  return canvas$1;
179737
179764
  }
179738
179765
 
179766
+ const createEmptyXmlBuckets = () => ({
179767
+ danmu: [],
179768
+ sc: [],
179769
+ guard: [],
179770
+ gift: [],
179771
+ });
179772
+ const isXmlDanmuItemType = (value) => {
179773
+ return ["danmu", "sc", "guard", "gift"].includes(String(value));
179774
+ };
179775
+ const isValidXmlItemForType = (type, item) => {
179776
+ if (type === "danmu") {
179777
+ return typeof item["@_p"] === "string" && item["@_p"].length > 0;
179778
+ }
179779
+ return ((typeof item["@_ts"] === "string" && item["@_ts"].length > 0) ||
179780
+ typeof item["@_ts"] === "number");
179781
+ };
179782
+ const logInvalidTransformedItem = (reason, sourceType, item, targetType) => {
179783
+ logObj.error("filterFunction transform item dropped", {
179784
+ reason,
179785
+ sourceType,
179786
+ targetType,
179787
+ item,
179788
+ });
179789
+ };
179790
+ const createFilterFunction = (filterFunction) => {
179791
+ if (!filterFunction.includes("filter")) {
179792
+ return;
179793
+ }
179794
+ return new Function("type", "danmu", "logger", `
179795
+ ${filterFunction}
179796
+ return filter(type, danmu, logger);`);
179797
+ };
179798
+ const createTransformFunction = (filterFunction) => {
179799
+ if (!filterFunction.includes("transform")) {
179800
+ return;
179801
+ }
179802
+ return new Function("type", "danmu", "logger", `
179803
+ ${filterFunction}
179804
+ return transform(type, danmu, logger);`);
179805
+ };
179806
+ const transformXmlItem = (sourceType, item, transformFunc) => {
179807
+ const transformed = transformFunc(sourceType, item, logObj);
179808
+ if (transformed == null || transformed === false) {
179809
+ return null;
179810
+ }
179811
+ const candidate = transformed === true || transformed === undefined ? item : transformed;
179812
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
179813
+ logInvalidTransformedItem("transform must return an object", sourceType, candidate);
179814
+ return null;
179815
+ }
179816
+ const nextType = "type" in candidate ? candidate.type : sourceType;
179817
+ if (!isXmlDanmuItemType(nextType)) {
179818
+ logInvalidTransformedItem("unsupported target type", sourceType, candidate, nextType);
179819
+ return null;
179820
+ }
179821
+ const normalizedItem = { ...candidate };
179822
+ delete normalizedItem.type;
179823
+ if (!isValidXmlItemForType(nextType, normalizedItem)) {
179824
+ logInvalidTransformedItem("missing required fields for target type", sourceType, candidate, nextType);
179825
+ return null;
179826
+ }
179827
+ return {
179828
+ type: nextType,
179829
+ item: normalizedItem,
179830
+ };
179831
+ };
179832
+ const processXmlItems = (events, filterFunction) => {
179833
+ const buckets = createEmptyXmlBuckets();
179834
+ const filterFunc = createFilterFunction(filterFunction);
179835
+ const transformFunc = createTransformFunction(filterFunction);
179836
+ for (const event of events) {
179837
+ if (filterFunc && !filterFunc(event.sourceType, event.item, logObj)) {
179838
+ continue;
179839
+ }
179840
+ if (!transformFunc) {
179841
+ buckets[event.sourceType].push(event.item);
179842
+ continue;
179843
+ }
179844
+ const transformedItem = transformXmlItem(event.sourceType, event.item, transformFunc);
179845
+ if (!transformedItem) {
179846
+ continue;
179847
+ }
179848
+ buckets[transformedItem.type].push(transformedItem.item);
179849
+ }
179850
+ return buckets;
179851
+ };
179852
+ const createXmlEvents = (items, sourceType) => {
179853
+ const result = [];
179854
+ for (const item of items) {
179855
+ result.push({
179856
+ sourceType,
179857
+ item,
179858
+ });
179859
+ }
179860
+ return result;
179861
+ };
179739
179862
  /**
179740
- * 生成过滤后的xml文件
179863
+ * 生成经过自定义处理后的xml文件
179741
179864
  * @param input
179742
179865
  * @param output
179743
- * @param filterFunction
179866
+ * @param options
179744
179867
  * @returns
179745
179868
  */
179746
- const genFilteredXml = async (input, output, filterFunction) => {
179747
- const filterFunc = new Function("type", "danmu", "logger", `
179748
- ${filterFunction}
179749
- return filter(type, danmu, logger);`);
179869
+ const genProcessedXml = async (input, output, filterFunction) => {
179750
179870
  const { jObj, danmuku, sc, guard, gift } = await parseXmlFile(input, true);
179751
- const filteredDanmuku = danmuku.filter((item) => {
179752
- return filterFunc("danmu", item, logObj);
179753
- });
179754
- const filteredSc = sc.filter((item) => {
179755
- return filterFunc("sc", item, logObj);
179756
- });
179757
- const filteredGuard = guard.filter((item) => {
179758
- return filterFunc("guard", item, logObj);
179759
- });
179760
- const filteredGift = gift.filter((item) => {
179761
- return filterFunc("gift", item, logObj);
179762
- });
179763
- const xmlData = generateMergedXmlContent(filteredDanmuku, filteredGift, filteredSc, filteredGuard, jObj.i?.metadata || {});
179871
+ const events = [
179872
+ ...createXmlEvents(danmuku, "danmu"),
179873
+ ...createXmlEvents(sc, "sc"),
179874
+ ...createXmlEvents(guard, "guard"),
179875
+ ...createXmlEvents(gift, "gift"),
179876
+ ];
179877
+ const processedBuckets = processXmlItems(events, filterFunction);
179878
+ const xmlData = generateMergedXmlContent(processedBuckets.danmu, processedBuckets.gift, processedBuckets.sc, processedBuckets.guard, jObj.i?.metadata || {});
179764
179879
  await fs$k.writeFile(output, xmlData);
179765
179880
  return output;
179766
179881
  };
@@ -179785,10 +179900,13 @@ const addConvertDanmu2AssTask = async (originInput, output, danmuOptions, option
179785
179900
  opts = await customChangeFunc(originInput, opts);
179786
179901
  }
179787
179902
  let filteredOutput;
179788
- if (opts.filterFunction && (opts.filterFunction ?? "").includes("filter")) {
179789
- // 如果存在自定义过滤函数,则需要把过滤后的xml保存到临时文件夹中
179903
+ const hasFilterFunction = Boolean((opts.filterFunction ?? "").trim()) && (opts.filterFunction ?? "").includes("filter");
179904
+ const hasTransformFunction = Boolean((opts.filterFunction ?? "").trim()) &&
179905
+ (opts.filterFunction ?? "").includes("transform");
179906
+ if (hasTransformFunction || hasFilterFunction) {
179907
+ // 如果存在自定义数据处理函数,则需要把处理后的xml保存到临时文件夹中
179790
179908
  filteredOutput = path$y.join(tempDir, `${uuid$5()}.xml`);
179791
- await genFilteredXml(originInput, filteredOutput, opts.filterFunction);
179909
+ await genProcessedXml(originInput, filteredOutput, opts.filterFunction);
179792
179910
  }
179793
179911
  if (opts.blacklist) {
179794
179912
  const fileTxtPath = path$y.join(tempDir, `${uuid$5()}.txt`);
@@ -181468,8 +181586,10 @@ async function addMediaApi(uid, video, options) {
181468
181586
  * 编辑视频接口
181469
181587
  */
181470
181588
  async function editMediaApi(uid, aid, video, options) {
181471
- const mediaOptions = {};
181472
- console.log("编辑视频", options);
181589
+ const mediaOptions = {
181590
+ sortByCid: options.sortByCid,
181591
+ };
181592
+ // console.log("编辑视频", options);
181473
181593
  // const globalConfig = container.resolve("globalConfig");
181474
181594
  // const mediaOptions = formatOptions(options, path.join(globalConfig.userDataPath, "cover"));
181475
181595
  const client = createClient(uid);
@@ -181808,6 +181928,7 @@ async function editMedia(aid, filePath, options, uid, extraOptions) {
181808
181928
  uid,
181809
181929
  mediaOptions: formattedOptions,
181810
181930
  aid,
181931
+ sortParams: extraOptions?.sortParams,
181811
181932
  }, {
181812
181933
  onEnd: async () => {
181813
181934
  // 审核检查
@@ -182159,6 +182280,8 @@ const biliApi = {
182159
182280
  editVideoPartName,
182160
182281
  queryVideoStatus,
182161
182282
  getPlayUrl,
182283
+ readUser,
182284
+ writeUser,
182162
182285
  };
182163
182286
 
182164
182287
  /**
@@ -184110,11 +184233,14 @@ class BiliPartVideoTask extends AbstractTask {
184110
184233
  logObj.error(`task ${this.taskId} error: ${error}`);
184111
184234
  }
184112
184235
  }
184113
- this.completedPart = data;
184236
+ this.completedPart = {
184237
+ ...data,
184238
+ filePath: this.command.filePath,
184239
+ };
184114
184240
  this.endTime = Date.now();
184115
184241
  // 重置进度追踪
184116
184242
  this.speedCalculator.reset();
184117
- callback.onEnd && callback.onEnd(data);
184243
+ callback.onEnd && callback.onEnd(this.completedPart);
184118
184244
  this.emitter.emit("task-end", { taskId: this.taskId });
184119
184245
  });
184120
184246
  command.emitter.on("error", (err) => {
@@ -184161,6 +184287,7 @@ class BiliPartVideoTask extends AbstractTask {
184161
184287
  cid: part.cid,
184162
184288
  filename: part.filename,
184163
184289
  title: this.command.title,
184290
+ filePath: this.command.filePath,
184164
184291
  };
184165
184292
  this.endTime = Date.now();
184166
184293
  // 重置进度追踪
@@ -184365,7 +184492,7 @@ class BiliAddVideoTask extends BiliVideoTask {
184365
184492
  this.progress = 100;
184366
184493
  this.callback.onEnd && this.callback.onEnd(data);
184367
184494
  this.output = String(data.aid);
184368
- this.emitter.emit("task-end", { taskId: this.taskId });
184495
+ this.emitter.emit("task-end", { taskId: this.taskId, data: parts });
184369
184496
  uploadPartService.removeByCids(parts.map((part) => part.cid));
184370
184497
  exports.statisticsService.addOrUpdate({
184371
184498
  where: { stat_key: this.lastUpdateTimeKey },
@@ -184391,10 +184518,12 @@ class BiliAddVideoTask extends BiliVideoTask {
184391
184518
  class BiliEditVideoTask extends BiliVideoTask {
184392
184519
  aid;
184393
184520
  mediaOptions;
184521
+ sortParams;
184394
184522
  constructor(options, callback) {
184395
184523
  super(options, callback);
184396
184524
  this.aid = options.aid;
184397
184525
  this.mediaOptions = options.mediaOptions;
184526
+ this.sortParams = options.sortParams;
184398
184527
  this.on("completed", () => {
184399
184528
  this.submit();
184400
184529
  });
@@ -184439,12 +184568,26 @@ class BiliEditVideoTask extends BiliVideoTask {
184439
184568
  return;
184440
184569
  }
184441
184570
  try {
184442
- const data = await retryWithAxiosError(() => editMediaApi(this.uid, this.aid, parts, this.mediaOptions), 5);
184571
+ const sortByCid = [];
184572
+ if (this.sortParams && Array.isArray(this.sortParams)) {
184573
+ for (const param of this.sortParams) {
184574
+ if (param.cid) {
184575
+ sortByCid.push(param.cid);
184576
+ }
184577
+ else {
184578
+ const part = parts.find((p) => p.filePath === param.filePath);
184579
+ if (part) {
184580
+ sortByCid.push(part.cid);
184581
+ }
184582
+ }
184583
+ }
184584
+ }
184585
+ const data = await retryWithAxiosError(() => editMediaApi(this.uid, this.aid, parts, { ...this.mediaOptions, sortByCid: sortByCid }), 5);
184443
184586
  this.status = "completed";
184444
184587
  this.progress = 100;
184445
184588
  this.callback.onEnd && this.callback.onEnd(data);
184446
184589
  this.output = String(data.aid);
184447
- this.emitter.emit("task-end", { taskId: this.taskId });
184590
+ this.emitter.emit("task-end", { taskId: this.taskId, data: parts });
184448
184591
  uploadPartService.removeByCids(parts.map((part) => part.cid));
184449
184592
  exports.statisticsService.addOrUpdate({
184450
184593
  where: { stat_key: this.lastUpdateTimeKey },
@@ -185517,7 +185660,7 @@ function getValuesFromArrayLikeFlexSpaceBetween(array, columnCount) {
185517
185660
  });
185518
185661
  return columnValues;
185519
185662
  }
185520
- function ensureFolderExist$5(fileOrFolderPath) {
185663
+ function ensureFolderExist(fileOrFolderPath) {
185521
185664
  const folder = path$y.dirname(fileOrFolderPath);
185522
185665
  if (!fs$D.existsSync(folder)) {
185523
185666
  fs$D.mkdirSync(folder, { recursive: true });
@@ -185926,7 +186069,7 @@ var utils$2 = {
185926
186069
  replaceExtName,
185927
186070
  singleton: singleton$1,
185928
186071
  getValuesFromArrayLikeFlexSpaceBetween,
185929
- ensureFolderExist: ensureFolderExist$5,
186072
+ ensureFolderExist,
185930
186073
  assert: assert$m,
185931
186074
  assertStringType,
185932
186075
  assertNumberType,
@@ -186091,71 +186234,80 @@ async function getBiliStatusInfoByRoomIds(RoomIds) {
186091
186234
 
186092
186235
  /**
186093
186236
  * XML流式写入控制器,用于实时写入弹幕、礼物等信息到XML文件
186094
- * 相比原有的json方案,这个实现每隔5秒就会写入数据,减少内存占用和数据丢失风险
186237
+ * 相比原有的json方案,这个实现每隔10秒就会写入数据,减少内存占用和数据丢失风险
186095
186238
  */
186239
+ const METADATA_PLACEHOLDER = "<!--METADATA_PLACEHOLDER-->";
186240
+ 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`;
186096
186241
  function createRecordExtraDataController(savePath) {
186097
186242
  const data = {
186243
+ header: XML_FILE_HEADER,
186098
186244
  meta: {
186099
186245
  recordStartTimestamp: Date.now(),
186100
186246
  },
186101
186247
  pendingMessages: [],
186102
186248
  };
186103
186249
  let hasCompleted = false;
186104
- let isWriting = false;
186105
- let isInitialized = false;
186106
- // 初始化文件
186107
- const initializeFile = async () => {
186108
- if (isInitialized)
186109
- return;
186110
- isInitialized = true;
186111
- try {
186112
- // 创建XML文件头,使用占位符预留metadata位置
186113
- 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>`;
186114
- await fs$D.promises.writeFile(savePath, header);
186115
- }
186116
- catch (error) {
186117
- console.error("初始化XML文件失败:", error);
186118
- isInitialized = false;
186119
- throw error;
186120
- }
186121
- };
186122
- // 每10秒写入一次数据
186123
- const scheduleWrite = asyncThrottle(() => writeToFile(), 10e3, {
186124
- immediateRunWhenEndOfDefer: true,
186250
+ let hasPersistedHeader = false;
186251
+ let danmaNum = 0;
186252
+ let scNum = 0;
186253
+ let guardNum = 0;
186254
+ const interactedUsers = new Set();
186255
+ const getStats = () => ({
186256
+ danmaNum,
186257
+ uniqMember: interactedUsers.size,
186258
+ scNum,
186259
+ guardNum,
186125
186260
  });
186126
- const writeToFile = async () => {
186127
- if (isWriting || hasCompleted || data.pendingMessages.length === 0) {
186261
+ const trackInteractedUser = (message) => {
186262
+ const userName = message.sender?.name?.trim();
186263
+ if (!userName)
186128
186264
  return;
186265
+ interactedUsers.add(userName);
186266
+ };
186267
+ const initializeFile = async (content) => {
186268
+ // 这里有个假设,那就是第一次保存必然存在metatdata信息
186269
+ const initialContent = data.header.replace(METADATA_PLACEHOLDER, generateMetadataXml(data.meta));
186270
+ await fs$D.promises.writeFile(savePath, initialContent + content);
186271
+ hasPersistedHeader = true;
186272
+ };
186273
+ const writeToFile = async (force = false) => {
186274
+ if (!force && data.pendingMessages.length === 0) {
186275
+ return Promise.resolve();
186129
186276
  }
186130
- // 确保文件已初始化
186131
- await initializeFile();
186132
- isWriting = true;
186277
+ const messagesToWrite = [...data.pendingMessages];
186278
+ data.pendingMessages = [];
186133
186279
  try {
186134
- // 获取待写入的消息
186135
- const messagesToWrite = [...data.pendingMessages];
186136
- data.pendingMessages = [];
186137
- // 生成XML内容
186138
186280
  const xmlContent = generateXmlContent(data.meta, messagesToWrite);
186139
- // 追加写入文件
186140
- await appendToXmlFile(savePath, xmlContent);
186281
+ if (!hasPersistedHeader) {
186282
+ await initializeFile(xmlContent);
186283
+ }
186284
+ else if (xmlContent) {
186285
+ await appendToXmlFile(savePath, xmlContent);
186286
+ }
186141
186287
  }
186142
186288
  catch (error) {
186143
186289
  console.error("写入XML文件失败:", error);
186144
- // 如果写入失败,将消息重新加入队列
186145
- data.pendingMessages = [...data.pendingMessages];
186146
- }
186147
- finally {
186148
- isWriting = false;
186290
+ data.pendingMessages = [...messagesToWrite, ...data.pendingMessages];
186149
186291
  }
186150
186292
  };
186293
+ // 每10秒写入一次数据
186294
+ const writeTimer = setInterval(() => {
186295
+ writeToFile();
186296
+ }, 10e3);
186151
186297
  const addMessage = (message) => {
186152
186298
  if (hasCompleted)
186153
186299
  return;
186154
- // if (!isInitialized) return;
186300
+ if (message.type === "comment") {
186301
+ danmaNum += 1;
186302
+ }
186303
+ else if (message.type === "super_chat") {
186304
+ scNum += 1;
186305
+ }
186306
+ else if (message.type === "guard") {
186307
+ guardNum += 1;
186308
+ }
186309
+ trackInteractedUser(message);
186155
186310
  data.pendingMessages.push(message);
186156
- // 确保文件已初始化
186157
- initializeFile().catch(console.error);
186158
- scheduleWrite();
186159
186311
  };
186160
186312
  const setMeta = async (meta) => {
186161
186313
  if (hasCompleted)
@@ -186164,30 +186316,31 @@ function createRecordExtraDataController(savePath) {
186164
186316
  ...data.meta,
186165
186317
  ...meta,
186166
186318
  };
186167
- // 确保文件已初始化,然后立即更新文件中的metadata
186168
- await initializeFile().catch(console.error);
186169
- await updateMetadataInFile(savePath, data.meta).catch(console.error);
186170
186319
  };
186171
186320
  const flush = async () => {
186172
186321
  if (hasCompleted)
186173
186322
  return;
186174
186323
  hasCompleted = true;
186175
- scheduleWrite.cancel();
186176
- await initializeFile().catch(console.error);
186177
- // 写入剩余的数据
186178
- if (data.pendingMessages.length > 0) {
186179
- await writeToFile();
186180
- }
186181
- // 完成XML文件(添加结束标签等)
186182
- await finalizeXmlFile(savePath);
186183
- // 清理内存
186184
- data.pendingMessages = [];
186324
+ writeTimer && clearInterval(writeTimer);
186325
+ try {
186326
+ await writeToFile(true);
186327
+ await appendToXmlFile(savePath, "</i>");
186328
+ }
186329
+ catch (error) {
186330
+ console.error("完成XML文件写入失败:", error);
186331
+ }
186332
+ finally {
186333
+ // 清理内存
186334
+ data.pendingMessages = [];
186335
+ interactedUsers.clear();
186336
+ }
186185
186337
  };
186186
186338
  return {
186187
186339
  data,
186188
186340
  addMessage,
186189
186341
  setMeta,
186190
186342
  flush,
186343
+ getStats,
186191
186344
  };
186192
186345
  }
186193
186346
  /**
@@ -186203,7 +186356,7 @@ function generateXmlContent(metadata, messages) {
186203
186356
  .filter((item) => item.type === "comment")
186204
186357
  .map((ele) => {
186205
186358
  const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
186206
- const data = {
186359
+ const attrs = {
186207
186360
  "@@p": "",
186208
186361
  "@@progress": progress,
186209
186362
  "@@mode": String(ele.mode ?? 1),
@@ -186218,18 +186371,18 @@ function generateXmlContent(metadata, messages) {
186218
186371
  "@@uid": String(ele?.sender?.uid),
186219
186372
  "@@timestamp": String(ele.timestamp),
186220
186373
  };
186221
- data["@@p"] = [
186222
- data["@@progress"],
186223
- data["@@mode"],
186224
- data["@@fontsize"],
186225
- data["@@color"],
186226
- data["@@ctime"],
186227
- data["@@pool"],
186228
- data["@@midHash"],
186229
- data["@@uid"],
186230
- data["@@weight"],
186374
+ attrs["@@p"] = [
186375
+ attrs["@@progress"],
186376
+ attrs["@@mode"],
186377
+ attrs["@@fontsize"],
186378
+ attrs["@@color"],
186379
+ attrs["@@ctime"],
186380
+ attrs["@@pool"],
186381
+ attrs["@@midHash"],
186382
+ attrs["@@uid"],
186383
+ attrs["@@weight"],
186231
186384
  ].join(",");
186232
- return pick$1(data, ["@@p", "#text", "@@user", "@@uid", "@@timestamp"]);
186385
+ return pick$1(attrs, ["@@p", "#text", "@@user", "@@uid", "@@timestamp"]);
186233
186386
  });
186234
186387
  const gifts = messages
186235
186388
  .filter((item) => item.type === "give_gift")
@@ -186295,55 +186448,22 @@ async function appendToXmlFile(filePath, content) {
186295
186448
  throw error;
186296
186449
  }
186297
186450
  }
186298
- /**
186299
- * 更新XML文件中的metadata
186300
- */
186301
- async function updateMetadataInFile(filePath, metadata) {
186302
- try {
186303
- const builder = new fxp.XMLBuilder({
186304
- ignoreAttributes: false,
186305
- attributeNamePrefix: "@@",
186306
- format: true,
186307
- });
186308
- // 生成metadata XML
186309
- const metadataXml = builder.build({
186310
- metadata: {
186311
- platform: metadata.platform,
186312
- video_start_time: metadata.recordStartTimestamp,
186313
- live_start_time: metadata.liveStartTimestamp,
186314
- room_title: metadata.title,
186315
- user_name: metadata.user_name,
186316
- room_id: metadata.room_id,
186317
- },
186318
- });
186319
- // 读取文件内容
186320
- const content = await fs$D.promises.readFile(filePath, "utf-8");
186321
- // 替换占位符为实际的metadata
186322
- const updatedContent = content.replace("<!--METADATA_PLACEHOLDER-->", metadataXml);
186323
- // 写回文件
186324
- await fs$D.promises.writeFile(filePath, updatedContent);
186325
- }
186326
- catch (error) {
186327
- console.error(`更新XML文件metadata失败: ${filePath}`, error);
186328
- throw error;
186329
- }
186330
- }
186331
- /**
186332
- * 完成XML文件写入
186333
- */
186334
- async function finalizeXmlFile(filePath) {
186335
- try {
186336
- // 读取文件内容
186337
- const content = await fs$D.promises.readFile(filePath, "utf-8");
186338
- // 添加结束标签
186339
- const finalContent = content + "</i>";
186340
- // 写回文件
186341
- await fs$D.promises.writeFile(filePath, finalContent);
186342
- }
186343
- catch (error) {
186344
- console.error(`完成XML文件写入失败: ${filePath}`, error);
186345
- throw error;
186346
- }
186451
+ function generateMetadataXml(metadata) {
186452
+ const builder = new fxp.XMLBuilder({
186453
+ ignoreAttributes: false,
186454
+ attributeNamePrefix: "@@",
186455
+ format: true,
186456
+ });
186457
+ return builder.build({
186458
+ metadata: {
186459
+ platform: metadata.platform,
186460
+ video_start_time: metadata.recordStartTimestamp,
186461
+ live_start_time: metadata.liveStartTimestamp,
186462
+ room_title: metadata.title,
186463
+ user_name: metadata.user_name,
186464
+ room_id: metadata.room_id,
186465
+ },
186466
+ });
186347
186467
  }
186348
186468
 
186349
186469
  class Segment extends EventEmitter$j {
@@ -186362,6 +186482,12 @@ class Segment extends EventEmitter$j {
186362
186482
  this.disableDanma = disableDanma;
186363
186483
  this.videoExt = videoExt;
186364
186484
  }
186485
+ getVideoFileCompletedPayload() {
186486
+ return {
186487
+ filename: this.outputFilePath,
186488
+ stats: this.extraDataController?.getStats(),
186489
+ };
186490
+ }
186365
186491
  async handleSegmentEnd() {
186366
186492
  if (!this.outputVideoFilePath) {
186367
186493
  this.emit("DebugLog", {
@@ -186370,6 +186496,7 @@ class Segment extends EventEmitter$j {
186370
186496
  });
186371
186497
  return;
186372
186498
  }
186499
+ const data = this.getVideoFileCompletedPayload();
186373
186500
  try {
186374
186501
  this.emit("DebugLog", {
186375
186502
  type: "info",
@@ -186379,7 +186506,7 @@ class Segment extends EventEmitter$j {
186379
186506
  retry$1(() => fs$E.rename(this.rawRecordingVideoPath, this.outputFilePath), 20, 1000),
186380
186507
  this.extraDataController?.flush(),
186381
186508
  ]);
186382
- this.emit("videoFileCompleted", { filename: this.outputFilePath });
186509
+ this.emit("videoFileCompleted", data);
186383
186510
  }
186384
186511
  catch (err) {
186385
186512
  this.emit("DebugLog", {
@@ -186387,7 +186514,7 @@ class Segment extends EventEmitter$j {
186387
186514
  text: "videoFileCompleted error " + String(err),
186388
186515
  });
186389
186516
  // 虽然重命名失败了,但是也当作完成处理,避免卡住录制流程
186390
- this.emit("videoFileCompleted", { filename: this.outputFilePath });
186517
+ this.emit("videoFileCompleted", data);
186391
186518
  }
186392
186519
  }
186393
186520
  async onSegmentStart(stderrLine, callBack) {
@@ -186408,11 +186535,20 @@ class Segment extends EventEmitter$j {
186408
186535
  });
186409
186536
  }
186410
186537
  }
186411
- this.outputVideoFilePath = this.getSavePath({
186538
+ let recordSavePath = this.getSavePath({
186412
186539
  startTime: startTime,
186413
- title: liveInfo?.title,
186540
+ title: liveInfo?.title ? liveInfo.title : undefined,
186414
186541
  });
186415
- ensureFolderExist$5(this.outputVideoFilePath);
186542
+ // 文件重复判断
186543
+ if (require$$0$6.existsSync(recordSavePath + "." + this.videoExt)) {
186544
+ recordSavePath = this.getSavePath({
186545
+ startTime: startTime,
186546
+ title: liveInfo?.title,
186547
+ extraMs: true,
186548
+ });
186549
+ }
186550
+ this.outputVideoFilePath = recordSavePath;
186551
+ ensureFolderExist(this.outputVideoFilePath);
186416
186552
  if (!this.disableDanma) {
186417
186553
  this.extraDataController = createRecordExtraDataController(`${this.outputVideoFilePath}.xml`);
186418
186554
  }
@@ -186456,12 +186592,19 @@ class StreamManager extends EventEmitter$j {
186456
186592
  callBack;
186457
186593
  constructor(getSavePath, hasSegment, disableDanma, recorderType, videoFormat, callBack) {
186458
186594
  super();
186459
- const recordSavePath = getSavePath({ startTime: Date.now() });
186460
- this.recordSavePath = recordSavePath;
186595
+ const startTime = Date.now();
186596
+ let recordSavePath = getSavePath({ startTime });
186461
186597
  this.videoFormat = videoFormat;
186462
186598
  this.recorderType = recorderType;
186463
186599
  this.hasSegment = hasSegment;
186464
186600
  this.callBack = callBack;
186601
+ console.log("Initial recordSavePath:", recordSavePath);
186602
+ // 文件重复判断
186603
+ if (require$$0$6.existsSync(recordSavePath + "." + videoFormat)) {
186604
+ console.log("File already exists, generating new save path with extraMs");
186605
+ recordSavePath = getSavePath({ startTime, extraMs: true });
186606
+ }
186607
+ this.recordSavePath = recordSavePath;
186465
186608
  if (hasSegment) {
186466
186609
  this.segment = new Segment(getSavePath, disableDanma, this.videoExt);
186467
186610
  this.segment.on("DebugLog", (data) => {
@@ -186475,6 +186618,7 @@ class StreamManager extends EventEmitter$j {
186475
186618
  });
186476
186619
  }
186477
186620
  else {
186621
+ ensureFolderExist(recordSavePath);
186478
186622
  const extraDataSavePath = `${recordSavePath}.xml`;
186479
186623
  if (!disableDanma) {
186480
186624
  this.extraDataController = createRecordExtraDataController(extraDataSavePath);
@@ -186520,8 +186664,13 @@ class StreamManager extends EventEmitter$j {
186520
186664
  }
186521
186665
  else {
186522
186666
  if (this.recordStartTime) {
186523
- await this.getExtraDataController()?.flush();
186524
- this.emit("videoFileCompleted", { filename: this.videoFilePath });
186667
+ const stats = this.extraDataController?.getStats();
186668
+ const extraDataController = this.getExtraDataController();
186669
+ await extraDataController?.flush();
186670
+ this.emit("videoFileCompleted", {
186671
+ filename: this.videoFilePath,
186672
+ stats: stats,
186673
+ });
186525
186674
  }
186526
186675
  }
186527
186676
  }
@@ -186685,7 +186834,7 @@ function createRecorderManager$1(opts) {
186685
186834
  }
186686
186835
  this.emit("videoFileCreated", { recorder: recorder.toJSON(), filename, rawFilename });
186687
186836
  });
186688
- recorder.on("videoFileCompleted", ({ filename }) => this.emit("videoFileCompleted", { recorder: recorder.toJSON(), filename }));
186837
+ recorder.on("videoFileCompleted", ({ filename, stats }) => this.emit("videoFileCompleted", { recorder: recorder.toJSON(), filename, stats }));
186689
186838
  recorder.on("Message", (message) => this.emit("Message", { recorder: recorder.toJSON(), message }));
186690
186839
  recorder.on("Updated", (keys) => this.emit("RecorderUpdated", { recorder: recorder.toJSON(), keys }));
186691
186840
  recorder.on("DebugLog", (log) => this.emit("RecorderDebugLog", { recorder: recorder, ...log }));
@@ -186830,18 +186979,9 @@ function createRecorderManager$1(opts) {
186830
186979
  }
186831
186980
  }
186832
186981
  else {
186833
- // 检查该 provider 是否还有 recorder
186834
- const hasRecorders = this.recorders.some((r) => r.providerId === providerId);
186835
- if (hasRecorders) {
186836
- // 继续循环
186837
- const timer = setTimeout(checkLoop, providerConfig.autoCheckInterval);
186838
- checkLoopTimers.set(providerId, timer);
186839
- }
186840
- else {
186841
- // 没有 recorder 了,停止该 provider 的检查循环
186842
- // TODO: 也许不需要删除定时器
186843
- checkLoopTimers.delete(providerId);
186844
- }
186982
+ // 即使当前 provider 暂时没有 recorder,也保留轮询,避免后续新增 recorder 时漏掉自动检查。
186983
+ const timer = setTimeout(checkLoop, providerConfig.autoCheckInterval);
186984
+ checkLoopTimers.set(providerId, timer);
186845
186985
  }
186846
186986
  }
186847
186987
  };
@@ -186932,6 +187072,9 @@ function genSavePathFromRule(manager, recorder, extData) {
186932
187072
  channelId,
186933
187073
  };
186934
187074
  let savePathRule = manager.savePathRule;
187075
+ if (extData?.extraMs) {
187076
+ savePathRule += "_{ms}";
187077
+ }
186935
187078
  try {
186936
187079
  savePathRule = ejs.render(savePathRule, params);
186937
187080
  }
@@ -187074,8 +187217,8 @@ class mesioDownloader extends EventEmitter$j {
187074
187217
  this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
187075
187218
  this.emit("videoFileCreated", { filename, cover, rawFilename, title });
187076
187219
  });
187077
- this.streamManager.on("videoFileCompleted", ({ filename }) => {
187078
- this.emit("videoFileCompleted", { filename });
187220
+ this.streamManager.on("videoFileCompleted", (data) => {
187221
+ this.emit("videoFileCompleted", data);
187079
187222
  });
187080
187223
  this.streamManager.on("DebugLog", (data) => {
187081
187224
  this.emit("DebugLog", data);
@@ -187277,8 +187420,8 @@ class BililiveDownloader extends EventEmitter$j {
187277
187420
  this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
187278
187421
  this.emit("videoFileCreated", { filename, cover, rawFilename, title });
187279
187422
  });
187280
- this.streamManager.on("videoFileCompleted", ({ filename }) => {
187281
- this.emit("videoFileCompleted", { filename });
187423
+ this.streamManager.on("videoFileCompleted", (data) => {
187424
+ this.emit("videoFileCompleted", data);
187282
187425
  });
187283
187426
  this.streamManager.on("DebugLog", (data) => {
187284
187427
  this.emit("DebugLog", data);
@@ -187331,6 +187474,11 @@ class BililiveDownloader extends EventEmitter$j {
187331
187474
  if (timeMatch) {
187332
187475
  time = timeMatch[1];
187333
187476
  }
187477
+ const spaceMath = line.match(/下载进度:\s*([\d.]+\s*MB)\s*/);
187478
+ if (spaceMath) {
187479
+ const space = spaceMath[1];
187480
+ time = time ? `${time} ${space}` : space;
187481
+ }
187334
187482
  return {
187335
187483
  time,
187336
187484
  };
@@ -187503,8 +187651,8 @@ class FFmpegDownloader extends EventEmitter$j {
187503
187651
  this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
187504
187652
  this.emit("videoFileCreated", { filename, cover, rawFilename, title });
187505
187653
  });
187506
- this.streamManager.on("videoFileCompleted", ({ filename }) => {
187507
- this.emit("videoFileCompleted", { filename });
187654
+ this.streamManager.on("videoFileCompleted", (data) => {
187655
+ this.emit("videoFileCompleted", data);
187508
187656
  });
187509
187657
  this.streamManager.on("DebugLog", (data) => {
187510
187658
  this.emit("DebugLog", data);
@@ -187590,7 +187738,7 @@ class FFmpegDownloader extends EventEmitter$j {
187590
187738
  let time = null;
187591
187739
  const timeMatch = line.match(/time=([0-9:.]+)/);
187592
187740
  if (timeMatch) {
187593
- time = timeMatch[1];
187741
+ time = timeMatch[1].split(".")[0];
187594
187742
  }
187595
187743
  return {
187596
187744
  time,
@@ -187694,12 +187842,6 @@ function getBililivePath() {
187694
187842
  return bililivePath;
187695
187843
  }
187696
187844
 
187697
- function ensureFolderExist$4(fileOrFolderPath) {
187698
- const folder = path$y.dirname(fileOrFolderPath);
187699
- if (!fs$D.existsSync(folder)) {
187700
- fs$D.mkdirSync(folder, { recursive: true });
187701
- }
187702
- }
187703
187845
  const uuid$1 = () => {
187704
187846
  return crypto$c.randomUUID();
187705
187847
  };
@@ -195043,6 +195185,7 @@ const checkLiveStatusAndRecord$4 = async function ({ getSavePath, banLiveId, isM
195043
195185
  startTime: opts.startTime,
195044
195186
  liveStartTime,
195045
195187
  recordStartTime,
195188
+ extraMs: opts.extraMs,
195046
195189
  }),
195047
195190
  disableDanma: this.disableProvideCommentsWhenRecording,
195048
195191
  videoFormat: this.videoFormat ?? "auto",
@@ -195052,20 +195195,6 @@ const checkLiveStatusAndRecord$4 = async function ({ getSavePath, banLiveId, isM
195052
195195
  const info = await getInfo$4(this.channelId);
195053
195196
  return info;
195054
195197
  });
195055
- const savePath = getSavePath({
195056
- owner,
195057
- title,
195058
- startTime: Date.now(),
195059
- liveStartTime,
195060
- recordStartTime,
195061
- });
195062
- try {
195063
- ensureFolderExist$4(savePath);
195064
- }
195065
- catch (err) {
195066
- this.state = "idle";
195067
- throw err;
195068
- }
195069
195198
  const handleVideoCreated = async ({ filename, title, cover, rawFilename }) => {
195070
195199
  this.emit("videoFileCreated", { filename, cover, rawFilename });
195071
195200
  if (title && this?.liveInfo) {
@@ -195085,8 +195214,8 @@ const checkLiveStatusAndRecord$4 = async function ({ getSavePath, banLiveId, isM
195085
195214
  });
195086
195215
  };
195087
195216
  downloader.on("videoFileCreated", handleVideoCreated);
195088
- downloader.on("videoFileCompleted", ({ filename }) => {
195089
- this.emit("videoFileCompleted", { filename });
195217
+ downloader.on("videoFileCompleted", (data) => {
195218
+ this.emit("videoFileCompleted", data);
195090
195219
  });
195091
195220
  downloader.on("DebugLog", (data) => {
195092
195221
  this.emit("DebugLog", data);
@@ -195277,7 +195406,7 @@ const checkLiveStatusAndRecord$4 = async function ({ getSavePath, banLiveId, isM
195277
195406
  recorderType: downloader.type,
195278
195407
  url: stream.url,
195279
195408
  downloaderArgs,
195280
- savePath: savePath,
195409
+ savePath: downloader.videoFilePath,
195281
195410
  stop,
195282
195411
  cut,
195283
195412
  };
@@ -196180,12 +196309,6 @@ const requester$2 = axios.create({
196180
196309
  proxy: false,
196181
196310
  });
196182
196311
 
196183
- function ensureFolderExist$3(fileOrFolderPath) {
196184
- const folder = path$y.dirname(fileOrFolderPath);
196185
- if (!fs$D.existsSync(folder)) {
196186
- fs$D.mkdirSync(folder, { recursive: true });
196187
- }
196188
- }
196189
196312
  function assert$l(assertion, msg) {
196190
196313
  if (!assertion) {
196191
196314
  throw new Error(msg);
@@ -211223,6 +211346,7 @@ const checkLiveStatusAndRecord$3 = async function ({ getSavePath, banLiveId, isM
211223
211346
  startTime: opts.startTime,
211224
211347
  liveStartTime,
211225
211348
  recordStartTime,
211349
+ extraMs: opts.extraMs,
211226
211350
  }),
211227
211351
  disableDanma: this.disableProvideCommentsWhenRecording,
211228
211352
  videoFormat: this.videoFormat ?? "auto",
@@ -211234,20 +211358,6 @@ const checkLiveStatusAndRecord$3 = async function ({ getSavePath, banLiveId, isM
211234
211358
  const info = await getInfo$3(this.channelId);
211235
211359
  return info;
211236
211360
  });
211237
- const savePath = getSavePath({
211238
- owner,
211239
- title,
211240
- startTime: Date.now(),
211241
- liveStartTime,
211242
- recordStartTime,
211243
- });
211244
- try {
211245
- ensureFolderExist$3(savePath);
211246
- }
211247
- catch (err) {
211248
- this.state = "idle";
211249
- throw err;
211250
- }
211251
211361
  const handleVideoCreated = async ({ filename, title, cover, rawFilename }) => {
211252
211362
  this.emit("videoFileCreated", { filename, cover, rawFilename });
211253
211363
  if (title && this?.liveInfo) {
@@ -211267,8 +211377,8 @@ const checkLiveStatusAndRecord$3 = async function ({ getSavePath, banLiveId, isM
211267
211377
  });
211268
211378
  };
211269
211379
  downloader.on("videoFileCreated", handleVideoCreated);
211270
- downloader.on("videoFileCompleted", ({ filename }) => {
211271
- this.emit("videoFileCompleted", { filename });
211380
+ downloader.on("videoFileCompleted", (data) => {
211381
+ this.emit("videoFileCompleted", data);
211272
211382
  });
211273
211383
  downloader.on("DebugLog", (data) => {
211274
211384
  this.emit("DebugLog", data);
@@ -211376,7 +211486,7 @@ const checkLiveStatusAndRecord$3 = async function ({ getSavePath, banLiveId, isM
211376
211486
  recorderType: downloader.type,
211377
211487
  url: stream.url,
211378
211488
  downloaderArgs,
211379
- savePath: savePath,
211489
+ savePath: downloader.videoFilePath,
211380
211490
  stop,
211381
211491
  cut,
211382
211492
  };
@@ -211413,12 +211523,6 @@ const provider$3 = {
211413
211523
  },
211414
211524
  };
211415
211525
 
211416
- function ensureFolderExist$2(fileOrFolderPath) {
211417
- const folder = path$y.dirname(fileOrFolderPath);
211418
- if (!fs$D.existsSync(folder)) {
211419
- fs$D.mkdirSync(folder, { recursive: true });
211420
- }
211421
- }
211422
211526
  function assert$j(assertion, msg) {
211423
211527
  if (!assertion) {
211424
211528
  throw new Error(msg);
@@ -212924,6 +213028,7 @@ const checkLiveStatusAndRecord$2 = async function ({ getSavePath, isManualStart,
212924
213028
  startTime: opts.startTime,
212925
213029
  liveStartTime: liveStartTime,
212926
213030
  recordStartTime,
213031
+ extraMs: opts.extraMs,
212927
213032
  }),
212928
213033
  formatName: streamOptions.format_name,
212929
213034
  disableDanma: this.disableProvideCommentsWhenRecording,
@@ -212936,20 +213041,6 @@ const checkLiveStatusAndRecord$2 = async function ({ getSavePath, isManualStart,
212936
213041
  const info = await getInfo$2(this.channelId);
212937
213042
  return info;
212938
213043
  });
212939
- const savePath = getSavePath({
212940
- owner,
212941
- title,
212942
- startTime: Date.now(),
212943
- liveStartTime: liveStartTime,
212944
- recordStartTime,
212945
- });
212946
- try {
212947
- ensureFolderExist$2(savePath);
212948
- }
212949
- catch (err) {
212950
- this.state = "idle";
212951
- throw err;
212952
- }
212953
213044
  const handleVideoCreated = async ({ filename, title, cover, rawFilename }) => {
212954
213045
  this.emit("videoFileCreated", { filename, cover, rawFilename });
212955
213046
  if (title && this?.liveInfo) {
@@ -212969,8 +213060,8 @@ const checkLiveStatusAndRecord$2 = async function ({ getSavePath, isManualStart,
212969
213060
  });
212970
213061
  };
212971
213062
  downloader.on("videoFileCreated", handleVideoCreated);
212972
- downloader.on("videoFileCompleted", ({ filename }) => {
212973
- this.emit("videoFileCompleted", { filename });
213063
+ downloader.on("videoFileCompleted", (data) => {
213064
+ this.emit("videoFileCompleted", data);
212974
213065
  });
212975
213066
  downloader.on("DebugLog", (data) => {
212976
213067
  this.emit("DebugLog", data);
@@ -213052,7 +213143,7 @@ const checkLiveStatusAndRecord$2 = async function ({ getSavePath, isManualStart,
213052
213143
  recorderType: downloader.type,
213053
213144
  url: stream.url,
213054
213145
  downloaderArgs,
213055
- savePath: savePath,
213146
+ savePath: downloader.videoFilePath,
213056
213147
  stop,
213057
213148
  cut,
213058
213149
  };
@@ -213109,12 +213200,6 @@ function singleton(fn) {
213109
213200
  return promise;
213110
213201
  };
213111
213202
  }
213112
- function ensureFolderExist$1(fileOrFolderPath) {
213113
- const folder = path$y.dirname(fileOrFolderPath);
213114
- if (!fs$D.existsSync(folder)) {
213115
- fs$D.mkdirSync(folder, { recursive: true });
213116
- }
213117
- }
213118
213203
  function assert$i(assertion, msg) {
213119
213204
  if (!assertion) {
213120
213205
  throw new Error(msg);
@@ -216585,6 +216670,9 @@ async function getRoomInfoByUserWeb(secUserId, opts = {}) {
216585
216670
  if (res.data.includes("验证码")) {
216586
216671
  throw new Error("需要验证码,请在浏览器中打开链接获取" + url);
216587
216672
  }
216673
+ if (!res.data.includes("抖音号")) {
216674
+ throw new Error("userHTML页面没有正常加载" + String(res.data));
216675
+ }
216588
216676
  if (!res.data.includes("直播中")) {
216589
216677
  return {
216590
216678
  living: false,
@@ -216776,6 +216864,7 @@ async function getRoomInfoByWeb(webRoomId, opts = {}) {
216776
216864
  }
216777
216865
  async function getRoomInfoByMobile(secUserId, opts = {}) {
216778
216866
  if (!secUserId) {
216867
+ console.error(opts);
216779
216868
  throw new Error("Mobile API need secUserId, please set uid field");
216780
216869
  }
216781
216870
  if (typeof secUserId === "number") {
@@ -216792,7 +216881,7 @@ async function getRoomInfoByMobile(secUserId, opts = {}) {
216792
216881
  const res = await requester.get(`https://webcast.amemv.com/webcast/room/reflow/info/`, {
216793
216882
  params,
216794
216883
  headers: {
216795
- cookie: opts.auth,
216884
+ // cookie: opts.auth,
216796
216885
  },
216797
216886
  });
216798
216887
  // @ts-ignore
@@ -249016,6 +249105,7 @@ const checkLiveStatusAndRecord$1 = async function ({ getSavePath, banLiveId, isM
249016
249105
  startTime: opts.startTime,
249017
249106
  liveStartTime: liveStartTime,
249018
249107
  recordStartTime,
249108
+ extraMs: opts.extraMs,
249019
249109
  }),
249020
249110
  disableDanma: this.disableProvideCommentsWhenRecording,
249021
249111
  videoFormat: this.videoFormat ?? "auto",
@@ -249030,20 +249120,6 @@ const checkLiveStatusAndRecord$1 = async function ({ getSavePath, banLiveId, isM
249030
249120
  });
249031
249121
  return info;
249032
249122
  });
249033
- const savePath = getSavePath({
249034
- owner,
249035
- title,
249036
- startTime: Date.now(),
249037
- liveStartTime,
249038
- recordStartTime,
249039
- });
249040
- try {
249041
- ensureFolderExist$1(savePath);
249042
- }
249043
- catch (err) {
249044
- this.state = "idle";
249045
- throw err;
249046
- }
249047
249123
  const handleVideoCreated = async ({ filename, title, cover, rawFilename }) => {
249048
249124
  this.emit("videoFileCreated", { filename, cover, rawFilename });
249049
249125
  if (title && this?.liveInfo) {
@@ -249063,8 +249139,8 @@ const checkLiveStatusAndRecord$1 = async function ({ getSavePath, banLiveId, isM
249063
249139
  });
249064
249140
  };
249065
249141
  downloader.on("videoFileCreated", handleVideoCreated);
249066
- downloader.on("videoFileCompleted", ({ filename }) => {
249067
- this.emit("videoFileCompleted", { filename });
249142
+ downloader.on("videoFileCompleted", (data) => {
249143
+ this.emit("videoFileCompleted", data);
249068
249144
  });
249069
249145
  downloader.on("DebugLog", (data) => {
249070
249146
  this.emit("DebugLog", data);
@@ -249280,7 +249356,7 @@ const checkLiveStatusAndRecord$1 = async function ({ getSavePath, banLiveId, isM
249280
249356
  recorderType: downloader.type,
249281
249357
  url: stream.url,
249282
249358
  downloaderArgs,
249283
- savePath: savePath,
249359
+ savePath: downloader.videoFilePath,
249284
249360
  stop,
249285
249361
  cut,
249286
249362
  };
@@ -290180,13 +290256,6 @@ async function getStream(opts) {
290180
290256
  };
290181
290257
  }
290182
290258
 
290183
- function ensureFolderExist(fileOrFolderPath) {
290184
- const folder = path$y.dirname(fileOrFolderPath);
290185
- if (!fs$D.existsSync(folder)) {
290186
- fs$D.mkdirSync(folder, { recursive: true });
290187
- }
290188
- }
290189
-
290190
290259
  function createRecorder(opts) {
290191
290260
  // 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过
290192
290261
  // 此标志来操作这个对象的地方,不然会跳过 proxy 的拦截。
@@ -290357,6 +290426,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
290357
290426
  startTime: opts.startTime,
290358
290427
  liveStartTime,
290359
290428
  recordStartTime,
290429
+ extraMs: opts.extraMs,
290360
290430
  }),
290361
290431
  disableDanma: this.disableProvideCommentsWhenRecording,
290362
290432
  videoFormat: this.videoFormat ?? "auto",
@@ -290365,20 +290435,6 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
290365
290435
  const info = await getInfo(roomId);
290366
290436
  return info;
290367
290437
  });
290368
- const savePath = getSavePath({
290369
- owner,
290370
- title,
290371
- startTime: Date.now(),
290372
- liveStartTime,
290373
- recordStartTime,
290374
- });
290375
- try {
290376
- ensureFolderExist(savePath);
290377
- }
290378
- catch (err) {
290379
- this.state = "idle";
290380
- throw err;
290381
- }
290382
290438
  const handleVideoCreated = async ({ filename, title, cover, rawFilename }) => {
290383
290439
  this.emit("videoFileCreated", { filename, cover, rawFilename });
290384
290440
  if (title && this?.liveInfo) {
@@ -290398,8 +290454,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
290398
290454
  });
290399
290455
  };
290400
290456
  downloader.on("videoFileCreated", handleVideoCreated);
290401
- downloader.on("videoFileCompleted", ({ filename }) => {
290402
- this.emit("videoFileCompleted", { filename });
290457
+ downloader.on("videoFileCompleted", (data) => {
290458
+ this.emit("videoFileCompleted", data);
290403
290459
  });
290404
290460
  downloader.on("DebugLog", (data) => {
290405
290461
  this.emit("DebugLog", data);
@@ -290445,7 +290501,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
290445
290501
  recorderType: downloader.type,
290446
290502
  url: stream.url,
290447
290503
  downloaderArgs,
290448
- savePath: savePath,
290504
+ savePath: downloader.videoFilePath,
290449
290505
  stop,
290450
290506
  cut,
290451
290507
  };
@@ -291047,7 +291103,7 @@ async function createRecorderManager(appConfig) {
291047
291103
  return;
291048
291104
  if (recorder.recordHandle) {
291049
291105
  const logFilePath = utils$2.replaceExtName(`${recorder.recordHandle.savePath}_${recorder.id}`, ".recorder.log");
291050
- fs$k.appendFileSync(logFilePath, log.text + "\n");
291106
+ fs$k.appendFile(logFilePath, log.text + "\n").catch(() => { });
291051
291107
  return;
291052
291108
  }
291053
291109
  else {
@@ -291126,8 +291182,8 @@ async function createRecorderManager(appConfig) {
291126
291182
  platform: recorder.providerId,
291127
291183
  });
291128
291184
  });
291129
- manager.on("videoFileCompleted", async ({ recorder, filename }) => {
291130
- logObj.info("Manager videoFileCompleted", { recorder, filename });
291185
+ manager.on("videoFileCompleted", async ({ recorder, filename, stats }) => {
291186
+ logObj.info("Manager videoFileCompleted", { recorder, filename, stats });
291131
291187
  const endTime = new Date();
291132
291188
  const data = recorderConfig.get(recorder.id);
291133
291189
  const title = recorder?.liveInfo?.title;
@@ -291158,7 +291214,16 @@ async function createRecorderManager(appConfig) {
291158
291214
  video_filename: videoFilename,
291159
291215
  quick_hash: quickHash,
291160
291216
  });
291161
- if (xmlFile && (await fs$k.pathExists(xmlFile))) {
291217
+ if (stats) {
291218
+ recordHistory.upadteLive({
291219
+ video_file: filename,
291220
+ live_id: liveId,
291221
+ }, {
291222
+ danma_num: stats.danmaNum,
291223
+ interact_num: stats.uniqMember,
291224
+ });
291225
+ }
291226
+ else if (xmlFile && (await fs$k.pathExists(xmlFile))) {
291162
291227
  const { uniqMember, danmaNum } = await danmaReport(xmlFile);
291163
291228
  recordHistory.upadteLive({
291164
291229
  video_file: filename,