koishi-plugin-video-parser-all 0.5.1 → 0.5.3
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 +28 -0
- package/lib/index.js +397 -189
- package/package.json +2 -2
- package/readme.md +0 -4
package/lib/index.d.ts
CHANGED
|
@@ -61,4 +61,32 @@ 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
|
+
DOUYIN_PARSE_FAILED = 5001,
|
|
86
|
+
XIAOHONGSHU_PARSE_FAILED = 5002,
|
|
87
|
+
BILIBILI_PARSE_FAILED = 5003,
|
|
88
|
+
KUAISHOU_PARSE_FAILED = 5004,
|
|
89
|
+
WEIBO_PARSE_FAILED = 5005
|
|
90
|
+
}
|
|
91
|
+
export declare const ErrorMessageMap: Record<ErrorCode, string>;
|
|
64
92
|
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.Config = exports.name = void 0;
|
|
6
|
+
exports.ErrorMessageMap = exports.ErrorCode = 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"));
|
|
@@ -22,7 +22,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
22
22
|
sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
|
|
23
23
|
}).description('基础设置'),
|
|
24
24
|
koishi_1.Schema.object({
|
|
25
|
-
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}').description('
|
|
25
|
+
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n音乐:${音乐名}').description('统一消息格式(无法获取的变量会自动隐藏)\n变量介绍:\n${标题} - 内容标题\n${作者} - 作者名称\n${简介} - 内容简介\n${视频时长} - 视频时长\n${点赞数} - 点赞数量\n${投币数} - 投币数量(仅B站)\n${收藏数} - 收藏数量\n${转发数} - 转发/分享数量\n${播放数} - 播放数量\n${评论数} - 评论数量\n${音乐名} - 背景音乐名称'),
|
|
26
26
|
}).description('统一消息格式'),
|
|
27
27
|
koishi_1.Schema.object({
|
|
28
28
|
showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
|
|
@@ -43,23 +43,78 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
43
43
|
}).description('错误与重试设置'),
|
|
44
44
|
koishi_1.Schema.object({
|
|
45
45
|
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot平台)'),
|
|
46
|
-
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('
|
|
46
|
+
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频'),
|
|
47
47
|
maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('最大视频大小限制(MB,0为不限制)'),
|
|
48
48
|
downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0为不使用多线程,1-10为启用对应线程数)'),
|
|
49
|
-
}).description('
|
|
49
|
+
}).description('发送方式设置'),
|
|
50
50
|
koishi_1.Schema.object({
|
|
51
51
|
messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒,批量处理链接)'),
|
|
52
52
|
}).description('消息处理设置'),
|
|
53
53
|
koishi_1.Schema.object({
|
|
54
54
|
autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0为关闭)'),
|
|
55
|
-
}).description('
|
|
55
|
+
}).description('缓存清理设置'),
|
|
56
56
|
]);
|
|
57
|
+
var ErrorCode;
|
|
58
|
+
(function (ErrorCode) {
|
|
59
|
+
ErrorCode[ErrorCode["SUCCESS"] = 0] = "SUCCESS";
|
|
60
|
+
ErrorCode[ErrorCode["UNKNOWN_ERROR"] = 1000] = "UNKNOWN_ERROR";
|
|
61
|
+
ErrorCode[ErrorCode["UNSUPPORTED_PLATFORM"] = 1001] = "UNSUPPORTED_PLATFORM";
|
|
62
|
+
ErrorCode[ErrorCode["PLATFORM_API_NOT_CONFIGURED"] = 1002] = "PLATFORM_API_NOT_CONFIGURED";
|
|
63
|
+
ErrorCode[ErrorCode["REQUEST_TIMEOUT"] = 1003] = "REQUEST_TIMEOUT";
|
|
64
|
+
ErrorCode[ErrorCode["NETWORK_ERROR"] = 1004] = "NETWORK_ERROR";
|
|
65
|
+
ErrorCode[ErrorCode["DUPLICATE_PARSE"] = 1005] = "DUPLICATE_PARSE";
|
|
66
|
+
ErrorCode[ErrorCode["INVALID_URL"] = 1006] = "INVALID_URL";
|
|
67
|
+
ErrorCode[ErrorCode["API_RETURN_ERROR"] = 2000] = "API_RETURN_ERROR";
|
|
68
|
+
ErrorCode[ErrorCode["API_DATA_PARSE_FAILED"] = 2001] = "API_DATA_PARSE_FAILED";
|
|
69
|
+
ErrorCode[ErrorCode["API_EMPTY_RESPONSE"] = 2002] = "API_EMPTY_RESPONSE";
|
|
70
|
+
ErrorCode[ErrorCode["API_INVALID_RESPONSE"] = 2003] = "API_INVALID_RESPONSE";
|
|
71
|
+
ErrorCode[ErrorCode["VIDEO_DOWNLOAD_FAILED"] = 3000] = "VIDEO_DOWNLOAD_FAILED";
|
|
72
|
+
ErrorCode[ErrorCode["VIDEO_SIZE_EXCEEDED"] = 3001] = "VIDEO_SIZE_EXCEEDED";
|
|
73
|
+
ErrorCode[ErrorCode["UNSUPPORTED_CONTENT_TYPE"] = 3002] = "UNSUPPORTED_CONTENT_TYPE";
|
|
74
|
+
ErrorCode[ErrorCode["NO_VIDEO_FOUND"] = 3003] = "NO_VIDEO_FOUND";
|
|
75
|
+
ErrorCode[ErrorCode["NO_IMAGE_FOUND"] = 3004] = "NO_IMAGE_FOUND";
|
|
76
|
+
ErrorCode[ErrorCode["MESSAGE_SEND_FAILED"] = 4000] = "MESSAGE_SEND_FAILED";
|
|
77
|
+
ErrorCode[ErrorCode["MESSAGE_SEND_TIMEOUT"] = 4001] = "MESSAGE_SEND_TIMEOUT";
|
|
78
|
+
ErrorCode[ErrorCode["FORWARD_MESSAGE_FAILED"] = 4002] = "FORWARD_MESSAGE_FAILED";
|
|
79
|
+
ErrorCode[ErrorCode["DOUYIN_PARSE_FAILED"] = 5001] = "DOUYIN_PARSE_FAILED";
|
|
80
|
+
ErrorCode[ErrorCode["XIAOHONGSHU_PARSE_FAILED"] = 5002] = "XIAOHONGSHU_PARSE_FAILED";
|
|
81
|
+
ErrorCode[ErrorCode["BILIBILI_PARSE_FAILED"] = 5003] = "BILIBILI_PARSE_FAILED";
|
|
82
|
+
ErrorCode[ErrorCode["KUAISHOU_PARSE_FAILED"] = 5004] = "KUAISHOU_PARSE_FAILED";
|
|
83
|
+
ErrorCode[ErrorCode["WEIBO_PARSE_FAILED"] = 5005] = "WEIBO_PARSE_FAILED";
|
|
84
|
+
})(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
|
|
85
|
+
exports.ErrorMessageMap = {
|
|
86
|
+
[ErrorCode.SUCCESS]: '操作成功',
|
|
87
|
+
[ErrorCode.UNKNOWN_ERROR]: '未知错误',
|
|
88
|
+
[ErrorCode.UNSUPPORTED_PLATFORM]: '不支持该平台链接',
|
|
89
|
+
[ErrorCode.PLATFORM_API_NOT_CONFIGURED]: '该平台暂未配置解析接口',
|
|
90
|
+
[ErrorCode.REQUEST_TIMEOUT]: '请求超时',
|
|
91
|
+
[ErrorCode.NETWORK_ERROR]: '网络请求失败',
|
|
92
|
+
[ErrorCode.DUPLICATE_PARSE]: '请勿重复解析(相同链接解析间隔未到)',
|
|
93
|
+
[ErrorCode.INVALID_URL]: '无效的链接格式',
|
|
94
|
+
[ErrorCode.API_RETURN_ERROR]: 'API返回错误',
|
|
95
|
+
[ErrorCode.API_DATA_PARSE_FAILED]: '数据解析异常',
|
|
96
|
+
[ErrorCode.API_EMPTY_RESPONSE]: 'API返回空数据',
|
|
97
|
+
[ErrorCode.API_INVALID_RESPONSE]: 'API返回无效格式',
|
|
98
|
+
[ErrorCode.VIDEO_DOWNLOAD_FAILED]: '视频下载失败',
|
|
99
|
+
[ErrorCode.VIDEO_SIZE_EXCEEDED]: '视频大小超过限制',
|
|
100
|
+
[ErrorCode.UNSUPPORTED_CONTENT_TYPE]: '不支持的内容类型',
|
|
101
|
+
[ErrorCode.NO_VIDEO_FOUND]: '未找到视频内容',
|
|
102
|
+
[ErrorCode.NO_IMAGE_FOUND]: '未找到图片内容',
|
|
103
|
+
[ErrorCode.MESSAGE_SEND_FAILED]: '消息发送失败',
|
|
104
|
+
[ErrorCode.MESSAGE_SEND_TIMEOUT]: '消息发送超时',
|
|
105
|
+
[ErrorCode.FORWARD_MESSAGE_FAILED]: '合并转发失败',
|
|
106
|
+
[ErrorCode.DOUYIN_PARSE_FAILED]: '抖音链接解析失败',
|
|
107
|
+
[ErrorCode.XIAOHONGSHU_PARSE_FAILED]: '小红书链接解析失败',
|
|
108
|
+
[ErrorCode.BILIBILI_PARSE_FAILED]: 'B站链接解析失败',
|
|
109
|
+
[ErrorCode.KUAISHOU_PARSE_FAILED]: '快手链接解析失败',
|
|
110
|
+
[ErrorCode.WEIBO_PARSE_FAILED]: '微博链接解析失败',
|
|
111
|
+
};
|
|
57
112
|
const processed = new Map();
|
|
58
113
|
const linkBuffer = new Map();
|
|
59
114
|
const logger = new koishi_1.Logger(exports.name);
|
|
60
115
|
const PLATFORM_KEYWORDS = {
|
|
61
116
|
bilibili: ['bilibili', 'b23', 'B站', 'www.bilibili.com', 'm.bilibili.com', '哔哩哔哩', 'bilibili.com/opus', 'bilibili.com/video', 'b23.tv', 't.bilibili.com', 'bilibili.com/bangumi'],
|
|
62
|
-
kuaishou: ['kuaishou', '快手', 'v.kuaishou.com', 'www.kuaishou.com', 'kwimgs.com', 'kuaishou.com/app'
|
|
117
|
+
kuaishou: ['kuaishou', '快手', 'v.kuaishou.com', 'www.kuaishou.com', 'kwimgs.com', 'kuaishou.com/app'],
|
|
63
118
|
xiaohongshu: ['xiaohongshu', '小红书', 'xhslink.com', 'xiaohongshu.com', 'xhscdn.com', 'xiaohongshu.com/explore', 'xhslink.com/'],
|
|
64
119
|
weibo: ['weibo', '微博', 'weibo.com', 'video.weibo.com', 'm.weibo.cn', 'weibo.com/tv/show', 'weibo.com/feed'],
|
|
65
120
|
toutiao: ['toutiao', '今日头条', 'm.toutiao.com', 'toutiao.com', 'ixigua.com', 'toutiao.com/video', 'ixigua.com/i'],
|
|
@@ -69,16 +124,40 @@ const PLATFORM_KEYWORDS = {
|
|
|
69
124
|
zuiyou: ['zuiyou', '最右', 'xiaochuankeji.cn', 'izuiyou.com', 'izuiyou.com/topic']
|
|
70
125
|
};
|
|
71
126
|
const API_CONFIG = {
|
|
72
|
-
bilibili: 'https://api.
|
|
73
|
-
douyin: 'https://api.bugpk.com/api/
|
|
74
|
-
kuaishou: 'https://api.bugpk.com/api/
|
|
75
|
-
xiaohongshu: 'https://api.bugpk.com/api/
|
|
127
|
+
bilibili: 'https://api.bugpk.com/api/bilibili',
|
|
128
|
+
douyin: 'https://api.bugpk.com/api/dyjx',
|
|
129
|
+
kuaishou: 'https://api.bugpk.com/api/ksjx',
|
|
130
|
+
xiaohongshu: 'https://api.bugpk.com/api/xhsjx',
|
|
76
131
|
weibo: 'https://api.bugpk.com/api/weibo',
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
132
|
+
toutiao: 'https://api.bugpk.com/api/toutiao',
|
|
133
|
+
pipigx: 'https://api.bugpk.com/api/pipigx',
|
|
134
|
+
pipixia: 'https://api.bugpk.com/api/ppx',
|
|
135
|
+
zuiyou: 'https://api.bugpk.com/api/zuiyou'
|
|
136
|
+
};
|
|
137
|
+
const PLATFORM_ERROR_CODE_MAP = {
|
|
138
|
+
douyin: ErrorCode.DOUYIN_PARSE_FAILED,
|
|
139
|
+
xiaohongshu: ErrorCode.XIAOHONGSHU_PARSE_FAILED,
|
|
140
|
+
bilibili: ErrorCode.BILIBILI_PARSE_FAILED,
|
|
141
|
+
kuaishou: ErrorCode.KUAISHOU_PARSE_FAILED,
|
|
142
|
+
weibo: ErrorCode.WEIBO_PARSE_FAILED
|
|
143
|
+
};
|
|
144
|
+
const VARIABLE_MAPPING = {
|
|
145
|
+
'标题': ['title', 'Title', 'TITLE'],
|
|
146
|
+
'作者': ['author.name', 'author', 'name', 'Author', 'Name'],
|
|
147
|
+
'简介': ['desc', 'description', 'Desc', 'Description', 'content', 'Content'],
|
|
148
|
+
'视频时长': ['duration', 'Duration', 'time', 'Time'],
|
|
149
|
+
'点赞数': ['like', 'Like', 'attitudes_count', 'digg_count', 'praise'],
|
|
150
|
+
'投币数': ['coin', 'Coin', 'bi', 'Bi'],
|
|
151
|
+
'收藏数': ['collect', 'Collect', 'favorite', 'Favorite', 'star', 'Star'],
|
|
152
|
+
'转发数': ['share', 'Share', 'forward', 'Forward', 'repost'],
|
|
153
|
+
'播放数': ['view', 'View', 'play_count', 'PlayCount', 'play'],
|
|
154
|
+
'评论数': ['comment', 'Comment', 'comments_count', 'comment_count', 'discuss'],
|
|
155
|
+
'音乐名': ['music.title', 'music_name', 'audio_name', 'sound_name']
|
|
81
156
|
};
|
|
157
|
+
function getErrorInfo(code, detail) {
|
|
158
|
+
const baseMsg = exports.ErrorMessageMap[code] || exports.ErrorMessageMap[ErrorCode.UNKNOWN_ERROR];
|
|
159
|
+
return detail ? `[错误码: ${code}] ${baseMsg}:${detail}` : `[错误码: ${code}] ${baseMsg}`;
|
|
160
|
+
}
|
|
82
161
|
function getErrorMessage(error) {
|
|
83
162
|
if (error instanceof Error)
|
|
84
163
|
return error.message;
|
|
@@ -143,11 +222,11 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
143
222
|
const filePath = path_1.default.join(dir, `${filename}.mp4`);
|
|
144
223
|
try {
|
|
145
224
|
if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
|
|
146
|
-
|
|
225
|
+
return { filePath: '', code: ErrorCode.UNSUPPORTED_CONTENT_TYPE };
|
|
147
226
|
}
|
|
148
227
|
const fileSize = await getFileSize(url, userAgent);
|
|
149
228
|
if (maxSize > 0 && fileSize > maxSize) {
|
|
150
|
-
|
|
229
|
+
return { filePath: '', code: ErrorCode.VIDEO_SIZE_EXCEEDED };
|
|
151
230
|
}
|
|
152
231
|
if (threads <= 0 || fileSize === 0) {
|
|
153
232
|
const response = await (0, axios_1.default)({
|
|
@@ -161,7 +240,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
161
240
|
});
|
|
162
241
|
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
163
242
|
await (0, promises_1.pipeline)(response.data, writeStream);
|
|
164
|
-
return filePath;
|
|
243
|
+
return { filePath, code: ErrorCode.SUCCESS };
|
|
165
244
|
}
|
|
166
245
|
const totalSize = fileSize * 1024 * 1024;
|
|
167
246
|
const chunkSize = Math.ceil(totalSize / threads);
|
|
@@ -187,7 +266,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
187
266
|
fs_1.default.unlinkSync(result.filePath);
|
|
188
267
|
}
|
|
189
268
|
writeStream.end();
|
|
190
|
-
return filePath;
|
|
269
|
+
return { filePath, code: ErrorCode.SUCCESS };
|
|
191
270
|
}
|
|
192
271
|
catch (error) {
|
|
193
272
|
if (fs_1.default.existsSync(filePath)) {
|
|
@@ -200,46 +279,9 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
200
279
|
}
|
|
201
280
|
catch (e) { }
|
|
202
281
|
});
|
|
203
|
-
|
|
282
|
+
return { filePath: '', code: ErrorCode.VIDEO_DOWNLOAD_FAILED };
|
|
204
283
|
}
|
|
205
284
|
}
|
|
206
|
-
function parseXingzhigeData(resData, platform) {
|
|
207
|
-
const result = {
|
|
208
|
-
title: '',
|
|
209
|
-
author: '未知作者',
|
|
210
|
-
desc: '',
|
|
211
|
-
cover: '',
|
|
212
|
-
video: '',
|
|
213
|
-
images: [],
|
|
214
|
-
stat: {
|
|
215
|
-
like: 0,
|
|
216
|
-
coin: 0,
|
|
217
|
-
favorite: 0,
|
|
218
|
-
share: 0,
|
|
219
|
-
view: 0,
|
|
220
|
-
duration: '00:00'
|
|
221
|
-
},
|
|
222
|
-
type: 'video'
|
|
223
|
-
};
|
|
224
|
-
if (platform === 'bilibili') {
|
|
225
|
-
const d = resData.data || resData;
|
|
226
|
-
result.title = d.video?.title || d.title || '';
|
|
227
|
-
result.author = d.owner?.name || d.name || '未知UP主';
|
|
228
|
-
result.desc = d.video?.desc || d.desc || '';
|
|
229
|
-
result.cover = d.video?.fm || d.fm || '';
|
|
230
|
-
result.video = d.video?.url || d.url || '';
|
|
231
|
-
result.stat = {
|
|
232
|
-
view: d.stat?.view || 0,
|
|
233
|
-
favorite: d.stat?.favorite || 0,
|
|
234
|
-
like: d.stat?.like || 0,
|
|
235
|
-
coin: d.stat?.coin || 0,
|
|
236
|
-
share: d.stat?.share || 0,
|
|
237
|
-
duration: formatDuration(d.duration || 0)
|
|
238
|
-
};
|
|
239
|
-
result.duration = d.duration || 0;
|
|
240
|
-
}
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
285
|
function extractUrl(content) {
|
|
244
286
|
let urlMatches = content.match(/https?:\/\/[^\s\"\'\>]+/gi) || [];
|
|
245
287
|
return urlMatches.filter(url => {
|
|
@@ -288,8 +330,16 @@ async function resolveShortUrl(url) {
|
|
|
288
330
|
return url;
|
|
289
331
|
}
|
|
290
332
|
}
|
|
291
|
-
function formatDuration(
|
|
292
|
-
if (!
|
|
333
|
+
function formatDuration(input) {
|
|
334
|
+
if (!input || input === 0 || input === '0' || input === '00:00')
|
|
335
|
+
return '00:00';
|
|
336
|
+
if (typeof input === 'string') {
|
|
337
|
+
if (input.includes(':'))
|
|
338
|
+
return input;
|
|
339
|
+
input = Number(input);
|
|
340
|
+
}
|
|
341
|
+
const seconds = Math.floor(Number(input));
|
|
342
|
+
if (isNaN(seconds) || seconds <= 0)
|
|
293
343
|
return '00:00';
|
|
294
344
|
const hours = Math.floor(seconds / 3600);
|
|
295
345
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
@@ -298,39 +348,86 @@ function formatDuration(seconds) {
|
|
|
298
348
|
? `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
|
299
349
|
: `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
300
350
|
}
|
|
301
|
-
function
|
|
351
|
+
function getNestedValue(obj, path) {
|
|
352
|
+
if (!obj || typeof obj !== 'object')
|
|
353
|
+
return undefined;
|
|
354
|
+
const keys = path.split('.');
|
|
355
|
+
let value = obj;
|
|
356
|
+
for (const key of keys) {
|
|
357
|
+
if (value === null || value === undefined)
|
|
358
|
+
return undefined;
|
|
359
|
+
value = value[key];
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
function findValueInObject(obj, keys) {
|
|
364
|
+
if (!obj || typeof obj !== 'object')
|
|
365
|
+
return undefined;
|
|
366
|
+
for (const key of keys) {
|
|
367
|
+
if (key.includes('.')) {
|
|
368
|
+
const value = getNestedValue(obj, key);
|
|
369
|
+
if (value !== undefined && value !== null && value !== '')
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
if (obj[key] !== undefined && obj[key] !== null && obj[key] !== '')
|
|
374
|
+
return obj[key];
|
|
375
|
+
const lowerKey = key.toLowerCase();
|
|
376
|
+
for (const objKey of Object.keys(obj)) {
|
|
377
|
+
if (objKey.toLowerCase() === lowerKey) {
|
|
378
|
+
return obj[objKey];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
function parseData(rawResponse, maxDescLength, platform) {
|
|
386
|
+
let data = rawResponse;
|
|
387
|
+
if (data.data) {
|
|
388
|
+
data = data.data;
|
|
389
|
+
}
|
|
390
|
+
const stat = {};
|
|
391
|
+
Object.entries(VARIABLE_MAPPING).forEach(([varName, keys]) => {
|
|
392
|
+
const value = findValueInObject(data, keys);
|
|
393
|
+
if (value !== undefined) {
|
|
394
|
+
stat[varName] = value;
|
|
395
|
+
}
|
|
396
|
+
});
|
|
302
397
|
let type = data.type || 'video';
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
let images = data
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
398
|
+
const title = findValueInObject(data, ['title']) || '无标题';
|
|
399
|
+
const author = findValueInObject(data, ['author.name', 'author']) || '未知作者';
|
|
400
|
+
const desc = (findValueInObject(data, ['desc', 'description', 'content']) || title).toString().slice(0, maxDescLength);
|
|
401
|
+
const cover = findValueInObject(data, ['cover', 'imgurl', 'pic', 'thumbnail']) || '';
|
|
402
|
+
let images = findValueInObject(data, ['imgurl', 'images', 'pics']) || [];
|
|
403
|
+
if (!Array.isArray(images))
|
|
404
|
+
images = [images];
|
|
405
|
+
const videoUrls = [
|
|
406
|
+
findValueInObject(data, ['url']),
|
|
407
|
+
findValueInObject(data, ['download_url']),
|
|
408
|
+
findValueInObject(data, ['video_backup']),
|
|
409
|
+
findValueInObject(data, ['playUrl']),
|
|
410
|
+
findValueInObject(data, ['video_url'])
|
|
411
|
+
];
|
|
412
|
+
let video = '';
|
|
413
|
+
if (Array.isArray(videoUrls[2])) {
|
|
414
|
+
video = videoUrls[2][0]?.url || '';
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
for (const url of videoUrls) {
|
|
418
|
+
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
419
|
+
video = url;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
327
423
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
424
|
+
const durationValue = findValueInObject(data, ['duration']);
|
|
425
|
+
const duration = typeof durationValue === 'number' ? durationValue : parseInt(durationValue) || 0;
|
|
426
|
+
const durationFormatted = formatDuration(durationValue || 0);
|
|
427
|
+
const live_photo = data.live_photo || [];
|
|
332
428
|
return {
|
|
333
|
-
type,
|
|
429
|
+
type: type,
|
|
430
|
+
rawData: rawResponse,
|
|
334
431
|
title,
|
|
335
432
|
author,
|
|
336
433
|
desc,
|
|
@@ -339,14 +436,8 @@ function parseData(data, maxDescLength, platform) {
|
|
|
339
436
|
video,
|
|
340
437
|
duration,
|
|
341
438
|
durationFormatted,
|
|
342
|
-
stat
|
|
343
|
-
|
|
344
|
-
coin: stat.coin || 0,
|
|
345
|
-
favorite: stat.favorite || 0,
|
|
346
|
-
share: stat.share || 0,
|
|
347
|
-
view: stat.view || 0,
|
|
348
|
-
duration: durationFormatted
|
|
349
|
-
}
|
|
439
|
+
stat,
|
|
440
|
+
live_photo
|
|
350
441
|
};
|
|
351
442
|
}
|
|
352
443
|
function generateFormattedText(platform, parseData, config) {
|
|
@@ -354,28 +445,31 @@ function generateFormattedText(platform, parseData, config) {
|
|
|
354
445
|
if (platform !== 'bilibili') {
|
|
355
446
|
format = format.replace(/投币:\$\{投币数\}\n?/g, '');
|
|
356
447
|
}
|
|
357
|
-
const variables = {
|
|
358
|
-
'标题': parseData.title || '',
|
|
359
|
-
'作者': parseData.author || '',
|
|
360
|
-
'简介': parseData.desc || '',
|
|
361
|
-
'视频时长': parseData.stat.duration || '',
|
|
362
|
-
'点赞数': parseData.stat.like > 0 ? parseData.stat.like : '',
|
|
363
|
-
'投币数': parseData.stat.coin > 0 ? parseData.stat.coin : '',
|
|
364
|
-
'收藏数': parseData.stat.favorite > 0 ? parseData.stat.favorite : '',
|
|
365
|
-
'转发数': parseData.stat.share > 0 ? parseData.stat.share : '',
|
|
366
|
-
'播放数': parseData.stat.view > 0 ? parseData.stat.view : ''
|
|
367
|
-
};
|
|
368
448
|
let result = format;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
449
|
+
const formatLines = result.split('\n');
|
|
450
|
+
const validLines = [];
|
|
451
|
+
formatLines.forEach((line) => {
|
|
452
|
+
let isValid = true;
|
|
453
|
+
let processedLine = line;
|
|
454
|
+
const varMatches = line.match(/\$\{([^}]+)\}/g) || [];
|
|
455
|
+
varMatches.forEach((varMatch) => {
|
|
456
|
+
const varName = varMatch.replace(/\$\{|\}/g, '');
|
|
457
|
+
const value = parseData.stat[varName];
|
|
458
|
+
if (value === undefined || value === null || value === '' || value === 0 || value === '00:00') {
|
|
459
|
+
isValid = false;
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
processedLine = processedLine.replace(varMatch, String(value));
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
if (isValid && processedLine.trim() !== '') {
|
|
466
|
+
validLines.push(processedLine);
|
|
376
467
|
}
|
|
377
468
|
});
|
|
378
|
-
result =
|
|
469
|
+
result = validLines.join('\n').trim();
|
|
470
|
+
if (!result) {
|
|
471
|
+
result = `标题:${parseData.title}\n作者:${parseData.author}\n简介:${parseData.desc}`;
|
|
472
|
+
}
|
|
379
473
|
return result;
|
|
380
474
|
}
|
|
381
475
|
function clearAllCache() {
|
|
@@ -422,62 +516,106 @@ function apply(ctx, config) {
|
|
|
422
516
|
const realUrl = await resolveShortUrl(url);
|
|
423
517
|
const platform = getPlatformType(realUrl);
|
|
424
518
|
if (!platform) {
|
|
425
|
-
|
|
426
|
-
|
|
519
|
+
const code = ErrorCode.UNSUPPORTED_PLATFORM;
|
|
520
|
+
const msg = getErrorInfo(code, url);
|
|
521
|
+
logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${url}`);
|
|
522
|
+
return { data: null, code, msg };
|
|
427
523
|
}
|
|
428
524
|
const apiUrl = API_CONFIG[platform];
|
|
429
525
|
if (!apiUrl) {
|
|
430
|
-
|
|
431
|
-
|
|
526
|
+
const code = ErrorCode.PLATFORM_API_NOT_CONFIGURED;
|
|
527
|
+
const msg = getErrorInfo(code, platform);
|
|
528
|
+
logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${platform}`);
|
|
529
|
+
return { data: null, code, msg };
|
|
432
530
|
}
|
|
433
531
|
try {
|
|
434
|
-
const res = await http.get(apiUrl, {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
532
|
+
const res = await http.get(apiUrl, {
|
|
533
|
+
params: { url: realUrl },
|
|
534
|
+
timeout: config.timeout
|
|
535
|
+
});
|
|
536
|
+
if (res.data.code !== 200 && res.data.code !== 0) {
|
|
537
|
+
const apiErrorMsg = res.data.msg || '解析失败';
|
|
538
|
+
const platformCode = PLATFORM_ERROR_CODE_MAP[platform] || ErrorCode.API_RETURN_ERROR;
|
|
539
|
+
const code = platformCode;
|
|
540
|
+
const msg = getErrorInfo(code, apiErrorMsg);
|
|
541
|
+
logger.error(`[${code}] API返回错误: ${platform}, URL: ${url}, 错误: ${apiErrorMsg}`);
|
|
542
|
+
return { data: null, code, msg };
|
|
439
543
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
544
|
+
try {
|
|
545
|
+
const parseResult = parseData(res.data, config.maxDescLength, platform);
|
|
546
|
+
if (!parseResult.video && !parseResult.images.length && !parseResult.live_photo?.length) {
|
|
547
|
+
const code = ErrorCode.NO_VIDEO_FOUND;
|
|
548
|
+
const msg = getErrorInfo(code, '未找到视频或图片内容');
|
|
549
|
+
logger.warn(`[${code}] 解析成功但无有效内容: ${platform}, URL: ${url}`);
|
|
550
|
+
return { data: null, code, msg };
|
|
443
551
|
}
|
|
552
|
+
logger.info(`[${ErrorCode.SUCCESS}] ${platform}解析成功: ${url}`);
|
|
553
|
+
return {
|
|
554
|
+
data: parseResult,
|
|
555
|
+
code: ErrorCode.SUCCESS,
|
|
556
|
+
msg: getErrorInfo(ErrorCode.SUCCESS)
|
|
557
|
+
};
|
|
444
558
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
logger.error(
|
|
450
|
-
return { data: null, msg
|
|
559
|
+
catch (parseError) {
|
|
560
|
+
const errorMsg = getErrorMessage(parseError);
|
|
561
|
+
const code = ErrorCode.API_DATA_PARSE_FAILED;
|
|
562
|
+
const msg = getErrorInfo(code, errorMsg);
|
|
563
|
+
logger.error(`[${code}] 解析数据失败: ${platform}, URL: ${url}, 错误: ${errorMsg}`);
|
|
564
|
+
return { data: null, code, msg };
|
|
451
565
|
}
|
|
452
566
|
}
|
|
453
567
|
catch (error) {
|
|
454
|
-
|
|
455
|
-
|
|
568
|
+
const errorMsg = getErrorMessage(error);
|
|
569
|
+
let code = ErrorCode.UNKNOWN_ERROR;
|
|
570
|
+
if (errorMsg.includes('timeout')) {
|
|
571
|
+
code = ErrorCode.REQUEST_TIMEOUT;
|
|
572
|
+
}
|
|
573
|
+
else if (errorMsg.includes('Network') || errorMsg.includes('network')) {
|
|
574
|
+
code = ErrorCode.NETWORK_ERROR;
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
code = ErrorCode.NETWORK_ERROR;
|
|
578
|
+
}
|
|
579
|
+
const msg = getErrorInfo(code, errorMsg);
|
|
580
|
+
logger.error(`[${code}] 解析请求失败: ${platform}, URL: ${url}, 错误: ${errorMsg}`);
|
|
581
|
+
return { data: null, code, msg };
|
|
456
582
|
}
|
|
457
583
|
}
|
|
458
584
|
async function processSingleUrl(session, url) {
|
|
459
585
|
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
460
586
|
const now = Date.now();
|
|
461
587
|
if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000) {
|
|
462
|
-
|
|
588
|
+
const code = ErrorCode.DUPLICATE_PARSE;
|
|
589
|
+
const msg = getErrorInfo(code);
|
|
590
|
+
return { data: null, code, msg };
|
|
463
591
|
}
|
|
464
592
|
processed.set(hash, now);
|
|
465
593
|
const result = await parse(url);
|
|
466
594
|
if (!result.data)
|
|
467
|
-
return { data: null, msg: result.msg };
|
|
595
|
+
return { data: null, code: result.code, msg: result.msg };
|
|
468
596
|
const parseData = result.data;
|
|
469
597
|
const platform = getPlatformType(url);
|
|
470
598
|
const text = generateFormattedText(platform, parseData, config);
|
|
471
599
|
return {
|
|
472
|
-
data: {
|
|
473
|
-
|
|
600
|
+
data: {
|
|
601
|
+
text,
|
|
602
|
+
cover: parseData.cover,
|
|
603
|
+
images: parseData.images,
|
|
604
|
+
video: parseData.video,
|
|
605
|
+
type: parseData.type,
|
|
606
|
+
live_photo: parseData.live_photo
|
|
607
|
+
},
|
|
608
|
+
code: ErrorCode.SUCCESS,
|
|
609
|
+
msg: getErrorInfo(ErrorCode.SUCCESS)
|
|
474
610
|
};
|
|
475
611
|
}
|
|
476
612
|
async function sendTimeout(session, content) {
|
|
477
613
|
if (config.videoSendTimeout <= 0) {
|
|
478
614
|
return session.send(content).catch((err) => {
|
|
615
|
+
const errorMsg = getErrorMessage(err);
|
|
616
|
+
logger.error(`[${ErrorCode.MESSAGE_SEND_FAILED}] 发送消息失败: ${errorMsg}`);
|
|
479
617
|
if (!config.ignoreSendError)
|
|
480
|
-
|
|
618
|
+
return null;
|
|
481
619
|
return null;
|
|
482
620
|
});
|
|
483
621
|
}
|
|
@@ -485,8 +623,11 @@ function apply(ctx, config) {
|
|
|
485
623
|
session.send(content),
|
|
486
624
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.videoSendTimeout))
|
|
487
625
|
]).catch((err) => {
|
|
626
|
+
const errorMsg = getErrorMessage(err);
|
|
627
|
+
const code = errorMsg.includes('timeout') ? ErrorCode.MESSAGE_SEND_TIMEOUT : ErrorCode.MESSAGE_SEND_FAILED;
|
|
628
|
+
logger.error(`[${code}] 发送消息失败: ${errorMsg}`);
|
|
488
629
|
if (!config.ignoreSendError)
|
|
489
|
-
|
|
630
|
+
return null;
|
|
490
631
|
return null;
|
|
491
632
|
});
|
|
492
633
|
}
|
|
@@ -499,43 +640,32 @@ function apply(ctx, config) {
|
|
|
499
640
|
linkBuffer.delete(key);
|
|
500
641
|
}
|
|
501
642
|
const items = [];
|
|
502
|
-
const
|
|
643
|
+
const errors = [];
|
|
503
644
|
for (const url of urls) {
|
|
504
645
|
const result = await processSingleUrl(session, url);
|
|
505
646
|
if (result.data) {
|
|
506
647
|
items.push(result.data);
|
|
507
648
|
}
|
|
508
649
|
else {
|
|
509
|
-
|
|
510
|
-
logger.error(
|
|
650
|
+
errors.push({ url, code: result.code, msg: result.msg });
|
|
651
|
+
logger.error(`[${result.code}] 解析失败: ${url}, 原因: ${result.msg}`);
|
|
511
652
|
}
|
|
512
653
|
}
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (errs.length) {
|
|
520
|
-
const errorMsg = `⚠ 部分解析失败\n${errs.join('\n')}`;
|
|
521
|
-
if (enableForward) {
|
|
522
|
-
forwardMessages.push(buildForwardNode(session, errorMsg, botName));
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
await sendTimeout(session, errorMsg);
|
|
526
|
-
await delay(600);
|
|
527
|
-
}
|
|
654
|
+
if (errors.length > 0) {
|
|
655
|
+
const errorLines = errors.map(err => `【${err.url}】: ${err.msg}`);
|
|
656
|
+
const errorMsg = `❌ 解析失败列表(共${errors.length}个链接):\n${errorLines.join('\n')}`;
|
|
657
|
+
logger.error(`解析失败数量: ${errors.length}, 错误码列表: ${errors.map(e => e.code).join(', ')}`);
|
|
658
|
+
await sendTimeout(session, errorMsg);
|
|
659
|
+
await delay(500);
|
|
528
660
|
}
|
|
529
661
|
if (items.length === 0) {
|
|
530
|
-
const failMsg =
|
|
531
|
-
|
|
532
|
-
forwardMessages.push(buildForwardNode(session, failMsg, botName));
|
|
533
|
-
}
|
|
534
|
-
else {
|
|
535
|
-
await sendTimeout(session, failMsg);
|
|
536
|
-
}
|
|
662
|
+
const failMsg = getErrorInfo(ErrorCode.UNKNOWN_ERROR, '所有链接均解析失败,请检查链接是否有效或稍后重试');
|
|
663
|
+
await sendTimeout(session, `⚠ ${failMsg}`);
|
|
537
664
|
return;
|
|
538
665
|
}
|
|
666
|
+
const enableForward = config.enableForward && session.platform === 'onebot';
|
|
667
|
+
const forwardMessages = [];
|
|
668
|
+
const botName = config.botName || '视频解析机器人';
|
|
539
669
|
for (const item of items) {
|
|
540
670
|
try {
|
|
541
671
|
if (enableForward) {
|
|
@@ -548,28 +678,60 @@ function apply(ctx, config) {
|
|
|
548
678
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.images[i]), botName));
|
|
549
679
|
}
|
|
550
680
|
}
|
|
681
|
+
if (item.type === 'live' || item.type === 'live_photo') {
|
|
682
|
+
if (item.live_photo?.length) {
|
|
683
|
+
for (let i = 0; i < item.live_photo.length && forwardMessages.length < 100; i++) {
|
|
684
|
+
const liveItem = item.live_photo[i];
|
|
685
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(liveItem.image), botName));
|
|
686
|
+
if (liveItem.video) {
|
|
687
|
+
try {
|
|
688
|
+
const videoElem = koishi_1.h.video(liveItem.video);
|
|
689
|
+
forwardMessages.push(buildForwardNode(session, videoElem, botName));
|
|
690
|
+
}
|
|
691
|
+
catch (e) {
|
|
692
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`视频链接: ${liveItem.video}`), botName));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
else if (item.images?.length) {
|
|
698
|
+
for (let i = 0; i < item.images.length && forwardMessages.length < 100; i++) {
|
|
699
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.images[i]), botName));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
551
703
|
if (item.video && config.showVideoFile && forwardMessages.length < 100) {
|
|
552
704
|
let videoElem;
|
|
553
705
|
try {
|
|
554
706
|
if (config.downloadVideoBeforeSend) {
|
|
555
707
|
const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
556
|
-
const
|
|
557
|
-
|
|
708
|
+
const downloadResult = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
709
|
+
if (downloadResult.code !== ErrorCode.SUCCESS) {
|
|
710
|
+
const errorMsg = getErrorInfo(downloadResult.code);
|
|
711
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`${errorMsg}\n链接:${item.video}`), botName));
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
videoElem = koishi_1.h.file(downloadResult.filePath);
|
|
715
|
+
forwardMessages.push(buildForwardNode(session, videoElem, botName));
|
|
716
|
+
}
|
|
558
717
|
}
|
|
559
718
|
else {
|
|
560
719
|
const fileSize = await getFileSize(item.video, config.userAgent);
|
|
561
720
|
if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
|
|
562
|
-
|
|
721
|
+
const code = ErrorCode.VIDEO_SIZE_EXCEEDED;
|
|
722
|
+
const errorMsg = getErrorInfo(code, `${fileSize}MB超过限制${config.maxVideoSize}MB`);
|
|
723
|
+
videoElem = koishi_1.h.text(`${errorMsg},仅发送链接:${item.video}`);
|
|
563
724
|
}
|
|
564
725
|
else {
|
|
565
726
|
videoElem = koishi_1.h.video(item.video);
|
|
566
727
|
}
|
|
567
728
|
}
|
|
568
|
-
forwardMessages.push(buildForwardNode(session, videoElem, botName));
|
|
569
729
|
}
|
|
570
730
|
catch (error) {
|
|
571
|
-
|
|
572
|
-
|
|
731
|
+
const errorMsg = getErrorMessage(error);
|
|
732
|
+
const code = ErrorCode.VIDEO_DOWNLOAD_FAILED;
|
|
733
|
+
logger.error(`[${code}] 视频处理失败: ${errorMsg}`);
|
|
734
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.text(getErrorInfo(code, errorMsg) + `\n链接:${item.video}`), botName));
|
|
573
735
|
}
|
|
574
736
|
}
|
|
575
737
|
}
|
|
@@ -578,7 +740,28 @@ function apply(ctx, config) {
|
|
|
578
740
|
await sendTimeout(session, item.text);
|
|
579
741
|
await delay(300);
|
|
580
742
|
}
|
|
581
|
-
if (item.type === '
|
|
743
|
+
if (item.type === 'live' || item.type === 'live_photo') {
|
|
744
|
+
if (item.live_photo?.length) {
|
|
745
|
+
for (const liveItem of item.live_photo) {
|
|
746
|
+
await sendTimeout(session, koishi_1.h.image(liveItem.image));
|
|
747
|
+
await delay(200);
|
|
748
|
+
if (liveItem.video) {
|
|
749
|
+
try {
|
|
750
|
+
await sendTimeout(session, koishi_1.h.video(liveItem.video));
|
|
751
|
+
}
|
|
752
|
+
catch (e) {
|
|
753
|
+
await sendTimeout(session, koishi_1.h.text(`Live Photo 视频链接: ${liveItem.video}`));
|
|
754
|
+
}
|
|
755
|
+
await delay(300);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
else if (item.images?.length) {
|
|
760
|
+
const imgMsg = (0, koishi_1.h)('message', ...item.images.map((url) => koishi_1.h.image(url)));
|
|
761
|
+
await sendTimeout(session, imgMsg);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
else if (item.type === 'image' && item.images?.length) {
|
|
582
765
|
const imgMsg = (0, koishi_1.h)('message', ...item.images.map((url) => koishi_1.h.image(url)));
|
|
583
766
|
await sendTimeout(session, imgMsg);
|
|
584
767
|
}
|
|
@@ -592,13 +775,22 @@ function apply(ctx, config) {
|
|
|
592
775
|
let videoElem;
|
|
593
776
|
if (config.downloadVideoBeforeSend) {
|
|
594
777
|
const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
595
|
-
const
|
|
596
|
-
|
|
778
|
+
const downloadResult = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
779
|
+
if (downloadResult.code !== ErrorCode.SUCCESS) {
|
|
780
|
+
const errorMsg = getErrorInfo(downloadResult.code);
|
|
781
|
+
await sendTimeout(session, koishi_1.h.text(`${errorMsg}\n链接:${item.video}`));
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
videoElem = koishi_1.h.file(downloadResult.filePath);
|
|
786
|
+
}
|
|
597
787
|
}
|
|
598
788
|
else {
|
|
599
789
|
const fileSize = await getFileSize(item.video, config.userAgent);
|
|
600
790
|
if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
|
|
601
|
-
|
|
791
|
+
const code = ErrorCode.VIDEO_SIZE_EXCEEDED;
|
|
792
|
+
const errorMsg = getErrorInfo(code, `${fileSize}MB超过限制${config.maxVideoSize}MB`);
|
|
793
|
+
videoElem = koishi_1.h.text(`${errorMsg},仅发送链接:${item.video}`);
|
|
602
794
|
}
|
|
603
795
|
else {
|
|
604
796
|
videoElem = koishi_1.h.video(item.video);
|
|
@@ -607,8 +799,10 @@ function apply(ctx, config) {
|
|
|
607
799
|
await sendTimeout(session, videoElem);
|
|
608
800
|
}
|
|
609
801
|
catch (error) {
|
|
610
|
-
|
|
611
|
-
|
|
802
|
+
const errorMsg = getErrorMessage(error);
|
|
803
|
+
const code = ErrorCode.VIDEO_DOWNLOAD_FAILED;
|
|
804
|
+
logger.error(`[${code}] 视频处理失败: ${errorMsg}`);
|
|
805
|
+
await sendTimeout(session, koishi_1.h.text(getErrorInfo(code, errorMsg) + `\n链接:${item.video}`));
|
|
612
806
|
}
|
|
613
807
|
}
|
|
614
808
|
}
|
|
@@ -616,8 +810,10 @@ function apply(ctx, config) {
|
|
|
616
810
|
}
|
|
617
811
|
}
|
|
618
812
|
catch (error) {
|
|
619
|
-
|
|
620
|
-
|
|
813
|
+
const errorMsg = getErrorMessage(error);
|
|
814
|
+
const code = ErrorCode.UNKNOWN_ERROR;
|
|
815
|
+
logger.error(`[${code}] 处理内容失败: ${errorMsg}`);
|
|
816
|
+
await sendTimeout(session, koishi_1.h.text(getErrorInfo(code, `处理${item.type}内容失败: ${errorMsg}`)));
|
|
621
817
|
}
|
|
622
818
|
}
|
|
623
819
|
if (enableForward && forwardMessages.length) {
|
|
@@ -627,7 +823,9 @@ function apply(ctx, config) {
|
|
|
627
823
|
await sendTimeout(session, forwardMsg);
|
|
628
824
|
}
|
|
629
825
|
catch (error) {
|
|
630
|
-
|
|
826
|
+
const errorMsg = getErrorMessage(error);
|
|
827
|
+
const code = ErrorCode.FORWARD_MESSAGE_FAILED;
|
|
828
|
+
logger.error(`[${code}] 合并转发失败: ${errorMsg}`);
|
|
631
829
|
for (const node of forwardMessages) {
|
|
632
830
|
await sendTimeout(session, node.data.content);
|
|
633
831
|
await delay(500);
|
|
@@ -670,21 +868,25 @@ function apply(ctx, config) {
|
|
|
670
868
|
});
|
|
671
869
|
ctx.command('parse <url>', '手动解析视频链接')
|
|
672
870
|
.action(async ({ session }, url) => {
|
|
673
|
-
if (!url)
|
|
674
|
-
|
|
871
|
+
if (!url) {
|
|
872
|
+
const code = ErrorCode.INVALID_URL;
|
|
873
|
+
return getErrorInfo(code, '请输入视频链接');
|
|
874
|
+
}
|
|
675
875
|
let urls = extractUrl(url);
|
|
676
876
|
if (urls.length === 0 && hasPlatformKeyword(url)) {
|
|
677
877
|
const allLinks = url.match(/https?:\/\/[^\s\"\'\>\]]+/gi) || [];
|
|
678
878
|
urls = allLinks.filter((u) => getPlatformType(u));
|
|
679
879
|
}
|
|
680
|
-
if (urls.length === 0)
|
|
681
|
-
|
|
880
|
+
if (urls.length === 0) {
|
|
881
|
+
const code = ErrorCode.UNSUPPORTED_PLATFORM;
|
|
882
|
+
return getErrorInfo(code, '不支持该链接');
|
|
883
|
+
}
|
|
682
884
|
await flush(session, urls);
|
|
683
885
|
});
|
|
684
886
|
ctx.command('clear-cache', '清空解析缓存与临时文件')
|
|
685
887
|
.action(() => {
|
|
686
888
|
clearAllCache();
|
|
687
|
-
return '
|
|
889
|
+
return getErrorInfo(ErrorCode.SUCCESS, '解析缓存已清空');
|
|
688
890
|
});
|
|
689
891
|
setInterval(() => {
|
|
690
892
|
const now = Date.now();
|
|
@@ -703,18 +905,24 @@ function apply(ctx, config) {
|
|
|
703
905
|
const stat = fs_1.default.statSync(path_1.default.join(tempDir, file));
|
|
704
906
|
if (now - stat.mtimeMs > 3600000) {
|
|
705
907
|
fs_1.default.unlinkSync(path_1.default.join(tempDir, file));
|
|
908
|
+
logger.info(`清理过期临时文件: ${file}`);
|
|
706
909
|
}
|
|
707
910
|
}
|
|
708
911
|
catch (error) {
|
|
709
|
-
|
|
912
|
+
const errorMsg = getErrorMessage(error);
|
|
913
|
+
logger.error(`[${ErrorCode.UNKNOWN_ERROR}] 清理临时文件失败: ${file}, ${errorMsg}`);
|
|
710
914
|
}
|
|
711
915
|
});
|
|
712
916
|
}, 1800000);
|
|
713
917
|
if (config.autoClearCacheInterval > 0) {
|
|
714
918
|
setInterval(() => {
|
|
715
919
|
clearAllCache();
|
|
920
|
+
logger.info(getErrorInfo(ErrorCode.SUCCESS, '自动清理缓存完成'));
|
|
716
921
|
}, config.autoClearCacheInterval * 60000);
|
|
717
922
|
}
|
|
718
|
-
process.on('exit',
|
|
719
|
-
|
|
923
|
+
process.on('exit', () => {
|
|
924
|
+
clearAllCache();
|
|
925
|
+
logger.info(getErrorInfo(ErrorCode.SUCCESS, '进程退出,已清理缓存'));
|
|
926
|
+
});
|
|
927
|
+
logger.info(getErrorInfo(ErrorCode.SUCCESS, '视频解析插件已加载'));
|
|
720
928
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-video-parser-all",
|
|
3
|
-
"description": "Koishi 全平台视频解析插件,支持抖音/快手/B
|
|
4
|
-
"version": "0.5.
|
|
3
|
+
"description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/小红书/微博/今日头条/皮皮搞笑/皮皮虾/最右视频链接解析",
|
|
4
|
+
"version": "0.5.3",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
package/readme.md
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
- 🎨 自定义解析结果格式、返回内容类型(封面/链接/视频)
|
|
9
9
|
- ⚡ 内置防重复解析、接口重试、自动缓存清理等实用功能
|
|
10
10
|
- 📤 支持 OneBot 平台消息合并转发,优化展示体验
|
|
11
|
-
- 🔌 内置多套解析 API,自动降级容错,提升解析成功率
|
|
12
11
|
|
|
13
12
|
### English
|
|
14
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:
|
|
@@ -16,7 +15,6 @@ This is a **video parsing plugin** developed for the Koishi bot framework, suppo
|
|
|
16
15
|
- 🎨 Customize the parsing result format and return content type (cover/link/video)
|
|
17
16
|
- ⚡ Built-in duplicate prevention, retry logic, auto cache cleanup
|
|
18
17
|
- 📤 Support OneBot message forwarding for better display experience
|
|
19
|
-
- 🔌 Multiple built-in parsing APIs with automatic failover
|
|
20
18
|
|
|
21
19
|
## 项目仓库 (Repository)
|
|
22
20
|
- GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
|
|
@@ -76,8 +74,6 @@ This is a **video parsing plugin** developed for the Koishi bot framework, suppo
|
|
|
76
74
|
|----------------------|-------------------------|
|
|
77
75
|
| Minecraft-1314 | 插件完整开发 (Complete plugin development) |
|
|
78
76
|
| JH-Ahua | BugPk-Api 支持 |
|
|
79
|
-
| 素颜API | 素颜API 支持 |
|
|
80
|
-
| 星之阁API | 星之阁API 支持 |
|
|
81
77
|
| (欢迎提交 PR 加入贡献者列表) | (Welcome to submit PR to join the contributor list) |
|
|
82
78
|
|
|
83
79
|
## 许可协议 (License)
|