koishi-plugin-video-parser-all 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +4 -0
- package/lib/index.js +279 -183
- package/package.json +2 -2
package/lib/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare const Config: Schema<{
|
|
|
24
24
|
} & {
|
|
25
25
|
enableForward?: boolean | null | undefined;
|
|
26
26
|
downloadVideoBeforeSend?: boolean | null | undefined;
|
|
27
|
+
maxVideoSize?: number | null | undefined;
|
|
28
|
+
downloadThreads?: number | null | undefined;
|
|
27
29
|
} & {
|
|
28
30
|
messageBufferDelay?: number | null | undefined;
|
|
29
31
|
} & {
|
|
@@ -52,6 +54,8 @@ export declare const Config: Schema<{
|
|
|
52
54
|
} & {
|
|
53
55
|
enableForward: boolean;
|
|
54
56
|
downloadVideoBeforeSend: boolean;
|
|
57
|
+
maxVideoSize: number;
|
|
58
|
+
downloadThreads: number;
|
|
55
59
|
} & {
|
|
56
60
|
messageBufferDelay: number;
|
|
57
61
|
} & {
|
package/lib/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
13
|
const promises_1 = require("stream/promises");
|
|
14
|
+
const worker_threads_1 = require("worker_threads");
|
|
14
15
|
exports.name = 'video-parser-all';
|
|
15
16
|
exports.Config = koishi_1.Schema.intersect([
|
|
16
17
|
koishi_1.Schema.object({
|
|
@@ -21,7 +22,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
21
22
|
sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
|
|
22
23
|
}).description('基础设置'),
|
|
23
24
|
koishi_1.Schema.object({
|
|
24
|
-
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n
|
|
25
|
+
unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n音乐:${音乐名}').description('统一消息格式(B站会显示投币,其他平台自动隐藏;无法获取的变量会自动隐藏)'),
|
|
25
26
|
}).description('统一消息格式'),
|
|
26
27
|
koishi_1.Schema.object({
|
|
27
28
|
showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
|
|
@@ -43,20 +44,22 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
43
44
|
koishi_1.Schema.object({
|
|
44
45
|
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot平台)'),
|
|
45
46
|
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频(避免链接失效)'),
|
|
46
|
-
|
|
47
|
+
maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('最大视频大小限制(MB,0为不限制)'),
|
|
48
|
+
downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0为不使用多线程,1-10为启用对应线程数)'),
|
|
49
|
+
}).description('发送方式设置(说明:视频大小超过限制将只发送链接;多线程下载可提升速度但可能增加服务器负载)'),
|
|
47
50
|
koishi_1.Schema.object({
|
|
48
51
|
messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒,批量处理链接)'),
|
|
49
52
|
}).description('消息处理设置'),
|
|
50
53
|
koishi_1.Schema.object({
|
|
51
54
|
autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0为关闭)'),
|
|
52
|
-
}).description('
|
|
55
|
+
}).description('缓存清理设置(说明:开启自动清理可定期删除过期的临时视频文件和解析缓存)'),
|
|
53
56
|
]);
|
|
54
57
|
const processed = new Map();
|
|
55
58
|
const linkBuffer = new Map();
|
|
56
59
|
const logger = new koishi_1.Logger(exports.name);
|
|
57
60
|
const PLATFORM_KEYWORDS = {
|
|
58
61
|
bilibili: ['bilibili', 'b23', 'B站', 'www.bilibili.com', 'm.bilibili.com', '哔哩哔哩', 'bilibili.com/opus', 'bilibili.com/video', 'b23.tv', 't.bilibili.com', 'bilibili.com/bangumi'],
|
|
59
|
-
kuaishou: ['kuaishou', '快手', 'v.kuaishou.com', 'www.kuaishou.com', 'kwimgs.com', 'kuaishou.com/app'
|
|
62
|
+
kuaishou: ['kuaishou', '快手', 'v.kuaishou.com', 'www.kuaishou.com', 'kwimgs.com', 'kuaishou.com/app'],
|
|
60
63
|
xiaohongshu: ['xiaohongshu', '小红书', 'xhslink.com', 'xiaohongshu.com', 'xhscdn.com', 'xiaohongshu.com/explore', 'xhslink.com/'],
|
|
61
64
|
weibo: ['weibo', '微博', 'weibo.com', 'video.weibo.com', 'm.weibo.cn', 'weibo.com/tv/show', 'weibo.com/feed'],
|
|
62
65
|
toutiao: ['toutiao', '今日头条', 'm.toutiao.com', 'toutiao.com', 'ixigua.com', 'toutiao.com/video', 'ixigua.com/i'],
|
|
@@ -66,22 +69,87 @@ const PLATFORM_KEYWORDS = {
|
|
|
66
69
|
zuiyou: ['zuiyou', '最右', 'xiaochuankeji.cn', 'izuiyou.com', 'izuiyou.com/topic']
|
|
67
70
|
};
|
|
68
71
|
const API_CONFIG = {
|
|
69
|
-
bilibili: 'https://api.
|
|
70
|
-
douyin: 'https://api.
|
|
71
|
-
kuaishou: 'https://api.
|
|
72
|
-
xiaohongshu: 'https://api.bugpk.com/api/
|
|
72
|
+
bilibili: 'https://api.bugpk.com/api/bilibili',
|
|
73
|
+
douyin: 'https://api.bugpk.com/api/dyjx',
|
|
74
|
+
kuaishou: 'https://api.bugpk.com/api/ksjx',
|
|
75
|
+
xiaohongshu: 'https://api.bugpk.com/api/xhsjx',
|
|
73
76
|
weibo: 'https://api.bugpk.com/api/weibo',
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
toutiao: 'https://api.bugpk.com/api/toutiao',
|
|
78
|
+
pipigx: 'https://api.bugpk.com/api/pipigx',
|
|
79
|
+
pipixia: 'https://api.bugpk.com/api/ppx',
|
|
80
|
+
zuiyou: 'https://api.bugpk.com/api/zuiyou'
|
|
81
|
+
};
|
|
82
|
+
const VARIABLE_MAPPING = {
|
|
83
|
+
'标题': ['title', 'Title', 'TITLE'],
|
|
84
|
+
'作者': ['author', 'name', 'Author', 'Name'],
|
|
85
|
+
'简介': ['desc', 'description', 'Desc', 'Description', 'content', 'Content'],
|
|
86
|
+
'视频时长': ['duration', 'Duration', 'time', 'Time'],
|
|
87
|
+
'点赞数': ['like', 'Like', 'attitudes_count', 'digg_count', 'praise'],
|
|
88
|
+
'投币数': ['coin', 'Coin', 'bi', 'Bi'],
|
|
89
|
+
'收藏数': ['collect', 'Collect', 'favorite', 'Favorite', 'star', 'Star'],
|
|
90
|
+
'转发数': ['share', 'Share', 'forward', 'Forward', 'repost'],
|
|
91
|
+
'播放数': ['view', 'View', 'play_count', 'PlayCount', 'play'],
|
|
92
|
+
'评论数': ['comment', 'Comment', 'comments_count', 'comment_count', 'discuss'],
|
|
93
|
+
'音乐名': ['music.title', 'music_name', 'audio_name', 'sound_name']
|
|
78
94
|
};
|
|
79
95
|
function getErrorMessage(error) {
|
|
80
96
|
if (error instanceof Error)
|
|
81
97
|
return error.message;
|
|
82
98
|
return String(error);
|
|
83
99
|
}
|
|
84
|
-
async function
|
|
100
|
+
async function getFileSize(url, userAgent) {
|
|
101
|
+
try {
|
|
102
|
+
const response = await axios_1.default.head(url, {
|
|
103
|
+
timeout: 10000,
|
|
104
|
+
headers: {
|
|
105
|
+
'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const contentLength = response.headers['content-length'];
|
|
109
|
+
if (contentLength) {
|
|
110
|
+
return Math.round(Number(contentLength) / 1024 / 1024 * 100) / 100;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) { }
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
async function downloadVideoThread(workerData) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const worker = new worker_threads_1.Worker(__filename, { workerData });
|
|
119
|
+
worker.on('message', resolve);
|
|
120
|
+
worker.on('error', reject);
|
|
121
|
+
worker.on('exit', (code) => {
|
|
122
|
+
if (code !== 0)
|
|
123
|
+
reject(new Error(`Worker stopped with exit code ${code}`));
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (!worker_threads_1.isMainThread) {
|
|
128
|
+
const { url, start, end, filename, userAgent } = worker_threads_1.workerData;
|
|
129
|
+
const filePath = path_1.default.join(process.cwd(), 'temp_videos', `${filename}_${start}_${end}.part`);
|
|
130
|
+
(0, axios_1.default)({
|
|
131
|
+
url,
|
|
132
|
+
method: 'GET',
|
|
133
|
+
responseType: 'stream',
|
|
134
|
+
timeout: 60000,
|
|
135
|
+
headers: {
|
|
136
|
+
'User-Agent': userAgent,
|
|
137
|
+
'Range': `bytes=${start}-${end}`
|
|
138
|
+
}
|
|
139
|
+
}).then(response => {
|
|
140
|
+
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
141
|
+
response.data.pipe(writeStream);
|
|
142
|
+
writeStream.on('finish', () => {
|
|
143
|
+
worker_threads_1.parentPort?.postMessage({ success: true, filePath, start, end });
|
|
144
|
+
});
|
|
145
|
+
writeStream.on('error', (error) => {
|
|
146
|
+
worker_threads_1.parentPort?.postMessage({ success: false, error: error.message });
|
|
147
|
+
});
|
|
148
|
+
}).catch(error => {
|
|
149
|
+
worker_threads_1.parentPort?.postMessage({ success: false, error: error.message });
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function downloadVideo(url, filename, userAgent, maxSize, threads) {
|
|
85
153
|
const dir = path_1.default.join(process.cwd(), 'temp_videos');
|
|
86
154
|
if (!fs_1.default.existsSync(dir))
|
|
87
155
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
@@ -90,99 +158,64 @@ async function downloadVideo(url, filename, userAgent) {
|
|
|
90
158
|
if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
|
|
91
159
|
throw new Error('不支持音频');
|
|
92
160
|
}
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
161
|
+
const fileSize = await getFileSize(url, userAgent);
|
|
162
|
+
if (maxSize > 0 && fileSize > maxSize) {
|
|
163
|
+
throw new Error(`视频大小${fileSize}MB超过限制${maxSize}MB`);
|
|
164
|
+
}
|
|
165
|
+
if (threads <= 0 || fileSize === 0) {
|
|
166
|
+
const response = await (0, axios_1.default)({
|
|
167
|
+
url,
|
|
168
|
+
method: 'GET',
|
|
169
|
+
responseType: 'stream',
|
|
170
|
+
timeout: 60000,
|
|
171
|
+
headers: {
|
|
172
|
+
'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
176
|
+
await (0, promises_1.pipeline)(response.data, writeStream);
|
|
177
|
+
return filePath;
|
|
178
|
+
}
|
|
179
|
+
const totalSize = fileSize * 1024 * 1024;
|
|
180
|
+
const chunkSize = Math.ceil(totalSize / threads);
|
|
181
|
+
const promises = [];
|
|
182
|
+
for (let i = 0; i < threads; i++) {
|
|
183
|
+
const start = i * chunkSize;
|
|
184
|
+
const end = i === threads - 1 ? totalSize - 1 : start + chunkSize - 1;
|
|
185
|
+
promises.push(downloadVideoThread({
|
|
186
|
+
url,
|
|
187
|
+
start,
|
|
188
|
+
end,
|
|
189
|
+
filename,
|
|
190
|
+
userAgent
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
const results = await Promise.all(promises);
|
|
102
194
|
const writeStream = fs_1.default.createWriteStream(filePath);
|
|
103
|
-
|
|
195
|
+
for (const result of results) {
|
|
196
|
+
if (!result.success)
|
|
197
|
+
throw new Error(result.error);
|
|
198
|
+
const readStream = fs_1.default.createReadStream(result.filePath);
|
|
199
|
+
await (0, promises_1.pipeline)(readStream, writeStream, { end: false });
|
|
200
|
+
fs_1.default.unlinkSync(result.filePath);
|
|
201
|
+
}
|
|
202
|
+
writeStream.end();
|
|
104
203
|
return filePath;
|
|
105
204
|
}
|
|
106
205
|
catch (error) {
|
|
107
206
|
if (fs_1.default.existsSync(filePath)) {
|
|
108
207
|
fs_1.default.unlinkSync(filePath);
|
|
109
208
|
}
|
|
209
|
+
const partFiles = fs_1.default.readdirSync(dir).filter(file => file.startsWith(`${filename}_`) && file.endsWith('.part'));
|
|
210
|
+
partFiles.forEach(file => {
|
|
211
|
+
try {
|
|
212
|
+
fs_1.default.unlinkSync(path_1.default.join(dir, file));
|
|
213
|
+
}
|
|
214
|
+
catch (e) { }
|
|
215
|
+
});
|
|
110
216
|
throw error;
|
|
111
217
|
}
|
|
112
218
|
}
|
|
113
|
-
function parseXingzhigeData(resData, platform) {
|
|
114
|
-
const result = {
|
|
115
|
-
title: '',
|
|
116
|
-
author: '未知作者',
|
|
117
|
-
desc: '',
|
|
118
|
-
cover: '',
|
|
119
|
-
video: '',
|
|
120
|
-
images: [],
|
|
121
|
-
stat: {
|
|
122
|
-
like: 0,
|
|
123
|
-
coin: 0,
|
|
124
|
-
favorite: 0,
|
|
125
|
-
share: 0,
|
|
126
|
-
view: 0,
|
|
127
|
-
size: 0,
|
|
128
|
-
duration: '00:00'
|
|
129
|
-
},
|
|
130
|
-
type: 'video'
|
|
131
|
-
};
|
|
132
|
-
if (platform === 'kuaishou' && resData.jx && resData.jx.length > 0) {
|
|
133
|
-
const item = resData.jx[0];
|
|
134
|
-
result.title = item.title || '';
|
|
135
|
-
result.cover = item.cover || '';
|
|
136
|
-
result.video = item.url || item.video || '';
|
|
137
|
-
result.images = item.images || [];
|
|
138
|
-
result.stat = {
|
|
139
|
-
like: resData.stat?.like || 0,
|
|
140
|
-
coin: 0,
|
|
141
|
-
favorite: 0,
|
|
142
|
-
share: resData.stat?.share || 0,
|
|
143
|
-
view: resData.stat?.view || 0,
|
|
144
|
-
size: 0,
|
|
145
|
-
duration: '00:00'
|
|
146
|
-
};
|
|
147
|
-
result.type = result.images.length > 0 ? 'image' : 'video';
|
|
148
|
-
}
|
|
149
|
-
else if (platform === 'douyin' && resData.jx && resData.jx.length > 0) {
|
|
150
|
-
const item = resData.jx[0];
|
|
151
|
-
result.title = item.title || '';
|
|
152
|
-
result.cover = item.cover || '';
|
|
153
|
-
result.video = item.url || '';
|
|
154
|
-
result.images = item.images || [];
|
|
155
|
-
result.stat = {
|
|
156
|
-
like: resData.stat?.like || 0,
|
|
157
|
-
coin: 0,
|
|
158
|
-
favorite: resData.stat?.collect || 0,
|
|
159
|
-
share: resData.stat?.share || 0,
|
|
160
|
-
view: 0,
|
|
161
|
-
size: 0,
|
|
162
|
-
duration: '00:00'
|
|
163
|
-
};
|
|
164
|
-
result.type = result.images.length > 0 ? 'image' : 'video';
|
|
165
|
-
}
|
|
166
|
-
else if (platform === 'bilibili') {
|
|
167
|
-
const d = resData.data || resData;
|
|
168
|
-
result.title = d.video?.title || d.title || '';
|
|
169
|
-
result.author = d.owner?.name || d.name || '未知UP主';
|
|
170
|
-
result.desc = d.video?.desc || d.desc || '';
|
|
171
|
-
result.cover = d.video?.fm || d.fm || '';
|
|
172
|
-
result.video = d.video?.url || d.url || '';
|
|
173
|
-
result.stat = {
|
|
174
|
-
view: d.stat?.view || 0,
|
|
175
|
-
favorite: d.stat?.favorite || 0,
|
|
176
|
-
like: d.stat?.like || 0,
|
|
177
|
-
coin: d.stat?.coin || 0,
|
|
178
|
-
share: d.stat?.share || 0,
|
|
179
|
-
size: 0,
|
|
180
|
-
duration: formatDuration(d.duration || 0)
|
|
181
|
-
};
|
|
182
|
-
result.duration = d.duration || 0;
|
|
183
|
-
}
|
|
184
|
-
return result;
|
|
185
|
-
}
|
|
186
219
|
function extractUrl(content) {
|
|
187
220
|
let urlMatches = content.match(/https?:\/\/[^\s\"\'\>]+/gi) || [];
|
|
188
221
|
return urlMatches.filter(url => {
|
|
@@ -231,8 +264,16 @@ async function resolveShortUrl(url) {
|
|
|
231
264
|
return url;
|
|
232
265
|
}
|
|
233
266
|
}
|
|
234
|
-
function formatDuration(
|
|
235
|
-
if (!
|
|
267
|
+
function formatDuration(input) {
|
|
268
|
+
if (!input || input === 0 || input === '0' || input === '00:00')
|
|
269
|
+
return '00:00';
|
|
270
|
+
if (typeof input === 'string') {
|
|
271
|
+
if (input.includes(':'))
|
|
272
|
+
return input;
|
|
273
|
+
input = Number(input);
|
|
274
|
+
}
|
|
275
|
+
const seconds = Math.floor(Number(input));
|
|
276
|
+
if (isNaN(seconds) || seconds <= 0)
|
|
236
277
|
return '00:00';
|
|
237
278
|
const hours = Math.floor(seconds / 3600);
|
|
238
279
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
@@ -241,40 +282,87 @@ function formatDuration(seconds) {
|
|
|
241
282
|
? `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
|
242
283
|
: `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
243
284
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
let
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
let stat = data.stat || {
|
|
254
|
-
like: 0,
|
|
255
|
-
coin: 0,
|
|
256
|
-
favorite: 0,
|
|
257
|
-
share: 0,
|
|
258
|
-
view: 0,
|
|
259
|
-
size: 0,
|
|
260
|
-
duration: '00:00'
|
|
261
|
-
};
|
|
262
|
-
const durationFormatted = formatDuration(duration);
|
|
263
|
-
if (platform === 'bilibili' && data.data) {
|
|
264
|
-
title = data.data.title || title;
|
|
265
|
-
author = data.data.owner?.name || '未知作者';
|
|
266
|
-
desc = (data.data.desc || title).slice(0, maxDescLength);
|
|
267
|
-
cover = data.data.pic || cover;
|
|
268
|
-
duration = data.data.duration || 0;
|
|
269
|
-
video = data.playUrl || video;
|
|
270
|
-
stat = data.data.stat || stat;
|
|
285
|
+
function getNestedValue(obj, path) {
|
|
286
|
+
if (!obj || typeof obj !== 'object')
|
|
287
|
+
return undefined;
|
|
288
|
+
const keys = path.split('.');
|
|
289
|
+
let value = obj;
|
|
290
|
+
for (const key of keys) {
|
|
291
|
+
if (value === null || value === undefined)
|
|
292
|
+
return undefined;
|
|
293
|
+
value = value[key];
|
|
271
294
|
}
|
|
272
|
-
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
function findValueInObject(obj, keys) {
|
|
298
|
+
if (!obj || typeof obj !== 'object')
|
|
299
|
+
return undefined;
|
|
300
|
+
for (const key of keys) {
|
|
301
|
+
if (key.includes('.')) {
|
|
302
|
+
const value = getNestedValue(obj, key);
|
|
303
|
+
if (value !== undefined && value !== null && value !== '')
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
if (obj[key] !== undefined && obj[key] !== null && obj[key] !== '')
|
|
308
|
+
return obj[key];
|
|
309
|
+
const lowerKey = key.toLowerCase();
|
|
310
|
+
for (const objKey of Object.keys(obj)) {
|
|
311
|
+
if (objKey.toLowerCase() === lowerKey) {
|
|
312
|
+
return obj[objKey];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
function parseData(rawResponse, maxDescLength, platform) {
|
|
320
|
+
let data = rawResponse;
|
|
321
|
+
if (data.data) {
|
|
322
|
+
data = data.data;
|
|
323
|
+
}
|
|
324
|
+
const stat = {};
|
|
325
|
+
Object.entries(VARIABLE_MAPPING).forEach(([varName, keys]) => {
|
|
326
|
+
const value = findValueInObject(data, keys);
|
|
327
|
+
if (value !== undefined) {
|
|
328
|
+
stat[varName] = value;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
let type = 'video';
|
|
332
|
+
const title = findValueInObject(data, ['title']) || '无标题';
|
|
333
|
+
const author = findValueInObject(data, ['author', 'name']) || '未知作者';
|
|
334
|
+
const desc = (findValueInObject(data, ['desc', 'description', 'content']) || title).toString().slice(0, maxDescLength);
|
|
335
|
+
const cover = findValueInObject(data, ['cover', 'imgurl', 'pic', 'thumbnail']) || '';
|
|
336
|
+
let images = findValueInObject(data, ['imgurl', 'images', 'pics']) || [];
|
|
337
|
+
if (!Array.isArray(images))
|
|
338
|
+
images = [images];
|
|
339
|
+
const videoUrls = [
|
|
340
|
+
findValueInObject(data, ['url']),
|
|
341
|
+
findValueInObject(data, ['download_url']),
|
|
342
|
+
findValueInObject(data, ['video_backup']),
|
|
343
|
+
findValueInObject(data, ['playUrl']),
|
|
344
|
+
findValueInObject(data, ['video_url'])
|
|
345
|
+
];
|
|
346
|
+
let video = '';
|
|
347
|
+
for (const url of videoUrls) {
|
|
348
|
+
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
349
|
+
video = url;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const durationValue = findValueInObject(data, ['duration']);
|
|
354
|
+
const duration = typeof durationValue === 'number' ? durationValue : parseInt(durationValue) || 0;
|
|
355
|
+
const durationFormatted = formatDuration(durationValue || 0);
|
|
356
|
+
const dataType = findValueInObject(data, ['type']);
|
|
357
|
+
if (dataType === 'image' || (images.length > 0 && !video)) {
|
|
273
358
|
type = 'image';
|
|
274
|
-
|
|
359
|
+
}
|
|
360
|
+
if (video && (video.endsWith('.m4a') || video.endsWith('.mp3'))) {
|
|
275
361
|
video = '';
|
|
362
|
+
}
|
|
276
363
|
return {
|
|
277
364
|
type,
|
|
365
|
+
rawData: rawResponse,
|
|
278
366
|
title,
|
|
279
367
|
author,
|
|
280
368
|
desc,
|
|
@@ -283,15 +371,7 @@ function parseData(data, maxDescLength, platform) {
|
|
|
283
371
|
video,
|
|
284
372
|
duration,
|
|
285
373
|
durationFormatted,
|
|
286
|
-
stat
|
|
287
|
-
like: stat.like || 0,
|
|
288
|
-
coin: stat.coin || 0,
|
|
289
|
-
favorite: stat.favorite || 0,
|
|
290
|
-
share: stat.share || 0,
|
|
291
|
-
view: stat.view || 0,
|
|
292
|
-
size: stat.size || 0,
|
|
293
|
-
duration: durationFormatted
|
|
294
|
-
}
|
|
374
|
+
stat
|
|
295
375
|
};
|
|
296
376
|
}
|
|
297
377
|
function generateFormattedText(platform, parseData, config) {
|
|
@@ -299,17 +379,29 @@ function generateFormattedText(platform, parseData, config) {
|
|
|
299
379
|
if (platform !== 'bilibili') {
|
|
300
380
|
format = format.replace(/投币:\$\{投币数\}\n?/g, '');
|
|
301
381
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
.
|
|
309
|
-
.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
382
|
+
let result = format;
|
|
383
|
+
const formatLines = result.split('\n');
|
|
384
|
+
const validLines = [];
|
|
385
|
+
formatLines.forEach((line) => {
|
|
386
|
+
let isValid = true;
|
|
387
|
+
let processedLine = line;
|
|
388
|
+
const varMatches = line.match(/\$\{([^}]+)\}/g) || [];
|
|
389
|
+
varMatches.forEach((varMatch) => {
|
|
390
|
+
const varName = varMatch.replace(/\$\{|\}/g, '');
|
|
391
|
+
const value = parseData.stat[varName];
|
|
392
|
+
if (value === undefined || value === null || value === '' || value === 0 || value === '00:00') {
|
|
393
|
+
isValid = false;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
processedLine = processedLine.replace(varMatch, String(value));
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
if (isValid && processedLine.trim() !== '') {
|
|
400
|
+
validLines.push(processedLine);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
result = validLines.join('\n').trim();
|
|
404
|
+
return result;
|
|
313
405
|
}
|
|
314
406
|
function clearAllCache() {
|
|
315
407
|
processed.clear();
|
|
@@ -364,24 +456,16 @@ function apply(ctx, config) {
|
|
|
364
456
|
return { data: null, msg: '该平台暂未配置解析接口' };
|
|
365
457
|
}
|
|
366
458
|
try {
|
|
367
|
-
const res = await http.get(apiUrl, {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if ((res.data.code === 200 || res.data.code === 0) && res.data.data) {
|
|
375
|
-
parseResult = parseData(res.data.data, config.maxDescLength, platform);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
if (parseResult) {
|
|
379
|
-
return { data: parseResult, msg: `${platform}解析成功` };
|
|
380
|
-
}
|
|
381
|
-
else {
|
|
382
|
-
logger.error(`解析返回数据无效: ${platform}, URL: ${url}`);
|
|
383
|
-
return { data: null, msg: '解析失败,返回数据无效' };
|
|
459
|
+
const res = await http.get(apiUrl, {
|
|
460
|
+
params: { url: realUrl },
|
|
461
|
+
timeout: config.timeout
|
|
462
|
+
});
|
|
463
|
+
if (res.data.code !== 200 && res.data.code !== 0) {
|
|
464
|
+
logger.error(`API返回错误: ${platform}, URL: ${url}, 错误: ${res.data.msg || '未知错误'}`);
|
|
465
|
+
return { data: null, msg: res.data.msg || '解析失败' };
|
|
384
466
|
}
|
|
467
|
+
const parseResult = parseData(res.data, config.maxDescLength, platform);
|
|
468
|
+
return { data: parseResult, msg: `${platform}解析成功` };
|
|
385
469
|
}
|
|
386
470
|
catch (error) {
|
|
387
471
|
logger.error(`解析请求失败: ${platform}, URL: ${url}, 错误: ${getErrorMessage(error)}`);
|
|
@@ -483,21 +567,27 @@ function apply(ctx, config) {
|
|
|
483
567
|
}
|
|
484
568
|
if (item.video && config.showVideoFile && forwardMessages.length < 100) {
|
|
485
569
|
let videoElem;
|
|
486
|
-
|
|
487
|
-
|
|
570
|
+
try {
|
|
571
|
+
if (config.downloadVideoBeforeSend) {
|
|
488
572
|
const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
489
|
-
const filePath = await downloadVideo(item.video, filename, config.userAgent);
|
|
573
|
+
const filePath = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
490
574
|
videoElem = koishi_1.h.file(filePath);
|
|
491
575
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
576
|
+
else {
|
|
577
|
+
const fileSize = await getFileSize(item.video, config.userAgent);
|
|
578
|
+
if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
|
|
579
|
+
videoElem = koishi_1.h.text(`视频大小${fileSize}MB超过限制${config.maxVideoSize}MB,仅发送链接:${item.video}`);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
videoElem = koishi_1.h.video(item.video);
|
|
583
|
+
}
|
|
495
584
|
}
|
|
585
|
+
forwardMessages.push(buildForwardNode(session, videoElem, botName));
|
|
496
586
|
}
|
|
497
|
-
|
|
498
|
-
|
|
587
|
+
catch (error) {
|
|
588
|
+
logger.error(`视频处理失败: ${getErrorMessage(error)}`);
|
|
589
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`视频处理失败:${getErrorMessage(error)}\n链接:${item.video}`), botName));
|
|
499
590
|
}
|
|
500
|
-
forwardMessages.push(buildForwardNode(session, videoElem, botName));
|
|
501
591
|
}
|
|
502
592
|
}
|
|
503
593
|
else {
|
|
@@ -515,22 +605,28 @@ function apply(ctx, config) {
|
|
|
515
605
|
await delay(300);
|
|
516
606
|
}
|
|
517
607
|
if (item.video && config.showVideoFile) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
608
|
+
try {
|
|
609
|
+
let videoElem;
|
|
610
|
+
if (config.downloadVideoBeforeSend) {
|
|
521
611
|
const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
|
|
522
|
-
const filePath = await downloadVideo(item.video, filename, config.userAgent);
|
|
612
|
+
const filePath = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
|
|
523
613
|
videoElem = koishi_1.h.file(filePath);
|
|
524
614
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
615
|
+
else {
|
|
616
|
+
const fileSize = await getFileSize(item.video, config.userAgent);
|
|
617
|
+
if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
|
|
618
|
+
videoElem = koishi_1.h.text(`视频大小${fileSize}MB超过限制${config.maxVideoSize}MB,仅发送链接:${item.video}`);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
videoElem = koishi_1.h.video(item.video);
|
|
622
|
+
}
|
|
528
623
|
}
|
|
624
|
+
await sendTimeout(session, videoElem);
|
|
529
625
|
}
|
|
530
|
-
|
|
531
|
-
|
|
626
|
+
catch (error) {
|
|
627
|
+
logger.error(`视频处理失败: ${getErrorMessage(error)}`);
|
|
628
|
+
await sendTimeout(session, koishi_1.h.text(`视频处理失败:${getErrorMessage(error)}\n链接:${item.video}`));
|
|
532
629
|
}
|
|
533
|
-
await sendTimeout(session, videoElem);
|
|
534
630
|
}
|
|
535
631
|
}
|
|
536
632
|
await delay(1000);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-video-parser-all",
|
|
3
|
-
"description": "Koishi 全平台视频解析插件,支持抖音/快手/B
|
|
4
|
-
"version": "0.5.
|
|
3
|
+
"description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/小红书/微博/今日头条/皮皮搞笑/皮皮虾/最右视频链接解析",
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|