import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, IDataObject, INodeProperties, } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; import { URL } from 'url'; import { pexelsApiRequest, pexelsApiRequestAllItems } from './GenericFunctions'; const photoOperations: INodeProperties = { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, displayOptions: { show: { resource: ['photo'], }, }, options: [ { name: 'Search', value: 'search', action: 'Search photos', description: 'Find photos that match a query', }, { name: 'Curated', value: 'curated', action: 'List curated photos', description: 'Browse Pexels curated photos', }, { name: 'Get', value: 'get', action: 'Get a photo', description: 'Retrieve a single photo by ID', }, ], default: 'search', }; const photoFields: INodeProperties[] = [ { displayName: 'Query', name: 'query', type: 'string', required: true, default: '', displayOptions: { show: { resource: ['photo'], operation: ['search'], }, }, description: 'Text to search for in photos (tags, title, description)', }, { displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false, displayOptions: { show: { resource: ['photo'], operation: ['search'], }, }, }, { displayName: 'Limit', name: 'limit', type: 'number', typeOptions: { minValue: 1, maxValue: 5000, }, default: 50, displayOptions: { show: { resource: ['photo'], operation: ['search'], returnAll: [false], }, }, description: 'Max number of photos to return (API allows up to 80 per request)', }, { displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, displayOptions: { show: { resource: ['photo'], operation: ['search'], }, }, options: [ { displayName: 'Color', name: 'color', type: 'options', default: '', options: [ { name: 'Black', value: 'black' }, { name: 'Blue', value: 'blue' }, { name: 'Brown', value: 'brown' }, { name: 'Gray', value: 'gray' }, { name: 'Green', value: 'green' }, { name: 'Orange', value: 'orange' }, { name: 'Pink', value: 'pink' }, { name: 'Red', value: 'red' }, { name: 'Turquoise', value: 'turquoise' }, { name: 'Violet', value: 'violet' }, { name: 'White', value: 'white' }, { name: 'Yellow', value: 'yellow' }, ], description: 'Limit results to a specific dominant color', }, { displayName: 'Locale', name: 'locale', type: 'string', default: '', description: 'Locale code such as en-US, es-ES, fr-FR. See Pexels docs for supported locales.', }, { displayName: 'Orientation', name: 'orientation', type: 'options', default: '', options: [ { name: 'Landscape', value: 'landscape' }, { name: 'Portrait', value: 'portrait' }, { name: 'Square', value: 'square' }, ], description: 'Filter by photo orientation', }, { displayName: 'Page', name: 'page', type: 'number', default: 1, typeOptions: { minValue: 1, }, description: 'Page of results to start from', }, { displayName: 'Per Page', name: 'perPage', type: 'number', default: 15, typeOptions: { minValue: 1, maxValue: 80, }, description: 'Number of results per request (max 80)', }, { displayName: 'Size', name: 'size', type: 'options', default: '', options: [ { name: 'Large (at least 24MP)', value: 'large' }, { name: 'Medium', value: 'medium' }, { name: 'Small', value: 'small' }, ], description: 'Filter by minimum photo size', }, ], }, { displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false, displayOptions: { show: { resource: ['photo'], operation: ['curated'], }, }, }, { displayName: 'Limit', name: 'limit', type: 'number', typeOptions: { minValue: 1, maxValue: 5000, }, default: 50, displayOptions: { show: { resource: ['photo'], operation: ['curated'], returnAll: [false], }, }, description: 'Max number of curated photos to return', }, { displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, displayOptions: { show: { resource: ['photo'], operation: ['curated'], }, }, options: [ { displayName: 'Page', name: 'page', type: 'number', default: 1, typeOptions: { minValue: 1, }, }, { displayName: 'Per Page', name: 'perPage', type: 'number', default: 15, typeOptions: { minValue: 1, maxValue: 80, }, description: 'Number of curated photos per request (max 80)', }, ], }, { displayName: 'Photo ID', name: 'photoId', type: 'string', required: true, default: '', displayOptions: { show: { resource: ['photo'], operation: ['get'], }, }, description: 'ID of the photo to retrieve', }, ]; const videoOperations: INodeProperties = { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, displayOptions: { show: { resource: ['video'], }, }, options: [ { name: 'Search', value: 'search', action: 'Search videos', description: 'Find videos that match a query', }, { name: 'Popular', value: 'popular', action: 'List popular videos', description: 'Browse popular Pexels videos', }, { name: 'Get', value: 'get', action: 'Get a video', description: 'Retrieve a single video by ID', }, ], default: 'search', }; const videoFields: INodeProperties[] = [ { displayName: 'Query', name: 'query', type: 'string', required: true, default: '', displayOptions: { show: { resource: ['video'], operation: ['search'], }, }, description: 'Text to search for in videos', }, { displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false, displayOptions: { show: { resource: ['video'], operation: ['search'], }, }, }, { displayName: 'Limit', name: 'limit', type: 'number', typeOptions: { minValue: 1, maxValue: 5000, }, default: 50, displayOptions: { show: { resource: ['video'], operation: ['search'], returnAll: [false], }, }, description: 'Max number of videos to return (API allows up to 80 per request)', }, { displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, displayOptions: { show: { resource: ['video'], operation: ['search'], }, }, options: [ { displayName: 'Max Duration', name: 'maxDuration', type: 'number', default: 0, description: 'Maximum duration in seconds', }, { displayName: 'Min Duration', name: 'minDuration', type: 'number', default: 0, description: 'Minimum duration in seconds', }, { displayName: 'Min Height', name: 'minHeight', type: 'number', default: 0, description: 'Only return videos at least this tall in pixels', }, { displayName: 'Min Width', name: 'minWidth', type: 'number', default: 0, description: 'Only return videos at least this wide in pixels', }, { displayName: 'Page', name: 'page', type: 'number', default: 1, typeOptions: { minValue: 1, }, }, { displayName: 'Per Page', name: 'perPage', type: 'number', default: 15, typeOptions: { minValue: 1, maxValue: 80, }, description: 'Number of videos per request (max 80)', }, { displayName: 'Size', name: 'size', type: 'options', default: '', options: [ { name: 'Large (HD/4K)', value: 'large' }, { name: 'Small', value: 'small' }, ], description: 'Filter by video size. Use large to restrict to HD or 4K.', }, ], }, { displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false, displayOptions: { show: { resource: ['video'], operation: ['popular'], }, }, }, { displayName: 'Limit', name: 'limit', type: 'number', typeOptions: { minValue: 1, maxValue: 5000, }, default: 50, displayOptions: { show: { resource: ['video'], operation: ['popular'], returnAll: [false], }, }, description: 'Max number of popular videos to return', }, { displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, displayOptions: { show: { resource: ['video'], operation: ['popular'], }, }, options: [ { displayName: 'Max Duration', name: 'maxDuration', type: 'number', default: 0, description: 'Maximum duration in seconds', }, { displayName: 'Min Duration', name: 'minDuration', type: 'number', default: 0, description: 'Minimum duration in seconds', }, { displayName: 'Min Height', name: 'minHeight', type: 'number', default: 0, }, { displayName: 'Min Width', name: 'minWidth', type: 'number', default: 0, }, { displayName: 'Page', name: 'page', type: 'number', default: 1, typeOptions: { minValue: 1, }, }, { displayName: 'Per Page', name: 'perPage', type: 'number', default: 15, typeOptions: { minValue: 1, maxValue: 80, }, description: 'Number of videos per request (max 80)', }, { displayName: 'Size', name: 'size', type: 'options', default: '', options: [ { name: 'Large (HD/4K)', value: 'large' }, { name: 'Small', value: 'small' }, ], }, ], }, { displayName: 'Video ID', name: 'videoId', type: 'string', required: true, default: '', displayOptions: { show: { resource: ['video'], operation: ['get'], }, }, description: 'ID of the video to retrieve', }, ]; const collectionOperations: INodeProperties = { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, displayOptions: { show: { resource: ['collection'], }, }, options: [ { name: 'List Collections', value: 'list', action: 'List collections', description: 'List your collections', }, { name: 'List Featured Collections', value: 'featured', action: 'List featured collections', description: 'Browse featured collections curated by Pexels', }, { name: 'Get Collection Media', value: 'get', action: 'Get collection media', description: 'Retrieve items inside a collection', }, ], default: 'list', }; const collectionFields: INodeProperties[] = [ { displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false, displayOptions: { show: { resource: ['collection'], operation: ['list'], }, }, }, { displayName: 'Limit', name: 'limit', type: 'number', typeOptions: { minValue: 1, maxValue: 500, }, default: 50, displayOptions: { show: { resource: ['collection'], operation: ['list'], returnAll: [false], }, }, description: 'Max number of collections to return', }, { displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, displayOptions: { show: { resource: ['collection'], operation: ['list'], }, }, options: [ { displayName: 'Page', name: 'page', type: 'number', default: 1, typeOptions: { minValue: 1, }, }, { displayName: 'Per Page', name: 'perPage', type: 'number', default: 15, typeOptions: { minValue: 1, maxValue: 80, }, description: 'Number of collections per request (max 80)', }, ], }, { displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false, displayOptions: { show: { resource: ['collection'], operation: ['featured'], }, }, }, { displayName: 'Limit', name: 'limit', type: 'number', typeOptions: { minValue: 1, maxValue: 500, }, default: 50, displayOptions: { show: { resource: ['collection'], operation: ['featured'], returnAll: [false], }, }, description: 'Max number of featured collections to return', }, { displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, displayOptions: { show: { resource: ['collection'], operation: ['featured'], }, }, options: [ { displayName: 'Page', name: 'page', type: 'number', default: 1, typeOptions: { minValue: 1, }, }, { displayName: 'Per Page', name: 'perPage', type: 'number', default: 15, typeOptions: { minValue: 1, maxValue: 80, }, description: 'Number of collections per request (max 80)', }, ], }, { displayName: 'Collection ID', name: 'collectionId', type: 'string', required: true, default: '', displayOptions: { show: { resource: ['collection'], operation: ['get'], }, }, description: 'ID of the collection to fetch', }, { displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false, displayOptions: { show: { resource: ['collection'], operation: ['get'], }, }, }, { displayName: 'Limit', name: 'limit', type: 'number', typeOptions: { minValue: 1, maxValue: 5000, }, default: 50, displayOptions: { show: { resource: ['collection'], operation: ['get'], returnAll: [false], }, }, description: 'Max number of media items to return from the collection', }, { displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, displayOptions: { show: { resource: ['collection'], operation: ['get'], }, }, options: [ { displayName: 'Media Type', name: 'type', type: 'options', default: 'all', options: [ { name: 'All', value: 'all' }, { name: 'Photos', value: 'photos' }, { name: 'Videos', value: 'videos' }, ], description: 'Restrict results to only photos, videos, or both', }, { displayName: 'Page', name: 'page', type: 'number', default: 1, typeOptions: { minValue: 1, }, }, { displayName: 'Per Page', name: 'perPage', type: 'number', default: 15, typeOptions: { minValue: 1, maxValue: 80, }, description: 'Number of media items per request (max 80)', }, ], }, ]; export class Pexels implements INodeType { description: INodeTypeDescription = { displayName: 'Pexels', name: 'pexels', icon: 'file:pexels.svg', group: ['transform'], version: 1, subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', description: 'Search and browse free photos and videos from Pexels | Documentation: https://dps.media/node-n8n-cho-pexels-tu-dong-hoa-tim-kiem-anh-va-video-mien-phi/', defaults: { name: 'Pexels', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'pexelsApi', required: true, }, ], properties: [ { displayName: 'Resource', name: 'resource', type: 'options', noDataExpression: true, options: [ { name: 'Photo', value: 'photo' }, { name: 'Video', value: 'video' }, { name: 'Collection', value: 'collection' }, ], default: 'photo', description: 'Resource to operate on', }, photoOperations, ...photoFields, videoOperations, ...videoFields, collectionOperations, ...collectionFields, ], }; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; for (let i = 0; i < items.length; i++) { const resource = this.getNodeParameter('resource', i) as string; const operation = this.getNodeParameter('operation', i) as string; let responseData: IDataObject | IDataObject[]; if (resource === 'photo') { if (operation === 'search') { const query = this.getNodeParameter('query', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const qs: IDataObject = { query }; if (additionalFields.orientation) qs.orientation = additionalFields.orientation; if (additionalFields.size) qs.size = additionalFields.size; if (additionalFields.color) qs.color = additionalFields.color; if (additionalFields.locale) qs.locale = additionalFields.locale; if (additionalFields.page) qs.page = additionalFields.page; if (additionalFields.perPage) qs.per_page = Math.min(additionalFields.perPage as number, 80); const limit = returnAll ? 0 : (this.getNodeParameter('limit', i) as number); responseData = await pexelsApiRequestAllItems.call( this, 'photos', 'GET', '/v1/search', {}, qs, limit, ); } else if (operation === 'curated') { const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const qs: IDataObject = {}; if (additionalFields.page) qs.page = additionalFields.page; if (additionalFields.perPage) qs.per_page = Math.min(additionalFields.perPage as number, 80); const limit = returnAll ? 0 : (this.getNodeParameter('limit', i) as number); responseData = await pexelsApiRequestAllItems.call( this, 'photos', 'GET', '/v1/curated', {}, qs, limit, ); } else if (operation === 'get') { const photoId = this.getNodeParameter('photoId', i) as string; responseData = (await pexelsApiRequest.call( this, 'GET', `/v1/photos/${photoId}`, )) as IDataObject; } else { throw new NodeOperationError(this.getNode(), `Unsupported operation ${operation}`, { itemIndex: i }); } } else if (resource === 'video') { if (operation === 'search') { const query = this.getNodeParameter('query', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const qs: IDataObject = { query }; if (additionalFields.page) qs.page = additionalFields.page; if (additionalFields.perPage) qs.per_page = Math.min(additionalFields.perPage as number, 80); if (additionalFields.minWidth) qs.min_width = additionalFields.minWidth; if (additionalFields.minHeight) qs.min_height = additionalFields.minHeight; if (additionalFields.minDuration) qs.min_duration = additionalFields.minDuration; if (additionalFields.maxDuration) qs.max_duration = additionalFields.maxDuration; if (additionalFields.size) qs.size = additionalFields.size; const limit = returnAll ? 0 : (this.getNodeParameter('limit', i) as number); responseData = await pexelsApiRequestAllItems.call( this, 'videos', 'GET', '/videos/search', {}, qs, limit, ); } else if (operation === 'popular') { const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const qs: IDataObject = {}; if (additionalFields.page) qs.page = additionalFields.page; if (additionalFields.perPage) qs.per_page = Math.min(additionalFields.perPage as number, 80); if (additionalFields.minWidth) qs.min_width = additionalFields.minWidth; if (additionalFields.minHeight) qs.min_height = additionalFields.minHeight; if (additionalFields.minDuration) qs.min_duration = additionalFields.minDuration; if (additionalFields.maxDuration) qs.max_duration = additionalFields.maxDuration; if (additionalFields.size) qs.size = additionalFields.size; const limit = returnAll ? 0 : (this.getNodeParameter('limit', i) as number); responseData = await pexelsApiRequestAllItems.call( this, 'videos', 'GET', '/videos/popular', {}, qs, limit, ); } else if (operation === 'get') { const videoId = this.getNodeParameter('videoId', i) as string; responseData = (await pexelsApiRequest.call( this, 'GET', `/videos/videos/${videoId}`, )) as IDataObject; } else { throw new NodeOperationError(this.getNode(), `Unsupported operation ${operation}`, { itemIndex: i }); } } else if (resource === 'collection') { if (operation === 'list') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject; const qs: IDataObject = {}; if (additionalFields.page) qs.page = additionalFields.page; if (additionalFields.perPage) qs.per_page = Math.min(additionalFields.perPage as number, 80); if (returnAll) { responseData = await pexelsApiRequestAllItems.call( this, 'collections', 'GET', '/v1/collections', {}, qs, 0, ); } else { const limit = this.getNodeParameter('limit', i) as number; responseData = await pexelsApiRequestAllItems.call( this, 'collections', 'GET', '/v1/collections', {}, qs, limit, ); } } else if (operation === 'featured') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject; const qs: IDataObject = {}; if (additionalFields.page) qs.page = additionalFields.page; if (additionalFields.perPage) qs.per_page = Math.min(additionalFields.perPage as number, 80); if (returnAll) { responseData = await pexelsApiRequestAllItems.call( this, 'collections', 'GET', '/v1/collections/featured', {}, qs, 0, ); } else { const limit = this.getNodeParameter('limit', i) as number; responseData = await pexelsApiRequestAllItems.call( this, 'collections', 'GET', '/v1/collections/featured', {}, qs, limit, ); } } else if (operation === 'get') { const collectionId = this.getNodeParameter('collectionId', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject; const qs: IDataObject = {}; if (additionalFields.page) qs.page = additionalFields.page; if (additionalFields.perPage) qs.per_page = Math.min(additionalFields.perPage as number, 80); if (additionalFields.type) qs.type = additionalFields.type; const limit = returnAll ? 0 : (this.getNodeParameter('limit', i) as number); const perPage = (qs.per_page as number) || limit || 80; if (perPage) qs.per_page = Math.min(perPage as number, 80); const media: IDataObject[] = []; let page = (qs.page as number) || 1; let collectionMeta: IDataObject | undefined; while (true) { qs.page = page; const result = (await pexelsApiRequest.call( this, 'GET', `/v1/collections/${collectionId}`, {}, qs, )) as IDataObject; if (!collectionMeta) { collectionMeta = { id: result.id, title: result.title, description: result.description, private: result.private, }; } const batch = ((result.media as IDataObject[]) || []).map((entry) => ({ ...entry, collection: collectionMeta, })); media.push(...batch); if (!returnAll && limit && media.length >= limit) { break; } const nextPageUrl = result.next_page as string | undefined; if (!nextPageUrl) break; try { const parsed = new URL(nextPageUrl); const nextPage = parsed.searchParams.get('page'); if (!nextPage) break; page = Number(nextPage); if (Number.isNaN(page)) break; } catch (error) { break; } if (!returnAll && limit && media.length >= limit) { break; } } responseData = limit ? media.slice(0, limit) : media; } else { throw new NodeOperationError(this.getNode(), `Unsupported operation ${operation}`, { itemIndex: i }); } } else { throw new NodeOperationError(this.getNode(), `Unsupported resource ${resource}`, { itemIndex: i }); } const executionData = this.helpers.returnJsonArray( Array.isArray(responseData) ? responseData : [responseData], ); returnData.push(...executionData); } return [returnData]; } }