diff --git a/README.md b/README.md index ccc31bb..b6790e4 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ We have deployed an example bound to a free Suno account, so it has daily usage - Perfectly implements the creation API from suno.ai. - Automatically keep the account active. - Solve CAPTCHAs automatically using [2Captcha](https://2captcha.com) and [Playwright](https://playwright.dev) with [rebrowser-patches](https://github.com/rebrowser/rebrowser-patches). -- Compatible with the format of OpenAI’s `/v1/chat/completions` API. +- Compatible with the format of OpenAI's `/v1/chat/completions` API. - Supports Custom Mode. - One-click deployment to [Vercel](#deploy-to-vercel) & [Docker](#docker). - In addition to the standard API, it also adapts to the API Schema of Agent platforms like GPTs and Coze, so you can use it as a tool/plugin/Action for LLMs and integrate it into any AI Agent. @@ -99,7 +99,7 @@ docker compose build && docker compose up - If deployed to Vercel, please add the environment variables in the Vercel dashboard. -- If you’re running this locally, be sure to add the following to your `.env` file: +- If you're running this locally, be sure to add the following to your `.env` file: #### Environment variables - `SUNO_COOKIE` — the `Cookie` header you obtained in the first step. - `TWOCAPTCHA_KEY` — your 2Captcha API key from the second step. @@ -118,7 +118,7 @@ BROWSER_HEADLESS=true ### 5. Run suno-api -- If you’ve deployed to Vercel: +- If you've deployed to Vercel: - Please click on Deploy in the Vercel dashboard and wait for the deployment to be successful. - Visit the `https:///api/get_limit` API for testing. - If running locally: @@ -148,10 +148,10 @@ Suno API currently mainly implements the following APIs: ```bash - `/api/generate`: Generate music -- `/v1/chat/completions`: Generate music - Call the generate API in a format that works with OpenAI’s API. +- `/v1/chat/completions`: Generate music - Call the generate API in a format that works with OpenAI's API. - `/api/custom_generate`: Generate music (Custom Mode, support setting lyrics, music style, title, etc.) - `/api/generate_lyrics`: Generate lyrics based on prompt -- `/api/get`: Get music information based on the id. Use “,” to separate multiple ids. +- `/api/get`: Get music information based on the id. Use "," to separate multiple ids. If no IDs are provided, all music will be returned. - `/api/get_limit`: Get quota Info - `/api/extend_audio`: Extend audio length @@ -159,6 +159,8 @@ Suno API currently mainly implements the following APIs: - `/api/get_aligned_lyrics`: Get list of timestamps for each word in the lyrics - `/api/clip`: Get clip information based on ID passed as query parameter `id` - `/api/concat`: Generate the whole song from extensions +- `/api/projects`: Get a list of projects +- `/api/projects/{id}`: Get a specific project by ID with options to hide disliked content ``` You can also specify the cookies in the `Cookie` header of your request, overriding the default cookies in the `SUNO_COOKIE` environment variable. This comes in handy when, for example, you want to use multiple free accounts at the same time. @@ -217,6 +219,16 @@ def generate_whole_song(clip_id): response = requests.post(url, json=payload) return response.json() +def get_projects(page=1): + url = f"{base_url}/api/projects?page={page}" + response = requests.get(url) + return response.json() + +def get_project(project_id, hide_disliked=False, page=1): + url = f"{base_url}/api/projects/{project_id}?hide_disliked={str(hide_disliked).lower()}&page={page}" + response = requests.get(url) + return response.json() + if __name__ == '__main__': data = generate_audio_by_prompt({ @@ -289,6 +301,18 @@ async function getClipInformation(clipId) { return response.data; } +async function getProjects(page = 1) { + const url = `${baseUrl}/api/projects?page=${page}`; + const response = await axios.get(url); + return response.data; +} + +async function getProject(projectId, hideDisliked = false, page = 1) { + const url = `${baseUrl}/api/projects/${projectId}?hide_disliked=${hideDisliked}&page=${page}`; + const response = await axios.get(url); + return response.data; +} + async function main() { const data = await generateAudioByPrompt({ prompt: diff --git a/src/app/api/projects/[id]/route.ts b/src/app/api/projects/[id]/route.ts new file mode 100644 index 0000000..0ac5cb1 --- /dev/null +++ b/src/app/api/projects/[id]/route.ts @@ -0,0 +1,60 @@ +import { NextResponse, NextRequest } from 'next/server'; +import { cookies } from 'next/headers'; +import { sunoApi } from '@/lib/SunoApi'; +import { corsHeaders } from '@/lib/utils'; + +export const dynamic = 'force-dynamic'; + +export async function GET(req: NextRequest, { params }: { params: { id: string } }) { + if (req.method === 'GET') { + try { + const projectId = params.id; + const url = new URL(req.url); + const hideDisliked = url.searchParams.get('hide_disliked') === 'true'; + const page = url.searchParams.get('page') || '1'; + const cookie = (await cookies()).toString(); + + const data = await (await sunoApi(cookie)).getProject( + projectId, + hideDisliked, + parseInt(page) + ); + + return new NextResponse(JSON.stringify(data), { + status: 200, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } catch (error) { + console.error('Error fetching project:', error); + + return new NextResponse( + JSON.stringify({ error: 'Internal server error' }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + } + ); + } + } else { + return new NextResponse('Method Not Allowed', { + headers: { + Allow: 'GET', + ...corsHeaders + }, + status: 405 + }); + } +} + +export async function OPTIONS(request: Request) { + return new Response(null, { + status: 200, + headers: corsHeaders + }); +} \ No newline at end of file diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts new file mode 100644 index 0000000..b224afb --- /dev/null +++ b/src/app/api/projects/route.ts @@ -0,0 +1,54 @@ +import { NextResponse, NextRequest } from 'next/server'; +import { cookies } from 'next/headers'; +import { sunoApi } from '@/lib/SunoApi'; +import { corsHeaders } from '@/lib/utils'; + +export const dynamic = 'force-dynamic'; + +export async function GET(req: NextRequest) { + if (req.method === 'GET') { + try { + const url = new URL(req.url); + const page = url.searchParams.get('page') || '1'; + const cookie = (await cookies()).toString(); + + const data = await (await sunoApi(cookie)).projects(parseInt(page)); + + return new NextResponse(JSON.stringify(data), { + status: 200, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } catch (error) { + console.error('Error fetching projects:', error); + + return new NextResponse( + JSON.stringify({ error: 'Internal server error' }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + } + ); + } + } else { + return new NextResponse('Method Not Allowed', { + headers: { + Allow: 'GET', + ...corsHeaders + }, + status: 405 + }); + } +} + +export async function OPTIONS(request: Request) { + return new Response(null, { + status: 200, + headers: corsHeaders + }); +} \ No newline at end of file diff --git a/src/app/docs/swagger-suno-api.json b/src/app/docs/swagger-suno-api.json index fbc03ea..ec96463 100644 --- a/src/app/docs/swagger-suno-api.json +++ b/src/app/docs/swagger-suno-api.json @@ -378,6 +378,195 @@ } } }, + "/api/projects": { + "get": { + "summary": "Get user's projects", + "description": "Retrieves the user's projects from Suno API.", + "tags": ["default"], + "parameters": [ + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1)", + "required": false, + "schema": { + "type": "number", + "default": 1 + } + } + ], + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Project ID" + }, + "name": { + "type": "string", + "description": "Project name" + }, + "created_at": { + "type": "string", + "description": "Creation date and time" + }, + "last_modified": { + "type": "string", + "description": "Last modification date and time" + } + } + } + }, + "total_results": { + "type": "integer", + "description": "Total number of projects" + }, + "current_page": { + "type": "integer", + "description": "Current page number" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal server error" + } + } + } + } + } + } + } + } + }, + "/api/projects/{id}": { + "get": { + "summary": "Get project by ID", + "description": "Retrieves a specific project by ID from Suno API.", + "tags": ["default"], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Project ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "hide_disliked", + "description": "Whether to hide disliked content (defaults to false)", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1)", + "required": false, + "schema": { + "type": "number", + "default": 1 + } + } + ], + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "project": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Project ID" + }, + "name": { + "type": "string", + "description": "Project name" + }, + "description": { + "type": "string", + "description": "Project description" + }, + "created_at": { + "type": "string", + "description": "Creation date and time" + }, + "last_modified": { + "type": "string", + "description": "Last modification date and time" + }, + "clips": { + "type": "array", + "items": { + "type": "object", + "description": "Clip information" + } + } + } + }, + "total_results": { + "type": "integer", + "description": "Total number of clips in the project" + }, + "current_page": { + "type": "integer", + "description": "Current page number" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal server error" + } + } + } + } + } + } + } + } + }, "/api/get_limit": { "get": { "summary": "Get quota information.", diff --git a/src/lib/SunoApi.ts b/src/lib/SunoApi.ts index f01ac72..a3abfcc 100644 --- a/src/lib/SunoApi.ts +++ b/src/lib/SunoApi.ts @@ -847,6 +847,56 @@ class SunoApi { return response.data; } + + /** + * Retrieves user's projects from Suno API. + * @param page An optional page number to retrieve projects from (default: 1). + * @returns A promise that resolves to the projects data from Suno. + */ + public async projects(page: number = 1): Promise { + await this.keepAlive(false); + + const url = `${SunoApi.BASE_URL}/api/project/me?page=${page}`; + + logger.info(`Fetching projects data: ${url}`); + + const response = await this.client.get(url, { + timeout: 10000 // 10 seconds timeout + }); + + if (response.status !== 200) { + throw new Error('Error response: ' + response.statusText); + } + + return response.data; + } + + /** + * Retrieves a specific project by ID from Suno API. + * @param projectId The ID of the project to retrieve. + * @param hideDisliked Whether to hide disliked content (default: false). + * @param page An optional page number to retrieve project data from (default: 1). + * @returns A promise that resolves to the project data from Suno. + */ + public async getProject(projectId: string, hideDisliked: boolean = false, page: number = 1): Promise { + await this.keepAlive(false); + + const url = new URL(`${SunoApi.BASE_URL}/api/project/${projectId}`); + url.searchParams.append('hide_disliked', hideDisliked ? 'true' : 'false'); + url.searchParams.append('page', page.toString()); + + logger.info(`Fetching project data: ${url.href}`); + + const response = await this.client.get(url.href, { + timeout: 10000 // 10 seconds timeout + }); + + if (response.status !== 200) { + throw new Error('Error response: ' + response.statusText); + } + + return response.data; + } } export const sunoApi = async (cookie?: string) => {