From 2bc500723f40bccc608874edaba0b09cac45c14a Mon Sep 17 00:00:00 2001 From: Son Tran Lam Date: Mon, 17 Feb 2025 19:28:27 +0800 Subject: [PATCH] feat(api): Add persona endpoint for retrieving persona information and clips - Implement `/api/persona` GET endpoint to fetch persona details - Add Swagger documentation for the new persona API endpoint - Update docs page to include new `/api/persona` route description - Extend SunoApi class with `getPersonaPaginated` method to support persona data retrieval --- src/app/api/persona/route.ts | 61 ++++++++++++ src/app/docs/page.tsx | 1 + src/app/docs/swagger-suno-api.json | 143 +++++++++++++++++++++++++++++ src/lib/SunoApi.ts | 46 ++++++++++ 4 files changed, 251 insertions(+) create mode 100644 src/app/api/persona/route.ts diff --git a/src/app/api/persona/route.ts b/src/app/api/persona/route.ts new file mode 100644 index 0000000..b697f24 --- /dev/null +++ b/src/app/api/persona/route.ts @@ -0,0 +1,61 @@ +import { NextResponse, NextRequest } from "next/server"; +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 personaId = url.searchParams.get('id'); + const page = url.searchParams.get('page'); + + if (personaId == null) { + return new NextResponse(JSON.stringify({ error: 'Missing parameter id' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } + + const pageNumber = page ? parseInt(page) : 1; + const personaInfo = await (await sunoApi()).getPersonaPaginated(personaId, pageNumber); + + return new NextResponse(JSON.stringify(personaInfo), { + status: 200, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } catch (error) { + console.error('Error fetching persona:', 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 + }); +} diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx index 379201c..9662ba5 100644 --- a/src/app/docs/page.tsx +++ b/src/app/docs/page.tsx @@ -33,6 +33,7 @@ export default function Docs() { - \`/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/persona\`: Get persona information and clips based on ID and page number \`\`\` Feel free to explore the detailed API parameters and conduct tests on this page. diff --git a/src/app/docs/swagger-suno-api.json b/src/app/docs/swagger-suno-api.json index 3c5ff33..fbc03ea 100644 --- a/src/app/docs/swagger-suno-api.json +++ b/src/app/docs/swagger-suno-api.json @@ -588,6 +588,149 @@ } } } + }, + "/api/persona": { + "get": { + "summary": "Get persona information and clips.", + "description": "Retrieve persona information, including associated clips and pagination data.", + "tags": ["default"], + "parameters": [ + { + "name": "id", + "in": "query", + "required": true, + "description": "Persona ID", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "description": "Page number (defaults to 1)", + "schema": { + "type": "integer", + "default": 1 + } + } + ], + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "persona": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Persona ID" + }, + "name": { + "type": "string", + "description": "Persona name" + }, + "description": { + "type": "string", + "description": "Persona description" + }, + "image_s3_id": { + "type": "string", + "description": "Persona image URL" + }, + "root_clip_id": { + "type": "string", + "description": "Root clip ID" + }, + "clip": { + "type": "object", + "description": "Root clip information" + }, + "persona_clips": { + "type": "array", + "items": { + "type": "object", + "properties": { + "clip": { + "type": "object", + "description": "Clip information" + } + } + } + }, + "is_suno_persona": { + "type": "boolean", + "description": "Whether this is a Suno official persona" + }, + "is_public": { + "type": "boolean", + "description": "Whether this persona is public" + }, + "upvote_count": { + "type": "integer", + "description": "Number of upvotes" + }, + "clip_count": { + "type": "integer", + "description": "Number of clips" + } + } + }, + "total_results": { + "type": "integer", + "description": "Total number of results" + }, + "current_page": { + "type": "integer", + "description": "Current page number" + }, + "is_following": { + "type": "boolean", + "description": "Whether the current user is following this persona" + } + } + } + } + } + }, + "400": { + "description": "Missing parameter id", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Missing parameter id" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal server error" + } + } + } + } + } + } + } + } } }, "components": { diff --git a/src/lib/SunoApi.ts b/src/lib/SunoApi.ts index 75e6bd7..f01ac72 100644 --- a/src/lib/SunoApi.ts +++ b/src/lib/SunoApi.ts @@ -39,6 +39,34 @@ export interface AudioInfo { error_message?: string; // Error message if any } +interface PersonaResponse { + persona: { + id: string; + name: string; + description: string; + image_s3_id: string; + root_clip_id: string; + clip: any; // You can define a more specific type if needed + user_display_name: string; + user_handle: string; + user_image_url: string; + persona_clips: Array<{ + clip: any; // You can define a more specific type if needed + }>; + is_suno_persona: boolean; + is_trashed: boolean; + is_owned: boolean; + is_public: boolean; + is_public_approved: boolean; + is_loved: boolean; + upvote_count: number; + clip_count: number; + }; + total_results: number; + current_page: number; + is_following: boolean; +} + class SunoApi { private static BASE_URL: string = 'https://studio-api.prod.suno.com'; private static CLERK_BASE_URL: string = 'https://clerk.suno.com'; @@ -801,6 +829,24 @@ class SunoApi { monthly_usage: response.data.monthly_usage }; } + + public async getPersonaPaginated(personaId: string, page: number = 1): Promise { + await this.keepAlive(false); + + const url = `${SunoApi.BASE_URL}/api/persona/get-persona-paginated/${personaId}/?page=${page}`; + + logger.info(`Fetching persona 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; + } } export const sunoApi = async (cookie?: string) => {