koishi-plugin-video-parser-all 0.6.8 → 0.7.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/lib/index.d.ts +0 -23
- package/lib/index.js +98 -135
- 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"));
|
|
@@ -42,8 +42,7 @@ IP属地:${'${IP属地}'}
|
|
|
42
42
|
直播间ID:${'${直播间ID}'}
|
|
43
43
|
直播间状态:${'${直播间状态}'}
|
|
44
44
|
图片数量:${'${图片数量}'}
|
|
45
|
-
作者ID:${'${作者ID}'}
|
|
46
|
-
视频备用链接:${'${视频备用链接}'}`).description('统一消息格式'),
|
|
45
|
+
作者ID:${'${作者ID}'}`).description('统一消息格式'),
|
|
47
46
|
}).description('统一消息格式'),
|
|
48
47
|
koishi_1.Schema.object({
|
|
49
48
|
showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
|
|
@@ -75,51 +74,6 @@ IP属地:${'${IP属地}'}
|
|
|
75
74
|
autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0为关闭)'),
|
|
76
75
|
}).description('缓存清理设置'),
|
|
77
76
|
]);
|
|
78
|
-
var ErrorCode;
|
|
79
|
-
(function (ErrorCode) {
|
|
80
|
-
ErrorCode[ErrorCode["SUCCESS"] = 0] = "SUCCESS";
|
|
81
|
-
ErrorCode[ErrorCode["UNKNOWN_ERROR"] = 1000] = "UNKNOWN_ERROR";
|
|
82
|
-
ErrorCode[ErrorCode["UNSUPPORTED_PLATFORM"] = 1001] = "UNSUPPORTED_PLATFORM";
|
|
83
|
-
ErrorCode[ErrorCode["PLATFORM_API_NOT_CONFIGURED"] = 1002] = "PLATFORM_API_NOT_CONFIGURED";
|
|
84
|
-
ErrorCode[ErrorCode["REQUEST_TIMEOUT"] = 1003] = "REQUEST_TIMEOUT";
|
|
85
|
-
ErrorCode[ErrorCode["NETWORK_ERROR"] = 1004] = "NETWORK_ERROR";
|
|
86
|
-
ErrorCode[ErrorCode["DUPLICATE_PARSE"] = 1005] = "DUPLICATE_PARSE";
|
|
87
|
-
ErrorCode[ErrorCode["INVALID_URL"] = 1006] = "INVALID_URL";
|
|
88
|
-
ErrorCode[ErrorCode["API_RETURN_ERROR"] = 2000] = "API_RETURN_ERROR";
|
|
89
|
-
ErrorCode[ErrorCode["API_DATA_PARSE_FAILED"] = 2001] = "API_DATA_PARSE_FAILED";
|
|
90
|
-
ErrorCode[ErrorCode["API_EMPTY_RESPONSE"] = 2002] = "API_EMPTY_RESPONSE";
|
|
91
|
-
ErrorCode[ErrorCode["API_INVALID_RESPONSE"] = 2003] = "API_INVALID_RESPONSE";
|
|
92
|
-
ErrorCode[ErrorCode["VIDEO_DOWNLOAD_FAILED"] = 3000] = "VIDEO_DOWNLOAD_FAILED";
|
|
93
|
-
ErrorCode[ErrorCode["VIDEO_SIZE_EXCEEDED"] = 3001] = "VIDEO_SIZE_EXCEEDED";
|
|
94
|
-
ErrorCode[ErrorCode["UNSUPPORTED_CONTENT_TYPE"] = 3002] = "UNSUPPORTED_CONTENT_TYPE";
|
|
95
|
-
ErrorCode[ErrorCode["NO_VIDEO_FOUND"] = 3003] = "NO_VIDEO_FOUND";
|
|
96
|
-
ErrorCode[ErrorCode["NO_IMAGE_FOUND"] = 3004] = "NO_IMAGE_FOUND";
|
|
97
|
-
ErrorCode[ErrorCode["MESSAGE_SEND_FAILED"] = 4000] = "MESSAGE_SEND_FAILED";
|
|
98
|
-
ErrorCode[ErrorCode["MESSAGE_SEND_TIMEOUT"] = 4001] = "MESSAGE_SEND_TIMEOUT";
|
|
99
|
-
ErrorCode[ErrorCode["FORWARD_MESSAGE_FAILED"] = 4002] = "FORWARD_MESSAGE_FAILED";
|
|
100
|
-
})(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
|
|
101
|
-
exports.ErrorMessageMap = {
|
|
102
|
-
[ErrorCode.SUCCESS]: '操作成功',
|
|
103
|
-
[ErrorCode.UNKNOWN_ERROR]: '未知错误',
|
|
104
|
-
[ErrorCode.UNSUPPORTED_PLATFORM]: '不支持该平台链接',
|
|
105
|
-
[ErrorCode.PLATFORM_API_NOT_CONFIGURED]: '该平台暂未配置解析接口',
|
|
106
|
-
[ErrorCode.REQUEST_TIMEOUT]: '请求超时',
|
|
107
|
-
[ErrorCode.NETWORK_ERROR]: '网络请求失败',
|
|
108
|
-
[ErrorCode.DUPLICATE_PARSE]: '请勿重复解析',
|
|
109
|
-
[ErrorCode.INVALID_URL]: '无效的链接格式',
|
|
110
|
-
[ErrorCode.API_RETURN_ERROR]: 'API返回错误',
|
|
111
|
-
[ErrorCode.API_DATA_PARSE_FAILED]: '数据解析异常',
|
|
112
|
-
[ErrorCode.API_EMPTY_RESPONSE]: 'API返回空数据',
|
|
113
|
-
[ErrorCode.API_INVALID_RESPONSE]: 'API返回无效格式',
|
|
114
|
-
[ErrorCode.VIDEO_DOWNLOAD_FAILED]: '视频下载失败',
|
|
115
|
-
[ErrorCode.VIDEO_SIZE_EXCEEDED]: '视频大小超过限制',
|
|
116
|
-
[ErrorCode.UNSUPPORTED_CONTENT_TYPE]: '不支持的内容类型',
|
|
117
|
-
[ErrorCode.NO_VIDEO_FOUND]: '未找到视频内容',
|
|
118
|
-
[ErrorCode.NO_IMAGE_FOUND]: '未找到图片内容',
|
|
119
|
-
[ErrorCode.MESSAGE_SEND_FAILED]: '消息发送失败',
|
|
120
|
-
[ErrorCode.MESSAGE_SEND_TIMEOUT]: '消息发送超时',
|
|
121
|
-
[ErrorCode.FORWARD_MESSAGE_FAILED]: '合并转发失败',
|
|
122
|
-
};
|
|
123
77
|
const processed = new Map();
|
|
124
78
|
const linkBuffer = new Map();
|
|
125
79
|
const logger = new koishi_1.Logger(exports.name);
|
|
@@ -166,13 +120,8 @@ const VARIABLE_MAPPING = {
|
|
|
166
120
|
'直播间ID': ['room_id', 'live.room_id', 'data.room_id', 'live.room_id', 'data.live.room_id'],
|
|
167
121
|
'直播间状态': ['status', 'live.status', 'data.status', 'room.status', 'data.live.status'],
|
|
168
122
|
'图片数量': ['count', 'data.count', 'item.count', 'images.length', 'data.images.length', 'data.item.count'],
|
|
169
|
-
'作者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']
|
|
170
|
-
'视频备用链接': ['data.video_backup', 'video_backup']
|
|
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']
|
|
171
124
|
};
|
|
172
|
-
function getErrorInfo(code, detail) {
|
|
173
|
-
const baseMsg = exports.ErrorMessageMap[code] || exports.ErrorMessageMap[ErrorCode.UNKNOWN_ERROR];
|
|
174
|
-
return detail ? `[错误码: ${code}] ${baseMsg}:${detail}` : `[错误码: ${code}] ${baseMsg}`;
|
|
175
|
-
}
|
|
176
125
|
function getErrorMessage(error) {
|
|
177
126
|
if (error instanceof Error)
|
|
178
127
|
return error.message;
|
|
@@ -201,7 +150,7 @@ async function downloadVideoThread(workerData) {
|
|
|
201
150
|
worker.on('error', reject);
|
|
202
151
|
worker.on('exit', (code) => {
|
|
203
152
|
if (code !== 0)
|
|
204
|
-
reject(new Error(
|
|
153
|
+
reject(new Error(`下载线程异常退出,代码:${code}`));
|
|
205
154
|
});
|
|
206
155
|
});
|
|
207
156
|
}
|
|
@@ -237,11 +186,11 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
237
186
|
const filePath = path_1.default.join(dir, `${filename}.mp4`);
|
|
238
187
|
try {
|
|
239
188
|
if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
|
|
240
|
-
return { filePath: '',
|
|
189
|
+
return { filePath: '', success: false };
|
|
241
190
|
}
|
|
242
191
|
const fileSize = await getFileSize(url, userAgent);
|
|
243
192
|
if (maxSize > 0 && fileSize > maxSize) {
|
|
244
|
-
return { filePath: '',
|
|
193
|
+
return { filePath: '', success: false };
|
|
245
194
|
}
|
|
246
195
|
if (threads <= 0 || fileSize === 0) {
|
|
247
196
|
const response = await (0, axios_1.default)({
|
|
@@ -255,7 +204,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
255
204
|
});
|
|
256
205
|
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
257
206
|
await (0, promises_1.pipeline)(response.data, writeStream);
|
|
258
|
-
return { filePath,
|
|
207
|
+
return { filePath, success: true };
|
|
259
208
|
}
|
|
260
209
|
const totalSize = fileSize * 1024 * 1024;
|
|
261
210
|
const chunkSize = Math.ceil(totalSize / threads);
|
|
@@ -281,7 +230,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
281
230
|
fs_1.default.unlinkSync(result.filePath);
|
|
282
231
|
}
|
|
283
232
|
writeStream.end();
|
|
284
|
-
return { filePath,
|
|
233
|
+
return { filePath, success: true };
|
|
285
234
|
}
|
|
286
235
|
catch (error) {
|
|
287
236
|
if (fs_1.default.existsSync(filePath)) {
|
|
@@ -294,7 +243,8 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
294
243
|
}
|
|
295
244
|
catch (e) { }
|
|
296
245
|
});
|
|
297
|
-
|
|
246
|
+
logger.error(`视频下载失败: ${getErrorMessage(error)}`);
|
|
247
|
+
return { filePath: '', success: false };
|
|
298
248
|
}
|
|
299
249
|
}
|
|
300
250
|
function extractUrl(content) {
|
|
@@ -465,14 +415,29 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
465
415
|
const root = rawResponse || {};
|
|
466
416
|
const data = root.data || root.result || root || {};
|
|
467
417
|
const stat = {};
|
|
418
|
+
let totalImageCount = 0;
|
|
468
419
|
Object.entries(VARIABLE_MAPPING).forEach(([varName, keys]) => {
|
|
469
420
|
let value = findValueInObject(data, keys) || findValueInObject(root, keys);
|
|
470
421
|
if (varName === '图片数量' && value === undefined) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
422
|
+
let imgCount = 0;
|
|
423
|
+
const imgSources = [
|
|
424
|
+
data.images, data.pics, data.pic_urls, data.image_list, data.imgurl,
|
|
425
|
+
root.images, root.pics, root.pic_urls, root.image_list, root.imgurl,
|
|
426
|
+
data.item?.images
|
|
427
|
+
];
|
|
428
|
+
for (const source of imgSources) {
|
|
429
|
+
if (Array.isArray(source) && source.length > 0) {
|
|
430
|
+
imgCount = source.filter(i => i && typeof i === 'string').length;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
totalImageCount = imgCount;
|
|
435
|
+
const cover = data.cover || data.video?.fm || data.imgurl || data.pic || data.thumbnail || data.cover_url ||
|
|
436
|
+
data.item?.cover || root.cover || data.live?.cover || data.live?.keyframe || '';
|
|
437
|
+
if (cover && imgCount > 0) {
|
|
438
|
+
imgCount = imgSources.find(source => Array.isArray(source))?.filter(i => i && typeof i === 'string' && i !== cover).length || 0;
|
|
439
|
+
}
|
|
440
|
+
value = totalImageCount;
|
|
476
441
|
}
|
|
477
442
|
if (value !== undefined && value !== null && value !== '' && value !== 0) {
|
|
478
443
|
stat[varName] = value;
|
|
@@ -493,9 +458,13 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
493
458
|
const title = stat['标题'] || data.note_title || data.title || data.content_title || data.video?.title ||
|
|
494
459
|
data.item?.title || root.title || data.live?.title || '无标题';
|
|
495
460
|
let author = stat['作者'] || data.author?.name || data.nickname || data.user_name || data.owner?.name ||
|
|
496
|
-
data.item?.author || root.author || data.user?.name || data.live?.author || '
|
|
497
|
-
if (typeof author === 'object'
|
|
498
|
-
author =
|
|
461
|
+
data.item?.author || root.author || data.user?.name || data.live?.author || '';
|
|
462
|
+
if (typeof author === 'object') {
|
|
463
|
+
author = '';
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
author = author || '未知作者';
|
|
467
|
+
}
|
|
499
468
|
const rawDesc = stat['简介'] || data.note_desc || data.content || data.text || data.description ||
|
|
500
469
|
data.video?.desc || data.item?.description || root.desc || root.description ||
|
|
501
470
|
data.live?.desc || (title !== '无标题' ? title : '') || '暂无简介';
|
|
@@ -513,7 +482,7 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
513
482
|
];
|
|
514
483
|
for (const source of imgSources) {
|
|
515
484
|
if (Array.isArray(source) && source.length > 0) {
|
|
516
|
-
images = source.filter(i => i && typeof i === 'string');
|
|
485
|
+
images = source.filter(i => i && typeof i === 'string' && i !== cover);
|
|
517
486
|
break;
|
|
518
487
|
}
|
|
519
488
|
}
|
|
@@ -534,6 +503,9 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
534
503
|
else {
|
|
535
504
|
delete stat['视频时长'];
|
|
536
505
|
}
|
|
506
|
+
if (stat['图片数量'] === 0) {
|
|
507
|
+
delete stat['图片数量'];
|
|
508
|
+
}
|
|
537
509
|
const sizeVal = stat['文件大小'];
|
|
538
510
|
if (sizeVal && !String(sizeVal).includes('MB')) {
|
|
539
511
|
const num = Number(sizeVal);
|
|
@@ -560,8 +532,6 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
560
532
|
stat['粉丝数'] = data.followers_count;
|
|
561
533
|
if (data.ip_info_str)
|
|
562
534
|
stat['IP属地'] = data.ip_info_str;
|
|
563
|
-
if (data.video_backup)
|
|
564
|
-
stat['视频备用链接'] = data.video_backup;
|
|
565
535
|
return {
|
|
566
536
|
type: type,
|
|
567
537
|
rawData: rawResponse,
|
|
@@ -574,6 +544,7 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
574
544
|
duration,
|
|
575
545
|
durationFormatted,
|
|
576
546
|
stat,
|
|
547
|
+
totalImageCount,
|
|
577
548
|
live_photo,
|
|
578
549
|
h_w,
|
|
579
550
|
jx: data.jx || null,
|
|
@@ -598,8 +569,7 @@ function generateFormattedText(parseData, config) {
|
|
|
598
569
|
收藏:${'${收藏数}'}
|
|
599
570
|
转发:${'${转发数}'}
|
|
600
571
|
播放:${'${播放数}'}
|
|
601
|
-
评论:${'${评论数}'}
|
|
602
|
-
视频备用链接:${'${视频备用链接}'}`;
|
|
572
|
+
评论:${'${评论数}'}`;
|
|
603
573
|
}
|
|
604
574
|
let result = format;
|
|
605
575
|
const varMatches = result.match(/\$\{([^}]+)\}/g) || [];
|
|
@@ -702,82 +672,67 @@ function apply(ctx, config) {
|
|
|
702
672
|
realUrl = cleanUrl(realUrl);
|
|
703
673
|
const platform = getPlatformType(realUrl);
|
|
704
674
|
if (!platform) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${url}`);
|
|
708
|
-
return { data: null, code, msg };
|
|
675
|
+
logger.error(`不支持的平台链接: ${url}`);
|
|
676
|
+
return { data: null, success: false, msg: '不支持该平台链接' };
|
|
709
677
|
}
|
|
710
678
|
const apiUrl = API_CONFIG[platform];
|
|
711
679
|
if (!apiUrl) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${platform}`);
|
|
715
|
-
return { data: null, code, msg };
|
|
680
|
+
logger.error(`该平台暂未配置解析接口: ${platform}`);
|
|
681
|
+
return { data: null, success: false, msg: '该平台暂未配置解析接口' };
|
|
716
682
|
}
|
|
717
683
|
try {
|
|
718
684
|
const resData = await parseWithRetry(realUrl, platform, config.retryTimes);
|
|
719
685
|
if (!resData || Object.keys(resData).length === 0) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
logger.error(`[${code}] ${url}`);
|
|
723
|
-
return { data: null, code, msg };
|
|
686
|
+
logger.error(`API返回空数据: ${url}`);
|
|
687
|
+
return { data: null, success: false, msg: '解析失败,API返回空数据' };
|
|
724
688
|
}
|
|
725
689
|
const isSuccess = resData.code === 0 || resData.code === 200 || resData.code === 1 ||
|
|
726
690
|
(resData.msg && (resData.msg.includes('解析成功') || resData.msg.includes('success') || resData.msg.includes('请求成功') || resData.msg === 'video' || resData.msg === 'cv' || resData.msg === 'live')) ||
|
|
727
691
|
!!resData.data || !!resData.result || !!resData.video || !!resData.images || !!resData.imgurl;
|
|
728
692
|
if (!isSuccess) {
|
|
729
693
|
const apiErrorMsg = resData.msg || resData.error || '解析失败';
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
logger.error(`[${code}] API返回错误: ${url}, 错误: ${apiErrorMsg}`);
|
|
733
|
-
return { data: null, code, msg };
|
|
694
|
+
logger.error(`API返回错误: ${url} - ${apiErrorMsg}`);
|
|
695
|
+
return { data: null, success: false, msg: `解析失败: ${apiErrorMsg}` };
|
|
734
696
|
}
|
|
735
697
|
try {
|
|
736
698
|
const parseResult = parseData(resData, config.maxDescLength);
|
|
737
|
-
logger.info(
|
|
699
|
+
logger.info(`解析成功: ${url}`);
|
|
738
700
|
return {
|
|
739
701
|
data: parseResult,
|
|
740
|
-
|
|
741
|
-
msg:
|
|
702
|
+
success: true,
|
|
703
|
+
msg: '解析成功'
|
|
742
704
|
};
|
|
743
705
|
}
|
|
744
706
|
catch (parseError) {
|
|
745
707
|
const errorMsg = getErrorMessage(parseError);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
logger.error(`[${code}] 解析数据失败: ${url}, 错误: ${errorMsg}`);
|
|
749
|
-
return { data: null, code, msg };
|
|
708
|
+
logger.error(`解析数据失败: ${url} - ${errorMsg}`);
|
|
709
|
+
return { data: null, success: false, msg: `解析数据失败: ${errorMsg}` };
|
|
750
710
|
}
|
|
751
711
|
}
|
|
752
712
|
catch (error) {
|
|
753
713
|
const errorMsg = getErrorMessage(error);
|
|
754
|
-
let
|
|
714
|
+
let msg = '未知错误';
|
|
755
715
|
if (errorMsg.includes('timeout')) {
|
|
756
|
-
|
|
716
|
+
msg = '请求超时';
|
|
757
717
|
}
|
|
758
718
|
else if (errorMsg.includes('Network') || errorMsg.includes('network') || errorMsg.includes('404') || errorMsg.includes('500')) {
|
|
759
|
-
|
|
719
|
+
msg = '网络请求失败';
|
|
760
720
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
const msg = getErrorInfo(code, errorMsg);
|
|
765
|
-
logger.error(`[${code}] 解析请求失败: ${url}, 错误: ${errorMsg}`);
|
|
766
|
-
return { data: null, code, msg };
|
|
721
|
+
logger.error(`解析请求失败: ${url} - ${errorMsg}`);
|
|
722
|
+
return { data: null, success: false, msg };
|
|
767
723
|
}
|
|
768
724
|
}
|
|
769
725
|
async function processSingleUrl(session, url) {
|
|
770
726
|
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
771
727
|
const now = Date.now();
|
|
772
728
|
if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000) {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
return { data: null, code, msg };
|
|
729
|
+
logger.warn(`相同链接重复解析: ${url}`);
|
|
730
|
+
return { data: null, success: false, msg: '请勿重复解析相同链接' };
|
|
776
731
|
}
|
|
777
732
|
processed.set(hash, now);
|
|
778
733
|
const result = await parse(url);
|
|
779
|
-
if (!result.
|
|
780
|
-
return { data: null,
|
|
734
|
+
if (!result.success)
|
|
735
|
+
return { data: null, success: false, msg: result.msg };
|
|
781
736
|
const parseData = result.data;
|
|
782
737
|
const text = generateFormattedText(parseData, config);
|
|
783
738
|
return {
|
|
@@ -787,21 +742,22 @@ function apply(ctx, config) {
|
|
|
787
742
|
images: parseData.images,
|
|
788
743
|
video: parseData.video,
|
|
789
744
|
type: parseData.type,
|
|
745
|
+
totalImageCount: parseData.totalImageCount,
|
|
790
746
|
live_photo: parseData.live_photo,
|
|
791
747
|
h_w: parseData.h_w,
|
|
792
748
|
quality_urls: parseData.quality_urls,
|
|
793
749
|
default_quality: parseData.default_quality,
|
|
794
750
|
download_url: parseData.download_url
|
|
795
751
|
},
|
|
796
|
-
|
|
797
|
-
msg:
|
|
752
|
+
success: true,
|
|
753
|
+
msg: '处理成功'
|
|
798
754
|
};
|
|
799
755
|
}
|
|
800
756
|
async function sendTimeout(session, content) {
|
|
801
757
|
if (config.videoSendTimeout <= 0) {
|
|
802
758
|
return session.send(content).catch((err) => {
|
|
803
759
|
const errorMsg = getErrorMessage(err);
|
|
804
|
-
logger.error(
|
|
760
|
+
logger.error(`发送消息失败: ${errorMsg}`);
|
|
805
761
|
if (!config.ignoreSendError)
|
|
806
762
|
return null;
|
|
807
763
|
return null;
|
|
@@ -812,8 +768,7 @@ function apply(ctx, config) {
|
|
|
812
768
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.videoSendTimeout))
|
|
813
769
|
]).catch((err) => {
|
|
814
770
|
const errorMsg = getErrorMessage(err);
|
|
815
|
-
|
|
816
|
-
logger.error(`[${code}] 发送消息失败: ${errorMsg}`);
|
|
771
|
+
logger.error(`发送消息超时: ${errorMsg}`);
|
|
817
772
|
if (!config.ignoreSendError)
|
|
818
773
|
return null;
|
|
819
774
|
return null;
|
|
@@ -831,7 +786,7 @@ function apply(ctx, config) {
|
|
|
831
786
|
const errors = [];
|
|
832
787
|
for (const url of urls) {
|
|
833
788
|
const result = await processSingleUrl(session, url);
|
|
834
|
-
if (result.
|
|
789
|
+
if (result.success) {
|
|
835
790
|
items.push(result.data);
|
|
836
791
|
}
|
|
837
792
|
else {
|
|
@@ -856,19 +811,15 @@ function apply(ctx, config) {
|
|
|
856
811
|
if (enableForward) {
|
|
857
812
|
if (item.text)
|
|
858
813
|
forwardMessages.push(buildForwardNode(session, item.text, botName));
|
|
859
|
-
if (item.cover)
|
|
814
|
+
if (item.cover && item.type !== '图集') {
|
|
860
815
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.cover), botName));
|
|
861
|
-
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
862
|
-
for (const img of item.images) {
|
|
863
|
-
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(img), botName));
|
|
864
|
-
}
|
|
865
816
|
}
|
|
866
817
|
if (item.video && config.showVideoFile) {
|
|
867
818
|
try {
|
|
868
819
|
if (config.downloadVideoBeforeSend) {
|
|
869
820
|
const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
870
821
|
const dl = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
871
|
-
if (dl.
|
|
822
|
+
if (dl.success) {
|
|
872
823
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.file(dl.filePath), botName));
|
|
873
824
|
}
|
|
874
825
|
else {
|
|
@@ -883,22 +834,22 @@ function apply(ctx, config) {
|
|
|
883
834
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(item.video), botName));
|
|
884
835
|
}
|
|
885
836
|
}
|
|
837
|
+
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
838
|
+
forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.totalImageCount}张)`, botName));
|
|
839
|
+
for (const img of item.images) {
|
|
840
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(img), botName));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
886
843
|
}
|
|
887
844
|
else {
|
|
888
845
|
if (item.text) {
|
|
889
846
|
await sendTimeout(session, item.text);
|
|
890
847
|
await delay(300);
|
|
891
848
|
}
|
|
892
|
-
if (item.cover) {
|
|
849
|
+
if (item.cover && item.type !== '图集') {
|
|
893
850
|
await sendTimeout(session, koishi_1.h.image(item.cover));
|
|
894
851
|
await delay(300);
|
|
895
852
|
}
|
|
896
|
-
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
897
|
-
for (const img of item.images) {
|
|
898
|
-
await sendTimeout(session, koishi_1.h.image(img));
|
|
899
|
-
await delay(200);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
853
|
if (item.video && config.showVideoFile) {
|
|
903
854
|
try {
|
|
904
855
|
await sendTimeout(session, koishi_1.h.video(item.video));
|
|
@@ -908,9 +859,19 @@ function apply(ctx, config) {
|
|
|
908
859
|
}
|
|
909
860
|
await delay(500);
|
|
910
861
|
}
|
|
862
|
+
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
863
|
+
await sendTimeout(session, `📸 图集内容(共${item.totalImageCount}张)`);
|
|
864
|
+
await delay(300);
|
|
865
|
+
for (const img of item.images) {
|
|
866
|
+
await sendTimeout(session, koishi_1.h.image(img));
|
|
867
|
+
await delay(200);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
911
870
|
}
|
|
912
871
|
}
|
|
913
|
-
catch (e) {
|
|
872
|
+
catch (e) {
|
|
873
|
+
logger.error(`处理消息发送失败: ${getErrorMessage(e)}`);
|
|
874
|
+
}
|
|
914
875
|
}
|
|
915
876
|
if (enableForward && forwardMessages.length) {
|
|
916
877
|
try {
|
|
@@ -937,13 +898,15 @@ function apply(ctx, config) {
|
|
|
937
898
|
});
|
|
938
899
|
ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
|
|
939
900
|
const us = extractUrl(url);
|
|
940
|
-
if (!us.length)
|
|
941
|
-
|
|
901
|
+
if (!us.length) {
|
|
902
|
+
await sendTimeout(session, '无效的视频链接');
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
942
905
|
await flush(session, us);
|
|
943
906
|
});
|
|
944
|
-
ctx.command('clear-cache', '清空缓存').action(() => {
|
|
907
|
+
ctx.command('clear-cache', '清空缓存').action(async ({ session }) => {
|
|
945
908
|
clearAllCache();
|
|
946
|
-
|
|
909
|
+
await sendTimeout(session, '✅ 缓存已清空');
|
|
947
910
|
});
|
|
948
911
|
setInterval(() => {
|
|
949
912
|
const now = Date.now();
|
|
@@ -955,5 +918,5 @@ function apply(ctx, config) {
|
|
|
955
918
|
logger.info('自动清理缓存完成');
|
|
956
919
|
}, config.autoClearCacheInterval * 60 * 1000);
|
|
957
920
|
}
|
|
958
|
-
logger.info('
|
|
921
|
+
logger.info('视频解析插件已启动');
|
|
959
922
|
}
|
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
|
|