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
This commit is contained in:
Son Tran Lam 2025-02-17 19:28:27 +08:00
parent c3a8c568a5
commit 2bc500723f
4 changed files with 251 additions and 0 deletions

View File

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

View File

@ -33,6 +33,7 @@ export default function Docs() {
- \`/api/get_aligned_lyrics\`: Get list of timestamps for each word in the lyrics - \`/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/clip\`: Get clip information based on ID passed as query parameter \`id\`
- \`/api/concat\`: Generate the whole song from extensions - \`/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. Feel free to explore the detailed API parameters and conduct tests on this page.

View File

@ -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": { "components": {

View File

@ -39,6 +39,34 @@ export interface AudioInfo {
error_message?: string; // Error message if any 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 { class SunoApi {
private static BASE_URL: string = 'https://studio-api.prod.suno.com'; private static BASE_URL: string = 'https://studio-api.prod.suno.com';
private static CLERK_BASE_URL: string = 'https://clerk.suno.com'; private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
@ -801,6 +829,24 @@ class SunoApi {
monthly_usage: response.data.monthly_usage monthly_usage: response.data.monthly_usage
}; };
} }
public async getPersonaPaginated(personaId: string, page: number = 1): Promise<PersonaResponse> {
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) => { export const sunoApi = async (cookie?: string) => {