feat(get): add page query param

This commit is contained in:
Wesley Seubring 2024-10-25 13:51:08 +02:00
parent 577926ab0c
commit b2f0c3aacc
3 changed files with 182 additions and 113 deletions

View File

@ -1,20 +1,22 @@
import { NextResponse, NextRequest } from "next/server";
import { sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
import { NextResponse, NextRequest } from 'next/server';
import { sunoApi } from '@/lib/SunoApi';
import { corsHeaders } from '@/lib/utils';
export const dynamic = "force-dynamic";
export const dynamic = 'force-dynamic';
export async function GET(req: NextRequest) {
if (req.method === 'GET') {
try {
const url = new URL(req.url);
const songIds = url.searchParams.get('ids');
const page = url.searchParams.get('page');
let audioInfo = [];
if (songIds && songIds.length > 0) {
const idsArray = songIds.split(',');
audioInfo = await (await sunoApi).get(idsArray);
audioInfo = await (await sunoApi).get(idsArray, page);
} else {
audioInfo = await (await sunoApi).get();
audioInfo = await (await sunoApi).get(undefined, page);
}
return new NextResponse(JSON.stringify(audioInfo), {
@ -27,13 +29,16 @@ export async function GET(req: NextRequest) {
} catch (error) {
console.error('Error fetching audio:', error);
return new NextResponse(JSON.stringify({ error: 'Internal server 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', {

View File

@ -307,6 +307,15 @@
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "page",
"description": "Page number",
"required": false,
"schema": {
"type": "number"
}
}
],
"responses": {

View File

@ -1,13 +1,12 @@
import axios, { AxiosInstance } from 'axios';
import UserAgent from 'user-agents';
import pino from 'pino';
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";
import { sleep } from "@/lib/utils";
import { wrapper } from 'axios-cookiejar-support';
import { CookieJar } from 'tough-cookie';
import { sleep } from '@/lib/utils';
const logger = pino();
export const DEFAULT_MODEL = "chirp-v3-5";
export const DEFAULT_MODEL = 'chirp-v3-5';
export interface AudioInfo {
id: string; // Unique identifier for the audio
@ -19,7 +18,7 @@ export interface AudioInfo {
created_at: string; // Date and time when the audio was created
model_name: string; // Name of the model used for audio generation
gpt_description_prompt?: string; // Prompt for GPT description
prompt?: string; // Prompt for audio generation 
prompt?: string; // Prompt for audio generation
status: string; // Status
type?: string;
tags?: string; // Genre of music.
@ -41,16 +40,19 @@ class SunoApi {
constructor(cookie: string) {
const cookieJar = new CookieJar();
const randomUserAgent = new UserAgent(/Chrome/).random().toString();
this.client = wrapper(axios.create({
this.client = wrapper(
axios.create({
jar: cookieJar,
withCredentials: true,
headers: {
'User-Agent': randomUserAgent,
'Cookie': cookie
Cookie: cookie
}
}))
})
);
this.client.interceptors.request.use((config) => {
if (this.currentToken) { // Use the current token status
if (this.currentToken) {
// Use the current token status
config.headers['Authorization'] = `Bearer ${this.currentToken}`;
}
return config;
@ -69,11 +71,13 @@ class SunoApi {
*/
private async getClerkLatestVersion() {
// URL to get clerk version ID
const getClerkVersionUrl = `${SunoApi.JSDELIVR_BASE_URL}/v1/package/npm/@clerk/clerk-js`; 
const getClerkVersionUrl = `${SunoApi.JSDELIVR_BASE_URL}/v1/package/npm/@clerk/clerk-js`;
// Get clerk version ID
const versionListResponse = await this.client.get(getClerkVersionUrl);
if (!versionListResponse?.data?.['tags']['latest']) {
throw new Error("Failed to get clerk version info, Please try again later");
throw new Error(
'Failed to get clerk version info, Please try again later'
);
}
// Save clerk version ID for auth
this.clerkVersion = versionListResponse?.data?.['tags']['latest'];
@ -84,11 +88,13 @@ class SunoApi {
*/
private async getAuthToken() {
// URL to get session ID
const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_clerk_js_version=${this.clerkVersion}`; 
const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_clerk_js_version=${this.clerkVersion}`;
// Get session ID
const sessionResponse = await this.client.get(getSessionUrl);
if (!sessionResponse?.data?.response?.['last_active_session_id']) {
throw new Error("Failed to get session id, you may need to update the SUNO_COOKIE");
throw new Error(
'Failed to get session id, you may need to update the SUNO_COOKIE'
);
}
// Save session ID for later use
this.sid = sessionResponse.data.response['last_active_session_id'];
@ -100,13 +106,13 @@ class SunoApi {
*/
public async keepAlive(isWait?: boolean): Promise<void> {
if (!this.sid) {
throw new Error("Session ID is not set. Cannot renew token.");
throw new Error('Session ID is not set. Cannot renew token.');
}
// URL to renew session token
const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens?_clerk_js_version==${this.clerkVersion}`; 
const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens?_clerk_js_version==${this.clerkVersion}`;
// Renew session token
const renewResponse = await this.client.post(renewUrl);
logger.info("KeepAlive...\n");
logger.info('KeepAlive...\n');
if (isWait) {
await sleep(1, 2);
}
@ -126,15 +132,22 @@ class SunoApi {
prompt: string,
make_instrumental: boolean = false,
model?: string,
wait_audio: boolean = false,
wait_audio: boolean = false
): Promise<AudioInfo[]> {
await this.keepAlive(false);
const startTime = Date.now();
const audios = this.generateSongs(prompt, false, undefined, undefined, make_instrumental, model, wait_audio);
const audios = this.generateSongs(
prompt,
false,
undefined,
undefined,
make_instrumental,
model,
wait_audio
);
const costTime = Date.now() - startTime;
logger.info("Generate Response:\n" + JSON.stringify(audios, null, 2));
logger.info("Cost time: " + costTime);
logger.info('Generate Response:\n' + JSON.stringify(audios, null, 2));
logger.info('Cost time: ' + costTime);
return audios;
}
@ -152,11 +165,11 @@ class SunoApi {
`${SunoApi.BASE_URL}/api/generate/concat/v2/`,
payload,
{
timeout: 10000, // 10 seconds timeout
},
timeout: 10000 // 10 seconds timeout
}
);
if (response.status !== 200) {
throw new Error("Error response:" + response.statusText);
throw new Error('Error response:' + response.statusText);
}
return response.data;
}
@ -172,22 +185,33 @@ class SunoApi {
* @param negative_tags Negative tags that should not be included in the generated audio.
* @returns A promise that resolves to an array of AudioInfo objects representing the generated audios.
*/
public async custom_generate(
public async custom_generate(
prompt: string,
tags: string,
title: string,
make_instrumental: boolean = false,
model?: string,
wait_audio: boolean = false,
negative_tags?: string,
): Promise<AudioInfo[]> {
negative_tags?: string
): Promise<AudioInfo[]> {
const startTime = Date.now();
const audios = await this.generateSongs(prompt, true, tags, title, make_instrumental, model, wait_audio, negative_tags);
const audios = await this.generateSongs(
prompt,
true,
tags,
title,
make_instrumental,
model,
wait_audio,
negative_tags
);
const costTime = Date.now() - startTime;
logger.info("Custom Generate Response:\n" + JSON.stringify(audios, null, 2));
logger.info("Cost time: " + costTime);
logger.info(
'Custom Generate Response:\n' + JSON.stringify(audios, null, 2)
);
logger.info('Cost time: ' + costTime);
return audios;
}
}
/**
* Generates songs based on the provided parameters.
@ -209,13 +233,13 @@ public async custom_generate(
make_instrumental?: boolean,
model?: string,
wait_audio: boolean = false,
negative_tags?: string,
negative_tags?: string
): Promise<AudioInfo[]> {
await this.keepAlive(false);
const payload: any = {
make_instrumental: make_instrumental == true,
mv: model || DEFAULT_MODEL,
prompt: "",
prompt: ''
};
if (isCustom) {
payload.tags = tags;
@ -225,7 +249,10 @@ public async custom_generate(
} else {
payload.gpt_description_prompt = prompt;
}
logger.info("generateSongs payload:\n" + JSON.stringify({
logger.info(
'generateSongs payload:\n' +
JSON.stringify(
{
prompt: prompt,
isCustom: isCustom,
tags: tags,
@ -233,18 +260,24 @@ public async custom_generate(
make_instrumental: make_instrumental,
wait_audio: wait_audio,
negative_tags: negative_tags,
payload: payload,
}, null, 2));
payload: payload
},
null,
2
)
);
const response = await this.client.post(
`${SunoApi.BASE_URL}/api/generate/v2/`,
payload,
{
timeout: 10000, // 10 seconds timeout
},
timeout: 10000 // 10 seconds timeout
}
);
logger.info(
'generateSongs Response:\n' + JSON.stringify(response.data, null, 2)
);
logger.info("generateSongs Response:\n" + JSON.stringify(response.data, null, 2));
if (response.status !== 200) {
throw new Error("Error response:" + response.statusText);
throw new Error('Error response:' + response.statusText);
}
const songIds = response.data['clips'].map((audio: any) => audio.id);
//Want to wait for music file generation
@ -255,11 +288,9 @@ public async custom_generate(
while (Date.now() - startTime < 100000) {
const response = await this.get(songIds);
const allCompleted = response.every(
audio => audio.status === 'streaming' || audio.status === 'complete'
);
const allError = response.every(
audio => audio.status === 'error'
(audio) => audio.status === 'streaming' || audio.status === 'complete'
);
const allError = response.every((audio) => audio.status === 'error');
if (allCompleted || allError) {
return response;
}
@ -285,7 +316,7 @@ public async custom_generate(
type: audio.metadata.type,
tags: audio.metadata.tags,
negative_tags: audio.metadata.negative_tags,
duration: audio.metadata.duration,
duration: audio.metadata.duration
}));
}
}
@ -298,14 +329,21 @@ public async custom_generate(
public async generateLyrics(prompt: string): Promise<string> {
await this.keepAlive(false);
// Initiate lyrics generation
const generateResponse = await this.client.post(`${SunoApi.BASE_URL}/api/generate/lyrics/`, { prompt });
const generateResponse = await this.client.post(
`${SunoApi.BASE_URL}/api/generate/lyrics/`,
{ prompt }
);
const generateId = generateResponse.data.id;
// Poll for lyrics completion
let lyricsResponse = await this.client.get(`${SunoApi.BASE_URL}/api/generate/lyrics/${generateId}`);
let lyricsResponse = await this.client.get(
`${SunoApi.BASE_URL}/api/generate/lyrics/${generateId}`
);
while (lyricsResponse?.data?.status !== 'complete') {
await sleep(2); // Wait for 2 seconds before polling again
lyricsResponse = await this.client.get(`${SunoApi.BASE_URL}/api/generate/lyrics/${generateId}`);
lyricsResponse = await this.client.get(
`${SunoApi.BASE_URL}/api/generate/lyrics/${generateId}`
);
}
// Return the generated lyrics text
@ -324,21 +362,24 @@ public async custom_generate(
*/
public async extendAudio(
audioId: string,
prompt: string = "",
continueAt: string = "0",
tags: string = "",
title: string = "",
model?: string,
prompt: string = '',
continueAt: string = '0',
tags: string = '',
title: string = '',
model?: string
): Promise<AudioInfo> {
const response = await this.client.post(`${SunoApi.BASE_URL}/api/generate/v2/`, {
const response = await this.client.post(
`${SunoApi.BASE_URL}/api/generate/v2/`,
{
continue_clip_id: audioId,
continue_at: continueAt,
mv: model || DEFAULT_MODEL,
prompt: prompt,
tags: tags,
title: title
});
console.log("response\n", response);
}
);
console.log('response\n', response);
return response.data;
}
@ -354,7 +395,7 @@ public async custom_generate(
// The following implementation assumes that the lyrics are already separated by newlines.
// Split the lyrics using newline and ensure to remove empty lines.
const lines = prompt.split('\n').filter(line => line.trim() !== '');
const lines = prompt.split('\n').filter((line) => line.trim() !== '');
// Reassemble the processed lyrics lines into a single string, separated by newlines between each line.
// Additional formatting logic can be added here, such as adding specific markers or handling special lines.
@ -364,26 +405,36 @@ public async custom_generate(
/**
* Retrieves audio information for the given song IDs.
* @param songIds An optional array of song IDs to retrieve information for.
* @param page An optional page number to retrieve audio information from.
* @returns A promise that resolves to an array of AudioInfo objects.
*/
public async get(songIds?: string[]): Promise<AudioInfo[]> {
public async get(
songIds?: string[],
page?: string | null
): Promise<AudioInfo[]> {
await this.keepAlive(false);
let url = `${SunoApi.BASE_URL}/api/feed/`;
let url = new URL(`${SunoApi.BASE_URL}/api/feed/`);
if (songIds) {
url = `${url}?ids=${songIds.join(',')}`;
url.searchParams.append('ids', songIds.join(','));
}
logger.info("Get audio status: " + url);
const response = await this.client.get(url, {
if (page) {
url.searchParams.append('page', page);
}
logger.info('Get audio status: ' + url.href);
const response = await this.client.get(url.href, {
// 3 seconds timeout
timeout: 3000
});
const audios = response.data;
return audios.map((audio: any) => ({
id: audio.id,
title: audio.title,
image_url: audio.image_url,
lyric: audio.metadata.prompt ? this.parseLyrics(audio.metadata.prompt) : "",
lyric: audio.metadata.prompt
? this.parseLyrics(audio.metadata.prompt)
: '',
audio_url: audio.audio_url,
video_url: audio.video_url,
created_at: audio.created_at,
@ -394,7 +445,7 @@ public async custom_generate(
type: audio.metadata.type,
tags: audio.metadata.tags,
duration: audio.metadata.duration,
error_message: audio.metadata.error_message,
error_message: audio.metadata.error_message
}));
}
@ -405,18 +456,22 @@ public async custom_generate(
*/
public async getClip(clipId: string): Promise<object> {
await this.keepAlive(false);
const response = await this.client.get(`${SunoApi.BASE_URL}/api/clip/${clipId}`);
const response = await this.client.get(
`${SunoApi.BASE_URL}/api/clip/${clipId}`
);
return response.data;
}
public async get_credits(): Promise<object> {
await this.keepAlive(false);
const response = await this.client.get(`${SunoApi.BASE_URL}/api/billing/info/`);
const response = await this.client.get(
`${SunoApi.BASE_URL}/api/billing/info/`
);
return {
credits_left: response.data.total_credits_left,
period: response.data.period,
monthly_limit: response.data.monthly_limit,
monthly_usage: response.data.monthly_usage,
monthly_usage: response.data.monthly_usage
};
}
}
@ -424,10 +479,10 @@ public async custom_generate(
const newSunoApi = async (cookie: string) => {
const sunoApi = new SunoApi(cookie);
return await sunoApi.init();
}
};
if (!process.env.SUNO_COOKIE) {
console.log("Environment does not contain SUNO_COOKIE.", process.env)
console.log('Environment does not contain SUNO_COOKIE.', process.env);
}
export const sunoApi = newSunoApi(process.env.SUNO_COOKIE || '');