koishi-plugin-video-parser-all 0.5.3 → 0.5.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.js +202 -48
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -38,8 +38,8 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
38
38
|
}).description('网络与API设置'),
|
|
39
39
|
koishi_1.Schema.object({
|
|
40
40
|
ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送失败错误'),
|
|
41
|
-
retryTimes: koishi_1.Schema.number().min(0).default(
|
|
42
|
-
retryInterval: koishi_1.Schema.number().min(0).default(
|
|
41
|
+
retryTimes: koishi_1.Schema.number().min(0).default(3).description('API请求重试次数'),
|
|
42
|
+
retryInterval: koishi_1.Schema.number().min(0).default(1000).description('重试间隔时间(毫秒)'),
|
|
43
43
|
}).description('错误与重试设置'),
|
|
44
44
|
koishi_1.Schema.object({
|
|
45
45
|
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot平台)'),
|
|
@@ -115,7 +115,7 @@ const logger = new koishi_1.Logger(exports.name);
|
|
|
115
115
|
const PLATFORM_KEYWORDS = {
|
|
116
116
|
bilibili: ['bilibili', 'b23', 'B站', 'www.bilibili.com', 'm.bilibili.com', '哔哩哔哩', 'bilibili.com/opus', 'bilibili.com/video', 'b23.tv', 't.bilibili.com', 'bilibili.com/bangumi'],
|
|
117
117
|
kuaishou: ['kuaishou', '快手', 'v.kuaishou.com', 'www.kuaishou.com', 'kwimgs.com', 'kuaishou.com/app'],
|
|
118
|
-
xiaohongshu: ['xiaohongshu', '小红书', 'xhslink.com', 'xiaohongshu.com', 'xhscdn.com', 'xiaohongshu.com/explore', 'xhslink.com/'],
|
|
118
|
+
xiaohongshu: ['xiaohongshu', '小红书', 'xhslink.com', 'xiaohongshu.com', 'xhscdn.com', 'xiaohongshu.com/explore', 'xhslink.com/', 'xiaohongshu.com/discovery/item'],
|
|
119
119
|
weibo: ['weibo', '微博', 'weibo.com', 'video.weibo.com', 'm.weibo.cn', 'weibo.com/tv/show', 'weibo.com/feed'],
|
|
120
120
|
toutiao: ['toutiao', '今日头条', 'm.toutiao.com', 'toutiao.com', 'ixigua.com', 'toutiao.com/video', 'ixigua.com/i'],
|
|
121
121
|
pipigx: ['pipigx', '皮皮搞笑', 'h5.pipigx.com', 'ippzone.com', 'pipigx.com/share'],
|
|
@@ -123,9 +123,10 @@ const PLATFORM_KEYWORDS = {
|
|
|
123
123
|
douyin: ['douyin', '抖音', 'v.douyin.com', 'douyinpic.com', 'douyinvod.com', 'douyin.com/video', 'douyin.com/note', 'www.douyin.com', 'tiktok.com'],
|
|
124
124
|
zuiyou: ['zuiyou', '最右', 'xiaochuankeji.cn', 'izuiyou.com', 'izuiyou.com/topic']
|
|
125
125
|
};
|
|
126
|
+
// 更新API配置:B站和抖音使用新的api.xingzhige.com,其他平台保持不变
|
|
126
127
|
const API_CONFIG = {
|
|
127
|
-
bilibili: 'https://api.
|
|
128
|
-
douyin: 'https://api.
|
|
128
|
+
bilibili: 'https://api.xingzhige.com/API/b_parse',
|
|
129
|
+
douyin: 'https://api.xingzhige.com/API/douyin/',
|
|
129
130
|
kuaishou: 'https://api.bugpk.com/api/ksjx',
|
|
130
131
|
xiaohongshu: 'https://api.bugpk.com/api/xhsjx',
|
|
131
132
|
weibo: 'https://api.bugpk.com/api/weibo',
|
|
@@ -139,20 +140,24 @@ const PLATFORM_ERROR_CODE_MAP = {
|
|
|
139
140
|
xiaohongshu: ErrorCode.XIAOHONGSHU_PARSE_FAILED,
|
|
140
141
|
bilibili: ErrorCode.BILIBILI_PARSE_FAILED,
|
|
141
142
|
kuaishou: ErrorCode.KUAISHOU_PARSE_FAILED,
|
|
142
|
-
weibo: ErrorCode.WEIBO_PARSE_FAILED
|
|
143
|
+
weibo: ErrorCode.WEIBO_PARSE_FAILED,
|
|
144
|
+
toutiao: ErrorCode.API_RETURN_ERROR,
|
|
145
|
+
pipigx: ErrorCode.API_RETURN_ERROR,
|
|
146
|
+
pipixia: ErrorCode.API_RETURN_ERROR,
|
|
147
|
+
zuiyou: ErrorCode.API_RETURN_ERROR
|
|
143
148
|
};
|
|
144
149
|
const VARIABLE_MAPPING = {
|
|
145
150
|
'标题': ['title', 'Title', 'TITLE'],
|
|
146
|
-
'作者': ['author.name', 'author', 'name', 'Author', 'Name'],
|
|
151
|
+
'作者': ['author.name', 'author', 'name', 'Author', 'Name', 'owner.name'],
|
|
147
152
|
'简介': ['desc', 'description', 'Desc', 'Description', 'content', 'Content'],
|
|
148
153
|
'视频时长': ['duration', 'Duration', 'time', 'Time'],
|
|
149
|
-
'点赞数': ['like', 'Like', 'attitudes_count', 'digg_count', 'praise'],
|
|
154
|
+
'点赞数': ['like', 'Like', 'attitudes_count', 'digg_count', 'praise', 'stat.like'],
|
|
150
155
|
'投币数': ['coin', 'Coin', 'bi', 'Bi'],
|
|
151
|
-
'收藏数': ['collect', 'Collect', 'favorite', 'Favorite', 'star', 'Star'],
|
|
152
|
-
'转发数': ['share', 'Share', 'forward', 'Forward', 'repost'],
|
|
156
|
+
'收藏数': ['collect', 'Collect', 'favorite', 'Favorite', 'star', 'Star', 'stat.collect'],
|
|
157
|
+
'转发数': ['share', 'Share', 'forward', 'Forward', 'repost', 'stat.share'],
|
|
153
158
|
'播放数': ['view', 'View', 'play_count', 'PlayCount', 'play'],
|
|
154
|
-
'评论数': ['comment', 'Comment', 'comments_count', 'comment_count', 'discuss'],
|
|
155
|
-
'音乐名': ['music.title', 'music_name', 'audio_name', 'sound_name']
|
|
159
|
+
'评论数': ['comment', 'Comment', 'comments_count', 'comment_count', 'discuss', 'stat.comment'],
|
|
160
|
+
'音乐名': ['music.title', 'music_name', 'audio_name', 'sound_name', 'muisic']
|
|
156
161
|
};
|
|
157
162
|
function getErrorInfo(code, detail) {
|
|
158
163
|
const baseMsg = exports.ErrorMessageMap[code] || exports.ErrorMessageMap[ErrorCode.UNKNOWN_ERROR];
|
|
@@ -315,19 +320,41 @@ function getPlatformType(url) {
|
|
|
315
320
|
return 'zuiyou';
|
|
316
321
|
return null;
|
|
317
322
|
}
|
|
323
|
+
function cleanUrl(url) {
|
|
324
|
+
try {
|
|
325
|
+
// 处理HTML实体编码
|
|
326
|
+
url = url.replace(/&/g, '&');
|
|
327
|
+
const urlObj = new URL(url);
|
|
328
|
+
if (urlObj.hostname.includes('xiaohongshu.com')) {
|
|
329
|
+
urlObj.searchParams.delete('source');
|
|
330
|
+
urlObj.searchParams.delete('xhsshare');
|
|
331
|
+
urlObj.searchParams.delete('xsec_token');
|
|
332
|
+
urlObj.searchParams.delete('xsec_source');
|
|
333
|
+
return urlObj.origin + urlObj.pathname + urlObj.search;
|
|
334
|
+
}
|
|
335
|
+
if (urlObj.hostname.includes('douyin.com') || urlObj.hostname.includes('v.douyin.com')) {
|
|
336
|
+
return urlObj.origin + urlObj.pathname;
|
|
337
|
+
}
|
|
338
|
+
return url;
|
|
339
|
+
}
|
|
340
|
+
catch (e) {
|
|
341
|
+
// 处理HTML实体编码
|
|
342
|
+
return url.replace(/&/g, '&');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
318
345
|
async function resolveShortUrl(url) {
|
|
319
346
|
try {
|
|
320
347
|
const res = await axios_1.default.head(url, {
|
|
321
|
-
timeout:
|
|
322
|
-
maxRedirects:
|
|
348
|
+
timeout: 10000,
|
|
349
|
+
maxRedirects: 10,
|
|
323
350
|
headers: {
|
|
324
351
|
'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'
|
|
325
352
|
}
|
|
326
353
|
});
|
|
327
|
-
return res.request.res?.responseUrl || url;
|
|
354
|
+
return cleanUrl(res.request.res?.responseUrl || url);
|
|
328
355
|
}
|
|
329
356
|
catch (e) {
|
|
330
|
-
return url;
|
|
357
|
+
return cleanUrl(url);
|
|
331
358
|
}
|
|
332
359
|
}
|
|
333
360
|
function formatDuration(input) {
|
|
@@ -382,48 +409,126 @@ function findValueInObject(obj, keys) {
|
|
|
382
409
|
}
|
|
383
410
|
return undefined;
|
|
384
411
|
}
|
|
412
|
+
// 适配新的API返回格式
|
|
385
413
|
function parseData(rawResponse, maxDescLength, platform) {
|
|
386
414
|
let data = rawResponse;
|
|
387
|
-
|
|
415
|
+
// 处理不同平台的返回结构差异
|
|
416
|
+
if (platform === 'bilibili' && rawResponse.data) {
|
|
417
|
+
// 适配api.xingzhige.com的B站返回格式
|
|
418
|
+
data = rawResponse.data;
|
|
419
|
+
}
|
|
420
|
+
else if (platform === 'douyin' && rawResponse.data) {
|
|
421
|
+
// 适配api.xingzhige.com的抖音返回格式
|
|
422
|
+
data = rawResponse.data;
|
|
423
|
+
}
|
|
424
|
+
else if (data.data) {
|
|
425
|
+
// 其他平台保持原有逻辑
|
|
388
426
|
data = data.data;
|
|
389
427
|
}
|
|
390
428
|
const stat = {};
|
|
429
|
+
// 适配新的字段映射
|
|
391
430
|
Object.entries(VARIABLE_MAPPING).forEach(([varName, keys]) => {
|
|
392
431
|
const value = findValueInObject(data, keys);
|
|
393
432
|
if (value !== undefined) {
|
|
394
433
|
stat[varName] = value;
|
|
395
434
|
}
|
|
396
435
|
});
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
436
|
+
// 处理不同平台的类型字段
|
|
437
|
+
let type = 'video';
|
|
438
|
+
if (platform === 'douyin' && data.jx && data.jx.type) {
|
|
439
|
+
type = data.jx.type; // 抖音的图集类型
|
|
440
|
+
}
|
|
441
|
+
else if (data.type) {
|
|
442
|
+
type = data.type;
|
|
443
|
+
}
|
|
444
|
+
// 适配不同平台的标题字段
|
|
445
|
+
let title = '无标题';
|
|
446
|
+
if (platform === 'bilibili' && data.video && data.video.title) {
|
|
447
|
+
title = data.video.title;
|
|
448
|
+
}
|
|
449
|
+
else if (platform === 'douyin' && data.item && data.item.title) {
|
|
450
|
+
title = data.item.title;
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
title = findValueInObject(data, ['title']) || '无标题';
|
|
454
|
+
}
|
|
455
|
+
// 适配不同平台的作者字段
|
|
456
|
+
let author = '未知作者';
|
|
457
|
+
if (platform === 'bilibili' && data.owner && data.owner.name) {
|
|
458
|
+
author = data.owner.name;
|
|
459
|
+
}
|
|
460
|
+
else if (platform === 'douyin' && data.author && data.author.name) {
|
|
461
|
+
author = data.author.name;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
author = findValueInObject(data, ['author.name', 'author', 'name', 'auther']) || '未知作者';
|
|
465
|
+
}
|
|
466
|
+
// 适配不同平台的描述字段
|
|
467
|
+
let desc = title;
|
|
468
|
+
if (platform === 'bilibili' && data.video && data.video.desc) {
|
|
469
|
+
desc = data.video.desc;
|
|
470
|
+
}
|
|
471
|
+
else if (platform === 'douyin') {
|
|
472
|
+
desc = title; // 抖音没有单独的描述字段
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
desc = findValueInObject(data, ['desc', 'description', 'content']) || title;
|
|
476
|
+
}
|
|
477
|
+
desc = desc.toString().slice(0, maxDescLength);
|
|
478
|
+
// 适配不同平台的封面字段
|
|
479
|
+
let cover = '';
|
|
480
|
+
if (platform === 'bilibili' && data.video && data.video.fm) {
|
|
481
|
+
cover = data.video.fm;
|
|
482
|
+
}
|
|
483
|
+
else if (platform === 'douyin' && data.item && data.item.cover) {
|
|
484
|
+
cover = data.item.cover;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
cover = findValueInObject(data, ['cover', 'imgurl', 'pic', 'thumbnail']) || '';
|
|
488
|
+
}
|
|
489
|
+
// 适配不同平台的图片字段
|
|
490
|
+
let images = [];
|
|
491
|
+
if (platform === 'douyin' && data.item && data.item.images) {
|
|
492
|
+
images = data.item.images;
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
images = findValueInObject(data, ['imgurl', 'images', 'pics']) || [];
|
|
496
|
+
}
|
|
403
497
|
if (!Array.isArray(images))
|
|
404
498
|
images = [images];
|
|
405
|
-
|
|
406
|
-
findValueInObject(data, ['url']),
|
|
407
|
-
findValueInObject(data, ['download_url']),
|
|
408
|
-
findValueInObject(data, ['video_backup']),
|
|
409
|
-
findValueInObject(data, ['playUrl']),
|
|
410
|
-
findValueInObject(data, ['video_url'])
|
|
411
|
-
];
|
|
499
|
+
// 适配不同平台的视频链接字段
|
|
412
500
|
let video = '';
|
|
413
|
-
if (
|
|
414
|
-
video =
|
|
501
|
+
if (platform === 'bilibili' && data.video && data.video.url) {
|
|
502
|
+
video = data.video.url;
|
|
503
|
+
}
|
|
504
|
+
else if (platform === 'douyin') {
|
|
505
|
+
video = ''; // 抖音图集没有视频链接
|
|
415
506
|
}
|
|
416
507
|
else {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
508
|
+
const videoUrls = [
|
|
509
|
+
findValueInObject(data, ['url']),
|
|
510
|
+
findValueInObject(data, ['download_url']),
|
|
511
|
+
findValueInObject(data, ['video_backup']),
|
|
512
|
+
findValueInObject(data, ['playUrl']),
|
|
513
|
+
findValueInObject(data, ['video_url'])
|
|
514
|
+
];
|
|
515
|
+
if (Array.isArray(videoUrls[2])) {
|
|
516
|
+
video = videoUrls[2][0]?.url || '';
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
for (const url of videoUrls) {
|
|
520
|
+
if (url && typeof url === 'string' && url.trim() !== '') {
|
|
521
|
+
video = url;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
421
524
|
}
|
|
422
525
|
}
|
|
423
526
|
}
|
|
527
|
+
// 适配不同平台的时长字段
|
|
424
528
|
const durationValue = findValueInObject(data, ['duration']);
|
|
425
529
|
const duration = typeof durationValue === 'number' ? durationValue : parseInt(durationValue) || 0;
|
|
426
530
|
const durationFormatted = formatDuration(durationValue || 0);
|
|
531
|
+
// 处理live_photo字段
|
|
427
532
|
const live_photo = data.live_photo || [];
|
|
428
533
|
return {
|
|
429
534
|
type: type,
|
|
@@ -512,8 +617,28 @@ function apply(ctx, config) {
|
|
|
512
617
|
timeout: config.timeout,
|
|
513
618
|
headers: { 'User-Agent': config.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }
|
|
514
619
|
});
|
|
620
|
+
async function parseWithRetry(url, platform, retryTimes) {
|
|
621
|
+
let lastError = null;
|
|
622
|
+
for (let i = 0; i <= retryTimes; i++) {
|
|
623
|
+
try {
|
|
624
|
+
const res = await http.get(API_CONFIG[platform], {
|
|
625
|
+
params: { url },
|
|
626
|
+
timeout: config.timeout
|
|
627
|
+
});
|
|
628
|
+
return res.data;
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
lastError = error;
|
|
632
|
+
if (i < retryTimes) {
|
|
633
|
+
await delay(config.retryInterval);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
throw lastError;
|
|
638
|
+
}
|
|
515
639
|
async function parse(url) {
|
|
516
|
-
|
|
640
|
+
let realUrl = await resolveShortUrl(url);
|
|
641
|
+
realUrl = cleanUrl(realUrl);
|
|
517
642
|
const platform = getPlatformType(realUrl);
|
|
518
643
|
if (!platform) {
|
|
519
644
|
const code = ErrorCode.UNSUPPORTED_PLATFORM;
|
|
@@ -529,23 +654,41 @@ function apply(ctx, config) {
|
|
|
529
654
|
return { data: null, code, msg };
|
|
530
655
|
}
|
|
531
656
|
try {
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (
|
|
537
|
-
|
|
657
|
+
const resData = await parseWithRetry(realUrl, platform, config.retryTimes);
|
|
658
|
+
// 适配不同平台的成功判断逻辑
|
|
659
|
+
let isSuccess = false;
|
|
660
|
+
// B站和抖音使用新的判断逻辑(code=0表示成功)
|
|
661
|
+
if (platform === 'bilibili' || platform === 'douyin') {
|
|
662
|
+
isSuccess = resData.code === 0 || (resData.msg && (resData.msg.includes('解析成功') || resData.msg === 'video'));
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
// 其他平台保持原有逻辑
|
|
666
|
+
isSuccess = resData.code === 200 || resData.code === 0 ||
|
|
667
|
+
(resData.msg && resData.msg.includes('解析成功'));
|
|
668
|
+
}
|
|
669
|
+
if (!isSuccess) {
|
|
670
|
+
const apiErrorMsg = resData.msg || '解析失败';
|
|
538
671
|
const platformCode = PLATFORM_ERROR_CODE_MAP[platform] || ErrorCode.API_RETURN_ERROR;
|
|
672
|
+
let detailedMsg = apiErrorMsg;
|
|
673
|
+
if (apiErrorMsg.includes('无法识别解析类型') || apiErrorMsg.includes('未找到有效内容')) {
|
|
674
|
+
detailedMsg = `链接格式不支持或内容已失效:${apiErrorMsg}`;
|
|
675
|
+
}
|
|
539
676
|
const code = platformCode;
|
|
540
|
-
const msg = getErrorInfo(code,
|
|
677
|
+
const msg = getErrorInfo(code, detailedMsg);
|
|
541
678
|
logger.error(`[${code}] API返回错误: ${platform}, URL: ${url}, 错误: ${apiErrorMsg}`);
|
|
542
679
|
return { data: null, code, msg };
|
|
543
680
|
}
|
|
544
681
|
try {
|
|
545
|
-
const parseResult = parseData(
|
|
546
|
-
|
|
682
|
+
const parseResult = parseData(resData, config.maxDescLength, platform);
|
|
683
|
+
// 修正内容判断逻辑:支持抖音图集、live类型和live_photo
|
|
684
|
+
const hasValidContent = parseResult.video ||
|
|
685
|
+
(parseResult.images && parseResult.images.length > 0) ||
|
|
686
|
+
(parseResult.live_photo && parseResult.live_photo.length > 0) ||
|
|
687
|
+
parseResult.type === 'live' ||
|
|
688
|
+
parseResult.type === '图集';
|
|
689
|
+
if (!hasValidContent) {
|
|
547
690
|
const code = ErrorCode.NO_VIDEO_FOUND;
|
|
548
|
-
const msg = getErrorInfo(code, '
|
|
691
|
+
const msg = getErrorInfo(code, '链接有效但未找到视频/图片内容(可能是直播、私密内容或已删除)');
|
|
549
692
|
logger.warn(`[${code}] 解析成功但无有效内容: ${platform}, URL: ${url}`);
|
|
550
693
|
return { data: null, code, msg };
|
|
551
694
|
}
|
|
@@ -673,6 +816,12 @@ function apply(ctx, config) {
|
|
|
673
816
|
forwardMessages.push(buildForwardNode(session, item.text, botName));
|
|
674
817
|
if (item.cover && forwardMessages.length < 100)
|
|
675
818
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.cover), botName));
|
|
819
|
+
// 处理抖音图集
|
|
820
|
+
if (item.type === '图集' && item.images?.length) {
|
|
821
|
+
for (let i = 0; i < item.images.length && forwardMessages.length < 100; i++) {
|
|
822
|
+
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.images[i]), botName));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
676
825
|
if (item.type === 'image' && item.images?.length) {
|
|
677
826
|
for (let i = 0; i < item.images.length && forwardMessages.length < 100; i++) {
|
|
678
827
|
forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.images[i]), botName));
|
|
@@ -740,7 +889,12 @@ function apply(ctx, config) {
|
|
|
740
889
|
await sendTimeout(session, item.text);
|
|
741
890
|
await delay(300);
|
|
742
891
|
}
|
|
743
|
-
|
|
892
|
+
// 处理抖音图集
|
|
893
|
+
if (item.type === '图集' && item.images?.length) {
|
|
894
|
+
const imgMsg = (0, koishi_1.h)('message', ...item.images.map((url) => koishi_1.h.image(url)));
|
|
895
|
+
await sendTimeout(session, imgMsg);
|
|
896
|
+
}
|
|
897
|
+
else if (item.type === 'live' || item.type === 'live_photo') {
|
|
744
898
|
if (item.live_photo?.length) {
|
|
745
899
|
for (const liveItem of item.live_photo) {
|
|
746
900
|
await sendTimeout(session, koishi_1.h.image(liveItem.image));
|
package/package.json
CHANGED