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') // Wait longer for the layout menu to appear await new Promise(res => setTimeout(res, 2000)) // Take a debug screenshot of the layout menu const debugMenuPath = path.resolve(`debug_layout_menu_${layout.replace(/\s+/g, '_')}.png`) as `${string}.png` await page.screenshot({ path: debugMenuPath }) console.log('Layout menu screenshot saved:', debugMenuPath) // Look for layout menu items with more specific selectors const layoutItemSelectors = [ `[data-name="chart-layout-list-item"]`, `[data-testid*="layout"]`, `.layout-item`, `[role="option"]`, `[role="menuitem"]`, `.tv-dropdown-behavior__item`, `.tv-menu__item`, `li[data-value*="${layout}"]`, `div[data-layout-name="${layout}"]` ] let layoutItem = null let foundMethod = '' let foundElement = false // Try to find layout item by exact text match first for (const selector of layoutItemSelectors) { try { console.log(`Trying selector: ${selector}`) const items = await page.$$(selector) console.log(`Found ${items.length} items with selector: ${selector}`) for (const item of items) { const text = await page.evaluate(el => { const element = el as HTMLElement return (element.innerText || element.textContent || '').trim() }, item) console.log(`Item text: "${text}"`) if (text && text.toLowerCase() === layout.toLowerCase()) { layoutItem = item foundMethod = `exact match with selector: ${selector}` break } } if (layoutItem) break } catch (e) { console.log(`Error with selector ${selector}:`, e) } } // If no exact match, try partial match if (!layoutItem) { 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 || '').trim() }, item) if (text && text.toLowerCase().includes(layout.toLowerCase())) { layoutItem = item foundMethod = `partial match with selector: ${selector}` break } } if (layoutItem) break } catch (e) { // Continue to next selector } } } // If still no match, try a more comprehensive search if (!layoutItem) { console.log('No layout item found with standard selectors, trying comprehensive search...') const foundElement = await page.evaluate((layout) => { const allElements = Array.from(document.querySelectorAll('*')) // Look for elements that contain the layout name const candidates = allElements.filter(el => { const text = (el.textContent || '').trim() return text && text.toLowerCase().includes(layout.toLowerCase()) }) console.log('Found candidates:', candidates.map(el => ({ tag: el.tagName, text: el.textContent?.trim(), classes: el.className }))) // Prioritize clickable elements const clickable = candidates.find(el => el.tagName === 'BUTTON' || el.tagName === 'A' || el.hasAttribute('role') || el.classList.contains('item') || el.classList.contains('option') || el.classList.contains('menu') ) if (clickable) { (clickable as HTMLElement).click() return true } // Fall back to exact text match const exactMatch = candidates.find(el => (el.textContent || '').trim().toLowerCase() === layout.toLowerCase() ) if (exactMatch) { (exactMatch as HTMLElement).click() return true } return false }, layout) if (foundElement) { foundMethod = 'comprehensive search with click' console.log(`Found and clicked layout item "${layout}" using: ${foundMethod}`) } } if (layoutItem) { console.log(`Found layout item "${layout}" using: ${foundMethod}`) await layoutItem.click() console.log('Clicked layout item:', layout) // Wait for layout to actually load await new Promise(res => setTimeout(res, 5000)) // Take a screenshot after layout change const debugAfterPath = path.resolve(`debug_after_layout_${layout.replace(/\s+/g, '_')}.png`) as `${string}.png` await page.screenshot({ path: debugAfterPath }) console.log('After layout change screenshot saved:', debugAfterPath) } else if (foundElement && foundMethod === 'comprehensive search with click') { console.log('Layout item was clicked via comprehensive search') // Wait for layout to actually load await new Promise(res => setTimeout(res, 5000)) // Take a screenshot after layout change const debugAfterPath = path.resolve(`debug_after_layout_${layout.replace(/\s+/g, '_')}.png`) as `${string}.png` await page.screenshot({ path: debugAfterPath }) console.log('After layout change screenshot saved:', debugAfterPath) } else { console.log('Layout item not found with any method') // List all text content on the page for debugging const allTexts = await page.evaluate(() => { const elements = Array.from(document.querySelectorAll('*')) return elements .map(el => (el.textContent || '').trim()) .filter(text => text && text.length > 0 && text.length < 100) .slice(0, 50) // Limit to first 50 for debugging }) console.log('Available texts on page:', allTexts) } } else { console.log('Layout button not found, skipping layout loading') } console.log('Layout loading completed for:', layout) } catch (e: any) { const debugLayoutErrorPath = path.resolve(`debug_layout_error_${layout.replace(/\s+/g, '_')}.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()