Implement direct URL-based layout loading for TradingView
- 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.
This commit is contained in:
BIN
debug_layout_menu_Diy_module.png
Normal file
BIN
debug_layout_menu_Diy_module.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
BIN
debug_layout_menu_ai.png
Normal file
BIN
debug_layout_menu_ai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
@@ -8,6 +8,13 @@ const TRADINGVIEW_PASSWORD = process.env.TRADINGVIEW_PASSWORD
|
|||||||
const TRADINGVIEW_LAYOUTS = (process.env.TRADINGVIEW_LAYOUTS || '').split(',').map(l => l.trim())
|
const TRADINGVIEW_LAYOUTS = (process.env.TRADINGVIEW_LAYOUTS || '').split(',').map(l => l.trim())
|
||||||
const PUPPETEER_EXECUTABLE_PATH = process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium'
|
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 {
|
export class TradingViewCapture {
|
||||||
private browser: Browser | null = null
|
private browser: Browser | null = null
|
||||||
private page: Page | null = null
|
private page: Page | null = null
|
||||||
@@ -211,19 +218,6 @@ export class TradingViewCapture {
|
|||||||
console.log('Using settings:', { symbol: finalSymbol, timeframe: finalTimeframe, layouts: finalLayouts })
|
console.log('Using settings:', { symbol: finalSymbol, timeframe: finalTimeframe, layouts: finalLayouts })
|
||||||
|
|
||||||
const page = await this.init()
|
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
|
// Capture screenshots for each layout
|
||||||
const screenshots: string[] = []
|
const screenshots: string[] = []
|
||||||
@@ -232,8 +226,45 @@ export class TradingViewCapture {
|
|||||||
const layout = finalLayouts[i]
|
const layout = finalLayouts[i]
|
||||||
console.log(`Processing layout ${i + 1}/${finalLayouts.length}: ${layout}`)
|
console.log(`Processing layout ${i + 1}/${finalLayouts.length}: ${layout}`)
|
||||||
|
|
||||||
// Load the layout
|
// Check if we have a direct URL for this layout
|
||||||
await this.loadLayout(page, 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
|
// Wait for layout to load
|
||||||
await new Promise(res => setTimeout(res, 3000))
|
await new Promise(res => setTimeout(res, 3000))
|
||||||
@@ -261,229 +292,45 @@ export class TradingViewCapture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadLayout(page: Page, layout: string): Promise<void> {
|
private async loadLayout(page: Page, layout: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Trying to load layout:', layout)
|
console.log('Loading layout using direct URL:', layout)
|
||||||
|
|
||||||
// Try multiple selectors for the layout button
|
// Check if we have a direct URL for this layout
|
||||||
const layoutSelectors = [
|
const layoutUrlPath = LAYOUT_URLS[layout]
|
||||||
'[data-name="load-chart-layout-dialog"]',
|
if (!layoutUrlPath) {
|
||||||
'[data-name="layouts-menu"]',
|
console.log(`No direct URL found for layout "${layout}". Available layouts:`, Object.keys(LAYOUT_URLS))
|
||||||
'[data-name="chart-layout-button"]',
|
console.log('Skipping layout loading and continuing with default chart')
|
||||||
'button[title*="Layout" i]',
|
return
|
||||||
'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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
export const tradingViewCapture = new TradingViewCapture()
|
||||||
|
|||||||
BIN
screenshots/SOLUSD_5_1752065182442_Diy module.png
Normal file
BIN
screenshots/SOLUSD_5_1752065182442_Diy module.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
BIN
screenshots/SOLUSD_5_1752065182442_ai.png
Normal file
BIN
screenshots/SOLUSD_5_1752065182442_ai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
Reference in New Issue
Block a user