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:
parent
52ad4dea00
commit
881c6c773c
@ -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
|
@ -2,13 +2,13 @@ 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 { paramsCoordinates } from '@2captcha/captcha-solver/dist/structs/2captcha';
|
||||
import { BrowserContext, Page, Locator, chromium, firefox } from 'rebrowser-playwright-core';
|
||||
import { createCursor, Cursor } from 'ghost-cursor-playwright';
|
||||
import { paramsCoordinates } from '@2captcha/captcha-solver/dist/structs/2captcha';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@ -180,7 +180,6 @@ class SunoApi {
|
||||
ctype: 'generation'
|
||||
});
|
||||
logger.info(resp.data);
|
||||
// await sleep(10);
|
||||
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 });
|
||||
|
||||
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
|
||||
|
||||
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');
|
||||
try {
|
||||
let wait = true;
|
||||
while (true) {
|
||||
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
|
||||
}
|
||||
}
|
||||
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);
|
||||
@ -338,6 +336,12 @@ class SunoApi {
|
||||
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,6 +352,7 @@ 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);
|
||||
@ -362,7 +367,8 @@ class SunoApi {
|
||||
});
|
||||
}
|
||||
} 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();
|
||||
else
|
||||
reject(e);
|
||||
@ -377,6 +383,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);
|
||||
|
@ -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': '*',
|
||||
|
Loading…
Reference in New Issue
Block a user