@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.
@@ -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>) => Promise<{
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.0",
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
- "ejs": "^3.1.10"
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",