@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 +16 -7
- package/lib/manager.d.ts +15 -1
- package/lib/manager.js +77 -30
- package/lib/recorder.d.ts +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
## 支持的平台
|
|
14
14
|
|
|
15
|
-
| 平台
|
|
16
|
-
|
|
|
17
|
-
| B站
|
|
18
|
-
| 斗鱼
|
|
19
|
-
| 虎牙
|
|
20
|
-
| 抖音
|
|
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
|
-
|
|
26
|
-
const
|
|
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
|
-
|
|
47
|
+
const needCheckRecorders = recorders
|
|
46
48
|
.filter((r) => !r.disableAutoCheck)
|
|
47
|
-
.filter((r) => isBetweenTimeRange(r.handleTime))
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|
86
|
+
threads.push(...range(0, providerConfig.maxThreadCount).map(async () => {
|
|
80
87
|
while (needCheckRecorders.length > 0) {
|
|
81
88
|
try {
|
|
82
89
|
await checkOnce();
|
|
83
|
-
if (
|
|
84
|
-
await sleep(
|
|
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 ??
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
41
|
+
"axios": "^1.15.0",
|
|
42
42
|
"fs-extra": "^11.2.0",
|
|
43
43
|
"ejs": "^3.1.10"
|
|
44
44
|
},
|