koishi-plugin-video-parser-all 0.1.6 → 0.1.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 CHANGED
@@ -3,7 +3,6 @@ export declare const name = "video-parser-all";
3
3
  export interface Config {
4
4
  enable: boolean;
5
5
  showWaitingTip: boolean;
6
- showEstimatedTime: boolean;
7
6
  revokeWaitingTip: boolean;
8
7
  waitingTipText: string;
9
8
  sameLinkInterval: number;
package/lib/index.js CHANGED
@@ -15,7 +15,6 @@ exports.name = 'video-parser-all';
15
15
  exports.Config = koishi_1.Schema.object({
16
16
  enable: koishi_1.Schema.boolean().default(true).description('是否启用插件'),
17
17
  showWaitingTip: koishi_1.Schema.boolean().default(true).description('是否显示解析等待提示'),
18
- showEstimatedTime: koishi_1.Schema.boolean().default(true).description('是否显示预计解析完成时间'),
19
18
  revokeWaitingTip: koishi_1.Schema.boolean().default(true).description('是否撤回等待提示文本'),
20
19
  waitingTipText: koishi_1.Schema.string().default('正在解析视频…').description('等待提示文本'),
21
20
  sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接重复间隔秒'),
@@ -26,21 +25,21 @@ exports.Config = koishi_1.Schema.object({
26
25
  returnContent: koishi_1.Schema.object({
27
26
  showImageText: koishi_1.Schema.boolean().default(true).description('返回图文'),
28
27
  showVideoUrl: koishi_1.Schema.boolean().default(false).description('返回视频直链'),
29
- showVideoFile: koishi_1.Schema.boolean().default(true).description('返回视频'),
28
+ showVideoFile: koishi_1.Schema.boolean().default(true).description('返回视频')
30
29
  }).description('内容组件'),
31
30
  maxDescLength: koishi_1.Schema.number().default(200).description('简介最大长度'),
32
31
  timeout: koishi_1.Schema.number().default(15000).description('请求超时毫秒'),
33
32
  ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送错误'),
34
33
  enableForward: koishi_1.Schema.boolean().default(false).description('合并转发(仅onebot)'),
35
34
  downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('先下载视频再发送(解决onebot问题)'),
36
- messageBufferDelay: koishi_1.Schema.number().default(1).description('消息接收缓冲延迟秒'),
35
+ messageBufferDelay: koishi_1.Schema.number().default(1).description('消息缓冲延迟秒'),
37
36
  commonApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('通用解析API'),
38
37
  douyin: koishi_1.Schema.object({
39
38
  mode: koishi_1.Schema.union([
40
39
  koishi_1.Schema.const('common').description('使用通用API'),
41
40
  koishi_1.Schema.const('own').description('使用平台专属API'),
42
41
  koishi_1.Schema.const('custom').description('使用自定义API'),
43
- ]).default('common').description('解析模式'),
42
+ ]).default('common').description('抖音解析模式'),
44
43
  ownApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/douyin').description('抖音专属API'),
45
44
  customApi: koishi_1.Schema.string().description('抖音自定义API'),
46
45
  }).description('抖音配置'),
@@ -49,7 +48,7 @@ exports.Config = koishi_1.Schema.object({
49
48
  koishi_1.Schema.const('common').description('使用通用API'),
50
49
  koishi_1.Schema.const('own').description('使用平台专属API'),
51
50
  koishi_1.Schema.const('custom').description('使用自定义API'),
52
- ]).default('common').description('解析模式'),
51
+ ]).default('common').description('快手解析模式'),
53
52
  ownApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/ksjx').description('快手专属API'),
54
53
  customApi: koishi_1.Schema.string().description('快手自定义API'),
55
54
  }).description('快手配置'),
@@ -58,18 +57,18 @@ exports.Config = koishi_1.Schema.object({
58
57
  koishi_1.Schema.const('common').description('使用通用API'),
59
58
  koishi_1.Schema.const('own').description('使用平台专属API'),
60
59
  koishi_1.Schema.const('custom').description('使用自定义API'),
61
- ]).default('common').description('解析模式'),
60
+ ]).default('common').description('B站解析模式'),
62
61
  ownApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/bilibili').description('B站专属API'),
63
62
  customApi: koishi_1.Schema.string().description('B站自定义API'),
64
63
  }).description('B站配置'),
65
64
  });
66
65
  const processed = new Map();
67
66
  const linkBuffer = new Map();
68
- const parseTimeRecords = new Map();
67
+ const parseCostHistory = new Map();
69
68
  const PLATFORM_KEYWORDS = {
70
69
  bilibili: ['bilibili', 'b23', 'B站'],
71
70
  kuaishou: ['kuaishou', '快手'],
72
- douyin: ['douyin', '抖音'],
71
+ douyin: ['douyin', '抖音']
73
72
  };
74
73
  function extractUrl(content) {
75
74
  const urlMatches = content.match(/https?:\/\/[^\s]+/gi) || [];
@@ -92,33 +91,6 @@ function getPlatformType(url) {
92
91
  return 'bilibili';
93
92
  return null;
94
93
  }
95
- function formatTime(date) {
96
- const hours = date.getHours().toString().padStart(2, '0');
97
- const minutes = date.getMinutes().toString().padStart(2, '0');
98
- const seconds = date.getSeconds().toString().padStart(2, '0');
99
- return `${hours}:${minutes}:${seconds}`;
100
- }
101
- function getEstimatedParseTime(urls) {
102
- let totalSeconds = 0;
103
- urls.forEach(url => {
104
- const platform = getPlatformType(url);
105
- if (platform) {
106
- const records = parseTimeRecords.get(platform) || [];
107
- if (records.length > 0) {
108
- const avgTime = records.reduce((sum, time) => sum + time, 0) / records.length;
109
- totalSeconds += avgTime / 1000;
110
- }
111
- else {
112
- totalSeconds += 3;
113
- }
114
- }
115
- else {
116
- totalSeconds += 3;
117
- }
118
- });
119
- totalSeconds += 1;
120
- return Math.ceil(totalSeconds);
121
- }
122
94
  async function downloadVideo(url, filename) {
123
95
  const dir = path_1.default.join(process.cwd(), 'temp_videos');
124
96
  if (!fs_1.default.existsSync(dir))
@@ -128,116 +100,112 @@ async function downloadVideo(url, filename) {
128
100
  await (0, promises_1.pipeline)(res.data, fs_1.default.createWriteStream(file));
129
101
  return file;
130
102
  }
131
- function parseData(data) {
103
+ function parseData(data, platform) {
104
+ if (platform === 'bilibili') {
105
+ return {
106
+ title: data.title || '无标题',
107
+ author: data.user?.name || data.author || '未知UP主',
108
+ desc: data.desc || data.description || data.title || '无简介',
109
+ digg: data.like || data.digg || 0,
110
+ coin: data.coin || 0,
111
+ collect: data.collect || data.favorite || 0,
112
+ share: data.share || 0,
113
+ play: data.play || data.view || 0,
114
+ danmaku: data.danmaku || data.comment || 0,
115
+ cover: data.cover || data.imgurl || data.pic || '',
116
+ video: data.url || data.video_url || (data.videos?.[0]?.url || '')
117
+ };
118
+ }
119
+ if (platform === 'douyin') {
120
+ return {
121
+ title: data.title || '无标题',
122
+ author: data.author?.name || '未知作者',
123
+ desc: data.desc || data.title || '无简介',
124
+ digg: data.like || data.digg || 0,
125
+ coin: 0,
126
+ collect: 0,
127
+ share: 0,
128
+ play: 0,
129
+ danmaku: 0,
130
+ cover: data.cover || '',
131
+ video: data.url || (data.live_photo?.[0]?.video || '')
132
+ };
133
+ }
132
134
  return {
133
135
  title: data.title || '无标题',
134
- author: data.author || data.user?.name || '未知作者',
135
- desc: (data.desc || data.title || '无简介').slice(0, 200),
136
- digg: data.like || data.digg || 0,
137
- coin: data.coin || 0,
138
- collect: data.collect || data.favorite || 0,
139
- share: data.share || 0,
140
- play: data.play || data.view || 0,
141
- danmaku: data.danmaku || data.comment || 0,
142
- cover: data.cover || data.imgurl || data.pic || '',
143
- video: data.url || data.video_url || data.play || '',
136
+ author: data.author || '未知作者',
137
+ desc: data.desc || data.title || '无简介',
138
+ digg: data.like || 0,
139
+ coin: 0,
140
+ collect: 0,
141
+ share: 0,
142
+ play: 0,
143
+ danmaku: 0,
144
+ cover: data.cover || '',
145
+ video: data.url || ''
144
146
  };
145
147
  }
146
148
  function apply(ctx, config) {
147
149
  const http = axios_1.default.create({ timeout: config.timeout });
148
150
  function getApi(platform) {
149
151
  const conf = config[platform];
152
+ if (!conf)
153
+ return config.commonApi;
150
154
  if (conf.mode === 'custom' && conf.customApi)
151
155
  return conf.customApi;
152
156
  if (conf.mode === 'own')
153
157
  return conf.ownApi;
154
158
  return config.commonApi;
155
159
  }
160
+ async function calculateRealEstimatedTime(urls, session) {
161
+ let totalTime = 0;
162
+ for (const url of urls) {
163
+ const platform = getPlatformType(url);
164
+ if (!platform)
165
+ continue;
166
+ const history = parseCostHistory.get(platform) || [];
167
+ let avgCost = 3000;
168
+ if (history.length > 0) {
169
+ avgCost = history.reduce((a, b) => a + b, 0) / history.length;
170
+ }
171
+ if (config.downloadVideoBeforeSend && platform === 'bilibili') {
172
+ avgCost += 2000;
173
+ }
174
+ totalTime += avgCost;
175
+ }
176
+ totalTime += config.messageBufferDelay * 1000;
177
+ return Math.ceil(totalTime / 1000);
178
+ }
156
179
  async function parse(url) {
157
- const startTime = Date.now();
158
- const pt = getPlatformType(url);
159
- if (!pt)
160
- return null;
161
- const api = getApi(pt);
180
+ const start = Date.now();
181
+ const platform = getPlatformType(url);
182
+ if (!platform)
183
+ return { data: null, cost: Date.now() - start };
184
+ const api = getApi(platform);
162
185
  if (!api)
163
- return null;
186
+ return { data: null, cost: Date.now() - start };
164
187
  try {
165
- const { data } = await http.get(api, { params: { url } });
166
- if (data.code === 200 && data.data) {
167
- const parseTime = Date.now() - startTime;
168
- const records = parseTimeRecords.get(pt) || [];
169
- records.push(parseTime);
170
- if (records.length > 10)
171
- records.shift();
172
- parseTimeRecords.set(pt, records);
173
- return parseData(data.data);
188
+ const res = await http.get(api, { params: { url } });
189
+ if (res.data.code === 200 && res.data.data) {
190
+ const cost = Date.now() - start;
191
+ const history = parseCostHistory.get(platform) || [];
192
+ if (history.length > 10)
193
+ history.shift();
194
+ history.push(cost);
195
+ parseCostHistory.set(platform, history);
196
+ return { data: parseData(res.data.data, platform), cost };
174
197
  }
175
198
  }
176
- catch { }
177
- return null;
199
+ catch (e) { }
200
+ return { data: null, cost: Date.now() - start };
178
201
  }
179
- async function revoke(session, key) {
202
+ async function revokeTip(session, key) {
180
203
  if (!config.revokeWaitingTip)
181
204
  return;
182
205
  const buf = linkBuffer.get(key);
183
- if (!buf?.tipMsgId)
184
- return;
185
- try {
186
- await session.bot.deleteMessage(session.channelId, buf.tipMsgId);
206
+ if (buf?.tipMsgId && session.platform === 'onebot') {
207
+ await session.bot.deleteMessage(session.channelId, buf.tipMsgId).catch(() => { });
187
208
  }
188
- catch { }
189
- }
190
- async function processOne(session, url) {
191
- const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
192
- const now = Date.now();
193
- if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000)
194
- return null;
195
- processed.set(hash, now);
196
- const data = await parse(url);
197
- if (!data)
198
- return null;
199
- let text = config.imageParseFormat
200
- .replace(/\${标题}/g, data.title)
201
- .replace(/\${UP主}/g, data.author)
202
- .replace(/\${简介}/g, data.desc)
203
- .replace(/\${点赞}/g, String(data.digg))
204
- .replace(/\${投币}/g, String(data.coin))
205
- .replace(/\${收藏}/g, String(data.collect))
206
- .replace(/\${转发}/g, String(data.share))
207
- .replace(/\${观看}/g, String(data.play))
208
- .replace(/\${弹幕}/g, String(data.danmaku))
209
- .replace(/\${tab}/g, '\t')
210
- .replace(/\${~~~}/g, '\n');
211
- const parts = [];
212
- if (config.returnContent.showImageText) {
213
- const [a, b] = text.split('${封面}');
214
- if (a?.trim())
215
- parts.push(a.trim());
216
- if (data.cover)
217
- parts.push(koishi_1.h.image(data.cover));
218
- if (b?.trim())
219
- parts.push(b.trim());
220
- }
221
- let videoPart = '';
222
- if (config.returnContent.showVideoFile && data.video && !config.returnContent.showVideoUrl) {
223
- if (config.downloadVideoBeforeSend) {
224
- try {
225
- const fn = crypto_1.default.createHash('md5').update(data.video).digest('hex');
226
- const fp = await downloadVideo(data.video, fn);
227
- videoPart = koishi_1.h.video(`file://${fp}`);
228
- }
229
- catch {
230
- videoPart = `视频:${data.video}`;
231
- }
232
- }
233
- else {
234
- videoPart = koishi_1.h.video(data.video);
235
- }
236
- }
237
- if (config.returnContent.showVideoUrl && data.video) {
238
- parts.push(`直链:${data.video}`);
239
- }
240
- return { content: parts.join('\n'), video: videoPart };
241
209
  }
242
210
  async function flush(session) {
243
211
  const key = `${session.platform}:${session.userId}:${session.channelId}`;
@@ -246,88 +214,79 @@ function apply(ctx, config) {
246
214
  return;
247
215
  clearTimeout(buf.timer);
248
216
  linkBuffer.delete(key);
249
- await revoke(session, key);
250
- const items = [];
251
- for (const u of buf.urls) {
252
- const it = await processOne(session, u);
253
- if (it)
254
- items.push(it);
217
+ await revokeTip(session, key);
218
+ const urls = buf.urls;
219
+ const estimated = await calculateRealEstimatedTime(urls, session);
220
+ let tipText = config.waitingTipText;
221
+ if (estimated > 0)
222
+ tipText += ` 预计${estimated}秒后完成`;
223
+ const results = [];
224
+ for (const url of urls) {
225
+ const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
226
+ if (processed.get(hash) && Date.now() - processed.get(hash) < config.sameLinkInterval * 1000)
227
+ continue;
228
+ processed.set(hash, Date.now());
229
+ const { data, cost } = await parse(url);
230
+ if (data)
231
+ results.push({ data, cost });
255
232
  }
256
- if (!items.length)
257
- return;
258
- if (config.enableForward && session.platform === 'onebot') {
259
- const nodeList = items.map(it => {
260
- const content = [];
261
- if (it.content)
262
- content.push(it.content);
263
- if (it.video)
264
- content.push(it.video);
265
- return {
266
- user_id: session.selfId,
267
- time: Math.floor(Date.now() / 1000),
268
- content: content.join('\n')
269
- };
233
+ if (config.enableForward && session.platform === 'onebot' && results.length > 0) {
234
+ const nodes = results.map(r => ({
235
+ user_id: session.selfId,
236
+ time: Math.floor(Date.now() / 1000),
237
+ content: [r.data.title, r.data.cover ? koishi_1.h.image(r.data.cover) : null, r.data.video ? koishi_1.h.video(r.data.video) : null].filter(Boolean).join('\n')
238
+ }));
239
+ await session.send((0, koishi_1.h)('forward', {
240
+ content: '群聊的聊天记录',
241
+ brief: '[聊天记录]',
242
+ data: nodes
243
+ })).catch(() => {
244
+ results.forEach(r => session.send(r.data.title).then(() => session.send(r.data.video)));
270
245
  });
271
- try {
272
- await session.send((0, koishi_1.h)('forward', {
273
- content: '群聊的聊天记录',
274
- brief: '[聊天记录]',
275
- data: nodeList
276
- }));
277
- return;
278
- }
279
- catch (e) {
280
- if (!config.ignoreSendError)
281
- ctx.logger.warn(`合并转发发送失败: ${e.message}`);
282
- }
283
246
  }
284
- for (const it of items) {
285
- if (it.content)
286
- await session.send(it.content).catch(() => { });
287
- if (it.video)
288
- await session.send(it.video).catch(() => { });
247
+ else {
248
+ results.forEach(r => {
249
+ session.send(r.data.title);
250
+ if (r.data.video)
251
+ session.send(r.data.video);
252
+ });
289
253
  }
290
254
  }
291
255
  ctx.on('message', async (session) => {
292
- if (!config.enable)
256
+ if (!config.enable || !hasPlatformKeyword(session.content))
293
257
  return;
294
- const content = session.content.trim();
295
- if (!hasPlatformKeyword(content))
296
- return;
297
- const urls = extractUrl(content);
258
+ const urls = extractUrl(session.content);
298
259
  if (!urls.length)
299
260
  return;
300
261
  const key = `${session.platform}:${session.userId}:${session.channelId}`;
301
- if (linkBuffer.has(key))
302
- return;
303
- let mid;
304
- if (config.showWaitingTip) {
262
+ if (linkBuffer.has(key)) {
263
+ linkBuffer.get(key).urls.push(...urls);
264
+ clearTimeout(linkBuffer.get(key).timer);
265
+ }
266
+ else {
267
+ const estimated = await calculateRealEstimatedTime(urls, session);
305
268
  let tipText = config.waitingTipText;
306
- if (config.showEstimatedTime) {
307
- const estimatedSeconds = getEstimatedParseTime(urls);
308
- const estimatedTime = new Date(Date.now() + estimatedSeconds * 1000);
309
- tipText += ` 预计${estimatedSeconds}秒后完成(${formatTime(estimatedTime)})`;
310
- }
311
- const tip = await session.send(tipText).catch(() => null);
312
- mid = tip?.messageId || tip?.id;
269
+ if (estimated > 0)
270
+ tipText += ` 预计${estimated}秒后完成`;
271
+ const tipMsg = await session.send(tipText).catch(() => null);
272
+ linkBuffer.set(key, {
273
+ urls,
274
+ timer: setTimeout(() => flush(session), config.messageBufferDelay * 1000),
275
+ tipMsgId: tipMsg?.messageId || tipMsg?.id
276
+ });
313
277
  }
314
- linkBuffer.set(key, {
315
- urls,
316
- timer: setTimeout(() => flush(session), config.messageBufferDelay * 1000),
317
- tipMsgId: mid,
318
- });
278
+ linkBuffer.get(key).timer = setTimeout(() => flush(session), config.messageBufferDelay * 1000);
319
279
  });
320
280
  setInterval(() => {
321
- const now = Date.now();
322
- processed.forEach((t, k) => now - t > 86400000 && processed.delete(k));
281
+ processed.forEach((t, k) => Date.now() - t > 86400000 && processed.delete(k));
323
282
  const dir = path_1.default.join(process.cwd(), 'temp_videos');
324
283
  if (fs_1.default.existsSync(dir)) {
325
284
  fs_1.default.readdirSync(dir).forEach(f => {
326
285
  const p = path_1.default.join(dir, f);
327
- if (now - fs_1.default.statSync(p).ctimeMs > 3600000)
286
+ if (Date.now() - fs_1.default.statSync(p).ctimeMs > 3600000)
328
287
  fs_1.default.unlinkSync(p);
329
288
  });
330
289
  }
331
290
  }, 3600000);
332
- ctx.logger.info('video-parser-all 加载完成');
291
+ ctx.logger.info('视频解析插件加载完成');
333
292
  }
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.1.6",
4
+ "version": "0.1.7",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [