@bililive-tools/manager 1.17.0 → 1.17.2
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/lib/downloader/IDownloader.d.ts +8 -12
- package/lib/downloader/streamManager.js +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/manager.d.ts +8 -1
- package/lib/manager.js +24 -1
- package/lib/recorder.d.ts +17 -7
- package/lib/utils.d.ts +11 -4
- package/lib/utils.js +7 -0
- package/package.json +10 -7
|
@@ -4,6 +4,12 @@ import type { FormatName } from "./index.js";
|
|
|
4
4
|
import type { XmlStreamController } from "../xml_stream_controller.js";
|
|
5
5
|
import type { VideoFileCompletedPayload } from "./streamManager.js";
|
|
6
6
|
export type Segment = number | string | undefined;
|
|
7
|
+
export interface VideoFileCreatedPayload {
|
|
8
|
+
filename: string;
|
|
9
|
+
cover?: string;
|
|
10
|
+
rawFilename?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
}
|
|
7
13
|
/**
|
|
8
14
|
* 录制器构造函数选项的基础接口
|
|
9
15
|
*/
|
|
@@ -51,12 +57,7 @@ export interface IDownloader extends EventEmitter {
|
|
|
51
57
|
getExtraDataController(): XmlStreamController | null;
|
|
52
58
|
createCommand(): any;
|
|
53
59
|
get videoFilePath(): string;
|
|
54
|
-
on(event: "videoFileCreated", listener: (data:
|
|
55
|
-
filename: string;
|
|
56
|
-
cover?: string;
|
|
57
|
-
rawFilename?: string;
|
|
58
|
-
title?: string;
|
|
59
|
-
}) => void): this;
|
|
60
|
+
on(event: "videoFileCreated", listener: (data: VideoFileCreatedPayload) => void): this;
|
|
60
61
|
on(event: "videoFileCompleted", listener: (data: VideoFileCompletedPayload) => void): this;
|
|
61
62
|
on(event: "DebugLog", listener: (data: {
|
|
62
63
|
type: string;
|
|
@@ -64,12 +65,7 @@ export interface IDownloader extends EventEmitter {
|
|
|
64
65
|
}) => void): this;
|
|
65
66
|
on(event: "progress", listener: (info: any) => void): this;
|
|
66
67
|
on(event: string, listener: (...args: any[]) => void): this;
|
|
67
|
-
emit(event: "videoFileCreated", data:
|
|
68
|
-
filename: string;
|
|
69
|
-
cover?: string;
|
|
70
|
-
rawFilename?: string;
|
|
71
|
-
title?: string;
|
|
72
|
-
}): boolean;
|
|
68
|
+
emit(event: "videoFileCreated", data: VideoFileCreatedPayload): boolean;
|
|
73
69
|
emit(event: "videoFileCompleted", data: VideoFileCompletedPayload): boolean;
|
|
74
70
|
emit(event: "DebugLog", data: {
|
|
75
71
|
type: string;
|
|
@@ -146,6 +146,7 @@ export class StreamManager extends EventEmitter {
|
|
|
146
146
|
recordSavePath = getSavePath({ startTime, extraMs: true });
|
|
147
147
|
}
|
|
148
148
|
this.recordSavePath = recordSavePath;
|
|
149
|
+
ensureFolderExist(recordSavePath);
|
|
149
150
|
if (hasSegment) {
|
|
150
151
|
this.segment = new Segment(getSavePath, disableDanma, this.videoExt, {
|
|
151
152
|
firstStartTime: startTime,
|
|
@@ -161,7 +162,6 @@ export class StreamManager extends EventEmitter {
|
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
164
|
else {
|
|
164
|
-
ensureFolderExist(recordSavePath);
|
|
165
165
|
const extraDataSavePath = `${recordSavePath}.xml`;
|
|
166
166
|
if (!disableDanma) {
|
|
167
167
|
this.extraDataController = createRecordExtraDataController(extraDataSavePath);
|
package/lib/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./manager.js";
|
|
|
9
9
|
export * from "./cache.js";
|
|
10
10
|
export * from "./downloader/FFmpegDownloader.js";
|
|
11
11
|
export { createDownloader } from "./downloader/index.js";
|
|
12
|
+
export type { VideoFileCreatedPayload } from "./downloader/IDownloader.js";
|
|
12
13
|
export { checkTitleKeywordsWhileRecording, checkTitleKeywordsBeforeRecord } from "./utils.js";
|
|
13
14
|
export { utils };
|
|
14
15
|
/**
|
package/lib/index.js
CHANGED
package/lib/manager.d.ts
CHANGED
|
@@ -13,12 +13,16 @@ export interface ProviderCheckConfig {
|
|
|
13
13
|
/** 每次检查之间的等待时间(毫秒) */
|
|
14
14
|
waitTime?: number;
|
|
15
15
|
}
|
|
16
|
+
export interface ResolveChannelOptions {
|
|
17
|
+
/** 解析频道信息时使用的网络代理 */
|
|
18
|
+
proxy?: string;
|
|
19
|
+
}
|
|
16
20
|
export interface RecorderProvider<E extends AnyObject> {
|
|
17
21
|
id: string;
|
|
18
22
|
name: string;
|
|
19
23
|
siteURL: string;
|
|
20
24
|
matchURL: (this: RecorderProvider<E>, channelURL: string) => boolean;
|
|
21
|
-
resolveChannelInfoFromURL: (this: RecorderProvider<E>, channelURL: string) => Promise<{
|
|
25
|
+
resolveChannelInfoFromURL: (this: RecorderProvider<E>, channelURL: string, options?: ResolveChannelOptions) => Promise<{
|
|
22
26
|
id: ChannelId;
|
|
23
27
|
title: string;
|
|
24
28
|
owner: string;
|
|
@@ -62,6 +66,9 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
|
|
|
62
66
|
RecoderLiveStart: {
|
|
63
67
|
recorder: Recorder<E>;
|
|
64
68
|
};
|
|
69
|
+
RecoderChargeLive: {
|
|
70
|
+
recorder: Recorder<E>;
|
|
71
|
+
};
|
|
65
72
|
RecordStop: {
|
|
66
73
|
recorder: SerializedRecorder<E>;
|
|
67
74
|
recordHandle: RecordHandle;
|
package/lib/manager.js
CHANGED
|
@@ -5,7 +5,7 @@ import { omit, range } from "lodash-es";
|
|
|
5
5
|
import { parseArgsStringToArgv } from "string-argv";
|
|
6
6
|
import { RecorderCacheImpl, MemoryCacheStore } from "./cache.js";
|
|
7
7
|
import { getBiliStatusInfoByRoomIds } from "./api.js";
|
|
8
|
-
import { formatDate, removeSystemReservedChars, formatTemplate, replaceExtName, downloadImage, isBetweenTimeRange, sleep, } from "./utils.js";
|
|
8
|
+
import { formatDate, removeSystemReservedChars, formatTemplate, replaceExtName, downloadImage, isBetweenTimeRange, sleep, replaceFourByteUnicode, } from "./utils.js";
|
|
9
9
|
import { StreamManager } from "./downloader/streamManager.js";
|
|
10
10
|
const RECORDER_TIMELINE_LIMIT = 40;
|
|
11
11
|
function normalizeTimelineText(text) {
|
|
@@ -43,6 +43,7 @@ function getStateTimelineText(state, msg) {
|
|
|
43
43
|
"stopping-record": "停止录制中",
|
|
44
44
|
"check-error": "检查失败",
|
|
45
45
|
"title-blocked": "标题命中过滤规则",
|
|
46
|
+
"charge-skipped": "特殊直播(付费/权限/加密),无法录制,已跳过",
|
|
46
47
|
};
|
|
47
48
|
return stateTextMap[state];
|
|
48
49
|
}
|
|
@@ -142,6 +143,8 @@ export function createRecorderManager(opts) {
|
|
|
142
143
|
const tempBanObj = {};
|
|
143
144
|
// 用于是否触发LiveStart事件,不要重复触发
|
|
144
145
|
const liveStartObj = {};
|
|
146
|
+
// 用于是否触发ChargeLive(充电直播)事件,按场次去重,不要每个检查周期重复触发
|
|
147
|
+
const chargeLiveObj = {};
|
|
145
148
|
// 用于记录触发重试直播场次的次数
|
|
146
149
|
const retryCountObj = {};
|
|
147
150
|
const manager = {
|
|
@@ -254,6 +257,15 @@ export function createRecorderManager(opts) {
|
|
|
254
257
|
liveStartObj[key] = true;
|
|
255
258
|
this.emit("RecoderLiveStart", { recorder: recorder });
|
|
256
259
|
});
|
|
260
|
+
recorder.on("ChargeLive", () => {
|
|
261
|
+
// 充电直播按场次去重,避免每个检查周期重复推送
|
|
262
|
+
const liveId = recorder.liveInfo?.liveId;
|
|
263
|
+
const key = `${recorder.channelId}-${liveId ?? ""}`;
|
|
264
|
+
if (chargeLiveObj[key])
|
|
265
|
+
return;
|
|
266
|
+
chargeLiveObj[key] = true;
|
|
267
|
+
this.emit("RecoderChargeLive", { recorder: recorder });
|
|
268
|
+
});
|
|
257
269
|
recorder.on("stateChange", ({ state, msg }) => {
|
|
258
270
|
recorder.state = state;
|
|
259
271
|
if (state !== "idle") {
|
|
@@ -274,6 +286,16 @@ export function createRecorderManager(opts) {
|
|
|
274
286
|
recorder.recordHandle?.stop("remove recorder");
|
|
275
287
|
this.recorders.splice(idx, 1);
|
|
276
288
|
delete tempBanObj[recorder.channelId];
|
|
289
|
+
// 清理该房间的 liveStart/chargeLive 去重标记,否则删除后重新添加同一场直播不会再次推送通知
|
|
290
|
+
const keyPrefix = `${recorder.channelId}-`;
|
|
291
|
+
Object.keys(liveStartObj).forEach((k) => {
|
|
292
|
+
if (k.startsWith(keyPrefix))
|
|
293
|
+
delete liveStartObj[k];
|
|
294
|
+
});
|
|
295
|
+
Object.keys(chargeLiveObj).forEach((k) => {
|
|
296
|
+
if (k.startsWith(keyPrefix))
|
|
297
|
+
delete chargeLiveObj[k];
|
|
298
|
+
});
|
|
277
299
|
this.emit("RecorderRemoved", recorder.toJSON());
|
|
278
300
|
},
|
|
279
301
|
getRecorder(id) {
|
|
@@ -444,6 +466,7 @@ export function genSavePathFromRule(manager, recorder, extData) {
|
|
|
444
466
|
title: title,
|
|
445
467
|
remarks: remarks,
|
|
446
468
|
channelId,
|
|
469
|
+
replaceFourByteUnicode,
|
|
447
470
|
};
|
|
448
471
|
let savePathRule = manager.savePathRule;
|
|
449
472
|
if (extData?.extraMs) {
|
package/lib/recorder.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { AnyObject, PickRequired, UnknownObject } from "./utils.js";
|
|
|
5
5
|
import type { NamespacedCache } from "./cache.js";
|
|
6
6
|
import type { DownloaderType } from "./downloader/index.js";
|
|
7
7
|
import type { XmlStreamStats } from "./xml_stream_controller.js";
|
|
8
|
-
type FormatName = "auto" | "flv" | "hls" | "fmp4" | "flv_only" | "hls_only" | "fmp4_only";
|
|
9
|
-
type CodecName = "auto" | "avc" | "hevc" | "avc_only" | "hevc_only";
|
|
8
|
+
export type FormatName = "auto" | "flv" | "hls" | "fmp4" | "flv_only" | "hls_only" | "fmp4_only";
|
|
9
|
+
export type CodecName = "auto" | "avc" | "hevc" | "av1" | "avc_only" | "hevc_only" | "av1_only";
|
|
10
10
|
export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
|
|
11
11
|
providerId: RecorderProvider<E>["id"];
|
|
12
12
|
channelId: ChannelId;
|
|
@@ -52,6 +52,8 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
|
|
|
52
52
|
* 2. 正则表达式:'/pattern/flags'(如:'/回放|录播/i')
|
|
53
53
|
*/
|
|
54
54
|
titleKeywords?: string;
|
|
55
|
+
/** B站直播间标题变更时分段 */
|
|
56
|
+
segmentOnTitleChange?: boolean;
|
|
55
57
|
/** 用于指定录制文件格式,auto时,分段使用ts,不分段使用mp4 */
|
|
56
58
|
videoFormat?: "auto" | "ts" | "mkv" | "flv";
|
|
57
59
|
/** 录制类型 */
|
|
@@ -70,9 +72,9 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
|
|
|
70
72
|
/** 调试等级 */
|
|
71
73
|
debugLevel?: "none" | "basic" | "verbose";
|
|
72
74
|
}
|
|
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">;
|
|
74
|
-
/** 录制状态,idle: 空闲中,recording: 录制中,stopping-record: 停止录制中,check-error: 检查错误,title-blocked:
|
|
75
|
-
export type RecorderState = "idle" | "recording" | "stopping-record" | "check-error" | "title-blocked";
|
|
75
|
+
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" | "segmentOnTitleChange">;
|
|
76
|
+
/** 录制状态,idle: 空闲中,recording: 录制中,stopping-record: 停止录制中,check-error: 检查错误,title-blocked: 标题黑名单,charge-skipped: 充电直播(DRM加密)已跳过 */
|
|
77
|
+
export type RecorderState = "idle" | "recording" | "stopping-record" | "check-error" | "title-blocked" | "charge-skipped";
|
|
76
78
|
export type Progress = {
|
|
77
79
|
time: string | null;
|
|
78
80
|
};
|
|
@@ -133,6 +135,9 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
|
|
|
133
135
|
Updated: (string | keyof Recorder)[];
|
|
134
136
|
Message: Message;
|
|
135
137
|
DebugLog: DebugLog;
|
|
138
|
+
ChargeLive: {
|
|
139
|
+
channelId: string;
|
|
140
|
+
};
|
|
136
141
|
}>, RecorderCreateOpts<E> {
|
|
137
142
|
id: string;
|
|
138
143
|
extra: Partial<E>;
|
|
@@ -153,6 +158,9 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
|
|
|
153
158
|
liveId?: string;
|
|
154
159
|
recordStartTime: Date;
|
|
155
160
|
area?: string;
|
|
161
|
+
liveType?: string;
|
|
162
|
+
liveTypeDesc?: string;
|
|
163
|
+
canRecord?: boolean;
|
|
156
164
|
};
|
|
157
165
|
tempStopIntervalCheck?: boolean;
|
|
158
166
|
timeline?: RecorderTimelineItem[];
|
|
@@ -177,10 +185,12 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
|
|
|
177
185
|
liveStartTime: Date;
|
|
178
186
|
area: string;
|
|
179
187
|
}>;
|
|
180
|
-
getStream: (this: Recorder<E
|
|
188
|
+
getStream: (this: Recorder<E>, opts?: {
|
|
189
|
+
formatName?: FormatName;
|
|
190
|
+
formatPriorities?: Array<"flv" | "hls">;
|
|
191
|
+
}) => Promise<{
|
|
181
192
|
source: string;
|
|
182
193
|
name: string;
|
|
183
194
|
url: string;
|
|
184
195
|
}>;
|
|
185
196
|
}
|
|
186
|
-
export {};
|
package/lib/utils.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export declare function assertNumberType(data: unknown, msg?: string): asserts d
|
|
|
36
36
|
export declare function assertObjectType(data: unknown, msg?: string): asserts data is object;
|
|
37
37
|
export declare function formatDate(date: Date, format: string): string;
|
|
38
38
|
export declare function removeSystemReservedChars(str: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* 替换四字节 Unicode 字符(如部分 emoji 表情)为指定字符。
|
|
41
|
+
*/
|
|
42
|
+
export declare function replaceFourByteUnicode(value: unknown, replacement?: string): string;
|
|
39
43
|
export declare function isFfmpegStartSegment(line: string): boolean;
|
|
40
44
|
export declare function isMesioStartSegment(line: string): boolean;
|
|
41
45
|
export declare function isBililiveStartSegment(line: string): boolean;
|
|
@@ -57,6 +61,8 @@ export declare function createTimeoutChecker(onTimeout: () => void, time: number
|
|
|
57
61
|
start: () => void;
|
|
58
62
|
};
|
|
59
63
|
export declare function downloadImage(imageUrl: string, savePath: string): Promise<void>;
|
|
64
|
+
declare const md5: (str: string) => string;
|
|
65
|
+
declare const uuid: () => `${string}-${string}-${string}-${string}-${string}`;
|
|
60
66
|
/**
|
|
61
67
|
* 根据指定的顺序对对象数组进行排序
|
|
62
68
|
* @param objects 要排序的对象数组
|
|
@@ -125,18 +131,19 @@ declare const _default: {
|
|
|
125
131
|
assertObjectType: typeof assertObjectType;
|
|
126
132
|
asyncThrottle: typeof asyncThrottle;
|
|
127
133
|
isFfmpegStartSegment: typeof isFfmpegStartSegment;
|
|
134
|
+
replaceFourByteUnicode: typeof replaceFourByteUnicode;
|
|
128
135
|
createFFmpegInvalidStreamChecker: typeof createFFmpegInvalidStreamChecker;
|
|
129
136
|
createTimeoutChecker: typeof createTimeoutChecker;
|
|
130
137
|
downloadImage: typeof downloadImage;
|
|
131
|
-
md5:
|
|
132
|
-
uuid:
|
|
138
|
+
md5: typeof md5;
|
|
139
|
+
uuid: typeof uuid;
|
|
133
140
|
sortByKeyOrder: typeof sortByKeyOrder;
|
|
134
141
|
retry: typeof retry;
|
|
135
|
-
isBetweenTimeRange:
|
|
142
|
+
isBetweenTimeRange: typeof isBetweenTimeRange;
|
|
136
143
|
hasBlockedTitleKeywords: typeof hasBlockedTitleKeywords;
|
|
137
144
|
shouldCheckTitleKeywords: typeof shouldCheckTitleKeywords;
|
|
138
145
|
shouldUseStrictQuality: typeof shouldUseStrictQuality;
|
|
139
|
-
sleep:
|
|
146
|
+
sleep: typeof sleep;
|
|
140
147
|
checkTitleKeywordsWhileRecording: typeof checkTitleKeywordsWhileRecording;
|
|
141
148
|
checkTitleKeywordsBeforeRecord: typeof checkTitleKeywordsBeforeRecord;
|
|
142
149
|
};
|
package/lib/utils.js
CHANGED
|
@@ -124,6 +124,12 @@ export function formatDate(date, format) {
|
|
|
124
124
|
export function removeSystemReservedChars(str) {
|
|
125
125
|
return filenamify(str, { replacement: "_" });
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* 替换四字节 Unicode 字符(如部分 emoji 表情)为指定字符。
|
|
129
|
+
*/
|
|
130
|
+
export function replaceFourByteUnicode(value, replacement = "_") {
|
|
131
|
+
return String(value ?? "").replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, replacement);
|
|
132
|
+
}
|
|
127
133
|
export function isFfmpegStartSegment(line) {
|
|
128
134
|
return line.includes("Opening ") && line.includes("for writing");
|
|
129
135
|
}
|
|
@@ -507,6 +513,7 @@ export default {
|
|
|
507
513
|
assertObjectType,
|
|
508
514
|
asyncThrottle,
|
|
509
515
|
isFfmpegStartSegment,
|
|
516
|
+
replaceFourByteUnicode,
|
|
510
517
|
createFFmpegInvalidStreamChecker,
|
|
511
518
|
createTimeoutChecker,
|
|
512
519
|
downloadImage,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bililive-tools/manager",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
4
4
|
"description": "Batch scheduling recorders",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -33,14 +33,17 @@
|
|
|
33
33
|
"license": "LGPL",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@renmu/fluent-ffmpeg": "2.3.3",
|
|
36
|
-
"fast-xml-parser": "^4.5.0",
|
|
37
|
-
"filenamify": "^7.0.0",
|
|
38
|
-
"mitt": "^3.0.1",
|
|
39
|
-
"string-argv": "^0.3.2",
|
|
40
|
-
"lodash-es": "^4.17.21",
|
|
41
36
|
"axios": "^1.15.0",
|
|
37
|
+
"ejs": "^3.1.10",
|
|
38
|
+
"fast-xml-parser": "^5.9.3",
|
|
39
|
+
"filenamify": "^7.0.0",
|
|
42
40
|
"fs-extra": "^11.2.0",
|
|
43
|
-
"
|
|
41
|
+
"lodash-es": "^4.17.21",
|
|
42
|
+
"mitt": "^3.0.1",
|
|
43
|
+
"string-argv": "^0.3.2"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/ejs": "^3.1.5"
|
|
44
47
|
},
|
|
45
48
|
"scripts": {
|
|
46
49
|
"build": "pnpm run test && tsc",
|