Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ TWOCAPTCHA_KEY= # Obtain from 2captcha.com
BROWSER=chromium # `chromium` or `firefox`, although `chromium` is highly recommended
BROWSER_GHOST_CURSOR=false
BROWSER_LOCALE=en
BROWSER_HEADLESS=true
BROWSER_HEADLESS=true

# CAPTCHA strategy:
# unset / false → automatic only (requires TWOCAPTCHA_KEY; may break on UI redesigns)
# true → manual only (skip auto, open a visible browser for the human to solve)
# fallback → try automatic first, fall back to manual on error (recommended for local use)
MANUAL_CAPTCHA=
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,30 @@ docker compose build && docker compose up
- `BROWSER_GHOST_CURSOR` — use ghost-cursor-playwright to simulate smooth mouse movements. Please note that it doesn't seem to make any difference in the rate of CAPTCHAs, so you can set it to `false`. Retained for future testing.
- `BROWSER_LOCALE` — the language of the browser. Using either `en` or `ru` is recommended, since those have the most workers on 2Captcha. [List of supported languages](https://2captcha.com/2captcha-api#language)
- `BROWSER_HEADLESS` — run the browser without the window. You probably want to set this to `true`.
- `MANUAL_CAPTCHA` *(new)* — how to handle CAPTCHAs. See the **CAPTCHA strategy** section below. Leave unset for the original automatic behavior.
```bash
SUNO_COOKIE=<…>
TWOCAPTCHA_KEY=<…>
BROWSER=chromium
BROWSER_GHOST_CURSOR=false
BROWSER_LOCALE=en
BROWSER_HEADLESS=true
MANUAL_CAPTCHA= # unset / true / fallback — see below
```

#### CAPTCHA strategy (`MANUAL_CAPTCHA`)

Suno's web UI is redesigned periodically, and the automatic CAPTCHA solver depends on DOM selectors that can drift (e.g. the v5.5 redesign removed the `.custom-textarea` selector, causing automatic flows to time out — issue [#263](https://github.com/gcui-art/suno-api/issues/263)). `MANUAL_CAPTCHA` lets you opt into a human-in-the-loop fallback that survives UI changes.

| Value | Behavior |
| --- | --- |
| *unset* / `false` | **Automatic only** — the original behavior. Requires `TWOCAPTCHA_KEY`. Fastest path when it works. |
| `true` | **Manual only** — skip the automatic flow entirely. Every CAPTCHA opens a visible browser; you fill the Suno UI and click *Create* yourself. The hCaptcha token is harvested from the network request you trigger. No DOM selectors involved, so it survives UI changes. |
| `fallback` | **Try automatic, fall back to manual on error.** Recommended for local/personal use: you get automation when Suno cooperates, and a working escape hatch when the UI changed or the 2Captcha solver fails. |

Manual / fallback modes are **not suitable for headless server deployments**, since they require a human to interact with the visible browser. For Vercel / Docker deployments, stick with the default automatic mode + `TWOCAPTCHA_KEY`.


### 5. Run suno-api

- If you’ve deployed to Vercel:
Expand Down
99 changes: 97 additions & 2 deletions src/lib/SunoApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ class SunoApi {

public async init(): Promise<SunoApi> {
//await this.getClerkLatestVersion();
const captchaMode = (process.env.MANUAL_CAPTCHA || '').toLowerCase().trim();
if (captchaMode === 'true' || captchaMode === '1' || captchaMode === 'yes') {
logger.info('CAPTCHA mode: MANUAL — every captcha will open a visible browser for human solving');
} else if (captchaMode === 'fallback') {
logger.info('CAPTCHA mode: FALLBACK — automatic first, manual on error');
}
await this.getAuthToken();
await this.keepAlive();
return this;
Expand Down Expand Up @@ -301,10 +307,99 @@ class SunoApi {
}

/**
* Checks for CAPTCHA verification and solves the CAPTCHA if needed
* @returns {string|null} hCaptcha token. If no verification is required, returns null
* Manual CAPTCHA mode: opens a visible browser, lets the human user fill the Suno
* web UI and click Create themselves. We just hijack the /api/generate/v2 request
* to steal the hCaptcha token from it. Works with any Suno UI version (no DOM
* selectors needed). Triggered by env var MANUAL_CAPTCHA=true.
*/
public async getCaptchaManual(): Promise<string|null> {
if (!await this.captchaRequired())
return null;

logger.info('CAPTCHA required. Opening VISIBLE browser for manual solve...');
// Force the browser to be visible even if BROWSER_HEADLESS=true is set.
const prevHeadless = process.env.BROWSER_HEADLESS;
process.env.BROWSER_HEADLESS = 'false';
const browser = await this.launchBrowser();
process.env.BROWSER_HEADLESS = prevHeadless;

const page = await browser.newPage();
await page.goto('https://suno.com/create', { waitUntil: 'domcontentloaded', timeout: 0 });
try { await page.bringToFront(); } catch {} // force window to front on macOS

logger.info('=================================================================');
logger.info('ACTION REQUIRED IN THE BROWSER:');
logger.info(' 1. Fill the Suno UI (anything: description, lyrics, style, ...)');
logger.info(' 2. Click the "Create" button');
logger.info(' 3. Solve any hCaptcha that appears');
logger.info('The browser will close automatically once the token is captured.');
logger.info('=================================================================');

return new Promise<string>((resolve, reject) => {
let captured = false;
page.route('**/api/generate/v2/**', async (route: any) => {
if (captured) {
try { await route.continue(); } catch {}
return;
}
captured = true;
try {
const request = route.request();
const auth = request.headers().authorization;
if (auth) this.currentToken = auth.split('Bearer ').pop();
const body = request.postDataJSON();
const token = body?.token;
logger.info(`hCaptcha token captured (length=${token?.length}). Closing browser.`);
try { await route.abort(); } catch {}
try { await browser.browser()?.close(); } catch {}
if (!token) reject(new Error('No token field in intercepted /api/generate/v2 body'));
else resolve(token);
} catch (err: any) {
reject(err);
}
});
// No timeout — let the user take as long as they need.
// Detect browser closed by user as a cancel.
browser.on('close', () => {
if (!captured) reject(new Error('Browser closed by user before captcha solved'));
});
});
}

/**
* Public CAPTCHA dispatcher. Routes to the automatic flow, the manual flow, or
* the fallback flow (try auto, on error use manual) based on MANUAL_CAPTCHA.
*
* MANUAL_CAPTCHA unset / false → automatic only (original gcui-art behavior)
* MANUAL_CAPTCHA=true → manual only (skip auto entirely)
* MANUAL_CAPTCHA=fallback → try auto, fall back to manual on any error
*/
public async getCaptcha(): Promise<string|null> {
const mode = (process.env.MANUAL_CAPTCHA || '').toLowerCase().trim();
if (mode === 'true' || mode === '1' || mode === 'yes') {
return this.getCaptchaManual();
}
if (mode === 'fallback') {
try {
return await this.getCaptchaAuto();
} catch (e: any) {
logger.warn({ err: e?.message ?? String(e) },
'Automatic CAPTCHA failed — falling back to MANUAL mode');
return this.getCaptchaManual();
}
}
return this.getCaptchaAuto();
}

/**
* Automated CAPTCHA flow: launches a headless browser, navigates the Suno web UI,
* and uses 2Captcha to solve any hCaptcha challenge. Fragile to UI changes —
* known to break against Suno v5.5+ where `.custom-textarea` no longer exists.
* Use MANUAL_CAPTCHA=true or MANUAL_CAPTCHA=fallback to opt into the manual flow.
*
* @returns {string|null} hCaptcha token. If no verification is required, returns null.
*/
private async getCaptchaAuto(): Promise<string|null> {
if (!await this.captchaRequired())
return null;

Expand Down