koishi-plugin-video-parser-all 0.0.5 → 0.0.6
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 -14
- package/lib/index.js +81 -215
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -4,23 +4,13 @@ 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
|
-
suyanApi: {
|
|
22
|
-
timeout: number;
|
|
23
|
-
};
|
|
11
|
+
api1: string;
|
|
12
|
+
api2: string;
|
|
13
|
+
timeout: number;
|
|
24
14
|
}
|
|
25
15
|
export declare const Config: Schema<Config>;
|
|
26
16
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -10,17 +10,10 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
10
10
|
const crypto_1 = __importDefault(require("crypto"));
|
|
11
11
|
exports.name = 'video-parser-all';
|
|
12
12
|
exports.Config = koishi_1.Schema.object({
|
|
13
|
-
enable: koishi_1.Schema.boolean().default(true)
|
|
14
|
-
showWaitingTip: koishi_1.Schema.boolean().default(true)
|
|
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('过长视频用图文解析'),
|
|
13
|
+
enable: koishi_1.Schema.boolean().default(true),
|
|
14
|
+
showWaitingTip: koishi_1.Schema.boolean().default(true),
|
|
15
|
+
waitingTipText: koishi_1.Schema.string().default('正在解析视频…'),
|
|
16
|
+
sameLinkInterval: koishi_1.Schema.number().default(180),
|
|
24
17
|
imageParseFormat: koishi_1.Schema.string()
|
|
25
18
|
.role('textarea')
|
|
26
19
|
.default(`\${标题} \${tab} \${UP主}
|
|
@@ -29,223 +22,96 @@ exports.Config = koishi_1.Schema.object({
|
|
|
29
22
|
收藏:\${收藏} \${tab} 转发:\${转发}
|
|
30
23
|
观看:\${观看} \${tab} 弹幕:\${弹幕}
|
|
31
24
|
\${~~~}
|
|
32
|
-
\${封面}`)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
messageBufferDelay: koishi_1.Schema.number().default(1).description('消息缓冲延迟(秒)'),
|
|
39
|
-
suyanApi: koishi_1.Schema.object({
|
|
40
|
-
timeout: koishi_1.Schema.number().default(15000).description('素颜API超时时间(毫秒)')
|
|
41
|
-
}).description('素颜API配置(无需密钥,直接使用)')
|
|
25
|
+
\${封面}`),
|
|
26
|
+
showVideoUrl: koishi_1.Schema.boolean().default(false),
|
|
27
|
+
maxDescLength: koishi_1.Schema.number().default(200),
|
|
28
|
+
api1: koishi_1.Schema.string().default('https://api.douyin.wtf/api/hybrid/video_data'),
|
|
29
|
+
api2: koishi_1.Schema.string().default('https://www.alapi.cn/api/video/jh'),
|
|
30
|
+
timeout: koishi_1.Schema.number().default(15000),
|
|
42
31
|
});
|
|
43
|
-
const
|
|
44
|
-
const messageQueue = new Map();
|
|
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
|
-
}
|
|
32
|
+
const processed = new Map();
|
|
65
33
|
function apply(ctx, config) {
|
|
66
|
-
const request = axios_1.default.create({
|
|
67
|
-
|
|
68
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
69
|
-
},
|
|
70
|
-
timeout: config.suyanApi.timeout,
|
|
71
|
-
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
|
|
72
|
-
});
|
|
73
|
-
async function parseWithSuyanApi(url, platform) {
|
|
34
|
+
const request = axios_1.default.create({ timeout: config.timeout });
|
|
35
|
+
async function parse(url) {
|
|
74
36
|
try {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
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);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
async function parseVideo(url, session) {
|
|
104
|
-
if (!config.enable)
|
|
105
|
-
return;
|
|
106
|
-
const linkHash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
107
|
-
const now = Date.now();
|
|
108
|
-
if (processedLinks.has(linkHash) && now - processedLinks.get(linkHash) < config.sameLinkInterval * 1000) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
processedLinks.set(linkHash, now);
|
|
112
|
-
if (config.showWaitingTip)
|
|
113
|
-
await session.send(config.waitingTipText);
|
|
114
|
-
let platform = 'bilibili';
|
|
115
|
-
if (url.includes('douyin') || url.includes('dy')) {
|
|
116
|
-
platform = 'douyin';
|
|
117
|
-
}
|
|
118
|
-
else if (url.includes('kuaishou') || url.includes('ks')) {
|
|
119
|
-
platform = 'kuaishou';
|
|
37
|
+
const res = await request.get(config.api1, { params: { url } });
|
|
38
|
+
const d = res.data.data?.aweme_detail || res.data.data;
|
|
39
|
+
if (d?.video?.play_addr?.url_list?.[0])
|
|
40
|
+
return pack(d);
|
|
120
41
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
42
|
+
catch { }
|
|
43
|
+
try {
|
|
44
|
+
const res = await request.get(config.api2, { params: { url } });
|
|
45
|
+
const d = res.data.data;
|
|
46
|
+
if (d?.video_url) {
|
|
47
|
+
return {
|
|
48
|
+
title: d.title || '',
|
|
49
|
+
author: '未知作者',
|
|
50
|
+
desc: d.title || '',
|
|
51
|
+
digg: 0, comment: 0, collect: 0, share: 0, play: 0,
|
|
52
|
+
cover: d.cover_url || '',
|
|
53
|
+
video: d.video_url || ''
|
|
54
|
+
};
|
|
132
55
|
}
|
|
133
56
|
}
|
|
134
|
-
|
|
57
|
+
catch { }
|
|
58
|
+
return { title: '解析失败', author: '', desc: '', digg: 0, comment: 0, collect: 0, share: 0, play: 0, cover: '', video: '' };
|
|
135
59
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.replace(/\${收藏}/g, videoInfo.collect?.toString() || '0')
|
|
150
|
-
.replace(/\${转发}/g, videoInfo.share?.toString() || '0')
|
|
151
|
-
.replace(/\${观看}/g, videoInfo.view?.toString() || '0')
|
|
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());
|
|
163
|
-
}
|
|
164
|
-
// 单独发送封面(Element类型)或文字提示
|
|
165
|
-
if (videoInfo.cover) {
|
|
166
|
-
await session.send(koishi_1.h.image(videoInfo.cover));
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
await session.send('无封面');
|
|
170
|
-
}
|
|
171
|
-
// 发送封面之后的内容
|
|
172
|
-
if (afterCover && afterCover.trim()) {
|
|
173
|
-
await session.send(afterCover.trim());
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
// 没有封面占位符时直接发送全部内容
|
|
178
|
-
await session.send(content.trim());
|
|
179
|
-
}
|
|
180
|
-
// 显示图集/链接
|
|
181
|
-
if (config.showVideoLink) {
|
|
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
|
-
}
|
|
188
|
-
}
|
|
60
|
+
function pack(d) {
|
|
61
|
+
return {
|
|
62
|
+
title: d.desc || '',
|
|
63
|
+
author: d.author?.nickname || d.author?.unique_id || '',
|
|
64
|
+
desc: d.desc || '',
|
|
65
|
+
digg: d.statistics?.digg_count || 0,
|
|
66
|
+
comment: d.statistics?.comment_count || 0,
|
|
67
|
+
collect: d.statistics?.collect_count || 0,
|
|
68
|
+
share: d.statistics?.share_count || 0,
|
|
69
|
+
play: d.statistics?.play_count || 0,
|
|
70
|
+
cover: d.video?.cover || d.video?.dynamic_cover || '',
|
|
71
|
+
video: d.video?.play_addr?.url_list?.[0] || ''
|
|
72
|
+
};
|
|
189
73
|
}
|
|
190
|
-
async function
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
await session.send(`❌ 消息发送失败:${e.message}`);
|
|
74
|
+
async function send(session, data) {
|
|
75
|
+
let t = config.imageParseFormat;
|
|
76
|
+
t = t.replace(/\${标题}/g, data.title);
|
|
77
|
+
t = t.replace(/\${UP主}/g, data.author);
|
|
78
|
+
t = t.replace(/\${简介}/g, data.desc.slice(0, config.maxDescLength));
|
|
79
|
+
t = t.replace(/\${点赞}/g, data.digg.toString());
|
|
80
|
+
t = t.replace(/\${投币}/g, '0');
|
|
81
|
+
t = t.replace(/\${收藏}/g, data.collect.toString());
|
|
82
|
+
t = t.replace(/\${转发}/g, data.share.toString());
|
|
83
|
+
t = t.replace(/\${观看}/g, data.play.toString());
|
|
84
|
+
t = t.replace(/\${弹幕}/g, data.comment.toString());
|
|
85
|
+
t = t.replace(/\${tab}/g, '\t');
|
|
86
|
+
t = t.replace(/\${~~~}/g, '——————————————');
|
|
87
|
+
const [a, b] = t.split('\${封面}');
|
|
88
|
+
if (a)
|
|
89
|
+
await session.send(a);
|
|
90
|
+
if (data.cover)
|
|
91
|
+
await session.send(koishi_1.h.image(data.cover));
|
|
92
|
+
if (b)
|
|
93
|
+
await session.send(b);
|
|
94
|
+
if (data.video) {
|
|
95
|
+
await session.send(koishi_1.h.video(data.video));
|
|
96
|
+
if (config.showVideoUrl)
|
|
97
|
+
await session.send(data.video);
|
|
215
98
|
}
|
|
216
99
|
}
|
|
217
100
|
ctx.on('message', async (session) => {
|
|
218
101
|
if (!config.enable)
|
|
219
102
|
return;
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
const matches = [...content.matchAll(reg)];
|
|
223
|
-
if (!matches.length)
|
|
103
|
+
const url = session.content.trim();
|
|
104
|
+
if (!url.startsWith('http'))
|
|
224
105
|
return;
|
|
225
|
-
const
|
|
226
|
-
if (config.messageBufferDelay > 0) {
|
|
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(() => {
|
|
106
|
+
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
244
107
|
const now = Date.now();
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
108
|
+
if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000)
|
|
109
|
+
return;
|
|
110
|
+
processed.set(hash, now);
|
|
111
|
+
if (config.showWaitingTip)
|
|
112
|
+
await session.send(config.waitingTipText);
|
|
113
|
+
const data = await parse(url);
|
|
114
|
+
await send(session, data);
|
|
115
|
+
});
|
|
116
|
+
ctx.logger.info('✅ 双API备用抖音解析加载完成');
|
|
251
117
|
}
|