import { tradingViewAutomation, TradingViewAutomation, TradingViewCredentials, NavigationOptions } from './tradingview-automation' import fs from 'fs/promises' import path from 'path' export interface ScreenshotConfig { symbol: string timeframe: string layouts?: string[] // Multiple chart layouts if needed credentials?: TradingViewCredentials // Optional if using .env } // Layout URL mappings for direct navigation const LAYOUT_URLS = { 'ai': 'Z1TzpUrf', 'diy': 'vWVvjLhP', 'Diy module': 'vWVvjLhP' // Alternative mapping for 'Diy module' } export class EnhancedScreenshotService { private static readonly OPERATION_TIMEOUT = 120000 // 2 minutes timeout for Docker private static aiSession: TradingViewAutomation | null = null private static diySession: TradingViewAutomation | null = null async captureWithLogin(config: ScreenshotConfig): Promise { console.log('πŸš€ Enhanced Screenshot Service - Docker Environment') console.log('πŸ“‹ Config:', { symbol: config.symbol, timeframe: config.timeframe, layouts: config.layouts, sessionId: config.sessionId, credentials: '[REDACTED]' }) const screenshotFiles: string[] = [] try { // Ensure screenshots directory exists const screenshotsDir = path.join(process.cwd(), 'screenshots') await fs.mkdir(screenshotsDir, { recursive: true }) const timestamp = Date.now() const layoutsToCapture = config.layouts || ['ai', 'diy'] console.log(`\nπŸ”„ Starting parallel capture of ${layoutsToCapture.length} layouts...`) // Create parallel session promises for true dual-session approach const sessionPromises = layoutsToCapture.map(async (layout) => { const layoutKey = layout.toLowerCase() let layoutSession: TradingViewAutomation | null = null try { console.log(`\nπŸ”§ Initializing ${layout.toUpperCase()} session (parallel)...`) // Get layout URL with better error handling let layoutUrl = LAYOUT_URLS[layoutKey as keyof typeof LAYOUT_URLS] // Try alternative key for 'Diy module' if (!layoutUrl && layout === 'Diy module') { layoutUrl = LAYOUT_URLS['diy'] } if (!layoutUrl) { throw new Error(`No URL mapping found for layout: ${layout} (tried keys: ${layoutKey}, diy)`) } console.log(`πŸ—ΊοΈ ${layout.toUpperCase()}: Using layout URL ${layoutUrl}`) // Create a dedicated automation instance for this layout layoutSession = new TradingViewAutomation() console.log(`🐳 Starting ${layout} browser session...`) await layoutSession.init() // Check login status and login if needed const isLoggedIn = await layoutSession.isLoggedIn() if (!isLoggedIn) { console.log(`πŸ” Logging in to ${layout} session...`) const loginSuccess = await layoutSession.smartLogin(config.credentials) if (!loginSuccess) { throw new Error(`Failed to login to ${layout} session`) } } else { console.log(`βœ… ${layout} session already logged in`) } // Navigate directly to the specific layout URL with symbol and timeframe const directUrl = `https://www.tradingview.com/chart/${layoutUrl}/?symbol=${config.symbol}&interval=${config.timeframe}` console.log(`🌐 ${layout.toUpperCase()}: Navigating directly to ${directUrl}`) // Get page from the session const page = (layoutSession as any).page if (!page) { throw new Error(`Failed to get page for ${layout} session`) } // Navigate directly to the layout URL with retries and progressive timeout strategy let navigationSuccess = false for (let attempt = 1; attempt <= 3; attempt++) { try { console.log(`πŸ”„ ${layout.toUpperCase()}: Navigation attempt ${attempt}/3`) // Progressive waiting strategy: first try domcontentloaded, then networkidle if that fails const waitUntilStrategy = attempt === 1 ? 'domcontentloaded' : 'networkidle0' const timeoutDuration = attempt === 1 ? 30000 : (60000 + (attempt - 1) * 30000) console.log(`πŸ“‹ ${layout.toUpperCase()}: Using waitUntil: ${waitUntilStrategy}, timeout: ${timeoutDuration}ms`) await page.goto(directUrl, { waitUntil: waitUntilStrategy, timeout: timeoutDuration }) // If we used domcontentloaded, wait a bit more for dynamic content if (waitUntilStrategy === 'domcontentloaded') { console.log(`⏳ ${layout.toUpperCase()}: Waiting additional 5s for dynamic content...`) await new Promise(resolve => setTimeout(resolve, 5000)) } navigationSuccess = true break } catch (navError: any) { console.warn(`⚠️ ${layout.toUpperCase()}: Navigation attempt ${attempt} failed:`, navError?.message || navError) if (attempt === 3) { throw new Error(`Failed to navigate to ${layout} layout after 3 attempts: ${navError?.message || navError}`) } // Progressive backoff const waitTime = 2000 * attempt console.log(`⏳ ${layout.toUpperCase()}: Waiting ${waitTime}ms before retry...`) await new Promise(resolve => setTimeout(resolve, waitTime)) } } if (!navigationSuccess) { throw new Error(`Failed to navigate to ${layout} layout`) } console.log(`βœ… ${layout.toUpperCase()}: Successfully navigated to layout`) // Progressive loading strategy: shorter initial wait, then chart-specific wait console.log(`⏳ ${layout.toUpperCase()}: Initial page stabilization (2s)...`) await new Promise(resolve => setTimeout(resolve, 2000)) // Wait for chart to load with multiple strategies console.log(`⏳ ${layout.toUpperCase()}: Waiting for chart to load...`) let chartLoadSuccess = false try { // Strategy 1: Use built-in chart data waiter (with shorter timeout) await Promise.race([ layoutSession.waitForChartData(), new Promise((_, reject) => setTimeout(() => reject(new Error('Chart data timeout')), 30000)) ]) console.log(`βœ… ${layout.toUpperCase()}: Chart data loaded successfully`) chartLoadSuccess = true } catch (chartError: any) { console.warn(`⚠️ ${layout.toUpperCase()}: Chart data wait failed:`, chartError?.message || chartError) // Strategy 2: Look for chart elements manually try { console.log(`πŸ” ${layout.toUpperCase()}: Checking for chart elements manually...`) await page.waitForSelector('.layout__area--center', { timeout: 15000 }) console.log(`βœ… ${layout.toUpperCase()}: Chart area found via selector`) chartLoadSuccess = true } catch (selectorError: any) { console.warn(`⚠️ ${layout.toUpperCase()}: Chart selector check failed:`, selectorError?.message || selectorError) } } if (!chartLoadSuccess) { console.warn(`⚠️ ${layout.toUpperCase()}: Chart loading uncertain, proceeding with fallback wait...`) await new Promise(resolve => setTimeout(resolve, 8000)) } else { // Additional stabilization wait after chart loads console.log(`⏳ ${layout.toUpperCase()}: Chart stabilization (3s)...`) await new Promise(resolve => setTimeout(resolve, 3000)) } // Take screenshot with better error handling const filename = `${config.symbol}_${config.timeframe}_${layout}_${timestamp}.png` console.log(`πŸ“Έ Taking ${layout} screenshot: ${filename}`) let screenshotFile = null try { screenshotFile = await layoutSession.takeScreenshot(filename) if (screenshotFile) { console.log(`βœ… ${layout} screenshot captured: ${screenshotFile}`) } else { throw new Error(`Screenshot file was not created for ${layout}`) } } catch (screenshotError: any) { console.error(`❌ ${layout.toUpperCase()}: Screenshot failed:`, screenshotError?.message || screenshotError) throw new Error(`Failed to capture ${layout} screenshot: ${screenshotError?.message || screenshotError}`) } // Store session for potential reuse if (layout === 'ai' || layoutKey === 'ai') { EnhancedScreenshotService.aiSession = layoutSession } else if (layout === 'diy' || layoutKey === 'diy' || layout === 'Diy module') { EnhancedScreenshotService.diySession = layoutSession } return screenshotFile } catch (error: any) { console.error(`❌ Error capturing ${layout} layout:`, error?.message || error) console.error(`❌ Full ${layout} error details:`, error) console.error(`❌ ${layout} error stack:`, error?.stack) // Attempt to capture browser state for debugging try { const page = (layoutSession as any)?.page if (page) { const url = await page.url() const title = await page.title() console.error(`❌ ${layout} browser state - URL: ${url}, Title: ${title}`) // Try to get page content for debugging const bodyText = await page.evaluate(() => document.body.innerText.slice(0, 200)) console.error(`❌ ${layout} page content preview:`, bodyText) } } catch (debugError: any) { console.error(`❌ Failed to capture ${layout} browser state:`, debugError?.message || debugError) } throw error // Re-throw to be caught by Promise.allSettled } }) // Execute all sessions in parallel and wait for completion console.log(`\n⚑ Executing ${layoutsToCapture.length} sessions in parallel...`) const results = await Promise.allSettled(sessionPromises) // Collect successful screenshots results.forEach((result, index) => { const layout = layoutsToCapture[index] if (result.status === 'fulfilled' && result.value) { screenshotFiles.push(result.value) console.log(`βœ… ${layout} parallel session completed successfully`) } else { console.error(`❌ ${layout} parallel session failed:`, result.status === 'rejected' ? result.reason : 'Unknown error') } }) console.log(`\n🎯 Parallel capture completed: ${screenshotFiles.length}/${layoutsToCapture.length} screenshots`) return screenshotFiles } catch (error) { console.error('Enhanced parallel screenshot capture failed:', error) throw error } } async cleanup(): Promise { console.log('🧹 Cleaning up parallel browser sessions...') const cleanupPromises = [] // Cleanup dedicated AI session if exists if (EnhancedScreenshotService.aiSession) { console.log('πŸ”§ Cleaning up AI session...') cleanupPromises.push( EnhancedScreenshotService.aiSession.close().catch((err: any) => console.error('AI session cleanup error:', err) ) ) EnhancedScreenshotService.aiSession = null } // Cleanup dedicated DIY session if exists if (EnhancedScreenshotService.diySession) { console.log('πŸ”§ Cleaning up DIY session...') cleanupPromises.push( EnhancedScreenshotService.diySession.close().catch((err: any) => console.error('DIY session cleanup error:', err) ) ) EnhancedScreenshotService.diySession = null } // Also cleanup the main singleton session cleanupPromises.push( tradingViewAutomation.close().catch((err: any) => console.error('Main session cleanup error:', err) ) ) await Promise.allSettled(cleanupPromises) console.log('βœ… All parallel browser sessions cleaned up') } } export const enhancedScreenshotService = new EnhancedScreenshotService()