koishi-plugin-video-parser-all 0.5.0 → 0.5.1
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 -0
- package/lib/index.js +165 -86
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare const Config: Schema<{
|
|
|
24
24
|
} & {
|
|
25
25
|
enableForward?: boolean | null | undefined;
|
|
26
26
|
downloadVideoBeforeSend?: boolean | null | undefined;
|
|
27
|
+
maxVideoSize?: number | null | undefined;
|
|
28
|
+
downloadThreads?: number | null | undefined;
|
|
27
29
|
} & {
|
|
28
30
|
messageBufferDelay?: number | null | undefined;
|
|
29
31
|
} & {
|
|
@@ -52,6 +54,8 @@ export declare const Config: Schema<{
|
|
|
52
54
|
} & {
|
|
53
55
|
enableForward: boolean;
|
|
54
56
|
downloadVideoBeforeSend: boolean;
|
|
57
|
+
maxVideoSize: number;
|
|
58
|
+
downloadThreads: number;
|
|
55
59
|
} & {
|
|
56
60
|
messageBufferDelay: number;
|
|
57
61
|
} & {
|
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.intersect([
|
|
16
17
|
koishi_1.Schema.object({
|
|
@@ -21,7 +22,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
21
22
|
sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
|
|
22
23
|
}).description('基础设置'),
|
|
23
24
|
koishi_1.Schema.object({
|
|
24
|
-
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}
|
|
25
|
+
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}').description('统一消息格式(B站会显示投币,其他平台自动隐藏;无法获取的变量会自动隐藏)'),
|
|
25
26
|
}).description('统一消息格式'),
|
|
26
27
|
koishi_1.Schema.object({
|
|
27
28
|
showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
|
|
@@ -43,13 +44,15 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
43
44
|
koishi_1.Schema.object({
|
|
44
45
|
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot平台)'),
|
|
45
46
|
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频(避免链接失效)'),
|
|
46
|
-
|
|
47
|
+
maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('最大视频大小限制(MB,0为不限制)'),
|
|
48
|
+
downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0为不使用多线程,1-10为启用对应线程数)'),
|
|
49
|
+
}).description('发送方式设置(说明:视频大小超过限制将只发送链接;多线程下载可提升速度但可能增加服务器负载)'),
|
|
47
50
|
koishi_1.Schema.object({
|
|
48
51
|
messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒,批量处理链接)'),
|
|
49
52
|
}).description('消息处理设置'),
|
|
50
53
|
koishi_1.Schema.object({
|
|
51
54
|
autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0为关闭)'),
|
|
52
|
-
}).description('
|
|
55
|
+
}).description('缓存清理设置(说明:开启自动清理可定期删除过期的临时视频文件和解析缓存)'),
|
|
53
56
|
]);
|
|
54
57
|
const processed = new Map();
|
|
55
58
|
const linkBuffer = new Map();
|
|
@@ -67,8 +70,8 @@ const PLATFORM_KEYWORDS = {
|
|
|
67
70
|
};
|
|
68
71
|
const API_CONFIG = {
|
|
69
72
|
bilibili: 'https://api.xingzhige.com/API/b_parse/',
|
|
70
|
-
douyin: 'https://api.
|
|
71
|
-
kuaishou: 'https://api.
|
|
73
|
+
douyin: 'https://api.bugpk.com/api/short_videos',
|
|
74
|
+
kuaishou: 'https://api.bugpk.com/api/short_videos',
|
|
72
75
|
xiaohongshu: 'https://api.bugpk.com/api/short_videos',
|
|
73
76
|
weibo: 'https://api.bugpk.com/api/weibo',
|
|
74
77
|
zuiyou: 'https://api.bugpk.com/api/short_videos',
|
|
@@ -81,7 +84,59 @@ function getErrorMessage(error) {
|
|
|
81
84
|
return error.message;
|
|
82
85
|
return String(error);
|
|
83
86
|
}
|
|
84
|
-
async function
|
|
87
|
+
async function getFileSize(url, userAgent) {
|
|
88
|
+
try {
|
|
89
|
+
const response = await axios_1.default.head(url, {
|
|
90
|
+
timeout: 10000,
|
|
91
|
+
headers: {
|
|
92
|
+
'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const contentLength = response.headers['content-length'];
|
|
96
|
+
if (contentLength) {
|
|
97
|
+
return Math.round(Number(contentLength) / 1024 / 1024 * 100) / 100;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) { }
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
async function downloadVideoThread(workerData) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const worker = new worker_threads_1.Worker(__filename, { workerData });
|
|
106
|
+
worker.on('message', resolve);
|
|
107
|
+
worker.on('error', reject);
|
|
108
|
+
worker.on('exit', (code) => {
|
|
109
|
+
if (code !== 0)
|
|
110
|
+
reject(new Error(`Worker stopped with exit code ${code}`));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (!worker_threads_1.isMainThread) {
|
|
115
|
+
const { url, start, end, filename, userAgent } = worker_threads_1.workerData;
|
|
116
|
+
const filePath = path_1.default.join(process.cwd(), 'temp_videos', `${filename}_${start}_${end}.part`);
|
|
117
|
+
(0, axios_1.default)({
|
|
118
|
+
url,
|
|
119
|
+
method: 'GET',
|
|
120
|
+
responseType: 'stream',
|
|
121
|
+
timeout: 60000,
|
|
122
|
+
headers: {
|
|
123
|
+
'User-Agent': userAgent,
|
|
124
|
+
'Range': `bytes=${start}-${end}`
|
|
125
|
+
}
|
|
126
|
+
}).then(response => {
|
|
127
|
+
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
128
|
+
response.data.pipe(writeStream);
|
|
129
|
+
writeStream.on('finish', () => {
|
|
130
|
+
worker_threads_1.parentPort?.postMessage({ success: true, filePath, start, end });
|
|
131
|
+
});
|
|
132
|
+
writeStream.on('error', (error) => {
|
|
133
|
+
worker_threads_1.parentPort?.postMessage({ success: false, error: error.message });
|
|
134
|
+
});
|
|
135
|
+
}).catch(error => {
|
|
136
|
+
worker_threads_1.parentPort?.postMessage({ success: false, error: error.message });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
85
140
|
const dir = path_1.default.join(process.cwd(), 'temp_videos');
|
|
86
141
|
if (!fs_1.default.existsSync(dir))
|
|
87
142
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
@@ -90,23 +145,61 @@ async function downloadVideo(url, filename, userAgent) {
|
|
|
90
145
|
if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
|
|
91
146
|
throw new Error('不支持音频');
|
|
92
147
|
}
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
148
|
+
const fileSize = await getFileSize(url, userAgent);
|
|
149
|
+
if (maxSize > 0 && fileSize > maxSize) {
|
|
150
|
+
throw new Error(`视频大小${fileSize}MB超过限制${maxSize}MB`);
|
|
151
|
+
}
|
|
152
|
+
if (threads <= 0 || fileSize === 0) {
|
|
153
|
+
const response = await (0, axios_1.default)({
|
|
154
|
+
url,
|
|
155
|
+
method: 'GET',
|
|
156
|
+
responseType: 'stream',
|
|
157
|
+
timeout: 60000,
|
|
158
|
+
headers: {
|
|
159
|
+
'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
163
|
+
await (0, promises_1.pipeline)(response.data, writeStream);
|
|
164
|
+
return filePath;
|
|
165
|
+
}
|
|
166
|
+
const totalSize = fileSize * 1024 * 1024;
|
|
167
|
+
const chunkSize = Math.ceil(totalSize / threads);
|
|
168
|
+
const promises = [];
|
|
169
|
+
for (let i = 0; i < threads; i++) {
|
|
170
|
+
const start = i * chunkSize;
|
|
171
|
+
const end = i === threads - 1 ? totalSize - 1 : start + chunkSize - 1;
|
|
172
|
+
promises.push(downloadVideoThread({
|
|
173
|
+
url,
|
|
174
|
+
start,
|
|
175
|
+
end,
|
|
176
|
+
filename,
|
|
177
|
+
userAgent
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
const results = await Promise.all(promises);
|
|
102
181
|
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
103
|
-
|
|
182
|
+
for (const result of results) {
|
|
183
|
+
if (!result.success)
|
|
184
|
+
throw new Error(result.error);
|
|
185
|
+
const readStream = fs_1.default.createReadStream(result.filePath);
|
|
186
|
+
await (0, promises_1.pipeline)(readStream, writeStream, { end: false });
|
|
187
|
+
fs_1.default.unlinkSync(result.filePath);
|
|
188
|
+
}
|
|
189
|
+
writeStream.end();
|
|
104
190
|
return filePath;
|
|
105
191
|
}
|
|
106
192
|
catch (error) {
|
|
107
193
|
if (fs_1.default.existsSync(filePath)) {
|
|
108
194
|
fs_1.default.unlinkSync(filePath);
|
|
109
195
|
}
|
|
196
|
+
const partFiles = fs_1.default.readdirSync(dir).filter(file => file.startsWith(`${filename}_`) && file.endsWith('.part'));
|
|
197
|
+
partFiles.forEach(file => {
|
|
198
|
+
try {
|
|
199
|
+
fs_1.default.unlinkSync(path_1.default.join(dir, file));
|
|
200
|
+
}
|
|
201
|
+
catch (e) { }
|
|
202
|
+
});
|
|
110
203
|
throw error;
|
|
111
204
|
}
|
|
112
205
|
}
|
|
@@ -124,46 +217,11 @@ function parseXingzhigeData(resData, platform) {
|
|
|
124
217
|
favorite: 0,
|
|
125
218
|
share: 0,
|
|
126
219
|
view: 0,
|
|
127
|
-
size: 0,
|
|
128
220
|
duration: '00:00'
|
|
129
221
|
},
|
|
130
222
|
type: 'video'
|
|
131
223
|
};
|
|
132
|
-
if (platform === '
|
|
133
|
-
const item = resData.jx[0];
|
|
134
|
-
result.title = item.title || '';
|
|
135
|
-
result.cover = item.cover || '';
|
|
136
|
-
result.video = item.url || item.video || '';
|
|
137
|
-
result.images = item.images || [];
|
|
138
|
-
result.stat = {
|
|
139
|
-
like: resData.stat?.like || 0,
|
|
140
|
-
coin: 0,
|
|
141
|
-
favorite: 0,
|
|
142
|
-
share: resData.stat?.share || 0,
|
|
143
|
-
view: resData.stat?.view || 0,
|
|
144
|
-
size: 0,
|
|
145
|
-
duration: '00:00'
|
|
146
|
-
};
|
|
147
|
-
result.type = result.images.length > 0 ? 'image' : 'video';
|
|
148
|
-
}
|
|
149
|
-
else if (platform === 'douyin' && resData.jx && resData.jx.length > 0) {
|
|
150
|
-
const item = resData.jx[0];
|
|
151
|
-
result.title = item.title || '';
|
|
152
|
-
result.cover = item.cover || '';
|
|
153
|
-
result.video = item.url || '';
|
|
154
|
-
result.images = item.images || [];
|
|
155
|
-
result.stat = {
|
|
156
|
-
like: resData.stat?.like || 0,
|
|
157
|
-
coin: 0,
|
|
158
|
-
favorite: resData.stat?.collect || 0,
|
|
159
|
-
share: resData.stat?.share || 0,
|
|
160
|
-
view: 0,
|
|
161
|
-
size: 0,
|
|
162
|
-
duration: '00:00'
|
|
163
|
-
};
|
|
164
|
-
result.type = result.images.length > 0 ? 'image' : 'video';
|
|
165
|
-
}
|
|
166
|
-
else if (platform === 'bilibili') {
|
|
224
|
+
if (platform === 'bilibili') {
|
|
167
225
|
const d = resData.data || resData;
|
|
168
226
|
result.title = d.video?.title || d.title || '';
|
|
169
227
|
result.author = d.owner?.name || d.name || '未知UP主';
|
|
@@ -176,7 +234,6 @@ function parseXingzhigeData(resData, platform) {
|
|
|
176
234
|
like: d.stat?.like || 0,
|
|
177
235
|
coin: d.stat?.coin || 0,
|
|
178
236
|
share: d.stat?.share || 0,
|
|
179
|
-
size: 0,
|
|
180
237
|
duration: formatDuration(d.duration || 0)
|
|
181
238
|
};
|
|
182
239
|
result.duration = d.duration || 0;
|
|
@@ -256,7 +313,6 @@ function parseData(data, maxDescLength, platform) {
|
|
|
256
313
|
favorite: 0,
|
|
257
314
|
share: 0,
|
|
258
315
|
view: 0,
|
|
259
|
-
size: 0,
|
|
260
316
|
duration: '00:00'
|
|
261
317
|
};
|
|
262
318
|
const durationFormatted = formatDuration(duration);
|
|
@@ -289,7 +345,6 @@ function parseData(data, maxDescLength, platform) {
|
|
|
289
345
|
favorite: stat.favorite || 0,
|
|
290
346
|
share: stat.share || 0,
|
|
291
347
|
view: stat.view || 0,
|
|
292
|
-
size: stat.size || 0,
|
|
293
348
|
duration: durationFormatted
|
|
294
349
|
}
|
|
295
350
|
};
|
|
@@ -299,17 +354,29 @@ function generateFormattedText(platform, parseData, config) {
|
|
|
299
354
|
if (platform !== 'bilibili') {
|
|
300
355
|
format = format.replace(/投币:\$\{投币数\}\n?/g, '');
|
|
301
356
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
357
|
+
const variables = {
|
|
358
|
+
'标题': parseData.title || '',
|
|
359
|
+
'作者': parseData.author || '',
|
|
360
|
+
'简介': parseData.desc || '',
|
|
361
|
+
'视频时长': parseData.stat.duration || '',
|
|
362
|
+
'点赞数': parseData.stat.like > 0 ? parseData.stat.like : '',
|
|
363
|
+
'投币数': parseData.stat.coin > 0 ? parseData.stat.coin : '',
|
|
364
|
+
'收藏数': parseData.stat.favorite > 0 ? parseData.stat.favorite : '',
|
|
365
|
+
'转发数': parseData.stat.share > 0 ? parseData.stat.share : '',
|
|
366
|
+
'播放数': parseData.stat.view > 0 ? parseData.stat.view : ''
|
|
367
|
+
};
|
|
368
|
+
let result = format;
|
|
369
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
370
|
+
const regex = new RegExp(`${key}:\\$\\{${key}\\\}([\\n]?)`, 'g');
|
|
371
|
+
if (!value) {
|
|
372
|
+
result = result.replace(regex, '');
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
result = result.replace(`$\{${key}\}`, value);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
result = result.replace(/\n+/g, '\n').trim();
|
|
379
|
+
return result;
|
|
313
380
|
}
|
|
314
381
|
function clearAllCache() {
|
|
315
382
|
processed.clear();
|
|
@@ -366,7 +433,7 @@ function apply(ctx, config) {
|
|
|
366
433
|
try {
|
|
367
434
|
const res = await http.get(apiUrl, { params: { url: realUrl } });
|
|
368
435
|
let parseResult = null;
|
|
369
|
-
if (
|
|
436
|
+
if (platform === 'bilibili') {
|
|
370
437
|
const xgData = parseXingzhigeData(res.data, platform);
|
|
371
438
|
parseResult = parseData(xgData, config.maxDescLength, platform);
|
|
372
439
|
}
|
|
@@ -483,21 +550,27 @@ function apply(ctx, config) {
|
|
|
483
550
|
}
|
|
484
551
|
if (item.video && config.showVideoFile && forwardMessages.length < 100) {
|
|
485
552
|
let videoElem;
|
|
486
|
-
|
|
487
|
-
|
|
553
|
+
try {
|
|
554
|
+
if (config.downloadVideoBeforeSend) {
|
|
488
555
|
const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
489
|
-
const filePath = await downloadVideo(item.video, filename, config.userAgent);
|
|
556
|
+
const filePath = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
490
557
|
videoElem = koishi_1.h.file(filePath);
|
|
491
558
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
559
|
+
else {
|
|
560
|
+
const fileSize = await getFileSize(item.video, config.userAgent);
|
|
561
|
+
if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
|
|
562
|
+
videoElem = koishi_1.h.text(`视频大小${fileSize}MB超过限制${config.maxVideoSize}MB,仅发送链接:${item.video}`);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
videoElem = koishi_1.h.video(item.video);
|
|
566
|
+
}
|
|
495
567
|
}
|
|
568
|
+
forwardMessages.push(buildForwardNode(session, videoElem, botName));
|
|
496
569
|
}
|
|
497
|
-
|
|
498
|
-
|
|
570
|
+
catch (error) {
|
|
571
|
+
logger.error(`视频处理失败: ${getErrorMessage(error)}`);
|
|
572
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`视频处理失败:${getErrorMessage(error)}\n链接:${item.video}`), botName));
|
|
499
573
|
}
|
|
500
|
-
forwardMessages.push(buildForwardNode(session, videoElem, botName));
|
|
501
574
|
}
|
|
502
575
|
}
|
|
503
576
|
else {
|
|
@@ -515,22 +588,28 @@ function apply(ctx, config) {
|
|
|
515
588
|
await delay(300);
|
|
516
589
|
}
|
|
517
590
|
if (item.video && config.showVideoFile) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
591
|
+
try {
|
|
592
|
+
let videoElem;
|
|
593
|
+
if (config.downloadVideoBeforeSend) {
|
|
521
594
|
const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
522
|
-
const filePath = await downloadVideo(item.video, filename, config.userAgent);
|
|
595
|
+
const filePath = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
523
596
|
videoElem = koishi_1.h.file(filePath);
|
|
524
597
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
598
|
+
else {
|
|
599
|
+
const fileSize = await getFileSize(item.video, config.userAgent);
|
|
600
|
+
if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
|
|
601
|
+
videoElem = koishi_1.h.text(`视频大小${fileSize}MB超过限制${config.maxVideoSize}MB,仅发送链接:${item.video}`);
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
videoElem = koishi_1.h.video(item.video);
|
|
605
|
+
}
|
|
528
606
|
}
|
|
607
|
+
await sendTimeout(session, videoElem);
|
|
529
608
|
}
|
|
530
|
-
|
|
531
|
-
|
|
609
|
+
catch (error) {
|
|
610
|
+
logger.error(`视频处理失败: ${getErrorMessage(error)}`);
|
|
611
|
+
await sendTimeout(session, koishi_1.h.text(`视频处理失败:${getErrorMessage(error)}\n链接:${item.video}`));
|
|
532
612
|
}
|
|
533
|
-
await sendTimeout(session, videoElem);
|
|
534
613
|
}
|
|
535
614
|
}
|
|
536
615
|
await delay(1000);
|