Implement hCaptcha solving via 2Captcha & other stuff

- implement hCaptcha solving via paid service 2Captcha and browser automation library Playwright with rebrowser-patches

- implement sunoApi instances caching so sessions won't be constantly updated

- add support for entering cookies not only in SUNO_COOKIE, but also the Cookie HTTP header

- update docs and add Russian docs
This commit is contained in:
gohoski
2025-01-06 05:25:08 +03:00
parent 3bffec1ea1
commit 7da7ac6ae2
24 changed files with 10837 additions and 138 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ export async function GET(req: NextRequest) {
});
}
const audioInfo = await (await sunoApi).getClip(clipId);
const audioInfo = await (await sunoApi()).getClip(clipId);
return new NextResponse(JSON.stringify(audioInfo), {
status: 200,
+2 -1
View File
@@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@@ -18,7 +19,7 @@ export async function POST(req: NextRequest) {
}
});
}
const audioInfo = await (await sunoApi).concatenate(clip_id);
const audioInfo = await (await sunoApi((await cookies()).toString())).concatenate(clip_id);
return new NextResponse(JSON.stringify(audioInfo), {
status: 200,
headers: {
+5 -13
View File
@@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers';
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { prompt, tags, title, make_instrumental, model, wait_audio, negative_tags } = body;
const audioInfo = await (await sunoApi).custom_generate(
const audioInfo = await (await sunoApi((await cookies()).toString())).custom_generate(
prompt, tags, title,
Boolean(make_instrumental),
model || DEFAULT_MODEL,
@@ -25,18 +26,9 @@ export async function POST(req: NextRequest) {
}
});
} catch (error: any) {
console.error('Error generating custom audio:', error.response.data);
if (error.response.status === 402) {
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
status: 402,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
console.error('Error generating custom audio:', error);
return new NextResponse(JSON.stringify({ error: error.response?.data?.detail || error.toString() }), {
status: error.response?.status || 500,
headers: {
'Content-Type': 'application/json',
...corsHeaders
+4 -3
View File
@@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@@ -8,7 +9,7 @@ export async function POST(req: NextRequest) {
if (req.method === 'POST') {
try {
const body = await req.json();
const { audio_id, prompt, continue_at, tags, title, model } = body;
const { audio_id, prompt, continue_at, tags, negative_tags, title, model, wait_audio } = body;
if (!audio_id) {
return new NextResponse(JSON.stringify({ error: 'Audio ID is required' }), {
@@ -20,8 +21,8 @@ export async function POST(req: NextRequest) {
});
}
const audioInfo = await (await sunoApi)
.extendAudio(audio_id, prompt, continue_at, tags, title, model || DEFAULT_MODEL);
const audioInfo = await (await sunoApi((await cookies()).toString()))
.extendAudio(audio_id, prompt, continue_at, tags || '', negative_tags || '', title, model || DEFAULT_MODEL, wait_audio || false);
return new NextResponse(JSON.stringify(audioInfo), {
status: 200,
+2 -1
View File
@@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
const body = await req.json();
const { prompt, make_instrumental, model, wait_audio } = body;
const audioInfo = await (await sunoApi).generate(
const audioInfo = await (await sunoApi((await cookies()).toString())).generate(
prompt,
Boolean(make_instrumental),
model || DEFAULT_MODEL,
+2 -1
View File
@@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
const body = await req.json();
const { prompt } = body;
const lyrics = await (await sunoApi).generateLyrics(prompt);
const lyrics = await (await sunoApi((await cookies()).toString())).generateLyrics(prompt);
return new NextResponse(JSON.stringify(lyrics), {
status: 200,
+2 -1
View File
@@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers';
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@@ -20,7 +21,7 @@ export async function POST(req: NextRequest) {
});
}
const audioInfo = await (await sunoApi)
const audioInfo = await (await sunoApi((await cookies()).toString()))
.generateStems(audio_id);
return new NextResponse(JSON.stringify(audioInfo), {
+4 -2
View File
@@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { sunoApi } from '@/lib/SunoApi';
import { corsHeaders } from '@/lib/utils';
@@ -10,13 +11,14 @@ export async function GET(req: NextRequest) {
const url = new URL(req.url);
const songIds = url.searchParams.get('ids');
const page = url.searchParams.get('page');
const cookie = (await cookies()).toString();
let audioInfo = [];
if (songIds && songIds.length > 0) {
const idsArray = songIds.split(',');
audioInfo = await (await sunoApi).get(idsArray, page);
audioInfo = await (await sunoApi(cookie)).get(idsArray, page);
} else {
audioInfo = await (await sunoApi).get(undefined, page);
audioInfo = await (await sunoApi(cookie)).get(undefined, page);
}
return new NextResponse(JSON.stringify(audioInfo), {
+2 -1
View File
@@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@@ -20,7 +21,7 @@ export async function GET(req: NextRequest) {
});
}
const lyricAlignment = await (await sunoApi).getLyricAlignment(song_id);
const lyricAlignment = await (await sunoApi((await cookies()).toString())).getLyricAlignment(song_id);
return new NextResponse(JSON.stringify(lyricAlignment), {
+2 -1
View File
@@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@@ -8,7 +9,7 @@ export async function GET(req: NextRequest) {
if (req.method === 'GET') {
try {
const limit = await (await sunoApi).get_credits();
const limit = await (await sunoApi((await cookies()).toString())).get_credits();
return new NextResponse(JSON.stringify(limit), {
+6 -1
View File
@@ -9,7 +9,12 @@ type Props = {
const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false });
function Swagger({ spec }: Props) {
return <SwaggerUI spec={spec}/>;
return <SwaggerUI spec={spec} requestInterceptor={(req) => {
// Remove cookies before sending requests
req.credentials = 'omit';
console.log(req);
return req;
}} />;
}
export default Swagger;
+5
View File
@@ -231,6 +231,11 @@
"description": "Music genre",
"example": ""
},
"negative_tags": {
"type": "string",
"description": "Negative Music genre",
"example":""
},
"model": {
"type": "string",
"description": "Model name ,default is chirp-v3-5",
+1 -1
View File
@@ -32,7 +32,7 @@ export async function POST(req: NextRequest) {
}
const audioInfo = await (await sunoApi).generate(userMessage.content, true, DEFAULT_MODEL, true);
const audioInfo = await (await sunoApi()).generate(userMessage.content, true, DEFAULT_MODEL, true);
const audio = audioInfo[0]
const data = `## Song Title: ${audio.title}\n![Song Cover](${audio.image_url})\n### Lyrics:\n${audio.lyric}\n### Listen to the song: ${audio.audio_url}`
+330 -73
View File
@@ -1,9 +1,18 @@
import axios, { AxiosInstance } from 'axios';
import UserAgent from 'user-agents';
import pino from 'pino';
import { wrapper } from 'axios-cookiejar-support';
import { CookieJar } from 'tough-cookie';
import { sleep } from '@/lib/utils';
import yn from 'yn';
import { sleep, isPage } from '@/lib/utils';
import * as cookie from 'cookie';
import { randomUUID } from 'node:crypto';
import { Solver } from '@2captcha/captcha-solver';
import { BrowserContext, Page, Locator, chromium, firefox } from 'rebrowser-playwright-core';
import { createCursor, Cursor } from 'ghost-cursor-playwright';
// sunoApi instance caching
const globalForSunoApi = global as unknown as { sunoApiCache?: Map<string, SunoApi> };
const cache = globalForSunoApi.sunoApiCache || new Map<string, SunoApi>();
globalForSunoApi.sunoApiCache = cache;
const logger = pino();
export const DEFAULT_MODEL = 'chirp-v3-5';
@@ -30,37 +39,58 @@ export interface AudioInfo {
class SunoApi {
private static BASE_URL: string = 'https://studio-api.prod.suno.com';
private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
private static JSDELIVR_BASE_URL: string = 'https://data.jsdelivr.com';
private static CLERK_VERSION = '5.15.0';
private readonly client: AxiosInstance;
private clerkVersion?: string;
private sid?: string;
private currentToken?: string;
private deviceId?: string;
private userAgent?: string;
private cookies: Record<string, string | undefined>;
private solver = new Solver(process.env.TWOCAPTCHA_KEY + '');
private ghostCursorEnabled = yn(process.env.BROWSER_GHOST_CURSOR, { default: false });
private cursor?: Cursor;
constructor(cookie: string) {
const cookieJar = new CookieJar();
const randomUserAgent = new UserAgent(/Chrome/).random().toString();
this.client = wrapper(
axios.create({
jar: cookieJar,
withCredentials: true,
headers: {
'User-Agent': randomUserAgent,
Cookie: cookie
}
})
);
this.client.interceptors.request.use((config) => {
if (this.currentToken) {
// Use the current token status
config.headers['Authorization'] = `Bearer ${this.currentToken}`;
constructor(cookies: string) {
this.userAgent = new UserAgent(/Macintosh/).random().toString(); // Usually Mac systems get less amount of CAPTCHAs
this.cookies = cookie.parse(cookies);
this.deviceId = this.cookies.ajs_anonymous_id || randomUUID();
this.client = axios.create({
withCredentials: true,
headers: {
'Affiliate-Id': 'undefined',
'Device-Id': `"${this.deviceId}"`,
'x-suno-client': 'Android prerelease-4nt180t 1.0.42',
'X-Requested-With': 'com.suno.android',
'sec-ch-ua': '"Chromium";v="130", "Android WebView";v="130", "Not?A_Brand";v="99"',
'sec-ch-ua-mobile': '?1',
'sec-ch-ua-platform': '"Android"',
'User-Agent': this.userAgent
}
});
this.client.interceptors.request.use(config => {
if (this.currentToken && !config.headers.Authorization)
config.headers.Authorization = `Bearer ${this.currentToken}`;
const cookiesArray = Object.entries(this.cookies).map(([key, value]) =>
cookie.serialize(key, value as string)
);
config.headers.Cookie = cookiesArray.join('; ');
return config;
});
this.client.interceptors.response.use(resp => {
const setCookieHeader = resp.headers['set-cookie'];
if (Array.isArray(setCookieHeader)) {
const newCookies = cookie.parse(setCookieHeader.join('; '));
for (const [key, value] of Object.entries(newCookies)) {
this.cookies[key] = value;
}
}
return resp;
})
}
public async init(): Promise<SunoApi> {
await this.getClerkLatestVersion();
//await this.getClerkLatestVersion();
await this.getAuthToken();
await this.keepAlive();
return this;
@@ -68,7 +98,8 @@ class SunoApi {
/**
* Get the clerk package latest version id.
*/
* This method is commented because we are now using a hard-coded Clerk version, hence this method is not needed.
private async getClerkLatestVersion() {
// URL to get clerk version ID
const getClerkVersionUrl = `${SunoApi.JSDELIVR_BASE_URL}/v1/package/npm/@clerk/clerk-js`;
@@ -80,26 +111,28 @@ class SunoApi {
);
}
// Save clerk version ID for auth
// this.clerkVersion = versionListResponse?.data?.['tags']['latest'];
// Use a Clerk version released before fraud detection was implemented
this.clerkVersion = "5.34.0";
SunoApi.clerkVersion = versionListResponse?.data?.['tags']['latest'];
}
*/
/**
* Get the session ID and save it for later use.
*/
private async getAuthToken() {
logger.info('Getting the session ID');
// URL to get session ID
const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_clerk_js_version=${this.clerkVersion}`;
const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_is_native=true&_clerk_js_version=${SunoApi.CLERK_VERSION}`;
// Get session ID
const sessionResponse = await this.client.get(getSessionUrl);
if (!sessionResponse?.data?.response?.['last_active_session_id']) {
const sessionResponse = await this.client.get(getSessionUrl, {
headers: { Authorization: this.cookies.__client }
});
if (!sessionResponse?.data?.response?.last_active_session_id) {
throw new Error(
'Failed to get session id, you may need to update the SUNO_COOKIE'
);
}
// Save session ID for later use
this.sid = sessionResponse.data.response['last_active_session_id'];
this.sid = sessionResponse.data.response.last_active_session_id;
}
/**
@@ -111,16 +144,238 @@ class SunoApi {
throw new Error('Session ID is not set. Cannot renew token.');
}
// URL to renew session token
const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens?_clerk_js_version==${this.clerkVersion}`;
const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens?_is_native=true&_clerk_js_version=${SunoApi.CLERK_VERSION}`;
// Renew session token
const renewResponse = await this.client.post(renewUrl);
logger.info('KeepAlive...\n');
const renewResponse = await this.client.post(renewUrl, {}, {
headers: { Authorization: this.cookies.__client }
});
if (isWait) {
await sleep(1, 2);
}
const newToken = renewResponse.data['jwt'];
const newToken = renewResponse.data.jwt;
// Update Authorization field in request header with the new JWT token
this.currentToken = newToken;
logger.info(this.currentToken);
}
/**
* Get the session token (not to be confused with session ID) and save it for later use.
*/
private async getSessionToken() {
const tokenResponse = await this.client.post(
`${SunoApi.BASE_URL}/api/user/create_session_id/`,
{
session_properties: JSON.stringify({ deviceId: this.deviceId }),
session_type: 1
}
);
return tokenResponse.data.session_id;
}
private async captchaRequired(): Promise<boolean> {
const resp = await this.client.post(`${SunoApi.BASE_URL}/api/c/check`, {
ctype: 'generation'
});
logger.info(resp.data);
// await sleep(10);
return resp.data.required;
}
/**
* Clicks on a locator or XY vector. This method is made because of the difference between ghost-cursor-playwright and Playwright methods
*/
private async click(target: Locator|Page, position?: { x: number, y: number }): Promise<void> {
if (this.ghostCursorEnabled) {
let pos: any = isPage(target) ? { x: 0, y: 0 } : await target.boundingBox();
if (position)
pos = {
...pos,
x: pos.x + position.x,
y: pos.y + position.y,
width: null,
height: null,
};
return this.cursor?.actions.click({
target: pos
});
} else {
if (isPage(target))
return target.mouse.click(position?.x ?? 0, position?.y ?? 0);
else
return target.click({ force: true, position });
}
}
/**
* Get the BrowserType from the `BROWSER` environment variable.
* @returns {BrowserType} chromium, firefox or webkit. Default is chromium
*/
private getBrowserType() {
const browser = process.env.BROWSER?.toLowerCase();
switch (browser) {
case 'firefox':
return firefox;
/*case 'webkit': ** doesn't work with rebrowser-patches
case 'safari':
return webkit;*/
default:
return chromium;
}
}
/**
* Launches a browser with the necessary cookies
* @returns {BrowserContext}
*/
private async launchBrowser(): Promise<BrowserContext> {
const browser = await this.getBrowserType().launch({
args: [
'--disable-blink-features=AutomationControlled',
'--disable-web-security',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-features=site-per-process',
'--disable-features=IsolateOrigins',
'--disable-extensions',
'--disable-infobars'
],
headless: yn(process.env.BROWSER_HEADLESS, { default: true })
});
const context = await browser.newContext({ userAgent: this.userAgent, locale: process.env.BROWSER_LOCALE, viewport: null });
const cookies = [];
const lax: 'Lax' | 'Strict' | 'None' = 'Lax';
cookies.push({
name: '__session',
value: this.currentToken+'',
domain: '.suno.com',
path: '/',
sameSite: lax
});
for (const key in this.cookies) {
cookies.push({
name: key,
value: this.cookies[key]+'',
domain: '.suno.com',
path: '/',
sameSite: lax
})
}
await context.addCookies(cookies);
return context;
}
/**
* Checks for CAPTCHA verification and solves the CAPTCHA if needed
* @returns {string|null} hCaptcha token. If no verification is required, returns null
*/
public async getCaptcha(): Promise<string|null> {
if (!await this.captchaRequired())
return null;
logger.info('CAPTCHA required. Launching browser...')
const browser = await this.launchBrowser();
const page = await browser.newPage();
await page.goto('https://suno.com/create', { referer: 'https://www.google.com/', waitUntil: 'domcontentloaded', timeout: 0 });
page.on('request', request => console.log('>>', request.method(), request.url()));
page.on('response', response => console.log('<<', response.status(), response.url()));
logger.info('Waiting for Suno interface to load');
await page.locator('.react-aria-GridList').waitFor({ timeout: 60000 });
if (this.ghostCursorEnabled)
this.cursor = await createCursor(page);
logger.info('Triggering the CAPTCHA');
await this.click(page, { x: 318, y: 13 }); // close all popups
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);
new Promise<void>(async (resolve, reject) => {
const frame = page.frameLocator('iframe[title*="hCaptcha"]');
const challenge = frame.locator('.challenge-container');
while (true) {
try {
await page.waitForResponse('https://img**.hcaptcha.com/**', { timeout: 60000 }); // wait for hCaptcha image to load
while (true) { // wait for all requests to finish
try {
await page.waitForResponse('https://img**.hcaptcha.com/**', { timeout: 1000 });
} catch(e) {
break
}
}
//await sleep(0.1); // sometimes it takes a couple of seconds to display the image itself. unfortunately, the only option is to wait and hope that it loads
const drag = (await challenge.locator('.prompt-text').first().innerText()).toLowerCase().includes('drag');
if (drag) {
logger.info('Got a dragging hCaptcha. This type of hCaptcha is currently not supported. Skipping...');
this.click(frame.locator('.button-submit'));
continue;
}
let captcha: any;
for (let j = 0; j < 3; j++) { // try several times because sometimes 2Captcha could send an error
try {
logger.info('Sending the CAPTCHA to 2Captcha');
captcha = await this.solver.coordinates({
body: (await challenge.screenshot()).toString('base64'),
lang: process.env.BROWSER_LOCALE
});
break;
} catch(err: any) {
logger.info(err.message);
if (j != 2)
logger.info('Retrying...');
else
throw err;
}
}
for (const data of captcha.data) {
logger.info(data);
await this.click(challenge, { x: +data.x, y: +data.y });
}
/*await*/ this.click(frame.locator('.button-submit')); // await is commented because we need to call waitForResponse at the same time
} catch(e: any) {
if (e.message.includes('viewport') || e.message.includes('timeout')) // when hCaptcha window has been closed due to inactivity,
this.click(button); // click the Create button again to trigger the CAPTCHA
else if (e.message.includes('been closed')) // catch error when closing the browser
resolve();
else
reject(e);
}
}
}).catch(e => {
//if (!e.message.includes('been closed'))
throw e;
});
return (new Promise((resolve, reject) => {
page.route('**/api/generate/v2/**', async (route: any) => {
try {
logger.info('hCaptcha token received. Closing browser');
route.abort();
browser.browser()?.close();
const request = route.request();
this.currentToken = request.headers().authorization.split('Bearer ').pop();
resolve(request.postDataJSON().token);
} catch(err) {
reject(err);
}
});
}));
}
/**
* Imitates Cloudflare Turnstile loading error. Unused right now, left for future
*/
private async getTurnstile() {
return this.client.post(
`https://clerk.suno.com/v1/client?__clerk_api_version=2021-02-05&_clerk_js_version=${SunoApi.CLERK_VERSION}&_method=PATCH`,
{ captcha_error: '300030,300030,300030' },
{ headers: { 'content-type': 'application/x-www-form-urlencoded' } });
}
/**
@@ -225,6 +480,8 @@ class SunoApi {
* @param make_instrumental Indicates if the generated song should be instrumental.
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
* @param negative_tags Negative tags that should not be included in the generated audio.
* @param task Optional indication of what to do. Enter 'extend' if extending an audio, otherwise specify null.
* @param continue_clip_id
* @returns A promise that resolves to an array of AudioInfo objects representing the generated songs.
*/
private async generateSongs(
@@ -235,14 +492,21 @@ class SunoApi {
make_instrumental?: boolean,
model?: string,
wait_audio: boolean = false,
negative_tags?: string
negative_tags?: string,
task?: string,
continue_clip_id?: string,
continue_at?: number
): Promise<AudioInfo[]> {
await this.keepAlive(false);
await this.keepAlive();
const payload: any = {
make_instrumental: make_instrumental,
mv: model || DEFAULT_MODEL,
prompt: '',
generation_type: 'TEXT'
generation_type: 'TEXT',
continue_at: continue_at,
continue_clip_id: continue_clip_id,
task: task,
token: await this.getCaptcha()
};
if (isCustom) {
payload.tags = tags;
@@ -276,13 +540,10 @@ class SunoApi {
timeout: 10000 // 10 seconds timeout
}
);
logger.info(
'generateSongs Response:\n' + JSON.stringify(response.data, null, 2)
);
if (response.status !== 200) {
throw new Error('Error response:' + response.statusText);
}
const songIds = response.data['clips'].map((audio: any) => audio.id);
const songIds = response.data.clips.map((audio: any) => audio.id);
//Want to wait for music file generation
if (wait_audio) {
const startTime = Date.now();
@@ -303,8 +564,7 @@ class SunoApi {
}
return lastResponse;
} else {
await this.keepAlive(true);
return response.data['clips'].map((audio: any) => ({
return response.data.clips.map((audio: any) => ({
id: audio.id,
title: audio.title,
image_url: audio.image_url,
@@ -366,26 +626,14 @@ class SunoApi {
public async extendAudio(
audioId: string,
prompt: string = '',
continueAt: string = '0',
continueAt: number,
tags: string = '',
negative_tags: string = '',
title: string = '',
model?: string
): Promise<AudioInfo> {
await this.keepAlive(false);
const response = await this.client.post(
`${SunoApi.BASE_URL}/api/generate/v2/`,
{
continue_clip_id: audioId,
continue_at: continueAt,
mv: model || DEFAULT_MODEL,
prompt: prompt,
tags: tags,
task: 'extend',
title: title
}
);
console.log('response\n', response);
return response.data;
model?: string,
wait_audio?: boolean
): Promise<AudioInfo[]> {
return this.generateSongs(prompt, true, tags, title, false, model, wait_audio, negative_tags, 'extend', audioId, continueAt);
}
/**
@@ -460,7 +708,7 @@ class SunoApi {
page?: string | null
): Promise<AudioInfo[]> {
await this.keepAlive(false);
let url = new URL(`${SunoApi.BASE_URL}/api/feed/`);
let url = new URL(`${SunoApi.BASE_URL}/api/feed/v2`);
if (songIds) {
url.searchParams.append('ids', songIds.join(','));
}
@@ -469,11 +717,11 @@ class SunoApi {
}
logger.info('Get audio status: ' + url.href);
const response = await this.client.get(url.href, {
// 3 seconds timeout
timeout: 3000
// 10 seconds timeout
timeout: 10000
});
const audios = response.data;
const audios = response.data.clips;
return audios.map((audio: any) => ({
id: audio.id,
@@ -523,13 +771,22 @@ class SunoApi {
}
}
const newSunoApi = async (cookie: string) => {
const sunoApi = new SunoApi(cookie);
return await sunoApi.init();
};
export const sunoApi = async (cookie?: string) => {
const resolvedCookie = cookie || process.env.SUNO_COOKIE;
if (!resolvedCookie) {
logger.info('No cookie provided! Aborting...\nPlease provide a cookie either in the .env file or in the Cookie header of your request.')
throw new Error('Please provide a cookie either in the .env file or in the Cookie header of your request.');
}
if (!process.env.SUNO_COOKIE) {
console.log('Environment does not contain SUNO_COOKIE.', process.env);
}
// Check if the instance for this cookie already exists in the cache
const cachedInstance = cache.get(resolvedCookie);
if (cachedInstance)
return cachedInstance;
export const sunoApi = newSunoApi(process.env.SUNO_COOKIE || '');
// If not, create a new instance and initialize it
const instance = await new SunoApi(resolvedCookie).init();
// Cache the initialized instance
cache.set(resolvedCookie, instance);
return instance;
};
+9
View File
@@ -1,4 +1,5 @@
import pino from "pino";
import { Page } from "rebrowser-playwright-core";
const logger = pino();
@@ -20,6 +21,14 @@ export const sleep = (x: number, y?: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, timeout));
}
/**
* @param target A Locator or a page
* @returns {boolean}
*/
export const isPage = (target: any): target is Page => {
return target.constructor.name === 'Page';
}
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',