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 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
- showVideoLink: boolean;
9
+ showVideoUrl: boolean;
17
10
  maxDescLength: number;
18
- enableMergeForward: boolean;
19
- downloadBeforeSend: boolean;
20
- messageBufferDelay: number;
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).description('开启解析功能'),
14
- showWaitingTip: koishi_1.Schema.boolean().default(true).description('是否返回等待提示'),
15
- waitingTipText: koishi_1.Schema.string().default('正在解析视频/图集链接...').description('等待提示文字内容'),
16
- allowBVAVParse: koishi_1.Schema.boolean().default(true).description('允许BV/AV号解析(B站备用)'),
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
- .description('图文解析格式(严格按你的要求配置)'),
34
- showVideoLink: koishi_1.Schema.boolean().default(true).description('显示解析后的链接/图集列表'),
35
- maxDescLength: koishi_1.Schema.number().default(200).description('简介最大长度'),
36
- enableMergeForward: koishi_1.Schema.boolean().default(false).description('合并转发(仅onebot)'),
37
- downloadBeforeSend: koishi_1.Schema.boolean().default(false).description('下载后发送(避免链接失效)'),
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 processedLinks = new Map();
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
- headers: {
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 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);
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
- const videoInfo = platform === 'douyin' || platform === 'kuaishou'
122
- ? await parseWithSuyanApi(url, platform)
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);
129
- }
130
- if (duration > config.maxVideoDuration) {
131
- return config.longVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(config.longVideoTip);
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
- await generateReply(videoInfo, session, platform);
57
+ catch { }
58
+ return { title: '解析失败', author: '', desc: '', digg: 0, comment: 0, collect: 0, share: 0, play: 0, cover: '', video: '' };
135
59
  }
136
- // 核心修复:拆分封面处理逻辑,避免replace接收非字符串类型
137
- async function generateImageParse(videoInfo, session, platform) {
138
- let content = config.imageParseFormat;
139
- const desc = videoInfo.description.length > config.maxDescLength
140
- ? videoInfo.description.slice(0, config.maxDescLength) + '...'
141
- : videoInfo.description;
142
- // 第一步:只替换字符串类型的变量(封面先标记为占位符)
143
- content = content
144
- .replace(/\${标题}/g, videoInfo.title || '')
145
- .replace(/\${UP主}/g, videoInfo.author || '')
146
- .replace(/\${简介}/g, desc || '')
147
- .replace(/\${点赞}/g, videoInfo.like?.toString() || '0')
148
- .replace(/\${投币}/g, videoInfo.coin?.toString() || '0')
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 generateReply(videoInfo, session, platform) {
191
- try {
192
- if (config.enableMergeForward) {
193
- const msgs = [];
194
- msgs.push(koishi_1.h.text(`${videoInfo.title} \t\t ${videoInfo.author}`));
195
- msgs.push(koishi_1.h.text(videoInfo.description));
196
- msgs.push(koishi_1.h.text(`点赞:${videoInfo.like} \t\t 投币:${videoInfo.coin}`));
197
- msgs.push(koishi_1.h.text(`收藏:${videoInfo.collect} \t\t 转发:${videoInfo.share}`));
198
- msgs.push(koishi_1.h.text(`观看:${videoInfo.view} \t\t 弹幕:${videoInfo.danmaku}`));
199
- if (videoInfo.cover)
200
- msgs.push(koishi_1.h.image(videoInfo.cover));
201
- if (videoInfo.images.length > 0) {
202
- msgs.push(koishi_1.h.text(`📁 图集共${videoInfo.images.length}张`));
203
- }
204
- else {
205
- msgs.push(koishi_1.h.text(`📥 原始链接:${videoInfo.url}`));
206
- }
207
- await session.send(msgs);
208
- }
209
- else {
210
- await generateImageParse(videoInfo, session, platform);
211
- }
212
- }
213
- catch (e) {
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 content = session.content.trim();
221
- const reg = /(https?:\/\/\S+)|(BV\w+)|(AV\d+)/gi;
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 uid = session.userId;
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
- for (const [k, t] of processedLinks) {
246
- if (now - t > 86400000)
247
- processedLinks.delete(k);
248
- }
249
- }, 3600000);
250
- ctx.logger.info('✅ 视频解析插件已启动(精准适配素颜API)');
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
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
3
  "description": "Koishi 视频解析插件,支持抖音/快手/B站链接解析,可自定义API和解析规则",
4
- "version": "0.0.5",
4
+ "version": "0.0.6",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [