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
This commit is contained in:
gohoski 2025-01-11 01:48:17 +03:00
parent 52ad4dea00
commit 881c6c773c
3 changed files with 108 additions and 20 deletions

View File

@ -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 BROWSER=chromium # chromium or firefox, although chromium is highly recommended
BROWSER_GHOST_CURSOR=false BROWSER_GHOST_CURSOR=false
BROWSER_LOCALE=en BROWSER_LOCALE=en
BROWSER_HEADLESS=true BROWSER_HEADLESS=true

View File

@ -2,13 +2,13 @@ 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 { sleep, isPage } from '@/lib/utils'; import { isPage, sleep, waitForRequests } 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';
@ -180,7 +180,6 @@ 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;
} }
@ -281,47 +280,46 @@ class SunoApi {
await page.goto('https://suno.com/create', { referer: 'https://www.google.com/', waitUntil: 'domcontentloaded', timeout: 0 }); await page.goto('https://suno.com/create', { referer: 'https://www.google.com/', waitUntil: 'domcontentloaded', timeout: 0 });
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/feed/v2**', { 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');
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'); 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');
await this.click(button); 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 { try {
let wait = true;
while (true) { while (true) {
await page.waitForResponse('https://img**.hcaptcha.com/**', { timeout: 60000 }); // wait for hCaptcha image to load if (wait)
while (true) { // wait for all requests to finish await waitForRequests(page, controller.signal);
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 send an error for (let j = 0; j < 3; j++) { // try several times because sometimes 2Captcha could return 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()).toString('base64'), body: (await challenge.screenshot({ timeout: 5000 })).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 = '! 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'); 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);
@ -333,11 +331,17 @@ class SunoApi {
else else
throw err; throw err;
} }
} }
if (drag) { if (drag) {
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];
@ -348,6 +352,7 @@ 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);
@ -362,7 +367,8 @@ class SunoApi {
}); });
} }
} catch(e: any) { } catch(e: any) {
if (e.message.includes('been closed')) // catch error when closing the browser if (e.message.includes('been closed') // catch error when closing the browser
|| e.message == 'AbortError') // catch error when waitForRequests is aborted
resolve(); resolve();
else else
reject(e); reject(e);
@ -377,6 +383,7 @@ 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);

View File

@ -29,6 +29,87 @@ 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': '*',