import puppeteer, { Browser, Page, Frame } from 'puppeteer' import path from 'path' import fs from 'fs/promises' import { settingsManager } from './settings' const TRADINGVIEW_EMAIL = process.env.TRADINGVIEW_EMAIL const TRADINGVIEW_PASSWORD = process.env.TRADINGVIEW_PASSWORD const TRADINGVIEW_LAYOUTS = (process.env.TRADINGVIEW_LAYOUTS || '').split(',').map(l => l.trim()) const PUPPETEER_EXECUTABLE_PATH = process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium' export class TradingViewCapture { private browser: Browser | null = null private page: Page | null = null private loggedIn = false async init() { if (!this.browser) { this.browser = await puppeteer.launch({ headless: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--disable-gpu' ], executablePath: PUPPETEER_EXECUTABLE_PATH }) console.log('Puppeteer browser launched') } if (!this.page) { this.page = await this.browser.newPage() await this.page.setViewport({ width: 1920, height: 1080 }) console.log('Puppeteer page created') } if (!this.loggedIn) { console.log('Logging in to TradingView...') await this.login() this.loggedIn = true console.log('Logged in to TradingView') } return this.page } async login() { if (!TRADINGVIEW_EMAIL || !TRADINGVIEW_PASSWORD) { throw new Error('TradingView credentials not set in .env') } const page = this.page || (await this.browser!.newPage()) console.log('Navigating to TradingView login page...') await page.goto('https://www.tradingview.com/#signin', { waitUntil: 'networkidle2' }) // Check if we're already logged in try { const loggedInIndicator = await page.waitForSelector('.tv-header__user-menu-button, [data-name="header-user-menu"]', { timeout: 3000 }) if (loggedInIndicator) { console.log('Already logged in to TradingView') return } } catch (e) { console.log('Not logged in yet, proceeding with login...') } try { // Wait for the login modal to appear and look for email input directly console.log('Looking for email input field...') // Try to find the email input field directly (new TradingView layout) const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 }) if (emailInput) { console.log('Found email input field directly') await emailInput.click() // Click to focus await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 }) // Find password field const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 }) if (!passwordInput) { throw new Error('Could not find password input field') } await passwordInput.click() // Click to focus await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 }) // Find and click the sign in button const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 }) if (!signInButton) { // Try to find button with sign in text const buttons = await page.$$('button') let foundButton = null for (const btn of buttons) { const text = await page.evaluate(el => el.innerText || el.textContent, btn) if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) { foundButton = btn break } } if (!foundButton) { throw new Error('Could not find sign in button') } await foundButton.click() } else { await signInButton.click() } } else { throw new Error('Could not find email input field') } } catch (e) { // Fallback: try to find email button first console.log('Fallback: looking for email button...') try { await page.waitForSelector('button', { timeout: 15000 }) const buttons = await page.$$('button') let emailBtn = null // Look for email button with various text patterns for (const btn of buttons) { const text = await page.evaluate(el => el.innerText || el.textContent, btn) if (text && ( text.trim().toLowerCase().includes('email') || text.trim().toLowerCase().includes('sign in with email') || text.trim().toLowerCase().includes('continue with email') )) { emailBtn = btn break } } if (emailBtn) { console.log('Found email button, clicking...') await emailBtn.click() await new Promise(res => setTimeout(res, 1000)) // Now fill in the form const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 }) if (!emailInput) { throw new Error('Could not find email input field after clicking email button') } await emailInput.click() // Click to focus await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 }) const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 }) if (!passwordInput) { throw new Error('Could not find password input field after clicking email button') } await passwordInput.click() // Click to focus await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 }) const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 }) if (!signInButton) { // Try to find button with sign in text const buttons = await page.$$('button') let foundButton = null for (const btn of buttons) { const text = await page.evaluate(el => el.innerText || el.textContent, btn) if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) { foundButton = btn break } } if (!foundButton) { throw new Error('Could not find sign in button after clicking email button') } await foundButton.click() } else { await signInButton.click() } } else { throw new Error('Could not find email button') } } catch (e2) { console.error('Could not find or click email button:', e2) const errorMessage = e2 instanceof Error ? e2.message : String(e2) throw new Error('Could not find or click email button on TradingView login page. ' + errorMessage) } } // Wait for navigation or dashboard (main page) try { console.log('Waiting for login to complete...') await page.waitForSelector('.tv-header__user-menu-button, .chart-container, [data-name="header-user-menu"]', { timeout: 30000 }) } catch (e) { console.error('Login navigation did not complete.') throw new Error('Login navigation did not complete.') } console.log('TradingView login complete') } async capture(symbol: string, filename: string, layouts?: string[], timeframe?: string) { console.log('Working directory:', process.cwd()) // Load settings and update if provided const settings = await settingsManager.loadSettings() if (symbol && symbol !== settings.symbol) { await settingsManager.setSymbol(symbol) } if (timeframe && timeframe !== settings.timeframe) { await settingsManager.setTimeframe(timeframe) } if (layouts && JSON.stringify(layouts) !== JSON.stringify(settings.layouts)) { await settingsManager.setLayouts(layouts) } // Use saved settings if not provided const finalSymbol = symbol || settings.symbol const finalTimeframe = timeframe || settings.timeframe const finalLayouts = layouts || settings.layouts console.log('Using settings:', { symbol: finalSymbol, timeframe: finalTimeframe, layouts: finalLayouts }) const page = await this.init() // Add timeframe to TradingView URL if provided let url = `https://www.tradingview.com/chart/?symbol=${finalSymbol}` if (finalTimeframe) { url += `&interval=${encodeURIComponent(finalTimeframe)}` } try { console.log('Navigating to TradingView chart:', url) await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 }) console.log('Successfully navigated to chart') } catch (e: any) { console.error('Failed to load TradingView chart page:', e) throw new Error('Failed to load TradingView chart page: ' + (e.message || e)) } // Capture screenshots for each layout const screenshots: string[] = [] for (let i = 0; i < finalLayouts.length; i++) { const layout = finalLayouts[i] console.log(`Processing layout ${i + 1}/${finalLayouts.length}: ${layout}`) // Load the layout await this.loadLayout(page, layout) // Wait for layout to load await new Promise(res => setTimeout(res, 3000)) // Generate filename for this layout const layoutFilename = filename.replace('.png', `_${layout}.png`) const screenshotsDir = path.join(process.cwd(), 'screenshots') await fs.mkdir(screenshotsDir, { recursive: true }) const filePath = path.join(screenshotsDir, layoutFilename) try { await page.screenshot({ path: filePath as `${string}.png`, type: 'png' }) console.log(`Screenshot saved for layout ${layout}:`, filePath) screenshots.push(filePath) } catch (e: any) { const debugScreenshotErrorPath = path.resolve(`debug_screenshot_error_${layout}.png`) as `${string}.png` await page.screenshot({ path: debugScreenshotErrorPath }) console.error(`Failed to capture screenshot for layout ${layout}:`, e) console.error('Screenshot on screenshot error:', debugScreenshotErrorPath) throw new Error(`Failed to capture screenshot for layout ${layout}: ` + (e.message || e)) } } return screenshots } private async loadLayout(page: Page, layout: string): Promise { try { console.log('Trying to load layout:', layout) // Try multiple selectors for the layout button const layoutSelectors = [ '[data-name="load-chart-layout-dialog"]', '[data-name="layouts-menu"]', '[data-name="chart-layout-button"]', 'button[title*="Layout" i]', 'button[aria-label*="Layout" i]', '[data-testid*="layout"]' ] let layoutButton = null for (const selector of layoutSelectors) { try { layoutButton = await page.waitForSelector(selector, { timeout: 3000 }) if (layoutButton) { console.log('Found layout button with selector:', selector) break } } catch (e) { // Continue to next selector } } if (!layoutButton) { // Try to find layout button by text content const buttons = await page.$$('button, [role="button"]') for (const btn of buttons) { const text = await page.evaluate(el => { const element = el as HTMLElement return element.innerText || element.textContent || element.title || element.getAttribute('aria-label') }, btn) if (text && text.toLowerCase().includes('layout')) { layoutButton = btn console.log('Found layout button by text:', text) break } } } if (layoutButton) { await layoutButton.click() console.log('Clicked layout button') await new Promise(res => setTimeout(res, 1000)) // Look for search input or layout items const searchSelectors = [ 'input[name="search"]', 'input[placeholder*="search" i]', 'input[type="search"]', 'input[data-name="search"]' ] let searchInput = null for (const selector of searchSelectors) { try { searchInput = await page.waitForSelector(selector, { timeout: 3000 }) if (searchInput) break } catch (e) { // Continue to next selector } } if (searchInput) { console.log('Found search input, typing layout name...') await searchInput.type(layout, { delay: 50 }) await new Promise(res => setTimeout(res, 2000)) // Try to find and click the layout item const layoutItemSelectors = [ '[data-name="chart-layout-list-item"]', '[data-testid*="layout-item"]', '.layout-item', '[role="option"]' ] let layoutItem = null for (const selector of layoutItemSelectors) { try { const items = await page.$$(selector) for (const item of items) { const text = await page.evaluate(el => { const element = el as HTMLElement return element.innerText || element.textContent }, item) if (text && text.toLowerCase().includes(layout.toLowerCase())) { layoutItem = item break } } if (layoutItem) break } catch (e) { // Continue to next selector } } if (layoutItem) { await layoutItem.click() console.log('Clicked layout item:', layout) } else { console.log('Layout item not found, trying generic approach...') await page.evaluate((layout) => { const items = Array.from(document.querySelectorAll('*')) const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase())) if (item) (item as HTMLElement).click() }, layout) } } else { console.log('Search input not found, trying to find layout directly...') await page.evaluate((layout) => { const items = Array.from(document.querySelectorAll('*')) const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase())) if (item) (item as HTMLElement).click() }, layout) } await new Promise(res => setTimeout(res, 4000)) console.log('Layout loaded:', layout) } else { console.log('Layout button not found, skipping layout loading') } } catch (e: any) { const debugLayoutErrorPath = path.resolve('debug_layout_error.png') as `${string}.png` await page.screenshot({ path: debugLayoutErrorPath }) console.error('TradingView layout not found or could not be loaded:', e) console.log('Continuing without layout...') // Don't throw error, just continue without layout } } } export const tradingViewCapture = new TradingViewCapture()