Skip to content

Commit 0646ebc

Browse files
authored
Merge pull request #234 from sontl/persona-api
feat(api): Add persona endpoint for retrieving persona information and clips
2 parents c3a8c56 + 2bc5007 commit 0646ebc

File tree

4 files changed

+251
-0
lines changed

4 files changed

+251
-0
lines changed

src/app/api/persona/route.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { NextResponse, NextRequest } from "next/server";
2+
import { sunoApi } from "@/lib/SunoApi";
3+
import { corsHeaders } from "@/lib/utils";
4+
5+
export const dynamic = "force-dynamic";
6+
7+
export async function GET(req: NextRequest) {
8+
if (req.method === 'GET') {
9+
try {
10+
const url = new URL(req.url);
11+
const personaId = url.searchParams.get('id');
12+
const page = url.searchParams.get('page');
13+
14+
if (personaId == null) {
15+
return new NextResponse(JSON.stringify({ error: 'Missing parameter id' }), {
16+
status: 400,
17+
headers: {
18+
'Content-Type': 'application/json',
19+
...corsHeaders
20+
}
21+
});
22+
}
23+
24+
const pageNumber = page ? parseInt(page) : 1;
25+
const personaInfo = await (await sunoApi()).getPersonaPaginated(personaId, pageNumber);
26+
27+
return new NextResponse(JSON.stringify(personaInfo), {
28+
status: 200,
29+
headers: {
30+
'Content-Type': 'application/json',
31+
...corsHeaders
32+
}
33+
});
34+
} catch (error) {
35+
console.error('Error fetching persona:', error);
36+
37+
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
38+
status: 500,
39+
headers: {
40+
'Content-Type': 'application/json',
41+
...corsHeaders
42+
}
43+
});
44+
}
45+
} else {
46+
return new NextResponse('Method Not Allowed', {
47+
headers: {
48+
Allow: 'GET',
49+
...corsHeaders
50+
},
51+
status: 405
52+
});
53+
}
54+
}
55+
56+
export async function OPTIONS(request: Request) {
57+
return new Response(null, {
58+
status: 200,
59+
headers: corsHeaders
60+
});
61+
}

src/app/docs/page.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export default function Docs() {
3333
- \`/api/get_aligned_lyrics\`: Get list of timestamps for each word in the lyrics
3434
- \`/api/clip\`: Get clip information based on ID passed as query parameter \`id\`
3535
- \`/api/concat\`: Generate the whole song from extensions
36+
- \`/api/persona\`: Get persona information and clips based on ID and page number
3637
\`\`\`
3738
3839
Feel free to explore the detailed API parameters and conduct tests on this page.

src/app/docs/swagger-suno-api.json

+143
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,149 @@
588588
}
589589
}
590590
}
591+
},
592+
"/api/persona": {
593+
"get": {
594+
"summary": "Get persona information and clips.",
595+
"description": "Retrieve persona information, including associated clips and pagination data.",
596+
"tags": ["default"],
597+
"parameters": [
598+
{
599+
"name": "id",
600+
"in": "query",
601+
"required": true,
602+
"description": "Persona ID",
603+
"schema": {
604+
"type": "string"
605+
}
606+
},
607+
{
608+
"name": "page",
609+
"in": "query",
610+
"required": false,
611+
"description": "Page number (defaults to 1)",
612+
"schema": {
613+
"type": "integer",
614+
"default": 1
615+
}
616+
}
617+
],
618+
"responses": {
619+
"200": {
620+
"description": "success",
621+
"content": {
622+
"application/json": {
623+
"schema": {
624+
"type": "object",
625+
"properties": {
626+
"persona": {
627+
"type": "object",
628+
"properties": {
629+
"id": {
630+
"type": "string",
631+
"description": "Persona ID"
632+
},
633+
"name": {
634+
"type": "string",
635+
"description": "Persona name"
636+
},
637+
"description": {
638+
"type": "string",
639+
"description": "Persona description"
640+
},
641+
"image_s3_id": {
642+
"type": "string",
643+
"description": "Persona image URL"
644+
},
645+
"root_clip_id": {
646+
"type": "string",
647+
"description": "Root clip ID"
648+
},
649+
"clip": {
650+
"type": "object",
651+
"description": "Root clip information"
652+
},
653+
"persona_clips": {
654+
"type": "array",
655+
"items": {
656+
"type": "object",
657+
"properties": {
658+
"clip": {
659+
"type": "object",
660+
"description": "Clip information"
661+
}
662+
}
663+
}
664+
},
665+
"is_suno_persona": {
666+
"type": "boolean",
667+
"description": "Whether this is a Suno official persona"
668+
},
669+
"is_public": {
670+
"type": "boolean",
671+
"description": "Whether this persona is public"
672+
},
673+
"upvote_count": {
674+
"type": "integer",
675+
"description": "Number of upvotes"
676+
},
677+
"clip_count": {
678+
"type": "integer",
679+
"description": "Number of clips"
680+
}
681+
}
682+
},
683+
"total_results": {
684+
"type": "integer",
685+
"description": "Total number of results"
686+
},
687+
"current_page": {
688+
"type": "integer",
689+
"description": "Current page number"
690+
},
691+
"is_following": {
692+
"type": "boolean",
693+
"description": "Whether the current user is following this persona"
694+
}
695+
}
696+
}
697+
}
698+
}
699+
},
700+
"400": {
701+
"description": "Missing parameter id",
702+
"content": {
703+
"application/json": {
704+
"schema": {
705+
"type": "object",
706+
"properties": {
707+
"error": {
708+
"type": "string",
709+
"example": "Missing parameter id"
710+
}
711+
}
712+
}
713+
}
714+
}
715+
},
716+
"500": {
717+
"description": "Internal server error",
718+
"content": {
719+
"application/json": {
720+
"schema": {
721+
"type": "object",
722+
"properties": {
723+
"error": {
724+
"type": "string",
725+
"example": "Internal server error"
726+
}
727+
}
728+
}
729+
}
730+
}
731+
}
732+
}
733+
}
591734
}
592735
},
593736
"components": {

src/lib/SunoApi.ts

+46
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,34 @@ export interface AudioInfo {
3939
error_message?: string; // Error message if any
4040
}
4141

42+
interface PersonaResponse {
43+
persona: {
44+
id: string;
45+
name: string;
46+
description: string;
47+
image_s3_id: string;
48+
root_clip_id: string;
49+
clip: any; // You can define a more specific type if needed
50+
user_display_name: string;
51+
user_handle: string;
52+
user_image_url: string;
53+
persona_clips: Array<{
54+
clip: any; // You can define a more specific type if needed
55+
}>;
56+
is_suno_persona: boolean;
57+
is_trashed: boolean;
58+
is_owned: boolean;
59+
is_public: boolean;
60+
is_public_approved: boolean;
61+
is_loved: boolean;
62+
upvote_count: number;
63+
clip_count: number;
64+
};
65+
total_results: number;
66+
current_page: number;
67+
is_following: boolean;
68+
}
69+
4270
class SunoApi {
4371
private static BASE_URL: string = 'https://studio-api.prod.suno.com';
4472
private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
@@ -801,6 +829,24 @@ class SunoApi {
801829
monthly_usage: response.data.monthly_usage
802830
};
803831
}
832+
833+
public async getPersonaPaginated(personaId: string, page: number = 1): Promise<PersonaResponse> {
834+
await this.keepAlive(false);
835+
836+
const url = `${SunoApi.BASE_URL}/api/persona/get-persona-paginated/${personaId}/?page=${page}`;
837+
838+
logger.info(`Fetching persona data: ${url}`);
839+
840+
const response = await this.client.get(url, {
841+
timeout: 10000 // 10 seconds timeout
842+
});
843+
844+
if (response.status !== 200) {
845+
throw new Error('Error response: ' + response.statusText);
846+
}
847+
848+
return response.data;
849+
}
804850
}
805851

806852
export const sunoApi = async (cookie?: string) => {

0 commit comments

Comments
 (0)