Compare commits
	
		
			No commits in common. "42cb19214f7217260060996ccf3dae2c1431e3db" and "9141a226b5f4fb53691ef4bca5756e893c7cab4c" have entirely different histories.
		
	
	
		
			42cb19214f
			...
			9141a226b5
		
	
		
@ -1,7 +1,7 @@
 | 
				
			|||||||
# For more information, please see the README.md
 | 
					# For more information, please see the README.md
 | 
				
			||||||
SUNO_COOKIE=
 | 
					SUNO_COOKIE=
 | 
				
			||||||
TWOCAPTCHA_KEY= # Obtain from 2captcha.com
 | 
					TWOCAPTCHA_KEY= # Obtain from 2captcha.com
 | 
				
			||||||
BROWSER=chromium # `chromium` or `firefox`, although `chromium` is highly recommended
 | 
					BROWSER=chromium # chromium or firefox
 | 
				
			||||||
BROWSER_GHOST_CURSOR=false
 | 
					BROWSER_GHOST_CURSOR=false
 | 
				
			||||||
BROWSER_LOCALE=en
 | 
					BROWSER_LOCALE=en
 | 
				
			||||||
BROWSER_HEADLESS=true
 | 
					BROWSER_HEADLESS=true
 | 
				
			||||||
							
								
								
									
										22
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								Dockerfile
									
									
									
									
									
								
							@ -1,31 +1,25 @@
 | 
				
			|||||||
 | 
					# syntax=docker/dockerfile:1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FROM node:lts-bookworm AS builder
 | 
					FROM node:lts-alpine AS builder
 | 
				
			||||||
WORKDIR /src
 | 
					WORKDIR /src
 | 
				
			||||||
COPY package*.json ./
 | 
					COPY package*.json ./
 | 
				
			||||||
RUN npm install
 | 
					RUN npm install
 | 
				
			||||||
COPY . .
 | 
					COPY . .
 | 
				
			||||||
RUN npm run build
 | 
					RUN npm run build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FROM node:lts-bookworm
 | 
					FROM node:lts-alpine
 | 
				
			||||||
WORKDIR /app
 | 
					WORKDIR /app
 | 
				
			||||||
COPY package*.json ./
 | 
					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
 | 
					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                                           
 | 
					ARG BROWSER
 | 
				
			||||||
 | 
					RUN if [ -z "$SUNO_COOKIE" ]; then echo "Warning: SUNO_COOKIE is not set"; fi
 | 
				
			||||||
ENV SUNO_COOKIE=${SUNO_COOKIE}
 | 
					ENV SUNO_COOKIE=${SUNO_COOKIE}
 | 
				
			||||||
# Disable GPU acceleration, as with it suno-api won't work in a Docker environment
 | 
					RUN if [ -z "$BROWSER" ]; then echo "Warning: BROWSER is not set; will use chromium by default"; fi
 | 
				
			||||||
ENV BROWSER_DISABLE_GPU=true
 | 
					ENV BROWSER=${BROWSER:-chromium}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN npm install --only=production
 | 
					RUN npm install --only=production
 | 
				
			||||||
                                                                                                                    
 | 
					RUN npx playwright install $BROWSER
 | 
				
			||||||
# 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
 | 
					COPY --from=builder /src/.next ./.next
 | 
				
			||||||
EXPOSE 3000
 | 
					EXPOSE 3000
 | 
				
			||||||
CMD ["npm", "run", "start"]
 | 
					CMD ["npm", "run", "start"]
 | 
				
			||||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							@ -1,5 +1,5 @@
 | 
				
			|||||||
<div align="center">
 | 
					<div align="center">
 | 
				
			||||||
  <h1 align="center">
 | 
					  <h1 align="center"">
 | 
				
			||||||
      Suno AI API
 | 
					      Suno AI API
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
  <p>Use API to call the music generation AI of Suno.ai and easily integrate it into agents like GPTs.</p>
 | 
					  <p>Use API to call the music generation AI of Suno.ai and easily integrate it into agents like GPTs.</p>
 | 
				
			||||||
@ -64,11 +64,7 @@ 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).
 | 
					[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.
 | 
				
			||||||
> 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
 | 
					### 3. Clone and deploy this project
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -84,11 +80,9 @@ You can choose your preferred deployment method:
 | 
				
			|||||||
git clone https://github.com/gcui-art/suno-api.git
 | 
					git clone https://github.com/gcui-art/suno-api.git
 | 
				
			||||||
cd suno-api
 | 
					cd suno-api
 | 
				
			||||||
npm install
 | 
					npm install
 | 
				
			||||||
 | 
					npx playwright install chromium
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
#### Docker
 | 
					#### 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.
 | 
					Alternatively, you can use [Docker Compose](https://docs.docker.com/compose/). However, follow the step below before running.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
 | 
				
			|||||||
@ -64,11 +64,7 @@ Suno — потрясающий сервис для ИИ-музыки. Несм
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[Создайте](https://2captcha.com/ru/auth/register?userType=customer) новый аккаунт, [пополните](https://2captcha.com/ru/pay) баланс и [получите свой API-ключ](https://2captcha.com/ru/enterpage#recognition).
 | 
					[Создайте](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. Это абсолютно тот же сервис, но данный интерфейс поддерживает платежи из этих стран.
 | 
				
			||||||
> Если вы находитесь в России или Беларуси, используйте интерфейс [ruCaptcha](https://rucaptcha.com) вместо 2Captcha. Это абсолютно тот же сервис, но данный интерфейс поддерживает платежи из этих стран.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
> [!TIP]
 | 
					 | 
				
			||||||
> Если вы хотите получать как можно меньше капч, рекомендуется использовать macOS. Системы на macOS обычно получают меньше капч, чем Linux и Windows — это связано с их непопулярностью в сфере веб-скрейпинга. Запуск suno-api на Windows и Linux будет работать, но в некоторых случаях вы можете получить довольно большое количество капч.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 3. Скачайте и запустите проект
 | 
					### 3. Скачайте и запустите проект
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -84,10 +80,9 @@ Suno — потрясающий сервис для ИИ-музыки. Несм
 | 
				
			|||||||
git clone https://github.com/gcui-art/suno-api.git
 | 
					git clone https://github.com/gcui-art/suno-api.git
 | 
				
			||||||
cd suno-api
 | 
					cd suno-api
 | 
				
			||||||
npm install
 | 
					npm install
 | 
				
			||||||
 | 
					npx playwright install chromium
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
#### Docker
 | 
					#### Docker
 | 
				
			||||||
>[!IMPORTANT]
 | 
					 | 
				
			||||||
> Аппаратное видеоускорение браузера будет отключено в Docker. Если у вас медленный процессор, рекомендуется [развернуть локально](#локально).
 | 
					 | 
				
			||||||
Также можно использовать [Docker Compose](https://docs.docker.com/compose/), однако перед запуском выполните шаг ниже.
 | 
					Также можно использовать [Docker Compose](https://docs.docker.com/compose/), однако перед запуском выполните шаг ниже.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
 | 
				
			|||||||
@ -2,13 +2,11 @@ version: '3'
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
services:
 | 
					services:
 | 
				
			||||||
  suno-api:
 | 
					  suno-api:
 | 
				
			||||||
    image: registry.cn-shanghai.aliyuncs.com/easyaigc/suno-api:latest
 | 
					    build:
 | 
				
			||||||
#    build:
 | 
					      context: .
 | 
				
			||||||
#      context: .
 | 
					      args:
 | 
				
			||||||
#      args:
 | 
					        SUNO_COOKIE: ${SUNO_COOKIE}
 | 
				
			||||||
#        SUNO_COOKIE: ${SUNO_COOKIE}
 | 
					 | 
				
			||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - ./public:/app/public
 | 
					      - ./public:/app/public
 | 
				
			||||||
    ports:
 | 
					    ports:
 | 
				
			||||||
      - "3013:3000"
 | 
					      - "3000:3000"
 | 
				
			||||||
    env_file: ".env"
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										86
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										86
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -10,7 +10,6 @@
 | 
				
			|||||||
      "license": "LGPL-3.0-or-later",
 | 
					      "license": "LGPL-3.0-or-later",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@2captcha/captcha-solver": "^1.3.0",
 | 
					        "@2captcha/captcha-solver": "^1.3.0",
 | 
				
			||||||
        "@playwright/browser-chromium": "^1.49.1",
 | 
					 | 
				
			||||||
        "@vercel/analytics": "^1.2.2",
 | 
					        "@vercel/analytics": "^1.2.2",
 | 
				
			||||||
        "axios": "^1.7.8",
 | 
					        "axios": "^1.7.8",
 | 
				
			||||||
        "bufferutil": "^4.0.8",
 | 
					        "bufferutil": "^4.0.8",
 | 
				
			||||||
@ -18,17 +17,16 @@
 | 
				
			|||||||
        "cookie": "^1.0.2",
 | 
					        "cookie": "^1.0.2",
 | 
				
			||||||
        "electron": "^33.2.1",
 | 
					        "electron": "^33.2.1",
 | 
				
			||||||
        "ghost-cursor-playwright": "^2.1.0",
 | 
					        "ghost-cursor-playwright": "^2.1.0",
 | 
				
			||||||
        "https-proxy-agent": "^7.0.6",
 | 
					 | 
				
			||||||
        "js-cookie": "^3.0.5",
 | 
					        "js-cookie": "^3.0.5",
 | 
				
			||||||
        "next": "14.1.4",
 | 
					        "next": "14.1.4",
 | 
				
			||||||
        "next-swagger-doc": "^0.4.0",
 | 
					        "next-swagger-doc": "^0.4.0",
 | 
				
			||||||
        "pino": "^8.19.0",
 | 
					        "pino": "^8.19.0",
 | 
				
			||||||
        "pino-pretty": "^11.0.0",
 | 
					        "pino-pretty": "^11.0.0",
 | 
				
			||||||
 | 
					        "playwright-core": "^1.49.1",
 | 
				
			||||||
        "react": "^18",
 | 
					        "react": "^18",
 | 
				
			||||||
        "react-dom": "^18",
 | 
					        "react-dom": "^18",
 | 
				
			||||||
        "react-markdown": "^9.0.1",
 | 
					        "react-markdown": "^9.0.1",
 | 
				
			||||||
        "rebrowser-playwright-core": "^1.49.1",
 | 
					        "rebrowser-playwright-core": "^1.49.1",
 | 
				
			||||||
        "socks-proxy-agent": "^8.0.5",
 | 
					 | 
				
			||||||
        "swagger-ui-react": "^5.18.2",
 | 
					        "swagger-ui-react": "^5.18.2",
 | 
				
			||||||
        "tough-cookie": "^4.1.4",
 | 
					        "tough-cookie": "^4.1.4",
 | 
				
			||||||
        "user-agents": "^1.1.156",
 | 
					        "user-agents": "^1.1.156",
 | 
				
			||||||
@ -605,19 +603,6 @@
 | 
				
			|||||||
        "node": ">=14"
 | 
					        "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": {
 | 
					    "node_modules/@rtsao/scc": {
 | 
				
			||||||
      "version": "1.1.0",
 | 
					      "version": "1.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
 | 
				
			||||||
@ -1602,15 +1587,6 @@
 | 
				
			|||||||
        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
 | 
					        "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": {
 | 
					    "node_modules/ajv": {
 | 
				
			||||||
      "version": "6.12.6",
 | 
					      "version": "6.12.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
 | 
				
			||||||
@ -4620,19 +4596,6 @@
 | 
				
			|||||||
        "node": ">=10.19.0"
 | 
					        "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": {
 | 
					    "node_modules/ieee754": {
 | 
				
			||||||
      "version": "1.2.1",
 | 
					      "version": "1.2.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
 | 
				
			||||||
@ -4753,15 +4716,6 @@
 | 
				
			|||||||
        "loose-envify": "^1.0.0"
 | 
					        "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": {
 | 
					    "node_modules/is-alphabetical": {
 | 
				
			||||||
      "version": "2.0.1",
 | 
					      "version": "2.0.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
 | 
				
			||||||
@ -8435,44 +8389,6 @@
 | 
				
			|||||||
        "node": ">=8"
 | 
					        "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": {
 | 
					    "node_modules/sonic-boom": {
 | 
				
			||||||
      "version": "3.8.1",
 | 
					      "version": "3.8.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -9,14 +9,13 @@
 | 
				
			|||||||
  "version": "1.1.0",
 | 
					  "version": "1.1.0",
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "dev": "PORT=3013 next dev",
 | 
					    "dev": "next dev",
 | 
				
			||||||
    "build": "next build",
 | 
					    "build": "next build",
 | 
				
			||||||
    "start": "next start",
 | 
					    "start": "next start",
 | 
				
			||||||
    "lint": "next lint"
 | 
					    "lint": "next lint"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@2captcha/captcha-solver": "^1.3.0",
 | 
					    "@2captcha/captcha-solver": "^1.3.0",
 | 
				
			||||||
    "@playwright/browser-chromium": "^1.49.1",
 | 
					 | 
				
			||||||
    "@vercel/analytics": "^1.2.2",
 | 
					    "@vercel/analytics": "^1.2.2",
 | 
				
			||||||
    "axios": "^1.7.8",
 | 
					    "axios": "^1.7.8",
 | 
				
			||||||
    "bufferutil": "^4.0.8",
 | 
					    "bufferutil": "^4.0.8",
 | 
				
			||||||
@ -24,7 +23,6 @@
 | 
				
			|||||||
    "cookie": "^1.0.2",
 | 
					    "cookie": "^1.0.2",
 | 
				
			||||||
    "electron": "^33.2.1",
 | 
					    "electron": "^33.2.1",
 | 
				
			||||||
    "ghost-cursor-playwright": "^2.1.0",
 | 
					    "ghost-cursor-playwright": "^2.1.0",
 | 
				
			||||||
    "https-proxy-agent": "^7.0.6",
 | 
					 | 
				
			||||||
    "js-cookie": "^3.0.5",
 | 
					    "js-cookie": "^3.0.5",
 | 
				
			||||||
    "next": "14.1.4",
 | 
					    "next": "14.1.4",
 | 
				
			||||||
    "next-swagger-doc": "^0.4.0",
 | 
					    "next-swagger-doc": "^0.4.0",
 | 
				
			||||||
@ -34,7 +32,6 @@
 | 
				
			|||||||
    "react-dom": "^18",
 | 
					    "react-dom": "^18",
 | 
				
			||||||
    "react-markdown": "^9.0.1",
 | 
					    "react-markdown": "^9.0.1",
 | 
				
			||||||
    "rebrowser-playwright-core": "^1.49.1",
 | 
					    "rebrowser-playwright-core": "^1.49.1",
 | 
				
			||||||
    "socks-proxy-agent": "^8.0.5",
 | 
					 | 
				
			||||||
    "swagger-ui-react": "^5.18.2",
 | 
					    "swagger-ui-react": "^5.18.2",
 | 
				
			||||||
    "tough-cookie": "^4.1.4",
 | 
					    "tough-cookie": "^4.1.4",
 | 
				
			||||||
    "user-agents": "^1.1.156",
 | 
					    "user-agents": "^1.1.156",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5345
									
								
								pnpm-lock.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5345
									
								
								pnpm-lock.yaml
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,61 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -33,7 +33,6 @@ export default function Docs() {
 | 
				
			|||||||
- \`/api/get_aligned_lyrics\`: Get list of timestamps for each word in the lyrics
 | 
					- \`/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/clip\`:  Get clip information based on ID passed as query parameter \`id\`
 | 
				
			||||||
- \`/api/concat\`: Generate the whole song from extensions
 | 
					- \`/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.
 | 
					Feel free to explore the detailed API parameters and conduct tests on this page.
 | 
				
			||||||
 | 
				
			|||||||
@ -588,149 +588,6 @@
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "/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": {
 | 
					  "components": {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,16 +2,15 @@ import axios, { AxiosInstance } from 'axios';
 | 
				
			|||||||
import UserAgent from 'user-agents';
 | 
					import UserAgent from 'user-agents';
 | 
				
			||||||
import pino from 'pino';
 | 
					import pino from 'pino';
 | 
				
			||||||
import yn from 'yn';
 | 
					import yn from 'yn';
 | 
				
			||||||
import { isPage, sleep, waitForRequests } from '@/lib/utils';
 | 
					import { sleep, isPage } from '@/lib/utils';
 | 
				
			||||||
import * as cookie from 'cookie';
 | 
					import * as cookie from 'cookie';
 | 
				
			||||||
import { randomUUID } from 'node:crypto';
 | 
					import { randomUUID } from 'node:crypto';
 | 
				
			||||||
import { Solver } from '@2captcha/captcha-solver';
 | 
					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 { BrowserContext, Page, Locator, chromium, firefox } from 'rebrowser-playwright-core';
 | 
				
			||||||
import { createCursor, Cursor } from 'ghost-cursor-playwright';
 | 
					import { createCursor, Cursor } from 'ghost-cursor-playwright';
 | 
				
			||||||
 | 
					import { paramsCoordinates } from '@2captcha/captcha-solver/dist/structs/2captcha';
 | 
				
			||||||
import { promises as fs } from 'fs';
 | 
					import { promises as fs } from 'fs';
 | 
				
			||||||
import path from 'node:path';
 | 
					import path from 'node:path';
 | 
				
			||||||
import { SocksProxyAgent } from 'socks-proxy-agent';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// sunoApi instance caching
 | 
					// sunoApi instance caching
 | 
				
			||||||
const globalForSunoApi = global as unknown as { sunoApiCache?: Map<string, SunoApi> };
 | 
					const globalForSunoApi = global as unknown as { sunoApiCache?: Map<string, SunoApi> };
 | 
				
			||||||
@ -40,34 +39,6 @@ export interface AudioInfo {
 | 
				
			|||||||
  error_message?: string; // Error message if any
 | 
					  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 {
 | 
					class SunoApi {
 | 
				
			||||||
  private static BASE_URL: string = 'https://studio-api.prod.suno.com';
 | 
					  private static BASE_URL: string = 'https://studio-api.prod.suno.com';
 | 
				
			||||||
  private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
 | 
					  private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
 | 
				
			||||||
@ -87,29 +58,6 @@ class SunoApi {
 | 
				
			|||||||
    this.userAgent = new UserAgent(/Macintosh/).random().toString(); // Usually Mac systems get less amount of CAPTCHAs
 | 
					    this.userAgent = new UserAgent(/Macintosh/).random().toString(); // Usually Mac systems get less amount of CAPTCHAs
 | 
				
			||||||
    this.cookies = cookie.parse(cookies);
 | 
					    this.cookies = cookie.parse(cookies);
 | 
				
			||||||
    this.deviceId = this.cookies.ajs_anonymous_id || randomUUID();
 | 
					    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({
 | 
					    this.client = axios.create({
 | 
				
			||||||
      withCredentials: true,
 | 
					      withCredentials: true,
 | 
				
			||||||
      headers: {
 | 
					      headers: {
 | 
				
			||||||
@ -121,8 +69,7 @@ class SunoApi {
 | 
				
			|||||||
        'sec-ch-ua-mobile': '?1',
 | 
					        'sec-ch-ua-mobile': '?1',
 | 
				
			||||||
        'sec-ch-ua-platform': '"Android"',
 | 
					        'sec-ch-ua-platform': '"Android"',
 | 
				
			||||||
        'User-Agent': this.userAgent
 | 
					        'User-Agent': this.userAgent
 | 
				
			||||||
      },
 | 
					      }
 | 
				
			||||||
      ...extraConfig,
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    this.client.interceptors.request.use(config => {
 | 
					    this.client.interceptors.request.use(config => {
 | 
				
			||||||
      if (this.currentToken && !config.headers.Authorization)
 | 
					      if (this.currentToken && !config.headers.Authorization)
 | 
				
			||||||
@ -233,6 +180,7 @@ class SunoApi {
 | 
				
			|||||||
      ctype: 'generation'
 | 
					      ctype: 'generation'
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    logger.info(resp.data);
 | 
					    logger.info(resp.data);
 | 
				
			||||||
 | 
					    // await sleep(10);
 | 
				
			||||||
    return resp.data.required;
 | 
					    return resp.data.required;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -283,7 +231,8 @@ class SunoApi {
 | 
				
			|||||||
   * @returns {BrowserContext}
 | 
					   * @returns {BrowserContext}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private async launchBrowser(): Promise<BrowserContext> {
 | 
					  private async launchBrowser(): Promise<BrowserContext> {
 | 
				
			||||||
    const args = [
 | 
					    const browser = await this.getBrowserType().launch({
 | 
				
			||||||
 | 
					      args: [
 | 
				
			||||||
        '--disable-blink-features=AutomationControlled',
 | 
					        '--disable-blink-features=AutomationControlled',
 | 
				
			||||||
        '--disable-web-security',
 | 
					        '--disable-web-security',
 | 
				
			||||||
        '--no-sandbox',
 | 
					        '--no-sandbox',
 | 
				
			||||||
@ -292,20 +241,9 @@ class SunoApi {
 | 
				
			|||||||
        '--disable-features=IsolateOrigins',
 | 
					        '--disable-features=IsolateOrigins',
 | 
				
			||||||
        '--disable-extensions',
 | 
					        '--disable-extensions',
 | 
				
			||||||
        '--disable-infobars'
 | 
					        '--disable-infobars'
 | 
				
			||||||
    ];
 | 
					      ],
 | 
				
			||||||
    // Check for GPU acceleration, as it is recommended to turn it off for Docker
 | 
					      headless: yn(process.env.BROWSER_HEADLESS, { default: true })
 | 
				
			||||||
    if (yn(process.env.BROWSER_DISABLE_GPU, { default: false }))
 | 
					    });
 | 
				
			||||||
      args.push('--enable-unsafe-swiftshader',
 | 
					 | 
				
			||||||
        '--disable-gpu',
 | 
					 | 
				
			||||||
        '--disable-setuid-sandbox');
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      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 context = await browser.newContext({ userAgent: this.userAgent, locale: process.env.BROWSER_LOCALE, viewport: null });
 | 
					    const context = await browser.newContext({ userAgent: this.userAgent, locale: process.env.BROWSER_LOCALE, viewport: null });
 | 
				
			||||||
    const cookies = [];
 | 
					    const cookies = [];
 | 
				
			||||||
    const lax: 'Lax' | 'Strict' | 'None' = 'Lax';
 | 
					    const lax: 'Lax' | 'Strict' | 'None' = 'Lax';
 | 
				
			||||||
@ -327,10 +265,6 @@ class SunoApi {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    await context.addCookies(cookies);
 | 
					    await context.addCookies(cookies);
 | 
				
			||||||
    return context;
 | 
					    return context;
 | 
				
			||||||
    }catch ( e){
 | 
					 | 
				
			||||||
      console.log(e);
 | 
					 | 
				
			||||||
      throw e;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -348,45 +282,46 @@ class SunoApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    logger.info('Waiting for Suno interface to load');
 | 
					    logger.info('Waiting for Suno interface to load');
 | 
				
			||||||
    //await page.locator('.react-aria-GridList').waitFor({ timeout: 60000 });
 | 
					    //await page.locator('.react-aria-GridList').waitFor({ timeout: 60000 });
 | 
				
			||||||
    await page.waitForResponse('**/api/project/**\\?**', { timeout: 60000 }); // wait for song list API call
 | 
					    await page.waitForResponse('**/api/feed/v2**', { timeout: 60000 }); // wait for song list API call
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.ghostCursorEnabled)
 | 
					    if (this.ghostCursorEnabled)
 | 
				
			||||||
      this.cursor = await createCursor(page);
 | 
					      this.cursor = await createCursor(page);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    logger.info('Triggering the CAPTCHA');
 | 
					    logger.info('Triggering the CAPTCHA');
 | 
				
			||||||
    try {
 | 
					    await this.click(page, { x: 318, y: 13 }); // close all popups
 | 
				
			||||||
      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');
 | 
					    const textarea = page.locator('.custom-textarea');
 | 
				
			||||||
    await this.click(textarea);
 | 
					    await this.click(textarea);
 | 
				
			||||||
    await textarea.pressSequentially('Lorem ipsum', { delay: 80 });
 | 
					    await textarea.pressSequentially('Lorem ipsum', { delay: 80 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const button = page.locator('button[aria-label="Create"]').locator('div.flex');
 | 
					    const button = page.locator('button[aria-label="Create"]').locator('div.flex');
 | 
				
			||||||
    this.click(button);
 | 
					    await this.click(button);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const controller = new AbortController();
 | 
					 | 
				
			||||||
    new Promise<void>(async (resolve, reject) => {
 | 
					    new Promise<void>(async (resolve, reject) => {
 | 
				
			||||||
      const frame = page.frameLocator('iframe[title*="hCaptcha"]');
 | 
					      const frame = page.frameLocator('iframe[title*="hCaptcha"]');
 | 
				
			||||||
      const challenge = frame.locator('.challenge-container');
 | 
					      const challenge = frame.locator('.challenge-container');
 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        let wait = true;
 | 
					 | 
				
			||||||
      while (true) {
 | 
					      while (true) {
 | 
				
			||||||
          if (wait)
 | 
					        try {
 | 
				
			||||||
            await waitForRequests(page, controller.signal);
 | 
					          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
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          const drag = (await challenge.locator('.prompt-text').first().innerText()).toLowerCase().includes('drag');
 | 
					          const drag = (await challenge.locator('.prompt-text').first().innerText()).toLowerCase().includes('drag');
 | 
				
			||||||
          let captcha: any;
 | 
					          let captcha: any;
 | 
				
			||||||
          for (let j = 0; j < 3; j++) { // try several times because sometimes 2Captcha could return an error
 | 
					          for (let j = 0; j < 3; j++) { // try several times because sometimes 2Captcha could send an error
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
              logger.info('Sending the CAPTCHA to 2Captcha');
 | 
					              logger.info('Sending the CAPTCHA to 2Captcha');
 | 
				
			||||||
              const payload: paramsCoordinates = {
 | 
					              const payload: paramsCoordinates = {
 | 
				
			||||||
                body: (await challenge.screenshot({ timeout: 5000 })).toString('base64'),
 | 
					                body: (await challenge.screenshot()).toString('base64'),
 | 
				
			||||||
                lang: process.env.BROWSER_LOCALE
 | 
					                lang: process.env.BROWSER_LOCALE
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
              if (drag) {
 | 
					              if (drag) {
 | 
				
			||||||
                // Say to the worker that he needs to click
 | 
					                // 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.textinstructions = '! Instead of dragging, CLICK on the shapes as shown in the image above !';
 | 
				
			||||||
                payload.imginstructions = (await fs.readFile(path.join(process.cwd(), 'public', 'drag-instructions.jpg'))).toString('base64');
 | 
					                payload.imginstructions = (await fs.readFile(path.join(process.cwd(), 'public', 'drag-instructions.jpg'))).toString('base64');
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              captcha = await this.solver.coordinates(payload);
 | 
					              captcha = await this.solver.coordinates(payload);
 | 
				
			||||||
@ -403,12 +338,6 @@ class SunoApi {
 | 
				
			|||||||
            const challengeBox = await challenge.boundingBox();
 | 
					            const challengeBox = await challenge.boundingBox();
 | 
				
			||||||
            if (challengeBox == null)
 | 
					            if (challengeBox == null)
 | 
				
			||||||
              throw new Error('.challenge-container boundingBox is 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) {
 | 
					            for (let i = 0; i < captcha.data.length; i += 2) {
 | 
				
			||||||
              const data1 = captcha.data[i];
 | 
					              const data1 = captcha.data[i];
 | 
				
			||||||
              const data2 = captcha.data[i+1];
 | 
					              const data2 = captcha.data[i+1];
 | 
				
			||||||
@ -419,27 +348,22 @@ class SunoApi {
 | 
				
			|||||||
              await page.mouse.move(challengeBox.x + +data2.x, challengeBox.y + +data2.y, { steps: 30 });
 | 
					              await page.mouse.move(challengeBox.x + +data2.x, challengeBox.y + +data2.y, { steps: 30 });
 | 
				
			||||||
              await page.mouse.up();
 | 
					              await page.mouse.up();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            wait = true;
 | 
					 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            for (const data of captcha.data) {
 | 
					            for (const data of captcha.data) {
 | 
				
			||||||
              logger.info(data);
 | 
					              logger.info(data);
 | 
				
			||||||
              await this.click(challenge, { x: +data.x, y: +data.y });
 | 
					              await this.click(challenge, { x: +data.x, y: +data.y });
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          this.click(frame.locator('.button-submit')).catch(e => {
 | 
					          /*await*/ this.click(frame.locator('.button-submit')); // await is commented because we need to call waitForResponse at the same time
 | 
				
			||||||
            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) {
 | 
					        } catch(e: any) {
 | 
				
			||||||
        if (e.message.includes('been closed') // catch error when closing the browser
 | 
					          if (e.message.includes('viewport') || e.message.includes('timeout')) // when hCaptcha window has been closed due to inactivity,
 | 
				
			||||||
          || e.message == 'AbortError') // catch error when waitForRequests is aborted
 | 
					            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();
 | 
					            resolve();
 | 
				
			||||||
          else
 | 
					          else
 | 
				
			||||||
            reject(e);
 | 
					            reject(e);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }).catch(e => {
 | 
					    }).catch(e => {
 | 
				
			||||||
      browser.browser()?.close();
 | 
					      browser.browser()?.close();
 | 
				
			||||||
      throw e;
 | 
					      throw e;
 | 
				
			||||||
@ -450,7 +374,6 @@ class SunoApi {
 | 
				
			|||||||
          logger.info('hCaptcha token received. Closing browser');
 | 
					          logger.info('hCaptcha token received. Closing browser');
 | 
				
			||||||
          route.abort();
 | 
					          route.abort();
 | 
				
			||||||
          browser.browser()?.close();
 | 
					          browser.browser()?.close();
 | 
				
			||||||
          controller.abort();
 | 
					 | 
				
			||||||
          const request = route.request();
 | 
					          const request = route.request();
 | 
				
			||||||
          this.currentToken = request.headers().authorization.split('Bearer ').pop();
 | 
					          this.currentToken = request.headers().authorization.split('Bearer ').pop();
 | 
				
			||||||
          resolve(request.postDataJSON().token);
 | 
					          resolve(request.postDataJSON().token);
 | 
				
			||||||
@ -486,7 +409,7 @@ class SunoApi {
 | 
				
			|||||||
  ): Promise<AudioInfo[]> {
 | 
					  ): Promise<AudioInfo[]> {
 | 
				
			||||||
    await this.keepAlive(false);
 | 
					    await this.keepAlive(false);
 | 
				
			||||||
    const startTime = Date.now();
 | 
					    const startTime = Date.now();
 | 
				
			||||||
    const audios = await this.generateSongs(
 | 
					    const audios = this.generateSongs(
 | 
				
			||||||
      prompt,
 | 
					      prompt,
 | 
				
			||||||
      false,
 | 
					      false,
 | 
				
			||||||
      undefined,
 | 
					      undefined,
 | 
				
			||||||
@ -862,28 +785,10 @@ class SunoApi {
 | 
				
			|||||||
      monthly_usage: response.data.monthly_usage
 | 
					      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) => {
 | 
					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*)
 | 
					  const resolvedCookie = cookie || process.env.SUNO_COOKIE;
 | 
				
			||||||
  if (!resolvedCookie) {
 | 
					  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.')
 | 
					    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.');
 | 
					    throw new Error('Please provide a cookie either in the .env file or in the Cookie header of your request.');
 | 
				
			||||||
 | 
				
			|||||||
@ -29,87 +29,6 @@ export const isPage = (target: any): target is Page => {
 | 
				
			|||||||
  return target.constructor.name === '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 = {
 | 
					export const corsHeaders = {
 | 
				
			||||||
  'Access-Control-Allow-Origin': '*',
 | 
					  'Access-Control-Allow-Origin': '*',
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user