Merge pull request #222 from gohoski/patch-1
Implement hCaptcha solving via 2Captcha [URGENT MERGE]
This commit is contained in:
commit
48d667b064
@ -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`, although `chromium` is highly recommended
|
||||
BROWSER_GHOST_CURSOR=false
|
||||
BROWSER_LOCALE=en
|
||||
BROWSER_HEADLESS=true
|
17
Dockerfile
17
Dockerfile
@ -1,21 +1,32 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:lts-alpine AS builder
|
||||
FROM node:lts-bookworm AS builder
|
||||
WORKDIR /src
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:lts-alpine
|
||||
FROM node:lts-bookworm
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y libnss3 \
|
||||
libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
|
||||
libgbm1 libxkbcommon0 libasound2 libcups2 xvfb
|
||||
|
||||
ARG SUNO_COOKIE
|
||||
RUN if [ -z "$SUNO_COOKIE" ]; then echo "SUNO_COOKIE is not set" && exit 1; fi
|
||||
RUN if [ -z "$SUNO_COOKIE" ]; then echo "Warning: SUNO_COOKIE is not set. You will have to set the cookies in the Cookie header of your requests."; fi
|
||||
ENV SUNO_COOKIE=${SUNO_COOKIE}
|
||||
# Disable GPU acceleration, as with it suno-api won't work in a Docker environment
|
||||
ENV BROWSER_DISABLE_GPU=true
|
||||
|
||||
RUN npm install --only=production
|
||||
|
||||
# Install all supported browsers, else switching browsers requires an image rebuild
|
||||
RUN npx playwright install chromium
|
||||
# RUN npx playwright install firefox
|
||||
|
||||
COPY --from=builder /src/.next ./.next
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "start"]
|
79
README.md
79
README.md
@ -8,9 +8,10 @@
|
||||
<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">Demo</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 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>
|
||||
@ -22,10 +23,12 @@
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
|
||||
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,47 @@ We have deployed an example bound to a free Suno account, so it has daily usage
|
||||
|
||||
## Features
|
||||
|
||||
- Perfectly implements the creation API from app.suno.ai
|
||||
- Perfectly implements the creation API from suno.ai.
|
||||
- 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.
|
||||
- Supports Custom Mode
|
||||
- One-click deployment to Vercel
|
||||
- Supports Custom Mode.
|
||||
- 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.
|
||||
- Permissive open-source license, allowing you to freely integrate and modify.
|
||||
|
||||
## 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`.
|
||||
3. Navigate to the `Network tab`.
|
||||
3. Navigate to the `Network` tab.
|
||||
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.
|
||||
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).
|
||||
|
||||
> [!NOTE]
|
||||
> 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.
|
||||
|
||||
> [!TIP]
|
||||
> If you want as few CAPTCHAs as possible, it is recommended to use a macOS system. macOS systems usually get fewer CAPTCHAs than Linux and Windows—this is due to its unpopularity in the web scraping industry. Running suno-api on Windows and Linux will work, but in some cases, you could get a pretty large number of CAPTCHAs.
|
||||
|
||||
### 3. Clone and deploy this project
|
||||
|
||||
You can choose your preferred deployment method:
|
||||
|
||||
#### 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
|
||||
|
||||
@ -70,24 +85,38 @@ git clone https://github.com/gcui-art/suno-api.git
|
||||
cd suno-api
|
||||
npm install
|
||||
```
|
||||
#### Docker
|
||||
>[!IMPORTANT]
|
||||
> GPU acceleration will be disabled in Docker. If you have a slow CPU, it is recommended to [deploy locally](#run-locally).
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
#### 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
|
||||
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:
|
||||
- Please click on Deploy in the Vercel dashboard and wait for the deployment to be successful.
|
||||
@ -108,7 +137,7 @@ SUNO_COOKIE=<your-cookie>
|
||||
|
||||
it means the program is running normally.
|
||||
|
||||
### 5. Use Suno API
|
||||
### 6. Use Suno API
|
||||
|
||||
You can check out the detailed API documentation at :
|
||||
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
||||
@ -132,10 +161,12 @@ Suno API currently mainly implements the following APIs:
|
||||
- `/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:
|
||||
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
||||
|
||||
## API Integration Code Example
|
||||
## API Integration Code Examples
|
||||
|
||||
### Python
|
||||
|
||||
@ -143,7 +174,7 @@ For more detailed documentation, please check out the demo site:
|
||||
import time
|
||||
import requests
|
||||
|
||||
# replace your vercel domain
|
||||
# replace with your suno-api URL
|
||||
base_url = 'http://localhost:3000'
|
||||
|
||||
|
||||
@ -208,7 +239,7 @@ if __name__ == '__main__':
|
||||
|
||||
```
|
||||
|
||||
### Js
|
||||
### JavaScript
|
||||
|
||||
```js
|
||||
const axios = require("axios");
|
||||
@ -304,18 +335,18 @@ You can integrate Suno AI as a tool/plugin/action into your AI agent.
|
||||
|
||||
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 enhance 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.
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
LGPL-3.0 or later
|
||||
The license of this project is LGPL-3.0 or later. See [LICENSE](LICENSE) for more information.
|
||||
|
||||
## Related Links
|
||||
|
||||
|
@ -8,9 +8,10 @@
|
||||
<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">Demo</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 align="center">
|
||||
@ -61,7 +62,7 @@ Suno.ai v3 是一个令人惊叹的 AI 音乐服务,虽然官方还没有开
|
||||
|
||||
#### 部署到 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)
|
||||
|
||||
#### 本地运行
|
||||
|
||||
|
360
README_RU.md
Normal file
360
README_RU.md
Normal file
@ -0,0 +1,360 @@
|
||||
<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).
|
||||
|
||||
> [!NOTE]
|
||||
> Если вы находитесь в России или Беларуси, используйте интерфейс [ruCaptcha](https://rucaptcha.com) вместо 2Captcha. Это абсолютно тот же сервис, но данный интерфейс поддерживает платежи из этих стран.
|
||||
|
||||
> [!TIP]
|
||||
> Если вы хотите получать как можно меньше капч, рекомендуется использовать macOS. Системы на macOS обычно получают меньше капч, чем Linux и Windows — это связано с их непопулярностью в сфере веб-скрейпинга. Запуск suno-api на Windows и Linux будет работать, но в некоторых случаях вы можете получить довольно большое количество капч.
|
||||
|
||||
### 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
|
||||
```
|
||||
#### Docker
|
||||
>[!IMPORTANT]
|
||||
> Аппаратное видеоускорение браузера будет отключено в 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 — это неофициальный проект с открытым исходным кодом, предназначенный только для учебных и исследовательских целей.
|
@ -10,3 +10,4 @@ services:
|
||||
- ./public:/app/public
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file: ".env"
|
@ -1,4 +1,15 @@
|
||||
/** @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;
|
||||
|
10021
package-lock.json
generated
Normal file
10021
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@ -15,9 +15,16 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@2captcha/captcha-solver": "^1.3.0",
|
||||
"@playwright/browser-chromium": "^1.49.1",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"axios": "^1.6.8",
|
||||
"axios-cookiejar-support": "^5.0.0",
|
||||
"axios": "^1.7.8",
|
||||
"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-swagger-doc": "^0.4.0",
|
||||
"pino": "^8.19.0",
|
||||
@ -25,12 +32,16 @@
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"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",
|
||||
"user-agents": "^1.1.156"
|
||||
"user-agents": "^1.1.156",
|
||||
"utf-8-validate": "^6.0.5",
|
||||
"yn": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.12",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
|
BIN
public/drag-instructions.jpg
Normal file
BIN
public/drag-instructions.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
@ -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), {
|
||||
status: 200,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { cookies } from 'next/headers'
|
||||
import { sunoApi } from "@/lib/SunoApi";
|
||||
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), {
|
||||
status: 200,
|
||||
headers: {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { cookies } from 'next/headers';
|
||||
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
||||
import { corsHeaders } from "@/lib/utils";
|
||||
|
||||
@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
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,
|
||||
Boolean(make_instrumental),
|
||||
model || DEFAULT_MODEL,
|
||||
@ -25,18 +26,9 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
});
|
||||
} 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',
|
||||
...corsHeaders
|
||||
}
|
||||
});
|
||||
}
|
||||
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
||||
status: 500,
|
||||
console.error('Error generating custom audio:', error);
|
||||
return new NextResponse(JSON.stringify({ error: error.response?.data?.detail || error.toString() }), {
|
||||
status: error.response?.status || 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...corsHeaders
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { cookies } from 'next/headers'
|
||||
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
||||
import { corsHeaders } from "@/lib/utils";
|
||||
|
||||
@ -8,7 +9,7 @@ export async function POST(req: NextRequest) {
|
||||
if (req.method === 'POST') {
|
||||
try {
|
||||
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) {
|
||||
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)
|
||||
.extendAudio(audio_id, prompt, continue_at, tags, title, model || DEFAULT_MODEL);
|
||||
const audioInfo = await (await sunoApi((await cookies()).toString()))
|
||||
.extendAudio(audio_id, prompt, continue_at, tags || '', negative_tags || '', title, model || DEFAULT_MODEL, wait_audio || false);
|
||||
|
||||
return new NextResponse(JSON.stringify(audioInfo), {
|
||||
status: 200,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { cookies } from 'next/headers'
|
||||
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
||||
import { corsHeaders } from "@/lib/utils";
|
||||
|
||||
@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
|
||||
const body = await req.json();
|
||||
const { prompt, make_instrumental, model, wait_audio } = body;
|
||||
|
||||
const audioInfo = await (await sunoApi).generate(
|
||||
const audioInfo = await (await sunoApi((await cookies()).toString())).generate(
|
||||
prompt,
|
||||
Boolean(make_instrumental),
|
||||
model || DEFAULT_MODEL,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { cookies } from 'next/headers'
|
||||
import { sunoApi } from "@/lib/SunoApi";
|
||||
import { corsHeaders } from "@/lib/utils";
|
||||
|
||||
@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
|
||||
const body = await req.json();
|
||||
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), {
|
||||
status: 200,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { cookies } from 'next/headers';
|
||||
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
||||
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);
|
||||
|
||||
return new NextResponse(JSON.stringify(audioInfo), {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NextResponse, NextRequest } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { sunoApi } from '@/lib/SunoApi';
|
||||
import { corsHeaders } from '@/lib/utils';
|
||||
|
||||
@ -10,13 +11,14 @@ export async function GET(req: NextRequest) {
|
||||
const url = new URL(req.url);
|
||||
const songIds = url.searchParams.get('ids');
|
||||
const page = url.searchParams.get('page');
|
||||
const cookie = (await cookies()).toString();
|
||||
|
||||
let audioInfo = [];
|
||||
if (songIds && songIds.length > 0) {
|
||||
const idsArray = songIds.split(',');
|
||||
audioInfo = await (await sunoApi).get(idsArray, page);
|
||||
audioInfo = await (await sunoApi(cookie)).get(idsArray, page);
|
||||
} else {
|
||||
audioInfo = await (await sunoApi).get(undefined, page);
|
||||
audioInfo = await (await sunoApi(cookie)).get(undefined, page);
|
||||
}
|
||||
|
||||
return new NextResponse(JSON.stringify(audioInfo), {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { cookies } from 'next/headers'
|
||||
import { sunoApi } from "@/lib/SunoApi";
|
||||
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), {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
import { cookies } from 'next/headers'
|
||||
import { sunoApi } from "@/lib/SunoApi";
|
||||
import { corsHeaders } from "@/lib/utils";
|
||||
|
||||
@ -8,7 +9,7 @@ export async function GET(req: NextRequest) {
|
||||
if (req.method === 'GET') {
|
||||
try {
|
||||
|
||||
const limit = await (await sunoApi).get_credits();
|
||||
const limit = await (await sunoApi((await cookies()).toString())).get_credits();
|
||||
|
||||
|
||||
return new NextResponse(JSON.stringify(limit), {
|
||||
|
@ -9,7 +9,12 @@ type Props = {
|
||||
const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false });
|
||||
|
||||
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;
|
@ -231,6 +231,11 @@
|
||||
"description": "Music genre",
|
||||
"example": ""
|
||||
},
|
||||
"negative_tags": {
|
||||
"type": "string",
|
||||
"description": "Negative Music genre",
|
||||
"example":""
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"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 data = `## Song Title: ${audio.title}\n\n### Lyrics:\n${audio.lyric}\n### Listen to the song: ${audio.audio_url}`
|
||||
|
@ -1,9 +1,21 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import UserAgent from 'user-agents';
|
||||
import pino from 'pino';
|
||||
import { wrapper } from 'axios-cookiejar-support';
|
||||
import { CookieJar } from 'tough-cookie';
|
||||
import { sleep } from '@/lib/utils';
|
||||
import yn from 'yn';
|
||||
import { isPage, sleep, waitForRequests } from '@/lib/utils';
|
||||
import * as cookie from 'cookie';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Solver } from '@2captcha/captcha-solver';
|
||||
import { paramsCoordinates } from '@2captcha/captcha-solver/dist/structs/2captcha';
|
||||
import { BrowserContext, Page, Locator, chromium, firefox } from 'rebrowser-playwright-core';
|
||||
import { createCursor, Cursor } from 'ghost-cursor-playwright';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// 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();
|
||||
export const DEFAULT_MODEL = 'chirp-v3-5';
|
||||
@ -30,37 +42,58 @@ export interface AudioInfo {
|
||||
class SunoApi {
|
||||
private static BASE_URL: string = 'https://studio-api.prod.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 clerkVersion?: string;
|
||||
private sid?: 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) {
|
||||
const cookieJar = new CookieJar();
|
||||
const randomUserAgent = new UserAgent(/Chrome/).random().toString();
|
||||
this.client = wrapper(
|
||||
axios.create({
|
||||
jar: cookieJar,
|
||||
constructor(cookies: string) {
|
||||
this.userAgent = new UserAgent(/Macintosh/).random().toString(); // Usually Mac systems get less amount of CAPTCHAs
|
||||
this.cookies = cookie.parse(cookies);
|
||||
this.deviceId = this.cookies.ajs_anonymous_id || randomUUID();
|
||||
this.client = axios.create({
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'User-Agent': randomUserAgent,
|
||||
Cookie: cookie
|
||||
'Affiliate-Id': 'undefined',
|
||||
'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) => {
|
||||
if (this.currentToken) {
|
||||
// Use the current token status
|
||||
config.headers['Authorization'] = `Bearer ${this.currentToken}`;
|
||||
}
|
||||
config.headers.Cookie = cookiesArray.join('; ');
|
||||
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> {
|
||||
await this.getClerkLatestVersion();
|
||||
//await this.getClerkLatestVersion();
|
||||
await this.getAuthToken();
|
||||
await this.keepAlive();
|
||||
return this;
|
||||
@ -68,7 +101,8 @@ class SunoApi {
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
// URL to get clerk version ID
|
||||
const getClerkVersionUrl = `${SunoApi.JSDELIVR_BASE_URL}/v1/package/npm/@clerk/clerk-js`;
|
||||
@ -80,26 +114,28 @@ class SunoApi {
|
||||
);
|
||||
}
|
||||
// Save clerk version ID for auth
|
||||
// this.clerkVersion = versionListResponse?.data?.['tags']['latest'];
|
||||
// Use a Clerk version released before fraud detection was implemented
|
||||
this.clerkVersion = "5.34.0";
|
||||
SunoApi.clerkVersion = versionListResponse?.data?.['tags']['latest'];
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the session ID and save it for later use.
|
||||
*/
|
||||
private async getAuthToken() {
|
||||
logger.info('Getting the 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
|
||||
const sessionResponse = await this.client.get(getSessionUrl);
|
||||
if (!sessionResponse?.data?.response?.['last_active_session_id']) {
|
||||
const sessionResponse = await this.client.get(getSessionUrl, {
|
||||
headers: { Authorization: this.cookies.__client }
|
||||
});
|
||||
if (!sessionResponse?.data?.response?.last_active_session_id) {
|
||||
throw new Error(
|
||||
'Failed to get session id, you may need to update the SUNO_COOKIE'
|
||||
);
|
||||
}
|
||||
// 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,18 +147,269 @@ class SunoApi {
|
||||
throw new Error('Session ID is not set. Cannot renew 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
|
||||
const renewResponse = await this.client.post(renewUrl);
|
||||
logger.info('KeepAlive...\n');
|
||||
const renewResponse = await this.client.post(renewUrl, {}, {
|
||||
headers: { Authorization: this.cookies.__client }
|
||||
});
|
||||
if (isWait) {
|
||||
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
|
||||
this.currentToken = newToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
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 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'
|
||||
];
|
||||
// Check for GPU acceleration, as it is recommended to turn it off for Docker
|
||||
if (yn(process.env.BROWSER_DISABLE_GPU, { default: false }))
|
||||
args.push('--enable-unsafe-swiftshader',
|
||||
'--disable-gpu',
|
||||
'--disable-setuid-sandbox');
|
||||
const browser = await this.getBrowserType().launch({
|
||||
args,
|
||||
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 });
|
||||
|
||||
logger.info('Waiting for Suno interface to load');
|
||||
// await page.locator('.react-aria-GridList').waitFor({ timeout: 60000 });
|
||||
await page.waitForResponse('**/api/project/**\\?**', { timeout: 60000 }); // wait for song list API call
|
||||
|
||||
if (this.ghostCursorEnabled)
|
||||
this.cursor = await createCursor(page);
|
||||
|
||||
logger.info('Triggering the CAPTCHA');
|
||||
try {
|
||||
await page.getByLabel('Close').click({ timeout: 2000 }); // close all popups
|
||||
// await this.click(page, { x: 318, y: 13 });
|
||||
} catch(e) {}
|
||||
|
||||
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');
|
||||
this.click(button);
|
||||
|
||||
const controller = new AbortController();
|
||||
new Promise<void>(async (resolve, reject) => {
|
||||
const frame = page.frameLocator('iframe[title*="hCaptcha"]');
|
||||
const challenge = frame.locator('.challenge-container');
|
||||
try {
|
||||
let wait = true;
|
||||
while (true) {
|
||||
if (wait)
|
||||
await waitForRequests(page, controller.signal);
|
||||
const drag = (await challenge.locator('.prompt-text').first().innerText()).toLowerCase().includes('drag');
|
||||
let captcha: any;
|
||||
for (let j = 0; j < 3; j++) { // try several times because sometimes 2Captcha could return an error
|
||||
try {
|
||||
logger.info('Sending the CAPTCHA to 2Captcha');
|
||||
const payload: paramsCoordinates = {
|
||||
body: (await challenge.screenshot({ timeout: 5000 })).toString('base64'),
|
||||
lang: process.env.BROWSER_LOCALE
|
||||
};
|
||||
if (drag) {
|
||||
// Say to the worker that he needs to click
|
||||
payload.textinstructions = 'CLICK on the shapes at their edge or center as shown above—please be precise!';
|
||||
payload.imginstructions = (await fs.readFile(path.join(process.cwd(), 'public', 'drag-instructions.jpg'))).toString('base64');
|
||||
}
|
||||
captcha = await this.solver.coordinates(payload);
|
||||
break;
|
||||
} catch(err: any) {
|
||||
logger.info(err.message);
|
||||
if (j != 2)
|
||||
logger.info('Retrying...');
|
||||
else
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (drag) {
|
||||
const challengeBox = await challenge.boundingBox();
|
||||
if (challengeBox == null)
|
||||
throw new Error('.challenge-container boundingBox is null!');
|
||||
if (captcha.data.length % 2) {
|
||||
logger.info('Solution does not have even amount of points required for dragging. Requesting new solution...');
|
||||
this.solver.badReport(captcha.id);
|
||||
wait = false;
|
||||
continue;
|
||||
}
|
||||
for (let i = 0; i < captcha.data.length; i += 2) {
|
||||
const data1 = captcha.data[i];
|
||||
const data2 = captcha.data[i+1];
|
||||
logger.info(JSON.stringify(data1) + JSON.stringify(data2));
|
||||
await page.mouse.move(challengeBox.x + +data1.x, challengeBox.y + +data1.y);
|
||||
await page.mouse.down();
|
||||
await sleep(1.1); // wait for the piece to be 'unlocked'
|
||||
await page.mouse.move(challengeBox.x + +data2.x, challengeBox.y + +data2.y, { steps: 30 });
|
||||
await page.mouse.up();
|
||||
}
|
||||
wait = true;
|
||||
} else {
|
||||
for (const data of captcha.data) {
|
||||
logger.info(data);
|
||||
await this.click(challenge, { x: +data.x, y: +data.y });
|
||||
};
|
||||
}
|
||||
this.click(frame.locator('.button-submit')).catch(e => {
|
||||
if (e.message.includes('viewport')) // when hCaptcha window has been closed due to inactivity,
|
||||
this.click(button); // click the Create button again to trigger the CAPTCHA
|
||||
else
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
} catch(e: any) {
|
||||
if (e.message.includes('been closed') // catch error when closing the browser
|
||||
|| e.message == 'AbortError') // catch error when waitForRequests is aborted
|
||||
resolve();
|
||||
else
|
||||
reject(e);
|
||||
}
|
||||
}).catch(e => {
|
||||
browser.browser()?.close();
|
||||
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();
|
||||
controller.abort();
|
||||
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' } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a song based on the prompt.
|
||||
* @param prompt The text prompt to generate audio from.
|
||||
@ -138,7 +425,7 @@ class SunoApi {
|
||||
): Promise<AudioInfo[]> {
|
||||
await this.keepAlive(false);
|
||||
const startTime = Date.now();
|
||||
const audios = this.generateSongs(
|
||||
const audios = await this.generateSongs(
|
||||
prompt,
|
||||
false,
|
||||
undefined,
|
||||
@ -225,6 +512,8 @@ class SunoApi {
|
||||
* @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 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.
|
||||
*/
|
||||
private async generateSongs(
|
||||
@ -235,14 +524,21 @@ class SunoApi {
|
||||
make_instrumental?: boolean,
|
||||
model?: string,
|
||||
wait_audio: boolean = false,
|
||||
negative_tags?: string
|
||||
negative_tags?: string,
|
||||
task?: string,
|
||||
continue_clip_id?: string,
|
||||
continue_at?: number
|
||||
): Promise<AudioInfo[]> {
|
||||
await this.keepAlive(false);
|
||||
await this.keepAlive();
|
||||
const payload: any = {
|
||||
make_instrumental: make_instrumental,
|
||||
mv: model || DEFAULT_MODEL,
|
||||
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) {
|
||||
payload.tags = tags;
|
||||
@ -276,13 +572,10 @@ class SunoApi {
|
||||
timeout: 10000 // 10 seconds timeout
|
||||
}
|
||||
);
|
||||
logger.info(
|
||||
'generateSongs Response:\n' + JSON.stringify(response.data, null, 2)
|
||||
);
|
||||
if (response.status !== 200) {
|
||||
throw new Error('Error response:' + response.statusText);
|
||||
}
|
||||
const songIds = response.data['clips'].map((audio: any) => audio.id);
|
||||
const songIds = response.data.clips.map((audio: any) => audio.id);
|
||||
//Want to wait for music file generation
|
||||
if (wait_audio) {
|
||||
const startTime = Date.now();
|
||||
@ -303,8 +596,7 @@ class SunoApi {
|
||||
}
|
||||
return lastResponse;
|
||||
} else {
|
||||
await this.keepAlive(true);
|
||||
return response.data['clips'].map((audio: any) => ({
|
||||
return response.data.clips.map((audio: any) => ({
|
||||
id: audio.id,
|
||||
title: audio.title,
|
||||
image_url: audio.image_url,
|
||||
@ -366,26 +658,14 @@ class SunoApi {
|
||||
public async extendAudio(
|
||||
audioId: string,
|
||||
prompt: string = '',
|
||||
continueAt: string = '0',
|
||||
continueAt: number,
|
||||
tags: string = '',
|
||||
negative_tags: string = '',
|
||||
title: string = '',
|
||||
model?: string
|
||||
): Promise<AudioInfo> {
|
||||
await this.keepAlive(false);
|
||||
const response = await this.client.post(
|
||||
`${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;
|
||||
model?: string,
|
||||
wait_audio?: boolean
|
||||
): Promise<AudioInfo[]> {
|
||||
return this.generateSongs(prompt, true, tags, title, false, model, wait_audio, negative_tags, 'extend', audioId, continueAt);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -460,7 +740,7 @@ class SunoApi {
|
||||
page?: string | null
|
||||
): Promise<AudioInfo[]> {
|
||||
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) {
|
||||
url.searchParams.append('ids', songIds.join(','));
|
||||
}
|
||||
@ -469,11 +749,11 @@ class SunoApi {
|
||||
}
|
||||
logger.info('Get audio status: ' + url.href);
|
||||
const response = await this.client.get(url.href, {
|
||||
// 3 seconds timeout
|
||||
timeout: 3000
|
||||
// 10 seconds timeout
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
const audios = response.data;
|
||||
const audios = response.data.clips;
|
||||
|
||||
return audios.map((audio: any) => ({
|
||||
id: audio.id,
|
||||
@ -523,13 +803,22 @@ class SunoApi {
|
||||
}
|
||||
}
|
||||
|
||||
const newSunoApi = async (cookie: string) => {
|
||||
const sunoApi = new SunoApi(cookie);
|
||||
return await sunoApi.init();
|
||||
export const sunoApi = async (cookie?: string) => {
|
||||
const resolvedCookie = cookie && cookie.includes('__client') ? cookie : process.env.SUNO_COOKIE; // Check for bad `Cookie` header (It's too expensive to actually parse the cookies *here*)
|
||||
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.');
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
if (!process.env.SUNO_COOKIE) {
|
||||
console.log('Environment does not contain SUNO_COOKIE.', process.env);
|
||||
}
|
||||
|
||||
export const sunoApi = newSunoApi(process.env.SUNO_COOKIE || '');
|
||||
|
@ -1,4 +1,5 @@
|
||||
import pino from "pino";
|
||||
import { Page } from "rebrowser-playwright-core";
|
||||
|
||||
const logger = pino();
|
||||
|
||||
@ -20,6 +21,95 @@ export const sleep = (x: number, y?: number): Promise<void> => {
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for an hCaptcha image requests and then waits for all of them to end
|
||||
* @param page
|
||||
* @param signal `const controller = new AbortController(); controller.status`
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const waitForRequests = (page: Page, signal: AbortSignal): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlPattern = /^https:\/\/img[a-zA-Z0-9]*\.hcaptcha\.com\/.*$/;
|
||||
let timeoutHandle: NodeJS.Timeout | null = null;
|
||||
let activeRequestCount = 0;
|
||||
let requestOccurred = false;
|
||||
|
||||
const cleanupListeners = () => {
|
||||
page.off('request', onRequest);
|
||||
page.off('requestfinished', onRequestFinished);
|
||||
page.off('requestfailed', onRequestFinished);
|
||||
};
|
||||
|
||||
const resetTimeout = () => {
|
||||
if (timeoutHandle)
|
||||
clearTimeout(timeoutHandle);
|
||||
if (activeRequestCount === 0) {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
cleanupListeners();
|
||||
resolve();
|
||||
}, 1000); // 1 second of no requests
|
||||
}
|
||||
};
|
||||
|
||||
const onRequest = (request: { url: () => string }) => {
|
||||
if (urlPattern.test(request.url())) {
|
||||
requestOccurred = true;
|
||||
activeRequestCount++;
|
||||
if (timeoutHandle)
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
};
|
||||
|
||||
const onRequestFinished = (request: { url: () => string }) => {
|
||||
if (urlPattern.test(request.url())) {
|
||||
activeRequestCount--;
|
||||
resetTimeout();
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for an hCaptcha request for up to 1 minute
|
||||
const initialTimeout = setTimeout(() => {
|
||||
if (!requestOccurred) {
|
||||
page.off('request', onRequest);
|
||||
cleanupListeners();
|
||||
reject(new Error('No hCaptcha request occurred within 1 minute.'));
|
||||
} else {
|
||||
// Start waiting for no hCaptcha requests
|
||||
resetTimeout();
|
||||
}
|
||||
}, 60000); // 1 minute timeout
|
||||
|
||||
page.on('request', onRequest);
|
||||
page.on('requestfinished', onRequestFinished);
|
||||
page.on('requestfailed', onRequestFinished);
|
||||
|
||||
// Cleanup the initial timeout if an hCaptcha request occurs
|
||||
page.on('request', (request: { url: () => string }) => {
|
||||
if (urlPattern.test(request.url())) {
|
||||
clearTimeout(initialTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
const onAbort = () => {
|
||||
cleanupListeners();
|
||||
clearTimeout(initialTimeout);
|
||||
if (timeoutHandle)
|
||||
clearTimeout(timeoutHandle);
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
reject(new Error('AbortError'));
|
||||
};
|
||||
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
export const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
|
@ -5,8 +5,10 @@
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
@ -18,7 +20,8 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"playwright-core": ["./node_modules/rebrowser-playwright-core"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
|
Loading…
Reference in New Issue
Block a user