Perspicacious commited on
Commit
ca6518a
·
verified ·
1 Parent(s): 9065542

Update pdf-server.js

Browse files
Files changed (1) hide show
  1. pdf-server.js +155 -188
pdf-server.js CHANGED
@@ -9,13 +9,35 @@ app.use(express.json({ limit: '50mb' }));
9
  const PORT = 3000;
10
  const CHROMIUM_PATH = '/usr/bin/chromium-browser';
11
 
12
- // ==================== DEVICES PRESETS ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  const DEVICES = {
14
- 'desktop': { width: 1920, height: 1080, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
15
- 'laptop': { width: 1366, height: 768, userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' },
16
- 'tablet': { width: 768, height: 1024, userAgent: 'Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15' },
17
- 'mobile': { width: 375, height: 812, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15' },
18
- 'mobile-android': { width: 360, height: 740, userAgent: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36' },
19
  };
20
 
21
  // Formats PDF
@@ -28,14 +50,27 @@ const FORMATS = {
28
  'A5': { width: '148mm', height: '210mm' },
29
  };
30
 
31
- // ==================== HEALTH CHECK ====================
32
- app.get('/health', (req, res) => {
33
- res.json({ status: 'ok', service: 'pdf-server', features: ['pdf', 'screenshot', 'video', 'scrape'] });
 
 
 
 
 
 
 
 
 
 
 
34
  });
35
 
36
- // ==================== GÉNÉRATION PDF ====================
37
  app.post('/generate-pdf', async (req, res) => {
38
  let browser = null;
 
 
39
  try {
40
  const {
41
  html,
@@ -45,62 +80,68 @@ app.post('/generate-pdf', async (req, res) => {
45
  printBackground = true,
46
  } = req.body;
47
 
 
 
48
  if (!html && !url) {
49
  return res.status(400).json({ error: 'html ou url requis' });
50
  }
51
 
52
- console.log(`📄 Génération PDF...`);
53
-
54
  browser = await chromium.launch({
55
  executablePath: CHROMIUM_PATH,
56
  headless: true,
57
- args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
58
  });
 
59
 
60
  const page = await browser.newPage();
 
61
 
62
  if (url) {
63
- await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
 
64
  } else {
65
- await page.setContent(html, { waitUntil: 'networkidle', timeout: 60000 });
 
66
  }
 
67
 
68
- // Attendre images
69
- await page.evaluate(() => {
70
- return Promise.all(
71
- Array.from(document.images)
72
- .filter(img => !img.complete)
73
- .map(img => new Promise(resolve => {
74
- img.onload = img.onerror = resolve;
75
- setTimeout(resolve, 5000);
76
- }))
77
- );
78
- });
79
-
80
  await page.waitForTimeout(1000);
81
 
82
  const formatConfig = FORMATS[format] || FORMATS['6x9'];
 
83
 
84
  const pdfBuffer = await page.pdf({
85
  width: formatConfig.width,
86
  height: formatConfig.height,
87
  margin: margins,
88
  printBackground,
89
- preferCSSPageSize: true,
90
  });
 
91
 
92
  await browser.close();
 
93
 
94
  res.json({
95
  success: true,
96
  pdf: pdfBuffer.toString('base64'),
 
97
  size_mb: (pdfBuffer.length / 1024 / 1024).toFixed(2),
 
98
  });
99
 
100
  } catch (error) {
101
- console.error('❌ PDF Error:', error.message);
102
- if (browser) await browser.close();
103
- res.status(500).json({ error: error.message, stack: error.stack });
 
 
 
 
 
 
 
104
  }
105
  });
106
 
@@ -116,7 +157,6 @@ app.post('/screenshot', async (req, res) => {
116
  width,
117
  height,
118
  selector,
119
- waitFor,
120
  delay = 0,
121
  } = req.body;
122
 
@@ -131,12 +171,11 @@ app.post('/screenshot', async (req, res) => {
131
  browser = await chromium.launch({
132
  executablePath: CHROMIUM_PATH,
133
  headless: true,
134
- args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
135
  });
136
 
137
  const context = await browser.newContext({
138
  viewport: { width: viewportWidth, height: viewportHeight },
139
- userAgent: deviceConfig.userAgent,
140
  isMobile: device.includes('mobile') || device === 'tablet',
141
  hasTouch: device.includes('mobile') || device === 'tablet',
142
  });
@@ -144,37 +183,27 @@ app.post('/screenshot', async (req, res) => {
144
  const page = await context.newPage();
145
 
146
  if (url) {
147
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
148
  } else {
149
- await page.setContent(html, { waitUntil: 'networkidle' });
150
- }
151
-
152
- if (waitFor) {
153
- await page.waitForSelector(waitFor, { timeout: 10000 });
154
  }
155
 
156
  if (delay > 0) {
157
  await page.waitForTimeout(delay);
158
  }
159
 
160
- let screenshotOptions = { type: 'png', fullPage };
161
-
162
  if (selector) {
163
  const element = await page.$(selector);
164
  if (element) {
165
- screenshotOptions = { type: 'png' };
166
- const screenshot = await element.screenshot(screenshotOptions);
167
- await browser.close();
168
- return res.json({
169
- success: true,
170
- screenshot: screenshot.toString('base64'),
171
- device,
172
- viewport: { width: viewportWidth, height: viewportHeight },
173
- });
174
  }
 
 
175
  }
176
 
177
- const screenshot = await page.screenshot(screenshotOptions);
178
  await browser.close();
179
 
180
  res.json({
@@ -185,130 +214,67 @@ app.post('/screenshot', async (req, res) => {
185
  });
186
 
187
  } catch (error) {
188
- console.error('❌ Screenshot Error:', error.message);
189
- if (browser) await browser.close();
 
 
190
  res.status(500).json({ error: error.message });
191
  }
192
  });
193
 
194
- // ==================== VIDEO RECORDING ====================
195
- app.post('/record-video', async (req, res) => {
196
  let browser = null;
197
- const videoDir = '/tmp/videos';
198
-
199
  try {
200
  const {
201
  url,
202
- device = 'desktop',
203
- duration = 5000,
204
- scrollSpeed = 100,
205
- scrollToBottom = true,
206
- actions = [],
207
  } = req.body;
208
 
209
  if (!url) {
210
  return res.status(400).json({ error: 'url requis' });
211
  }
212
 
213
- // Créer le dossier vidéo
214
- if (!fs.existsSync(videoDir)) {
215
- fs.mkdirSync(videoDir, { recursive: true });
216
- }
217
-
218
- const deviceConfig = DEVICES[device] || DEVICES['desktop'];
219
-
220
  browser = await chromium.launch({
221
  executablePath: CHROMIUM_PATH,
222
  headless: true,
223
- args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
224
- });
225
-
226
- const context = await browser.newContext({
227
- viewport: { width: deviceConfig.width, height: deviceConfig.height },
228
- userAgent: deviceConfig.userAgent,
229
- isMobile: device.includes('mobile') || device === 'tablet',
230
- recordVideo: {
231
- dir: videoDir,
232
- size: { width: deviceConfig.width, height: deviceConfig.height },
233
- },
234
  });
235
 
236
- const page = await context.newPage();
237
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
238
-
239
- // Exécuter les actions personnalisées
240
- for (const action of actions) {
241
- switch (action.type) {
242
- case 'wait':
243
- await page.waitForTimeout(action.duration || 1000);
244
- break;
245
- case 'click':
246
- await page.click(action.selector);
247
- break;
248
- case 'type':
249
- await page.fill(action.selector, action.text);
250
- break;
251
- case 'scroll':
252
- await page.evaluate((y) => window.scrollBy(0, y), action.y || 500);
253
- break;
254
- case 'screenshot':
255
- // Pause visuelle
256
- await page.waitForTimeout(500);
257
- break;
258
- }
259
- }
260
-
261
- // Scroll automatique vers le bas
262
- if (scrollToBottom) {
263
- const scrollHeight = await page.evaluate(() => document.body.scrollHeight);
264
- const viewportHeight = deviceConfig.height;
265
- let currentScroll = 0;
266
 
267
- while (currentScroll < scrollHeight) {
268
- await page.evaluate((y) => window.scrollTo(0, y), currentScroll);
269
- await page.waitForTimeout(scrollSpeed);
270
- currentScroll += viewportHeight / 2;
271
- }
272
 
273
- // Retour en haut
274
- await page.evaluate(() => window.scrollTo(0, 0));
275
- } else {
276
- await page.waitForTimeout(duration);
277
- }
278
 
279
- await page.waitForTimeout(1000);
280
- await context.close();
281
- await browser.close();
282
 
283
- // Récupérer le fichier vidéo
284
- const files = fs.readdirSync(videoDir);
285
- const videoFile = files.find(f => f.endsWith('.webm'));
286
 
287
- if (!videoFile) {
288
- return res.status(500).json({ error: 'Video file not created' });
289
  }
290
 
291
- const videoPath = path.join(videoDir, videoFile);
292
- const videoBuffer = fs.readFileSync(videoPath);
293
-
294
- // Nettoyer
295
- fs.unlinkSync(videoPath);
296
 
297
- res.json({
298
- success: true,
299
- video: videoBuffer.toString('base64'),
300
- format: 'webm',
301
- size_mb: (videoBuffer.length / 1024 / 1024).toFixed(2),
302
- });
303
 
304
  } catch (error) {
305
- console.error('❌ Video Error:', error.message);
306
- if (browser) await browser.close();
 
 
307
  res.status(500).json({ error: error.message });
308
  }
309
  });
310
 
311
- // ==================== SCRAPING ====================
312
  app.post('/scrape', async (req, res) => {
313
  let browser = null;
314
  try {
@@ -329,19 +295,15 @@ app.post('/scrape', async (req, res) => {
329
  browser = await chromium.launch({
330
  executablePath: CHROMIUM_PATH,
331
  headless: true,
332
- args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
333
- });
334
-
335
- const context = await browser.newContext({
336
- viewport: { width: deviceConfig.width, height: deviceConfig.height },
337
- userAgent: deviceConfig.userAgent,
338
  });
339
 
340
- const page = await context.newPage();
341
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
 
342
 
343
  if (waitFor) {
344
- await page.waitForSelector(waitFor, { timeout: 10000 });
345
  }
346
 
347
  const result = {
@@ -349,12 +311,11 @@ app.post('/scrape', async (req, res) => {
349
  title: await page.title(),
350
  };
351
 
352
- // Extraire avec les sélecteurs fournis
353
  for (const [key, selector] of Object.entries(selectors)) {
354
  try {
355
  result[key] = await page.$$eval(selector, els => els.map(el => ({
356
  text: el.textContent?.trim(),
357
- html: el.innerHTML,
358
  href: el.href || null,
359
  src: el.src || null,
360
  })));
@@ -363,59 +324,63 @@ app.post('/scrape', async (req, res) => {
363
  }
364
  }
365
 
366
- // Exécuter du JavaScript personnalisé
367
  if (javascript) {
368
- result.custom = await page.evaluate(javascript);
 
 
 
 
369
  }
370
 
371
  await browser.close();
372
  res.json({ success: true, ...result });
373
 
374
  } catch (error) {
375
- console.error('❌ Scrape Error:', error.message);
376
- if (browser) await browser.close();
 
 
377
  res.status(500).json({ error: error.message });
378
  }
379
  });
380
 
381
- // ==================== MULTI-SCREENSHOT ====================
382
- app.post('/multi-screenshot', async (req, res) => {
383
  let browser = null;
384
  try {
385
  const {
386
  url,
387
- devices = ['desktop', 'tablet', 'mobile'],
388
- fullPage = true,
 
389
  } = req.body;
390
 
391
  if (!url) {
392
  return res.status(400).json({ error: 'url requis' });
393
  }
394
 
 
 
395
  browser = await chromium.launch({
396
  executablePath: CHROMIUM_PATH,
397
  headless: true,
398
- args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
399
  });
400
 
401
- const screenshots = {};
402
-
403
- for (const device of devices) {
404
- const deviceConfig = DEVICES[device] || DEVICES['desktop'];
405
-
406
- const context = await browser.newContext({
407
- viewport: { width: deviceConfig.width, height: deviceConfig.height },
408
- userAgent: deviceConfig.userAgent,
409
- isMobile: device.includes('mobile') || device === 'tablet',
410
- });
411
-
412
- const page = await context.newPage();
413
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
414
-
415
- const screenshot = await page.screenshot({ type: 'png', fullPage });
416
- screenshots[device] = screenshot.toString('base64');
417
-
418
- await context.close();
419
  }
420
 
421
  await browser.close();
@@ -423,19 +388,21 @@ app.post('/multi-screenshot', async (req, res) => {
423
  res.json({
424
  success: true,
425
  screenshots,
426
- devices,
 
427
  });
428
 
429
  } catch (error) {
430
- console.error('❌ Multi-Screenshot Error:', error.message);
431
- if (browser) await browser.close();
 
 
432
  res.status(500).json({ error: error.message });
433
  }
434
  });
435
 
436
- // ==================== DÉMARRAGE ====================
437
  app.listen(PORT, '0.0.0.0', () => {
438
- console.log(`🚀 PDF Server running on port ${PORT}`);
439
- console.log(`📱 Devices: ${Object.keys(DEVICES).join(', ')}`);
440
- console.log(`📄 PDF Formats: ${Object.keys(FORMATS).join(', ')}`);
441
  });
 
9
  const PORT = 3000;
10
  const CHROMIUM_PATH = '/usr/bin/chromium-browser';
11
 
12
+ // Arguments Chromium pour environnement restreint (HuggingFace)
13
+ const CHROMIUM_ARGS = [
14
+ '--no-sandbox',
15
+ '--disable-setuid-sandbox',
16
+ '--disable-dev-shm-usage',
17
+ '--disable-gpu',
18
+ '--disable-software-rasterizer',
19
+ '--disable-extensions',
20
+ '--disable-background-networking',
21
+ '--disable-sync',
22
+ '--disable-translate',
23
+ '--disable-crash-reporter',
24
+ '--disable-breakpad',
25
+ '--disable-features=TranslateUI',
26
+ '--disable-ipc-flooding-protection',
27
+ '--no-first-run',
28
+ '--no-zygote',
29
+ '--single-process',
30
+ '--deterministic-fetch',
31
+ '--disable-features=IsolateOrigins',
32
+ '--disable-site-isolation-trials',
33
+ ];
34
+
35
+ // Devices
36
  const DEVICES = {
37
+ 'desktop': { width: 1920, height: 1080 },
38
+ 'laptop': { width: 1366, height: 768 },
39
+ 'tablet': { width: 768, height: 1024 },
40
+ 'mobile': { width: 375, height: 812 },
 
41
  };
42
 
43
  // Formats PDF
 
50
  'A5': { width: '148mm', height: '210mm' },
51
  };
52
 
53
+ // ==================== HEALTH ====================
54
+ app.get('/health', async (req, res) => {
55
+ let browser = null;
56
+ try {
57
+ browser = await chromium.launch({
58
+ executablePath: CHROMIUM_PATH,
59
+ headless: true,
60
+ args: CHROMIUM_ARGS,
61
+ });
62
+ await browser.close();
63
+ res.json({ status: 'ok', chromium: 'working' });
64
+ } catch (error) {
65
+ res.json({ status: 'error', chromium: error.message });
66
+ }
67
  });
68
 
69
+ // ==================== PDF ====================
70
  app.post('/generate-pdf', async (req, res) => {
71
  let browser = null;
72
+ const startTime = Date.now();
73
+
74
  try {
75
  const {
76
  html,
 
80
  printBackground = true,
81
  } = req.body;
82
 
83
+ console.log(`[PDF] Starting... format=${format}`);
84
+
85
  if (!html && !url) {
86
  return res.status(400).json({ error: 'html ou url requis' });
87
  }
88
 
89
+ console.log('[PDF] Launching browser...');
 
90
  browser = await chromium.launch({
91
  executablePath: CHROMIUM_PATH,
92
  headless: true,
93
+ args: CHROMIUM_ARGS,
94
  });
95
+ console.log(`[PDF] Browser launched (${Date.now() - startTime}ms)`);
96
 
97
  const page = await browser.newPage();
98
+ console.log(`[PDF] Page created (${Date.now() - startTime}ms)`);
99
 
100
  if (url) {
101
+ console.log(`[PDF] Loading URL: ${url}`);
102
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
103
  } else {
104
+ console.log(`[PDF] Setting HTML content (${html.length} chars)`);
105
+ await page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 30000 });
106
  }
107
+ console.log(`[PDF] Content loaded (${Date.now() - startTime}ms)`);
108
 
109
+ // Attendre un peu pour le rendu
 
 
 
 
 
 
 
 
 
 
 
110
  await page.waitForTimeout(1000);
111
 
112
  const formatConfig = FORMATS[format] || FORMATS['6x9'];
113
+ console.log(`[PDF] Generating with format: ${JSON.stringify(formatConfig)}`);
114
 
115
  const pdfBuffer = await page.pdf({
116
  width: formatConfig.width,
117
  height: formatConfig.height,
118
  margin: margins,
119
  printBackground,
 
120
  });
121
+ console.log(`[PDF] Generated! Size: ${pdfBuffer.length} bytes (${Date.now() - startTime}ms)`);
122
 
123
  await browser.close();
124
+ browser = null;
125
 
126
  res.json({
127
  success: true,
128
  pdf: pdfBuffer.toString('base64'),
129
+ size_bytes: pdfBuffer.length,
130
  size_mb: (pdfBuffer.length / 1024 / 1024).toFixed(2),
131
+ duration_ms: Date.now() - startTime,
132
  });
133
 
134
  } catch (error) {
135
+ console.error(`[PDF] ERROR: ${error.message}`);
136
+ console.error(error.stack);
137
+ if (browser) {
138
+ try { await browser.close(); } catch (e) {}
139
+ }
140
+ res.status(500).json({
141
+ error: error.message,
142
+ stack: error.stack,
143
+ duration_ms: Date.now() - startTime,
144
+ });
145
  }
146
  });
147
 
 
157
  width,
158
  height,
159
  selector,
 
160
  delay = 0,
161
  } = req.body;
162
 
 
171
  browser = await chromium.launch({
172
  executablePath: CHROMIUM_PATH,
173
  headless: true,
174
+ args: CHROMIUM_ARGS,
175
  });
176
 
177
  const context = await browser.newContext({
178
  viewport: { width: viewportWidth, height: viewportHeight },
 
179
  isMobile: device.includes('mobile') || device === 'tablet',
180
  hasTouch: device.includes('mobile') || device === 'tablet',
181
  });
 
183
  const page = await context.newPage();
184
 
185
  if (url) {
186
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
187
  } else {
188
+ await page.setContent(html, { waitUntil: 'domcontentloaded' });
 
 
 
 
189
  }
190
 
191
  if (delay > 0) {
192
  await page.waitForTimeout(delay);
193
  }
194
 
195
+ let screenshot;
 
196
  if (selector) {
197
  const element = await page.$(selector);
198
  if (element) {
199
+ screenshot = await element.screenshot({ type: 'png' });
200
+ } else {
201
+ screenshot = await page.screenshot({ type: 'png', fullPage });
 
 
 
 
 
 
202
  }
203
+ } else {
204
+ screenshot = await page.screenshot({ type: 'png', fullPage });
205
  }
206
 
 
207
  await browser.close();
208
 
209
  res.json({
 
214
  });
215
 
216
  } catch (error) {
217
+ console.error(`[SCREENSHOT] ERROR: ${error.message}`);
218
+ if (browser) {
219
+ try { await browser.close(); } catch (e) {}
220
+ }
221
  res.status(500).json({ error: error.message });
222
  }
223
  });
224
 
225
+ // ==================== MULTI-SCREENSHOT ====================
226
+ app.post('/multi-screenshot', async (req, res) => {
227
  let browser = null;
 
 
228
  try {
229
  const {
230
  url,
231
+ devices = ['desktop', 'tablet', 'mobile'],
232
+ fullPage = true,
 
 
 
233
  } = req.body;
234
 
235
  if (!url) {
236
  return res.status(400).json({ error: 'url requis' });
237
  }
238
 
 
 
 
 
 
 
 
239
  browser = await chromium.launch({
240
  executablePath: CHROMIUM_PATH,
241
  headless: true,
242
+ args: CHROMIUM_ARGS,
 
 
 
 
 
 
 
 
 
 
243
  });
244
 
245
+ const screenshots = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
+ for (const device of devices) {
248
+ const deviceConfig = DEVICES[device] || DEVICES['desktop'];
 
 
 
249
 
250
+ const context = await browser.newContext({
251
+ viewport: { width: deviceConfig.width, height: deviceConfig.height },
252
+ isMobile: device.includes('mobile') || device === 'tablet',
253
+ });
 
254
 
255
+ const page = await context.newPage();
256
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
 
257
 
258
+ const screenshot = await page.screenshot({ type: 'png', fullPage });
259
+ screenshots[device] = screenshot.toString('base64');
 
260
 
261
+ await context.close();
 
262
  }
263
 
264
+ await browser.close();
 
 
 
 
265
 
266
+ res.json({ success: true, screenshots, devices });
 
 
 
 
 
267
 
268
  } catch (error) {
269
+ console.error(`[MULTI-SCREENSHOT] ERROR: ${error.message}`);
270
+ if (browser) {
271
+ try { await browser.close(); } catch (e) {}
272
+ }
273
  res.status(500).json({ error: error.message });
274
  }
275
  });
276
 
277
+ // ==================== SCRAPE ====================
278
  app.post('/scrape', async (req, res) => {
279
  let browser = null;
280
  try {
 
295
  browser = await chromium.launch({
296
  executablePath: CHROMIUM_PATH,
297
  headless: true,
298
+ args: CHROMIUM_ARGS,
 
 
 
 
 
299
  });
300
 
301
+ const page = await browser.newPage();
302
+ await page.setViewportSize({ width: deviceConfig.width, height: deviceConfig.height });
303
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
304
 
305
  if (waitFor) {
306
+ await page.waitForSelector(waitFor, { timeout: 10000 }).catch(() => {});
307
  }
308
 
309
  const result = {
 
311
  title: await page.title(),
312
  };
313
 
314
+ // Extraire avec les sélecteurs
315
  for (const [key, selector] of Object.entries(selectors)) {
316
  try {
317
  result[key] = await page.$$eval(selector, els => els.map(el => ({
318
  text: el.textContent?.trim(),
 
319
  href: el.href || null,
320
  src: el.src || null,
321
  })));
 
324
  }
325
  }
326
 
327
+ // JavaScript custom
328
  if (javascript) {
329
+ try {
330
+ result.custom = await page.evaluate(javascript);
331
+ } catch (e) {
332
+ result.custom = { error: e.message };
333
+ }
334
  }
335
 
336
  await browser.close();
337
  res.json({ success: true, ...result });
338
 
339
  } catch (error) {
340
+ console.error(`[SCRAPE] ERROR: ${error.message}`);
341
+ if (browser) {
342
+ try { await browser.close(); } catch (e) {}
343
+ }
344
  res.status(500).json({ error: error.message });
345
  }
346
  });
347
 
348
+ // ==================== SCROLL & SCREENSHOT (Alternative à vidéo) ====================
349
+ app.post('/scroll-capture', async (req, res) => {
350
  let browser = null;
351
  try {
352
  const {
353
  url,
354
+ device = 'desktop',
355
+ steps = 5,
356
+ delayBetweenSteps = 500,
357
  } = req.body;
358
 
359
  if (!url) {
360
  return res.status(400).json({ error: 'url requis' });
361
  }
362
 
363
+ const deviceConfig = DEVICES[device] || DEVICES['desktop'];
364
+
365
  browser = await chromium.launch({
366
  executablePath: CHROMIUM_PATH,
367
  headless: true,
368
+ args: CHROMIUM_ARGS,
369
  });
370
 
371
+ const page = await browser.newPage();
372
+ await page.setViewportSize({ width: deviceConfig.width, height: deviceConfig.height });
373
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
374
+
375
+ const screenshots = [];
376
+ const scrollHeight = await page.evaluate(() => document.body.scrollHeight);
377
+ const stepHeight = scrollHeight / steps;
378
+
379
+ for (let i = 0; i <= steps; i++) {
380
+ await page.evaluate((y) => window.scrollTo(0, y), i * stepHeight);
381
+ await page.waitForTimeout(delayBetweenSteps);
382
+ const screenshot = await page.screenshot({ type: 'png' });
383
+ screenshots.push(screenshot.toString('base64'));
 
 
 
 
 
384
  }
385
 
386
  await browser.close();
 
388
  res.json({
389
  success: true,
390
  screenshots,
391
+ count: screenshots.length,
392
+ device,
393
  });
394
 
395
  } catch (error) {
396
+ console.error(`[SCROLL-CAPTURE] ERROR: ${error.message}`);
397
+ if (browser) {
398
+ try { await browser.close(); } catch (e) {}
399
+ }
400
  res.status(500).json({ error: error.message });
401
  }
402
  });
403
 
404
+ // ==================== START ====================
405
  app.listen(PORT, '0.0.0.0', () => {
406
+ console.log(`🚀 PDF Server v2 running on port ${PORT}`);
407
+ console.log(`📄 Endpoints: /health, /generate-pdf, /screenshot, /multi-screenshot, /scrape, /scroll-capture`);
 
408
  });