koishi-plugin-xhs-parser 0.1.3 → 0.1.4
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 +5 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +105 -3
- package/lib/parser.d.ts +1 -0
- package/lib/parser.js +6 -0
- package/package.json +1 -1
- package/src/index.ts +111 -3
- package/src/parser.ts +8 -0
package/README.md
CHANGED
|
@@ -45,6 +45,8 @@ plugins:
|
|
|
45
45
|
showVideo: true
|
|
46
46
|
downloadVideoAsFile: false
|
|
47
47
|
videoDownloadMode: buffer
|
|
48
|
+
maxDownloadedVideoSizeMB: 20
|
|
49
|
+
maxVideoSendSizeMB: 100
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
说明:
|
|
@@ -54,5 +56,8 @@ plugins:
|
|
|
54
56
|
- `videoDownloadMode: buffer`:以二进制 Buffer 传给 `h.video()`,通常最推荐。
|
|
55
57
|
- `videoDownloadMode: base64`:转换成 `base64://...` 发送,适合部分 OneBot 实现。
|
|
56
58
|
- `videoDownloadMode: file`:写入系统临时目录并以 `file://` URL 发送,适合部分 Napcat / 本地网关场景。
|
|
59
|
+
- `maxDownloadedVideoSizeMB: 20`:下载后的视频超过该大小时,自动回退为发送小红书视频直链,避免大视频转 base64 时超过 Node 字符串长度限制。设为 `0` 表示不限制。
|
|
60
|
+
- `maxVideoSendSizeMB: 100`:视频超过该大小时不发送视频,包括直链。设为 `0` 表示不限制。
|
|
61
|
+
- `loggerinfo: true`:输出视频下载开始、下载结果、大小阈值判断、发送模式和失败原因等调试日志。
|
|
57
62
|
|
|
58
63
|
下载模式只处理每个笔记的首个视频,避免一次消息消耗过多带宽。
|
package/lib/index.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface Config {
|
|
|
20
20
|
showVideo: boolean;
|
|
21
21
|
downloadVideoAsFile: boolean;
|
|
22
22
|
videoDownloadMode: 'buffer' | 'file' | 'base64';
|
|
23
|
+
maxDownloadedVideoSizeMB: number;
|
|
24
|
+
maxVideoSendSizeMB: number;
|
|
23
25
|
showStats: boolean;
|
|
24
26
|
showLink: boolean;
|
|
25
27
|
showError: boolean;
|
package/lib/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const node_fs_1 = require("node:fs");
|
|
|
13
13
|
const parser_1 = require("./parser");
|
|
14
14
|
exports.name = 'xhs-parser';
|
|
15
15
|
const logger = new koishi_1.Logger(exports.name);
|
|
16
|
+
const VIDEO_TOO_LARGE_MESSAGE = '[视频文件过大,跳过解析]';
|
|
16
17
|
exports.Config = koishi_1.Schema.intersect([
|
|
17
18
|
koishi_1.Schema.object({
|
|
18
19
|
enabled: koishi_1.Schema.boolean().default(true).description('开启小红书链接/卡片解析。'),
|
|
@@ -52,6 +53,8 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
52
53
|
koishi_1.Schema.const('base64').description('使用 base64:// 段发送视频(OneBot 常用格式,buffer 失败时可尝试)'),
|
|
53
54
|
koishi_1.Schema.const('file').description('写入临时文件并通过 file:// URL 发送(Napcat 等特殊环境)'),
|
|
54
55
|
]).default('buffer').description('下载视频后的发送方式。'),
|
|
56
|
+
maxDownloadedVideoSizeMB: koishi_1.Schema.number().min(0).max(2048).step(1).default(20).description('下载视频大小上限,单位 MB。超过后自动回退为发送视频直链;设为 0 表示不限制。'),
|
|
57
|
+
maxVideoSendSizeMB: koishi_1.Schema.number().min(0).max(2048).step(1).default(100).description('视频发送大小上限,单位 MB。超过后不发送视频,包括直链;设为 0 表示不限制。'),
|
|
55
58
|
showStats: koishi_1.Schema.boolean().default(true).description('展示点赞、收藏、评论、分享数据。'),
|
|
56
59
|
showLink: koishi_1.Schema.boolean().default(true).description('展示原文链接。'),
|
|
57
60
|
}).description('内容设置'),
|
|
@@ -136,12 +139,41 @@ async function handleLinks(ctx, session, links, config) {
|
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
async function prepareNoteVideo(ctx, note, config) {
|
|
139
|
-
if (!config.
|
|
142
|
+
if (!config.showVideo) {
|
|
143
|
+
if (config.loggerinfo) {
|
|
144
|
+
logger.info('skip video: showVideo=false');
|
|
145
|
+
}
|
|
140
146
|
return note;
|
|
147
|
+
}
|
|
141
148
|
const firstVideo = note.videoUrls[0];
|
|
142
|
-
if (!firstVideo
|
|
149
|
+
if (!firstVideo) {
|
|
150
|
+
if (config.loggerinfo)
|
|
151
|
+
logger.info('skip video download: no video URL found');
|
|
152
|
+
return note;
|
|
153
|
+
}
|
|
154
|
+
if (!/^https?:\/\//i.test(firstVideo)) {
|
|
155
|
+
if (config.loggerinfo)
|
|
156
|
+
logger.info(`skip video download: unsupported video URL protocol (${firstVideo})`);
|
|
143
157
|
return note;
|
|
158
|
+
}
|
|
159
|
+
const maxSendSizeBytes = getSizeLimitBytes(config.maxVideoSendSizeMB, 100);
|
|
160
|
+
const remoteSize = await getRemoteVideoSize(ctx, firstVideo, config);
|
|
161
|
+
if (remoteSize !== undefined && maxSendSizeBytes && remoteSize > maxSendSizeBytes) {
|
|
162
|
+
if (config.loggerinfo) {
|
|
163
|
+
logger.info(`skip video send: remote size=${formatBytes(remoteSize)}, max=${formatBytes(maxSendSizeBytes)}, url=${firstVideo}`);
|
|
164
|
+
}
|
|
165
|
+
return removeNoteVideos(note);
|
|
166
|
+
}
|
|
167
|
+
if (!config.downloadVideoAsFile) {
|
|
168
|
+
if (config.loggerinfo) {
|
|
169
|
+
logger.info('skip video download: downloadVideoAsFile=false, use direct URL');
|
|
170
|
+
}
|
|
171
|
+
return note;
|
|
172
|
+
}
|
|
144
173
|
try {
|
|
174
|
+
if (config.loggerinfo) {
|
|
175
|
+
logger.info(`download first video start: url=${firstVideo}`);
|
|
176
|
+
}
|
|
145
177
|
const file = await ctx.http.file(firstVideo);
|
|
146
178
|
if (!file?.data) {
|
|
147
179
|
if (config.loggerinfo)
|
|
@@ -151,11 +183,29 @@ async function prepareNoteVideo(ctx, note, config) {
|
|
|
151
183
|
const buffer = Buffer.from(file.data);
|
|
152
184
|
const mimeType = file.type || file.mime || 'video/mp4';
|
|
153
185
|
const mode = config.videoDownloadMode || 'buffer';
|
|
186
|
+
const maxSizeBytes = config.maxDownloadedVideoSizeMB > 0
|
|
187
|
+
? config.maxDownloadedVideoSizeMB * 1024 * 1024
|
|
188
|
+
: 0;
|
|
154
189
|
if (config.loggerinfo) {
|
|
155
|
-
logger.info(`
|
|
190
|
+
logger.info(`download first video success: size=${formatBytes(buffer.length)}, mime=${mimeType}, mode=${mode}, max=${maxSizeBytes ? formatBytes(maxSizeBytes) : 'unlimited'}`);
|
|
191
|
+
}
|
|
192
|
+
if (maxSendSizeBytes && buffer.length > maxSendSizeBytes) {
|
|
193
|
+
if (config.loggerinfo) {
|
|
194
|
+
logger.info(`skip video send: downloaded size=${formatBytes(buffer.length)}, max=${formatBytes(maxSendSizeBytes)}`);
|
|
195
|
+
}
|
|
196
|
+
return removeNoteVideos(note);
|
|
197
|
+
}
|
|
198
|
+
if (maxSizeBytes && buffer.length > maxSizeBytes) {
|
|
199
|
+
if (config.loggerinfo) {
|
|
200
|
+
logger.info(`downloaded video exceeds limit: size=${formatBytes(buffer.length)}, max=${formatBytes(maxSizeBytes)}, fallback=direct URL`);
|
|
201
|
+
}
|
|
202
|
+
return note;
|
|
156
203
|
}
|
|
157
204
|
const remainingVideoUrls = note.videoUrls.slice(1);
|
|
158
205
|
if (mode === 'buffer') {
|
|
206
|
+
if (config.loggerinfo) {
|
|
207
|
+
logger.info(`use downloaded video buffer: size=${formatBytes(buffer.length)}, remainingVideoUrls=${remainingVideoUrls.length}`);
|
|
208
|
+
}
|
|
159
209
|
return {
|
|
160
210
|
...note,
|
|
161
211
|
videoBuffer: buffer,
|
|
@@ -166,6 +216,9 @@ async function prepareNoteVideo(ctx, note, config) {
|
|
|
166
216
|
const replacement = mode === 'file'
|
|
167
217
|
? await createTempVideoFile(buffer, mimeType)
|
|
168
218
|
: `base64://${buffer.toString('base64')}`;
|
|
219
|
+
if (config.loggerinfo) {
|
|
220
|
+
logger.info(`use downloaded video ${mode}: src=${mode === 'file' ? replacement : `base64://${formatBytes(buffer.length)} raw`}, remainingVideoUrls=${remainingVideoUrls.length}`);
|
|
221
|
+
}
|
|
169
222
|
return {
|
|
170
223
|
...note,
|
|
171
224
|
videoBuffer: undefined,
|
|
@@ -179,6 +232,55 @@ async function prepareNoteVideo(ctx, note, config) {
|
|
|
179
232
|
return note;
|
|
180
233
|
}
|
|
181
234
|
}
|
|
235
|
+
async function getRemoteVideoSize(ctx, url, config) {
|
|
236
|
+
const maxSendSizeBytes = getSizeLimitBytes(config.maxVideoSendSizeMB, 100);
|
|
237
|
+
if (!maxSendSizeBytes)
|
|
238
|
+
return undefined;
|
|
239
|
+
try {
|
|
240
|
+
if (config.loggerinfo)
|
|
241
|
+
logger.info(`check remote video size start: url=${url}`);
|
|
242
|
+
const headers = await ctx.http.head(url, { timeout: config.timeout * 1000 });
|
|
243
|
+
const contentLength = headers.get('content-length');
|
|
244
|
+
if (!contentLength) {
|
|
245
|
+
if (config.loggerinfo)
|
|
246
|
+
logger.info('check remote video size skipped: missing content-length');
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
const size = Number.parseInt(contentLength, 10);
|
|
250
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
251
|
+
if (config.loggerinfo)
|
|
252
|
+
logger.info(`check remote video size skipped: invalid content-length=${contentLength}`);
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
if (config.loggerinfo)
|
|
256
|
+
logger.info(`check remote video size success: size=${formatBytes(size)}, max=${formatBytes(maxSendSizeBytes)}`);
|
|
257
|
+
return size;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (config.loggerinfo)
|
|
261
|
+
logger.info(`check remote video size failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function removeNoteVideos(note) {
|
|
266
|
+
return {
|
|
267
|
+
...note,
|
|
268
|
+
videoBuffer: undefined,
|
|
269
|
+
videoUrls: [],
|
|
270
|
+
videoSkippedMessage: VIDEO_TOO_LARGE_MESSAGE,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function getSizeLimitBytes(sizeMB, fallbackMB) {
|
|
274
|
+
const normalized = sizeMB ?? fallbackMB;
|
|
275
|
+
return normalized > 0 ? normalized * 1024 * 1024 : 0;
|
|
276
|
+
}
|
|
277
|
+
function formatBytes(bytes) {
|
|
278
|
+
if (bytes < 1024)
|
|
279
|
+
return `${bytes} B`;
|
|
280
|
+
if (bytes < 1024 * 1024)
|
|
281
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
282
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
283
|
+
}
|
|
182
284
|
async function createTempVideoFile(buffer, mimeType) {
|
|
183
285
|
const fileName = `xhs-video-${Date.now()}-${Math.random().toString(16).slice(2)}${getVideoFileExtension(mimeType)}`;
|
|
184
286
|
const filePath = node_path_1.default.join(node_os_1.default.tmpdir(), fileName);
|
package/lib/parser.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface XhsNote {
|
|
|
28
28
|
videoUrls: string[];
|
|
29
29
|
videoBuffer?: Buffer;
|
|
30
30
|
videoMimeType?: string;
|
|
31
|
+
videoSkippedMessage?: string;
|
|
31
32
|
}
|
|
32
33
|
export declare function extractXhsLinks(content: string): string[];
|
|
33
34
|
export declare function resolveXhsLink(rawUrl: string, config: XhsConfigLike): Promise<string>;
|
package/lib/parser.js
CHANGED
|
@@ -83,6 +83,12 @@ function buildXhsMessages(note, config, session) {
|
|
|
83
83
|
}, koishi_1.h.video(videoUrl)));
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
if (note.videoSkippedMessage) {
|
|
87
|
+
messages.push((0, koishi_1.h)('message', {
|
|
88
|
+
userId: session?.userId,
|
|
89
|
+
nickname: session?.author?.nickname || session?.username,
|
|
90
|
+
}, koishi_1.h.text(note.videoSkippedMessage)));
|
|
91
|
+
}
|
|
86
92
|
return messages;
|
|
87
93
|
}
|
|
88
94
|
function extractInitialStateNote(html) {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { buildXhsMessages, extractXhsLinks, fetchXhsNote, XhsNote } from './pars
|
|
|
8
8
|
export const name = 'xhs-parser'
|
|
9
9
|
|
|
10
10
|
const logger = new Logger(name)
|
|
11
|
+
const VIDEO_TOO_LARGE_MESSAGE = '[视频文件过大,跳过解析]'
|
|
11
12
|
|
|
12
13
|
export interface Config {
|
|
13
14
|
enabled: boolean
|
|
@@ -29,6 +30,8 @@ export interface Config {
|
|
|
29
30
|
showVideo: boolean
|
|
30
31
|
downloadVideoAsFile: boolean
|
|
31
32
|
videoDownloadMode: 'buffer' | 'file' | 'base64'
|
|
33
|
+
maxDownloadedVideoSizeMB: number
|
|
34
|
+
maxVideoSendSizeMB: number
|
|
32
35
|
showStats: boolean
|
|
33
36
|
showLink: boolean
|
|
34
37
|
showError: boolean
|
|
@@ -74,6 +77,8 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
74
77
|
Schema.const('base64').description('使用 base64:// 段发送视频(OneBot 常用格式,buffer 失败时可尝试)'),
|
|
75
78
|
Schema.const('file').description('写入临时文件并通过 file:// URL 发送(Napcat 等特殊环境)'),
|
|
76
79
|
]).default('buffer').description('下载视频后的发送方式。'),
|
|
80
|
+
maxDownloadedVideoSizeMB: Schema.number().min(0).max(2048).step(1).default(20).description('下载视频大小上限,单位 MB。超过后自动回退为发送视频直链;设为 0 表示不限制。'),
|
|
81
|
+
maxVideoSendSizeMB: Schema.number().min(0).max(2048).step(1).default(100).description('视频发送大小上限,单位 MB。超过后不发送视频,包括直链;设为 0 表示不限制。'),
|
|
77
82
|
showStats: Schema.boolean().default(true).description('展示点赞、收藏、评论、分享数据。'),
|
|
78
83
|
showLink: Schema.boolean().default(true).description('展示原文链接。'),
|
|
79
84
|
}).description('内容设置'),
|
|
@@ -164,12 +169,44 @@ async function handleLinks(ctx: Context, session: any, links: string[], config:
|
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
async function prepareNoteVideo(ctx: Context, note: XhsNote, config: Config): Promise<XhsNote> {
|
|
167
|
-
if (!config.
|
|
172
|
+
if (!config.showVideo) {
|
|
173
|
+
if (config.loggerinfo) {
|
|
174
|
+
logger.info('skip video: showVideo=false')
|
|
175
|
+
}
|
|
176
|
+
return note
|
|
177
|
+
}
|
|
168
178
|
|
|
169
179
|
const firstVideo = note.videoUrls[0]
|
|
170
|
-
if (!firstVideo
|
|
180
|
+
if (!firstVideo) {
|
|
181
|
+
if (config.loggerinfo) logger.info('skip video download: no video URL found')
|
|
182
|
+
return note
|
|
183
|
+
}
|
|
184
|
+
if (!/^https?:\/\//i.test(firstVideo)) {
|
|
185
|
+
if (config.loggerinfo) logger.info(`skip video download: unsupported video URL protocol (${firstVideo})`)
|
|
186
|
+
return note
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const maxSendSizeBytes = getSizeLimitBytes(config.maxVideoSendSizeMB, 100)
|
|
190
|
+
const remoteSize = await getRemoteVideoSize(ctx, firstVideo, config)
|
|
191
|
+
if (remoteSize !== undefined && maxSendSizeBytes && remoteSize > maxSendSizeBytes) {
|
|
192
|
+
if (config.loggerinfo) {
|
|
193
|
+
logger.info(`skip video send: remote size=${formatBytes(remoteSize)}, max=${formatBytes(maxSendSizeBytes)}, url=${firstVideo}`)
|
|
194
|
+
}
|
|
195
|
+
return removeNoteVideos(note)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!config.downloadVideoAsFile) {
|
|
199
|
+
if (config.loggerinfo) {
|
|
200
|
+
logger.info('skip video download: downloadVideoAsFile=false, use direct URL')
|
|
201
|
+
}
|
|
202
|
+
return note
|
|
203
|
+
}
|
|
171
204
|
|
|
172
205
|
try {
|
|
206
|
+
if (config.loggerinfo) {
|
|
207
|
+
logger.info(`download first video start: url=${firstVideo}`)
|
|
208
|
+
}
|
|
209
|
+
|
|
173
210
|
const file = await ctx.http.file(firstVideo)
|
|
174
211
|
if (!file?.data) {
|
|
175
212
|
if (config.loggerinfo) logger.info('download video failed: empty response data')
|
|
@@ -179,14 +216,34 @@ async function prepareNoteVideo(ctx: Context, note: XhsNote, config: Config): Pr
|
|
|
179
216
|
const buffer = Buffer.from(file.data)
|
|
180
217
|
const mimeType = (file as any).type || (file as any).mime || 'video/mp4'
|
|
181
218
|
const mode = config.videoDownloadMode || 'buffer'
|
|
219
|
+
const maxSizeBytes = config.maxDownloadedVideoSizeMB > 0
|
|
220
|
+
? config.maxDownloadedVideoSizeMB * 1024 * 1024
|
|
221
|
+
: 0
|
|
182
222
|
|
|
183
223
|
if (config.loggerinfo) {
|
|
184
|
-
logger.info(`
|
|
224
|
+
logger.info(`download first video success: size=${formatBytes(buffer.length)}, mime=${mimeType}, mode=${mode}, max=${maxSizeBytes ? formatBytes(maxSizeBytes) : 'unlimited'}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (maxSendSizeBytes && buffer.length > maxSendSizeBytes) {
|
|
228
|
+
if (config.loggerinfo) {
|
|
229
|
+
logger.info(`skip video send: downloaded size=${formatBytes(buffer.length)}, max=${formatBytes(maxSendSizeBytes)}`)
|
|
230
|
+
}
|
|
231
|
+
return removeNoteVideos(note)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (maxSizeBytes && buffer.length > maxSizeBytes) {
|
|
235
|
+
if (config.loggerinfo) {
|
|
236
|
+
logger.info(`downloaded video exceeds limit: size=${formatBytes(buffer.length)}, max=${formatBytes(maxSizeBytes)}, fallback=direct URL`)
|
|
237
|
+
}
|
|
238
|
+
return note
|
|
185
239
|
}
|
|
186
240
|
|
|
187
241
|
const remainingVideoUrls = note.videoUrls.slice(1)
|
|
188
242
|
|
|
189
243
|
if (mode === 'buffer') {
|
|
244
|
+
if (config.loggerinfo) {
|
|
245
|
+
logger.info(`use downloaded video buffer: size=${formatBytes(buffer.length)}, remainingVideoUrls=${remainingVideoUrls.length}`)
|
|
246
|
+
}
|
|
190
247
|
return {
|
|
191
248
|
...note,
|
|
192
249
|
videoBuffer: buffer,
|
|
@@ -199,6 +256,10 @@ async function prepareNoteVideo(ctx: Context, note: XhsNote, config: Config): Pr
|
|
|
199
256
|
? await createTempVideoFile(buffer, mimeType)
|
|
200
257
|
: `base64://${buffer.toString('base64')}`
|
|
201
258
|
|
|
259
|
+
if (config.loggerinfo) {
|
|
260
|
+
logger.info(`use downloaded video ${mode}: src=${mode === 'file' ? replacement : `base64://${formatBytes(buffer.length)} raw`}, remainingVideoUrls=${remainingVideoUrls.length}`)
|
|
261
|
+
}
|
|
262
|
+
|
|
202
263
|
return {
|
|
203
264
|
...note,
|
|
204
265
|
videoBuffer: undefined,
|
|
@@ -211,6 +272,53 @@ async function prepareNoteVideo(ctx: Context, note: XhsNote, config: Config): Pr
|
|
|
211
272
|
}
|
|
212
273
|
}
|
|
213
274
|
|
|
275
|
+
async function getRemoteVideoSize(ctx: Context, url: string, config: Config): Promise<number | undefined> {
|
|
276
|
+
const maxSendSizeBytes = getSizeLimitBytes(config.maxVideoSendSizeMB, 100)
|
|
277
|
+
if (!maxSendSizeBytes) return undefined
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
if (config.loggerinfo) logger.info(`check remote video size start: url=${url}`)
|
|
281
|
+
const headers = await ctx.http.head(url, { timeout: config.timeout * 1000 })
|
|
282
|
+
const contentLength = headers.get('content-length')
|
|
283
|
+
if (!contentLength) {
|
|
284
|
+
if (config.loggerinfo) logger.info('check remote video size skipped: missing content-length')
|
|
285
|
+
return undefined
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const size = Number.parseInt(contentLength, 10)
|
|
289
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
290
|
+
if (config.loggerinfo) logger.info(`check remote video size skipped: invalid content-length=${contentLength}`)
|
|
291
|
+
return undefined
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (config.loggerinfo) logger.info(`check remote video size success: size=${formatBytes(size)}, max=${formatBytes(maxSendSizeBytes)}`)
|
|
295
|
+
return size
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (config.loggerinfo) logger.info(`check remote video size failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
298
|
+
return undefined
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function removeNoteVideos(note: XhsNote): XhsNote {
|
|
303
|
+
return {
|
|
304
|
+
...note,
|
|
305
|
+
videoBuffer: undefined,
|
|
306
|
+
videoUrls: [],
|
|
307
|
+
videoSkippedMessage: VIDEO_TOO_LARGE_MESSAGE,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function getSizeLimitBytes(sizeMB: number | undefined, fallbackMB: number): number {
|
|
312
|
+
const normalized = sizeMB ?? fallbackMB
|
|
313
|
+
return normalized > 0 ? normalized * 1024 * 1024 : 0
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function formatBytes(bytes: number): string {
|
|
317
|
+
if (bytes < 1024) return `${bytes} B`
|
|
318
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
319
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
320
|
+
}
|
|
321
|
+
|
|
214
322
|
async function createTempVideoFile(buffer: Buffer, mimeType: string): Promise<string> {
|
|
215
323
|
const fileName = `xhs-video-${Date.now()}-${Math.random().toString(16).slice(2)}${getVideoFileExtension(mimeType)}`
|
|
216
324
|
const filePath = path.join(os.tmpdir(), fileName)
|
package/src/parser.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface XhsNote {
|
|
|
30
30
|
videoUrls: string[]
|
|
31
31
|
videoBuffer?: Buffer
|
|
32
32
|
videoMimeType?: string
|
|
33
|
+
videoSkippedMessage?: string
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const URL_BOUNDARY = '[^\\s"\'<>\\\\^`{|},。;!?、【】《》]+'
|
|
@@ -123,6 +124,13 @@ export function buildXhsMessages(note: XhsNote, config: XhsConfigLike, session?:
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
if (note.videoSkippedMessage) {
|
|
128
|
+
messages.push(h('message', {
|
|
129
|
+
userId: session?.userId,
|
|
130
|
+
nickname: session?.author?.nickname || session?.username,
|
|
131
|
+
}, h.text(note.videoSkippedMessage)))
|
|
132
|
+
}
|
|
133
|
+
|
|
126
134
|
return messages
|
|
127
135
|
}
|
|
128
136
|
|