-
Notifications
You must be signed in to change notification settings - Fork 284
Open
Description
So I am trying to cloud connection to scrape TradingView, but each time I run the script it fails, should I be doing this with a locally run lightpanda browser or what is the issue?
* ------------------------------------------------
*
* Description:
* - Uses Puppeteer Core connected to a remote browser (Lightpanda Cloud or similar)
* - Captures TradingView chart screenshots with optional indicators
* - Manages a browser session with auto-timeout and a request queue
*
* Environment Variables:
* - LIGHTPANDA_TOKEN: Token for remote browser connection (required)
* - TV_SESSION_ID: (optional) TradingView session cookie
* - TV_SESSION_ID_SIGN: (optional) TradingView session signature cookie
* - IGNORE_INDICATORS_CHARTS: (optional) Comma-separated chart IDs to skip indicator loading
* - BASE_URL: (optional) Override default TradingView base URL
*/
const puppeteer = require("puppeteer-core");
const BASE_URL = process.env.BASE_URL || "https://www.tradingview.com";
let browserPage = undefined;
let browser = undefined;
let browserLastUsed = Date.now();
let isLightpanda = false;
const BROWSER_IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
const screenshotQueue = [];
let isProcessingScreenshot = false;
// Get or create Puppeteer browser connection
const getBrowser = async () => {
console.log("[Puppeteer] Setup start:", new Date().toISOString());
try {
if (!browser) {
const lightpandaToken = process.env.LIGHTPANDA_TOKEN;
if (!lightpandaToken) throw new Error("LIGHTPANDA_TOKEN is required");
console.log("[Puppeteer] Connecting to remote browser (Lightpanda)...");
const wsEndpoint = `wss://euwest.cloud.lightpanda.io/ws?token=${lightpandaToken}`;
browser = await puppeteer.connect({
browserWSEndpoint: wsEndpoint,
defaultViewport: null
});
isLightpanda = true;
console.log("[Puppeteer] Connected successfully");
browser.on("disconnected", () => {
console.error("[Puppeteer] Browser disconnected unexpectedly");
browser = null;
browserPage = null;
isLightpanda = false;
});
}
// Create new page if needed
if (!browserPage || browserPage.isClosed()) {
console.log("[Puppeteer] Creating new page...");
browserPage = await browser.newPage();
await browserPage.setViewport({ width: 1920, height: 1080 });
// Optional cookies
if (process.env.TV_SESSION_ID && process.env.TV_SESSION_ID_SIGN) {
await browserPage.setCookie(
{ name: "sessionid", value: process.env.TV_SESSION_ID, domain: new URL(BASE_URL).hostname, path: "/" },
{ name: "sessionid_sign", value: process.env.TV_SESSION_ID_SIGN, domain: new URL(BASE_URL).hostname, path: "/" }
);
console.log("[Puppeteer] Added TradingView cookies");
}
// Clipboard permissions
try {
const context = browser.defaultBrowserContext();
await context.overridePermissions(BASE_URL, ["clipboard-read", "clipboard-write"]);
} catch {
console.log("[Puppeteer] Clipboard permissions not supported (non-critical)");
}
// Request interception (skip ads/tracking)
await browserPage.setRequestInterception(true);
browserPage.on("request", (request) => {
const url = request.url();
const blockList = ["adzerk", "doubleclick", "google-analytics", "mixpanel", "zedo"];
if (blockList.some((e) => url.includes(e))) request.abort();
else request.continue();
});
}
console.log("[Puppeteer] Browser ready");
return browserPage;
} catch (error) {
console.error("[Puppeteer] Setup failed:", error.message);
throw error;
}
};
// Close browser cleanly
const closeBrowser = async () => {
try {
if (browserPage) await browserPage.close();
if (browser) await browser.disconnect();
browser = undefined;
browserPage = undefined;
console.log("[Puppeteer] Browser closed");
} catch (error) {
console.error("[Puppeteer] Error closing browser:", error.message);
}
};
// Auto-close after idle timeout
setInterval(() => {
if (browserPage && Date.now() - browserLastUsed > BROWSER_IDLE_TIMEOUT) {
console.log("[Puppeteer] Idle timeout reached — closing browser");
closeBrowser();
}
}, 2 * 60 * 1000);
// Process queue
const processScreenshotQueue = async () => {
if (isProcessingScreenshot || screenshotQueue.length === 0) return;
isProcessingScreenshot = true;
const { chart, ticker, timeframe, indicator, resolve, reject } = screenshotQueue.shift();
console.log(`[Queue] Processing screenshot (remaining: ${screenshotQueue.length})`);
try {
const result = await screenshotInternal(chart, ticker, timeframe, indicator);
resolve(result);
} catch (error) {
reject(error);
} finally {
isProcessingScreenshot = false;
if (screenshotQueue.length > 0) setImmediate(processScreenshotQueue);
}
};
// Public screenshot API
const screenshot = async (chart, ticker, timeframe = null, indicator = null) => {
return new Promise((resolve, reject) => {
screenshotQueue.push({ chart, ticker, timeframe, indicator, resolve, reject });
console.log(`[Queue] Queued screenshot (length: ${screenshotQueue.length})`);
processScreenshotQueue();
});
};
// Internal screenshot function
const screenshotInternal = async (chart, ticker, timeframe = null, indicator = null) => {
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
console.log(`[Screenshot:${id}] Start for ${chart}/${ticker}`);
const url = `${BASE_URL}/chart/${chart}?symbol=${ticker}${timeframe ? `&interval=${timeframe}` : ""}`;
try {
browserPage = browserPage || (await getBrowser());
browserLastUsed = Date.now();
await browserPage.goto(url, { waitUntil: "domcontentloaded" });
await browserPage.waitForSelector("#header-toolbar-screenshot", { visible: true, timeout: 15000 });
console.log(`[Screenshot:${id}] Chart loaded successfully`);
// Optional: add indicator
const ignoredCharts = (process.env.IGNORE_INDICATORS_CHARTS || "").split(",");
if (indicator && !ignoredCharts.includes(chart)) {
try {
await browserPage.keyboard.press("Slash");
await browserPage.waitForSelector("#indicators-dialog-search-input", { visible: true });
await browserPage.type("#indicators-dialog-search-input", indicator, { delay: 20 });
await browserPage.waitForSelector(`[data-role="list-item"][data-title*="${indicator}"]`, { visible: true });
await browserPage.click(`[data-role="list-item"][data-title*="${indicator}"]`);
await browserPage.keyboard.press("Escape");
console.log(`[Screenshot:${id}] Indicator '${indicator}' added`);
} catch {
console.log(`[Screenshot:${id}] Failed to add indicator (non-critical)`);
}
}
// Trigger TradingView screenshot
await browserPage.keyboard.down("Alt");
await browserPage.keyboard.press("KeyS");
await browserPage.keyboard.up("Alt");
await browserPage.waitForFunction(
() => document.body.innerText.includes("copied to clipboard"),
{ timeout: 30000 }
);
const imageUrl = await browserPage.evaluate("navigator.clipboard.readText()");
const imageId = imageUrl.split("/").reverse()[1];
const finalUrl = `${BASE_URL.replace("www", "s3")}/snapshots/${imageId[0].toLowerCase()}/${imageId}.png`;
console.log(`[Screenshot:${id}] Completed: ${finalUrl}`);
return finalUrl;
} catch (error) {
console.error(`[Screenshot:${id}] Failed: ${error.message}`);
return undefined;
}
};
module.exports = { screenshot, closeBrowser };
output:
[QUEUE] Screenshot queued. Queue length: 1
[QUEUE] Processing screenshot request. Queue length: 0
[SCREENSHOT:1761576969880] START
[SCREENSHOT:1761576969880] Chart: *******, Ticker: ********, Timeframe: 30, Indicator: none
[SCREENSHOT:1761576969880] URL: https://www.tradingview.com/chart/*******?symbol=******&interval=30
[SCREENSHOT:1761576969880] Getting browser page...
[INFO] Setup start: 2025-10-27T14:56:09.880Z
[INFO] Connecting to remote browser...
[INFO] Connected to remote browser successfully
[INFO] Creating new page...
[INFO] New page created
[INFO] Cookies added (sessionid + sessionid_sign)
[INFO] Clipboard permissions not supported - continuing anyway
[INFO] Skipping request interception for remote browser
[INFO] Setup complete: 2025-10-27T14:56:11.019Z
[SCREENSHOT:1761576969880] Browser page ready
[SCREENSHOT:1761576969880] Navigating to chart URL...
[ERROR] Browser disconnected unexpectedly
[SCREENSHOT:1761576969880] SCREENSHOT FAILED
[ERROR] Navigating frame was detached
at LifecycleWatcher.js
at CdpFrame.goto
at screenshotInternal
at processScreenshotQueue
[SCREENSHOT:1761576969880] Full error: { "cause": {} }
[INFO] Screenshot failed - returned undefined
[INFO] Screenshot request processing complete (partial failure)
Metadata
Metadata
Assignees
Labels
No labels