koishi-plugin-video-parser-all 0.7.8 → 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 +220 -472
- package/package.json +19 -3
- package/readme.md +48 -23
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('显示图文内容'),
|
|
@@ -42,7 +38,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
42
38
|
}).description('内容长度限制'),
|
|
43
39
|
koishi_1.Schema.object({
|
|
44
40
|
timeout: koishi_1.Schema.number().min(0).default(180000).description('API请求超时时间(毫秒)'),
|
|
45
|
-
videoSendTimeout: koishi_1.Schema.number().min(0).default(
|
|
41
|
+
videoSendTimeout: koishi_1.Schema.number().min(0).default(60000).description('视频发送超时时间(毫秒,0为不限制)'),
|
|
46
42
|
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标识'),
|
|
47
43
|
}).description('网络与API设置'),
|
|
48
44
|
koishi_1.Schema.object({
|
|
@@ -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,170 +371,117 @@ 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
|
-
// 【关键修改1】控制台日志改为更精准的提示
|
|
641
|
-
logger.warn(`解析结果均为默认值(可能暂不支持该链接): ${url}`);
|
|
642
|
-
return {
|
|
643
|
-
data: null,
|
|
644
|
-
success: false,
|
|
645
|
-
msg: '解析失败: 暂不支持解析该链接'
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
logger.info(`解析成功: ${url}`);
|
|
649
|
-
return {
|
|
650
|
-
data: parseResult,
|
|
651
|
-
success: true,
|
|
652
|
-
msg: '解析成功'
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
catch (parseError) {
|
|
656
|
-
const errorMsg = getErrorMessage(parseError);
|
|
657
|
-
logger.error(`解析数据失败: ${url} - ${errorMsg}`);
|
|
658
|
-
return { data: null, success: false, msg: `解析数据失败: ${errorMsg}` };
|
|
659
|
-
}
|
|
426
|
+
const info = await parseViaApi(realUrl);
|
|
427
|
+
debugLog('INFO', `解析成功: ${info.title}`);
|
|
428
|
+
return { info, success: true, msg: '解析成功' };
|
|
660
429
|
}
|
|
661
430
|
catch (error) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (errorMsg.includes('timeout')) {
|
|
665
|
-
msg = '请求超时';
|
|
666
|
-
}
|
|
667
|
-
else if (errorMsg.includes('Network') || errorMsg.includes('network') || errorMsg.includes('404') || errorMsg.includes('500')) {
|
|
668
|
-
msg = '网络请求失败';
|
|
669
|
-
}
|
|
670
|
-
logger.error(`解析请求失败: ${url} - ${errorMsg}`);
|
|
671
|
-
return { data: null, success: false, msg };
|
|
431
|
+
debugLog('ERROR', `解析失败: ${getErrorMessage(error)}`);
|
|
432
|
+
return { info: null, success: false, msg: getErrorMessage(error) };
|
|
672
433
|
}
|
|
673
434
|
}
|
|
674
435
|
async function processSingleUrl(session, url) {
|
|
675
436
|
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
676
437
|
const now = Date.now();
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
438
|
+
const last = processed.get(hash);
|
|
439
|
+
if (last && (now - last) < config.sameLinkInterval * 1000) {
|
|
440
|
+
debugLog('WARN', `重复解析: ${url}`);
|
|
441
|
+
return { success: false, msg: '请勿重复解析相同链接' };
|
|
680
442
|
}
|
|
681
443
|
processed.set(hash, now);
|
|
682
|
-
const result = await
|
|
683
|
-
if (!result.
|
|
684
|
-
return {
|
|
685
|
-
const
|
|
686
|
-
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);
|
|
687
448
|
return {
|
|
449
|
+
success: true,
|
|
688
450
|
data: {
|
|
689
451
|
text,
|
|
690
|
-
cover:
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
download_url: parseData.download_url
|
|
700
|
-
},
|
|
701
|
-
success: true,
|
|
702
|
-
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
|
+
}
|
|
703
461
|
};
|
|
704
462
|
}
|
|
705
|
-
async function
|
|
463
|
+
async function sendWithTimeout(session, content) {
|
|
706
464
|
if (config.videoSendTimeout <= 0) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
465
|
+
try {
|
|
466
|
+
return await session.send(content);
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
710
469
|
if (!config.ignoreSendError)
|
|
711
|
-
|
|
470
|
+
throw err;
|
|
712
471
|
return null;
|
|
713
|
-
}
|
|
472
|
+
}
|
|
714
473
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
474
|
+
try {
|
|
475
|
+
return await Promise.race([
|
|
476
|
+
session.send(content),
|
|
477
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.videoSendTimeout))
|
|
478
|
+
]);
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
721
481
|
if (!config.ignoreSendError)
|
|
722
|
-
|
|
482
|
+
throw err;
|
|
723
483
|
return null;
|
|
724
|
-
}
|
|
484
|
+
}
|
|
725
485
|
}
|
|
726
486
|
async function flush(session, manualUrls) {
|
|
727
487
|
const key = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
@@ -735,101 +495,84 @@ function apply(ctx, config) {
|
|
|
735
495
|
const errors = [];
|
|
736
496
|
for (const url of urls) {
|
|
737
497
|
const result = await processSingleUrl(session, url);
|
|
738
|
-
if (result.success)
|
|
498
|
+
if (result.success)
|
|
739
499
|
items.push(result.data);
|
|
740
|
-
|
|
741
|
-
else {
|
|
500
|
+
else
|
|
742
501
|
errors.push({ url, msg: result.msg });
|
|
743
|
-
}
|
|
744
502
|
}
|
|
745
503
|
if (errors.length > 0) {
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
await sendTimeout(session, errorMsg);
|
|
504
|
+
const errorMsg = `❌ 解析失败:\n${errors.map((e) => `【${e.url.slice(0, 50)}...】: ${e.msg}`).join('\n')}`;
|
|
505
|
+
await sendWithTimeout(session, errorMsg).catch(() => { });
|
|
749
506
|
await delay(500);
|
|
750
507
|
}
|
|
751
|
-
|
|
752
|
-
if (items.length === 0) {
|
|
508
|
+
if (items.length === 0)
|
|
753
509
|
return;
|
|
754
|
-
}
|
|
755
510
|
const enableForward = config.enableForward && session.platform === 'onebot';
|
|
756
|
-
const forwardMessages = [];
|
|
757
511
|
const botName = config.botName || '视频解析机器人';
|
|
512
|
+
const forwardMessages = [];
|
|
758
513
|
for (const item of items) {
|
|
759
514
|
try {
|
|
760
515
|
if (enableForward) {
|
|
761
516
|
if (item.text)
|
|
762
517
|
forwardMessages.push(buildForwardNode(session, item.text, botName));
|
|
763
|
-
if (item.cover
|
|
518
|
+
if (item.cover)
|
|
764
519
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.cover), botName));
|
|
765
|
-
}
|
|
766
520
|
if (item.video && config.showVideoFile) {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
else {
|
|
775
|
-
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(item.video), botName));
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
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
|
|
779
527
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(item.video), botName));
|
|
780
|
-
}
|
|
781
528
|
}
|
|
782
|
-
|
|
529
|
+
else
|
|
783
530
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(item.video), botName));
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
787
|
-
forwardMessages.push(buildForwardNode(session, `📸 图集内容(共${item.totalImageCount}张)`, botName));
|
|
788
|
-
for (const img of item.images) {
|
|
789
|
-
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(img), botName));
|
|
790
|
-
}
|
|
791
531
|
}
|
|
792
532
|
}
|
|
793
533
|
else {
|
|
794
534
|
if (item.text) {
|
|
795
|
-
await
|
|
535
|
+
await sendWithTimeout(session, item.text);
|
|
796
536
|
await delay(300);
|
|
797
537
|
}
|
|
798
|
-
if (item.cover
|
|
799
|
-
await
|
|
538
|
+
if (item.cover) {
|
|
539
|
+
await sendWithTimeout(session, koishi_1.h.image(item.cover)).catch(() => { });
|
|
800
540
|
await delay(300);
|
|
801
541
|
}
|
|
802
542
|
if (item.video && config.showVideoFile) {
|
|
803
543
|
try {
|
|
804
|
-
|
|
544
|
+
if (config.downloadVideoBeforeSend) {
|
|
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));
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
await sendWithTimeout(session, koishi_1.h.video(item.video));
|
|
551
|
+
}
|
|
805
552
|
}
|
|
806
|
-
catch
|
|
807
|
-
|
|
553
|
+
catch {
|
|
554
|
+
try {
|
|
555
|
+
await sendWithTimeout(session, koishi_1.h.video(item.video));
|
|
556
|
+
}
|
|
557
|
+
catch { }
|
|
808
558
|
}
|
|
809
559
|
await delay(500);
|
|
810
560
|
}
|
|
811
|
-
if ((item.type === '图集' || item.type === 'image') && item.images?.length) {
|
|
812
|
-
await sendTimeout(session, `📸 图集内容(共${item.totalImageCount}张)`);
|
|
813
|
-
await delay(300);
|
|
814
|
-
for (const img of item.images) {
|
|
815
|
-
await sendTimeout(session, koishi_1.h.image(img));
|
|
816
|
-
await delay(200);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
561
|
}
|
|
820
562
|
}
|
|
821
|
-
catch (e) {
|
|
822
|
-
logger.error(`处理消息发送失败: ${getErrorMessage(e)}`);
|
|
823
|
-
}
|
|
563
|
+
catch (e) { }
|
|
824
564
|
}
|
|
825
565
|
if (enableForward && forwardMessages.length) {
|
|
826
566
|
try {
|
|
827
|
-
await
|
|
567
|
+
await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100)));
|
|
828
568
|
}
|
|
829
|
-
catch
|
|
569
|
+
catch {
|
|
830
570
|
for (const node of forwardMessages) {
|
|
831
|
-
|
|
832
|
-
|
|
571
|
+
try {
|
|
572
|
+
await sendWithTimeout(session, node.data.content);
|
|
573
|
+
await delay(300);
|
|
574
|
+
}
|
|
575
|
+
catch { }
|
|
833
576
|
}
|
|
834
577
|
}
|
|
835
578
|
}
|
|
@@ -841,31 +584,36 @@ function apply(ctx, config) {
|
|
|
841
584
|
const urls = extractUrl(content);
|
|
842
585
|
if (!urls.length)
|
|
843
586
|
return;
|
|
844
|
-
if (config.showWaitingTip)
|
|
845
|
-
|
|
587
|
+
if (config.showWaitingTip) {
|
|
588
|
+
try {
|
|
589
|
+
await sendWithTimeout(session, config.waitingTipText);
|
|
590
|
+
}
|
|
591
|
+
catch { }
|
|
592
|
+
}
|
|
846
593
|
await flush(session, urls);
|
|
847
594
|
});
|
|
848
595
|
ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
|
|
849
596
|
const us = extractUrl(url);
|
|
850
597
|
if (!us.length) {
|
|
851
|
-
await
|
|
598
|
+
await sendWithTimeout(session, '无效的视频链接');
|
|
852
599
|
return;
|
|
853
600
|
}
|
|
854
601
|
await flush(session, us);
|
|
855
602
|
});
|
|
856
603
|
ctx.command('clear-cache', '清空缓存').action(async ({ session }) => {
|
|
857
604
|
clearAllCache();
|
|
858
|
-
await
|
|
605
|
+
await sendWithTimeout(session, '✅ 缓存已清空');
|
|
859
606
|
});
|
|
860
607
|
setInterval(() => {
|
|
861
608
|
const now = Date.now();
|
|
862
|
-
processed.forEach((t, h) => now - t > 86400000
|
|
609
|
+
processed.forEach((t, h) => { if (now - t > 86400000)
|
|
610
|
+
processed.delete(h); });
|
|
863
611
|
}, 3600000);
|
|
864
612
|
if (config.autoClearCacheInterval > 0) {
|
|
865
613
|
setInterval(() => {
|
|
866
614
|
clearAllCache();
|
|
867
|
-
|
|
615
|
+
debugLog('INFO', '自动清理缓存');
|
|
868
616
|
}, config.autoClearCacheInterval * 60 * 1000);
|
|
869
617
|
}
|
|
870
|
-
|
|
618
|
+
debugLog('INFO', '插件初始化完成');
|
|
871
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.
|
|
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,7 +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
|
-
|
|
|
157
|
+
| shangxue | 灵感来源 |
|
|
158
|
+
|
|
159
|
+
(欢迎通过 Issues 或 PR 加入贡献者列表)
|
|
135
160
|
|
|
136
161
|
## 许可协议 (License)
|
|
137
162
|
|