diff --git a/components/rendi/actions/get-ffmpeg-command-status/get-ffmpeg-command-status.mjs b/components/rendi/actions/get-ffmpeg-command-status/get-ffmpeg-command-status.mjs new file mode 100644 index 0000000000000..b8f62db05e531 --- /dev/null +++ b/components/rendi/actions/get-ffmpeg-command-status/get-ffmpeg-command-status.mjs @@ -0,0 +1,25 @@ +import rendi from "../../rendi.app.mjs"; + +export default { + key: "rendi-get-ffmpeg-command-status", + name: "Get FFmpeg Command Status", + description: "Get the status of a previously submitted FFmpeg command. [See the documentation](https://docs.rendi.dev/api-reference/endpoint/poll-command)", + version: "0.0.1", + type: "action", + props: { + rendi, + commandId: { + type: "string", + label: "Command ID", + description: "ID of the FFmpeg command to check", + }, + }, + async run({ $ }) { + const response = await this.rendi.getFfmpegCommand({ + $, + commandId: this.commandId, + }); + $.export("$summary", `Successfully retrieved status of FFmpeg command with ID: ${this.commandId}`); + return response; + }, +}; diff --git a/components/rendi/actions/list-stored-files/list-stored-files.mjs b/components/rendi/actions/list-stored-files/list-stored-files.mjs new file mode 100644 index 0000000000000..f32ebff495506 --- /dev/null +++ b/components/rendi/actions/list-stored-files/list-stored-files.mjs @@ -0,0 +1,21 @@ +import rendi from "../../rendi.app.mjs"; + +export default { + key: "rendi-list-stored-files", + name: "List Stored Files", + description: "Get the list of all stored files for an account. [See the documentation](https://docs.rendi.dev/api-reference/endpoint/list-files)", + version: "0.0.1", + type: "action", + props: { + rendi, + }, + async run({ $ }) { + const response = await this.rendi.listFiles({ + $, + }); + $.export("$summary", `Successfully retrieved ${response.length} file${response.length === 1 + ? "" + : "s"}`); + return response; + }, +}; diff --git a/components/rendi/actions/run-ffmpeg-command/run-ffmpeg-command.mjs b/components/rendi/actions/run-ffmpeg-command/run-ffmpeg-command.mjs new file mode 100644 index 0000000000000..7b13c83d5b4ba --- /dev/null +++ b/components/rendi/actions/run-ffmpeg-command/run-ffmpeg-command.mjs @@ -0,0 +1,116 @@ +import rendi from "../../rendi.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import { axios } from "@pipedream/platform"; +import fs from "fs"; + +export default { + key: "rendi-run-ffmpeg-command", + name: "Run FFmpeg Command", + description: "Submit an FFmpeg command for processing with input and output file specifications. [See the documentation](https://docs.rendi.dev/api-reference/endpoint/run-ffmpeg-command)", + version: "0.0.1", + type: "action", + props: { + rendi, + inputFiles: { + type: "object", + label: "Input File URL(s)", + description: "Dictionary mapping file aliases to their publicly accessible paths, file name should appear in the end of the url, keys must start with 'in_'. You can use public file urls, google drive, dropbox, rendi stored files, s3 stored files, etc. as long as they are publicly accessible. [See the documentation](https://docs.rendi.dev/api-reference/endpoint/run-ffmpeg-command) for more information", + }, + outputFiles: { + type: "object", + label: "Output File Name(s)", + description: "Dictionary mapping file aliases to their desired output file names, keys must start with 'out_'. [See the documentation](https://docs.rendi.dev/api-reference/endpoint/run-ffmpeg-command) for more information", + }, + command: { + type: "string", + label: "FFmpeg Command", + description: "FFmpeg command string using {{alias}} placeholders for input and output files. `{{}}` is a reserved string, instead you can use `\\{\\{\\}\\}` , so for example `{{in_1}}` should be `\\{\\{in_1\\}\\}`. Example: `-i \\{\\{in_1\\}\\} \\{\\{out_1\\}\\}`", + }, + maxCommandRunSeconds: { + type: "string", + label: "Max Command Run Seconds", + description: "Maximum allowed runtime in seconds for a single FFmpeg command, the default is 300 seconds", + optional: true, + }, + waitForCompletion: { + type: "boolean", + label: "Wait for Completion", + description: "Set to `true` to poll the API in 3-second intervals until the command is completed", + optional: true, + reloadProps: true, + }, + }, + additionalProps() { + if (this.waitForCompletion) { + return { + downloadFilesToTmp: { + type: "boolean", + label: "Download Files to /tmp", + description: "Set to `true` to download the output files to the workflow's /tmp directory", + optional: true, + }, + }; + } + return {}; + }, + async run({ $ }) { + const inputFiles = parseObject(this.inputFiles); + const outputFiles = parseObject(this.outputFiles); + + if (Object.keys(inputFiles).some((key) => !key.startsWith("in_"))) { + throw new ConfigurationError("Input file keys must start with 'in_'"); + } + if (Object.keys(outputFiles).some((key) => !key.startsWith("out_"))) { + throw new ConfigurationError("Output file keys must start with 'out_'"); + } + + let response = await this.rendi.runFfmpegCommand({ + $, + data: { + input_files: inputFiles, + output_files: outputFiles, + ffmpeg_command: this.command, + max_command_run_seconds: this.maxCommandRunSeconds, + }, + }); + + if (this.waitForCompletion) { + const commandId = response.command_id; + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + while (response?.status !== "SUCCESS" && response?.status !== "FAILED") { + response = await this.rendi.getFfmpegCommand({ + $, + commandId, + }); + await timer(3000); + } + if (response?.status === "SUCCESS" && this.downloadFilesToTmp) { + response.tmpFiles = []; + for (const value of Object.values(response.output_files)) { + const resp = await axios($, { + url: value.storage_url, + responseType: "arraybuffer", + }); + const filename = value.storage_url.split("/").pop(); + const downloadedFilepath = `/tmp/${filename}`; + fs.writeFileSync(downloadedFilepath, resp); + + response.tmpFiles.push({ + filename, + downloadedFilepath, + }); + } + } + + if (response?.error_message) { + throw new ConfigurationError(response.error_message); + } + } + + $.export("$summary", `FFmpeg command ${this.waitForCompletion + ? "submitted and completed" + : "submitted"} successfully`); + return response; + }, +}; diff --git a/components/rendi/common/utils.mjs b/components/rendi/common/utils.mjs new file mode 100644 index 0000000000000..c9fe55e04ca6c --- /dev/null +++ b/components/rendi/common/utils.mjs @@ -0,0 +1,9 @@ +export function parseObject(obj) { + if (!obj) return undefined; + + if (typeof obj === "string") { + return JSON.parse(obj); + } + + return obj; +} diff --git a/components/rendi/package.json b/components/rendi/package.json index fe1e2c173792e..3afbeaca6513e 100644 --- a/components/rendi/package.json +++ b/components/rendi/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/rendi", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Rendi Components", "main": "rendi.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/rendi/rendi.app.mjs b/components/rendi/rendi.app.mjs index bae1e824fec34..a61a22afee21f 100644 --- a/components/rendi/rendi.app.mjs +++ b/components/rendi/rendi.app.mjs @@ -1,11 +1,52 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "rendi", propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.rendi.dev/v1"; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "x-api-key": `${this.$auth.api_key}`, + }, + ...otherOpts, + }); + }, + listFiles(opts = {}) { + return this._makeRequest({ + path: "/files", + ...opts, + }); + }, + listCommands(opts = {}) { + return this._makeRequest({ + path: "/commands", + ...opts, + }); + }, + getFfmpegCommand({ + commandId, ...opts + }) { + return this._makeRequest({ + path: `/commands/${commandId}`, + ...opts, + }); + }, + runFfmpegCommand(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/run-ffmpeg-command", + ...opts, + }); }, }, }; diff --git a/components/rendi/sources/new-ffmpeg-command/new-ffmpeg-command.mjs b/components/rendi/sources/new-ffmpeg-command/new-ffmpeg-command.mjs new file mode 100644 index 0000000000000..862180e1a7f23 --- /dev/null +++ b/components/rendi/sources/new-ffmpeg-command/new-ffmpeg-command.mjs @@ -0,0 +1,73 @@ +import rendi from "../../rendi.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "rendi-new-ffmpeg-command", + name: "New FFmpeg Command", + description: "Emit new event when a new FFmpeg command is submitted. [See the documentation](https://docs.rendi.dev/api-reference/endpoint/list-commands)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + rendi, + db: "$.service.db", + timer: { + label: "Polling interval", + description: "Pipedream will poll the Trello API on this schedule", + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(ts) { + this.db.set("lastTs", ts); + }, + generateMeta(command) { + return { + id: command.command_id, + summary: `New FFmpeg command: ${command.command_id}`, + ts: Date.parse(command.created_at), + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + let commands = []; + const results = await this.rendi.listCommands(); + for (const command of results) { + const ts = Date.parse(command.created_at); + if (ts > lastTs) { + commands.push(command); + maxTs = Math.max(maxTs, ts); + } + } + + if (max && commands.length > max) { + commands = commands.slice(-1 * max); + } + + commands.forEach((command) => { + const meta = this.generateMeta(command); + this.$emit(command, meta); + }); + + this._setLastTs(maxTs); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/rendi/sources/new-ffmpeg-command/test-event.mjs b/components/rendi/sources/new-ffmpeg-command/test-event.mjs new file mode 100644 index 0000000000000..b3104ec839a94 --- /dev/null +++ b/components/rendi/sources/new-ffmpeg-command/test-event.mjs @@ -0,0 +1,5 @@ +export default { + "command_id": "5eda862d-b5d5-41f5-ac92-e6872b054", + "status": "SUCCESS", + "created_at": "2025-05-19T18:15:20.349538Z" + } \ No newline at end of file diff --git a/components/rendi/sources/new-stored-file/new-stored-file.mjs b/components/rendi/sources/new-stored-file/new-stored-file.mjs new file mode 100644 index 0000000000000..14ed01f6bd0be --- /dev/null +++ b/components/rendi/sources/new-stored-file/new-stored-file.mjs @@ -0,0 +1,38 @@ +import rendi from "../../rendi.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "rendi-new-stored-file", + name: "New Stored File", + description: "Emit new event when a new file is uploaded to an account. [See the documentation](https://docs.rendi.dev/api-reference/endpoint/list-files)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + rendi, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta(file) { + return { + id: file.file_id, + summary: `New Stored File with ID: ${file.file_id}`, + ts: Date.now(), + }; + }, + }, + async run() { + const files = await this.rendi.listFiles(); + for (const file of files) { + const meta = this.generateMeta(file); + this.$emit(file, meta); + } + }, + sampleEmit, +}; diff --git a/components/rendi/sources/new-stored-file/test-event.mjs b/components/rendi/sources/new-stored-file/test-event.mjs new file mode 100644 index 0000000000000..224c86008dcef --- /dev/null +++ b/components/rendi/sources/new-stored-file/test-event.mjs @@ -0,0 +1,5 @@ +export default { + "file_id": "8e30e8fb-a037-48e0-aca2-eaa6df7ec", + "size_mbytes": 34.04952430725098, + "storage_url": "https://storage.rendi.dev/trial_files/86f043b9-dc30-465b-995c-371382ee1c74/5eda862d-b5d5-41f5-ac92-e6872b054/output_one.avi" + } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98ee964905365..837c17640907d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10877,7 +10877,11 @@ importers: components/renderform: {} - components/rendi: {} + components/rendi: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/rentcast: dependencies: