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:
parent
c3a8c568a5
commit
2bc500723f
61
src/app/api/persona/route.ts
Normal file
61
src/app/api/persona/route.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
@ -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.
|
||||||
|
@ -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": {
|
||||||
|
@ -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) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user