n8n-nodes-vidflow 0.1.9 → 0.1.11
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/README.md
CHANGED
|
@@ -44,9 +44,13 @@ npm run release
|
|
|
44
44
|
|
|
45
45
|
## 节点运行说明
|
|
46
46
|
|
|
47
|
-
- `Douyin ->
|
|
48
|
-
- `Douyin -> Download
|
|
49
|
-
- `Douyin -> Download
|
|
47
|
+
- `Douyin -> Extract Link` 可从抖音分享文案中提取一套明确字段:`pageUrl`、`resolvedPageUrl`、`playUrl`、`downloadUrl`,再附带 `videoId`、作者、标题、封面、媒体类型和文件大小等信息。
|
|
48
|
+
- `Douyin -> Download Media` 现在用于下载媒体文件,输入参数为 `sourceUrl`,建议传 `resolvedPageUrl` 页面链接,或在已拿到真实直链时直接传 `downloadUrl`。
|
|
49
|
+
- `Douyin -> Download Media` 新增 `Media Type` 参数,可直接下载视频,或先下载视频再通过 `ffmpeg` 提取音频。
|
|
50
|
+
- 当 `Media Type` 为 `Audio` 时,可额外配置 `Audio Format` 和 `FFmpeg Path`。
|
|
51
|
+
- `Douyin -> Download Media` 默认会优先尝试分块并发下载;当源站支持 `Range` 时可断点续传,不支持时会自动降级为单连接流式下载。
|
|
52
|
+
- `Douyin -> Download Media` 保留 `Chunk Size (MB)` 和 `Worker Count` 两个调优参数,默认分别为 `5` 和 `8`,运行时会限制在 `1-64 MB` 与 `1-16` 的安全范围内。
|
|
53
|
+
- 推荐工作流可以直接写成:`Extract Link` 输出 `resolvedPageUrl` 或 `downloadUrl`,再赋给 `Download Media` 的 `sourceUrl`。
|
|
50
54
|
- `Audio -> Extract From Video` 依赖本机 `ffmpeg`,默认路径是 `/usr/bin/ffmpeg`。
|
|
51
55
|
- `Transcription -> Transcribe` 会调用 BCut 转录接口,适合接在音频提取之后使用。
|
|
52
56
|
|
|
@@ -66,12 +66,17 @@ const transcriptionDisplayOptions = {
|
|
|
66
66
|
};
|
|
67
67
|
const douyinDownloadDisplayOptions = {
|
|
68
68
|
resource: ['douyin'],
|
|
69
|
-
operation: ['
|
|
69
|
+
operation: ['downloadMedia'],
|
|
70
70
|
};
|
|
71
71
|
const douyinExtractLinkDisplayOptions = {
|
|
72
72
|
resource: ['douyin'],
|
|
73
73
|
operation: ['extractLink'],
|
|
74
74
|
};
|
|
75
|
+
const douyinAudioDownloadDisplayOptions = {
|
|
76
|
+
resource: ['douyin'],
|
|
77
|
+
operation: ['downloadMedia'],
|
|
78
|
+
mediaType: ['audio'],
|
|
79
|
+
};
|
|
75
80
|
const transcriptionDescription = [
|
|
76
81
|
{
|
|
77
82
|
displayName: 'Operation',
|
|
@@ -270,13 +275,13 @@ const douyinDescription = [
|
|
|
270
275
|
description: 'Extract the Douyin link from a share message and resolve short links',
|
|
271
276
|
},
|
|
272
277
|
{
|
|
273
|
-
name: 'Download
|
|
274
|
-
value: '
|
|
275
|
-
action: 'Download
|
|
276
|
-
description: 'Download
|
|
278
|
+
name: 'Download Media',
|
|
279
|
+
value: 'downloadMedia',
|
|
280
|
+
action: 'Download media',
|
|
281
|
+
description: 'Download audio or video from a Douyin page link or resolved download link',
|
|
277
282
|
},
|
|
278
283
|
],
|
|
279
|
-
default: '
|
|
284
|
+
default: 'downloadMedia',
|
|
280
285
|
},
|
|
281
286
|
{
|
|
282
287
|
displayName: 'Share Text Or Link',
|
|
@@ -293,15 +298,35 @@ const douyinDescription = [
|
|
|
293
298
|
description: 'A Douyin share message, short link, or direct video link to extract from',
|
|
294
299
|
},
|
|
295
300
|
{
|
|
296
|
-
displayName: '
|
|
297
|
-
name: '
|
|
301
|
+
displayName: 'Source URL',
|
|
302
|
+
name: 'sourceUrl',
|
|
298
303
|
type: 'string',
|
|
299
304
|
required: true,
|
|
300
305
|
default: '',
|
|
301
306
|
displayOptions: {
|
|
302
307
|
show: douyinDownloadDisplayOptions,
|
|
303
308
|
},
|
|
304
|
-
description: 'A Douyin
|
|
309
|
+
description: 'A Douyin page link or direct media link from the Extract Link operation. Prefer resolvedPageUrl for page links and downloadUrl for direct downloads.',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
displayName: 'Media Type',
|
|
313
|
+
name: 'mediaType',
|
|
314
|
+
type: 'options',
|
|
315
|
+
default: 'video',
|
|
316
|
+
displayOptions: {
|
|
317
|
+
show: douyinDownloadDisplayOptions,
|
|
318
|
+
},
|
|
319
|
+
options: [
|
|
320
|
+
{
|
|
321
|
+
name: 'Video',
|
|
322
|
+
value: 'video',
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: 'Audio',
|
|
326
|
+
value: 'audio',
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
description: 'Whether to download the original video file or extract audio after download',
|
|
305
330
|
},
|
|
306
331
|
{
|
|
307
332
|
displayName: 'Output Binary Property',
|
|
@@ -312,7 +337,7 @@ const douyinDescription = [
|
|
|
312
337
|
displayOptions: {
|
|
313
338
|
show: douyinDownloadDisplayOptions,
|
|
314
339
|
},
|
|
315
|
-
description: 'Name of the output binary property that will contain the
|
|
340
|
+
description: 'Name of the output binary property that will contain the downloaded media file',
|
|
316
341
|
},
|
|
317
342
|
{
|
|
318
343
|
displayName: 'File Name',
|
|
@@ -324,6 +349,44 @@ const douyinDescription = [
|
|
|
324
349
|
},
|
|
325
350
|
description: 'Optional output file name override',
|
|
326
351
|
},
|
|
352
|
+
{
|
|
353
|
+
displayName: 'Audio Format',
|
|
354
|
+
name: 'audioFormat',
|
|
355
|
+
type: 'options',
|
|
356
|
+
default: 'mp3',
|
|
357
|
+
displayOptions: {
|
|
358
|
+
show: douyinAudioDownloadDisplayOptions,
|
|
359
|
+
},
|
|
360
|
+
options: [
|
|
361
|
+
{
|
|
362
|
+
name: 'AAC',
|
|
363
|
+
value: 'aac',
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: 'MP3',
|
|
367
|
+
value: 'mp3',
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: 'OGG',
|
|
371
|
+
value: 'ogg',
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'WAV',
|
|
375
|
+
value: 'wav',
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
description: 'Audio format used when Media Type is set to Audio',
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
displayName: 'FFmpeg Path',
|
|
382
|
+
name: 'audioFfmpegPath',
|
|
383
|
+
type: 'string',
|
|
384
|
+
default: DEFAULT_FFMPEG_PATH,
|
|
385
|
+
displayOptions: {
|
|
386
|
+
show: douyinAudioDownloadDisplayOptions,
|
|
387
|
+
},
|
|
388
|
+
description: 'FFmpeg executable path used when extracting audio from the downloaded video',
|
|
389
|
+
},
|
|
327
390
|
{
|
|
328
391
|
displayName: 'Chunk Size (MB)',
|
|
329
392
|
name: 'chunkSizeMb',
|
|
@@ -522,6 +585,18 @@ function getDouyinContentLength(headers) {
|
|
|
522
585
|
const rawValue = Array.isArray(contentLength) ? contentLength[0] : contentLength;
|
|
523
586
|
return Number(rawValue);
|
|
524
587
|
}
|
|
588
|
+
function getDouyinContentRange(headers) {
|
|
589
|
+
const contentRange = headers['content-range'];
|
|
590
|
+
return Array.isArray(contentRange) ? contentRange[0] : contentRange;
|
|
591
|
+
}
|
|
592
|
+
function getDouyinTotalBytesFromContentRange(contentRange) {
|
|
593
|
+
var _a;
|
|
594
|
+
if (!contentRange) {
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
const totalBytes = (_a = contentRange.match(/\/(\d+)$/)) === null || _a === void 0 ? void 0 : _a[1];
|
|
598
|
+
return totalBytes ? Number(totalBytes) : 0;
|
|
599
|
+
}
|
|
525
600
|
function supportsDouyinRange(headers) {
|
|
526
601
|
var _a;
|
|
527
602
|
const acceptRanges = headers['accept-ranges'];
|
|
@@ -532,7 +607,7 @@ function emitDouyinDownloadProgress(context, itemIndex, percent, downloadedBytes
|
|
|
532
607
|
context.sendMessageToUI({
|
|
533
608
|
itemIndex,
|
|
534
609
|
resource: 'douyin',
|
|
535
|
-
operation: '
|
|
610
|
+
operation: 'downloadMedia',
|
|
536
611
|
stage,
|
|
537
612
|
progressPercent: percent,
|
|
538
613
|
downloadedBytes,
|
|
@@ -571,7 +646,7 @@ function sanitizeFileName(fileName) {
|
|
|
571
646
|
}
|
|
572
647
|
return sanitized.slice(0, 200);
|
|
573
648
|
}
|
|
574
|
-
function parseRouterDataVideoInfo(html,
|
|
649
|
+
function parseRouterDataVideoInfo(html, pageUrl, resolvedPageUrl) {
|
|
575
650
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
576
651
|
const routerDataMatch = html.match(/window\._ROUTER_DATA\s*=\s*(.*?)<\/script>/s);
|
|
577
652
|
if (!(routerDataMatch === null || routerDataMatch === void 0 ? void 0 : routerDataMatch[1])) {
|
|
@@ -593,17 +668,17 @@ function parseRouterDataVideoInfo(html, originalUrl, resolvedUrl) {
|
|
|
593
668
|
throw new n8n_workflow_1.ApplicationError('Douyin video URL was missing from the page data.');
|
|
594
669
|
}
|
|
595
670
|
return {
|
|
596
|
-
|
|
671
|
+
videoId: (_j = item.aweme_id) !== null && _j !== void 0 ? _j : '',
|
|
597
672
|
title: (_k = item.desc) !== null && _k !== void 0 ? _k : '',
|
|
598
673
|
author: (_m = (_l = item.author) === null || _l === void 0 ? void 0 : _l.nickname) !== null && _m !== void 0 ? _m : '',
|
|
599
674
|
cover: (_q = (_p = (_o = item.video) === null || _o === void 0 ? void 0 : _o.cover) === null || _p === void 0 ? void 0 : _p.url_list) === null || _q === void 0 ? void 0 : _q[0],
|
|
600
|
-
videoUrl,
|
|
675
|
+
playUrl: videoUrl,
|
|
601
676
|
createTime: item.create_time,
|
|
602
|
-
|
|
603
|
-
|
|
677
|
+
pageUrl,
|
|
678
|
+
resolvedPageUrl,
|
|
604
679
|
};
|
|
605
680
|
}
|
|
606
|
-
function parseRenderDataVideoInfo(html,
|
|
681
|
+
function parseRenderDataVideoInfo(html, pageUrl, resolvedPageUrl) {
|
|
607
682
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
608
683
|
const renderDataMatch = html.match(/<script id="RENDER_DATA" type="application\/json">([^<]+)<\/script>/);
|
|
609
684
|
if (!(renderDataMatch === null || renderDataMatch === void 0 ? void 0 : renderDataMatch[1])) {
|
|
@@ -621,13 +696,13 @@ function parseRenderDataVideoInfo(html, originalUrl, resolvedUrl) {
|
|
|
621
696
|
throw new n8n_workflow_1.ApplicationError('Douyin video URL was missing from RENDER_DATA.');
|
|
622
697
|
}
|
|
623
698
|
return {
|
|
624
|
-
|
|
699
|
+
videoId: String((_j = pageRoot.awemeId) !== null && _j !== void 0 ? _j : ''),
|
|
625
700
|
title: String((_k = pageRoot.desc) !== null && _k !== void 0 ? _k : ''),
|
|
626
701
|
author: String((_m = (_l = pageRoot.author) === null || _l === void 0 ? void 0 : _l.nickname) !== null && _m !== void 0 ? _m : ''),
|
|
627
702
|
cover: ((_p = (_o = video === null || video === void 0 ? void 0 : video.cover) === null || _o === void 0 ? void 0 : _o.urlList) !== null && _p !== void 0 ? _p : [])[0],
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
703
|
+
playUrl,
|
|
704
|
+
pageUrl,
|
|
705
|
+
resolvedPageUrl,
|
|
631
706
|
};
|
|
632
707
|
}
|
|
633
708
|
async function bcutRequest(context, requestOptions) {
|
|
@@ -660,51 +735,93 @@ async function douyinRequestText(context, url, allowRedirect = true) {
|
|
|
660
735
|
};
|
|
661
736
|
}
|
|
662
737
|
async function resolveDouyinUrl(context, shareText) {
|
|
663
|
-
const
|
|
664
|
-
if (!
|
|
738
|
+
const pageUrl = sanitizeSharedLink(extractFirstUrl(shareText.trim()));
|
|
739
|
+
if (!pageUrl.includes('v.douyin.com')) {
|
|
665
740
|
return {
|
|
666
|
-
|
|
667
|
-
|
|
741
|
+
pageUrl,
|
|
742
|
+
resolvedPageUrl: pageUrl,
|
|
668
743
|
};
|
|
669
744
|
}
|
|
670
|
-
const response = await douyinRequestText(context,
|
|
745
|
+
const response = await douyinRequestText(context, pageUrl, false);
|
|
671
746
|
const redirectLocation = getHeaderValue(response.headers, 'location');
|
|
672
747
|
return {
|
|
673
|
-
|
|
674
|
-
|
|
748
|
+
pageUrl,
|
|
749
|
+
resolvedPageUrl: redirectLocation !== null && redirectLocation !== void 0 ? redirectLocation : pageUrl,
|
|
675
750
|
};
|
|
676
751
|
}
|
|
677
|
-
function
|
|
752
|
+
function getDirectDouyinSourceUrl(input) {
|
|
678
753
|
const directUrl = sanitizeSharedLink(extractFirstUrl(input.trim()));
|
|
679
754
|
if (directUrl.includes('v.douyin.com')) {
|
|
680
|
-
throw new n8n_workflow_1.ApplicationError('
|
|
755
|
+
throw new n8n_workflow_1.ApplicationError('Source URL does not accept Douyin short links. Use the Extract Link operation first and pass its resolvedPageUrl value to Download Media.');
|
|
681
756
|
}
|
|
682
757
|
return directUrl;
|
|
683
758
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
759
|
+
function isDouyinPageUrl(urlString) {
|
|
760
|
+
try {
|
|
761
|
+
const parsedUrl = new URL(urlString);
|
|
762
|
+
const host = parsedUrl.hostname.toLowerCase();
|
|
763
|
+
if (!['www.douyin.com', 'douyin.com', 'iesdouyin.com'].includes(host)) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
return /\/video\//.test(parsedUrl.pathname) || parsedUrl.searchParams.has('modal_id');
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async function getDouyinVideoInfo(context, sourceUrlInput) {
|
|
773
|
+
const pageUrl = getDirectDouyinSourceUrl(sourceUrlInput);
|
|
774
|
+
const resolvedPageUrl = pageUrl;
|
|
775
|
+
const videoId = extractDouyinVideoId(resolvedPageUrl);
|
|
688
776
|
if (videoId === '') {
|
|
689
|
-
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'A Douyin video ID could not be extracted from
|
|
777
|
+
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'A Douyin video ID could not be extracted from Source URL.');
|
|
690
778
|
}
|
|
691
779
|
try {
|
|
692
780
|
const iesPageResponse = await douyinRequestText(context, `https://www.iesdouyin.com/share/video/${videoId}`);
|
|
693
|
-
return parseRouterDataVideoInfo(iesPageResponse.body,
|
|
781
|
+
return parseRouterDataVideoInfo(iesPageResponse.body, pageUrl, resolvedPageUrl);
|
|
694
782
|
}
|
|
695
783
|
catch (iesError) {
|
|
696
784
|
const webPageResponse = await douyinRequestText(context, `https://www.douyin.com/video/${videoId}`);
|
|
697
785
|
try {
|
|
698
|
-
return parseRenderDataVideoInfo(webPageResponse.body,
|
|
786
|
+
return parseRenderDataVideoInfo(webPageResponse.body, pageUrl, resolvedPageUrl);
|
|
699
787
|
}
|
|
700
788
|
catch {
|
|
701
789
|
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Douyin video details could not be resolved: ${asErrorMessage(iesError)}`);
|
|
702
790
|
}
|
|
703
791
|
}
|
|
704
792
|
}
|
|
793
|
+
async function resolveDouyinMediaSource(context, input) {
|
|
794
|
+
const normalizedInput = sanitizeSharedLink(extractFirstUrl(input.trim()));
|
|
795
|
+
if (normalizedInput.includes('v.douyin.com')) {
|
|
796
|
+
throw new n8n_workflow_1.ApplicationError('Download does not accept Douyin short links. Use the Extract Link operation first and pass its resolvedPageUrl or downloadUrl value.');
|
|
797
|
+
}
|
|
798
|
+
if (isDouyinPageUrl(normalizedInput)) {
|
|
799
|
+
const videoInfo = await getDouyinVideoInfo(context, normalizedInput);
|
|
800
|
+
const metadata = await getDouyinDownloadMetadata(videoInfo.playUrl);
|
|
801
|
+
return {
|
|
802
|
+
downloadId: sanitizeFileName(videoInfo.videoId || 'douyin_video'),
|
|
803
|
+
metadata,
|
|
804
|
+
requestUrl: videoInfo.playUrl,
|
|
805
|
+
sourceType: 'page',
|
|
806
|
+
videoInfo,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
const metadata = await getDouyinDownloadMetadata(normalizedInput);
|
|
810
|
+
const derivedVideoId = extractDouyinVideoId(normalizedInput) || extractDouyinVideoId(metadata.downloadUrl);
|
|
811
|
+
return {
|
|
812
|
+
downloadId: sanitizeFileName(derivedVideoId || 'douyin_media'),
|
|
813
|
+
metadata,
|
|
814
|
+
requestUrl: normalizedInput,
|
|
815
|
+
sourceType: 'media',
|
|
816
|
+
};
|
|
817
|
+
}
|
|
705
818
|
function getRequestModule(urlString) {
|
|
706
819
|
return urlString.startsWith('https:') ? https : http;
|
|
707
820
|
}
|
|
821
|
+
function shouldFallbackToStreamDownload(error) {
|
|
822
|
+
const message = asErrorMessage(error);
|
|
823
|
+
return message.includes('Range request was not honored') || message.includes('Content-Range');
|
|
824
|
+
}
|
|
708
825
|
async function requestDouyinResponse(url, method, headers, redirectCount = 0) {
|
|
709
826
|
if (redirectCount > DOUYIN_DOWNLOAD_MAX_REDIRECTS) {
|
|
710
827
|
throw new n8n_workflow_1.ApplicationError('Douyin download exceeded the redirect limit.');
|
|
@@ -736,15 +853,23 @@ async function requestDouyinResponse(url, method, headers, redirectCount = 0) {
|
|
|
736
853
|
async function getDouyinDownloadMetadata(videoUrl) {
|
|
737
854
|
const headers = {
|
|
738
855
|
Referer: 'https://www.douyin.com/',
|
|
856
|
+
Range: 'bytes=0-0',
|
|
739
857
|
'User-Agent': DOUYIN_MOBILE_USER_AGENT,
|
|
740
858
|
};
|
|
741
|
-
const { response, resolvedUrl } = await requestDouyinResponse(videoUrl, '
|
|
742
|
-
response.
|
|
859
|
+
const { response, resolvedUrl } = await requestDouyinResponse(videoUrl, 'GET', headers);
|
|
860
|
+
const contentRange = getDouyinContentRange(response.headers);
|
|
861
|
+
const mimeType = getDouyinMimeTypeFromHeaders(response.headers);
|
|
862
|
+
const totalBytes = getDouyinTotalBytesFromContentRange(contentRange) || getDouyinContentLength(response.headers);
|
|
863
|
+
const acceptRanges = supportsDouyinRange(response.headers) || response.statusCode === 206 || contentRange !== undefined;
|
|
864
|
+
response.destroy();
|
|
865
|
+
if (mimeType.startsWith('text/html')) {
|
|
866
|
+
throw new n8n_workflow_1.ApplicationError('Douyin media probe returned HTML instead of a downloadable media file. The extracted link is not a direct download URL.');
|
|
867
|
+
}
|
|
743
868
|
return {
|
|
744
|
-
acceptRanges
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
totalBytes
|
|
869
|
+
acceptRanges,
|
|
870
|
+
downloadUrl: resolvedUrl,
|
|
871
|
+
mimeType,
|
|
872
|
+
totalBytes,
|
|
748
873
|
};
|
|
749
874
|
}
|
|
750
875
|
async function loadDouyinDownloadState(stateFilePath) {
|
|
@@ -784,6 +909,11 @@ async function downloadDouyinChunk(resolvedUrl, headers, start, end) {
|
|
|
784
909
|
...headers,
|
|
785
910
|
Range: `bytes=${String(start)}-${String(end)}`,
|
|
786
911
|
});
|
|
912
|
+
const contentRange = getDouyinContentRange(response.headers);
|
|
913
|
+
if (response.statusCode !== 206 || !(contentRange === null || contentRange === void 0 ? void 0 : contentRange.startsWith(`bytes ${String(start)}-${String(end)}/`))) {
|
|
914
|
+
response.resume();
|
|
915
|
+
throw new n8n_workflow_1.ApplicationError('Range request was not honored by the download server. Falling back to a single-stream download is required.');
|
|
916
|
+
}
|
|
787
917
|
return await new Promise((resolve, reject) => {
|
|
788
918
|
const chunks = [];
|
|
789
919
|
response.on('data', (chunk) => {
|
|
@@ -823,9 +953,9 @@ async function streamDouyinDownloadToFile(context, itemIndex, resolvedUrl, outpu
|
|
|
823
953
|
return {
|
|
824
954
|
buffer,
|
|
825
955
|
downloadMode: 'stream',
|
|
956
|
+
downloadUrl: resolvedUrl,
|
|
826
957
|
mimeType: getDouyinMimeTypeFromHeaders(response.headers),
|
|
827
958
|
progressPercent: 100,
|
|
828
|
-
resolvedDownloadUrl: resolvedUrl,
|
|
829
959
|
totalBytes: totalBytes > 0 ? totalBytes : buffer.length,
|
|
830
960
|
};
|
|
831
961
|
}
|
|
@@ -909,13 +1039,13 @@ async function chunkedDouyinDownloadToFile(context, itemIndex, resolvedUrl, outp
|
|
|
909
1039
|
return {
|
|
910
1040
|
buffer,
|
|
911
1041
|
downloadMode: 'chunked',
|
|
1042
|
+
downloadUrl: resolvedUrl,
|
|
912
1043
|
mimeType: metadata.mimeType,
|
|
913
1044
|
progressPercent: 100,
|
|
914
|
-
resolvedDownloadUrl: resolvedUrl,
|
|
915
1045
|
totalBytes: metadata.totalBytes,
|
|
916
1046
|
};
|
|
917
1047
|
}
|
|
918
|
-
async function streamDownloadWithProgress(context, itemIndex, videoUrl, downloadId, chunkSize, workerCount) {
|
|
1048
|
+
async function streamDownloadWithProgress(context, itemIndex, videoUrl, downloadId, chunkSize, workerCount, metadata) {
|
|
919
1049
|
const requestHeaders = {
|
|
920
1050
|
Referer: 'https://www.douyin.com/',
|
|
921
1051
|
'User-Agent': DOUYIN_MOBILE_USER_AGENT,
|
|
@@ -923,16 +1053,26 @@ async function streamDownloadWithProgress(context, itemIndex, videoUrl, download
|
|
|
923
1053
|
const downloadDirectory = getDouyinDownloadDirectory(downloadId);
|
|
924
1054
|
const outputFile = (0, node_path_1.join)(downloadDirectory, 'video.mp4');
|
|
925
1055
|
await node_fs_1.promises.mkdir(downloadDirectory, { recursive: true });
|
|
926
|
-
const
|
|
1056
|
+
const downloadMetadata = metadata !== null && metadata !== void 0 ? metadata : (await getDouyinDownloadMetadata(videoUrl));
|
|
927
1057
|
try {
|
|
928
|
-
if (
|
|
929
|
-
|
|
1058
|
+
if (downloadMetadata.acceptRanges && downloadMetadata.totalBytes > 0) {
|
|
1059
|
+
const chunkedResult = await chunkedDouyinDownloadToFile(context, itemIndex, downloadMetadata.downloadUrl, outputFile, requestHeaders, downloadMetadata, chunkSize, workerCount).catch(async (error) => {
|
|
1060
|
+
if (!shouldFallbackToStreamDownload(error)) {
|
|
1061
|
+
return await Promise.reject(error);
|
|
1062
|
+
}
|
|
1063
|
+
emitDouyinDownloadProgress(context, itemIndex, 0, 0, downloadMetadata.totalBytes, 'range_fallback', { reason: asErrorMessage(error) });
|
|
1064
|
+
await node_fs_1.promises.rm(downloadDirectory, { force: true, recursive: true });
|
|
1065
|
+
await node_fs_1.promises.mkdir(downloadDirectory, { recursive: true });
|
|
1066
|
+
return undefined;
|
|
1067
|
+
});
|
|
1068
|
+
if (chunkedResult) {
|
|
1069
|
+
return chunkedResult;
|
|
1070
|
+
}
|
|
930
1071
|
}
|
|
931
|
-
return await streamDouyinDownloadToFile(context, itemIndex,
|
|
1072
|
+
return await streamDouyinDownloadToFile(context, itemIndex, downloadMetadata.downloadUrl, outputFile, requestHeaders, downloadMetadata.totalBytes);
|
|
932
1073
|
}
|
|
933
1074
|
finally {
|
|
934
|
-
await node_fs_1.promises.rm(
|
|
935
|
-
await node_fs_1.promises.rm(downloadDirectory, { force: true, recursive: false }).catch(() => undefined);
|
|
1075
|
+
await node_fs_1.promises.rm(downloadDirectory, { force: true, recursive: true }).catch(() => undefined);
|
|
936
1076
|
}
|
|
937
1077
|
}
|
|
938
1078
|
async function extractAudioFromVideo(context, buffer, inputFileName, outputFileName, format, ffmpegPath, mimeType) {
|
|
@@ -1196,7 +1336,7 @@ class Vidflow {
|
|
|
1196
1336
|
};
|
|
1197
1337
|
}
|
|
1198
1338
|
async execute() {
|
|
1199
|
-
var _a, _b, _c;
|
|
1339
|
+
var _a, _b, _c, _d;
|
|
1200
1340
|
const items = this.getInputData();
|
|
1201
1341
|
const returnData = [];
|
|
1202
1342
|
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
@@ -1276,15 +1416,23 @@ class Vidflow {
|
|
|
1276
1416
|
}
|
|
1277
1417
|
if (resource === 'douyin' && operation === 'extractLink') {
|
|
1278
1418
|
const shareText = this.getNodeParameter('shareText', itemIndex);
|
|
1279
|
-
const {
|
|
1280
|
-
const
|
|
1419
|
+
const { pageUrl, resolvedPageUrl } = await resolveDouyinUrl(this, shareText);
|
|
1420
|
+
const mediaSource = await resolveDouyinMediaSource(this, resolvedPageUrl);
|
|
1421
|
+
const videoInfo = mediaSource.videoInfo;
|
|
1281
1422
|
returnData.push({
|
|
1282
1423
|
json: {
|
|
1283
1424
|
shareText,
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
videoId,
|
|
1287
|
-
isShortLink:
|
|
1425
|
+
pageUrl,
|
|
1426
|
+
resolvedPageUrl,
|
|
1427
|
+
videoId: (_d = videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.videoId) !== null && _d !== void 0 ? _d : extractDouyinVideoId(resolvedPageUrl),
|
|
1428
|
+
isShortLink: pageUrl !== resolvedPageUrl,
|
|
1429
|
+
title: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.title,
|
|
1430
|
+
author: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.author,
|
|
1431
|
+
cover: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.cover,
|
|
1432
|
+
playUrl: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.playUrl,
|
|
1433
|
+
downloadUrl: mediaSource.metadata.downloadUrl,
|
|
1434
|
+
mimeType: mediaSource.metadata.mimeType,
|
|
1435
|
+
totalBytes: mediaSource.metadata.totalBytes,
|
|
1288
1436
|
},
|
|
1289
1437
|
pairedItem: {
|
|
1290
1438
|
item: itemIndex,
|
|
@@ -1292,42 +1440,56 @@ class Vidflow {
|
|
|
1292
1440
|
});
|
|
1293
1441
|
continue;
|
|
1294
1442
|
}
|
|
1295
|
-
if (resource === 'douyin' && operation === '
|
|
1296
|
-
const
|
|
1443
|
+
if (resource === 'douyin' && operation === 'downloadMedia') {
|
|
1444
|
+
const sourceUrl = this.getNodeParameter('sourceUrl', itemIndex);
|
|
1445
|
+
const mediaType = this.getNodeParameter('mediaType', itemIndex, 'video');
|
|
1297
1446
|
const outputBinaryPropertyName = this.getNodeParameter('outputBinaryPropertyName', itemIndex);
|
|
1298
1447
|
const outputFileName = this.getNodeParameter('outputFileName', itemIndex, '');
|
|
1448
|
+
const audioFormat = this.getNodeParameter('audioFormat', itemIndex, 'mp3');
|
|
1449
|
+
const audioFfmpegPath = this.getNodeParameter('audioFfmpegPath', itemIndex, '');
|
|
1299
1450
|
const chunkSizeMb = this.getNodeParameter('chunkSizeMb', itemIndex);
|
|
1300
1451
|
const workerCount = this.getNodeParameter('workerCount', itemIndex);
|
|
1301
|
-
const
|
|
1302
|
-
const
|
|
1452
|
+
const mediaSource = await resolveDouyinMediaSource(this, sourceUrl);
|
|
1453
|
+
const videoInfo = mediaSource.videoInfo;
|
|
1454
|
+
const videoId = mediaSource.downloadId;
|
|
1303
1455
|
const normalizedChunkSizeMb = Math.min(DOUYIN_DOWNLOAD_MAX_CHUNK_SIZE_MB, Math.max(1, Math.floor(chunkSizeMb || DOUYIN_DOWNLOAD_CHUNK_SIZE / (1024 * 1024))));
|
|
1304
1456
|
const chunkSize = normalizedChunkSizeMb * 1024 * 1024;
|
|
1305
1457
|
const normalizedWorkerCount = Math.min(DOUYIN_DOWNLOAD_MAX_WORKERS, Math.max(1, Math.floor(workerCount || DOUYIN_DOWNLOAD_WORKERS)));
|
|
1306
|
-
const { buffer, downloadMode, mimeType, progressPercent,
|
|
1307
|
-
|
|
1458
|
+
const { buffer, downloadMode, downloadUrl, mimeType, progressPercent, totalBytes, } = await streamDownloadWithProgress(this, itemIndex, mediaSource.requestUrl, videoId, chunkSize, normalizedWorkerCount, mediaSource.metadata);
|
|
1459
|
+
let finalBuffer = buffer;
|
|
1460
|
+
let finalFileName = outputFileName.trim() !== ''
|
|
1308
1461
|
? sanitizeFileName(outputFileName.trim())
|
|
1309
1462
|
: `${videoId}.mp4`;
|
|
1310
|
-
|
|
1463
|
+
let finalMimeType = mimeType;
|
|
1464
|
+
if (mediaType === 'audio') {
|
|
1465
|
+
const extractedAudio = await extractAudioFromVideo(this, buffer, `${videoId}.${inferVideoExtension(mimeType)}`, outputFileName, audioFormat, audioFfmpegPath, mimeType);
|
|
1466
|
+
finalBuffer = extractedAudio.buffer;
|
|
1467
|
+
finalFileName = extractedAudio.fileName;
|
|
1468
|
+
finalMimeType = extractedAudio.mimeType;
|
|
1469
|
+
}
|
|
1470
|
+
const binaryData = await this.helpers.prepareBinaryData(finalBuffer, finalFileName, finalMimeType);
|
|
1311
1471
|
returnData.push({
|
|
1312
1472
|
json: {
|
|
1313
1473
|
videoId,
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1474
|
+
title: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.title,
|
|
1475
|
+
author: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.author,
|
|
1476
|
+
cover: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.cover,
|
|
1477
|
+
playUrl: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.playUrl,
|
|
1478
|
+
createTime: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.createTime,
|
|
1479
|
+
pageUrl: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.pageUrl,
|
|
1480
|
+
resolvedPageUrl: videoInfo === null || videoInfo === void 0 ? void 0 : videoInfo.resolvedPageUrl,
|
|
1481
|
+
downloadUrl,
|
|
1482
|
+
sourceType: mediaSource.sourceType,
|
|
1483
|
+
mediaType,
|
|
1323
1484
|
downloadMode,
|
|
1324
1485
|
fileName: finalFileName,
|
|
1325
|
-
mimeType,
|
|
1326
|
-
size:
|
|
1486
|
+
mimeType: finalMimeType,
|
|
1487
|
+
size: finalBuffer.length,
|
|
1327
1488
|
totalBytes,
|
|
1328
1489
|
downloadProgressPercent: progressPercent,
|
|
1329
1490
|
chunkSizeMb: normalizedChunkSizeMb,
|
|
1330
1491
|
workerCount: normalizedWorkerCount,
|
|
1492
|
+
audioFormat: mediaType === 'audio' ? audioFormat : undefined,
|
|
1331
1493
|
},
|
|
1332
1494
|
binary: {
|
|
1333
1495
|
[outputBinaryPropertyName]: binaryData,
|