UNPKG

clip-magic

Version:

n8n node for ClipMagic API - Video and audio processing tools

1,102 lines (1,101 loc) 46.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClipMagic = void 0; const n8n_workflow_1 = require("n8n-workflow"); class ClipMagic { constructor() { this.description = { displayName: 'ClipMagic', name: 'clipMagic', icon: 'file:clipmagic.svg', group: ['transform'], version: 1, subtitle: '={{$parameter["operation"]}}', description: 'Interact with ClipMagic API for video and audio processing', defaults: { name: 'ClipMagic', }, inputs: ["main"], outputs: ["main"], credentials: [ { name: 'clipMagicApi', required: true, }, ], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Convert Media', value: 'convert', description: 'Convert video/audio to different formats', action: 'Convert media', }, { name: 'Trim Media', value: 'trim', description: 'Trim segments from video/audio', action: 'Trim media', }, { name: 'Compress Video', value: 'compress', description: 'Compress video to reduce file size', action: 'Compress video', }, { name: 'Burn Captions', value: 'burnCaptions', description: 'Burn subtitles into video', action: 'Burn captions', }, { name: 'Remove Silence', value: 'remove_silence', description: 'Detect and remove silent sections from video/audio', action: 'Remove silence from media', }, { name: 'Stitch Videos', value: 'stitch', description: 'Stitch multiple videos sequentially', action: 'Stitch multiple videos', }, { name: 'Generate AI Clips', value: 'generateClips', description: 'Generate AI clips with karaoke subtitles', action: 'Generate AI clips', }, ], default: 'convert', }, { displayName: 'Media URL', name: 'url', type: 'string', required: true, default: '', description: 'Direct file URL or Google Drive share link', displayOptions: { show: { operation: ['convert'], }, }, }, { displayName: 'Output Format', name: 'outputFormat', type: 'options', options: [ { name: 'MP3', value: 'mp3' }, { name: 'WAV', value: 'wav' }, { name: 'AAC', value: 'aac' }, { name: 'MP4', value: 'mp4' }, { name: 'MOV', value: 'mov' }, ], default: 'mp3', description: 'Target container format', displayOptions: { show: { operation: ['convert'], }, }, }, { displayName: 'Resolution', name: 'resolution', type: 'options', options: [ { name: 'Original', value: '' }, { name: '360p', value: '360p' }, { name: '480p', value: '480p' }, { name: '720p', value: '720p' }, { name: '1080p', value: '1080p' }, ], default: '', description: 'Scale video to specific height (ignored for audio)', displayOptions: { show: { operation: ['convert'], }, }, }, { displayName: 'Bitrate (kbps)', name: 'bitrateKbps', type: 'number', default: 0, description: 'Target video bitrate in kbps (max 3000, 0 for automatic)', displayOptions: { show: { operation: ['convert'], }, }, }, { displayName: 'Media URL', name: 'url', type: 'string', required: true, default: '', description: 'Input media URL to trim', displayOptions: { show: { operation: ['trim'], }, }, }, { displayName: 'Trim Mode', name: 'trimMode', type: 'options', options: [ { name: 'Single Segment', value: 'single' }, { name: 'Multiple Segments', value: 'multiple' }, ], default: 'single', displayOptions: { show: { operation: ['trim'], }, }, }, { displayName: 'Start Time', name: 'start', type: 'string', default: '00:00:00', description: 'Start time in HH:MM:SS format or seconds', displayOptions: { show: { operation: ['trim'], trimMode: ['single'], }, }, }, { displayName: 'End Mode', name: 'endMode', type: 'options', options: [ { name: 'End Time', value: 'end' }, { name: 'Duration', value: 'duration' }, ], default: 'duration', displayOptions: { show: { operation: ['trim'], trimMode: ['single'], }, }, }, { displayName: 'End Time', name: 'end', type: 'string', default: '00:00:10', description: 'End time in HH:MM:SS format', displayOptions: { show: { operation: ['trim'], trimMode: ['single'], endMode: ['end'], }, }, }, { displayName: 'Duration', name: 'duration', type: 'string', default: '10', description: 'Duration in seconds or HH:MM:SS format', displayOptions: { show: { operation: ['trim'], trimMode: ['single'], endMode: ['duration'], }, }, }, { displayName: 'Segments', name: 'segments', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, description: 'Multiple segments to trim', displayOptions: { show: { operation: ['trim'], trimMode: ['multiple'], }, }, options: [ { name: 'segment', displayName: 'Segment', values: [ { displayName: 'Start', name: 'start', type: 'string', default: '00:00:00', description: 'Start time in HH:MM:SS format or seconds', }, { displayName: 'End Mode', name: 'endMode', type: 'options', options: [ { name: 'End Time', value: 'end' }, { name: 'Duration', value: 'duration' }, ], default: 'duration', }, { displayName: 'End Time', name: 'end', type: 'string', default: '00:00:10', description: 'End time in HH:MM:SS format', displayOptions: { show: { endMode: ['end'], }, }, }, { displayName: 'Duration', name: 'duration', type: 'string', default: '10', description: 'Duration in seconds or HH:MM:SS format', displayOptions: { show: { endMode: ['duration'], }, }, }, ], }, ], }, { displayName: 'Output Format', name: 'outputFormat', type: 'options', options: [ { name: 'MP4', value: 'mp4' }, { name: 'GIF', value: 'gif' }, { name: 'MOV', value: 'mov' }, { name: 'MP3', value: 'mp3' }, { name: 'WAV', value: 'wav' }, ], default: 'mp4', description: 'Target format for trimmed clips', displayOptions: { show: { operation: ['trim'], }, }, }, { displayName: 'Output Filename', name: 'outputFilename', type: 'string', default: '', description: 'Custom filename for the output (optional)', displayOptions: { show: { operation: ['trim'], }, }, }, { displayName: 'Video URL', name: 'url', type: 'string', required: true, default: '', description: 'URL of the input video', displayOptions: { show: { operation: ['compress'], }, }, }, { displayName: 'Preset', name: 'preset', type: 'options', options: [ { name: 'Ultra Fast', value: 'ultrafast' }, { name: 'Super Fast', value: 'superfast' }, { name: 'Very Fast', value: 'veryfast' }, { name: 'Faster', value: 'faster' }, { name: 'Fast', value: 'fast' }, { name: 'Medium', value: 'medium' }, { name: 'Slow', value: 'slow' }, { name: 'Slower', value: 'slower' }, { name: 'Very Slow', value: 'veryslow' }, { name: 'Placebo', value: 'placebo' }, ], default: 'medium', description: 'Encoding speed preset', displayOptions: { show: { operation: ['compress'], }, }, }, { displayName: 'CRF (Quality)', name: 'crf', type: 'number', default: 23, description: 'Constant Rate Factor (18=nearly lossless, 23=default, 28=low quality)', displayOptions: { show: { operation: ['compress'], }, }, }, { displayName: 'Output Format', name: 'outputFormat', type: 'options', options: [ { name: 'MP4', value: 'mp4' }, { name: 'MOV', value: 'mov' }, { name: 'AVI', value: 'avi' }, ], default: 'mp4', description: 'Output container format', displayOptions: { show: { operation: ['compress'], }, }, }, { displayName: 'Video URL', name: 'url', type: 'string', required: true, default: '', description: 'URL of the input video', displayOptions: { show: { operation: ['burnCaptions'], }, }, }, { displayName: 'Subtitle URL', name: 'subtitleUrl', type: 'string', required: true, default: '', description: 'URL of the .srt or .ass subtitle file', displayOptions: { show: { operation: ['burnCaptions'], }, }, }, { displayName: 'Font Size', name: 'fontSize', type: 'number', default: 24, description: 'Font size in pixels', displayOptions: { show: { operation: ['burnCaptions'], }, }, }, { displayName: 'Font Color', name: 'fontColor', type: 'color', default: '#FFFFFF', description: 'Primary color of the subtitle text', displayOptions: { show: { operation: ['burnCaptions'], }, }, }, { displayName: 'Font Name', name: 'fontName', type: 'string', default: '', description: 'Font name (system font or URL to font file)', displayOptions: { show: { operation: ['burnCaptions'], }, }, }, { displayName: 'Outline Width', name: 'outline', type: 'number', default: 0, description: 'Width of text outline (0 for no outline)', displayOptions: { show: { operation: ['burnCaptions'], }, }, }, { displayName: 'Boxed Subtitles', name: 'boxed', type: 'boolean', default: false, description: 'Whether subtitles should have a background box', displayOptions: { show: { operation: ['burnCaptions'], }, }, }, { displayName: 'Resolution', name: 'resolution', type: 'options', options: [ { name: '480p', value: '480p' }, { name: '720p', value: '720p' }, { name: '1080p', value: '1080p' }, { name: '1440p', value: '1440p' }, { name: '4K', value: '4k' }, ], default: '720p', description: 'Resolution of the output video', displayOptions: { show: { operation: ['burnCaptions'], }, }, }, { displayName: 'Orientation', name: 'orientation', type: 'options', options: [ { name: 'Landscape', value: 'landscape' }, { name: 'Portrait', value: 'portrait' }, { name: 'Square', value: 'square' }, ], default: 'landscape', description: 'Orientation of the output video', displayOptions: { show: { operation: ['burnCaptions'], }, }, }, { displayName: 'Output Filename', name: 'outputFilename', type: 'string', default: '', description: 'Desired filename for the output file (without extension)', displayOptions: { show: { operation: ['burnCaptions'], }, }, }, { displayName: 'Video URL', name: 'url', type: 'string', required: true, default: '', description: 'Direct file URL or Google Drive link to the video file', displayOptions: { show: { operation: ['remove_silence'], }, }, }, { displayName: 'Noise Threshold', name: 'noiseThreshold', type: 'string', default: '-45dB', description: 'Noise threshold passed to FFmpeg silencedetect filter, e.g. -45dB', displayOptions: { show: { operation: ['remove_silence'], }, }, }, { displayName: 'Silence Duration (s)', name: 'duration', type: 'number', default: 0.35, description: 'Minimum duration (in seconds) that a section must stay below the threshold to be considered silence', displayOptions: { show: { operation: ['remove_silence'], }, }, }, { displayName: 'Encoding Preset', name: 'preset', type: 'options', options: [ { name: 'Ultra Fast', value: 'ultrafast' }, { name: 'Super Fast', value: 'superfast' }, { name: 'Very Fast', value: 'veryfast' }, { name: 'Faster', value: 'faster' }, { name: 'Fast', value: 'fast' }, { name: 'Medium', value: 'medium' }, { name: 'Slow', value: 'slow' }, { name: 'Slower', value: 'slower' }, { name: 'Very Slow', value: 'veryslow' }, ], default: 'fast', description: 'Encoding speed/quality trade-off preset', displayOptions: { show: { operation: ['remove_silence'], }, }, }, { displayName: 'CRF (Quality)', name: 'crf', type: 'number', default: 20, description: 'Quality setting for libx264 (0-51, lower = better quality)', displayOptions: { show: { operation: ['remove_silence'], }, }, }, { displayName: 'Output Format', name: 'outputFormat', type: 'options', options: [ { name: 'MP4', value: 'mp4' }, { name: 'MOV', value: 'mov' }, { name: 'MP3', value: 'mp3' }, { name: 'WAV', value: 'wav' }, ], default: 'mp4', description: 'The container format for the output video/audio', displayOptions: { show: { operation: ['remove_silence'], }, }, }, { displayName: 'Output Filename', name: 'outputFilename', type: 'string', default: '', description: 'Desired filename for the output, without extension', displayOptions: { show: { operation: ['remove_silence'], }, }, }, { displayName: 'Video URLs', name: 'urls', type: 'string', required: true, default: '', description: 'JSON array or comma-separated list of video URLs to stitch in order', displayOptions: { show: { operation: ['stitch'], }, }, }, { displayName: 'Output Format', name: 'outputFormat', type: 'options', options: [ { name: 'MP4', value: 'mp4' }, { name: 'MOV', value: 'mov' }, ], default: 'mp4', description: 'Output container/codec', displayOptions: { show: { operation: ['stitch'], }, }, }, { displayName: 'Output Filename', name: 'outputFilename', type: 'string', default: '', description: 'Desired filename for the output video (without extension)', displayOptions: { show: { operation: ['stitch'], }, }, }, { displayName: 'Video URL', name: 'url', type: 'string', required: true, default: '', description: 'Source video URL', displayOptions: { show: { operation: ['generateClips'], }, }, }, { displayName: 'Generate Subtitles', name: 'subtitles', type: 'boolean', default: true, description: 'Whether to generate karaoke style subtitles', displayOptions: { show: { operation: ['generateClips'], }, }, }, { displayName: 'Highlight Color', name: 'highlightColor', type: 'color', default: '#FFC000', description: 'Color of the highlighted text in karaoke mode', displayOptions: { show: { operation: ['generateClips'], subtitles: [true], }, }, }, { displayName: 'Primary Color', name: 'primaryColor', type: 'color', default: '#FFFFFF', description: 'Primary color of the subtitle text', displayOptions: { show: { operation: ['generateClips'], subtitles: [true], }, }, }, { displayName: 'Font Name', name: 'fontName', type: 'string', default: '', description: 'Font name to use for subtitles', displayOptions: { show: { operation: ['generateClips'], subtitles: [true], }, }, }, { displayName: 'Font Size', name: 'fontSize', type: 'number', default: 32, description: 'Size of the subtitle text', displayOptions: { show: { operation: ['generateClips'], subtitles: [true], }, }, }, { displayName: 'Font Weight', name: 'fontWeight', type: 'options', options: [ { name: 'Normal', value: 'normal' }, { name: 'Bold', value: 'bold' }, ], default: 'normal', description: 'Weight of the subtitle text', displayOptions: { show: { operation: ['generateClips'], subtitles: [true], }, }, }, { displayName: 'Shadow Color', name: 'shadowColor', type: 'color', default: '#000000', description: 'Color of text shadow', displayOptions: { show: { operation: ['generateClips'], subtitles: [true], }, }, }, { displayName: 'Shadow Intensity', name: 'shadowIntensity', type: 'number', default: 1, description: 'Intensity of the text shadow (0 for no shadow)', displayOptions: { show: { operation: ['generateClips'], subtitles: [true], }, }, }, { displayName: 'Display Mode', name: 'displayMode', type: 'options', options: [ { name: 'Word by Word', value: 'word' }, { name: 'Full Text', value: 'text' }, ], default: 'word', description: 'How to display subtitles', displayOptions: { show: { operation: ['generateClips'], subtitles: [true], }, }, }, { displayName: 'Subtitle Position', name: 'subtitlePosition', type: 'options', options: [ { name: 'Top', value: 'top' }, { name: 'Middle', value: 'middle' }, { name: 'Bottom', value: 'bottom' }, ], default: 'bottom', description: 'Position of subtitles in the frame', displayOptions: { show: { operation: ['generateClips'], subtitles: [true], }, }, }, ], }; } async execute() { var _a, _b, _c, _d; const items = this.getInputData(); const returnData = []; const credentials = await this.getCredentials('clipMagicApi'); const baseUrl = credentials.baseUrl; for (let i = 0; i < items.length; i++) { try { const operation = this.getNodeParameter('operation', i); let endpoint = ''; let method = 'GET'; let body = {}; let qs = {}; switch (operation) { case 'convert': endpoint = '/convert'; method = 'GET'; qs = { url: this.getNodeParameter('url', i), output_format: this.getNodeParameter('outputFormat', i), }; const resolution = this.getNodeParameter('resolution', i); if (resolution) { qs.resolution = resolution; } const bitrate = this.getNodeParameter('bitrateKbps', i); if (bitrate > 0) { qs.bitrate_kbps = bitrate; } break; case 'trim': endpoint = '/operations/trim'; method = 'POST'; body = { url: this.getNodeParameter('url', i), output_format: this.getNodeParameter('outputFormat', i), }; const outputFilename = this.getNodeParameter('outputFilename', i); if (outputFilename) { body.output_filename = outputFilename; } const trimMode = this.getNodeParameter('trimMode', i); if (trimMode === 'single') { body.start = this.getNodeParameter('start', i); const endMode = this.getNodeParameter('endMode', i); if (endMode === 'end') { body.end = this.getNodeParameter('end', i); } else { body.duration = this.getNodeParameter('duration', i); } } else { const segments = this.getNodeParameter('segments', i); body.segments = ((_a = segments.segment) === null || _a === void 0 ? void 0 : _a.map((seg) => { const segment = { start: seg.start }; if (seg.endMode === 'end') { segment.end = seg.end; } else { segment.duration = seg.duration; } return segment; })) || []; } break; case 'compress': endpoint = '/operations/compress'; method = 'POST'; body = { url: this.getNodeParameter('url', i), preset: this.getNodeParameter('preset', i), crf: this.getNodeParameter('crf', i), output_format: this.getNodeParameter('outputFormat', i), }; break; case 'burnCaptions': endpoint = '/operations/burn-captions'; method = 'POST'; body = { url: this.getNodeParameter('url', i), subtitle_url: this.getNodeParameter('subtitleUrl', i), font_size: this.getNodeParameter('fontSize', i), primary_color: this.getNodeParameter('fontColor', i), subtitle_position: this.getNodeParameter('subtitlePosition', i), output_format: this.getNodeParameter('outputFormat', i), }; const captionFontName = this.getNodeParameter('fontName', i, ''); if (captionFontName) { body.font_name = captionFontName; } const captionResolution = this.getNodeParameter('resolution', i, ''); if (captionResolution) { body.resolution = captionResolution; } const captionOrientation = this.getNodeParameter('orientation', i, ''); if (captionOrientation) { body.orientation = captionOrientation; } const outline = this.getNodeParameter('outline', i, 0); if (outline > 0) { body.outline = outline; } const boxed = this.getNodeParameter('boxed', i, false); body.boxed = boxed; const captionOutputFilename = this.getNodeParameter('outputFilename', i, ''); if (captionOutputFilename) { body.output_filename = captionOutputFilename; } break; case 'remove_silence': endpoint = '/operations/remove-silence'; method = 'POST'; body = { url: this.getNodeParameter('url', i), noise_threshold: this.getNodeParameter('noiseThreshold', i), duration: this.getNodeParameter('duration', i), preset: this.getNodeParameter('preset', i), crf: this.getNodeParameter('crf', i), output_format: this.getNodeParameter('outputFormat', i), }; const silenceOutputFilename = this.getNodeParameter('outputFilename', i, ''); if (silenceOutputFilename) { body.output_filename = silenceOutputFilename; } break; case 'stitch': endpoint = '/operations/stitch'; method = 'POST'; const urlsParam = this.getNodeParameter('urls', i); let urls; try { urls = JSON.parse(urlsParam); if (!Array.isArray(urls)) throw new Error(); } catch (_) { urls = urlsParam.split(',').map((u) => u.trim()).filter((u) => u); } body = { urls, output_format: this.getNodeParameter('outputFormat', i), }; const stitchFilename = this.getNodeParameter('outputFilename', i, ''); if (stitchFilename) { body.output_filename = stitchFilename; } break; case 'generateClips': endpoint = '/workflows/generate-clips'; method = 'POST'; body = { url: this.getNodeParameter('url', i), subtitles: this.getNodeParameter('subtitles', i), }; try { body.resolution = this.getNodeParameter('resolution', i); } catch (e) { } try { body.orientation = this.getNodeParameter('orientation', i); } catch (e) { } const subtitles = this.getNodeParameter('subtitles', i); if (subtitles) { try { body.highlight_color = this.getNodeParameter('highlightColor', i); } catch (e) { } try { body.primary_color = this.getNodeParameter('primaryColor', i); } catch (e) { } try { body.font_name = this.getNodeParameter('fontName', i); } catch (e) { } try { body.font_size = this.getNodeParameter('fontSize', i); } catch (e) { } try { body.font_weight = this.getNodeParameter('fontWeight', i) === 'bold' ? 1 : 0; } catch (e) { } try { body.shadow_color = this.getNodeParameter('shadowColor', i); } catch (e) { } try { body.shadow_intensity = this.getNodeParameter('shadowIntensity', i); } catch (e) { } try { body.display_mode = this.getNodeParameter('displayMode', i); } catch (e) { } try { body.subtitle_position = this.getNodeParameter('subtitlePosition', i); } catch (e) { } } break; default: throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, { itemIndex: i, }); } const options = { headers: { 'Content-Type': 'application/json', }, method, body: method === 'POST' ? body : undefined, qs: method === 'GET' ? qs : undefined, url: `${baseUrl}${endpoint}`, encoding: 'binary', resolveWithFullResponse: true, }; const response = await this.helpers.requestWithAuthentication.call(this, 'clipMagicApi', options); if (((_b = response.headers['content-type']) === null || _b === void 0 ? void 0 : _b.includes('application/')) || ((_c = response.headers['content-type']) === null || _c === void 0 ? void 0 : _c.includes('video/')) || ((_d = response.headers['content-type']) === null || _d === void 0 ? void 0 : _d.includes('audio/'))) { const contentDisposition = response.headers['content-disposition']; let filename = 'output'; if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/); if (filenameMatch) { filename = filenameMatch[1]; } } const binaryData = await this.helpers.prepareBinaryData(response.body, filename, response.headers['content-type']); returnData.push({ json: { success: true, operation, filename, contentType: response.headers['content-type'], }, binary: { data: binaryData, }, }); } else { let jsonData; try { jsonData = typeof response.body === 'string' ? JSON.parse(response.body) : response.body; } catch (error) { jsonData = { raw: response.body }; } returnData.push({ json: { success: true, operation, ...jsonData, }, }); } } catch (error) { if (this.continueOnFail()) { returnData.push({ json: { success: false, error: error instanceof Error ? error.message : String(error), }, }); } else { throw error; } } } return [returnData]; } } exports.ClipMagic = ClipMagic;