- Replace complex UI navigation with direct layout URLs - Add LAYOUT_URLS mapping for 'ai' and 'Diy module' layouts - Update capture() method to navigate directly to layout URLs - Maintain fallback to menu navigation for layouts without direct URLs - Improve reliability and speed of layout switching - Add better error handling and debug logging for layout loading This should resolve the issue where layouts weren't actually changing between screenshots.
337 lines
14 KiB
TypeScript
337 lines
14 KiB
TypeScript
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'
|
|
|
|
// Layout name to URL mapping
|
|
const LAYOUT_URLS: { [key: string]: string } = {
|
|
'ai': 'Z1TzpUrf',
|
|
'Diy module': 'vWVvjLhP',
|
|
// Add more layout mappings as needed
|
|
}
|
|
|
|
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()
|
|
|
|
// 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) {
|
|
// Use direct layout URL
|
|
let url = `https://www.tradingview.com/chart/${layoutUrlPath}/?symbol=${finalSymbol}`
|
|
if (finalTimeframe) {
|
|
url += `&interval=${encodeURIComponent(finalTimeframe)}`
|
|
}
|
|
|
|
try {
|
|
console.log('Navigating to layout URL:', url)
|
|
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 })
|
|
console.log('Successfully navigated to layout:', layout)
|
|
} 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 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<void> {
|
|
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
|
|
}
|
|
|
|
// Construct the full URL for the layout
|
|
const layoutUrl = `https://www.tradingview.com/chart/${layoutUrlPath}/`
|
|
console.log('Navigating to layout URL:', layoutUrl)
|
|
|
|
// Navigate directly to the layout URL
|
|
await page.goto(layoutUrl, { waitUntil: 'networkidle2', timeout: 60000 })
|
|
console.log('Successfully navigated to layout:', layout)
|
|
|
|
// Wait for the layout to fully load
|
|
await new Promise(res => setTimeout(res, 3000))
|
|
|
|
// Take a screenshot after layout loads for debugging
|
|
const debugAfterPath = path.resolve(`debug_after_layout_${layout.replace(/\s+/g, '_')}.png`) as `${string}.png`
|
|
await page.screenshot({ path: debugAfterPath })
|
|
console.log('After layout load screenshot saved:', debugAfterPath)
|
|
|
|
} catch (e: any) {
|
|
console.error(`Failed to load layout "${layout}":`, e)
|
|
|
|
// Take debug screenshot on error
|
|
const debugErrorPath = path.resolve(`debug_layout_error_${layout.replace(/\s+/g, '_')}.png`) as `${string}.png`
|
|
await page.screenshot({ path: debugErrorPath })
|
|
console.log('Layout error screenshot saved:', debugErrorPath)
|
|
|
|
// Don't throw error, just continue with default chart
|
|
console.log('Continuing with default chart layout...')
|
|
}
|
|
}
|
|
}
|
|
|
|
export const tradingViewCapture = new TradingViewCapture()
|