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.
Files changed (3) hide show
  1. package/lib/index.d.ts +6 -14
  2. package/lib/index.js +150 -202
  3. 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
- 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
+ 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('正在解析视频/图集链接...').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('过长视频用图文解析'),
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
- showVideoLink: koishi_1.Schema.boolean().default(true).description('显示解析后的链接/图集列表'),
26
+ .description('解析输出格式(请勿修改占位符)'),
27
+ showVideoUrl: koishi_1.Schema.boolean().default(false).description('额外显示无水印视频链接'),
35
28
  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配置(无需密钥,直接使用)')
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
- 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
- }
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 (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) {
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
- 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';
120
- }
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);
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
- if (duration > config.maxVideoDuration) {
131
- return config.longVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(config.longVideoTip);
66
+ catch (e) {
67
+ ctx.logger.warn(`B站主API解析失败,尝试备用API: ${e.message}`);
132
68
  }
133
69
  }
134
- await generateReply(videoInfo, session, platform);
135
- }
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());
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
- // 单独发送封面(Element类型)或文字提示
165
- if (videoInfo.cover) {
166
- await session.send(koishi_1.h.image(videoInfo.cover));
89
+ catch (e) {
90
+ ctx.logger.warn(`快手主API解析失败,尝试备用API: ${e.message}`);
167
91
  }
168
- else {
169
- await session.send('无封面');
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
- if (afterCover && afterCover.trim()) {
173
- await session.send(afterCover.trim());
112
+ catch (e) {
113
+ ctx.logger.warn(`抖音主API解析失败,尝试备用API: ${e.message}`);
174
114
  }
175
115
  }
176
- else {
177
- // 没有封面占位符时直接发送全部内容
178
- await session.send(content.trim());
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
- 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
- }
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
- 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);
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
- else {
210
- await generateImageParse(videoInfo, session, platform);
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 content = session.content.trim();
221
- const reg = /(https?:\/\/\S+)|(BV\w+)|(AV\d+)/gi;
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
- 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(() => {
185
+ // 去重逻辑
186
+ const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
244
187
  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)');
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
  }
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.7",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [