koishi-plugin-video-parser-all 0.6.9 → 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 +83 -139
- 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) {
|
|
@@ -463,6 +415,7 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
463
415
|
const root = rawResponse || {};
|
|
464
416
|
const data = root.data || root.result || root || {};
|
|
465
417
|
const stat = {};
|
|
418
|
+
let totalImageCount = 0;
|
|
466
419
|
Object.entries(VARIABLE_MAPPING).forEach(([varName, keys]) => {
|
|
467
420
|
let value = findValueInObject(data, keys) || findValueInObject(root, keys);
|
|
468
421
|
if (varName === '图片数量' && value === undefined) {
|
|
@@ -478,12 +431,13 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
478
431
|
break;
|
|
479
432
|
}
|
|
480
433
|
}
|
|
434
|
+
totalImageCount = imgCount;
|
|
481
435
|
const cover = data.cover || data.video?.fm || data.imgurl || data.pic || data.thumbnail || data.cover_url ||
|
|
482
436
|
data.item?.cover || root.cover || data.live?.cover || data.live?.keyframe || '';
|
|
483
437
|
if (cover && imgCount > 0) {
|
|
484
438
|
imgCount = imgSources.find(source => Array.isArray(source))?.filter(i => i && typeof i === 'string' && i !== cover).length || 0;
|
|
485
439
|
}
|
|
486
|
-
value =
|
|
440
|
+
value = totalImageCount;
|
|
487
441
|
}
|
|
488
442
|
if (value !== undefined && value !== null && value !== '' && value !== 0) {
|
|
489
443
|
stat[varName] = value;
|
|
@@ -590,6 +544,7 @@ function parseData(rawResponse, maxDescLength) {
|
|
|
590
544
|
duration,
|
|
591
545
|
durationFormatted,
|
|
592
546
|
stat,
|
|
547
|
+
totalImageCount,
|
|
593
548
|
live_photo,
|
|
594
549
|
h_w,
|
|
595
550
|
jx: data.jx || null,
|
|
@@ -717,82 +672,67 @@ function apply(ctx, config) {
|
|
|
717
672
|
realUrl = cleanUrl(realUrl);
|
|
718
673
|
const platform = getPlatformType(realUrl);
|
|
719
674
|
if (!platform) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${url}`);
|
|
723
|
-
return { data: null, code, msg };
|
|
675
|
+
logger.error(`不支持的平台链接: ${url}`);
|
|
676
|
+
return { data: null, success: false, msg: '不支持该平台链接' };
|
|
724
677
|
}
|
|
725
678
|
const apiUrl = API_CONFIG[platform];
|
|
726
679
|
if (!apiUrl) {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${platform}`);
|
|
730
|
-
return { data: null, code, msg };
|
|
680
|
+
logger.error(`该平台暂未配置解析接口: ${platform}`);
|
|
681
|
+
return { data: null, success: false, msg: '该平台暂未配置解析接口' };
|
|
731
682
|
}
|
|
732
683
|
try {
|
|
733
684
|
const resData = await parseWithRetry(realUrl, platform, config.retryTimes);
|
|
734
685
|
if (!resData || Object.keys(resData).length === 0) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
logger.error(`[${code}] ${url}`);
|
|
738
|
-
return { data: null, code, msg };
|
|
686
|
+
logger.error(`API返回空数据: ${url}`);
|
|
687
|
+
return { data: null, success: false, msg: '解析失败,API返回空数据' };
|
|
739
688
|
}
|
|
740
689
|
const isSuccess = resData.code === 0 || resData.code === 200 || resData.code === 1 ||
|
|
741
690
|
(resData.msg && (resData.msg.includes('解析成功') || resData.msg.includes('success') || resData.msg.includes('请求成功') || resData.msg === 'video' || resData.msg === 'cv' || resData.msg === 'live')) ||
|
|
742
691
|
!!resData.data || !!resData.result || !!resData.video || !!resData.images || !!resData.imgurl;
|
|
743
692
|
if (!isSuccess) {
|
|
744
693
|
const apiErrorMsg = resData.msg || resData.error || '解析失败';
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
logger.error(`[${code}] API返回错误: ${url}, 错误: ${apiErrorMsg}`);
|
|
748
|
-
return { data: null, code, msg };
|
|
694
|
+
logger.error(`API返回错误: ${url} - ${apiErrorMsg}`);
|
|
695
|
+
return { data: null, success: false, msg: `解析失败: ${apiErrorMsg}` };
|
|
749
696
|
}
|
|
750
697
|
try {
|
|
751
698
|
const parseResult = parseData(resData, config.maxDescLength);
|
|
752
|
-
logger.info(
|
|
699
|
+
logger.info(`解析成功: ${url}`);
|
|
753
700
|
return {
|
|
754
701
|
data: parseResult,
|
|
755
|
-
|
|
756
|
-
msg:
|
|
702
|
+
success: true,
|
|
703
|
+
msg: '解析成功'
|
|
757
704
|
};
|
|
758
705
|
}
|
|
759
706
|
catch (parseError) {
|
|
760
707
|
const errorMsg = getErrorMessage(parseError);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
logger.error(`[${code}] 解析数据失败: ${url}, 错误: ${errorMsg}`);
|
|
764
|
-
return { data: null, code, msg };
|
|
708
|
+
logger.error(`解析数据失败: ${url} - ${errorMsg}`);
|
|
709
|
+
return { data: null, success: false, msg: `解析数据失败: ${errorMsg}` };
|
|
765
710
|
}
|
|
766
711
|
}
|
|
767
712
|
catch (error) {
|
|
768
713
|
const errorMsg = getErrorMessage(error);
|
|
769
|
-
let
|
|
714
|
+
let msg = '未知错误';
|
|
770
715
|
if (errorMsg.includes('timeout')) {
|
|
771
|
-
|
|
716
|
+
msg = '请求超时';
|
|
772
717
|
}
|
|
773
718
|
else if (errorMsg.includes('Network') || errorMsg.includes('network') || errorMsg.includes('404') || errorMsg.includes('500')) {
|
|
774
|
-
|
|
719
|
+
msg = '网络请求失败';
|
|
775
720
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
const msg = getErrorInfo(code, errorMsg);
|
|
780
|
-
logger.error(`[${code}] 解析请求失败: ${url}, 错误: ${errorMsg}`);
|
|
781
|
-
return { data: null, code, msg };
|
|
721
|
+
logger.error(`解析请求失败: ${url} - ${errorMsg}`);
|
|
722
|
+
return { data: null, success: false, msg };
|
|
782
723
|
}
|
|
783
724
|
}
|
|
784
725
|
async function processSingleUrl(session, url) {
|
|
785
726
|
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
786
727
|
const now = Date.now();
|
|
787
728
|
if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
return { data: null, code, msg };
|
|
729
|
+
logger.warn(`相同链接重复解析: ${url}`);
|
|
730
|
+
return { data: null, success: false, msg: '请勿重复解析相同链接' };
|
|
791
731
|
}
|
|
792
732
|
processed.set(hash, now);
|
|
793
733
|
const result = await parse(url);
|
|
794
|
-
if (!result.
|
|
795
|
-
return { data: null,
|
|
734
|
+
if (!result.success)
|
|
735
|
+
return { data: null, success: false, msg: result.msg };
|
|
796
736
|
const parseData = result.data;
|
|
797
737
|
const text = generateFormattedText(parseData, config);
|
|
798
738
|
return {
|
|
@@ -802,21 +742,22 @@ function apply(ctx, config) {
|
|
|
802
742
|
images: parseData.images,
|
|
803
743
|
video: parseData.video,
|
|
804
744
|
type: parseData.type,
|
|
745
|
+
totalImageCount: parseData.totalImageCount,
|
|
805
746
|
live_photo: parseData.live_photo,
|
|
806
747
|
h_w: parseData.h_w,
|
|
807
748
|
quality_urls: parseData.quality_urls,
|
|
808
749
|
default_quality: parseData.default_quality,
|
|
809
750
|
download_url: parseData.download_url
|
|
810
751
|
},
|
|
811
|
-
|
|
812
|
-
msg:
|
|
752
|
+
success: true,
|
|
753
|
+
msg: '处理成功'
|
|
813
754
|
};
|
|
814
755
|
}
|
|
815
756
|
async function sendTimeout(session, content) {
|
|
816
757
|
if (config.videoSendTimeout <= 0) {
|
|
817
758
|
return session.send(content).catch((err) => {
|
|
818
759
|
const errorMsg = getErrorMessage(err);
|
|
819
|
-
logger.error(
|
|
760
|
+
logger.error(`发送消息失败: ${errorMsg}`);
|
|
820
761
|
if (!config.ignoreSendError)
|
|
821
762
|
return null;
|
|
822
763
|
return null;
|
|
@@ -827,8 +768,7 @@ function apply(ctx, config) {
|
|
|
827
768
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.videoSendTimeout))
|
|
828
769
|
]).catch((err) => {
|
|
829
770
|
const errorMsg = getErrorMessage(err);
|
|
830
|
-
|
|
831
|
-
logger.error(`[${code}] 发送消息失败: ${errorMsg}`);
|
|
771
|
+
logger.error(`发送消息超时: ${errorMsg}`);
|
|
832
772
|
if (!config.ignoreSendError)
|
|
833
773
|
return null;
|
|
834
774
|
return null;
|
|
@@ -846,7 +786,7 @@ function apply(ctx, config) {
|
|
|
846
786
|
const errors = [];
|
|
847
787
|
for (const url of urls) {
|
|
848
788
|
const result = await processSingleUrl(session, url);
|
|
849
|
-
if (result.
|
|
789
|
+
if (result.success) {
|
|
850
790
|
items.push(result.data);
|
|
851
791
|
}
|
|
852
792
|
else {
|
|
@@ -879,7 +819,7 @@ function apply(ctx, config) {
|
|
|
879
819
|
if (config.downloadVideoBeforeSend) {
|
|
880
820
|
const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
881
821
|
const dl = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
882
|
-
if (dl.
|
|
822
|
+
if (dl.success) {
|
|
883
823
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.file(dl.filePath), botName));
|
|
884
824
|
}
|
|
885
825
|
else {
|
|
@@ -895,7 +835,7 @@ function apply(ctx, config) {
|
|
|
895
835
|
}
|
|
896
836
|
}
|
|
897
837
|
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
898
|
-
forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.
|
|
838
|
+
forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.totalImageCount}张)`, botName));
|
|
899
839
|
for (const img of item.images) {
|
|
900
840
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(img), botName));
|
|
901
841
|
}
|
|
@@ -920,7 +860,7 @@ function apply(ctx, config) {
|
|
|
920
860
|
await delay(500);
|
|
921
861
|
}
|
|
922
862
|
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
923
|
-
await sendTimeout(session, `📸 图集内容(共${item.
|
|
863
|
+
await sendTimeout(session, `📸 图集内容(共${item.totalImageCount}张)`);
|
|
924
864
|
await delay(300);
|
|
925
865
|
for (const img of item.images) {
|
|
926
866
|
await sendTimeout(session, koishi_1.h.image(img));
|
|
@@ -929,7 +869,9 @@ function apply(ctx, config) {
|
|
|
929
869
|
}
|
|
930
870
|
}
|
|
931
871
|
}
|
|
932
|
-
catch (e) {
|
|
872
|
+
catch (e) {
|
|
873
|
+
logger.error(`处理消息发送失败: ${getErrorMessage(e)}`);
|
|
874
|
+
}
|
|
933
875
|
}
|
|
934
876
|
if (enableForward && forwardMessages.length) {
|
|
935
877
|
try {
|
|
@@ -956,13 +898,15 @@ function apply(ctx, config) {
|
|
|
956
898
|
});
|
|
957
899
|
ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
|
|
958
900
|
const us = extractUrl(url);
|
|
959
|
-
if (!us.length)
|
|
960
|
-
|
|
901
|
+
if (!us.length) {
|
|
902
|
+
await sendTimeout(session, '无效的视频链接');
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
961
905
|
await flush(session, us);
|
|
962
906
|
});
|
|
963
|
-
ctx.command('clear-cache', '清空缓存').action(() => {
|
|
907
|
+
ctx.command('clear-cache', '清空缓存').action(async ({ session }) => {
|
|
964
908
|
clearAllCache();
|
|
965
|
-
|
|
909
|
+
await sendTimeout(session, '✅ 缓存已清空');
|
|
966
910
|
});
|
|
967
911
|
setInterval(() => {
|
|
968
912
|
const now = Date.now();
|
|
@@ -974,5 +918,5 @@ function apply(ctx, config) {
|
|
|
974
918
|
logger.info('自动清理缓存完成');
|
|
975
919
|
}, config.autoClearCacheInterval * 60 * 1000);
|
|
976
920
|
}
|
|
977
|
-
logger.info('
|
|
921
|
+
logger.info('视频解析插件已启动');
|
|
978
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
|
|