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
config.json
build/
# Logs

View File

@@ -2,6 +2,13 @@
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
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",
"start": "node ./dist/index.js",
"start-bun": "bun ./dist/index.js",
"build": "swc ./src -d dist"
"build": "swc ./src -d dist --strip-leading-paths"
},
"devDependencies": {
"@swc-node/register": "^1.10.10",
"@swc-node/register": "^1.11.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.11.20",
"@types/bun": "latest",
"pino-pretty": "^13.0.0"
"@swc/core": "^1.15.11",
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
"typescript": "^5.8.3"
},
"dependencies": {
"patchright": "^1.51.3",
"pino": "^9.6.0",
"playwright": "^1.51.1"
"@ghostery/adblocker-playwright": "^2.14.1",
"patchright": "^1.57.0",
"pino": "^9.14.0",
"pino-pretty": "^13.1.3",
"playwright": "^1.58.2"
},
"trustedDependencies": [
"@swc/core"

View File

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

View File

@@ -1,3 +1,4 @@
const VERSION = "0.1.0";
const REFLECT4_SERVERS = [
"https://www.blockaway.net",
"https://www.croxyproxy.com",
@@ -9,4 +10,4 @@ const REFLECT4_SERVERS = [
"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 fs from 'node:fs';
import * as reflect4 from "./proxy/reflect4";
import logger from "./logger";
import Config from "./config";
const version = "0.1.0";
logger.info(`Castorsrm v${version} - https://github.com/teppyboy/castorsrm`)
logger.warn("This software is provided by the author as is, without any warranty. Use at your own risk.");
let config = new Config();
if (fs.existsSync("config.json")) {
logger.info("Reading configuration from 'config.json'...");
const text = await fs.promises.readFile("config.json", "utf-8");
config = Config.fromJSON(text);
} else {
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.level = process.env.LOG_LEVEL || config.logger.level;
logger.info(`Logger level set to '${logger.level}'`);
let browser: Browser;
let launchOptions: LaunchOptions = {
headless: config.playwright.headless,
}
logger.info("Launching browser...");
logger.debug(`Launch options: ${JSON.stringify(launchOptions)}`);
switch (config.playwright.browser) {
case "chromium":
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 });
break;
case "chrome":
logger.info("Using Google Chrome as the browser provider.");
browser = await chromium.launch({ ...launchOptions, channel: "chrome" });
break;
case "cdp":
logger.info("Using Chrome DevTools Protocol (CDP) for browser connection.");
browser = await chromium.connectOverCDP(config.playwright.cdp);
break;
default:
logger.warn(`Unsupported browser channel: '${config.playwright.browser}'`);
logger.warn("Castorsrm will try to launch the browser anyway, but it may not work as expected.");
browser = await chromium.launch({ ...launchOptions, channel: config.playwright.browser });
break;
}
logger.info("Browser launched successfully, spawning proxies");
if (config.proxy.mode === "reflect4") {
const context = await browser.newContext();
const tasks: Array<Promise<void>> = [];
logger.info(`Spawning ${config.proxy.count} proxies...`);
for (let i = 0; i < config.proxy.count; i++) {
logger.debug(`Spawning proxy ${i + 1}...`);
tasks.push(reflect4.spawn(context, config.playwright.url));
}
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();
});
import { chromium, type Browser, type LaunchOptions } from "patchright";
import { PlaywrightBlocker } from '@ghostery/adblocker-playwright';
import fs from 'node:fs';
import * as reflect4 from "./proxy/reflect4.js";
import logger from "./logger.js";
import Config from "./config.js";
import { VERSION } from "./constants.js";
logger.info(`Castorsrm v${VERSION}`)
logger.warn("This software is provided by the author as is, without any warranty. Use at your own risk.");
let config = new Config();
if (fs.existsSync("config.json")) {
logger.info("Reading configuration from 'config.json'...");
const text = await fs.promises.readFile("config.json", "utf-8");
config = Config.fromJSON(text);
} else {
logger.info("No configuration file found. Using the default configuration.");
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());
// Validate configuration
if (config.proxy.count < 1) {
logger.error("Proxy count must be greater than 0.");
process.exit(1);
}
logger.level = process.env.LOG_LEVEL || config.logger.level;
logger.info(`Logger level set to '${logger.level}'`);
let browser: Browser;
let launchOptions: LaunchOptions = {
headless: config.playwright.headless,
}
logger.info("Launching browser...");
logger.debug(`Launch options: ${JSON.stringify(launchOptions)}`);
switch (config.playwright.browser) {
case "chromium":
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 });
break;
case "chrome":
logger.info("Using Google Chrome as the browser provider.");
browser = await chromium.launch({ ...launchOptions, channel: "chrome" });
break;
case "cdp":
logger.info("Using Chrome DevTools Protocol (CDP) for browser connection.");
browser = await chromium.connectOverCDP(config.playwright.cdp);
break;
default:
logger.warn(`Unsupported browser channel: '${config.playwright.browser}'`);
logger.warn("Castorsrm will try to launch the browser anyway, but it may not work as expected.");
browser = await chromium.launch({ ...launchOptions, channel: config.playwright.browser });
break;
}
logger.info("Browser launched successfully, enabling adblocker...");
let blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch);
logger.info("Spawning proxies...");
if (config.proxy.mode === "reflect4") {
const context = await browser.newContext();
const tasks: Array<Promise<void>> = [];
logger.info(`Spawning ${config.proxy.count} proxies...`);
for (let i = 0; i < config.proxy.count; i++) {
logger.debug(`Spawning proxy ${i + 1}...`);
tasks.push((async () => {
const spawnId = `${i + 1}`;
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 logger from "../logger";
import * as twitch from "../website/twitch";
import * as constants from "../constants";
async function spawn(context: BrowserContext, targetUrl: string) {
const spawnId = btoa(Math.random().toString()).substring(4,10);
const server = constants.REFLECT4_SERVERS[Math.floor(Math.random() * constants.REFLECT4_SERVERS.length)];
logger.debug(`[${spawnId}] Using reflect4 server: ${server}`);
const page = await context.newPage();
await page.goto(server);
let targetInput: Locator | null = null;
const allInput = await page.locator("input").all();
for (const input of allInput) {
const placeholder = await input.getAttribute("placeholder");
if (placeholder?.includes("URL")) {
targetInput = input;
break;
}
}
if (!targetInput) {
logger.error(`[${spawnId}] Failed to find input field for URL input`);
return;
}
await targetInput.fill(targetUrl);
await targetInput.press("Enter");
// Keep-alive the page open for 5 minutes then refresh
if (targetUrl.startsWith("https://www.twitch.tv/")) {
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 };
import { devices, type BrowserContext, type Locator } from "patchright";
import logger from "../logger.js";
import * as twitch from "../website/twitch.js";
import * as constants from "../constants.js";
import type { PlaywrightBlocker } from "@ghostery/adblocker-playwright";
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)];
logger.debug(`[${spawnId}] Using reflect4 server: ${server}`);
const page = await context.newPage();
logger.debug(`[${spawnId}] New page created, enabling adblocker...`);
await blocker.enableBlockingInPage(page as any); // As any because technically patchright Page != playwright Page :D
if (changeViewport) {
logger.debug(`[${spawnId}] Changing viewport size...`);
const deviceName = Object.keys(devices)[Math.floor(Math.random() * Object.keys(devices).length)];
const device = devices[deviceName];
logger.debug(`[${spawnId}] Using device: ${deviceName}`);
await page.setViewportSize(device.viewport);
}
try {
await page.goto(server);
} catch (e) {
logger.error(`[${spawnId}] Error while navigating to proxy website: ${e}`);
await page.close();
throw e;
}
let targetInput: Locator | null = null;
const allInput = await page.locator("input").all();
for (const input of allInput) {
const placeholder = await input.getAttribute("placeholder");
if (placeholder?.includes("URL")) {
targetInput = input;
break;
}
}
if (!targetInput) {
logger.error(`[${spawnId}] Failed to find input field for URL input`);
await page.close();
throw new Error(`Failed to find input field for URL input`);
}
await targetInput.fill(targetUrl);
await targetInput.press("Enter");
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 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") {
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) {
logger.debug(`[${spawnId}] Player encountered an error, refreshing the page...`);
await page.reload({timeout: 0, waitUntil: "domcontentloaded"});
waitTime = 0;
continue;
}
await checkConsentButton(page, spawnId);
if (waitTime > 5 * 60 * 1000) {
logger.debug(`[${spawnId}] Waited for more than 5 minutes, refreshing the page...`);
await page.reload({timeout: 0, waitUntil: "domcontentloaded"});
@@ -21,6 +32,7 @@ async function keepAlive(page: Page, spawnId: string = "unknown") {
}
}
} catch (e) {
await page.close();
logger.error(`[${spawnId}] Error while keeping the page alive: ${e}`);
}
}