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:
google-labs-jules[bot] 2025-06-11 16:09:26 +00:00
commit 18768ebb98
5 changed files with 592 additions and 256 deletions

View file

@ -1,23 +1,21 @@
import {
IAuthenticateGeneric,
ICredentialTestRequest,
// IAuthenticateGeneric, // Removed as 'authenticate' block is removed
// ICredentialTestRequest, // Removed as 'test' block is removed
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class ExampleCredentialsApi implements ICredentialType {
name = 'exampleCredentialsApi';
displayName = 'Example Credentials API';
// import * as sunoApi from '../../utils/sunoApi'; // Would be used for a real test
documentationUrl = 'https://your-docs-url';
export class SunoApi implements ICredentialType { // Renamed class
name = 'sunoApi'; // Renamed
displayName = 'Suno API'; // Renamed
documentationUrl = 'https://suno.ai/'; // Updated URL
properties: INodeProperties[] = [
// The credentials to get from user and save encrypted.
// Properties can be defined exactly in the same way
// as node properties.
{
displayName: 'User Name',
name: 'username',
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
},
@ -32,28 +30,12 @@ export class ExampleCredentialsApi implements ICredentialType {
},
];
// This credential is currently not used by any node directly
// but the HTTP Request node can use it to make requests.
// The credential is also testable due to the `test` property below
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
auth: {
username: '={{ $credentials.username }}',
password: '={{ $credentials.password }}',
},
qs: {
// Send this as part of the query string
n8n: 'rocks',
},
},
};
// The 'authenticate' object is removed for this phase.
// Authentication will be handled by functions in utils/sunoApi.ts.
// authenticate: IAuthenticateGeneric = { ... };
// The block below tells how this credential can be tested
test: ICredentialTestRequest = {
request: {
baseURL: 'https://example.com/',
url: '',
},
};
// The 'test' object is removed for this phase.
// It will be added back when actual API endpoints for testing are known
// or if a more suitable mock test can be devised without calling sunoApi.ts directly.
// test: ICredentialTestRequest = { ... };
}

View file

@ -79,3 +79,169 @@ Based on these typical patterns and hypotheses, the next steps will involve:
3. **Credential Management:** Ensure that sensitive information like passwords and tokens are handled securely and align with n8n's credential system.
This structured approach allows for progress in development while anticipating the need for adaptation once concrete details are available from the manual investigation phase.
## 2024-07-29: Initial Project Context & SDLC Analysis
This entry analyzes the current state of the `n8n-nodes-suno-ai` project to understand its context within the Software Development Life Cycle (SDLC).
* **Project Name:** n8n-nodes-suno-ai (derived from `package.json`)
* **Stated Purpose:** "custom n8n node for interacting with the Suno AI music generation service" (from `README.md`).
* **Version:** 0.1.0 (from `package.json`).
* **SDLC Stage Inference:**
* The version `0.1.0` and the `README.md` statement "This node is currently **under development**" strongly suggest the project is in an **early development phase** or **initial implementation stage**.
* It is likely pre-MVP (Minimum Viable Product) or in the process of building towards an initial MVP. The current focus on scaffolding, mock implementations, and foundational structure (like this analysis) supports this.
* **Development Methodology Clues:**
* **Structured TypeScript Development:** The presence of `package.json` with scripts for `lint` (ESLint), `format` (Prettier), `build` (TypeScript Compiler `tsc`), and `dev` (using `tsc --watch`) indicates a modern, structured approach to TypeScript development.
* **Code Quality Focus:** The explicit use of ESLint and Prettier from the outset suggests a commitment to code quality, consistency, and maintainability.
* **Build System:** `gulpfile.js` is present, and the `package.json` build script includes `gulp build:icons`, indicating Gulp is used for specific build tasks, likely related to n8n node assets.
* **NPM Publishing Awareness:** The `prepublishOnly` script in `package.json` shows an understanding of the npm package publishing lifecycle, even at this early stage.
* **n8n Custom Node Structure:** The directory structure (`nodes`, `credentials`, `interfaces`, `utils`, `tests`) is conventional for n8n custom node development and was established through the initial project setup and subsequent scaffolding tasks.
* **Iterative Refinement:** The current request for a "Senior Software Engineer" review and refactoring (which this series of tasks represents) indicates a phase where the project is focusing on solidifying its architectural foundation and code quality before expanding features or considering a public release. This iterative refinement is a good sign.
* **CI/CD:**
* No dedicated CI/CD configuration files (e.g., `.github/workflows/main.yml`, `Jenkinsfile`, `.gitlab-ci.yml`) were found in the root directory during `ls()` checks.
* This suggests that a formal, automated CI/CD pipeline is likely not yet implemented. Given the early stage, this is not unusual, but it would be a key area for future improvement to automate testing, building, and potentially publishing.
* **Overall Maturity:**
* The project is clearly in its **infancy** but is being established with sound software engineering practices (TypeScript, linting, formatting, structured layout).
* The current set of activities (scaffolding, mock API implementation, test structure creation, and this analysis) is aimed at building a robust foundation for future development and feature implementation.
* The focus is on getting the "skeleton" right before adding significant "flesh" to it.
* Key next steps from an SDLC perspective would involve:
1. Actual API integration (moving from mock to real).
2. More comprehensive unit and integration testing.
3. Setting up a CI/CD pipeline.
4. User/developer documentation beyond the basic README.
5. Gathering feedback if an early version is shared.
## 2024-07-29: Functional Architecture & Structure Audit - Initial Pass
This entry provides an initial audit of the project's functional architecture and code structure.
* **Overall Directory Structure:**
* `.vscode/`: Editor configuration (e.g., `extensions.json`). Standard.
* `credentials/`: Contains n8n credential types.
* `SunoApi.credentials.ts`: Defines fields for Suno authentication (email/password).
* `HttpBinApi.credentials.ts`: Appears to be a leftover example credential, potentially unused. **(Action Item: Verify and remove if unused)**.
* `docs/`: Project documentation.
* `dev-log.md`: This development log.
* `interfaces/`: TypeScript type definitions.
* `SunoTypes.ts`: Currently populated with `SunoTrack`, `SunoJob`, `SunoAuthResponse`, `SunoPromptOptions`. This is good.
* `README.md`: Placeholder.
* `nodes/`: Contains n8n node implementations.
* `Suno/`: Specific directory for the Suno integration.
* `Suno.node.ts`: Implements the main operational logic for interacting with Suno (generate, status, download, etc.).
* `SunoTrigger.node.ts`: Implements trigger logic for Suno events (e.g., track completion).
* `suno.svg`: Icon for the Suno node.
* `README.md`: Placeholder.
* `tests/`: Test files.
* `checkEndpoints.ts`: Script for basic (currently mocked) API endpoint checks.
* `README.md`: Placeholder.
* `utils/`: Utility scripts and modules.
* `sunoApi.ts`: Core module for encapsulating all (currently mocked) API calls to Suno. Handles authentication token management internally.
* `README.md`: Placeholder.
* Root files: `package.json`, `tsconfig.json`, `.eslintrc.js`, `.prettierrc.js`, `gulpfile.js`, `index.js` (empty), `README.md`, etc. define the project, build process, and standards.
* **Core Modules & Responsibilities (Initial Thoughts):**
* **`SunoApi.credentials.ts` (Credentials Module):**
* Interface: Defines how n8n collects and stores Suno credentials (email, password).
* Processing: Securely provides these credentials to other parts of the node when required.
* Output: Credential data for authentication.
* **`utils/sunoApi.ts` (API Interaction Module):**
* Interface: Exports functions for specific Suno actions (login, submitPrompt, pollJobStatus, etc.).
* Processing:
* Manages authentication state (stores/retrieves a session token - currently mocked).
* Constructs and (will construct) actual API requests.
* Parses responses and (will parse) actual API responses.
* Handles API-level errors.
* Output: Data from Suno API (e.g., job status, track info, audio buffer - currently mocked) or throws errors.
* *This is the primary candidate for Domain-Driven Design's "Service" or "Repository" pattern for the Suno external system.*
* **`nodes/Suno/Suno.node.ts` (n8n Action Node Module):**
* Interface: Defines node properties (UI in n8n) for different operations.
* Processing:
* Takes user input from n8n.
* Uses `utils/sunoApi.ts` to perform actions based on the selected operation.
* Formats results for n8n output, including binary data handling.
* Output: JSON data or binary data to the n8n workflow.
* **`nodes/Suno/SunoTrigger.node.ts` (n8n Trigger Node Module):**
* Interface: Defines trigger properties (UI in n8n).
* Processing:
* Manages polling schedule (using n8n's `manualTriggerFunction`).
* Uses `utils/sunoApi.ts` to check for events (track completion, new songs).
* (Will need state management to avoid duplicate triggers for "New Song Available").
* Output: Emits data to start n8n workflows.
* **`interfaces/SunoTypes.ts` (Data Transfer Objects Module):**
* Interface: Defines TypeScript types for data exchanged with the Suno API and within the node.
* *This is crucial for type safety and clarity. It has been populated, which is a positive step.*
* **Key Data Flows (Example - Generate Song):**
1. User configures "Suno" node in n8n UI for "Generate Song from Prompt", enters prompt.
2. `Suno.node.ts` (`execute` method for 'generateSongFromPrompt'):
* Retrieves prompt parameter.
* Retrieves credentials via `this.getCredentials('sunoApi')`.
* Calls `sunoApi.loginWithCredentials(email, password)` (implicitly done as `sunoApi.ts` handles token persistence after initial login, or explicitly if the node's "login" operation is used first by the user).
* Calls `sunoApi.submitPrompt(promptText)`.
3. `utils/sunoApi.ts` (`submitPrompt` function):
* Checks `isAuthenticated()`.
* (Future: Constructs actual HTTP request to Suno API with prompt and auth token).
* (Future: Receives response, e.g., a job ID).
* Returns mocked `SunoJob` object.
4. `Suno.node.ts`:
* Receives `SunoJob` object.
* Formats it using `this.helpers.returnJsonArray()`.
* Returns data to n8n workflow.
* **Initial Architectural Observations:**
* The separation of concerns between node logic (`Suno.node.ts`), API interaction (`sunoApi.ts`), and credential definition (`SunoApi.credentials.ts`) is good and follows n8n best practices.
* The use of a utility module (`sunoApi.ts`) for all external communication is a key architectural strength, centralizing where API knowledge resides.
* The population of `SunoTypes.ts` for typed data exchange is a positive development.
* `index.js` being empty is fine as `package.json` handles node/credential registration.
* The `HttpBinApi.credentials.ts` file seems out of place for a Suno-specific node and should be reviewed for removal.
* The `tests/checkEndpoints.ts` provides a good starting point for functional/integration testing of the API utility module, even with mocked endpoints.
* State management for triggers (e.g., to prevent duplicate "New Song Available" events) is noted as a future consideration for `SunoTrigger.node.ts`.
## 2024-07-29: Code Quality & Style - Initial Review
This entry provides an initial review of the project's code quality and style, based on the current state of the codebase.
* **Overall Style and Formatting:**
* The codebase uses TypeScript, promoting type safety.
* The presence of `.eslintrc.js` and `.prettierrc.js` (and associated scripts in `package.json`) confirms that ESLint and Prettier are configured for linting and formatting. This is crucial for maintaining a consistent codebase.
* Visual inspection of files like `Suno.node.ts`, `sunoApi.ts`, etc., shows generally consistent formatting, likely due to Prettier's enforcement.
* Type assertions using `as` (e.g., `this.getNodeParameter('prompt', 0, '') as string`) are used, which is common in n8n nodes for parameter retrieval. While acceptable, minimizing their use by ensuring default values and parameter types are well-defined can enhance type safety.
* **Naming Conventions:**
* Class names (`SunoNode`, `SunoTrigger`, `SunoApi`) consistently use PascalCase.
* Method names (`generateSongFromPrompt`, `pollJobStatus`, `loginWithCredentials`) use camelCase.
* Variable names (`activeSessionToken`, `mockJobs`) also follow camelCase.
* Node property names (`prompt`, `trackId`, `pollingInterval`, `triggerEvent`) are descriptive and use camelCase.
* Overall, naming conventions are clear, descriptive, and adhere to common JavaScript/TypeScript standards.
* **Comments and JSDoc:**
* **`utils/sunoApi.ts`**: Exhibits good JSDoc coverage for exported functions, detailing their purpose, parameters, and return types, and noting their mocked nature.
* **`nodes/Suno/Suno.node.ts`**: Contains JSDoc for individual operation methods and a well-structured `description` object for the node's UI.
* **`nodes/Suno/SunoTrigger.node.ts`**: Has JSDoc for `trigger` and `manualTrigger` methods, and a well-structured `description` object.
* **`credentials/SunoApi.credentials.ts`**: Includes comments explaining the rationale for omitting `authenticate` and `test` blocks during the mock phase.
* **`interfaces/SunoTypes.ts`**: This file is currently **empty**, as confirmed by the latest `read_files` check. If it were populated, JSDoc for each type and property would be expected. This is a significant gap to be addressed.
* **Inline Comments:** `// TODO:` comments are appropriately used to mark areas needing future implementation or review.
* **Console Logging:** Extensive use of `console.log`, `console.error`, and `console.warn` is present, especially in `utils/sunoApi.ts` and `SunoTrigger.node.ts`. While beneficial for debugging mocked behavior, these should be replaced with a more robust logging strategy (e.g., conditional logging or a dedicated logger) or removed when transitioning to actual API calls.
* **Code Structure & Potential Smells (Initial Observations):**
* **Error Handling:** Node operation methods (`Suno.node.ts`) and trigger methods (`SunoTrigger.node.ts`) consistently use `try...catch` blocks and rethrow errors as `NodeOperationError`, which is standard n8n practice. The API utility functions in `sunoApi.ts` also correctly throw errors (e.g., for authentication failures).
* **Method Length:** Functions are generally concise. The `execute` method in `Suno.node.ts` uses a clear `switch` statement. The node `properties` array in `Suno.node.ts` and `SunoTrigger.node.ts` is lengthy, but this is typical and necessary for defining the n8n node UI.
* **Duplication:** No significant code duplication is apparent in the reviewed core files.
* **`interfaces/SunoTypes.ts` Emptiness:** The fact that `interfaces/SunoTypes.ts` is empty is a major gap. Defining data structures is crucial for type safety and for a clear understanding of the data being passed around, especially before implementing actual API calls. **(Action Item: Populate `interfaces/SunoTypes.ts` as a high priority)**.
* **Mocking Logic:** The code in `utils/sunoApi.ts` is explicitly designed for mocking, with in-memory stores (`activeSessionToken`, `mockJobs`). This is suitable for the current development phase but will need complete replacement for actual API integration. JSDoc and comments clearly state this.
* **Linting/Formatting Tools:**
* `.eslintrc.js` and `.prettierrc.js` are present.
* `package.json` includes scripts: `"format": "prettier nodes credentials --write"`, `"lint": "eslint nodes credentials package.json"`, `"lintfix": "eslint nodes credentials package.json --fix"`.
* These demonstrate that the project is well-equipped to enforce code style and catch potential issues automatically.
* **Summary:**
* The project leverages TypeScript, ESLint, and Prettier effectively, establishing a good foundation for code quality.
* Naming conventions and general code structure are sound.
* JSDoc usage is generally good, though it needs to be applied to `interfaces/SunoTypes.ts` once populated.
* The primary immediate concerns are the **empty `interfaces/SunoTypes.ts` file** and the pervasive `console.log` statements that will need refinement before any production-level code or real API integration.
* The current mocked nature of `utils/sunoApi.ts` is well-documented and appropriate for this stage.

View file

@ -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;
}
}
}

View file

@ -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)}` }])]);
}
}
}

View file

@ -1,5 +1,9 @@
// TODO: Import necessary modules, e.g., for making HTTP requests (like axios or node-fetch)
// import { IDataObject } from 'n8n-workflow'; // Or other relevant n8n types
import type { SunoAuthResponse, SunoJob, SunoTrack, SunoPromptOptions } from '../../interfaces/SunoTypes';
// Module-level variable to store the dummy session token
let activeSessionToken: string | null = null;
// In-memory store for mock jobs
let mockJobs: Record<string, SunoJob> = {};
/**
* @namespace SunoApiUtils
@ -9,98 +13,96 @@
*/
/**
* Logs in to the Suno AI service using email and password.
* This function will likely interact with a login endpoint and store
* session information (e.g., cookies, tokens) for subsequent requests.
* Logs in to the Suno AI service using email and password (mocked).
* This function simulates a login by setting a dummy session token.
*
* @async
* @memberof SunoApiUtils
* @param {string} email - The user's email address.
* @param {string} password - The user's password.
* @returns {Promise<boolean>} A promise that resolves to true if login is successful, false otherwise.
* @throws {Error} If login fails or an API error occurs.
* @param {string} [email] - The user's email address.
* @param {string} [password] - The user's password.
* @returns {Promise<SunoAuthResponse>} A promise that resolves with the auth response (token or error).
*/
export async function loginWithCredentials(email, password) {
// TODO: Implement actual API call to login endpoint.
// TODO: Store session token/cookie upon successful login.
// TODO: Implement error handling based on research from docs/dev-log.md
console.log('Attempting login for:', email); // Placeholder
throw new Error('Not implemented: loginWithCredentials');
// return Promise.resolve(true); // Placeholder
export async function loginWithCredentials(email?: string, password?: string): Promise<SunoAuthResponse> {
console.log('[SunoApiUtils.loginWithCredentials] Called with email:', email);
if (!email || !password) {
console.error('[SunoApiUtils.loginWithCredentials] Email and password are required.');
return Promise.resolve({ error: 'Email and password are required.' });
}
// Simulate a successful login
activeSessionToken = 'dummy-session-token-' + Date.now();
console.log('[SunoApiUtils.loginWithCredentials] Mock login successful. Dummy token set:', activeSessionToken);
return Promise.resolve({ token: activeSessionToken });
}
/**
* Retrieves the current session token or authentication cookie.
* This function should access the stored session information.
* Retrieves the current session token (mocked).
* This function returns the stored dummy session token.
*
* @async
* @memberof SunoApiUtils
* @returns {Promise<string | null>} A promise that resolves to the session token/cookie string, or null if not authenticated.
* @returns {Promise<string | null>} A promise that resolves to the session token string, or null if not authenticated.
*/
export async function getSessionToken() {
// TODO: Implement logic to retrieve stored session token/cookie.
// TODO: Implement error handling based on research from docs/dev-log.md
throw new Error('Not implemented: getSessionToken');
// return Promise.resolve('mock_session_token'); // Placeholder
export async function getSessionToken(): Promise<string | null> {
console.log('[SunoApiUtils.getSessionToken] Returning active token:', activeSessionToken);
return Promise.resolve(activeSessionToken);
}
/**
* Checks if the current session is active/valid and refreshes it if necessary.
* This might involve making a test API call or using a dedicated refresh token endpoint.
* Checks if the current session is active/valid and refreshes it if necessary (mocked).
* This function logs that it's called but doesn't implement refresh logic.
*
* @async
* @memberof SunoApiUtils
* @returns {Promise<boolean>} A promise that resolves to true if the session is active or refreshed, false otherwise.
* @returns {Promise<string | null>} A promise that resolves to the current token (no actual refresh).
*/
export async function refreshSessionIfExpired() {
// TODO: Implement logic to check session validity (e.g., by calling a protected endpoint).
// TODO: If session is expired, attempt to refresh it using a refresh token or re-login mechanism.
// TODO: Implement error handling based on research from docs/dev-log.md
throw new Error('Not implemented: refreshSessionIfExpired');
// return Promise.resolve(true); // Placeholder
export async function refreshSessionIfExpired(): Promise<string | null> {
console.log('[SunoApiUtils.refreshSessionIfExpired] Called. Mocked: No refresh logic implemented, returning current token.');
return Promise.resolve(activeSessionToken);
}
/**
* Checks if the user is currently authenticated.
* This could involve checking for a valid session token and/or its expiry.
* Checks if the user is currently authenticated (mocked).
* This checks for the presence of a dummy session token.
*
* @async
* @memberof SunoApiUtils
* @returns {Promise<boolean>} A promise that resolves to true if authenticated, false otherwise.
*/
export async function isAuthenticated() {
// TODO: Implement logic to check for a valid, non-expired session token/cookie.
// TODO: May call getSessionToken() and refreshSessionIfExpired() internally.
// TODO: Implement error handling based on research from docs/dev-log.md
throw new Error('Not implemented: isAuthenticated');
// return Promise.resolve(true); // Placeholder
export async function isAuthenticated(): Promise<boolean> {
const authenticated = !!activeSessionToken;
console.log('[SunoApiUtils.isAuthenticated] Checked token. Authenticated:', authenticated);
return Promise.resolve(authenticated);
}
/**
* Submits a prompt to Suno AI to generate music.
* Submits a prompt to Suno AI to generate music (mocked).
*
* @async
* @memberof SunoApiUtils
* @param {string} promptText - The text prompt describing the desired music.
* @param {object} [options] - Optional parameters for the generation process.
* @param {string} [options.style] - Desired style of music.
* @param {boolean} [options.instrumental] - Whether to generate instrumental music.
* @param {string} [options.customLyrics] - Custom lyrics to use.
* @returns {Promise<any>} A promise that resolves with the API response (e.g., job ID for polling).
* @throws {Error} If the API request fails.
* @param {SunoPromptOptions} [options] - Optional parameters for the generation process.
* @returns {Promise<SunoJob>} A promise that resolves with the mock job details.
* @throws {Error} If not authenticated.
*/
export async function submitPrompt(promptText, options = {}) {
// TODO: Ensure user is authenticated before making the call.
// TODO: Implement actual API call to the prompt submission endpoint.
// TODO: Structure the payload according to API requirements.
// TODO: Implement error handling based on research from docs/dev-log.md
console.log('Submitting prompt:', promptText, 'with options:', options); // Placeholder
throw new Error('Not implemented: submitPrompt');
// return Promise.resolve({ jobId: 'mock_job_id_123' }); // Placeholder
export async function submitPrompt(promptText: string, options?: SunoPromptOptions): Promise<SunoJob> {
if (!await isAuthenticated()) {
throw new Error('Not authenticated. Please login first.');
}
console.log(`[SunoApiUtils.submitPrompt] Mock API: Submitting prompt "${promptText}" with options: ${JSON.stringify(options)}`);
const mockJob: SunoJob = {
id: 'job_' + Date.now(),
status: 'queued',
createdAt: new Date().toISOString(),
progress: 0,
};
mockJobs[mockJob.id] = mockJob; // Store the job for polling simulation
return Promise.resolve(mockJob);
}
/**
* Uploads a reference audio track to Suno AI.
* Uploads a reference audio track to Suno AI (placeholder).
* This might be used for features like "continue track" or style transfer.
*
* @async
@ -109,126 +111,178 @@ export async function submitPrompt(promptText, options = {}) {
* @param {object} [options] - Optional parameters for the upload.
* @param {string} [options.title] - Title for the reference track.
* @returns {Promise<any>} A promise that resolves with the API response (e.g., track ID).
* @throws {Error} If the file upload fails or API error occurs.
* @throws {Error} If not authenticated or if the API request fails.
*/
export async function uploadReferenceTrack(filePath, options = {}) {
// TODO: Ensure user is authenticated.
// TODO: Implement file reading and multipart/form-data request for upload.
// TODO: Implement error handling based on research from docs/dev-log.md
console.log('Uploading reference track:', filePath, 'with options:', options); // Placeholder
throw new Error('Not implemented: uploadReferenceTrack');
// return Promise.resolve({ referenceTrackId: 'mock_ref_track_456' }); // Placeholder
export async function uploadReferenceTrack(filePath: string, options: any = {}): Promise<any> {
if (!await isAuthenticated()) {
throw new Error('Not authenticated. Please login first.');
}
console.log('[SunoApiUtils.uploadReferenceTrack] Uploading reference track:', filePath, 'with options:', options);
// TODO: Implement file reading and multipart/form-data request for upload for actual API.
// For mock, we can just return a dummy ID.
return Promise.resolve({ referenceTrackId: 'mock_ref_' + Date.now() });
}
/**
* Selects a specific voice or instrument for generation.
* Selects a specific voice or instrument for generation (placeholder).
* This assumes Suno AI has a concept of selectable voices/instruments.
*
* @async
* @memberof SunoApiUtils
* @param {string} voiceId - The ID of the voice/instrument to select.
* @returns {Promise<void>} A promise that resolves when the selection is successful.
* @throws {Error} If the API request fails.
* @throws {Error} If not authenticated or if the API request fails.
*/
export async function selectVoice(voiceId) {
// TODO: Ensure user is authenticated.
// TODO: Implement API call to select a voice/instrument.
// TODO: Implement error handling based on research from docs/dev-log.md
console.log('Selecting voice:', voiceId); // Placeholder
throw new Error('Not implemented: selectVoice');
// return Promise.resolve(); // Placeholder
export async function selectVoice(voiceId: string): Promise<void> {
if (!await isAuthenticated()) {
throw new Error('Not authenticated. Please login first.');
}
console.log('[SunoApiUtils.selectVoice] Selecting voice:', voiceId);
// TODO: Implement API call to select a voice/instrument for actual API.
return Promise.resolve();
}
/**
* Polls the status of a generation job.
* Polls the status of a generation job (mocked).
*
* @async
* @memberof SunoApiUtils
* @param {string} jobId - The ID of the job to poll.
* @returns {Promise<any>} A promise that resolves with the job status information (e.g., progress, completion, URLs to tracks).
* @throws {Error} If the API request fails.
* @returns {Promise<SunoJob>} A promise that resolves with the job status information.
* @throws {Error} If not authenticated.
*/
export async function pollJobStatus(jobId) {
// TODO: Ensure user is authenticated.
// TODO: Implement API call to get job status. This might need to be called repeatedly.
// TODO: Implement error handling based on research from docs/dev-log.md
console.log('Polling job status for:', jobId); // Placeholder
throw new Error('Not implemented: pollJobStatus');
// return Promise.resolve({ status: 'completed', trackUrl: 'https://example.com/track.mp3' }); // Placeholder
export async function pollJobStatus(jobId: string): Promise<SunoJob> {
if (!await isAuthenticated()) {
throw new Error('Not authenticated. Please login first.');
}
console.log(`[SunoApiUtils.pollJobStatus] Mock API: Polling job status for "${jobId}"`);
let job = mockJobs[jobId];
if (!job) {
return Promise.resolve({ id: jobId, status: 'failed', error: 'Job not found', createdAt: new Date().toISOString() } as SunoJob);
}
// Simulate status change
if (job.status === 'queued') {
job.status = 'generating';
job.progress = 50;
} else if (job.status === 'generating') {
job.status = 'complete';
job.progress = 100;
job.trackId = 'track_' + Date.now(); // Assign a trackId upon completion
}
// If 'complete' or 'failed', no further changes in this mock.
mockJobs[jobId] = job; // Update the job in the store
return Promise.resolve(job);
}
/**
* Downloads a generated audio track.
* Downloads a generated audio track (mocked).
*
* @async
* @memberof SunoApiUtils
* @param {string} trackId - The ID of the track to download.
* @returns {Promise<any>} A promise that resolves with the audio data (e.g., a Buffer or Stream).
* @throws {Error} If the download fails or API error occurs.
* @returns {Promise<Buffer>} A promise that resolves with the mock audio data.
* @throws {Error} If not authenticated.
*/
export async function downloadTrack(trackId) {
// TODO: Ensure user is authenticated.
// TODO: Implement API call to download the track file.
// TODO: Handle binary data response.
// TODO: Implement error handling based on research from docs/dev-log.md
console.log('Downloading track:', trackId); // Placeholder
throw new Error('Not implemented: downloadTrack');
// return Promise.resolve(Buffer.from('mock_audio_data')); // Placeholder
export async function downloadTrack(trackId: string): Promise<Buffer> {
if (!await isAuthenticated()) {
throw new Error('Not authenticated. Please login first.');
}
console.log(`[SunoApiUtils.downloadTrack] Mock API: Downloading track "${trackId}"`);
return Promise.resolve(Buffer.from('mock MP3 audio data for track ' + trackId));
}
/**
* Lists previously generated songs by the user.
* Lists previously generated songs by the user (mocked).
*
* @async
* @memberof SunoApiUtils
* @param {object} [options] - Optional parameters for listing songs.
* @param {number} [options.limit] - Maximum number of songs to retrieve.
* @param {number} [options.offset] - Offset for pagination.
* @returns {Promise<any[]>} A promise that resolves with an array of song objects.
* @throws {Error} If the API request fails.
* @param {object} [options] - Optional parameters for listing songs (not used in mock).
* @returns {Promise<SunoTrack[]>} A promise that resolves with an array of mock song objects.
* @throws {Error} If not authenticated.
*/
export async function listPreviousSongs(options = {}) {
// TODO: Ensure user is authenticated.
// TODO: Implement API call to list songs.
// TODO: Implement error handling based on research from docs/dev-log.md
console.log('Listing previous songs with options:', options); // Placeholder
throw new Error('Not implemented: listPreviousSongs');
// return Promise.resolve([{ id: 'song1', title: 'My First Song' }, { id: 'song2', title: 'Another Hit' }]); // Placeholder
export async function listPreviousSongs(options: any = {}): Promise<SunoTrack[]> {
if (!await isAuthenticated()) {
throw new Error('Not authenticated. Please login first.');
}
console.log('[SunoApiUtils.listPreviousSongs] Mock API: Listing previous songs.');
const mockTracksArray: SunoTrack[] = [
{
id: 'track_' + (Date.now() - 10000),
title: 'Mock Song Alpha',
artist: 'Suno AI (Mock)',
status: 'complete',
audioUrl: 'https://example.com/mock_alpha.mp3',
imageUrl: 'https://example.com/mock_alpha.png',
duration: 180,
createdAt: new Date(Date.now() - 10000).toISOString(),
isPublic: true,
},
{
id: 'track_' + Date.now(),
title: 'Mock Song Beta',
artist: 'Suno AI (Mock)',
status: 'complete',
audioUrl: 'https://example.com/mock_beta.mp3',
imageUrl: 'https://example.com/mock_beta.png',
duration: 210,
createdAt: new Date().toISOString(),
isPublic: false,
},
];
return Promise.resolve(mockTracksArray);
}
// Example of how these might be called (for testing/ideation only):
/*
async function main() {
try {
const loggedIn = await loginWithCredentials('test@example.com', 'password123');
if (loggedIn) {
const token = await getSessionToken();
console.log('Session token:', token);
const loginResponse = await loginWithCredentials('test@example.com', 'password123');
if (loginResponse.token) {
console.log('Login successful, token:', loginResponse.token);
if (await isAuthenticated()) {
const job = await submitPrompt('Epic orchestral score for a space battle', { style: 'cinematic' });
console.log('Submitted job:', job.jobId);
console.log('User is authenticated.');
let status;
do {
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5s
status = await pollJobStatus(job.jobId);
console.log('Job status:', status);
} while (status.status !== 'completed' && status.status !== 'failed');
// Test submitPrompt
const job = await submitPrompt('Epic orchestral score for a space battle', { style: 'cinematic', instrumental: true });
console.log('Submitted job:', job);
if (status.status === 'completed') {
const audioData = await downloadTrack(status.trackUrl); // Assuming trackUrl is the ID or direct URL
// Test pollJobStatus - first poll (queued -> generating)
let status = await pollJobStatus(job.id);
console.log('Job status (1st poll):', status);
// Test pollJobStatus - second poll (generating -> complete)
status = await pollJobStatus(job.id);
console.log('Job status (2nd poll):', status);
if (status.trackId) {
// Test downloadTrack
const audioData = await downloadTrack(status.trackId);
console.log('Downloaded track data length:', audioData.length);
}
const songs = await listPreviousSongs({ limit: 5 });
// Test listPreviousSongs
const songs = await listPreviousSongs();
console.log('Previous songs:', songs);
// Test job not found
const notFoundJob = await pollJobStatus('job_invalid_id');
console.log('Status for non-existent job:', notFoundJob);
}
} else {
console.error('Login failed:', loginResponse.error);
}
} catch (error) {
console.error('Suno API Error:', error.message);
// console.error('Suno API Error:', error.message);
}
}
// main(); // Uncomment to run example (ensure to handle promises correctly if top-level await is not available)
// main();
*/