Compare commits

...
12 Commits
Author SHA1 Message Date
wangbo c14a25fbc2 自动测试脚本 2025-09-09 21:50:33 +08:00
wangbo 93b37907f3 优化构建路径前缀 2025-09-09 11:52:03 +08:00
wangbo 42cb19214f 优化代理 2025-09-07 18:42:24 +08:00
blueeonandGitHub 0646ebc59b Merge pull request #234 from sontl/persona-api
feat(api): Add persona endpoint for retrieving persona information and clips
2025-03-17 22:24:33 +08:00
Son Tran Lam 2bc500723f feat(api): Add persona endpoint for retrieving persona information and clips
- Implement `/api/persona` GET endpoint to fetch persona details
- Add Swagger documentation for the new persona API endpoint
- Update docs page to include new `/api/persona` route description
- Extend SunoApi class with `getPersonaPaginated` method to support persona data retrieval
2025-02-17 19:28:27 +08:00
blueeonandGitHub c3a8c568a5 Merge pull request #225 from CharlesCNorton/patch-1
fix(readme): remove the extra quotation mark in the <h1> tag
2025-01-31 16:13:17 +08:00
CharlesCNortonandGitHub defaaf1b7f fix(readme): remove the extra quotation mark in the <h1> tag
An extra quotation mark in the <h1> align attribute was causing
syntax issues in the README. This commit corrects that to ensure
valid HTML rendering.
2025-01-28 10:48:35 -05:00
blueeonandGitHub 48d667b064 Merge pull request #222 from gohoski/patch-1
Implement hCaptcha solving via 2Captcha [URGENT MERGE]
2025-01-27 22:39:18 +08:00
gohoski 48a39a77f4 implement cookie check, use browser NPM package for auto install instead of a manual command, fix Docker & add notice about macOS recommendation 2025-01-21 23:15:53 +03:00
gohoski 72bdbe083e change song API url in interface wait trigger 2025-01-15 23:17:00 +03:00
gohoski 881c6c773c changed wait for hCaptcha image logic & other stuff
- fixed bug in dragging type of hCaptcha when worker did not select an even amount of coordinates and it would crash
- change waitForResponse function to a waitForRequests util function with more proper checks
2025-01-11 01:48:17 +03:00
gohoski 52ad4dea00 properly catch hCaptcha window closing after timeout.
please note that you can't increase the timeout in any way, even by clicking,so the only option we have is to just reinstate the solving process
2025-01-08 03:10:49 +03:00
14 changed files with 1289 additions and 5448 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# For more information, please see the README.md
SUNO_COOKIE=
TWOCAPTCHA_KEY= # Obtain from 2captcha.com
BROWSER=chromium # chromium or firefox
BROWSER=chromium # `chromium` or `firefox`, although `chromium` is highly recommended
BROWSER_GHOST_CURSOR=false
BROWSER_LOCALE=en
BROWSER_HEADLESS=true
+28 -22
View File
@@ -1,25 +1,31 @@
# syntax=docker/dockerfile:1
FROM node:lts-alpine AS builder
WORKDIR /src
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:lts-alpine
WORKDIR /app
COPY package*.json ./
ARG SUNO_COOKIE
ARG BROWSER
RUN if [ -z "$SUNO_COOKIE" ]; then echo "Warning: SUNO_COOKIE is not set"; fi
FROM node:lts-bookworm AS builder
WORKDIR /src
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
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 "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}
RUN if [ -z "$BROWSER" ]; then echo "Warning: BROWSER is not set; will use chromium by default"; fi
ENV BROWSER=${BROWSER:-chromium}
# 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
RUN npx playwright install $BROWSER
COPY --from=builder /src/.next ./.next
EXPOSE 3000
CMD ["npm", "run", "start"]
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"]
+9 -3
View File
@@ -1,5 +1,5 @@
<div align="center">
<h1 align="center"">
<h1 align="center">
Suno AI API
</h1>
<p>Use API to call the music generation AI of Suno.ai and easily integrate it into agents like GPTs.</p>
@@ -64,7 +64,11 @@ We have deployed an example bound to a free Suno account, so it has daily usage
[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.
> [!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
@@ -80,9 +84,11 @@ You can choose your preferred deployment method:
git clone https://github.com/gcui-art/suno-api.git
cd suno-api
npm install
npx playwright install chromium
```
#### 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/). However, follow the step below before running.
```bash
+7 -2
View File
@@ -64,7 +64,11 @@ Suno — потрясающий сервис для ИИ-музыки. Несм
[Создайте](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. Это абсолютно тот же сервис, но данный интерфейс поддерживает платежи из этих стран.
> [!NOTE]
> Если вы находитесь в России или Беларуси, используйте интерфейс [ruCaptcha](https://rucaptcha.com) вместо 2Captcha. Это абсолютно тот же сервис, но данный интерфейс поддерживает платежи из этих стран.
> [!TIP]
> Если вы хотите получать как можно меньше капч, рекомендуется использовать macOS. Системы на macOS обычно получают меньше капч, чем Linux и Windows — это связано с их непопулярностью в сфере веб-скрейпинга. Запуск suno-api на Windows и Linux будет работать, но в некоторых случаях вы можете получить довольно большое количество капч.
### 3. Скачайте и запустите проект
@@ -80,9 +84,10 @@ Suno — потрясающий сервис для ИИ-музыки. Несм
git clone https://github.com/gcui-art/suno-api.git
cd suno-api
npm install
npx playwright install chromium
```
#### Docker
>[!IMPORTANT]
> Аппаратное видеоускорение браузера будет отключено в Docker. Если у вас медленный процессор, рекомендуется [развернуть локально](#локально).
Также можно использовать [Docker Compose](https://docs.docker.com/compose/), однако перед запуском выполните шаг ниже.
```bash
+7 -5
View File
@@ -2,11 +2,13 @@ version: '3'
services:
suno-api:
build:
context: .
args:
SUNO_COOKIE: ${SUNO_COOKIE}
image: registry.cn-shanghai.aliyuncs.com/easyaigc/suno-api:latest
# build:
# context: .
# args:
# SUNO_COOKIE: ${SUNO_COOKIE}
volumes:
- ./public:/app/public
ports:
- "3000:3000"
- "3013:3000"
env_file: ".env"
+430 -6
View File
@@ -10,6 +10,7 @@
"license": "LGPL-3.0-or-later",
"dependencies": {
"@2captcha/captcha-solver": "^1.3.0",
"@playwright/browser-chromium": "^1.49.1",
"@vercel/analytics": "^1.2.2",
"axios": "^1.7.8",
"bufferutil": "^4.0.8",
@@ -17,16 +18,19 @@
"cookie": "^1.0.2",
"electron": "^33.2.1",
"ghost-cursor-playwright": "^2.1.0",
"https-proxy-agent": "^7.0.6",
"js-cookie": "^3.0.5",
"next": "14.1.4",
"next-swagger-doc": "^0.4.0",
"pino": "^8.19.0",
"pino-pretty": "^11.0.0",
"playwright-core": "^1.49.1",
"playwright-extra": "^4.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"react": "^18",
"react-dom": "^18",
"react-markdown": "^9.0.1",
"rebrowser-playwright-core": "^1.49.1",
"socks-proxy-agent": "^8.0.5",
"swagger-ui-react": "^5.18.2",
"tough-cookie": "^4.1.4",
"user-agents": "^1.1.156",
@@ -603,6 +607,19 @@
"node": ">=14"
}
},
"node_modules/@playwright/browser-chromium": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.49.1.tgz",
"integrity": "sha512-LLeyllKSucbojsJBOpdJshwW27ZXZs3oypqffkVWLUvxX2azHJMOevsOcWpjCfoYbpevkaEozM2xHeSUGF00lg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1587,6 +1604,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -1680,6 +1706,15 @@
"node": ">= 0.4"
}
},
"node_modules/arr-union": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/arr-union/-/arr-union-3.1.0.tgz",
"integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/array-buffer-byte-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
@@ -2453,6 +2488,22 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/clone-deep": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/clone-deep/-/clone-deep-0.2.4.tgz",
"integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==",
"license": "MIT",
"dependencies": {
"for-own": "^0.1.3",
"is-plain-object": "^2.0.1",
"kind-of": "^3.0.2",
"lazy-cache": "^1.0.3",
"shallow-clone": "^0.1.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/clone-response": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
@@ -3896,6 +3947,27 @@
"is-callable": "^1.1.3"
}
},
"node_modules/for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/for-in/-/for-in-1.0.2.tgz",
"integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/for-own": {
"version": "0.1.5",
"resolved": "https://registry.npmmirror.com/for-own/-/for-own-0.1.5.tgz",
"integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==",
"license": "MIT",
"dependencies": {
"for-in": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/foreground-child": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
@@ -4596,6 +4668,19 @@
"node": ">=10.19.0"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -4716,6 +4801,15 @@
"loose-envify": "^1.0.0"
}
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -4816,6 +4910,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"license": "MIT"
},
"node_modules/is-bun-module": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.2.1.tgz",
@@ -4897,6 +4997,15 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -5043,6 +5152,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz",
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
"license": "MIT",
"dependencies": {
"isobject": "^3.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@@ -5193,6 +5314,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/isobject": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/iterator.prototype": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz",
@@ -5355,6 +5485,18 @@
"json-buffer": "3.0.1"
}
},
"node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
"license": "MIT",
"dependencies": {
"is-buffer": "^1.1.5"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -5375,6 +5517,15 @@
"node": ">=0.10"
}
},
"node_modules/lazy-cache": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/lazy-cache/-/lazy-cache-1.0.4.tgz",
"integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -5707,6 +5858,20 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/merge-deep": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/merge-deep/-/merge-deep-3.0.3.tgz",
"integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==",
"license": "MIT",
"dependencies": {
"arr-union": "^3.1.0",
"clone-deep": "^0.2.4",
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -6255,6 +6420,28 @@
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mixin-object": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/mixin-object/-/mixin-object-2.0.1.tgz",
"integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==",
"license": "MIT",
"dependencies": {
"for-in": "^0.1.3",
"is-extendable": "^0.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/mixin-object/node_modules/for-in": {
"version": "0.1.8",
"resolved": "https://registry.npmmirror.com/for-in/-/for-in-0.1.8.tgz",
"integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@@ -7024,6 +7211,30 @@
"node": ">=18"
}
},
"node_modules/playwright-extra": {
"version": "4.3.6",
"resolved": "https://registry.npmmirror.com/playwright-extra/-/playwright-extra-4.3.6.tgz",
"integrity": "sha512-q2rVtcE8V8K3vPVF1zny4pvwZveHLH8KBuVU2MoE3Jw4OKVoBWsHI9CH9zPydovHHOCDxjGN2Vg+2m644q3ijA==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"playwright": "*",
"playwright-core": "*"
},
"peerDependenciesMeta": {
"playwright": {
"optional": true
},
"playwright-core": {
"optional": true
}
}
},
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@@ -7349,6 +7560,147 @@
"node": ">=6"
}
},
"node_modules/puppeteer-extra-plugin": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/puppeteer-extra-plugin/-/puppeteer-extra-plugin-3.2.3.tgz",
"integrity": "sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q==",
"license": "MIT",
"dependencies": {
"@types/debug": "^4.1.0",
"debug": "^4.1.1",
"merge-deep": "^3.0.1"
},
"engines": {
"node": ">=9.11.2"
},
"peerDependencies": {
"playwright-extra": "*",
"puppeteer-extra": "*"
},
"peerDependenciesMeta": {
"playwright-extra": {
"optional": true
},
"puppeteer-extra": {
"optional": true
}
}
},
"node_modules/puppeteer-extra-plugin-stealth": {
"version": "2.11.2",
"resolved": "https://registry.npmmirror.com/puppeteer-extra-plugin-stealth/-/puppeteer-extra-plugin-stealth-2.11.2.tgz",
"integrity": "sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"puppeteer-extra-plugin": "^3.2.3",
"puppeteer-extra-plugin-user-preferences": "^2.4.1"
},
"engines": {
"node": ">=8"
},
"peerDependencies": {
"playwright-extra": "*",
"puppeteer-extra": "*"
},
"peerDependenciesMeta": {
"playwright-extra": {
"optional": true
},
"puppeteer-extra": {
"optional": true
}
}
},
"node_modules/puppeteer-extra-plugin-user-data-dir": {
"version": "2.4.1",
"resolved": "https://registry.npmmirror.com/puppeteer-extra-plugin-user-data-dir/-/puppeteer-extra-plugin-user-data-dir-2.4.1.tgz",
"integrity": "sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g==",
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"fs-extra": "^10.0.0",
"puppeteer-extra-plugin": "^3.2.3",
"rimraf": "^3.0.2"
},
"engines": {
"node": ">=8"
},
"peerDependencies": {
"playwright-extra": "*",
"puppeteer-extra": "*"
},
"peerDependenciesMeta": {
"playwright-extra": {
"optional": true
},
"puppeteer-extra": {
"optional": true
}
}
},
"node_modules/puppeteer-extra-plugin-user-data-dir/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/puppeteer-extra-plugin-user-data-dir/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/puppeteer-extra-plugin-user-data-dir/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/puppeteer-extra-plugin-user-preferences": {
"version": "2.4.1",
"resolved": "https://registry.npmmirror.com/puppeteer-extra-plugin-user-preferences/-/puppeteer-extra-plugin-user-preferences-2.4.1.tgz",
"integrity": "sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A==",
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"deepmerge": "^4.2.2",
"puppeteer-extra-plugin": "^3.2.3",
"puppeteer-extra-plugin-user-data-dir": "^2.4.1"
},
"engines": {
"node": ">=8"
},
"peerDependencies": {
"playwright-extra": "*",
"puppeteer-extra": "*"
},
"peerDependenciesMeta": {
"playwright-extra": {
"optional": true
},
"puppeteer-extra": {
"optional": true
}
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@@ -8021,7 +8373,6 @@
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
@@ -8038,7 +8389,6 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -8267,6 +8617,42 @@
"sha.js": "bin.js"
}
},
"node_modules/shallow-clone": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/shallow-clone/-/shallow-clone-0.1.2.tgz",
"integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==",
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.1",
"kind-of": "^2.0.1",
"lazy-cache": "^0.2.3",
"mixin-object": "^2.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/shallow-clone/node_modules/kind-of": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-2.0.1.tgz",
"integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==",
"license": "MIT",
"dependencies": {
"is-buffer": "^1.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/shallow-clone/node_modules/lazy-cache": {
"version": "0.2.7",
"resolved": "https://registry.npmmirror.com/lazy-cache/-/lazy-cache-0.2.7.tgz",
"integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -8389,6 +8775,44 @@
"node": ">=8"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.5",
"resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/sonic-boom": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz",
@@ -9568,9 +9992,9 @@
}
},
"node_modules/user-agents": {
"version": "1.1.362",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.1.362.tgz",
"integrity": "sha512-mwDzPkR3IZswVYotnQJU4t/H56K0QBWGlkr3eDPHPzMYiUkxZCFICU1n4H3OHUN2QHVHsdlRpDMbsi39hhvvMg==",
"version": "1.1.655",
"resolved": "https://registry.npmmirror.com/user-agents/-/user-agents-1.1.655.tgz",
"integrity": "sha512-3zdmOqszMxPoqTzOAtOacV6L4N+g5+n4NStyZh+PSoYf4A/i5ZPQQNlW5tEkLaDLtu/XjQCaii2BZt//ZgaLrw==",
"license": "BSD-2-Clause",
"dependencies": {
"lodash.clonedeep": "^4.5.0"
+9 -1
View File
@@ -9,13 +9,17 @@
"version": "1.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "PORT=3013 next dev",
"build": "next build",
"build:docker": "docker build . --platform linux/amd64 -t registry.cn-shanghai.aliyuncs.com/easyaigc/suno-api:latest",
"docker:push": "docker push registry.cn-shanghai.aliyuncs.com/easyaigc/suno-api:latest",
"deploy": "npm build:docker && npm docker:push",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@2captcha/captcha-solver": "^1.3.0",
"@playwright/browser-chromium": "^1.49.1",
"@vercel/analytics": "^1.2.2",
"axios": "^1.7.8",
"bufferutil": "^4.0.8",
@@ -23,15 +27,19 @@
"cookie": "^1.0.2",
"electron": "^33.2.1",
"ghost-cursor-playwright": "^2.1.0",
"https-proxy-agent": "^7.0.6",
"js-cookie": "^3.0.5",
"next": "14.1.4",
"next-swagger-doc": "^0.4.0",
"pino": "^8.19.0",
"pino-pretty": "^11.0.0",
"playwright-extra": "^4.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"react": "^18",
"react-dom": "^18",
"react-markdown": "^9.0.1",
"rebrowser-playwright-core": "^1.49.1",
"socks-proxy-agent": "^8.0.5",
"swagger-ui-react": "^5.18.2",
"tough-cookie": "^4.1.4",
"user-agents": "^1.1.156",
-5345
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
import { NextResponse, NextRequest } from "next/server";
import { sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
if (req.method === 'GET') {
try {
const url = new URL(req.url);
const personaId = url.searchParams.get('id');
const page = url.searchParams.get('page');
if (personaId == null) {
return new NextResponse(JSON.stringify({ error: 'Missing parameter id' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
const pageNumber = page ? parseInt(page) : 1;
const personaInfo = await (await sunoApi()).getPersonaPaginated(personaId, pageNumber);
return new NextResponse(JSON.stringify(personaInfo), {
status: 200,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
} catch (error) {
console.error('Error fetching persona:', error);
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
} else {
return new NextResponse('Method Not Allowed', {
headers: {
Allow: 'GET',
...corsHeaders
},
status: 405
});
}
}
export async function OPTIONS(request: Request) {
return new Response(null, {
status: 200,
headers: corsHeaders
});
}
+1
View File
@@ -33,6 +33,7 @@ export default function Docs() {
- \`/api/get_aligned_lyrics\`: Get list of timestamps for each word in the lyrics
- \`/api/clip\`: Get clip information based on ID passed as query parameter \`id\`
- \`/api/concat\`: Generate the whole song from extensions
- \`/api/persona\`: Get persona information and clips based on ID and page number
\`\`\`
Feel free to explore the detailed API parameters and conduct tests on this page.
+143
View File
@@ -588,6 +588,149 @@
}
}
}
},
"/api/persona": {
"get": {
"summary": "Get persona information and clips.",
"description": "Retrieve persona information, including associated clips and pagination data.",
"tags": ["default"],
"parameters": [
{
"name": "id",
"in": "query",
"required": true,
"description": "Persona ID",
"schema": {
"type": "string"
}
},
{
"name": "page",
"in": "query",
"required": false,
"description": "Page number (defaults to 1)",
"schema": {
"type": "integer",
"default": 1
}
}
],
"responses": {
"200": {
"description": "success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"persona": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Persona ID"
},
"name": {
"type": "string",
"description": "Persona name"
},
"description": {
"type": "string",
"description": "Persona description"
},
"image_s3_id": {
"type": "string",
"description": "Persona image URL"
},
"root_clip_id": {
"type": "string",
"description": "Root clip ID"
},
"clip": {
"type": "object",
"description": "Root clip information"
},
"persona_clips": {
"type": "array",
"items": {
"type": "object",
"properties": {
"clip": {
"type": "object",
"description": "Clip information"
}
}
}
},
"is_suno_persona": {
"type": "boolean",
"description": "Whether this is a Suno official persona"
},
"is_public": {
"type": "boolean",
"description": "Whether this persona is public"
},
"upvote_count": {
"type": "integer",
"description": "Number of upvotes"
},
"clip_count": {
"type": "integer",
"description": "Number of clips"
}
}
},
"total_results": {
"type": "integer",
"description": "Total number of results"
},
"current_page": {
"type": "integer",
"description": "Current page number"
},
"is_following": {
"type": "boolean",
"description": "Whether the current user is following this persona"
}
}
}
}
}
},
"400": {
"description": "Missing parameter id",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "Missing parameter id"
}
}
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "Internal server error"
}
}
}
}
}
}
}
}
}
},
"components": {
+180 -63
View File
@@ -2,15 +2,19 @@ import axios, { AxiosInstance } from 'axios';
import UserAgent from 'user-agents';
import pino from 'pino';
import yn from 'yn';
import { sleep, isPage } from '@/lib/utils';
import { isPage, sleep, waitForRequests } 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';
import { paramsCoordinates } from '@2captcha/captcha-solver/dist/structs/2captcha';
import { BrowserContext, Page,chromium, Locator, firefox } from 'rebrowser-playwright-core';
import { createCursor, Cursor } from 'ghost-cursor-playwright';
import { promises as fs } from 'fs';
import path from 'node:path';
import { SocksProxyAgent } from 'socks-proxy-agent';
// import { chromium } from 'playwright-extra'; // Import from playwright-extra
// import StealthPlugin from 'puppeteer-extra-plugin-stealth'
// sunoApi instance caching
const globalForSunoApi = global as unknown as { sunoApiCache?: Map<string, SunoApi> };
@@ -39,6 +43,34 @@ export interface AudioInfo {
error_message?: string; // Error message if any
}
interface PersonaResponse {
persona: {
id: string;
name: string;
description: string;
image_s3_id: string;
root_clip_id: string;
clip: any; // You can define a more specific type if needed
user_display_name: string;
user_handle: string;
user_image_url: string;
persona_clips: Array<{
clip: any; // You can define a more specific type if needed
}>;
is_suno_persona: boolean;
is_trashed: boolean;
is_owned: boolean;
is_public: boolean;
is_public_approved: boolean;
is_loved: boolean;
upvote_count: number;
clip_count: number;
};
total_results: number;
current_page: number;
is_following: boolean;
}
class SunoApi {
private static BASE_URL: string = 'https://studio-api.prod.suno.com';
private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
@@ -58,6 +90,29 @@ class SunoApi {
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();
const proxyUrl = process.env.PROXY_URL;
let extraConfig: Record<string, any> = {};
if (proxyUrl) {
if (proxyUrl.startsWith('socks')) {
// SOCKS5 代理
const agent = new SocksProxyAgent(proxyUrl);
extraConfig = {
httpAgent: agent,
httpsAgent: agent,
proxy: false // 一定要关掉 axios 自带的 proxy
};
} else {
// HTTP/HTTPS 代理
const url = new URL(proxyUrl);
extraConfig = {
proxy: {
protocol: url.protocol.replace(':', ''), // 去掉末尾冒号
host: url.hostname,
port: Number(url.port)
}
};
}
}
this.client = axios.create({
withCredentials: true,
headers: {
@@ -69,7 +124,8 @@ class SunoApi {
'sec-ch-ua-mobile': '?1',
'sec-ch-ua-platform': '"Android"',
'User-Agent': this.userAgent
}
},
...extraConfig,
});
this.client.interceptors.request.use(config => {
if (this.currentToken && !config.headers.Authorization)
@@ -180,7 +236,6 @@ class SunoApi {
ctype: 'generation'
});
logger.info(resp.data);
// await sleep(10);
return resp.data.required;
}
@@ -231,40 +286,63 @@ class SunoApi {
* @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) {
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');
try {
// chromium.use(StealthPlugin())
// const browser = await this.getBrowserType().launch({
// args,
// headless: yn(process.env.BROWSER_HEADLESS, { default: true }),
// ...(process.env.PROXY_URL &&{ proxy: {
// server: process.env.PROXY_URL,
// }})
// })
const browser = await chromium.launch({
args,
headless: yn(process.env.BROWSER_HEADLESS, { default: true }),
...(process.env.PROXY_URL &&{ proxy: {
server: process.env.PROXY_URL,
}})
})
const context = await browser.newContext({ userAgent: this.userAgent, locale: process.env.BROWSER_LOCALE, viewport: { width: 1920, height: 1080 } });
const cookies = [];
const lax: 'Lax' | 'Strict' | 'None' = 'Lax';
cookies.push({
name: key,
value: this.cookies[key]+'',
name: '__session',
value: this.currentToken+'',
domain: '.suno.com',
path: '/',
sameSite: lax
})
sameSite: lax,
});
for (const key in this.cookies) {
cookies.push({
name: key,
value: this.cookies[key]+'',
domain: '.suno.com',
path: '/',
sameSite: lax,
secure: true, // Cookies from real browsers are often secure
})
}
await context.addCookies(cookies);
return context;
}catch ( e){
console.log(e);
throw e;
}
await context.addCookies(cookies);
return context;
}
/**
@@ -278,50 +356,59 @@ class SunoApi {
logger.info('CAPTCHA required. Launching browser...')
const browser = await this.launchBrowser();
const page = await browser.newPage();
// 1. 在 page.goto 之前设置监听器
page.on('request', request => {
// 检查请求 URL 是否包含关键词
console.log('>> Found project API request:', request.url());
// if (request.url().includes('studio-api.prod.suno.com/api/project/default')) {
// console.log('>> Found project API request:', request.url());
// }
});
await page.goto('https://suno.com/create', { referer: 'https://www.google.com/', waitUntil: 'domcontentloaded', timeout: 0 });
//弹出cloudfare验证
logger.info('Waiting for Suno interface to load');
//await page.locator('.react-aria-GridList').waitFor({ timeout: 60000 });
await page.waitForResponse('**/api/feed/v2**', { timeout: 60000 }); // wait for song list API call
// 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');
await this.click(page, { x: 318, y: 13 }); // close all popups
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');
await this.click(button);
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');
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
}
}
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 send an error
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()).toString('base64'),
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 = '! Instead of dragging, CLICK on the shapes as shown in the image above !';
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);
@@ -333,11 +420,17 @@ class SunoApi {
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];
@@ -348,21 +441,26 @@ class SunoApi {
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 });
};
}
/*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);
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();
@@ -374,6 +472,7 @@ class SunoApi {
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);
@@ -409,7 +508,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,
@@ -785,10 +884,28 @@ class SunoApi {
monthly_usage: response.data.monthly_usage
};
}
public async getPersonaPaginated(personaId: string, page: number = 1): Promise<PersonaResponse> {
await this.keepAlive(false);
const url = `${SunoApi.BASE_URL}/api/persona/get-persona-paginated/${personaId}/?page=${page}`;
logger.info(`Fetching persona data: ${url}`);
const response = await this.client.get(url, {
timeout: 10000 // 10 seconds timeout
});
if (response.status !== 200) {
throw new Error('Error response: ' + response.statusText);
}
return response.data;
}
}
export const sunoApi = async (cookie?: string) => {
const resolvedCookie = cookie || process.env.SUNO_COOKIE;
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.');
+81
View File
@@ -29,6 +29,87 @@ 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': '*',
+332
View File
@@ -0,0 +1,332 @@
import { chromium } from 'playwright-extra'; // Import from playwright-extra
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
import yn from 'yn';
import UserAgent from 'user-agents';
// 把字符串转为对象数组
function parseCookies(cookieString:string, domain:string) {
return cookieString.split(';').map(c => {
const [name, ...rest] = c.trim().split('=');
const lax: 'Lax' | 'Strict' | 'None' = 'Lax';
return {
name,
value: rest.join('='), // 防止 value 里有 "=" 的情况
domain, // 必须指定 domain
path: '/', // 一般都是根路径
httpOnly: false,
secure: true,
sameSite: lax,
};
});
}
const API_KEY = "475f30640c5860c432064a2c37f06fd6"; // 你的 2Captcha key
// 等待 Turnstile 渲染出来(最多 60 秒)
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function waitForSiteKey(page, { timeout = 90000, pollInterval = 500 } = {}) {
const start = Date.now();
while (Date.now() - start < timeout) {
// 1) 快速在主文档中寻找(data-sitekey、iframe src query、script 文本)
const mainCheck = await page.evaluate(() => {
const attrNames = ['data-sitekey','data-key','data-cf-turnstile-sitekey','data-hcaptcha-sitekey','data-captcha'];
for (const a of attrNames) {
const el = document.querySelector(`[${a}]`);
if (el) return { sitekey: el.getAttribute(a), source: 'dom', attr: a };
}
// 收集 iframe src/srcdoc(只返回字符串,不触碰 frame 内部以避免跨域问题)
const iframes = Array.from(document.querySelectorAll('iframe')).map(f => ({ src: f.src || f.getAttribute('src') || '', srcdoc: f.srcdoc || '' }));
for (const f of iframes) {
if (f.src) {
try {
const url = new URL(f.src, location.href);
const qp = Object.fromEntries(url.searchParams.entries());
const possible = qp.sitekey || qp.k || qp['data-sitekey'] || qp.s || qp.key;
if (possible) return { sitekey: possible, source: 'iframe-src', iframeSrc: f.src };
} catch(e){}
}
if (f.srcdoc && f.srcdoc.includes('sitekey')) {
const m = f.srcdoc.match(/sitekey['"]?\s*[:=]\s*['"]([\w\-]{8,})['"]/i) || f.srcdoc.match(/k=([A-Za-z0-9_-]{8,})/i);
if (m) return { sitekey: m[1], source: 'iframe-srcdoc' };
}
}
// 在内联 script 中查找 sitekey (有些站点把 sitekey 写在脚本里)
for (const s of Array.from(document.scripts)) {
const t = s.textContent || '';
if (!t) continue;
const m = t.match(/sitekey['"]?\s*[:=]\s*['"]([\w\-]{8,})['"]?/i) || t.match(/k=([A-Za-z0-9_-]{8,})/i);
if (m) return { sitekey: m[1], source: 'script' };
}
// window 变量(有些实现会在 window 上挂载)
try {
const candidates = ['turnstile','__turnstile','hcaptcha','__hcaptcha'];
for (const k of candidates) {
// 访问 window[k] 可能为 undefined 或对象
if (window[k] && window[k].sitekey) return { sitekey: window[k].sitekey, source: 'window.' + k };
}
} catch(e){}
return null;
});
// 判断页面是否已经进入登录/应用界面
const isLoginPage = await page.$('input[type="email"], input[name="username"], button:has-text("Sign in"), button:has-text("Log in")');
if (isLoginPage) {
// console.log('已经是登录界面,不需要验证码');
return null;
}
if (mainCheck && mainCheck.sitekey) return mainCheck;
// 2) 逐 frame 检查(对于可访问的 frame 直接 evaluate;对于跨域,解析 frame.url
const frames = page.frames();
for (const f of frames) {
try {
// 可能跨域,evaluate 会抛错,如果可访问就能直接从 frame DOM 找到
const res = await f.evaluate(() => {
const attrNames = ['data-sitekey','data-key','data-cf-turnstile-sitekey','data-hcaptcha-sitekey'];
for (const a of attrNames) {
const el = document.querySelector(`[${a}]`);
if (el) return { sitekey: el.getAttribute(a), source: 'frame-dom', attr: a };
}
for (const s of Array.from(document.scripts)) {
const t = s.textContent || '';
if (!t) continue;
const m = t.match(/sitekey['"]?\s*[:=]\s*['"]([\w\-]{8,})['"]/i) || t.match(/k=([A-Za-z0-9_-]{8,})/i);
if (m) return { sitekey: m[1], source: 'frame-script' };
}
// 也检查 meta 等可见位置(可扩展)
return null;
});
if (res && res.sitekey) return { ...res, frameUrl: f.url() };
} catch (err) {
// 跨域 frame:不能 evaluate,改为解析 frame.url(常见 sitekey 在 query中)
try {
const fu = f.url();
if (fu && (fu.includes('turnstile') || fu.includes('hcaptcha') || fu.includes('challenges.cloudflare.com') || fu.includes('hcaptcha.com'))) {
const u = new URL(fu);
const qp = u.searchParams;
const possible = qp.get('sitekey') || qp.get('k') || qp.get('s') || qp.get('key');
if (possible) return { sitekey: possible, source: 'frame-url', frameUrl: fu };
}
} catch(e){}
}
}
// 3) 如果 Cloudflare 仍在“Checking your browser”,继续等
const checking = await page.$('text="Checking your browser"') || await page.$('text=Checking') || null;
if (checking) {
// 仅作日志,继续等待
// console.log('Cloudflare still checking your browser...');
}
await sleep(pollInterval);
}
return null; // 超时
}
async function create2captchaTurnstileTask(apiKey, websiteURL, websiteKey) {
const createRes = await fetch('https://api.2captcha.com/createTask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientKey: apiKey,
task: {
type: 'TurnstileTaskProxyless',
websiteURL,
websiteKey
}
})
});
const json = await createRes.json();
if (json.errorId && json.errorId !== 0) throw new Error('createTask error: ' + JSON.stringify(json));
return json.taskId;
}
async function get2captchaResult(apiKey, taskId, { timeout = 120000, pollInterval = 5000 } = {}) {
const start = Date.now();
while (Date.now() - start < timeout) {
await sleep(pollInterval);
const res = await fetch('https://api.2captcha.com/getTaskResult', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clientKey: apiKey, taskId })
});
const json = await res.json();
if (json.errorId && json.errorId !== 0) throw new Error('getTaskResult error: ' + JSON.stringify(json));
if (json.status === 'ready' && json.solution && json.solution.token) return json.solution.token;
// else keep polling
}
throw new Error('2Captcha getTaskResult timeout');
}
async function injectTurnstileToken(page, token) {
// 多种注入方式以提高兼容性
await page.evaluate((t) => {
// 常见隐藏字段
const selectors = [
'textarea[name="cf-turnstile-response"]',
'input[name="cf-turnstile-response"]',
'textarea[name="cf_captcha_token"]',
'input[name="cf_captcha_token"]'
];
let injected = false;
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) {
el.value = t;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
injected = true;
}
}
// 如果没有直接字段,寻找任意 textarea 并设置(降级方案)
if (!injected) {
const ta = document.querySelector('textarea');
if (ta) {
ta.value = t;
ta.dispatchEvent(new Event('input', { bubbles: true }));
injected = true;
}
}
// 调用可能挂载的回调(site 端可能传 callback 名称)
try {
if (window.turnstile && typeof window.turnstile.renderResponse === 'function') {
window.turnstile.renderResponse(t);
}
if (window.tsCallback && typeof window.tsCallback === 'function') {
window.tsCallback(t);
}
// 查找 data-callback 属性并触发
document.querySelectorAll('[data-sitekey]').forEach(el => {
const cb = el.getAttribute('data-callback');
if (cb && window[cb] && typeof window[cb] === 'function') {
try { window[cb](t); } catch(e) {}
}
});
} catch(e){}
}, token);
}
async function solveTurnstile(page, apiKey=API_KEY) {
const found = await waitForSiteKey(page, { timeout: 90000, pollInterval: 700 });
if (!found) {
console.warn('⚠️ 超时未检测到 Turnstile sitekey,建议开启调试快照(screenshot / html)进行排查');
// 调试输出(保存快照/HTML
try {
await page.screenshot({ path: 'turnstile-debug.png', fullPage: true });
const html = await page.content();
require('fs').writeFileSync('turnstile-debug.html', html);
console.log('已保存 turnstile-debug.png 和 turnstile-debug.html 用于排查(当前目录)');
} catch(e) { console.error('保存调试文件失败', e); }
return false;
}
console.log('找到 sitekey ->', found);
// 调用 2Captcha
const pageUrl = page.url();
const taskId = await create2captchaTurnstileTask(apiKey, pageUrl, found.sitekey);
console.log('2Captcha createTask 返回 taskId:', taskId);
const token = await get2captchaResult(apiKey, taskId, { timeout: 180000, pollInterval: 5000 });
console.log('2Captcha 返回 token (长度):', token?.length);
await injectTurnstileToken(page, token);
console.log('token 注入完成,等待站点验证或跳转');
// 站点通常会在 token 注入后提交表单或自动验证,给点时间
await page.waitForTimeout(2000);
return true;
}
const test = async () => {
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'
];
chromium.use(StealthPlugin())
// const browser = await this.getBrowserType().launch({
// args,
// headless: yn(process.env.BROWSER_HEADLESS, { default: true }),
// ...(process.env.PROXY_URL &&{ proxy: {
// server: process.env.PROXY_URL,
// }})
// })
const userAgent = new UserAgent(/Macintosh/).random().toString();
const browser = await chromium.launch({
args,
headless:false,
proxy: {
server: 'http://127.0.0.1:12334',
},
})
const context = await browser.newContext({ userAgent, locale:'en', viewport: { width: 1920, height: 1080 } });
const cookiesStr = '_gcl_au=1.1.1122395686.1756802685; _ga=GA1.1.785768328.1756802695; _axwrt=e01097fa-f771-4c63-a49d-78538080b57a; singular_device_id=822605c1-fd96-4312-8e7f-b27653bdcbaf; ajs_anonymous_id=8240937f-6930-4d54-ae11-830db6ef8262; _tt_enable_cookie=1; _ttp=01K44SN90TAWV4SBPRAYF3SEN1_.tt.1; _fbp=fb.1.1756802753737.293820645972563697; afUserId=ef07b62f-6c47-44f3-b92a-269f61235b6f-p; AF_SYNC=1756802770762; _clck=fqr409%5E2%5Efz4%5E0%5E2071; __client=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNsaWVudF8zMkp6djBjV1U1MGozTW1rdHAzUHdnZ1JJbVYiLCJyb3RhdGluZ190b2tlbiI6Im1iZWd1bHc0aHJ2aDA3Z3praGNmaXA0M2o1enVkMDgxYzczNjhwMW0ifQ.GfLgVl0x0OC7yA7HxSneUpsKnvcMvNSqBonmNuxwURAPhBjJx8MoGy5sojC9Kx8zmO36kaahB4uTDiOj-9VTRZaob01iSk7lzciCjW_iEM5gDZfTeQbYW62JmXk8ymr84twVBNy1xmYAQQKb9TTEnCZHbGjgf3yfPnJOndEgRLeMqTLmGTCz0Vi-1OBx-zOSxS0CwPmPUlmRDhTzrw76x9puzh9PQeP3mbVF8uJe4ZvCA-BSWLjURt8VzcpLW0BYWO_yGjuk-kI8dJ6eUmDOfjCXSsh2SLqqwnE_bWirBHM4ce7JLc09iok6LUsDI2O0g2RQxQW0HwfCtuQxtDV7Rg; __client_uat=1757235449; __client_uat_U9tcbTPE=1757235449; __stripe_mid=f1915dd0-feed-4d1e-8402-e52760e3237b4face4; __cf_bm=Wxfi2Ww131iNPv2i27d32fnSJ2ee7aGUtY3Nz7nkpLk-1757392628-1.0.1.1-hwovJvwM_F.ejTX6HkfZd4F5PSugqHgOtI7X56t3p.FS1sUCrUOuGg4YZmwjNBZ4RCZA_HT7j6AAAnF55rF7K9r9T9pVz5FHDy..P6JSBko; _cfuvid=awR2fvj9zdJbloFbbUfGfhWFS51WZtQu_.7_uBwLKJA-1757392628089-0.0.1.1-604800000; __stripe_sid=c848008e-a8a1-423c-9c31-0b74682bd6a3e48eaf; _ga_7B0KEDD7XP=GS2.1.s1757392637$o11$g1$t1757392720$j46$l0$h0; _uetsid=ac2191508d3611f0afd391282bdea7cd|1phqefk|2|fz6|0|2078; ax_visitor=%7B%22firstVisitTs%22%3A1756802706366%2C%22lastVisitTs%22%3A1757317725011%2C%22currentVisitStartTs%22%3A1757392645129%2C%22ts%22%3A1757392720941%2C%22visitCount%22%3A9%7D; ttcsid=1757392640440::ZYVwpHKeEnpgc8y37liU.10.1757392723928; ttcsid_CT67HURC77UB52N3JFBG=1757392640440::rKUF-WAAJUhQ9nYMbwOe.10.1757392724155; _uetvid=1ecc230087d911f08b445db6fd2178ad|yluce0|1757392724319|4|1|bat.bing.com/p/conversions/c/a'
const cookies = parseCookies(cookiesStr, '.suno.com');
// cookies.push({
// name: '__session',
// value: this.currentToken+'',
// domain: '.suno.com',
// path: '/',
// httpOnly: true,
// secure: true,
// sameSite: 'Lax',
// });
// await context.addCookies(cookies);
const page = await browser.newPage();
console.log('Testing the stealth plugin..')
await page.goto('https://www.suno.com/create', { waitUntil: 'networkidle' })
await solveTurnstile(page);
// const frame = page
// .frames()
// .find((f) => f.url().includes("hcaptcha.com") || f.url().includes("challenges.cloudflare.com"));
//
// if (frame) {
// console.log("检测到 Cloudflare 验证组件,开始调用 2Captcha...");
// }
// 点击 Google 登录按钮
const googleBtn = await page.waitForSelector("button.cl-button__google");
await googleBtn.click();
// 等待跳转到 Google 登录页
await page.waitForURL(/accounts\.google\.com/);
// 输入邮箱
await page.fill('input[type="email"]', 'easyai202502@gmail.com');
await page.click('#identifierNext');
// 等待密码输入框
await page.waitForSelector('input[type="password"]', { timeout: 15000 });
await page.fill('input[type="password"]', 'easyai@2025');
await page.click('#passwordNext');
// 登录成功后会跳转回 suno.com
await page.waitForURL(/suno\.com/, { timeout: 60000 });
// const frame = page.mainFrame();
// const captchaEl = await frame.$("iframe[src*='hcaptcha.com'], iframe[src*='challenges.cloudflare.com']");
console.log('All done, check the screenshot. ✨')
};
test()