import puppeteer, { Browser, Page, Frame } from 'puppeteer' import path from 'path' import fs from 'fs/promises' import { settingsManager } from './settings' // Video recording support - Simple implementation without external dependencies let isRecordingSupported = true try { // Test if we can use basic screenshot recording require('fs/promises') } catch (e) { console.warn('Basic video recording not available') isRecordingSupported = false } 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' // Layout name to URL mapping const LAYOUT_URLS: { [key: string]: string } = { 'ai': 'Z1TzpUrf', 'Diy module': 'vWVvjLhP', // Add more layout mappings as needed } // Construct layout URL with hash parameters const getLayoutUrl = (layoutId: string, symbol: string, timeframe?: string): string => { const baseParams = `symbol=${symbol}${timeframe ? `&interval=${encodeURIComponent(timeframe)}` : ''}` return `https://www.tradingview.com/chart/${layoutId}/#${baseParams}` } export class TradingViewCapture { private browser: Browser | null = null private page: Page | null = null private loggedIn = false private recorder: any = null private async debugScreenshot(step: string, page: Page): Promise { try { const timestamp = Date.now() const filename = `debug_${step.replace(/[^a-zA-Z0-9]/g, '_')}_${timestamp}.png` const screenshotsDir = path.join(process.cwd(), 'screenshots') await fs.mkdir(screenshotsDir, { recursive: true }) const filePath = path.join(screenshotsDir, filename) await page.screenshot({ path: filePath as `${string}.png`, type: 'png', fullPage: true }) // Also get page info for debugging const pageInfo = await page.evaluate(() => ({ url: window.location.href, title: document.title, hasChart: document.querySelector('.chart-container, [data-name="chart"], canvas') !== null, bodyText: document.body.textContent?.substring(0, 500) || '' })) console.log(`🔍 DEBUG Screenshot [${step}]: ${filePath}`) console.log(`📄 Page Info:`, pageInfo) } catch (e) { console.error(`Failed to take debug screenshot for step ${step}:`, e) } } async init() { if (!this.browser) { // Check for debug mode from environment variable const isDebugMode = process.env.TRADINGVIEW_DEBUG === 'true' const isDocker = process.env.DOCKER_ENV === 'true' || process.env.NODE_ENV === 'production' // Docker-optimized browser args const dockerArgs = [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--disable-gpu', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-features=TranslateUI', '--disable-ipc-flooding-protection', '--memory-pressure-off', '--max_old_space_size=4096' ] // Additional args for non-Docker debug mode const debugArgs = isDebugMode && !isDocker ? ['--start-maximized'] : [] this.browser = await puppeteer.launch({ headless: isDocker ? true : !isDebugMode, // Always headless in Docker devtools: isDebugMode && !isDocker, // DevTools only in local debug mode slowMo: isDebugMode ? 250 : 0, // Slow down actions in debug mode args: [...dockerArgs, ...debugArgs], executablePath: PUPPETEER_EXECUTABLE_PATH }) const mode = isDocker ? 'Docker headless mode' : (isDebugMode ? 'visible debug mode' : 'headless mode') console.log(`Puppeteer browser launched (${mode})`) } 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() 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()) // Start video recording for login process await this.startVideoRecording('login_process') console.log('Navigating to TradingView login page...') await page.goto('https://www.tradingview.com/#signin', { waitUntil: 'networkidle2' }) // Debug screenshot after initial navigation await this.debugScreenshot('login_01_initial_page', page) // Check if we're already properly logged in with our account try { const loggedInIndicator = await page.waitForSelector('.tv-header__user-menu-button, [data-name="header-user-menu"]', { timeout: 3000 }) if (loggedInIndicator) { // Check if we're logged in with our specific account by looking for account-specific elements const isProperlyLoggedIn = await page.evaluate(() => { // Look for specific logged-in indicators that show we have an actual account const hasUserMenu = document.querySelector('.tv-header__user-menu-button, [data-name="header-user-menu"]') !== null const notGuestSession = !document.body.textContent?.includes('Guest') && !document.body.textContent?.includes('Sign up') && !document.body.textContent?.includes('Get started for free') return hasUserMenu && notGuestSession }) if (isProperlyLoggedIn) { console.log('Already properly logged in to TradingView with account') await this.debugScreenshot('login_02_already_logged_in', page) this.loggedIn = true return } else { console.log('Detected guest session, forcing proper login...') await this.debugScreenshot('login_02b_guest_session_detected', page) // Force logout first, then login try { await page.goto('https://www.tradingview.com/accounts/logout/', { waitUntil: 'networkidle2', timeout: 10000 }) await new Promise(res => setTimeout(res, 2000)) } catch (e) { console.log('Logout attempt completed, proceeding with login...') } } } } catch (e) { console.log('Not logged in yet, proceeding with login...') await this.debugScreenshot('login_03_not_logged_in', page) } // Reset login flag since we need to login this.loggedIn = false // Navigate to fresh login page console.log('Navigating to fresh login page...') await page.goto('https://www.tradingview.com/accounts/signin/', { waitUntil: 'networkidle2', timeout: 30000 }) await this.debugScreenshot('login_04_fresh_login_page', page) try { // Wait for the page to load and look for login form console.log('Looking for login form...') await page.waitForSelector('form, input[type="email"], input[name="username"]', { timeout: 10000 }) // Look for email input field with multiple selectors let emailInput = await page.$('input[name="username"]') || await page.$('input[name="email"]') || await page.$('input[type="email"]') || await page.$('input[placeholder*="email" i]') if (!emailInput) { // Try to find and click "Email" button if login options are presented console.log('Looking for email login option...') const emailButton = await page.evaluateHandle(() => { const buttons = Array.from(document.querySelectorAll('button, a, div[role="button"]')) return buttons.find(btn => { const text = btn.textContent?.toLowerCase() || '' return text.includes('email') || text.includes('continue with email') || text.includes('sign in with email') }) }) if (emailButton.asElement()) { console.log('Found email login button, clicking...') const elementHandle = emailButton.asElement() as any if (elementHandle) { await elementHandle.click() } await new Promise(res => setTimeout(res, 2000)) await this.debugScreenshot('login_04b_after_email_button', page) // Now look for email input again emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"]', { timeout: 10000 }) } } if (!emailInput) { throw new Error('Could not find email input field') } console.log('Found email input, filling credentials...') 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"]', { 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 }) await this.debugScreenshot('login_05_credentials_filled', page) // Find and click the sign in button let signInButton = await page.$('button[type="submit"]') if (!signInButton) { // Look for button with sign in text signInButton = await page.evaluateHandle(() => { const buttons = Array.from(document.querySelectorAll('button')) return buttons.find(btn => { const text = btn.textContent?.toLowerCase() || '' return text.includes('sign in') || text.includes('login') || text.includes('submit') }) }).then(handle => handle.asElement()) as any } if (!signInButton) { throw new Error('Could not find sign in button') } console.log('Clicking sign in button...') await signInButton.click() await this.debugScreenshot('login_06_after_signin_click', page) } 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 }) // Check if we're on the Supercharts selection page const isSuperchartsPage = await page.evaluate(() => { const text = document.body.textContent || '' return text.includes('Supercharts') && text.includes('The one terminal to rule them all') }) if (isSuperchartsPage) { console.log('🎯 On Supercharts selection page, clicking Supercharts...') // Look for Supercharts button/link let superchartsClicked = false // Try different approaches to find and click Supercharts try { // Approach 1: Look for direct link to chart const chartLink = await page.$('a[href*="/chart"]') if (chartLink) { console.log('Found direct chart link, clicking...') await chartLink.click() superchartsClicked = true } } catch (e) { console.log('Direct chart link not found, trying text-based search...') } if (!superchartsClicked) { // Approach 2: Find by text content const clicked = await page.evaluate(() => { const elements = Array.from(document.querySelectorAll('a, button, div[role="button"]')) for (const el of elements) { if (el.textContent?.includes('Supercharts')) { (el as HTMLElement).click() return true } } return false }) superchartsClicked = clicked } if (superchartsClicked) { console.log('✅ Clicked Supercharts, waiting for charts interface...') // Wait for navigation to charts interface await new Promise(res => setTimeout(res, 3000)) await page.waitForSelector('.chart-container, [data-name="chart"]', { timeout: 15000 }) console.log('✅ Successfully navigated to Supercharts interface') } else { console.log('⚠️ Supercharts button not found, trying direct navigation...') // Fallback: navigate directly to charts await page.goto('https://www.tradingview.com/chart/', { waitUntil: 'networkidle2', timeout: 30000 }) } } this.loggedIn = true console.log('TradingView login complete') await this.debugScreenshot('login_04_complete', page) // Stop video recording await this.stopVideoRecording() } catch (e) { console.error('Login navigation did not complete.') this.loggedIn = false // Stop video recording on error await this.stopVideoRecording() throw new Error('Login navigation did not 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() // Start video recording for capture process await this.startVideoRecording(`capture_${finalSymbol}_${finalTimeframe || 'default'}`) // 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}`) // Check if we have a direct URL for this layout const layoutUrlPath = LAYOUT_URLS[layout] if (layoutUrlPath) { // Navigate to layout URL with hash parameters, then to base chart interface let layoutUrl = `https://www.tradingview.com/chart/${layoutUrlPath}/#symbol=${finalSymbol}` if (finalTimeframe) { layoutUrl += `&interval=${encodeURIComponent(finalTimeframe)}` } try { console.log('🎯 Navigating to layout URL:', layoutUrl) // Navigate to the specific layout URL with hash parameters and stay there await page.goto(layoutUrl, { waitUntil: 'networkidle2', timeout: 60000 }) await this.debugScreenshot(`layout_${layout}_01_after_navigation`, page) // Check if we get a "Chart Not Found" or "can't open this chart layout" error const pageContent = await page.content() const currentUrl = page.url() const pageTitle = await page.title() const isPrivateLayout = pageContent.includes("can't open this chart layout") || pageContent.includes("Chart Not Found") || pageTitle.includes("Chart Not Found") || await page.$('.tv-dialog__error, .tv-dialog__warning') !== null if (isPrivateLayout) { console.log(`⚠️ Layout "${layout}" appears to be private or not found. This might be due to:`) console.log(' 1. Layout is private and requires proper account access') console.log(' 2. Layout ID is incorrect or outdated') console.log(' 3. Account doesn\'t have access to this layout') console.log(` Current URL: ${currentUrl}`) console.log(` Page title: ${pageTitle}`) await this.debugScreenshot(`layout_${layout}_01b_private_layout_detected`, page) // Check if we're properly logged in with account that should have access const loginStatus = await page.evaluate(() => { const hasUserMenu = document.querySelector('.tv-header__user-menu-button, [data-name="header-user-menu"]') !== null const hasGuestIndicators = document.body.textContent?.includes('Guest') || document.body.textContent?.includes('Sign up') || document.body.textContent?.includes('Get started for free') return { hasUserMenu, hasGuestIndicators, bodyText: document.body.textContent?.substring(0, 200) } }) if (loginStatus.hasGuestIndicators || !loginStatus.hasUserMenu) { console.log('🔄 Detected we might not be properly logged in. Forcing re-login...') this.loggedIn = false await this.login() // Try the layout URL again after proper login console.log('🔄 Retrying layout URL after proper login...') await page.goto(layoutUrl, { waitUntil: 'networkidle2', timeout: 60000 }) await this.debugScreenshot(`layout_${layout}_01d_retry_after_login`, page) // Check again if layout is accessible const retryPageContent = await page.content() const retryIsPrivate = retryPageContent.includes("can't open this chart layout") || retryPageContent.includes("Chart Not Found") || await page.title().then(t => t.includes("Chart Not Found")) if (retryIsPrivate) { console.log(`❌ Layout "${layout}" is still not accessible after proper login. Falling back to default chart.`) } else { console.log(`✅ Layout "${layout}" is now accessible after proper login!`) // Continue with normal flow return } } // Navigate to default chart with the symbol and timeframe const fallbackUrl = `https://www.tradingview.com/chart/?symbol=${finalSymbol}&interval=${encodeURIComponent(finalTimeframe || '5')}` console.log('🔄 Falling back to default chart:', fallbackUrl) await page.goto(fallbackUrl, { waitUntil: 'networkidle2', timeout: 60000 }) await this.debugScreenshot(`layout_${layout}_01c_fallback_navigation`, page) } // Wait for the layout to load properly await new Promise(res => setTimeout(res, 5000)) await this.debugScreenshot(`layout_${layout}_02_after_wait`, page) // Verify we're on the correct layout by checking if we can see chart content const layoutLoaded = await page.evaluate(() => { const hasChart = document.querySelector('.chart-container, [data-name="chart"], canvas') !== null const title = document.title const url = window.location.href return { hasChart, title, url } }) console.log('📊 Layout verification:', layoutLoaded) await this.debugScreenshot(`layout_${layout}_03_after_verification`, page) if (!layoutLoaded.hasChart) { console.log('⚠️ Chart not detected, waiting longer for layout to load...') await new Promise(res => setTimeout(res, 5000)) await this.debugScreenshot(`layout_${layout}_04_after_extra_wait`, page) } console.log(`✅ Successfully loaded layout "${layout}" and staying on it`) await this.debugScreenshot(`layout_${layout}_05_final_success`, page) } catch (e: any) { console.error(`Failed to load layout "${layout}":`, e) throw new Error(`Failed to load layout "${layout}": ` + (e.message || e)) } } else { // Fallback to loading layout via menu (for layouts without direct URLs) console.log(`No direct URL found for layout "${layout}", trying menu navigation...`) // Navigate to base chart URL first let url = `https://www.tradingview.com/chart/?symbol=${finalSymbol}` if (finalTimeframe) { url += `&interval=${encodeURIComponent(finalTimeframe)}` } try { console.log('Navigating to base chart URL:', url) await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 }) console.log('Successfully navigated to base 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)) } // Try to load the layout via menu 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 this.debugScreenshot(`final_screenshot_${layout}_before_capture`, page) 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)) } } // Stop video recording await this.stopVideoRecording() return screenshots } private async loadLayout(page: Page, layout: string): Promise { try { console.log('Loading layout using direct URL:', layout) // Check if we have a direct URL for this layout const layoutUrlPath = LAYOUT_URLS[layout] if (!layoutUrlPath) { console.log(`No direct URL found for layout "${layout}". Available layouts:`, Object.keys(LAYOUT_URLS)) console.log('Skipping layout loading and continuing with default chart') return } // This method is deprecated - the layout loading logic is now in captureScreenshots console.log('Note: This method is deprecated. Layout loading handled in captureScreenshots.') } catch (e: any) { console.error(`Failed to load layout "${layout}":`, e) console.log('Continuing with default chart layout...') } } private async startVideoRecording(filename: string): Promise { if (!isRecordingSupported || !this.page) { console.log('Video recording not available or page not initialized') return } try { const isRecordingEnabled = process.env.TRADINGVIEW_RECORD_VIDEO === 'true' const isDebugMode = process.env.TRADINGVIEW_DEBUG === 'true' if (!isRecordingEnabled && !isDebugMode) { console.log('Video recording disabled (set TRADINGVIEW_RECORD_VIDEO=true to enable)') return } const videosDir = path.join(process.cwd(), 'videos') await fs.mkdir(videosDir, { recursive: true }) const timestamp = Date.now() const videoFilename = `${filename.replace('.png', '')}_${timestamp}` // Simple screenshot-based recording this.recorder = { isRecording: true, filename: videoFilename, videosDir, screenshotCount: 0, interval: null as NodeJS.Timeout | null } // Take screenshots every 2 seconds for a basic "video" this.recorder.interval = setInterval(async () => { if (this.recorder && this.recorder.isRecording && this.page) { try { const screenshotPath = path.join(this.recorder.videosDir, `${this.recorder.filename}_frame_${String(this.recorder.screenshotCount).padStart(4, '0')}.png`) await this.page.screenshot({ path: screenshotPath as `${string}.png`, type: 'png' }) this.recorder.screenshotCount++ } catch (e) { console.error('Failed to capture video frame:', e) } } }, 2000) console.log(`🎥 Video recording started (screenshot mode): ${videosDir}/${videoFilename}_frame_*.png`) } catch (e) { console.error('Failed to start video recording:', e) this.recorder = null } } private async stopVideoRecording(): Promise { if (!this.recorder) { return null } try { this.recorder.isRecording = false if (this.recorder.interval) { clearInterval(this.recorder.interval) } const videoPath = `${this.recorder.videosDir}/${this.recorder.filename}_frames` console.log(`🎥 Video recording stopped: ${this.recorder.screenshotCount} frames saved to ${videoPath}_*.png`) // Optionally create a simple HTML viewer for the frames const htmlPath = path.join(this.recorder.videosDir, `${this.recorder.filename}_viewer.html`) const framesList = Array.from({length: this.recorder.screenshotCount}, (_, i) => `${this.recorder!.filename}_frame_${String(i).padStart(4, '0')}.png` ) const htmlContent = ` Video Recording: ${this.recorder.filename}

Video Recording: ${this.recorder.filename}

Frame: 1 / ${this.recorder.screenshotCount}

Total frames: ${this.recorder.screenshotCount} | Captured every 2 seconds

Video frame ` await fs.writeFile(htmlPath, htmlContent, 'utf8') console.log(`📄 Video viewer created: ${htmlPath}`) this.recorder = null return videoPath } catch (e) { console.error('Failed to stop video recording:', e) this.recorder = null return null } } async cleanup() { try { // Stop any ongoing video recording if (this.recorder) { await this.stopVideoRecording() } // Close the page if (this.page) { await this.page.close() this.page = null } // Close the browser if (this.browser) { await this.browser.close() this.browser = null } this.loggedIn = false console.log('TradingViewCapture cleaned up successfully') } catch (e) { console.error('Error during cleanup:', e) } } } export const tradingViewCapture = new TradingViewCapture()