koishi-plugin-video-parser-all 1.5.1 → 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('强制下载作者头像'),
@@ -135,7 +136,7 @@ exports.Config = koishi_1.Schema.intersect([
135
136
  downloadEngine: koishi_1.Schema.union([
136
137
  koishi_1.Schema.const('internal').description('内置下载'),
137
138
  koishi_1.Schema.const('aria2').description('aria2 下载(需 koishi-plugin-aria2-plus)'),
138
- koishi_1.Schema.const('downloads').description('downloads 服务下载(需 koishi-plugin-downloads)'),
139
+ koishi_1.Schema.const('downloads').description('downloads 服务下载'),
139
140
  ]).default('internal').description('下载引擎'),
140
141
  }).description('性能与限制'),
141
142
  koishi_1.Schema.object({
@@ -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
  }
@@ -1114,7 +1139,7 @@ function apply(ctx, config) {
1114
1139
  if (config.showAuthorAvatar && p.avatar) {
1115
1140
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.avatar), botName));
1116
1141
  }
1117
- 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') {
1118
1143
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
1119
1144
  }
1120
1145
  if (config.showMusicCover && p.music.cover) {
@@ -1125,7 +1150,7 @@ function apply(ctx, config) {
1125
1150
  for (const imgUrl of imageUrls)
1126
1151
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(imgUrl), botName));
1127
1152
  }
1128
- if (p.video)
1153
+ if (p.video && p.type !== 'live')
1129
1154
  forwardMessages.push(buildForwardNode(session, koishi_1.h.video(p.video), botName));
1130
1155
  if (config.showMusicVoice && p.music.url) {
1131
1156
  forwardMessages.push(buildForwardNode(session, koishi_1.h.audio(p.music.url), botName));
@@ -1160,7 +1185,7 @@ function apply(ctx, config) {
1160
1185
  await sendMedia(session, p.avatar, 'image', config.forceDownloadAuthorAvatar, config.showAuthorAvatarFile).catch(() => { });
1161
1186
  await delay(300);
1162
1187
  }
1163
- 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') {
1164
1189
  await sendMedia(session, p.cover, 'image', config.forceDownloadCover, config.showCoverFile).catch(() => { });
1165
1190
  await delay(300);
1166
1191
  }
@@ -1168,17 +1193,21 @@ function apply(ctx, config) {
1168
1193
  await sendMedia(session, p.music.cover, 'image', false, true).catch(() => { });
1169
1194
  await delay(300);
1170
1195
  }
1171
- if (p.video && (p.type === 'video' || (p.type === 'live' && !p.live_photo?.length && !p.images?.length))) {
1196
+ if (p.video && p.type !== 'live') {
1172
1197
  await sendMedia(session, p.video, 'video', config.forceDownloadVideo, config.showVideoFile).catch(() => { });
1173
1198
  await delay(500);
1174
1199
  }
1175
1200
  if (p.type === 'image' || p.type === 'live_photo' || (p.type === 'live' && (p.live_photo?.length || p.images?.length))) {
1176
1201
  const imageUrls = p.images?.length ? p.images : (p.live_photo?.map(lp => lp.image) ?? []);
1177
- for (const imgUrl of imageUrls) {
1178
- await sendMedia(session, imgUrl, 'image', config.forceDownloadImageNew, config.showImageFileNew).catch(() => { });
1179
- 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);
1180
1206
  }
1181
1207
  }
1208
+ if (p.type === 'live' && config.sendLiveMessage) {
1209
+ await sendWithTimeout(session, '直播进行中,无法发送视频流。').catch(() => { });
1210
+ }
1182
1211
  if (config.showMusicVoice && p.music.url) {
1183
1212
  await sendMedia(session, p.music.url, 'audio', config.forceDownloadMusicVoice, config.showMusicVoiceFile).catch(() => { });
1184
1213
  await delay(300);
@@ -1413,21 +1442,23 @@ function apply(ctx, config) {
1413
1442
  dedupCache.clear();
1414
1443
  debugLog('INFO', '插件已卸载');
1415
1444
  });
1416
- process.on('beforeExit', async () => {
1417
- try {
1418
- const files = await promises_1.default.readdir(cacheDir);
1419
- for (const file of files) {
1420
- if ((file.startsWith('video_') && file.endsWith('.mp4')) ||
1421
- (file.startsWith('img_') && file.match(/\.(png|jpg|jpeg|gif|webp)$/i)) ||
1422
- (file.startsWith('music_') && file.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i))) {
1423
- 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
+ }
1424
1455
  }
1425
1456
  }
1426
- }
1427
- catch (e) {
1428
- if (e?.code !== 'ENOENT')
1429
- debugLog('WARN', '退出清理临时文件失败:', e);
1430
- }
1431
- });
1457
+ catch (e) {
1458
+ if (e?.code !== 'ENOENT')
1459
+ debugLog('WARN', '退出清理临时文件失败:', e);
1460
+ }
1461
+ });
1462
+ }
1432
1463
  debugLog('INFO', '插件初始化完成');
1433
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.1",
4
+ "version": "1.5.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [