Implement hCaptcha solving via 2Captcha & other stuff
- implement hCaptcha solving via paid service 2Captcha and browser automation library Playwright with rebrowser-patches - implement sunoApi instances caching so sessions won't be constantly updated - add support for entering cookies not only in SUNO_COOKIE, but also the Cookie HTTP header - update docs and add Russian docs
This commit is contained in:
parent
3bffec1ea1
commit
7da7ac6ae2
@ -1,2 +1,7 @@
|
|||||||
SUNO_COOKIE=<your-suno-cookie>
|
# For more information, please see the README.md
|
||||||
|
SUNO_COOKIE=
|
||||||
|
TWOCAPTCHA_KEY= # Obtain from 2captcha.com
|
||||||
|
BROWSER=chromium # chromium or firefox
|
||||||
|
BROWSER_GHOST_CURSOR=false
|
||||||
|
BROWSER_LOCALE=en
|
||||||
|
BROWSER_HEADLESS=true
|
@ -12,10 +12,14 @@ WORKDIR /app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
ARG SUNO_COOKIE
|
ARG SUNO_COOKIE
|
||||||
RUN if [ -z "$SUNO_COOKIE" ]; then echo "SUNO_COOKIE is not set" && exit 1; fi
|
ARG BROWSER
|
||||||
|
RUN if [ -z "$SUNO_COOKIE" ]; then echo "Warning: SUNO_COOKIE is not set"; fi
|
||||||
ENV SUNO_COOKIE=${SUNO_COOKIE}
|
ENV SUNO_COOKIE=${SUNO_COOKIE}
|
||||||
|
RUN if [ -z "$BROWSER" ]; then echo "Warning: BROWSER is not set; will use chromium by default"; fi
|
||||||
|
ENV BROWSER=${BROWSER:-chromium}
|
||||||
|
|
||||||
RUN npm install --only=production
|
RUN npm install --only=production
|
||||||
|
RUN npx playwright install $BROWSER
|
||||||
COPY --from=builder /src/.next ./.next
|
COPY --from=builder /src/.next ./.next
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start"]
|
||||||
|
75
README.md
75
README.md
@ -8,9 +8,10 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a target="_blank" href="./README.md">English</a>
|
<a target="_blank" href="./README.md">English</a>
|
||||||
| <a target="_blank" href="./README_CN.md">简体中文</a>
|
| <a target="_blank" href="./README_CN.md">简体中文</a>
|
||||||
|
| <a target="_blank" href="./README_RU.md">русский</a>
|
||||||
| <a target="_blank" href="https://suno.gcui.ai">Demo</a>
|
| <a target="_blank" href="https://suno.gcui.ai">Demo</a>
|
||||||
| <a target="_blank" href="https://suno.gcui.ai/docs">Docs</a>
|
| <a target="_blank" href="https://suno.gcui.ai/docs">Docs</a>
|
||||||
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api">Deploy with Vercel</a>
|
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api">Deploy with Vercel</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.producthunt.com/products/gcui-art-suno-api-open-source-sunoai-api/reviews?utm_source=badge-product_review&utm_medium=badge&utm_souce=badge-gcui-art-suno-api-open-source-sunoai-api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/product_review.svg?product_id=577408&theme=light" alt="gcui-art/suno-api:Open-source SunoAI API - Use API to call the music generation AI of suno.ai. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/products/gcui-art-suno-api-open-source-sunoai-api/reviews?utm_source=badge-product_review&utm_medium=badge&utm_souce=badge-gcui-art-suno-api-open-source-sunoai-api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/product_review.svg?product_id=577408&theme=light" alt="gcui-art/suno-api:Open-source SunoAI API - Use API to call the music generation AI of suno.ai. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
@ -22,10 +23,12 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Suno.ai v3 is an amazing AI music service. Although the official API is not yet available, we couldn't wait to integrate its capabilities somewhere.
|
Suno is an amazing AI music service. Although the official API is not yet available, we couldn't wait to integrate its capabilities somewhere.
|
||||||
|
|
||||||
We discovered that some users have similar needs, so we decided to open-source this project, hoping you'll like it.
|
We discovered that some users have similar needs, so we decided to open-source this project, hoping you'll like it.
|
||||||
|
|
||||||
|
This implementation uses the paid [2Captcha](https://2captcha.com/about) service (a.k.a. ruCaptcha) to solve the hCaptcha challenges automatically and does not use any already made closed-source paid Suno API implementations.
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
We have deployed an example bound to a free Suno account, so it has daily usage limits, but you can see how it runs:
|
We have deployed an example bound to a free Suno account, so it has daily usage limits, but you can see how it runs:
|
||||||
@ -33,35 +36,43 @@ We have deployed an example bound to a free Suno account, so it has daily usage
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Perfectly implements the creation API from app.suno.ai
|
- Perfectly implements the creation API from suno.ai.
|
||||||
- Automatically keep the account active.
|
- Automatically keep the account active.
|
||||||
|
- Solve CAPTCHAs automatically using [2Captcha](https://2captcha.com) and [Playwright](https://playwright.dev) with [rebrowser-patches](https://github.com/rebrowser/rebrowser-patches).
|
||||||
- Compatible with the format of OpenAI’s `/v1/chat/completions` API.
|
- Compatible with the format of OpenAI’s `/v1/chat/completions` API.
|
||||||
- Supports Custom Mode
|
- Supports Custom Mode.
|
||||||
- One-click deployment to Vercel
|
- One-click deployment to [Vercel](#deploy-to-vercel) & [Docker](#docker).
|
||||||
- In addition to the standard API, it also adapts to the API Schema of Agent platforms like GPTs and Coze, so you can use it as a tool/plugin/Action for LLMs and integrate it into any AI Agent.
|
- In addition to the standard API, it also adapts to the API Schema of Agent platforms like GPTs and Coze, so you can use it as a tool/plugin/Action for LLMs and integrate it into any AI Agent.
|
||||||
- Permissive open-source license, allowing you to freely integrate and modify.
|
- Permissive open-source license, allowing you to freely integrate and modify.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### 1. Obtain the cookie of your app.suno.ai account
|
### 1. Obtain the cookie of your Suno account
|
||||||
|
|
||||||
1. Head over to [app.suno.ai](https://app.suno.ai) using your browser.
|
1. Head over to [suno.com/create](https://suno.com/create) using your browser.
|
||||||
2. Open up the browser console: hit `F12` or access the `Developer Tools`.
|
2. Open up the browser console: hit `F12` or access the `Developer Tools`.
|
||||||
3. Navigate to the `Network tab`.
|
3. Navigate to the `Network` tab.
|
||||||
4. Give the page a quick refresh.
|
4. Give the page a quick refresh.
|
||||||
5. Identify the request that includes the keyword `client?_clerk_js_version`.
|
5. Identify the latest request that includes the keyword `?__clerk_api_version`.
|
||||||
6. Click on it and switch over to the `Header` tab.
|
6. Click on it and switch over to the `Header` tab.
|
||||||
7. Locate the `Cookie` section, hover your mouse over it, and copy the value of the Cookie.
|
7. Locate the `Cookie` section, hover your mouse over it, and copy the value of the Cookie.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 2. Clone and deploy this project
|
### 2. Register on 2Captcha and top up your balance
|
||||||
|
[2Captcha](https://2captcha.com/about) is a paid CAPTCHA solving service that uses real workers to solve the CAPTCHA and has high accuracy. It is needed because of Suno constantly requesting hCaptcha solving that currently isn't possible for free by any means.
|
||||||
|
|
||||||
|
[Create](https://2captcha.com/auth/register?userType=customer) a new 2Captcha account, [top up](https://2captcha.com/pay) your balance and [get your API key](https://2captcha.com/enterpage#recognition).
|
||||||
|
|
||||||
|
If you are located in Russia or Belarus, use the [ruCaptcha](https://rucaptcha.com) interface instead of 2Captcha. It's the same service, but it supports payments from those countries.
|
||||||
|
|
||||||
|
### 3. Clone and deploy this project
|
||||||
|
|
||||||
You can choose your preferred deployment method:
|
You can choose your preferred deployment method:
|
||||||
|
|
||||||
#### Deploy to Vercel
|
#### Deploy to Vercel
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api)
|
||||||
|
|
||||||
#### Run locally
|
#### Run locally
|
||||||
|
|
||||||
@ -69,25 +80,37 @@ You can choose your preferred deployment method:
|
|||||||
git clone https://github.com/gcui-art/suno-api.git
|
git clone https://github.com/gcui-art/suno-api.git
|
||||||
cd suno-api
|
cd suno-api
|
||||||
npm install
|
npm install
|
||||||
|
npx playwright install chromium
|
||||||
```
|
```
|
||||||
|
#### Docker
|
||||||
Alternatively, you can use [Docker Compose](https://docs.docker.com/compose/)
|
Alternatively, you can use [Docker Compose](https://docs.docker.com/compose/). However, follow the step below before running.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose build && docker compose up
|
docker compose build && docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Configure suno-api
|
### 4. Configure suno-api
|
||||||
|
|
||||||
- If deployed to Vercel, please add an environment variable `SUNO_COOKIE` in the Vercel dashboard, with the value of the cookie obtained in the first step.
|
- If deployed to Vercel, please add the environment variables in the Vercel dashboard.
|
||||||
|
|
||||||
- If you’re running this locally, be sure to add the following to your `.env` file:
|
- If you’re running this locally, be sure to add the following to your `.env` file:
|
||||||
|
#### Environment variables
|
||||||
|
- `SUNO_COOKIE` — the `Cookie` header you obtained in the first step.
|
||||||
|
- `TWOCAPTCHA_KEY` — your 2Captcha API key from the second step.
|
||||||
|
- `BROWSER` — the name of the browser that is going to be used to solve the CAPTCHA. Only `chromium` and `firefox` supported.
|
||||||
|
- `BROWSER_GHOST_CURSOR` — use ghost-cursor-playwright to simulate smooth mouse movements. Please note that it doesn't seem to make any difference in the rate of CAPTCHAs, so you can set it to `false`. Retained for future testing.
|
||||||
|
- `BROWSER_LOCALE` — the language of the browser. Using either `en` or `ru` is recommended, since those have the most workers on 2Captcha. [List of supported languages](https://2captcha.com/2captcha-api#language)
|
||||||
|
- `BROWSER_HEADLESS` — run the browser without the window. You probably want to set this to `true`.
|
||||||
```bash
|
```bash
|
||||||
SUNO_COOKIE=<your-cookie>
|
SUNO_COOKIE=<…>
|
||||||
|
TWOCAPTCHA_KEY=<…>
|
||||||
|
BROWSER=chromium
|
||||||
|
BROWSER_GHOST_CURSOR=false
|
||||||
|
BROWSER_LOCALE=en
|
||||||
|
BROWSER_HEADLESS=true
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Run suno api
|
### 5. Run suno-api
|
||||||
|
|
||||||
- If you’ve deployed to Vercel:
|
- If you’ve deployed to Vercel:
|
||||||
- Please click on Deploy in the Vercel dashboard and wait for the deployment to be successful.
|
- Please click on Deploy in the Vercel dashboard and wait for the deployment to be successful.
|
||||||
@ -108,7 +131,7 @@ SUNO_COOKIE=<your-cookie>
|
|||||||
|
|
||||||
it means the program is running normally.
|
it means the program is running normally.
|
||||||
|
|
||||||
### 5. Use Suno API
|
### 6. Use Suno API
|
||||||
|
|
||||||
You can check out the detailed API documentation at :
|
You can check out the detailed API documentation at :
|
||||||
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
||||||
@ -132,10 +155,12 @@ Suno API currently mainly implements the following APIs:
|
|||||||
- `/api/concat`: Generate the whole song from extensions
|
- `/api/concat`: Generate the whole song from extensions
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also specify the cookies in the `Cookie` header of your request, overriding the default cookies in the `SUNO_COOKIE` environment variable. This comes in handy when, for example, you want to use multiple free accounts at the same time.
|
||||||
|
|
||||||
For more detailed documentation, please check out the demo site:
|
For more detailed documentation, please check out the demo site:
|
||||||
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
||||||
|
|
||||||
## API Integration Code Example
|
## API Integration Code Examples
|
||||||
|
|
||||||
### Python
|
### Python
|
||||||
|
|
||||||
@ -143,7 +168,7 @@ For more detailed documentation, please check out the demo site:
|
|||||||
import time
|
import time
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# replace your vercel domain
|
# replace with your suno-api URL
|
||||||
base_url = 'http://localhost:3000'
|
base_url = 'http://localhost:3000'
|
||||||
|
|
||||||
|
|
||||||
@ -208,7 +233,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Js
|
### JavaScript
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
@ -304,18 +329,18 @@ You can integrate Suno AI as a tool/plugin/action into your AI agent.
|
|||||||
|
|
||||||
There are four ways you can support this project:
|
There are four ways you can support this project:
|
||||||
|
|
||||||
1. Fork and Submit Pull Requests: We welcome any PRs that enhance the component or editor.
|
1. Fork and Submit Pull Requests: We welcome any PRs that enhances the functionality, APIs, response time and availability. You can also help us just by translating this README into your language—any help for this project is welcome!
|
||||||
2. Open Issues: We appreciate reasonable suggestions and bug reports.
|
2. Open Issues: We appreciate reasonable suggestions and bug reports.
|
||||||
3. Donate: If this project has helped you, consider buying us a coffee using the Sponsor button at the top of the project. Cheers! ☕
|
3. Donate: If this project has helped you, consider buying us a coffee using the Sponsor button at the top of the project. Cheers! ☕
|
||||||
4. Spread the Word: Recommend this project to others, star the repo, or add a backlink after using the project.
|
4. Spread the Word: Recommend this project to others, star the repo, or add a backlink after using the project.
|
||||||
|
|
||||||
## Questions, Suggestions, Issues, or Bugs?
|
## Questions, Suggestions, Issues, or Bugs?
|
||||||
|
|
||||||
We use GitHub Issues to manage feedback. Feel free to open an issue, and we'll address it promptly.
|
We use [GitHub Issues](https://github.com/gcui-art/suno-api/issues) to manage feedback. Feel free to open an issue, and we'll address it promptly.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
LGPL-3.0 or later
|
The license of this project is LGPL-3.0 or later. See [LICENSE](LICENSE) for more information.
|
||||||
|
|
||||||
## Related Links
|
## Related Links
|
||||||
|
|
||||||
|
@ -8,9 +8,10 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a target="_blank" href="./README.md">English</a>
|
<a target="_blank" href="./README.md">English</a>
|
||||||
| <a target="_blank" href="./README_CN.md">简体中文</a>
|
| <a target="_blank" href="./README_CN.md">简体中文</a>
|
||||||
|
| <a target="_blank" href="./README_RU.md">русский</a>
|
||||||
| <a target="_blank" href="https://suno.gcui.ai">Demo</a>
|
| <a target="_blank" href="https://suno.gcui.ai">Demo</a>
|
||||||
| <a target="_blank" href="https://suno.gcui.ai/docs">文档</a>
|
| <a target="_blank" href="https://suno.gcui.ai/docs">文档</a>
|
||||||
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api">一键部署到 Vercel</a>
|
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api">一键部署到 Vercel</a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@ -61,7 +62,7 @@ Suno.ai v3 是一个令人惊叹的 AI 音乐服务,虽然官方还没有开
|
|||||||
|
|
||||||
#### 部署到 Vercel
|
#### 部署到 Vercel
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api)
|
||||||
|
|
||||||
#### 本地运行
|
#### 本地运行
|
||||||
|
|
||||||
|
355
README_RU.md
Normal file
355
README_RU.md
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
<div align="center">
|
||||||
|
<h1 align="center"">
|
||||||
|
Suno AI API
|
||||||
|
</h1>
|
||||||
|
<p>Используйте API для генерации музыки через Suno.ai и с лёгкостью интегрируйте его в агенты, такие как GPT.</p>
|
||||||
|
<p>👉 Мы обновляемся быстро, пожалуйста, поставьте звёздочку.</p>
|
||||||
|
</div>
|
||||||
|
<p align="center">
|
||||||
|
<a target="_blank" href="./README.md">English</a>
|
||||||
|
| <a target="_blank" href="./README_CN.md">简体中文</a>
|
||||||
|
| <a target="_blank" href="./README_RU.md">русский</a>
|
||||||
|
| <a target="_blank" href="https://suno.gcui.ai">Демо</a>
|
||||||
|
| <a target="_blank" href="https://suno.gcui.ai/docs">Документация</a>
|
||||||
|
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api">Развёртывание на Vercel</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.producthunt.com/products/gcui-art-suno-api-open-source-sunoai-api/reviews?utm_source=badge-product_review&utm_medium=badge&utm_souce=badge-gcui-art-suno-api-open-source-sunoai-api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/product_review.svg?product_id=577408&theme=light" alt="gcui-art/suno-api:Open-source SunoAI API - Use API to call the music generation AI of suno.ai. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
> 🔥 Посмотрите мой новый проект: [ReadPo — 10-кратное ускорение чтения и печатания](https://readpo.com?utm_source=github&utm_medium=suno-ai)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Вступление
|
||||||
|
|
||||||
|
Suno — потрясающий сервис для ИИ-музыки. Несмотря на отстутствие официального API, мы не могли дождаться, чтобы интегрировать его возможности где-нибудь.
|
||||||
|
|
||||||
|
Мы узнали, что у других пользователей есть схожие потребности, поэтому решили выложить этот проект в открытый доступ, надеясь, что он вам понравится.
|
||||||
|
|
||||||
|
Данная реализация использует платный сервис [2Captcha](https://2captcha.com/about) (a.k.a. ruCaptcha) для автоматического решения капч hCaptcha и не использует какие-либо готовые реализации API Suno с закрытым исходным кодом.
|
||||||
|
|
||||||
|
## Демо
|
||||||
|
|
||||||
|
Мы опубликовали пример, привязанный к бесплатному аккаунту, так что имеются дневные лимиты, но вы всё равно можете посмотреть, как оно работает:
|
||||||
|
[suno.gcui.ai](https://suno.gcui.ai)
|
||||||
|
|
||||||
|
## Функции
|
||||||
|
|
||||||
|
- Идеально реализует API suno.ai.
|
||||||
|
- Автоматическое поддержание сессии аккаунта.
|
||||||
|
- Автоматическое решение капч через [ruCaptcha](https://rucaptcha.com/about) и [Playwright](https://playwright.dev) с патчами [rebrowser-patches](https://github.com/rebrowser/rebrowser-patches).
|
||||||
|
- Совместим с форматом API OpenAI `/v1/chat/completions`.
|
||||||
|
- Поддержка пользовательского текста песни.
|
||||||
|
- Развёртывание в один клик через [Vercel](#развёртывание-на-vercel) и [Docker](#docker).
|
||||||
|
- В дополнение к стандартному API, он также адаптируется к схеме API агентских платформ, таких как GPT и Coze, поэтому вы можете использовать его как инструмент/плагин/действие для LLM и интегрировать его в любой AI-агент.
|
||||||
|
- Разрешительная лицензия с открытым исходным кодом, позволяющая свободно интегрировать и модифицировать.
|
||||||
|
|
||||||
|
## Начало работы
|
||||||
|
|
||||||
|
### 1. Получите куки вашего аккаунта Suno
|
||||||
|
|
||||||
|
1. Зайдите на [suno.com/create](https://suno.com/create).
|
||||||
|
2. Откройте консоль браузера: нажмите `F12` или откройте инструменты разработчика.
|
||||||
|
3. Перейдите на вкладку `Сеть` (`Network`).
|
||||||
|
4. Перезагрузите страницу.
|
||||||
|
5. Найдите запрос, адрес которого содержит `client?__clerk_api_version`.
|
||||||
|
6. Нажмите на него и перейдите на вкладку `Заголовки` (`Header`).
|
||||||
|
7. Найдите заголовок `Cookie`, нажмите ПКМ по нему и скопируйте его значение.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 2. Зарегистрируйтесь на 2Captcha и пополните баланс
|
||||||
|
[2Captcha](https://2captcha.com/ru/about) — это платный сервис для решения капч, использующий реальных работников для этого и обладающий высокой точностью. Он необходим из-за того, что Suno постоянно запрашивает решение hCaptcha, что невозможно за бесплатно каким-либо автоматическим способом.
|
||||||
|
|
||||||
|
[Создайте](https://2captcha.com/ru/auth/register?userType=customer) новый аккаунт, [пополните](https://2captcha.com/ru/pay) баланс и [получите свой API-ключ](https://2captcha.com/ru/enterpage#recognition).
|
||||||
|
|
||||||
|
ℹ Если вы находитесь в России или Беларуси, используйте интерфейс [ruCaptcha](https://rucaptcha.com) вместо 2Captcha. Это абсолютно тот же сервис, но данный интерфейс поддерживает платежи из этих стран.
|
||||||
|
|
||||||
|
### 3. Скачайте и запустите проект
|
||||||
|
|
||||||
|
Вы можете выбрать свой предпочитаемый способ запуска:
|
||||||
|
|
||||||
|
#### Развёртывание на Vercel
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api)
|
||||||
|
|
||||||
|
#### Локально
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/gcui-art/suno-api.git
|
||||||
|
cd suno-api
|
||||||
|
npm install
|
||||||
|
npx playwright install chromium
|
||||||
|
```
|
||||||
|
#### Docker
|
||||||
|
Также можно использовать [Docker Compose](https://docs.docker.com/compose/), однако перед запуском выполните шаг ниже.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build && docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Настройте suno-api
|
||||||
|
|
||||||
|
- Если вы используете Vercel, настройте переменные среды в панели управления Vercel.
|
||||||
|
|
||||||
|
- Если вы установили suno-api локально, добавьте следующее в ваш `.env`-файл:
|
||||||
|
#### Environment variables
|
||||||
|
- `SUNO_COOKIE` — заголовок `Cookie`, который вы скопировали ещё в первом шаге.
|
||||||
|
- `TWOCAPTCHA_KEY` — ваш API-ключ 2Captcha из второго шага.
|
||||||
|
- `BROWSER` — название браузера, который будет использоваться для решения капч. Поддерживаются только `chromium` и `firefox`.
|
||||||
|
- `BROWSER_GHOST_CURSOR` — использовать ли ghost-cursor-playwright для симуляции плавных движений мышкой. Обратите внимание, что это, похоже, никак не влияет на появление капч, так что вы можете спокойно установить значение `false`.
|
||||||
|
- `BROWSER_LOCALE` — язык браузера. Рекомендуется использовать либо `en`, либо `ru`, т. к. данные языки имеют больше всего работников на 2Captcha. [Список поддерживаемых языков](https://2captcha.com/ru/2captcha-api#language)
|
||||||
|
- `BROWSER_HEADLESS` — запускать ли браузер без отдельного окна. Скорее всего, вам надо установить значение `true`.
|
||||||
|
```bash
|
||||||
|
SUNO_COOKIE=<…>
|
||||||
|
TWOCAPTCHA_KEY=<…>
|
||||||
|
BROWSER=chromium
|
||||||
|
BROWSER_GHOST_CURSOR=false
|
||||||
|
BROWSER_LOCALE=en
|
||||||
|
BROWSER_HEADLESS=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Запустите suno-api
|
||||||
|
|
||||||
|
- Если вы используете Vercel:
|
||||||
|
- Нажмите на кнопку `Deploy` в панели Vercel и дождитесь успеха.
|
||||||
|
- Посетите API `https://<присовенный-домен-vercel>/api/get_limit` для тестирования.
|
||||||
|
- Если вы установили проект локально:
|
||||||
|
- Выполните `npm run dev`.
|
||||||
|
- Посетите API `http://localhost:3000/api/get_limit` для тестирования.
|
||||||
|
- Если вернулся следующий результат:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"credits_left": 50,
|
||||||
|
"period": "day",
|
||||||
|
"monthly_limit": 50,
|
||||||
|
"monthly_usage": 50
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
то программа работает корректно.
|
||||||
|
|
||||||
|
### 6. Используйте Suno API
|
||||||
|
|
||||||
|
Вы можете посмотреть документацию suno-api здесь:
|
||||||
|
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
||||||
|
|
||||||
|
## Справочник по API
|
||||||
|
|
||||||
|
На данный момент suno-api реализует следующие API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
- `/api/generate`: Сгенерировать музыку
|
||||||
|
- `/v1/chat/completions`: Сгенерировать музыку - Вызов API в формате OpenAI.
|
||||||
|
- `/api/custom_generate`: Сгенерировать музыку (Custom Mode, поддержка ручного текста песни, стиля музыки, названия и т. д.)
|
||||||
|
- `/api/generate_lyrics`: Сгенерировать текст песни на основе промпта
|
||||||
|
- `/api/get`: Получить информацию песни по ID. Перечисляйте несколько ID через запятую.
|
||||||
|
Если ID не предоставлен, то отобразятся все песни.
|
||||||
|
- `/api/get_limit`: Получить лимиты на сегодня
|
||||||
|
- `/api/extend_audio`: Расширить длину песни
|
||||||
|
- `/api/generate_stems`: Создать стем-треки (отдельную звуковую и музыкальную дорожку)
|
||||||
|
- `/api/get_aligned_lyrics`: Получить список временных меток для каждого слова в тексте песни
|
||||||
|
- `/api/clip`: Получить информацию о клипе на основе идентификатора, переданного в качестве параметра запроса `id`.
|
||||||
|
- `/api/concat`: Сгенерировать всю песню из расширений
|
||||||
|
```
|
||||||
|
|
||||||
|
Вы также можете указать куки в заголовок `Cookie` вашего запроса, переопределяя дефолтные куки в переменной среды `SUNO_COOKIE`. Это удобно, например, когда вы хотите использовать несколько бесплатных аккаунтов одновременно.
|
||||||
|
|
||||||
|
Для более подробной документации посетите демо-сайт:
|
||||||
|
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
||||||
|
|
||||||
|
## Пример кода интеграции API
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# замените на URL-адрес вашего suno-api
|
||||||
|
base_url = 'http://localhost:3000'
|
||||||
|
|
||||||
|
|
||||||
|
def custom_generate_audio(payload):
|
||||||
|
url = f"{base_url}/api/custom_generate"
|
||||||
|
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def extend_audio(payload):
|
||||||
|
url = f"{base_url}/api/extend_audio"
|
||||||
|
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def generate_audio_by_prompt(payload):
|
||||||
|
url = f"{base_url}/api/generate"
|
||||||
|
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def get_audio_information(audio_ids):
|
||||||
|
url = f"{base_url}/api/get?ids={audio_ids}"
|
||||||
|
response = requests.get(url)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def get_quota_information():
|
||||||
|
url = f"{base_url}/api/get_limit"
|
||||||
|
response = requests.get(url)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_clip(clip_id):
|
||||||
|
url = f"{base_url}/api/clip?id={clip_id}"
|
||||||
|
response = requests.get(url)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def generate_whole_song(clip_id):
|
||||||
|
payload = {"clip_id": clip_id}
|
||||||
|
url = f"{base_url}/api/concat"
|
||||||
|
response = requests.post(url, json=payload)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
data = generate_audio_by_prompt({
|
||||||
|
"prompt": "Популярная хэви-метал песня о войне, исполненная глубоким мужским голосом, медленно и мелодично. В тексте изображена печаль людей после войны.",
|
||||||
|
"make_instrumental": False,
|
||||||
|
"wait_audio": False
|
||||||
|
})
|
||||||
|
|
||||||
|
ids = f"{data[0]['id']},{data[1]['id']}"
|
||||||
|
print(f"ids: {ids}")
|
||||||
|
|
||||||
|
for _ in range(60):
|
||||||
|
data = get_audio_information(ids)
|
||||||
|
if data[0]["status"] == 'streaming':
|
||||||
|
print(f"{data[0]['id']} ==> {data[0]['audio_url']}")
|
||||||
|
print(f"{data[1]['id']} ==> {data[1]['audio_url']}")
|
||||||
|
break
|
||||||
|
# sleep 5s
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```js
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
// замените на URL-адрес вашего suno-api
|
||||||
|
const baseUrl = "http://localhost:3000";
|
||||||
|
|
||||||
|
async function customGenerateAudio(payload) {
|
||||||
|
const url = `${baseUrl}/api/custom_generate`;
|
||||||
|
const response = await axios.post(url, payload, {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateAudioByPrompt(payload) {
|
||||||
|
const url = `${baseUrl}/api/generate`;
|
||||||
|
const response = await axios.post(url, payload, {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extendAudio(payload) {
|
||||||
|
const url = `${baseUrl}/api/extend_audio`;
|
||||||
|
const response = await axios.post(url, payload, {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAudioInformation(audioIds) {
|
||||||
|
const url = `${baseUrl}/api/get?ids=${audioIds}`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getQuotaInformation() {
|
||||||
|
const url = `${baseUrl}/api/get_limit`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getClipInformation(clipId) {
|
||||||
|
const url = `${baseUrl}/api/clip?id=${clipId}`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const data = await generateAudioByPrompt({
|
||||||
|
prompt:
|
||||||
|
"Популярная хэви-метал песня о войне, исполненная глубоким мужским голосом, медленно и мелодично. В тексте изображена печаль людей после войны.",
|
||||||
|
make_instrumental: false,
|
||||||
|
wait_audio: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ids = `${data[0].id},${data[1].id}`;
|
||||||
|
console.log(`ids: ${ids}`);
|
||||||
|
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
const data = await getAudioInformation(ids);
|
||||||
|
if (data[0].status === "streaming") {
|
||||||
|
console.log(`${data[0].id} ==> ${data[0].audio_url}`);
|
||||||
|
console.log(`${data[1].id} ==> ${data[1].audio_url}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// sleep 5s
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Интеграция с пользовательскими агентами
|
||||||
|
|
||||||
|
Вы можете интегрировать Suno AI как инструмент/плагин/действие в ваш ИИ-агент.
|
||||||
|
|
||||||
|
### Интеграция с GPT
|
||||||
|
|
||||||
|
[скоро...]
|
||||||
|
|
||||||
|
### Интеграция с Coze
|
||||||
|
|
||||||
|
[скоро...]
|
||||||
|
|
||||||
|
### Интеграция с LangChain
|
||||||
|
|
||||||
|
[скоро...]
|
||||||
|
|
||||||
|
## Вклад в развитие
|
||||||
|
|
||||||
|
Вы можете поддержать этот проект четырьмя способами:
|
||||||
|
|
||||||
|
1. Fork и публикация pull request'ов: мы приветствуем любые PR, которые улучшают данный проект. Вы также можете помочь простым переводом этого README на ваш язык.
|
||||||
|
2. Создавайте [issue](https://github.com/gcui-art/suno-api/issues): мы ценим разумные предложения и сообщения об ошибках.
|
||||||
|
3. Пожертвование: если этот проект помог вам, угостите нас кофе, воспользовавшись кнопкой «Sponsor» в верхней части проекта. Спасибо! ☕
|
||||||
|
4. Распространяйте информацию: порекомендуйте этот проект другим, поставьте звезду в репо или добавьте обратную ссылку после использования проекта.
|
||||||
|
|
||||||
|
## Вопросы, предложения, проблемы или ошибки?
|
||||||
|
|
||||||
|
Мы используем [Issues на GitHub](https://github.com/gcui-art/suno-api/issues) для обратной связи. Не стестняйтесь создавать issue, мы оперативно решим вашу проблему.
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Лицензия данного проекта — LGPL-3.0 или более поздняя версия. Для большей информации см. [LICENSE](LICENSE).
|
||||||
|
|
||||||
|
## Полезные ссылки
|
||||||
|
|
||||||
|
- Репозиторий проекта: [github.com/gcui-art/suno-api](https://github.com/gcui-art/suno-api)
|
||||||
|
- Официальный сайт Suno.ai: [suno.ai](https://suno.ai)
|
||||||
|
- Демо: [suno.gcui.ai](https://suno.gcui.ai)
|
||||||
|
- [Readpo](https://readpo.com?utm_source=github&utm_medium=suno-api): ReadPo — это помощник для чтения и письма, работающий на основе искусственного интеллекта. Собирайте, курируйте и создавайте контент с молниеносной скоростью.
|
||||||
|
- Album AI: [Автоматическое создание метаданных изображения и общение с альбомом. RAG + Альбом.](https://github.com/gcui-art/album-ai)
|
||||||
|
|
||||||
|
## Заявление
|
||||||
|
|
||||||
|
suno-api — это неофициальный проект с открытым исходным кодом, предназначенный только для учебных и исследовательских целей.
|
@ -1,4 +1,15 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {};
|
const nextConfig = {
|
||||||
|
webpack: (config) => {
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.(ttf|html)$/i,
|
||||||
|
type: 'asset/resource'
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
serverMinification: false, // the server minification unfortunately breaks the selector class names
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
10008
package-lock.json
generated
Normal file
10008
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -15,9 +15,15 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@2captcha/captcha-solver": "^1.3.0",
|
||||||
"@vercel/analytics": "^1.2.2",
|
"@vercel/analytics": "^1.2.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.7.8",
|
||||||
"axios-cookiejar-support": "^5.0.0",
|
"bufferutil": "^4.0.8",
|
||||||
|
"chromium-bidi": "^0.10.1",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
|
"electron": "^33.2.1",
|
||||||
|
"ghost-cursor-playwright": "^2.1.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"next": "14.1.4",
|
"next": "14.1.4",
|
||||||
"next-swagger-doc": "^0.4.0",
|
"next-swagger-doc": "^0.4.0",
|
||||||
"pino": "^8.19.0",
|
"pino": "^8.19.0",
|
||||||
@ -25,12 +31,16 @@
|
|||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"swagger-ui-react": "^5.12.3",
|
"rebrowser-playwright-core": "^1.49.1",
|
||||||
|
"swagger-ui-react": "^5.18.2",
|
||||||
"tough-cookie": "^4.1.4",
|
"tough-cookie": "^4.1.4",
|
||||||
"user-agents": "^1.1.156"
|
"user-agents": "^1.1.156",
|
||||||
|
"utf-8-validate": "^6.0.5",
|
||||||
|
"yn": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
"@tailwindcss/typography": "^0.5.12",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
@ -19,7 +19,7 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioInfo = await (await sunoApi).getClip(clipId);
|
const audioInfo = await (await sunoApi()).getClip(clipId);
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify(audioInfo), {
|
return new NextResponse(JSON.stringify(audioInfo), {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
import { sunoApi } from "@/lib/SunoApi";
|
import { sunoApi } from "@/lib/SunoApi";
|
||||||
import { corsHeaders } from "@/lib/utils";
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const audioInfo = await (await sunoApi).concatenate(clip_id);
|
const audioInfo = await (await sunoApi((await cookies()).toString())).concatenate(clip_id);
|
||||||
return new NextResponse(JSON.stringify(audioInfo), {
|
return new NextResponse(JSON.stringify(audioInfo), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
||||||
import { corsHeaders } from "@/lib/utils";
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { prompt, tags, title, make_instrumental, model, wait_audio, negative_tags } = body;
|
const { prompt, tags, title, make_instrumental, model, wait_audio, negative_tags } = body;
|
||||||
const audioInfo = await (await sunoApi).custom_generate(
|
const audioInfo = await (await sunoApi((await cookies()).toString())).custom_generate(
|
||||||
prompt, tags, title,
|
prompt, tags, title,
|
||||||
Boolean(make_instrumental),
|
Boolean(make_instrumental),
|
||||||
model || DEFAULT_MODEL,
|
model || DEFAULT_MODEL,
|
||||||
@ -25,18 +26,9 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error generating custom audio:', error.response.data);
|
console.error('Error generating custom audio:', error);
|
||||||
if (error.response.status === 402) {
|
return new NextResponse(JSON.stringify({ error: error.response?.data?.detail || error.toString() }), {
|
||||||
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
status: error.response?.status || 500,
|
||||||
status: 402,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...corsHeaders
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...corsHeaders
|
...corsHeaders
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
||||||
import { corsHeaders } from "@/lib/utils";
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
@ -8,7 +9,7 @@ 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 { audio_id, prompt, continue_at, tags, title, model } = body;
|
const { audio_id, prompt, continue_at, tags, negative_tags, title, model, wait_audio } = body;
|
||||||
|
|
||||||
if (!audio_id) {
|
if (!audio_id) {
|
||||||
return new NextResponse(JSON.stringify({ error: 'Audio ID is required' }), {
|
return new NextResponse(JSON.stringify({ error: 'Audio ID is required' }), {
|
||||||
@ -20,8 +21,8 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioInfo = await (await sunoApi)
|
const audioInfo = await (await sunoApi((await cookies()).toString()))
|
||||||
.extendAudio(audio_id, prompt, continue_at, tags, title, model || DEFAULT_MODEL);
|
.extendAudio(audio_id, prompt, continue_at, tags || '', negative_tags || '', title, model || DEFAULT_MODEL, wait_audio || false);
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify(audioInfo), {
|
return new NextResponse(JSON.stringify(audioInfo), {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
||||||
import { corsHeaders } from "@/lib/utils";
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { prompt, make_instrumental, model, wait_audio } = body;
|
const { prompt, make_instrumental, model, wait_audio } = body;
|
||||||
|
|
||||||
const audioInfo = await (await sunoApi).generate(
|
const audioInfo = await (await sunoApi((await cookies()).toString())).generate(
|
||||||
prompt,
|
prompt,
|
||||||
Boolean(make_instrumental),
|
Boolean(make_instrumental),
|
||||||
model || DEFAULT_MODEL,
|
model || DEFAULT_MODEL,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
import { sunoApi } from "@/lib/SunoApi";
|
import { sunoApi } from "@/lib/SunoApi";
|
||||||
import { corsHeaders } from "@/lib/utils";
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { prompt } = body;
|
const { prompt } = body;
|
||||||
|
|
||||||
const lyrics = await (await sunoApi).generateLyrics(prompt);
|
const lyrics = await (await sunoApi((await cookies()).toString())).generateLyrics(prompt);
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify(lyrics), {
|
return new NextResponse(JSON.stringify(lyrics), {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
||||||
import { corsHeaders } from "@/lib/utils";
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioInfo = await (await sunoApi)
|
const audioInfo = await (await sunoApi((await cookies()).toString()))
|
||||||
.generateStems(audio_id);
|
.generateStems(audio_id);
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify(audioInfo), {
|
return new NextResponse(JSON.stringify(audioInfo), {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NextResponse, NextRequest } from 'next/server';
|
import { NextResponse, NextRequest } from 'next/server';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import { sunoApi } from '@/lib/SunoApi';
|
import { sunoApi } from '@/lib/SunoApi';
|
||||||
import { corsHeaders } from '@/lib/utils';
|
import { corsHeaders } from '@/lib/utils';
|
||||||
|
|
||||||
@ -10,13 +11,14 @@ export async function GET(req: NextRequest) {
|
|||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const songIds = url.searchParams.get('ids');
|
const songIds = url.searchParams.get('ids');
|
||||||
const page = url.searchParams.get('page');
|
const page = url.searchParams.get('page');
|
||||||
|
const cookie = (await cookies()).toString();
|
||||||
|
|
||||||
let audioInfo = [];
|
let audioInfo = [];
|
||||||
if (songIds && songIds.length > 0) {
|
if (songIds && songIds.length > 0) {
|
||||||
const idsArray = songIds.split(',');
|
const idsArray = songIds.split(',');
|
||||||
audioInfo = await (await sunoApi).get(idsArray, page);
|
audioInfo = await (await sunoApi(cookie)).get(idsArray, page);
|
||||||
} else {
|
} else {
|
||||||
audioInfo = await (await sunoApi).get(undefined, page);
|
audioInfo = await (await sunoApi(cookie)).get(undefined, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify(audioInfo), {
|
return new NextResponse(JSON.stringify(audioInfo), {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
import { sunoApi } from "@/lib/SunoApi";
|
import { sunoApi } from "@/lib/SunoApi";
|
||||||
import { corsHeaders } from "@/lib/utils";
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const lyricAlignment = await (await sunoApi).getLyricAlignment(song_id);
|
const lyricAlignment = await (await sunoApi((await cookies()).toString())).getLyricAlignment(song_id);
|
||||||
|
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify(lyricAlignment), {
|
return new NextResponse(JSON.stringify(lyricAlignment), {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
import { sunoApi } from "@/lib/SunoApi";
|
import { sunoApi } from "@/lib/SunoApi";
|
||||||
import { corsHeaders } from "@/lib/utils";
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
@ -8,7 +9,7 @@ export async function GET(req: NextRequest) {
|
|||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const limit = await (await sunoApi).get_credits();
|
const limit = await (await sunoApi((await cookies()).toString())).get_credits();
|
||||||
|
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify(limit), {
|
return new NextResponse(JSON.stringify(limit), {
|
||||||
|
@ -9,7 +9,12 @@ type Props = {
|
|||||||
const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false });
|
const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false });
|
||||||
|
|
||||||
function Swagger({ spec }: Props) {
|
function Swagger({ spec }: Props) {
|
||||||
return <SwaggerUI spec={spec}/>;
|
return <SwaggerUI spec={spec} requestInterceptor={(req) => {
|
||||||
|
// Remove cookies before sending requests
|
||||||
|
req.credentials = 'omit';
|
||||||
|
console.log(req);
|
||||||
|
return req;
|
||||||
|
}} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Swagger;
|
export default Swagger;
|
@ -231,6 +231,11 @@
|
|||||||
"description": "Music genre",
|
"description": "Music genre",
|
||||||
"example": ""
|
"example": ""
|
||||||
},
|
},
|
||||||
|
"negative_tags": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Negative Music genre",
|
||||||
|
"example":""
|
||||||
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Model name ,default is chirp-v3-5",
|
"description": "Model name ,default is chirp-v3-5",
|
||||||
|
@ -32,7 +32,7 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const audioInfo = await (await sunoApi).generate(userMessage.content, true, DEFAULT_MODEL, true);
|
const audioInfo = await (await sunoApi()).generate(userMessage.content, true, DEFAULT_MODEL, true);
|
||||||
|
|
||||||
const audio = audioInfo[0]
|
const audio = audioInfo[0]
|
||||||
const data = `## Song Title: ${audio.title}\n\n### Lyrics:\n${audio.lyric}\n### Listen to the song: ${audio.audio_url}`
|
const data = `## Song Title: ${audio.title}\n\n### Lyrics:\n${audio.lyric}\n### Listen to the song: ${audio.audio_url}`
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
import axios, { AxiosInstance } 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 yn from 'yn';
|
||||||
import { CookieJar } from 'tough-cookie';
|
import { sleep, isPage } from '@/lib/utils';
|
||||||
import { sleep } from '@/lib/utils';
|
import * as cookie from 'cookie';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { Solver } from '@2captcha/captcha-solver';
|
||||||
|
import { BrowserContext, Page, Locator, chromium, firefox } from 'rebrowser-playwright-core';
|
||||||
|
import { createCursor, Cursor } from 'ghost-cursor-playwright';
|
||||||
|
|
||||||
|
// sunoApi instance caching
|
||||||
|
const globalForSunoApi = global as unknown as { sunoApiCache?: Map<string, SunoApi> };
|
||||||
|
const cache = globalForSunoApi.sunoApiCache || new Map<string, SunoApi>();
|
||||||
|
globalForSunoApi.sunoApiCache = cache;
|
||||||
|
|
||||||
const logger = pino();
|
const logger = pino();
|
||||||
export const DEFAULT_MODEL = 'chirp-v3-5';
|
export const DEFAULT_MODEL = 'chirp-v3-5';
|
||||||
@ -30,37 +39,58 @@ export interface AudioInfo {
|
|||||||
class SunoApi {
|
class SunoApi {
|
||||||
private static BASE_URL: string = 'https://studio-api.prod.suno.com';
|
private static BASE_URL: string = 'https://studio-api.prod.suno.com';
|
||||||
private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
|
private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
|
||||||
private static JSDELIVR_BASE_URL: string = 'https://data.jsdelivr.com';
|
private static CLERK_VERSION = '5.15.0';
|
||||||
|
|
||||||
private readonly client: AxiosInstance;
|
private readonly client: AxiosInstance;
|
||||||
private clerkVersion?: string;
|
|
||||||
private sid?: string;
|
private sid?: string;
|
||||||
private currentToken?: string;
|
private currentToken?: string;
|
||||||
|
private deviceId?: string;
|
||||||
|
private userAgent?: string;
|
||||||
|
private cookies: Record<string, string | undefined>;
|
||||||
|
private solver = new Solver(process.env.TWOCAPTCHA_KEY + '');
|
||||||
|
private ghostCursorEnabled = yn(process.env.BROWSER_GHOST_CURSOR, { default: false });
|
||||||
|
private cursor?: Cursor;
|
||||||
|
|
||||||
constructor(cookie: string) {
|
constructor(cookies: string) {
|
||||||
const cookieJar = new CookieJar();
|
this.userAgent = new UserAgent(/Macintosh/).random().toString(); // Usually Mac systems get less amount of CAPTCHAs
|
||||||
const randomUserAgent = new UserAgent(/Chrome/).random().toString();
|
this.cookies = cookie.parse(cookies);
|
||||||
this.client = wrapper(
|
this.deviceId = this.cookies.ajs_anonymous_id || randomUUID();
|
||||||
axios.create({
|
this.client = axios.create({
|
||||||
jar: cookieJar,
|
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': randomUserAgent,
|
'Affiliate-Id': 'undefined',
|
||||||
Cookie: cookie
|
'Device-Id': `"${this.deviceId}"`,
|
||||||
|
'x-suno-client': 'Android prerelease-4nt180t 1.0.42',
|
||||||
|
'X-Requested-With': 'com.suno.android',
|
||||||
|
'sec-ch-ua': '"Chromium";v="130", "Android WebView";v="130", "Not?A_Brand";v="99"',
|
||||||
|
'sec-ch-ua-mobile': '?1',
|
||||||
|
'sec-ch-ua-platform': '"Android"',
|
||||||
|
'User-Agent': this.userAgent
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
this.client.interceptors.request.use(config => {
|
||||||
|
if (this.currentToken && !config.headers.Authorization)
|
||||||
|
config.headers.Authorization = `Bearer ${this.currentToken}`;
|
||||||
|
const cookiesArray = Object.entries(this.cookies).map(([key, value]) =>
|
||||||
|
cookie.serialize(key, value as string)
|
||||||
);
|
);
|
||||||
this.client.interceptors.request.use((config) => {
|
config.headers.Cookie = cookiesArray.join('; ');
|
||||||
if (this.currentToken) {
|
|
||||||
// Use the current token status
|
|
||||||
config.headers['Authorization'] = `Bearer ${this.currentToken}`;
|
|
||||||
}
|
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
this.client.interceptors.response.use(resp => {
|
||||||
|
const setCookieHeader = resp.headers['set-cookie'];
|
||||||
|
if (Array.isArray(setCookieHeader)) {
|
||||||
|
const newCookies = cookie.parse(setCookieHeader.join('; '));
|
||||||
|
for (const [key, value] of Object.entries(newCookies)) {
|
||||||
|
this.cookies[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async init(): Promise<SunoApi> {
|
public async init(): Promise<SunoApi> {
|
||||||
await this.getClerkLatestVersion();
|
//await this.getClerkLatestVersion();
|
||||||
await this.getAuthToken();
|
await this.getAuthToken();
|
||||||
await this.keepAlive();
|
await this.keepAlive();
|
||||||
return this;
|
return this;
|
||||||
@ -68,7 +98,8 @@ class SunoApi {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the clerk package latest version id.
|
* Get the clerk package latest version id.
|
||||||
*/
|
* This method is commented because we are now using a hard-coded Clerk version, hence this method is not needed.
|
||||||
|
|
||||||
private async getClerkLatestVersion() {
|
private async getClerkLatestVersion() {
|
||||||
// URL to get clerk version ID
|
// URL to get clerk version ID
|
||||||
const getClerkVersionUrl = `${SunoApi.JSDELIVR_BASE_URL}/v1/package/npm/@clerk/clerk-js`;
|
const getClerkVersionUrl = `${SunoApi.JSDELIVR_BASE_URL}/v1/package/npm/@clerk/clerk-js`;
|
||||||
@ -80,26 +111,28 @@ class SunoApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Save clerk version ID for auth
|
// Save clerk version ID for auth
|
||||||
// this.clerkVersion = versionListResponse?.data?.['tags']['latest'];
|
SunoApi.clerkVersion = versionListResponse?.data?.['tags']['latest'];
|
||||||
// Use a Clerk version released before fraud detection was implemented
|
|
||||||
this.clerkVersion = "5.34.0";
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the session ID and save it for later use.
|
* Get the session ID and save it for later use.
|
||||||
*/
|
*/
|
||||||
private async getAuthToken() {
|
private async getAuthToken() {
|
||||||
|
logger.info('Getting the session ID');
|
||||||
// URL to get session ID
|
// URL to get session ID
|
||||||
const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_clerk_js_version=${this.clerkVersion}`;
|
const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_is_native=true&_clerk_js_version=${SunoApi.CLERK_VERSION}`;
|
||||||
// Get session ID
|
// Get session ID
|
||||||
const sessionResponse = await this.client.get(getSessionUrl);
|
const sessionResponse = await this.client.get(getSessionUrl, {
|
||||||
if (!sessionResponse?.data?.response?.['last_active_session_id']) {
|
headers: { Authorization: this.cookies.__client }
|
||||||
|
});
|
||||||
|
if (!sessionResponse?.data?.response?.last_active_session_id) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Failed to get session id, you may need to update the SUNO_COOKIE'
|
'Failed to get session id, you may need to update the SUNO_COOKIE'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Save session ID for later use
|
// Save session ID for later use
|
||||||
this.sid = sessionResponse.data.response['last_active_session_id'];
|
this.sid = sessionResponse.data.response.last_active_session_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,16 +144,238 @@ class SunoApi {
|
|||||||
throw new Error('Session ID is not set. Cannot renew token.');
|
throw new Error('Session ID is not set. Cannot renew token.');
|
||||||
}
|
}
|
||||||
// URL to renew session token
|
// URL to renew session token
|
||||||
const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens?_clerk_js_version==${this.clerkVersion}`;
|
const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens?_is_native=true&_clerk_js_version=${SunoApi.CLERK_VERSION}`;
|
||||||
// Renew session token
|
// Renew session token
|
||||||
const renewResponse = await this.client.post(renewUrl);
|
|
||||||
logger.info('KeepAlive...\n');
|
logger.info('KeepAlive...\n');
|
||||||
|
const renewResponse = await this.client.post(renewUrl, {}, {
|
||||||
|
headers: { Authorization: this.cookies.__client }
|
||||||
|
});
|
||||||
if (isWait) {
|
if (isWait) {
|
||||||
await sleep(1, 2);
|
await sleep(1, 2);
|
||||||
}
|
}
|
||||||
const newToken = renewResponse.data['jwt'];
|
const newToken = renewResponse.data.jwt;
|
||||||
// Update Authorization field in request header with the new JWT token
|
// Update Authorization field in request header with the new JWT token
|
||||||
this.currentToken = newToken;
|
this.currentToken = newToken;
|
||||||
|
logger.info(this.currentToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the session token (not to be confused with session ID) and save it for later use.
|
||||||
|
*/
|
||||||
|
private async getSessionToken() {
|
||||||
|
const tokenResponse = await this.client.post(
|
||||||
|
`${SunoApi.BASE_URL}/api/user/create_session_id/`,
|
||||||
|
{
|
||||||
|
session_properties: JSON.stringify({ deviceId: this.deviceId }),
|
||||||
|
session_type: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return tokenResponse.data.session_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async captchaRequired(): Promise<boolean> {
|
||||||
|
const resp = await this.client.post(`${SunoApi.BASE_URL}/api/c/check`, {
|
||||||
|
ctype: 'generation'
|
||||||
|
});
|
||||||
|
logger.info(resp.data);
|
||||||
|
// await sleep(10);
|
||||||
|
return resp.data.required;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicks on a locator or XY vector. This method is made because of the difference between ghost-cursor-playwright and Playwright methods
|
||||||
|
*/
|
||||||
|
private async click(target: Locator|Page, position?: { x: number, y: number }): Promise<void> {
|
||||||
|
if (this.ghostCursorEnabled) {
|
||||||
|
let pos: any = isPage(target) ? { x: 0, y: 0 } : await target.boundingBox();
|
||||||
|
if (position)
|
||||||
|
pos = {
|
||||||
|
...pos,
|
||||||
|
x: pos.x + position.x,
|
||||||
|
y: pos.y + position.y,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
|
};
|
||||||
|
return this.cursor?.actions.click({
|
||||||
|
target: pos
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (isPage(target))
|
||||||
|
return target.mouse.click(position?.x ?? 0, position?.y ?? 0);
|
||||||
|
else
|
||||||
|
return target.click({ force: true, position });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the BrowserType from the `BROWSER` environment variable.
|
||||||
|
* @returns {BrowserType} chromium, firefox or webkit. Default is chromium
|
||||||
|
*/
|
||||||
|
private getBrowserType() {
|
||||||
|
const browser = process.env.BROWSER?.toLowerCase();
|
||||||
|
switch (browser) {
|
||||||
|
case 'firefox':
|
||||||
|
return firefox;
|
||||||
|
/*case 'webkit': ** doesn't work with rebrowser-patches
|
||||||
|
case 'safari':
|
||||||
|
return webkit;*/
|
||||||
|
default:
|
||||||
|
return chromium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches a browser with the necessary cookies
|
||||||
|
* @returns {BrowserContext}
|
||||||
|
*/
|
||||||
|
private async launchBrowser(): Promise<BrowserContext> {
|
||||||
|
const browser = await this.getBrowserType().launch({
|
||||||
|
args: [
|
||||||
|
'--disable-blink-features=AutomationControlled',
|
||||||
|
'--disable-web-security',
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--disable-features=site-per-process',
|
||||||
|
'--disable-features=IsolateOrigins',
|
||||||
|
'--disable-extensions',
|
||||||
|
'--disable-infobars'
|
||||||
|
],
|
||||||
|
headless: yn(process.env.BROWSER_HEADLESS, { default: true })
|
||||||
|
});
|
||||||
|
const context = await browser.newContext({ userAgent: this.userAgent, locale: process.env.BROWSER_LOCALE, viewport: null });
|
||||||
|
const cookies = [];
|
||||||
|
const lax: 'Lax' | 'Strict' | 'None' = 'Lax';
|
||||||
|
cookies.push({
|
||||||
|
name: '__session',
|
||||||
|
value: this.currentToken+'',
|
||||||
|
domain: '.suno.com',
|
||||||
|
path: '/',
|
||||||
|
sameSite: lax
|
||||||
|
});
|
||||||
|
for (const key in this.cookies) {
|
||||||
|
cookies.push({
|
||||||
|
name: key,
|
||||||
|
value: this.cookies[key]+'',
|
||||||
|
domain: '.suno.com',
|
||||||
|
path: '/',
|
||||||
|
sameSite: lax
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await context.addCookies(cookies);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for CAPTCHA verification and solves the CAPTCHA if needed
|
||||||
|
* @returns {string|null} hCaptcha token. If no verification is required, returns null
|
||||||
|
*/
|
||||||
|
public async getCaptcha(): Promise<string|null> {
|
||||||
|
if (!await this.captchaRequired())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
logger.info('CAPTCHA required. Launching browser...')
|
||||||
|
const browser = await this.launchBrowser();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.goto('https://suno.com/create', { referer: 'https://www.google.com/', waitUntil: 'domcontentloaded', timeout: 0 });
|
||||||
|
|
||||||
|
page.on('request', request => console.log('>>', request.method(), request.url()));
|
||||||
|
page.on('response', response => console.log('<<', response.status(), response.url()));
|
||||||
|
|
||||||
|
logger.info('Waiting for Suno interface to load');
|
||||||
|
await page.locator('.react-aria-GridList').waitFor({ timeout: 60000 });
|
||||||
|
|
||||||
|
if (this.ghostCursorEnabled)
|
||||||
|
this.cursor = await createCursor(page);
|
||||||
|
|
||||||
|
logger.info('Triggering the CAPTCHA');
|
||||||
|
await this.click(page, { x: 318, y: 13 }); // close all popups
|
||||||
|
|
||||||
|
const textarea = page.locator('.custom-textarea');
|
||||||
|
await this.click(textarea);
|
||||||
|
await textarea.pressSequentially('Lorem ipsum', { delay: 80 });
|
||||||
|
|
||||||
|
const button = page.locator('button[aria-label="Create"]').locator('div.flex');
|
||||||
|
await this.click(button);
|
||||||
|
|
||||||
|
new Promise<void>(async (resolve, reject) => {
|
||||||
|
const frame = page.frameLocator('iframe[title*="hCaptcha"]');
|
||||||
|
const challenge = frame.locator('.challenge-container');
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
await page.waitForResponse('https://img**.hcaptcha.com/**', { timeout: 60000 }); // wait for hCaptcha image to load
|
||||||
|
while (true) { // wait for all requests to finish
|
||||||
|
try {
|
||||||
|
await page.waitForResponse('https://img**.hcaptcha.com/**', { timeout: 1000 });
|
||||||
|
} catch(e) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//await sleep(0.1); // sometimes it takes a couple of seconds to display the image itself. unfortunately, the only option is to wait and hope that it loads
|
||||||
|
const drag = (await challenge.locator('.prompt-text').first().innerText()).toLowerCase().includes('drag');
|
||||||
|
if (drag) {
|
||||||
|
logger.info('Got a dragging hCaptcha. This type of hCaptcha is currently not supported. Skipping...');
|
||||||
|
this.click(frame.locator('.button-submit'));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let captcha: any;
|
||||||
|
for (let j = 0; j < 3; j++) { // try several times because sometimes 2Captcha could send an error
|
||||||
|
try {
|
||||||
|
logger.info('Sending the CAPTCHA to 2Captcha');
|
||||||
|
captcha = await this.solver.coordinates({
|
||||||
|
body: (await challenge.screenshot()).toString('base64'),
|
||||||
|
lang: process.env.BROWSER_LOCALE
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
} catch(err: any) {
|
||||||
|
logger.info(err.message);
|
||||||
|
if (j != 2)
|
||||||
|
logger.info('Retrying...');
|
||||||
|
else
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const data of captcha.data) {
|
||||||
|
logger.info(data);
|
||||||
|
await this.click(challenge, { x: +data.x, y: +data.y });
|
||||||
|
}
|
||||||
|
/*await*/ this.click(frame.locator('.button-submit')); // await is commented because we need to call waitForResponse at the same time
|
||||||
|
} catch(e: any) {
|
||||||
|
if (e.message.includes('viewport') || e.message.includes('timeout')) // when hCaptcha window has been closed due to inactivity,
|
||||||
|
this.click(button); // click the Create button again to trigger the CAPTCHA
|
||||||
|
else if (e.message.includes('been closed')) // catch error when closing the browser
|
||||||
|
resolve();
|
||||||
|
else
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
//if (!e.message.includes('been closed'))
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
return (new Promise((resolve, reject) => {
|
||||||
|
page.route('**/api/generate/v2/**', async (route: any) => {
|
||||||
|
try {
|
||||||
|
logger.info('hCaptcha token received. Closing browser');
|
||||||
|
route.abort();
|
||||||
|
browser.browser()?.close();
|
||||||
|
const request = route.request();
|
||||||
|
this.currentToken = request.headers().authorization.split('Bearer ').pop();
|
||||||
|
resolve(request.postDataJSON().token);
|
||||||
|
} catch(err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imitates Cloudflare Turnstile loading error. Unused right now, left for future
|
||||||
|
*/
|
||||||
|
private async getTurnstile() {
|
||||||
|
return this.client.post(
|
||||||
|
`https://clerk.suno.com/v1/client?__clerk_api_version=2021-02-05&_clerk_js_version=${SunoApi.CLERK_VERSION}&_method=PATCH`,
|
||||||
|
{ captcha_error: '300030,300030,300030' },
|
||||||
|
{ headers: { 'content-type': 'application/x-www-form-urlencoded' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -225,6 +480,8 @@ class SunoApi {
|
|||||||
* @param make_instrumental Indicates if the generated song should be instrumental.
|
* @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.
|
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
|
||||||
* @param negative_tags Negative tags that should not be included in the generated audio.
|
* @param negative_tags Negative tags that should not be included in the generated audio.
|
||||||
|
* @param task Optional indication of what to do. Enter 'extend' if extending an audio, otherwise specify null.
|
||||||
|
* @param continue_clip_id
|
||||||
* @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 async generateSongs(
|
private async generateSongs(
|
||||||
@ -235,14 +492,21 @@ class SunoApi {
|
|||||||
make_instrumental?: boolean,
|
make_instrumental?: boolean,
|
||||||
model?: string,
|
model?: string,
|
||||||
wait_audio: boolean = false,
|
wait_audio: boolean = false,
|
||||||
negative_tags?: string
|
negative_tags?: string,
|
||||||
|
task?: string,
|
||||||
|
continue_clip_id?: string,
|
||||||
|
continue_at?: number
|
||||||
): Promise<AudioInfo[]> {
|
): Promise<AudioInfo[]> {
|
||||||
await this.keepAlive(false);
|
await this.keepAlive();
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
make_instrumental: make_instrumental,
|
make_instrumental: make_instrumental,
|
||||||
mv: model || DEFAULT_MODEL,
|
mv: model || DEFAULT_MODEL,
|
||||||
prompt: '',
|
prompt: '',
|
||||||
generation_type: 'TEXT'
|
generation_type: 'TEXT',
|
||||||
|
continue_at: continue_at,
|
||||||
|
continue_clip_id: continue_clip_id,
|
||||||
|
task: task,
|
||||||
|
token: await this.getCaptcha()
|
||||||
};
|
};
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
payload.tags = tags;
|
payload.tags = tags;
|
||||||
@ -276,13 +540,10 @@ class SunoApi {
|
|||||||
timeout: 10000 // 10 seconds timeout
|
timeout: 10000 // 10 seconds timeout
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
logger.info(
|
|
||||||
'generateSongs Response:\n' + JSON.stringify(response.data, null, 2)
|
|
||||||
);
|
|
||||||
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) {
|
if (wait_audio) {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
@ -303,8 +564,7 @@ class SunoApi {
|
|||||||
}
|
}
|
||||||
return lastResponse;
|
return lastResponse;
|
||||||
} else {
|
} else {
|
||||||
await this.keepAlive(true);
|
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,
|
||||||
@ -366,26 +626,14 @@ class SunoApi {
|
|||||||
public async extendAudio(
|
public async extendAudio(
|
||||||
audioId: string,
|
audioId: string,
|
||||||
prompt: string = '',
|
prompt: string = '',
|
||||||
continueAt: string = '0',
|
continueAt: number,
|
||||||
tags: string = '',
|
tags: string = '',
|
||||||
|
negative_tags: string = '',
|
||||||
title: string = '',
|
title: string = '',
|
||||||
model?: string
|
model?: string,
|
||||||
): Promise<AudioInfo> {
|
wait_audio?: boolean
|
||||||
await this.keepAlive(false);
|
): Promise<AudioInfo[]> {
|
||||||
const response = await this.client.post(
|
return this.generateSongs(prompt, true, tags, title, false, model, wait_audio, negative_tags, 'extend', audioId, continueAt);
|
||||||
`${SunoApi.BASE_URL}/api/generate/v2/`,
|
|
||||||
{
|
|
||||||
continue_clip_id: audioId,
|
|
||||||
continue_at: continueAt,
|
|
||||||
mv: model || DEFAULT_MODEL,
|
|
||||||
prompt: prompt,
|
|
||||||
tags: tags,
|
|
||||||
task: 'extend',
|
|
||||||
title: title
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log('response:\n', response);
|
|
||||||
return response.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -460,7 +708,7 @@ class SunoApi {
|
|||||||
page?: string | null
|
page?: string | null
|
||||||
): Promise<AudioInfo[]> {
|
): Promise<AudioInfo[]> {
|
||||||
await this.keepAlive(false);
|
await this.keepAlive(false);
|
||||||
let url = new URL(`${SunoApi.BASE_URL}/api/feed/`);
|
let url = new URL(`${SunoApi.BASE_URL}/api/feed/v2`);
|
||||||
if (songIds) {
|
if (songIds) {
|
||||||
url.searchParams.append('ids', songIds.join(','));
|
url.searchParams.append('ids', songIds.join(','));
|
||||||
}
|
}
|
||||||
@ -469,11 +717,11 @@ class SunoApi {
|
|||||||
}
|
}
|
||||||
logger.info('Get audio status: ' + url.href);
|
logger.info('Get audio status: ' + url.href);
|
||||||
const response = await this.client.get(url.href, {
|
const response = await this.client.get(url.href, {
|
||||||
// 3 seconds timeout
|
// 10 seconds timeout
|
||||||
timeout: 3000
|
timeout: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
const audios = response.data;
|
const audios = response.data.clips;
|
||||||
|
|
||||||
return audios.map((audio: any) => ({
|
return audios.map((audio: any) => ({
|
||||||
id: audio.id,
|
id: audio.id,
|
||||||
@ -523,13 +771,22 @@ class SunoApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSunoApi = async (cookie: string) => {
|
export const sunoApi = async (cookie?: string) => {
|
||||||
const sunoApi = new SunoApi(cookie);
|
const resolvedCookie = cookie || process.env.SUNO_COOKIE;
|
||||||
return await sunoApi.init();
|
if (!resolvedCookie) {
|
||||||
};
|
logger.info('No cookie provided! Aborting...\nPlease provide a cookie either in the .env file or in the Cookie header of your request.')
|
||||||
|
throw new Error('Please provide a cookie either in the .env file or in the Cookie header of your request.');
|
||||||
if (!process.env.SUNO_COOKIE) {
|
|
||||||
console.log('Environment does not contain SUNO_COOKIE.', process.env);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sunoApi = newSunoApi(process.env.SUNO_COOKIE || '');
|
// Check if the instance for this cookie already exists in the cache
|
||||||
|
const cachedInstance = cache.get(resolvedCookie);
|
||||||
|
if (cachedInstance)
|
||||||
|
return cachedInstance;
|
||||||
|
|
||||||
|
// If not, create a new instance and initialize it
|
||||||
|
const instance = await new SunoApi(resolvedCookie).init();
|
||||||
|
// Cache the initialized instance
|
||||||
|
cache.set(resolvedCookie, instance);
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import pino from "pino";
|
import pino from "pino";
|
||||||
|
import { Page } from "rebrowser-playwright-core";
|
||||||
|
|
||||||
const logger = pino();
|
const logger = pino();
|
||||||
|
|
||||||
@ -20,6 +21,14 @@ export const sleep = (x: number, y?: number): Promise<void> => {
|
|||||||
return new Promise(resolve => setTimeout(resolve, timeout));
|
return new Promise(resolve => setTimeout(resolve, timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param target A Locator or a page
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const isPage = (target: any): target is Page => {
|
||||||
|
return target.constructor.name === 'Page';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const corsHeaders = {
|
export const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
@ -5,8 +5,10 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
@ -18,7 +20,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"],
|
||||||
|
"playwright-core": ["./node_modules/rebrowser-playwright-core"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
Loading…
Reference in New Issue
Block a user