Compare commits

..

11 Commits

Author SHA1 Message Date
b1c4ed915e feat: add ghostery adblocker & update deps
Woah, two years :)
2026-02-14 14:36:20 +07:00
bb9a31cc78 fix: close the page if an error occurred while automating
Prevent massive memory leak...
2025-04-20 07:23:40 +07:00
c6b571c68d chore: move pino-pretty to dependencies
Sacrifice minimal performance for readability :cat_smirk:
2025-04-14 08:43:02 +07:00
a63e92e91f chore: prevent launching if proxy count is 0 2025-04-14 01:23:09 +07:00
92cb93b4cb chore: version in constants 2025-04-14 01:10:52 +07:00
6cf4096064 chore: fix running dist package in nodejs
So we have to add ".js" to the import name...
2025-04-13 13:29:36 +07:00
358fe53be9 feat: add change_viewport option 2025-04-13 00:43:55 +07:00
50c5927be7 fix: check consent button every second 2025-04-13 00:34:57 +07:00
cb427a9fa4 feat: random device resolution & accept cookies 2025-04-13 00:31:57 +07:00
2ad2f9993e feat: catch all errors 2025-04-12 21:58:19 +07:00
27b4f4a9cc feat: wait for 15s before execute the keepAlive strategy 2025-04-12 21:48:25 +07:00
9 changed files with 193 additions and 129 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
# Castorsrm # Castorsrm
config.json config.json
build/
# Logs # Logs

View File

@@ -2,6 +2,13 @@
Rice shirt rice money :( Rice shirt rice money :(
## Notice
> [!WARNING]
> This software is provided by the author as is, without any warranty. Use at your own risk
The view buffed should be around 1/2 (or 2/5) of the amount of proxy you chosen in the worst case, and possibly near the amount in the best case.
## Installation ## Installation
This project uses Bun for dependency management and Node.js (or Bun on UNIX platforms) for the app execution. This project uses Bun for dependency management and Node.js (or Bun on UNIX platforms) for the app execution.

BIN
bun.lockb

Binary file not shown.

View File

@@ -8,22 +8,23 @@
"dev-bun": "bun ./src/index.ts", "dev-bun": "bun ./src/index.ts",
"start": "node ./dist/index.js", "start": "node ./dist/index.js",
"start-bun": "bun ./dist/index.js", "start-bun": "bun ./dist/index.js",
"build": "swc ./src -d dist" "build": "swc ./src -d dist --strip-leading-paths"
}, },
"devDependencies": { "devDependencies": {
"@swc-node/register": "^1.10.10", "@swc-node/register": "^1.11.1",
"@swc/cli": "^0.6.0", "@swc/cli": "^0.6.0",
"@swc/core": "^1.11.20", "@swc/core": "^1.15.11",
"@types/bun": "latest", "@types/bun": "latest"
"pino-pretty": "^13.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.0.0" "typescript": "^5.8.3"
}, },
"dependencies": { "dependencies": {
"patchright": "^1.51.3", "@ghostery/adblocker-playwright": "^2.14.1",
"pino": "^9.6.0", "patchright": "^1.57.0",
"playwright": "^1.51.1" "pino": "^9.14.0",
"pino-pretty": "^13.1.3",
"playwright": "^1.58.2"
}, },
"trustedDependencies": [ "trustedDependencies": [
"@swc/core" "@swc/core"

View File

@@ -1,11 +1,13 @@
class Config { class Config {
playwright: { playwright: {
browser: string; browser: string;
change_viewport: boolean;
headless: boolean; headless: boolean;
cdp: string; cdp: string;
url: string; url: string;
} = { } = {
browser: "chromium", browser: "chromium",
change_viewport: true,
headless: true, headless: true,
cdp: "ws://127.0.0.1:9222", cdp: "ws://127.0.0.1:9222",
url: "https://www.twitch.tv/", url: "https://www.twitch.tv/",

View File

@@ -1,3 +1,4 @@
const VERSION = "0.1.0";
const REFLECT4_SERVERS = [ const REFLECT4_SERVERS = [
"https://www.blockaway.net", "https://www.blockaway.net",
"https://www.croxyproxy.com", "https://www.croxyproxy.com",
@@ -9,4 +10,4 @@ const REFLECT4_SERVERS = [
"https://proxyium.com" "https://proxyium.com"
]; ];
export { REFLECT4_SERVERS }; export { REFLECT4_SERVERS, VERSION };

View File

@@ -1,75 +1,96 @@
import { chromium, type Browser, type LaunchOptions } from "patchright"; import { chromium, type Browser, type LaunchOptions } from "patchright";
import fs from 'node:fs'; import { PlaywrightBlocker } from '@ghostery/adblocker-playwright';
import * as reflect4 from "./proxy/reflect4"; import fs from 'node:fs';
import logger from "./logger"; import * as reflect4 from "./proxy/reflect4.js";
import Config from "./config"; import logger from "./logger.js";
import Config from "./config.js";
const version = "0.1.0"; import { VERSION } from "./constants.js";
logger.info(`Castorsrm v${version} - https://github.com/teppyboy/castorsrm`) logger.info(`Castorsrm v${VERSION}`)
logger.warn("This software is provided by the author as is, without any warranty. Use at your own risk."); logger.warn("This software is provided by the author as is, without any warranty. Use at your own risk.");
let config = new Config(); let config = new Config();
if (fs.existsSync("config.json")) { if (fs.existsSync("config.json")) {
logger.info("Reading configuration from 'config.json'..."); logger.info("Reading configuration from 'config.json'...");
const text = await fs.promises.readFile("config.json", "utf-8"); const text = await fs.promises.readFile("config.json", "utf-8");
config = Config.fromJSON(text); config = Config.fromJSON(text);
} else { } else {
logger.info("No configuration file found. Using the default configuration."); logger.info("No configuration file found. Using the default configuration.");
await fs.promises.writeFile("config.json", config.toJSON()); logger.info("Default configuration file 'config.json' created.");
logger.info("Default configuration file 'config.json' created."); }
} // Write the new config file in case we updated something.
await fs.promises.writeFile("config.json", config.toJSON());
logger.level = process.env.LOG_LEVEL || config.logger.level;
logger.info(`Logger level set to '${logger.level}'`); // Validate configuration
if (config.proxy.count < 1) {
let browser: Browser; logger.error("Proxy count must be greater than 0.");
let launchOptions: LaunchOptions = { process.exit(1);
headless: config.playwright.headless, }
}
logger.level = process.env.LOG_LEVEL || config.logger.level;
logger.info("Launching browser..."); logger.info(`Logger level set to '${logger.level}'`);
logger.debug(`Launch options: ${JSON.stringify(launchOptions)}`);
let browser: Browser;
switch (config.playwright.browser) { let launchOptions: LaunchOptions = {
case "chromium": headless: config.playwright.headless,
logger.info("Using Chromium as the browser provider."); }
logger.warn("Chromium is not supported by Twitch. Use at your own risk.");
browser = await chromium.launch({ ...launchOptions }); logger.info("Launching browser...");
break; logger.debug(`Launch options: ${JSON.stringify(launchOptions)}`);
case "chrome":
logger.info("Using Google Chrome as the browser provider."); switch (config.playwright.browser) {
browser = await chromium.launch({ ...launchOptions, channel: "chrome" }); case "chromium":
break; logger.info("Using Chromium as the browser provider.");
case "cdp": logger.warn("Chromium is not supported by Twitch. Use at your own risk.");
logger.info("Using Chrome DevTools Protocol (CDP) for browser connection."); browser = await chromium.launch({ ...launchOptions });
browser = await chromium.connectOverCDP(config.playwright.cdp); break;
break; case "chrome":
default: logger.info("Using Google Chrome as the browser provider.");
logger.warn(`Unsupported browser channel: '${config.playwright.browser}'`); browser = await chromium.launch({ ...launchOptions, channel: "chrome" });
logger.warn("Castorsrm will try to launch the browser anyway, but it may not work as expected."); break;
browser = await chromium.launch({ ...launchOptions, channel: config.playwright.browser }); case "cdp":
break; logger.info("Using Chrome DevTools Protocol (CDP) for browser connection.");
} browser = await chromium.connectOverCDP(config.playwright.cdp);
break;
logger.info("Browser launched successfully, spawning proxies"); default:
if (config.proxy.mode === "reflect4") { logger.warn(`Unsupported browser channel: '${config.playwright.browser}'`);
const context = await browser.newContext(); logger.warn("Castorsrm will try to launch the browser anyway, but it may not work as expected.");
const tasks: Array<Promise<void>> = []; browser = await chromium.launch({ ...launchOptions, channel: config.playwright.browser });
logger.info(`Spawning ${config.proxy.count} proxies...`); break;
for (let i = 0; i < config.proxy.count; i++) { }
logger.debug(`Spawning proxy ${i + 1}...`);
tasks.push(reflect4.spawn(context, config.playwright.url)); logger.info("Browser launched successfully, enabling adblocker...");
} let blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch);
await Promise.all(tasks);
logger.info("All proxies spawned successfully."); logger.info("Spawning proxies...");
} if (config.proxy.mode === "reflect4") {
const context = await browser.newContext();
process.on("SIGINT", async () => { const tasks: Array<Promise<void>> = [];
logger.info("Received SIGINT. Closing browser..."); logger.info(`Spawning ${config.proxy.count} proxies...`);
for (const context of browser.contexts()) { for (let i = 0; i < config.proxy.count; i++) {
await context.close(); logger.debug(`Spawning proxy ${i + 1}...`);
} tasks.push((async () => {
await browser.close(); const spawnId = `${i + 1}`;
process.exit(); while (true) {
}); try {
await reflect4.spawn(context, blocker, config.playwright.url, config.playwright.change_viewport, spawnId);
} catch (e) {
logger.error(`[${spawnId}] Error while running: ${e}`);
}
logger.warn(`[${spawnId}] Restarting in 3 seconds...`);
await new Promise(resolve => setTimeout(resolve, 3 * 1000));
}
})());
}
await Promise.all(tasks);
logger.info("All proxies spawned successfully.");
}
process.on("SIGINT", async () => {
logger.info("Received SIGINT. Closing browser...");
for (const context of browser.contexts()) {
await context.close();
}
await browser.close();
process.exit();
});

View File

@@ -1,43 +1,62 @@
import { type BrowserContext, type Locator } from "patchright"; import { devices, type BrowserContext, type Locator } from "patchright";
import logger from "../logger"; import logger from "../logger.js";
import * as twitch from "../website/twitch"; import * as twitch from "../website/twitch.js";
import * as constants from "../constants"; import * as constants from "../constants.js";
import type { PlaywrightBlocker } from "@ghostery/adblocker-playwright";
async function spawn(context: BrowserContext, targetUrl: string) {
const spawnId = btoa(Math.random().toString()).substring(4,10); async function spawn(context: BrowserContext, blocker: PlaywrightBlocker, targetUrl: string, changeViewport: boolean = false, spawnId: string = "unknown") {
const server = constants.REFLECT4_SERVERS[Math.floor(Math.random() * constants.REFLECT4_SERVERS.length)]; const server = constants.REFLECT4_SERVERS[Math.floor(Math.random() * constants.REFLECT4_SERVERS.length)];
logger.debug(`[${spawnId}] Using reflect4 server: ${server}`); logger.debug(`[${spawnId}] Using reflect4 server: ${server}`);
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server); logger.debug(`[${spawnId}] New page created, enabling adblocker...`);
let targetInput: Locator | null = null; await blocker.enableBlockingInPage(page as any); // As any because technically patchright Page != playwright Page :D
const allInput = await page.locator("input").all(); if (changeViewport) {
for (const input of allInput) { logger.debug(`[${spawnId}] Changing viewport size...`);
const placeholder = await input.getAttribute("placeholder"); const deviceName = Object.keys(devices)[Math.floor(Math.random() * Object.keys(devices).length)];
if (placeholder?.includes("URL")) { const device = devices[deviceName];
targetInput = input; logger.debug(`[${spawnId}] Using device: ${deviceName}`);
break; await page.setViewportSize(device.viewport);
} }
} try {
if (!targetInput) { await page.goto(server);
logger.error(`[${spawnId}] Failed to find input field for URL input`); } catch (e) {
return; logger.error(`[${spawnId}] Error while navigating to proxy website: ${e}`);
} await page.close();
await targetInput.fill(targetUrl); throw e;
await targetInput.press("Enter"); }
// Keep-alive the page open for 5 minutes then refresh let targetInput: Locator | null = null;
if (targetUrl.startsWith("https://www.twitch.tv/")) { const allInput = await page.locator("input").all();
await twitch.keepAlive(page, spawnId); for (const input of allInput) {
} else { const placeholder = await input.getAttribute("placeholder");
logger.warn(`[${spawnId}] Unsupported URL: ${targetUrl}`); if (placeholder?.includes("URL")) {
logger.warn(`[${spawnId}] Will try to keep alive, but no guarantees`); targetInput = input;
while (true) { break;
await page.waitForTimeout(5 * 60 * 1000); // 5 minutes }
await page.reload(); }
logger.debug(`[${spawnId}] Reloaded page`); if (!targetInput) {
} logger.error(`[${spawnId}] Failed to find input field for URL input`);
} await page.close();
await page.close(); throw new Error(`Failed to find input field for URL input`);
logger.info(`[${spawnId}] Proxy with the server ${server} closed`); }
} await targetInput.fill(targetUrl);
await targetInput.press("Enter");
export { spawn }; logger.info(`[${spawnId}] Navigating to ${targetUrl}`);
await page.waitForTimeout(15000); // Wait for 15 second to let the page load
// Keep-alive the page open for 5 minutes then refresh
if (targetUrl.startsWith("https://www.twitch.tv/")) {
logger.info(`[${spawnId}] Twitch URL detected, using Twitch mode...`);
await twitch.keepAlive(page, spawnId);
} else {
logger.warn(`[${spawnId}] Unsupported URL: ${targetUrl}`);
logger.warn(`[${spawnId}] Will try to keep alive, but no guarantees`);
while (true) {
await page.waitForTimeout(5 * 60 * 1000); // 5 minutes
await page.reload();
logger.debug(`[${spawnId}] Reloaded page`);
}
}
await page.close();
logger.info(`[${spawnId}] Proxy with the server ${server} closed`);
}
export { spawn };

View File

@@ -1,5 +1,13 @@
import type { Page } from "patchright"; import type { Page } from "patchright";
import logger from "../logger"; import logger from "../logger.js";
async function checkConsentButton(page: Page, spawnId: string = "unknown") {
const consentButton = page.locator("button[data-a-target='consent-banner-accept']");
if ((await consentButton.all()).length > 0) {
logger.debug(`[${spawnId}] Consent button found, clicking it...`);
await consentButton.click();
}
}
async function keepAlive(page: Page, spawnId: string = "unknown") { async function keepAlive(page: Page, spawnId: string = "unknown") {
try { try {
@@ -13,7 +21,10 @@ async function keepAlive(page: Page, spawnId: string = "unknown") {
if ((await page.locator(".ScCoreButton-sc-ocjdkq-0.ggPgVz").all()).length > 0) { if ((await page.locator(".ScCoreButton-sc-ocjdkq-0.ggPgVz").all()).length > 0) {
logger.debug(`[${spawnId}] Player encountered an error, refreshing the page...`); logger.debug(`[${spawnId}] Player encountered an error, refreshing the page...`);
await page.reload({timeout: 0, waitUntil: "domcontentloaded"}); await page.reload({timeout: 0, waitUntil: "domcontentloaded"});
waitTime = 0;
continue;
} }
await checkConsentButton(page, spawnId);
if (waitTime > 5 * 60 * 1000) { if (waitTime > 5 * 60 * 1000) {
logger.debug(`[${spawnId}] Waited for more than 5 minutes, refreshing the page...`); logger.debug(`[${spawnId}] Waited for more than 5 minutes, refreshing the page...`);
await page.reload({timeout: 0, waitUntil: "domcontentloaded"}); await page.reload({timeout: 0, waitUntil: "domcontentloaded"});
@@ -21,6 +32,7 @@ async function keepAlive(page: Page, spawnId: string = "unknown") {
} }
} }
} catch (e) { } catch (e) {
await page.close();
logger.error(`[${spawnId}] Error while keeping the page alive: ${e}`); logger.error(`[${spawnId}] Error while keeping the page alive: ${e}`);
} }
} }