Spaces:
Running
Running
| 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`); | |
| }); |