mirror of
https://github.com/n8n-io/n8n-nodes-starter.git
synced 2025-11-01 15:52:24 -05:00
Jules was unable to complete the task in time. Please review the work done so far and provide feedback for Jules to continue.
This commit is contained in:
parent
6c69a287fe
commit
18768ebb98
5 changed files with 592 additions and 256 deletions
|
|
@ -3,13 +3,12 @@ import type {
|
|||
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';
|
||||
import * as sunoApi from '../../utils/sunoApi';
|
||||
import type { SunoPromptOptions } from '../../interfaces/SunoTypes';
|
||||
|
||||
export class SunoNode implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
|
@ -27,7 +26,7 @@ export class SunoNode implements INodeType {
|
|||
outputs: [NodeConnectionType.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'sunoApi', // Matches the name in SunoApi.credentials.ts
|
||||
name: 'sunoApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
|
@ -38,6 +37,12 @@ export class SunoNode implements INodeType {
|
|||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Login',
|
||||
value: 'login',
|
||||
description: 'Perform login to Suno API (for testing)',
|
||||
action: 'Login',
|
||||
},
|
||||
{
|
||||
name: 'Generate Song from Prompt',
|
||||
value: 'generateSongFromPrompt',
|
||||
|
|
@ -71,12 +76,37 @@ export class SunoNode implements INodeType {
|
|||
],
|
||||
default: 'generateSongFromPrompt',
|
||||
},
|
||||
// Properties for 'login' (if not using credentials directly for this action)
|
||||
{
|
||||
displayName: 'Email (for Login Operation)',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['login'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Password (for Login Operation)',
|
||||
name: 'password',
|
||||
type: 'string',
|
||||
typeOptions: { password: true },
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['login'],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Properties for 'generateSongFromPrompt'
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'prompt',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'e.g., Epic orchestral score for a space battle',
|
||||
description: 'The text prompt to generate music from',
|
||||
displayOptions: {
|
||||
|
|
@ -85,6 +115,8 @@ export class SunoNode implements INodeType {
|
|||
},
|
||||
},
|
||||
},
|
||||
// TODO: Add more options for prompt generation if desired
|
||||
// e.g., style, instrumental, mood
|
||||
// Properties for 'getTrackStatus' and 'downloadTrack'
|
||||
{
|
||||
displayName: 'Track ID',
|
||||
|
|
@ -115,68 +147,134 @@ export class SunoNode implements INodeType {
|
|||
},
|
||||
},
|
||||
},
|
||||
] as INodeProperties[], // Cast to INodeProperties[]
|
||||
{
|
||||
displayName: 'Binary Property Name (for Download)',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'sunoTrackAudio',
|
||||
description: 'Name of the binary property where audio data will be stored for download operation.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['downloadTrack'],
|
||||
},
|
||||
},
|
||||
},
|
||||
] as INodeProperties[],
|
||||
};
|
||||
|
||||
/**
|
||||
* Placeholder method for generating a song from a prompt.
|
||||
* Performs login to the Suno API.
|
||||
* @param {IExecuteFunctions} this - The execution context.
|
||||
* @returns {Promise<INodeExecutionData[][]>}
|
||||
*/
|
||||
async login(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const email = this.getNodeParameter('email', 0, '') as string;
|
||||
const password = this.getNodeParameter('password', 0, '') as string;
|
||||
try {
|
||||
const response = await sunoApi.loginWithCredentials(email, password);
|
||||
return [this.helpers.returnJsonArray([response])];
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new NodeOperationError(this.getNode(), error, { itemIndex: 0 });
|
||||
}
|
||||
throw new NodeOperationError(this.getNode(), new Error(String(error)), { itemIndex: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a song from a prompt using the Suno API.
|
||||
* @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([])];
|
||||
const promptText = this.getNodeParameter('prompt', 0, '') as string;
|
||||
// const options: SunoPromptOptions = {}; // TODO: Get from node parameters if added
|
||||
try {
|
||||
const job = await sunoApi.submitPrompt(promptText /*, options */);
|
||||
return [this.helpers.returnJsonArray([job])];
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new NodeOperationError(this.getNode(), error, { itemIndex: 0 });
|
||||
}
|
||||
throw new NodeOperationError(this.getNode(), new Error(String(error)), { itemIndex: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder method for uploading a track reference.
|
||||
* Uploads a track reference to the Suno API.
|
||||
* @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([])];
|
||||
try {
|
||||
// TODO: Add options if necessary
|
||||
const result = await sunoApi.uploadReferenceTrack(filePath);
|
||||
return [this.helpers.returnJsonArray([result])];
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new NodeOperationError(this.getNode(), error, { itemIndex: 0 });
|
||||
}
|
||||
throw new NodeOperationError(this.getNode(), new Error(String(error)), { itemIndex: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder method for getting track status.
|
||||
* Gets the status of a track from the Suno API.
|
||||
* @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([])];
|
||||
try {
|
||||
const status = await sunoApi.pollJobStatus(trackId);
|
||||
return [this.helpers.returnJsonArray([status])];
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new NodeOperationError(this.getNode(), error, { itemIndex: 0 });
|
||||
}
|
||||
throw new NodeOperationError(this.getNode(), new Error(String(error)), { itemIndex: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder method for downloading a track.
|
||||
* Downloads a track from the Suno API.
|
||||
* @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([])];
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0, 'sunoTrackAudio') as string;
|
||||
try {
|
||||
const audioBuffer = await sunoApi.downloadTrack(trackId);
|
||||
const executionData = this.helpers.returnJsonArray([{ trackId, message: 'Audio data attached in binary property.' }])[0];
|
||||
if (executionData.json) { // Ensure json property exists
|
||||
executionData.binary = { [binaryPropertyName]: await this.helpers.prepareBinaryData(audioBuffer, binaryPropertyName + '.mp3') };
|
||||
}
|
||||
return [[executionData]];
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new NodeOperationError(this.getNode(), error, { itemIndex: 0 });
|
||||
}
|
||||
throw new NodeOperationError(this.getNode(), new Error(String(error)), { itemIndex: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder method for listing previous songs.
|
||||
* Lists previous songs from the Suno API.
|
||||
* @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([])];
|
||||
try {
|
||||
const songs = await sunoApi.listPreviousSongs();
|
||||
return [this.helpers.returnJsonArray(songs)];
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new NodeOperationError(this.getNode(), error, { itemIndex: 0 });
|
||||
}
|
||||
throw new NodeOperationError(this.getNode(), new Error(String(error)), { itemIndex: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
|
|
@ -184,6 +282,8 @@ export class SunoNode implements INodeType {
|
|||
|
||||
try {
|
||||
switch (operation) {
|
||||
case 'login':
|
||||
return this.login(this);
|
||||
case 'generateSongFromPrompt':
|
||||
return this.generateSongFromPrompt(this);
|
||||
case 'uploadTrackReference':
|
||||
|
|
@ -198,14 +298,11 @@ export class SunoNode implements INodeType {
|
|||
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
|
||||
const item = this.getInputData(0)?.[0];
|
||||
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
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,21 +3,29 @@ import type {
|
|||
INodeTypeDescription,
|
||||
ITriggerFunctions,
|
||||
ITriggerResponse,
|
||||
ICredentialDataDecryptedObject, // Added
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow'; // Added
|
||||
|
||||
import {
|
||||
pollJobStatus,
|
||||
listPreviousSongs,
|
||||
loginWithCredentials,
|
||||
} from '../../utils/sunoApi';
|
||||
|
||||
export class SunoTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Suno Trigger',
|
||||
name: 'sunoTrigger',
|
||||
icon: 'file:suno.svg', // Re-use the icon
|
||||
icon: 'file:suno.svg',
|
||||
group: ['trigger', 'ai'],
|
||||
version: 1,
|
||||
description: 'Triggers when a Suno AI event occurs',
|
||||
description: 'Triggers when a Suno AI event occurs (polling)',
|
||||
defaults: {
|
||||
name: 'Suno Trigger',
|
||||
},
|
||||
inputs: [], // Triggers usually do not have inputs
|
||||
outputs: ['main'], // Main output for triggered data
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'sunoApi',
|
||||
|
|
@ -25,7 +33,6 @@ export class SunoTrigger implements INodeType {
|
|||
},
|
||||
],
|
||||
properties: [
|
||||
// Define properties for the trigger
|
||||
{
|
||||
displayName: 'Trigger Event',
|
||||
name: 'triggerEvent',
|
||||
|
|
@ -46,7 +53,7 @@ export class SunoTrigger implements INodeType {
|
|||
description: 'The Suno event that will trigger this node',
|
||||
},
|
||||
{
|
||||
displayName: 'Track ID',
|
||||
displayName: 'Track ID (for Track Generation Complete)',
|
||||
name: 'trackId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
|
|
@ -61,72 +68,102 @@ export class SunoTrigger implements INodeType {
|
|||
displayName: 'Polling Interval (minutes)',
|
||||
name: 'pollingInterval',
|
||||
type: 'number',
|
||||
default: 5,
|
||||
description: 'How often to check for new songs (if applicable)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
triggerEvent: ['newSongAvailable'],
|
||||
},
|
||||
},
|
||||
default: 5, // Default to 5 minutes
|
||||
description: 'How often to check for events. n8n will manage the actual polling schedule based on this.',
|
||||
// displayOptions: { // Not strictly needed to show for both, but can be kept
|
||||
// show: {
|
||||
// triggerEvent: ['newSongAvailable', 'trackGenerationComplete'],
|
||||
// },
|
||||
// },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Placeholder for trigger methods
|
||||
/**
|
||||
* The `trigger` method is called when the workflow is activated.
|
||||
* For polling triggers using `manualTriggerFunction`, this method can be used
|
||||
* for initial setup, like setting the polling interval or initial authentication.
|
||||
*/
|
||||
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;
|
||||
const pollingIntervalMinutes = this.getNodeParameter('pollingInterval', 5) as number;
|
||||
this.setPollingInterval(pollingIntervalMinutes * 60 * 1000); // Set n8n's polling interval
|
||||
|
||||
// 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
|
||||
try {
|
||||
const credentials = await this.getCredentials('sunoApi') as ICredentialDataDecryptedObject;
|
||||
if (!credentials || !credentials.email || !credentials.password) {
|
||||
throw new NodeOperationError(this.getNode(), 'Suno API credentials are not configured or incomplete.');
|
||||
}
|
||||
// Perform an initial login to ensure credentials are valid and API is reachable
|
||||
await loginWithCredentials(credentials.email as string, credentials.password as string);
|
||||
console.log('SunoTrigger: Initial authentication successful for polling setup.');
|
||||
} catch (error) {
|
||||
console.error('SunoTrigger: Initial authentication or setup failed.', error);
|
||||
// Depending on how n8n handles this, we might throw or just log.
|
||||
// For a trigger, throwing here might prevent it from starting.
|
||||
if (error instanceof NodeOperationError) throw error;
|
||||
throw new NodeOperationError(this.getNode(), `Initial setup failed: ${error.message || String(error)}`);
|
||||
}
|
||||
|
||||
// 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;
|
||||
// For pure polling, manualTriggerFunction will handle the periodic checks.
|
||||
// If specific cleanup is needed when the workflow deactivates, return a closeFunction.
|
||||
return {
|
||||
closeFunction: async () => {
|
||||
console.log('SunoTrigger: Polling stopped.');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
// }
|
||||
/**
|
||||
* `manualTriggerFunction` is called by n8n at the defined polling interval.
|
||||
* It should fetch data and emit items if new events are found.
|
||||
*/
|
||||
public async manualTrigger(this: ITriggerFunctions): Promise<void> {
|
||||
const triggerEvent = this.getNodeParameter('triggerEvent') as string;
|
||||
|
||||
try {
|
||||
const credentials = await this.getCredentials('sunoApi') as ICredentialDataDecryptedObject;
|
||||
if (!credentials || !credentials.email || !credentials.password) {
|
||||
console.error('SunoTrigger: Credentials not available for polling.');
|
||||
// Optionally emit an error to the workflow execution log, but be careful not to flood it.
|
||||
// this.emit([this.helpers.returnJsonArray([{ error: 'Credentials missing for polling' }])]);
|
||||
return; // Stop further execution for this poll if creds are missing
|
||||
}
|
||||
|
||||
// Ensure we are "logged in" for each poll execution, as the token state is managed in sunoApi.ts
|
||||
// and might "expire" or the trigger instance could be new.
|
||||
await loginWithCredentials(credentials.email as string, credentials.password as string);
|
||||
|
||||
if (triggerEvent === 'trackGenerationComplete') {
|
||||
const trackId = this.getNodeParameter('trackId', '') as string;
|
||||
if (!trackId) {
|
||||
console.warn('SunoTrigger: Track ID not provided for "Track Generation Complete" event. Skipping poll.');
|
||||
return;
|
||||
}
|
||||
const jobStatus = await pollJobStatus(trackId);
|
||||
// Simple mock: emit if the job is 'complete' and has a trackId.
|
||||
// A real implementation would need state to avoid re-emitting for the same completed track.
|
||||
if (jobStatus && jobStatus.status === 'complete' && jobStatus.trackId) {
|
||||
console.log(`SunoTrigger: Track ${jobStatus.trackId} (Job ID: ${jobStatus.id}) is complete. Emitting.`);
|
||||
this.emit([this.helpers.returnJsonArray([jobStatus])]);
|
||||
} else {
|
||||
console.log(`SunoTrigger: Track ID ${trackId} status: ${jobStatus.status || 'unknown'}. Not emitting.`);
|
||||
}
|
||||
} else if (triggerEvent === 'newSongAvailable') {
|
||||
const songs = await listPreviousSongs();
|
||||
// Simple mock: if songs are found, emit the first one.
|
||||
// A real implementation needs sophisticated state management to detect "new" songs
|
||||
// (e.g., comparing against IDs seen in the previous poll).
|
||||
if (songs && songs.length > 0) {
|
||||
console.log('SunoTrigger: New songs found (mock implementation). Emitting the first song from the list.');
|
||||
this.emit([this.helpers.returnJsonArray([songs[0]])]);
|
||||
} else {
|
||||
console.log('SunoTrigger: No new songs found (mock implementation).');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SunoTrigger: Error during polling execution:', error);
|
||||
// Optionally emit an error item to the workflow execution log
|
||||
// this.emit([this.helpers.returnJsonArray([{ error: `Polling error: ${error.message || String(error)}` }])]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue