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

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. 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. 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, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
// Assuming INodeProperties is needed for properties definition
INodeProperties, INodeProperties,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } 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 { export class SunoNode implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -27,7 +26,7 @@ export class SunoNode implements INodeType {
outputs: [NodeConnectionType.Main], outputs: [NodeConnectionType.Main],
credentials: [ credentials: [
{ {
name: 'sunoApi', // Matches the name in SunoApi.credentials.ts name: 'sunoApi',
required: true, required: true,
}, },
], ],
@ -38,6 +37,12 @@ export class SunoNode implements INodeType {
type: 'options', type: 'options',
noDataExpression: true, noDataExpression: true,
options: [ options: [
{
name: 'Login',
value: 'login',
description: 'Perform login to Suno API (for testing)',
action: 'Login',
},
{ {
name: 'Generate Song from Prompt', name: 'Generate Song from Prompt',
value: 'generateSongFromPrompt', value: 'generateSongFromPrompt',
@ -71,12 +76,37 @@ export class SunoNode implements INodeType {
], ],
default: 'generateSongFromPrompt', 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' // Properties for 'generateSongFromPrompt'
{ {
displayName: 'Prompt', displayName: 'Prompt',
name: 'prompt', name: 'prompt',
type: 'string', type: 'string',
default: '', default: '',
required: true,
placeholder: 'e.g., Epic orchestral score for a space battle', placeholder: 'e.g., Epic orchestral score for a space battle',
description: 'The text prompt to generate music from', description: 'The text prompt to generate music from',
displayOptions: { 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' // Properties for 'getTrackStatus' and 'downloadTrack'
{ {
displayName: 'Track ID', 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. * @param {IExecuteFunctions} this - The execution context.
* @returns {Promise<INodeExecutionData[][]>} * @returns {Promise<INodeExecutionData[][]>}
*/ */
async generateSongFromPrompt(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async generateSongFromPrompt(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const prompt = this.getNodeParameter('prompt', 0, '') as string; const promptText = this.getNodeParameter('prompt', 0, '') as string;
// TODO: Call the appropriate sunoApi.ts function (sunoApi.submitPrompt) and handle response // const options: SunoPromptOptions = {}; // TODO: Get from node parameters if added
console.log('Executing generateSongFromPrompt with prompt:', prompt); try {
// For now, return empty data const job = await sunoApi.submitPrompt(promptText /*, options */);
return [this.prepareOutputData([])]; 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. * @param {IExecuteFunctions} this - The execution context.
* @returns {Promise<INodeExecutionData[][]>} * @returns {Promise<INodeExecutionData[][]>}
*/ */
async uploadTrackReference(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async uploadTrackReference(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const filePath = this.getNodeParameter('filePath', 0, '') as string; const filePath = this.getNodeParameter('filePath', 0, '') as string;
// TODO: Call the appropriate sunoApi.ts function (sunoApi.uploadReferenceTrack) and handle response try {
console.log('Executing uploadTrackReference with filePath:', filePath); // TODO: Add options if necessary
return [this.prepareOutputData([])]; 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. * @param {IExecuteFunctions} this - The execution context.
* @returns {Promise<INodeExecutionData[][]>} * @returns {Promise<INodeExecutionData[][]>}
*/ */
async getTrackStatus(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async getTrackStatus(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const trackId = this.getNodeParameter('trackId', 0, '') as string; const trackId = this.getNodeParameter('trackId', 0, '') as string;
// TODO: Call the appropriate sunoApi.ts function (sunoApi.pollJobStatus or similar) and handle response try {
console.log('Executing getTrackStatus with trackId:', trackId); const status = await sunoApi.pollJobStatus(trackId);
return [this.prepareOutputData([])]; 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. * @param {IExecuteFunctions} this - The execution context.
* @returns {Promise<INodeExecutionData[][]>} * @returns {Promise<INodeExecutionData[][]>}
*/ */
async downloadTrack(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async downloadTrack(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const trackId = this.getNodeParameter('trackId', 0, '') as string; const trackId = this.getNodeParameter('trackId', 0, '') as string;
// TODO: Call the appropriate sunoApi.ts function (sunoApi.downloadTrack) and handle response const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0, 'sunoTrackAudio') as string;
// TODO: Consider how to handle binary data output in n8n try {
console.log('Executing downloadTrack with trackId:', trackId); const audioBuffer = await sunoApi.downloadTrack(trackId);
return [this.prepareOutputData([])]; 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. * @param {IExecuteFunctions} this - The execution context.
* @returns {Promise<INodeExecutionData[][]>} * @returns {Promise<INodeExecutionData[][]>}
*/ */
async listPreviousSongs(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async listPreviousSongs(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
// TODO: Call the appropriate sunoApi.ts function (sunoApi.listPreviousSongs) and handle response try {
console.log('Executing listPreviousSongs'); const songs = await sunoApi.listPreviousSongs();
return [this.prepareOutputData([])]; 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[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
@ -184,6 +282,8 @@ export class SunoNode implements INodeType {
try { try {
switch (operation) { switch (operation) {
case 'login':
return this.login(this);
case 'generateSongFromPrompt': case 'generateSongFromPrompt':
return this.generateSongFromPrompt(this); return this.generateSongFromPrompt(this);
case 'uploadTrackReference': case 'uploadTrackReference':
@ -198,14 +298,11 @@ export class SunoNode implements INodeType {
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not supported.`); throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not supported.`);
} }
} catch (error) { } catch (error) {
// This node should use NodeOperationError when an error occurs directly within this node's execution logic
if (this.continueOnFail()) { if (this.continueOnFail()) {
// Return error data as per n8n's guidelines for allowing the workflow to continue const item = this.getInputData(0)?.[0];
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 }])]; return [this.prepareOutputData([{ json: {}, error: error, pairedItem: item ? { item: 0 } : undefined }])];
} else { } else {
// If not continuing on fail, rethrow the error to halt the workflow throw error;
throw error; // NodeOperationError should already be structured correctly
} }
} }
} }

View file

@ -3,21 +3,29 @@ import type {
INodeTypeDescription, INodeTypeDescription,
ITriggerFunctions, ITriggerFunctions,
ITriggerResponse, ITriggerResponse,
ICredentialDataDecryptedObject, // Added
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow'; // Added
import {
pollJobStatus,
listPreviousSongs,
loginWithCredentials,
} from '../../utils/sunoApi';
export class SunoTrigger implements INodeType { export class SunoTrigger implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Suno Trigger', displayName: 'Suno Trigger',
name: 'sunoTrigger', name: 'sunoTrigger',
icon: 'file:suno.svg', // Re-use the icon icon: 'file:suno.svg',
group: ['trigger', 'ai'], group: ['trigger', 'ai'],
version: 1, version: 1,
description: 'Triggers when a Suno AI event occurs', description: 'Triggers when a Suno AI event occurs (polling)',
defaults: { defaults: {
name: 'Suno Trigger', name: 'Suno Trigger',
}, },
inputs: [], // Triggers usually do not have inputs inputs: [],
outputs: ['main'], // Main output for triggered data outputs: ['main'],
credentials: [ credentials: [
{ {
name: 'sunoApi', name: 'sunoApi',
@ -25,7 +33,6 @@ export class SunoTrigger implements INodeType {
}, },
], ],
properties: [ properties: [
// Define properties for the trigger
{ {
displayName: 'Trigger Event', displayName: 'Trigger Event',
name: 'triggerEvent', name: 'triggerEvent',
@ -46,7 +53,7 @@ export class SunoTrigger implements INodeType {
description: 'The Suno event that will trigger this node', description: 'The Suno event that will trigger this node',
}, },
{ {
displayName: 'Track ID', displayName: 'Track ID (for Track Generation Complete)',
name: 'trackId', name: 'trackId',
type: 'string', type: 'string',
default: '', default: '',
@ -61,72 +68,102 @@ export class SunoTrigger implements INodeType {
displayName: 'Polling Interval (minutes)', displayName: 'Polling Interval (minutes)',
name: 'pollingInterval', name: 'pollingInterval',
type: 'number', type: 'number',
default: 5, default: 5, // Default to 5 minutes
description: 'How often to check for new songs (if applicable)', description: 'How often to check for events. n8n will manage the actual polling schedule based on this.',
displayOptions: { // displayOptions: { // Not strictly needed to show for both, but can be kept
show: { // show: {
triggerEvent: ['newSongAvailable'], // 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> { async trigger(this: ITriggerFunctions): Promise<ITriggerResponse | undefined> {
// const credentials = await this.getCredentials('sunoApi'); const pollingIntervalMinutes = this.getNodeParameter('pollingInterval', 5) as number;
// const triggerEvent = this.getNodeParameter('triggerEvent') as string; this.setPollingInterval(pollingIntervalMinutes * 60 * 1000); // Set n8n's polling interval
// const trackId = this.getNodeParameter('trackId') as string;
// const pollingInterval = this.getNodeParameter('pollingInterval') as number;
// TODO: Implement actual trigger logic based on triggerEvent try {
// For 'trackGenerationComplete', this might involve polling pollJobStatus(trackId) const credentials = await this.getCredentials('sunoApi') as ICredentialDataDecryptedObject;
// For 'newSongAvailable', this might involve polling listPreviousSongs() and checking for new entries if (!credentials || !credentials.email || !credentials.password) {
throw new NodeOperationError(this.getNode(), 'Suno API credentials are not configured or incomplete.');
// If webhook based, this method would be different, }
// this.emit([this.helpers.returnJsonArray([{ eventData: 'example' }])]); // Perform an initial login to ensure credentials are valid and API is reachable
// this.on('close', () => { /* remove webhook */ }); await loginWithCredentials(credentials.email as string, credentials.password as string);
// return { webhookId: 'your-webhook-id' }; console.log('SunoTrigger: Initial authentication successful for polling setup.');
} catch (error) {
// For polling triggers, this method might not be used directly if using manualTriggerFunction console.error('SunoTrigger: Initial authentication or setup failed.', error);
if (this.manualTriggerFunction) { // Corrected placeholder name // Depending on how n8n handles this, we might throw or just log.
// Manual trigger logic for polling // For a trigger, throwing here might prevent it from starting.
// This will be called by n8n based on the schedule if `manualTriggerFunction` is defined if (error instanceof NodeOperationError) throw error;
// Example: throw new NodeOperationError(this.getNode(), `Initial setup failed: ${error.message || String(error)}`);
// 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 pure polling, manualTriggerFunction will handle the periodic checks.
// For a polling trigger, you might set up an interval here or use manualTriggerFunction // If specific cleanup is needed when the workflow deactivates, return a closeFunction.
// For a webhook trigger, you would register the webhook here. return {
return undefined; 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> { * `manualTriggerFunction` is called by n8n at the defined polling interval.
// const triggerEvent = this.getNodeParameter('triggerEvent') as string; * It should fetch data and emit items if new events are found.
// // ... get other params and credentials */
// public async manualTrigger(this: ITriggerFunctions): Promise<void> {
// if (triggerEvent === 'newSongAvailable') { const triggerEvent = this.getNodeParameter('triggerEvent') as string;
// console.log('Polling for new songs...');
// // const newSongs = await sunoApi.listPreviousSongs(credentials, /* potential pagination params */); try {
// // Check against previously seen songs (requires state management, complex for this scaffold) const credentials = await this.getCredentials('sunoApi') as ICredentialDataDecryptedObject;
// // For now, let's simulate finding one new song: if (!credentials || !credentials.email || !credentials.password) {
// // const simulatedNewSong = [{ id: 'new_track_123', title: 'A New Song', status: 'complete' }]; console.error('SunoTrigger: Credentials not available for polling.');
// // return [this.helpers.returnJsonArray(simulatedNewSong)]; // Optionally emit an error to the workflow execution log, but be careful not to flood it.
// } else if (triggerEvent === 'trackGenerationComplete') { // this.emit([this.helpers.returnJsonArray([{ error: 'Credentials missing for polling' }])]);
// // const trackId = this.getNodeParameter('trackId') as string; return; // Stop further execution for this poll if creds are missing
// // const status = await sunoApi.pollJobStatus(credentials, trackId); }
// // if (status && status.status === 'complete') {
// // return [this.helpers.returnJsonArray([status])]; // 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);
// return undefined; // No trigger event
// } 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 type { SunoAuthResponse, SunoJob, SunoTrack, SunoPromptOptions } from '../../interfaces/SunoTypes';
// import { IDataObject } from 'n8n-workflow'; // Or other relevant n8n types
// 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 * @namespace SunoApiUtils
@ -9,98 +13,96 @@
*/ */
/** /**
* Logs in to the Suno AI service using email and password. * Logs in to the Suno AI service using email and password (mocked).
* This function will likely interact with a login endpoint and store * This function simulates a login by setting a dummy session token.
* session information (e.g., cookies, tokens) for subsequent requests.
* *
* @async * @async
* @memberof SunoApiUtils * @memberof SunoApiUtils
* @param {string} email - The user's email address. * @param {string} [email] - The user's email address.
* @param {string} password - The user's password. * @param {string} [password] - The user's password.
* @returns {Promise<boolean>} A promise that resolves to true if login is successful, false otherwise. * @returns {Promise<SunoAuthResponse>} A promise that resolves with the auth response (token or error).
* @throws {Error} If login fails or an API error occurs.
*/ */
export async function loginWithCredentials(email, password) { export async function loginWithCredentials(email?: string, password?: string): Promise<SunoAuthResponse> {
// TODO: Implement actual API call to login endpoint. console.log('[SunoApiUtils.loginWithCredentials] Called with email:', email);
// TODO: Store session token/cookie upon successful login. if (!email || !password) {
// TODO: Implement error handling based on research from docs/dev-log.md console.error('[SunoApiUtils.loginWithCredentials] Email and password are required.');
console.log('Attempting login for:', email); // Placeholder return Promise.resolve({ error: 'Email and password are required.' });
throw new Error('Not implemented: loginWithCredentials'); }
// return Promise.resolve(true); // Placeholder // 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. * Retrieves the current session token (mocked).
* This function should access the stored session information. * This function returns the stored dummy session token.
* *
* @async * @async
* @memberof SunoApiUtils * @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() { export async function getSessionToken(): Promise<string | null> {
// TODO: Implement logic to retrieve stored session token/cookie. console.log('[SunoApiUtils.getSessionToken] Returning active token:', activeSessionToken);
// TODO: Implement error handling based on research from docs/dev-log.md return Promise.resolve(activeSessionToken);
throw new Error('Not implemented: getSessionToken');
// return Promise.resolve('mock_session_token'); // Placeholder
} }
/** /**
* Checks if the current session is active/valid and refreshes it if necessary. * Checks if the current session is active/valid and refreshes it if necessary (mocked).
* This might involve making a test API call or using a dedicated refresh token endpoint. * This function logs that it's called but doesn't implement refresh logic.
* *
* @async * @async
* @memberof SunoApiUtils * @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() { export async function refreshSessionIfExpired(): Promise<string | null> {
// TODO: Implement logic to check session validity (e.g., by calling a protected endpoint). console.log('[SunoApiUtils.refreshSessionIfExpired] Called. Mocked: No refresh logic implemented, returning current token.');
// TODO: If session is expired, attempt to refresh it using a refresh token or re-login mechanism. return Promise.resolve(activeSessionToken);
// TODO: Implement error handling based on research from docs/dev-log.md
throw new Error('Not implemented: refreshSessionIfExpired');
// return Promise.resolve(true); // Placeholder
} }
/** /**
* Checks if the user is currently authenticated. * Checks if the user is currently authenticated (mocked).
* This could involve checking for a valid session token and/or its expiry. * This checks for the presence of a dummy session token.
* *
* @async * @async
* @memberof SunoApiUtils * @memberof SunoApiUtils
* @returns {Promise<boolean>} A promise that resolves to true if authenticated, false otherwise. * @returns {Promise<boolean>} A promise that resolves to true if authenticated, false otherwise.
*/ */
export async function isAuthenticated() { export async function isAuthenticated(): Promise<boolean> {
// TODO: Implement logic to check for a valid, non-expired session token/cookie. const authenticated = !!activeSessionToken;
// TODO: May call getSessionToken() and refreshSessionIfExpired() internally. console.log('[SunoApiUtils.isAuthenticated] Checked token. Authenticated:', authenticated);
// TODO: Implement error handling based on research from docs/dev-log.md return Promise.resolve(authenticated);
throw new Error('Not implemented: isAuthenticated');
// return Promise.resolve(true); // Placeholder
} }
/** /**
* Submits a prompt to Suno AI to generate music. * Submits a prompt to Suno AI to generate music (mocked).
* *
* @async * @async
* @memberof SunoApiUtils * @memberof SunoApiUtils
* @param {string} promptText - The text prompt describing the desired music. * @param {string} promptText - The text prompt describing the desired music.
* @param {object} [options] - Optional parameters for the generation process. * @param {SunoPromptOptions} [options] - Optional parameters for the generation process.
* @param {string} [options.style] - Desired style of music. * @returns {Promise<SunoJob>} A promise that resolves with the mock job details.
* @param {boolean} [options.instrumental] - Whether to generate instrumental music. * @throws {Error} If not authenticated.
* @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.
*/ */
export async function submitPrompt(promptText, options = {}) { export async function submitPrompt(promptText: string, options?: SunoPromptOptions): Promise<SunoJob> {
// TODO: Ensure user is authenticated before making the call. if (!await isAuthenticated()) {
// TODO: Implement actual API call to the prompt submission endpoint. throw new Error('Not authenticated. Please login first.');
// TODO: Structure the payload according to API requirements. }
// TODO: Implement error handling based on research from docs/dev-log.md console.log(`[SunoApiUtils.submitPrompt] Mock API: Submitting prompt "${promptText}" with options: ${JSON.stringify(options)}`);
console.log('Submitting prompt:', promptText, 'with options:', options); // Placeholder
throw new Error('Not implemented: submitPrompt'); const mockJob: SunoJob = {
// return Promise.resolve({ jobId: 'mock_job_id_123' }); // Placeholder 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. * This might be used for features like "continue track" or style transfer.
* *
* @async * @async
@ -109,126 +111,178 @@ export async function submitPrompt(promptText, options = {}) {
* @param {object} [options] - Optional parameters for the upload. * @param {object} [options] - Optional parameters for the upload.
* @param {string} [options.title] - Title for the reference track. * @param {string} [options.title] - Title for the reference track.
* @returns {Promise<any>} A promise that resolves with the API response (e.g., track ID). * @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 = {}) { export async function uploadReferenceTrack(filePath: string, options: any = {}): Promise<any> {
// TODO: Ensure user is authenticated. if (!await isAuthenticated()) {
// TODO: Implement file reading and multipart/form-data request for upload. throw new Error('Not authenticated. Please login first.');
// TODO: Implement error handling based on research from docs/dev-log.md }
console.log('Uploading reference track:', filePath, 'with options:', options); // Placeholder console.log('[SunoApiUtils.uploadReferenceTrack] Uploading reference track:', filePath, 'with options:', options);
throw new Error('Not implemented: uploadReferenceTrack'); // TODO: Implement file reading and multipart/form-data request for upload for actual API.
// return Promise.resolve({ referenceTrackId: 'mock_ref_track_456' }); // Placeholder // 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. * This assumes Suno AI has a concept of selectable voices/instruments.
* *
* @async * @async
* @memberof SunoApiUtils * @memberof SunoApiUtils
* @param {string} voiceId - The ID of the voice/instrument to select. * @param {string} voiceId - The ID of the voice/instrument to select.
* @returns {Promise<void>} A promise that resolves when the selection is successful. * @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) { export async function selectVoice(voiceId: string): Promise<void> {
// TODO: Ensure user is authenticated. if (!await isAuthenticated()) {
// TODO: Implement API call to select a voice/instrument. throw new Error('Not authenticated. Please login first.');
// TODO: Implement error handling based on research from docs/dev-log.md }
console.log('Selecting voice:', voiceId); // Placeholder console.log('[SunoApiUtils.selectVoice] Selecting voice:', voiceId);
throw new Error('Not implemented: selectVoice'); // TODO: Implement API call to select a voice/instrument for actual API.
// return Promise.resolve(); // Placeholder return Promise.resolve();
} }
/** /**
* Polls the status of a generation job. * Polls the status of a generation job (mocked).
* *
* @async * @async
* @memberof SunoApiUtils * @memberof SunoApiUtils
* @param {string} jobId - The ID of the job to poll. * @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). * @returns {Promise<SunoJob>} A promise that resolves with the job status information.
* @throws {Error} If the API request fails. * @throws {Error} If not authenticated.
*/ */
export async function pollJobStatus(jobId) { export async function pollJobStatus(jobId: string): Promise<SunoJob> {
// TODO: Ensure user is authenticated. if (!await isAuthenticated()) {
// TODO: Implement API call to get job status. This might need to be called repeatedly. throw new Error('Not authenticated. Please login first.');
// TODO: Implement error handling based on research from docs/dev-log.md }
console.log('Polling job status for:', jobId); // Placeholder console.log(`[SunoApiUtils.pollJobStatus] Mock API: Polling job status for "${jobId}"`);
throw new Error('Not implemented: pollJobStatus');
// return Promise.resolve({ status: 'completed', trackUrl: 'https://example.com/track.mp3' }); // Placeholder 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 * @async
* @memberof SunoApiUtils * @memberof SunoApiUtils
* @param {string} trackId - The ID of the track to download. * @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). * @returns {Promise<Buffer>} A promise that resolves with the mock audio data.
* @throws {Error} If the download fails or API error occurs. * @throws {Error} If not authenticated.
*/ */
export async function downloadTrack(trackId) { export async function downloadTrack(trackId: string): Promise<Buffer> {
// TODO: Ensure user is authenticated. if (!await isAuthenticated()) {
// TODO: Implement API call to download the track file. throw new Error('Not authenticated. Please login first.');
// TODO: Handle binary data response. }
// TODO: Implement error handling based on research from docs/dev-log.md console.log(`[SunoApiUtils.downloadTrack] Mock API: Downloading track "${trackId}"`);
console.log('Downloading track:', trackId); // Placeholder return Promise.resolve(Buffer.from('mock MP3 audio data for track ' + trackId));
throw new Error('Not implemented: downloadTrack');
// return Promise.resolve(Buffer.from('mock_audio_data')); // Placeholder
} }
/** /**
* Lists previously generated songs by the user. * Lists previously generated songs by the user (mocked).
* *
* @async * @async
* @memberof SunoApiUtils * @memberof SunoApiUtils
* @param {object} [options] - Optional parameters for listing songs. * @param {object} [options] - Optional parameters for listing songs (not used in mock).
* @param {number} [options.limit] - Maximum number of songs to retrieve. * @returns {Promise<SunoTrack[]>} A promise that resolves with an array of mock song objects.
* @param {number} [options.offset] - Offset for pagination. * @throws {Error} If not authenticated.
* @returns {Promise<any[]>} A promise that resolves with an array of song objects.
* @throws {Error} If the API request fails.
*/ */
export async function listPreviousSongs(options = {}) { export async function listPreviousSongs(options: any = {}): Promise<SunoTrack[]> {
// TODO: Ensure user is authenticated. if (!await isAuthenticated()) {
// TODO: Implement API call to list songs. throw new Error('Not authenticated. Please login first.');
// TODO: Implement error handling based on research from docs/dev-log.md }
console.log('Listing previous songs with options:', options); // Placeholder console.log('[SunoApiUtils.listPreviousSongs] Mock API: Listing previous songs.');
throw new Error('Not implemented: listPreviousSongs');
// return Promise.resolve([{ id: 'song1', title: 'My First Song' }, { id: 'song2', title: 'Another Hit' }]); // Placeholder 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): // Example of how these might be called (for testing/ideation only):
/* /*
async function main() { async function main() {
try { try {
const loggedIn = await loginWithCredentials('test@example.com', 'password123'); const loginResponse = await loginWithCredentials('test@example.com', 'password123');
if (loggedIn) { if (loginResponse.token) {
const token = await getSessionToken(); console.log('Login successful, token:', loginResponse.token);
console.log('Session token:', token);
if (await isAuthenticated()) { if (await isAuthenticated()) {
const job = await submitPrompt('Epic orchestral score for a space battle', { style: 'cinematic' }); console.log('User is authenticated.');
console.log('Submitted job:', job.jobId);
let status; // Test submitPrompt
do { const job = await submitPrompt('Epic orchestral score for a space battle', { style: 'cinematic', instrumental: true });
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5s console.log('Submitted job:', job);
status = await pollJobStatus(job.jobId);
console.log('Job status:', status);
} while (status.status !== 'completed' && status.status !== 'failed');
if (status.status === 'completed') { // Test pollJobStatus - first poll (queued -> generating)
const audioData = await downloadTrack(status.trackUrl); // Assuming trackUrl is the ID or direct URL 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); 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); 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) { } 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();
*/ */