diff --git a/components/pexels/actions/download-photo/download-photo.mjs b/components/pexels/actions/download-photo/download-photo.mjs new file mode 100644 index 0000000000000..5ab4dbaf1c47f --- /dev/null +++ b/components/pexels/actions/download-photo/download-photo.mjs @@ -0,0 +1,61 @@ +import { axios } from "@pipedream/platform"; +import fs from "fs"; +import stream from "stream"; +import { promisify } from "util"; +import pexels from "../../pexels.app.mjs"; + +export default { + key: "pexels-download-photo", + name: "Download Photo", + description: "Download a specific photo by providing its photo ID and optionally choosing the desired size. [See the documentation](https://www.pexels.com/api/documentation/)", + version: "0.0.1", + type: "action", + props: { + pexels, + photoId: { + propDefinition: [ + pexels, + "photoId", + ], + }, + filePath: { + type: "string", + label: "File Path", + description: "The destination path in [`/tmp`](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory) for the downloaded the file (e.g., `/tmp/myFile.jpg`). Make sure to include the file extension.", + }, + }, + methods: { + getFileStream({ + $, downloadUrl, + }) { + return axios($, { + url: downloadUrl, + responseType: "stream", + }); + }, + }, + async run({ $ }) { + const response = await this.pexels.getPhoto({ + $, + photoId: this.photoId, + }); + + const fileStream = await this.getFileStream({ + $, + downloadUrl: response.src.original, + }); + + const pipeline = promisify(stream.pipeline); + const resp = await pipeline( + fileStream, + fs.createWriteStream(this.filePath.includes("/tmp") + ? this.filePath + : `/tmp/${this.filePath}`), + ); + + $.export("$summary", `Successfully downloaded photo with ID ${this.photoId} to ${this.filePath}`); + return { + resp, + }; + }, +}; diff --git a/components/pexels/actions/get-photo-details/get-photo-details.mjs b/components/pexels/actions/get-photo-details/get-photo-details.mjs new file mode 100644 index 0000000000000..46d1fd6913d6b --- /dev/null +++ b/components/pexels/actions/get-photo-details/get-photo-details.mjs @@ -0,0 +1,26 @@ +import pexels from "../../pexels.app.mjs"; + +export default { + key: "pexels-get-photo-details", + name: "Get Photo Details", + description: "Retrieve detailed information about a specific photo by providing its photo ID. [See the documentation](https://www.pexels.com/api/documentation/#photos-show)", + version: "0.0.1", + type: "action", + props: { + pexels, + photoId: { + propDefinition: [ + pexels, + "photoId", + ], + }, + }, + async run({ $ }) { + const response = await this.pexels.getPhoto({ + $, + photoId: this.photoId, + }); + $.export("$summary", `Successfully retrieved details for photo ID: ${this.photoId}`); + return response; + }, +}; diff --git a/components/pexels/actions/search-photos/search-photos.mjs b/components/pexels/actions/search-photos/search-photos.mjs new file mode 100644 index 0000000000000..3b8e2aa4729df --- /dev/null +++ b/components/pexels/actions/search-photos/search-photos.mjs @@ -0,0 +1,65 @@ +import pexels from "../../pexels.app.mjs"; + +export default { + key: "pexels-search-photos", + name: "Search Photos", + description: "Search for photos on Pexels using a keyword or phrase. [See the documentation](https://www.pexels.com/api/documentation/#photos-search)", + version: "0.0.1", + type: "action", + props: { + pexels, + searchQuery: { + propDefinition: [ + pexels, + "searchQuery", + ], + }, + orientation: { + propDefinition: [ + pexels, + "orientation", + ], + }, + size: { + propDefinition: [ + pexels, + "size", + ], + }, + color: { + propDefinition: [ + pexels, + "color", + ], + }, + page: { + type: "integer", + label: "Page", + description: "The page number you are requesting", + optional: true, + }, + perPage: { + type: "integer", + label: "Per Page", + description: "The number of results you are requesting per page", + max: 80, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.pexels.searchPhotos({ + $, + params: { + query: this.searchQuery, + orientation: this.orientation, + size: this.size, + color: this.color, + page: this.page || 1, + per_page: this.perPage || 15, + }, + }); + + $.export("$summary", `Successfully retrieved ${response.photos.length} photos for the query "${this.searchQuery}".`); + return response; + }, +}; diff --git a/components/pexels/common/constants.mjs b/components/pexels/common/constants.mjs new file mode 100644 index 0000000000000..ece9f78eef47e --- /dev/null +++ b/components/pexels/common/constants.mjs @@ -0,0 +1,80 @@ +export const ORIENTATION_OPTIONS = [ + { + label: "Landscape", + value: "landscape", + }, + { + label: "Portrait", + value: "portrait", + }, + { + label: "Square", + value: "square", + }, +]; + +export const SIZE_OPTIONS = [ + { + label: "Large (24MP)", + value: "large", + }, + { + label: "Medium (12MP)", + value: "medium", + }, + { + label: "Small (4MP)", + value: "small", + }, +]; + +export const COLOR_OPTIONS = [ + { + label: "Red", + value: "red", + }, + { + label: "Orange", + value: "orange", + }, + { + label: "Yellow", + value: "yellow", + }, + { + label: "Green", + value: "green", + }, + { + label: "Turquoise", + value: "turquoise", + }, + { + label: "Blue", + value: "blue", + }, + { + label: "Violet", + value: "violet", + }, + { + label: "Pink", + value: "pink", + }, + { + label: "Brown", + value: "brown", + }, + { + label: "Black", + value: "black", + }, + { + label: "Gray", + value: "gray", + }, + { + label: "White", + value: "white", + }, +]; diff --git a/components/pexels/package.json b/components/pexels/package.json index 67455c9c44563..b6fb19fa9ae84 100644 --- a/components/pexels/package.json +++ b/components/pexels/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/pexels", - "version": "0.6.0", + "version": "0.1.0", "description": "Pipedream pexels Components", "main": "pexels.app.mjs", "keywords": [ @@ -13,6 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^3.0.0" + "@pipedream/platform": "^3.0.3", + "stream": "^0.0.3" } } diff --git a/components/pexels/pexels.app.mjs b/components/pexels/pexels.app.mjs index 514b68f9b00b0..a7a464a1bbf7a 100644 --- a/components/pexels/pexels.app.mjs +++ b/components/pexels/pexels.app.mjs @@ -1,11 +1,108 @@ +import { axios } from "@pipedream/platform"; +import { + COLOR_OPTIONS, + ORIENTATION_OPTIONS, + SIZE_OPTIONS, +} from "./common/constants.mjs"; + export default { type: "app", app: "pexels", - propDefinitions: {}, + propDefinitions: { + searchQuery: { + type: "string", + label: "Search Query", + description: "The search query. **Ocean**, **Tigers**, **Pears**, etc.", + }, + orientation: { + type: "string", + label: "Orientation", + description: "Desired photo orientation.", + optional: true, + options: ORIENTATION_OPTIONS, + }, + size: { + type: "string", + label: "Size", + description: "Minimum photo size.", + optional: true, + options: SIZE_OPTIONS, + }, + color: { + type: "string", + label: "Color", + description: "Desired photo color. You can set any color listed or any hexidecimal color code (eg. #ffffff)", + optional: true, + options: COLOR_OPTIONS, + }, + photoId: { + type: "string", + label: "Photo ID", + description: "The ID of the photo to retrieve or download.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.pexels.com/v1"; + }, + _headers() { + return { + "Authorization": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + searchPhotos(opts = {}) { + return this._makeRequest({ + path: "/search", + ...opts, + }); + }, + getPhoto({ + photoId, ...opts + } = {}) { + return this._makeRequest({ + path: `/photos/${photoId}`, + ...opts, + }); + }, + getCuratedPhotos(opts = {}) { + return this._makeRequest({ + path: "/curated", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { photos } = await fn({ + params, + ...opts, + }); + for (const d of photos) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = photos.length; + + } while (hasMore); }, }, }; diff --git a/components/pexels/sources/common/base.mjs b/components/pexels/sources/common/base.mjs new file mode 100644 index 0000000000000..0addac1b812d9 --- /dev/null +++ b/components/pexels/sources/common/base.mjs @@ -0,0 +1,61 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import pexels from "../../pexels.app.mjs"; + +export default { + props: { + pexels, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + getParams() { + return {}; + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + + const response = this.pexels.paginate({ + fn: this.getFunction(), + params: this.getParams(), + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + if (item.id === lastId) break; + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.now(), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/pexels/sources/new-curated-photo/new-curated-photo.mjs b/components/pexels/sources/new-curated-photo/new-curated-photo.mjs new file mode 100644 index 0000000000000..cfbbcdbb5987d --- /dev/null +++ b/components/pexels/sources/new-curated-photo/new-curated-photo.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pexels-new-curated-photo", + name: "New Curated Photo", + description: "Emit new event when a new curated photo is added to the Pexels curated collection. [See the documentation](https://www.pexels.com/api/documentation/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.pexels.getCuratedPhotos; + }, + getSummary(item) { + return `New curated photo with ID: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/pexels/sources/new-curated-photo/test-event.mjs b/components/pexels/sources/new-curated-photo/test-event.mjs new file mode 100644 index 0000000000000..425b8e51c8bcd --- /dev/null +++ b/components/pexels/sources/new-curated-photo/test-event.mjs @@ -0,0 +1,22 @@ +export default { + "id": 2880507, + "width": 4000, + "height": 6000, + "url": "https://www.pexels.com/photo/woman-in-white-long-sleeved-top-and-skirt-standing-on-field-2880507/", + "photographer": "Deden Dicky Ramdhani", + "photographer_url": "https://www.pexels.com/@drdeden88", + "photographer_id": 1378810, + "avg_color": "#7E7F7B", + "src": { + "original": "https://images.pexels.com/photos/2880507/pexels-photo-2880507.jpeg", + "large2x": "https://images.pexels.com/photos/2880507/pexels-photo-2880507.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940", + "large": "https://images.pexels.com/photos/2880507/pexels-photo-2880507.jpeg?auto=compress&cs=tinysrgb&h=650&w=940", + "medium": "https://images.pexels.com/photos/2880507/pexels-photo-2880507.jpeg?auto=compress&cs=tinysrgb&h=350", + "small": "https://images.pexels.com/photos/2880507/pexels-photo-2880507.jpeg?auto=compress&cs=tinysrgb&h=130", + "portrait": "https://images.pexels.com/photos/2880507/pexels-photo-2880507.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=1200&w=800", + "landscape": "https://images.pexels.com/photos/2880507/pexels-photo-2880507.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=627&w=1200", + "tiny": "https://images.pexels.com/photos/2880507/pexels-photo-2880507.jpeg?auto=compress&cs=tinysrgb&dpr=1&fit=crop&h=200&w=280" + }, + "liked": false, + "alt": "Brown Rocks During Golden Hour" +} \ No newline at end of file diff --git a/components/pexels/sources/new-photo-by-search/new-photo-by-search.mjs b/components/pexels/sources/new-photo-by-search/new-photo-by-search.mjs new file mode 100644 index 0000000000000..06a3a34205ae5 --- /dev/null +++ b/components/pexels/sources/new-photo-by-search/new-photo-by-search.mjs @@ -0,0 +1,57 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pexels-new-photo-by-search", + name: "New Photo by Search", + description: "Emit new event when a photo is published that matches a specified search query. [See the documentation](https://www.pexels.com/api/documentation/#photos-search)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + searchQuery: { + propDefinition: [ + common.props.pexels, + "searchQuery", + ], + }, + orientation: { + propDefinition: [ + common.props.pexels, + "orientation", + ], + }, + size: { + propDefinition: [ + common.props.pexels, + "size", + ], + }, + color: { + propDefinition: [ + common.props.pexels, + "color", + ], + }, + }, + methods: { + ...common.methods, + getFunction() { + return this.pexels.searchPhotos; + }, + getParams() { + return { + query: this.searchQuery, + orientation: this.orientation, + size: this.size, + color: this.color, + }; + }, + getSummary(item) { + return `New photo with ID: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/pexels/sources/new-photo-by-search/test-event.mjs b/components/pexels/sources/new-photo-by-search/test-event.mjs new file mode 100644 index 0000000000000..bb7de09eef0bf --- /dev/null +++ b/components/pexels/sources/new-photo-by-search/test-event.mjs @@ -0,0 +1,22 @@ +export default { + "id": 3573351, + "width": 3066, + "height": 3968, + "url": "https://www.pexels.com/photo/trees-during-day-3573351/", + "photographer": "Lukas Rodriguez", + "photographer_url": "https://www.pexels.com/@lukas-rodriguez-1845331", + "photographer_id": 1845331, + "avg_color": "#374824", + "src": { + "original": "https://images.pexels.com/photos/3573351/pexels-photo-3573351.png", + "large2x": "https://images.pexels.com/photos/3573351/pexels-photo-3573351.png?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940", + "large": "https://images.pexels.com/photos/3573351/pexels-photo-3573351.png?auto=compress&cs=tinysrgb&h=650&w=940", + "medium": "https://images.pexels.com/photos/3573351/pexels-photo-3573351.png?auto=compress&cs=tinysrgb&h=350", + "small": "https://images.pexels.com/photos/3573351/pexels-photo-3573351.png?auto=compress&cs=tinysrgb&h=130", + "portrait": "https://images.pexels.com/photos/3573351/pexels-photo-3573351.png?auto=compress&cs=tinysrgb&fit=crop&h=1200&w=800", + "landscape": "https://images.pexels.com/photos/3573351/pexels-photo-3573351.png?auto=compress&cs=tinysrgb&fit=crop&h=627&w=1200", + "tiny": "https://images.pexels.com/photos/3573351/pexels-photo-3573351.png?auto=compress&cs=tinysrgb&dpr=1&fit=crop&h=200&w=280" + }, + "liked": false, + "alt": "Brown Rocks During Golden Hour" +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5f890e57c6ac..609d7a1f70e5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4095,8 +4095,7 @@ importers: specifier: ^1.3.0 version: 1.6.6 - components/emailverify_io: - specifiers: {} + components/emailverify_io: {} components/emelia: {} @@ -9595,8 +9594,11 @@ importers: components/pexels: dependencies: '@pipedream/platform': - specifier: ^3.0.0 + specifier: ^3.0.3 version: 3.0.3 + stream: + specifier: ^0.0.3 + version: 0.0.3 components/phantombuster: dependencies: @@ -12796,8 +12798,7 @@ importers: specifier: ^1.2.0 version: 1.6.6 - components/swarmnode: - specifiers: {} + components/swarmnode: {} components/swell: dependencies: @@ -34753,6 +34754,8 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: