koishi-plugin-video-parser-all 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +4 -0
- package/lib/index.js +186 -493
- package/package.json +19 -3
- package/readme.md +47 -24
package/lib/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare const Config: Schema<{
|
|
|
6
6
|
showWaitingTip?: boolean | null | undefined;
|
|
7
7
|
waitingTipText?: string | null | undefined;
|
|
8
8
|
sameLinkInterval?: number | null | undefined;
|
|
9
|
+
debug?: boolean | null | undefined;
|
|
10
|
+
debugFile?: boolean | null | undefined;
|
|
9
11
|
} & import("cosmokit").Dict & {
|
|
10
12
|
unifiedMessageFormat?: string | null | undefined;
|
|
11
13
|
} & {
|
|
@@ -36,6 +38,8 @@ export declare const Config: Schema<{
|
|
|
36
38
|
showWaitingTip: boolean;
|
|
37
39
|
waitingTipText: string;
|
|
38
40
|
sameLinkInterval: number;
|
|
41
|
+
debug: boolean;
|
|
42
|
+
debugFile: boolean;
|
|
39
43
|
} & import("cosmokit").Dict & {
|
|
40
44
|
unifiedMessageFormat: string;
|
|
41
45
|
} & {
|
package/lib/index.js
CHANGED
|
@@ -20,18 +20,14 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
20
20
|
showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
|
|
21
21
|
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('等待提示文本内容'),
|
|
22
22
|
sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
|
|
23
|
+
debug: koishi_1.Schema.boolean().default(false).description('开启调试模式'),
|
|
24
|
+
debugFile: koishi_1.Schema.boolean().default(false).description('调试日志写入文件'),
|
|
23
25
|
}).description('基础设置'),
|
|
24
26
|
koishi_1.Schema.object({
|
|
25
27
|
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(`标题:${'${标题}'}
|
|
26
28
|
作者:${'${作者}'}
|
|
27
|
-
简介:${'${简介}'}
|
|
28
|
-
时长:${'${视频时长}'}
|
|
29
29
|
点赞:${'${点赞数}'}
|
|
30
|
-
|
|
31
|
-
收藏:${'${收藏数}'}
|
|
32
|
-
转发:${'${转发数}'}
|
|
33
|
-
播放:${'${播放数}'}
|
|
34
|
-
评论:${'${评论数}'}`).description('统一消息格式'),
|
|
30
|
+
链接:${'${视频链接}'}`).description('统一消息格式'),
|
|
35
31
|
}).description('统一消息格式'),
|
|
36
32
|
koishi_1.Schema.object({
|
|
37
33
|
showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
|
|
@@ -66,49 +62,56 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
66
62
|
const processed = new Map();
|
|
67
63
|
const linkBuffer = new Map();
|
|
68
64
|
const logger = new koishi_1.Logger(exports.name);
|
|
65
|
+
let debugEnabled = false;
|
|
66
|
+
let debugFileEnabled = false;
|
|
67
|
+
let debugStream = null;
|
|
68
|
+
function debugLog(level, ...args) {
|
|
69
|
+
if (!debugEnabled)
|
|
70
|
+
return;
|
|
71
|
+
const timestamp = new Date().toISOString();
|
|
72
|
+
const message = `[${timestamp}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' ')}`;
|
|
73
|
+
logger.info(message);
|
|
74
|
+
if (debugFileEnabled && debugStream) {
|
|
75
|
+
debugStream.write(message + '\n');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function initDebug(enabled, fileEnabled) {
|
|
79
|
+
debugEnabled = enabled;
|
|
80
|
+
debugFileEnabled = fileEnabled;
|
|
81
|
+
if (fileEnabled && enabled) {
|
|
82
|
+
const logPath = path_1.default.join(process.cwd(), 'debug.log');
|
|
83
|
+
debugStream = fs_1.default.createWriteStream(logPath, { flags: 'a' });
|
|
84
|
+
debugStream.write(`\n=== Debug session started at ${new Date().toISOString()} ===\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
69
87
|
const PLATFORM_KEYWORDS = {
|
|
70
|
-
bilibili: ['bilibili', 'b23', '
|
|
71
|
-
kuaishou: ['kuaishou', '
|
|
72
|
-
weibo: ['weibo', '
|
|
73
|
-
toutiao: ['toutiao', '
|
|
74
|
-
pipigx: ['pipigx', '
|
|
75
|
-
pipixia: ['pipixia', '
|
|
76
|
-
douyin: ['douyin', '
|
|
77
|
-
zuiyou: ['zuiyou', '
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
'投币数': ['coin', 'bi', 'stat.coin', 'stast.coin', 'data.coin', 'data.coin_num', 'data.coin_count'],
|
|
96
|
-
'收藏数': ['collect', 'favorite', 'star', 'stat.collect', 'collected_count', 'stast.favorite', 'data.favorite', 'data.collect_count', 'data.collection_count', 'data.star_count', 'data.bookmark_count', 'data.fav_count', 'data.save_count'],
|
|
97
|
-
'转发数': ['share', 'forward', 'repost', 'stat.share', 'reposts_count', 'shared_count', 'stast.share', 'data.reposts_count', 'data.item.reposts_count', 'data.share_count', 'data.forward_count', 'data.repost_count'],
|
|
98
|
-
'播放数': ['view', 'play_count', 'play', 'stat.view', 'play_times', 'stast.view', 'data.play_count', 'item.play_count', 'data.item.play_count', 'data.view_count', 'data.views', 'data.play_num', 'data.watch_count', 'data.video_play_count'],
|
|
99
|
-
'评论数': ['comment', 'comments_count', 'comment_count', 'discuss', 'stat.comment', 'stast.reply', 'data.comments_count', 'item.comments_count', 'data.item.comments_count', 'stat.reply', 'data.comment_num', 'data.reply_count', 'data.review_count'],
|
|
100
|
-
'IP属地': ['ip', 'ip_info', 'ip_location', 'ip_info_str', 'data.ip_info_str', 'item.ip', 'item.ip_info', 'data.item.ip_info_str', 'data.user_ip', 'data.location', 'data.region', 'data.area', 'data.addr'],
|
|
101
|
-
'发布时间': ['date', 'time', 'publish_time', 'create_time', 'ctime', 'pubdate', 'data.date', 'item.publish_time', 'live.time', 'stast.publish_time', 'stat.time', 'data.time.publish_time', 'data.live.time', 'stat.ctime', 'data.upload_time', 'data.post_time', 'data.create_date', 'data.pub_time', 'data.timestamp'],
|
|
102
|
-
'粉丝数': ['fans', 'fans_count', 'follower', 'followers', 'follower_count', 'followers_count', 'data.followers_count', 'item.followers', 'author.fans', 'data.item.followers_count', 'data.user_fans', 'data.fan_num'],
|
|
103
|
-
'在线人数': ['online', 'online_count', 'data.online', 'live.online', 'room.online', 'data.live.online', 'data.watching', 'data.viewer_count', 'data.audience_count'],
|
|
104
|
-
'关注数': ['follow', 'follow_count', 'attention', 'data.attention', 'live.attention', 'stast.attention', 'data.live.attention', 'data.following_count', 'data.follow_num'],
|
|
105
|
-
'文件大小': ['size', 'size_str', 'item.size', 'item.size_str', 'data.size', 'data.item.size_str', 'data.file_size', 'data.video_size', 'data.vid_size'],
|
|
106
|
-
'直播间地址': ['room_url', 'live.room_url', 'data.room_url', 'live.url', 'data.live.room_url', 'data.room_link', 'data.live_url', 'data.stream_url'],
|
|
107
|
-
'直播间ID': ['room_id', 'live.room_id', 'data.room_id', 'live.room_id', 'data.live.room_id', 'data.live_id', 'data.stream_id'],
|
|
108
|
-
'直播间状态': ['status', 'live_status', 'live.status', 'data.status', 'room.status', 'data.live.status', 'data.stream_status', 'data.room_status'],
|
|
109
|
-
'图片数量': ['count', 'img_count', 'image_count', 'pic_count', 'data.count', 'item.count', 'images.length', 'data.images.length', 'data.item.count', 'data.pic_num', 'data.img_num'],
|
|
110
|
-
'作者ID': ['uid', 'userid', 'user_id', 'userId', 'userID', 'author_id', 'data.userId', 'item.userID', 'author.mid', 'user.mid', 'data.item.userID', 'data.author_id', 'data.user.mid', 'author.id', 'short_id', 'data.author.id', 'data.uid', 'data.mid', 'data.open_id', 'data.account_id']
|
|
88
|
+
bilibili: ['bilibili', 'b23', 'www.bilibili.com', 'm.bilibili.com', 'b23.tv', 't.bilibili.com', 'bilibili.com/video', 'bilibili.com/opus', 'bilibili.com/bangumi'],
|
|
89
|
+
kuaishou: ['kuaishou', 'v.kuaishou.com', 'www.kuaishou.com', 'kwimgs.com'],
|
|
90
|
+
weibo: ['weibo', 'weibo.com', 'video.weibo.com', 'm.weibo.cn', 'weibo.com/tv/show', 'weibo.com/feed'],
|
|
91
|
+
toutiao: ['toutiao', 'm.toutiao.com', 'toutiao.com', 'ixigua.com', 'toutiao.com/video'],
|
|
92
|
+
pipigx: ['pipigx', 'h5.pipigx.com', 'ippzone.com'],
|
|
93
|
+
pipixia: ['pipixia', 'pipix', 'h5.pipix.com', 'ppxsign.byteimg.com', 'pipix.com'],
|
|
94
|
+
douyin: ['douyin', 'v.douyin.com', 'douyinpic.com', 'douyinvod.com', 'douyin.com/video', 'douyin.com/note', 'www.douyin.com'],
|
|
95
|
+
zuiyou: ['zuiyou', 'xiaochuankeji.cn', 'izuiyou.com'],
|
|
96
|
+
xiaohongshu: ['xiaohongshu', 'xhslink.com', 'www.xiaohongshu.com'],
|
|
97
|
+
jianying: ['jianying', 'jimeng.jianying.com', 'lv.ulikecam.com'],
|
|
98
|
+
acfun: ['acfun', 'acfun.cn', 'www.acfun.cn'],
|
|
99
|
+
zhihu: ['zhihu', 'zhihu.com', 'www.zhihu.com'],
|
|
100
|
+
weishi: ['weishi', 'weishi.qq.com'],
|
|
101
|
+
huya: ['huya', 'huya.com', 'www.huya.com'],
|
|
102
|
+
youtube: ['youtube', 'youtube.com', 'youtu.be', 'www.youtube.com'],
|
|
103
|
+
tiktok: ['tiktok', 'tiktok.com', 'www.tiktok.com'],
|
|
104
|
+
xigua: ['xigua', 'ixigua.com'],
|
|
105
|
+
haokan: ['haokan', 'haokan.baidu.com'],
|
|
106
|
+
li: ['li', 'video.li'],
|
|
107
|
+
meipai: ['meipai', 'meipai.com'],
|
|
108
|
+
quanmin: ['quanmin', 'quanmin.tv'],
|
|
109
|
+
twitter: ['twitter', 'x.com'],
|
|
110
|
+
instagram: ['instagram', 'instagram.com'],
|
|
111
|
+
doubao: ['doubao', 'doubao.com'],
|
|
112
|
+
jimeng: ['jimeng', 'jimeng.ai'],
|
|
111
113
|
};
|
|
114
|
+
const PLATFORM_TYPES = Object.keys(PLATFORM_KEYWORDS);
|
|
112
115
|
function getErrorMessage(error) {
|
|
113
116
|
if (error instanceof Error)
|
|
114
117
|
return error.message;
|
|
@@ -118,14 +121,11 @@ async function getFileSize(url, userAgent) {
|
|
|
118
121
|
try {
|
|
119
122
|
const response = await axios_1.default.head(url, {
|
|
120
123
|
timeout: 10000,
|
|
121
|
-
headers: {
|
|
122
|
-
'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
123
|
-
}
|
|
124
|
+
headers: { 'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }
|
|
124
125
|
});
|
|
125
126
|
const contentLength = response.headers['content-length'];
|
|
126
|
-
if (contentLength)
|
|
127
|
+
if (contentLength)
|
|
127
128
|
return Math.round(Number(contentLength) / 1024 / 1024 * 100) / 100;
|
|
128
|
-
}
|
|
129
129
|
}
|
|
130
130
|
catch (error) { }
|
|
131
131
|
return 0;
|
|
@@ -156,15 +156,9 @@ if (!worker_threads_1.isMainThread) {
|
|
|
156
156
|
}).then(response => {
|
|
157
157
|
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
158
158
|
response.data.pipe(writeStream);
|
|
159
|
-
writeStream.on('finish', () => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
writeStream.on('error', (error) => {
|
|
163
|
-
worker_threads_1.parentPort?.postMessage({ success: false, error: error.message });
|
|
164
|
-
});
|
|
165
|
-
}).catch(error => {
|
|
166
|
-
worker_threads_1.parentPort?.postMessage({ success: false, error: error.message });
|
|
167
|
-
});
|
|
159
|
+
writeStream.on('finish', () => worker_threads_1.parentPort?.postMessage({ success: true, filePath, start, end }));
|
|
160
|
+
writeStream.on('error', (error) => worker_threads_1.parentPort?.postMessage({ success: false, error: error.message }));
|
|
161
|
+
}).catch((error) => worker_threads_1.parentPort?.postMessage({ success: false, error: error.message }));
|
|
168
162
|
}
|
|
169
163
|
async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
170
164
|
const dir = path_1.default.join(process.cwd(), 'temp_videos');
|
|
@@ -172,22 +166,18 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
172
166
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
173
167
|
const filePath = path_1.default.join(dir, `${filename}.mp4`);
|
|
174
168
|
try {
|
|
175
|
-
if (url.endsWith('.m4a') || url.endsWith('.mp3'))
|
|
169
|
+
if (url.endsWith('.m4a') || url.endsWith('.mp3'))
|
|
176
170
|
return { filePath: '', success: false };
|
|
177
|
-
}
|
|
178
171
|
const fileSize = await getFileSize(url, userAgent);
|
|
179
|
-
if (maxSize > 0 && fileSize > maxSize)
|
|
172
|
+
if (maxSize > 0 && fileSize > maxSize)
|
|
180
173
|
return { filePath: '', success: false };
|
|
181
|
-
}
|
|
182
174
|
if (threads <= 0 || fileSize === 0) {
|
|
183
175
|
const response = await (0, axios_1.default)({
|
|
184
176
|
url,
|
|
185
177
|
method: 'GET',
|
|
186
178
|
responseType: 'stream',
|
|
187
179
|
timeout: 60000,
|
|
188
|
-
headers: {
|
|
189
|
-
'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
190
|
-
}
|
|
180
|
+
headers: { 'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }
|
|
191
181
|
});
|
|
192
182
|
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
193
183
|
await (0, promises_1.pipeline)(response.data, writeStream);
|
|
@@ -199,13 +189,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
199
189
|
for (let i = 0; i < threads; i++) {
|
|
200
190
|
const start = i * chunkSize;
|
|
201
191
|
const end = i === threads - 1 ? totalSize - 1 : start + chunkSize - 1;
|
|
202
|
-
promises.push(downloadVideoThread({
|
|
203
|
-
url,
|
|
204
|
-
start,
|
|
205
|
-
end,
|
|
206
|
-
filename,
|
|
207
|
-
userAgent
|
|
208
|
-
}));
|
|
192
|
+
promises.push(downloadVideoThread({ url, start, end, filename, userAgent }));
|
|
209
193
|
}
|
|
210
194
|
const results = await Promise.all(promises);
|
|
211
195
|
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
@@ -220,49 +204,29 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
|
220
204
|
return { filePath, success: true };
|
|
221
205
|
}
|
|
222
206
|
catch (error) {
|
|
223
|
-
if (fs_1.default.existsSync(filePath))
|
|
207
|
+
if (fs_1.default.existsSync(filePath))
|
|
224
208
|
fs_1.default.unlinkSync(filePath);
|
|
225
|
-
}
|
|
226
209
|
const partFiles = fs_1.default.readdirSync(dir).filter(file => file.startsWith(`${filename}_`) && file.endsWith('.part'));
|
|
227
|
-
partFiles.forEach(file => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
catch (e) { }
|
|
232
|
-
});
|
|
233
|
-
logger.error(`视频下载失败: ${getErrorMessage(error)}`);
|
|
210
|
+
partFiles.forEach(file => { try {
|
|
211
|
+
fs_1.default.unlinkSync(path_1.default.join(dir, file));
|
|
212
|
+
}
|
|
213
|
+
catch (e) { } });
|
|
234
214
|
return { filePath: '', success: false };
|
|
235
215
|
}
|
|
236
216
|
}
|
|
237
217
|
function extractUrl(content) {
|
|
238
|
-
|
|
218
|
+
const urlMatches = content.match(/https?:\/\/[^\s\"\'\>]+/gi) || [];
|
|
239
219
|
return urlMatches.filter(url => {
|
|
240
220
|
const lower = url.toLowerCase();
|
|
241
221
|
return Object.values(PLATFORM_KEYWORDS).some(group => group.some(keyword => lower.includes(keyword)));
|
|
242
222
|
});
|
|
243
223
|
}
|
|
244
|
-
function hasPlatformKeyword(content) {
|
|
245
|
-
const lower = content.toLowerCase();
|
|
246
|
-
return Object.values(PLATFORM_KEYWORDS).some(group => group.some(keyword => lower.includes(keyword)));
|
|
247
|
-
}
|
|
248
224
|
function getPlatformType(url) {
|
|
249
225
|
const lower = url.toLowerCase();
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (PLATFORM_KEYWORDS.weibo.some(k => lower.includes(k)))
|
|
255
|
-
return 'weibo';
|
|
256
|
-
if (PLATFORM_KEYWORDS.toutiao.some(k => lower.includes(k)))
|
|
257
|
-
return 'toutiao';
|
|
258
|
-
if (PLATFORM_KEYWORDS.pipigx.some(k => lower.includes(k)))
|
|
259
|
-
return 'pipigx';
|
|
260
|
-
if (PLATFORM_KEYWORDS.pipixia.some(k => lower.includes(k)))
|
|
261
|
-
return 'pipixia';
|
|
262
|
-
if (PLATFORM_KEYWORDS.douyin.some(k => lower.includes(k)))
|
|
263
|
-
return 'douyin';
|
|
264
|
-
if (PLATFORM_KEYWORDS.zuiyou.some(k => lower.includes(k)))
|
|
265
|
-
return 'zuiyou';
|
|
226
|
+
for (const [platform, keywords] of Object.entries(PLATFORM_KEYWORDS)) {
|
|
227
|
+
if (keywords.some(k => lower.includes(k)))
|
|
228
|
+
return platform;
|
|
229
|
+
}
|
|
266
230
|
return null;
|
|
267
231
|
}
|
|
268
232
|
function cleanUrl(url) {
|
|
@@ -286,7 +250,7 @@ async function resolveShortUrl(url) {
|
|
|
286
250
|
timeout: 10000,
|
|
287
251
|
maxRedirects: 10,
|
|
288
252
|
headers: {
|
|
289
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
|
|
253
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
290
254
|
'Referer': 'https://www.baidu.com/',
|
|
291
255
|
},
|
|
292
256
|
validateStatus: status => true
|
|
@@ -326,23 +290,17 @@ function formatPublishTime(value) {
|
|
|
326
290
|
const str = String(value).trim();
|
|
327
291
|
if (value === 'ctime')
|
|
328
292
|
return '';
|
|
329
|
-
if (/^\d{10}$/.test(str))
|
|
293
|
+
if (/^\d{10}$/.test(str))
|
|
330
294
|
value = Number(str) * 1000;
|
|
331
|
-
}
|
|
332
295
|
if (/^\d{10,}$/.test(str) && Number(str) > 1e12) {
|
|
333
|
-
if (Number(str) > 1e15)
|
|
296
|
+
if (Number(str) > 1e15)
|
|
334
297
|
value = Number(str) / 1000;
|
|
335
|
-
}
|
|
336
298
|
}
|
|
337
299
|
try {
|
|
338
300
|
const d = new Date(/^\d+$/.test(str) ? Number(str) : str);
|
|
339
301
|
if (isNaN(d.getTime()))
|
|
340
302
|
return str;
|
|
341
|
-
const y = d.getFullYear();
|
|
342
|
-
const m = (d.getMonth() + 1).toString().padStart(2, '0');
|
|
343
|
-
const d_ = d.getDate().toString().padStart(2, '0');
|
|
344
|
-
const H = d.getHours().toString().padStart(2, '0');
|
|
345
|
-
const i = d.getMinutes().toString().padStart(2, '0');
|
|
303
|
+
const y = d.getFullYear(), m = (d.getMonth() + 1).toString().padStart(2, '0'), d_ = d.getDate().toString().padStart(2, '0'), H = d.getHours().toString().padStart(2, '0'), i = d.getMinutes().toString().padStart(2, '0');
|
|
346
304
|
const parts = [];
|
|
347
305
|
if (y > 2000)
|
|
348
306
|
parts.push(`${y}年`);
|
|
@@ -358,187 +316,42 @@ function formatPublishTime(value) {
|
|
|
358
316
|
return str;
|
|
359
317
|
}
|
|
360
318
|
}
|
|
361
|
-
function
|
|
362
|
-
|
|
363
|
-
return undefined;
|
|
364
|
-
const keys = path.split('.');
|
|
365
|
-
let value = obj;
|
|
366
|
-
for (const key of keys) {
|
|
367
|
-
if (value === null || value === undefined)
|
|
368
|
-
return undefined;
|
|
369
|
-
value = value[key];
|
|
370
|
-
}
|
|
371
|
-
return value;
|
|
372
|
-
}
|
|
373
|
-
function findValueInObject(obj, keys) {
|
|
374
|
-
if (!obj || typeof obj !== 'object' || !keys || keys.length === 0)
|
|
375
|
-
return undefined;
|
|
376
|
-
for (const key of keys) {
|
|
377
|
-
if (key.includes('.')) {
|
|
378
|
-
const value = getNestedValue(obj, key);
|
|
379
|
-
if (value !== undefined && value !== null && value !== '' && value !== 0)
|
|
380
|
-
return value;
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
if (obj[key] !== undefined && obj[key] !== null && obj[key] !== '' && obj[key] !== 0)
|
|
384
|
-
return obj[key];
|
|
385
|
-
const lowerKey = key.toLowerCase();
|
|
386
|
-
for (const objKey of Object.keys(obj)) {
|
|
387
|
-
if (objKey.toLowerCase() === lowerKey) {
|
|
388
|
-
const val = obj[objKey];
|
|
389
|
-
if (val !== undefined && val !== null && val !== '' && val !== 0)
|
|
390
|
-
return val;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return undefined;
|
|
396
|
-
}
|
|
397
|
-
function parseData(rawResponse, maxDescLength) {
|
|
398
|
-
const root = rawResponse || {};
|
|
399
|
-
const data = root.data || root;
|
|
400
|
-
const stat = {};
|
|
401
|
-
let totalImageCount = 0;
|
|
402
|
-
if (root.msg === 'live' && data.live) {
|
|
403
|
-
const liveData = data.live;
|
|
404
|
-
stat['标题'] = liveData.title || '';
|
|
405
|
-
stat['直播间地址'] = liveData.room_url || '';
|
|
406
|
-
stat['直播间ID'] = liveData.room_id || '';
|
|
407
|
-
stat['直播间状态'] = liveData.status === 1 ? '直播中' : (liveData.status === 0 ? '未开播' : '未知');
|
|
408
|
-
stat['在线人数'] = liveData.online || '';
|
|
409
|
-
stat['关注数'] = liveData.attention || '';
|
|
410
|
-
stat['发布时间'] = formatPublishTime(liveData.time);
|
|
411
|
-
stat['简介'] = liveData.desc || '';
|
|
412
|
-
}
|
|
413
|
-
Object.entries(VARIABLE_MAPPING).forEach(([varName, keys]) => {
|
|
414
|
-
if (stat[varName] !== undefined)
|
|
415
|
-
return;
|
|
416
|
-
let value = findValueInObject(data, keys) || findValueInObject(root, keys);
|
|
417
|
-
if (varName === '图片数量' && value === undefined) {
|
|
418
|
-
let imgCount = 0;
|
|
419
|
-
const imgSources = [
|
|
420
|
-
data.images, data.pics, data.pic_urls, data.image_list, data.imgurl,
|
|
421
|
-
root.images, root.pics, root.pic_urls, root.image_list, root.imgurl,
|
|
422
|
-
data.item?.images
|
|
423
|
-
];
|
|
424
|
-
for (const source of imgSources) {
|
|
425
|
-
if (Array.isArray(source) && source.length > 0) {
|
|
426
|
-
imgCount = source.filter(i => i && typeof i === 'string').length;
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
totalImageCount = imgCount;
|
|
431
|
-
const cover = data.cover || data.video?.fm || data.imgurl || data.pic || data.thumbnail || data.cover_url ||
|
|
432
|
-
data.item?.cover || root.cover || data.live?.cover || data.live?.keyframe || '';
|
|
433
|
-
if (cover && imgCount > 0) {
|
|
434
|
-
imgCount = imgSources.find(source => Array.isArray(source))?.filter(i => i && typeof i === 'string' && i !== cover).length || 0;
|
|
435
|
-
}
|
|
436
|
-
value = totalImageCount;
|
|
437
|
-
}
|
|
438
|
-
if (value !== undefined && value !== null && value !== '' && value !== 0) {
|
|
439
|
-
stat[varName] = value;
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
let type = 'video';
|
|
443
|
-
if (data.jx?.type)
|
|
444
|
-
type = data.jx.type;
|
|
445
|
-
else if (data.type)
|
|
446
|
-
type = data.type;
|
|
447
|
-
else if (root.msg === 'cv')
|
|
448
|
-
type = 'cv';
|
|
449
|
-
else if (root.msg === 'live')
|
|
450
|
-
type = 'live';
|
|
451
|
-
else if ((data.images && data.images.length > 1) || (root.images && root.images.length > 1) ||
|
|
452
|
-
(data.imgurl && data.imgurl.length > 1) || (root.imgurl && root.imgurl.length > 1))
|
|
453
|
-
type = '图集';
|
|
454
|
-
const title = stat['标题'] || data.title || '无标题';
|
|
455
|
-
let author = '';
|
|
456
|
-
if (data.author && typeof data.author === 'object') {
|
|
457
|
-
author = data.author.name || '';
|
|
458
|
-
}
|
|
459
|
-
else {
|
|
460
|
-
author = data.author || '';
|
|
461
|
-
}
|
|
462
|
-
author = author || stat['作者'] || '未知作者';
|
|
463
|
-
const rawDesc = data.desc || data.content || stat['简介'] || '暂无简介';
|
|
464
|
-
const desc = rawDesc.slice(0, maxDescLength);
|
|
465
|
-
const cover = data.cover || data.live?.cover || data.live?.keyframe || '';
|
|
466
|
-
const images = Array.isArray(data.images) ? data.images : [];
|
|
467
|
-
const video = data.url || data.video_backup || (data.live?.url && Array.isArray(data.live.url) ? data.live.url[0] : '') || '';
|
|
468
|
-
const durationValue = data.duration || 0;
|
|
469
|
-
const duration = typeof durationValue === 'number' ? durationValue : parseInt(durationValue) || 0;
|
|
470
|
-
const durationFormatted = formatDuration(durationValue);
|
|
471
|
-
const pubTime = formatPublishTime(data.create_time || data.publish_time || data.live?.time);
|
|
472
|
-
if (pubTime)
|
|
473
|
-
stat['发布时间'] = pubTime;
|
|
474
|
-
if (durationFormatted !== '00:00:00')
|
|
475
|
-
stat['视频时长'] = durationFormatted;
|
|
476
|
-
if (stat['图片数量'] === 0)
|
|
477
|
-
delete stat['图片数量'];
|
|
478
|
-
const live_photo = data.live_photo || [];
|
|
479
|
-
const h_w = data.item?.h_w || [];
|
|
480
|
-
const quality_urls = data.quality_urls || {};
|
|
481
|
-
const default_quality = data.default_quality || '';
|
|
482
|
-
const download_url = video;
|
|
483
|
-
const play_count = stat['播放数'] || '';
|
|
484
|
-
const reposts_count = Number(stat['转发数']) || 0;
|
|
485
|
-
const attitudes_count = Number(stat['点赞数']) || 0;
|
|
486
|
-
const comments_count = Number(stat['评论数']) || 0;
|
|
319
|
+
function parseApiResponse(rawResponse) {
|
|
320
|
+
const data = rawResponse?.data || {};
|
|
487
321
|
return {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
h_w,
|
|
502
|
-
jx: data.jx || null,
|
|
503
|
-
quality_urls,
|
|
504
|
-
default_quality,
|
|
505
|
-
download_url,
|
|
506
|
-
play_count,
|
|
507
|
-
reposts_count,
|
|
508
|
-
attitudes_count,
|
|
509
|
-
comments_count
|
|
322
|
+
author: data.author || '',
|
|
323
|
+
uid: data.uid || '',
|
|
324
|
+
avatar: data.avatar || '',
|
|
325
|
+
like: Number(data.like) || 0,
|
|
326
|
+
time: Number(data.time) || 0,
|
|
327
|
+
title: data.title || '',
|
|
328
|
+
cover: data.cover || '',
|
|
329
|
+
url: data.url || '',
|
|
330
|
+
music: data.music ? {
|
|
331
|
+
author: data.music.author || '',
|
|
332
|
+
avatar: data.music.avatar || '',
|
|
333
|
+
title: data.music.title || ''
|
|
334
|
+
} : undefined
|
|
510
335
|
};
|
|
511
336
|
}
|
|
512
|
-
function generateFormattedText(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
337
|
+
function generateFormattedText(info, config) {
|
|
338
|
+
const format = config.unifiedMessageFormat || `标题:${'${标题}'}\n作者:${'${作者}'}\n点赞:${'${点赞数}'}\n链接:${'${视频链接}'}`;
|
|
339
|
+
const vars = {
|
|
340
|
+
'标题': info.title,
|
|
341
|
+
'作者': info.author,
|
|
342
|
+
'点赞数': String(info.like),
|
|
343
|
+
'作者ID': info.uid,
|
|
344
|
+
'视频链接': info.url,
|
|
345
|
+
'发布时间': info.time ? formatPublishTime(info.time) : '',
|
|
346
|
+
'封面': info.cover,
|
|
347
|
+
'音乐作者': info.music?.author || '',
|
|
348
|
+
'音乐封面': info.music?.avatar || '',
|
|
349
|
+
};
|
|
526
350
|
let result = format;
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (value === undefined || value === null || value === '') {
|
|
532
|
-
const lines = result.split('\n');
|
|
533
|
-
result = lines.filter((line) => !line.includes(varMatch)).join('\n');
|
|
534
|
-
}
|
|
535
|
-
else {
|
|
536
|
-
result = result.replace(varMatch, String(value));
|
|
537
|
-
}
|
|
538
|
-
});
|
|
539
|
-
return result.trim() || `标题:${parseData.title}
|
|
540
|
-
作者:${parseData.author}
|
|
541
|
-
简介:${parseData.desc}`;
|
|
351
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
352
|
+
result = result.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), value);
|
|
353
|
+
}
|
|
354
|
+
return result.replace(/^\s*\n/gm, '').trim();
|
|
542
355
|
}
|
|
543
356
|
function clearAllCache() {
|
|
544
357
|
processed.clear();
|
|
@@ -558,147 +371,93 @@ function clearAllCache() {
|
|
|
558
371
|
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
559
372
|
function buildForwardNode(session, content, botName) {
|
|
560
373
|
let messageContent;
|
|
561
|
-
if (Array.isArray(content))
|
|
374
|
+
if (Array.isArray(content))
|
|
562
375
|
messageContent = content;
|
|
563
|
-
|
|
564
|
-
else if (content && typeof content === 'object' && content.type) {
|
|
376
|
+
else if (content && typeof content === 'object' && content.type)
|
|
565
377
|
messageContent = [content];
|
|
566
|
-
|
|
567
|
-
else {
|
|
378
|
+
else
|
|
568
379
|
messageContent = [koishi_1.h.text(String(content))];
|
|
569
|
-
}
|
|
570
|
-
return (0, koishi_1.h)('node', {
|
|
571
|
-
user: {
|
|
572
|
-
nickname: botName.substring(0, 15),
|
|
573
|
-
user_id: session.selfId
|
|
574
|
-
}
|
|
575
|
-
}, messageContent);
|
|
380
|
+
return (0, koishi_1.h)('node', { user: { nickname: botName.substring(0, 15), user_id: session.selfId } }, messageContent);
|
|
576
381
|
}
|
|
577
382
|
function apply(ctx, config) {
|
|
383
|
+
initDebug(config.debug, config.debugFile);
|
|
384
|
+
debugLog('INFO', '插件初始化开始');
|
|
578
385
|
clearAllCache();
|
|
579
386
|
const http = axios_1.default.create({
|
|
580
387
|
timeout: config.timeout,
|
|
581
388
|
headers: {
|
|
582
|
-
'User-Agent': config.userAgent
|
|
389
|
+
'User-Agent': config.userAgent,
|
|
583
390
|
'Referer': 'https://www.baidu.com/',
|
|
584
391
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
585
392
|
}
|
|
586
393
|
});
|
|
587
|
-
async function
|
|
588
|
-
let
|
|
589
|
-
for (let i = 0; i <= retryTimes; i++) {
|
|
394
|
+
async function parseViaApi(url) {
|
|
395
|
+
for (let i = 0; i <= config.retryTimes; i++) {
|
|
590
396
|
try {
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
params,
|
|
397
|
+
const res = await http.get('https://api.bugpk.com/api/short_videos', {
|
|
398
|
+
params: { url },
|
|
594
399
|
timeout: config.timeout
|
|
595
400
|
});
|
|
596
|
-
|
|
401
|
+
debugLog('INFO', `API响应: code=${res.data?.code}, msg=${res.data?.msg}`);
|
|
402
|
+
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
403
|
+
return parseApiResponse(res.data);
|
|
404
|
+
}
|
|
405
|
+
throw new Error(res.data?.msg || '解析失败');
|
|
597
406
|
}
|
|
598
407
|
catch (error) {
|
|
599
|
-
|
|
600
|
-
if (i < retryTimes)
|
|
408
|
+
debugLog('ERROR', `第${i + 1}次请求失败: ${getErrorMessage(error)}`);
|
|
409
|
+
if (i < config.retryTimes)
|
|
601
410
|
await delay(config.retryInterval * (i + 1));
|
|
602
|
-
}
|
|
603
411
|
}
|
|
604
412
|
}
|
|
605
|
-
throw
|
|
413
|
+
throw new Error('API请求全部失败');
|
|
606
414
|
}
|
|
607
|
-
async function
|
|
415
|
+
async function parseUrl(url) {
|
|
416
|
+
debugLog('INFO', `解析链接: ${url}`);
|
|
608
417
|
let realUrl = await resolveShortUrl(url);
|
|
609
418
|
realUrl = cleanUrl(realUrl);
|
|
419
|
+
debugLog('DEBUG', `实际URL: ${realUrl}`);
|
|
610
420
|
const platform = getPlatformType(realUrl);
|
|
611
421
|
if (!platform) {
|
|
612
|
-
|
|
613
|
-
return {
|
|
614
|
-
}
|
|
615
|
-
const apiUrl = API_CONFIG[platform];
|
|
616
|
-
if (!apiUrl) {
|
|
617
|
-
logger.error(`该平台暂未配置解析接口: ${platform}`);
|
|
618
|
-
return { data: null, success: false, msg: '该平台暂未配置解析接口' };
|
|
422
|
+
debugLog('WARN', `不支持的平台: ${realUrl}`);
|
|
423
|
+
return { info: null, success: false, msg: '不支持该平台链接' };
|
|
619
424
|
}
|
|
620
425
|
try {
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
return { data: null, success: false, msg: '解析失败,API返回空数据' };
|
|
625
|
-
}
|
|
626
|
-
const isSuccess = resData.code === 0 || resData.code === 200 || resData.code === 1 ||
|
|
627
|
-
(resData.msg && (resData.msg.includes('解析成功') || resData.msg.includes('success') || resData.msg.includes('请求成功') || resData.msg === 'video' || resData.msg === 'cv' || resData.msg === 'live')) ||
|
|
628
|
-
!!resData.data || !!resData.result || !!resData.video || !!resData.images || !!resData.imgurl;
|
|
629
|
-
if (!isSuccess) {
|
|
630
|
-
const apiErrorMsg = resData.msg || resData.error || '解析失败';
|
|
631
|
-
logger.error(`API返回错误: ${url} - ${apiErrorMsg}`);
|
|
632
|
-
return { data: null, success: false, msg: `解析失败: ${apiErrorMsg}` };
|
|
633
|
-
}
|
|
634
|
-
try {
|
|
635
|
-
const parseResult = parseData(resData, config.maxDescLength);
|
|
636
|
-
const isAllDefault = parseResult.title === '无标题' &&
|
|
637
|
-
parseResult.author === '未知作者' &&
|
|
638
|
-
parseResult.desc === '暂无简介';
|
|
639
|
-
if (isAllDefault) {
|
|
640
|
-
logger.warn(`解析结果均为默认值(可能暂不支持该链接): ${url}`);
|
|
641
|
-
return {
|
|
642
|
-
data: null,
|
|
643
|
-
success: false,
|
|
644
|
-
msg: '解析失败: 暂不支持解析该链接'
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
logger.info(`解析成功: ${url}`);
|
|
648
|
-
return {
|
|
649
|
-
data: parseResult,
|
|
650
|
-
success: true,
|
|
651
|
-
msg: '解析成功'
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
catch (parseError) {
|
|
655
|
-
const errorMsg = getErrorMessage(parseError);
|
|
656
|
-
logger.error(`解析数据失败: ${url} - ${errorMsg}`);
|
|
657
|
-
return { data: null, success: false, msg: `解析数据失败: ${errorMsg}` };
|
|
658
|
-
}
|
|
426
|
+
const info = await parseViaApi(realUrl);
|
|
427
|
+
debugLog('INFO', `解析成功: ${info.title}`);
|
|
428
|
+
return { info, success: true, msg: '解析成功' };
|
|
659
429
|
}
|
|
660
430
|
catch (error) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (errorMsg.includes('timeout')) {
|
|
664
|
-
msg = '请求超时';
|
|
665
|
-
}
|
|
666
|
-
else if (errorMsg.includes('Network') || errorMsg.includes('network') || errorMsg.includes('404') || errorMsg.includes('500')) {
|
|
667
|
-
msg = '网络请求失败';
|
|
668
|
-
}
|
|
669
|
-
logger.error(`解析请求失败: ${url} - ${errorMsg}`);
|
|
670
|
-
return { data: null, success: false, msg };
|
|
431
|
+
debugLog('ERROR', `解析失败: ${getErrorMessage(error)}`);
|
|
432
|
+
return { info: null, success: false, msg: getErrorMessage(error) };
|
|
671
433
|
}
|
|
672
434
|
}
|
|
673
435
|
async function processSingleUrl(session, url) {
|
|
674
436
|
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
675
437
|
const now = Date.now();
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
438
|
+
const last = processed.get(hash);
|
|
439
|
+
if (last && (now - last) < config.sameLinkInterval * 1000) {
|
|
440
|
+
debugLog('WARN', `重复解析: ${url}`);
|
|
441
|
+
return { success: false, msg: '请勿重复解析相同链接' };
|
|
679
442
|
}
|
|
680
443
|
processed.set(hash, now);
|
|
681
|
-
const result = await
|
|
682
|
-
if (!result.
|
|
683
|
-
return {
|
|
684
|
-
const
|
|
685
|
-
const text = generateFormattedText(parseData, config);
|
|
444
|
+
const result = await parseUrl(url);
|
|
445
|
+
if (!result.info)
|
|
446
|
+
return { success: false, msg: result.msg };
|
|
447
|
+
const text = generateFormattedText(result.info, config);
|
|
686
448
|
return {
|
|
449
|
+
success: true,
|
|
687
450
|
data: {
|
|
688
451
|
text,
|
|
689
|
-
cover:
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
download_url: parseData.download_url
|
|
699
|
-
},
|
|
700
|
-
success: true,
|
|
701
|
-
msg: '处理成功'
|
|
452
|
+
cover: result.info.cover,
|
|
453
|
+
video: result.info.url,
|
|
454
|
+
music: result.info.music,
|
|
455
|
+
author: result.info.author,
|
|
456
|
+
uid: result.info.uid,
|
|
457
|
+
avatar: result.info.avatar,
|
|
458
|
+
like: result.info.like,
|
|
459
|
+
title: result.info.title
|
|
460
|
+
}
|
|
702
461
|
};
|
|
703
462
|
}
|
|
704
463
|
async function sendWithTimeout(session, content) {
|
|
@@ -707,22 +466,18 @@ function apply(ctx, config) {
|
|
|
707
466
|
return await session.send(content);
|
|
708
467
|
}
|
|
709
468
|
catch (err) {
|
|
710
|
-
const errorMsg = getErrorMessage(err);
|
|
711
|
-
logger.error(`发送消息失败: ${errorMsg}`);
|
|
712
469
|
if (!config.ignoreSendError)
|
|
713
470
|
throw err;
|
|
714
471
|
return null;
|
|
715
472
|
}
|
|
716
473
|
}
|
|
717
474
|
try {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
475
|
+
return await Promise.race([
|
|
476
|
+
session.send(content),
|
|
477
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout))
|
|
478
|
+
]);
|
|
722
479
|
}
|
|
723
480
|
catch (err) {
|
|
724
|
-
const errorMsg = getErrorMessage(err);
|
|
725
|
-
logger.error(`发送消息超时或失败: ${errorMsg}`);
|
|
726
481
|
if (!config.ignoreSendError)
|
|
727
482
|
throw err;
|
|
728
483
|
return null;
|
|
@@ -740,64 +495,39 @@ function apply(ctx, config) {
|
|
|
740
495
|
const errors = [];
|
|
741
496
|
for (const url of urls) {
|
|
742
497
|
const result = await processSingleUrl(session, url);
|
|
743
|
-
if (result.success
|
|
498
|
+
if (result.success)
|
|
744
499
|
items.push(result.data);
|
|
745
|
-
|
|
746
|
-
else {
|
|
500
|
+
else
|
|
747
501
|
errors.push({ url, msg: result.msg });
|
|
748
|
-
}
|
|
749
502
|
}
|
|
750
503
|
if (errors.length > 0) {
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
try {
|
|
754
|
-
await sendWithTimeout(session, errorMsg);
|
|
755
|
-
}
|
|
756
|
-
catch (e) {
|
|
757
|
-
logger.error(`发送错误消息失败: ${getErrorMessage(e)}`);
|
|
758
|
-
}
|
|
504
|
+
const errorMsg = `❌ 解析失败:\n${errors.map((e) => `【${e.url.slice(0, 50)}...】: ${e.msg}`).join('\n')}`;
|
|
505
|
+
await sendWithTimeout(session, errorMsg).catch(() => { });
|
|
759
506
|
await delay(500);
|
|
760
507
|
}
|
|
761
|
-
if (items.length === 0)
|
|
508
|
+
if (items.length === 0)
|
|
762
509
|
return;
|
|
763
|
-
}
|
|
764
510
|
const enableForward = config.enableForward && session.platform === 'onebot';
|
|
765
|
-
const forwardMessages = [];
|
|
766
511
|
const botName = config.botName || '视频解析机器人';
|
|
512
|
+
const forwardMessages = [];
|
|
767
513
|
for (const item of items) {
|
|
768
514
|
try {
|
|
769
515
|
if (enableForward) {
|
|
770
516
|
if (item.text)
|
|
771
517
|
forwardMessages.push(buildForwardNode(session, item.text, botName));
|
|
772
|
-
if (item.cover
|
|
518
|
+
if (item.cover)
|
|
773
519
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.cover), botName));
|
|
774
|
-
}
|
|
775
520
|
if (item.video && config.showVideoFile) {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
else {
|
|
784
|
-
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(item.video), botName));
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
else {
|
|
521
|
+
if (config.downloadVideoBeforeSend) {
|
|
522
|
+
const fname = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
523
|
+
const dl = await downloadVideo(item.video, fname, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
524
|
+
if (dl.success)
|
|
525
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.file(dl.filePath), botName));
|
|
526
|
+
else
|
|
788
527
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(item.video), botName));
|
|
789
|
-
}
|
|
790
528
|
}
|
|
791
|
-
|
|
792
|
-
logger.error(`处理视频发送失败: ${getErrorMessage(e)}`);
|
|
529
|
+
else
|
|
793
530
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(item.video), botName));
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
797
|
-
forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.totalImageCount}张)`, botName));
|
|
798
|
-
for (const img of item.images) {
|
|
799
|
-
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(img), botName));
|
|
800
|
-
}
|
|
801
531
|
}
|
|
802
532
|
}
|
|
803
533
|
else {
|
|
@@ -805,80 +535,44 @@ function apply(ctx, config) {
|
|
|
805
535
|
await sendWithTimeout(session, item.text);
|
|
806
536
|
await delay(300);
|
|
807
537
|
}
|
|
808
|
-
if (item.cover
|
|
809
|
-
|
|
810
|
-
await sendWithTimeout(session, koishi_1.h.image(item.cover));
|
|
811
|
-
}
|
|
812
|
-
catch (e) {
|
|
813
|
-
logger.error(`发送封面失败: ${getErrorMessage(e)}`);
|
|
814
|
-
}
|
|
538
|
+
if (item.cover) {
|
|
539
|
+
await sendWithTimeout(session, koishi_1.h.image(item.cover)).catch(() => { });
|
|
815
540
|
await delay(300);
|
|
816
541
|
}
|
|
817
542
|
if (item.video && config.showVideoFile) {
|
|
818
543
|
try {
|
|
819
544
|
if (config.downloadVideoBeforeSend) {
|
|
820
|
-
const
|
|
821
|
-
const dl = await downloadVideo(item.video,
|
|
822
|
-
|
|
823
|
-
await sendWithTimeout(session, koishi_1.h.file(dl.filePath));
|
|
824
|
-
}
|
|
825
|
-
else {
|
|
826
|
-
await sendWithTimeout(session, koishi_1.h.video(item.video));
|
|
827
|
-
}
|
|
545
|
+
const fname = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
546
|
+
const dl = await downloadVideo(item.video, fname, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
547
|
+
await sendWithTimeout(session, dl.success ? koishi_1.h.file(dl.filePath) : koishi_1.h.video(item.video));
|
|
828
548
|
}
|
|
829
549
|
else {
|
|
830
550
|
await sendWithTimeout(session, koishi_1.h.video(item.video));
|
|
831
551
|
}
|
|
832
552
|
}
|
|
833
|
-
catch
|
|
834
|
-
logger.error(`发送视频失败: ${getErrorMessage(e)}`);
|
|
553
|
+
catch {
|
|
835
554
|
try {
|
|
836
555
|
await sendWithTimeout(session, koishi_1.h.video(item.video));
|
|
837
556
|
}
|
|
838
|
-
catch
|
|
839
|
-
logger.error(`发送视频链接也失败: ${getErrorMessage(e2)}`);
|
|
840
|
-
}
|
|
557
|
+
catch { }
|
|
841
558
|
}
|
|
842
559
|
await delay(500);
|
|
843
560
|
}
|
|
844
|
-
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
845
|
-
try {
|
|
846
|
-
await sendWithTimeout(session, `📸 图集内容(共${item.totalImageCount}张)`);
|
|
847
|
-
}
|
|
848
|
-
catch (e) {
|
|
849
|
-
logger.error(`发送图集提示失败: ${getErrorMessage(e)}`);
|
|
850
|
-
}
|
|
851
|
-
await delay(300);
|
|
852
|
-
for (const img of item.images) {
|
|
853
|
-
try {
|
|
854
|
-
await sendWithTimeout(session, koishi_1.h.image(img));
|
|
855
|
-
}
|
|
856
|
-
catch (e) {
|
|
857
|
-
logger.error(`发送图集图片失败: ${getErrorMessage(e)}`);
|
|
858
|
-
}
|
|
859
|
-
await delay(200);
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
561
|
}
|
|
863
562
|
}
|
|
864
|
-
catch (e) {
|
|
865
|
-
logger.error(`处理消息发送失败: ${getErrorMessage(e)}`);
|
|
866
|
-
}
|
|
563
|
+
catch (e) { }
|
|
867
564
|
}
|
|
868
565
|
if (enableForward && forwardMessages.length) {
|
|
869
566
|
try {
|
|
870
567
|
await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100)));
|
|
871
568
|
}
|
|
872
|
-
catch
|
|
873
|
-
logger.error(`合并转发失败,降级为逐条发送: ${getErrorMessage(e)}`);
|
|
569
|
+
catch {
|
|
874
570
|
for (const node of forwardMessages) {
|
|
875
571
|
try {
|
|
876
572
|
await sendWithTimeout(session, node.data.content);
|
|
877
573
|
await delay(300);
|
|
878
574
|
}
|
|
879
|
-
catch
|
|
880
|
-
logger.error(`降级发送失败: ${getErrorMessage(e2)}`);
|
|
881
|
-
}
|
|
575
|
+
catch { }
|
|
882
576
|
}
|
|
883
577
|
}
|
|
884
578
|
}
|
|
@@ -894,9 +588,7 @@ function apply(ctx, config) {
|
|
|
894
588
|
try {
|
|
895
589
|
await sendWithTimeout(session, config.waitingTipText);
|
|
896
590
|
}
|
|
897
|
-
catch
|
|
898
|
-
logger.error(`发送等待提示失败: ${getErrorMessage(e)}`);
|
|
899
|
-
}
|
|
591
|
+
catch { }
|
|
900
592
|
}
|
|
901
593
|
await flush(session, urls);
|
|
902
594
|
});
|
|
@@ -914,13 +606,14 @@ function apply(ctx, config) {
|
|
|
914
606
|
});
|
|
915
607
|
setInterval(() => {
|
|
916
608
|
const now = Date.now();
|
|
917
|
-
processed.forEach((t, h) => now - t > 86400000
|
|
609
|
+
processed.forEach((t, h) => { if (now - t > 86400000)
|
|
610
|
+
processed.delete(h); });
|
|
918
611
|
}, 3600000);
|
|
919
612
|
if (config.autoClearCacheInterval > 0) {
|
|
920
613
|
setInterval(() => {
|
|
921
614
|
clearAllCache();
|
|
922
|
-
|
|
615
|
+
debugLog('INFO', '自动清理缓存');
|
|
923
616
|
}, config.autoClearCacheInterval * 60 * 1000);
|
|
924
617
|
}
|
|
925
|
-
|
|
618
|
+
debugLog('INFO', '插件初始化完成');
|
|
926
619
|
}
|
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.8.
|
|
3
|
+
"description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/微博/小红书/剪映/YouTube/TikTok等20+平台",
|
|
4
|
+
"version": "0.8.1",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"card",
|
|
24
24
|
"miniprogram",
|
|
25
25
|
"bilibili",
|
|
26
|
-
"bv",
|
|
27
26
|
"douyin",
|
|
28
27
|
"kuaishou",
|
|
29
28
|
"weibo",
|
|
@@ -32,6 +31,23 @@
|
|
|
32
31
|
"pipixia",
|
|
33
32
|
"xigua",
|
|
34
33
|
"zuiyou",
|
|
34
|
+
"xiaohongshu",
|
|
35
|
+
"jianying",
|
|
36
|
+
"acfun",
|
|
37
|
+
"zhihu",
|
|
38
|
+
"weishi",
|
|
39
|
+
"huya",
|
|
40
|
+
"youtube",
|
|
41
|
+
"tiktok",
|
|
42
|
+
"haokan",
|
|
43
|
+
"meipai",
|
|
44
|
+
"quanmin",
|
|
45
|
+
"twitter",
|
|
46
|
+
"instagram",
|
|
47
|
+
"doubao",
|
|
48
|
+
"jimeng",
|
|
49
|
+
"debug",
|
|
50
|
+
"unified-api",
|
|
35
51
|
"视频解析"
|
|
36
52
|
],
|
|
37
53
|
"devDependencies": {
|
package/readme.md
CHANGED
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
## 项目介绍 (Project Introduction)
|
|
4
4
|
|
|
5
5
|
### 中文
|
|
6
|
-
这是一个为 Koishi
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
6
|
+
这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙等20+主流平台的短视频/图集链接。核心特性:
|
|
7
|
+
- 🌐 统一API解析,覆盖20+热门平台,无需繁琐配置
|
|
8
|
+
- 🤖 自动识别链接来源,即丢即用
|
|
9
|
+
- 🎨 完全自定义的解析结果格式,支持20+丰富变量替换
|
|
10
|
+
- 🐛 内置Debug调试模式,可详细记录所有操作与API交互日志
|
|
11
|
+
- ⚡ 防重复解析、API重试、本地视频下载、多线程加速等实用功能
|
|
12
|
+
- 📤 支持OneBot平台消息合并转发,优化多图文展示体验
|
|
11
13
|
|
|
12
14
|
### English
|
|
13
|
-
This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework,
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
15
|
+
This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, using a unified API interface to automatically recognize and parse short video/image links from 20+ mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, YouTube, TikTok, Jianying, AcFun, Zhihu, Huya and more. Core features:
|
|
16
|
+
- 🌐 Unified API parsing, covering 20+ popular platforms without complex configuration
|
|
17
|
+
- 🤖 Auto-detection of link sources, just drop & go
|
|
18
|
+
- 🎨 Fully customizable parsing result format with 20+ variable substitutions
|
|
19
|
+
- 🐛 Built-in Debug mode, recording detailed operations and API interaction logs
|
|
20
|
+
- ⚡ Duplicate parsing prevention, API retry, local video download, multithread acceleration
|
|
21
|
+
- 📤 Support OneBot message forwarding for better image/video display
|
|
18
22
|
|
|
19
23
|
## 项目仓库 (Repository)
|
|
20
24
|
- GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
|
|
@@ -37,11 +41,13 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
37
41
|
| `showWaitingTip` | boolean | true | 解析时显示等待提示 |
|
|
38
42
|
| `waitingTipText` | string | 正在解析视频,请稍候... | 等待提示文本内容 |
|
|
39
43
|
| `sameLinkInterval` | number | 180 | 相同链接重复解析间隔(秒),防止频繁解析 |
|
|
44
|
+
| `debug` | boolean | false | 是否开启Debug调试模式,控制台输出详细日志 |
|
|
45
|
+
| `debugFile` | boolean | false | 开启Debug时将日志同时写入本地`debug.log`文件 |
|
|
40
46
|
|
|
41
47
|
### 统一消息格式
|
|
42
48
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
43
49
|
|--------|------|--------|------|
|
|
44
|
-
| `unifiedMessageFormat` | string |
|
|
50
|
+
| `unifiedMessageFormat` | string | 见变量说明 | 自定义解析结果的输出格式,支持变量替换 |
|
|
45
51
|
|
|
46
52
|
### 内容显示设置
|
|
47
53
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
@@ -58,7 +64,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
58
64
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
59
65
|
|--------|------|--------|------|
|
|
60
66
|
| `timeout` | number | 180000 | API请求超时时间(毫秒) |
|
|
61
|
-
| `videoSendTimeout` | number |
|
|
67
|
+
| `videoSendTimeout` | number | 60000 | 视频消息发送超时时间(毫秒,0为不限制) |
|
|
62
68
|
| `userAgent` | string | Chrome 124 UA | API请求使用的User-Agent标识 |
|
|
63
69
|
|
|
64
70
|
### 错误与重试设置
|
|
@@ -72,7 +78,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
72
78
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
73
79
|
|--------|------|--------|------|
|
|
74
80
|
| `enableForward` | boolean | false | 启用合并转发功能(仅OneBot平台) |
|
|
75
|
-
| `downloadVideoBeforeSend` | boolean | false |
|
|
81
|
+
| `downloadVideoBeforeSend` | boolean | false | 发送前先下载视频到本地并发送文件 |
|
|
76
82
|
| `maxVideoSize` | number | 0 | 最大视频下载大小限制(MB,0为不限制) |
|
|
77
83
|
| `downloadThreads` | number | 0 | 多线程下载线程数(0为单线程,最大10) |
|
|
78
84
|
|
|
@@ -96,7 +102,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
96
102
|
| `${简介}` | 内容简介/描述 | 部分平台 |
|
|
97
103
|
| `${视频时长}` | 视频时长 | 部分平台 |
|
|
98
104
|
| `${点赞数}` | 点赞数量 | 所有平台 |
|
|
99
|
-
| `${投币数}` | 投币数量 | 部分平台 |
|
|
105
|
+
| `${投币数}` | 投币数量 | 部分平台 (B站) |
|
|
100
106
|
| `${收藏数}` | 收藏数量 | 所有平台 |
|
|
101
107
|
| `${转发数}` | 转发/分享数量 | 所有平台 |
|
|
102
108
|
| `${播放数}` | 播放量 | 部分平台 |
|
|
@@ -116,14 +122,31 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
116
122
|
## 支持的平台 (Supported Platforms)
|
|
117
123
|
| 平台名称 | 关键词识别 | 解析能力 |
|
|
118
124
|
|----------|------------|----------|
|
|
119
|
-
| 哔哩哔哩 (B站) | bilibili
|
|
120
|
-
| 抖音 | douyin
|
|
121
|
-
| 快手 | kuaishou
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
126
|
-
|
|
|
125
|
+
| 哔哩哔哩 (B站) | bilibili, b23.tv, bilibili.com | 视频、直播、图文 |
|
|
126
|
+
| 抖音 | douyin, v.douyin.com | 短视频、图集 |
|
|
127
|
+
| 快手 | kuaishou, v.kuaishou.com | 短视频、图集 |
|
|
128
|
+
| 小红书 | xiaohongshu, xhslink.com | 图文、视频 |
|
|
129
|
+
| 微博 | weibo, video.weibo.com | 视频、图集 |
|
|
130
|
+
| 剪映 / 即梦 | jianying, jimeng.jianying.com | 视频模板 |
|
|
131
|
+
| 今日头条 / 西瓜视频 | toutiao, ixigua.com | 短视频 |
|
|
132
|
+
| AcFun (A站) | acfun, acfun.cn | 视频 |
|
|
133
|
+
| 知乎 | zhihu, zhihu.com | 视频、回答 |
|
|
134
|
+
| 微视 | weishi, weishi.qq.com | 短视频 |
|
|
135
|
+
| 虎牙 | huya, huya.com | 直播、视频 |
|
|
136
|
+
| YouTube (油管) | youtube, youtu.be | 视频 |
|
|
137
|
+
| TikTok (国际版抖音) | tiktok, tiktok.com | 短视频 |
|
|
138
|
+
| 好看视频 | haokan, haokan.baidu.com | 短视频 |
|
|
139
|
+
| 梨视频 | li (video.li) | 短视频 |
|
|
140
|
+
| 美拍 | meipai, meipai.com | 短视频 |
|
|
141
|
+
| 全民直播 | quanmin (quanmin.tv) | 直播 |
|
|
142
|
+
| Twitter / X | twitter, x.com | 视频、图文 |
|
|
143
|
+
| Instagram | instagram, instagram.com | 图文、Reels |
|
|
144
|
+
| 豆包 | doubao (doubao.com) | 视频 |
|
|
145
|
+
| 皮皮搞笑 | pipigx, h5.pipigx.com | 短视频 |
|
|
146
|
+
| 皮皮虾 | pipixia, h5.pipix.com | 短视频 |
|
|
147
|
+
| 最右 | zuiyou, xiaochuankeji.cn | 短视频 |
|
|
148
|
+
|
|
149
|
+
> 注:部分平台解析能力可能因API限制有所差异,具体以实际解析结果为准。
|
|
127
150
|
|
|
128
151
|
## 项目贡献者 (Contributors)
|
|
129
152
|
|
|
@@ -131,9 +154,9 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
131
154
|
|----------------------|-------------------------|
|
|
132
155
|
| Minecraft-1314 | 插件完整开发 (Complete plugin development) |
|
|
133
156
|
| JH-Ahua | BugPk-Api 支持 |
|
|
134
|
-
| 星之阁API | 星之阁API 支持 |
|
|
135
157
|
| shangxue | 灵感来源 |
|
|
136
|
-
|
|
158
|
+
|
|
159
|
+
(欢迎通过 Issues 或 PR 加入贡献者列表)
|
|
137
160
|
|
|
138
161
|
## 许可协议 (License)
|
|
139
162
|
|