@lee-jisoo/n8n-nodes-mediafx 1.6.0 → 1.6.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.
Files changed (54) hide show
  1. package/dist/fonts/DejaVuSans.ttf +0 -0
  2. package/dist/fonts/Inter-Regular.ttf +0 -0
  3. package/dist/fonts/NanumGothic-Regular.ttf +0 -0
  4. package/dist/fonts/NotoSansKR-Regular.ttf +0 -0
  5. package/dist/fonts/Pretendard-Regular.otf +0 -0
  6. package/dist/fonts/Roboto-Regular.ttf +0 -0
  7. package/dist/nodes/MediaFX/MediaFX.node.d.ts +12 -0
  8. package/dist/nodes/MediaFX/MediaFX.node.js +559 -0
  9. package/dist/nodes/MediaFX/mediafx.png +0 -0
  10. package/dist/nodes/MediaFX/operations/addSubtitle.d.ts +2 -0
  11. package/dist/nodes/MediaFX/operations/addSubtitle.js +202 -0
  12. package/dist/nodes/MediaFX/operations/addText.d.ts +2 -0
  13. package/dist/nodes/MediaFX/operations/addText.js +108 -0
  14. package/dist/nodes/MediaFX/operations/extractAudio.d.ts +2 -0
  15. package/dist/nodes/MediaFX/operations/extractAudio.js +57 -0
  16. package/dist/nodes/MediaFX/operations/imageToVideo.d.ts +5 -0
  17. package/dist/nodes/MediaFX/operations/imageToVideo.js +65 -0
  18. package/dist/nodes/MediaFX/operations/index.d.ts +13 -0
  19. package/dist/nodes/MediaFX/operations/index.js +29 -0
  20. package/dist/nodes/MediaFX/operations/merge.d.ts +2 -0
  21. package/dist/nodes/MediaFX/operations/merge.js +121 -0
  22. package/dist/nodes/MediaFX/operations/mixAudio.d.ts +2 -0
  23. package/dist/nodes/MediaFX/operations/mixAudio.js +141 -0
  24. package/dist/nodes/MediaFX/operations/multiVideoTransition.d.ts +2 -0
  25. package/dist/nodes/MediaFX/operations/multiVideoTransition.js +245 -0
  26. package/dist/nodes/MediaFX/operations/overlayVideo.d.ts +16 -0
  27. package/dist/nodes/MediaFX/operations/overlayVideo.js +240 -0
  28. package/dist/nodes/MediaFX/operations/separateAudio.d.ts +17 -0
  29. package/dist/nodes/MediaFX/operations/separateAudio.js +78 -0
  30. package/dist/nodes/MediaFX/operations/singleVideoFade.d.ts +2 -0
  31. package/dist/nodes/MediaFX/operations/singleVideoFade.js +60 -0
  32. package/dist/nodes/MediaFX/operations/speed.d.ts +12 -0
  33. package/dist/nodes/MediaFX/operations/speed.js +110 -0
  34. package/dist/nodes/MediaFX/operations/stampImage.d.ts +2 -0
  35. package/dist/nodes/MediaFX/operations/stampImage.js +146 -0
  36. package/dist/nodes/MediaFX/operations/trim.d.ts +2 -0
  37. package/dist/nodes/MediaFX/operations/trim.js +49 -0
  38. package/dist/nodes/MediaFX/properties/audio.properties.d.ts +2 -0
  39. package/dist/nodes/MediaFX/properties/audio.properties.js +394 -0
  40. package/dist/nodes/MediaFX/properties/font.properties.d.ts +2 -0
  41. package/dist/nodes/MediaFX/properties/font.properties.js +186 -0
  42. package/dist/nodes/MediaFX/properties/image.properties.d.ts +2 -0
  43. package/dist/nodes/MediaFX/properties/image.properties.js +333 -0
  44. package/dist/nodes/MediaFX/properties/resources.properties.d.ts +2 -0
  45. package/dist/nodes/MediaFX/properties/resources.properties.js +34 -0
  46. package/dist/nodes/MediaFX/properties/subtitle.properties.d.ts +2 -0
  47. package/dist/nodes/MediaFX/properties/subtitle.properties.js +361 -0
  48. package/dist/nodes/MediaFX/properties/video.properties.d.ts +2 -0
  49. package/dist/nodes/MediaFX/properties/video.properties.js +1135 -0
  50. package/dist/nodes/MediaFX/utils/ffmpegVersion.d.ts +14 -0
  51. package/dist/nodes/MediaFX/utils/ffmpegVersion.js +97 -0
  52. package/dist/nodes/MediaFX/utils.d.ts +43 -0
  53. package/dist/nodes/MediaFX/utils.js +410 -0
  54. package/package.json +1 -1
@@ -0,0 +1,14 @@
1
+ export interface FFmpegCapabilities {
2
+ version: string;
3
+ major: number;
4
+ minor: number;
5
+ patch: number;
6
+ hasXfade: boolean;
7
+ hasGlTransitions: boolean;
8
+ }
9
+ export declare function getFFmpegCapabilities(): Promise<FFmpegCapabilities>;
10
+ export declare function checkTransitionSupport(transition: string): Promise<{
11
+ supported: boolean;
12
+ alternative?: string;
13
+ message?: string;
14
+ }>;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkTransitionSupport = exports.getFFmpegCapabilities = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const util_1 = require("util");
6
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
7
+ let cachedCapabilities = null;
8
+ async function getFFmpegCapabilities() {
9
+ if (cachedCapabilities) {
10
+ return cachedCapabilities;
11
+ }
12
+ try {
13
+ const { stdout } = await execAsync('ffmpeg -version');
14
+ const versionMatch = stdout.match(/ffmpeg version (\d+)\.(\d+)(?:\.(\d+))?/);
15
+ if (!versionMatch) {
16
+ // Fallback for non-standard version formats
17
+ const altMatch = stdout.match(/ffmpeg version N-\d+-g[a-f0-9]+-static/);
18
+ if (altMatch) {
19
+ // This is likely an old static build
20
+ cachedCapabilities = {
21
+ version: altMatch[0],
22
+ major: 4,
23
+ minor: 2,
24
+ patch: 0,
25
+ hasXfade: false,
26
+ hasGlTransitions: false,
27
+ };
28
+ return cachedCapabilities;
29
+ }
30
+ throw new Error('Could not parse FFmpeg version');
31
+ }
32
+ const major = parseInt(versionMatch[1], 10);
33
+ const minor = parseInt(versionMatch[2], 10);
34
+ const patch = parseInt(versionMatch[3] || '0', 10);
35
+ // xfade was added in FFmpeg 4.3
36
+ const hasXfade = major > 4 || (major === 4 && minor >= 3);
37
+ // GL transitions require FFmpeg built with OpenGL support
38
+ const hasGlTransitions = stdout.includes('--enable-opengl');
39
+ cachedCapabilities = {
40
+ version: `${major}.${minor}.${patch}`,
41
+ major,
42
+ minor,
43
+ patch,
44
+ hasXfade,
45
+ hasGlTransitions,
46
+ };
47
+ return cachedCapabilities;
48
+ }
49
+ catch (error) {
50
+ // Default to conservative capabilities if version check fails
51
+ cachedCapabilities = {
52
+ version: 'unknown',
53
+ major: 4,
54
+ minor: 0,
55
+ patch: 0,
56
+ hasXfade: false,
57
+ hasGlTransitions: false,
58
+ };
59
+ return cachedCapabilities;
60
+ }
61
+ }
62
+ exports.getFFmpegCapabilities = getFFmpegCapabilities;
63
+ async function checkTransitionSupport(transition) {
64
+ const capabilities = await getFFmpegCapabilities();
65
+ // Simple transitions that work with basic filters
66
+ const simpleTransitions = ['fade', 'fadeblack', 'fadewhite'];
67
+ // Transitions that require xfade filter
68
+ const xfadeTransitions = [
69
+ 'wipeleft', 'wiperight', 'wipeup', 'wipedown',
70
+ 'slideleft', 'slideright', 'slideup', 'slidedown',
71
+ 'circlecrop', 'rectcrop', 'distance', 'fadegrays',
72
+ 'radial', 'circleopen', 'circleclose', 'pixelize',
73
+ 'dissolve', 'diagtl', 'boxin', 'iris'
74
+ ];
75
+ if (simpleTransitions.includes(transition)) {
76
+ return { supported: true };
77
+ }
78
+ if (xfadeTransitions.includes(transition)) {
79
+ if (capabilities.hasXfade) {
80
+ return { supported: true };
81
+ }
82
+ else {
83
+ return {
84
+ supported: false,
85
+ alternative: 'fade',
86
+ message: `The '${transition}' effect requires FFmpeg 4.3+. Using 'fade' as fallback.`
87
+ };
88
+ }
89
+ }
90
+ // Unknown transition
91
+ return {
92
+ supported: false,
93
+ alternative: 'fade',
94
+ message: `Unknown transition '${transition}'. Using 'fade' as fallback.`
95
+ };
96
+ }
97
+ exports.checkTransitionSupport = checkTransitionSupport;
@@ -0,0 +1,43 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import ffmpeg = require('fluent-ffmpeg');
4
+ import { IExecuteFunctions } from 'n8n-workflow';
5
+ import { IDataObject } from 'n8n-workflow';
6
+ export declare function verifyFfmpegAvailability(): void;
7
+ export declare function getTempFile(extension: string): string;
8
+ export declare function cleanupOldTempFiles(maxAgeHours?: number): Promise<void>;
9
+ export declare function downloadSource(url: string): Promise<{
10
+ filePath: string;
11
+ cleanup: () => Promise<void>;
12
+ }>;
13
+ export declare function createTempFileFromBuffer(buffer: Buffer, originalFilename?: string): Promise<{
14
+ filePath: string;
15
+ cleanup: () => Promise<void>;
16
+ }>;
17
+ export declare function resolveInputs(executeFunctions: IExecuteFunctions, itemIndex: number, sourcesConfig: Array<{
18
+ sourceType: string;
19
+ value: string;
20
+ binaryProperty?: string;
21
+ }>): Promise<{
22
+ paths: string[];
23
+ cleanup: () => Promise<void>;
24
+ }>;
25
+ export declare function getDuration(filePath: string): Promise<number>;
26
+ export declare function createSilentAudio(duration: number): Promise<{
27
+ filePath: string;
28
+ cleanup: () => Promise<void>;
29
+ }>;
30
+ export declare function runFfmpeg(command: ffmpeg.FfmpegCommand): Promise<void>;
31
+ export declare function getVideoStreamInfo(filePath: string): Promise<ffmpeg.FfprobeStream | undefined>;
32
+ export declare function fileHasAudio(filePath: string): Promise<boolean>;
33
+ export declare const REGISTERED_FONTS: IDataObject;
34
+ export declare function getUserFonts(): IDataObject;
35
+ export declare function getAvailableFonts(): IDataObject;
36
+ export declare function validateFontKey(fontKey: string): void;
37
+ export declare function saveUserFont(fontKey: string, fontName: string, description: string, originalFilename: string, buffer: Buffer): {
38
+ fontKey: string;
39
+ path: string;
40
+ metadata: IDataObject | import("n8n-workflow").GenericValue | import("n8n-workflow").GenericValue[] | IDataObject[];
41
+ };
42
+ export declare function deleteUserFont(fontKey: string): boolean;
43
+ export { getFFmpegCapabilities, checkTransitionSupport } from './utils/ffmpegVersion';
@@ -0,0 +1,410 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.checkTransitionSupport = exports.getFFmpegCapabilities = exports.deleteUserFont = exports.saveUserFont = exports.validateFontKey = exports.getAvailableFonts = exports.getUserFonts = exports.REGISTERED_FONTS = exports.fileHasAudio = exports.getVideoStreamInfo = exports.runFfmpeg = exports.createSilentAudio = exports.getDuration = exports.resolveInputs = exports.createTempFileFromBuffer = exports.downloadSource = exports.cleanupOldTempFiles = exports.getTempFile = exports.verifyFfmpegAvailability = void 0;
30
+ const fs = __importStar(require("fs-extra"));
31
+ const path = __importStar(require("path"));
32
+ const uuid_1 = require("uuid");
33
+ const ffmpeg = require("fluent-ffmpeg");
34
+ const axios_1 = __importDefault(require("axios"));
35
+ const n8n_workflow_1 = require("n8n-workflow");
36
+ const os = __importStar(require("os"));
37
+ // Initialize FFmpeg with comprehensive fallback strategy
38
+ let ffmpegPath = null;
39
+ let ffmpegInitialized = false;
40
+ function tryInitializeFfmpeg() {
41
+ if (ffmpegInitialized)
42
+ return true;
43
+ try {
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
45
+ const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
47
+ const ffprobeInstaller = require('@ffprobe-installer/ffprobe');
48
+ const ffmpegInstallerPath = ffmpegInstaller.path;
49
+ const ffprobeInstallerPath = ffprobeInstaller.path;
50
+ if (ffmpegInstallerPath && fs.existsSync(ffmpegInstallerPath) && ffprobeInstallerPath && fs.existsSync(ffprobeInstallerPath)) {
51
+ // Set executable permissions dynamically
52
+ if (os.platform() !== 'win32') {
53
+ try {
54
+ fs.chmodSync(ffmpegInstallerPath, '755');
55
+ fs.chmodSync(ffprobeInstallerPath, '755');
56
+ console.log('Dynamically set permissions for ffmpeg and ffprobe.');
57
+ }
58
+ catch (permissionError) {
59
+ console.warn('Failed to set executable permissions dynamically:', permissionError);
60
+ }
61
+ }
62
+ ffmpeg.setFfmpegPath(ffmpegInstallerPath);
63
+ ffmpeg.setFfprobePath(ffprobeInstallerPath);
64
+ ffmpegPath = ffmpegInstallerPath;
65
+ ffmpegInitialized = true;
66
+ console.log(`FFmpeg initialized with @ffmpeg-installer: ${ffmpegInstallerPath}`);
67
+ console.log(`FFprobe initialized with @ffprobe-installer: ${ffprobeInstallerPath}`);
68
+ return true;
69
+ }
70
+ }
71
+ catch (error) {
72
+ // This is the only strategy, so if it fails, we throw.
73
+ console.error('Failed to load FFmpeg/FFprobe from node_modules.', error);
74
+ throw new n8n_workflow_1.NodeOperationError(
75
+ // We can't use `this.getNode()` here as we are in a utility function.
76
+ // A generic error is sufficient.
77
+ { name: 'MediaFX', type: 'n8n-nodes-mediafx.mediaFX' }, 'Could not load the required FFmpeg executable from the package. ' +
78
+ 'This might be due to a restricted execution environment or a broken installation. ' +
79
+ 'Please check your n8n environment permissions. ' +
80
+ `Original error: ${error.message}`);
81
+ }
82
+ // If we get here, something went wrong, but the catch didn't trigger.
83
+ console.error('FFmpeg binaries were not found in the expected package path.');
84
+ return false;
85
+ }
86
+ // Try to initialize FFmpeg on module load
87
+ tryInitializeFfmpeg();
88
+ const TEMP_DIR = path.resolve(__dirname, '..', '..', 'temp_mediafx');
89
+ fs.ensureDirSync(TEMP_DIR);
90
+ // Function to verify FFmpeg is available
91
+ function verifyFfmpegAvailability() {
92
+ if (!ffmpegInitialized) {
93
+ const success = tryInitializeFfmpeg();
94
+ if (!success) {
95
+ // The error is now thrown inside tryInitializeFfmpeg, but as a fallback:
96
+ throw new n8n_workflow_1.NodeOperationError({ name: 'MediaFX', type: 'n8n-nodes-mediafx.mediaFX' }, 'FFmpeg is not available. The node failed to initialize it from its internal dependencies.');
97
+ }
98
+ }
99
+ // The rest of the verification logic can be simplified or removed
100
+ // as we now only trust our single source of truth.
101
+ console.log(`FFmpeg verification successful: ${ffmpegPath}`);
102
+ }
103
+ exports.verifyFfmpegAvailability = verifyFfmpegAvailability;
104
+ function getTempFile(extension) {
105
+ return path.join(TEMP_DIR, `${(0, uuid_1.v4)()}${extension}`);
106
+ }
107
+ exports.getTempFile = getTempFile;
108
+ // Utility function to clean up old temporary files
109
+ async function cleanupOldTempFiles(maxAgeHours = 24) {
110
+ try {
111
+ if (!fs.existsSync(TEMP_DIR)) {
112
+ return;
113
+ }
114
+ const files = await fs.readdir(TEMP_DIR);
115
+ const now = Date.now();
116
+ const maxAge = maxAgeHours * 60 * 60 * 1000; // Convert hours to milliseconds
117
+ for (const file of files) {
118
+ const filePath = path.join(TEMP_DIR, file);
119
+ try {
120
+ const stats = await fs.stat(filePath);
121
+ const age = now - stats.mtime.getTime();
122
+ if (age > maxAge) {
123
+ await fs.remove(filePath);
124
+ console.log(`Cleaned up old temp file: ${file}`);
125
+ }
126
+ }
127
+ catch (error) {
128
+ // Ignore errors for individual files (file might be in use, etc.)
129
+ console.warn(`Could not clean up temp file ${file}:`, error.message);
130
+ }
131
+ }
132
+ }
133
+ catch (error) {
134
+ console.warn('Error during temp file cleanup:', error.message);
135
+ }
136
+ }
137
+ exports.cleanupOldTempFiles = cleanupOldTempFiles;
138
+ async function downloadSource(url) {
139
+ const tempPath = getTempFile(path.extname(new URL(url).pathname) || '.tmp');
140
+ const writer = fs.createWriteStream(tempPath);
141
+ const response = await (0, axios_1.default)({
142
+ url,
143
+ method: 'GET',
144
+ responseType: 'stream',
145
+ });
146
+ response.data.pipe(writer);
147
+ return new Promise((resolve, reject) => {
148
+ writer.on('finish', () => resolve({ filePath: tempPath, cleanup: () => fs.remove(tempPath) }));
149
+ writer.on('error', (err) => {
150
+ fs.remove(tempPath).catch(() => { });
151
+ reject(err);
152
+ });
153
+ });
154
+ }
155
+ exports.downloadSource = downloadSource;
156
+ async function createTempFileFromBuffer(buffer, originalFilename) {
157
+ const extension = path.extname(originalFilename || '.tmp');
158
+ const tempPath = getTempFile(extension);
159
+ await fs.writeFile(tempPath, buffer);
160
+ return { filePath: tempPath, cleanup: () => fs.remove(tempPath) };
161
+ }
162
+ exports.createTempFileFromBuffer = createTempFileFromBuffer;
163
+ async function resolveInputs(executeFunctions, itemIndex, sourcesConfig) {
164
+ var _a, _b, _c;
165
+ const cleanupFunctions = [];
166
+ const paths = [];
167
+ for (const source of sourcesConfig) {
168
+ let inputPath;
169
+ switch (source.sourceType) {
170
+ case 'url': {
171
+ const { filePath, cleanup } = await downloadSource(source.value);
172
+ inputPath = filePath;
173
+ cleanupFunctions.push(cleanup);
174
+ break;
175
+ }
176
+ case 'binary': {
177
+ if (!source.binaryProperty) {
178
+ throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), 'Binary property name is not defined for binary source.');
179
+ }
180
+ const inputData = executeFunctions.getInputData();
181
+ let binaryData = (_a = inputData[itemIndex]) === null || _a === void 0 ? void 0 : _a.binary;
182
+ let actualItemIndex = itemIndex;
183
+ // Check if binary data exists in current item
184
+ if (!binaryData || !binaryData[source.binaryProperty]) {
185
+ // For merged inputs, check the first item (index 0) as well
186
+ // This handles cases where Merge node combines multiple inputs into one item
187
+ if (itemIndex !== 0 && ((_b = inputData[0]) === null || _b === void 0 ? void 0 : _b.binary) && inputData[0].binary[source.binaryProperty]) {
188
+ binaryData = inputData[0].binary;
189
+ actualItemIndex = 0;
190
+ console.log(`[MediaFX] Using binary data from item 0 for property "${source.binaryProperty}"`);
191
+ }
192
+ else {
193
+ // Provide helpful debugging information
194
+ const currentItemProps = binaryData ? Object.keys(binaryData) : [];
195
+ const firstItemProps = ((_c = inputData[0]) === null || _c === void 0 ? void 0 : _c.binary) ? Object.keys(inputData[0].binary) : [];
196
+ const allProps = [...new Set([...currentItemProps, ...firstItemProps])];
197
+ const errorMessage = `Binary data not found in property "${source.binaryProperty}". ` +
198
+ `Available properties in current item: ${currentItemProps.length > 0 ? currentItemProps.join(', ') : 'none'}. ` +
199
+ (itemIndex !== 0 && firstItemProps.length > 0 ?
200
+ `Available in first item: ${firstItemProps.join(', ')}. ` : '') +
201
+ `All available: ${allProps.length > 0 ? allProps.join(', ') : 'none'}`;
202
+ throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), errorMessage, {
203
+ itemIndex,
204
+ description: 'Make sure the binary property name matches the output from the previous node. ' +
205
+ 'If using a Merge node, check that the binary properties are correctly named (e.g., data1, data2).'
206
+ });
207
+ }
208
+ }
209
+ const originalFilename = binaryData[source.binaryProperty].fileName;
210
+ const buffer = await executeFunctions.helpers.getBinaryDataBuffer(actualItemIndex, source.binaryProperty);
211
+ const { filePath, cleanup } = await createTempFileFromBuffer(buffer, originalFilename);
212
+ inputPath = filePath;
213
+ cleanupFunctions.push(cleanup);
214
+ break;
215
+ }
216
+ default:
217
+ throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Unsupported source type: ${source.sourceType}`);
218
+ }
219
+ paths.push(inputPath);
220
+ }
221
+ const cleanup = async () => {
222
+ for (const func of cleanupFunctions) {
223
+ await func();
224
+ }
225
+ };
226
+ return { paths, cleanup };
227
+ }
228
+ exports.resolveInputs = resolveInputs;
229
+ function getDuration(filePath) {
230
+ return new Promise((resolve, reject) => {
231
+ ffmpeg.ffprobe(filePath, (err, metadata) => {
232
+ if (err) {
233
+ return reject(new Error(`Failed to get video duration: ${err.message}`));
234
+ }
235
+ const duration = metadata.format.duration;
236
+ if (typeof duration !== 'number' || !isFinite(duration)) {
237
+ // Fallback for streams with no duration metadata (like image inputs)
238
+ const videoStream = metadata.streams.find(s => s.codec_type === 'video');
239
+ if (videoStream && videoStream.duration && isFinite(Number(videoStream.duration))) {
240
+ return resolve(Number(videoStream.duration));
241
+ }
242
+ // If it's an image or format without duration, default to a sensible value like 0
243
+ // The caller should handle this case.
244
+ return resolve(0);
245
+ }
246
+ resolve(duration);
247
+ });
248
+ });
249
+ }
250
+ exports.getDuration = getDuration;
251
+ async function createSilentAudio(duration) {
252
+ const outputPath = getTempFile('.aac');
253
+ const cleanup = () => fs.remove(outputPath);
254
+ if (duration <= 0) {
255
+ // Create a very short, almost zero-length silent audio file for inputs like images.
256
+ duration = 0.01;
257
+ }
258
+ const command = ffmpeg()
259
+ .input('anullsrc')
260
+ .inputOptions('-f', 'lavfi')
261
+ .audioCodec('aac')
262
+ .duration(duration)
263
+ .save(outputPath);
264
+ await runFfmpeg(command);
265
+ return { filePath: outputPath, cleanup };
266
+ }
267
+ exports.createSilentAudio = createSilentAudio;
268
+ function runFfmpeg(command) {
269
+ return new Promise((resolve, reject) => {
270
+ command.on('end', () => resolve());
271
+ // @ts-ignore - The type definitions for fluent-ffmpeg seem to have an issue here.
272
+ command.on('error', (err, stdout, stderr) => {
273
+ const errorMessage = `${err.message} (ffmpeg stderr: ${stderr})`;
274
+ reject(new Error(errorMessage));
275
+ });
276
+ command.run();
277
+ });
278
+ }
279
+ exports.runFfmpeg = runFfmpeg;
280
+ function getVideoStreamInfo(filePath) {
281
+ return new Promise((resolve, reject) => {
282
+ ffmpeg.ffprobe(filePath, (err, metadata) => {
283
+ if (err) {
284
+ return reject(new Error(`Failed to probe file: ${err.message}`));
285
+ }
286
+ const videoStream = metadata.streams.find((s) => s.codec_type === 'video');
287
+ resolve(videoStream);
288
+ });
289
+ });
290
+ }
291
+ exports.getVideoStreamInfo = getVideoStreamInfo;
292
+ function fileHasAudio(filePath) {
293
+ return new Promise((resolve, reject) => {
294
+ ffmpeg.ffprobe(filePath, (err, metadata) => {
295
+ if (err) {
296
+ return reject(new Error(`Failed to probe file: ${err.message}`));
297
+ }
298
+ const hasAudio = metadata.streams.some((s) => s.codec_type === 'audio');
299
+ resolve(hasAudio);
300
+ });
301
+ });
302
+ }
303
+ exports.fileHasAudio = fileHasAudio;
304
+ // ====================================================================
305
+ // FONT MANAGEMENT HELPERS
306
+ // ====================================================================
307
+ // Define paths for font management
308
+ const BASE_FONTS_DIR = path.resolve(__dirname, '..', '..', 'fonts');
309
+ const USER_FONTS_DIR = path.join(BASE_FONTS_DIR, 'user');
310
+ const USER_FONTS_JSON = path.join(USER_FONTS_DIR, 'user-fonts.json');
311
+ // System-registered fonts (must exist in BASE_FONTS_DIR)
312
+ exports.REGISTERED_FONTS = {
313
+ 'noto-sans-kr': { name: 'Noto Sans KR', filename: 'NotoSansKR-Regular.ttf', description: 'Google Noto Sans KR', type: 'korean' },
314
+ 'nanum-gothic': { name: 'Nanum Gothic', filename: 'NanumGothic-Regular.ttf', description: 'Naver Nanum Gothic', type: 'korean' },
315
+ 'pretendard': { name: 'Pretendard', filename: 'Pretendard-Regular.otf', description: 'Pretendard', type: 'korean' },
316
+ 'roboto': { name: 'Roboto', filename: 'Roboto-Regular.ttf', description: 'Google Roboto', type: 'global' },
317
+ 'inter': { name: 'Inter', filename: 'Inter-Regular.ttf', description: 'Inter UI Font', type: 'global' },
318
+ 'dejavu-sans': { name: 'DejaVu Sans', filename: 'DejaVuSans.ttf', description: 'Default fallback font', type: 'fallback' },
319
+ };
320
+ // Helper functions for font management
321
+ function ensureUserFontsDirectory() {
322
+ if (!fs.existsSync(USER_FONTS_DIR)) {
323
+ fs.mkdirSync(USER_FONTS_DIR, { recursive: true });
324
+ }
325
+ }
326
+ function getUserFonts() {
327
+ ensureUserFontsDirectory();
328
+ if (!fs.existsSync(USER_FONTS_JSON)) {
329
+ return {};
330
+ }
331
+ try {
332
+ const data = fs.readFileSync(USER_FONTS_JSON, 'utf8');
333
+ return JSON.parse(data);
334
+ }
335
+ catch (error) {
336
+ return {};
337
+ }
338
+ }
339
+ exports.getUserFonts = getUserFonts;
340
+ function saveUserFonts(userFonts) {
341
+ ensureUserFontsDirectory();
342
+ fs.writeFileSync(USER_FONTS_JSON, JSON.stringify(userFonts, null, 2));
343
+ }
344
+ function getAvailableFonts() {
345
+ const fonts = {};
346
+ // Add registered fonts
347
+ for (const [key, font] of Object.entries(exports.REGISTERED_FONTS)) {
348
+ const fontPath = path.join(BASE_FONTS_DIR, font.filename);
349
+ if (fs.existsSync(fontPath)) {
350
+ fonts[key] = { ...font, path: fontPath, type: font.type || 'system' };
351
+ }
352
+ }
353
+ // Add user fonts
354
+ const userFonts = getUserFonts();
355
+ for (const [key, font] of Object.entries(userFonts)) {
356
+ const fontPath = path.join(USER_FONTS_DIR, font.filename);
357
+ if (fs.existsSync(fontPath)) {
358
+ fonts[key] = { ...font, path: fontPath, type: 'user' };
359
+ }
360
+ }
361
+ return fonts;
362
+ }
363
+ exports.getAvailableFonts = getAvailableFonts;
364
+ function validateFontKey(fontKey) {
365
+ const keyPattern = /^[a-zA-Z0-9_-]{3,50}$/;
366
+ if (!keyPattern.test(fontKey)) {
367
+ throw new Error('Font key must be 3-50 characters, containing only letters, numbers, hyphens, and underscores.');
368
+ }
369
+ const allFonts = getAvailableFonts();
370
+ if (allFonts[fontKey]) {
371
+ throw new Error('Font key already exists. Please use a different key.');
372
+ }
373
+ }
374
+ exports.validateFontKey = validateFontKey;
375
+ function saveUserFont(fontKey, fontName, description, originalFilename, buffer) {
376
+ validateFontKey(fontKey);
377
+ const ext = path.extname(originalFilename);
378
+ const filename = `${fontKey}${ext}`;
379
+ const fontPath = path.join(USER_FONTS_DIR, filename);
380
+ fs.writeFileSync(fontPath, buffer);
381
+ const userFonts = getUserFonts();
382
+ userFonts[fontKey] = {
383
+ name: fontName || fontKey,
384
+ filename,
385
+ description,
386
+ createdAt: new Date().toISOString(),
387
+ };
388
+ saveUserFonts(userFonts);
389
+ return { fontKey, path: fontPath, metadata: userFonts[fontKey] };
390
+ }
391
+ exports.saveUserFont = saveUserFont;
392
+ function deleteUserFont(fontKey) {
393
+ const userFonts = getUserFonts();
394
+ const font = userFonts[fontKey];
395
+ if (!font) {
396
+ throw new Error(`User font with key '${fontKey}' not found.`);
397
+ }
398
+ const fontPath = path.join(USER_FONTS_DIR, font.filename);
399
+ if (fs.existsSync(fontPath)) {
400
+ fs.unlinkSync(fontPath);
401
+ }
402
+ delete userFonts[fontKey];
403
+ saveUserFonts(userFonts);
404
+ return true;
405
+ }
406
+ exports.deleteUserFont = deleteUserFont;
407
+ // Re-export ffmpeg version utilities
408
+ var ffmpegVersion_1 = require("./utils/ffmpegVersion");
409
+ Object.defineProperty(exports, "getFFmpegCapabilities", { enumerable: true, get: function () { return ffmpegVersion_1.getFFmpegCapabilities; } });
410
+ Object.defineProperty(exports, "checkTransitionSupport", { enumerable: true, get: function () { return ffmpegVersion_1.checkTransitionSupport; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lee-jisoo/n8n-nodes-mediafx",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "N8N custom nodes for video editing and media processing (Enhanced fork with Speed control and Subtitle fixes)",
5
5
  "license": "MIT",
6
6
  "author": {