@bililive-tools/manager 1.13.0 → 1.14.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.
package/README.md CHANGED
@@ -12,12 +12,13 @@
12
12
 
13
13
  ## 支持的平台
14
14
 
15
- | 平台 | 包名 |
16
- | ---- | ----------------------------------- |
17
- | B站 | `@bililive-tools/bilibili-recorder` |
18
- | 斗鱼 | `@bililive-tools/douyu-recorder` |
19
- | 虎牙 | `@bililive-tools/huya-recorder` |
20
- | 抖音 | `@bililive-tools/douyin-recorder` |
15
+ | 平台 | 包名 |
16
+ | ------ | ----------------------------------- |
17
+ | B站 | `@bililive-tools/bilibili-recorder` |
18
+ | 斗鱼 | `@bililive-tools/douyu-recorder` |
19
+ | 虎牙 | `@bililive-tools/huya-recorder` |
20
+ | 抖音 | `@bililive-tools/douyin-recorder` |
21
+ | 小红书 | `@bililive-tools/xhs-recorder` |
21
22
 
22
23
  # 使用
23
24
 
@@ -32,9 +33,17 @@ const manager = createRecorderManager({
32
33
  savePathRule: "D:\\录制\\{platforme}}/{owner}/{year}-{month}-{date} {hour}-{min}-{sec} {title}", // 保存路径,占位符见文档,支持 [ejs](https://ejs.co/) 模板引擎
33
34
  autoCheckInterval: 1000 * 60, // 自动检查间隔,单位秒
34
35
  maxThreadCount: 3, // 检查并发数
35
- waitTime: 0, // 检查后等待时间
36
+ waitTime: 0, // 检查后等待时间,单位毫秒
36
37
  autoRemoveSystemReservedChars: true, // 移除系统非法字符串
37
38
  biliBatchQuery: false, // B站检查使用批量接口
39
+ providerCheckConfig: {
40
+ // Bilibili 配置:高频检查,更多线程
41
+ Bilibili: {
42
+ autoCheckInterval: 5000, // 5秒检查一次
43
+ maxThreadCount: 5, // 最多5个并发线程
44
+ waitTime: 500, // 每次检查间隔0.5秒
45
+ },
46
+ },
38
47
  });
39
48
 
40
49
  // 不同provider支持的参数不尽相同,具体见相关文档
package/lib/manager.d.ts CHANGED
@@ -4,6 +4,14 @@ import { RecorderCache } from "./cache.js";
4
4
  import { RecorderCreateOpts, Recorder, SerializedRecorder, RecordHandle, DebugLog, Progress } from "./recorder.js";
5
5
  import { AnyObject, UnknownObject } from "./utils.js";
6
6
  import { StreamManager } from "./downloader/streamManager.js";
7
+ export interface ProviderCheckConfig {
8
+ /** 检查循环间隔(毫秒) */
9
+ autoCheckInterval?: number;
10
+ /** 最大并发检查线程数 */
11
+ maxThreadCount?: number;
12
+ /** 每次检查之间的等待时间(毫秒) */
13
+ waitTime?: number;
14
+ }
7
15
  export interface RecorderProvider<E extends AnyObject> {
8
16
  id: string;
9
17
  name: string;
@@ -20,7 +28,7 @@ export interface RecorderProvider<E extends AnyObject> {
20
28
  fromJSON: <T extends SerializedRecorder<E>>(this: RecorderProvider<E>, json: T) => Recorder<E>;
21
29
  setFFMPEGOutputArgs: (this: RecorderProvider<E>, args: string[]) => void;
22
30
  }
23
- declare const configurableProps: readonly ["savePathRule", "autoRemoveSystemReservedChars", "autoCheckInterval", "maxThreadCount", "waitTime", "ffmpegOutputArgs", "biliBatchQuery", "recordRetryImmediately"];
31
+ declare const configurableProps: readonly ["savePathRule", "autoRemoveSystemReservedChars", "autoCheckInterval", "maxThreadCount", "waitTime", "ffmpegOutputArgs", "biliBatchQuery", "recordRetryImmediately", "providerCheckConfig"];
24
32
  type ConfigurableProp = (typeof configurableProps)[number];
25
33
  export interface RecorderManager<ME extends UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE> extends Emitter<{
26
34
  error: {
@@ -98,11 +106,17 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
98
106
  recordRetryImmediately: boolean;
99
107
  /** 缓存系统 */
100
108
  cache: RecorderCache;
109
+ /** 每个 provider 的检查配置 */
110
+ providerCheckConfig: Record<string, ProviderCheckConfig>;
111
+ /** 获取指定 provider 的检查配置(自动 fallback 到全局配置) */
112
+ getProviderCheckConfig: (this: RecorderManager<ME, P, PE, E>, providerId: string) => Required<ProviderCheckConfig>;
101
113
  }
102
114
  export type RecorderManagerCreateOpts<ME extends AnyObject = UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE> = Partial<Pick<RecorderManager<ME, P, PE, E>, ConfigurableProp>> & {
103
115
  providers: P[];
104
116
  /** 自定义缓存实现,不提供则使用默认的内存缓存 */
105
117
  cache?: RecorderCache;
118
+ /** 每个 provider 的检查配置,key 为 provider.id */
119
+ providerCheckConfig?: Record<string, ProviderCheckConfig>;
106
120
  };
107
121
  export declare function createRecorderManager<ME extends AnyObject = UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE>(opts: RecorderManagerCreateOpts<ME, P, PE, E>): RecorderManager<ME, P, PE, E>;
108
122
  export declare function genSavePathFromRule<ME extends AnyObject, P extends RecorderProvider<AnyObject>, PE extends AnyObject, E extends AnyObject>(manager: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>, extData: {
package/lib/manager.js CHANGED
@@ -16,14 +16,16 @@ const configurableProps = [
16
16
  "ffmpegOutputArgs",
17
17
  "biliBatchQuery",
18
18
  "recordRetryImmediately",
19
+ "providerCheckConfig",
19
20
  ];
20
21
  function isConfigurableProp(prop) {
21
22
  return configurableProps.includes(prop);
22
23
  }
23
24
  export function createRecorderManager(opts) {
24
25
  const recorders = [];
25
- let checkLoopTimer;
26
- const multiThreadCheck = async (manager) => {
26
+ // 存储每个 provider 的 timer,key 为 providerId
27
+ const checkLoopTimers = new Map();
28
+ const multiThreadCheck = async (manager, providerId) => {
27
29
  const handleBatchQuery = async (obj) => {
28
30
  for (const recorder of recorders
29
31
  .filter((r) => !r.disableAutoCheck)
@@ -42,15 +44,16 @@ export function createRecorderManager(opts) {
42
44
  };
43
45
  // 这里暂时不打算用 state == recording 来过滤,provider 必须内部自己处理录制过程中的 check,
44
46
  // 这样可以防止一些意外调用 checkLiveStatusAndRecord 时出现重复录制。
45
- let needCheckRecorders = recorders
47
+ const needCheckRecorders = recorders
46
48
  .filter((r) => !r.disableAutoCheck)
47
- .filter((r) => isBetweenTimeRange(r.handleTime));
48
- let threads = [];
49
- if (manager.biliBatchQuery) {
50
- const biliNeedCheckRecorders = needCheckRecorders
51
- .filter((r) => r.providerId === "Bilibili")
52
- .filter((r) => r.recordHandle == null);
53
- needCheckRecorders = needCheckRecorders.filter((r) => r.providerId !== "Bilibili");
49
+ .filter((r) => isBetweenTimeRange(r.handleTime))
50
+ .filter((r) => r.providerId === providerId);
51
+ const providerConfig = manager.getProviderCheckConfig(providerId);
52
+ const threads = [];
53
+ // Bilibili 批量查询特殊处理
54
+ if (providerId === "Bilibili" && manager.biliBatchQuery) {
55
+ const biliNeedCheckRecorders = needCheckRecorders.filter((r) => r.recordHandle == null);
56
+ // const biliRecordingRecorders = needCheckRecorders.filter((r) => r.recordHandle != null);
54
57
  const roomIds = biliNeedCheckRecorders.map((r) => r.channelId).map(Number);
55
58
  try {
56
59
  if (roomIds.length !== 0) {
@@ -61,9 +64,13 @@ export function createRecorderManager(opts) {
61
64
  catch (err) {
62
65
  manager.emit("error", { source: "getBiliStatusInfoByRoomIds", err });
63
66
  // 如果批量查询失败,则使用单个查询
64
- needCheckRecorders = needCheckRecorders.concat(biliNeedCheckRecorders);
67
+ needCheckRecorders.push(...biliNeedCheckRecorders);
65
68
  }
69
+ // 正在录制的也需要检查(放回队列)
70
+ // needCheckRecorders.length = 0;
71
+ // needCheckRecorders.push(...biliRecordingRecorders);
66
72
  }
73
+ // 为当前 provider 创建线程池
67
74
  const checkOnce = async () => {
68
75
  const recorder = needCheckRecorders.shift();
69
76
  if (recorder == null)
@@ -76,12 +83,12 @@ export function createRecorderManager(opts) {
76
83
  banLiveId,
77
84
  });
78
85
  };
79
- threads = threads.concat(range(0, manager.maxThreadCount).map(async () => {
86
+ threads.push(...range(0, providerConfig.maxThreadCount).map(async () => {
80
87
  while (needCheckRecorders.length > 0) {
81
88
  try {
82
89
  await checkOnce();
83
- if (manager.waitTime > 0) {
84
- await sleep(manager.waitTime);
90
+ if (providerConfig.waitTime > 0) {
91
+ await sleep(providerConfig.waitTime);
85
92
  }
86
93
  }
87
94
  catch (err) {
@@ -183,6 +190,7 @@ export function createRecorderManager(opts) {
183
190
  this.emit("RecoderLiveStart", { recorder: recorder });
184
191
  });
185
192
  this.emit("RecorderAdded", recorder.toJSON());
193
+ // startCheckLoop 会为所有注册的 provider 启动检查循环,无需在此处额外处理
186
194
  return recorder;
187
195
  },
188
196
  removeRecorder(recorder) {
@@ -241,7 +249,7 @@ export function createRecorderManager(opts) {
241
249
  await recorder.recordHandle.cut();
242
250
  return recorder;
243
251
  },
244
- autoCheckInterval: opts.autoCheckInterval ?? 1000,
252
+ autoCheckInterval: opts.autoCheckInterval ?? 60000,
245
253
  maxThreadCount: opts.maxThreadCount ?? 3,
246
254
  waitTime: opts.waitTime ?? 0,
247
255
  isCheckLoopRunning: false,
@@ -250,30 +258,60 @@ export function createRecorderManager(opts) {
250
258
  return;
251
259
  this.isCheckLoopRunning = true;
252
260
  // TODO: emit updated event
253
- const checkLoop = async () => {
254
- try {
255
- await multiThreadCheck(this);
256
- }
257
- catch (err) {
258
- this.emit("error", { source: "multiThreadCheck", err });
259
- }
260
- finally {
261
- if (!this.isCheckLoopRunning) {
262
- // do nothing
261
+ // 为每个 provider 创建独立的检查循环
262
+ const startProviderCheckLoop = (providerId) => {
263
+ const providerConfig = this.getProviderCheckConfig(providerId);
264
+ const checkLoop = async () => {
265
+ try {
266
+ // 只检查当前 provider recorders
267
+ await multiThreadCheck(this, providerId);
263
268
  }
264
- else {
265
- checkLoopTimer = setTimeout(checkLoop, this.autoCheckInterval);
269
+ catch (err) {
270
+ this.emit("error", { source: "multiThreadCheck", err });
266
271
  }
267
- }
272
+ finally {
273
+ if (!this.isCheckLoopRunning) {
274
+ // 停止了,清理 timer
275
+ const timer = checkLoopTimers.get(providerId);
276
+ if (timer) {
277
+ clearTimeout(timer);
278
+ checkLoopTimers.delete(providerId);
279
+ }
280
+ }
281
+ else {
282
+ // 检查该 provider 是否还有 recorder
283
+ const hasRecorders = this.recorders.some((r) => r.providerId === providerId);
284
+ if (hasRecorders) {
285
+ // 继续循环
286
+ const timer = setTimeout(checkLoop, providerConfig.autoCheckInterval);
287
+ checkLoopTimers.set(providerId, timer);
288
+ }
289
+ else {
290
+ // 没有 recorder 了,停止该 provider 的检查循环
291
+ // TODO: 也许不需要删除定时器
292
+ checkLoopTimers.delete(providerId);
293
+ }
294
+ }
295
+ }
296
+ };
297
+ void checkLoop();
268
298
  };
269
- void checkLoop();
299
+ // 直接从注册的 provider 获取所有 provider IDs
300
+ const providerIds = this.providers.map((p) => p.id);
301
+ for (const providerId of providerIds) {
302
+ startProviderCheckLoop(providerId);
303
+ }
270
304
  },
271
305
  stopCheckLoop() {
272
306
  if (!this.isCheckLoopRunning)
273
307
  return;
274
308
  this.isCheckLoopRunning = false;
275
309
  // TODO: emit updated event
276
- clearTimeout(checkLoopTimer);
310
+ // 清理所有 provider 的 timer
311
+ for (const timer of checkLoopTimers.values()) {
312
+ clearTimeout(timer);
313
+ }
314
+ checkLoopTimers.clear();
277
315
  },
278
316
  savePathRule: opts.savePathRule ??
279
317
  path.join(process.cwd(), "{platform}/{owner}/{year}-{month}-{date} {hour}-{min}-{sec} {title}"),
@@ -281,6 +319,15 @@ export function createRecorderManager(opts) {
281
319
  biliBatchQuery: opts.biliBatchQuery ?? false,
282
320
  recordRetryImmediately: opts.recordRetryImmediately ?? false,
283
321
  cache: opts.cache ?? new RecorderCacheImpl(new MemoryCacheStore()),
322
+ providerCheckConfig: opts.providerCheckConfig ?? {},
323
+ getProviderCheckConfig(providerId) {
324
+ const providerConfig = this.providerCheckConfig[providerId];
325
+ return {
326
+ autoCheckInterval: providerConfig?.autoCheckInterval ?? this.autoCheckInterval,
327
+ maxThreadCount: providerConfig?.maxThreadCount ?? this.maxThreadCount,
328
+ waitTime: providerConfig?.waitTime ?? this.waitTime,
329
+ };
330
+ },
284
331
  ffmpegOutputArgs: opts.ffmpegOutputArgs ??
285
332
  "-c copy" +
286
333
  /**
package/lib/recorder.d.ts CHANGED
@@ -132,6 +132,7 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
132
132
  cover: string;
133
133
  liveId?: string;
134
134
  recordStartTime: Date;
135
+ area?: string;
135
136
  };
136
137
  tempStopIntervalCheck?: boolean;
137
138
  /** 缓存实例(命名空间) */
@@ -152,6 +153,7 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
152
153
  channelId: ChannelId;
153
154
  living: boolean;
154
155
  liveStartTime: Date;
156
+ area: string;
155
157
  }>;
156
158
  getStream: (this: Recorder<E>) => Promise<{
157
159
  source: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/manager",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "description": "Batch scheduling recorders",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  "mitt": "^3.0.1",
39
39
  "string-argv": "^0.3.2",
40
40
  "lodash-es": "^4.17.21",
41
- "axios": "^1.7.8",
41
+ "axios": "^1.15.0",
42
42
  "fs-extra": "^11.2.0",
43
43
  "ejs": "^3.1.10"
44
44
  },