koishi-plugin-video-parser-all 1.4.6 → 1.4.8

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 +98 -24
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -543,8 +543,8 @@ function parseApiResponse(raw, maxDescLen, fieldMapping) {
543
543
  uid = String(mapField('uid', () => data.uid || data.userID || data.author_id || ''));
544
544
  avatar = mapField('avatar', () => data.avatar || '');
545
545
  }
546
- const title = mapField('title', () => data.title || '');
547
- const desc = mapField('desc', () => data.desc || data.description || '').slice(0, maxDescLen).trim();
546
+ let title = mapField('title', () => data.title || '');
547
+ let desc = mapField('desc', () => data.desc || data.description || '').slice(0, maxDescLen).trim();
548
548
  const coverRaw = mapField('cover', () => data.cover || '');
549
549
  const cover = coverRaw ? (String(coverRaw).startsWith('http') ? String(coverRaw) : 'https:' + coverRaw) : '';
550
550
  let video = '';
@@ -620,7 +620,30 @@ function parseApiResponse(raw, maxDescLen, fieldMapping) {
620
620
  else if (extra.create_time) {
621
621
  publishTime = Number(extra.create_time) * 1000;
622
622
  }
623
- return { type, title, desc, author, uid, avatar, cover, video, videos, images, live_photo, music, like, comment, collect, share, play, duration, publishTime };
623
+ const author_followers = parseCount(mapField('author_followers', () => extra.author_extra?.follower_count ?? data.author_extra?.follower_count ?? 0));
624
+ const author_signature = String(mapField('author_signature', () => extra.author_extra?.signature ?? data.author_extra?.signature ?? ''));
625
+ const admire = parseCount(mapField('admire', () => extra.statistics?.admire_count ?? data.statistics?.admire_count ?? 0));
626
+ let hashtags = '';
627
+ const tagsArray = mapField('hashtags', () => extra.hashtags ?? data.hashtags);
628
+ if (Array.isArray(tagsArray) && tagsArray.length > 0) {
629
+ const tagNames = tagsArray.map((t) => t?.name ? t.name : '').filter(Boolean);
630
+ hashtags = tagNames.map(name => `#${name}`).join(' ');
631
+ if (tagNames.length > 0 && desc) {
632
+ const escapedTags = tagNames.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
633
+ const tagRegex = new RegExp(`[##](${escapedTags.join('|')})\\b`, 'gi');
634
+ desc = desc.replace(tagRegex, '').replace(/\s{2,}/g, ' ').trim();
635
+ }
636
+ }
637
+ title = title.replace(/\[话题\]/g, '');
638
+ desc = desc.replace(/\[话题\]/g, '');
639
+ if (title && desc && title.trim() === desc.trim()) {
640
+ desc = '';
641
+ }
642
+ if (title.trim().startsWith('#'))
643
+ title = '';
644
+ if (desc.trim().startsWith('#'))
645
+ desc = '';
646
+ return { type, title, desc, author, uid, avatar, cover, video, videos, images, live_photo, music, like, comment, collect, share, play, duration, publishTime, author_followers, author_signature, admire, hashtags };
624
647
  }
625
648
  const formatVarRegex = /\$\{([^}]+)\}/g;
626
649
  function generateFormattedText(p, format) {
@@ -628,6 +651,8 @@ function generateFormattedText(p, format) {
628
651
  const vars = {
629
652
  '标题': p.title,
630
653
  '作者': p.author,
654
+ '作者名称': p.author,
655
+ 'author-name': p.author,
631
656
  '简介': p.desc,
632
657
  '视频时长': p.duration > 0 ? formatDuration(p.duration) : '',
633
658
  '点赞数': String(p.like),
@@ -638,10 +663,22 @@ function generateFormattedText(p, format) {
638
663
  '发布时间': p.publishTime ? formatPublishTime(p.publishTime) : '',
639
664
  '图片数量': String(imageCount),
640
665
  '作者ID': p.uid,
666
+ 'author-id': p.uid,
641
667
  '视频链接': p.video,
642
668
  '音乐标题': p.music.title || '',
669
+ 'music-title': p.music.title || '',
643
670
  '音乐作者': p.music.author || '',
671
+ 'music-author': p.music.author || '',
644
672
  '音乐封面': p.music.cover || '',
673
+ 'author-avatar': p.avatar,
674
+ '作者粉丝数': String(p.author_followers),
675
+ 'author-followers': String(p.author_followers),
676
+ '作者签名': p.author_signature,
677
+ 'author-signature': p.author_signature,
678
+ '赞赏数': String(p.admire),
679
+ 'admire-count': String(p.admire),
680
+ '话题标签': p.hashtags,
681
+ 'hashtags': p.hashtags,
645
682
  };
646
683
  const varReplacements = Object.entries(vars).map(([key, val]) => ({
647
684
  regex: new RegExp(`\\$\\{${key}\\}`, 'g'),
@@ -806,7 +843,7 @@ function apply(ctx, config) {
806
843
  return {};
807
844
  }
808
845
  const extRegexCache = {};
809
- async function downloadFile(url, timeout, maxSize, filePrefix, fileExts) {
846
+ async function downloadFile(url, timeout, maxSize, filePrefix, fileExts, onProgress) {
810
847
  if (!url)
811
848
  throw new Error('链接为空');
812
849
  await promises_1.default.mkdir(cacheDir, { recursive: true });
@@ -817,23 +854,21 @@ function apply(ctx, config) {
817
854
  const fileName = `${filePrefix}_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
818
855
  const filePath = path_1.default.resolve(cacheDir, fileName);
819
856
  if (downloadEngine === 'downloads' && ctx.downloads) {
820
- try {
821
- const dest = await ctx.downloads.download(url, path_1.default.join(cacheDir, fileName), {
822
- headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
823
- timeout
824
- });
825
- const stat = await promises_1.default.stat(dest);
826
- if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
827
- await promises_1.default.unlink(dest).catch(() => { });
828
- throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
829
- }
830
- return dest;
831
- }
832
- catch (e) {
833
- debugLog('ERROR', `downloads 服务下载失败,回退内置下载: ${getErrorMessage(e)}`);
857
+ logger.info(`开始下载: ${url}`);
858
+ const dest = await ctx.downloads.download(url, path_1.default.join(cacheDir, fileName), {
859
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
860
+ timeout
861
+ });
862
+ const stat = await promises_1.default.stat(dest);
863
+ const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
864
+ logger.info(`下载完成: ${dest} (${sizeMB} MB)`);
865
+ if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
866
+ await promises_1.default.unlink(dest).catch(() => { });
867
+ throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
834
868
  }
869
+ return dest;
835
870
  }
836
- else if (downloadEngine === 'aria2' && ctx.aria2) {
871
+ if (downloadEngine === 'aria2' && ctx.aria2) {
837
872
  try {
838
873
  const gid = await ctx.aria2.addUri([url], {
839
874
  dir: cacheDir,
@@ -848,20 +883,31 @@ function apply(ctx, config) {
848
883
  header: [`User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36`, `Referer: https://www.baidu.com/`]
849
884
  });
850
885
  let completed = false;
851
- const ariaStartTime = Date.now();
886
+ const startTime = Date.now();
887
+ let lastProgressTime = 0;
852
888
  while (!completed) {
853
- if (Date.now() - ariaStartTime > timeout) {
889
+ if (Date.now() - startTime > timeout) {
854
890
  await ctx.aria2.remove(gid).catch(() => { });
855
891
  throw new Error('aria2下载超时');
856
892
  }
857
893
  const status = await ctx.aria2.tellStatus(gid);
858
894
  if (status.status === 'complete') {
859
895
  completed = true;
896
+ if (onProgress)
897
+ onProgress(100, '');
860
898
  }
861
899
  else if (status.status === 'error' || status.status === 'removed') {
862
900
  throw new Error('aria2下载失败');
863
901
  }
864
902
  else {
903
+ if (onProgress && Date.now() - lastProgressTime > 500) {
904
+ lastProgressTime = Date.now();
905
+ const total = parseInt(status.totalLength) || 0;
906
+ const completed = parseInt(status.completedLength) || 0;
907
+ const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
908
+ const speed = formatSpeed(parseInt(status.downloadSpeed) || 0);
909
+ onProgress(percent, speed);
910
+ }
865
911
  await delay(1000);
866
912
  }
867
913
  }
@@ -901,8 +947,20 @@ function apply(ctx, config) {
901
947
  await promises_1.default.unlink(filePath).catch(() => { });
902
948
  throw new Error(`文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
903
949
  }
950
+ let downloaded = 0;
951
+ let lastProgressTime = 0;
952
+ response.data.on('data', (chunk) => {
953
+ downloaded += chunk.length;
954
+ if (onProgress && contentLength > 0 && Date.now() - lastProgressTime > 500) {
955
+ lastProgressTime = Date.now();
956
+ const percent = Math.round((downloaded / contentLength) * 100);
957
+ onProgress(percent, '');
958
+ }
959
+ });
904
960
  try {
905
961
  await (0, promises_2.pipeline)(response.data, writer);
962
+ if (onProgress)
963
+ onProgress(100, '');
906
964
  return filePath;
907
965
  }
908
966
  catch (e) {
@@ -910,6 +968,18 @@ function apply(ctx, config) {
910
968
  throw new Error(`写入文件失败: ${getErrorMessage(e)}`);
911
969
  }
912
970
  }
971
+ function formatSpeed(bytesPerSec) {
972
+ if (bytesPerSec <= 0)
973
+ return '';
974
+ const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
975
+ let i = 0;
976
+ let speed = bytesPerSec;
977
+ while (speed >= 1024 && i < units.length - 1) {
978
+ speed /= 1024;
979
+ i++;
980
+ }
981
+ return `${speed.toFixed(1)}${units[i]}`;
982
+ }
913
983
  async function sendMedia(session, url, type, forceDownload, showFile) {
914
984
  if (!url)
915
985
  return;
@@ -923,9 +993,13 @@ function apply(ctx, config) {
923
993
  };
924
994
  const prefixMap = { image: 'img', video: 'video', audio: 'music' };
925
995
  const sendFunc = type === 'audio' ? koishi_1.h.audio : type === 'video' ? koishi_1.h.video : koishi_1.h.image;
996
+ const onProgress = forceDownload ? (percent, speed) => {
997
+ const text = `下载进度: ${percent}%` + (speed ? ` (${speed})` : '');
998
+ logger.info(text);
999
+ } : undefined;
926
1000
  if (forceDownload) {
927
1001
  try {
928
- const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type]);
1002
+ const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type], onProgress);
929
1003
  try {
930
1004
  await sendWithTimeout(session, sendFunc(`file://${localPath}`));
931
1005
  }
@@ -1033,7 +1107,7 @@ function apply(ctx, config) {
1033
1107
  const text = item.text;
1034
1108
  if (text && config.showImageText)
1035
1109
  forwardMessages.push(buildForwardNode(session, text, botName));
1036
- if (p.cover && config.showCoverImage && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
1110
+ if (p.cover && config.showCoverImage && p.type !== 'live_photo' && p.type !== 'image' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
1037
1111
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
1038
1112
  }
1039
1113
  if (config.showMusicCover && p.music.cover) {
@@ -1071,7 +1145,7 @@ function apply(ctx, config) {
1071
1145
  await sendWithTimeout(session, text);
1072
1146
  await delay(300);
1073
1147
  }
1074
- if (p.cover && config.showCoverImage && p.type !== 'live_photo' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
1148
+ if (p.cover && config.showCoverImage && p.type !== 'live_photo' && p.type !== 'image' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
1075
1149
  await sendMedia(session, p.cover, 'image', config.forceDownloadImage, config.showImageFile).catch(() => { });
1076
1150
  await delay(300);
1077
1151
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
3
  "description": "Koishi 全平台视频/图集解析插件,支持抖音/快手/B站/微博/小红书/剪映/YouTube/TikTok等20+平台",
4
- "version": "1.4.6",
4
+ "version": "1.4.8",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [