feat: Add pino-pretty logging and update SunoApi methods

Introduce pino-pretty for improved logging and update SunoApi methods to
incorporate new functionalities for generating custom audios, retrieving
audio information, and obtaining credit details. The new pino-pretty
logging enhances readability and provides better insights into
application behavior. These changes align with the broader goal of
enhancing developer experience and maintaining application stability.

#N/A
This commit is contained in:
blueeon 2024-03-28 11:50:10 +08:00
parent 0fcf77df6d
commit 2a309f87a7
4 changed files with 132 additions and 39 deletions

View File

@ -18,6 +18,7 @@
"axios": "^1.6.8",
"next": "14.1.4",
"pino": "^8.19.0",
"pino-pretty": "^11.0.0",
"react": "^18",
"react-dom": "^18",
"user-agents": "^1.1.156"

View File

@ -14,6 +14,9 @@ dependencies:
pino:
specifier: ^8.19.0
version: 8.19.0
pino-pretty:
specifier: ^11.0.0
version: 11.0.0
react:
specifier: ^18
version: 18.2.0
@ -763,6 +766,10 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, tarball: https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz}
dev: true
/colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==, tarball: https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz}
dev: false
/combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, tarball: https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz}
engines: {node: '>= 0.8'}
@ -829,6 +836,10 @@ packages:
is-data-view: 1.0.1
dev: true
/dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==, tarball: https://registry.npmmirror.com/dateformat/-/dateformat-4.6.3.tgz}
dev: false
/debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==, tarball: https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz}
peerDependencies:
@ -929,6 +940,12 @@ packages:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, tarball: https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz}
dev: true
/end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==, tarball: https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.4.tgz}
dependencies:
once: 1.4.0
dev: false
/enhanced-resolve@5.16.0:
resolution: {integrity: sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==, tarball: https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz}
engines: {node: '>=10.13.0'}
@ -1349,6 +1366,10 @@ packages:
engines: {node: '>=0.8.x'}
dev: false
/fast-copy@3.0.2:
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==, tarball: https://registry.npmmirror.com/fast-copy/-/fast-copy-3.0.2.tgz}
dev: false
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, tarball: https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz}
dev: true
@ -1377,6 +1398,10 @@ packages:
engines: {node: '>=6'}
dev: false
/fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==, tarball: https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz}
dev: false
/fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==, tarball: https://registry.npmmirror.com/fastq/-/fastq-1.17.1.tgz}
dependencies:
@ -1626,6 +1651,10 @@ packages:
function-bind: 1.1.2
dev: true
/help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==, tarball: https://registry.npmmirror.com/help-me/-/help-me-5.0.0.tgz}
dev: false
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, tarball: https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz}
dev: false
@ -1878,6 +1907,11 @@ packages:
hasBin: true
dev: true
/joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==, tarball: https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz}
engines: {node: '>=10'}
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, tarball: https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz}
@ -2029,7 +2063,6 @@ packages:
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, tarball: https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz}
dev: true
/minipass@7.0.4:
resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==, tarball: https://registry.npmmirror.com/minipass/-/minipass-7.0.4.tgz}
@ -2198,7 +2231,6 @@ packages:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, tarball: https://registry.npmmirror.com/once/-/once-1.4.0.tgz}
dependencies:
wrappy: 1.0.2
dev: true
/optionator@0.9.3:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==, tarball: https://registry.npmmirror.com/optionator/-/optionator-0.9.3.tgz}
@ -2285,6 +2317,26 @@ packages:
split2: 4.2.0
dev: false
/pino-pretty@11.0.0:
resolution: {integrity: sha512-YFJZqw59mHIY72wBnBs7XhLGG6qpJMa4pEQTRgEPEbjIYbng2LXEZZF1DoyDg9CfejEy8uZCyzpcBXXG0oOCwQ==, tarball: https://registry.npmmirror.com/pino-pretty/-/pino-pretty-11.0.0.tgz}
hasBin: true
dependencies:
colorette: 2.0.20
dateformat: 4.6.3
fast-copy: 3.0.2
fast-safe-stringify: 2.1.1
help-me: 5.0.0
joycon: 3.1.1
minimist: 1.2.8
on-exit-leak-free: 2.1.2
pino-abstract-transport: 1.1.0
pump: 3.0.0
readable-stream: 4.5.2
secure-json-parse: 2.7.0
sonic-boom: 3.8.0
strip-json-comments: 3.1.1
dev: false
/pino-std-serializers@6.2.2:
resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==, tarball: https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz}
dev: false
@ -2421,6 +2473,13 @@ packages:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, tarball: https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz}
dev: false
/pump@3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==, tarball: https://registry.npmmirror.com/pump/-/pump-3.0.0.tgz}
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
dev: false
/punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, tarball: https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz}
engines: {node: '>=6'}
@ -2590,6 +2649,10 @@ packages:
loose-envify: 1.4.0
dev: false
/secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==, tarball: https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz}
dev: false
/semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, tarball: https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz}
hasBin: true
@ -2768,7 +2831,6 @@ packages:
/strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, tarball: https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz}
engines: {node: '>=8'}
dev: true
/styled-jsx@5.1.1(react@18.2.0):
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==, tarball: https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.1.1.tgz}
@ -3084,7 +3146,6 @@ packages:
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, tarball: https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz}
dev: true
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==, tarball: https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz}

View File

@ -5,10 +5,10 @@ export async function GET(req: NextRequest) {
if (req.method === 'GET') {
try {
// 调用 SunoApi.get_limit 方法获取剩余的信用额度
const limit = await SunoApi.get_limit();
const limit = await SunoApi.get_credits();
// 使用 NextResponse 构建成功响应
return new NextResponse(JSON.stringify({ limit }), {
return new NextResponse(JSON.stringify(limit), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});

View File

@ -1,6 +1,7 @@
import axios from 'axios';
import UserAgent from 'user-agents';
import pino from 'pino';
const logger = pino();
interface AudioInfo {
@ -31,7 +32,9 @@ const sleep = (x: number, y?: number): Promise<void> => {
const max = Math.max(x, y);
timeout = Math.floor(Math.random() * (max - min + 1) + min) * 1000;
}
console.log(`Sleeping for ${timeout / 1000} seconds`);
// console.log(`Sleeping for ${timeout / 1000} seconds`);
logger.info(`Sleeping for ${timeout / 1000} seconds`);
return new Promise(resolve => setTimeout(resolve, timeout));
}
@ -60,14 +63,10 @@ class SunoApi {
if (!sid) {
throw new Error("Failed to get session id");
}
console.log(`Successfully retrieved session ID: ${sid}`);
SunoApi.sid = sid; // 保存会话ID以备后用
// 使用会话ID获取JWT令牌
const exchangeTokenUrl = exchangeTokenUrlTemplate.replace('{sid}', sid);
// console.log("Exchange Token URL:\n", exchangeTokenUrl);
// console.log("Exchange User-Agent:\n", SunoApi.userAgent);
// console.log("Exchange Cookie:\n", SunoApi.cookie);
const tokenResponse = await axios.post(
exchangeTokenUrl,
{},
@ -78,8 +77,6 @@ class SunoApi {
},
},
);
console.log("Token Response:\n", JSON.stringify(tokenResponse.data, null, 2));
return tokenResponse.data.jwt;
}
public static async KeepAlive(): Promise<void> {
@ -99,7 +96,7 @@ class SunoApi {
},
},
);
console.log("Renew Response:\n", JSON.stringify(renewResponse.data, null, 2));
logger.info("KeepAlive...\n");
await sleep(1, 2);
const newToken = renewResponse.data.jwt;
// 更新请求头中的Authorization字段使用新的JWT令牌
@ -111,12 +108,24 @@ class SunoApi {
make_instrumental: boolean = false,
wait_audio: boolean = false,
): Promise<AudioInfo[]> {
const startTime = Date.now();
const audios = this.generateSongs(prompt, false, undefined, undefined, make_instrumental, wait_audio);
console.log("Custom Generate Response:\n", JSON.stringify(audios, null, 2));
const costTime = Date.now() - startTime;
logger.info("Generate Response:\n", JSON.stringify(audios, null, 2));
logger.info("Cost time: ", costTime);
return audios;
}
/**
* Generates custom audio based on provided parameters.
*
* @param prompt The text prompt to generate audio from.
* @param tags Tags to categorize the generated audio.
* @param title The title for the generated audio.
* @param make_instrumental Indicates if the generated audio should be instrumental.
* @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(
prompt: string,
tags: string,
@ -124,12 +133,25 @@ class SunoApi {
make_instrumental: boolean = false,
wait_audio: boolean = false,
): Promise<AudioInfo[]> {
const startTime = Date.now();
const audios = await this.generateSongs(prompt, true, tags, title, make_instrumental, wait_audio);
console.log("Custom Generate Response:\n", JSON.stringify(audios, null, 2));
const costTime = Date.now() - startTime;
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.
*
* @param prompt The text prompt to generate songs from.
* @param isCustom Indicates if the generation should consider custom parameters like tags and title.
* @param tags Optional tags to categorize the song, used only if isCustom is true.
* @param title Optional title for the song, used only if isCustom is true.
* @param make_instrumental Indicates if the generated song should be instrumental.
* @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(
prompt: string,
isCustom: boolean,
@ -151,7 +173,7 @@ class SunoApi {
} else {
payload.gpt_description_prompt = prompt;
}
console.log("generateSongs payload:\n", {
logger.info("generateSongs payload:\n", {
prompt: prompt,
isCustom: isCustom,
tags: tags,
@ -171,7 +193,7 @@ class SunoApi {
timeout: 10000, // 10 seconds timeout
},
);
console.log("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);
}
@ -180,10 +202,9 @@ class SunoApi {
if (wait_audio === true) {
const startTime = Date.now();
let lastResponse: AudioInfo[] = [];
await sleep(2, 4);
while (Date.now() - startTime < 30000) {
await sleep(5, 5);
while (Date.now() - startTime < 100000) {
const response = await SunoApi.get(songIds);
console.log("Waiting for audio Response:\n", JSON.stringify(response, null, 2));
const allCompleted = response.every(
audio => audio.status === 'streaming' || audio.status === 'complete'
);
@ -191,7 +212,7 @@ class SunoApi {
return response;
}
lastResponse = response;
await sleep(2, 4);
await sleep(3, 6);
this.KeepAlive();
}
return lastResponse;
@ -216,32 +237,38 @@ class SunoApi {
}
}
/**
* prompt
* @param prompt
* @returns
* 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 {
// 假设原始歌词是以特定分隔符(例如,换行符)分隔的,我们可以将其转换为更易于阅读的格式。
// 这里的实现可以根据实际的歌词格式进行调整。
// 例如,如果歌词是以连续的文本形式存在,可能需要根据特定的标记(如句号、逗号等)来分割。
// 下面的实现假设歌词已经是以换行符分隔的。
// 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.).
// 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() !== '');
// 将处理后的歌词行重新组合成一个字符串,每行之间用换行符分隔。
// 可以在这里添加额外的格式化逻辑,如添加特定的标记或者处理特殊的行。
// 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.
const formattedLyrics = lines.join('\n');
return formattedLyrics;
}
/**
* Retrieves audio information for the given song IDs.
* @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[]> {
const authToken = await this.getAuthToken();
let url = `${SunoApi.baseUrl}/api/feed/`;
if (songIds) {
url = `${url}?ids=${songIds.join(',')}`;
}
console.log("Get URL:\n", url);
logger.info("Get audio status: ", url);
const response = await axios.get(url, {
headers: {
'Authorization': `Bearer ${authToken}`,
@ -251,7 +278,6 @@ class SunoApi {
});
const audios = response.data;
console.log("Get Response:\n", JSON.stringify(audios, null, 2));
return audios.map((audio: any) => ({
id: audio.id,
title: audio.title,
@ -270,7 +296,7 @@ class SunoApi {
}));
}
public static async get_limit(): Promise<number> {
public static async get_credits(): Promise<object> {
const authToken = await this.getAuthToken();
const response = await axios.get(`${SunoApi.baseUrl}/api/billing/info/`, {
headers: {
@ -278,7 +304,12 @@ class SunoApi {
'User-Agent': SunoApi.userAgent,
},
});
return response.data.total_credits_left;
return {
credits_left: response.data.total_credits_left,
period: response.data.period,
monthly_limit: response.data.monthly_limit,
monthly_usage: response.data.monthly_usage,
};
}
}