feat: optimize code
This commit is contained in:
		
							parent
							
								
									2a309f87a7
								
							
						
					
					
						commit
						d3160ce5f9
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -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
									
								
							
							
						
						
									
										5676
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -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
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -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
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -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
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -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,
 | 
				
			||||||
@ -118,7 +126,7 @@ class SunoApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Generates custom audio based on provided parameters.
 | 
					   * Generates custom audio based on provided parameters.
 | 
				
			||||||
   * 
 | 
					   *
 | 
				
			||||||
   * @param prompt The text prompt to generate audio from.
 | 
					   * @param prompt The text prompt to generate audio from.
 | 
				
			||||||
   * @param tags Tags to categorize the generated audio.
 | 
					   * @param tags Tags to categorize the generated audio.
 | 
				
			||||||
   * @param title The title for the generated audio.
 | 
					   * @param title The title for the generated audio.
 | 
				
			||||||
@ -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,
 | 
				
			||||||
@ -143,7 +151,7 @@ class SunoApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Generates songs based on the provided parameters.
 | 
					   * Generates songs based on the provided parameters.
 | 
				
			||||||
   * 
 | 
					   *
 | 
				
			||||||
   * @param prompt The text prompt to generate songs from.
 | 
					   * @param prompt The text prompt to generate songs from.
 | 
				
			||||||
   * @param isCustom Indicates if the generation should consider custom parameters like tags and title.
 | 
					   * @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 tags Optional tags to categorize the song, used only if isCustom is true.
 | 
				
			||||||
@ -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 || '');
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user