diff --git a/nodes/WizPixelFlow/WizPixelFlow.node.ts b/nodes/WizPixelFlow/WizPixelFlow.node.ts index 3ed8c0e..60c4279 100644 --- a/nodes/WizPixelFlow/WizPixelFlow.node.ts +++ b/nodes/WizPixelFlow/WizPixelFlow.node.ts @@ -32,6 +32,8 @@ const OUTPUT_FORMATS = [ { name: 'Twitter Square 1080x1080', value: 'twitter_square_1080x1080' }, ]; +const TERMINAL_STATUSES = ['done', 'failed', 'complete', 'error']; + export class WizPixelFlow implements INodeType { description: INodeTypeDescription = { displayName: 'WizPixel Flow', @@ -53,30 +55,106 @@ export class WizPixelFlow implements INodeType { noDataExpression: true, options: [ { name: 'Render', value: 'render', description: 'Submit a render job' }, - { name: 'Get Job', value: 'getJob', description: 'Get status of a render job' }, - { name: 'List Templates', value: 'listTemplates', description: 'List available templates' }, + { name: 'Get Job', value: 'getJob', description: 'Get status of a render job' }, + { name: 'List Templates', value: 'listTemplates', description: 'List available templates' }, ], default: 'render', }, - { displayName: 'Template ID', name: 'templateId', type: 'string', default: '', required: true, displayOptions: { show: { operation: ['render'] } }, description: 'The ID of the Flow template to render' }, - { displayName: 'Output Formats', name: 'outputFormats', type: 'multiOptions', options: OUTPUT_FORMATS, default: ['leaderboard_728x90', 'medium_rectangle_300x250'], required: true, displayOptions: { show: { operation: ['render'] } } }, - { displayName: 'Locales', name: 'locales', type: 'string', default: 'en', displayOptions: { show: { operation: ['render'] } }, description: 'Comma-separated locale codes, e.g. en,it,fr' }, - { displayName: 'Variables (JSON)', name: 'variables', type: 'json', default: '{}', displayOptions: { show: { operation: ['render'] } }, description: 'Key/value pairs for {{variable}} substitution' }, - { displayName: 'Include Video (MP4)', name: 'includeVideo', type: 'boolean', default: false, displayOptions: { show: { operation: ['render'] } } }, - { displayName: 'Wait for Completion', name: 'waitForCompletion', type: 'boolean', default: true, displayOptions: { show: { operation: ['render'] } }, description: 'Poll until done and return ZIP download URL' }, - { displayName: 'Poll Interval (seconds)', name: 'pollInterval', type: 'number', default: 5, displayOptions: { show: { operation: ['render'], waitForCompletion: [true] } } }, - { displayName: 'Max Wait (seconds)', name: 'maxWait', type: 'number', default: 300, displayOptions: { show: { operation: ['render'], waitForCompletion: [true] } } }, - { displayName: 'Job ID', name: 'jobId', type: 'string', default: '', required: true, displayOptions: { show: { operation: ['getJob'] } } }, - { displayName: 'Limit', name: 'limit', type: 'number', default: 50, typeOptions: { minValue: 1, maxValue: 200 }, displayOptions: { show: { operation: ['listTemplates'] } } }, + { + displayName: 'Template ID', + name: 'templateId', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['render'] } }, + description: 'UID of the Flow template to render', + }, + { + displayName: 'Job Type', + name: 'jobType', + type: 'options', + options: [ + { name: 'Banner (HTML5)', value: 'banner' }, + { name: 'Video (MP4)', value: 'video' }, + ], + default: 'banner', + required: true, + displayOptions: { show: { operation: ['render'] } }, + description: 'Render HTML5 banners or MP4 videos (different credit pools)', + }, + { + displayName: 'Output Formats', + name: 'outputFormats', + type: 'multiOptions', + options: OUTPUT_FORMATS, + default: ['leaderboard_728x90', 'medium_rectangle_300x250'], + required: true, + displayOptions: { show: { operation: ['render'] } }, + }, + { + displayName: 'Locales', + name: 'locales', + type: 'string', + default: 'en', + displayOptions: { show: { operation: ['render'] } }, + description: 'Comma-separated locale codes, e.g. en,it,fr', + }, + { + displayName: 'Variables (JSON)', + name: 'variables', + type: 'json', + default: '{}', + displayOptions: { show: { operation: ['render'] } }, + description: 'Key/value pairs for {{variable}} substitution', + }, + { + displayName: 'Wait for Completion', + name: 'waitForCompletion', + type: 'boolean', + default: true, + displayOptions: { show: { operation: ['render'] } }, + description: 'Poll until done and return ZIP download URL', + }, + { + displayName: 'Poll Interval (seconds)', + name: 'pollInterval', + type: 'number', + default: 5, + displayOptions: { show: { operation: ['render'], waitForCompletion: [true] } }, + }, + { + displayName: 'Max Wait (seconds)', + name: 'maxWait', + type: 'number', + default: 300, + displayOptions: { show: { operation: ['render'], waitForCompletion: [true] } }, + }, + { + displayName: 'Job ID', + name: 'jobId', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['getJob'] } }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + typeOptions: { minValue: 1, maxValue: 200 }, + displayOptions: { show: { operation: ['listTemplates'] } }, + }, ], }; async execute(this: IExecuteFunctions): Promise { const credentials = await this.getCredentials('wizPixelFlowApi'); - const baseUrl = (credentials.baseUrl as string).replace(/\/$/, ''); - const apiKey = credentials.apiKey as string; - const headers = { 'X-WPF-API-Key': apiKey, 'Content-Type': 'application/json' }; - const items = this.getInputData(); + const baseUrl = (credentials.baseUrl as string).replace(/\/$/, ''); + const apiKey = credentials.apiKey as string; + const headers = { 'X-WPF-API-Key': apiKey, 'Content-Type': 'application/json' }; + + const items = this.getInputData(); const returnData: INodeExecutionData[] = []; const parse = (r: unknown): IDataObject => @@ -84,46 +162,98 @@ export class WizPixelFlow implements INodeType { for (let i = 0; i < items.length; i++) { const operation = this.getNodeParameter('operation', i) as string; + try { if (operation === 'render') { const templateId = this.getNodeParameter('templateId', i) as string; + const jobType = this.getNodeParameter('jobType', i) as string; const outputFormats = this.getNodeParameter('outputFormats', i) as string[]; const localesRaw = this.getNodeParameter('locales', i) as string; const variablesRaw = this.getNodeParameter('variables', i) as string; - const includeVideo = this.getNodeParameter('includeVideo', i) as boolean; const waitForCompletion = this.getNodeParameter('waitForCompletion', i) as boolean; const pollInterval = this.getNodeParameter('pollInterval', i) as number; const maxWait = this.getNodeParameter('maxWait', i) as number; + const locales = localesRaw.split(',').map((l) => l.trim()).filter(Boolean); const variables = typeof variablesRaw === 'string' ? JSON.parse(variablesRaw) : variablesRaw; - const body = { template_id: templateId, formats: outputFormats, locales, variables, include_video: includeVideo }; - const submitted = parse(await this.helpers.httpRequest({ method: 'POST', url: `${baseUrl}/render`, headers, body: JSON.stringify(body) })); - const jobId = submitted.job_id as string; - if (!waitForCompletion) { returnData.push({ json: submitted }); continue; } + + const body = { + template_id: templateId, + job_type: jobType, + formats: outputFormats, + locales, + variables, + }; + + const submitted = parse( + await this.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/render`, + headers, + body: JSON.stringify(body), + }), + ); + + if (!waitForCompletion) { + returnData.push({ json: submitted }); + continue; + } + + const jobId = submitted.job_id as string; const deadline = Date.now() + maxWait * 1000; let jobData: IDataObject = {}; + while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, pollInterval * 1000)); - jobData = parse(await this.helpers.httpRequest({ method: 'GET', url: `${baseUrl}/jobs/${jobId}`, headers })); + jobData = parse( + await this.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/jobs.php?id=${jobId}`, + headers, + }), + ); const job = jobData.job as IDataObject | undefined; - if (job?.status === 'done' || job?.status === 'failed') break; + if (job?.status && TERMINAL_STATUSES.includes(job.status as string)) break; } + returnData.push({ json: jobData }); + } else if (operation === 'getJob') { const jobId = this.getNodeParameter('jobId', i) as string; - returnData.push({ json: parse(await this.helpers.httpRequest({ method: 'GET', url: `${baseUrl}/jobs/${jobId}`, headers })) }); + returnData.push({ + json: parse( + await this.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/jobs.php?id=${jobId}`, + headers, + }), + ), + }); + } else if (operation === 'listTemplates') { const limit = this.getNodeParameter('limit', i) as number; - returnData.push({ json: parse(await this.helpers.httpRequest({ method: 'GET', url: `${baseUrl}/templates?limit=${limit}`, headers })) }); + returnData.push({ + json: parse( + await this.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/templates?limit=${limit}`, + headers, + }), + ), + }); } } catch (error) { if (this.continueOnFail()) { - returnData.push({ json: { error: (error as Error).message } as IDataObject, pairedItem: i }); + returnData.push({ + json: { error: (error as Error).message } as IDataObject, + pairedItem: i, + }); } else { throw new NodeOperationError(this.getNode(), error as Error, { itemIndex: i }); } } } + return [returnData]; } }