@bililive-tools/manager 1.17.0 → 1.17.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/lib/downloader/IDownloader.d.ts +8 -12
- package/lib/downloader/streamManager.js +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/manager.d.ts +8 -1
- package/lib/manager.js +24 -1
- package/lib/recorder.d.ts +14 -6
- package/lib/utils.d.ts +5 -0
- 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/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;
|
|
@@ -71,8 +71,8 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
|
|
|
71
71
|
debugLevel?: "none" | "basic" | "verbose";
|
|
72
72
|
}
|
|
73
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";
|
|
74
|
+
/** 录制状态,idle: 空闲中,recording: 录制中,stopping-record: 停止录制中,check-error: 检查错误,title-blocked: 标题黑名单,charge-skipped: 充电直播(DRM加密)已跳过 */
|
|
75
|
+
export type RecorderState = "idle" | "recording" | "stopping-record" | "check-error" | "title-blocked" | "charge-skipped";
|
|
76
76
|
export type Progress = {
|
|
77
77
|
time: string | null;
|
|
78
78
|
};
|
|
@@ -133,6 +133,9 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
|
|
|
133
133
|
Updated: (string | keyof Recorder)[];
|
|
134
134
|
Message: Message;
|
|
135
135
|
DebugLog: DebugLog;
|
|
136
|
+
ChargeLive: {
|
|
137
|
+
channelId: string;
|
|
138
|
+
};
|
|
136
139
|
}>, RecorderCreateOpts<E> {
|
|
137
140
|
id: string;
|
|
138
141
|
extra: Partial<E>;
|
|
@@ -153,6 +156,9 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
|
|
|
153
156
|
liveId?: string;
|
|
154
157
|
recordStartTime: Date;
|
|
155
158
|
area?: string;
|
|
159
|
+
liveType?: string;
|
|
160
|
+
liveTypeDesc?: string;
|
|
161
|
+
canRecord?: boolean;
|
|
156
162
|
};
|
|
157
163
|
tempStopIntervalCheck?: boolean;
|
|
158
164
|
timeline?: RecorderTimelineItem[];
|
|
@@ -177,10 +183,12 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
|
|
|
177
183
|
liveStartTime: Date;
|
|
178
184
|
area: string;
|
|
179
185
|
}>;
|
|
180
|
-
getStream: (this: Recorder<E
|
|
186
|
+
getStream: (this: Recorder<E>, opts?: {
|
|
187
|
+
formatName?: FormatName;
|
|
188
|
+
formatPriorities?: Array<"flv" | "hls">;
|
|
189
|
+
}) => Promise<{
|
|
181
190
|
source: string;
|
|
182
191
|
name: string;
|
|
183
192
|
url: string;
|
|
184
193
|
}>;
|
|
185
194
|
}
|
|
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;
|
|
@@ -125,6 +129,7 @@ declare const _default: {
|
|
|
125
129
|
assertObjectType: typeof assertObjectType;
|
|
126
130
|
asyncThrottle: typeof asyncThrottle;
|
|
127
131
|
isFfmpegStartSegment: typeof isFfmpegStartSegment;
|
|
132
|
+
replaceFourByteUnicode: typeof replaceFourByteUnicode;
|
|
128
133
|
createFFmpegInvalidStreamChecker: typeof createFFmpegInvalidStreamChecker;
|
|
129
134
|
createTimeoutChecker: typeof createTimeoutChecker;
|
|
130
135
|
downloadImage: typeof downloadImage;
|
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.1",
|
|
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",
|