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
*.tsbuildinfo
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": {
"axios": "^1.6.8",
"axios-cookiejar-support": "^5.0.0",
"next": "14.1.4",
"pino": "^8.19.0",
"pino-pretty": "^11.0.0",
"react": "^18",
"react-dom": "^18",
"tough-cookie": "^4.1.3",
"user-agents": "^1.1.156"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/tough-cookie": "^4.0.5",
"@types/user-agents": "^1.0.4",
"autoprefixer": "^10.0.1",
"eslint": "^8",

View File

@ -1,5 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import SunoApi from '@/lib/sunoApi';
import { sunoApi } from "@/lib/SunoApi";
export async function POST(req: NextRequest) {
if (req.method === 'POST') {
@ -16,7 +16,7 @@ export async function POST(req: NextRequest) {
}
// 调用 SunoApi.custom_generate 方法生成定制音频
const audioInfo = await SunoApi.custom_generate(
const audioInfo = await (await sunoApi).custom_generate(
prompt, tags, title,
make_instrumental == true,
wait_audio == true

View File

@ -1,5 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import SunoApi from '@/lib/sunoApi';
import { sunoApi } from "@/lib/SunoApi";
export async function POST(req: NextRequest) {
if (req.method === 'POST') {
@ -16,7 +16,7 @@ export async function POST(req: NextRequest) {
}
// 调用 SunoApi.generate 方法生成音频
const audioInfo = await SunoApi.generate(prompt, make_instrumental == true, wait_audio == true);
const audioInfo = await (await sunoApi).generate(prompt, make_instrumental == true, wait_audio == true);
// 使用 NextResponse 构建成功响应
return new NextResponse(JSON.stringify(audioInfo), {

View File

@ -1,5 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import SunoApi from '@/lib/sunoApi';
import { sunoApi } from "@/lib/SunoApi";
export async function GET(req: NextRequest) {
if (req.method === 'GET') {
@ -11,9 +11,9 @@ export async function GET(req: NextRequest) {
if (songIds && songIds.length > 0) {
const idsArray = songIds.split(',');
// 调用 SunoApi.get 方法获取音频信息
audioInfo = await SunoApi.get(idsArray);
audioInfo = await (await sunoApi).get(idsArray);
} else {
audioInfo = await SunoApi.get();
audioInfo = await (await sunoApi).get();
}
// 使用 NextResponse 构建成功响应

View File

@ -1,11 +1,11 @@
import { NextResponse, NextRequest } from "next/server";
import SunoApi from '@/lib/sunoApi';
import { sunoApi } from "@/lib/SunoApi";
export async function GET(req: NextRequest) {
if (req.method === 'GET') {
try {
// 调用 SunoApi.get_limit 方法获取剩余的信用额度
const limit = await SunoApi.get_credits();
const limit = await (await sunoApi).get_credits();
// 使用 NextResponse 构建成功响应
return new NextResponse(JSON.stringify(limit), {

View File

@ -1,6 +1,9 @@
import axios from 'axios';
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";
const logger = pino();
@ -20,6 +23,7 @@ interface AudioInfo {
tags?: string;
duration?: string;
}
/**
*
* @param x
@ -39,71 +43,75 @@ const sleep = (x: number, y?: number): Promise<void> => {
}
class SunoApi {
private static baseUrl: string = 'https://studio-api.suno.ai';
private static clerkBaseUrl: 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 BASE_URL: string = 'https://studio-api.suno.ai';
private static CLERK_BASE_URL: string = 'https://clerk.suno.ai';
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
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模板
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
const sessionResponse = await axios.get(getSessionUrl, {
headers: {
'User-Agent': SunoApi.userAgent,
'Cookie': SunoApi.cookie,
},
});
const sid = sessionResponse.data.response?.last_active_session_id;
const sessionResponse = await this.client.get(getSessionUrl);
const sid = sessionResponse.data.response['last_active_session_id'];
if (!sid) {
throw new Error("Failed to get session id");
}
SunoApi.sid = sid; // 保存会话ID以备后用
// 保存会话ID以备后用
this.sid = sid;
// 使用会话ID获取JWT令牌
const exchangeTokenUrl = exchangeTokenUrlTemplate.replace('{sid}', sid);
const tokenResponse = await axios.post(
exchangeTokenUrl,
{},
{
headers: {
'User-Agent': SunoApi.userAgent,
'Cookie': SunoApi.cookie,
},
},
);
return tokenResponse.data.jwt;
const tokenResponse = await this.client.post(exchangeTokenUrl);
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.");
}
// 续订会话令牌的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(
renewUrl,
{},
{
headers: {
'User-Agent': SunoApi.userAgent,
'Cookie': SunoApi.cookie,
},
},
);
const renewResponse = await this.client.post(renewUrl);
logger.info("KeepAlive...\n");
await sleep(1, 2);
const newToken = renewResponse.data.jwt;
const newToken = renewResponse.data['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,
make_instrumental: 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.
* @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,
tags: 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.
* @returns A promise that resolves to an array of AudioInfo objects representing the generated songs.
*/
private static async generateSongs(
private async generateSongs(
prompt: string,
isCustom: boolean,
tags?: string,
@ -182,14 +190,10 @@ class SunoApi {
wait_audio: wait_audio,
payload: payload,
});
const response = await axios.post(
`${SunoApi.baseUrl}/api/generate/v2/`,
const response = await this.client.post(
`${SunoApi.BASE_URL}/api/generate/v2/`,
payload,
{
headers: {
'Authorization': `Bearer ${authToken}`,
'User-Agent': SunoApi.userAgent,
},
timeout: 10000, // 10 seconds timeout
},
);
@ -197,14 +201,14 @@ class SunoApi {
if (response.status !== 200) {
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
if (wait_audio === true) {
if (wait_audio) {
const startTime = Date.now();
let lastResponse: AudioInfo[] = [];
await sleep(5, 5);
while (Date.now() - startTime < 100000) {
const response = await SunoApi.get(songIds);
const response = await this.get(songIds);
const allCompleted = response.every(
audio => audio.status === 'streaming' || audio.status === 'complete'
);
@ -213,12 +217,12 @@ class SunoApi {
}
lastResponse = response;
await sleep(3, 6);
this.KeepAlive();
await this.KeepAlive();
}
return lastResponse;
} else {
this.KeepAlive();
return response.data.clips.map((audio: any) => ({
await this.KeepAlive();
return response.data['clips'].map((audio: any) => ({
id: audio.id,
title: audio.title,
image_url: audio.image_url,
@ -236,12 +240,13 @@ class SunoApi {
}));
}
}
/**
* Processes the lyrics (prompt) from the audio metadata into a more readable format.
* @param prompt The original 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.
// 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.).
@ -262,19 +267,16 @@ class SunoApi {
* @param songIds An optional array of song IDs to retrieve information for.
* @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();
let url = `${SunoApi.baseUrl}/api/feed/`;
let url = `${SunoApi.BASE_URL}/api/feed/`;
if (songIds) {
url = `${url}?ids=${songIds.join(',')}`;
}
logger.info("Get audio status: ", url);
const response = await axios.get(url, {
headers: {
'Authorization': `Bearer ${authToken}`,
'User-Agent': SunoApi.userAgent,
},
timeout: 3000, // 3 seconds timeout
const response = await this.client.get(url, {
// 3 seconds timeout
timeout: 3000
});
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 response = await axios.get(`${SunoApi.baseUrl}/api/billing/info/`, {
headers: {
'Authorization': `Bearer ${authToken}`,
'User-Agent': SunoApi.userAgent,
},
});
const response = await this.client.get(`${SunoApi.BASE_URL}/api/billing/info/`);
return {
credits_left: response.data.total_credits_left,
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 || '');