@bililive-tools/manager 1.8.0 → 1.9.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.
- package/README.md +16 -13
- package/lib/common.d.ts +1 -2
- package/lib/common.js +0 -1
- package/lib/index.d.ts +1 -0
- package/lib/manager.d.ts +3 -1
- package/lib/manager.js +9 -10
- package/lib/recorder/BililiveRecorder.js +2 -3
- package/lib/recorder/FFMPEGRecorder.d.ts +4 -2
- package/lib/recorder/FFMPEGRecorder.js +21 -6
- package/lib/recorder/IRecorder.d.ts +6 -2
- package/lib/recorder/mesioRecorder.js +3 -2
- package/lib/recorder/streamManager.d.ts +1 -1
- package/lib/recorder.d.ts +7 -3
- package/lib/utils.d.ts +1 -1
- package/lib/utils.js +2 -2
- package/lib/xml_stream_controller.d.ts +1 -1
- package/lib/xml_stream_controller.js +26 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,19 +94,22 @@ setBililivePath("BililiveRecorder.Cli.exe");
|
|
|
94
94
|
|
|
95
95
|
默认值为 `{platform}/{owner}/{year}-{month}-{date} {hour}-{min}-{sec} {title}`
|
|
96
96
|
|
|
97
|
-
| 值
|
|
98
|
-
|
|
|
99
|
-
| {platform}
|
|
100
|
-
| {channelId}
|
|
101
|
-
| {remarks}
|
|
102
|
-
| {owner}
|
|
103
|
-
| {title}
|
|
104
|
-
| {year}
|
|
105
|
-
| {month}
|
|
106
|
-
| {date}
|
|
107
|
-
| {hour}
|
|
108
|
-
| {min}
|
|
109
|
-
| {sec}
|
|
97
|
+
| 值 | 标签 |
|
|
98
|
+
| ----------------- | ------------------------------------------ |
|
|
99
|
+
| {platform} | 平台 |
|
|
100
|
+
| {channelId} | 房间号 |
|
|
101
|
+
| {remarks} | 备注 |
|
|
102
|
+
| {owner} | 主播名 |
|
|
103
|
+
| {title} | 标题 |
|
|
104
|
+
| {year} | 年 |
|
|
105
|
+
| {month} | 月 |
|
|
106
|
+
| {date} | 日 |
|
|
107
|
+
| {hour} | 时 |
|
|
108
|
+
| {min} | 分 |
|
|
109
|
+
| {sec} | 秒 |
|
|
110
|
+
| {startTime} | 分段开始时间,Date对象 |
|
|
111
|
+
| {recordStartTime} | 录制开始时间,Date对象 |
|
|
112
|
+
| {liveStartTime} | 直播开始时间,Date对象,抖音同录制开始时间 |
|
|
110
113
|
|
|
111
114
|
## 事件
|
|
112
115
|
|
package/lib/common.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { AnyObject, UnknownObject } from "./utils.js";
|
|
2
2
|
export type ChannelId = string;
|
|
3
3
|
export declare const Qualities: readonly ["lowest", "low", "medium", "high", "highest"];
|
|
4
|
-
export declare const BiliQualities: readonly [30000, 20000, 10000, 400, 250, 150, 80];
|
|
5
4
|
export declare const DouyuQualities: readonly [0, 2, 3, 4, 8];
|
|
6
5
|
export declare const HuYaQualities: readonly [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500, -1];
|
|
7
6
|
export declare const DouYinQualities: readonly ["origin", "uhd", "hd", "sd", "ld", "ao", "real_origin"];
|
|
8
|
-
export type Quality =
|
|
7
|
+
export type Quality = string | number;
|
|
9
8
|
export interface MessageSender<E extends AnyObject = UnknownObject> {
|
|
10
9
|
uid?: string;
|
|
11
10
|
name: string;
|
package/lib/common.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export const Qualities = ["lowest", "low", "medium", "high", "highest"];
|
|
2
|
-
export const BiliQualities = [30000, 20000, 10000, 400, 250, 150, 80];
|
|
3
2
|
export const DouyuQualities = [0, 2, 3, 4, 8];
|
|
4
3
|
// 14100: 2K HDR;14000:2K;4200:HDR(10M);0:原画;8000:蓝光8M;4000:蓝光4M;2000:超清;500:流畅
|
|
5
4
|
export const HuYaQualities = [
|
package/lib/index.d.ts
CHANGED
|
@@ -24,3 +24,4 @@ export declare function getMesioPath(): string;
|
|
|
24
24
|
export declare function setBililivePath(newPath: string): void;
|
|
25
25
|
export declare function getBililivePath(): string;
|
|
26
26
|
export declare function getDataFolderPath<E extends AnyObject>(provider: RecorderProvider<E>): string;
|
|
27
|
+
export type VideoFormat = "auto" | "ts" | "mkv" | "flv" | "mp4" | "m4s";
|
package/lib/manager.d.ts
CHANGED
|
@@ -103,7 +103,9 @@ export declare function createRecorderManager<ME extends AnyObject = UnknownObje
|
|
|
103
103
|
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: {
|
|
104
104
|
owner: string;
|
|
105
105
|
title: string;
|
|
106
|
-
startTime
|
|
106
|
+
startTime: number;
|
|
107
|
+
liveStartTime: Date;
|
|
108
|
+
recordStartTime: Date;
|
|
107
109
|
}): string;
|
|
108
110
|
export type GetProviderExtra<P> = P extends RecorderProvider<infer E> ? E : never;
|
|
109
111
|
export { StreamManager, Cache };
|
package/lib/manager.js
CHANGED
|
@@ -312,12 +312,12 @@ export function genSavePathFromRule(manager, recorder, extData) {
|
|
|
312
312
|
// TODO: 这里随便写的,后面再优化
|
|
313
313
|
const provider = manager.providers.find((p) => p.id === recorder.toJSON().providerId);
|
|
314
314
|
const now = extData?.startTime ? new Date(extData.startTime) : new Date();
|
|
315
|
-
const owner = (extData?.owner ?? "").replaceAll("%", "_");
|
|
316
|
-
const title = (extData?.title ?? "").replaceAll("%", "_");
|
|
315
|
+
const owner = removeSystemReservedChars((extData?.owner ?? "").replaceAll("%", "_"));
|
|
316
|
+
const title = removeSystemReservedChars((extData?.title ?? "").replaceAll("%", "_"));
|
|
317
|
+
const remarks = removeSystemReservedChars((recorder.remarks ?? "").replaceAll("%", "_"));
|
|
318
|
+
const channelId = removeSystemReservedChars(String(recorder.channelId));
|
|
317
319
|
const params = {
|
|
318
320
|
platform: provider?.name ?? "unknown",
|
|
319
|
-
channelId: recorder.channelId,
|
|
320
|
-
remarks: recorder.remarks ?? "",
|
|
321
321
|
year: formatDate(now, "yyyy"),
|
|
322
322
|
month: formatDate(now, "MM"),
|
|
323
323
|
date: formatDate(now, "dd"),
|
|
@@ -325,20 +325,19 @@ export function genSavePathFromRule(manager, recorder, extData) {
|
|
|
325
325
|
min: formatDate(now, "mm"),
|
|
326
326
|
sec: formatDate(now, "ss"),
|
|
327
327
|
...extData,
|
|
328
|
+
startTime: now,
|
|
328
329
|
owner: owner,
|
|
329
330
|
title: title,
|
|
331
|
+
remarks: remarks,
|
|
332
|
+
channelId,
|
|
330
333
|
};
|
|
331
|
-
if (manager.autoRemoveSystemReservedChars) {
|
|
332
|
-
for (const key in params) {
|
|
333
|
-
params[key] = removeSystemReservedChars(String(params[key])).trim();
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
334
|
let savePathRule = manager.savePathRule;
|
|
337
335
|
try {
|
|
338
336
|
savePathRule = ejs.render(savePathRule, params);
|
|
337
|
+
console.log("解析后保存路径模板:", savePathRule, params);
|
|
339
338
|
}
|
|
340
339
|
catch (error) {
|
|
341
|
-
console.error("模板解析错误", error);
|
|
340
|
+
console.error("模板解析错误", error, savePathRule, params);
|
|
342
341
|
}
|
|
343
342
|
return formatTemplate(savePathRule, params);
|
|
344
343
|
}
|
|
@@ -43,7 +43,6 @@ class BililiveRecorderCommand extends EventEmitter {
|
|
|
43
43
|
run() {
|
|
44
44
|
const args = this._getArguments();
|
|
45
45
|
const bililiveExecutable = getBililivePath();
|
|
46
|
-
console.log("Starting BililiveRecorder with args:", bililiveExecutable, args);
|
|
47
46
|
this.process = spawn(bililiveExecutable, args, {
|
|
48
47
|
stdio: ["pipe", "pipe", "pipe"],
|
|
49
48
|
});
|
|
@@ -116,8 +115,8 @@ export class BililiveRecorder extends EventEmitter {
|
|
|
116
115
|
this.segment = opts.segment;
|
|
117
116
|
this.headers = opts.headers;
|
|
118
117
|
this.command = this.createCommand();
|
|
119
|
-
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename }) => {
|
|
120
|
-
this.emit("videoFileCreated", { filename, cover, rawFilename });
|
|
118
|
+
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
|
|
119
|
+
this.emit("videoFileCreated", { filename, cover, rawFilename, title });
|
|
121
120
|
});
|
|
122
121
|
this.streamManager.on("videoFileCompleted", ({ filename }) => {
|
|
123
122
|
this.emit("videoFileCompleted", { filename });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
import { IRecorder, FFMPEGRecorderOptions } from "./IRecorder.js";
|
|
3
|
-
import
|
|
3
|
+
import { FormatName } from "./index.js";
|
|
4
|
+
import type { VideoFormat } from "../index.js";
|
|
4
5
|
export declare class FFMPEGRecorder extends EventEmitter implements IRecorder {
|
|
5
6
|
private onEnd;
|
|
6
7
|
private onUpdateLiveInfo;
|
|
@@ -20,7 +21,7 @@ export declare class FFMPEGRecorder extends EventEmitter implements IRecorder {
|
|
|
20
21
|
readonly disableDanma: boolean;
|
|
21
22
|
readonly url: string;
|
|
22
23
|
formatName: FormatName;
|
|
23
|
-
videoFormat:
|
|
24
|
+
videoFormat: VideoFormat;
|
|
24
25
|
readonly debugLevel: "none" | "basic" | "verbose";
|
|
25
26
|
readonly headers: {
|
|
26
27
|
[key: string]: string | undefined;
|
|
@@ -30,6 +31,7 @@ export declare class FFMPEGRecorder extends EventEmitter implements IRecorder {
|
|
|
30
31
|
cover?: string;
|
|
31
32
|
}>);
|
|
32
33
|
createCommand(): import("@renmu/fluent-ffmpeg").FfmpegCommand;
|
|
34
|
+
buildOutputOptions(): string[];
|
|
33
35
|
formatLine(line: string): {
|
|
34
36
|
time: string | null;
|
|
35
37
|
} | null;
|
|
@@ -37,7 +37,7 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
37
37
|
let videoFormat = opts.videoFormat ?? "auto";
|
|
38
38
|
if (videoFormat === "auto") {
|
|
39
39
|
if (!this.hasSegment) {
|
|
40
|
-
videoFormat = "
|
|
40
|
+
videoFormat = "m4s";
|
|
41
41
|
if (this.formatName === "ts") {
|
|
42
42
|
videoFormat = "ts";
|
|
43
43
|
}
|
|
@@ -59,8 +59,8 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
59
59
|
this.segment = opts.segment;
|
|
60
60
|
this.headers = opts.headers;
|
|
61
61
|
this.command = this.createCommand();
|
|
62
|
-
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename }) => {
|
|
63
|
-
this.emit("videoFileCreated", { filename, cover, rawFilename });
|
|
62
|
+
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
|
|
63
|
+
this.emit("videoFileCreated", { filename, cover, rawFilename, title });
|
|
64
64
|
});
|
|
65
65
|
this.streamManager.on("videoFileCompleted", ({ filename }) => {
|
|
66
66
|
this.emit("videoFileCompleted", { filename });
|
|
@@ -95,10 +95,11 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
95
95
|
inputOptions.push("-headers", headers.join("\\r\\n"));
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
+
const outputOptions = this.buildOutputOptions();
|
|
98
99
|
const command = createFFMPEGBuilder()
|
|
99
100
|
.input(this.url)
|
|
100
101
|
.inputOptions(inputOptions)
|
|
101
|
-
.outputOptions(
|
|
102
|
+
.outputOptions(outputOptions)
|
|
102
103
|
.output(this.streamManager.videoFilePath)
|
|
103
104
|
.on("error", this.onEnd)
|
|
104
105
|
.on("end", () => this.onEnd("finished"))
|
|
@@ -116,10 +117,24 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
116
117
|
}
|
|
117
118
|
})
|
|
118
119
|
.on("stderr", this.timeoutChecker?.update);
|
|
120
|
+
return command;
|
|
121
|
+
}
|
|
122
|
+
buildOutputOptions() {
|
|
123
|
+
const options = [];
|
|
124
|
+
options.push(...this.ffmpegOutputOptions);
|
|
125
|
+
options.push("-c", "copy", "-movflags", "+frag_keyframe+empty_moov+separate_moof", "-fflags", "+genpts+igndts", "-min_frag_duration", "10000000");
|
|
119
126
|
if (this.hasSegment) {
|
|
120
|
-
|
|
127
|
+
options.push("-f", "segment", "-segment_time", String(this.segment * 60), "-reset_timestamps", "1");
|
|
128
|
+
if (this.videoFormat === "m4s") {
|
|
129
|
+
options.push("-segment_format", "mp4");
|
|
130
|
+
}
|
|
121
131
|
}
|
|
122
|
-
|
|
132
|
+
else {
|
|
133
|
+
if (this.videoFormat === "m4s") {
|
|
134
|
+
options.push("-f", "mp4");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return options;
|
|
123
138
|
}
|
|
124
139
|
formatLine(line) {
|
|
125
140
|
if (!line.includes("time=")) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { VideoFormat } from "../index.js";
|
|
2
3
|
import type { FormatName } from "./index.js";
|
|
4
|
+
import type { XmlStreamController } from "../xml_stream_controller.js";
|
|
3
5
|
/**
|
|
4
6
|
* 录制器构造函数选项的基础接口
|
|
5
7
|
*/
|
|
@@ -17,7 +19,7 @@ export interface BaseRecorderOptions {
|
|
|
17
19
|
headers?: {
|
|
18
20
|
[key: string]: string | undefined;
|
|
19
21
|
};
|
|
20
|
-
videoFormat?:
|
|
22
|
+
videoFormat?: VideoFormat;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* 录制器接口定义
|
|
@@ -39,12 +41,13 @@ export interface IRecorder extends EventEmitter {
|
|
|
39
41
|
run(): void;
|
|
40
42
|
stop(): Promise<void>;
|
|
41
43
|
getArguments(): string[];
|
|
42
|
-
getExtraDataController():
|
|
44
|
+
getExtraDataController(): XmlStreamController | null;
|
|
43
45
|
createCommand(): any;
|
|
44
46
|
on(event: "videoFileCreated", listener: (data: {
|
|
45
47
|
filename: string;
|
|
46
48
|
cover?: string;
|
|
47
49
|
rawFilename?: string;
|
|
50
|
+
title?: string;
|
|
48
51
|
}) => void): this;
|
|
49
52
|
on(event: "videoFileCompleted", listener: (data: {
|
|
50
53
|
filename: string;
|
|
@@ -59,6 +62,7 @@ export interface IRecorder extends EventEmitter {
|
|
|
59
62
|
filename: string;
|
|
60
63
|
cover?: string;
|
|
61
64
|
rawFilename?: string;
|
|
65
|
+
title?: string;
|
|
62
66
|
}): boolean;
|
|
63
67
|
emit(event: "videoFileCompleted", data: {
|
|
64
68
|
filename: string;
|
|
@@ -128,8 +128,8 @@ export class MesioRecorder extends EventEmitter {
|
|
|
128
128
|
this.segment = opts.segment;
|
|
129
129
|
this.headers = opts.headers;
|
|
130
130
|
this.command = this.createCommand();
|
|
131
|
-
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename }) => {
|
|
132
|
-
this.emit("videoFileCreated", { filename, cover, rawFilename });
|
|
131
|
+
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
|
|
132
|
+
this.emit("videoFileCreated", { filename, cover, rawFilename, title });
|
|
133
133
|
});
|
|
134
134
|
this.streamManager.on("videoFileCompleted", ({ filename }) => {
|
|
135
135
|
this.emit("videoFileCompleted", { filename });
|
|
@@ -144,6 +144,7 @@ export class MesioRecorder extends EventEmitter {
|
|
|
144
144
|
"--fix",
|
|
145
145
|
"-H",
|
|
146
146
|
"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",
|
|
147
|
+
"--no-proxy",
|
|
147
148
|
];
|
|
148
149
|
if (this.debugLevel === "verbose") {
|
|
149
150
|
inputOptions.push("-v");
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
import { createRecordExtraDataController } from "../xml_stream_controller.js";
|
|
3
3
|
import type { RecorderCreateOpts } from "../recorder.js";
|
|
4
|
+
import type { VideoFormat } from "../index.js";
|
|
4
5
|
export type GetSavePath = (data: {
|
|
5
6
|
startTime: number;
|
|
6
7
|
title?: string;
|
|
7
8
|
}) => string;
|
|
8
9
|
type RecorderType = Exclude<RecorderCreateOpts["recorderType"], undefined | "auto">;
|
|
9
|
-
type VideoFormat = "auto" | "ts" | "mkv" | "flv" | "mp4" | "m4s";
|
|
10
10
|
export declare class Segment extends EventEmitter {
|
|
11
11
|
extraDataController: ReturnType<typeof createRecordExtraDataController> | null;
|
|
12
12
|
init: boolean;
|
package/lib/recorder.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ChannelId, Message, Quality } from "./common.js";
|
|
|
3
3
|
import { RecorderProvider } from "./manager.js";
|
|
4
4
|
import { AnyObject, PickRequired, UnknownObject } from "./utils.js";
|
|
5
5
|
import { Cache } from "./cache.js";
|
|
6
|
+
import type { RecorderType } from "./recorder/index.js";
|
|
6
7
|
type FormatName = "auto" | "flv" | "hls" | "fmp4" | "flv_only" | "hls_only" | "fmp4_only";
|
|
7
8
|
type CodecName = "auto" | "avc" | "hevc" | "avc_only" | "hevc_only";
|
|
8
9
|
export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
|
|
@@ -44,7 +45,7 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
|
|
|
44
45
|
/** 标题关键词,如果直播间标题包含这些关键词,则不会自动录制(仅对斗鱼有效),多个关键词用英文逗号分隔 */
|
|
45
46
|
titleKeywords?: string;
|
|
46
47
|
/** 用于指定录制文件格式,auto时,分段使用ts,不分段使用mp4 */
|
|
47
|
-
videoFormat?: "auto" | "ts" | "mkv";
|
|
48
|
+
videoFormat?: "auto" | "ts" | "mkv" | "flv";
|
|
48
49
|
/** 录制类型 */
|
|
49
50
|
recorderType?: "auto" | "ffmpeg" | "mesio" | "bililive";
|
|
50
51
|
/** 流格式优先级 */
|
|
@@ -71,6 +72,7 @@ export interface RecordHandle {
|
|
|
71
72
|
id: string;
|
|
72
73
|
stream: string;
|
|
73
74
|
source: string;
|
|
75
|
+
recorderType?: RecorderType;
|
|
74
76
|
url: string;
|
|
75
77
|
ffmpegArgs?: string[];
|
|
76
78
|
progress?: Progress;
|
|
@@ -85,7 +87,9 @@ export interface DebugLog {
|
|
|
85
87
|
export type GetSavePath = (data: {
|
|
86
88
|
owner: string;
|
|
87
89
|
title: string;
|
|
88
|
-
startTime
|
|
90
|
+
startTime: number;
|
|
91
|
+
liveStartTime: Date;
|
|
92
|
+
recordStartTime: Date;
|
|
89
93
|
}) => string;
|
|
90
94
|
export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
|
|
91
95
|
RecordStart: RecordHandle;
|
|
@@ -121,7 +125,7 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
|
|
|
121
125
|
living: boolean;
|
|
122
126
|
owner: string;
|
|
123
127
|
title: string;
|
|
124
|
-
startTime
|
|
128
|
+
startTime: Date;
|
|
125
129
|
avatar: string;
|
|
126
130
|
cover: string;
|
|
127
131
|
liveId?: string;
|
package/lib/utils.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare function assertStringType(data: unknown, msg?: string): asserts d
|
|
|
34
34
|
export declare function assertNumberType(data: unknown, msg?: string): asserts data is number;
|
|
35
35
|
export declare function assertObjectType(data: unknown, msg?: string): asserts data is object;
|
|
36
36
|
export declare function formatDate(date: Date, format: string): string;
|
|
37
|
-
export declare function removeSystemReservedChars(
|
|
37
|
+
export declare function removeSystemReservedChars(str: string): string;
|
|
38
38
|
export declare function isFfmpegStartSegment(line: string): boolean;
|
|
39
39
|
export declare function isMesioStartSegment(line: string): boolean;
|
|
40
40
|
export declare function isBililiveStartSegment(line: string): boolean;
|
package/lib/utils.js
CHANGED
|
@@ -120,8 +120,8 @@ export function formatDate(date, format) {
|
|
|
120
120
|
};
|
|
121
121
|
return format.replace(/yyyy|MM|dd|HH|mm|ss/g, (matched) => map[matched]);
|
|
122
122
|
}
|
|
123
|
-
export function removeSystemReservedChars(
|
|
124
|
-
return filenamify(
|
|
123
|
+
export function removeSystemReservedChars(str) {
|
|
124
|
+
return filenamify(str, { replacement: "_" });
|
|
125
125
|
}
|
|
126
126
|
export function isFfmpegStartSegment(line) {
|
|
127
127
|
return line.includes("Opening ") && line.includes("for writing");
|
|
@@ -17,7 +17,7 @@ export interface XmlStreamController {
|
|
|
17
17
|
/** 设计上来说,外部程序不应该能直接修改 data 上的东西 */
|
|
18
18
|
readonly data: XmlStreamData;
|
|
19
19
|
addMessage: (message: Message) => void;
|
|
20
|
-
setMeta: (meta: Partial<XmlStreamData["meta"]>) => void
|
|
20
|
+
setMeta: (meta: Partial<XmlStreamData["meta"]>) => Promise<void>;
|
|
21
21
|
flush: () => Promise<void>;
|
|
22
22
|
}
|
|
23
23
|
export declare function createRecordExtraDataController(savePath: string): XmlStreamController;
|
|
@@ -70,13 +70,16 @@ export function createRecordExtraDataController(savePath) {
|
|
|
70
70
|
initializeFile().catch(console.error);
|
|
71
71
|
scheduleWrite();
|
|
72
72
|
};
|
|
73
|
-
const setMeta = (meta) => {
|
|
73
|
+
const setMeta = async (meta) => {
|
|
74
74
|
if (hasCompleted)
|
|
75
75
|
return;
|
|
76
76
|
data.meta = {
|
|
77
77
|
...data.meta,
|
|
78
78
|
...meta,
|
|
79
79
|
};
|
|
80
|
+
// 确保文件已初始化,然后立即更新文件中的metadata
|
|
81
|
+
await initializeFile().catch(console.error);
|
|
82
|
+
await updateMetadataInFile(savePath, data.meta).catch(console.error);
|
|
80
83
|
};
|
|
81
84
|
const flush = async () => {
|
|
82
85
|
if (hasCompleted)
|
|
@@ -89,7 +92,7 @@ export function createRecordExtraDataController(savePath) {
|
|
|
89
92
|
await writeToFile();
|
|
90
93
|
}
|
|
91
94
|
// 完成XML文件(添加结束标签等)
|
|
92
|
-
await finalizeXmlFile(savePath
|
|
95
|
+
await finalizeXmlFile(savePath);
|
|
93
96
|
// 清理内存
|
|
94
97
|
data.pendingMessages = [];
|
|
95
98
|
};
|
|
@@ -206,9 +209,9 @@ async function appendToXmlFile(filePath, content) {
|
|
|
206
209
|
}
|
|
207
210
|
}
|
|
208
211
|
/**
|
|
209
|
-
*
|
|
212
|
+
* 更新XML文件中的metadata
|
|
210
213
|
*/
|
|
211
|
-
async function
|
|
214
|
+
async function updateMetadataInFile(filePath, metadata) {
|
|
212
215
|
try {
|
|
213
216
|
const builder = new XMLBuilder({
|
|
214
217
|
ignoreAttributes: false,
|
|
@@ -228,8 +231,25 @@ async function finalizeXmlFile(filePath, metadata) {
|
|
|
228
231
|
});
|
|
229
232
|
// 读取文件内容
|
|
230
233
|
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
231
|
-
// 替换占位符为实际的metadata
|
|
232
|
-
const
|
|
234
|
+
// 替换占位符为实际的metadata
|
|
235
|
+
const updatedContent = content.replace("<!--METADATA_PLACEHOLDER-->", metadataXml);
|
|
236
|
+
// 写回文件
|
|
237
|
+
await fs.promises.writeFile(filePath, updatedContent);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.error(`更新XML文件metadata失败: ${filePath}`, error);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 完成XML文件写入
|
|
246
|
+
*/
|
|
247
|
+
async function finalizeXmlFile(filePath) {
|
|
248
|
+
try {
|
|
249
|
+
// 读取文件内容
|
|
250
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
251
|
+
// 添加结束标签
|
|
252
|
+
const finalContent = content + "</i>";
|
|
233
253
|
// 写回文件
|
|
234
254
|
await fs.promises.writeFile(filePath, finalContent);
|
|
235
255
|
}
|