n8n-nodes-vidflow 0.1.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.
@@ -0,0 +1,932 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Vidflow = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const node_path_1 = require("node:path");
8
+ const node_util_1 = require("node:util");
9
+ const n8n_workflow_1 = require("n8n-workflow");
10
+ const BCUT_API_BASE_URL = 'https://member.bilibili.com/x/bcut/rubick-interface';
11
+ const BCUT_MODEL_ID = '8';
12
+ const BCUT_QUERY_MODEL_ID = 7;
13
+ const BCUT_POLL_STATE_FAILED = 3;
14
+ const BCUT_POLL_STATE_COMPLETED = 4;
15
+ const DOUYIN_MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/121.0.2277.107 Version/17.0 Mobile/15E148 Safari/604.1';
16
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
17
+ const audioExtractDisplayOptions = {
18
+ resource: ['audio'],
19
+ operation: ['extractFromVideo'],
20
+ };
21
+ const transcriptionDisplayOptions = {
22
+ resource: ['transcription'],
23
+ operation: ['transcribe'],
24
+ };
25
+ const douyinDownloadDisplayOptions = {
26
+ resource: ['douyin'],
27
+ operation: ['downloadVideo'],
28
+ };
29
+ const transcriptionDescription = [
30
+ {
31
+ displayName: 'Operation',
32
+ name: 'operation',
33
+ type: 'options',
34
+ noDataExpression: true,
35
+ displayOptions: {
36
+ show: {
37
+ resource: ['transcription'],
38
+ },
39
+ },
40
+ options: [
41
+ {
42
+ name: 'Transcribe',
43
+ value: 'transcribe',
44
+ action: 'Transcribe audio using bcut',
45
+ description: 'Upload an audio file to BCut and return the transcript',
46
+ },
47
+ ],
48
+ default: 'transcribe',
49
+ },
50
+ {
51
+ displayName: 'Binary Property',
52
+ name: 'binaryPropertyName',
53
+ type: 'string',
54
+ required: true,
55
+ default: 'data',
56
+ displayOptions: {
57
+ show: transcriptionDisplayOptions,
58
+ },
59
+ description: 'Name of the binary property that contains the audio file',
60
+ },
61
+ {
62
+ displayName: 'File Name',
63
+ name: 'fileName',
64
+ type: 'string',
65
+ default: '',
66
+ displayOptions: {
67
+ show: transcriptionDisplayOptions,
68
+ },
69
+ description: 'Optional file name override. When empty, the binary metadata is used.',
70
+ },
71
+ {
72
+ displayName: 'Include Raw Result',
73
+ name: 'includeRawResult',
74
+ type: 'boolean',
75
+ default: false,
76
+ displayOptions: {
77
+ show: transcriptionDisplayOptions,
78
+ },
79
+ description: 'Whether to include the raw BCut response in the output',
80
+ },
81
+ {
82
+ displayName: 'Options',
83
+ name: 'options',
84
+ type: 'collection',
85
+ placeholder: 'Add Option',
86
+ default: {},
87
+ displayOptions: {
88
+ show: transcriptionDisplayOptions,
89
+ },
90
+ options: [
91
+ {
92
+ displayName: 'Polling Interval Seconds',
93
+ name: 'pollingIntervalSeconds',
94
+ type: 'number',
95
+ default: 1,
96
+ typeOptions: {
97
+ minValue: 1,
98
+ maxValue: 30,
99
+ },
100
+ description: 'How often to poll BCut for task completion',
101
+ },
102
+ {
103
+ displayName: 'Timeout Seconds',
104
+ name: 'timeoutSeconds',
105
+ type: 'number',
106
+ default: 500,
107
+ typeOptions: {
108
+ minValue: 10,
109
+ maxValue: 3600,
110
+ },
111
+ description: 'How long to wait before timing out the transcription task',
112
+ },
113
+ ],
114
+ },
115
+ ];
116
+ const audioDescription = [
117
+ {
118
+ displayName: 'Operation',
119
+ name: 'operation',
120
+ type: 'options',
121
+ noDataExpression: true,
122
+ displayOptions: {
123
+ show: {
124
+ resource: ['audio'],
125
+ },
126
+ },
127
+ options: [
128
+ {
129
+ name: 'Extract From Video',
130
+ value: 'extractFromVideo',
131
+ action: 'Extract audio from a video',
132
+ description: 'Extract the audio track from an input video using ffmpeg',
133
+ },
134
+ ],
135
+ default: 'extractFromVideo',
136
+ },
137
+ {
138
+ displayName: 'Binary Property',
139
+ name: 'binaryPropertyName',
140
+ type: 'string',
141
+ required: true,
142
+ default: 'data',
143
+ displayOptions: {
144
+ show: audioExtractDisplayOptions,
145
+ },
146
+ description: 'Name of the binary property that contains the input video',
147
+ },
148
+ {
149
+ displayName: 'Output Binary Property',
150
+ name: 'outputBinaryPropertyName',
151
+ type: 'string',
152
+ required: true,
153
+ default: 'audio',
154
+ displayOptions: {
155
+ show: audioExtractDisplayOptions,
156
+ },
157
+ description: 'Name of the output binary property that will contain the extracted audio',
158
+ },
159
+ {
160
+ displayName: 'Output Format',
161
+ name: 'outputFormat',
162
+ type: 'options',
163
+ default: 'mp3',
164
+ displayOptions: {
165
+ show: audioExtractDisplayOptions,
166
+ },
167
+ options: [
168
+ {
169
+ name: 'AAC',
170
+ value: 'aac',
171
+ },
172
+ {
173
+ name: 'MP3',
174
+ value: 'mp3',
175
+ },
176
+ {
177
+ name: 'OGG',
178
+ value: 'ogg',
179
+ },
180
+ {
181
+ name: 'WAV',
182
+ value: 'wav',
183
+ },
184
+ ],
185
+ description: 'Audio format for the extracted file',
186
+ },
187
+ {
188
+ displayName: 'File Name',
189
+ name: 'outputFileName',
190
+ type: 'string',
191
+ default: '',
192
+ displayOptions: {
193
+ show: audioExtractDisplayOptions,
194
+ },
195
+ description: 'Optional output file name override',
196
+ },
197
+ {
198
+ displayName: 'FFmpeg Path',
199
+ name: 'ffmpegPath',
200
+ type: 'string',
201
+ default: '',
202
+ displayOptions: {
203
+ show: audioExtractDisplayOptions,
204
+ },
205
+ description: 'Optional ffmpeg executable path. When empty, ffmpeg is resolved from PATH.',
206
+ },
207
+ ];
208
+ const douyinDescription = [
209
+ {
210
+ displayName: 'Operation',
211
+ name: 'operation',
212
+ type: 'options',
213
+ noDataExpression: true,
214
+ displayOptions: {
215
+ show: {
216
+ resource: ['douyin'],
217
+ },
218
+ },
219
+ options: [
220
+ {
221
+ name: 'Download Video',
222
+ value: 'downloadVideo',
223
+ action: 'Download a video',
224
+ description: 'Download a Douyin video from a share link',
225
+ },
226
+ ],
227
+ default: 'downloadVideo',
228
+ },
229
+ {
230
+ displayName: 'Share Text Or Link',
231
+ name: 'shareText',
232
+ type: 'string',
233
+ typeOptions: {
234
+ rows: 4,
235
+ },
236
+ required: true,
237
+ default: '',
238
+ displayOptions: {
239
+ show: douyinDownloadDisplayOptions,
240
+ },
241
+ description: 'A Douyin share message, short link, or direct video link',
242
+ },
243
+ {
244
+ displayName: 'Output Binary Property',
245
+ name: 'outputBinaryPropertyName',
246
+ type: 'string',
247
+ required: true,
248
+ default: 'video',
249
+ displayOptions: {
250
+ show: douyinDownloadDisplayOptions,
251
+ },
252
+ description: 'Name of the output binary property that will contain the MP4 file',
253
+ },
254
+ {
255
+ displayName: 'File Name',
256
+ name: 'outputFileName',
257
+ type: 'string',
258
+ default: '',
259
+ displayOptions: {
260
+ show: douyinDownloadDisplayOptions,
261
+ },
262
+ description: 'Optional output file name override',
263
+ },
264
+ ];
265
+ function getBcutHeaders() {
266
+ return {
267
+ Accept: 'application/json',
268
+ 'Content-Type': 'application/json',
269
+ 'User-Agent': 'Bilibili/1.0.0 (https://www.bilibili.com)',
270
+ };
271
+ }
272
+ function getFileExtension(fileName) {
273
+ const lastDotIndex = fileName.lastIndexOf('.');
274
+ if (lastDotIndex === -1 || lastDotIndex === fileName.length - 1) {
275
+ return undefined;
276
+ }
277
+ return fileName.slice(lastDotIndex + 1).toLowerCase();
278
+ }
279
+ function inferExtensionFromMimeType(mimeType) {
280
+ var _a;
281
+ const normalizedMimeType = (_a = mimeType === null || mimeType === void 0 ? void 0 : mimeType.toLowerCase()) !== null && _a !== void 0 ? _a : '';
282
+ if (normalizedMimeType.includes('mpeg') || normalizedMimeType.includes('mp3')) {
283
+ return 'mp3';
284
+ }
285
+ if (normalizedMimeType.includes('wav')) {
286
+ return 'wav';
287
+ }
288
+ if (normalizedMimeType.includes('x-m4a') || normalizedMimeType.includes('m4a')) {
289
+ return 'm4a';
290
+ }
291
+ if (normalizedMimeType.includes('aac')) {
292
+ return 'aac';
293
+ }
294
+ if (normalizedMimeType.includes('flac')) {
295
+ return 'flac';
296
+ }
297
+ if (normalizedMimeType.includes('ogg') || normalizedMimeType.includes('opus')) {
298
+ return 'ogg';
299
+ }
300
+ return 'mp3';
301
+ }
302
+ function resolveFileName(binaryData, fileNameOverride) {
303
+ const normalizedOverride = fileNameOverride.trim();
304
+ if (normalizedOverride !== '') {
305
+ return normalizedOverride;
306
+ }
307
+ if (binaryData.fileName) {
308
+ return binaryData.fileName;
309
+ }
310
+ return `audio.${inferExtensionFromMimeType(binaryData.mimeType)}`;
311
+ }
312
+ function resolveResourceFileType(fileName, mimeType) {
313
+ var _a;
314
+ return (_a = getFileExtension(fileName)) !== null && _a !== void 0 ? _a : inferExtensionFromMimeType(mimeType);
315
+ }
316
+ function inferVideoExtension(mimeType) {
317
+ var _a;
318
+ const normalizedMimeType = (_a = mimeType === null || mimeType === void 0 ? void 0 : mimeType.toLowerCase()) !== null && _a !== void 0 ? _a : '';
319
+ if (normalizedMimeType.includes('quicktime')) {
320
+ return 'mov';
321
+ }
322
+ if (normalizedMimeType.includes('webm')) {
323
+ return 'webm';
324
+ }
325
+ if (normalizedMimeType.includes('x-matroska') || normalizedMimeType.includes('mkv')) {
326
+ return 'mkv';
327
+ }
328
+ return 'mp4';
329
+ }
330
+ function getOutputAudioMimeType(format) {
331
+ switch (format) {
332
+ case 'aac':
333
+ return 'audio/aac';
334
+ case 'ogg':
335
+ return 'audio/ogg';
336
+ case 'wav':
337
+ return 'audio/wav';
338
+ case 'mp3':
339
+ default:
340
+ return 'audio/mpeg';
341
+ }
342
+ }
343
+ function buildFfmpegAudioArguments(inputPath, outputPath, format) {
344
+ const baseArgs = ['-i', inputPath, '-vn'];
345
+ switch (format) {
346
+ case 'wav':
347
+ return [...baseArgs, '-acodec', 'pcm_s16le', '-ar', '16000', '-ac', '1', '-y', outputPath];
348
+ case 'aac':
349
+ return [...baseArgs, '-acodec', 'aac', '-ab', '192k', '-ar', '44100', '-ac', '2', '-y', outputPath];
350
+ case 'ogg':
351
+ return [
352
+ ...baseArgs,
353
+ '-acodec',
354
+ 'libvorbis',
355
+ '-ab',
356
+ '192k',
357
+ '-ar',
358
+ '44100',
359
+ '-ac',
360
+ '2',
361
+ '-y',
362
+ outputPath,
363
+ ];
364
+ case 'mp3':
365
+ default:
366
+ return [...baseArgs, '-acodec', 'libmp3lame', '-ab', '192k', '-ar', '44100', '-ac', '2', '-y', outputPath];
367
+ }
368
+ }
369
+ function resolveAudioOutputFileName(inputFileName, outputFileName, format) {
370
+ if (outputFileName.trim() !== '') {
371
+ const sanitizedName = sanitizeFileName(outputFileName.trim());
372
+ return (0, node_path_1.extname)(sanitizedName).toLowerCase() === `.${format}`
373
+ ? sanitizedName
374
+ : `${sanitizedName}.${format}`;
375
+ }
376
+ const baseName = sanitizeFileName(inputFileName.replace(/\.[^.]+$/, ''));
377
+ return `${baseName}.${format}`;
378
+ }
379
+ function asErrorMessage(error) {
380
+ if (error instanceof Error) {
381
+ return error.message;
382
+ }
383
+ return 'Unknown error';
384
+ }
385
+ function getHeaderValue(headers, name) {
386
+ var _a, _b;
387
+ if (!headers) {
388
+ return undefined;
389
+ }
390
+ const headerValue = (_b = (_a = headers[name]) !== null && _a !== void 0 ? _a : headers[name.toLowerCase()]) !== null && _b !== void 0 ? _b : headers[name.toUpperCase()];
391
+ if (Array.isArray(headerValue)) {
392
+ return headerValue[0];
393
+ }
394
+ return headerValue;
395
+ }
396
+ function extractFirstUrl(input) {
397
+ const match = input.match(/https?:\/\/[^\s]+/i);
398
+ if (!match) {
399
+ throw new n8n_workflow_1.ApplicationError('No URL was found in Share Text Or Link.');
400
+ }
401
+ return match[0].replace(/[),.;!?]+$/, '');
402
+ }
403
+ function extractDouyinVideoId(urlString) {
404
+ var _a;
405
+ const pathMatch = urlString.match(/\/video\/(\d+)/);
406
+ if (pathMatch === null || pathMatch === void 0 ? void 0 : pathMatch[1]) {
407
+ return pathMatch[1];
408
+ }
409
+ try {
410
+ const parsedUrl = new URL(urlString);
411
+ return (_a = parsedUrl.searchParams.get('modal_id')) !== null && _a !== void 0 ? _a : '';
412
+ }
413
+ catch {
414
+ return '';
415
+ }
416
+ }
417
+ function sanitizeFileName(fileName) {
418
+ const sanitized = fileName.replace(/[<>:"/\\|?*]/g, '_').trim();
419
+ if (sanitized === '') {
420
+ return 'video';
421
+ }
422
+ return sanitized.slice(0, 200);
423
+ }
424
+ function parseRouterDataVideoInfo(html, originalUrl, resolvedUrl) {
425
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
426
+ const routerDataMatch = html.match(/window\._ROUTER_DATA\s*=\s*(.*?)<\/script>/s);
427
+ if (!(routerDataMatch === null || routerDataMatch === void 0 ? void 0 : routerDataMatch[1])) {
428
+ throw new n8n_workflow_1.ApplicationError('Douyin page did not contain window._ROUTER_DATA.');
429
+ }
430
+ const routerDataJson = routerDataMatch[1].trim().replace(/;\s*$/, '');
431
+ const routerData = JSON.parse(routerDataJson);
432
+ const loaderData = routerData.loaderData;
433
+ if (!loaderData) {
434
+ throw new n8n_workflow_1.ApplicationError('Douyin page did not include loaderData.');
435
+ }
436
+ const pageData = (_a = loaderData['video_(id)/page']) !== null && _a !== void 0 ? _a : loaderData['note_(id)/page'];
437
+ const item = (_c = (_b = pageData === null || pageData === void 0 ? void 0 : pageData.videoInfoRes) === null || _b === void 0 ? void 0 : _b.item_list) === null || _c === void 0 ? void 0 : _c[0];
438
+ if (!item) {
439
+ throw new n8n_workflow_1.ApplicationError('Douyin video details could not be extracted from loaderData.');
440
+ }
441
+ const videoUrl = (_h = (_g = (_f = (_e = (_d = item.video) === null || _d === void 0 ? void 0 : _d.play_addr) === null || _e === void 0 ? void 0 : _e.url_list) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.replace(/playwm/g, 'play')) !== null && _h !== void 0 ? _h : '';
442
+ if (videoUrl === '') {
443
+ throw new n8n_workflow_1.ApplicationError('Douyin video URL was missing from the page data.');
444
+ }
445
+ return {
446
+ awemeId: (_j = item.aweme_id) !== null && _j !== void 0 ? _j : '',
447
+ title: (_k = item.desc) !== null && _k !== void 0 ? _k : '',
448
+ author: (_m = (_l = item.author) === null || _l === void 0 ? void 0 : _l.nickname) !== null && _m !== void 0 ? _m : '',
449
+ 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],
450
+ videoUrl,
451
+ createTime: item.create_time,
452
+ originalUrl,
453
+ resolvedUrl,
454
+ };
455
+ }
456
+ function parseRenderDataVideoInfo(html, originalUrl, resolvedUrl) {
457
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
458
+ const renderDataMatch = html.match(/<script id="RENDER_DATA" type="application\/json">([^<]+)<\/script>/);
459
+ if (!(renderDataMatch === null || renderDataMatch === void 0 ? void 0 : renderDataMatch[1])) {
460
+ throw new n8n_workflow_1.ApplicationError('Douyin page did not contain RENDER_DATA.');
461
+ }
462
+ const decodedJson = decodeURIComponent(renderDataMatch[1]);
463
+ const renderData = JSON.parse(decodedJson);
464
+ const pageRoot = (_c = (_b = (_a = renderData['23']) === null || _a === void 0 ? void 0 : _a.aweme) === null || _b === void 0 ? void 0 : _b.detail) !== null && _c !== void 0 ? _c : (_d = renderData.aweme) === null || _d === void 0 ? void 0 : _d.detail;
465
+ if (!pageRoot) {
466
+ throw new n8n_workflow_1.ApplicationError('Douyin video details could not be extracted from RENDER_DATA.');
467
+ }
468
+ const video = pageRoot.video;
469
+ const playUrl = (_h = (_g = (_f = (_e = video === null || video === void 0 ? void 0 : video.playAddr) === null || _e === void 0 ? void 0 : _e.urlList) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.replace(/playwm/g, 'play')) !== null && _h !== void 0 ? _h : '';
470
+ if (playUrl === '') {
471
+ throw new n8n_workflow_1.ApplicationError('Douyin video URL was missing from RENDER_DATA.');
472
+ }
473
+ return {
474
+ awemeId: String((_j = pageRoot.awemeId) !== null && _j !== void 0 ? _j : ''),
475
+ title: String((_k = pageRoot.desc) !== null && _k !== void 0 ? _k : ''),
476
+ author: String((_m = (_l = pageRoot.author) === null || _l === void 0 ? void 0 : _l.nickname) !== null && _m !== void 0 ? _m : ''),
477
+ 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],
478
+ videoUrl: playUrl,
479
+ originalUrl,
480
+ resolvedUrl,
481
+ };
482
+ }
483
+ async function bcutRequest(context, requestOptions) {
484
+ return (await context.helpers.httpRequest(requestOptions));
485
+ }
486
+ async function douyinRequestText(context, url, allowRedirect = true) {
487
+ var _a;
488
+ const response = (await context.helpers.httpRequest({
489
+ url,
490
+ method: 'GET',
491
+ headers: {
492
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
493
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
494
+ Referer: 'https://www.douyin.com/',
495
+ 'User-Agent': DOUYIN_MOBILE_USER_AGENT,
496
+ },
497
+ json: false,
498
+ encoding: 'text',
499
+ disableFollowRedirect: !allowRedirect,
500
+ ignoreHttpStatusErrors: !allowRedirect,
501
+ returnFullResponse: !allowRedirect,
502
+ }));
503
+ if (typeof response === 'string') {
504
+ return { body: response };
505
+ }
506
+ return {
507
+ body: (_a = response.body) !== null && _a !== void 0 ? _a : '',
508
+ statusCode: response.statusCode,
509
+ headers: response.headers,
510
+ };
511
+ }
512
+ async function resolveDouyinUrl(context, shareText) {
513
+ const originalUrl = extractFirstUrl(shareText.trim());
514
+ if (!originalUrl.includes('v.douyin.com')) {
515
+ return {
516
+ originalUrl,
517
+ resolvedUrl: originalUrl,
518
+ };
519
+ }
520
+ const response = await douyinRequestText(context, originalUrl, false);
521
+ const redirectLocation = getHeaderValue(response.headers, 'location');
522
+ return {
523
+ originalUrl,
524
+ resolvedUrl: redirectLocation !== null && redirectLocation !== void 0 ? redirectLocation : originalUrl,
525
+ };
526
+ }
527
+ async function getDouyinVideoInfo(context, shareText) {
528
+ const { originalUrl, resolvedUrl } = await resolveDouyinUrl(context, shareText);
529
+ const videoId = extractDouyinVideoId(resolvedUrl);
530
+ if (videoId === '') {
531
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'A Douyin video ID could not be extracted from the provided link.');
532
+ }
533
+ try {
534
+ const iesPageResponse = await douyinRequestText(context, `https://www.iesdouyin.com/share/video/${videoId}`);
535
+ return parseRouterDataVideoInfo(iesPageResponse.body, originalUrl, resolvedUrl);
536
+ }
537
+ catch (iesError) {
538
+ const webPageResponse = await douyinRequestText(context, `https://www.douyin.com/video/${videoId}`);
539
+ try {
540
+ return parseRenderDataVideoInfo(webPageResponse.body, originalUrl, resolvedUrl);
541
+ }
542
+ catch {
543
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Douyin video details could not be resolved: ${asErrorMessage(iesError)}`);
544
+ }
545
+ }
546
+ }
547
+ async function downloadDouyinVideoBuffer(context, videoUrl) {
548
+ var _a;
549
+ const response = (await context.helpers.httpRequest({
550
+ url: videoUrl,
551
+ method: 'GET',
552
+ headers: {
553
+ Referer: 'https://www.douyin.com/',
554
+ 'User-Agent': DOUYIN_MOBILE_USER_AGENT,
555
+ },
556
+ json: false,
557
+ encoding: 'arraybuffer',
558
+ returnFullResponse: true,
559
+ }));
560
+ const buffer = response.body;
561
+ if (!buffer || !(buffer instanceof Buffer)) {
562
+ throw new n8n_workflow_1.ApplicationError('Douyin video download did not return binary content.');
563
+ }
564
+ const mimeTypeHeader = (_a = getHeaderValue(response.headers, 'content-type')) !== null && _a !== void 0 ? _a : 'video/mp4';
565
+ const mimeType = mimeTypeHeader.split(';')[0] || 'video/mp4';
566
+ return { buffer, mimeType };
567
+ }
568
+ async function extractAudioFromVideo(context, buffer, inputFileName, outputFileName, format, ffmpegPath, mimeType) {
569
+ var _a;
570
+ const resolvedFfmpegPath = ffmpegPath.trim() || process.env.FFMPEG_PATH || 'ffmpeg';
571
+ const tempDirectory = await node_fs_1.promises.mkdtemp((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'vidflow-audio-'));
572
+ const inputExtension = (_a = getFileExtension(inputFileName)) !== null && _a !== void 0 ? _a : inferVideoExtension(mimeType);
573
+ const tempInputPath = (0, node_path_1.join)(tempDirectory, `input.${inputExtension}`);
574
+ const finalFileName = resolveAudioOutputFileName(inputFileName, outputFileName, format);
575
+ const tempOutputPath = (0, node_path_1.join)(tempDirectory, finalFileName);
576
+ try {
577
+ await node_fs_1.promises.writeFile(tempInputPath, buffer);
578
+ const ffmpegArgs = buildFfmpegAudioArguments(tempInputPath, tempOutputPath, format);
579
+ try {
580
+ await execFileAsync(resolvedFfmpegPath, ffmpegArgs);
581
+ }
582
+ catch (error) {
583
+ const execError = error;
584
+ const commandOutput = execError.stderr || execError.stdout || execError.message;
585
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Audio extraction failed: ${commandOutput || 'ffmpeg returned an unknown error'}`);
586
+ }
587
+ const outputBuffer = await node_fs_1.promises.readFile(tempOutputPath);
588
+ return {
589
+ buffer: outputBuffer,
590
+ ffmpegPath: resolvedFfmpegPath,
591
+ fileName: finalFileName,
592
+ format,
593
+ mimeType: getOutputAudioMimeType(format),
594
+ size: outputBuffer.length,
595
+ };
596
+ }
597
+ finally {
598
+ await node_fs_1.promises.rm(tempDirectory, { force: true, recursive: true });
599
+ }
600
+ }
601
+ async function uploadAudioToBcut(context, buffer, fileName, resourceFileType) {
602
+ var _a, _b, _c, _d, _e, _f, _g, _h;
603
+ const createPayload = {
604
+ type: 2,
605
+ name: fileName,
606
+ size: buffer.length,
607
+ ResourceFileType: resourceFileType,
608
+ model_id: BCUT_MODEL_ID,
609
+ };
610
+ const createResponse = await bcutRequest(context, {
611
+ url: `${BCUT_API_BASE_URL}/resource/create`,
612
+ method: 'POST',
613
+ headers: getBcutHeaders(),
614
+ body: createPayload,
615
+ json: true,
616
+ });
617
+ if (createResponse.code !== 0 || createResponse.data === undefined) {
618
+ throw new n8n_workflow_1.ApplicationError(`BCut upload initialization failed: ${(_a = createResponse.message) !== null && _a !== void 0 ? _a : 'Unknown error'}`);
619
+ }
620
+ const { data } = createResponse;
621
+ const resourceId = data.resource_id;
622
+ const inBossKey = data.in_boss_key;
623
+ const uploadId = data.upload_id;
624
+ const uploadUrls = (_b = data.upload_urls) !== null && _b !== void 0 ? _b : [];
625
+ const partSize = (_c = data.per_size) !== null && _c !== void 0 ? _c : 0;
626
+ if (!resourceId || !inBossKey || !uploadId || uploadUrls.length === 0 || partSize <= 0) {
627
+ throw new n8n_workflow_1.ApplicationError('BCut upload initialization returned incomplete upload metadata.');
628
+ }
629
+ const etags = [];
630
+ for (let index = 0; index < uploadUrls.length; index++) {
631
+ const startRange = index * partSize;
632
+ const endRange = Math.min((index + 1) * partSize, buffer.length);
633
+ const uploadResponse = (await context.helpers.httpRequest({
634
+ url: uploadUrls[index],
635
+ method: 'PUT',
636
+ headers: {
637
+ 'Content-Type': 'application/octet-stream',
638
+ },
639
+ body: buffer.subarray(startRange, endRange),
640
+ json: false,
641
+ returnFullResponse: true,
642
+ }));
643
+ const rawEtag = (_e = (_d = uploadResponse.headers) === null || _d === void 0 ? void 0 : _d.etag) !== null && _e !== void 0 ? _e : (_f = uploadResponse.headers) === null || _f === void 0 ? void 0 : _f.Etag;
644
+ const etag = Array.isArray(rawEtag) ? rawEtag[0] : rawEtag;
645
+ if (!etag) {
646
+ throw new n8n_workflow_1.ApplicationError(`BCut upload part ${index + 1} did not return an ETag header.`);
647
+ }
648
+ etags.push(etag.replace(/^"|"$/g, ''));
649
+ }
650
+ const commitResponse = await bcutRequest(context, {
651
+ url: `${BCUT_API_BASE_URL}/resource/create/complete`,
652
+ method: 'POST',
653
+ headers: getBcutHeaders(),
654
+ body: {
655
+ InBossKey: inBossKey,
656
+ ResourceId: resourceId,
657
+ Etags: etags.join(','),
658
+ UploadId: uploadId,
659
+ model_id: BCUT_MODEL_ID,
660
+ },
661
+ json: true,
662
+ });
663
+ if (commitResponse.code !== 0 || !((_g = commitResponse.data) === null || _g === void 0 ? void 0 : _g.download_url)) {
664
+ throw new n8n_workflow_1.ApplicationError(`BCut upload commit failed: ${(_h = commitResponse.message) !== null && _h !== void 0 ? _h : 'Unknown error'}`);
665
+ }
666
+ return {
667
+ resourceId,
668
+ downloadUrl: commitResponse.data.download_url,
669
+ };
670
+ }
671
+ async function createBcutTask(context, downloadUrl) {
672
+ var _a, _b;
673
+ const taskResponse = await bcutRequest(context, {
674
+ url: `${BCUT_API_BASE_URL}/task`,
675
+ method: 'POST',
676
+ headers: getBcutHeaders(),
677
+ body: {
678
+ resource: downloadUrl,
679
+ model_id: BCUT_MODEL_ID,
680
+ },
681
+ json: true,
682
+ });
683
+ if (taskResponse.code !== 0 || !((_a = taskResponse.data) === null || _a === void 0 ? void 0 : _a.task_id)) {
684
+ throw new n8n_workflow_1.ApplicationError(`BCut task creation failed: ${(_b = taskResponse.message) !== null && _b !== void 0 ? _b : 'Unknown error'}`);
685
+ }
686
+ return taskResponse.data.task_id;
687
+ }
688
+ async function queryBcutTask(context, taskId) {
689
+ var _a;
690
+ const taskResultResponse = await bcutRequest(context, {
691
+ url: `${BCUT_API_BASE_URL}/task/result`,
692
+ method: 'GET',
693
+ headers: getBcutHeaders(),
694
+ qs: {
695
+ model_id: BCUT_QUERY_MODEL_ID,
696
+ task_id: taskId,
697
+ },
698
+ json: true,
699
+ });
700
+ if (taskResultResponse.code !== 0 || taskResultResponse.data === undefined) {
701
+ throw new n8n_workflow_1.ApplicationError(`BCut task polling failed: ${(_a = taskResultResponse.message) !== null && _a !== void 0 ? _a : 'Unknown error'}`);
702
+ }
703
+ return taskResultResponse.data;
704
+ }
705
+ async function transcribeWithBcut(context, buffer, fileName, resourceFileType, pollingIntervalSeconds, timeoutSeconds) {
706
+ var _a, _b, _c;
707
+ const { resourceId, downloadUrl } = await uploadAudioToBcut(context, buffer, fileName, resourceFileType);
708
+ const taskId = await createBcutTask(context, downloadUrl);
709
+ const maxAttempts = Math.max(1, Math.ceil(timeoutSeconds / pollingIntervalSeconds));
710
+ let taskResult;
711
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
712
+ taskResult = await queryBcutTask(context, taskId);
713
+ if (taskResult.state === BCUT_POLL_STATE_COMPLETED) {
714
+ break;
715
+ }
716
+ if (taskResult.state === BCUT_POLL_STATE_FAILED) {
717
+ throw new n8n_workflow_1.ApplicationError(`BCut transcription failed with state ${String(taskResult.state)}.`);
718
+ }
719
+ await (0, n8n_workflow_1.sleep)(pollingIntervalSeconds * 1000);
720
+ }
721
+ if ((taskResult === null || taskResult === void 0 ? void 0 : taskResult.state) !== BCUT_POLL_STATE_COMPLETED || !taskResult.result) {
722
+ throw new n8n_workflow_1.ApplicationError(`BCut transcription timed out with state ${String((_a = taskResult === null || taskResult === void 0 ? void 0 : taskResult.state) !== null && _a !== void 0 ? _a : 'unknown')}.`);
723
+ }
724
+ const rawResult = JSON.parse(taskResult.result);
725
+ const segments = ((_b = rawResult.utterances) !== null && _b !== void 0 ? _b : []).map((utterance) => {
726
+ var _a, _b, _c;
727
+ return ({
728
+ start: Number((_a = utterance.start_time) !== null && _a !== void 0 ? _a : 0) / 1000,
729
+ end: Number((_b = utterance.end_time) !== null && _b !== void 0 ? _b : 0) / 1000,
730
+ text: String((_c = utterance.transcript) !== null && _c !== void 0 ? _c : '').trim(),
731
+ });
732
+ });
733
+ const text = segments
734
+ .map((segment) => segment.text)
735
+ .filter((segmentText) => segmentText !== '')
736
+ .join(' ');
737
+ return {
738
+ taskId,
739
+ resourceId,
740
+ downloadUrl,
741
+ language: (_c = rawResult.language) !== null && _c !== void 0 ? _c : 'zh',
742
+ text,
743
+ segments,
744
+ raw: rawResult,
745
+ };
746
+ }
747
+ class Vidflow {
748
+ constructor() {
749
+ this.description = {
750
+ displayName: 'Vidflow',
751
+ name: 'vidflow',
752
+ icon: { light: 'file:vidflow.svg', dark: 'file:vidflow.dark.svg' },
753
+ group: ['transform'],
754
+ version: 1,
755
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
756
+ description: 'Extract audio, transcribe with BCut, and download Douyin videos',
757
+ defaults: {
758
+ name: 'Vidflow',
759
+ },
760
+ usableAsTool: true,
761
+ inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
762
+ outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
763
+ properties: [
764
+ {
765
+ displayName: 'Resource',
766
+ name: 'resource',
767
+ type: 'options',
768
+ noDataExpression: true,
769
+ options: [
770
+ {
771
+ name: 'Audio',
772
+ value: 'audio',
773
+ },
774
+ {
775
+ name: 'Douyin',
776
+ value: 'douyin',
777
+ },
778
+ {
779
+ name: 'Transcription',
780
+ value: 'transcription',
781
+ },
782
+ ],
783
+ default: 'transcription',
784
+ },
785
+ ...audioDescription,
786
+ ...transcriptionDescription,
787
+ ...douyinDescription,
788
+ ],
789
+ };
790
+ }
791
+ async execute() {
792
+ var _a, _b, _c;
793
+ const items = this.getInputData();
794
+ const returnData = [];
795
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
796
+ try {
797
+ const resource = this.getNodeParameter('resource', itemIndex);
798
+ const operation = this.getNodeParameter('operation', itemIndex);
799
+ if (resource === 'audio' && operation === 'extractFromVideo') {
800
+ const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex);
801
+ const outputBinaryPropertyName = this.getNodeParameter('outputBinaryPropertyName', itemIndex);
802
+ const outputFormat = this.getNodeParameter('outputFormat', itemIndex);
803
+ const outputFileName = this.getNodeParameter('outputFileName', itemIndex, '');
804
+ const ffmpegPath = this.getNodeParameter('ffmpegPath', itemIndex, '');
805
+ const binaryData = this.helpers.assertBinaryData(itemIndex, binaryPropertyName);
806
+ const inputBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName);
807
+ const inputFileName = (_a = binaryData.fileName) !== null && _a !== void 0 ? _a : `video.${inferVideoExtension(binaryData.mimeType)}`;
808
+ const extractedAudio = await extractAudioFromVideo(this, inputBuffer, inputFileName, outputFileName, outputFormat, ffmpegPath, binaryData.mimeType);
809
+ const outputBinaryData = await this.helpers.prepareBinaryData(extractedAudio.buffer, extractedAudio.fileName, extractedAudio.mimeType);
810
+ returnData.push({
811
+ json: {
812
+ inputBinaryPropertyName: binaryPropertyName,
813
+ inputFileName,
814
+ outputBinaryPropertyName,
815
+ outputFileName: extractedAudio.fileName,
816
+ outputFormat: extractedAudio.format,
817
+ mimeType: extractedAudio.mimeType,
818
+ size: extractedAudio.size,
819
+ ffmpegPath: extractedAudio.ffmpegPath,
820
+ },
821
+ binary: {
822
+ ...items[itemIndex].binary,
823
+ [outputBinaryPropertyName]: outputBinaryData,
824
+ },
825
+ pairedItem: {
826
+ item: itemIndex,
827
+ },
828
+ });
829
+ continue;
830
+ }
831
+ if (resource === 'transcription' && operation === 'transcribe') {
832
+ const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex);
833
+ const fileNameOverride = this.getNodeParameter('fileName', itemIndex, '');
834
+ const includeRawResult = this.getNodeParameter('includeRawResult', itemIndex, false);
835
+ const options = this.getNodeParameter('options', itemIndex, {});
836
+ const pollingIntervalSeconds = Number((_b = options.pollingIntervalSeconds) !== null && _b !== void 0 ? _b : 1);
837
+ const timeoutSeconds = Number((_c = options.timeoutSeconds) !== null && _c !== void 0 ? _c : 500);
838
+ if (Number.isNaN(pollingIntervalSeconds) || pollingIntervalSeconds < 1) {
839
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Polling Interval Seconds must be greater than or equal to 1', { itemIndex });
840
+ }
841
+ if (Number.isNaN(timeoutSeconds) || timeoutSeconds < pollingIntervalSeconds) {
842
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Timeout Seconds must be greater than or equal to the polling interval', { itemIndex });
843
+ }
844
+ const binaryData = this.helpers.assertBinaryData(itemIndex, binaryPropertyName);
845
+ const buffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName);
846
+ const fileName = resolveFileName(binaryData, fileNameOverride);
847
+ const resourceFileType = resolveResourceFileType(fileName, binaryData.mimeType);
848
+ const transcript = await transcribeWithBcut(this, buffer, fileName, resourceFileType, pollingIntervalSeconds, timeoutSeconds);
849
+ const result = {
850
+ taskId: transcript.taskId,
851
+ resourceId: transcript.resourceId,
852
+ downloadUrl: transcript.downloadUrl,
853
+ language: transcript.language,
854
+ text: transcript.text,
855
+ segments: transcript.segments,
856
+ };
857
+ if (includeRawResult) {
858
+ result.raw = transcript.raw;
859
+ }
860
+ returnData.push({
861
+ json: result,
862
+ binary: items[itemIndex].binary,
863
+ pairedItem: {
864
+ item: itemIndex,
865
+ },
866
+ });
867
+ continue;
868
+ }
869
+ if (resource === 'douyin' && operation === 'downloadVideo') {
870
+ const shareText = this.getNodeParameter('shareText', itemIndex);
871
+ const outputBinaryPropertyName = this.getNodeParameter('outputBinaryPropertyName', itemIndex);
872
+ const outputFileName = this.getNodeParameter('outputFileName', itemIndex, '');
873
+ const videoInfo = await getDouyinVideoInfo(this, shareText);
874
+ const { buffer, mimeType } = await downloadDouyinVideoBuffer(this, videoInfo.videoUrl);
875
+ const finalFileName = outputFileName.trim() !== ''
876
+ ? sanitizeFileName(outputFileName.trim())
877
+ : `${sanitizeFileName(videoInfo.awemeId || videoInfo.title || 'douyin_video')}.mp4`;
878
+ const binaryData = await this.helpers.prepareBinaryData(buffer, finalFileName, mimeType);
879
+ returnData.push({
880
+ json: {
881
+ awemeId: videoInfo.awemeId,
882
+ title: videoInfo.title,
883
+ author: videoInfo.author,
884
+ cover: videoInfo.cover,
885
+ videoUrl: videoInfo.videoUrl,
886
+ createTime: videoInfo.createTime,
887
+ originalUrl: videoInfo.originalUrl,
888
+ resolvedUrl: videoInfo.resolvedUrl,
889
+ fileName: finalFileName,
890
+ mimeType,
891
+ size: buffer.length,
892
+ },
893
+ binary: {
894
+ [outputBinaryPropertyName]: binaryData,
895
+ },
896
+ pairedItem: {
897
+ item: itemIndex,
898
+ },
899
+ });
900
+ continue;
901
+ }
902
+ if (resource !== 'transcription' || operation !== 'transcribe') {
903
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unsupported resource or operation: ${resource}.${operation}`, { itemIndex });
904
+ }
905
+ }
906
+ catch (error) {
907
+ if (this.continueOnFail()) {
908
+ returnData.push({
909
+ json: {
910
+ error: asErrorMessage(error),
911
+ },
912
+ pairedItem: {
913
+ item: itemIndex,
914
+ },
915
+ });
916
+ continue;
917
+ }
918
+ if (error instanceof n8n_workflow_1.NodeOperationError) {
919
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
920
+ itemIndex,
921
+ });
922
+ }
923
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
924
+ itemIndex,
925
+ });
926
+ }
927
+ }
928
+ return [returnData];
929
+ }
930
+ }
931
+ exports.Vidflow = Vidflow;
932
+ //# sourceMappingURL=Vidflow.node.js.map