koishi-plugin-video-parser-all 1.4.7 → 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.
- package/lib/index.d.ts +0 -3
- package/lib/index.js +98 -25
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
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
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
|
|
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
|
-
|
|
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
|
|
886
|
+
const startTime = Date.now();
|
|
887
|
+
let lastProgressTime = 0;
|
|
852
888
|
while (!completed) {
|
|
853
|
-
if (Date.now() -
|
|
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
|
}
|
|
@@ -1342,4 +1416,3 @@ function apply(ctx, config) {
|
|
|
1342
1416
|
});
|
|
1343
1417
|
debugLog('INFO', '插件初始化完成');
|
|
1344
1418
|
}
|
|
1345
|
-
apply.inject = ['aria2', 'downloads'];
|
package/package.json
CHANGED