const express = require('express'); const { chromium } = require('playwright-core'); const fs = require('fs'); const path = require('path'); const app = express(); app.use(express.json({ limit: '50mb' })); const PORT = 3000; const CHROMIUM_PATH = '/usr/bin/chromium-browser'; // Arguments Chromium pour environnement restreint (HuggingFace) const CHROMIUM_ARGS = [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--disable-extensions', '--disable-background-networking', '--disable-sync', '--disable-translate', '--disable-crash-reporter', '--disable-breakpad', '--disable-features=TranslateUI', '--disable-ipc-flooding-protection', '--no-first-run', '--no-zygote', '--single-process', '--deterministic-fetch', '--disable-features=IsolateOrigins', '--disable-site-isolation-trials', ]; // Devices const DEVICES = { 'desktop': { width: 1920, height: 1080 }, 'laptop': { width: 1366, height: 768 }, 'tablet': { width: 768, height: 1024 }, 'mobile': { width: 375, height: 812 }, }; // Formats PDF const FORMATS = { '6x9': { width: '6in', height: '9in' }, '5x8': { width: '5in', height: '8in' }, '5.5x8.5': { width: '5.5in', height: '8.5in' }, '8.5x11': { width: '8.5in', height: '11in' }, 'A4': { width: '210mm', height: '297mm' }, 'A5': { width: '148mm', height: '210mm' }, }; // ==================== HEALTH ==================== app.get('/health', async (req, res) => { let browser = null; try { browser = await chromium.launch({ executablePath: CHROMIUM_PATH, headless: true, args: CHROMIUM_ARGS, }); await browser.close(); res.json({ status: 'ok', chromium: 'working' }); } catch (error) { res.json({ status: 'error', chromium: error.message }); } }); // ==================== PDF ==================== app.post('/generate-pdf', async (req, res) => { let browser = null; const startTime = Date.now(); try { const { html, url, format = '6x9', margins = { top: '0.75in', bottom: '0.75in', left: '0.5in', right: '0.5in' }, printBackground = true, } = req.body; console.log(`[PDF] Starting... format=${format}`); if (!html && !url) { return res.status(400).json({ error: 'html ou url requis' }); } console.log('[PDF] Launching browser...'); browser = await chromium.launch({ executablePath: CHROMIUM_PATH, headless: true, args: CHROMIUM_ARGS, }); console.log(`[PDF] Browser launched (${Date.now() - startTime}ms)`); const page = await browser.newPage(); console.log(`[PDF] Page created (${Date.now() - startTime}ms)`); if (url) { console.log(`[PDF] Loading URL: ${url}`); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); } else { console.log(`[PDF] Setting HTML content (${html.length} chars)`); await page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 30000 }); } console.log(`[PDF] Content loaded (${Date.now() - startTime}ms)`); // Attendre un peu pour le rendu await page.waitForTimeout(1000); const formatConfig = FORMATS[format] || FORMATS['6x9']; console.log(`[PDF] Generating with format: ${JSON.stringify(formatConfig)}`); const pdfBuffer = await page.pdf({ width: formatConfig.width, height: formatConfig.height, margin: margins, printBackground, }); console.log(`[PDF] Generated! Size: ${pdfBuffer.length} bytes (${Date.now() - startTime}ms)`); await browser.close(); browser = null; res.json({ success: true, pdf: pdfBuffer.toString('base64'), size_bytes: pdfBuffer.length, size_mb: (pdfBuffer.length / 1024 / 1024).toFixed(2), duration_ms: Date.now() - startTime, }); } catch (error) { console.error(`[PDF] ERROR: ${error.message}`); console.error(error.stack); if (browser) { try { await browser.close(); } catch (e) {} } res.status(500).json({ error: error.message, stack: error.stack, duration_ms: Date.now() - startTime, }); } }); // ==================== SCREENSHOT ==================== app.post('/screenshot', async (req, res) => { let browser = null; try { const { url, html, device = 'desktop', fullPage = true, width, height, selector, delay = 0, } = req.body; if (!url && !html) { return res.status(400).json({ error: 'url ou html requis' }); } const deviceConfig = DEVICES[device] || DEVICES['desktop']; const viewportWidth = width || deviceConfig.width; const viewportHeight = height || deviceConfig.height; browser = await chromium.launch({ executablePath: CHROMIUM_PATH, headless: true, args: CHROMIUM_ARGS, }); const context = await browser.newContext({ viewport: { width: viewportWidth, height: viewportHeight }, isMobile: device.includes('mobile') || device === 'tablet', hasTouch: device.includes('mobile') || device === 'tablet', }); const page = await context.newPage(); if (url) { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); } else { await page.setContent(html, { waitUntil: 'domcontentloaded' }); } if (delay > 0) { await page.waitForTimeout(delay); } let screenshot; if (selector) { const element = await page.$(selector); if (element) { screenshot = await element.screenshot({ type: 'png' }); } else { screenshot = await page.screenshot({ type: 'png', fullPage }); } } else { screenshot = await page.screenshot({ type: 'png', fullPage }); } await browser.close(); res.json({ success: true, screenshot: screenshot.toString('base64'), device, viewport: { width: viewportWidth, height: viewportHeight }, }); } catch (error) { console.error(`[SCREENSHOT] ERROR: ${error.message}`); if (browser) { try { await browser.close(); } catch (e) {} } res.status(500).json({ error: error.message }); } }); // ==================== MULTI-SCREENSHOT ==================== app.post('/multi-screenshot', async (req, res) => { let browser = null; try { const { url, devices = ['desktop', 'tablet', 'mobile'], fullPage = true, } = req.body; if (!url) { return res.status(400).json({ error: 'url requis' }); } browser = await chromium.launch({ executablePath: CHROMIUM_PATH, headless: true, args: CHROMIUM_ARGS, }); const screenshots = {}; for (const device of devices) { const deviceConfig = DEVICES[device] || DEVICES['desktop']; const context = await browser.newContext({ viewport: { width: deviceConfig.width, height: deviceConfig.height }, isMobile: device.includes('mobile') || device === 'tablet', }); const page = await context.newPage(); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); const screenshot = await page.screenshot({ type: 'png', fullPage }); screenshots[device] = screenshot.toString('base64'); await context.close(); } await browser.close(); res.json({ success: true, screenshots, devices }); } catch (error) { console.error(`[MULTI-SCREENSHOT] ERROR: ${error.message}`); if (browser) { try { await browser.close(); } catch (e) {} } res.status(500).json({ error: error.message }); } }); // ==================== SCRAPE ==================== app.post('/scrape', async (req, res) => { let browser = null; try { const { url, device = 'desktop', selectors = {}, waitFor, javascript, } = req.body; if (!url) { return res.status(400).json({ error: 'url requis' }); } const deviceConfig = DEVICES[device] || DEVICES['desktop']; browser = await chromium.launch({ executablePath: CHROMIUM_PATH, headless: true, args: CHROMIUM_ARGS, }); const page = await browser.newPage(); await page.setViewportSize({ width: deviceConfig.width, height: deviceConfig.height }); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); if (waitFor) { await page.waitForSelector(waitFor, { timeout: 10000 }).catch(() => {}); } const result = { url: page.url(), title: await page.title(), }; // Extraire avec les sélecteurs for (const [key, selector] of Object.entries(selectors)) { try { result[key] = await page.$$eval(selector, els => els.map(el => ({ text: el.textContent?.trim(), href: el.href || null, src: el.src || null, }))); } catch (e) { result[key] = []; } } // JavaScript custom if (javascript) { try { result.custom = await page.evaluate(javascript); } catch (e) { result.custom = { error: e.message }; } } await browser.close(); res.json({ success: true, ...result }); } catch (error) { console.error(`[SCRAPE] ERROR: ${error.message}`); if (browser) { try { await browser.close(); } catch (e) {} } res.status(500).json({ error: error.message }); } }); // ==================== SCROLL & SCREENSHOT (Alternative à vidéo) ==================== app.post('/scroll-capture', async (req, res) => { let browser = null; try { const { url, device = 'desktop', steps = 5, delayBetweenSteps = 500, } = req.body; if (!url) { return res.status(400).json({ error: 'url requis' }); } const deviceConfig = DEVICES[device] || DEVICES['desktop']; browser = await chromium.launch({ executablePath: CHROMIUM_PATH, headless: true, args: CHROMIUM_ARGS, }); const page = await browser.newPage(); await page.setViewportSize({ width: deviceConfig.width, height: deviceConfig.height }); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); const screenshots = []; const scrollHeight = await page.evaluate(() => document.body.scrollHeight); const stepHeight = scrollHeight / steps; for (let i = 0; i <= steps; i++) { await page.evaluate((y) => window.scrollTo(0, y), i * stepHeight); await page.waitForTimeout(delayBetweenSteps); const screenshot = await page.screenshot({ type: 'png' }); screenshots.push(screenshot.toString('base64')); } await browser.close(); res.json({ success: true, screenshots, count: screenshots.length, device, }); } catch (error) { console.error(`[SCROLL-CAPTURE] ERROR: ${error.message}`); if (browser) { try { await browser.close(); } catch (e) {} } res.status(500).json({ error: error.message }); } }); // ==================== START ==================== app.listen(PORT, '0.0.0.0', () => { console.log(`🚀 PDF Server v2 running on port ${PORT}`); console.log(`📄 Endpoints: /health, /generate-pdf, /screenshot, /multi-screenshot, /scrape, /scroll-capture`); });