Payment-free self-hosted builds keep full API access with optional webhookUrl callbacks and the published n8n-nodes-songs2vid package source under integrations/n8n.
1141 lines
33 KiB
TypeScript
1141 lines
33 KiB
TypeScript
import type {
|
|
IDataObject,
|
|
IExecuteFunctions,
|
|
INodeExecutionData,
|
|
INodeType,
|
|
INodeTypeDescription,
|
|
IRequestOptions,
|
|
} from "n8n-workflow";
|
|
import { NodeOperationError } from "n8n-workflow";
|
|
|
|
const LAYOUT_TEMPLATES = [
|
|
"COVER_LEFT_TEXT_RIGHT",
|
|
"COVER_TOP_TEXT_BOTTOM",
|
|
"COVER_RIGHT_TEXT_LEFT",
|
|
"CENTERED_COMPACT",
|
|
"LOWER_LEFT_COVER_TEXT",
|
|
"LOWER_RIGHT_COVER_TEXT",
|
|
] as const;
|
|
|
|
const RESOLUTIONS = [
|
|
"1920x1080",
|
|
"1280x720",
|
|
"854x480",
|
|
"720x720",
|
|
"640x360",
|
|
"426x240",
|
|
] as const;
|
|
|
|
const PRIVACY = ["PUBLIC", "PRIVATE", "UNLISTED"] as const;
|
|
|
|
const WATERMARK_MODES = ["none", "default", "text", "logo"] as const;
|
|
|
|
const WATERMARK_POSITIONS = [
|
|
"top-left",
|
|
"top-right",
|
|
"bottom-left",
|
|
"bottom-right",
|
|
"center",
|
|
] as const;
|
|
|
|
const FONT_KEYS = [
|
|
"system",
|
|
"inter",
|
|
"montserrat",
|
|
"roboto",
|
|
"oswald",
|
|
"playfair",
|
|
"custom",
|
|
] as const;
|
|
|
|
/**
|
|
* Full Songs2VID REST surface for n8n — every documented /api/v1 endpoint.
|
|
* Optional fields live under Additional Fields collections.
|
|
*/
|
|
export class Songs2Vid implements INodeType {
|
|
description: INodeTypeDescription = {
|
|
displayName: "Songs2VID",
|
|
name: "songs2Vid",
|
|
icon: "file:songs2vid.svg",
|
|
group: ["transform"],
|
|
version: 1,
|
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
description:
|
|
"Upload media, create YouTube renders, poll jobs, manage playlists & API keys",
|
|
defaults: {
|
|
name: "Songs2VID",
|
|
},
|
|
inputs: ["main"],
|
|
outputs: ["main"],
|
|
credentials: [
|
|
{
|
|
name: "songs2VidApi",
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: [
|
|
"file",
|
|
"render",
|
|
"playlist",
|
|
"apiKey",
|
|
],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "songs2VidApi",
|
|
required: false,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ["discovery"],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
requestDefaults: {
|
|
baseURL: '={{($credentials.baseUrl || "https://songs2vid.com").replace(/\\/$/, "")}}',
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
},
|
|
properties: [
|
|
{
|
|
displayName: "Resource",
|
|
name: "resource",
|
|
type: "options",
|
|
noDataExpression: true,
|
|
options: [
|
|
{ name: "API Key", value: "apiKey" },
|
|
{ name: "Discovery", value: "discovery" },
|
|
{ name: "File", value: "file" },
|
|
{ name: "Playlist", value: "playlist" },
|
|
{ name: "Render", value: "render" },
|
|
],
|
|
default: "render",
|
|
},
|
|
|
|
// ── Discovery ──────────────────────────────────────────────
|
|
{
|
|
displayName: "Operation",
|
|
name: "operation",
|
|
type: "options",
|
|
noDataExpression: true,
|
|
displayOptions: { show: { resource: ["discovery"] } },
|
|
options: [
|
|
{
|
|
name: "Get API Catalog",
|
|
value: "getCatalog",
|
|
description: "GET /api/v1 — endpoints, limits, layout templates (no auth)",
|
|
action: "Get API catalog",
|
|
},
|
|
],
|
|
default: "getCatalog",
|
|
},
|
|
|
|
// ── File ───────────────────────────────────────────────────
|
|
{
|
|
displayName: "Operation",
|
|
name: "operation",
|
|
type: "options",
|
|
noDataExpression: true,
|
|
displayOptions: { show: { resource: ["file"] } },
|
|
options: [
|
|
{
|
|
name: "Upload",
|
|
value: "upload",
|
|
description: "POST /api/v1/upload — image | audio | logo | font",
|
|
action: "Upload a file",
|
|
},
|
|
],
|
|
default: "upload",
|
|
},
|
|
{
|
|
displayName: "Binary Property",
|
|
name: "binaryPropertyName",
|
|
type: "string",
|
|
default: "data",
|
|
required: true,
|
|
displayOptions: {
|
|
show: { resource: ["file"], operation: ["upload"] },
|
|
},
|
|
description: "Name of the binary property that holds the file",
|
|
},
|
|
{
|
|
displayName: "Type",
|
|
name: "uploadType",
|
|
type: "options",
|
|
required: true,
|
|
options: [
|
|
{ name: "Image (cover)", value: "image" },
|
|
{ name: "Audio", value: "audio" },
|
|
{ name: "Logo (PNG watermark)", value: "logo" },
|
|
{ name: "Font (.ttf / .otf)", value: "font" },
|
|
],
|
|
default: "audio",
|
|
displayOptions: {
|
|
show: { resource: ["file"], operation: ["upload"] },
|
|
},
|
|
},
|
|
|
|
// ── Render ─────────────────────────────────────────────────
|
|
{
|
|
displayName: "Operation",
|
|
name: "operation",
|
|
type: "options",
|
|
noDataExpression: true,
|
|
displayOptions: { show: { resource: ["render"] } },
|
|
options: [
|
|
{
|
|
name: "Create Render",
|
|
value: "create",
|
|
description: "POST /api/v1/render — start encode + YouTube upload",
|
|
action: "Create a render",
|
|
},
|
|
{
|
|
name: "Create Job (alias)",
|
|
value: "createJob",
|
|
description: "POST /api/v1/jobs — identical to Create Render",
|
|
action: "Create a job",
|
|
},
|
|
{
|
|
name: "Create Batch",
|
|
value: "createBatch",
|
|
description:
|
|
"POST /api/v1/jobs/batch — one-shot multipart (no webhookUrl)",
|
|
action: "Create a batch job",
|
|
},
|
|
{
|
|
name: "Get Render Status",
|
|
value: "get",
|
|
description: "GET /api/v1/jobs/:id",
|
|
action: "Get render status",
|
|
},
|
|
{
|
|
name: "List Renders",
|
|
value: "list",
|
|
description: "GET /api/v1/jobs (or /api/v1/render)",
|
|
action: "List renders",
|
|
},
|
|
],
|
|
default: "create",
|
|
},
|
|
{
|
|
displayName: "Use Render Alias Path",
|
|
name: "useRenderAlias",
|
|
type: "boolean",
|
|
default: false,
|
|
displayOptions: {
|
|
show: { resource: ["render"], operation: ["list"] },
|
|
},
|
|
description: "If true, call GET /api/v1/render instead of /api/v1/jobs",
|
|
},
|
|
{
|
|
displayName: "Job ID",
|
|
name: "jobId",
|
|
type: "string",
|
|
required: true,
|
|
default: "",
|
|
displayOptions: {
|
|
show: { resource: ["render"], operation: ["get"] },
|
|
},
|
|
},
|
|
{
|
|
displayName: "Limit",
|
|
name: "limit",
|
|
type: "number",
|
|
typeOptions: { minValue: 1, maxValue: 100 },
|
|
default: 20,
|
|
displayOptions: {
|
|
show: { resource: ["render"], operation: ["list"] },
|
|
},
|
|
description: "Max jobs to return (default 20, max 100)",
|
|
},
|
|
{
|
|
displayName: "Image Path",
|
|
name: "imagePath",
|
|
type: "string",
|
|
required: true,
|
|
default: "",
|
|
displayOptions: {
|
|
show: {
|
|
resource: ["render"],
|
|
operation: ["create", "createJob"],
|
|
},
|
|
},
|
|
description: "Server path from Upload (type=image)",
|
|
},
|
|
{
|
|
displayName: "Audio Path",
|
|
name: "audioPath",
|
|
type: "string",
|
|
required: true,
|
|
default: "",
|
|
displayOptions: {
|
|
show: {
|
|
resource: ["render"],
|
|
operation: ["create", "createJob"],
|
|
},
|
|
},
|
|
description: "Server path from Upload (type=audio)",
|
|
},
|
|
{
|
|
displayName: "Audio Filename",
|
|
name: "audioFilename",
|
|
type: "string",
|
|
required: true,
|
|
default: "",
|
|
displayOptions: {
|
|
show: {
|
|
resource: ["render"],
|
|
operation: ["create", "createJob"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: "YouTube Title",
|
|
name: "title",
|
|
type: "string",
|
|
default: "",
|
|
displayOptions: {
|
|
show: {
|
|
resource: ["render"],
|
|
operation: ["create", "createJob"],
|
|
},
|
|
},
|
|
description:
|
|
"YouTube video title. Falls back to artist - songTitle when empty.",
|
|
},
|
|
{
|
|
displayName: "Privacy",
|
|
name: "privacy",
|
|
type: "options",
|
|
options: PRIVACY.map((p) => ({ name: p, value: p })),
|
|
default: "UNLISTED",
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ["render"],
|
|
operation: ["create", "createJob"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: "Resolution",
|
|
name: "resolution",
|
|
type: "options",
|
|
options: RESOLUTIONS.map((r) => ({ name: r, value: r })),
|
|
default: "1920x1080",
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ["render"],
|
|
operation: ["create", "createJob"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: "Additional Fields",
|
|
name: "additionalFields",
|
|
type: "collection",
|
|
placeholder: "Add Field",
|
|
default: {},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ["render"],
|
|
operation: ["create", "createJob"],
|
|
},
|
|
},
|
|
options: [
|
|
{
|
|
displayName: "Webhook URL",
|
|
name: "webhookUrl",
|
|
type: "string",
|
|
default: "",
|
|
description:
|
|
"Absolute https URL — Songs2VID POSTs job.item.* / job.* events",
|
|
},
|
|
{
|
|
displayName: "Song Title (on-video)",
|
|
name: "songTitle",
|
|
type: "string",
|
|
default: "",
|
|
description: "Art-track on-video title (max 120)",
|
|
},
|
|
{
|
|
displayName: "Artist (on-video)",
|
|
name: "artist",
|
|
type: "string",
|
|
default: "",
|
|
description: "Art-track on-video artist (max 80)",
|
|
},
|
|
{
|
|
displayName: "Description",
|
|
name: "description",
|
|
type: "string",
|
|
default: "",
|
|
typeOptions: { rows: 3 },
|
|
},
|
|
{
|
|
displayName: "Tags",
|
|
name: "tags",
|
|
type: "string",
|
|
default: "",
|
|
description: "Comma-separated YouTube tags",
|
|
},
|
|
{
|
|
displayName: "Category ID",
|
|
name: "categoryId",
|
|
type: "string",
|
|
default: "10",
|
|
description: "YouTube category (10 = Music)",
|
|
},
|
|
{
|
|
displayName: "Notify Subscribers",
|
|
name: "notifySubscribers",
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
{
|
|
displayName: "Made for Kids",
|
|
name: "madeForKids",
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
{
|
|
displayName: "Embeddable",
|
|
name: "embeddable",
|
|
type: "boolean",
|
|
default: true,
|
|
},
|
|
{
|
|
displayName: "Creative Commons",
|
|
name: "creativeCommons",
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
{
|
|
displayName: "Include Watermark",
|
|
name: "includeWatermark",
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
{
|
|
displayName: "Per-track Cover Path",
|
|
name: "itemImagePath",
|
|
type: "string",
|
|
default: "",
|
|
description: "Overrides job imagePath for this track",
|
|
},
|
|
{
|
|
displayName: "Background Image Path",
|
|
name: "backgroundImagePath",
|
|
type: "string",
|
|
default: "",
|
|
description: "Lower-corner templates only",
|
|
},
|
|
{
|
|
displayName: "Playlist ID",
|
|
name: "playlistId",
|
|
type: "string",
|
|
default: "",
|
|
},
|
|
{
|
|
displayName: "Create Playlist Title",
|
|
name: "createPlaylistTitle",
|
|
type: "string",
|
|
default: "",
|
|
description: "If set, creates a playlist and attaches all videos",
|
|
},
|
|
{
|
|
displayName: "Create Playlist Description",
|
|
name: "createPlaylistDescription",
|
|
type: "string",
|
|
default: "",
|
|
},
|
|
{
|
|
displayName: "Create Playlist Privacy",
|
|
name: "createPlaylistPrivacy",
|
|
type: "options",
|
|
options: [
|
|
{ name: "public", value: "public" },
|
|
{ name: "unlisted", value: "unlisted" },
|
|
{ name: "private", value: "private" },
|
|
],
|
|
default: "private",
|
|
},
|
|
{
|
|
displayName: "Layout Template",
|
|
name: "layoutTemplate",
|
|
type: "options",
|
|
options: [
|
|
{ name: "(classic letterbox)", value: "" },
|
|
...LAYOUT_TEMPLATES.map((t) => ({ name: t, value: t })),
|
|
],
|
|
default: "",
|
|
},
|
|
{
|
|
displayName: "Blur Amount",
|
|
name: "blurAmount",
|
|
type: "number",
|
|
typeOptions: { minValue: 0, maxValue: 100 },
|
|
default: 55,
|
|
},
|
|
{
|
|
displayName: "Blur Opacity",
|
|
name: "blurOpacity",
|
|
type: "number",
|
|
typeOptions: { minValue: 0, maxValue: 100 },
|
|
default: 100,
|
|
},
|
|
{
|
|
displayName: "Blur Fill (letterbox)",
|
|
name: "blurFill",
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
{
|
|
displayName: "Text Padding",
|
|
name: "textPadding",
|
|
type: "number",
|
|
typeOptions: { minValue: 16, maxValue: 120 },
|
|
default: 48,
|
|
},
|
|
{
|
|
displayName: "Title / Artist Gap",
|
|
name: "titleArtistGap",
|
|
type: "number",
|
|
typeOptions: { minValue: 0, maxValue: 64 },
|
|
default: 10,
|
|
},
|
|
{
|
|
displayName: "Title Bold",
|
|
name: "titleBold",
|
|
type: "boolean",
|
|
default: true,
|
|
},
|
|
{
|
|
displayName: "Text Offset X",
|
|
name: "textOffsetX",
|
|
type: "number",
|
|
typeOptions: { minValue: -120, maxValue: 120 },
|
|
default: 0,
|
|
},
|
|
{
|
|
displayName: "Text Offset Y",
|
|
name: "textOffsetY",
|
|
type: "number",
|
|
typeOptions: { minValue: -120, maxValue: 120 },
|
|
default: 0,
|
|
},
|
|
{
|
|
displayName: "Watermark Mode",
|
|
name: "watermarkMode",
|
|
type: "options",
|
|
options: WATERMARK_MODES.map((m) => ({ name: m, value: m })),
|
|
default: "none",
|
|
},
|
|
{
|
|
displayName: "Watermark Text",
|
|
name: "watermarkText",
|
|
type: "string",
|
|
default: "",
|
|
},
|
|
{
|
|
displayName: "Watermark Logo Path",
|
|
name: "watermarkLogoPath",
|
|
type: "string",
|
|
default: "",
|
|
},
|
|
{
|
|
displayName: "Watermark Position",
|
|
name: "watermarkPosition",
|
|
type: "options",
|
|
options: WATERMARK_POSITIONS.map((p) => ({ name: p, value: p })),
|
|
default: "bottom-right",
|
|
},
|
|
{
|
|
displayName: "Watermark Offset X",
|
|
name: "watermarkOffsetX",
|
|
type: "number",
|
|
typeOptions: { minValue: 0, maxValue: 200 },
|
|
default: 20,
|
|
},
|
|
{
|
|
displayName: "Watermark Offset Y",
|
|
name: "watermarkOffsetY",
|
|
type: "number",
|
|
typeOptions: { minValue: 0, maxValue: 200 },
|
|
default: 20,
|
|
},
|
|
{
|
|
displayName: "Font Key",
|
|
name: "fontKey",
|
|
type: "options",
|
|
options: FONT_KEYS.map((k) => ({ name: k, value: k })),
|
|
default: "montserrat",
|
|
},
|
|
{
|
|
displayName: "Custom Font Path",
|
|
name: "fontPath",
|
|
type: "string",
|
|
default: "",
|
|
description: "Required when Font Key = custom",
|
|
},
|
|
{
|
|
displayName: "Extra Items JSON",
|
|
name: "extraItemsJson",
|
|
type: "json",
|
|
default: "[]",
|
|
description:
|
|
"Optional array of additional { audioPath, audioFilename, metadata } objects for multi-track jobs",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Batch fields
|
|
{
|
|
displayName: "Cover Binary Property",
|
|
name: "batchImageBinary",
|
|
type: "string",
|
|
default: "cover",
|
|
required: true,
|
|
displayOptions: {
|
|
show: { resource: ["render"], operation: ["createBatch"] },
|
|
},
|
|
},
|
|
{
|
|
displayName: "Audio Binary Property",
|
|
name: "batchAudioBinary",
|
|
type: "string",
|
|
default: "audio",
|
|
required: true,
|
|
displayOptions: {
|
|
show: { resource: ["render"], operation: ["createBatch"] },
|
|
},
|
|
description:
|
|
"Binary field name for audio. For multiple files, pass items with this property or use Audio Binary Properties list.",
|
|
},
|
|
{
|
|
displayName: "Additional Fields",
|
|
name: "batchAdditionalFields",
|
|
type: "collection",
|
|
placeholder: "Add Field",
|
|
default: {},
|
|
displayOptions: {
|
|
show: { resource: ["render"], operation: ["createBatch"] },
|
|
},
|
|
options: [
|
|
{
|
|
displayName: "Metadata JSON",
|
|
name: "metadataJson",
|
|
type: "json",
|
|
default:
|
|
'{"defaults":{"privacy":"PUBLIC","resolution":"1920x1080"},"items":[]}',
|
|
description:
|
|
"Multipart metadata string: { defaults?, items?, createPlaylist? }",
|
|
},
|
|
],
|
|
},
|
|
|
|
// ── Playlist ───────────────────────────────────────────────
|
|
{
|
|
displayName: "Operation",
|
|
name: "operation",
|
|
type: "options",
|
|
noDataExpression: true,
|
|
displayOptions: { show: { resource: ["playlist"] } },
|
|
options: [
|
|
{
|
|
name: "List",
|
|
value: "list",
|
|
description: "GET /api/v1/playlists",
|
|
action: "List playlists",
|
|
},
|
|
{
|
|
name: "Create",
|
|
value: "create",
|
|
description: "POST /api/v1/playlists",
|
|
action: "Create a playlist",
|
|
},
|
|
],
|
|
default: "list",
|
|
},
|
|
{
|
|
displayName: "Title",
|
|
name: "playlistTitle",
|
|
type: "string",
|
|
required: true,
|
|
default: "",
|
|
displayOptions: {
|
|
show: { resource: ["playlist"], operation: ["create"] },
|
|
},
|
|
},
|
|
{
|
|
displayName: "Additional Fields",
|
|
name: "playlistAdditionalFields",
|
|
type: "collection",
|
|
placeholder: "Add Field",
|
|
default: {},
|
|
displayOptions: {
|
|
show: { resource: ["playlist"], operation: ["create"] },
|
|
},
|
|
options: [
|
|
{
|
|
displayName: "Description",
|
|
name: "description",
|
|
type: "string",
|
|
default: "",
|
|
},
|
|
{
|
|
displayName: "Privacy",
|
|
name: "privacy",
|
|
type: "options",
|
|
options: [
|
|
{ name: "public", value: "public" },
|
|
{ name: "unlisted", value: "unlisted" },
|
|
{ name: "private", value: "private" },
|
|
],
|
|
default: "private",
|
|
},
|
|
],
|
|
},
|
|
|
|
// ── API Key ────────────────────────────────────────────────
|
|
{
|
|
displayName: "Operation",
|
|
name: "operation",
|
|
type: "options",
|
|
noDataExpression: true,
|
|
displayOptions: { show: { resource: ["apiKey"] } },
|
|
options: [
|
|
{
|
|
name: "List",
|
|
value: "list",
|
|
description: "GET /api/v1/user/api-keys",
|
|
action: "List API keys",
|
|
},
|
|
{
|
|
name: "Create",
|
|
value: "create",
|
|
description: "POST /api/v1/user/api-keys",
|
|
action: "Create an API key",
|
|
},
|
|
{
|
|
name: "Delete",
|
|
value: "delete",
|
|
description: "DELETE /api/v1/user/api-keys",
|
|
action: "Delete API key(s)",
|
|
},
|
|
],
|
|
default: "list",
|
|
},
|
|
{
|
|
displayName: "Name",
|
|
name: "apiKeyName",
|
|
type: "string",
|
|
default: "Default",
|
|
displayOptions: {
|
|
show: { resource: ["apiKey"], operation: ["create"] },
|
|
},
|
|
description: "Optional label (max 64)",
|
|
},
|
|
{
|
|
displayName: "Additional Fields",
|
|
name: "apiKeyAdditionalFields",
|
|
type: "collection",
|
|
placeholder: "Add Field",
|
|
default: {},
|
|
displayOptions: {
|
|
show: { resource: ["apiKey"], operation: ["delete"] },
|
|
},
|
|
options: [
|
|
{
|
|
displayName: "Key ID",
|
|
name: "id",
|
|
type: "string",
|
|
default: "",
|
|
description: "Omit to revoke all keys",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
const items = this.getInputData();
|
|
const returnData: INodeExecutionData[] = [];
|
|
const resource = this.getNodeParameter("resource", 0) as string;
|
|
const operation = this.getNodeParameter("operation", 0) as string;
|
|
|
|
const credentials = await this.getCredentials("songs2VidApi").catch(
|
|
() => null,
|
|
);
|
|
const baseUrl = String(
|
|
credentials?.baseUrl || "https://songs2vid.com",
|
|
).replace(/\/$/, "");
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
try {
|
|
let response: IDataObject | IDataObject[] | undefined;
|
|
|
|
if (resource === "discovery" && operation === "getCatalog") {
|
|
response = (await this.helpers.request({
|
|
method: "GET",
|
|
url: `${baseUrl}/api/v1`,
|
|
json: true,
|
|
} as IRequestOptions)) as IDataObject;
|
|
} else if (resource === "file" && operation === "upload") {
|
|
const binaryPropertyName = this.getNodeParameter(
|
|
"binaryPropertyName",
|
|
i,
|
|
) as string;
|
|
const uploadType = this.getNodeParameter("uploadType", i) as string;
|
|
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
|
const buffer = await this.helpers.getBinaryDataBuffer(
|
|
i,
|
|
binaryPropertyName,
|
|
);
|
|
|
|
response = (await this.helpers.requestWithAuthentication.call(
|
|
this,
|
|
"songs2VidApi",
|
|
{
|
|
method: "POST",
|
|
url: `${baseUrl}/api/v1/upload`,
|
|
formData: {
|
|
file: {
|
|
value: buffer,
|
|
options: {
|
|
filename: binaryData.fileName || "upload.bin",
|
|
contentType: binaryData.mimeType || "application/octet-stream",
|
|
},
|
|
},
|
|
type: uploadType,
|
|
},
|
|
json: true,
|
|
} as IRequestOptions,
|
|
)) as IDataObject;
|
|
} else if (
|
|
resource === "render" &&
|
|
(operation === "create" || operation === "createJob")
|
|
) {
|
|
response = (await this.helpers.requestWithAuthentication.call(
|
|
this,
|
|
"songs2VidApi",
|
|
{
|
|
method: "POST",
|
|
url: `${baseUrl}/api/v1/${operation === "create" ? "render" : "jobs"}`,
|
|
body: buildCreateJobBody(this, i),
|
|
json: true,
|
|
} as IRequestOptions,
|
|
)) as IDataObject;
|
|
} else if (resource === "render" && operation === "get") {
|
|
const jobId = this.getNodeParameter("jobId", i) as string;
|
|
response = (await this.helpers.requestWithAuthentication.call(
|
|
this,
|
|
"songs2VidApi",
|
|
{
|
|
method: "GET",
|
|
url: `${baseUrl}/api/v1/jobs/${encodeURIComponent(jobId)}`,
|
|
json: true,
|
|
} as IRequestOptions,
|
|
)) as IDataObject;
|
|
} else if (resource === "render" && operation === "list") {
|
|
const limit = this.getNodeParameter("limit", i) as number;
|
|
const useAlias = this.getNodeParameter(
|
|
"useRenderAlias",
|
|
i,
|
|
false,
|
|
) as boolean;
|
|
response = (await this.helpers.requestWithAuthentication.call(
|
|
this,
|
|
"songs2VidApi",
|
|
{
|
|
method: "GET",
|
|
url: `${baseUrl}/api/v1/${useAlias ? "render" : "jobs"}`,
|
|
qs: { limit },
|
|
json: true,
|
|
} as IRequestOptions,
|
|
)) as IDataObject;
|
|
} else if (resource === "render" && operation === "createBatch") {
|
|
const imageProp = this.getNodeParameter(
|
|
"batchImageBinary",
|
|
i,
|
|
) as string;
|
|
const audioProp = this.getNodeParameter(
|
|
"batchAudioBinary",
|
|
i,
|
|
) as string;
|
|
const extra = this.getNodeParameter(
|
|
"batchAdditionalFields",
|
|
i,
|
|
{},
|
|
) as IDataObject;
|
|
|
|
const imageBin = this.helpers.assertBinaryData(i, imageProp);
|
|
const imageBuf = await this.helpers.getBinaryDataBuffer(i, imageProp);
|
|
const audioBin = this.helpers.assertBinaryData(i, audioProp);
|
|
const audioBuf = await this.helpers.getBinaryDataBuffer(i, audioProp);
|
|
|
|
const formData: IDataObject = {
|
|
image: {
|
|
value: imageBuf,
|
|
options: {
|
|
filename: imageBin.fileName || "cover.jpg",
|
|
contentType: imageBin.mimeType || "image/jpeg",
|
|
},
|
|
},
|
|
audio: {
|
|
value: audioBuf,
|
|
options: {
|
|
filename: audioBin.fileName || "track.mp3",
|
|
contentType: audioBin.mimeType || "audio/mpeg",
|
|
},
|
|
},
|
|
};
|
|
|
|
if (extra.metadataJson) {
|
|
formData.metadata =
|
|
typeof extra.metadataJson === "string"
|
|
? extra.metadataJson
|
|
: JSON.stringify(extra.metadataJson);
|
|
}
|
|
|
|
response = (await this.helpers.requestWithAuthentication.call(
|
|
this,
|
|
"songs2VidApi",
|
|
{
|
|
method: "POST",
|
|
url: `${baseUrl}/api/v1/jobs/batch`,
|
|
formData,
|
|
json: true,
|
|
} as IRequestOptions,
|
|
)) as IDataObject;
|
|
} else if (resource === "playlist" && operation === "list") {
|
|
response = (await this.helpers.requestWithAuthentication.call(
|
|
this,
|
|
"songs2VidApi",
|
|
{
|
|
method: "GET",
|
|
url: `${baseUrl}/api/v1/playlists`,
|
|
json: true,
|
|
} as IRequestOptions,
|
|
)) as IDataObject;
|
|
} else if (resource === "playlist" && operation === "create") {
|
|
const title = this.getNodeParameter("playlistTitle", i) as string;
|
|
const extra = this.getNodeParameter(
|
|
"playlistAdditionalFields",
|
|
i,
|
|
{},
|
|
) as IDataObject;
|
|
const body: IDataObject = { title };
|
|
if (extra.description) body.description = extra.description;
|
|
if (extra.privacy) body.privacy = extra.privacy;
|
|
response = (await this.helpers.requestWithAuthentication.call(
|
|
this,
|
|
"songs2VidApi",
|
|
{
|
|
method: "POST",
|
|
url: `${baseUrl}/api/v1/playlists`,
|
|
body,
|
|
json: true,
|
|
} as IRequestOptions,
|
|
)) as IDataObject;
|
|
} else if (resource === "apiKey" && operation === "list") {
|
|
response = (await this.helpers.requestWithAuthentication.call(
|
|
this,
|
|
"songs2VidApi",
|
|
{
|
|
method: "GET",
|
|
url: `${baseUrl}/api/v1/user/api-keys`,
|
|
json: true,
|
|
} as IRequestOptions,
|
|
)) as IDataObject;
|
|
} else if (resource === "apiKey" && operation === "create") {
|
|
const name = this.getNodeParameter("apiKeyName", i) as string;
|
|
response = (await this.helpers.requestWithAuthentication.call(
|
|
this,
|
|
"songs2VidApi",
|
|
{
|
|
method: "POST",
|
|
url: `${baseUrl}/api/v1/user/api-keys`,
|
|
body: { name },
|
|
json: true,
|
|
} as IRequestOptions,
|
|
)) as IDataObject;
|
|
} else if (resource === "apiKey" && operation === "delete") {
|
|
const extra = this.getNodeParameter(
|
|
"apiKeyAdditionalFields",
|
|
i,
|
|
{},
|
|
) as IDataObject;
|
|
const qs: IDataObject = {};
|
|
if (extra.id) qs.id = extra.id;
|
|
response = (await this.helpers.requestWithAuthentication.call(
|
|
this,
|
|
"songs2VidApi",
|
|
{
|
|
method: "DELETE",
|
|
url: `${baseUrl}/api/v1/user/api-keys`,
|
|
qs,
|
|
json: true,
|
|
} as IRequestOptions,
|
|
)) as IDataObject;
|
|
} else {
|
|
throw new NodeOperationError(
|
|
this.getNode(),
|
|
`Unknown resource/operation: ${resource}/${operation}`,
|
|
{ itemIndex: i },
|
|
);
|
|
}
|
|
|
|
const executionData = this.helpers.constructExecutionMetaData(
|
|
this.helpers.returnJsonArray(response as IDataObject),
|
|
{ itemData: { item: i } },
|
|
);
|
|
returnData.push(...executionData);
|
|
} catch (error) {
|
|
if (this.continueOnFail()) {
|
|
returnData.push({
|
|
json: {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
},
|
|
pairedItem: { item: i },
|
|
});
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return [returnData];
|
|
}
|
|
}
|
|
|
|
function buildCreateJobBody(
|
|
ctx: IExecuteFunctions,
|
|
itemIndex: number,
|
|
): IDataObject {
|
|
const imagePath = ctx.getNodeParameter("imagePath", itemIndex) as string;
|
|
const audioPath = ctx.getNodeParameter("audioPath", itemIndex) as string;
|
|
const audioFilename = ctx.getNodeParameter(
|
|
"audioFilename",
|
|
itemIndex,
|
|
) as string;
|
|
const title = ctx.getNodeParameter("title", itemIndex) as string;
|
|
const privacy = ctx.getNodeParameter("privacy", itemIndex) as string;
|
|
const resolution = ctx.getNodeParameter("resolution", itemIndex) as string;
|
|
const extra = ctx.getNodeParameter(
|
|
"additionalFields",
|
|
itemIndex,
|
|
{},
|
|
) as IDataObject;
|
|
|
|
const metadata: IDataObject = {
|
|
title,
|
|
privacy,
|
|
resolution,
|
|
};
|
|
|
|
const passthrough = [
|
|
"songTitle",
|
|
"artist",
|
|
"description",
|
|
"tags",
|
|
"categoryId",
|
|
"notifySubscribers",
|
|
"madeForKids",
|
|
"embeddable",
|
|
"creativeCommons",
|
|
"includeWatermark",
|
|
"backgroundImagePath",
|
|
"playlistId",
|
|
] as const;
|
|
|
|
for (const key of passthrough) {
|
|
const value = extra[key];
|
|
if (value !== undefined && value !== null && value !== "") {
|
|
metadata[key] = value;
|
|
}
|
|
}
|
|
if (extra.itemImagePath) {
|
|
metadata.imagePath = extra.itemImagePath;
|
|
}
|
|
|
|
if (extra.layoutTemplate) {
|
|
metadata.layout = {
|
|
template: extra.layoutTemplate,
|
|
blurAmount: extra.blurAmount ?? 55,
|
|
blurOpacity: extra.blurOpacity ?? 100,
|
|
blurFill: extra.blurFill ?? false,
|
|
textPadding: extra.textPadding ?? 48,
|
|
titleArtistGap: extra.titleArtistGap ?? 10,
|
|
titleBold: extra.titleBold ?? true,
|
|
textOffsetX: extra.textOffsetX ?? 0,
|
|
textOffsetY: extra.textOffsetY ?? 0,
|
|
};
|
|
} else if (extra.blurFill === true) {
|
|
metadata.layout = {
|
|
blurFill: true,
|
|
blurAmount: extra.blurAmount ?? 55,
|
|
blurOpacity: extra.blurOpacity ?? 100,
|
|
};
|
|
}
|
|
|
|
if (
|
|
extra.watermarkMode ||
|
|
extra.watermarkText ||
|
|
extra.watermarkLogoPath ||
|
|
extra.fontKey
|
|
) {
|
|
const watermark: IDataObject = {
|
|
mode: (extra.watermarkMode as string) || "none",
|
|
position: (extra.watermarkPosition as string) || "bottom-right",
|
|
offsetX: extra.watermarkOffsetX ?? 20,
|
|
offsetY: extra.watermarkOffsetY ?? 20,
|
|
fontKey: (extra.fontKey as string) || "montserrat",
|
|
};
|
|
if (extra.watermarkText) watermark.text = extra.watermarkText;
|
|
if (extra.watermarkLogoPath) watermark.logoPath = extra.watermarkLogoPath;
|
|
if (extra.fontPath) watermark.fontPath = extra.fontPath;
|
|
metadata.watermark = watermark;
|
|
}
|
|
|
|
const items: IDataObject[] = [
|
|
{
|
|
audioPath,
|
|
audioFilename,
|
|
metadata,
|
|
},
|
|
];
|
|
|
|
if (extra.extraItemsJson) {
|
|
const parsed =
|
|
typeof extra.extraItemsJson === "string"
|
|
? (JSON.parse(extra.extraItemsJson) as IDataObject[])
|
|
: (extra.extraItemsJson as IDataObject[]);
|
|
if (Array.isArray(parsed)) {
|
|
items.push(...parsed);
|
|
}
|
|
}
|
|
|
|
const body: IDataObject = {
|
|
imagePath,
|
|
items,
|
|
};
|
|
|
|
if (extra.webhookUrl) {
|
|
body.webhookUrl = extra.webhookUrl;
|
|
}
|
|
|
|
if (extra.createPlaylistTitle) {
|
|
body.createPlaylist = {
|
|
title: extra.createPlaylistTitle,
|
|
description: extra.createPlaylistDescription || undefined,
|
|
privacy: extra.createPlaylistPrivacy || "private",
|
|
};
|
|
}
|
|
|
|
return body;
|
|
}
|