koishi-plugin-video-parser-all 0.8.1 → 0.8.3
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 +18 -8
- package/lib/index.js +295 -194
- package/package.json +1 -1
- package/readme.md +24 -19
package/lib/index.d.ts
CHANGED
|
@@ -3,9 +3,6 @@ export declare const name = "video-parser-all";
|
|
|
3
3
|
export declare const Config: Schema<{
|
|
4
4
|
enable?: boolean | null | undefined;
|
|
5
5
|
botName?: string | null | undefined;
|
|
6
|
-
showWaitingTip?: boolean | null | undefined;
|
|
7
|
-
waitingTipText?: string | null | undefined;
|
|
8
|
-
sameLinkInterval?: number | null | undefined;
|
|
9
6
|
debug?: boolean | null | undefined;
|
|
10
7
|
debugFile?: boolean | null | undefined;
|
|
11
8
|
} & import("cosmokit").Dict & {
|
|
@@ -13,7 +10,7 @@ export declare const Config: Schema<{
|
|
|
13
10
|
} & {
|
|
14
11
|
showImageText?: boolean | null | undefined;
|
|
15
12
|
showVideoFile?: boolean | null | undefined;
|
|
16
|
-
|
|
13
|
+
sendLivePhotoVideos?: boolean | null | undefined;
|
|
17
14
|
maxDescLength?: number | null | undefined;
|
|
18
15
|
} & {
|
|
19
16
|
timeout?: number | null | undefined;
|
|
@@ -32,12 +29,17 @@ export declare const Config: Schema<{
|
|
|
32
29
|
messageBufferDelay?: number | null | undefined;
|
|
33
30
|
} & {
|
|
34
31
|
autoClearCacheInterval?: number | null | undefined;
|
|
32
|
+
} & {
|
|
33
|
+
waitingTipText?: string | null | undefined;
|
|
34
|
+
duplicateLinkText?: string | null | undefined;
|
|
35
|
+
unsupportedPlatformText?: string | null | undefined;
|
|
36
|
+
invalidLinkText?: string | null | undefined;
|
|
37
|
+
cacheClearedText?: string | null | undefined;
|
|
38
|
+
parseErrorPrefix?: string | null | undefined;
|
|
39
|
+
parseErrorItemFormat?: string | null | undefined;
|
|
35
40
|
}, {
|
|
36
41
|
enable: boolean;
|
|
37
42
|
botName: string;
|
|
38
|
-
showWaitingTip: boolean;
|
|
39
|
-
waitingTipText: string;
|
|
40
|
-
sameLinkInterval: number;
|
|
41
43
|
debug: boolean;
|
|
42
44
|
debugFile: boolean;
|
|
43
45
|
} & import("cosmokit").Dict & {
|
|
@@ -45,7 +47,7 @@ export declare const Config: Schema<{
|
|
|
45
47
|
} & {
|
|
46
48
|
showImageText: boolean;
|
|
47
49
|
showVideoFile: boolean;
|
|
48
|
-
|
|
50
|
+
sendLivePhotoVideos: boolean;
|
|
49
51
|
maxDescLength: number;
|
|
50
52
|
} & {
|
|
51
53
|
timeout: number;
|
|
@@ -64,5 +66,13 @@ export declare const Config: Schema<{
|
|
|
64
66
|
messageBufferDelay: number;
|
|
65
67
|
} & {
|
|
66
68
|
autoClearCacheInterval: number;
|
|
69
|
+
} & {
|
|
70
|
+
waitingTipText: string;
|
|
71
|
+
duplicateLinkText: string;
|
|
72
|
+
unsupportedPlatformText: string;
|
|
73
|
+
invalidLinkText: string;
|
|
74
|
+
cacheClearedText: string;
|
|
75
|
+
parseErrorPrefix: string;
|
|
76
|
+
parseErrorItemFormat: string;
|
|
67
77
|
}>;
|
|
68
78
|
export declare function apply(ctx: Context, config: any): void;
|
package/lib/index.js
CHANGED
|
@@ -10,54 +10,57 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
10
10
|
const crypto_1 = __importDefault(require("crypto"));
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const url_1 = require("url");
|
|
13
14
|
const promises_1 = require("stream/promises");
|
|
14
15
|
const worker_threads_1 = require("worker_threads");
|
|
15
16
|
exports.name = 'video-parser-all';
|
|
16
17
|
exports.Config = koishi_1.Schema.intersect([
|
|
17
18
|
koishi_1.Schema.object({
|
|
18
19
|
enable: koishi_1.Schema.boolean().default(true).description('是否启用视频解析插件'),
|
|
19
|
-
botName: koishi_1.Schema.string().default('视频解析机器人').description('
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
|
|
23
|
-
debug: koishi_1.Schema.boolean().default(false).description('开启调试模式'),
|
|
24
|
-
debugFile: koishi_1.Schema.boolean().default(false).description('调试日志写入文件'),
|
|
20
|
+
botName: koishi_1.Schema.string().default('视频解析机器人').description('合并转发消息中显示的机器人名称'),
|
|
21
|
+
debug: koishi_1.Schema.boolean().default(false).description('开启调试模式(详细日志输出至控制台)'),
|
|
22
|
+
debugFile: koishi_1.Schema.boolean().default(false).description('调试日志同时写入本地 debug.log 文件'),
|
|
25
23
|
}).description('基础设置'),
|
|
26
24
|
koishi_1.Schema.object({
|
|
27
|
-
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(
|
|
28
|
-
|
|
29
|
-
点赞:${'${点赞数}'}
|
|
30
|
-
链接:${'${视频链接}'}`).description('统一消息格式'),
|
|
31
|
-
}).description('统一消息格式'),
|
|
25
|
+
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(`标题:\${标题}\n作者:\${作者}\n点赞:\${点赞数}\n视频链接:\${视频链接}`).description('统一消息格式,可用变量:${标题} ${作者} ${简介} ${视频时长} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${发布时间} ${图片数量} ${作者ID} ${视频链接} ${封面} ${音乐作者} ${音乐标题}'),
|
|
26
|
+
}).description('消息格式设置'),
|
|
32
27
|
koishi_1.Schema.object({
|
|
33
|
-
showImageText: koishi_1.Schema.boolean().default(true).description('
|
|
34
|
-
showVideoFile: koishi_1.Schema.boolean().default(true).description('
|
|
28
|
+
showImageText: koishi_1.Schema.boolean().default(true).description('是否发送解析后的文字内容'),
|
|
29
|
+
showVideoFile: koishi_1.Schema.boolean().default(true).description('是否发送视频文件(关闭则只发送视频链接)'),
|
|
30
|
+
sendLivePhotoVideos: koishi_1.Schema.boolean().default(true).description('发送实况图集时是否附带短视频'),
|
|
31
|
+
maxDescLength: koishi_1.Schema.number().default(200).description('简介内容最大长度(字符),超出自动截断'),
|
|
35
32
|
}).description('内容显示设置'),
|
|
36
33
|
koishi_1.Schema.object({
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
timeout: koishi_1.Schema.number().min(0).default(180000).description('API 请求超时(毫秒)'),
|
|
35
|
+
videoSendTimeout: koishi_1.Schema.number().min(0).default(60000).description('视频消息发送超时(毫秒,0 为不限制)'),
|
|
36
|
+
userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36').description('API 请求 UA'),
|
|
37
|
+
}).description('网络与 API 设置'),
|
|
39
38
|
koishi_1.Schema.object({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}).description('网络与API设置'),
|
|
44
|
-
koishi_1.Schema.object({
|
|
45
|
-
ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送失败错误'),
|
|
46
|
-
retryTimes: koishi_1.Schema.number().min(0).default(3).description('API请求重试次数'),
|
|
47
|
-
retryInterval: koishi_1.Schema.number().min(0).default(1000).description('重试间隔时间(毫秒)'),
|
|
39
|
+
ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略消息发送失败,避免插件崩溃'),
|
|
40
|
+
retryTimes: koishi_1.Schema.number().min(0).default(3).description('API 请求重试次数'),
|
|
41
|
+
retryInterval: koishi_1.Schema.number().min(0).default(1000).description('重试间隔(毫秒)'),
|
|
48
42
|
}).description('错误与重试设置'),
|
|
49
43
|
koishi_1.Schema.object({
|
|
50
|
-
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot平台)'),
|
|
51
|
-
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('
|
|
52
|
-
maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('
|
|
53
|
-
downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0
|
|
44
|
+
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot 平台)'),
|
|
45
|
+
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频到本地'),
|
|
46
|
+
maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('视频下载大小限制(MB,0 为不限制)'),
|
|
47
|
+
downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0 为单线程)'),
|
|
54
48
|
}).description('发送方式设置'),
|
|
55
49
|
koishi_1.Schema.object({
|
|
56
50
|
messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒)'),
|
|
57
|
-
}).description('
|
|
51
|
+
}).description('消息缓冲'),
|
|
52
|
+
koishi_1.Schema.object({
|
|
53
|
+
autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0 为关闭)'),
|
|
54
|
+
}).description('缓存清理'),
|
|
58
55
|
koishi_1.Schema.object({
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('解析等待提示'),
|
|
57
|
+
duplicateLinkText: koishi_1.Schema.string().default('请勿重复解析相同链接').description('重复链接提示'),
|
|
58
|
+
unsupportedPlatformText: koishi_1.Schema.string().default('不支持该平台链接').description('不支持的平台提示'),
|
|
59
|
+
invalidLinkText: koishi_1.Schema.string().default('无效的视频链接').description('无效链接提示(parse 指令)'),
|
|
60
|
+
cacheClearedText: koishi_1.Schema.string().default('✅ 缓存已清空').description('缓存清理提示'),
|
|
61
|
+
parseErrorPrefix: koishi_1.Schema.string().default('❌ 解析失败:').description('解析失败消息前缀'),
|
|
62
|
+
parseErrorItemFormat: koishi_1.Schema.string().default('【${url}】: ${msg}').description('每条解析失败格式,可用 ${url} ${msg}'),
|
|
63
|
+
}).description('界面文字设置'),
|
|
61
64
|
]);
|
|
62
65
|
const processed = new Map();
|
|
63
66
|
const linkBuffer = new Map();
|
|
@@ -111,7 +114,6 @@ const PLATFORM_KEYWORDS = {
|
|
|
111
114
|
doubao: ['doubao', 'doubao.com'],
|
|
112
115
|
jimeng: ['jimeng', 'jimeng.ai'],
|
|
113
116
|
};
|
|
114
|
-
const PLATFORM_TYPES = Object.keys(PLATFORM_KEYWORDS);
|
|
115
117
|
function getErrorMessage(error) {
|
|
116
118
|
if (error instanceof Error)
|
|
117
119
|
return error.message;
|
|
@@ -262,96 +264,139 @@ async function resolveShortUrl(url) {
|
|
|
262
264
|
return cleanUrl(url);
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
|
-
function formatDuration(
|
|
266
|
-
if (!
|
|
267
|
+
function formatDuration(seconds) {
|
|
268
|
+
if (!seconds || seconds <= 0)
|
|
267
269
|
return '00:00:00';
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return `00:${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`;
|
|
273
|
-
if (parts.length === 3)
|
|
274
|
-
return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}:${parts[2].padStart(2, '0')}`;
|
|
275
|
-
return '00:00:00';
|
|
276
|
-
}
|
|
277
|
-
input = Number(input);
|
|
278
|
-
}
|
|
279
|
-
const seconds = Math.floor(Number(input));
|
|
280
|
-
if (isNaN(seconds) || seconds <= 0 || seconds > 315360000)
|
|
281
|
-
return '00:00:00';
|
|
282
|
-
const hours = Math.floor(seconds / 3600);
|
|
283
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
284
|
-
const secs = Math.floor(seconds % 60);
|
|
285
|
-
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
270
|
+
const h = Math.floor(seconds / 3600);
|
|
271
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
272
|
+
const s = Math.floor(seconds % 60);
|
|
273
|
+
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
286
274
|
}
|
|
287
|
-
function formatPublishTime(
|
|
288
|
-
if (!
|
|
289
|
-
return '';
|
|
290
|
-
const str = String(value).trim();
|
|
291
|
-
if (value === 'ctime')
|
|
275
|
+
function formatPublishTime(ms) {
|
|
276
|
+
if (!ms)
|
|
292
277
|
return '';
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
278
|
+
const d = new Date(ms);
|
|
279
|
+
const y = d.getFullYear(), mo = (d.getMonth() + 1).toString().padStart(2, '0'), day = d.getDate().toString().padStart(2, '0'), H = d.getHours().toString().padStart(2, '0'), i = d.getMinutes().toString().padStart(2, '0');
|
|
280
|
+
return `${y}年${mo}月${day}日 ${H}:${i}`;
|
|
281
|
+
}
|
|
282
|
+
function pickBestQuality(videoBackup) {
|
|
283
|
+
if (!Array.isArray(videoBackup))
|
|
284
|
+
return [];
|
|
285
|
+
return videoBackup
|
|
286
|
+
.map(v => ({ quality: v.quality || v.label, url: v.url, bit_rate: v.bit_rate || 0 }))
|
|
287
|
+
.sort((a, b) => (b.bit_rate || 0) - (a.bit_rate || 0));
|
|
288
|
+
}
|
|
289
|
+
function parseApiResponse(raw, maxDescLen) {
|
|
290
|
+
debugLog('DEBUG', '原始API返回数据:', raw);
|
|
291
|
+
const data = raw?.data || {};
|
|
292
|
+
const extra = data.extra || {};
|
|
293
|
+
let type = data.type || '';
|
|
294
|
+
if (!type) {
|
|
295
|
+
if (data.images?.length > 0 && !data.url)
|
|
296
|
+
type = 'image';
|
|
297
|
+
else if (data.live_photo?.length > 0)
|
|
298
|
+
type = 'live_photo';
|
|
299
|
+
else if (raw.msg === 'live' || data.live)
|
|
300
|
+
type = 'live';
|
|
301
|
+
else
|
|
302
|
+
type = 'video';
|
|
298
303
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (y > 2000)
|
|
306
|
-
parts.push(`${y}年`);
|
|
307
|
-
if (m)
|
|
308
|
-
parts.push(`${m}月`);
|
|
309
|
-
if (d_)
|
|
310
|
-
parts.push(`${d_}日`);
|
|
311
|
-
if (H && i)
|
|
312
|
-
parts.push(`${H}:${i}`);
|
|
313
|
-
return parts.join(' ').trim();
|
|
304
|
+
const authorObj = data.author;
|
|
305
|
+
let author = '', uid = '', avatar = '';
|
|
306
|
+
if (typeof authorObj === 'object' && authorObj) {
|
|
307
|
+
author = authorObj.name || authorObj.author || '';
|
|
308
|
+
uid = String(authorObj.id || data.uid || '');
|
|
309
|
+
avatar = authorObj.avatar || data.avatar || '';
|
|
314
310
|
}
|
|
315
|
-
|
|
316
|
-
|
|
311
|
+
else {
|
|
312
|
+
author = data.author || data.auther || '';
|
|
313
|
+
uid = String(data.uid || '');
|
|
314
|
+
avatar = data.avatar || '';
|
|
317
315
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
316
|
+
const title = data.title || '';
|
|
317
|
+
const desc = (data.desc || data.description || '').slice(0, maxDescLen);
|
|
318
|
+
const cover = data.cover || '';
|
|
319
|
+
let video = '';
|
|
320
|
+
let videos = [];
|
|
321
|
+
if (data.video_backup?.length) {
|
|
322
|
+
const bestQ = pickBestQuality(data.video_backup);
|
|
323
|
+
videos = bestQ;
|
|
324
|
+
video = bestQ[0]?.url || data.url || '';
|
|
325
|
+
}
|
|
326
|
+
else if (data.videos?.length) {
|
|
327
|
+
video = data.videos[0]?.url || '';
|
|
328
|
+
videos = data.videos.map((v) => ({ quality: v.accept?.[0] || 'unknown', url: v.url }));
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
video = data.url || '';
|
|
332
|
+
}
|
|
333
|
+
const images = Array.isArray(data.images) ? data.images : [];
|
|
334
|
+
const live_photo = Array.isArray(data.live_photo) ? data.live_photo : [];
|
|
335
|
+
const music = {
|
|
336
|
+
title: data.music?.title || data.music?.name || '',
|
|
337
|
+
author: data.music?.author || data.music?.artist || '',
|
|
338
|
+
cover: data.music?.cover || '',
|
|
339
|
+
url: data.music?.url || ''
|
|
335
340
|
};
|
|
341
|
+
const stats = extra.statistics || {};
|
|
342
|
+
const like = Number(data.like || stats.digg_count || 0);
|
|
343
|
+
const comment = Number(stats.comment_count || 0);
|
|
344
|
+
const collect = Number(stats.collect_count || 0);
|
|
345
|
+
const share = Number(stats.share_count || 0);
|
|
346
|
+
const play = Number(stats.play_count || 0);
|
|
347
|
+
let duration = 0;
|
|
348
|
+
if (data.duration) {
|
|
349
|
+
duration = typeof data.duration === 'string' ? parseInt(data.duration) : data.duration;
|
|
350
|
+
if (duration > 1000000)
|
|
351
|
+
duration = Math.floor(duration / 1000);
|
|
352
|
+
}
|
|
353
|
+
else if (extra.duration_ms) {
|
|
354
|
+
duration = Math.floor(extra.duration_ms / 1000);
|
|
355
|
+
}
|
|
356
|
+
let publishTime = 0;
|
|
357
|
+
if (data.time) {
|
|
358
|
+
publishTime = typeof data.time === 'number' ? data.time : parseInt(data.time);
|
|
359
|
+
if (publishTime < 1000000000000)
|
|
360
|
+
publishTime *= 1000;
|
|
361
|
+
}
|
|
362
|
+
else if (extra.create_time) {
|
|
363
|
+
publishTime = extra.create_time * 1000;
|
|
364
|
+
}
|
|
365
|
+
const result = {
|
|
366
|
+
type, title, desc, author, uid, avatar, cover,
|
|
367
|
+
video, videos, images, live_photo, music,
|
|
368
|
+
like, comment, collect, share, play,
|
|
369
|
+
duration, publishTime
|
|
370
|
+
};
|
|
371
|
+
debugLog('DEBUG', '解析后的数据:', result);
|
|
372
|
+
return result;
|
|
336
373
|
}
|
|
337
|
-
function generateFormattedText(
|
|
338
|
-
const format = config.unifiedMessageFormat || `标题:${'${标题}'}\n作者:${'${作者}'}\n点赞:${'${点赞数}'}\n链接:${'${视频链接}'}`;
|
|
374
|
+
function generateFormattedText(p, format) {
|
|
339
375
|
const vars = {
|
|
340
|
-
'标题':
|
|
341
|
-
'作者':
|
|
342
|
-
'
|
|
343
|
-
'
|
|
344
|
-
'
|
|
345
|
-
'
|
|
346
|
-
'
|
|
347
|
-
'
|
|
348
|
-
'
|
|
376
|
+
'标题': p.title,
|
|
377
|
+
'作者': p.author,
|
|
378
|
+
'简介': p.desc,
|
|
379
|
+
'视频时长': p.duration > 0 ? formatDuration(p.duration) : '',
|
|
380
|
+
'点赞数': String(p.like),
|
|
381
|
+
'收藏数': String(p.collect),
|
|
382
|
+
'转发数': String(p.share),
|
|
383
|
+
'播放数': String(p.play),
|
|
384
|
+
'评论数': String(p.comment),
|
|
385
|
+
'发布时间': p.publishTime ? formatPublishTime(p.publishTime) : '',
|
|
386
|
+
'图片数量': String(p.images.length || p.live_photo.length),
|
|
387
|
+
'作者ID': p.uid,
|
|
388
|
+
'视频链接': p.video,
|
|
389
|
+
'封面': p.cover,
|
|
390
|
+
'音乐作者': p.music.author || '',
|
|
391
|
+
'音乐标题': p.music.title || '',
|
|
349
392
|
};
|
|
350
393
|
let result = format;
|
|
351
394
|
for (const [key, value] of Object.entries(vars)) {
|
|
352
|
-
result = result.replace(new RegExp(
|
|
395
|
+
result = result.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value);
|
|
353
396
|
}
|
|
354
|
-
|
|
397
|
+
const final = result.replace(/^\s*\n/gm, '').trim();
|
|
398
|
+
debugLog('DEBUG', '生成格式化文本:', final);
|
|
399
|
+
return final;
|
|
355
400
|
}
|
|
356
401
|
function clearAllCache() {
|
|
357
402
|
processed.clear();
|
|
@@ -382,16 +427,27 @@ function buildForwardNode(session, content, botName) {
|
|
|
382
427
|
function apply(ctx, config) {
|
|
383
428
|
initDebug(config.debug, config.debugFile);
|
|
384
429
|
debugLog('INFO', '插件初始化开始');
|
|
430
|
+
debugLog('INFO', '当前配置:', config);
|
|
431
|
+
const texts = {
|
|
432
|
+
waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
|
|
433
|
+
duplicateLinkText: config.duplicateLinkText || '请勿重复解析相同链接',
|
|
434
|
+
unsupportedPlatformText: config.unsupportedPlatformText || '不支持该平台链接',
|
|
435
|
+
invalidLinkText: config.invalidLinkText || '无效的视频链接',
|
|
436
|
+
cacheClearedText: config.cacheClearedText || '✅ 缓存已清空',
|
|
437
|
+
parseErrorPrefix: config.parseErrorPrefix || '❌ 解析失败:',
|
|
438
|
+
parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
|
|
439
|
+
};
|
|
385
440
|
clearAllCache();
|
|
386
441
|
const http = axios_1.default.create({
|
|
387
442
|
timeout: config.timeout,
|
|
388
443
|
headers: {
|
|
389
|
-
'User-Agent': config.userAgent,
|
|
444
|
+
'User-Agent': config.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
390
445
|
'Referer': 'https://www.baidu.com/',
|
|
391
446
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
392
447
|
}
|
|
393
448
|
});
|
|
394
|
-
async function
|
|
449
|
+
async function fetchApi(url) {
|
|
450
|
+
debugLog('INFO', `调用API解析: ${url}`);
|
|
395
451
|
for (let i = 0; i <= config.retryTimes; i++) {
|
|
396
452
|
try {
|
|
397
453
|
const res = await http.get('https://api.bugpk.com/api/short_videos', {
|
|
@@ -399,8 +455,9 @@ function apply(ctx, config) {
|
|
|
399
455
|
timeout: config.timeout
|
|
400
456
|
});
|
|
401
457
|
debugLog('INFO', `API响应: code=${res.data?.code}, msg=${res.data?.msg}`);
|
|
458
|
+
debugLog('DEBUG', 'API完整响应:', res.data);
|
|
402
459
|
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
|
|
403
|
-
return parseApiResponse(res.data);
|
|
460
|
+
return parseApiResponse(res.data, config.maxDescLength);
|
|
404
461
|
}
|
|
405
462
|
throw new Error(res.data?.msg || '解析失败');
|
|
406
463
|
}
|
|
@@ -413,59 +470,53 @@ function apply(ctx, config) {
|
|
|
413
470
|
throw new Error('API请求全部失败');
|
|
414
471
|
}
|
|
415
472
|
async function parseUrl(url) {
|
|
416
|
-
debugLog('INFO',
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
debugLog('DEBUG', `实际URL: ${realUrl}`);
|
|
473
|
+
debugLog('INFO', `开始解析链接: ${url}`);
|
|
474
|
+
const realUrl = await resolveShortUrl(url);
|
|
475
|
+
debugLog('DEBUG', `重定向后的URL: ${realUrl}`);
|
|
420
476
|
const platform = getPlatformType(realUrl);
|
|
421
477
|
if (!platform) {
|
|
422
478
|
debugLog('WARN', `不支持的平台: ${realUrl}`);
|
|
423
|
-
return {
|
|
479
|
+
return { success: false, msg: texts.unsupportedPlatformText };
|
|
424
480
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
481
|
+
const candidates = [url, realUrl];
|
|
482
|
+
let lastError = null;
|
|
483
|
+
for (const candidate of candidates) {
|
|
484
|
+
try {
|
|
485
|
+
const info = await fetchApi(candidate);
|
|
486
|
+
debugLog('INFO', `解析成功: ${info.title}`);
|
|
487
|
+
return { success: true, data: info };
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
lastError = getErrorMessage(error);
|
|
491
|
+
debugLog('ERROR', `候选链接解析失败: ${candidate} => ${lastError}`);
|
|
492
|
+
}
|
|
433
493
|
}
|
|
494
|
+
return { success: false, msg: lastError || '解析失败' };
|
|
434
495
|
}
|
|
435
496
|
async function processSingleUrl(session, url) {
|
|
497
|
+
debugLog('INFO', `处理单个URL: ${url}, 用户: ${session.userId}`);
|
|
436
498
|
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
437
499
|
const now = Date.now();
|
|
438
500
|
const last = processed.get(hash);
|
|
439
501
|
if (last && (now - last) < config.sameLinkInterval * 1000) {
|
|
440
502
|
debugLog('WARN', `重复解析: ${url}`);
|
|
441
|
-
return { success: false, msg:
|
|
503
|
+
return { success: false, msg: texts.duplicateLinkText };
|
|
442
504
|
}
|
|
443
505
|
processed.set(hash, now);
|
|
444
506
|
const result = await parseUrl(url);
|
|
445
|
-
if (!result.
|
|
446
|
-
return
|
|
447
|
-
const text = generateFormattedText(result.
|
|
448
|
-
return {
|
|
449
|
-
success: true,
|
|
450
|
-
data: {
|
|
451
|
-
text,
|
|
452
|
-
cover: result.info.cover,
|
|
453
|
-
video: result.info.url,
|
|
454
|
-
music: result.info.music,
|
|
455
|
-
author: result.info.author,
|
|
456
|
-
uid: result.info.uid,
|
|
457
|
-
avatar: result.info.avatar,
|
|
458
|
-
like: result.info.like,
|
|
459
|
-
title: result.info.title
|
|
460
|
-
}
|
|
461
|
-
};
|
|
507
|
+
if (!result.success)
|
|
508
|
+
return result;
|
|
509
|
+
const text = generateFormattedText(result.data, config.unifiedMessageFormat);
|
|
510
|
+
return { success: true, data: { text, parsed: result.data } };
|
|
462
511
|
}
|
|
463
512
|
async function sendWithTimeout(session, content) {
|
|
513
|
+
debugLog('DEBUG', `发送消息: ${JSON.stringify(content)}`);
|
|
464
514
|
if (config.videoSendTimeout <= 0) {
|
|
465
515
|
try {
|
|
466
516
|
return await session.send(content);
|
|
467
517
|
}
|
|
468
518
|
catch (err) {
|
|
519
|
+
debugLog('ERROR', `发送消息失败: ${getErrorMessage(err)}`);
|
|
469
520
|
if (!config.ignoreSendError)
|
|
470
521
|
throw err;
|
|
471
522
|
return null;
|
|
@@ -478,6 +529,7 @@ function apply(ctx, config) {
|
|
|
478
529
|
]);
|
|
479
530
|
}
|
|
480
531
|
catch (err) {
|
|
532
|
+
debugLog('ERROR', `发送消息超时或失败: ${getErrorMessage(err)}`);
|
|
481
533
|
if (!config.ignoreSendError)
|
|
482
534
|
throw err;
|
|
483
535
|
return null;
|
|
@@ -494,79 +546,123 @@ function apply(ctx, config) {
|
|
|
494
546
|
const items = [];
|
|
495
547
|
const errors = [];
|
|
496
548
|
for (const url of urls) {
|
|
497
|
-
const
|
|
498
|
-
if (
|
|
499
|
-
items.push(
|
|
500
|
-
|
|
501
|
-
|
|
549
|
+
const res = await processSingleUrl(session, url);
|
|
550
|
+
if (res.success) {
|
|
551
|
+
items.push(res.data);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
const item = texts.parseErrorItemFormat
|
|
555
|
+
.replace(/\$\{url\}/g, url.length > 50 ? url.slice(0, 50) + '...' : url)
|
|
556
|
+
.replace(/\$\{msg\}/g, res.msg);
|
|
557
|
+
errors.push(item);
|
|
558
|
+
}
|
|
502
559
|
}
|
|
503
|
-
if (errors.length
|
|
504
|
-
|
|
505
|
-
await sendWithTimeout(session, errorMsg).catch(() => { });
|
|
560
|
+
if (errors.length) {
|
|
561
|
+
await sendWithTimeout(session, `${texts.parseErrorPrefix}\n${errors.join('\n')}`).catch(() => { });
|
|
506
562
|
await delay(500);
|
|
507
563
|
}
|
|
508
|
-
if (items.length
|
|
564
|
+
if (!items.length)
|
|
509
565
|
return;
|
|
510
566
|
const enableForward = config.enableForward && session.platform === 'onebot';
|
|
511
567
|
const botName = config.botName || '视频解析机器人';
|
|
512
568
|
const forwardMessages = [];
|
|
513
569
|
for (const item of items) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
570
|
+
const p = item.parsed;
|
|
571
|
+
const text = item.text;
|
|
572
|
+
debugLog('INFO', `开始发送内容,类型: ${p.type}, 标题: ${p.title}`);
|
|
573
|
+
if (text && config.showImageText) {
|
|
574
|
+
debugLog('DEBUG', '发送文本消息');
|
|
575
|
+
if (enableForward)
|
|
576
|
+
forwardMessages.push(buildForwardNode(session, text, botName));
|
|
577
|
+
else {
|
|
578
|
+
await sendWithTimeout(session, text);
|
|
579
|
+
await delay(300);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (p.cover && p.type !== 'live_photo') {
|
|
583
|
+
debugLog('DEBUG', '发送封面图片:', p.cover);
|
|
584
|
+
if (enableForward)
|
|
585
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
|
|
586
|
+
else {
|
|
587
|
+
await sendWithTimeout(session, koishi_1.h.image(p.cover)).catch(() => { });
|
|
588
|
+
await delay(300);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (p.video && config.showVideoFile && (p.type === 'video' || p.type === 'live')) {
|
|
592
|
+
const sendVideo = async () => {
|
|
593
|
+
if (config.downloadVideoBeforeSend) {
|
|
594
|
+
const fname = crypto_1.default.createHash('md5').update(p.video).digest('hex');
|
|
595
|
+
const dl = await downloadVideo(p.video, fname, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
596
|
+
if (dl.success) {
|
|
597
|
+
const fileUrl = (0, url_1.pathToFileURL)(dl.filePath).href;
|
|
598
|
+
debugLog('INFO', `视频下载成功,发送文件: ${fileUrl}`);
|
|
599
|
+
return koishi_1.h.file(fileUrl);
|
|
528
600
|
}
|
|
529
|
-
else
|
|
530
|
-
forwardMessages.push(buildForwardNode(session, koishi_1.h.video(item.video), botName));
|
|
531
601
|
}
|
|
602
|
+
debugLog('INFO', `发送视频链接: ${p.video}`);
|
|
603
|
+
return koishi_1.h.video(p.video);
|
|
604
|
+
};
|
|
605
|
+
if (enableForward) {
|
|
606
|
+
const vMsg = await sendVideo();
|
|
607
|
+
forwardMessages.push(buildForwardNode(session, vMsg, botName));
|
|
532
608
|
}
|
|
533
609
|
else {
|
|
534
|
-
|
|
535
|
-
await
|
|
536
|
-
await
|
|
610
|
+
try {
|
|
611
|
+
const vMsg = await sendVideo();
|
|
612
|
+
await sendWithTimeout(session, vMsg);
|
|
537
613
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
614
|
+
catch (e) {
|
|
615
|
+
debugLog('ERROR', `发送视频失败: ${getErrorMessage(e)},尝试直接发送链接`);
|
|
616
|
+
try {
|
|
617
|
+
await sendWithTimeout(session, koishi_1.h.video(p.video));
|
|
618
|
+
}
|
|
619
|
+
catch { }
|
|
541
620
|
}
|
|
542
|
-
|
|
621
|
+
await delay(500);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (p.type === 'image' || p.type === 'live_photo') {
|
|
625
|
+
const mediaList = [];
|
|
626
|
+
if (p.type === 'live_photo' && p.live_photo?.length) {
|
|
627
|
+
debugLog('INFO', `发送实况图集,共 ${p.live_photo.length} 张`);
|
|
628
|
+
for (const lp of p.live_photo) {
|
|
629
|
+
if (lp.image)
|
|
630
|
+
mediaList.push({ type: 'image', url: lp.image });
|
|
631
|
+
if (lp.video && config.sendLivePhotoVideos)
|
|
632
|
+
mediaList.push({ type: 'video', url: lp.video });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
else if (p.images?.length) {
|
|
636
|
+
debugLog('INFO', `发送图集,共 ${p.images.length} 张`);
|
|
637
|
+
p.images.forEach(url => mediaList.push({ type: 'image', url }));
|
|
638
|
+
}
|
|
639
|
+
if (enableForward) {
|
|
640
|
+
for (const m of mediaList) {
|
|
641
|
+
const msg = m.type === 'image' ? koishi_1.h.image(m.url) : koishi_1.h.video(m.url);
|
|
642
|
+
forwardMessages.push(buildForwardNode(session, msg, botName));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
for (const m of mediaList) {
|
|
647
|
+
debugLog('DEBUG', `发送${m.type}: ${m.url}`);
|
|
543
648
|
try {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const dl = await downloadVideo(item.video, fname, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
547
|
-
await sendWithTimeout(session, dl.success ? koishi_1.h.file(dl.filePath) : koishi_1.h.video(item.video));
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
await sendWithTimeout(session, koishi_1.h.video(item.video));
|
|
551
|
-
}
|
|
649
|
+
await sendWithTimeout(session, m.type === 'image' ? koishi_1.h.image(m.url) : koishi_1.h.video(m.url));
|
|
650
|
+
await delay(200);
|
|
552
651
|
}
|
|
553
|
-
catch {
|
|
554
|
-
|
|
555
|
-
await sendWithTimeout(session, koishi_1.h.video(item.video));
|
|
556
|
-
}
|
|
557
|
-
catch { }
|
|
652
|
+
catch (e) {
|
|
653
|
+
debugLog('ERROR', `发送${m.type}失败: ${getErrorMessage(e)}`);
|
|
558
654
|
}
|
|
559
|
-
await delay(500);
|
|
560
655
|
}
|
|
561
656
|
}
|
|
562
657
|
}
|
|
563
|
-
catch (e) { }
|
|
564
658
|
}
|
|
565
659
|
if (enableForward && forwardMessages.length) {
|
|
660
|
+
debugLog('INFO', `合并转发消息,共 ${forwardMessages.length} 条`);
|
|
566
661
|
try {
|
|
567
662
|
await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100)));
|
|
568
663
|
}
|
|
569
|
-
catch {
|
|
664
|
+
catch (e) {
|
|
665
|
+
debugLog('ERROR', `合并转发失败,降级逐条发送: ${getErrorMessage(e)}`);
|
|
570
666
|
for (const node of forwardMessages) {
|
|
571
667
|
try {
|
|
572
668
|
await sendWithTimeout(session, node.data.content);
|
|
@@ -581,28 +677,33 @@ function apply(ctx, config) {
|
|
|
581
677
|
if (!config.enable)
|
|
582
678
|
return;
|
|
583
679
|
const content = session.content?.trim() || '';
|
|
680
|
+
debugLog('INFO', `收到消息: "${content}"`);
|
|
584
681
|
const urls = extractUrl(content);
|
|
585
|
-
if (!urls.length)
|
|
682
|
+
if (!urls.length) {
|
|
683
|
+
debugLog('DEBUG', '消息中未检测到平台链接');
|
|
586
684
|
return;
|
|
685
|
+
}
|
|
686
|
+
debugLog('INFO', '检测到链接:', urls);
|
|
587
687
|
if (config.showWaitingTip) {
|
|
588
688
|
try {
|
|
589
|
-
await sendWithTimeout(session,
|
|
689
|
+
await sendWithTimeout(session, texts.waitingTipText);
|
|
590
690
|
}
|
|
591
691
|
catch { }
|
|
592
692
|
}
|
|
593
693
|
await flush(session, urls);
|
|
594
694
|
});
|
|
595
695
|
ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
|
|
696
|
+
debugLog('INFO', `手动解析指令: ${url}`);
|
|
596
697
|
const us = extractUrl(url);
|
|
597
698
|
if (!us.length) {
|
|
598
|
-
await sendWithTimeout(session,
|
|
699
|
+
await sendWithTimeout(session, texts.invalidLinkText);
|
|
599
700
|
return;
|
|
600
701
|
}
|
|
601
702
|
await flush(session, us);
|
|
602
703
|
});
|
|
603
704
|
ctx.command('clear-cache', '清空缓存').action(async ({ session }) => {
|
|
604
705
|
clearAllCache();
|
|
605
|
-
await sendWithTimeout(session,
|
|
706
|
+
await sendWithTimeout(session, texts.cacheClearedText);
|
|
606
707
|
});
|
|
607
708
|
setInterval(() => {
|
|
608
709
|
const now = Date.now();
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -3,22 +3,24 @@
|
|
|
3
3
|
## 项目介绍 (Project Introduction)
|
|
4
4
|
|
|
5
5
|
### 中文
|
|
6
|
-
这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙等20
|
|
6
|
+
这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙等20+主流平台的短视频/图集/实况链接。核心特性:
|
|
7
7
|
- 🌐 统一API解析,覆盖20+热门平台,无需繁琐配置
|
|
8
8
|
- 🤖 自动识别链接来源,即丢即用
|
|
9
|
-
- 🎨
|
|
9
|
+
- 🎨 完全自定义的解析结果格式,支持多项变量替换
|
|
10
10
|
- 🐛 内置Debug调试模式,可详细记录所有操作与API交互日志
|
|
11
11
|
- ⚡ 防重复解析、API重试、本地视频下载、多线程加速等实用功能
|
|
12
12
|
- 📤 支持OneBot平台消息合并转发,优化多图文展示体验
|
|
13
|
+
- 🧹 内置手动/自动双模式缓存清理,自动删除临时视频与冗余缓存
|
|
13
14
|
|
|
14
15
|
### English
|
|
15
|
-
This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, using a unified API interface to automatically recognize and parse short video/image links from 20+ mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, YouTube, TikTok, Jianying, AcFun, Zhihu, Huya and more. Core features:
|
|
16
|
+
This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, using a unified API interface to automatically recognize and parse short video/image/live photo links from 20+ mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, YouTube, TikTok, Jianying, AcFun, Zhihu, Huya and more. Core features:
|
|
16
17
|
- 🌐 Unified API parsing, covering 20+ popular platforms without complex configuration
|
|
17
18
|
- 🤖 Auto-detection of link sources, just drop & go
|
|
18
|
-
- 🎨 Fully customizable parsing result format with
|
|
19
|
+
- 🎨 Fully customizable parsing result format with variable substitutions
|
|
19
20
|
- 🐛 Built-in Debug mode, recording detailed operations and API interaction logs
|
|
20
21
|
- ⚡ Duplicate parsing prevention, API retry, local video download, multithread acceleration
|
|
21
22
|
- 📤 Support OneBot message forwarding for better image/video display
|
|
23
|
+
- 🧹 Manual & automatic cache cleaning, auto delete temporary videos and redundant cache
|
|
22
24
|
|
|
23
25
|
## 项目仓库 (Repository)
|
|
24
26
|
- GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
|
|
@@ -54,6 +56,7 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
54
56
|
|--------|------|--------|------|
|
|
55
57
|
| `showImageText` | boolean | true | 是否显示解析后的图文内容 |
|
|
56
58
|
| `showVideoFile` | boolean | true | 是否发送视频文件(关闭则只发送视频链接) |
|
|
59
|
+
| `sendLivePhotoVideos` | boolean | true | 是否发送实况图片附带的短视频(仅 live_photo 类型) |
|
|
57
60
|
|
|
58
61
|
### 内容长度限制
|
|
59
62
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
@@ -92,38 +95,40 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
92
95
|
|--------|------|--------|------|
|
|
93
96
|
| `autoClearCacheInterval` | number | 0 | 自动清理缓存间隔(分钟,0为关闭自动清理) |
|
|
94
97
|
|
|
98
|
+
## 缓存机制说明 (Cache Mechanism)
|
|
99
|
+
- 临时视频默认存放目录:项目根目录 `temp_videos`
|
|
100
|
+
- 支持定时自动清空缓存、手动执行 `clear-cache` 一键清理
|
|
101
|
+
- 自动清理过期解析记录、残留分段下载文件,避免磁盘占用
|
|
102
|
+
|
|
95
103
|
## 支持的变量 (Supported Variables)
|
|
96
104
|
在 `unifiedMessageFormat` 中可使用以下变量进行自定义格式化:
|
|
97
105
|
|
|
98
106
|
| 变量名 | 说明 | 适用平台 |
|
|
99
107
|
|--------|------|----------|
|
|
100
108
|
| `${标题}` | 视频/图集标题 | 所有平台 |
|
|
101
|
-
| `${作者}` |
|
|
102
|
-
| `${简介}` | 内容简介/描述 |
|
|
103
|
-
| `${视频时长}` |
|
|
109
|
+
| `${作者}` | 作者/发布者名称 | 所有平台 |
|
|
110
|
+
| `${简介}` | 内容简介/描述 | 所有平台 |
|
|
111
|
+
| `${视频时长}` | 视频时长(时:分:秒) | 视频/实况 |
|
|
104
112
|
| `${点赞数}` | 点赞数量 | 所有平台 |
|
|
105
|
-
| `${投币数}` | 投币数量 | 部分平台 (B站) |
|
|
106
113
|
| `${收藏数}` | 收藏数量 | 所有平台 |
|
|
107
114
|
| `${转发数}` | 转发/分享数量 | 所有平台 |
|
|
108
115
|
| `${播放数}` | 播放量 | 部分平台 |
|
|
109
116
|
| `${评论数}` | 评论数量 | 所有平台 |
|
|
110
|
-
| `${IP属地}` | 作者IP属地 | 部分平台 |
|
|
111
117
|
| `${发布时间}` | 发布时间(格式化) | 所有平台 |
|
|
112
|
-
| `${
|
|
113
|
-
| `${在线人数}` | 直播间在线人数 | 部分平台 |
|
|
114
|
-
| `${关注数}` | 关注数 | 部分平台 |
|
|
115
|
-
| `${文件大小}` | 文件大小(MB) | 部分平台 |
|
|
116
|
-
| `${直播间地址}` | 直播间链接 | 部分平台 |
|
|
117
|
-
| `${直播间ID}` | 直播间ID | 部分平台 |
|
|
118
|
-
| `${直播间状态}` | 直播间状态(直播中/未开播) | 部分平台 |
|
|
119
|
-
| `${图片数量}` | 图集图片数量 | 部分平台 |
|
|
118
|
+
| `${图片数量}` | 图集/实况图片数量 | 图集/实况 |
|
|
120
119
|
| `${作者ID}` | 作者唯一标识ID | 部分平台 |
|
|
120
|
+
| `${视频链接}` | 视频直链地址 | 视频/实况 |
|
|
121
|
+
| `${封面}` | 封面图片地址 | 所有平台 |
|
|
122
|
+
| `${音乐作者}` | 背景音乐作者 | 部分平台 |
|
|
123
|
+
| `${音乐标题}` | 背景音乐标题 | 部分平台 |
|
|
124
|
+
|
|
125
|
+
> 注:部分变量可能因平台API返回数据不同而显示为空,请根据实际情况自定义格式。
|
|
121
126
|
|
|
122
127
|
## 支持的平台 (Supported Platforms)
|
|
123
128
|
| 平台名称 | 关键词识别 | 解析能力 |
|
|
124
129
|
|----------|------------|----------|
|
|
125
|
-
| 哔哩哔哩 (B站) | bilibili, b23.tv, bilibili.com |
|
|
126
|
-
| 抖音 | douyin, v.douyin.com |
|
|
130
|
+
| 哔哩哔哩 (B站) | bilibili, b23.tv, bilibili.com | 视频(不含番剧/直播/图文) |
|
|
131
|
+
| 抖音 | douyin, v.douyin.com | 短视频、图集、实况 |
|
|
127
132
|
| 快手 | kuaishou, v.kuaishou.com | 短视频、图集 |
|
|
128
133
|
| 小红书 | xiaohongshu, xhslink.com | 图文、视频 |
|
|
129
134
|
| 微博 | weibo, video.weibo.com | 视频、图集 |
|