@bililive-tools/manager 1.10.0 → 1.11.0

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.
@@ -12,7 +12,8 @@ declare class BililiveRecorderCommand extends EventEmitter {
12
12
  inputOptions(...options: string[]): BililiveRecorderCommand;
13
13
  _getArguments(): string[];
14
14
  run(): void;
15
- kill(signal?: NodeJS.Signals): void;
15
+ kill(): void;
16
+ cut(): void;
16
17
  }
17
18
  export declare const createBililiveBuilder: () => BililiveRecorderCommand;
18
19
  export declare class BililiveDownloader extends EventEmitter implements IDownloader {
@@ -28,6 +29,7 @@ export declare class BililiveDownloader extends EventEmitter implements IDownloa
28
29
  }) => string;
29
30
  readonly segment: Segment;
30
31
  readonly inputOptions: string[];
32
+ readonly disableDanma: boolean;
31
33
  readonly url: string;
32
34
  readonly debugLevel: "none" | "basic" | "verbose";
33
35
  readonly headers: {
@@ -46,5 +48,6 @@ export declare class BililiveDownloader extends EventEmitter implements IDownloa
46
48
  stop(): Promise<void>;
47
49
  getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
48
50
  get videoFilePath(): string;
51
+ cut(): void;
49
52
  }
50
53
  export {};
@@ -1,5 +1,6 @@
1
1
  import EventEmitter from "node:events";
2
2
  import { spawn } from "node:child_process";
3
+ import { DEFAULT_USER_AGENT } from "./index.js";
3
4
  import { StreamManager, getBililivePath } from "../index.js";
4
5
  import { byte2MB } from "../utils.js";
5
6
  // Bililive command builder class similar to ffmpeg
@@ -64,7 +65,6 @@ class BililiveRecorderCommand extends EventEmitter {
64
65
  this.process.on("error", (error) => {
65
66
  this.emit("error", error);
66
67
  });
67
- [];
68
68
  this.process.on("close", (code) => {
69
69
  if (code === 0) {
70
70
  this.emit("end");
@@ -74,9 +74,15 @@ class BililiveRecorderCommand extends EventEmitter {
74
74
  }
75
75
  });
76
76
  }
77
- kill(signal = "SIGTERM") {
77
+ kill() {
78
78
  if (this.process) {
79
- this.process.kill(signal);
79
+ this.process.stdin?.write("q\n");
80
+ // this.process.kill("SIGTERM");
81
+ }
82
+ }
83
+ cut() {
84
+ if (this.process) {
85
+ this.process.stdin?.write("s\n");
80
86
  }
81
87
  }
82
88
  }
@@ -94,6 +100,7 @@ export class BililiveDownloader extends EventEmitter {
94
100
  getSavePath;
95
101
  segment;
96
102
  inputOptions = [];
103
+ disableDanma = false;
97
104
  url;
98
105
  debugLevel = "none";
99
106
  headers;
@@ -104,16 +111,20 @@ export class BililiveDownloader extends EventEmitter {
104
111
  // 存在自动分段,永远为true
105
112
  const hasSegment = true;
106
113
  this.hasSegment = hasSegment;
114
+ this.disableDanma = opts.disableDanma ?? false;
107
115
  this.debugLevel = opts.debugLevel ?? "none";
108
116
  let videoFormat = "flv";
109
- this.streamManager = new StreamManager(opts.getSavePath, hasSegment, "bililive", videoFormat, {
117
+ this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "bililive", videoFormat, {
110
118
  onUpdateLiveInfo: this.onUpdateLiveInfo,
111
119
  });
112
120
  this.getSavePath = opts.getSavePath;
113
121
  this.inputOptions = [];
114
122
  this.url = opts.url;
115
123
  this.segment = opts.segment;
116
- this.headers = opts.headers;
124
+ this.headers = {
125
+ "User-Agent": DEFAULT_USER_AGENT,
126
+ ...(opts.headers || {}),
127
+ };
117
128
  this.command = this.createCommand();
118
129
  this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
119
130
  this.emit("videoFileCreated", { filename, cover, rawFilename, title });
@@ -126,11 +137,7 @@ export class BililiveDownloader extends EventEmitter {
126
137
  });
127
138
  }
128
139
  createCommand() {
129
- const inputOptions = [
130
- ...this.inputOptions,
131
- "-h",
132
- "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
133
- ];
140
+ const inputOptions = [...this.inputOptions, "--disable-log-file", "true"];
134
141
  if (this.debugLevel === "verbose") {
135
142
  inputOptions.push("-l", "Debug");
136
143
  }
@@ -187,7 +194,7 @@ export class BililiveDownloader extends EventEmitter {
187
194
  async stop() {
188
195
  try {
189
196
  // 直接发送SIGINT信号,会导致数据丢失
190
- this.command.kill("SIGINT");
197
+ this.command.kill();
191
198
  await this.streamManager.handleVideoCompleted();
192
199
  }
193
200
  catch (err) {
@@ -200,4 +207,7 @@ export class BililiveDownloader extends EventEmitter {
200
207
  get videoFilePath() {
201
208
  return this.streamManager.videoFilePath;
202
209
  }
210
+ cut() {
211
+ this.command.cut();
212
+ }
203
213
  }
@@ -18,6 +18,7 @@ export declare class FFmpegDownloader extends EventEmitter implements IDownloade
18
18
  ffmpegOutputOptions: string[];
19
19
  readonly inputOptions: string[];
20
20
  readonly isHls: boolean;
21
+ readonly disableDanma: boolean;
21
22
  readonly url: string;
22
23
  formatName: FormatName;
23
24
  videoFormat: VideoFormat;
@@ -39,4 +40,5 @@ export declare class FFmpegDownloader extends EventEmitter implements IDownloade
39
40
  stop(): Promise<void>;
40
41
  getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
41
42
  get videoFilePath(): string;
43
+ cut(): void;
42
44
  }
@@ -1,6 +1,7 @@
1
1
  import EventEmitter from "node:events";
2
2
  import { createFFMPEGBuilder, StreamManager, utils } from "../index.js";
3
3
  import { createInvalidStreamChecker, assert } from "../utils.js";
4
+ import { DEFAULT_USER_AGENT } from "./index.js";
4
5
  export class FFmpegDownloader extends EventEmitter {
5
6
  onEnd;
6
7
  onUpdateLiveInfo;
@@ -14,6 +15,7 @@ export class FFmpegDownloader extends EventEmitter {
14
15
  ffmpegOutputOptions = [];
15
16
  inputOptions = [];
16
17
  isHls;
18
+ disableDanma = false;
17
19
  url;
18
20
  formatName;
19
21
  videoFormat;
@@ -50,7 +52,8 @@ export class FFmpegDownloader extends EventEmitter {
50
52
  }
51
53
  }
52
54
  this.videoFormat = videoFormat;
53
- this.streamManager = new StreamManager(opts.getSavePath, this.hasSegment, "ffmpeg", this.videoFormat, {
55
+ this.disableDanma = opts.disableDanma ?? false;
56
+ this.streamManager = new StreamManager(opts.getSavePath, this.hasSegment, this.disableDanma, "ffmpeg", this.videoFormat, {
54
57
  onUpdateLiveInfo: this.onUpdateLiveInfo,
55
58
  });
56
59
  this.timeoutChecker = utils.createTimeoutChecker(() => this.onEnd("ffmpeg timeout"), 3 * 10e3, false);
@@ -78,7 +81,7 @@ export class FFmpegDownloader extends EventEmitter {
78
81
  const inputOptions = [
79
82
  ...this.inputOptions,
80
83
  "-user_agent",
81
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
84
+ this.headers?.["User-Agent"] ?? DEFAULT_USER_AGENT,
82
85
  ];
83
86
  if (this.isHls) {
84
87
  inputOptions.push(...["-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "3"]);
@@ -89,8 +92,8 @@ export class FFmpegDownloader extends EventEmitter {
89
92
  if (this.headers) {
90
93
  const headers = [];
91
94
  Object.entries(this.headers).forEach(([key, value]) => {
92
- if (!value)
93
- return;
95
+ if (!value || key === "User-Agent")
96
+ return; // User-Agent单独处理
94
97
  headers.push(`${key}:${value}`);
95
98
  });
96
99
  if (headers.length) {
@@ -186,4 +189,7 @@ export class FFmpegDownloader extends EventEmitter {
186
189
  get videoFilePath() {
187
190
  return this.streamManager.videoFilePath;
188
191
  }
192
+ cut() {
193
+ throw new Error("FFmpeg downloader does not support cut operation.");
194
+ }
189
195
  }
@@ -14,6 +14,7 @@ export interface BaseRecorderOptions {
14
14
  }) => string;
15
15
  segment: Segment;
16
16
  inputOptions?: string[];
17
+ disableDanma?: boolean;
17
18
  formatName: FormatName;
18
19
  debugLevel?: "none" | "basic" | "verbose";
19
20
  headers?: {
@@ -29,6 +30,7 @@ export interface IDownloader extends EventEmitter {
29
30
  readonly hasSegment: boolean;
30
31
  readonly segment: Segment;
31
32
  readonly inputOptions: string[];
33
+ readonly disableDanma: boolean;
32
34
  readonly url: string;
33
35
  readonly headers: {
34
36
  [key: string]: string | undefined;
@@ -39,6 +41,7 @@ export interface IDownloader extends EventEmitter {
39
41
  }) => string;
40
42
  run(): void;
41
43
  stop(): Promise<void>;
44
+ cut(): void;
42
45
  getArguments(): string[];
43
46
  getExtraDataController(): XmlStreamController | null;
44
47
  createCommand(): any;
@@ -19,6 +19,7 @@ export type RecorderOptions<T extends DownloaderType> = T extends "ffmpeg" ? FFM
19
19
  */
20
20
  export type RecorderInstance<T extends DownloaderType> = T extends "ffmpeg" ? FFmpegDownloader : T extends "mesio" ? mesioDownloader : BililiveDownloader;
21
21
  type RecorderOpts = FFMPEGRecorderOptions | MesioRecorderOptions | BililiveRecorderOptions;
22
+ export declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";
22
23
  /**
23
24
  * 创建录制器的工厂函数
24
25
  */
@@ -5,6 +5,7 @@ import { parseSizeToBytes } from "../utils.js";
5
5
  export { FFmpegDownloader } from "./FFmpegDownloader.js";
6
6
  export { mesioDownloader } from "./mesioDownloader.js";
7
7
  export { BililiveDownloader } from "./BililiveDownloader.js";
8
+ export const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";
8
9
  /**
9
10
  * 创建录制器的工厂函数
10
11
  */
@@ -12,7 +12,7 @@ declare class MesioCommand extends EventEmitter {
12
12
  inputOptions(...options: string[]): MesioCommand;
13
13
  _getArguments(): string[];
14
14
  run(): void;
15
- kill(signal?: NodeJS.Signals): void;
15
+ kill(): void;
16
16
  }
17
17
  export declare const createMesioBuilder: () => MesioCommand;
18
18
  export declare class mesioDownloader extends EventEmitter implements IDownloader {
@@ -28,6 +28,7 @@ export declare class mesioDownloader extends EventEmitter implements IDownloader
28
28
  }) => string;
29
29
  readonly segment: Segment;
30
30
  readonly inputOptions: string[];
31
+ readonly disableDanma: boolean;
31
32
  readonly url: string;
32
33
  readonly debugLevel: "none" | "basic" | "verbose";
33
34
  readonly headers: {
@@ -43,5 +44,6 @@ export declare class mesioDownloader extends EventEmitter implements IDownloader
43
44
  stop(): Promise<void>;
44
45
  getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
45
46
  get videoFilePath(): string;
47
+ cut(): void;
46
48
  }
47
49
  export {};
@@ -1,8 +1,8 @@
1
1
  import path from "node:path";
2
2
  import EventEmitter from "node:events";
3
3
  import { spawn } from "node:child_process";
4
+ import { DEFAULT_USER_AGENT } from "./index.js";
4
5
  import { StreamManager, getMesioPath } from "../index.js";
5
- // Mesio command builder class similar to ffmpeg
6
6
  class MesioCommand extends EventEmitter {
7
7
  _input = "";
8
8
  _output = "";
@@ -74,9 +74,10 @@ class MesioCommand extends EventEmitter {
74
74
  }
75
75
  });
76
76
  }
77
- kill(signal = "SIGTERM") {
77
+ kill() {
78
78
  if (this.process) {
79
- this.process.kill(signal);
79
+ this.process.stdin?.write("q");
80
+ this.process.stdin?.end();
80
81
  }
81
82
  }
82
83
  }
@@ -94,6 +95,7 @@ export class mesioDownloader extends EventEmitter {
94
95
  getSavePath;
95
96
  segment;
96
97
  inputOptions = [];
98
+ disableDanma = false;
97
99
  url;
98
100
  debugLevel = "none";
99
101
  headers;
@@ -104,6 +106,7 @@ export class mesioDownloader extends EventEmitter {
104
106
  // 存在自动分段,永远为true
105
107
  const hasSegment = true;
106
108
  this.hasSegment = hasSegment;
109
+ this.disableDanma = opts.disableDanma ?? false;
107
110
  this.debugLevel = opts.debugLevel ?? "none";
108
111
  let videoFormat = "flv";
109
112
  if (opts.url.includes(".m3u8")) {
@@ -118,14 +121,17 @@ export class mesioDownloader extends EventEmitter {
118
121
  else if (opts.formatName === "flv") {
119
122
  videoFormat = "flv";
120
123
  }
121
- this.streamManager = new StreamManager(opts.getSavePath, hasSegment, "mesio", videoFormat, {
124
+ this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "mesio", videoFormat, {
122
125
  onUpdateLiveInfo: this.onUpdateLiveInfo,
123
126
  });
124
127
  this.getSavePath = opts.getSavePath;
125
128
  this.inputOptions = [];
126
129
  this.url = opts.url;
127
130
  this.segment = opts.segment;
128
- this.headers = opts.headers;
131
+ this.headers = {
132
+ "User-Agent": DEFAULT_USER_AGENT,
133
+ ...(opts.headers || {}),
134
+ };
129
135
  this.command = this.createCommand();
130
136
  this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
131
137
  this.emit("videoFileCreated", { filename, cover, rawFilename, title });
@@ -138,13 +144,7 @@ export class mesioDownloader extends EventEmitter {
138
144
  });
139
145
  }
140
146
  createCommand() {
141
- const inputOptions = [
142
- ...this.inputOptions,
143
- "--fix",
144
- "-H",
145
- "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
146
- "--no-proxy",
147
- ];
147
+ const inputOptions = [...this.inputOptions, "--fix", "--no-proxy"];
148
148
  if (this.debugLevel === "verbose") {
149
149
  inputOptions.push("-v");
150
150
  }
@@ -183,8 +183,8 @@ export class mesioDownloader extends EventEmitter {
183
183
  }
184
184
  async stop() {
185
185
  try {
186
- // 直接发送SIGINT信号,会导致数据丢失
187
- this.command.kill("SIGINT");
186
+ this.command.kill();
187
+ await new Promise((resolve) => setTimeout(resolve, 2000));
188
188
  await this.streamManager.handleVideoCompleted();
189
189
  }
190
190
  catch (err) {
@@ -197,4 +197,7 @@ export class mesioDownloader extends EventEmitter {
197
197
  get videoFilePath() {
198
198
  return this.streamManager.videoFilePath;
199
199
  }
200
+ cut() {
201
+ throw new Error("Mesio downloader does not support cut operation.");
202
+ }
200
203
  }
@@ -15,8 +15,9 @@ export declare class Segment extends EventEmitter {
15
15
  rawRecordingVideoPath: string;
16
16
  /** 输出文件名名,不包含拓展名 */
17
17
  outputVideoFilePath: string;
18
+ disableDanma: boolean;
18
19
  videoExt: TrueVideoFormat;
19
- constructor(getSavePath: GetSavePath, videoExt: TrueVideoFormat);
20
+ constructor(getSavePath: GetSavePath, disableDanma: boolean, videoExt: TrueVideoFormat);
20
21
  handleSegmentEnd(): Promise<void>;
21
22
  onSegmentStart(stderrLine: string, callBack?: {
22
23
  onUpdateLiveInfo: () => Promise<{
@@ -35,7 +36,7 @@ export declare class StreamManager extends EventEmitter {
35
36
  recorderType: RecorderType;
36
37
  private videoFormat;
37
38
  private callBack?;
38
- constructor(getSavePath: GetSavePath, hasSegment: boolean, recorderType: RecorderType, videoFormat: TrueVideoFormat, callBack?: {
39
+ constructor(getSavePath: GetSavePath, hasSegment: boolean, disableDanma: boolean, recorderType: RecorderType, videoFormat: TrueVideoFormat, callBack?: {
39
40
  onUpdateLiveInfo: () => Promise<{
40
41
  title?: string;
41
42
  cover?: string;
@@ -1,7 +1,7 @@
1
1
  import EventEmitter from "node:events";
2
2
  import fs from "fs/promises";
3
3
  import { createRecordExtraDataController } from "../xml_stream_controller.js";
4
- import { replaceExtName, ensureFolderExist, isFfmpegStartSegment, isMesioStartSegment, isBililiveStartSegment, isFfmpegStart, retry, cleanTerminalText, } from "../utils.js";
4
+ import { ensureFolderExist, isFfmpegStartSegment, isMesioStartSegment, isBililiveStartSegment, isFfmpegStart, retry, cleanTerminalText, } from "../utils.js";
5
5
  export class Segment extends EventEmitter {
6
6
  extraDataController = null;
7
7
  init = true;
@@ -10,10 +10,12 @@ export class Segment extends EventEmitter {
10
10
  rawRecordingVideoPath;
11
11
  /** 输出文件名名,不包含拓展名 */
12
12
  outputVideoFilePath;
13
+ disableDanma;
13
14
  videoExt;
14
- constructor(getSavePath, videoExt) {
15
+ constructor(getSavePath, disableDanma, videoExt) {
15
16
  super();
16
17
  this.getSavePath = getSavePath;
18
+ this.disableDanma = disableDanma;
17
19
  this.videoExt = videoExt;
18
20
  }
19
21
  async handleSegmentEnd() {
@@ -67,7 +69,9 @@ export class Segment extends EventEmitter {
67
69
  title: liveInfo?.title,
68
70
  });
69
71
  ensureFolderExist(this.outputVideoFilePath);
70
- this.extraDataController = createRecordExtraDataController(`${this.outputVideoFilePath}.xml`);
72
+ if (!this.disableDanma) {
73
+ this.extraDataController = createRecordExtraDataController(`${this.outputVideoFilePath}.xml`);
74
+ }
71
75
  // 支持两种格式的正则表达式
72
76
  // 1. FFmpeg格式: Opening 'filename' for writing
73
77
  // 2. Mesio格式: Opening FLV segment path=filename Processing
@@ -106,7 +110,7 @@ export class StreamManager extends EventEmitter {
106
110
  recorderType;
107
111
  videoFormat;
108
112
  callBack;
109
- constructor(getSavePath, hasSegment, recorderType, videoFormat, callBack) {
113
+ constructor(getSavePath, hasSegment, disableDanma, recorderType, videoFormat, callBack) {
110
114
  super();
111
115
  const recordSavePath = getSavePath({ startTime: Date.now() });
112
116
  this.recordSavePath = recordSavePath;
@@ -115,7 +119,7 @@ export class StreamManager extends EventEmitter {
115
119
  this.hasSegment = hasSegment;
116
120
  this.callBack = callBack;
117
121
  if (hasSegment) {
118
- this.segment = new Segment(getSavePath, this.videoExt);
122
+ this.segment = new Segment(getSavePath, disableDanma, this.videoExt);
119
123
  this.segment.on("DebugLog", (data) => {
120
124
  this.emit("DebugLog", data);
121
125
  });
@@ -127,8 +131,10 @@ export class StreamManager extends EventEmitter {
127
131
  });
128
132
  }
129
133
  else {
130
- const extraDataSavePath = replaceExtName(recordSavePath, ".xml");
131
- this.extraDataController = createRecordExtraDataController(extraDataSavePath);
134
+ const extraDataSavePath = `${recordSavePath}.xml`;
135
+ if (!disableDanma) {
136
+ this.extraDataController = createRecordExtraDataController(extraDataSavePath);
137
+ }
132
138
  }
133
139
  }
134
140
  async handleVideoStarted(stderrLine) {
package/lib/manager.d.ts CHANGED
@@ -77,6 +77,7 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
77
77
  recorders: Recorder<E>[];
78
78
  addRecorder: (this: RecorderManager<ME, P, PE, E>, opts: RecorderCreateOpts<E>) => Recorder<E>;
79
79
  removeRecorder: (this: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>) => void;
80
+ getRecorder: (this: RecorderManager<ME, P, PE, E>, id: string) => Recorder<E> | null;
80
81
  startRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
81
82
  stopRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
82
83
  cutRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
package/lib/manager.js CHANGED
@@ -194,6 +194,10 @@ export function createRecorderManager(opts) {
194
194
  delete tempBanObj[recorder.channelId];
195
195
  this.emit("RecorderRemoved", recorder.toJSON());
196
196
  },
197
+ getRecorder(id) {
198
+ const recorder = this.recorders.find((item) => item.id === id);
199
+ return recorder ?? null;
200
+ },
197
201
  async startRecord(id) {
198
202
  const recorder = this.recorders.find((item) => item.id === id);
199
203
  if (recorder == null)
package/lib/recorder.d.ts CHANGED
@@ -40,8 +40,8 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
40
40
  formatName?: FormatName;
41
41
  /** 流编码 */
42
42
  codecName?: CodecName;
43
- /** 选择使用的api,虎牙支持: auto,web,mp,抖音支持:web,webHTML,mobile,userHTML */
44
- api?: "auto" | "web" | "mp" | "webHTML" | "mobile" | "userHTML";
43
+ /** 选择使用的api,虎牙支持: auto,web,mp,wup,抖音支持:web,webHTML,mobile,userHTML */
44
+ api?: "auto" | "web" | "mp" | "wup" | "webHTML" | "mobile" | "userHTML" | "balance" | "random" | string;
45
45
  /** 标题关键词,如果直播间标题包含这些关键词,则不会自动录制(仅对斗鱼有效),多个关键词用英文逗号分隔 */
46
46
  titleKeywords?: string;
47
47
  /** 用于指定录制文件格式,auto时,分段使用ts,不分段使用mp4 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/manager",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Batch scheduling recorders",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",