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 -> Download Video` 支持直接粘贴抖音分享文案,节点会优先从整段文本里提取抖音链接再下载视频。
48
- - `Douyin -> Download Video` 默认会优先尝试分块并发下载;当源站支持 `Range` 时可断点续传,不支持时会自动降级为单连接流式下载。
49
- - `Douyin -> Download Video` 新增 `Chunk Size (MB)` `Worker Count` 两个调优参数,默认分别为 `5` 和 `8`,运行时会限制在 `1-64 MB` 与 `1-16` 的安全范围内。
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: ['downloadVideo'],
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 Video',
274
- value: 'downloadVideo',
275
- action: 'Download a video',
276
- description: 'Download a Douyin video from a share link',
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: 'downloadVideo',
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: 'Video Link',
297
- name: 'videoUrl',
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 video page link from the Extract Link operation, such as the resolvedUrl value',
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 MP4 file',
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: 'downloadVideo',
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, originalUrl, resolvedUrl) {
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
- awemeId: (_j = item.aweme_id) !== null && _j !== void 0 ? _j : '',
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
- originalUrl,
603
- resolvedUrl,
677
+ pageUrl,
678
+ resolvedPageUrl,
604
679
  };
605
680
  }
606
- function parseRenderDataVideoInfo(html, originalUrl, resolvedUrl) {
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
- awemeId: String((_j = pageRoot.awemeId) !== null && _j !== void 0 ? _j : ''),
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
- videoUrl: playUrl,
629
- originalUrl,
630
- resolvedUrl,
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 originalUrl = sanitizeSharedLink(extractFirstUrl(shareText.trim()));
664
- if (!originalUrl.includes('v.douyin.com')) {
738
+ const pageUrl = sanitizeSharedLink(extractFirstUrl(shareText.trim()));
739
+ if (!pageUrl.includes('v.douyin.com')) {
665
740
  return {
666
- originalUrl,
667
- resolvedUrl: originalUrl,
741
+ pageUrl,
742
+ resolvedPageUrl: pageUrl,
668
743
  };
669
744
  }
670
- const response = await douyinRequestText(context, originalUrl, false);
745
+ const response = await douyinRequestText(context, pageUrl, false);
671
746
  const redirectLocation = getHeaderValue(response.headers, 'location');
672
747
  return {
673
- originalUrl,
674
- resolvedUrl: redirectLocation !== null && redirectLocation !== void 0 ? redirectLocation : originalUrl,
748
+ pageUrl,
749
+ resolvedPageUrl: redirectLocation !== null && redirectLocation !== void 0 ? redirectLocation : pageUrl,
675
750
  };
676
751
  }
677
- function getDirectDouyinVideoUrl(input) {
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('Video Link does not accept Douyin short links. Use the Extract Link operation first and pass its resolvedUrl value to Download Video.');
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
- async function getDouyinVideoInfo(context, videoUrlInput) {
685
- const originalUrl = getDirectDouyinVideoUrl(videoUrlInput);
686
- const resolvedUrl = originalUrl;
687
- const videoId = extractDouyinVideoId(resolvedUrl);
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 Video Link.');
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, originalUrl, resolvedUrl);
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, originalUrl, resolvedUrl);
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, 'HEAD', headers);
742
- response.resume();
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: supportsDouyinRange(response.headers),
745
- mimeType: getDouyinMimeTypeFromHeaders(response.headers),
746
- resolvedUrl,
747
- totalBytes: getDouyinContentLength(response.headers),
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 metadata = await getDouyinDownloadMetadata(videoUrl);
1056
+ const downloadMetadata = metadata !== null && metadata !== void 0 ? metadata : (await getDouyinDownloadMetadata(videoUrl));
927
1057
  try {
928
- if (metadata.acceptRanges && metadata.totalBytes > 0) {
929
- return await chunkedDouyinDownloadToFile(context, itemIndex, metadata.resolvedUrl, outputFile, requestHeaders, metadata, chunkSize, workerCount);
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, metadata.resolvedUrl, outputFile, requestHeaders, metadata.totalBytes);
1072
+ return await streamDouyinDownloadToFile(context, itemIndex, downloadMetadata.downloadUrl, outputFile, requestHeaders, downloadMetadata.totalBytes);
932
1073
  }
933
1074
  finally {
934
- await node_fs_1.promises.rm(outputFile, { force: true });
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 { originalUrl, resolvedUrl } = await resolveDouyinUrl(this, shareText);
1280
- const videoId = extractDouyinVideoId(resolvedUrl);
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
- extractedUrl: originalUrl,
1285
- resolvedUrl,
1286
- videoId,
1287
- isShortLink: originalUrl !== resolvedUrl,
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 === 'downloadVideo') {
1296
- const videoUrl = this.getNodeParameter('videoUrl', itemIndex);
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 videoInfo = await getDouyinVideoInfo(this, videoUrl);
1302
- const videoId = sanitizeFileName(videoInfo.awemeId || 'douyin_video');
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, resolvedDownloadUrl, totalBytes, } = await streamDownloadWithProgress(this, itemIndex, videoInfo.videoUrl, videoId, chunkSize, normalizedWorkerCount);
1307
- const finalFileName = outputFileName.trim() !== ''
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
- const binaryData = await this.helpers.prepareBinaryData(buffer, finalFileName, mimeType);
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
- awemeId: videoInfo.awemeId,
1315
- title: videoInfo.title,
1316
- author: videoInfo.author,
1317
- cover: videoInfo.cover,
1318
- videoUrl: videoInfo.videoUrl,
1319
- createTime: videoInfo.createTime,
1320
- originalUrl: videoInfo.originalUrl,
1321
- resolvedUrl: videoInfo.resolvedUrl,
1322
- resolvedDownloadUrl,
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: buffer.length,
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,