koishi-plugin-video-parser-all 0.6.9 → 0.7.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/index.d.ts +0 -23
- package/lib/index.js +93 -144
- package/package.json +1 -1
- package/readme.md +105 -47
package/lib/index.d.ts
CHANGED
|
@@ -61,27 +61,4 @@ export declare const Config: Schema<{
|
|
|
61
61
|
} & {
|
|
62
62
|
autoClearCacheInterval: number;
|
|
63
63
|
}>;
|
|
64
|
-
export declare enum ErrorCode {
|
|
65
|
-
SUCCESS = 0,
|
|
66
|
-
UNKNOWN_ERROR = 1000,
|
|
67
|
-
UNSUPPORTED_PLATFORM = 1001,
|
|
68
|
-
PLATFORM_API_NOT_CONFIGURED = 1002,
|
|
69
|
-
REQUEST_TIMEOUT = 1003,
|
|
70
|
-
NETWORK_ERROR = 1004,
|
|
71
|
-
DUPLICATE_PARSE = 1005,
|
|
72
|
-
INVALID_URL = 1006,
|
|
73
|
-
API_RETURN_ERROR = 2000,
|
|
74
|
-
API_DATA_PARSE_FAILED = 2001,
|
|
75
|
-
API_EMPTY_RESPONSE = 2002,
|
|
76
|
-
API_INVALID_RESPONSE = 2003,
|
|
77
|
-
VIDEO_DOWNLOAD_FAILED = 3000,
|
|
78
|
-
VIDEO_SIZE_EXCEEDED = 3001,
|
|
79
|
-
UNSUPPORTED_CONTENT_TYPE = 3002,
|
|
80
|
-
NO_VIDEO_FOUND = 3003,
|
|
81
|
-
NO_IMAGE_FOUND = 3004,
|
|
82
|
-
MESSAGE_SEND_FAILED = 4000,
|
|
83
|
-
MESSAGE_SEND_TIMEOUT = 4001,
|
|
84
|
-
FORWARD_MESSAGE_FAILED = 4002
|
|
85
|
-
}
|
|
86
|
-
export declare const ErrorMessageMap: Record<ErrorCode, string>;
|
|
87
64
|
export declare function apply(ctx: Context, config: any): void;
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.Config = exports.name = void 0;
|
|
7
7
|
exports.apply = apply;
|
|
8
8
|
const koishi_1 = require("koishi");
|
|
9
9
|
const axios_1 = __importDefault(require("axios"));
|
|
@@ -15,12 +15,12 @@ const worker_threads_1 = require("worker_threads");
|
|
|
15
15
|
exports.name = 'video-parser-all';
|
|
16
16
|
exports.Config = koishi_1.Schema.intersect([
|
|
17
17
|
koishi_1.Schema.object({
|
|
18
|
-
enable: koishi_1.Schema.boolean().default(true),
|
|
19
|
-
botName: koishi_1.Schema.string().default('视频解析机器人'),
|
|
20
|
-
showWaitingTip: koishi_1.Schema.boolean().default(true),
|
|
21
|
-
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...'),
|
|
22
|
-
sameLinkInterval: koishi_1.Schema.number().min(0).default(180),
|
|
23
|
-
}),
|
|
18
|
+
enable: koishi_1.Schema.boolean().default(true).description('是否启用视频解析插件'),
|
|
19
|
+
botName: koishi_1.Schema.string().default('视频解析机器人').description('机器人显示名称'),
|
|
20
|
+
showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
|
|
21
|
+
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('等待提示文本内容'),
|
|
22
|
+
sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
|
|
23
|
+
}).description('基础设置'),
|
|
24
24
|
koishi_1.Schema.object({
|
|
25
25
|
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(`标题:${'${标题}'}
|
|
26
26
|
作者:${'${作者}'}
|
|
@@ -42,83 +42,38 @@ IP属地:${'${IP属地}'}
|
|
|
42
42
|
直播间ID:${'${直播间ID}'}
|
|
43
43
|
直播间状态:${'${直播间状态}'}
|
|
44
44
|
图片数量:${'${图片数量}'}
|
|
45
|
-
作者ID:${'${作者ID}'}`),
|
|
46
|
-
}),
|
|
45
|
+
作者ID:${'${作者ID}'}`).description('统一消息格式'),
|
|
46
|
+
}).description('统一消息格式'),
|
|
47
47
|
koishi_1.Schema.object({
|
|
48
|
-
showImageText: koishi_1.Schema.boolean().default(true),
|
|
49
|
-
showVideoFile: koishi_1.Schema.boolean().default(true),
|
|
50
|
-
}),
|
|
48
|
+
showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
|
|
49
|
+
showVideoFile: koishi_1.Schema.boolean().default(true).description('发送视频文件(关闭则只发链接)'),
|
|
50
|
+
}).description('内容显示设置'),
|
|
51
51
|
koishi_1.Schema.object({
|
|
52
|
-
maxDescLength: koishi_1.Schema.number().default(200),
|
|
53
|
-
}),
|
|
52
|
+
maxDescLength: koishi_1.Schema.number().default(200).description('简介内容最大长度(字符)'),
|
|
53
|
+
}).description('内容长度限制'),
|
|
54
54
|
koishi_1.Schema.object({
|
|
55
|
-
timeout: koishi_1.Schema.number().min(0).default(180000),
|
|
56
|
-
videoSendTimeout: koishi_1.Schema.number().min(0).default(0),
|
|
57
|
-
userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'),
|
|
58
|
-
}),
|
|
55
|
+
timeout: koishi_1.Schema.number().min(0).default(180000).description('API请求超时时间(毫秒)'),
|
|
56
|
+
videoSendTimeout: koishi_1.Schema.number().min(0).default(0).description('视频发送超时时间(毫秒,0为不限制)'),
|
|
57
|
+
userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36').description('请求UA标识'),
|
|
58
|
+
}).description('网络与API设置'),
|
|
59
59
|
koishi_1.Schema.object({
|
|
60
|
-
ignoreSendError: koishi_1.Schema.boolean().default(true),
|
|
61
|
-
retryTimes: koishi_1.Schema.number().min(0).default(3),
|
|
62
|
-
retryInterval: koishi_1.Schema.number().min(0).default(1000),
|
|
63
|
-
}),
|
|
60
|
+
ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送失败错误'),
|
|
61
|
+
retryTimes: koishi_1.Schema.number().min(0).default(3).description('API请求重试次数'),
|
|
62
|
+
retryInterval: koishi_1.Schema.number().min(0).default(1000).description('重试间隔时间(毫秒)'),
|
|
63
|
+
}).description('错误与重试设置'),
|
|
64
64
|
koishi_1.Schema.object({
|
|
65
|
-
enableForward: koishi_1.Schema.boolean().default(false),
|
|
66
|
-
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false),
|
|
67
|
-
maxVideoSize: koishi_1.Schema.number().min(0).default(0),
|
|
68
|
-
downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0),
|
|
69
|
-
}),
|
|
65
|
+
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot平台)'),
|
|
66
|
+
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频'),
|
|
67
|
+
maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('最大视频大小限制(MB,0为不限制)'),
|
|
68
|
+
downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0为不使用多线程)'),
|
|
69
|
+
}).description('发送方式设置'),
|
|
70
70
|
koishi_1.Schema.object({
|
|
71
|
-
messageBufferDelay: koishi_1.Schema.number().min(0).default(0),
|
|
72
|
-
}),
|
|
71
|
+
messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒)'),
|
|
72
|
+
}).description('消息处理设置'),
|
|
73
73
|
koishi_1.Schema.object({
|
|
74
|
-
autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0),
|
|
75
|
-
}),
|
|
74
|
+
autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0为关闭)'),
|
|
75
|
+
}).description('缓存清理设置'),
|
|
76
76
|
]);
|
|
77
|
-
var ErrorCode;
|
|
78
|
-
(function (ErrorCode) {
|
|
79
|
-
ErrorCode[ErrorCode["SUCCESS"] = 0] = "SUCCESS";
|
|
80
|
-
ErrorCode[ErrorCode["UNKNOWN_ERROR"] = 1000] = "UNKNOWN_ERROR";
|
|
81
|
-
ErrorCode[ErrorCode["UNSUPPORTED_PLATFORM"] = 1001] = "UNSUPPORTED_PLATFORM";
|
|
82
|
-
ErrorCode[ErrorCode["PLATFORM_API_NOT_CONFIGURED"] = 1002] = "PLATFORM_API_NOT_CONFIGURED";
|
|
83
|
-
ErrorCode[ErrorCode["REQUEST_TIMEOUT"] = 1003] = "REQUEST_TIMEOUT";
|
|
84
|
-
ErrorCode[ErrorCode["NETWORK_ERROR"] = 1004] = "NETWORK_ERROR";
|
|
85
|
-
ErrorCode[ErrorCode["DUPLICATE_PARSE"] = 1005] = "DUPLICATE_PARSE";
|
|
86
|
-
ErrorCode[ErrorCode["INVALID_URL"] = 1006] = "INVALID_URL";
|
|
87
|
-
ErrorCode[ErrorCode["API_RETURN_ERROR"] = 2000] = "API_RETURN_ERROR";
|
|
88
|
-
ErrorCode[ErrorCode["API_DATA_PARSE_FAILED"] = 2001] = "API_DATA_PARSE_FAILED";
|
|
89
|
-
ErrorCode[ErrorCode["API_EMPTY_RESPONSE"] = 2002] = "API_EMPTY_RESPONSE";
|
|
90
|
-
ErrorCode[ErrorCode["API_INVALID_RESPONSE"] = 2003] = "API_INVALID_RESPONSE";
|
|
91
|
-
ErrorCode[ErrorCode["VIDEO_DOWNLOAD_FAILED"] = 3000] = "VIDEO_DOWNLOAD_FAILED";
|
|
92
|
-
ErrorCode[ErrorCode["VIDEO_SIZE_EXCEEDED"] = 3001] = "VIDEO_SIZE_EXCEEDED";
|
|
93
|
-
ErrorCode[ErrorCode["UNSUPPORTED_CONTENT_TYPE"] = 3002] = "UNSUPPORTED_CONTENT_TYPE";
|
|
94
|
-
ErrorCode[ErrorCode["NO_VIDEO_FOUND"] = 3003] = "NO_VIDEO_FOUND";
|
|
95
|
-
ErrorCode[ErrorCode["NO_IMAGE_FOUND"] = 3004] = "NO_IMAGE_FOUND";
|
|
96
|
-
ErrorCode[ErrorCode["MESSAGE_SEND_FAILED"] = 4000] = "MESSAGE_SEND_FAILED";
|
|
97
|
-
ErrorCode[ErrorCode["MESSAGE_SEND_TIMEOUT"] = 4001] = "MESSAGE_SEND_TIMEOUT";
|
|
98
|
-
ErrorCode[ErrorCode["FORWARD_MESSAGE_FAILED"] = 4002] = "FORWARD_MESSAGE_FAILED";
|
|
99
|
-
})(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
|
|
100
|
-
exports.ErrorMessageMap = {
|
|
101
|
-
[ErrorCode.SUCCESS]: '操作成功',
|
|
102
|
-
[ErrorCode.UNKNOWN_ERROR]: '未知错误',
|
|
103
|
-
[ErrorCode.UNSUPPORTED_PLATFORM]: '不支持该平台链接',
|
|
104
|
-
[ErrorCode.PLATFORM_API_NOT_CONFIGURED]: '该平台暂未配置解析接口',
|
|
105
|
-
[ErrorCode.REQUEST_TIMEOUT]: '请求超时',
|
|
106
|
-
[ErrorCode.NETWORK_ERROR]: '网络请求失败',
|
|
107
|
-
[ErrorCode.DUPLICATE_PARSE]: '请勿重复解析',
|
|
108
|
-
[ErrorCode.INVALID_URL]: '无效的链接格式',
|
|
109
|
-
[ErrorCode.API_RETURN_ERROR]: 'API返回错误',
|
|
110
|
-
[ErrorCode.API_DATA_PARSE_FAILED]: '数据解析异常',
|
|
111
|
-
[ErrorCode.API_EMPTY_RESPONSE]: 'API返回空数据',
|
|
112
|
-
[ErrorCode.API_INVALID_RESPONSE]: 'API返回无效格式',
|
|
113
|
-
[ErrorCode.VIDEO_DOWNLOAD_FAILED]: '视频下载失败',
|
|
114
|
-
[ErrorCode.VIDEO_SIZE_EXCEEDED]: '视频大小超过限制',
|
|
115
|
-
[ErrorCode.UNSUPPORTED_CONTENT_TYPE]: '不支持的内容类型',
|
|
116
|
-
[ErrorCode.NO_VIDEO_FOUND]: '未找到视频内容',
|
|
117
|
-
[ErrorCode.NO_IMAGE_FOUND]: '未找到图片内容',
|
|
118
|
-
[ErrorCode.MESSAGE_SEND_FAILED]: '消息发送失败',
|
|
119
|
-
[ErrorCode.MESSAGE_SEND_TIMEOUT]: '消息发送超时',
|
|
120
|
-
[ErrorCode.FORWARD_MESSAGE_FAILED]: '合并转发失败',
|
|
121
|
-
};
|
|
122
77
|
const processed = new Map();
|
|
123
78
|
const linkBuffer = new Map();
|
|
124
79
|
const logger = new koishi_1.Logger(exports.name);
|
|
@@ -167,10 +122,6 @@ const VARIABLE_MAPPING = {
|
|
|
167
122
|
'图片数量': ['count', 'data.count', 'item.count', 'images.length', 'data.images.length', 'data.item.count'],
|
|
168
123
|
'作者ID': ['userId', 'userID', 'author_id', 'data.userId', 'item.userID', 'author.mid', 'user.mid', 'data.item.userID', 'data.author_id', 'data.user.mid', 'author.id', 'uid', 'short_id', 'data.author.id']
|
|
169
124
|
};
|
|
170
|
-
function getErrorInfo(code, detail) {
|
|
171
|
-
const baseMsg = exports.ErrorMessageMap[code] || exports.ErrorMessageMap[ErrorCode.UNKNOWN_ERROR];
|
|
172
|
-
return detail ? `[错误码: ${code}] ${baseMsg}:${detail}` : `[错误码: ${code}] ${baseMsg}`;
|
|
173
|
-
}
|
|
174
125
|
function getErrorMessage(error) {
|
|
175
126
|
if (error instanceof Error)
|
|
176
127
|
return error.message;
|
|
@@ -199,7 +150,7 @@ async function downloadVideoThread(workerData) {
|
|
|
199
150
|
worker.on('error', reject);
|
|
200
151
|
worker.on('exit', (code) => {
|
|
201
152
|
if (code !== 0)
|
|
202
|
-
reject(new Error(
|
|
153
|
+
reject(new Error(`下载线程异常退出,代码:${code}`));
|
|
203
154
|
});
|
|
204
155
|
});
|
|
205
156
|
}
|
|
@@ -235,11 +186,11 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
235
186
|
const filePath = path_1.default.join(dir, `${filename}.mp4`);
|
|
236
187
|
try {
|
|
237
188
|
if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
|
|
238
|
-
return { filePath: '',
|
|
189
|
+
return { filePath: '', success: false };
|
|
239
190
|
}
|
|
240
191
|
const fileSize = await getFileSize(url, userAgent);
|
|
241
192
|
if (maxSize > 0 && fileSize > maxSize) {
|
|
242
|
-
return { filePath: '',
|
|
193
|
+
return { filePath: '', success: false };
|
|
243
194
|
}
|
|
244
195
|
if (threads <= 0 || fileSize === 0) {
|
|
245
196
|
const response = await (0, axios_1.default)({
|
|
@@ -253,7 +204,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
253
204
|
});
|
|
254
205
|
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
255
206
|
await (0, promises_1.pipeline)(response.data, writeStream);
|
|
256
|
-
return { filePath,
|
|
207
|
+
return { filePath, success: true };
|
|
257
208
|
}
|
|
258
209
|
const totalSize = fileSize * 1024 * 1024;
|
|
259
210
|
const chunkSize = Math.ceil(totalSize / threads);
|
|
@@ -279,7 +230,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
279
230
|
fs_1.default.unlinkSync(result.filePath);
|
|
280
231
|
}
|
|
281
232
|
writeStream.end();
|
|
282
|
-
return { filePath,
|
|
233
|
+
return { filePath, success: true };
|
|
283
234
|
}
|
|
284
235
|
catch (error) {
|
|
285
236
|
if (fs_1.default.existsSync(filePath)) {
|
|
@@ -292,7 +243,8 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
292
243
|
}
|
|
293
244
|
catch (e) { }
|
|
294
245
|
});
|
|
295
|
-
|
|
246
|
+
logger.error(`视频下载失败: ${getErrorMessage(error)}`);
|
|
247
|
+
return { filePath: '', success: false };
|
|
296
248
|
}
|
|
297
249
|
}
|
|
298
250
|
function extractUrl(content) {
|
|
@@ -333,8 +285,10 @@ function cleanUrl(url) {
|
|
|
333
285
|
url = url.replace(/&/g, '&');
|
|
334
286
|
const urlObj = new URL(url);
|
|
335
287
|
if (urlObj.hostname.includes('xiaohongshu.com')) {
|
|
288
|
+
const pathname = urlObj.pathname;
|
|
336
289
|
urlObj.searchParams.forEach((_, key) => urlObj.searchParams.delete(key));
|
|
337
|
-
|
|
290
|
+
urlObj.pathname = pathname;
|
|
291
|
+
return urlObj.href;
|
|
338
292
|
}
|
|
339
293
|
if (urlObj.hostname.includes('douyin.com') || urlObj.hostname.includes('v.douyin.com')) {
|
|
340
294
|
urlObj.searchParams.delete('source');
|
|
@@ -350,12 +304,13 @@ function cleanUrl(url) {
|
|
|
350
304
|
async function resolveShortUrl(url) {
|
|
351
305
|
try {
|
|
352
306
|
const res = await axios_1.default.head(url, {
|
|
353
|
-
timeout:
|
|
354
|
-
maxRedirects:
|
|
307
|
+
timeout: 15000,
|
|
308
|
+
maxRedirects: 15,
|
|
355
309
|
headers: {
|
|
356
310
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
357
311
|
'Referer': 'https://www.baidu.com/',
|
|
358
|
-
}
|
|
312
|
+
},
|
|
313
|
+
validateStatus: (status) => status >= 200 && status < 400
|
|
359
314
|
});
|
|
360
315
|
return cleanUrl(res.request.res?.responseUrl || url);
|
|
361
316
|
}
|
|
@@ -463,6 +418,7 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
463
418
|
const root = rawResponse || {};
|
|
464
419
|
const data = root.data || root.result || root || {};
|
|
465
420
|
const stat = {};
|
|
421
|
+
let totalImageCount = 0;
|
|
466
422
|
Object.entries(VARIABLE_MAPPING).forEach(([varName, keys]) => {
|
|
467
423
|
let value = findValueInObject(data, keys) || findValueInObject(root, keys);
|
|
468
424
|
if (varName === '图片数量' && value === undefined) {
|
|
@@ -478,12 +434,13 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
478
434
|
break;
|
|
479
435
|
}
|
|
480
436
|
}
|
|
437
|
+
totalImageCount = imgCount;
|
|
481
438
|
const cover = data.cover || data.video?.fm || data.imgurl || data.pic || data.thumbnail || data.cover_url ||
|
|
482
439
|
data.item?.cover || root.cover || data.live?.cover || data.live?.keyframe || '';
|
|
483
440
|
if (cover && imgCount > 0) {
|
|
484
441
|
imgCount = imgSources.find(source => Array.isArray(source))?.filter(i => i && typeof i === 'string' && i !== cover).length || 0;
|
|
485
442
|
}
|
|
486
|
-
value =
|
|
443
|
+
value = totalImageCount;
|
|
487
444
|
}
|
|
488
445
|
if (value !== undefined && value !== null && value !== '' && value !== 0) {
|
|
489
446
|
stat[varName] = value;
|
|
@@ -590,6 +547,7 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
590
547
|
duration,
|
|
591
548
|
durationFormatted,
|
|
592
549
|
stat,
|
|
550
|
+
totalImageCount,
|
|
593
551
|
live_photo,
|
|
594
552
|
h_w,
|
|
595
553
|
jx: data.jx || null,
|
|
@@ -687,7 +645,9 @@ function apply(ctx, config) {
|
|
|
687
645
|
headers: {
|
|
688
646
|
'X-Requested-With': 'XMLHttpRequest',
|
|
689
647
|
'Origin': 'https://api.bugpk.com',
|
|
690
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
648
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
649
|
+
'Cache-Control': 'no-cache',
|
|
650
|
+
'Pragma': 'no-cache'
|
|
691
651
|
}
|
|
692
652
|
});
|
|
693
653
|
}
|
|
@@ -717,82 +677,67 @@ function apply(ctx, config) {
|
|
|
717
677
|
realUrl = cleanUrl(realUrl);
|
|
718
678
|
const platform = getPlatformType(realUrl);
|
|
719
679
|
if (!platform) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${url}`);
|
|
723
|
-
return { data: null, code, msg };
|
|
680
|
+
logger.error(`不支持的平台链接: ${url}`);
|
|
681
|
+
return { data: null, success: false, msg: '不支持该平台链接' };
|
|
724
682
|
}
|
|
725
683
|
const apiUrl = API_CONFIG[platform];
|
|
726
684
|
if (!apiUrl) {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${platform}`);
|
|
730
|
-
return { data: null, code, msg };
|
|
685
|
+
logger.error(`该平台暂未配置解析接口: ${platform}`);
|
|
686
|
+
return { data: null, success: false, msg: '该平台暂未配置解析接口' };
|
|
731
687
|
}
|
|
732
688
|
try {
|
|
733
689
|
const resData = await parseWithRetry(realUrl, platform, config.retryTimes);
|
|
734
690
|
if (!resData || Object.keys(resData).length === 0) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
logger.error(`[${code}] ${url}`);
|
|
738
|
-
return { data: null, code, msg };
|
|
691
|
+
logger.error(`API返回空数据: ${url}`);
|
|
692
|
+
return { data: null, success: false, msg: '解析失败,API返回空数据' };
|
|
739
693
|
}
|
|
740
694
|
const isSuccess = resData.code === 0 || resData.code === 200 || resData.code === 1 ||
|
|
741
695
|
(resData.msg && (resData.msg.includes('解析成功') || resData.msg.includes('success') || resData.msg.includes('请求成功') || resData.msg === 'video' || resData.msg === 'cv' || resData.msg === 'live')) ||
|
|
742
696
|
!!resData.data || !!resData.result || !!resData.video || !!resData.images || !!resData.imgurl;
|
|
743
697
|
if (!isSuccess) {
|
|
744
698
|
const apiErrorMsg = resData.msg || resData.error || '解析失败';
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
logger.error(`[${code}] API返回错误: ${url}, 错误: ${apiErrorMsg}`);
|
|
748
|
-
return { data: null, code, msg };
|
|
699
|
+
logger.error(`API返回错误: ${url} - ${apiErrorMsg}`);
|
|
700
|
+
return { data: null, success: false, msg: `解析失败: ${apiErrorMsg}` };
|
|
749
701
|
}
|
|
750
702
|
try {
|
|
751
703
|
const parseResult = parseData(resData, config.maxDescLength);
|
|
752
|
-
logger.info(
|
|
704
|
+
logger.info(`解析成功: ${url}`);
|
|
753
705
|
return {
|
|
754
706
|
data: parseResult,
|
|
755
|
-
|
|
756
|
-
msg:
|
|
707
|
+
success: true,
|
|
708
|
+
msg: '解析成功'
|
|
757
709
|
};
|
|
758
710
|
}
|
|
759
711
|
catch (parseError) {
|
|
760
712
|
const errorMsg = getErrorMessage(parseError);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
logger.error(`[${code}] 解析数据失败: ${url}, 错误: ${errorMsg}`);
|
|
764
|
-
return { data: null, code, msg };
|
|
713
|
+
logger.error(`解析数据失败: ${url} - ${errorMsg}`);
|
|
714
|
+
return { data: null, success: false, msg: `解析数据失败: ${errorMsg}` };
|
|
765
715
|
}
|
|
766
716
|
}
|
|
767
717
|
catch (error) {
|
|
768
718
|
const errorMsg = getErrorMessage(error);
|
|
769
|
-
let
|
|
719
|
+
let msg = '未知错误';
|
|
770
720
|
if (errorMsg.includes('timeout')) {
|
|
771
|
-
|
|
721
|
+
msg = '请求超时';
|
|
772
722
|
}
|
|
773
723
|
else if (errorMsg.includes('Network') || errorMsg.includes('network') || errorMsg.includes('404') || errorMsg.includes('500')) {
|
|
774
|
-
|
|
724
|
+
msg = '网络请求失败';
|
|
775
725
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
const msg = getErrorInfo(code, errorMsg);
|
|
780
|
-
logger.error(`[${code}] 解析请求失败: ${url}, 错误: ${errorMsg}`);
|
|
781
|
-
return { data: null, code, msg };
|
|
726
|
+
logger.error(`解析请求失败: ${url} - ${errorMsg}`);
|
|
727
|
+
return { data: null, success: false, msg };
|
|
782
728
|
}
|
|
783
729
|
}
|
|
784
730
|
async function processSingleUrl(session, url) {
|
|
785
731
|
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
786
732
|
const now = Date.now();
|
|
787
733
|
if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
return { data: null, code, msg };
|
|
734
|
+
logger.warn(`相同链接重复解析: ${url}`);
|
|
735
|
+
return { data: null, success: false, msg: '请勿重复解析相同链接' };
|
|
791
736
|
}
|
|
792
737
|
processed.set(hash, now);
|
|
793
738
|
const result = await parse(url);
|
|
794
|
-
if (!result.
|
|
795
|
-
return { data: null,
|
|
739
|
+
if (!result.success)
|
|
740
|
+
return { data: null, success: false, msg: result.msg };
|
|
796
741
|
const parseData = result.data;
|
|
797
742
|
const text = generateFormattedText(parseData, config);
|
|
798
743
|
return {
|
|
@@ -802,21 +747,22 @@ function apply(ctx, config) {
|
|
|
802
747
|
images: parseData.images,
|
|
803
748
|
video: parseData.video,
|
|
804
749
|
type: parseData.type,
|
|
750
|
+
totalImageCount: parseData.totalImageCount,
|
|
805
751
|
live_photo: parseData.live_photo,
|
|
806
752
|
h_w: parseData.h_w,
|
|
807
753
|
quality_urls: parseData.quality_urls,
|
|
808
754
|
default_quality: parseData.default_quality,
|
|
809
755
|
download_url: parseData.download_url
|
|
810
756
|
},
|
|
811
|
-
|
|
812
|
-
msg:
|
|
757
|
+
success: true,
|
|
758
|
+
msg: '处理成功'
|
|
813
759
|
};
|
|
814
760
|
}
|
|
815
761
|
async function sendTimeout(session, content) {
|
|
816
762
|
if (config.videoSendTimeout <= 0) {
|
|
817
763
|
return session.send(content).catch((err) => {
|
|
818
764
|
const errorMsg = getErrorMessage(err);
|
|
819
|
-
logger.error(
|
|
765
|
+
logger.error(`发送消息失败: ${errorMsg}`);
|
|
820
766
|
if (!config.ignoreSendError)
|
|
821
767
|
return null;
|
|
822
768
|
return null;
|
|
@@ -827,8 +773,7 @@ function apply(ctx, config) {
|
|
|
827
773
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.videoSendTimeout))
|
|
828
774
|
]).catch((err) => {
|
|
829
775
|
const errorMsg = getErrorMessage(err);
|
|
830
|
-
|
|
831
|
-
logger.error(`[${code}] 发送消息失败: ${errorMsg}`);
|
|
776
|
+
logger.error(`发送消息超时: ${errorMsg}`);
|
|
832
777
|
if (!config.ignoreSendError)
|
|
833
778
|
return null;
|
|
834
779
|
return null;
|
|
@@ -846,7 +791,7 @@ function apply(ctx, config) {
|
|
|
846
791
|
const errors = [];
|
|
847
792
|
for (const url of urls) {
|
|
848
793
|
const result = await processSingleUrl(session, url);
|
|
849
|
-
if (result.
|
|
794
|
+
if (result.success) {
|
|
850
795
|
items.push(result.data);
|
|
851
796
|
}
|
|
852
797
|
else {
|
|
@@ -879,7 +824,7 @@ function apply(ctx, config) {
|
|
|
879
824
|
if (config.downloadVideoBeforeSend) {
|
|
880
825
|
const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
881
826
|
const dl = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
882
|
-
if (dl.
|
|
827
|
+
if (dl.success) {
|
|
883
828
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.file(dl.filePath), botName));
|
|
884
829
|
}
|
|
885
830
|
else {
|
|
@@ -895,7 +840,7 @@ function apply(ctx, config) {
|
|
|
895
840
|
}
|
|
896
841
|
}
|
|
897
842
|
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
898
|
-
forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.
|
|
843
|
+
forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.totalImageCount}张)`, botName));
|
|
899
844
|
for (const img of item.images) {
|
|
900
845
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(img), botName));
|
|
901
846
|
}
|
|
@@ -920,7 +865,7 @@ function apply(ctx, config) {
|
|
|
920
865
|
await delay(500);
|
|
921
866
|
}
|
|
922
867
|
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
923
|
-
await sendTimeout(session, `📸 图集内容(共${item.
|
|
868
|
+
await sendTimeout(session, `📸 图集内容(共${item.totalImageCount}张)`);
|
|
924
869
|
await delay(300);
|
|
925
870
|
for (const img of item.images) {
|
|
926
871
|
await sendTimeout(session, koishi_1.h.image(img));
|
|
@@ -929,7 +874,9 @@ function apply(ctx, config) {
|
|
|
929
874
|
}
|
|
930
875
|
}
|
|
931
876
|
}
|
|
932
|
-
catch (e) {
|
|
877
|
+
catch (e) {
|
|
878
|
+
logger.error(`处理消息发送失败: ${getErrorMessage(e)}`);
|
|
879
|
+
}
|
|
933
880
|
}
|
|
934
881
|
if (enableForward && forwardMessages.length) {
|
|
935
882
|
try {
|
|
@@ -956,13 +903,15 @@ function apply(ctx, config) {
|
|
|
956
903
|
});
|
|
957
904
|
ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
|
|
958
905
|
const us = extractUrl(url);
|
|
959
|
-
if (!us.length)
|
|
960
|
-
|
|
906
|
+
if (!us.length) {
|
|
907
|
+
await sendTimeout(session, '无效的视频链接');
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
961
910
|
await flush(session, us);
|
|
962
911
|
});
|
|
963
|
-
ctx.command('clear-cache', '清空缓存').action(() => {
|
|
912
|
+
ctx.command('clear-cache', '清空缓存').action(async ({ session }) => {
|
|
964
913
|
clearAllCache();
|
|
965
|
-
|
|
914
|
+
await sendTimeout(session, '✅ 缓存已清空');
|
|
966
915
|
});
|
|
967
916
|
setInterval(() => {
|
|
968
917
|
const now = Date.now();
|
|
@@ -974,5 +923,5 @@ function apply(ctx, config) {
|
|
|
974
923
|
logger.info('自动清理缓存完成');
|
|
975
924
|
}, config.autoClearCacheInterval * 60 * 1000);
|
|
976
925
|
}
|
|
977
|
-
logger.info('
|
|
926
|
+
logger.info('视频解析插件已启动');
|
|
978
927
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
## 项目介绍 (Project Introduction)
|
|
4
4
|
|
|
5
5
|
### 中文
|
|
6
|
-
这是一个为 Koishi
|
|
7
|
-
- 🚀
|
|
8
|
-
- 🎨
|
|
6
|
+
这是一个为 Koishi 机器人框架开发的**多平台视频/图集解析插件**,支持自动识别并解析抖音、快手、B站、小红书、微博、今日头条、皮皮搞笑、皮皮虾、最右等主流平台的短视频/图集链接。核心特性:
|
|
7
|
+
- 🚀 自动识别多平台链接,无需手动指定平台
|
|
8
|
+
- 🎨 自定义解析结果格式,支持丰富的变量替换
|
|
9
9
|
- ⚡ 内置防重复解析、接口重试、自动缓存清理等实用功能
|
|
10
10
|
- 📤 支持 OneBot 平台消息合并转发,优化展示体验
|
|
11
11
|
|
|
12
12
|
### English
|
|
13
|
-
This is a **video parsing plugin** developed for the Koishi bot framework, supporting automatic recognition and parsing of short video links from mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, Toutiao, Pipi Funny, Pipi Shrimp, and Zuiyou. Core features:
|
|
14
|
-
- 🚀 Automatically recognizes
|
|
15
|
-
- 🎨
|
|
16
|
-
- ⚡ Built-in duplicate prevention, retry logic,
|
|
17
|
-
- 📤 Support OneBot message forwarding for better display experience
|
|
13
|
+
This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, supporting automatic recognition and parsing of short video/image links from mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, Toutiao, Pipi Funny, Pipi Shrimp, and Zuiyou. Core features:
|
|
14
|
+
- 🚀 Automatically recognizes multi-platform links without manual platform specification
|
|
15
|
+
- 🎨 Customizable parsing result format with rich variable substitution support
|
|
16
|
+
- ⚡ Built-in duplicate parsing prevention, API retry logic, and automatic cache cleanup
|
|
17
|
+
- 📤 Support OneBot platform message forwarding for better display experience
|
|
18
18
|
|
|
19
19
|
## 项目仓库 (Repository)
|
|
20
20
|
- GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
|
|
@@ -24,49 +24,107 @@ This is a **video parsing plugin** developed for the Koishi bot framework, suppo
|
|
|
24
24
|
|
|
25
25
|
| 指令 (Command) | 说明 (Description) | 示例 (Example) |
|
|
26
26
|
|----------------|--------------------|----------------|
|
|
27
|
-
| `parse <url>` |
|
|
28
|
-
| `clear-cache` |
|
|
27
|
+
| `parse <url>` | 手动解析指定的视频/图集链接 | `parse https://v.douyin.com/xxxx/` |
|
|
28
|
+
| `clear-cache` | 清理解析缓存和临时下载的视频文件 | `clear-cache` |
|
|
29
29
|
|
|
30
30
|
## 配置项说明 (Configuration)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
### 基础设置
|
|
33
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
34
|
+
|--------|------|--------|------|
|
|
35
|
+
| `enable` | boolean | true | 是否启用视频解析插件 |
|
|
35
36
|
| `botName` | string | 视频解析机器人 | 合并转发消息中显示的机器人名称 |
|
|
36
|
-
| `showWaitingTip` | boolean | true |
|
|
37
|
-
| `waitingTipText` | string | 正在解析视频,请稍候... |
|
|
38
|
-
| `sameLinkInterval` | number | 180 |
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
| `
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
|
47
|
-
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
|
53
|
-
|
|
54
|
-
| `
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
|
58
|
-
|
|
59
|
-
| `timeout` | number | 180000 | API
|
|
60
|
-
| `videoSendTimeout` | number | 0 |
|
|
61
|
-
| `userAgent` | string |
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
|
|
37
|
+
| `showWaitingTip` | boolean | true | 解析时显示等待提示 |
|
|
38
|
+
| `waitingTipText` | string | 正在解析视频,请稍候... | 等待提示文本内容 |
|
|
39
|
+
| `sameLinkInterval` | number | 180 | 相同链接重复解析间隔(秒),防止频繁解析 |
|
|
40
|
+
|
|
41
|
+
### 统一消息格式
|
|
42
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
43
|
+
|--------|------|--------|------|
|
|
44
|
+
| `unifiedMessageFormat` | string | 详见下方变量说明 | 自定义解析结果的输出格式,支持变量替换 |
|
|
45
|
+
|
|
46
|
+
### 内容显示设置
|
|
47
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
48
|
+
|--------|------|--------|------|
|
|
49
|
+
| `showImageText` | boolean | true | 是否显示解析后的图文内容 |
|
|
50
|
+
| `showVideoFile` | boolean | true | 是否发送视频文件(关闭则只发送视频链接) |
|
|
51
|
+
|
|
52
|
+
### 内容长度限制
|
|
53
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
54
|
+
|--------|------|--------|------|
|
|
55
|
+
| `maxDescLength` | number | 200 | 简介内容最大长度(字符),超出部分自动截断 |
|
|
56
|
+
|
|
57
|
+
### 网络与API设置
|
|
58
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
59
|
+
|--------|------|--------|------|
|
|
60
|
+
| `timeout` | number | 180000 | API请求超时时间(毫秒) |
|
|
61
|
+
| `videoSendTimeout` | number | 0 | 视频消息发送超时时间(毫秒,0为不限制) |
|
|
62
|
+
| `userAgent` | string | Chrome 124 UA | API请求使用的User-Agent标识 |
|
|
63
|
+
|
|
64
|
+
### 错误与重试设置
|
|
65
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
66
|
+
|--------|------|--------|------|
|
|
67
|
+
| `ignoreSendError` | boolean | true | 忽略消息发送失败错误,避免插件崩溃 |
|
|
68
|
+
| `retryTimes` | number | 3 | API请求失败时的重试次数 |
|
|
69
|
+
| `retryInterval` | number | 1000 | 每次重试的间隔时间(毫秒) |
|
|
70
|
+
|
|
71
|
+
### 发送方式设置
|
|
72
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
73
|
+
|--------|------|--------|------|
|
|
74
|
+
| `enableForward` | boolean | false | 启用合并转发功能(仅OneBot平台) |
|
|
75
|
+
| `downloadVideoBeforeSend` | boolean | false | 发送前先下载视频到本地(再发送文件) |
|
|
76
|
+
| `maxVideoSize` | number | 0 | 最大视频下载大小限制(MB,0为不限制) |
|
|
77
|
+
| `downloadThreads` | number | 0 | 多线程下载线程数(0为单线程,最大10) |
|
|
78
|
+
|
|
79
|
+
### 消息处理设置
|
|
80
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
81
|
+
|--------|------|--------|------|
|
|
82
|
+
| `messageBufferDelay` | number | 0 | 消息缓冲延迟(毫秒),合并短时间内的解析请求 |
|
|
83
|
+
|
|
84
|
+
### 缓存清理设置
|
|
85
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
86
|
+
|--------|------|--------|------|
|
|
87
|
+
| `autoClearCacheInterval` | number | 0 | 自动清理缓存间隔(分钟,0为关闭自动清理) |
|
|
88
|
+
|
|
89
|
+
## 支持的变量 (Supported Variables)
|
|
90
|
+
在 `unifiedMessageFormat` 中可使用以下变量进行自定义格式化:
|
|
91
|
+
|
|
92
|
+
| 变量名 | 说明 | 适用平台 |
|
|
93
|
+
|--------|------|----------|
|
|
94
|
+
| `${标题}` | 视频/图集标题 | 所有平台 |
|
|
95
|
+
| `${作者}` | 作者/UP主/发布者名称 | 所有平台 |
|
|
96
|
+
| `${简介}` | 内容简介/描述 | 部分平台 |
|
|
97
|
+
| `${视频时长}` | 视频时长 | 部分平台 |
|
|
98
|
+
| `${点赞数}` | 点赞数量 | 所有平台 |
|
|
99
|
+
| `${投币数}` | 投币数量 | 部分平台 |
|
|
100
|
+
| `${收藏数}` | 收藏数量 | 所有平台 |
|
|
101
|
+
| `${转发数}` | 转发/分享数量 | 所有平台 |
|
|
102
|
+
| `${播放数}` | 播放量 | 部分平台 |
|
|
103
|
+
| `${评论数}` | 评论数量 | 所有平台 |
|
|
104
|
+
| `${IP属地}` | 作者IP属地 | 部分平台 |
|
|
105
|
+
| `${发布时间}` | 发布时间(格式化) | 所有平台 |
|
|
106
|
+
| `${粉丝数}` | 作者粉丝数量 | 部分平台 |
|
|
107
|
+
| `${在线人数}` | 直播间在线人数 | 部分平台 |
|
|
108
|
+
| `${关注数}` | 关注数 | 部分平台 |
|
|
109
|
+
| `${文件大小}` | 文件大小(MB) | 部分平台 |
|
|
110
|
+
| `${直播间地址}` | 直播间链接 | 部分平台 |
|
|
111
|
+
| `${直播间ID}` | 直播间ID | 部分平台 |
|
|
112
|
+
| `${直播间状态}` | 直播间状态(直播中/未开播) | 部分平台 |
|
|
113
|
+
| `${图片数量}` | 图集图片数量 | 部分平台 |
|
|
114
|
+
| `${作者ID}` | 作者唯一标识ID | 部分平台 |
|
|
115
|
+
|
|
116
|
+
## 支持的平台 (Supported Platforms)
|
|
117
|
+
| 平台名称 | 关键词识别 | 解析能力 |
|
|
118
|
+
|----------|------------|----------|
|
|
119
|
+
| 哔哩哔哩 (B站) | bilibili、b23、B站 | 视频、番剧、直播、图集 |
|
|
120
|
+
| 抖音 | douyin、v.douyin.com | 短视频、图集、直播 |
|
|
121
|
+
| 快手 | kuaishou、v.kuaishou.com | 短视频、图集 |
|
|
122
|
+
| 小红书 | xiaohongshu、xhslink.com | 笔记、图集、视频 |
|
|
123
|
+
| 微博 | weibo、video.weibo.com | 视频、图集 |
|
|
124
|
+
| 今日头条 | toutiao、ixigua.com | 短视频 |
|
|
125
|
+
| 皮皮搞笑 | pipigx、h5.pipigx.com | 短视频 |
|
|
126
|
+
| 皮皮虾 | pipixia、h5.pipix.com | 短视频 |
|
|
127
|
+
| 最右 | zuiyou、xiaochuankeji.cn | 短视频 |
|
|
70
128
|
|
|
71
129
|
## 项目贡献者 (Contributors)
|
|
72
130
|
|