@bililive-tools/manager 1.16.1 → 1.17.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.
@@ -34,6 +34,7 @@ export declare class BililiveDownloader extends EventEmitter implements IDownloa
34
34
  readonly disableDanma: boolean;
35
35
  readonly url: string;
36
36
  readonly debugLevel: "none" | "basic" | "verbose";
37
+ readonly proxy?: string;
37
38
  readonly headers: {
38
39
  [key: string]: string | undefined;
39
40
  } | undefined;
@@ -109,6 +109,7 @@ export class BililiveDownloader extends EventEmitter {
109
109
  disableDanma = false;
110
110
  url;
111
111
  debugLevel = "none";
112
+ proxy;
112
113
  headers;
113
114
  constructor(opts, onEnd, onUpdateLiveInfo) {
114
115
  super();
@@ -119,6 +120,7 @@ export class BililiveDownloader extends EventEmitter {
119
120
  this.hasSegment = hasSegment;
120
121
  this.disableDanma = opts.disableDanma ?? false;
121
122
  this.debugLevel = opts.debugLevel ?? "none";
123
+ this.proxy = opts.proxy;
122
124
  let videoFormat = "flv";
123
125
  this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "bililive", videoFormat, {
124
126
  onUpdateLiveInfo: this.onUpdateLiveInfo,
@@ -153,6 +155,9 @@ export class BililiveDownloader extends EventEmitter {
153
155
  if (this.debugLevel === "verbose") {
154
156
  inputOptions.push("-l", "Debug");
155
157
  }
158
+ if (this.proxy) {
159
+ inputOptions.push(...["--proxy", this.proxy]);
160
+ }
156
161
  if (this.headers) {
157
162
  Object.entries(this.headers).forEach(([key, value]) => {
158
163
  if (!value)
@@ -23,6 +23,7 @@ export declare class FFmpegDownloader extends EventEmitter implements IDownloade
23
23
  formatName: FormatName;
24
24
  videoFormat: VideoFormat;
25
25
  readonly debugLevel: "none" | "basic" | "verbose";
26
+ readonly proxy?: string;
26
27
  readonly headers: {
27
28
  [key: string]: string | undefined;
28
29
  } | undefined;
@@ -20,6 +20,7 @@ export class FFmpegDownloader extends EventEmitter {
20
20
  formatName;
21
21
  videoFormat;
22
22
  debugLevel = "none";
23
+ proxy;
23
24
  headers;
24
25
  constructor(opts, onEnd, onUpdateLiveInfo) {
25
26
  super();
@@ -63,6 +64,7 @@ export class FFmpegDownloader extends EventEmitter {
63
64
  this.url = opts.url;
64
65
  this.segment = opts.segment;
65
66
  this.headers = opts.headers;
67
+ this.proxy = opts.proxy;
66
68
  this.command = this.createCommand();
67
69
  this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
68
70
  this.emit("videoFileCreated", { filename, cover, rawFilename, title });
@@ -86,6 +88,9 @@ export class FFmpegDownloader extends EventEmitter {
86
88
  if (this.isHls) {
87
89
  inputOptions.push(...["-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "3"]);
88
90
  }
91
+ if (this.proxy) {
92
+ inputOptions.push(...["-http_proxy", this.proxy]);
93
+ }
89
94
  if (this.debugLevel === "verbose") {
90
95
  inputOptions.push("-loglevel", "debug");
91
96
  }
@@ -23,6 +23,7 @@ export interface BaseRecorderOptions {
23
23
  [key: string]: string | undefined;
24
24
  };
25
25
  videoFormat?: VideoFormat;
26
+ proxy?: string;
26
27
  }
27
28
  /**
28
29
  * 录制器接口定义
@@ -37,6 +38,7 @@ export interface IDownloader extends EventEmitter {
37
38
  readonly headers: {
38
39
  [key: string]: string | undefined;
39
40
  } | undefined;
41
+ readonly proxy?: string;
40
42
  readonly getSavePath: (data: {
41
43
  startTime: number;
42
44
  title?: string;
@@ -31,6 +31,7 @@ export declare class mesioDownloader extends EventEmitter implements IDownloader
31
31
  readonly disableDanma: boolean;
32
32
  readonly url: string;
33
33
  readonly debugLevel: "none" | "basic" | "verbose";
34
+ readonly proxy?: string;
34
35
  readonly headers: {
35
36
  [key: string]: string | undefined;
36
37
  } | undefined;
@@ -99,6 +99,7 @@ export class mesioDownloader extends EventEmitter {
99
99
  disableDanma = false;
100
100
  url;
101
101
  debugLevel = "none";
102
+ proxy;
102
103
  headers;
103
104
  constructor(opts, onEnd, onUpdateLiveInfo) {
104
105
  super();
@@ -109,6 +110,7 @@ export class mesioDownloader extends EventEmitter {
109
110
  this.hasSegment = hasSegment;
110
111
  this.disableDanma = opts.disableDanma ?? false;
111
112
  this.debugLevel = opts.debugLevel ?? "none";
113
+ this.proxy = opts.proxy;
112
114
  let videoFormat = "flv";
113
115
  if (opts.url.includes(".m3u8")) {
114
116
  videoFormat = "ts";
@@ -145,7 +147,29 @@ export class mesioDownloader extends EventEmitter {
145
147
  });
146
148
  }
147
149
  createCommand() {
148
- const inputOptions = [...this.inputOptions, "--fix", "--no-proxy"];
150
+ const inputOptions = [...this.inputOptions, "--fix", "--disable-log-file"];
151
+ if (this.proxy) {
152
+ try {
153
+ const { protocol, username, password, origin, search, pathname } = new URL(this.proxy);
154
+ if (protocol.startsWith("https")) {
155
+ inputOptions.push("--proxy-type", "https");
156
+ }
157
+ else if (protocol.startsWith("socks5")) {
158
+ inputOptions.push("--proxy-type", "socks5");
159
+ }
160
+ if (username && password) {
161
+ inputOptions.push("--proxy-user", `${username}`);
162
+ inputOptions.push("--proxy-pass", `${password}`);
163
+ }
164
+ inputOptions.push("--proxy", origin + pathname + search);
165
+ }
166
+ catch (err) {
167
+ this.emit("DebugLog", { type: "error", text: `Invalid proxy URL: ${this.proxy}` });
168
+ }
169
+ }
170
+ else {
171
+ inputOptions.push("--no-proxy");
172
+ }
149
173
  if (this.debugLevel === "verbose") {
150
174
  inputOptions.push("-v");
151
175
  }
@@ -23,7 +23,12 @@ export declare class Segment extends EventEmitter {
23
23
  outputVideoFilePath: string;
24
24
  disableDanma: boolean;
25
25
  videoExt: TrueVideoFormat;
26
- constructor(getSavePath: GetSavePath, disableDanma: boolean, videoExt: TrueVideoFormat);
26
+ options?: {
27
+ firstStartTime: number;
28
+ };
29
+ constructor(getSavePath: GetSavePath, disableDanma: boolean, videoExt: TrueVideoFormat, options?: {
30
+ firstStartTime: number;
31
+ });
27
32
  private getVideoFileCompletedPayload;
28
33
  handleSegmentEnd(): Promise<void>;
29
34
  onSegmentStart(stderrLine: string, callBack?: {
@@ -13,11 +13,13 @@ export class Segment extends EventEmitter {
13
13
  outputVideoFilePath;
14
14
  disableDanma;
15
15
  videoExt;
16
- constructor(getSavePath, disableDanma, videoExt) {
16
+ options;
17
+ constructor(getSavePath, disableDanma, videoExt, options) {
17
18
  super();
18
19
  this.getSavePath = getSavePath;
19
20
  this.disableDanma = disableDanma;
20
21
  this.videoExt = videoExt;
22
+ this.options = options;
21
23
  }
22
24
  getVideoFileCompletedPayload() {
23
25
  return {
@@ -58,11 +60,13 @@ export class Segment extends EventEmitter {
58
60
  if (!this.init) {
59
61
  await this.handleSegmentEnd();
60
62
  }
63
+ // 首次创建使用上次的时间戳,后续创建使用当前时间戳
64
+ const startTime = this.init ? (this.options?.firstStartTime ?? Date.now()) : Date.now();
61
65
  this.init = false;
62
- const startTime = Date.now();
63
66
  let liveInfo = { title: "", cover: "" };
64
67
  if (callBack?.onUpdateLiveInfo) {
65
68
  try {
69
+ // TODO:这里存在bug,当调用onUpdateLiveInfo并在等待时,handleSegmentEnd被调用,那么会造成竞态导致数据错误,后续需要优化,需要保存segment状态
66
70
  liveInfo = await callBack.onUpdateLiveInfo();
67
71
  }
68
72
  catch (err) {
@@ -143,7 +147,9 @@ export class StreamManager extends EventEmitter {
143
147
  }
144
148
  this.recordSavePath = recordSavePath;
145
149
  if (hasSegment) {
146
- this.segment = new Segment(getSavePath, disableDanma, this.videoExt);
150
+ this.segment = new Segment(getSavePath, disableDanma, this.videoExt, {
151
+ firstStartTime: startTime,
152
+ });
147
153
  this.segment.on("DebugLog", (data) => {
148
154
  this.emit("DebugLog", data);
149
155
  });
package/lib/index.js CHANGED
@@ -32,6 +32,7 @@ export function defaultToJSON(provider, recorder) {
32
32
  "segment",
33
33
  "saveSCDanma",
34
34
  "saveCover",
35
+ "convert2Mp4",
35
36
  "saveGiftDanma",
36
37
  "disableProvideCommentsWhenRecording",
37
38
  "liveInfo",
package/lib/manager.js CHANGED
@@ -7,6 +7,45 @@ import { RecorderCacheImpl, MemoryCacheStore } from "./cache.js";
7
7
  import { getBiliStatusInfoByRoomIds } from "./api.js";
8
8
  import { formatDate, removeSystemReservedChars, formatTemplate, replaceExtName, downloadImage, isBetweenTimeRange, sleep, } from "./utils.js";
9
9
  import { StreamManager } from "./downloader/streamManager.js";
10
+ const RECORDER_TIMELINE_LIMIT = 40;
11
+ function normalizeTimelineText(text) {
12
+ return text.trim();
13
+ }
14
+ function appendRecorderTimeline(recorder, args) {
15
+ const text = normalizeTimelineText(args.text);
16
+ if (!text) {
17
+ return recorder.timeline ?? [];
18
+ }
19
+ const startTime = args.startTime ?? Date.now();
20
+ const timeline = recorder.timeline ?? [];
21
+ const lastItem = timeline[timeline.length - 1];
22
+ if (lastItem && lastItem.text === text) {
23
+ lastItem.endTime = Math.max(lastItem.endTime ?? lastItem.startTime, startTime);
24
+ recorder.timeline = [...timeline];
25
+ return recorder.timeline;
26
+ }
27
+ const nextTimeline = [...timeline, { startTime, text }].slice(-RECORDER_TIMELINE_LIMIT);
28
+ recorder.timeline = nextTimeline;
29
+ return nextTimeline;
30
+ }
31
+ function ensureRecorderTimeline(recorder) {
32
+ recorder.timeline = Array.isArray(recorder.timeline) ? recorder.timeline : [];
33
+ recorder.appendTimeline = (args) => appendRecorderTimeline(recorder, args);
34
+ }
35
+ function getStateTimelineText(state, msg) {
36
+ const text = normalizeTimelineText(msg ?? "");
37
+ if (text) {
38
+ return text;
39
+ }
40
+ const stateTextMap = {
41
+ idle: "空闲中",
42
+ recording: "录制中",
43
+ "stopping-record": "停止录制中",
44
+ "check-error": "检查失败",
45
+ "title-blocked": "标题命中过滤规则",
46
+ };
47
+ return stateTextMap[state];
48
+ }
10
49
  const configurableProps = [
11
50
  "savePathRule",
12
51
  "autoRemoveSystemReservedChars",
@@ -25,7 +64,7 @@ export function createRecorderManager(opts) {
25
64
  const recorders = [];
26
65
  // 存储每个 provider 的 timer,key 为 providerId
27
66
  const checkLoopTimers = new Map();
28
- const multiThreadCheck = async (manager, providerId) => {
67
+ const multiThreadCheck = async (manager, providerId, providerConfig) => {
29
68
  const handleBatchQuery = async (obj) => {
30
69
  for (const recorder of recorders
31
70
  .filter((r) => !r.disableAutoCheck)
@@ -48,7 +87,6 @@ export function createRecorderManager(opts) {
48
87
  .filter((r) => !r.disableAutoCheck)
49
88
  .filter((r) => isBetweenTimeRange(r.handleTime))
50
89
  .filter((r) => r.providerId === providerId);
51
- const providerConfig = manager.getProviderCheckConfig(providerId);
52
90
  const threads = [];
53
91
  // Bilibili 批量查询特殊处理
54
92
  if (providerId === "Bilibili" && manager.biliBatchQuery) {
@@ -87,13 +125,15 @@ export function createRecorderManager(opts) {
87
125
  while (needCheckRecorders.length > 0) {
88
126
  try {
89
127
  await checkOnce();
90
- if (providerConfig.waitTime > 0) {
91
- await sleep(providerConfig.waitTime);
92
- }
93
128
  }
94
129
  catch (err) {
95
130
  manager.emit("error", { source: "checkOnceInThread", err });
96
131
  }
132
+ finally {
133
+ if (providerConfig.waitTime > 0) {
134
+ await sleep(providerConfig.waitTime);
135
+ }
136
+ }
97
137
  }
98
138
  }));
99
139
  await Promise.all(threads);
@@ -124,21 +164,42 @@ export function createRecorderManager(opts) {
124
164
  });
125
165
  // 为录制器注入独立的缓存命名空间
126
166
  recorder.cache = this.cache.createNamespace(recorder.id);
167
+ ensureRecorderTimeline(recorder);
127
168
  this.recorders.push(recorder);
128
- recorder.on("RecordStart", (recordHandle) => this.emit("RecordStart", { recorder: recorder.toJSON(), recordHandle }));
169
+ recorder.on("RecordStart", (recordHandle) => {
170
+ recorder.appendTimeline?.({
171
+ startTime: recorder.liveInfo?.recordStartTime?.getTime() ?? Date.now(),
172
+ text: `开始录制:${recordHandle.stream}/${recordHandle.source}/${recordHandle.recorderType}/${recordHandle.url}`,
173
+ });
174
+ this.emit("RecordStart", { recorder: recorder.toJSON(), recordHandle });
175
+ });
129
176
  recorder.on("RecordSegment", (recordHandle) => this.emit("RecordSegment", { recorder: recorder.toJSON(), recordHandle }));
130
177
  recorder.on("videoFileCreated", ({ filename, cover, rawFilename }) => {
131
178
  if (recorder.saveCover && recorder?.liveInfo?.cover) {
132
179
  const coverPath = replaceExtName(filename, ".jpg");
133
180
  downloadImage(cover ?? recorder?.liveInfo?.cover, coverPath);
134
181
  }
182
+ recorder.appendTimeline?.({
183
+ startTime: Date.now(),
184
+ text: `视频文件创建:${filename}`,
185
+ });
135
186
  this.emit("videoFileCreated", { recorder: recorder.toJSON(), filename, rawFilename });
136
187
  });
137
- recorder.on("videoFileCompleted", ({ filename, stats }) => this.emit("videoFileCompleted", { recorder: recorder.toJSON(), filename, stats }));
188
+ recorder.on("videoFileCompleted", ({ filename, stats }) => {
189
+ recorder.appendTimeline?.({
190
+ startTime: Date.now(),
191
+ text: `视频文件创建完成:${filename}`,
192
+ });
193
+ this.emit("videoFileCompleted", { recorder: recorder.toJSON(), filename, stats });
194
+ });
138
195
  recorder.on("Message", (message) => this.emit("Message", { recorder: recorder.toJSON(), message }));
139
196
  recorder.on("Updated", (keys) => this.emit("RecorderUpdated", { recorder: recorder.toJSON(), keys }));
140
197
  recorder.on("DebugLog", (log) => this.emit("RecorderDebugLog", { recorder: recorder, ...log }));
141
198
  recorder.on("RecordStop", ({ recordHandle, reason }) => {
199
+ recorder.appendTimeline?.({
200
+ startTime: Date.now(),
201
+ text: reason ? `停止录制:${reason}` : "停止录制",
202
+ });
142
203
  this.emit("RecordStop", { recorder: recorder.toJSON(), recordHandle, reason });
143
204
  const maxRetryCount = 10;
144
205
  // 默认策略下,如果录制被中断,那么会在下一个检查周期时重新检查直播状态并重新开始录制,这种策略的问题就是一部分时间会被漏掉。
@@ -167,6 +228,10 @@ export function createRecorderManager(opts) {
167
228
  type: "common",
168
229
  text: `录制${recorder.channelId}中断,立即触发重试(${retryCountObj[key]}/${maxRetryCount})`,
169
230
  });
231
+ recorder.appendTimeline?.({
232
+ startTime: Date.now(),
233
+ text: `录制中断,立即触发重试(${retryCountObj[key]}/${maxRetryCount})`,
234
+ });
170
235
  // 触发一次检查,等待一秒使状态清理完毕
171
236
  setTimeout(() => {
172
237
  recorder.checkLiveStatusAndRecord({
@@ -189,6 +254,15 @@ export function createRecorderManager(opts) {
189
254
  liveStartObj[key] = true;
190
255
  this.emit("RecoderLiveStart", { recorder: recorder });
191
256
  });
257
+ recorder.on("stateChange", ({ state, msg }) => {
258
+ recorder.state = state;
259
+ if (state !== "idle") {
260
+ recorder.appendTimeline?.({
261
+ startTime: Date.now(),
262
+ text: `状态变更:${getStateTimelineText(state, msg)}`,
263
+ });
264
+ }
265
+ });
192
266
  this.emit("RecorderAdded", recorder.toJSON());
193
267
  // startCheckLoop 会为所有注册的 provider 启动检查循环,无需在此处额外处理
194
268
  return recorder;
@@ -260,11 +334,11 @@ export function createRecorderManager(opts) {
260
334
  // TODO: emit updated event
261
335
  // 为每个 provider 创建独立的检查循环
262
336
  const startProviderCheckLoop = (providerId) => {
263
- const providerConfig = this.getProviderCheckConfig(providerId);
264
337
  const checkLoop = async () => {
338
+ const providerConfig = this.getProviderCheckConfig(providerId);
265
339
  try {
266
340
  // 只检查当前 provider 的 recorders
267
- await multiThreadCheck(this, providerId);
341
+ await multiThreadCheck(this, providerId, providerConfig);
268
342
  }
269
343
  catch (err) {
270
344
  this.emit("error", { source: "multiThreadCheck", err });
package/lib/recorder.d.ts CHANGED
@@ -25,6 +25,8 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
25
25
  saveSCDanma?: boolean;
26
26
  /** 保存封面 */
27
27
  saveCover?: boolean;
28
+ /** 转封装为 mp4 */
29
+ convert2Mp4?: boolean;
28
30
  /** 身份验证 */
29
31
  auth?: string;
30
32
  /** cookie所有者uid,B站弹幕录制 */
@@ -44,7 +46,7 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
44
46
  /** 流编码 */
45
47
  codecName?: CodecName;
46
48
  /** 选择使用的api,虎牙支持: auto,web,mp,wup,抖音支持:web,webHTML,mobile,userHTML */
47
- api?: "auto" | "web" | "mp" | "wup" | "webHTML" | "mobile" | "userHTML" | "balance" | "random" | string;
49
+ api?: "auto" | "web" | "mp" | "wup" | "webHTML" | "mobile" | "userHTML" | "balance" | "random" | "newAPI" | "oldAPI" | string;
48
50
  /** 标题关键词,如果直播间标题包含这些关键词,则不会自动录制,支持两种格式:
49
51
  * 1. 逗号分隔的关键词:'回放,录播,重播'
50
52
  * 2. 正则表达式:'/pattern/flags'(如:'/回放|录播/i')
@@ -63,15 +65,26 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
63
65
  /** 控制弹幕是否使用服务端时间戳 */
64
66
  useServerTimestamp?: boolean;
65
67
  extra?: Partial<E>;
68
+ /** 代理 如http://127.0.0.1:7890 */
69
+ proxy?: string;
66
70
  /** 调试等级 */
67
71
  debugLevel?: "none" | "basic" | "verbose";
68
72
  }
69
- export type SerializedRecorder<E extends AnyObject> = PickRequired<RecorderCreateOpts<E>, "id"> & Pick<Recorder<E>, "id" | "channelId" | "remarks" | "disableAutoCheck" | "quality" | "streamPriorities" | "sourcePriorities" | "extra" | "segment" | "saveSCDanma" | "saveCover" | "saveGiftDanma" | "disableProvideCommentsWhenRecording" | "liveInfo" | "uid" | "titleKeywords">;
73
+ export type SerializedRecorder<E extends AnyObject> = PickRequired<RecorderCreateOpts<E>, "id"> & Pick<Recorder<E>, "id" | "channelId" | "remarks" | "disableAutoCheck" | "quality" | "streamPriorities" | "sourcePriorities" | "extra" | "segment" | "saveSCDanma" | "saveCover" | "convert2Mp4" | "saveGiftDanma" | "disableProvideCommentsWhenRecording" | "liveInfo" | "uid" | "titleKeywords">;
70
74
  /** 录制状态,idle: 空闲中,recording: 录制中,stopping-record: 停止录制中,check-error: 检查错误,title-blocked: 标题黑名单 */
71
75
  export type RecorderState = "idle" | "recording" | "stopping-record" | "check-error" | "title-blocked";
72
76
  export type Progress = {
73
77
  time: string | null;
74
78
  };
79
+ export interface RecorderTimelineItem {
80
+ startTime: number;
81
+ text: string;
82
+ endTime?: number;
83
+ }
84
+ export interface AppendRecorderTimelineArgs {
85
+ startTime?: number;
86
+ text: string;
87
+ }
75
88
  export interface RecordHandle {
76
89
  id: string;
77
90
  stream: string;
@@ -104,6 +117,10 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
104
117
  cover?: string;
105
118
  rawFilename?: string;
106
119
  };
120
+ stateChange: {
121
+ state: RecorderState;
122
+ msg?: string;
123
+ };
107
124
  videoFileCompleted: {
108
125
  filename: string;
109
126
  stats?: XmlStreamStats;
@@ -138,6 +155,8 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
138
155
  area?: string;
139
156
  };
140
157
  tempStopIntervalCheck?: boolean;
158
+ timeline?: RecorderTimelineItem[];
159
+ appendTimeline: (args: AppendRecorderTimelineArgs) => RecorderTimelineItem[];
141
160
  /** 缓存实例(命名空间) */
142
161
  cache: NamespacedCache;
143
162
  getChannelURL: (this: Recorder<E>) => string;
package/lib/utils.js CHANGED
@@ -460,10 +460,13 @@ export async function checkTitleKeywordsWhileRecording(recorder, isManualStart,
460
460
  const { title } = liveInfo;
461
461
  // 检查标题是否包含关键词
462
462
  if (hasBlockedTitleKeywords(title, recorder.titleKeywords)) {
463
- recorder.state = "title-blocked";
463
+ recorder.emit("stateChange", {
464
+ state: "title-blocked",
465
+ msg: `停止录制,直播间标题 "${title}" 包含关键词 "${recorder.titleKeywords}"`,
466
+ });
464
467
  recorder.emit("DebugLog", {
465
468
  type: "common",
466
- text: `检测到标题包含关键词,停止录制:直播间标题 "${title}" 包含关键词 "${recorder.titleKeywords}"`,
469
+ text: `停止录制,直播间标题 "${title}" 包含关键词 "${recorder.titleKeywords}"`,
467
470
  });
468
471
  // 停止录制
469
472
  await recorder?.recordHandle?.stop("直播间标题包含关键词");
@@ -485,10 +488,9 @@ export function checkTitleKeywordsBeforeRecord(title, recorder, isManualStart) {
485
488
  return false;
486
489
  }
487
490
  if (hasBlockedTitleKeywords(title, recorder.titleKeywords)) {
488
- recorder.state = "title-blocked";
489
- recorder.emit("DebugLog", {
490
- type: "common",
491
- text: `跳过录制:直播间标题 "${title}" 包含关键词 "${recorder.titleKeywords}"`,
491
+ recorder.emit("stateChange", {
492
+ state: "title-blocked",
493
+ msg: `跳过录制,直播间标题 "${title}" 包含关键词 "${recorder.titleKeywords}"`,
492
494
  });
493
495
  return true;
494
496
  }
@@ -102,6 +102,9 @@ export function createRecordExtraDataController(savePath) {
102
102
  // 清理内存
103
103
  data.pendingMessages = [];
104
104
  interactedUsers.clear();
105
+ danmaNum = 0;
106
+ scNum = 0;
107
+ guardNum = 0;
105
108
  }
106
109
  };
107
110
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/manager",
3
- "version": "1.16.1",
3
+ "version": "1.17.0",
4
4
  "description": "Batch scheduling recorders",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",