feat: optimize code

This commit is contained in:
GitPusher99 2024-03-28 13:21:33 +08:00
parent 2a309f87a7
commit d3160ce5f9
8 changed files with 5902 additions and 218 deletions

2
.gitignore vendored
View File

@ -34,3 +34,5 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
.idea

5676
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -16,17 +16,20 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.6.8", "axios": "^1.6.8",
"axios-cookiejar-support": "^5.0.0",
"next": "14.1.4", "next": "14.1.4",
"pino": "^8.19.0", "pino": "^8.19.0",
"pino-pretty": "^11.0.0", "pino-pretty": "^11.0.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"tough-cookie": "^4.1.3",
"user-agents": "^1.1.156" "user-agents": "^1.1.156"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/tough-cookie": "^4.0.5",
"@types/user-agents": "^1.0.4", "@types/user-agents": "^1.0.4",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"eslint": "^8", "eslint": "^8",

View File

@ -1,50 +1,50 @@
import { NextResponse, NextRequest } from "next/server"; import { NextResponse, NextRequest } from "next/server";
import SunoApi from '@/lib/sunoApi'; import { sunoApi } from "@/lib/SunoApi";
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
if (req.method === 'POST') { if (req.method === 'POST') {
try { try {
const body = await req.json(); const body = await req.json();
const { prompt, tags, title, make_instrumental, wait_audio } = body; const { prompt, tags, title, make_instrumental, wait_audio } = body;
// 校验输入参数 // 校验输入参数
if (!prompt || !tags || !title) { if (!prompt || !tags || !title) {
return new NextResponse(JSON.stringify({ error: 'Prompt, tags, and title are required' }), { return new NextResponse(JSON.stringify({ error: 'Prompt, tags, and title are required' }), {
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
});
}
// 调用 SunoApi.custom_generate 方法生成定制音频
const audioInfo = await SunoApi.custom_generate(
prompt, tags, title,
make_instrumental == true,
wait_audio == true
);
// 使用 NextResponse 构建成功响应
return new NextResponse(JSON.stringify(audioInfo), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
console.error('Error generating custom audio:', error.response.data);
if (error.response.status === 402) {
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
status: 402,
headers: { 'Content-Type': 'application/json' }
});
}
// 使用 NextResponse 构建错误响应
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
} else {
return new NextResponse('Method Not Allowed', {
headers: { Allow: 'POST' },
status: 405
}); });
}
// 调用 SunoApi.custom_generate 方法生成定制音频
const audioInfo = await (await sunoApi).custom_generate(
prompt, tags, title,
make_instrumental == true,
wait_audio == true
);
// 使用 NextResponse 构建成功响应
return new NextResponse(JSON.stringify(audioInfo), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
console.error('Error generating custom audio:', error.response.data);
if (error.response.status === 402) {
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
status: 402,
headers: { 'Content-Type': 'application/json' }
});
}
// 使用 NextResponse 构建错误响应
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
} }
} else {
return new NextResponse('Method Not Allowed', {
headers: { Allow: 'POST' },
status: 405
});
}
} }

View File

@ -1,46 +1,46 @@
import { NextResponse, NextRequest } from "next/server"; import { NextResponse, NextRequest } from "next/server";
import SunoApi from '@/lib/sunoApi'; import { sunoApi } from "@/lib/SunoApi";
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
if (req.method === 'POST') { if (req.method === 'POST') {
try { try {
const body = await req.json(); const body = await req.json();
const { prompt, make_instrumental, wait_audio } = body; const { prompt, make_instrumental, wait_audio } = body;
// 校验输入参数 // 校验输入参数
if (!prompt) { if (!prompt) {
return new NextResponse(JSON.stringify({ error: 'Prompt is required' }), { return new NextResponse(JSON.stringify({ error: 'Prompt is required' }), {
status: 400, status: 400,
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
});
}
// 调用 SunoApi.generate 方法生成音频
const audioInfo = await SunoApi.generate(prompt, make_instrumental == true, wait_audio == true);
// 使用 NextResponse 构建成功响应
return new NextResponse(JSON.stringify(audioInfo), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
console.error('Error generating custom audio:', JSON.stringify(error.response.data));
if (error.response.status === 402) {
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
status: 402,
headers: { 'Content-Type': 'application/json' }
});
}
// 使用 NextResponse 构建错误响应
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
} else {
return new NextResponse('Method Not Allowed', {
headers: { Allow: 'POST' },
status: 405
}); });
}
// 调用 SunoApi.generate 方法生成音频
const audioInfo = await (await sunoApi).generate(prompt, make_instrumental == true, wait_audio == true);
// 使用 NextResponse 构建成功响应
return new NextResponse(JSON.stringify(audioInfo), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
console.error('Error generating custom audio:', JSON.stringify(error.response.data));
if (error.response.status === 402) {
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
status: 402,
headers: { 'Content-Type': 'application/json' }
});
}
// 使用 NextResponse 构建错误响应
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
} }
} else {
return new NextResponse('Method Not Allowed', {
headers: { Allow: 'POST' },
status: 405
});
}
} }

View File

@ -1,38 +1,38 @@
import { NextResponse, NextRequest } from "next/server"; import { NextResponse, NextRequest } from "next/server";
import SunoApi from '@/lib/sunoApi'; import { sunoApi } from "@/lib/SunoApi";
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
if (req.method === 'GET') { if (req.method === 'GET') {
try { try {
// 修复了获取查询参数的方式 // 修复了获取查询参数的方式
const url = new URL(req.url); const url = new URL(req.url);
const songIds = url.searchParams.get('ids'); const songIds = url.searchParams.get('ids');
let audioInfo = []; let audioInfo = [];
if (songIds && songIds.length > 0) { if (songIds && songIds.length > 0) {
const idsArray = songIds.split(','); const idsArray = songIds.split(',');
// 调用 SunoApi.get 方法获取音频信息 // 调用 SunoApi.get 方法获取音频信息
audioInfo = await SunoApi.get(idsArray); audioInfo = await (await sunoApi).get(idsArray);
} else { } else {
audioInfo = await SunoApi.get(); audioInfo = await (await sunoApi).get();
} }
// 使用 NextResponse 构建成功响应 // 使用 NextResponse 构建成功响应
return new NextResponse(JSON.stringify(audioInfo), { return new NextResponse(JSON.stringify(audioInfo), {
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}); });
} catch (error) { } catch (error) {
console.error('Error fetching audio:', error); console.error('Error fetching audio:', error);
// 使用 NextResponse 构建错误响应 // 使用 NextResponse 构建错误响应
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), { return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}); });
}
} else {
return new NextResponse('Method Not Allowed', {
headers: { Allow: 'GET' },
status: 405
});
} }
} else {
return new NextResponse('Method Not Allowed', {
headers: { Allow: 'GET' },
status: 405
});
}
} }

View File

@ -1,29 +1,29 @@
import { NextResponse, NextRequest } from "next/server"; import { NextResponse, NextRequest } from "next/server";
import SunoApi from '@/lib/sunoApi'; import { sunoApi } from "@/lib/SunoApi";
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
if (req.method === 'GET') { if (req.method === 'GET') {
try { try {
// 调用 SunoApi.get_limit 方法获取剩余的信用额度 // 调用 SunoApi.get_limit 方法获取剩余的信用额度
const limit = await SunoApi.get_credits(); const limit = await (await sunoApi).get_credits();
// 使用 NextResponse 构建成功响应 // 使用 NextResponse 构建成功响应
return new NextResponse(JSON.stringify(limit), { return new NextResponse(JSON.stringify(limit), {
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}); });
} catch (error) { } catch (error) {
console.error('Error fetching limit:', error); console.error('Error fetching limit:', error);
// 使用 NextResponse 构建错误响应 // 使用 NextResponse 构建错误响应
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), { return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}); });
}
} else {
return new NextResponse('Method Not Allowed', {
headers: { Allow: 'GET' },
status: 405
});
} }
} else {
return new NextResponse('Method Not Allowed', {
headers: { Allow: 'GET' },
status: 405
});
}
} }

View File

@ -1,6 +1,9 @@
import axios from 'axios'; import axios, { AxiosInstance } from 'axios';
import UserAgent from 'user-agents'; import UserAgent from 'user-agents';
import pino from 'pino'; import pino from 'pino';
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";
const logger = pino(); const logger = pino();
@ -20,6 +23,7 @@ interface AudioInfo {
tags?: string; tags?: string;
duration?: string; duration?: string;
} }
/** /**
* *
* @param x * @param x
@ -39,71 +43,75 @@ const sleep = (x: number, y?: number): Promise<void> => {
} }
class SunoApi { class SunoApi {
private static baseUrl: string = 'https://studio-api.suno.ai'; private static BASE_URL: string = 'https://studio-api.suno.ai';
private static clerkBaseUrl: string = 'https://clerk.suno.ai'; private static CLERK_BASE_URL: string = 'https://clerk.suno.ai';
private static cookie: string = process.env.SUNO_COOKIE || '';
private static userAgent: string = new UserAgent().toString();
private static sid: string | null = null;
private static async getAuthToken(): Promise<string> { private readonly client: AxiosInstance;
private sid?: string;
constructor(cookie: string) {
const cookieJar = new CookieJar();
const randomUserAgent = new UserAgent(/Chrome/).random().toString();
this.client = wrapper(axios.create({
jar: cookieJar,
withCredentials: true,
headers: {
'User-Agent': randomUserAgent,
'Cookie': cookie
}
}))
}
public async init(): Promise<SunoApi> {
const token = await this.getAuthToken();
this.client.interceptors.request.use(function (config) {
config.headers['Authorization'] = `Bearer ${token}`
return config;
})
return this;
}
private async getAuthToken(): Promise<string> {
// 获取会话ID的URL // 获取会话ID的URL
const getSessionUrl = `${SunoApi.clerkBaseUrl}/v1/client?_clerk_js_version=4.70.5`; const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_clerk_js_version=4.70.5`;
// 交换令牌的URL模板 // 交换令牌的URL模板
const exchangeTokenUrlTemplate = `${SunoApi.clerkBaseUrl}/v1/client/sessions/{sid}/tokens/api?_clerk_js_version=4.70.0`; const exchangeTokenUrlTemplate = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/{sid}/tokens/api?_clerk_js_version=4.70.0`;
// 获取会话ID // 获取会话ID
const sessionResponse = await axios.get(getSessionUrl, { const sessionResponse = await this.client.get(getSessionUrl);
headers: { const sid = sessionResponse.data.response['last_active_session_id'];
'User-Agent': SunoApi.userAgent,
'Cookie': SunoApi.cookie,
},
});
const sid = sessionResponse.data.response?.last_active_session_id;
if (!sid) { if (!sid) {
throw new Error("Failed to get session id"); throw new Error("Failed to get session id");
} }
SunoApi.sid = sid; // 保存会话ID以备后用 // 保存会话ID以备后用
this.sid = sid;
// 使用会话ID获取JWT令牌 // 使用会话ID获取JWT令牌
const exchangeTokenUrl = exchangeTokenUrlTemplate.replace('{sid}', sid); const exchangeTokenUrl = exchangeTokenUrlTemplate.replace('{sid}', sid);
const tokenResponse = await axios.post( const tokenResponse = await this.client.post(exchangeTokenUrl);
exchangeTokenUrl, return tokenResponse.data['jwt'];
{},
{
headers: {
'User-Agent': SunoApi.userAgent,
'Cookie': SunoApi.cookie,
},
},
);
return tokenResponse.data.jwt;
} }
public static async KeepAlive(): Promise<void> {
if (!SunoApi.sid) { public async KeepAlive(): 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 // 续订会话令牌的URL
const renewUrl = `${SunoApi.clerkBaseUrl}/v1/client/sessions/${SunoApi.sid}/tokens/api?_clerk_js_version=4.70.0`; const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens/api?_clerk_js_version=4.70.0`;
// 续订会话令牌 // 续订会话令牌
const renewResponse = await axios.post( const renewResponse = await this.client.post(renewUrl);
renewUrl,
{},
{
headers: {
'User-Agent': SunoApi.userAgent,
'Cookie': SunoApi.cookie,
},
},
);
logger.info("KeepAlive...\n"); logger.info("KeepAlive...\n");
await sleep(1, 2); await sleep(1, 2);
const newToken = renewResponse.data.jwt; const newToken = renewResponse.data['jwt'];
// 更新请求头中的Authorization字段使用新的JWT令牌 // 更新请求头中的Authorization字段使用新的JWT令牌
axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; this.client.interceptors.request.use(function (config) {
config.headers['Authorization'] = `Bearer ${newToken}`
return config;
})
} }
public static async generate( public async generate(
prompt: string, prompt: string,
make_instrumental: boolean = false, make_instrumental: boolean = false,
wait_audio: boolean = false, wait_audio: boolean = false,
@ -126,7 +134,7 @@ class SunoApi {
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning. * @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
* @returns A promise that resolves to an array of AudioInfo objects representing the generated audios. * @returns A promise that resolves to an array of AudioInfo objects representing the generated audios.
*/ */
public static async custom_generate( public async custom_generate(
prompt: string, prompt: string,
tags: string, tags: string,
title: string, title: string,
@ -152,7 +160,7 @@ class SunoApi {
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning. * @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
* @returns A promise that resolves to an array of AudioInfo objects representing the generated songs. * @returns A promise that resolves to an array of AudioInfo objects representing the generated songs.
*/ */
private static async generateSongs( private async generateSongs(
prompt: string, prompt: string,
isCustom: boolean, isCustom: boolean,
tags?: string, tags?: string,
@ -182,14 +190,10 @@ class SunoApi {
wait_audio: wait_audio, wait_audio: wait_audio,
payload: payload, payload: payload,
}); });
const response = await axios.post( const response = await this.client.post(
`${SunoApi.baseUrl}/api/generate/v2/`, `${SunoApi.BASE_URL}/api/generate/v2/`,
payload, payload,
{ {
headers: {
'Authorization': `Bearer ${authToken}`,
'User-Agent': SunoApi.userAgent,
},
timeout: 10000, // 10 seconds timeout timeout: 10000, // 10 seconds timeout
}, },
); );
@ -197,14 +201,14 @@ class SunoApi {
if (response.status !== 200) { 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); const songIds = response.data['clips'].map((audio: any) => audio.id);
//Want to wait for music file generation //Want to wait for music file generation
if (wait_audio === true) { if (wait_audio) {
const startTime = Date.now(); const startTime = Date.now();
let lastResponse: AudioInfo[] = []; let lastResponse: AudioInfo[] = [];
await sleep(5, 5); await sleep(5, 5);
while (Date.now() - startTime < 100000) { while (Date.now() - startTime < 100000) {
const response = await SunoApi.get(songIds); const response = await this.get(songIds);
const allCompleted = response.every( const allCompleted = response.every(
audio => audio.status === 'streaming' || audio.status === 'complete' audio => audio.status === 'streaming' || audio.status === 'complete'
); );
@ -213,12 +217,12 @@ class SunoApi {
} }
lastResponse = response; lastResponse = response;
await sleep(3, 6); await sleep(3, 6);
this.KeepAlive(); await this.KeepAlive();
} }
return lastResponse; return lastResponse;
} else { } else {
this.KeepAlive(); await this.KeepAlive();
return response.data.clips.map((audio: any) => ({ return response.data['clips'].map((audio: any) => ({
id: audio.id, id: audio.id,
title: audio.title, title: audio.title,
image_url: audio.image_url, image_url: audio.image_url,
@ -236,12 +240,13 @@ class SunoApi {
})); }));
} }
} }
/** /**
* Processes the lyrics (prompt) from the audio metadata into a more readable format. * Processes the lyrics (prompt) from the audio metadata into a more readable format.
* @param prompt The original lyrics text. * @param prompt The original lyrics text.
* @returns The processed lyrics text. * @returns The processed lyrics text.
*/ */
private static parseLyrics(prompt: string): string { private parseLyrics(prompt: string): string {
// Assuming the original lyrics are separated by a specific delimiter (e.g., newline), we can convert it into a more readable format. // Assuming the original lyrics are separated by a specific delimiter (e.g., newline), we can convert it into a more readable format.
// The implementation here can be adjusted according to the actual lyrics format. // The implementation here can be adjusted according to the actual lyrics format.
// For example, if the lyrics exist as continuous text, it might be necessary to split them based on specific markers (such as periods, commas, etc.). // For example, if the lyrics exist as continuous text, it might be necessary to split them based on specific markers (such as periods, commas, etc.).
@ -262,19 +267,16 @@ class SunoApi {
* @param songIds An optional array of song IDs to retrieve information for. * @param songIds An optional array of song IDs to retrieve information for.
* @returns A promise that resolves to an array of AudioInfo objects. * @returns A promise that resolves to an array of AudioInfo objects.
*/ */
public static async get(songIds?: string[]): Promise<AudioInfo[]> { public async get(songIds?: string[]): Promise<AudioInfo[]> {
const authToken = await this.getAuthToken(); const authToken = await this.getAuthToken();
let url = `${SunoApi.baseUrl}/api/feed/`; let url = `${SunoApi.BASE_URL}/api/feed/`;
if (songIds) { if (songIds) {
url = `${url}?ids=${songIds.join(',')}`; url = `${url}?ids=${songIds.join(',')}`;
} }
logger.info("Get audio status: ", url); logger.info("Get audio status: ", url);
const response = await axios.get(url, { const response = await this.client.get(url, {
headers: { // 3 seconds timeout
'Authorization': `Bearer ${authToken}`, timeout: 3000
'User-Agent': SunoApi.userAgent,
},
timeout: 3000, // 3 seconds timeout
}); });
const audios = response.data; const audios = response.data;
@ -296,14 +298,9 @@ class SunoApi {
})); }));
} }
public static async get_credits(): Promise<object> { public async get_credits(): Promise<object> {
const authToken = await this.getAuthToken(); const authToken = await this.getAuthToken();
const response = await axios.get(`${SunoApi.baseUrl}/api/billing/info/`, { const response = await this.client.get(`${SunoApi.BASE_URL}/api/billing/info/`);
headers: {
'Authorization': `Bearer ${authToken}`,
'User-Agent': SunoApi.userAgent,
},
});
return { return {
credits_left: response.data.total_credits_left, credits_left: response.data.total_credits_left,
period: response.data.period, period: response.data.period,
@ -313,4 +310,10 @@ class SunoApi {
} }
} }
export default SunoApi; const newSunoApi = async (cookie: string) => {
const sunoApi = new SunoApi(cookie);
await sunoApi.init();
return sunoApi;
}
export const sunoApi = newSunoApi(process.env.SUNO_COOKIE || '');