mirror of
https://github.com/n8n-io/n8n-nodes-starter.git
synced 2025-11-11 19:07:35 -06:00
feat: Initial structure for Suno AI n8n node
I've set up the foundational boilerplate for the Suno AI integration. Key changes include: - Restructured directories for nodes, credentials, interfaces, utils, tests, and docs. - Renamed and updated example files to Suno-specific names and conventions (SunoApi.credentials.ts, Suno.node.ts). - Updated package.json and root README.md for the Suno AI node. - Created .env.example with placeholders for Suno environment variables. - Added a dev-log.md with initial notes on authentication research strategy. - Scaffolded utils/sunoApi.ts with placeholder API functions and JSDoc comments. - Scaffolded nodes/Suno/Suno.node.ts with operations, properties, execute routing, and a placeholder SVG icon. - Scaffolded nodes/Suno/SunoTrigger.node.ts with a basic trigger structure and properties. - Defined initial TypeScript types in interfaces/SunoTypes.ts for common data structures (SunoTrack, SunoJob, etc.). - Created placeholder README.md files in new subdirectories. This commit establishes the project structure and lays the groundwork for implementing Suno AI API interactions and node functionality.
This commit is contained in:
parent
65e06338e3
commit
6c69a287fe
20 changed files with 684 additions and 449 deletions
212
nodes/Suno/Suno.node.ts
Normal file
212
nodes/Suno/Suno.node.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
// Assuming INodeProperties is needed for properties definition
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
// TODO: Import functions from sunoApi.ts when they are implemented
|
||||
// import * as sunoApi from '../../utils/sunoApi';
|
||||
|
||||
export class SunoNode implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Suno',
|
||||
name: 'suno',
|
||||
icon: 'file:suno.svg',
|
||||
group: ['ai'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"]}}',
|
||||
description: 'Integrates with Suno AI for music generation',
|
||||
defaults: {
|
||||
name: 'Suno',
|
||||
},
|
||||
inputs: [NodeConnectionType.Main],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'sunoApi', // Matches the name in SunoApi.credentials.ts
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Generate Song from Prompt',
|
||||
value: 'generateSongFromPrompt',
|
||||
description: 'Create a new song based on a text prompt',
|
||||
action: 'Generate song from prompt',
|
||||
},
|
||||
{
|
||||
name: 'Upload Track Reference',
|
||||
value: 'uploadTrackReference',
|
||||
description: 'Upload a reference track for generation',
|
||||
action: 'Upload track reference',
|
||||
},
|
||||
{
|
||||
name: 'Get Track Status',
|
||||
value: 'getTrackStatus',
|
||||
description: 'Get the status of a generated track',
|
||||
action: 'Get track status',
|
||||
},
|
||||
{
|
||||
name: 'Download Track',
|
||||
value: 'downloadTrack',
|
||||
description: 'Download a generated track',
|
||||
action: 'Download track',
|
||||
},
|
||||
{
|
||||
name: 'List Previous Songs',
|
||||
value: 'listPreviousSongs',
|
||||
description: 'List previously generated songs',
|
||||
action: 'List previous songs',
|
||||
},
|
||||
],
|
||||
default: 'generateSongFromPrompt',
|
||||
},
|
||||
// Properties for 'generateSongFromPrompt'
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'prompt',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g., Epic orchestral score for a space battle',
|
||||
description: 'The text prompt to generate music from',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['generateSongFromPrompt'],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Properties for 'getTrackStatus' and 'downloadTrack'
|
||||
{
|
||||
displayName: 'Track ID',
|
||||
name: 'trackId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'Enter Track ID',
|
||||
description: 'The ID of the track',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['getTrackStatus', 'downloadTrack'],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Properties for 'uploadTrackReference'
|
||||
{
|
||||
displayName: 'File Path',
|
||||
name: 'filePath',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: '/path/to/your/audio.mp3',
|
||||
description: 'Path to the audio file to upload as reference',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['uploadTrackReference'],
|
||||
},
|
||||
},
|
||||
},
|
||||
] as INodeProperties[], // Cast to INodeProperties[]
|
||||
};
|
||||
|
||||
/**
|
||||
* Placeholder method for generating a song from a prompt.
|
||||
* @param {IExecuteFunctions} this - The execution context.
|
||||
* @returns {Promise<INodeExecutionData[][]>}
|
||||
*/
|
||||
async generateSongFromPrompt(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const prompt = this.getNodeParameter('prompt', 0, '') as string;
|
||||
// TODO: Call the appropriate sunoApi.ts function (sunoApi.submitPrompt) and handle response
|
||||
console.log('Executing generateSongFromPrompt with prompt:', prompt);
|
||||
// For now, return empty data
|
||||
return [this.prepareOutputData([])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder method for uploading a track reference.
|
||||
* @param {IExecuteFunctions} this - The execution context.
|
||||
* @returns {Promise<INodeExecutionData[][]>}
|
||||
*/
|
||||
async uploadTrackReference(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const filePath = this.getNodeParameter('filePath', 0, '') as string;
|
||||
// TODO: Call the appropriate sunoApi.ts function (sunoApi.uploadReferenceTrack) and handle response
|
||||
console.log('Executing uploadTrackReference with filePath:', filePath);
|
||||
return [this.prepareOutputData([])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder method for getting track status.
|
||||
* @param {IExecuteFunctions} this - The execution context.
|
||||
* @returns {Promise<INodeExecutionData[][]>}
|
||||
*/
|
||||
async getTrackStatus(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const trackId = this.getNodeParameter('trackId', 0, '') as string;
|
||||
// TODO: Call the appropriate sunoApi.ts function (sunoApi.pollJobStatus or similar) and handle response
|
||||
console.log('Executing getTrackStatus with trackId:', trackId);
|
||||
return [this.prepareOutputData([])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder method for downloading a track.
|
||||
* @param {IExecuteFunctions} this - The execution context.
|
||||
* @returns {Promise<INodeExecutionData[][]>}
|
||||
*/
|
||||
async downloadTrack(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const trackId = this.getNodeParameter('trackId', 0, '') as string;
|
||||
// TODO: Call the appropriate sunoApi.ts function (sunoApi.downloadTrack) and handle response
|
||||
// TODO: Consider how to handle binary data output in n8n
|
||||
console.log('Executing downloadTrack with trackId:', trackId);
|
||||
return [this.prepareOutputData([])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder method for listing previous songs.
|
||||
* @param {IExecuteFunctions} this - The execution context.
|
||||
* @returns {Promise<INodeExecutionData[][]>}
|
||||
*/
|
||||
async listPreviousSongs(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
// TODO: Call the appropriate sunoApi.ts function (sunoApi.listPreviousSongs) and handle response
|
||||
console.log('Executing listPreviousSongs');
|
||||
return [this.prepareOutputData([])];
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const operation = this.getNodeParameter('operation', 0, '') as string;
|
||||
|
||||
try {
|
||||
switch (operation) {
|
||||
case 'generateSongFromPrompt':
|
||||
return this.generateSongFromPrompt(this);
|
||||
case 'uploadTrackReference':
|
||||
return this.uploadTrackReference(this);
|
||||
case 'getTrackStatus':
|
||||
return this.getTrackStatus(this);
|
||||
case 'downloadTrack':
|
||||
return this.downloadTrack(this);
|
||||
case 'listPreviousSongs':
|
||||
return this.listPreviousSongs(this);
|
||||
default:
|
||||
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not supported.`);
|
||||
}
|
||||
} catch (error) {
|
||||
// This node should use NodeOperationError when an error occurs directly within this node's execution logic
|
||||
if (this.continueOnFail()) {
|
||||
// Return error data as per n8n's guidelines for allowing the workflow to continue
|
||||
const item = this.getInputData(0)[0]; // Get the first item if available, otherwise undefined
|
||||
return [this.prepareOutputData([{ json: {}, error: error, pairedItem: item ? { item: 0 } : undefined }])];
|
||||
} else {
|
||||
// If not continuing on fail, rethrow the error to halt the workflow
|
||||
throw error; // NodeOperationError should already be structured correctly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
nodes/Suno/SunoTrigger.node.ts
Normal file
132
nodes/Suno/SunoTrigger.node.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import type {
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
ITriggerFunctions,
|
||||
ITriggerResponse,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class SunoTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Suno Trigger',
|
||||
name: 'sunoTrigger',
|
||||
icon: 'file:suno.svg', // Re-use the icon
|
||||
group: ['trigger', 'ai'],
|
||||
version: 1,
|
||||
description: 'Triggers when a Suno AI event occurs',
|
||||
defaults: {
|
||||
name: 'Suno Trigger',
|
||||
},
|
||||
inputs: [], // Triggers usually do not have inputs
|
||||
outputs: ['main'], // Main output for triggered data
|
||||
credentials: [
|
||||
{
|
||||
name: 'sunoApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
// Define properties for the trigger
|
||||
{
|
||||
displayName: 'Trigger Event',
|
||||
name: 'triggerEvent',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Track Generation Complete',
|
||||
value: 'trackGenerationComplete',
|
||||
description: 'Triggers when a specific track finishes generation',
|
||||
},
|
||||
{
|
||||
name: 'New Song Available',
|
||||
value: 'newSongAvailable',
|
||||
description: 'Triggers when any new song is available in the library (polling)',
|
||||
},
|
||||
],
|
||||
default: 'trackGenerationComplete',
|
||||
description: 'The Suno event that will trigger this node',
|
||||
},
|
||||
{
|
||||
displayName: 'Track ID',
|
||||
name: 'trackId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The ID of the track to monitor for completion',
|
||||
displayOptions: {
|
||||
show: {
|
||||
triggerEvent: ['trackGenerationComplete'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Polling Interval (minutes)',
|
||||
name: 'pollingInterval',
|
||||
type: 'number',
|
||||
default: 5,
|
||||
description: 'How often to check for new songs (if applicable)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
triggerEvent: ['newSongAvailable'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Placeholder for trigger methods
|
||||
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse | undefined> {
|
||||
// const credentials = await this.getCredentials('sunoApi');
|
||||
// const triggerEvent = this.getNodeParameter('triggerEvent') as string;
|
||||
// const trackId = this.getNodeParameter('trackId') as string;
|
||||
// const pollingInterval = this.getNodeParameter('pollingInterval') as number;
|
||||
|
||||
// TODO: Implement actual trigger logic based on triggerEvent
|
||||
// For 'trackGenerationComplete', this might involve polling pollJobStatus(trackId)
|
||||
// For 'newSongAvailable', this might involve polling listPreviousSongs() and checking for new entries
|
||||
|
||||
// If webhook based, this method would be different,
|
||||
// this.emit([this.helpers.returnJsonArray([{ eventData: 'example' }])]);
|
||||
// this.on('close', () => { /* remove webhook */ });
|
||||
// return { webhookId: 'your-webhook-id' };
|
||||
|
||||
// For polling triggers, this method might not be used directly if using manualTriggerFunction
|
||||
if (this.manualTriggerFunction) { // Corrected placeholder name
|
||||
// Manual trigger logic for polling
|
||||
// This will be called by n8n based on the schedule if `manualTriggerFunction` is defined
|
||||
// Example:
|
||||
// const items = await pollSunoApiForUpdates();
|
||||
// if (items.length > 0) {
|
||||
// return {
|
||||
// items: this.helpers.returnJsonArray(items),
|
||||
// };
|
||||
// }
|
||||
// return undefined; // No new items
|
||||
}
|
||||
|
||||
// For now, returning undefined as it's a placeholder
|
||||
// For a polling trigger, you might set up an interval here or use manualTriggerFunction
|
||||
// For a webhook trigger, you would register the webhook here.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Example of how a manual trigger function might be structured for polling
|
||||
// async manualTrigger(this: ITriggerFunctions): Promise<INodeExecutionData[][] | undefined> {
|
||||
// const triggerEvent = this.getNodeParameter('triggerEvent') as string;
|
||||
// // ... get other params and credentials
|
||||
//
|
||||
// if (triggerEvent === 'newSongAvailable') {
|
||||
// console.log('Polling for new songs...');
|
||||
// // const newSongs = await sunoApi.listPreviousSongs(credentials, /* potential pagination params */);
|
||||
// // Check against previously seen songs (requires state management, complex for this scaffold)
|
||||
// // For now, let's simulate finding one new song:
|
||||
// // const simulatedNewSong = [{ id: 'new_track_123', title: 'A New Song', status: 'complete' }];
|
||||
// // return [this.helpers.returnJsonArray(simulatedNewSong)];
|
||||
// } else if (triggerEvent === 'trackGenerationComplete') {
|
||||
// // const trackId = this.getNodeParameter('trackId') as string;
|
||||
// // const status = await sunoApi.pollJobStatus(credentials, trackId);
|
||||
// // if (status && status.status === 'complete') {
|
||||
// // return [this.helpers.returnJsonArray([status])];
|
||||
// // }
|
||||
// }
|
||||
// return undefined; // No trigger event
|
||||
// }
|
||||
}
|
||||
1
nodes/Suno/suno.svg
Normal file
1
nodes/Suno/suno.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="blue" /><text x="50" y="50" font-size="30" fill="white" text-anchor="middle" dy=".3em">Suno</text></svg>
|
||||
|
After Width: | Height: | Size: 204 B |
Loading…
Add table
Add a link
Reference in a new issue