koishi-plugin-video-parser-all 1.5.0 → 1.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 CHANGED
@@ -45,6 +45,7 @@ export declare const Config: Schema<{
45
45
  authorAvatarText?: string | null | undefined;
46
46
  showMusicCover?: boolean | null | undefined;
47
47
  showVideoFile?: boolean | null | undefined;
48
+ sendLiveMessage?: boolean | null | undefined;
48
49
  forceDownloadCover?: boolean | null | undefined;
49
50
  forceDownloadImageNew?: boolean | null | undefined;
50
51
  forceDownloadAuthorAvatar?: boolean | null | undefined;
@@ -197,6 +198,7 @@ export declare const Config: Schema<{
197
198
  authorAvatarText: string;
198
199
  showMusicCover: boolean;
199
200
  showVideoFile: boolean;
201
+ sendLiveMessage: boolean;
200
202
  forceDownloadCover: boolean;
201
203
  forceDownloadImageNew: boolean;
202
204
  forceDownloadAuthorAvatar: boolean;
package/lib/index.js CHANGED
@@ -72,7 +72,7 @@ exports.Config = koishi_1.Schema.intersect([
72
72
  koishi_1.Schema.object({
73
73
  enable: koishi_1.Schema.boolean().default(true).description('是否启用视频解析插件'),
74
74
  botName: koishi_1.Schema.string().default('视频解析机器人').description('合并转发中显示的昵称'),
75
- showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
75
+ showWaitingTip: koishi_1.Schema.boolean().default(true).description('显示等待提示'),
76
76
  debug: koishi_1.Schema.boolean().default(false).description('开启调试日志'),
77
77
  platformEnabled: koishi_1.Schema.object({
78
78
  bilibili: koishi_1.Schema.boolean().default(true).description('哔哩哔哩'),
@@ -116,6 +116,7 @@ exports.Config = koishi_1.Schema.intersect([
116
116
  authorAvatarText: koishi_1.Schema.string().default('作者头像:').description('作者头像前显示的文字'),
117
117
  showMusicCover: koishi_1.Schema.boolean().default(true).description('发送音乐封面图片'),
118
118
  showVideoFile: koishi_1.Schema.boolean().default(true).description('视频是否以视频形式发送(关闭则只发送链接)'),
119
+ sendLiveMessage: koishi_1.Schema.boolean().default(true).description('直播作品发送文字消息(不发送视频)'),
119
120
  forceDownloadCover: koishi_1.Schema.boolean().default(false).description('强制下载封面'),
120
121
  forceDownloadImageNew: koishi_1.Schema.boolean().default(false).description('强制下载图片'),
121
122
  forceDownloadAuthorAvatar: koishi_1.Schema.boolean().default(false).description('强制下载作者头像'),
@@ -437,8 +438,9 @@ function cleanUrl(url) {
437
438
  try {
438
439
  url = url.replace(/&amp;/g, '&');
439
440
  const urlObj = new URL(url);
440
- if (urlObj.protocol === 'http:')
441
- urlObj.protocol = 'https:';
441
+ if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
442
+ throw new Error('非法的协议');
443
+ }
442
444
  if (urlObj.hostname.includes('douyin.com') || urlObj.hostname.includes('v.douyin.com')) {
443
445
  ['source', 'share_type', 'share_token', 'timestamp', 'from', 'isappinstalled'].forEach(p => urlObj.searchParams.delete(p));
444
446
  return urlObj.origin + urlObj.pathname;
@@ -838,23 +840,32 @@ function apply(ctx, config) {
838
840
  }) || fileExts[0];
839
841
  const fileName = `${filePrefix}_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.${ext}`;
840
842
  const filePath = path_1.default.resolve(cacheDir, fileName);
843
+ const startTime = Date.now();
841
844
  if (downloadEngine === 'downloads' && downloadsService) {
842
- logger.info(`开始下载: ${url}`);
843
- const dest = await downloadsService.download(url, path_1.default.join(cacheDir, fileName), {
844
- headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
845
- timeout
846
- });
847
- const stat = await promises_1.default.stat(dest);
848
- const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
849
- logger.info(`下载完成: ${dest} (${sizeMB} MB)`);
850
- if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
851
- await promises_1.default.unlink(dest).catch(() => { });
852
- throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
845
+ logger.info(`[下载] 开始 (downloads): ${url}`);
846
+ try {
847
+ const dest = await downloadsService.download(url, path_1.default.join(cacheDir, fileName), {
848
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
849
+ timeout
850
+ });
851
+ const stat = await promises_1.default.stat(dest);
852
+ const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
853
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
854
+ logger.info(`[下载] 完成 (downloads): ${dest} (${sizeMB} MB, 耗时 ${elapsed}s)`);
855
+ if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
856
+ await promises_1.default.unlink(dest).catch(() => { });
857
+ throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
858
+ }
859
+ return dest;
860
+ }
861
+ catch (e) {
862
+ debugLog('ERROR', 'downloads 下载失败,回退内置下载:', e);
863
+ throw e;
853
864
  }
854
- return dest;
855
865
  }
856
866
  if (downloadEngine === 'aria2' && aria2Service) {
857
867
  try {
868
+ logger.info(`[下载] 开始 (aria2): ${url}`);
858
869
  const gid = await aria2Service.addUri([url], {
859
870
  dir: cacheDir,
860
871
  out: fileName,
@@ -868,10 +879,10 @@ function apply(ctx, config) {
868
879
  header: [`User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36`, `Referer: https://www.baidu.com/`]
869
880
  });
870
881
  let completed = false;
871
- const startTime = Date.now();
882
+ const ariaStart = Date.now();
872
883
  let lastProgressTime = 0;
873
884
  while (!completed) {
874
- if (Date.now() - startTime > timeout) {
885
+ if (Date.now() - ariaStart > timeout) {
875
886
  await aria2Service.remove(gid).catch(() => { });
876
887
  throw new Error('aria2下载超时');
877
888
  }
@@ -897,6 +908,9 @@ function apply(ctx, config) {
897
908
  }
898
909
  }
899
910
  const stat = await promises_1.default.stat(filePath);
911
+ const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
912
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
913
+ logger.info(`[下载] 完成 (aria2): ${filePath} (${sizeMB} MB, 耗时 ${elapsed}s)`);
900
914
  if (maxSize > 0 && stat.size > maxSize * 1024 * 1024) {
901
915
  await promises_1.default.unlink(filePath).catch(() => { });
902
916
  throw new Error(`文件过大(${Math.round(stat.size / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
@@ -907,6 +921,10 @@ function apply(ctx, config) {
907
921
  debugLog('ERROR', `aria2下载失败,回退内置下载: ${getErrorMessage(e)}`);
908
922
  }
909
923
  }
924
+ const urlObj = new URL(url);
925
+ if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
926
+ throw new Error('不支持的下载协议:' + urlObj.protocol);
927
+ }
910
928
  const writer = (0, fs_1.createWriteStream)(filePath);
911
929
  let response;
912
930
  try {
@@ -932,6 +950,8 @@ function apply(ctx, config) {
932
950
  await promises_1.default.unlink(filePath).catch(() => { });
933
951
  throw new Error(`文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${maxSize}MB)`);
934
952
  }
953
+ const sizeStr = contentLength > 0 ? `${(contentLength / (1024 * 1024)).toFixed(2)}MB` : '未知大小';
954
+ logger.info(`[下载] 开始 (内置): ${url} (${sizeStr})`);
935
955
  let downloaded = 0;
936
956
  let lastProgressTime = 0;
937
957
  response.data.on('data', (chunk) => {
@@ -940,22 +960,27 @@ function apply(ctx, config) {
940
960
  lastProgressTime = Date.now();
941
961
  if (contentLength > 0) {
942
962
  const percent = Math.round((downloaded / contentLength) * 100);
943
- onProgress(percent, '');
963
+ const mb = (downloaded / (1024 * 1024)).toFixed(2);
964
+ const totalMB = (contentLength / (1024 * 1024)).toFixed(2);
965
+ onProgress(percent, `${mb}/${totalMB}MB`);
944
966
  }
945
967
  else {
946
- onProgress(-1, formatSpeed(downloaded / ((Date.now() - lastProgressTime) / 1000)));
968
+ const mb = (downloaded / (1024 * 1024)).toFixed(2);
969
+ onProgress(-1, `已下载 ${mb} MB`);
947
970
  }
948
971
  }
949
972
  });
950
973
  try {
951
974
  await (0, promises_2.pipeline)(response.data, writer);
975
+ const stat = await promises_1.default.stat(filePath);
976
+ const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
977
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
978
+ logger.info(`[下载] 完成 (内置): ${filePath} (${sizeMB} MB, 耗时 ${elapsed}s)`);
952
979
  if (onProgress) {
953
- if (contentLength > 0) {
954
- onProgress(100, '');
955
- }
956
- else {
957
- onProgress(-1, formatSpeed(downloaded / ((Date.now() - lastProgressTime) / 1000)));
958
- }
980
+ if (contentLength > 0)
981
+ onProgress(100, `${sizeMB}MB`);
982
+ else
983
+ onProgress(-1, `完成,共 ${sizeMB} MB`);
959
984
  }
960
985
  return filePath;
961
986
  }
@@ -989,15 +1014,15 @@ function apply(ctx, config) {
989
1014
  };
990
1015
  const prefixMap = { image: 'img', video: 'video', audio: 'music' };
991
1016
  const sendFunc = type === 'audio' ? koishi_1.h.audio : type === 'video' ? koishi_1.h.video : koishi_1.h.image;
992
- const onProgress = forceDownload ? (percent, speed) => {
1017
+ const onProgress = (percent, info) => {
993
1018
  if (percent === -1) {
994
- logger.info(`下载进度: 已下载 ${speed}`);
1019
+ logger.info(`[下载] ${info}`);
995
1020
  }
996
1021
  else {
997
- const text = `下载进度: ${percent}%` + (speed ? ` (${speed})` : '');
1022
+ const text = `[下载] ${percent}%` + (info ? ` (${info})` : '');
998
1023
  logger.info(text);
999
1024
  }
1000
- } : undefined;
1025
+ };
1001
1026
  if (forceDownload) {
1002
1027
  try {
1003
1028
  const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type], onProgress);
@@ -1029,7 +1054,7 @@ function apply(ctx, config) {
1029
1054
  }
1030
1055
  catch {
1031
1056
  try {
1032
- const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type]);
1057
+ const localPath = await downloadFile(url, mediaDownloadTimeout, maxMediaSize, prefixMap[type], extMap[type], onProgress);
1033
1058
  try {
1034
1059
  await sendWithTimeout(session, sendFunc(`file://${localPath}`));
1035
1060
  }
@@ -1108,12 +1133,13 @@ function apply(ctx, config) {
1108
1133
  const text = item.text;
1109
1134
  if (text && config.showImageText)
1110
1135
  forwardMessages.push(buildForwardNode(session, text, botName));
1136
+ if (config.showAuthorAvatar && p.avatar && config.showAuthorAvatarText) {
1137
+ forwardMessages.push(buildForwardNode(session, config.authorAvatarText || '作者头像:', botName));
1138
+ }
1111
1139
  if (config.showAuthorAvatar && p.avatar) {
1112
- if (config.showAuthorAvatarText)
1113
- forwardMessages.push(buildForwardNode(session, config.authorAvatarText || '作者头像:', botName));
1114
1140
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.avatar), botName));
1115
1141
  }
1116
- if (p.cover && config.showCoverImage && p.type !== 'live_photo' && p.type !== 'image' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
1142
+ if (p.cover && config.showCoverImage && p.type !== 'live_photo' && p.type !== 'image' && p.type !== 'live') {
1117
1143
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
1118
1144
  }
1119
1145
  if (config.showMusicCover && p.music.cover) {
@@ -1124,7 +1150,7 @@ function apply(ctx, config) {
1124
1150
  for (const imgUrl of imageUrls)
1125
1151
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
1126
1152
  }
1127
- if (p.video)
1153
+ if (p.video && p.type !== 'live')
1128
1154
  forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
1129
1155
  if (config.showMusicVoice && p.music.url) {
1130
1156
  forwardMessages.push(buildForwardNode(session, koishi_1.h.audio(p.music.url), botName));
@@ -1151,13 +1177,15 @@ function apply(ctx, config) {
1151
1177
  await sendWithTimeout(session, text);
1152
1178
  await delay(300);
1153
1179
  }
1180
+ if (config.showAuthorAvatar && p.avatar && config.showAuthorAvatarText) {
1181
+ await sendWithTimeout(session, config.authorAvatarText || '作者头像:');
1182
+ await delay(300);
1183
+ }
1154
1184
  if (config.showAuthorAvatar && p.avatar) {
1155
- if (config.showAuthorAvatarText)
1156
- await sendWithTimeout(session, config.authorAvatarText || '作者头像:');
1157
1185
  await sendMedia(session, p.avatar, 'image', config.forceDownloadAuthorAvatar, config.showAuthorAvatarFile).catch(() => { });
1158
1186
  await delay(300);
1159
1187
  }
1160
- if (p.cover && config.showCoverImage && p.type !== 'live_photo' && p.type !== 'image' && !(p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
1188
+ if (p.cover && config.showCoverImage && p.type !== 'live_photo' && p.type !== 'image' && p.type !== 'live') {
1161
1189
  await sendMedia(session, p.cover, 'image', config.forceDownloadCover, config.showCoverFile).catch(() => { });
1162
1190
  await delay(300);
1163
1191
  }
@@ -1165,17 +1193,21 @@ function apply(ctx, config) {
1165
1193
  await sendMedia(session, p.music.cover, 'image', false, true).catch(() => { });
1166
1194
  await delay(300);
1167
1195
  }
1168
- if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
1196
+ if (p.video && p.type !== 'live') {
1169
1197
  await sendMedia(session, p.video, 'video', config.forceDownloadVideo, config.showVideoFile).catch(() => { });
1170
1198
  await delay(500);
1171
1199
  }
1172
1200
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
1173
1201
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
1174
- for (const imgUrl of imageUrls) {
1175
- await sendMedia(session, imgUrl, 'image', config.forceDownloadImageNew, config.showImageFileNew).catch(() => { });
1176
- await delay(200);
1202
+ for (let i = 0; i < imageUrls.length; i++) {
1203
+ logger.info(`[发送] 图片 ${i + 1}/${imageUrls.length}`);
1204
+ await sendMedia(session, imageUrls[i], 'image', config.forceDownloadImageNew, config.showImageFileNew).catch(() => { });
1205
+ await delay(1000);
1177
1206
  }
1178
1207
  }
1208
+ if (p.type === 'live' && config.sendLiveMessage) {
1209
+ await sendWithTimeout(session, '直播进行中,无法发送视频流。').catch(() => { });
1210
+ }
1179
1211
  if (config.showMusicVoice && p.music.url) {
1180
1212
  await sendMedia(session, p.music.url, 'audio', config.forceDownloadMusicVoice, config.showMusicVoiceFile).catch(() => { });
1181
1213
  await delay(300);
@@ -1410,21 +1442,23 @@ function apply(ctx, config) {
1410
1442
  dedupCache.clear();
1411
1443
  debugLog('INFO', '插件已卸载');
1412
1444
  });
1413
- process.on('beforeExit', async () => {
1414
- try {
1415
- const files = await promises_1.default.readdir(cacheDir);
1416
- for (const file of files) {
1417
- if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
1418
- (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
1419
- (file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
1420
- await promises_1.default.unlink(path_1.default.join(cacheDir, file)).catch(() => { });
1445
+ if (!process.listenerCount('beforeExit')) {
1446
+ process.once('beforeExit', async () => {
1447
+ try {
1448
+ const files = await promises_1.default.readdir(cacheDir);
1449
+ for (const file of files) {
1450
+ if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
1451
+ (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
1452
+ (file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
1453
+ await promises_1.default.unlink(path_1.default.join(cacheDir, file)).catch(() => { });
1454
+ }
1421
1455
  }
1422
1456
  }
1423
- }
1424
- catch (e) {
1425
- if (e?.code !== 'ENOENT')
1426
- debugLog('WARN', '退出清理临时文件失败:', e);
1427
- }
1428
- });
1457
+ catch (e) {
1458
+ if (e?.code !== 'ENOENT')
1459
+ debugLog('WARN', '退出清理临时文件失败:', e);
1460
+ }
1461
+ });
1462
+ }
1429
1463
  debugLog('INFO', '插件初始化完成');
1430
1464
  }
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.5.0",
4
+ "version": "1.5.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -179,7 +179,7 @@ If you use `downloadEngine: 'downloads'`, please install the optional dependency
179
179
  | 皮皮搞笑 Pipigx | `h5.pipigx.com/pp/post/`, `ippzone.com` | 短视频 (Short video) |
180
180
  | 皮皮虾 Pipixia | `pipix.com`, `pipixia.com` | 短视频 (Short video) |
181
181
  | 最右 Zuiyou | `share.xiaochuankeji.cn/hybrid/share/post`, `izuiyou.com` | 短视频 (Short video) |
182
- | 自定义平台 Custom | 通过 `customPlatforms` 配置添加 (Add via `customPlatforms`) | 取决于提供的 API (Depends on API) |
182
+ | 🔧 自定义平台 Custom | 通过 `customPlatforms` 配置添加 (Add via `customPlatforms`) | 取决于提供的 API (Depends on API) |
183
183
 
184
184
  ## 项目贡献者 (Contributors)
185
185
 
@@ -188,9 +188,9 @@ If you use `downloadEngine: 'downloads'`, please install the optional dependency
188
188
  | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
189
189
  | ShiraiKuroko003 | 修复消息格式设置问题并且PR-1.2.5版本已修复 (Fixed message format issue, PR-1.2.5) |
190
190
  | cyavb | 提交功能建议-给自定义API添加KEY认证-已修复 (Suggested custom API key auth - fixed) |
191
- | Keep785 | 提交Bug-无法正常关闭发送封面-已修复<br>提交Bug-解析问题-已修复 (Reported bugs - cannot disable cover sending and parsing issues - fixed) |
191
+ | Keep785 | 提交Bug-无法正常关闭发送封面-已修复<br>提交Bug-解析问题-已修复 (Reported bug - cannot disable cover sending - fixed<br>Reported bug - parsing issue - fixed) |
192
192
  | dzt2008 + Apricityx | 提交Bug-会对非支持视频平台URL进行误解析-已修复 (Reported incorrect parsing of unsupported URLs - fixed) |
193
- | linyves | 提交Bug-小红书图集重复发送封面-已修复<br>提交Bug-话题显示异常 #**[话题]#-已修复<br>提交建议-Live Photo 全部按普通图片处理-已采纳<br>提交Bug-解析后会把作者头像一起发送-已修复 |
193
+ | linyves | 提交Bug-小红书图集重复发送封面-已修复<br>提交Bug-话题显示异常 #**[话题]#-已修复<br>提交建议-Live Photo 全部按普通图片处理-已采纳<br>提交Bug-解析后会把作者头像一起发送-已修复 (Reported bug - duplicate cover for Xiaohongshu image posts - fixed<br>Reported bug - abnormal topic display #**[topic]# - fixed<br>Suggestion - treat Live Photos as normal images - adopted<br>Reported bug - author avatar sent together after parsing - fixed) |
194
194
  | JH-Ahua | BugPk-Api 支持 (BugPk-Api support) |
195
195
  | shangxue | 灵感来源 (Inspiration) |
196
196