koishi-plugin-video-parser-all 0.0.3 → 0.0.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 +1 -45
- package/lib/index.js +137 -316
- 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,15 @@ 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
|
-
builtinApi: {
|
|
21
|
+
suyanApi: {
|
|
49
22
|
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
23
|
};
|
|
67
24
|
}
|
|
68
25
|
export declare const Config: Schema<Config>;
|
|
69
26
|
export declare function apply(ctx: Context, config: Config): void;
|
|
70
|
-
export {};
|
package/lib/index.js
CHANGED
|
@@ -12,364 +12,197 @@ 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
|
-
|
|
17
|
-
allowBVAVParse: koishi_1.Schema.boolean().default(true).description('允许BV/AV号解析'),
|
|
15
|
+
waitingTipText: koishi_1.Schema.string().default('正在解析视频/图集链接...').description('等待提示文字内容'),
|
|
16
|
+
allowBVAVParse: koishi_1.Schema.boolean().default(true).description('允许BV/AV号解析(B站备用)'),
|
|
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('
|
|
21
|
-
shortVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('
|
|
22
|
-
maxVideoDuration: koishi_1.Schema.number().default(
|
|
23
|
-
longVideoTip: koishi_1.Schema.string().default('
|
|
24
|
-
longVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('
|
|
25
|
-
maxFileSize: koishi_1.Schema.number().default(50).description('最大文件大小(MB)'),
|
|
19
|
+
shortVideoTip: koishi_1.Schema.string().default('视频时长过短,不解析~').description('过短提示'),
|
|
20
|
+
shortVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过短视频用图文解析'),
|
|
21
|
+
maxVideoDuration: koishi_1.Schema.number().default(60).description('最大时长(分钟)'),
|
|
22
|
+
longVideoTip: koishi_1.Schema.string().default('视频时长过长,不解析~').description('过长提示'),
|
|
23
|
+
longVideoUseImageParse: koishi_1.Schema.boolean().default(false).description('过长视频用图文解析'),
|
|
26
24
|
imageParseFormat: koishi_1.Schema.string()
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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配置(备用)
|
|
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)')
|
|
25
|
+
.role('textarea')
|
|
26
|
+
.default(`\${标题} \${tab} \${UP主}
|
|
27
|
+
\${简介}
|
|
28
|
+
点赞:\${点赞} \${tab} 投币:\${投币}
|
|
29
|
+
收藏:\${收藏} \${tab} 转发:\${转发}
|
|
30
|
+
观看:\${观看} \${tab} 弹幕:\${弹幕}
|
|
31
|
+
\${~~~}
|
|
32
|
+
\${封面}`)
|
|
33
|
+
.description('图文解析格式(严格按你的要求配置)'),
|
|
34
|
+
showVideoLink: koishi_1.Schema.boolean().default(true).description('显示解析后的链接/图集列表'),
|
|
35
|
+
maxDescLength: koishi_1.Schema.number().default(200).description('简介最大长度'),
|
|
36
|
+
enableMergeForward: koishi_1.Schema.boolean().default(false).description('合并转发(仅onebot)'),
|
|
37
|
+
downloadBeforeSend: koishi_1.Schema.boolean().default(false).description('下载后发送(避免链接失效)'),
|
|
38
|
+
messageBufferDelay: koishi_1.Schema.number().default(1).description('消息缓冲延迟(秒)'),
|
|
39
|
+
suyanApi: koishi_1.Schema.object({
|
|
40
|
+
timeout: koishi_1.Schema.number().default(15000).description('素颜API超时时间(毫秒)')
|
|
41
|
+
}).description('素颜API配置(无需密钥,直接使用)')
|
|
103
42
|
});
|
|
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
43
|
const processedLinks = new Map();
|
|
123
44
|
const messageQueue = new Map();
|
|
45
|
+
const SUYAN_API_MAP = {
|
|
46
|
+
douyin: 'https://api.suyanw.cn/api/douyin.php',
|
|
47
|
+
kuaishou: 'https://api.suyanw.cn/api/kuaishou.php'
|
|
48
|
+
};
|
|
49
|
+
function parseLocal(url, platform) {
|
|
50
|
+
return {
|
|
51
|
+
title: platform === 'bilibili' ? 'B站视频' : `${platform === 'douyin' ? '抖音' : '快手'}视频/图集`,
|
|
52
|
+
author: '未知作者',
|
|
53
|
+
description: '无简介',
|
|
54
|
+
like: 0,
|
|
55
|
+
coin: 0,
|
|
56
|
+
collect: 0,
|
|
57
|
+
share: 0,
|
|
58
|
+
view: 0,
|
|
59
|
+
danmaku: 0,
|
|
60
|
+
cover: '',
|
|
61
|
+
url: url,
|
|
62
|
+
images: []
|
|
63
|
+
};
|
|
64
|
+
}
|
|
124
65
|
function apply(ctx, config) {
|
|
125
|
-
// 创建请求实例
|
|
126
66
|
const request = axios_1.default.create({
|
|
127
67
|
headers: {
|
|
128
|
-
'User-Agent':
|
|
129
|
-
'Referer': 'https://www.baidu.com',
|
|
130
|
-
'Origin': 'https://www.baidu.com'
|
|
68
|
+
'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'
|
|
131
69
|
},
|
|
132
|
-
timeout: config.
|
|
133
|
-
|
|
70
|
+
timeout: config.suyanApi.timeout,
|
|
71
|
+
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
|
|
134
72
|
});
|
|
135
|
-
|
|
136
|
-
async function parseWithBuiltinApi(url, platform, retry = 0) {
|
|
137
|
-
const apiList = Object.values(BUILTIN_APIS);
|
|
138
|
-
const currentApi = apiList[retry % apiList.length];
|
|
73
|
+
async function parseWithSuyanApi(url, platform) {
|
|
139
74
|
try {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
75
|
+
const apiUrl = SUYAN_API_MAP[platform];
|
|
76
|
+
const res = await request.get(apiUrl, {
|
|
77
|
+
params: { url: encodeURIComponent(url) }
|
|
143
78
|
});
|
|
144
79
|
const data = res.data;
|
|
145
|
-
if (
|
|
146
|
-
throw new Error(
|
|
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
|
-
};
|
|
80
|
+
if (data.code !== 200) {
|
|
81
|
+
throw new Error(`素颜API解析失败:${data.msg || '未知错误'}(错误码:${data.code})`);
|
|
169
82
|
}
|
|
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
83
|
return {
|
|
198
|
-
title:
|
|
199
|
-
author:
|
|
200
|
-
description:
|
|
201
|
-
like:
|
|
202
|
-
coin:
|
|
203
|
-
collect:
|
|
204
|
-
share:
|
|
205
|
-
view:
|
|
206
|
-
danmaku:
|
|
207
|
-
cover:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
url: getValue(data, fieldMap.url) || ''
|
|
84
|
+
title: data.data?.title || '未知标题',
|
|
85
|
+
author: data.data?.author || '未知作者',
|
|
86
|
+
description: data.data?.title || '无简介',
|
|
87
|
+
like: data.data?.likes || 0,
|
|
88
|
+
coin: 0,
|
|
89
|
+
collect: 0,
|
|
90
|
+
share: 0,
|
|
91
|
+
view: data.data?.views || 0,
|
|
92
|
+
danmaku: data.data?.comments || 0,
|
|
93
|
+
cover: data.data?.cover || '',
|
|
94
|
+
url: url,
|
|
95
|
+
images: data.image || []
|
|
211
96
|
};
|
|
212
97
|
}
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
};
|
|
98
|
+
catch (e) {
|
|
99
|
+
ctx.logger.warn(`素颜API解析${platform}失败:${e.message}`);
|
|
100
|
+
return parseLocal(url, platform);
|
|
224
101
|
}
|
|
225
102
|
}
|
|
226
|
-
// 核心解析函数
|
|
227
103
|
async function parseVideo(url, session) {
|
|
228
104
|
if (!config.enable)
|
|
229
105
|
return;
|
|
230
|
-
// 去重逻辑
|
|
231
|
-
const now = Date.now();
|
|
232
106
|
const linkHash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
107
|
+
const now = Date.now();
|
|
233
108
|
if (processedLinks.has(linkHash) && now - processedLinks.get(linkHash) < config.sameLinkInterval * 1000) {
|
|
234
|
-
ctx.logger.debug(`相同链接 ${url} 短时间内已解析,跳过`);
|
|
235
109
|
return;
|
|
236
110
|
}
|
|
237
111
|
processedLinks.set(linkHash, now);
|
|
238
|
-
// 发送等待提示
|
|
239
112
|
if (config.showWaitingTip)
|
|
240
113
|
await session.send(config.waitingTipText);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (url.includes('bilibili') || /(BV|AV)\w+/.test(url)) {
|
|
244
|
-
platform = 'bilibili';
|
|
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('抖音')) {
|
|
114
|
+
let platform = 'bilibili';
|
|
115
|
+
if (url.includes('douyin') || url.includes('dy')) {
|
|
251
116
|
platform = 'douyin';
|
|
252
117
|
}
|
|
253
|
-
else if (url.includes('kuaishou') || url.includes('ks')
|
|
118
|
+
else if (url.includes('kuaishou') || url.includes('ks')) {
|
|
254
119
|
platform = 'kuaishou';
|
|
255
120
|
}
|
|
256
|
-
|
|
257
|
-
await
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
videoInfo = await parseWithCustomApi(url, platform);
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
videoInfo = await parseWithBuiltinApi(url, platform);
|
|
121
|
+
const videoInfo = platform === 'douyin' || platform === 'kuaishou'
|
|
122
|
+
? await parseWithSuyanApi(url, platform)
|
|
123
|
+
: parseLocal(url, platform);
|
|
124
|
+
const isImageSet = videoInfo.images.length > 0;
|
|
125
|
+
if (!isImageSet) {
|
|
126
|
+
const duration = 0;
|
|
127
|
+
if (duration < config.minVideoDuration) {
|
|
128
|
+
return config.shortVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(config.shortVideoTip);
|
|
268
129
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
throw new Error('解析失败:未获取到视频链接');
|
|
130
|
+
if (duration > config.maxVideoDuration) {
|
|
131
|
+
return config.longVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(config.longVideoTip);
|
|
272
132
|
}
|
|
273
|
-
if (!videoInfo.title) {
|
|
274
|
-
videoInfo.title = '未知标题';
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
catch (e) {
|
|
278
|
-
await session.send(`❌ 解析失败:${e.message}`);
|
|
279
|
-
ctx.logger.error(`解析 ${url} 失败:`, e);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
// 时长过滤
|
|
283
|
-
const duration = videoInfo.duration / 60;
|
|
284
|
-
if (duration < config.minVideoDuration) {
|
|
285
|
-
const msg = config.shortVideoTip || '❌ 视频时长过短,不解析';
|
|
286
|
-
return config.shortVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(msg);
|
|
287
|
-
}
|
|
288
|
-
if (duration > config.maxVideoDuration) {
|
|
289
|
-
const msg = config.longVideoTip || '❌ 视频时长过长,不解析';
|
|
290
|
-
return config.longVideoUseImageParse ? await generateImageParse(videoInfo, session, platform) : await session.send(msg);
|
|
291
133
|
}
|
|
292
|
-
// 大小过滤
|
|
293
|
-
if (config.maxFileSize > 0 && videoInfo.size > config.maxFileSize * 1024 * 1024) {
|
|
294
|
-
await session.send(`❌ 视频文件过大(>${config.maxFileSize}MB),无法发送`);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
// 生成回复
|
|
298
134
|
await generateReply(videoInfo, session, platform);
|
|
299
135
|
}
|
|
300
|
-
//
|
|
136
|
+
// 核心修复:拆分封面处理逻辑,避免replace接收非字符串类型
|
|
301
137
|
async function generateImageParse(videoInfo, session, platform) {
|
|
302
138
|
let content = config.imageParseFormat;
|
|
303
139
|
const desc = videoInfo.description.length > config.maxDescLength
|
|
304
140
|
? videoInfo.description.slice(0, config.maxDescLength) + '...'
|
|
305
141
|
: videoInfo.description;
|
|
306
|
-
//
|
|
307
|
-
content = content
|
|
142
|
+
// 第一步:只替换字符串类型的变量(封面先标记为占位符)
|
|
143
|
+
content = content
|
|
144
|
+
.replace(/\${标题}/g, videoInfo.title || '')
|
|
308
145
|
.replace(/\${UP主}/g, videoInfo.author || '')
|
|
309
146
|
.replace(/\${简介}/g, desc || '')
|
|
310
|
-
.replace(/\${
|
|
311
|
-
.replace(/\${
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
147
|
+
.replace(/\${点赞}/g, videoInfo.like?.toString() || '0')
|
|
148
|
+
.replace(/\${投币}/g, videoInfo.coin?.toString() || '0')
|
|
149
|
+
.replace(/\${收藏}/g, videoInfo.collect?.toString() || '0')
|
|
150
|
+
.replace(/\${转发}/g, videoInfo.share?.toString() || '0')
|
|
151
|
+
.replace(/\${观看}/g, videoInfo.view?.toString() || '0')
|
|
152
|
+
.replace(/\${弹幕}/g, videoInfo.danmaku?.toString() || '0')
|
|
153
|
+
.replace(/\${tab}/g, '\t\t')
|
|
154
|
+
.replace(/\${~~~}/g, '————————————————————');
|
|
155
|
+
// 第二步:拆分内容和封面,分别发送(修复类型错误的核心)
|
|
156
|
+
const coverPlaceholder = '\${封面}';
|
|
157
|
+
if (content.includes(coverPlaceholder)) {
|
|
158
|
+
// 分割内容为封面前后两部分
|
|
159
|
+
const [beforeCover, afterCover] = content.split(coverPlaceholder);
|
|
160
|
+
// 发送封面之前的内容
|
|
161
|
+
if (beforeCover.trim()) {
|
|
162
|
+
await session.send(beforeCover.trim());
|
|
163
|
+
}
|
|
164
|
+
// 单独发送封面(Element类型)或文字提示
|
|
165
|
+
if (videoInfo.cover) {
|
|
166
|
+
await session.send(koishi_1.h.image(videoInfo.cover));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
await session.send('无封面');
|
|
170
|
+
}
|
|
171
|
+
// 发送封面之后的内容
|
|
172
|
+
if (afterCover && afterCover.trim()) {
|
|
173
|
+
await session.send(afterCover.trim());
|
|
174
|
+
}
|
|
320
175
|
}
|
|
321
176
|
else {
|
|
322
|
-
//
|
|
323
|
-
|
|
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
|
-
// 发送解析内容
|
|
337
|
-
const parts = content.split(/\${~~~}/);
|
|
338
|
-
for (const p of parts) {
|
|
339
|
-
if (p.trim())
|
|
340
|
-
await session.send(p.trim());
|
|
177
|
+
// 没有封面占位符时直接发送全部内容
|
|
178
|
+
await session.send(content.trim());
|
|
341
179
|
}
|
|
342
|
-
|
|
343
|
-
|
|
180
|
+
// 显示图集/链接
|
|
181
|
+
if (config.showVideoLink) {
|
|
182
|
+
if (videoInfo.images.length > 0) {
|
|
183
|
+
await session.send(`📁 图集共${videoInfo.images.length}张:\n${videoInfo.images.slice(0, 10).join('\n')}${videoInfo.images.length > 10 ? '\n...(省略剩余图片)' : ''}`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
await session.send(`📥 原始链接:${videoInfo.url}`);
|
|
187
|
+
}
|
|
344
188
|
}
|
|
345
189
|
}
|
|
346
|
-
// 生成最终回复
|
|
347
190
|
async function generateReply(videoInfo, session, platform) {
|
|
348
191
|
try {
|
|
349
192
|
if (config.enableMergeForward) {
|
|
350
193
|
const msgs = [];
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
194
|
+
msgs.push(koishi_1.h.text(`${videoInfo.title} \t\t ${videoInfo.author}`));
|
|
195
|
+
msgs.push(koishi_1.h.text(videoInfo.description));
|
|
196
|
+
msgs.push(koishi_1.h.text(`点赞:${videoInfo.like} \t\t 投币:${videoInfo.coin}`));
|
|
197
|
+
msgs.push(koishi_1.h.text(`收藏:${videoInfo.collect} \t\t 转发:${videoInfo.share}`));
|
|
198
|
+
msgs.push(koishi_1.h.text(`观看:${videoInfo.view} \t\t 弹幕:${videoInfo.danmaku}`));
|
|
355
199
|
if (videoInfo.cover)
|
|
356
200
|
msgs.push(koishi_1.h.image(videoInfo.cover));
|
|
357
|
-
if (videoInfo.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
}
|
|
201
|
+
if (videoInfo.images.length > 0) {
|
|
202
|
+
msgs.push(koishi_1.h.text(`📁 图集共${videoInfo.images.length}张`));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
msgs.push(koishi_1.h.text(`📥 原始链接:${videoInfo.url}`));
|
|
373
206
|
}
|
|
374
207
|
await session.send(msgs);
|
|
375
208
|
}
|
|
@@ -378,22 +211,18 @@ function apply(ctx, config) {
|
|
|
378
211
|
}
|
|
379
212
|
}
|
|
380
213
|
catch (e) {
|
|
381
|
-
await session.send(`❌ 消息发送失败:${e.message
|
|
382
|
-
ctx.logger.error(`发送消息失败:`, e);
|
|
214
|
+
await session.send(`❌ 消息发送失败:${e.message}`);
|
|
383
215
|
}
|
|
384
216
|
}
|
|
385
|
-
// 监听消息
|
|
386
217
|
ctx.on('message', async (session) => {
|
|
387
218
|
if (!config.enable)
|
|
388
219
|
return;
|
|
389
220
|
const content = session.content.trim();
|
|
390
|
-
// 匹配视频链接(支持短链接/长链接/BV/AV号)
|
|
391
221
|
const reg = /(https?:\/\/\S+)|(BV\w+)|(AV\d+)/gi;
|
|
392
222
|
const matches = [...content.matchAll(reg)];
|
|
393
223
|
if (!matches.length)
|
|
394
224
|
return;
|
|
395
225
|
const uid = session.userId;
|
|
396
|
-
// 消息缓冲,避免重复解析
|
|
397
226
|
if (config.messageBufferDelay > 0) {
|
|
398
227
|
if (!messageQueue.has(uid)) {
|
|
399
228
|
messageQueue.set(uid, []);
|
|
@@ -411,20 +240,12 @@ function apply(ctx, config) {
|
|
|
411
240
|
await parseVideo(m[0], session);
|
|
412
241
|
}
|
|
413
242
|
});
|
|
414
|
-
// 定时清理过期链接记录
|
|
415
243
|
setInterval(() => {
|
|
416
244
|
const now = Date.now();
|
|
417
|
-
let count = 0;
|
|
418
245
|
for (const [k, t] of processedLinks) {
|
|
419
|
-
if (now - t > 86400000)
|
|
246
|
+
if (now - t > 86400000)
|
|
420
247
|
processedLinks.delete(k);
|
|
421
|
-
count++;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
if (count > 0) {
|
|
425
|
-
ctx.logger.debug(`清理了 ${count} 条过期链接记录`);
|
|
426
248
|
}
|
|
427
249
|
}, 3600000);
|
|
428
|
-
|
|
429
|
-
ctx.logger.info(`✅ 视频解析插件已启动(解析来源:${config.parserSource})`);
|
|
250
|
+
ctx.logger.info('✅ 视频解析插件已启动(精准适配素颜API)');
|
|
430
251
|
}
|