koishi-plugin-video-parser-all 0.0.1 → 0.0.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 +33 -48
- package/lib/index.js +312 -163
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import { Context, Schema } from 'koishi';
|
|
2
|
-
export declare const name = "video-parser";
|
|
2
|
+
export declare const name = "video-parser-all";
|
|
3
|
+
interface BilibiliFieldMap {
|
|
4
|
+
title: string;
|
|
5
|
+
author: string;
|
|
6
|
+
description: string;
|
|
7
|
+
like: string;
|
|
8
|
+
coin: string;
|
|
9
|
+
collect: string;
|
|
10
|
+
share: string;
|
|
11
|
+
view: string;
|
|
12
|
+
danmaku: string;
|
|
13
|
+
cover: string;
|
|
14
|
+
duration: string;
|
|
15
|
+
size: string;
|
|
16
|
+
url: string;
|
|
17
|
+
}
|
|
18
|
+
interface DyKsFieldMap {
|
|
19
|
+
title: string;
|
|
20
|
+
author: string;
|
|
21
|
+
description: string;
|
|
22
|
+
cover: string;
|
|
23
|
+
duration: string;
|
|
24
|
+
size: string;
|
|
25
|
+
url: string;
|
|
26
|
+
}
|
|
3
27
|
export interface Config {
|
|
4
28
|
enable: boolean;
|
|
5
29
|
showWaitingTip: boolean;
|
|
6
30
|
waitingTipText: string;
|
|
7
31
|
parserSource: string;
|
|
8
|
-
returnComponents: string[];
|
|
9
32
|
allowBVAVParse: boolean;
|
|
10
33
|
sameLinkInterval: number;
|
|
11
34
|
minVideoDuration: number;
|
|
@@ -22,64 +45,26 @@ export interface Config {
|
|
|
22
45
|
downloadBeforeSend: boolean;
|
|
23
46
|
messageBufferDelay: number;
|
|
24
47
|
userAgent: string;
|
|
25
|
-
|
|
48
|
+
builtinApi: {
|
|
49
|
+
timeout: number;
|
|
50
|
+
retryCount: number;
|
|
51
|
+
};
|
|
26
52
|
bilibili: {
|
|
27
53
|
customApi: string;
|
|
28
54
|
apiKey: string;
|
|
29
|
-
fieldMap:
|
|
30
|
-
title: string;
|
|
31
|
-
author: string;
|
|
32
|
-
description: string;
|
|
33
|
-
like: string;
|
|
34
|
-
coin: string;
|
|
35
|
-
collect: string;
|
|
36
|
-
share: string;
|
|
37
|
-
view: string;
|
|
38
|
-
danmaku: string;
|
|
39
|
-
cover: string;
|
|
40
|
-
duration: string;
|
|
41
|
-
size: string;
|
|
42
|
-
url: string;
|
|
43
|
-
};
|
|
55
|
+
fieldMap: BilibiliFieldMap;
|
|
44
56
|
};
|
|
45
57
|
douyin: {
|
|
46
58
|
customApi: string;
|
|
47
59
|
apiKey: string;
|
|
48
|
-
fieldMap:
|
|
49
|
-
title: string;
|
|
50
|
-
author: string;
|
|
51
|
-
description: string;
|
|
52
|
-
like: string;
|
|
53
|
-
coin: string;
|
|
54
|
-
collect: string;
|
|
55
|
-
share: string;
|
|
56
|
-
view: string;
|
|
57
|
-
danmaku: string;
|
|
58
|
-
cover: string;
|
|
59
|
-
duration: string;
|
|
60
|
-
size: string;
|
|
61
|
-
url: string;
|
|
62
|
-
};
|
|
60
|
+
fieldMap: DyKsFieldMap;
|
|
63
61
|
};
|
|
64
62
|
kuaishou: {
|
|
65
63
|
customApi: string;
|
|
66
64
|
apiKey: string;
|
|
67
|
-
fieldMap:
|
|
68
|
-
title: string;
|
|
69
|
-
author: string;
|
|
70
|
-
description: string;
|
|
71
|
-
like: string;
|
|
72
|
-
coin: string;
|
|
73
|
-
collect: string;
|
|
74
|
-
share: string;
|
|
75
|
-
view: string;
|
|
76
|
-
danmaku: string;
|
|
77
|
-
cover: string;
|
|
78
|
-
duration: string;
|
|
79
|
-
size: string;
|
|
80
|
-
url: string;
|
|
81
|
-
};
|
|
65
|
+
fieldMap: DyKsFieldMap;
|
|
82
66
|
};
|
|
83
67
|
}
|
|
84
68
|
export declare const Config: Schema<Config>;
|
|
85
69
|
export declare function apply(ctx: Context, config: Config): void;
|
|
70
|
+
export {};
|
package/lib/index.js
CHANGED
|
@@ -8,14 +8,12 @@ exports.apply = apply;
|
|
|
8
8
|
const koishi_1 = require("koishi");
|
|
9
9
|
const axios_1 = __importDefault(require("axios"));
|
|
10
10
|
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
-
exports.name = 'video-parser';
|
|
12
|
-
// 简化 Schema 定义,避免类型冲突
|
|
11
|
+
exports.name = 'video-parser-all';
|
|
13
12
|
exports.Config = koishi_1.Schema.object({
|
|
14
13
|
enable: koishi_1.Schema.boolean().default(true).description('开启解析功能'),
|
|
15
14
|
showWaitingTip: koishi_1.Schema.boolean().default(true).description('是否返回等待提示'),
|
|
16
15
|
waitingTipText: koishi_1.Schema.string().default('正在解析视频链接...可能需要稍等一下...').description('等待提示文字内容'),
|
|
17
|
-
parserSource: koishi_1.Schema.string().default('
|
|
18
|
-
returnComponents: koishi_1.Schema.array(String).default(['title', 'author', 'cover']).description('返回内容组件'),
|
|
16
|
+
parserSource: koishi_1.Schema.string().default('builtin').description('解析来源:builtin(内置免费API) / custom(自定义API)'),
|
|
19
17
|
allowBVAVParse: koishi_1.Schema.boolean().default(true).description('允许BV/AV号解析'),
|
|
20
18
|
sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接处理间隔(秒)'),
|
|
21
19
|
minVideoDuration: koishi_1.Schema.number().default(0).description('最小时长(分钟)'),
|
|
@@ -25,234 +23,377 @@ exports.Config = koishi_1.Schema.object({
|
|
|
25
23
|
longVideoTip: koishi_1.Schema.string().default('视频太长啦!去B站看吧~').description('过长提示'),
|
|
26
24
|
longVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过长视频图文解析'),
|
|
27
25
|
maxFileSize: koishi_1.Schema.number().default(50).description('最大文件大小(MB)'),
|
|
28
|
-
imageParseFormat: koishi_1.Schema.string()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
imageParseFormat: koishi_1.Schema.string()
|
|
27
|
+
.default(`\
|
|
28
|
+
${'='.repeat(40)}
|
|
29
|
+
标题:${'${标题}'}
|
|
30
|
+
UP主:${'${UP主}'}
|
|
31
|
+
简介:${'${简介}'}
|
|
32
|
+
${'='.repeat(40)}
|
|
33
|
+
点赞:${'${点赞}'} 投币:${'${投币}'}
|
|
34
|
+
收藏:${'${收藏}'} 转发:${'${转发}'}
|
|
35
|
+
观看:${'${观看}'} 弹幕:${'${弹幕}'}
|
|
36
|
+
${'='.repeat(40)}
|
|
37
|
+
${'${封面}'}
|
|
38
|
+
`)
|
|
39
|
+
.description(`图文解析的返回格式(支持换行)
|
|
40
|
+
注意变量格式,以及变量名称:
|
|
41
|
+
- ${'${标题}'} / ${'${UP主}'} / ${'${简介}'}
|
|
42
|
+
- ${'${点赞}'} / ${'${投币}'} / ${'${收藏}'} / ${'${转发}'}
|
|
43
|
+
- ${'${观看}'} / ${'${弹幕}'} / ${'${封面}'}
|
|
44
|
+
- ${'${tab}'} 制表符 | ${'${~~~}'} 分割线
|
|
45
|
+
抖音/快手会自动过滤点赞/投币等统计字段`),
|
|
46
|
+
showVideoLink: koishi_1.Schema.boolean().default(true).description('在末尾显示视频的链接地址(推荐开启)'),
|
|
47
|
+
maxDescLength: koishi_1.Schema.number().default(100).description('视频的简介最大的字符长度'),
|
|
48
|
+
enableMergeForward: koishi_1.Schema.boolean().default(false).description('是否开启合并转发 仅支持 onebot 适配器 其他平台开启 无效'),
|
|
49
|
+
downloadBeforeSend: koishi_1.Schema.boolean().default(false).description('是否将视频链接下载后再发送 (以解决部分onebot协议端的问题)'),
|
|
50
|
+
messageBufferDelay: koishi_1.Schema.number().default(1).description('消息接收缓冲延迟(秒),避免重复解析'),
|
|
51
|
+
userAgent: koishi_1.Schema.string().default('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36').description('所有 API 请求所用的 User-Agent'),
|
|
52
|
+
// 内置免费API配置
|
|
53
|
+
builtinApi: koishi_1.Schema.object({
|
|
54
|
+
timeout: koishi_1.Schema.number().default(10000).description('内置API请求超时时间(毫秒)'),
|
|
55
|
+
retryCount: koishi_1.Schema.number().default(2).description('内置API失败重试次数')
|
|
56
|
+
}).description('内置免费解析API配置'),
|
|
57
|
+
// 自定义API配置(备用)
|
|
36
58
|
bilibili: koishi_1.Schema.object({
|
|
37
|
-
customApi: koishi_1.Schema.string().default('').description('B
|
|
59
|
+
customApi: koishi_1.Schema.string().default('').description('B站自定义解析API地址(留空使用内置API)'),
|
|
38
60
|
apiKey: koishi_1.Schema.string().default('').description('B站API密钥'),
|
|
39
61
|
fieldMap: koishi_1.Schema.object({
|
|
40
|
-
title: koishi_1.Schema.string().default('title'),
|
|
41
|
-
author: koishi_1.Schema.string().default('author'),
|
|
42
|
-
description: koishi_1.Schema.string().default('desc'),
|
|
43
|
-
like: koishi_1.Schema.string().default('likeCount'),
|
|
44
|
-
coin: koishi_1.Schema.string().default('coinCount'),
|
|
45
|
-
collect: koishi_1.Schema.string().default('collectCount'),
|
|
46
|
-
share: koishi_1.Schema.string().default('shareCount'),
|
|
47
|
-
view: koishi_1.Schema.string().default('viewCount'),
|
|
48
|
-
danmaku: koishi_1.Schema.string().default('danmakuCount'),
|
|
49
|
-
cover: koishi_1.Schema.string().default('cover'),
|
|
50
|
-
duration: koishi_1.Schema.string().default('duration'),
|
|
51
|
-
size: koishi_1.Schema.string().default('size'),
|
|
52
|
-
url: koishi_1.Schema.string().default('video_url')
|
|
53
|
-
}).description('B
|
|
54
|
-
}).description('B
|
|
62
|
+
title: koishi_1.Schema.string().default('title').description('标题字段映射'),
|
|
63
|
+
author: koishi_1.Schema.string().default('author').description('UP主字段映射'),
|
|
64
|
+
description: koishi_1.Schema.string().default('desc').description('简介字段映射'),
|
|
65
|
+
like: koishi_1.Schema.string().default('likeCount').description('点赞字段映射'),
|
|
66
|
+
coin: koishi_1.Schema.string().default('coinCount').description('投币字段映射'),
|
|
67
|
+
collect: koishi_1.Schema.string().default('collectCount').description('收藏字段映射'),
|
|
68
|
+
share: koishi_1.Schema.string().default('shareCount').description('转发字段映射'),
|
|
69
|
+
view: koishi_1.Schema.string().default('viewCount').description('观看字段映射'),
|
|
70
|
+
danmaku: koishi_1.Schema.string().default('danmakuCount').description('弹幕字段映射'),
|
|
71
|
+
cover: koishi_1.Schema.string().default('cover').description('封面字段映射'),
|
|
72
|
+
duration: koishi_1.Schema.string().default('duration').description('时长字段映射'),
|
|
73
|
+
size: koishi_1.Schema.string().default('size').description('文件大小字段映射'),
|
|
74
|
+
url: koishi_1.Schema.string().default('video_url').description('视频链接字段映射')
|
|
75
|
+
}).description('B站返回字段映射')
|
|
76
|
+
}).description('B站自定义API配置(留空使用内置免费API)'),
|
|
55
77
|
douyin: koishi_1.Schema.object({
|
|
56
|
-
customApi: koishi_1.Schema.string().default('').description('
|
|
78
|
+
customApi: koishi_1.Schema.string().default('').description('抖音自定义解析API地址(留空使用内置API)'),
|
|
57
79
|
apiKey: koishi_1.Schema.string().default('').description('抖音API密钥'),
|
|
58
80
|
fieldMap: koishi_1.Schema.object({
|
|
59
|
-
title: koishi_1.Schema.string().default('title'),
|
|
60
|
-
author: koishi_1.Schema.string().default('author'),
|
|
61
|
-
description: koishi_1.Schema.string().default('desc'),
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
cover: koishi_1.Schema.string().default('cover'),
|
|
69
|
-
duration: koishi_1.Schema.string().default('duration'),
|
|
70
|
-
size: koishi_1.Schema.string().default('size'),
|
|
71
|
-
url: koishi_1.Schema.string().default('video_url')
|
|
72
|
-
}).description('抖音字段映射')
|
|
73
|
-
}).description('抖音配置'),
|
|
81
|
+
title: koishi_1.Schema.string().default('title').description('标题字段映射'),
|
|
82
|
+
author: koishi_1.Schema.string().default('author').description('作者字段映射'),
|
|
83
|
+
description: koishi_1.Schema.string().default('desc').description('简介字段映射'),
|
|
84
|
+
cover: koishi_1.Schema.string().default('cover').description('封面字段映射'),
|
|
85
|
+
duration: koishi_1.Schema.string().default('duration').description('时长字段映射'),
|
|
86
|
+
size: koishi_1.Schema.string().default('size').description('文件大小字段映射'),
|
|
87
|
+
url: koishi_1.Schema.string().default('video_url').description('视频链接字段映射')
|
|
88
|
+
}).description('抖音返回字段映射')
|
|
89
|
+
}).description('抖音自定义API配置(留空使用内置免费API)'),
|
|
74
90
|
kuaishou: koishi_1.Schema.object({
|
|
75
|
-
customApi: koishi_1.Schema.string().default('').description('
|
|
91
|
+
customApi: koishi_1.Schema.string().default('').description('快手自定义解析API地址(留空使用内置API)'),
|
|
76
92
|
apiKey: koishi_1.Schema.string().default('').description('快手API密钥'),
|
|
77
93
|
fieldMap: koishi_1.Schema.object({
|
|
78
|
-
title: koishi_1.Schema.string().default('title'),
|
|
79
|
-
author: koishi_1.Schema.string().default('author'),
|
|
80
|
-
description: koishi_1.Schema.string().default('desc'),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
cover: koishi_1.Schema.string().default('cover'),
|
|
88
|
-
duration: koishi_1.Schema.string().default('duration'),
|
|
89
|
-
size: koishi_1.Schema.string().default('size'),
|
|
90
|
-
url: koishi_1.Schema.string().default('video_url')
|
|
91
|
-
}).description('快手字段映射')
|
|
92
|
-
}).description('快手配置')
|
|
94
|
+
title: koishi_1.Schema.string().default('title').description('标题字段映射'),
|
|
95
|
+
author: koishi_1.Schema.string().default('author').description('作者字段映射'),
|
|
96
|
+
description: koishi_1.Schema.string().default('desc').description('简介字段映射'),
|
|
97
|
+
cover: koishi_1.Schema.string().default('cover').description('封面字段映射'),
|
|
98
|
+
duration: koishi_1.Schema.string().default('duration').description('时长字段映射'),
|
|
99
|
+
size: koishi_1.Schema.string().default('size').description('文件大小字段映射'),
|
|
100
|
+
url: koishi_1.Schema.string().default('video_url').description('视频链接字段映射')
|
|
101
|
+
}).description('快手返回字段映射')
|
|
102
|
+
}).description('快手自定义API配置(留空使用内置免费API)')
|
|
93
103
|
});
|
|
104
|
+
// 内置免费解析API列表(自动切换重试)
|
|
105
|
+
const BUILTIN_APIS = {
|
|
106
|
+
// API 1: 稳定免费的视频解析接口
|
|
107
|
+
api1: {
|
|
108
|
+
url: 'https://jx.jsonapi.cn/api.php',
|
|
109
|
+
params: (url) => ({ url, type: 'json' })
|
|
110
|
+
},
|
|
111
|
+
// API 2: 备用解析接口
|
|
112
|
+
api2: {
|
|
113
|
+
url: 'https://api.vvhan.com/api/video',
|
|
114
|
+
params: (url) => ({ url, type: 'json' })
|
|
115
|
+
},
|
|
116
|
+
// API 3: 兜底解析接口
|
|
117
|
+
api3: {
|
|
118
|
+
url: 'https://www.xiaoyangapi.com/api/video/analysis',
|
|
119
|
+
params: (url) => ({ url, appid: '1001', appkey: 'xiaoyangapi' })
|
|
120
|
+
}
|
|
121
|
+
};
|
|
94
122
|
const processedLinks = new Map();
|
|
95
123
|
const messageQueue = new Map();
|
|
96
124
|
function apply(ctx, config) {
|
|
125
|
+
// 创建请求实例
|
|
97
126
|
const request = axios_1.default.create({
|
|
98
|
-
headers: {
|
|
99
|
-
|
|
127
|
+
headers: {
|
|
128
|
+
'User-Agent': config.userAgent,
|
|
129
|
+
'Referer': 'https://www.baidu.com',
|
|
130
|
+
'Origin': 'https://www.baidu.com'
|
|
131
|
+
},
|
|
132
|
+
timeout: config.builtinApi.timeout,
|
|
133
|
+
decompress: true
|
|
100
134
|
});
|
|
135
|
+
// 内置API解析核心函数(自动重试)
|
|
136
|
+
async function parseWithBuiltinApi(url, platform, retry = 0) {
|
|
137
|
+
const apiList = Object.values(BUILTIN_APIS);
|
|
138
|
+
const currentApi = apiList[retry % apiList.length];
|
|
139
|
+
try {
|
|
140
|
+
const res = await request.get(currentApi.url, {
|
|
141
|
+
params: currentApi.params(url),
|
|
142
|
+
timeout: config.builtinApi.timeout
|
|
143
|
+
});
|
|
144
|
+
const data = res.data;
|
|
145
|
+
if (!data || (data.code && data.code !== 200 && data.code !== 0)) {
|
|
146
|
+
throw new Error(`API返回错误:${data.msg || '未知错误'}`);
|
|
147
|
+
}
|
|
148
|
+
// 统一返回格式
|
|
149
|
+
const result = {
|
|
150
|
+
title: data.title || data.video_title || '',
|
|
151
|
+
author: data.author || data.nickname || data.up主 || '',
|
|
152
|
+
description: data.desc || data.description || data.content || '',
|
|
153
|
+
cover: data.cover || data.cover_url || data.thumbnail || '',
|
|
154
|
+
url: data.url || data.video_url || data.play || '',
|
|
155
|
+
duration: data.duration || data.time || 0,
|
|
156
|
+
size: data.size || 0
|
|
157
|
+
};
|
|
158
|
+
// B站额外补充统计字段
|
|
159
|
+
if (platform === 'bilibili') {
|
|
160
|
+
return {
|
|
161
|
+
...result,
|
|
162
|
+
like: data.like || data.likeCount || 0,
|
|
163
|
+
coin: data.coin || data.coinCount || 0,
|
|
164
|
+
collect: data.collect || data.collectCount || 0,
|
|
165
|
+
share: data.share || data.shareCount || 0,
|
|
166
|
+
view: data.playCount || data.view || data.views || 0,
|
|
167
|
+
danmaku: data.danmaku || data.comment || 0
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
// 重试逻辑
|
|
174
|
+
if (retry < config.builtinApi.retryCount) {
|
|
175
|
+
ctx.logger.warn(`内置API ${retry + 1} 解析失败:${e.message},重试下一个API...`);
|
|
176
|
+
return parseWithBuiltinApi(url, platform, retry + 1);
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`所有内置API解析失败:${e.message}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// 自定义API解析函数
|
|
182
|
+
async function parseWithCustomApi(url, platform) {
|
|
183
|
+
const platformConfig = config[platform];
|
|
184
|
+
if (!platformConfig.customApi) {
|
|
185
|
+
throw new Error(`${platform} 自定义API地址未配置`);
|
|
186
|
+
}
|
|
187
|
+
const res = await request.get(platformConfig.customApi, {
|
|
188
|
+
params: { url, key: platformConfig.apiKey },
|
|
189
|
+
timeout: config.builtinApi.timeout
|
|
190
|
+
});
|
|
191
|
+
const data = res.data.data || res.data;
|
|
192
|
+
const getValue = (obj, path) => {
|
|
193
|
+
return path.split('.').reduce((o, k) => o?.[k], obj);
|
|
194
|
+
};
|
|
195
|
+
if (platform === 'bilibili') {
|
|
196
|
+
const fieldMap = platformConfig.fieldMap;
|
|
197
|
+
return {
|
|
198
|
+
title: getValue(data, fieldMap.title) || '',
|
|
199
|
+
author: getValue(data, fieldMap.author) || '',
|
|
200
|
+
description: getValue(data, fieldMap.description) || '',
|
|
201
|
+
like: getValue(data, fieldMap.like) || 0,
|
|
202
|
+
coin: getValue(data, fieldMap.coin) || 0,
|
|
203
|
+
collect: getValue(data, fieldMap.collect) || 0,
|
|
204
|
+
share: getValue(data, fieldMap.share) || 0,
|
|
205
|
+
view: getValue(data, fieldMap.view) || 0,
|
|
206
|
+
danmaku: getValue(data, fieldMap.danmaku) || 0,
|
|
207
|
+
cover: getValue(data, fieldMap.cover) || '',
|
|
208
|
+
duration: getValue(data, fieldMap.duration) || 0,
|
|
209
|
+
size: getValue(data, fieldMap.size) || 0,
|
|
210
|
+
url: getValue(data, fieldMap.url) || ''
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const fieldMap = platformConfig.fieldMap;
|
|
215
|
+
return {
|
|
216
|
+
title: getValue(data, fieldMap.title) || '',
|
|
217
|
+
author: getValue(data, fieldMap.author) || '',
|
|
218
|
+
description: getValue(data, fieldMap.description) || '',
|
|
219
|
+
cover: getValue(data, fieldMap.cover) || '',
|
|
220
|
+
duration: getValue(data, fieldMap.duration) || 0,
|
|
221
|
+
size: getValue(data, fieldMap.size) || 0,
|
|
222
|
+
url: getValue(data, fieldMap.url) || ''
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// 核心解析函数
|
|
101
227
|
async function parseVideo(url, session) {
|
|
102
228
|
if (!config.enable)
|
|
103
229
|
return;
|
|
230
|
+
// 去重逻辑
|
|
104
231
|
const now = Date.now();
|
|
105
232
|
const linkHash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
106
|
-
if (processedLinks.has(linkHash) && now - processedLinks.get(linkHash) < config.sameLinkInterval * 1000)
|
|
233
|
+
if (processedLinks.has(linkHash) && now - processedLinks.get(linkHash) < config.sameLinkInterval * 1000) {
|
|
234
|
+
ctx.logger.debug(`相同链接 ${url} 短时间内已解析,跳过`);
|
|
107
235
|
return;
|
|
236
|
+
}
|
|
108
237
|
processedLinks.set(linkHash, now);
|
|
238
|
+
// 发送等待提示
|
|
109
239
|
if (config.showWaitingTip)
|
|
110
240
|
await session.send(config.waitingTipText);
|
|
241
|
+
// 判断平台
|
|
111
242
|
let platform = '';
|
|
112
|
-
if (url.includes('bilibili') || /(BV|AV)\w+/.test(url))
|
|
243
|
+
if (url.includes('bilibili') || /(BV|AV)\w+/.test(url)) {
|
|
113
244
|
platform = 'bilibili';
|
|
114
|
-
|
|
245
|
+
// BV/AV号补全链接
|
|
246
|
+
if (/^(BV|AV)/i.test(url)) {
|
|
247
|
+
url = `https://www.bilibili.com/video/${url}`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else if (url.includes('douyin') || url.includes('dy') || url.includes('抖音')) {
|
|
115
251
|
platform = 'douyin';
|
|
116
|
-
|
|
252
|
+
}
|
|
253
|
+
else if (url.includes('kuaishou') || url.includes('ks') || url.includes('快手')) {
|
|
117
254
|
platform = 'kuaishou';
|
|
118
|
-
|
|
119
|
-
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
await session.send('❌ 不支持的链接类型(仅支持B站/抖音/快手)');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// 解析视频信息
|
|
120
261
|
let videoInfo;
|
|
121
262
|
try {
|
|
122
|
-
if (config.parserSource === '
|
|
123
|
-
videoInfo = await
|
|
263
|
+
if (config.parserSource === 'custom' && config[platform].customApi) {
|
|
264
|
+
videoInfo = await parseWithCustomApi(url, platform);
|
|
124
265
|
}
|
|
125
266
|
else {
|
|
126
|
-
videoInfo = await
|
|
267
|
+
videoInfo = await parseWithBuiltinApi(url, platform);
|
|
268
|
+
}
|
|
269
|
+
// 校验解析结果
|
|
270
|
+
if (!videoInfo.url) {
|
|
271
|
+
throw new Error('解析失败:未获取到视频链接');
|
|
272
|
+
}
|
|
273
|
+
if (!videoInfo.title) {
|
|
274
|
+
videoInfo.title = '未知标题';
|
|
127
275
|
}
|
|
128
276
|
}
|
|
129
277
|
catch (e) {
|
|
130
|
-
await session.send(
|
|
278
|
+
await session.send(`❌ 解析失败:${e.message}`);
|
|
279
|
+
ctx.logger.error(`解析 ${url} 失败:`, e);
|
|
131
280
|
return;
|
|
132
281
|
}
|
|
282
|
+
// 时长过滤
|
|
133
283
|
const duration = videoInfo.duration / 60;
|
|
134
284
|
if (duration < config.minVideoDuration) {
|
|
135
|
-
|
|
285
|
+
const msg = config.shortVideoTip || '❌ 视频时长过短,不解析';
|
|
286
|
+
return config.shortVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(msg);
|
|
136
287
|
}
|
|
137
288
|
if (duration > config.maxVideoDuration) {
|
|
138
|
-
|
|
289
|
+
const msg = config.longVideoTip || '❌ 视频时长过长,不解析';
|
|
290
|
+
return config.longVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(msg);
|
|
139
291
|
}
|
|
292
|
+
// 大小过滤
|
|
140
293
|
if (config.maxFileSize > 0 && videoInfo.size > config.maxFileSize * 1024 * 1024) {
|
|
141
|
-
|
|
294
|
+
await session.send(`❌ 视频文件过大(>${config.maxFileSize}MB),无法发送`);
|
|
295
|
+
return;
|
|
142
296
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
async function parsePublicApi(url, platform) {
|
|
146
|
-
const res = await request.get('https://api.obtaindown.com/obApi/api/analysis', {
|
|
147
|
-
params: { url, key: config.publicApiKey }
|
|
148
|
-
});
|
|
149
|
-
const data = res.data.data || {};
|
|
150
|
-
return {
|
|
151
|
-
title: data.title || '',
|
|
152
|
-
author: data.author || data.userName || '',
|
|
153
|
-
description: data.desc || data.description || '',
|
|
154
|
-
like: data.likeCount || 0,
|
|
155
|
-
coin: data.coinCount || 0,
|
|
156
|
-
collect: data.collectCount || 0,
|
|
157
|
-
share: data.shareCount || 0,
|
|
158
|
-
view: data.viewCount || data.playCount || 0,
|
|
159
|
-
danmaku: data.danmakuCount || data.commentCount || 0,
|
|
160
|
-
cover: data.cover || data.thumbnail_url || '',
|
|
161
|
-
duration: data.duration || 0,
|
|
162
|
-
size: data.size || 0,
|
|
163
|
-
url: data.video_url || data.download_url || ''
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
async function parseCustomApi(url, platform) {
|
|
167
|
-
const platformConfig = config[platform];
|
|
168
|
-
const res = await request.get(platformConfig.customApi, {
|
|
169
|
-
params: { url, key: platformConfig.apiKey }
|
|
170
|
-
});
|
|
171
|
-
const data = res.data.data || res.data;
|
|
172
|
-
const fieldMap = platformConfig.fieldMap;
|
|
173
|
-
// 简化嵌套字段取值逻辑
|
|
174
|
-
const getValue = (obj, path) => {
|
|
175
|
-
return path.split('.').reduce((o, k) => o?.[k], obj);
|
|
176
|
-
};
|
|
177
|
-
return {
|
|
178
|
-
title: getValue(data, fieldMap.title) || '',
|
|
179
|
-
author: getValue(data, fieldMap.author) || '',
|
|
180
|
-
description: getValue(data, fieldMap.description) || '',
|
|
181
|
-
like: getValue(data, fieldMap.like) || 0,
|
|
182
|
-
coin: getValue(data, fieldMap.coin) || 0,
|
|
183
|
-
collect: getValue(data, fieldMap.collect) || 0,
|
|
184
|
-
share: getValue(data, fieldMap.share) || 0,
|
|
185
|
-
view: getValue(data, fieldMap.view) || 0,
|
|
186
|
-
danmaku: getValue(data, fieldMap.danmaku) || 0,
|
|
187
|
-
cover: getValue(data, fieldMap.cover) || '',
|
|
188
|
-
duration: getValue(data, fieldMap.duration) || 0,
|
|
189
|
-
size: getValue(data, fieldMap.size) || 0,
|
|
190
|
-
url: getValue(data, fieldMap.url) || ''
|
|
191
|
-
};
|
|
297
|
+
// 生成回复
|
|
298
|
+
await generateReply(videoInfo, session, platform);
|
|
192
299
|
}
|
|
193
|
-
|
|
300
|
+
// 生成图文解析内容
|
|
301
|
+
async function generateImageParse(videoInfo, session, platform) {
|
|
194
302
|
let content = config.imageParseFormat;
|
|
195
303
|
const desc = videoInfo.description.length > config.maxDescLength
|
|
196
304
|
? videoInfo.description.slice(0, config.maxDescLength) + '...'
|
|
197
305
|
: videoInfo.description;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.replace(/\${
|
|
201
|
-
.replace(/\${
|
|
202
|
-
.replace(/\${
|
|
203
|
-
.replace(/\${收藏}/g, videoInfo.collect)
|
|
204
|
-
.replace(/\${转发}/g, videoInfo.share)
|
|
205
|
-
.replace(/\${观看}/g, videoInfo.view)
|
|
206
|
-
.replace(/\${弹幕}/g, videoInfo.danmaku)
|
|
207
|
-
.replace(/\${封面}/g, videoInfo.cover)
|
|
306
|
+
// 基础变量替换
|
|
307
|
+
content = content.replace(/\${标题}/g, videoInfo.title || '')
|
|
308
|
+
.replace(/\${UP主}/g, videoInfo.author || '')
|
|
309
|
+
.replace(/\${简介}/g, desc || '')
|
|
310
|
+
.replace(/\${封面}/g, videoInfo.cover || '')
|
|
208
311
|
.replace(/\${tab}/g, '\t');
|
|
312
|
+
// 仅B站替换统计字段
|
|
313
|
+
if (platform === 'bilibili') {
|
|
314
|
+
content = content.replace(/\${点赞}/g, videoInfo.like?.toString() || '0')
|
|
315
|
+
.replace(/\${投币}/g, videoInfo.coin?.toString() || '0')
|
|
316
|
+
.replace(/\${收藏}/g, videoInfo.collect?.toString() || '0')
|
|
317
|
+
.replace(/\${转发}/g, videoInfo.share?.toString() || '0')
|
|
318
|
+
.replace(/\${观看}/g, videoInfo.view?.toString() || '0')
|
|
319
|
+
.replace(/\${弹幕}/g, videoInfo.danmaku?.toString() || '0');
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// 抖音/快手清空统计字段
|
|
323
|
+
content = content.replace(/\${点赞}/g, '')
|
|
324
|
+
.replace(/\${投币}/g, '')
|
|
325
|
+
.replace(/\${收藏}/g, '')
|
|
326
|
+
.replace(/\${转发}/g, '')
|
|
327
|
+
.replace(/\${观看}/g, '')
|
|
328
|
+
.replace(/\${弹幕}/g, '');
|
|
329
|
+
// 清理空行和多余符号
|
|
330
|
+
content = content.replace(/点赞:\s*\t*\s*投币:\s*\n/g, '')
|
|
331
|
+
.replace(/收藏:\s*\t*\s*转发:\s*\n/g, '')
|
|
332
|
+
.replace(/观看:\s*\t*\s*弹幕:\s*\n/g, '')
|
|
333
|
+
.replace(/={2,}/g, (match) => match.trim() ? match : '')
|
|
334
|
+
.replace(/\n+/g, '\n').trim();
|
|
335
|
+
}
|
|
336
|
+
// 发送解析内容
|
|
209
337
|
const parts = content.split(/\${~~~}/);
|
|
210
338
|
for (const p of parts) {
|
|
211
339
|
if (p.trim())
|
|
212
340
|
await session.send(p.trim());
|
|
213
341
|
}
|
|
214
|
-
if (config.showVideoLink)
|
|
215
|
-
await session.send(videoInfo.url);
|
|
342
|
+
if (config.showVideoLink && videoInfo.url) {
|
|
343
|
+
await session.send(`📥 视频链接:${videoInfo.url}`);
|
|
344
|
+
}
|
|
216
345
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
346
|
+
// 生成最终回复
|
|
347
|
+
async function generateReply(videoInfo, session, platform) {
|
|
348
|
+
try {
|
|
349
|
+
if (config.enableMergeForward) {
|
|
350
|
+
const msgs = [];
|
|
351
|
+
if (videoInfo.title)
|
|
352
|
+
msgs.push(koishi_1.h.text(`📌 标题:${videoInfo.title}`));
|
|
353
|
+
if (videoInfo.author)
|
|
354
|
+
msgs.push(koishi_1.h.text(`👤 作者:${videoInfo.author}`));
|
|
355
|
+
if (videoInfo.cover)
|
|
356
|
+
msgs.push(koishi_1.h.image(videoInfo.cover));
|
|
357
|
+
if (videoInfo.url) {
|
|
358
|
+
if (config.downloadBeforeSend) {
|
|
359
|
+
try {
|
|
360
|
+
const response = await request.get(videoInfo.url, {
|
|
361
|
+
responseType: 'stream',
|
|
362
|
+
timeout: 60000
|
|
363
|
+
});
|
|
364
|
+
msgs.push(koishi_1.h.video(response.data));
|
|
365
|
+
}
|
|
366
|
+
catch (e) {
|
|
367
|
+
msgs.push(koishi_1.h.text(`📥 视频链接:${videoInfo.url}`));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
msgs.push(koishi_1.h.text(`📥 视频链接:${videoInfo.url}`));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
await session.send(msgs);
|
|
230
375
|
}
|
|
231
376
|
else {
|
|
232
|
-
|
|
377
|
+
await generateImageParse(videoInfo, session, platform);
|
|
233
378
|
}
|
|
234
|
-
await session.send(msgs);
|
|
235
379
|
}
|
|
236
|
-
|
|
237
|
-
await
|
|
238
|
-
|
|
239
|
-
const stream = await request.get(videoInfo.url, { responseType: 'stream' });
|
|
240
|
-
await session.send(koishi_1.h.video(stream.data));
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
await session.send(koishi_1.h.video(videoInfo.url));
|
|
244
|
-
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
await session.send(`❌ 消息发送失败:${e.message || '请稍后重试'}`);
|
|
382
|
+
ctx.logger.error(`发送消息失败:`, e);
|
|
245
383
|
}
|
|
246
384
|
}
|
|
385
|
+
// 监听消息
|
|
247
386
|
ctx.on('message', async (session) => {
|
|
248
387
|
if (!config.enable)
|
|
249
388
|
return;
|
|
250
389
|
const content = session.content.trim();
|
|
390
|
+
// 匹配视频链接(支持短链接/长链接/BV/AV号)
|
|
251
391
|
const reg = /(https?:\/\/\S+)|(BV\w+)|(AV\d+)/gi;
|
|
252
392
|
const matches = [...content.matchAll(reg)];
|
|
253
393
|
if (!matches.length)
|
|
254
394
|
return;
|
|
255
395
|
const uid = session.userId;
|
|
396
|
+
// 消息缓冲,避免重复解析
|
|
256
397
|
if (config.messageBufferDelay > 0) {
|
|
257
398
|
if (!messageQueue.has(uid)) {
|
|
258
399
|
messageQueue.set(uid, []);
|
|
@@ -270,12 +411,20 @@ function apply(ctx, config) {
|
|
|
270
411
|
await parseVideo(m[0], session);
|
|
271
412
|
}
|
|
272
413
|
});
|
|
273
|
-
//
|
|
414
|
+
// 定时清理过期链接记录
|
|
274
415
|
setInterval(() => {
|
|
275
416
|
const now = Date.now();
|
|
417
|
+
let count = 0;
|
|
276
418
|
for (const [k, t] of processedLinks) {
|
|
277
|
-
if (now - t > 86400000)
|
|
419
|
+
if (now - t > 86400000) {
|
|
278
420
|
processedLinks.delete(k);
|
|
421
|
+
count++;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (count > 0) {
|
|
425
|
+
ctx.logger.debug(`清理了 ${count} 条过期链接记录`);
|
|
279
426
|
}
|
|
280
427
|
}, 3600000);
|
|
428
|
+
// 插件启动日志
|
|
429
|
+
ctx.logger.info(`✅ 视频解析插件已启动(解析来源:${config.parserSource})`);
|
|
281
430
|
}
|