koishi-plugin-video-parser-all 0.0.5 → 0.0.7
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 +6 -14
- package/lib/index.js +150 -202
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -4,23 +4,15 @@ export interface Config {
|
|
|
4
4
|
enable: boolean;
|
|
5
5
|
showWaitingTip: boolean;
|
|
6
6
|
waitingTipText: string;
|
|
7
|
-
allowBVAVParse: boolean;
|
|
8
7
|
sameLinkInterval: number;
|
|
9
|
-
minVideoDuration: number;
|
|
10
|
-
shortVideoTip: string;
|
|
11
|
-
shortVideoUseImageParse: boolean;
|
|
12
|
-
maxVideoDuration: number;
|
|
13
|
-
longVideoTip: string;
|
|
14
|
-
longVideoUseImageParse: boolean;
|
|
15
8
|
imageParseFormat: string;
|
|
16
|
-
|
|
9
|
+
showVideoUrl: boolean;
|
|
17
10
|
maxDescLength: number;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
};
|
|
11
|
+
douyinApi: string;
|
|
12
|
+
bilibiliApi: string;
|
|
13
|
+
kuaishouApi: string;
|
|
14
|
+
backupApi: string;
|
|
15
|
+
timeout: number;
|
|
24
16
|
}
|
|
25
17
|
export declare const Config: Schema<Config>;
|
|
26
18
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -11,16 +11,9 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
11
11
|
exports.name = 'video-parser-all';
|
|
12
12
|
exports.Config = koishi_1.Schema.object({
|
|
13
13
|
enable: koishi_1.Schema.boolean().default(true).description('开启解析功能'),
|
|
14
|
-
showWaitingTip: koishi_1.Schema.boolean().default(true).description('
|
|
15
|
-
waitingTipText: koishi_1.Schema.string().default('
|
|
16
|
-
|
|
17
|
-
sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接处理间隔(秒)'),
|
|
18
|
-
minVideoDuration: koishi_1.Schema.number().default(0).description('最小时长(分钟)'),
|
|
19
|
-
shortVideoTip: koishi_1.Schema.string().default('视频时长过短,不解析~').description('过短提示'),
|
|
20
|
-
shortVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过短视频用图文解析'),
|
|
21
|
-
maxVideoDuration: koishi_1.Schema.number().default(60).description('最大时长(分钟)'),
|
|
22
|
-
longVideoTip: koishi_1.Schema.string().default('视频时长过长,不解析~').description('过长提示'),
|
|
23
|
-
longVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过长视频用图文解析'),
|
|
14
|
+
showWaitingTip: koishi_1.Schema.boolean().default(true).description('显示等待提示'),
|
|
15
|
+
waitingTipText: koishi_1.Schema.string().default('正在解析视频…').description('等待提示文字'),
|
|
16
|
+
sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接去重间隔(秒)'),
|
|
24
17
|
imageParseFormat: koishi_1.Schema.string()
|
|
25
18
|
.role('textarea')
|
|
26
19
|
.default(`\${标题} \${tab} \${UP主}
|
|
@@ -30,222 +23,177 @@ exports.Config = koishi_1.Schema.object({
|
|
|
30
23
|
观看:\${观看} \${tab} 弹幕:\${弹幕}
|
|
31
24
|
\${~~~}
|
|
32
25
|
\${封面}`)
|
|
33
|
-
.description('
|
|
34
|
-
|
|
26
|
+
.description('解析输出格式(请勿修改占位符)'),
|
|
27
|
+
showVideoUrl: koishi_1.Schema.boolean().default(false).description('额外显示无水印视频链接'),
|
|
35
28
|
maxDescLength: koishi_1.Schema.number().default(200).description('简介最大长度'),
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}).description('素颜API配置(无需密钥,直接使用)')
|
|
29
|
+
douyinApi: koishi_1.Schema.string().default('https://api.douyin.wtf/api/hybrid/video_data').description('抖音主API'),
|
|
30
|
+
bilibiliApi: koishi_1.Schema.string().default('https://api.douyin.wtf/api/bilibili/web/fetch_one_video').description('B站主API'),
|
|
31
|
+
kuaishouApi: koishi_1.Schema.string().default('https://api.douyin.wtf/api/hybrid/video_data').description('快手主API'),
|
|
32
|
+
backupApi: koishi_1.Schema.string().default('https://www.alapi.cn/api/video/jh').description('聚合备用API'),
|
|
33
|
+
timeout: koishi_1.Schema.number().default(15000).description('API请求超时(毫秒)'),
|
|
42
34
|
});
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const SUYAN_API_MAP = {
|
|
46
|
-
douyin: 'https://api.suyanw.cn/api/douyin.php',
|
|
47
|
-
kuaishou: 'https://api.suyanw.cn/api/kuaishou.php'
|
|
48
|
-
};
|
|
49
|
-
function parseLocal(url, platform) {
|
|
50
|
-
return {
|
|
51
|
-
title: platform === 'bilibili' ? 'B站视频' : `${platform === 'douyin' ? '抖音' : '快手'}视频/图集`,
|
|
52
|
-
author: '未知作者',
|
|
53
|
-
description: '无简介',
|
|
54
|
-
like: 0,
|
|
55
|
-
coin: 0,
|
|
56
|
-
collect: 0,
|
|
57
|
-
share: 0,
|
|
58
|
-
view: 0,
|
|
59
|
-
danmaku: 0,
|
|
60
|
-
cover: '',
|
|
61
|
-
url: url,
|
|
62
|
-
images: []
|
|
63
|
-
};
|
|
64
|
-
}
|
|
35
|
+
// 去重缓存
|
|
36
|
+
const processed = new Map();
|
|
65
37
|
function apply(ctx, config) {
|
|
38
|
+
// 创建请求实例
|
|
66
39
|
const request = axios_1.default.create({
|
|
40
|
+
timeout: config.timeout,
|
|
67
41
|
headers: {
|
|
68
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
|
|
69
|
-
},
|
|
70
|
-
timeout: config.suyanApi.timeout,
|
|
71
|
-
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
|
|
72
|
-
});
|
|
73
|
-
async function parseWithSuyanApi(url, platform) {
|
|
74
|
-
try {
|
|
75
|
-
const apiUrl = SUYAN_API_MAP[platform];
|
|
76
|
-
const res = await request.get(apiUrl, {
|
|
77
|
-
params: { url: encodeURIComponent(url) }
|
|
78
|
-
});
|
|
79
|
-
const data = res.data;
|
|
80
|
-
if (data.code !== 200) {
|
|
81
|
-
throw new Error(`素颜API解析失败:${data.msg || '未知错误'}(错误码:${data.code})`);
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
title: data.data?.title || '未知标题',
|
|
85
|
-
author: data.data?.author || '未知作者',
|
|
86
|
-
description: data.data?.title || '无简介',
|
|
87
|
-
like: data.data?.likes || 0,
|
|
88
|
-
coin: 0,
|
|
89
|
-
collect: 0,
|
|
90
|
-
share: 0,
|
|
91
|
-
view: data.data?.views || 0,
|
|
92
|
-
danmaku: data.data?.comments || 0,
|
|
93
|
-
cover: data.data?.cover || '',
|
|
94
|
-
url: url,
|
|
95
|
-
images: data.image || []
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
catch (e) {
|
|
99
|
-
ctx.logger.warn(`素颜API解析${platform}失败:${e.message}`);
|
|
100
|
-
return parseLocal(url, platform);
|
|
42
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
101
43
|
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
: parseLocal(url, platform);
|
|
124
|
-
const isImageSet = videoInfo.images.length > 0;
|
|
125
|
-
if (!isImageSet) {
|
|
126
|
-
const duration = 0;
|
|
127
|
-
if (duration < config.minVideoDuration) {
|
|
128
|
-
return config.shortVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(config.shortVideoTip);
|
|
44
|
+
});
|
|
45
|
+
// 核心解析函数(按平台自动匹配API)
|
|
46
|
+
async function parseVideo(url) {
|
|
47
|
+
// 1. B站解析(优先主API)
|
|
48
|
+
if (url.includes('bilibili.com') || url.includes('b23.tv')) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await request.get(config.bilibiliApi, { params: { url } });
|
|
51
|
+
const d = res.data.data;
|
|
52
|
+
return {
|
|
53
|
+
title: d.title || '无标题',
|
|
54
|
+
author: d.owner?.name || '未知UP主',
|
|
55
|
+
desc: d.desc || '无简介',
|
|
56
|
+
digg: d.stat?.like || 0,
|
|
57
|
+
coin: d.stat?.coin || 0,
|
|
58
|
+
collect: d.stat?.favorite || 0,
|
|
59
|
+
share: d.stat?.share || 0,
|
|
60
|
+
play: d.stat?.view || 0,
|
|
61
|
+
danmaku: d.stat?.danmaku || 0,
|
|
62
|
+
cover: d.pic || '',
|
|
63
|
+
video: d.video_url || ''
|
|
64
|
+
};
|
|
129
65
|
}
|
|
130
|
-
|
|
131
|
-
|
|
66
|
+
catch (e) {
|
|
67
|
+
ctx.logger.warn(`B站主API解析失败,尝试备用API: ${e.message}`);
|
|
132
68
|
}
|
|
133
69
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.replace(/\${弹幕}/g, videoInfo.danmaku?.toString() || '0')
|
|
153
|
-
.replace(/\${tab}/g, '\t\t')
|
|
154
|
-
.replace(/\${~~~}/g, '————————————————————');
|
|
155
|
-
// 第二步:拆分内容和封面,分别发送(修复类型错误的核心)
|
|
156
|
-
const coverPlaceholder = '\${封面}';
|
|
157
|
-
if (content.includes(coverPlaceholder)) {
|
|
158
|
-
// 分割内容为封面前后两部分
|
|
159
|
-
const [beforeCover, afterCover] = content.split(coverPlaceholder);
|
|
160
|
-
// 发送封面之前的内容
|
|
161
|
-
if (beforeCover.trim()) {
|
|
162
|
-
await session.send(beforeCover.trim());
|
|
70
|
+
// 2. 快手解析(优先主API)
|
|
71
|
+
if (url.includes('kuaishou.com') || url.includes('ksweb')) {
|
|
72
|
+
try {
|
|
73
|
+
const res = await request.get(config.kuaishouApi, { params: { url } });
|
|
74
|
+
const d = res.data.data?.aweme_detail || res.data.data;
|
|
75
|
+
return {
|
|
76
|
+
title: d.desc || '无标题',
|
|
77
|
+
author: d.author?.nickname || '未知作者',
|
|
78
|
+
desc: d.desc || '无简介',
|
|
79
|
+
digg: d.statistics?.digg_count || 0,
|
|
80
|
+
coin: 0, // 快手无投币
|
|
81
|
+
collect: d.statistics?.collect_count || 0,
|
|
82
|
+
share: d.statistics?.share_count || 0,
|
|
83
|
+
play: d.statistics?.play_count || 0,
|
|
84
|
+
danmaku: d.statistics?.comment_count || 0, // 用评论数替代弹幕
|
|
85
|
+
cover: d.video?.cover || '',
|
|
86
|
+
video: d.video?.play_addr?.url_list?.[0] || ''
|
|
87
|
+
};
|
|
163
88
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
await session.send(koishi_1.h.image(videoInfo.cover));
|
|
89
|
+
catch (e) {
|
|
90
|
+
ctx.logger.warn(`快手主API解析失败,尝试备用API: ${e.message}`);
|
|
167
91
|
}
|
|
168
|
-
|
|
169
|
-
|
|
92
|
+
}
|
|
93
|
+
// 3. 抖音解析(优先主API)
|
|
94
|
+
if (url.includes('douyin.com') || url.includes('dy')) {
|
|
95
|
+
try {
|
|
96
|
+
const res = await request.get(config.douyinApi, { params: { url } });
|
|
97
|
+
const d = res.data.data?.aweme_detail || res.data.data;
|
|
98
|
+
return {
|
|
99
|
+
title: d.desc || '无标题',
|
|
100
|
+
author: d.author?.nickname || '未知作者',
|
|
101
|
+
desc: d.desc || '无简介',
|
|
102
|
+
digg: d.statistics?.digg_count || 0,
|
|
103
|
+
coin: 0, // 抖音无投币
|
|
104
|
+
collect: d.statistics?.collect_count || 0,
|
|
105
|
+
share: d.statistics?.share_count || 0,
|
|
106
|
+
play: d.statistics?.play_count || 0,
|
|
107
|
+
danmaku: d.statistics?.comment_count || 0, // 用评论数替代弹幕
|
|
108
|
+
cover: d.video?.cover || '',
|
|
109
|
+
video: d.video?.play_addr?.url_list?.[0] || ''
|
|
110
|
+
};
|
|
170
111
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
await session.send(afterCover.trim());
|
|
112
|
+
catch (e) {
|
|
113
|
+
ctx.logger.warn(`抖音主API解析失败,尝试备用API: ${e.message}`);
|
|
174
114
|
}
|
|
175
115
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
await
|
|
116
|
+
// 4. 备用聚合API(所有主API失败时)
|
|
117
|
+
try {
|
|
118
|
+
const res = await request.get(config.backupApi, { params: { url } });
|
|
119
|
+
const d = res.data.data;
|
|
120
|
+
return {
|
|
121
|
+
title: d.title || '无标题',
|
|
122
|
+
author: '未知作者',
|
|
123
|
+
desc: d.title || '无简介',
|
|
124
|
+
digg: 0,
|
|
125
|
+
coin: 0,
|
|
126
|
+
collect: 0,
|
|
127
|
+
share: 0,
|
|
128
|
+
play: 0,
|
|
129
|
+
danmaku: 0,
|
|
130
|
+
cover: d.cover_url || '',
|
|
131
|
+
video: d.video_url || ''
|
|
132
|
+
};
|
|
179
133
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (videoInfo.images.length > 0) {
|
|
183
|
-
await session.send(`📁 图集共${videoInfo.images.length}张:\n${videoInfo.images.slice(0, 10).join('\n')}${videoInfo.images.length > 10 ? '\n...(省略剩余图片)' : ''}`);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
await session.send(`📥 原始链接:${videoInfo.url}`);
|
|
187
|
-
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
ctx.logger.error(`备用API解析失败: ${e.message}`);
|
|
188
136
|
}
|
|
137
|
+
// 兜底返回
|
|
138
|
+
return {
|
|
139
|
+
title: '解析失败', author: '', desc: '无法获取视频信息',
|
|
140
|
+
digg: 0, coin: 0, collect: 0, share: 0, play: 0, danmaku: 0,
|
|
141
|
+
cover: '', video: ''
|
|
142
|
+
};
|
|
189
143
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
144
|
+
// 生成并发送解析结果(严格匹配你的格式)
|
|
145
|
+
async function sendResult(session, data) {
|
|
146
|
+
let text = config.imageParseFormat
|
|
147
|
+
.replace(/\${标题}/g, data.title)
|
|
148
|
+
.replace(/\${UP主}/g, data.author)
|
|
149
|
+
.replace(/\${简介}/g, data.desc.slice(0, config.maxDescLength))
|
|
150
|
+
.replace(/\${点赞}/g, data.digg.toString())
|
|
151
|
+
.replace(/\${投币}/g, data.coin.toString())
|
|
152
|
+
.replace(/\${收藏}/g, data.collect.toString())
|
|
153
|
+
.replace(/\${转发}/g, data.share.toString())
|
|
154
|
+
.replace(/\${观看}/g, data.play.toString())
|
|
155
|
+
.replace(/\${弹幕}/g, data.danmaku.toString())
|
|
156
|
+
.replace(/\${tab}/g, '\t')
|
|
157
|
+
.replace(/\${~~~}/g, '——————————————');
|
|
158
|
+
// 拆分封面占位符,避免类型错误
|
|
159
|
+
const [beforeCover, afterCover] = text.split('\${封面}');
|
|
160
|
+
if (beforeCover)
|
|
161
|
+
await session.send(beforeCover.trim());
|
|
162
|
+
if (data.cover)
|
|
163
|
+
await session.send(koishi_1.h.image(data.cover));
|
|
164
|
+
if (afterCover)
|
|
165
|
+
await session.send(afterCover.trim());
|
|
166
|
+
// 发送无水印视频
|
|
167
|
+
if (data.video) {
|
|
168
|
+
try {
|
|
169
|
+
await session.send(koishi_1.h.video(data.video));
|
|
170
|
+
if (config.showVideoUrl)
|
|
171
|
+
await session.send(`🔗 无水印链接:${data.video}`);
|
|
208
172
|
}
|
|
209
|
-
|
|
210
|
-
await
|
|
173
|
+
catch (e) {
|
|
174
|
+
await session.send(`📥 无水印视频:${data.video}`);
|
|
211
175
|
}
|
|
212
176
|
}
|
|
213
|
-
catch (e) {
|
|
214
|
-
await session.send(`❌ 消息发送失败:${e.message}`);
|
|
215
|
-
}
|
|
216
177
|
}
|
|
178
|
+
// 消息监听与处理
|
|
217
179
|
ctx.on('message', async (session) => {
|
|
218
180
|
if (!config.enable)
|
|
219
181
|
return;
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
const matches = [...content.matchAll(reg)];
|
|
223
|
-
if (!matches.length)
|
|
182
|
+
const url = session.content.trim();
|
|
183
|
+
if (!url.startsWith('http'))
|
|
224
184
|
return;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (!messageQueue.has(uid)) {
|
|
228
|
-
messageQueue.set(uid, []);
|
|
229
|
-
setTimeout(async () => {
|
|
230
|
-
const links = [...new Set(messageQueue.get(uid))];
|
|
231
|
-
messageQueue.delete(uid);
|
|
232
|
-
for (const l of links)
|
|
233
|
-
await parseVideo(l, session);
|
|
234
|
-
}, config.messageBufferDelay * 1000);
|
|
235
|
-
}
|
|
236
|
-
matches.forEach(m => messageQueue.get(uid).push(m[0]));
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
for (const m of matches)
|
|
240
|
-
await parseVideo(m[0], session);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
setInterval(() => {
|
|
185
|
+
// 去重逻辑
|
|
186
|
+
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
244
187
|
const now = Date.now();
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
188
|
+
if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000)
|
|
189
|
+
return;
|
|
190
|
+
processed.set(hash, now);
|
|
191
|
+
// 发送等待提示
|
|
192
|
+
if (config.showWaitingTip)
|
|
193
|
+
await session.send(config.waitingTipText);
|
|
194
|
+
// 解析并发送结果
|
|
195
|
+
const data = await parseVideo(url);
|
|
196
|
+
await sendResult(session, data);
|
|
197
|
+
});
|
|
198
|
+
ctx.logger.info('✅ 抖音+B站+快手 三平台解析插件加载完成');
|
|
251
199
|
}
|