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.
Files changed (2) hide show
  1. package/lib/index.js +202 -48
  2. 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(0).description('API请求重试次数'),
42
- retryInterval: koishi_1.Schema.number().min(0).default(0).description('重试间隔时间(毫秒)'),
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.bugpk.com/api/bilibili',
128
- douyin: 'https://api.bugpk.com/api/dyjx',
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: 5000,
322
- maxRedirects: 5,
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
- if (data.data) {
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
- let type = data.type || 'video';
398
- const title = findValueInObject(data, ['title']) || '无标题';
399
- const author = findValueInObject(data, ['author.name', 'author']) || '未知作者';
400
- const desc = (findValueInObject(data, ['desc', 'description', 'content']) || title).toString().slice(0, maxDescLength);
401
- const cover = findValueInObject(data, ['cover', 'imgurl', 'pic', 'thumbnail']) || '';
402
- let images = findValueInObject(data, ['imgurl', 'images', 'pics']) || [];
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
- const videoUrls = [
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 (Array.isArray(videoUrls[2])) {
414
- video = videoUrls[2][0]?.url || '';
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
- for (const url of videoUrls) {
418
- if (url && typeof url === 'string' && url.trim() !== '') {
419
- video = url;
420
- break;
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
- const realUrl = await resolveShortUrl(url);
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 res = await http.get(apiUrl, {
533
- params: { url: realUrl },
534
- timeout: config.timeout
535
- });
536
- if (res.data.code !== 200 && res.data.code !== 0) {
537
- const apiErrorMsg = res.data.msg || '解析失败';
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, apiErrorMsg);
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(res.data, config.maxDescLength, platform);
546
- if (!parseResult.video && !parseResult.images.length && !parseResult.live_photo?.length) {
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
- if (item.type === 'live' || item.type === 'live_photo') {
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
3
  "description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/小红书/微博/今日头条/皮皮搞笑/皮皮虾/最右视频链接解析",
4
- "version": "0.5.3",
4
+ "version": "0.5.5",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [