- Add login restriction detection for private layout URLs - Implement re-authentication when layout access is denied - Add proper login state management with flag updates - Verify user login status before accessing layout URLs - Add fallback to base chart when layout is not accessible - Enhance error handling for login-protected layouts - Ensure session persistence across layout navigation This fixes the issue where layout URLs were failing due to insufficient login verification.
385 lines
16 KiB
TypeScript
385 lines
16 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()
|
|
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')
|
|
// Reset the loggedIn flag to true to ensure we don't re-login unnecessarily
|
|
this.loggedIn = true
|
|
return
|
|
}
|
|
} catch (e) {
|
|
console.log('Not logged in yet, proceeding with login...')
|
|
}
|
|
|
|
// Reset login flag since we need to login
|
|
this.loggedIn = false
|
|
|
|
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 })
|
|
this.loggedIn = true
|
|
console.log('TradingView login complete')
|
|
} catch (e) {
|
|
console.error('Login navigation did not complete.')
|
|
this.loggedIn = false
|
|
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()
|
|
|
|
// 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 })
|
|
|
|
// Check if we landed on the login restriction page
|
|
const restrictionCheck = await page.evaluate(() => {
|
|
const text = document.body.textContent || ''
|
|
return text.includes("We can't open this chart layout for you") ||
|
|
text.includes("log in to see it") ||
|
|
text.includes("chart layout sharing")
|
|
})
|
|
|
|
if (restrictionCheck) {
|
|
console.log(`Layout "${layout}" requires login verification, checking login status...`)
|
|
|
|
// Verify we're actually logged in by checking for user menu
|
|
const loggedInCheck = await page.waitForSelector('.tv-header__user-menu-button, [data-name="header-user-menu"]', { timeout: 5000 }).catch(() => null)
|
|
|
|
if (!loggedInCheck) {
|
|
console.log('Not properly logged in, re-authenticating...')
|
|
await this.login()
|
|
|
|
// Try navigating to the layout URL again
|
|
console.log('Retrying navigation to layout URL after login:', url)
|
|
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 })
|
|
|
|
// Check again if we still get the restriction
|
|
const secondCheck = await page.evaluate(() => {
|
|
const text = document.body.textContent || ''
|
|
return text.includes("We can't open this chart layout for you") ||
|
|
text.includes("log in to see it") ||
|
|
text.includes("chart layout sharing")
|
|
})
|
|
|
|
if (secondCheck) {
|
|
console.log(`Layout "${layout}" is private or not accessible, falling back to base chart`)
|
|
// Navigate to base chart instead
|
|
let baseUrl = `https://www.tradingview.com/chart/?symbol=${finalSymbol}`
|
|
if (finalTimeframe) {
|
|
baseUrl += `&interval=${encodeURIComponent(finalTimeframe)}`
|
|
}
|
|
await page.goto(baseUrl, { 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()
|