koishi-plugin-video-parser-all 0.0.3 → 0.0.4
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 +3 -45
- package/lib/index.js +130 -260
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -1,34 +1,9 @@
|
|
|
1
1
|
import { Context, Schema } from 'koishi';
|
|
2
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
|
-
}
|
|
27
3
|
export interface Config {
|
|
28
4
|
enable: boolean;
|
|
29
5
|
showWaitingTip: boolean;
|
|
30
6
|
waitingTipText: string;
|
|
31
|
-
parserSource: string;
|
|
32
7
|
allowBVAVParse: boolean;
|
|
33
8
|
sameLinkInterval: number;
|
|
34
9
|
minVideoDuration: number;
|
|
@@ -37,34 +12,17 @@ export interface Config {
|
|
|
37
12
|
maxVideoDuration: number;
|
|
38
13
|
longVideoTip: string;
|
|
39
14
|
longVideoUseImageParse: boolean;
|
|
40
|
-
maxFileSize: number;
|
|
41
15
|
imageParseFormat: string;
|
|
42
16
|
showVideoLink: boolean;
|
|
43
17
|
maxDescLength: number;
|
|
44
18
|
enableMergeForward: boolean;
|
|
45
19
|
downloadBeforeSend: boolean;
|
|
46
20
|
messageBufferDelay: number;
|
|
47
|
-
|
|
48
|
-
|
|
21
|
+
publicApi: {
|
|
22
|
+
enable: boolean;
|
|
23
|
+
url: string;
|
|
49
24
|
timeout: number;
|
|
50
|
-
retryCount: number;
|
|
51
|
-
};
|
|
52
|
-
bilibili: {
|
|
53
|
-
customApi: string;
|
|
54
|
-
apiKey: string;
|
|
55
|
-
fieldMap: BilibiliFieldMap;
|
|
56
|
-
};
|
|
57
|
-
douyin: {
|
|
58
|
-
customApi: string;
|
|
59
|
-
apiKey: string;
|
|
60
|
-
fieldMap: DyKsFieldMap;
|
|
61
|
-
};
|
|
62
|
-
kuaishou: {
|
|
63
|
-
customApi: string;
|
|
64
|
-
apiKey: string;
|
|
65
|
-
fieldMap: DyKsFieldMap;
|
|
66
25
|
};
|
|
67
26
|
}
|
|
68
27
|
export declare const Config: Schema<Config>;
|
|
69
28
|
export declare function apply(ctx: Context, config: Config): void;
|
|
70
|
-
export {};
|
package/lib/index.js
CHANGED
|
@@ -12,215 +12,130 @@ exports.name = 'video-parser-all';
|
|
|
12
12
|
exports.Config = koishi_1.Schema.object({
|
|
13
13
|
enable: koishi_1.Schema.boolean().default(true).description('开启解析功能'),
|
|
14
14
|
showWaitingTip: koishi_1.Schema.boolean().default(true).description('是否返回等待提示'),
|
|
15
|
-
waitingTipText: koishi_1.Schema.string().default('
|
|
16
|
-
parserSource: koishi_1.Schema.string().default('builtin').description('解析来源:builtin(内置免费API) / custom(自定义API)'),
|
|
15
|
+
waitingTipText: koishi_1.Schema.string().default('正在解析视频链接...').description('等待提示文字内容'),
|
|
17
16
|
allowBVAVParse: koishi_1.Schema.boolean().default(true).description('允许BV/AV号解析'),
|
|
18
17
|
sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接处理间隔(秒)'),
|
|
19
18
|
minVideoDuration: koishi_1.Schema.number().default(0).description('最小时长(分钟)'),
|
|
20
|
-
shortVideoTip: koishi_1.Schema.string().default('
|
|
19
|
+
shortVideoTip: koishi_1.Schema.string().default('视频时长过短,不解析~').description('过短提示'),
|
|
21
20
|
shortVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过短视频图文解析'),
|
|
22
|
-
maxVideoDuration: koishi_1.Schema.number().default(
|
|
23
|
-
longVideoTip: koishi_1.Schema.string().default('
|
|
21
|
+
maxVideoDuration: koishi_1.Schema.number().default(60).description('最大时长(分钟)'),
|
|
22
|
+
longVideoTip: koishi_1.Schema.string().default('视频时长过长,不解析~').description('过长提示'),
|
|
24
23
|
longVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过长视频图文解析'),
|
|
25
|
-
|
|
24
|
+
// 核心修改1:移除字段映射配置
|
|
25
|
+
// 核心修改2:超大输入框(通过多行默认值+textarea属性实现)
|
|
26
26
|
imageParseFormat: koishi_1.Schema.string()
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
${'
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
${'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- ${'${
|
|
44
|
-
- ${'${
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
bilibili: koishi_1.Schema.object({
|
|
59
|
-
customApi: koishi_1.Schema.string().default('').description('B站自定义解析API地址(留空使用内置API)'),
|
|
60
|
-
apiKey: koishi_1.Schema.string().default('').description('B站API密钥'),
|
|
61
|
-
fieldMap: koishi_1.Schema.object({
|
|
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)'),
|
|
77
|
-
douyin: koishi_1.Schema.object({
|
|
78
|
-
customApi: koishi_1.Schema.string().default('').description('抖音自定义解析API地址(留空使用内置API)'),
|
|
79
|
-
apiKey: koishi_1.Schema.string().default('').description('抖音API密钥'),
|
|
80
|
-
fieldMap: koishi_1.Schema.object({
|
|
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)'),
|
|
90
|
-
kuaishou: koishi_1.Schema.object({
|
|
91
|
-
customApi: koishi_1.Schema.string().default('').description('快手自定义解析API地址(留空使用内置API)'),
|
|
92
|
-
apiKey: koishi_1.Schema.string().default('').description('快手API密钥'),
|
|
93
|
-
fieldMap: koishi_1.Schema.object({
|
|
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)')
|
|
27
|
+
.role('textarea') // 关键:强制渲染为多行文本框
|
|
28
|
+
.default(`┌────────────────────────────────────────────────────────────────────┐
|
|
29
|
+
│ 视频解析信息 │
|
|
30
|
+
├────────────────────────────────────────────────────────────────────┤
|
|
31
|
+
│ 标题:${'${标题}'}
|
|
32
|
+
│ UP主:${'${UP主}'}
|
|
33
|
+
│ 简介:${'${简介}'}
|
|
34
|
+
├────────────────────────────────────────────────────────────────────┤
|
|
35
|
+
│ 点赞:${'${点赞}'} 投币:${'${投币}'} 收藏:${'${收藏}'}
|
|
36
|
+
│ 转发:${'${转发}'} 观看:${'${观看}'} 弹幕:${'${弹幕}'}
|
|
37
|
+
├────────────────────────────────────────────────────────────────────┤
|
|
38
|
+
│ 封面:${'${封面}'}
|
|
39
|
+
└────────────────────────────────────────────────────────────────────┘
|
|
40
|
+
📌 提示:抖音/快手会自动隐藏点赞/投币等统计字段`)
|
|
41
|
+
.description(`图文解析格式配置(超大输入框)
|
|
42
|
+
支持的变量:
|
|
43
|
+
- 基础:${'${标题}'} / ${'${UP主}'} / ${'${简介}'} / ${'${封面}'}
|
|
44
|
+
- B站专属:${'${点赞}'} / ${'${投币}'} / ${'${收藏}'} / ${'${转发}'} / ${'${观看}'} / ${'${弹幕}'}
|
|
45
|
+
- 特殊:${'${tab}'}(制表符)
|
|
46
|
+
抖音/快手会自动过滤B站专属变量,无需手动删除`),
|
|
47
|
+
showVideoLink: koishi_1.Schema.boolean().default(true).description('显示视频链接'),
|
|
48
|
+
maxDescLength: koishi_1.Schema.number().default(100).description('简介最大长度'),
|
|
49
|
+
enableMergeForward: koishi_1.Schema.boolean().default(false).description('合并转发(仅onebot)'),
|
|
50
|
+
downloadBeforeSend: koishi_1.Schema.boolean().default(false).description('下载后发送(避免1200错误)'),
|
|
51
|
+
messageBufferDelay: koishi_1.Schema.number().default(1).description('消息缓冲延迟(秒)'),
|
|
52
|
+
// 国内可访问的公益API配置
|
|
53
|
+
publicApi: koishi_1.Schema.object({
|
|
54
|
+
enable: koishi_1.Schema.boolean().default(true).description('启用国内公益API'),
|
|
55
|
+
url: koishi_1.Schema.string().default('https://api.btstu.cn/yanapi/yjx/').description('国内公益解析API地址'),
|
|
56
|
+
timeout: koishi_1.Schema.number().default(15000).description('API超时时间(毫秒)')
|
|
57
|
+
}).description('国内免费公益解析API(无需密钥)')
|
|
103
58
|
});
|
|
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
|
-
};
|
|
122
59
|
const processedLinks = new Map();
|
|
123
60
|
const messageQueue = new Map();
|
|
61
|
+
// 纯本地解析函数(兜底方案)
|
|
62
|
+
function parseLocal(url, platform) {
|
|
63
|
+
const result = {
|
|
64
|
+
title: '未知视频',
|
|
65
|
+
author: '未知作者',
|
|
66
|
+
description: '无简介',
|
|
67
|
+
like: 0,
|
|
68
|
+
coin: 0,
|
|
69
|
+
collect: 0,
|
|
70
|
+
share: 0,
|
|
71
|
+
view: 0,
|
|
72
|
+
danmaku: 0,
|
|
73
|
+
cover: '',
|
|
74
|
+
duration: 0,
|
|
75
|
+
url: url
|
|
76
|
+
};
|
|
77
|
+
// B站本地解析(提取BV号+生成封面)
|
|
78
|
+
if (platform === 'bilibili') {
|
|
79
|
+
const bvMatch = url.match(/(BV\w+|AV\d+)/);
|
|
80
|
+
if (bvMatch) {
|
|
81
|
+
const bvid = bvMatch[0];
|
|
82
|
+
result.title = `B站视频-${bvid}`;
|
|
83
|
+
result.cover = `https://i0.hdslb.com/bfs/archive/${bvid}.jpg`;
|
|
84
|
+
result.url = `https://www.bilibili.com/video/${bvid}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 抖音本地解析
|
|
88
|
+
else if (platform === 'douyin') {
|
|
89
|
+
result.title = '抖音视频';
|
|
90
|
+
result.cover = 'https://p3-passport.byteacctimg.com/img/user-avatar/8888888888888888~300x300.image';
|
|
91
|
+
}
|
|
92
|
+
// 快手本地解析
|
|
93
|
+
else if (platform === 'kuaishou') {
|
|
94
|
+
result.title = '快手视频';
|
|
95
|
+
result.cover = 'https://www.kuaishou.com/favicon.ico';
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
124
99
|
function apply(ctx, config) {
|
|
125
|
-
//
|
|
100
|
+
// 创建请求实例(适配国内环境)
|
|
126
101
|
const request = axios_1.default.create({
|
|
127
102
|
headers: {
|
|
128
|
-
'User-Agent':
|
|
129
|
-
'Referer': 'https://www.baidu.com'
|
|
130
|
-
'Origin': 'https://www.baidu.com'
|
|
103
|
+
'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',
|
|
104
|
+
'Referer': 'https://www.baidu.com'
|
|
131
105
|
},
|
|
132
|
-
timeout: config.
|
|
133
|
-
|
|
106
|
+
timeout: config.publicApi.timeout,
|
|
107
|
+
// 禁用SSL验证(解决部分国内API证书问题)
|
|
108
|
+
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
|
|
134
109
|
});
|
|
135
|
-
//
|
|
136
|
-
async function
|
|
137
|
-
const apiList = Object.values(BUILTIN_APIS);
|
|
138
|
-
const currentApi = apiList[retry % apiList.length];
|
|
110
|
+
// 国内公益API解析函数
|
|
111
|
+
async function parsePublicApi(url, platform) {
|
|
139
112
|
try {
|
|
140
|
-
const res = await request.get(
|
|
141
|
-
params:
|
|
142
|
-
timeout: config.
|
|
113
|
+
const res = await request.get(config.publicApi.url, {
|
|
114
|
+
params: { url: encodeURIComponent(url), n: '123456' },
|
|
115
|
+
timeout: config.publicApi.timeout
|
|
143
116
|
});
|
|
117
|
+
// 统一解析结果格式
|
|
144
118
|
const data = res.data;
|
|
145
|
-
if (
|
|
146
|
-
throw new Error(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
119
|
+
if (data.code !== 200 && data.code !== 0)
|
|
120
|
+
throw new Error(data.msg || '解析失败');
|
|
121
|
+
return {
|
|
122
|
+
title: data.title || data.video_title || `【${platform}视频】`,
|
|
123
|
+
author: data.author || data.nickname || '未知作者',
|
|
124
|
+
description: data.desc || data.content || '无简介',
|
|
125
|
+
like: data.like || data.like_count || 0,
|
|
126
|
+
coin: data.coin || data.coin_count || 0,
|
|
127
|
+
collect: data.collect || data.collect_count || 0,
|
|
128
|
+
share: data.share || data.share_count || 0,
|
|
129
|
+
view: data.play || data.view_count || 0,
|
|
130
|
+
danmaku: data.danmaku || data.comment_count || 0,
|
|
131
|
+
cover: data.cover || data.thumbnail || data.img || '',
|
|
155
132
|
duration: data.duration || data.time || 0,
|
|
156
|
-
|
|
133
|
+
url: data.url || data.video_url || data.play_url || url
|
|
157
134
|
};
|
|
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
135
|
}
|
|
172
136
|
catch (e) {
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
};
|
|
137
|
+
ctx.logger.warn(`公益API解析失败:${e.message},使用本地解析兜底`);
|
|
138
|
+
return parseLocal(url, platform);
|
|
224
139
|
}
|
|
225
140
|
}
|
|
226
141
|
// 核心解析函数
|
|
@@ -228,14 +143,13 @@ function apply(ctx, config) {
|
|
|
228
143
|
if (!config.enable)
|
|
229
144
|
return;
|
|
230
145
|
// 去重逻辑
|
|
231
|
-
const now = Date.now();
|
|
232
146
|
const linkHash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
147
|
+
const now = Date.now();
|
|
233
148
|
if (processedLinks.has(linkHash) && now - processedLinks.get(linkHash) < config.sameLinkInterval * 1000) {
|
|
234
|
-
ctx.logger.debug(`相同链接 ${url} 短时间内已解析,跳过`);
|
|
235
149
|
return;
|
|
236
150
|
}
|
|
237
151
|
processedLinks.set(linkHash, now);
|
|
238
|
-
//
|
|
152
|
+
// 等待提示
|
|
239
153
|
if (config.showWaitingTip)
|
|
240
154
|
await session.send(config.waitingTipText);
|
|
241
155
|
// 判断平台
|
|
@@ -243,9 +157,8 @@ function apply(ctx, config) {
|
|
|
243
157
|
if (url.includes('bilibili') || /(BV|AV)\w+/.test(url)) {
|
|
244
158
|
platform = 'bilibili';
|
|
245
159
|
// BV/AV号补全链接
|
|
246
|
-
if (/^(BV|AV)/i.test(url))
|
|
160
|
+
if (/^(BV|AV)/i.test(url))
|
|
247
161
|
url = `https://www.bilibili.com/video/${url}`;
|
|
248
|
-
}
|
|
249
162
|
}
|
|
250
163
|
else if (url.includes('douyin') || url.includes('dy') || url.includes('抖音')) {
|
|
251
164
|
platform = 'douyin';
|
|
@@ -254,46 +167,23 @@ function apply(ctx, config) {
|
|
|
254
167
|
platform = 'kuaishou';
|
|
255
168
|
}
|
|
256
169
|
else {
|
|
257
|
-
await session.send('❌
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
// 解析视频信息
|
|
261
|
-
let videoInfo;
|
|
262
|
-
try {
|
|
263
|
-
if (config.parserSource === 'custom' && config[platform].customApi) {
|
|
264
|
-
videoInfo = await parseWithCustomApi(url, platform);
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
videoInfo = await parseWithBuiltinApi(url, platform);
|
|
268
|
-
}
|
|
269
|
-
// 校验解析结果
|
|
270
|
-
if (!videoInfo.url) {
|
|
271
|
-
throw new Error('解析失败:未获取到视频链接');
|
|
272
|
-
}
|
|
273
|
-
if (!videoInfo.title) {
|
|
274
|
-
videoInfo.title = '未知标题';
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
catch (e) {
|
|
278
|
-
await session.send(`❌ 解析失败:${e.message}`);
|
|
279
|
-
ctx.logger.error(`解析 ${url} 失败:`, e);
|
|
170
|
+
await session.send('❌ 仅支持B站/抖音/快手视频链接解析');
|
|
280
171
|
return;
|
|
281
172
|
}
|
|
173
|
+
// 解析视频信息(优先API,失败则本地)
|
|
174
|
+
const videoInfo = config.publicApi.enable
|
|
175
|
+
? await parsePublicApi(url, platform)
|
|
176
|
+
: parseLocal(url, platform);
|
|
282
177
|
// 时长过滤
|
|
283
178
|
const duration = videoInfo.duration / 60;
|
|
284
179
|
if (duration < config.minVideoDuration) {
|
|
285
|
-
const msg = config.shortVideoTip
|
|
180
|
+
const msg = config.shortVideoTip;
|
|
286
181
|
return config.shortVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(msg);
|
|
287
182
|
}
|
|
288
183
|
if (duration > config.maxVideoDuration) {
|
|
289
|
-
const msg = config.longVideoTip
|
|
184
|
+
const msg = config.longVideoTip;
|
|
290
185
|
return config.longVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(msg);
|
|
291
186
|
}
|
|
292
|
-
// 大小过滤
|
|
293
|
-
if (config.maxFileSize > 0 && videoInfo.size > config.maxFileSize * 1024 * 1024) {
|
|
294
|
-
await session.send(`❌ 视频文件过大(>${config.maxFileSize}MB),无法发送`);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
187
|
// 生成回复
|
|
298
188
|
await generateReply(videoInfo, session, platform);
|
|
299
189
|
}
|
|
@@ -309,7 +199,7 @@ function apply(ctx, config) {
|
|
|
309
199
|
.replace(/\${简介}/g, desc || '')
|
|
310
200
|
.replace(/\${封面}/g, videoInfo.cover || '')
|
|
311
201
|
.replace(/\${tab}/g, '\t');
|
|
312
|
-
// 仅B
|
|
202
|
+
// 仅B站显示统计字段,抖音/快手自动过滤
|
|
313
203
|
if (platform === 'bilibili') {
|
|
314
204
|
content = content.replace(/\${点赞}/g, videoInfo.like?.toString() || '0')
|
|
315
205
|
.replace(/\${投币}/g, videoInfo.coin?.toString() || '0')
|
|
@@ -319,26 +209,19 @@ function apply(ctx, config) {
|
|
|
319
209
|
.replace(/\${弹幕}/g, videoInfo.danmaku?.toString() || '0');
|
|
320
210
|
}
|
|
321
211
|
else {
|
|
322
|
-
//
|
|
323
|
-
content = content.replace(/\${点赞}/g, '')
|
|
324
|
-
.replace(/\${
|
|
325
|
-
.replace(/\${
|
|
326
|
-
|
|
327
|
-
.replace(
|
|
328
|
-
.replace(/\${弹幕}/g, '');
|
|
329
|
-
// 清理空行和多余符号
|
|
330
|
-
content = content.replace(/点赞:\s*\t*\s*投币:\s*\n/g, '')
|
|
212
|
+
// 清空抖音/快手的统计字段
|
|
213
|
+
content = content.replace(/\${点赞}/g, '').replace(/\${投币}/g, '')
|
|
214
|
+
.replace(/\${收藏}/g, '').replace(/\${转发}/g, '')
|
|
215
|
+
.replace(/\${观看}/g, '').replace(/\${弹幕}/g, '')
|
|
216
|
+
// 过滤空行和多余符号
|
|
217
|
+
.replace(/点赞:\s*\t*\s*投币:\s*\n/g, '')
|
|
331
218
|
.replace(/收藏:\s*\t*\s*转发:\s*\n/g, '')
|
|
332
219
|
.replace(/观看:\s*\t*\s*弹幕:\s*\n/g, '')
|
|
333
|
-
.replace(/={2,}/g, (match) => match.trim() ? match : '')
|
|
334
220
|
.replace(/\n+/g, '\n').trim();
|
|
335
221
|
}
|
|
336
222
|
// 发送解析内容
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (p.trim())
|
|
340
|
-
await session.send(p.trim());
|
|
341
|
-
}
|
|
223
|
+
await session.send(content);
|
|
224
|
+
// 显示视频链接
|
|
342
225
|
if (config.showVideoLink && videoInfo.url) {
|
|
343
226
|
await session.send(`📥 视频链接:${videoInfo.url}`);
|
|
344
227
|
}
|
|
@@ -349,19 +232,16 @@ function apply(ctx, config) {
|
|
|
349
232
|
if (config.enableMergeForward) {
|
|
350
233
|
const msgs = [];
|
|
351
234
|
if (videoInfo.title)
|
|
352
|
-
msgs.push(koishi_1.h.text(`📌
|
|
235
|
+
msgs.push(koishi_1.h.text(`📌 ${videoInfo.title}`));
|
|
353
236
|
if (videoInfo.author)
|
|
354
|
-
msgs.push(koishi_1.h.text(`👤
|
|
237
|
+
msgs.push(koishi_1.h.text(`👤 ${videoInfo.author}`));
|
|
355
238
|
if (videoInfo.cover)
|
|
356
239
|
msgs.push(koishi_1.h.image(videoInfo.cover));
|
|
357
240
|
if (videoInfo.url) {
|
|
358
241
|
if (config.downloadBeforeSend) {
|
|
359
242
|
try {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
timeout: 60000
|
|
363
|
-
});
|
|
364
|
-
msgs.push(koishi_1.h.video(response.data));
|
|
243
|
+
const res = await request.get(videoInfo.url, { responseType: 'stream', timeout: 60000 });
|
|
244
|
+
msgs.push(koishi_1.h.video(res.data));
|
|
365
245
|
}
|
|
366
246
|
catch (e) {
|
|
367
247
|
msgs.push(koishi_1.h.text(`📥 视频链接:${videoInfo.url}`));
|
|
@@ -378,8 +258,7 @@ function apply(ctx, config) {
|
|
|
378
258
|
}
|
|
379
259
|
}
|
|
380
260
|
catch (e) {
|
|
381
|
-
await session.send(`❌ 消息发送失败:${e.message
|
|
382
|
-
ctx.logger.error(`发送消息失败:`, e);
|
|
261
|
+
await session.send(`❌ 消息发送失败:${e.message}`);
|
|
383
262
|
}
|
|
384
263
|
}
|
|
385
264
|
// 监听消息
|
|
@@ -387,13 +266,11 @@ function apply(ctx, config) {
|
|
|
387
266
|
if (!config.enable)
|
|
388
267
|
return;
|
|
389
268
|
const content = session.content.trim();
|
|
390
|
-
// 匹配视频链接(支持短链接/长链接/BV/AV号)
|
|
391
269
|
const reg = /(https?:\/\/\S+)|(BV\w+)|(AV\d+)/gi;
|
|
392
270
|
const matches = [...content.matchAll(reg)];
|
|
393
271
|
if (!matches.length)
|
|
394
272
|
return;
|
|
395
273
|
const uid = session.userId;
|
|
396
|
-
// 消息缓冲,避免重复解析
|
|
397
274
|
if (config.messageBufferDelay > 0) {
|
|
398
275
|
if (!messageQueue.has(uid)) {
|
|
399
276
|
messageQueue.set(uid, []);
|
|
@@ -411,20 +288,13 @@ function apply(ctx, config) {
|
|
|
411
288
|
await parseVideo(m[0], session);
|
|
412
289
|
}
|
|
413
290
|
});
|
|
414
|
-
//
|
|
291
|
+
// 定时清理过期链接
|
|
415
292
|
setInterval(() => {
|
|
416
293
|
const now = Date.now();
|
|
417
|
-
let count = 0;
|
|
418
294
|
for (const [k, t] of processedLinks) {
|
|
419
|
-
if (now - t > 86400000)
|
|
295
|
+
if (now - t > 86400000)
|
|
420
296
|
processedLinks.delete(k);
|
|
421
|
-
count++;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
if (count > 0) {
|
|
425
|
-
ctx.logger.debug(`清理了 ${count} 条过期链接记录`);
|
|
426
297
|
}
|
|
427
298
|
}, 3600000);
|
|
428
|
-
|
|
429
|
-
ctx.logger.info(`✅ 视频解析插件已启动(解析来源:${config.parserSource})`);
|
|
299
|
+
ctx.logger.info('✅ 视频解析插件已启动(移除字段映射+超大输入框)');
|
|
430
300
|
}
|