@ferchy/aimc-n8n-toolkit 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AIMC Toolkit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # AIMC Toolkit for n8n
2
+
3
+ AIMC Toolkit is a community node package for n8n with two focused nodes:
4
+
5
+ - **AIMC Code**: Run JavaScript with a curated library toolbox.
6
+ - **AIMC Media**: FFmpeg-based media operations (convert, compress, extract audio, merge, metadata).
7
+
8
+ ## Why AIMC Toolkit
9
+
10
+ - **Faster workflows**: replace multiple utility nodes with one code node.
11
+ - **Media ready**: common FFmpeg tasks in a single, consistent UI.
12
+ - **Practical libraries**: data parsing, validation, formatting, and web utilities built-in.
13
+
14
+ ## Installation
15
+
16
+ ### Community Nodes
17
+ 1. Open **Settings > Community Nodes** in n8n.
18
+ 2. Install: `@ferchy/aimc-n8n-toolkit`.
19
+
20
+ ### Manual
21
+ ```bash
22
+ npm install @ferchy/aimc-n8n-toolkit
23
+ ```
24
+
25
+ ## FFmpeg Setup
26
+
27
+ AIMC Media will use the first available option:
28
+
29
+ 1. `FFMPEG_PATH` / `FFPROBE_PATH` environment variables
30
+ 2. Optional `ffmpeg-static` / `ffprobe-static`
31
+ 3. System FFmpeg on `PATH`
32
+
33
+ Recommended for servers:
34
+ ```bash
35
+ # Debian/Ubuntu
36
+ apt-get update && apt-get install -y ffmpeg
37
+ ```
38
+
39
+ ## Nodes
40
+
41
+ ### AIMC Code
42
+
43
+ **Features**
44
+ - Run once for all items or once per item.
45
+ - Access libraries as globals (`axios`, `_`, `zod`) or via `libs`.
46
+
47
+ **Example**
48
+ ```javascript
49
+ const rows = $input.all().map((i) => i.json);
50
+ const ids = rows.map(() => nanoid());
51
+
52
+ return rows.map((row, index) => ({
53
+ ...row,
54
+ id: ids[index],
55
+ normalizedEmail: validator.normalizeEmail(row.email || ''),
56
+ createdAt: utils.now(),
57
+ }));
58
+ ```
59
+
60
+ **Library snapshot**
61
+ `axios`, `lodash`, `zod`, `joi`, `yup`, `dayjs`, `date-fns`, `cheerio`, `papaparse`,
62
+ `yaml`, `xml2js`, `qs`, `form-data`, `uuid`, `nanoid`, and more.
63
+
64
+ ### AIMC Media
65
+
66
+ **Operations**
67
+ - Convert / Transcode
68
+ - Compress
69
+ - Extract Audio
70
+ - Merge Video + Audio
71
+ - Metadata (ffprobe)
72
+
73
+ **Example**
74
+ Convert a video to WebM and scale it:
75
+ ```text
76
+ Operation: Convert / Transcode
77
+ Output Format: WebM
78
+ Additional Output Options:
79
+ -vf scale=1280:-2
80
+ ```
81
+
82
+ **Large files**
83
+ Use **Input Mode = File Path** to avoid loading big files into memory.
84
+
85
+ ## Configuration
86
+
87
+ Environment variables:
88
+
89
+ - `FFMPEG_PATH`: custom FFmpeg binary path
90
+ - `FFPROBE_PATH`: custom ffprobe binary path
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,5 @@
1
+ import { INodeExecutionData, INodeType, INodeTypeDescription, IExecuteFunctions } from 'n8n-workflow';
2
+ export declare class AimcCode implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AimcCode = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const vm_1 = require("vm");
6
+ const optionalRequire = (moduleName) => () => {
7
+ try {
8
+ return require(moduleName);
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ };
14
+ const libraryLoaders = {
15
+ _: () => require('lodash'),
16
+ lodash: () => require('lodash'),
17
+ axios: () => require('axios'),
18
+ cheerio: () => require('cheerio'),
19
+ dayjs: () => require('dayjs'),
20
+ moment: () => require('moment-timezone'),
21
+ dateFns: () => require('date-fns'),
22
+ dateFnsTz: () => require('date-fns-tz'),
23
+ joi: () => require('joi'),
24
+ Joi: () => require('joi'),
25
+ validator: () => require('validator'),
26
+ uuid: () => require('uuid'),
27
+ Ajv: () => require('ajv'),
28
+ yup: () => require('yup'),
29
+ zod: () => require('zod'),
30
+ z: () => require('zod'),
31
+ xml2js: () => require('xml2js'),
32
+ XMLParser: () => require('fast-xml-parser').XMLParser,
33
+ YAML: () => require('yaml'),
34
+ papaparse: () => require('papaparse'),
35
+ Papa: () => require('papaparse'),
36
+ Handlebars: () => require('handlebars'),
37
+ htmlToText: () => require('html-to-text'),
38
+ marked: () => require('marked'),
39
+ slug: () => {
40
+ const slugLib = require('slug');
41
+ return slugLib.default || slugLib;
42
+ },
43
+ pluralize: () => require('pluralize'),
44
+ qs: () => require('qs'),
45
+ FormData: () => require('form-data'),
46
+ ini: () => require('ini'),
47
+ toml: () => require('toml'),
48
+ nanoid: () => {
49
+ const nanoidLib = require('nanoid');
50
+ return nanoidLib.nanoid || nanoidLib;
51
+ },
52
+ bytes: () => require('bytes'),
53
+ ms: () => require('ms'),
54
+ phoneNumber: () => require('libphonenumber-js'),
55
+ iban: () => require('iban'),
56
+ fuzzy: () => require('fuse.js'),
57
+ stringSimilarity: () => require('string-similarity'),
58
+ pRetry: () => require('p-retry'),
59
+ jsonDiff: () => require('json-diff-ts'),
60
+ cronParser: () => require('cron-parser'),
61
+ franc: () => require('franc-min'),
62
+ compromise: () => require('compromise'),
63
+ protobuf: () => require('protobufjs'),
64
+ protobufjs: () => require('protobufjs'),
65
+ knex: () => require('knex'),
66
+ QRCode: () => require('qrcode'),
67
+ qrcode: () => require('qrcode'),
68
+ ytdl: () => require('@distube/ytdl-core'),
69
+ httpProxyAgent: () => require('http-proxy-agent'),
70
+ socksProxyAgent: () => require('socks-proxy-agent'),
71
+ bufferutil: optionalRequire('bufferutil'),
72
+ utf8Validate: optionalRequire('utf-8-validate'),
73
+ };
74
+ function attachLibraries(target, cache) {
75
+ for (const [name, loader] of Object.entries(libraryLoaders)) {
76
+ Object.defineProperty(target, name, {
77
+ enumerable: true,
78
+ get() {
79
+ if (!(name in cache)) {
80
+ cache[name] = loader();
81
+ }
82
+ return cache[name];
83
+ },
84
+ });
85
+ }
86
+ }
87
+ function normalizeResult(result, fallbackItems) {
88
+ const isItem = (value) => !!value && typeof value === 'object' && 'json' in value;
89
+ const toJson = (value) => value && typeof value === 'object' ? JSON.parse(JSON.stringify(value)) : value;
90
+ if (result === undefined) {
91
+ return fallbackItems;
92
+ }
93
+ if (Array.isArray(result)) {
94
+ if (result.every(isItem)) {
95
+ return result;
96
+ }
97
+ return result.map((entry) => ({
98
+ json: entry && typeof entry === 'object' ? toJson(entry) : { data: entry },
99
+ }));
100
+ }
101
+ if (isItem(result)) {
102
+ return [result];
103
+ }
104
+ return [
105
+ {
106
+ json: result && typeof result === 'object' ? toJson(result) : { data: result },
107
+ },
108
+ ];
109
+ }
110
+ function buildSandbox(params) {
111
+ const cache = {};
112
+ const sandbox = {
113
+ items: params.items,
114
+ item: params.item,
115
+ $input: {
116
+ all: () => params.items,
117
+ first: () => params.items[0],
118
+ last: () => params.items[params.items.length - 1],
119
+ item: () => params.item,
120
+ json: () => params.mode === 'runOnceForEachItem' && params.item
121
+ ? params.item.json
122
+ : params.items.map((entry) => entry.json),
123
+ },
124
+ params: params.nodeParams,
125
+ utils: {
126
+ now: () => new Date().toISOString(),
127
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
128
+ isEmpty: (value) => value === null || value === undefined || value === '' ||
129
+ (Array.isArray(value) && value.length === 0),
130
+ safeJson: (value) => {
131
+ try {
132
+ return JSON.parse(JSON.stringify(value));
133
+ }
134
+ catch {
135
+ return value;
136
+ }
137
+ },
138
+ toArray: (value) => (Array.isArray(value) ? value : [value]),
139
+ availableLibraries: () => Object.keys(libraryLoaders).sort(),
140
+ },
141
+ console: {
142
+ log: (...args) => console.log('[AIMC Code]', ...args),
143
+ warn: (...args) => console.warn('[AIMC Code]', ...args),
144
+ error: (...args) => console.error('[AIMC Code]', ...args),
145
+ },
146
+ Buffer,
147
+ Date,
148
+ Math,
149
+ JSON,
150
+ Promise,
151
+ setTimeout,
152
+ clearTimeout,
153
+ };
154
+ attachLibraries(sandbox, cache);
155
+ const libs = {};
156
+ attachLibraries(libs, cache);
157
+ sandbox.libs = libs;
158
+ return sandbox;
159
+ }
160
+ class AimcCode {
161
+ constructor() {
162
+ this.description = {
163
+ displayName: 'AIMC Code',
164
+ name: 'aimcCode',
165
+ icon: 'file:aimc-code.svg',
166
+ group: ['transform'],
167
+ version: 1,
168
+ description: 'Execute JavaScript with a curated toolkit of libraries.',
169
+ defaults: {
170
+ name: 'AIMC Code',
171
+ },
172
+ inputs: ['main'],
173
+ outputs: ['main'],
174
+ properties: [
175
+ {
176
+ displayName: 'Mode',
177
+ name: 'mode',
178
+ type: 'options',
179
+ options: [
180
+ {
181
+ name: 'Run Once for All Items',
182
+ value: 'runOnceForAllItems',
183
+ },
184
+ {
185
+ name: 'Run Once for Each Item',
186
+ value: 'runOnceForEachItem',
187
+ },
188
+ ],
189
+ default: 'runOnceForAllItems',
190
+ },
191
+ {
192
+ displayName: 'Timeout (Seconds)',
193
+ name: 'timeoutSeconds',
194
+ type: 'number',
195
+ default: 30,
196
+ typeOptions: {
197
+ minValue: 1,
198
+ maxValue: 300,
199
+ },
200
+ },
201
+ {
202
+ displayName: 'Code',
203
+ name: 'code',
204
+ type: 'string',
205
+ typeOptions: {
206
+ editor: 'jsEditor',
207
+ },
208
+ default: `// AIMC Code node\n// Use libs.<name> or direct globals like axios, _, zod\n\n// Example: transform all items\nconst data = $input.all().map((entry) => entry.json);\nconst ids = data.map((row) => nanoid());\n\nreturn data.map((row, index) => ({\n ...row,\n id: ids[index],\n processedAt: utils.now(),\n}));\n`,
209
+ description: 'Write JavaScript to transform data.',
210
+ noDataExpression: true,
211
+ },
212
+ ],
213
+ };
214
+ }
215
+ async execute() {
216
+ const items = this.getInputData();
217
+ const mode = this.getNodeParameter('mode', 0, 'runOnceForAllItems');
218
+ const code = this.getNodeParameter('code', 0, '');
219
+ const timeoutSeconds = this.getNodeParameter('timeoutSeconds', 0, 30);
220
+ if (!code.trim()) {
221
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No code provided.');
222
+ }
223
+ const timeoutMs = Math.max(1, timeoutSeconds) * 1000;
224
+ const nodeParams = this.getNode().parameters;
225
+ const runCode = async (sandbox) => {
226
+ const context = (0, vm_1.createContext)(sandbox);
227
+ const wrapped = `(async () => {\n${code}\n})()`;
228
+ return (0, vm_1.runInContext)(wrapped, context, { timeout: timeoutMs });
229
+ };
230
+ try {
231
+ if (mode === 'runOnceForEachItem') {
232
+ const results = [];
233
+ for (let index = 0; index < items.length; index++) {
234
+ const item = items[index];
235
+ const sandbox = buildSandbox({
236
+ items: [item],
237
+ item,
238
+ mode,
239
+ nodeParams,
240
+ });
241
+ const result = await runCode(sandbox);
242
+ const normalized = normalizeResult(result, [item]);
243
+ results.push(...normalized);
244
+ }
245
+ return [results];
246
+ }
247
+ const sandbox = buildSandbox({
248
+ items,
249
+ mode,
250
+ nodeParams,
251
+ });
252
+ const result = await runCode(sandbox);
253
+ const normalized = normalizeResult(result, items);
254
+ return [normalized];
255
+ }
256
+ catch (error) {
257
+ const message = error instanceof Error ? error.message : 'Unknown error';
258
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Code execution failed: ${message}`);
259
+ }
260
+ }
261
+ }
262
+ exports.AimcCode = AimcCode;
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
+ <rect x="6" y="6" width="52" height="52" rx="8" fill="#111827"/>
3
+ <path d="M24 22L14 32L24 42" stroke="#E5E7EB" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M40 22L50 32L40 42" stroke="#E5E7EB" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <path d="M30 20L34 44" stroke="#3B82F6" stroke-width="4" fill="none" stroke-linecap="round"/>
6
+ </svg>
@@ -0,0 +1,5 @@
1
+ import { INodeExecutionData, INodeType, INodeTypeDescription, IExecuteFunctions } from 'n8n-workflow';
2
+ export declare class AimcMedia implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,549 @@
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 () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.AimcMedia = void 0;
40
+ const n8n_workflow_1 = require("n8n-workflow");
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const os = __importStar(require("os"));
44
+ const fluent_ffmpeg_1 = __importDefault(require("fluent-ffmpeg"));
45
+ let ffmpegConfigured = false;
46
+ function resolveOptional(moduleName) {
47
+ try {
48
+ const resolved = require(moduleName);
49
+ if (typeof resolved === 'string') {
50
+ return resolved;
51
+ }
52
+ if (resolved && typeof resolved.path === 'string') {
53
+ return resolved.path;
54
+ }
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ return undefined;
60
+ }
61
+ function configureFfmpeg() {
62
+ if (ffmpegConfigured) {
63
+ return;
64
+ }
65
+ const envFfmpeg = process.env.FFMPEG_PATH;
66
+ const envFfprobe = process.env.FFPROBE_PATH;
67
+ const staticFfmpeg = resolveOptional('ffmpeg-static');
68
+ const staticFfprobe = resolveOptional('ffprobe-static');
69
+ if (envFfmpeg) {
70
+ fluent_ffmpeg_1.default.setFfmpegPath(envFfmpeg);
71
+ }
72
+ else if (staticFfmpeg) {
73
+ fluent_ffmpeg_1.default.setFfmpegPath(staticFfmpeg);
74
+ }
75
+ if (envFfprobe) {
76
+ fluent_ffmpeg_1.default.setFfprobePath(envFfprobe);
77
+ }
78
+ else if (staticFfprobe) {
79
+ fluent_ffmpeg_1.default.setFfprobePath(staticFfprobe);
80
+ }
81
+ ffmpegConfigured = true;
82
+ }
83
+ async function createTempDir() {
84
+ return fs.promises.mkdtemp(path.join(os.tmpdir(), 'aimc-media-'));
85
+ }
86
+ async function writeTempFile(dir, fileName, data) {
87
+ const filePath = path.join(dir, fileName);
88
+ await fs.promises.writeFile(filePath, data);
89
+ return filePath;
90
+ }
91
+ function parseExtraArgs(raw) {
92
+ if (!raw) {
93
+ return [];
94
+ }
95
+ return raw
96
+ .split('\n')
97
+ .map((entry) => entry.trim())
98
+ .filter(Boolean);
99
+ }
100
+ function ensureFileExists(filePath) {
101
+ if (!fs.existsSync(filePath)) {
102
+ throw new Error(`File does not exist: ${filePath}`);
103
+ }
104
+ }
105
+ class AimcMedia {
106
+ constructor() {
107
+ this.description = {
108
+ displayName: 'AIMC Media',
109
+ name: 'aimcMedia',
110
+ icon: 'file:aimc-media.svg',
111
+ group: ['transform'],
112
+ version: 1,
113
+ description: 'Media operations using FFmpeg.',
114
+ defaults: {
115
+ name: 'AIMC Media',
116
+ },
117
+ inputs: ['main'],
118
+ outputs: ['main'],
119
+ properties: [
120
+ {
121
+ displayName: 'Operation',
122
+ name: 'operation',
123
+ type: 'options',
124
+ options: [
125
+ { name: 'Convert / Transcode', value: 'convert' },
126
+ { name: 'Compress', value: 'compress' },
127
+ { name: 'Extract Audio', value: 'extractAudio' },
128
+ { name: 'Merge Video + Audio', value: 'merge' },
129
+ { name: 'Metadata', value: 'metadata' },
130
+ ],
131
+ default: 'convert',
132
+ },
133
+ {
134
+ displayName: 'Input Mode',
135
+ name: 'inputMode',
136
+ type: 'options',
137
+ options: [
138
+ { name: 'Binary', value: 'binary' },
139
+ { name: 'File Path', value: 'filePath' },
140
+ ],
141
+ default: 'binary',
142
+ },
143
+ {
144
+ displayName: 'Input Binary Property',
145
+ name: 'binaryProperty',
146
+ type: 'string',
147
+ default: 'data',
148
+ displayOptions: {
149
+ show: {
150
+ inputMode: ['binary'],
151
+ operation: ['convert', 'compress', 'extractAudio', 'metadata'],
152
+ },
153
+ },
154
+ },
155
+ {
156
+ displayName: 'Input File Path',
157
+ name: 'inputFilePath',
158
+ type: 'string',
159
+ default: '',
160
+ displayOptions: {
161
+ show: {
162
+ inputMode: ['filePath'],
163
+ operation: ['convert', 'compress', 'extractAudio', 'metadata'],
164
+ },
165
+ },
166
+ placeholder: '/path/to/input.mp4',
167
+ },
168
+ {
169
+ displayName: 'Video Binary Property',
170
+ name: 'videoBinaryProperty',
171
+ type: 'string',
172
+ default: 'video',
173
+ displayOptions: {
174
+ show: {
175
+ inputMode: ['binary'],
176
+ operation: ['merge'],
177
+ },
178
+ },
179
+ },
180
+ {
181
+ displayName: 'Audio Binary Property',
182
+ name: 'audioBinaryProperty',
183
+ type: 'string',
184
+ default: 'audio',
185
+ displayOptions: {
186
+ show: {
187
+ inputMode: ['binary'],
188
+ operation: ['merge'],
189
+ },
190
+ },
191
+ },
192
+ {
193
+ displayName: 'Video File Path',
194
+ name: 'videoFilePath',
195
+ type: 'string',
196
+ default: '',
197
+ displayOptions: {
198
+ show: {
199
+ inputMode: ['filePath'],
200
+ operation: ['merge'],
201
+ },
202
+ },
203
+ placeholder: '/path/to/video.mp4',
204
+ },
205
+ {
206
+ displayName: 'Audio File Path',
207
+ name: 'audioFilePath',
208
+ type: 'string',
209
+ default: '',
210
+ displayOptions: {
211
+ show: {
212
+ inputMode: ['filePath'],
213
+ operation: ['merge'],
214
+ },
215
+ },
216
+ placeholder: '/path/to/audio.mp3',
217
+ },
218
+ {
219
+ displayName: 'Output Mode',
220
+ name: 'outputMode',
221
+ type: 'options',
222
+ options: [
223
+ { name: 'Binary', value: 'binary' },
224
+ { name: 'File Path', value: 'filePath' },
225
+ ],
226
+ default: 'binary',
227
+ displayOptions: {
228
+ show: {
229
+ operation: ['convert', 'compress', 'extractAudio', 'merge'],
230
+ },
231
+ },
232
+ },
233
+ {
234
+ displayName: 'Output Binary Property',
235
+ name: 'outputBinaryProperty',
236
+ type: 'string',
237
+ default: 'data',
238
+ displayOptions: {
239
+ show: {
240
+ outputMode: ['binary'],
241
+ operation: ['convert', 'compress', 'extractAudio', 'merge'],
242
+ },
243
+ },
244
+ },
245
+ {
246
+ displayName: 'Output File Path',
247
+ name: 'outputFilePath',
248
+ type: 'string',
249
+ default: '',
250
+ placeholder: '/path/to/output.mp4',
251
+ displayOptions: {
252
+ show: {
253
+ outputMode: ['filePath'],
254
+ operation: ['convert', 'compress', 'extractAudio', 'merge'],
255
+ },
256
+ },
257
+ },
258
+ {
259
+ displayName: 'Output Format',
260
+ name: 'outputFormat',
261
+ type: 'options',
262
+ options: [
263
+ { name: 'MP4', value: 'mp4' },
264
+ { name: 'MOV', value: 'mov' },
265
+ { name: 'WebM', value: 'webm' },
266
+ { name: 'MP3', value: 'mp3' },
267
+ { name: 'WAV', value: 'wav' },
268
+ { name: 'AAC', value: 'aac' },
269
+ { name: 'M4A', value: 'm4a' },
270
+ { name: 'FLAC', value: 'flac' },
271
+ { name: 'OGG', value: 'ogg' },
272
+ { name: 'Custom', value: 'custom' },
273
+ ],
274
+ default: 'mp4',
275
+ displayOptions: {
276
+ show: {
277
+ operation: ['convert', 'compress', 'extractAudio', 'merge'],
278
+ },
279
+ },
280
+ },
281
+ {
282
+ displayName: 'Custom Output Format',
283
+ name: 'customOutputFormat',
284
+ type: 'string',
285
+ default: '',
286
+ placeholder: 'mkv',
287
+ displayOptions: {
288
+ show: {
289
+ outputFormat: ['custom'],
290
+ operation: ['convert', 'compress', 'extractAudio', 'merge'],
291
+ },
292
+ },
293
+ },
294
+ {
295
+ displayName: 'Video Codec',
296
+ name: 'videoCodec',
297
+ type: 'string',
298
+ default: '',
299
+ placeholder: 'libx264',
300
+ displayOptions: {
301
+ show: {
302
+ operation: ['convert'],
303
+ },
304
+ },
305
+ },
306
+ {
307
+ displayName: 'Audio Codec',
308
+ name: 'audioCodec',
309
+ type: 'string',
310
+ default: '',
311
+ placeholder: 'aac',
312
+ displayOptions: {
313
+ show: {
314
+ operation: ['convert'],
315
+ },
316
+ },
317
+ },
318
+ {
319
+ displayName: 'CRF',
320
+ name: 'videoCrf',
321
+ type: 'number',
322
+ default: 23,
323
+ typeOptions: {
324
+ minValue: 0,
325
+ maxValue: 51,
326
+ },
327
+ displayOptions: {
328
+ show: {
329
+ operation: ['compress'],
330
+ },
331
+ },
332
+ },
333
+ {
334
+ displayName: 'Preset',
335
+ name: 'videoPreset',
336
+ type: 'options',
337
+ options: [
338
+ { name: 'Ultrafast', value: 'ultrafast' },
339
+ { name: 'Superfast', value: 'superfast' },
340
+ { name: 'Veryfast', value: 'veryfast' },
341
+ { name: 'Faster', value: 'faster' },
342
+ { name: 'Fast', value: 'fast' },
343
+ { name: 'Medium', value: 'medium' },
344
+ { name: 'Slow', value: 'slow' },
345
+ { name: 'Slower', value: 'slower' },
346
+ { name: 'Veryslow', value: 'veryslow' },
347
+ ],
348
+ default: 'medium',
349
+ displayOptions: {
350
+ show: {
351
+ operation: ['compress'],
352
+ },
353
+ },
354
+ },
355
+ {
356
+ displayName: 'Audio Bitrate',
357
+ name: 'audioBitrate',
358
+ type: 'string',
359
+ default: '128k',
360
+ placeholder: '128k',
361
+ displayOptions: {
362
+ show: {
363
+ operation: ['compress'],
364
+ },
365
+ },
366
+ },
367
+ {
368
+ displayName: 'Additional Output Options',
369
+ name: 'additionalOutputOptions',
370
+ type: 'string',
371
+ default: '',
372
+ description: 'One option per line, e.g. -vf scale=1280:-2',
373
+ typeOptions: {
374
+ rows: 4,
375
+ },
376
+ displayOptions: {
377
+ show: {
378
+ operation: ['convert', 'compress', 'extractAudio', 'merge'],
379
+ },
380
+ },
381
+ },
382
+ ],
383
+ };
384
+ }
385
+ async execute() {
386
+ configureFfmpeg();
387
+ const items = this.getInputData();
388
+ const results = [];
389
+ for (let index = 0; index < items.length; index++) {
390
+ const item = items[index];
391
+ const operation = this.getNodeParameter('operation', index);
392
+ const inputMode = this.getNodeParameter('inputMode', index);
393
+ const outputMode = this.getNodeParameter('outputMode', index, 'binary');
394
+ const outputFormatParam = this.getNodeParameter('outputFormat', index, 'mp4');
395
+ const customOutputFormat = this.getNodeParameter('customOutputFormat', index, '');
396
+ let outputFormat = outputFormatParam === 'custom' ? customOutputFormat : outputFormatParam;
397
+ if (operation === 'extractAudio' && outputFormatParam === 'mp4' && !customOutputFormat) {
398
+ outputFormat = 'mp3';
399
+ }
400
+ if (operation !== 'metadata' && !outputFormat) {
401
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Output format is required.');
402
+ }
403
+ const tempDir = await createTempDir();
404
+ try {
405
+ let inputPath = '';
406
+ let videoPath = '';
407
+ let audioPath = '';
408
+ if (inputMode === 'binary') {
409
+ if (operation === 'merge') {
410
+ const videoProp = this.getNodeParameter('videoBinaryProperty', index);
411
+ const audioProp = this.getNodeParameter('audioBinaryProperty', index);
412
+ const videoBinary = this.helpers.assertBinaryData(index, videoProp);
413
+ const audioBinary = this.helpers.assertBinaryData(index, audioProp);
414
+ const videoBuffer = await this.helpers.getBinaryDataBuffer(index, videoProp);
415
+ const audioBuffer = await this.helpers.getBinaryDataBuffer(index, audioProp);
416
+ videoPath = await writeTempFile(tempDir, `video.${videoBinary.fileExtension || 'bin'}`, videoBuffer);
417
+ audioPath = await writeTempFile(tempDir, `audio.${audioBinary.fileExtension || 'bin'}`, audioBuffer);
418
+ }
419
+ else {
420
+ const binaryProperty = this.getNodeParameter('binaryProperty', index);
421
+ const binary = this.helpers.assertBinaryData(index, binaryProperty);
422
+ const buffer = await this.helpers.getBinaryDataBuffer(index, binaryProperty);
423
+ inputPath = await writeTempFile(tempDir, `input.${binary.fileExtension || 'bin'}`, buffer);
424
+ }
425
+ }
426
+ else {
427
+ if (operation === 'merge') {
428
+ videoPath = this.getNodeParameter('videoFilePath', index);
429
+ audioPath = this.getNodeParameter('audioFilePath', index);
430
+ ensureFileExists(videoPath);
431
+ ensureFileExists(audioPath);
432
+ }
433
+ else {
434
+ inputPath = this.getNodeParameter('inputFilePath', index);
435
+ ensureFileExists(inputPath);
436
+ }
437
+ }
438
+ if (operation === 'metadata') {
439
+ const targetPath = inputMode === 'binary' ? inputPath : inputPath;
440
+ const metadata = await new Promise((resolve, reject) => {
441
+ fluent_ffmpeg_1.default.ffprobe(targetPath, (err, data) => {
442
+ if (err) {
443
+ reject(err);
444
+ return;
445
+ }
446
+ resolve(data);
447
+ });
448
+ });
449
+ results.push({
450
+ json: {
451
+ ...item.json,
452
+ media: {
453
+ operation,
454
+ metadata,
455
+ },
456
+ },
457
+ });
458
+ continue;
459
+ }
460
+ const outputModeValue = outputMode;
461
+ const outputPathParam = this.getNodeParameter('outputFilePath', index, '');
462
+ const outputPath = outputModeValue === 'filePath' && outputPathParam
463
+ ? outputPathParam
464
+ : path.join(tempDir, `output.${outputFormat || 'mp4'}`);
465
+ const extraOptions = parseExtraArgs(this.getNodeParameter('additionalOutputOptions', index, ''));
466
+ let command = (0, fluent_ffmpeg_1.default)();
467
+ if (operation === 'merge') {
468
+ command = command.input(videoPath).input(audioPath);
469
+ command.outputOptions(['-map 0:v:0', '-map 1:a:0']);
470
+ }
471
+ else {
472
+ command = command.input(inputPath);
473
+ }
474
+ if (operation === 'convert') {
475
+ const videoCodec = this.getNodeParameter('videoCodec', index, '');
476
+ const audioCodec = this.getNodeParameter('audioCodec', index, '');
477
+ if (videoCodec) {
478
+ command.videoCodec(videoCodec);
479
+ }
480
+ if (audioCodec) {
481
+ command.audioCodec(audioCodec);
482
+ }
483
+ if (outputFormat) {
484
+ command.format(outputFormat);
485
+ }
486
+ }
487
+ if (operation === 'compress') {
488
+ const crf = this.getNodeParameter('videoCrf', index, 23);
489
+ const preset = this.getNodeParameter('videoPreset', index, 'medium');
490
+ const audioBitrate = this.getNodeParameter('audioBitrate', index, '128k');
491
+ command.videoCodec('libx264');
492
+ command.outputOptions([`-crf ${crf}`, `-preset ${preset}`]);
493
+ command.audioBitrate(audioBitrate);
494
+ if (outputFormat) {
495
+ command.format(outputFormat);
496
+ }
497
+ }
498
+ if (operation === 'extractAudio') {
499
+ command.noVideo();
500
+ if (outputFormat) {
501
+ command.format(outputFormat);
502
+ }
503
+ }
504
+ if (extraOptions.length) {
505
+ command.outputOptions(extraOptions);
506
+ }
507
+ await new Promise((resolve, reject) => {
508
+ command
509
+ .on('error', (err) => reject(err))
510
+ .on('end', () => resolve())
511
+ .save(outputPath);
512
+ });
513
+ let outputItem = {
514
+ json: {
515
+ ...item.json,
516
+ media: {
517
+ operation,
518
+ outputFormat,
519
+ outputPath: outputModeValue === 'filePath' ? outputPath : undefined,
520
+ },
521
+ },
522
+ };
523
+ if (outputModeValue === 'binary') {
524
+ const outputBinaryProperty = this.getNodeParameter('outputBinaryProperty', index, 'data');
525
+ const data = await fs.promises.readFile(outputPath);
526
+ const binaryData = await this.helpers.prepareBinaryData(data, path.basename(outputPath));
527
+ outputItem = {
528
+ ...outputItem,
529
+ binary: {
530
+ [outputBinaryProperty]: binaryData,
531
+ },
532
+ };
533
+ }
534
+ results.push(outputItem);
535
+ }
536
+ catch (error) {
537
+ const message = error instanceof Error ? error.message : 'Unknown error';
538
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Media operation failed: ${message}`);
539
+ }
540
+ finally {
541
+ if (tempDir) {
542
+ await fs.promises.rm(tempDir, { recursive: true, force: true });
543
+ }
544
+ }
545
+ }
546
+ return [results];
547
+ }
548
+ }
549
+ exports.AimcMedia = AimcMedia;
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
+ <rect x="6" y="6" width="52" height="52" rx="8" fill="#0F172A"/>
3
+ <polygon points="26,20 26,44 46,32" fill="#22D3EE"/>
4
+ <path d="M14 48C22 40 30 40 38 48" stroke="#94A3B8" stroke-width="3" fill="none" stroke-linecap="round"/>
5
+ </svg>
package/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Entry point for n8n custom nodes package
2
+ module.exports = {
3
+ description: {
4
+ packageName: '@ferchy/aimc-n8n-toolkit'
5
+ }
6
+ };
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "@ferchy/aimc-n8n-toolkit",
3
+ "version": "0.1.0",
4
+ "description": "AIMC Toolkit nodes for n8n: code execution and media operations.",
5
+ "license": "MIT",
6
+ "author": "Ferchy",
7
+ "engines": {
8
+ "node": ">=18.0.0"
9
+ },
10
+ "main": "index.js",
11
+ "files": [
12
+ "dist/**/*",
13
+ "index.js",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "rimraf dist && tsc && gulp build:icons",
19
+ "dev": "tsc --watch"
20
+ },
21
+ "keywords": [
22
+ "n8n-community-node-package",
23
+ "n8n",
24
+ "aimc",
25
+ "toolkit"
26
+ ],
27
+ "n8n": {
28
+ "n8nNodesApiVersion": 1,
29
+ "nodes": [
30
+ "dist/nodes/AimcCode/AimcCode.node.js",
31
+ "dist/nodes/AimcMedia/AimcMedia.node.js"
32
+ ]
33
+ },
34
+ "dependencies": {
35
+ "@distube/ytdl-core": "^4.15.1",
36
+ "ajv": "^8.17.1",
37
+ "axios": "^1.6.0",
38
+ "bytes": "^3.1.2",
39
+ "cheerio": "^1.0.0-rc.12",
40
+ "compromise": "^14.14.3",
41
+ "cron-parser": "^5.3.0",
42
+ "date-fns": "^4.1.0",
43
+ "date-fns-tz": "^3.2.0",
44
+ "dayjs": "^1.11.10",
45
+ "fast-xml-parser": "^5.2.5",
46
+ "fluent-ffmpeg": "^2.1.2",
47
+ "form-data": "^4.0.4",
48
+ "franc-min": "^6.2.0",
49
+ "fuse.js": "^7.1.0",
50
+ "handlebars": "^4.7.8",
51
+ "html-to-text": "^9.0.5",
52
+ "http-proxy-agent": "^7.0.2",
53
+ "iban": "^0.0.14",
54
+ "ini": "^5.0.0",
55
+ "joi": "^17.11.0",
56
+ "json-diff-ts": "^4.8.2",
57
+ "knex": "^3.1.0",
58
+ "libphonenumber-js": "^1.12.10",
59
+ "lodash": "^4.17.21",
60
+ "marked": "^15.0.6",
61
+ "moment-timezone": "^0.6.0",
62
+ "ms": "^2.1.3",
63
+ "nanoid": "^5.1.5",
64
+ "p-retry": "^5.1.2",
65
+ "papaparse": "^5.5.3",
66
+ "pluralize": "^8.0.0",
67
+ "protobufjs": "^7.5.4",
68
+ "qrcode": "^1.5.4",
69
+ "qs": "^6.14.0",
70
+ "slug": "^11.0.0",
71
+ "socks-proxy-agent": "^8.0.5",
72
+ "string-similarity": "^4.0.4",
73
+ "toml": "^3.0.0",
74
+ "uuid": "^11.1.0",
75
+ "validator": "^13.11.0",
76
+ "xml2js": "^0.6.2",
77
+ "yaml": "^2.8.1",
78
+ "yup": "^1.7.0",
79
+ "zod": "^3.25.76"
80
+ },
81
+ "optionalDependencies": {
82
+ "bufferutil": "^4.0.8",
83
+ "ffmpeg-static": "^5.3.0",
84
+ "ffprobe-static": "^3.1.0",
85
+ "utf-8-validate": "^6.0.4"
86
+ },
87
+ "devDependencies": {
88
+ "@types/fluent-ffmpeg": "^2.1.28",
89
+ "@types/node": "^20.11.30",
90
+ "gulp": "^5.0.0",
91
+ "n8n-workflow": "^1.70.0",
92
+ "rimraf": "^6.0.1",
93
+ "typescript": "^5.7.2"
94
+ },
95
+ "peerDependencies": {
96
+ "n8n-workflow": "*"
97
+ }
98
+ }