koishi-plugin-video-parser-all 0.2.2 → 0.2.5
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 -2
- package/lib/index.js +198 -192
- package/package.json +11 -3
- package/readme.md +48 -3
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
|
-
revokeWaitingTip: boolean;
|
|
7
6
|
waitingTipText: string;
|
|
8
7
|
sameLinkInterval: number;
|
|
9
8
|
imageParseFormat: string;
|
|
@@ -20,8 +19,11 @@ export interface Config {
|
|
|
20
19
|
messageBufferDelay: number;
|
|
21
20
|
retryTimes: number;
|
|
22
21
|
retryInterval: number;
|
|
23
|
-
|
|
22
|
+
apiMode: 'builtin' | 'custom';
|
|
23
|
+
builtinApiUrl: string;
|
|
24
|
+
customApiUrl: string;
|
|
24
25
|
videoSendTimeout: number;
|
|
26
|
+
autoClearCacheInterval: number;
|
|
25
27
|
}
|
|
26
28
|
export declare const Config: Schema<Config>;
|
|
27
29
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -16,26 +16,28 @@ exports.name = 'video-parser-all';
|
|
|
16
16
|
exports.Config = koishi_1.Schema.object({
|
|
17
17
|
enable: koishi_1.Schema.boolean().default(true).description('启用插件'),
|
|
18
18
|
showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
|
|
19
|
-
revokeWaitingTip: koishi_1.Schema.boolean().default(true).description('解析完成后撤回等待提示'),
|
|
20
19
|
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('等待提示文本'),
|
|
21
|
-
sameLinkInterval: koishi_1.Schema.number().default(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
支持变量:\${标题} \${UP主} \${简介} \${点赞} \${投币} \${收藏} \${转发} \${观看} \${弹幕} \${tab} \${~~~} \${封面}`),
|
|
20
|
+
sameLinkInterval: koishi_1.Schema.number().default(0).min(0).description('相同链接解析间隔(秒)'),
|
|
21
|
+
imageParseFormat: koishi_1.Schema.string().role('textarea').default('${标题}\n${UP主}').description(`解析结果格式
|
|
22
|
+
支持变量:\${标题} \${UP主} \${简介} \${tab} \${~~~}`),
|
|
25
23
|
returnContent: koishi_1.Schema.object({
|
|
26
24
|
showImageText: koishi_1.Schema.boolean().default(true).description('显示文本与封面'),
|
|
27
25
|
showVideoUrl: koishi_1.Schema.boolean().default(false).description('显示无水印链接'),
|
|
28
26
|
showVideoFile: koishi_1.Schema.boolean().default(true).description('发送视频消息'),
|
|
29
27
|
}).description('返回内容设置'),
|
|
30
28
|
maxDescLength: koishi_1.Schema.number().default(200).description('简介最大长度'),
|
|
31
|
-
timeout: koishi_1.Schema.number().default(
|
|
29
|
+
timeout: koishi_1.Schema.number().default(0).min(0).description('API请求超时(毫秒)'),
|
|
32
30
|
ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略消息发送错误'),
|
|
33
31
|
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot)'),
|
|
34
32
|
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频(仅OneBot)'),
|
|
35
|
-
messageBufferDelay: koishi_1.Schema.number().default(
|
|
36
|
-
retryTimes: koishi_1.Schema.number().default(
|
|
37
|
-
retryInterval: koishi_1.Schema.number().default(
|
|
38
|
-
|
|
33
|
+
messageBufferDelay: koishi_1.Schema.number().default(0).min(0).description('消息缓冲延迟(秒)'),
|
|
34
|
+
retryTimes: koishi_1.Schema.number().default(0).min(0).description('接口重试次数'),
|
|
35
|
+
retryInterval: koishi_1.Schema.number().default(0).min(0).description('重试间隔(毫秒)'),
|
|
36
|
+
apiMode: koishi_1.Schema.union([
|
|
37
|
+
koishi_1.Schema.const('builtin').description('使用内置API'),
|
|
38
|
+
koishi_1.Schema.const('custom').description('使用自定义API')
|
|
39
|
+
]).default('builtin').description('API使用模式'),
|
|
40
|
+
builtinApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description(`内置API地址
|
|
39
41
|
解析失败可能原因:
|
|
40
42
|
1. 视频为私密/付费/下架/限制
|
|
41
43
|
2. 接口限流或维护
|
|
@@ -43,10 +45,12 @@ exports.Config = koishi_1.Schema.object({
|
|
|
43
45
|
4. 平台链接格式更新
|
|
44
46
|
注意事项:
|
|
45
47
|
1. 勿频繁解析相同链接
|
|
46
|
-
2.
|
|
48
|
+
2. 仅支持快手/B站/小红书/微博公开视频
|
|
47
49
|
3. 公共接口不保证100%成功
|
|
48
50
|
4. 失败可检查链接或稍后重试`),
|
|
49
|
-
|
|
51
|
+
customApiUrl: koishi_1.Schema.string().default('').description('自定义API地址(优先使用)'),
|
|
52
|
+
videoSendTimeout: koishi_1.Schema.number().default(0).min(0).description('视频发送超时(毫秒)'),
|
|
53
|
+
autoClearCacheInterval: koishi_1.Schema.number().default(0).min(0).description('自动清理缓存间隔(分钟),0表示不自动清理'),
|
|
50
54
|
});
|
|
51
55
|
if (!worker_threads_1.isMainThread) {
|
|
52
56
|
const { url, filePath } = worker_threads_1.workerData;
|
|
@@ -77,8 +81,9 @@ const processed = new Map();
|
|
|
77
81
|
const linkBuffer = new Map();
|
|
78
82
|
const PLATFORM_KEYWORDS = {
|
|
79
83
|
bilibili: ['bilibili', 'b23', 'B站', 'www.bilibili.com', 'm.bilibili.com'],
|
|
80
|
-
kuaishou: ['kuaishou', '快手', 'v.
|
|
81
|
-
|
|
84
|
+
kuaishou: ['kuaishou', '快手', 'v.kuishou.com', 'www.kuishou.com', 'kwimgs.com'],
|
|
85
|
+
xiaohongshu: ['xiaohongshu', '小红书', 'xhslink.com', 'xiaohongshu.com', 'xhscdn.com'],
|
|
86
|
+
weibo: ['weibo', '微博', 'weibo.com', 'video.weibo.com', 'svproxy.168299.xyz']
|
|
82
87
|
};
|
|
83
88
|
function extractUrl(content) {
|
|
84
89
|
const urlMatches = content.match(/https?:\/\/[^\s]+/gi) || [];
|
|
@@ -93,14 +98,25 @@ function hasPlatformKeyword(content) {
|
|
|
93
98
|
}
|
|
94
99
|
function getPlatformType(url) {
|
|
95
100
|
const lower = url.toLowerCase();
|
|
96
|
-
if (PLATFORM_KEYWORDS.douyin.some(k => lower.includes(k)))
|
|
97
|
-
return 'douyin';
|
|
98
101
|
if (PLATFORM_KEYWORDS.kuaishou.some(k => lower.includes(k)))
|
|
99
102
|
return 'kuaishou';
|
|
100
103
|
if (PLATFORM_KEYWORDS.bilibili.some(k => lower.includes(k)))
|
|
101
104
|
return 'bilibili';
|
|
105
|
+
if (PLATFORM_KEYWORDS.xiaohongshu.some(k => lower.includes(k)))
|
|
106
|
+
return 'xiaohongshu';
|
|
107
|
+
if (PLATFORM_KEYWORDS.weibo.some(k => lower.includes(k)))
|
|
108
|
+
return 'weibo';
|
|
102
109
|
return null;
|
|
103
110
|
}
|
|
111
|
+
async function shortUrl(url) {
|
|
112
|
+
try {
|
|
113
|
+
const res = await axios_1.default.get('https://api.oick.cn/dwz/api.php', { params: { url }, timeout: 5000 });
|
|
114
|
+
if (res.data.code === 200)
|
|
115
|
+
return res.data.short_url;
|
|
116
|
+
}
|
|
117
|
+
catch { }
|
|
118
|
+
return url;
|
|
119
|
+
}
|
|
104
120
|
async function downloadVideoWithThreads(url, filename) {
|
|
105
121
|
return new Promise((resolve, reject) => {
|
|
106
122
|
const dir = path_1.default.join(process.cwd(), 'temp_videos');
|
|
@@ -108,58 +124,39 @@ async function downloadVideoWithThreads(url, filename) {
|
|
|
108
124
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
109
125
|
const filePath = path_1.default.join(dir, `${filename}.mp4`);
|
|
110
126
|
const worker = new worker_threads_1.Worker(__filename, { workerData: { url, filePath } });
|
|
111
|
-
worker.on('message', (result) =>
|
|
112
|
-
if (result.success)
|
|
113
|
-
resolve(result.filePath);
|
|
114
|
-
else
|
|
115
|
-
reject(new Error(result.error));
|
|
116
|
-
});
|
|
127
|
+
worker.on('message', (result) => result.success ? resolve(result.filePath) : reject(new Error(result.error)));
|
|
117
128
|
worker.on('error', reject);
|
|
118
|
-
worker.on('exit', (code) =>
|
|
119
|
-
if (code !== 0)
|
|
120
|
-
reject(new Error('视频下载线程异常'));
|
|
121
|
-
});
|
|
129
|
+
worker.on('exit', (code) => code !== 0 && reject(new Error('视频下载线程异常')));
|
|
122
130
|
});
|
|
123
131
|
}
|
|
124
132
|
function parseData(data, maxDescLength) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
desc = desc.slice(0, maxDescLength);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
let collect = data.collect || data.favorite || 0;
|
|
132
|
-
let share = data.share || 0;
|
|
133
|
-
let play = data.play || data.view || 0;
|
|
134
|
-
let danmaku = data.danmaku || data.comment || 0;
|
|
135
|
-
let cover = data.cover || data.imgurl || data.pic || '';
|
|
133
|
+
const type = data.type || 'video';
|
|
134
|
+
const title = data.title || data.desc || '无标题';
|
|
135
|
+
const author = data.author?.name || data.author || data.auther || data.user?.name || '未知作者';
|
|
136
|
+
const desc = (data.desc || data.description || title).slice(0, maxDescLength);
|
|
137
|
+
const cover = data.cover || data.imgurl || data.pic || '';
|
|
138
|
+
const images = data.images || [];
|
|
136
139
|
let video = '';
|
|
137
|
-
if (data.
|
|
138
|
-
video = data.videos[0].url;
|
|
139
|
-
}
|
|
140
|
-
else if (data.url) {
|
|
140
|
+
if (data.url)
|
|
141
141
|
video = data.url;
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
else if (data.videos?.[0]?.url)
|
|
143
|
+
video = data.videos[0].url;
|
|
144
|
+
else if (data.video_backup?.[0]?.url)
|
|
144
145
|
video = data.video_backup[0].url;
|
|
145
|
-
}
|
|
146
146
|
if (video.endsWith('.m4a') || video.endsWith('.mp3'))
|
|
147
147
|
video = '';
|
|
148
|
-
return { title, author, desc,
|
|
148
|
+
return { type, title, author, desc, cover, images, video };
|
|
149
149
|
}
|
|
150
150
|
function clearAllCache() {
|
|
151
151
|
processed.clear();
|
|
152
152
|
linkBuffer.forEach(b => clearTimeout(b.timer));
|
|
153
153
|
linkBuffer.clear();
|
|
154
|
-
const
|
|
155
|
-
if (fs_1.default.existsSync(
|
|
156
|
-
fs_1.default.readdirSync(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
catch { }
|
|
161
|
-
});
|
|
162
|
-
}
|
|
154
|
+
const d = path_1.default.join(process.cwd(), 'temp_videos');
|
|
155
|
+
if (fs_1.default.existsSync(d))
|
|
156
|
+
fs_1.default.readdirSync(d).forEach(f => { try {
|
|
157
|
+
fs_1.default.unlinkSync(path_1.default.join(d, f));
|
|
158
|
+
}
|
|
159
|
+
catch { } });
|
|
163
160
|
return true;
|
|
164
161
|
}
|
|
165
162
|
const delay = (ms) => new Promise(r => setTimeout(r, ms));
|
|
@@ -167,21 +164,24 @@ function apply(ctx, config) {
|
|
|
167
164
|
if (!worker_threads_1.isMainThread)
|
|
168
165
|
return;
|
|
169
166
|
clearAllCache();
|
|
167
|
+
const getApiUrl = () => {
|
|
168
|
+
if (config.apiMode === 'custom' && config.customApiUrl.trim()) {
|
|
169
|
+
return config.customApiUrl.trim();
|
|
170
|
+
}
|
|
171
|
+
return config.builtinApiUrl;
|
|
172
|
+
};
|
|
170
173
|
const http = axios_1.default.create({
|
|
171
174
|
timeout: config.timeout,
|
|
172
|
-
headers: {
|
|
173
|
-
'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'
|
|
174
|
-
}
|
|
175
|
+
headers: { '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' }
|
|
175
176
|
});
|
|
176
177
|
async function parse(url) {
|
|
177
|
-
const
|
|
178
|
-
if (!
|
|
178
|
+
const p = getPlatformType(url);
|
|
179
|
+
if (!p)
|
|
179
180
|
return { data: null, msg: '不支持该平台链接' };
|
|
180
181
|
for (let i = 0; i <= config.retryTimes; i++) {
|
|
181
182
|
try {
|
|
182
|
-
const res = await http.get(
|
|
183
|
-
|
|
184
|
-
if (isSuccess) {
|
|
183
|
+
const res = await http.get(getApiUrl(), { params: { url } });
|
|
184
|
+
if ((res.data.code === 200 || res.data.code === 0) && res.data.data) {
|
|
185
185
|
return { data: parseData(res.data.data, config.maxDescLength), msg: '解析成功' };
|
|
186
186
|
}
|
|
187
187
|
else {
|
|
@@ -203,163 +203,164 @@ function apply(ctx, config) {
|
|
|
203
203
|
return { data: null, msg: '请勿重复解析' };
|
|
204
204
|
}
|
|
205
205
|
processed.set(hash, now);
|
|
206
|
-
const
|
|
207
|
-
if (!
|
|
208
|
-
return { data: null, msg:
|
|
209
|
-
|
|
206
|
+
const r = await parse(url);
|
|
207
|
+
if (!r.data)
|
|
208
|
+
return { data: null, msg: r.msg };
|
|
209
|
+
const d = r.data;
|
|
210
210
|
let text = config.imageParseFormat
|
|
211
|
-
.replace(/\${标题}/g,
|
|
212
|
-
.replace(/\${UP主}/g,
|
|
213
|
-
.replace(/\${简介}/g,
|
|
214
|
-
.replace(/\${
|
|
215
|
-
.replace(/\${
|
|
216
|
-
|
|
217
|
-
.replace(/\${转发}/g, String(parseResult.data.share))
|
|
218
|
-
.replace(/\${观看}/g, String(parseResult.data.play))
|
|
219
|
-
.replace(/\${弹幕}/g, String(parseResult.data.danmaku))
|
|
220
|
-
.replace(/\${tab}/g, '\t').replace(/\${~~~}/g, '\n');
|
|
221
|
-
const contentParts = [];
|
|
222
|
-
if (config.returnContent.showImageText) {
|
|
223
|
-
const [b, a] = text.split('${封面}');
|
|
224
|
-
if (b?.trim())
|
|
225
|
-
contentParts.push(b.trim());
|
|
226
|
-
if (parseResult.data.cover)
|
|
227
|
-
contentParts.push(koishi_1.h.image(parseResult.data.cover));
|
|
228
|
-
if (a?.trim())
|
|
229
|
-
contentParts.push(a.trim());
|
|
230
|
-
}
|
|
231
|
-
let videoContent = '';
|
|
232
|
-
if (config.returnContent.showVideoFile && parseResult.data.video) {
|
|
233
|
-
if (config.downloadVideoBeforeSend && session.platform === 'onebot') {
|
|
234
|
-
try {
|
|
235
|
-
const name = crypto_1.default.createHash('md5').update(parseResult.data.video).digest('hex');
|
|
236
|
-
const fp = await downloadVideoWithThreads(parseResult.data.video, name);
|
|
237
|
-
videoContent = koishi_1.h.file(fp);
|
|
238
|
-
}
|
|
239
|
-
catch (e) {
|
|
240
|
-
videoContent = koishi_1.h.video(parseResult.data.video);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
videoContent = koishi_1.h.video(parseResult.data.video);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (config.returnContent.showVideoUrl && parseResult.data.video) {
|
|
248
|
-
contentParts.push(`🔗 无水印视频:${parseResult.data.video}`);
|
|
249
|
-
}
|
|
250
|
-
return {
|
|
251
|
-
data: {
|
|
252
|
-
textContent: contentParts.join('\n'),
|
|
253
|
-
videoContent
|
|
254
|
-
},
|
|
255
|
-
msg: '解析成功'
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
async function revokeTip(session, key) {
|
|
259
|
-
if (!config.revokeWaitingTip || session.platform !== 'onebot')
|
|
260
|
-
return;
|
|
261
|
-
const buf = linkBuffer.get(key);
|
|
262
|
-
if (!buf?.tipMsgId)
|
|
263
|
-
return;
|
|
264
|
-
try {
|
|
265
|
-
await session.bot.deleteMessage(session.channelId, buf.tipMsgId.toString());
|
|
266
|
-
}
|
|
267
|
-
catch { }
|
|
211
|
+
.replace(/\${标题}/g, d.title)
|
|
212
|
+
.replace(/\${UP主}/g, d.author)
|
|
213
|
+
.replace(/\${简介}/g, d.desc)
|
|
214
|
+
.replace(/\${tab}/g, '\t')
|
|
215
|
+
.replace(/\${~~~}/g, '\n');
|
|
216
|
+
return { data: { text, cover: d.cover, images: d.images, video: d.video, type: d.type }, msg: 'ok' };
|
|
268
217
|
}
|
|
269
|
-
async function
|
|
270
|
-
|
|
271
|
-
session.send(
|
|
272
|
-
|
|
273
|
-
]).catch(() => null);
|
|
218
|
+
async function sendTimeout(session, c) {
|
|
219
|
+
if (config.videoSendTimeout <= 0)
|
|
220
|
+
return session.send(c).catch(() => null);
|
|
221
|
+
return Promise.race([session.send(c), new Promise((_, r) => setTimeout(() => r('timeout'), config.videoSendTimeout))]).catch(() => null);
|
|
274
222
|
}
|
|
275
223
|
async function flush(session, manualUrls) {
|
|
276
224
|
const key = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
277
|
-
|
|
278
|
-
|
|
225
|
+
const buf = linkBuffer.get(key);
|
|
226
|
+
const urls = manualUrls || buf?.urls || [];
|
|
279
227
|
if (buf) {
|
|
280
228
|
clearTimeout(buf.timer);
|
|
281
229
|
linkBuffer.delete(key);
|
|
282
|
-
await revokeTip(session, key);
|
|
283
230
|
}
|
|
284
|
-
const
|
|
285
|
-
const
|
|
231
|
+
const items = [];
|
|
232
|
+
const errs = [];
|
|
286
233
|
for (const u of urls) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
234
|
+
const one = await processSingleUrl(session, u);
|
|
235
|
+
one.data ? items.push(one.data) : errs.push(`【${u.slice(0, 22)}...】:${one.msg}`);
|
|
236
|
+
}
|
|
237
|
+
const forwardMessages = [];
|
|
238
|
+
const botName = '视频解析机器人';
|
|
239
|
+
if (errs.length) {
|
|
240
|
+
const errorMsg = `⚠️ 部分解析失败\n${errs.join('\n')}`;
|
|
241
|
+
if (config.enableForward && session.platform === 'onebot') {
|
|
242
|
+
forwardMessages.push((0, koishi_1.h)('message', [
|
|
243
|
+
(0, koishi_1.h)('author', { id: session.selfId, name: botName }),
|
|
244
|
+
errorMsg
|
|
245
|
+
]));
|
|
290
246
|
}
|
|
291
247
|
else {
|
|
292
|
-
|
|
248
|
+
await sendTimeout(session, errorMsg);
|
|
249
|
+
await delay(600);
|
|
293
250
|
}
|
|
294
251
|
}
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
if (config.enableForward && session.platform === 'onebot') {
|
|
304
|
-
try {
|
|
305
|
-
const forwardMessages = results.map(result => {
|
|
306
|
-
const content = [];
|
|
307
|
-
if (result.textContent)
|
|
308
|
-
content.push(result.textContent);
|
|
309
|
-
if (result.videoContent)
|
|
310
|
-
content.push(result.videoContent);
|
|
311
|
-
return (0, koishi_1.h)('message', [
|
|
312
|
-
(0, koishi_1.h)('author', { id: session.selfId, name: '视频解析' }),
|
|
313
|
-
...content
|
|
314
|
-
]);
|
|
315
|
-
});
|
|
316
|
-
await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages), config.videoSendTimeout);
|
|
317
|
-
return;
|
|
252
|
+
if (items.length === 0) {
|
|
253
|
+
const failMsg = `❌ 全部解析失败\n${errs.join('\n')}`;
|
|
254
|
+
if (config.enableForward && session.platform === 'onebot') {
|
|
255
|
+
forwardMessages.push((0, koishi_1.h)('message', [
|
|
256
|
+
(0, koishi_1.h)('author', { id: session.selfId, name: botName }),
|
|
257
|
+
failMsg
|
|
258
|
+
]));
|
|
318
259
|
}
|
|
319
|
-
|
|
320
|
-
await
|
|
260
|
+
else {
|
|
261
|
+
await sendTimeout(session, failMsg);
|
|
321
262
|
}
|
|
263
|
+
return;
|
|
322
264
|
}
|
|
323
|
-
for (const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
265
|
+
for (const it of items) {
|
|
266
|
+
if (config.enableForward && session.platform === 'onebot') {
|
|
267
|
+
forwardMessages.push((0, koishi_1.h)('message', [
|
|
268
|
+
(0, koishi_1.h)('author', { id: session.selfId, name: botName }),
|
|
269
|
+
it.text
|
|
270
|
+
]));
|
|
271
|
+
if (it.cover) {
|
|
272
|
+
forwardMessages.push((0, koishi_1.h)('message', [
|
|
273
|
+
(0, koishi_1.h)('author', { id: session.selfId, name: botName }),
|
|
274
|
+
koishi_1.h.image(it.cover)
|
|
275
|
+
]));
|
|
276
|
+
}
|
|
277
|
+
if (it.video && config.returnContent.showVideoFile) {
|
|
278
|
+
let vid = koishi_1.h.video(it.video);
|
|
279
|
+
if (config.downloadVideoBeforeSend) {
|
|
280
|
+
try {
|
|
281
|
+
const name = crypto_1.default.createHash('md5').update(it.video).digest('hex');
|
|
282
|
+
vid = koishi_1.h.file(await downloadVideoWithThreads(it.video, name));
|
|
283
|
+
}
|
|
284
|
+
catch { }
|
|
285
|
+
}
|
|
286
|
+
forwardMessages.push((0, koishi_1.h)('message', [
|
|
287
|
+
(0, koishi_1.h)('author', { id: session.selfId, name: botName }),
|
|
288
|
+
vid
|
|
289
|
+
]));
|
|
290
|
+
}
|
|
291
|
+
if (it.video && config.returnContent.showVideoUrl) {
|
|
292
|
+
const s = await shortUrl(it.video);
|
|
293
|
+
forwardMessages.push((0, koishi_1.h)('message', [
|
|
294
|
+
(0, koishi_1.h)('author', { id: session.selfId, name: botName }),
|
|
295
|
+
`🔗 无水印:${s}`
|
|
296
|
+
]));
|
|
297
|
+
}
|
|
298
|
+
if (it.type === 'image' && it.images?.length) {
|
|
299
|
+
it.images.forEach(imgUrl => {
|
|
300
|
+
forwardMessages.push((0, koishi_1.h)('message', [
|
|
301
|
+
(0, koishi_1.h)('author', { id: session.selfId, name: botName }),
|
|
302
|
+
koishi_1.h.image(imgUrl)
|
|
303
|
+
]));
|
|
304
|
+
});
|
|
330
305
|
}
|
|
331
|
-
await delay(1000);
|
|
332
306
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
307
|
+
else {
|
|
308
|
+
await sendTimeout(session, it.text);
|
|
309
|
+
await delay(300);
|
|
310
|
+
if (it.type === 'image' && it.images?.length) {
|
|
311
|
+
const msg = (0, koishi_1.h)('message', ...it.images.map(u => koishi_1.h.image(u)));
|
|
312
|
+
await sendTimeout(session, msg);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
if (it.cover) {
|
|
316
|
+
await sendTimeout(session, koishi_1.h.image(it.cover));
|
|
317
|
+
await delay(300);
|
|
318
|
+
}
|
|
319
|
+
if (it.video && config.returnContent.showVideoFile) {
|
|
320
|
+
let vid = koishi_1.h.video(it.video);
|
|
321
|
+
if (config.downloadVideoBeforeSend) {
|
|
322
|
+
try {
|
|
323
|
+
const name = crypto_1.default.createHash('md5').update(it.video).digest('hex');
|
|
324
|
+
vid = koishi_1.h.file(await downloadVideoWithThreads(it.video, name));
|
|
325
|
+
}
|
|
326
|
+
catch { }
|
|
327
|
+
}
|
|
328
|
+
await sendTimeout(session, vid);
|
|
329
|
+
}
|
|
330
|
+
if (it.video && config.returnContent.showVideoUrl) {
|
|
331
|
+
await delay(300);
|
|
332
|
+
const s = await shortUrl(it.video);
|
|
333
|
+
await sendTimeout(session, `🔗 无水印:${s}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
await delay(1000);
|
|
336
337
|
}
|
|
337
338
|
}
|
|
339
|
+
if (config.enableForward && session.platform === 'onebot' && forwardMessages.length) {
|
|
340
|
+
const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages);
|
|
341
|
+
await sendTimeout(session, forwardMsg);
|
|
342
|
+
}
|
|
338
343
|
}
|
|
339
344
|
ctx.on('message', async (session) => {
|
|
340
345
|
if (!config.enable)
|
|
341
346
|
return;
|
|
342
|
-
const
|
|
343
|
-
if (!
|
|
344
|
-
return;
|
|
345
|
-
const urls = extractUrl(content);
|
|
346
|
-
if (urls.length === 0)
|
|
347
|
+
const urls = extractUrl(session.content.trim());
|
|
348
|
+
if (!urls.length)
|
|
347
349
|
return;
|
|
348
350
|
const key = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
349
351
|
if (linkBuffer.has(key)) {
|
|
350
352
|
const b = linkBuffer.get(key);
|
|
351
353
|
const newUrls = urls.filter(u => !b.urls.includes(u));
|
|
352
|
-
if (newUrls.length
|
|
354
|
+
if (newUrls.length) {
|
|
353
355
|
b.urls.push(...newUrls);
|
|
354
356
|
clearTimeout(b.timer);
|
|
355
357
|
b.timer = setTimeout(() => flush(session), config.messageBufferDelay * 1000);
|
|
356
|
-
linkBuffer.set(key, b);
|
|
357
358
|
}
|
|
358
359
|
return;
|
|
359
360
|
}
|
|
360
361
|
let tipId;
|
|
361
362
|
if (config.showWaitingTip) {
|
|
362
|
-
const m = await
|
|
363
|
+
const m = await sendTimeout(session, config.waitingTipText);
|
|
363
364
|
tipId = m?.messageId || m?.id || m;
|
|
364
365
|
}
|
|
365
366
|
linkBuffer.set(key, {
|
|
@@ -368,16 +369,16 @@ function apply(ctx, config) {
|
|
|
368
369
|
tipMsgId: tipId
|
|
369
370
|
});
|
|
370
371
|
});
|
|
371
|
-
ctx.command('
|
|
372
|
+
ctx.command('parse <url>', '手动解析视频链接')
|
|
372
373
|
.action(async ({ session }, url) => {
|
|
373
374
|
if (!url)
|
|
374
375
|
return '请输入视频链接';
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
376
|
+
const us = extractUrl(url);
|
|
377
|
+
if (!us.length)
|
|
377
378
|
return '不支持该链接';
|
|
378
|
-
await flush(session,
|
|
379
|
+
await flush(session, us);
|
|
379
380
|
});
|
|
380
|
-
ctx.command('
|
|
381
|
+
ctx.command('clear-cache', '清空解析缓存与临时文件')
|
|
381
382
|
.action(() => {
|
|
382
383
|
clearAllCache();
|
|
383
384
|
return '✅ 解析缓存已清空';
|
|
@@ -387,20 +388,25 @@ function apply(ctx, config) {
|
|
|
387
388
|
processed.forEach((t, h) => now - t > 86400000 && processed.delete(h));
|
|
388
389
|
}, 3600000);
|
|
389
390
|
setInterval(() => {
|
|
390
|
-
const
|
|
391
|
-
if (!fs_1.default.existsSync(
|
|
391
|
+
const d = path_1.default.join(process.cwd(), 'temp_videos');
|
|
392
|
+
if (!fs_1.default.existsSync(d))
|
|
392
393
|
return;
|
|
393
|
-
const files = fs_1.default.readdirSync(tempDir);
|
|
394
394
|
const now = Date.now();
|
|
395
|
-
|
|
395
|
+
fs_1.default.readdirSync(d).forEach(f => {
|
|
396
396
|
try {
|
|
397
|
-
const st = fs_1.default.statSync(path_1.default.join(
|
|
397
|
+
const st = fs_1.default.statSync(path_1.default.join(d, f));
|
|
398
398
|
if (now - st.mtimeMs > 3600000)
|
|
399
|
-
fs_1.default.unlinkSync(path_1.default.join(
|
|
399
|
+
fs_1.default.unlinkSync(path_1.default.join(d, f));
|
|
400
400
|
}
|
|
401
401
|
catch { }
|
|
402
402
|
});
|
|
403
403
|
}, 1800000);
|
|
404
|
+
if (config.autoClearCacheInterval > 0) {
|
|
405
|
+
setInterval(() => {
|
|
406
|
+
clearAllCache();
|
|
407
|
+
ctx.logger.info('自动清理缓存完成');
|
|
408
|
+
}, config.autoClearCacheInterval * 60000);
|
|
409
|
+
}
|
|
404
410
|
process.on('exit', clearAllCache);
|
|
405
411
|
ctx.logger.info('视频解析插件已加载');
|
|
406
412
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-video-parser-all",
|
|
3
|
-
"description": "Koishi 视频解析插件,支持抖音/快手/B
|
|
4
|
-
"version": "0.2.
|
|
3
|
+
"description": "Koishi 视频解析插件,支持抖音/快手/B站/小红书视频链接解析",
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -35,5 +35,13 @@
|
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@koishijs/plugin-console": "^5.30.4",
|
|
37
37
|
"koishi": "^4.18.7"
|
|
38
|
-
}
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/Minecraft-1314/koishi-plugin-video-parser-all.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/Minecraft-1314/koishi-plugin-video-parser-all/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/Minecraft-1314/koishi-plugin-video-parser-all#readme"
|
|
39
47
|
}
|
package/readme.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
|
-
# koishi-plugin-video-parser
|
|
1
|
+
# koishi-plugin-video-parser-all
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 项目介绍 (Project Introduction)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### 中文
|
|
6
|
+
这是一个为 Koishi 机器人框架开发的**多平台视频解析插件**,支持自动识别并解析抖音、快手、B站、小红书、微博等主流短视频平台链接。核心特性:
|
|
7
|
+
- 自动识别多平台视频链接,无需手动指定平台
|
|
8
|
+
- 自定义解析结果格式、返回内容类型(封面/链接/视频文件)
|
|
9
|
+
- 内置防重复解析、接口重试、自动缓存清理等实用功能
|
|
10
|
+
- 支持 OneBot 平台消息合并转发,优化展示体验
|
|
11
|
+
- 可切换内置 API 或自定义 API,适配不同网络环境
|
|
12
|
+
|
|
13
|
+
### English
|
|
14
|
+
A multi-platform video parsing plugin for the Koishi bot framework, supporting automatic recognition and parsing of video links from Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo and other mainstream platforms. Core features:
|
|
15
|
+
- Auto-detect video links across platforms without manual specification
|
|
16
|
+
- Customize result formatting and output types (cover/link/video file)
|
|
17
|
+
- Built-in duplicate prevention, retry logic, auto cache cleanup
|
|
18
|
+
- Support OneBot message forwarding for better display experience
|
|
19
|
+
- Switchable between built-in and custom API for different network environments
|
|
20
|
+
|
|
21
|
+
## 项目仓库 (Repository)
|
|
22
|
+
- GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
|
|
23
|
+
- Issues: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all/issues`
|
|
24
|
+
|
|
25
|
+
## 核心指令 (Core Commands)
|
|
26
|
+
```bash
|
|
27
|
+
# 手动解析指定视频链接
|
|
28
|
+
parse <url>
|
|
29
|
+
|
|
30
|
+
# 清空解析缓存与临时视频文件
|
|
31
|
+
clear-cache
|
|
32
|
+
|
|
33
|
+
## 项目贡献者 (Contributors)
|
|
34
|
+
|
|
35
|
+
| 贡献者 (Contributor) | 贡献内容 (Contribution) |
|
|
36
|
+
|----------------------|-------------------------|
|
|
37
|
+
| [donlon](https://github.com/donlon) | 项目基础框架提供 (Provided basic project framework) |
|
|
38
|
+
| (欢迎提交 PR 加入贡献者列表) | (Welcome to submit PR to join the contributor list) |
|
|
39
|
+
|
|
40
|
+
## 许可协议 (License)
|
|
41
|
+
|
|
42
|
+
本项目采用 MIT 许可证,详情参见 [LICENSE](LICENSE) 文件。
|
|
43
|
+
|
|
44
|
+
This project is licensed under the MIT License, see the [LICENSE](LICENSE) file for details.
|
|
45
|
+
|
|
46
|
+
## 支持我们 (Support Us)
|
|
47
|
+
|
|
48
|
+
如果这个项目对您有帮助,欢迎点亮右上角的 Star ⭐ 支持我们,这将是对所有贡献者最大的鼓励!
|
|
49
|
+
|
|
50
|
+
If this project is helpful to you, please feel free to star it in the upper right corner ⭐ to support us, which will be the greatest encouragement to all contributors!
|