diff --git a/package.json b/package.json index d102a1f..db2e36c 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18c3328..a2ccbab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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} diff --git a/src/app/api/get_limit/route.ts b/src/app/api/get_limit/route.ts index 0bac475..195da1c 100644 --- a/src/app/api/get_limit/route.ts +++ b/src/app/api/get_limit/route.ts @@ -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' } }); diff --git a/src/lib/sunoApi.ts b/src/lib/sunoApi.ts index db14d10..30deb78 100644 --- a/src/lib/sunoApi.ts +++ b/src/lib/sunoApi.ts @@ -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 => { 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 { @@ -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 { - + 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 { - + 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 { 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 { + public static async get_credits(): Promise { 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, + }; } }