koishi-plugin-video-parser-all 0.1.7 → 0.1.8
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.js +263 -156
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
13
|
const promises_1 = require("stream/promises");
|
|
14
|
+
const worker_threads_1 = require("worker_threads");
|
|
14
15
|
exports.name = 'video-parser-all';
|
|
15
16
|
exports.Config = koishi_1.Schema.object({
|
|
16
17
|
enable: koishi_1.Schema.boolean().default(true).description('是否启用插件'),
|
|
@@ -62,9 +63,33 @@ exports.Config = koishi_1.Schema.object({
|
|
|
62
63
|
customApi: koishi_1.Schema.string().description('B站自定义API'),
|
|
63
64
|
}).description('B站配置'),
|
|
64
65
|
});
|
|
66
|
+
// 工作线程处理下载逻辑
|
|
67
|
+
if (!worker_threads_1.isMainThread) {
|
|
68
|
+
const { url, filePath } = worker_threads_1.workerData;
|
|
69
|
+
(async () => {
|
|
70
|
+
try {
|
|
71
|
+
const response = await (0, axios_1.default)({
|
|
72
|
+
url,
|
|
73
|
+
method: 'GET',
|
|
74
|
+
responseType: 'stream',
|
|
75
|
+
timeout: 60000,
|
|
76
|
+
headers: {
|
|
77
|
+
'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'
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
await (0, promises_1.pipeline)(response.data, fs_1.default.createWriteStream(filePath));
|
|
81
|
+
worker_threads_1.parentPort?.postMessage({ success: true, filePath });
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
worker_threads_1.parentPort?.postMessage({
|
|
85
|
+
success: false,
|
|
86
|
+
error: error.message
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
}
|
|
65
91
|
const processed = new Map();
|
|
66
92
|
const linkBuffer = new Map();
|
|
67
|
-
const parseCostHistory = new Map();
|
|
68
93
|
const PLATFORM_KEYWORDS = {
|
|
69
94
|
bilibili: ['bilibili', 'b23', 'B站'],
|
|
70
95
|
kuaishou: ['kuaishou', '快手'],
|
|
@@ -91,200 +116,282 @@ function getPlatformType(url) {
|
|
|
91
116
|
return 'bilibili';
|
|
92
117
|
return null;
|
|
93
118
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
119
|
+
// 多线程下载视频
|
|
120
|
+
async function downloadVideoWithThreads(url, filename) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const dir = path_1.default.join(process.cwd(), 'temp_videos');
|
|
123
|
+
if (!fs_1.default.existsSync(dir))
|
|
124
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
125
|
+
const filePath = path_1.default.join(dir, `${filename}.mp4`);
|
|
126
|
+
const worker = new worker_threads_1.Worker(__filename, {
|
|
127
|
+
workerData: { url, filePath }
|
|
128
|
+
});
|
|
129
|
+
worker.on('message', (result) => {
|
|
130
|
+
if (result.success) {
|
|
131
|
+
resolve(result.filePath);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
reject(new Error(result.error));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
worker.on('error', (error) => {
|
|
138
|
+
reject(error);
|
|
139
|
+
});
|
|
140
|
+
worker.on('exit', (code) => {
|
|
141
|
+
if (code !== 0) {
|
|
142
|
+
reject(new Error(`下载线程退出,代码: ${code}`));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
102
146
|
}
|
|
103
|
-
function parseData(data, platform) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
147
|
+
function parseData(data, platform, maxDescLength) {
|
|
148
|
+
let title = data.title || '无标题';
|
|
149
|
+
let author = data.author?.name || data.user?.name || data.author || '未知作者';
|
|
150
|
+
let desc = data.desc || data.description || title || '无简介';
|
|
151
|
+
desc = desc.slice(0, maxDescLength);
|
|
152
|
+
let digg = data.like || data.digg || 0;
|
|
153
|
+
let coin = data.coin || 0;
|
|
154
|
+
let collect = data.collect || data.favorite || 0;
|
|
155
|
+
let share = data.share || 0;
|
|
156
|
+
let play = data.play || data.view || 0;
|
|
157
|
+
let danmaku = data.danmaku || data.comment || 0;
|
|
158
|
+
let cover = data.cover || data.imgurl || data.pic || '';
|
|
159
|
+
let video = '';
|
|
160
|
+
if (data.url)
|
|
161
|
+
video = data.url;
|
|
162
|
+
else if (data.video_url)
|
|
163
|
+
video = data.video_url;
|
|
164
|
+
else if (data.videos && Array.isArray(data.videos) && data.videos.length > 0) {
|
|
165
|
+
video = data.videos.reduce((prev, curr) => (prev.size || 0) > (curr.size || 0) ? prev : curr).url || '';
|
|
118
166
|
}
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
}
|
|
134
|
-
return {
|
|
135
|
-
title: data.title || '无标题',
|
|
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 || ''
|
|
146
|
-
};
|
|
167
|
+
else if (data.play)
|
|
168
|
+
video = data.play;
|
|
169
|
+
else if (data.hd_url)
|
|
170
|
+
video = data.hd_url;
|
|
171
|
+
return { title, author, desc, digg, coin, collect, share, play, danmaku, cover, video };
|
|
147
172
|
}
|
|
148
173
|
function apply(ctx, config) {
|
|
149
|
-
|
|
174
|
+
if (!worker_threads_1.isMainThread)
|
|
175
|
+
return;
|
|
176
|
+
const http = axios_1.default.create({
|
|
177
|
+
timeout: config.timeout,
|
|
178
|
+
headers: {
|
|
179
|
+
'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'
|
|
180
|
+
}
|
|
181
|
+
});
|
|
150
182
|
function getApi(platform) {
|
|
151
183
|
const conf = config[platform];
|
|
152
|
-
if (!conf)
|
|
153
|
-
return config.commonApi;
|
|
154
184
|
if (conf.mode === 'custom' && conf.customApi)
|
|
155
185
|
return conf.customApi;
|
|
156
186
|
if (conf.mode === 'own')
|
|
157
187
|
return conf.ownApi;
|
|
158
188
|
return config.commonApi;
|
|
159
189
|
}
|
|
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
|
-
}
|
|
179
190
|
async function parse(url) {
|
|
180
|
-
const start = Date.now();
|
|
181
191
|
const platform = getPlatformType(url);
|
|
182
192
|
if (!platform)
|
|
183
|
-
return { data: null,
|
|
193
|
+
return { data: null, platform: null };
|
|
184
194
|
const api = getApi(platform);
|
|
185
195
|
if (!api)
|
|
186
|
-
return { data: null,
|
|
196
|
+
return { data: null, platform: null };
|
|
187
197
|
try {
|
|
188
198
|
const res = await http.get(api, { params: { url } });
|
|
189
199
|
if (res.data.code === 200 && res.data.data) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
return {
|
|
201
|
+
data: parseData(res.data.data, platform, config.maxDescLength),
|
|
202
|
+
platform
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
ctx.logger.error(`解析失败: ${e.message}`);
|
|
208
|
+
}
|
|
209
|
+
return { data: null, platform: null };
|
|
210
|
+
}
|
|
211
|
+
async function processSingleUrl(session, url) {
|
|
212
|
+
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
processed.set(hash, now);
|
|
218
|
+
const parseResult = await parse(url);
|
|
219
|
+
if (!parseResult.data)
|
|
220
|
+
return null;
|
|
221
|
+
let text = config.imageParseFormat
|
|
222
|
+
.replace(/\${标题}/g, parseResult.data.title)
|
|
223
|
+
.replace(/\${UP主}/g, parseResult.data.author)
|
|
224
|
+
.replace(/\${简介}/g, parseResult.data.desc)
|
|
225
|
+
.replace(/\${点赞}/g, String(parseResult.data.digg))
|
|
226
|
+
.replace(/\${投币}/g, String(parseResult.data.coin))
|
|
227
|
+
.replace(/\${收藏}/g, String(parseResult.data.collect))
|
|
228
|
+
.replace(/\${转发}/g, String(parseResult.data.share))
|
|
229
|
+
.replace(/\${观看}/g, String(parseResult.data.play))
|
|
230
|
+
.replace(/\${弹幕}/g, String(parseResult.data.danmaku))
|
|
231
|
+
.replace(/\${tab}/g, '\t')
|
|
232
|
+
.replace(/\${~~~}/g, '\n');
|
|
233
|
+
const contentParts = [];
|
|
234
|
+
if (config.returnContent.showImageText) {
|
|
235
|
+
const [beforeCover, afterCover] = text.split('${封面}');
|
|
236
|
+
if (beforeCover && beforeCover.trim())
|
|
237
|
+
contentParts.push(beforeCover.trim());
|
|
238
|
+
if (parseResult.data.cover)
|
|
239
|
+
contentParts.push(koishi_1.h.image(parseResult.data.cover));
|
|
240
|
+
if (afterCover && afterCover.trim())
|
|
241
|
+
contentParts.push(afterCover.trim());
|
|
242
|
+
}
|
|
243
|
+
let videoContent = '';
|
|
244
|
+
if (config.returnContent.showVideoFile && parseResult.data.video) {
|
|
245
|
+
if (config.downloadVideoBeforeSend && session.platform === 'onebot') {
|
|
246
|
+
try {
|
|
247
|
+
const filename = crypto_1.default.createHash('md5').update(parseResult.data.video).digest('hex');
|
|
248
|
+
const filePath = await downloadVideoWithThreads(parseResult.data.video, filename);
|
|
249
|
+
videoContent = koishi_1.h.video(`file://${filePath}`);
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
ctx.logger.error(`下载视频失败: ${e.message}`);
|
|
253
|
+
videoContent = koishi_1.h.video(parseResult.data.video);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
videoContent = koishi_1.h.video(parseResult.data.video);
|
|
197
258
|
}
|
|
198
259
|
}
|
|
199
|
-
|
|
200
|
-
|
|
260
|
+
if (config.returnContent.showVideoUrl && parseResult.data.video) {
|
|
261
|
+
contentParts.push(`🔗 无水印链接:${parseResult.data.video}`);
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
textContent: contentParts.join('\n'),
|
|
265
|
+
videoContent,
|
|
266
|
+
rawData: parseResult.data
|
|
267
|
+
};
|
|
201
268
|
}
|
|
202
|
-
async function
|
|
203
|
-
if (!config.revokeWaitingTip)
|
|
269
|
+
async function revokeWaitingTip(session, sessionKey) {
|
|
270
|
+
if (!config.revokeWaitingTip || session.platform !== 'onebot')
|
|
204
271
|
return;
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
|
|
272
|
+
const bufferData = linkBuffer.get(sessionKey);
|
|
273
|
+
if (!bufferData?.tipMsgId)
|
|
274
|
+
return;
|
|
275
|
+
try {
|
|
276
|
+
await session.bot.internal.deleteMessage(session.channelId, bufferData.tipMsgId);
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
ctx.logger.debug(`撤回等待提示失败: ${e.message}`);
|
|
208
280
|
}
|
|
209
281
|
}
|
|
210
|
-
async function
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
if (!
|
|
282
|
+
async function flushBuffer(session) {
|
|
283
|
+
const sessionKey = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
284
|
+
const bufferData = linkBuffer.get(sessionKey);
|
|
285
|
+
if (!bufferData)
|
|
214
286
|
return;
|
|
215
|
-
clearTimeout(
|
|
216
|
-
linkBuffer.delete(
|
|
217
|
-
await
|
|
218
|
-
const urls = buf.urls;
|
|
219
|
-
const estimated = await calculateRealEstimatedTime(urls, session);
|
|
220
|
-
let tipText = config.waitingTipText;
|
|
221
|
-
if (estimated > 0)
|
|
222
|
-
tipText += ` 预计${estimated}秒后完成`;
|
|
287
|
+
clearTimeout(bufferData.timer);
|
|
288
|
+
linkBuffer.delete(sessionKey);
|
|
289
|
+
await revokeWaitingTip(session, sessionKey);
|
|
223
290
|
const results = [];
|
|
224
|
-
for (const url of urls) {
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
processed.set(hash, Date.now());
|
|
229
|
-
const { data, cost } = await parse(url);
|
|
230
|
-
if (data)
|
|
231
|
-
results.push({ data, cost });
|
|
291
|
+
for (const url of bufferData.urls) {
|
|
292
|
+
const result = await processSingleUrl(session, url);
|
|
293
|
+
if (result)
|
|
294
|
+
results.push(result);
|
|
232
295
|
}
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
296
|
+
if (results.length === 0)
|
|
297
|
+
return;
|
|
298
|
+
if (config.enableForward && session.platform === 'onebot') {
|
|
299
|
+
const forwardNodes = results.map(result => {
|
|
300
|
+
const nodeContent = [];
|
|
301
|
+
if (result.textContent)
|
|
302
|
+
nodeContent.push(result.textContent);
|
|
303
|
+
if (result.videoContent)
|
|
304
|
+
nodeContent.push(result.videoContent);
|
|
305
|
+
return {
|
|
306
|
+
type: 'node',
|
|
307
|
+
data: {
|
|
308
|
+
name: '视频解析机器人',
|
|
309
|
+
uin: session.selfId,
|
|
310
|
+
content: nodeContent
|
|
311
|
+
}
|
|
312
|
+
};
|
|
245
313
|
});
|
|
314
|
+
try {
|
|
315
|
+
await session.send((0, koishi_1.h)('message', {
|
|
316
|
+
type: 'forward',
|
|
317
|
+
data: {
|
|
318
|
+
messages: forwardNodes
|
|
319
|
+
}
|
|
320
|
+
}));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
ctx.logger.error(`合并转发失败: ${e.message}`);
|
|
325
|
+
}
|
|
246
326
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
327
|
+
for (const result of results) {
|
|
328
|
+
try {
|
|
329
|
+
if (result.textContent)
|
|
330
|
+
await session.send(result.textContent);
|
|
331
|
+
if (result.videoContent)
|
|
332
|
+
await session.send(result.videoContent);
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
if (!config.ignoreSendError) {
|
|
336
|
+
ctx.logger.warn(`发送消息失败: ${e.message}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
253
339
|
}
|
|
254
340
|
}
|
|
255
341
|
ctx.on('message', async (session) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
342
|
+
try {
|
|
343
|
+
if (!config.enable)
|
|
344
|
+
return;
|
|
345
|
+
const content = session.content.trim();
|
|
346
|
+
if (!hasPlatformKeyword(content))
|
|
347
|
+
return;
|
|
348
|
+
const urls = extractUrl(content);
|
|
349
|
+
if (urls.length === 0)
|
|
350
|
+
return;
|
|
351
|
+
const sessionKey = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
352
|
+
if (linkBuffer.has(sessionKey)) {
|
|
353
|
+
const existing = linkBuffer.get(sessionKey);
|
|
354
|
+
existing.urls.push(...urls);
|
|
355
|
+
clearTimeout(existing.timer);
|
|
356
|
+
existing.timer = setTimeout(() => flushBuffer(session), config.messageBufferDelay * 1000);
|
|
357
|
+
linkBuffer.set(sessionKey, existing);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
let tipMsgId;
|
|
361
|
+
if (config.showWaitingTip) {
|
|
362
|
+
const tipText = config.waitingTipText;
|
|
363
|
+
const tipMsg = await session.send(tipText).catch(e => {
|
|
364
|
+
if (!config.ignoreSendError)
|
|
365
|
+
ctx.logger.warn(`发送等待提示失败: ${e.message}`);
|
|
366
|
+
return null;
|
|
367
|
+
});
|
|
368
|
+
tipMsgId = tipMsg?.messageId || tipMsg?.id;
|
|
369
|
+
}
|
|
370
|
+
linkBuffer.set(sessionKey, {
|
|
273
371
|
urls,
|
|
274
|
-
timer: setTimeout(() =>
|
|
275
|
-
tipMsgId
|
|
372
|
+
timer: setTimeout(() => flushBuffer(session), config.messageBufferDelay * 1000),
|
|
373
|
+
tipMsgId
|
|
276
374
|
});
|
|
277
375
|
}
|
|
278
|
-
|
|
376
|
+
catch (e) {
|
|
377
|
+
ctx.logger.error(`消息处理异常: ${e.message}`);
|
|
378
|
+
}
|
|
279
379
|
});
|
|
280
380
|
setInterval(() => {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
381
|
+
const now = Date.now();
|
|
382
|
+
processed.forEach((timestamp, hash) => {
|
|
383
|
+
if (now - timestamp > 86400000) {
|
|
384
|
+
processed.delete(hash);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
const tempDir = path_1.default.join(process.cwd(), 'temp_videos');
|
|
388
|
+
if (fs_1.default.existsSync(tempDir)) {
|
|
389
|
+
fs_1.default.readdirSync(tempDir).forEach(file => {
|
|
390
|
+
const filePath = path_1.default.join(tempDir, file);
|
|
391
|
+
const stat = fs_1.default.statSync(filePath);
|
|
392
|
+
if (now - stat.ctimeMs > 3600000) {
|
|
393
|
+
fs_1.default.unlinkSync(filePath);
|
|
394
|
+
}
|
|
288
395
|
});
|
|
289
396
|
}
|
|
290
397
|
}, 3600000);
|