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

Update pdf-server.js

Browse files
Files changed (1) hide show
  1. pdf-server.js +290 -80
pdf-server.js CHANGED
@@ -1,14 +1,24 @@
1
  const express = require('express');
2
  const { chromium } = require('playwright-core');
 
 
3
 
4
  const app = express();
5
  app.use(express.json({ limit: '50mb' }));
6
 
7
- // ==================== CONFIGURATION ====================
8
  const PORT = 3000;
9
  const CHROMIUM_PATH = '/usr/bin/chromium-browser';
10
 
11
- // Formats KDP
 
 
 
 
 
 
 
 
 
12
  const FORMATS = {
13
  '6x9': { width: '6in', height: '9in' },
14
  '5x8': { width: '5in', height: '8in' },
@@ -20,128 +30,118 @@ const FORMATS = {
20
 
21
  // ==================== HEALTH CHECK ====================
22
  app.get('/health', (req, res) => {
23
- res.json({ status: 'ok', service: 'pdf-server' });
24
  });
25
 
26
  // ==================== GÉNÉRATION PDF ====================
27
  app.post('/generate-pdf', async (req, res) => {
28
- const startTime = Date.now();
29
  let browser = null;
30
-
31
  try {
32
  const {
33
  html,
 
34
  format = '6x9',
35
  margins = { top: '0.75in', bottom: '0.75in', left: '0.5in', right: '0.5in' },
36
  printBackground = true,
37
- waitForImages = true,
38
- waitTimeout = 30000,
39
  } = req.body;
40
 
41
- if (!html) {
42
- return res.status(400).json({ error: 'html requis' });
43
  }
44
 
45
- console.log(`📄 Génération PDF (format: ${format})...`);
46
 
47
- // Lancer le navigateur
48
  browser = await chromium.launch({
49
  executablePath: CHROMIUM_PATH,
50
  headless: true,
51
- args: [
52
- '--no-sandbox',
53
- '--disable-setuid-sandbox',
54
- '--disable-dev-shm-usage',
55
- '--disable-gpu',
56
- '--single-process',
57
- ],
58
  });
59
 
60
- const context = await browser.newContext();
61
- const page = await context.newPage();
62
-
63
- // Charger le HTML
64
- await page.setContent(html, {
65
- waitUntil: 'networkidle',
66
- timeout: waitTimeout,
67
- });
68
 
69
- // Attendre que les images soient chargées
70
- if (waitForImages) {
71
- await page.evaluate(() => {
72
- return Promise.all(
73
- Array.from(document.images)
74
- .filter(img => !img.complete)
75
- .map(img => new Promise(resolve => {
76
- img.onload = img.onerror = resolve;
77
- setTimeout(resolve, 5000);
78
- }))
79
- );
80
- });
81
  }
82
 
83
- // Attendre un peu pour le rendu CSS
84
- await page.waitForTimeout(500);
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- // Configuration du format
87
  const formatConfig = FORMATS[format] || FORMATS['6x9'];
88
 
89
- // Générer le PDF
90
  const pdfBuffer = await page.pdf({
91
  width: formatConfig.width,
92
  height: formatConfig.height,
93
  margin: margins,
94
- printBackground: printBackground,
95
  preferCSSPageSize: true,
96
  });
97
 
98
  await browser.close();
99
- browser = null;
100
-
101
- const duration = Date.now() - startTime;
102
- console.log(`✅ PDF généré (${(pdfBuffer.length / 1024 / 1024).toFixed(2)} MB, ${duration}ms)`);
103
 
104
- // Retourner en base64
105
  res.json({
106
  success: true,
107
  pdf: pdfBuffer.toString('base64'),
108
- size_bytes: pdfBuffer.length,
109
  size_mb: (pdfBuffer.length / 1024 / 1024).toFixed(2),
110
- duration_ms: duration,
111
  });
112
 
113
  } catch (error) {
114
- console.error('❌ Erreur:', error.message);
115
  if (browser) await browser.close();
116
- res.status(500).json({ error: error.message });
117
  }
118
  });
119
 
120
  // ==================== SCREENSHOT ====================
121
  app.post('/screenshot', async (req, res) => {
122
  let browser = null;
123
-
124
  try {
125
  const {
126
  url,
127
  html,
 
128
  fullPage = true,
129
- width = 1920,
130
- height = 1080,
 
 
 
131
  } = req.body;
132
 
133
  if (!url && !html) {
134
  return res.status(400).json({ error: 'url ou html requis' });
135
  }
136
 
 
 
 
 
137
  browser = await chromium.launch({
138
  executablePath: CHROMIUM_PATH,
139
  headless: true,
140
  args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
141
  });
142
 
143
- const page = await browser.newPage();
144
- await page.setViewportSize({ width, height });
 
 
 
 
 
 
145
 
146
  if (url) {
147
  await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
@@ -149,16 +149,160 @@ app.post('/screenshot', async (req, res) => {
149
  await page.setContent(html, { waitUntil: 'networkidle' });
150
  }
151
 
152
- const screenshot = await page.screenshot({ fullPage, type: 'png' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  await browser.close();
154
 
155
  res.json({
156
  success: true,
157
  screenshot: screenshot.toString('base64'),
158
- size_bytes: screenshot.length,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  });
160
 
161
  } catch (error) {
 
162
  if (browser) await browser.close();
163
  res.status(500).json({ error: error.message });
164
  }
@@ -167,59 +311,123 @@ app.post('/screenshot', async (req, res) => {
167
  // ==================== SCRAPING ====================
168
  app.post('/scrape', async (req, res) => {
169
  let browser = null;
170
-
171
  try {
172
  const {
173
  url,
174
- selector,
 
175
  waitFor,
176
- evaluate,
177
  } = req.body;
178
 
179
  if (!url) {
180
  return res.status(400).json({ error: 'url requis' });
181
  }
182
 
 
 
183
  browser = await chromium.launch({
184
  executablePath: CHROMIUM_PATH,
185
  headless: true,
186
  args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
187
  });
188
 
189
- const page = await browser.newPage();
 
 
 
 
 
190
  await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
191
 
192
  if (waitFor) {
193
  await page.waitForSelector(waitFor, { timeout: 10000 });
194
  }
195
 
196
- let result = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
- // Récupérer le HTML
199
- result.html = await page.content();
200
- result.title = await page.title();
201
- result.url = page.url();
202
 
203
- // Si un sélecteur est spécifié
204
- if (selector) {
205
- result.elements = await page.$$eval(selector, els => els.map(el => ({
206
- text: el.textContent?.trim(),
207
- html: el.innerHTML,
208
- href: el.href || null,
209
- src: el.src || null,
210
- })));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  }
212
 
213
- // Si un script personnalisé est fourni
214
- if (evaluate) {
215
- result.custom = await page.evaluate(evaluate);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  }
217
 
218
  await browser.close();
219
 
220
- res.json({ success: true, ...result });
 
 
 
 
221
 
222
  } catch (error) {
 
223
  if (browser) await browser.close();
224
  res.status(500).json({ error: error.message });
225
  }
@@ -228,4 +436,6 @@ app.post('/scrape', async (req, res) => {
228
  // ==================== DÉMARRAGE ====================
229
  app.listen(PORT, '0.0.0.0', () => {
230
  console.log(`🚀 PDF Server running on port ${PORT}`);
 
 
231
  });
 
1
  const express = require('express');
2
  const { chromium } = require('playwright-core');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
 
6
  const app = express();
7
  app.use(express.json({ limit: '50mb' }));
8
 
 
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
22
  const FORMATS = {
23
  '6x9': { width: '6in', height: '9in' },
24
  '5x8': { width: '5in', height: '8in' },
 
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,
42
+ url,
43
  format = '6x9',
44
  margins = { top: '0.75in', bottom: '0.75in', left: '0.5in', right: '0.5in' },
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
 
107
  // ==================== SCREENSHOT ====================
108
  app.post('/screenshot', async (req, res) => {
109
  let browser = null;
 
110
  try {
111
  const {
112
  url,
113
  html,
114
+ device = 'desktop',
115
  fullPage = true,
116
+ width,
117
+ height,
118
+ selector,
119
+ waitFor,
120
+ delay = 0,
121
  } = req.body;
122
 
123
  if (!url && !html) {
124
  return res.status(400).json({ error: 'url ou html requis' });
125
  }
126
 
127
+ const deviceConfig = DEVICES[device] || DEVICES['desktop'];
128
+ const viewportWidth = width || deviceConfig.width;
129
+ const viewportHeight = height || deviceConfig.height;
130
+
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
+ });
143
+
144
+ const page = await context.newPage();
145
 
146
  if (url) {
147
  await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
 
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({
181
  success: true,
182
  screenshot: screenshot.toString('base64'),
183
+ device,
184
+ viewport: { width: viewportWidth, height: viewportHeight },
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
  }
 
311
  // ==================== SCRAPING ====================
312
  app.post('/scrape', async (req, res) => {
313
  let browser = null;
 
314
  try {
315
  const {
316
  url,
317
+ device = 'desktop',
318
+ selectors = {},
319
  waitFor,
320
+ javascript,
321
  } = req.body;
322
 
323
  if (!url) {
324
  return res.status(400).json({ error: 'url requis' });
325
  }
326
 
327
+ const deviceConfig = DEVICES[device] || DEVICES['desktop'];
328
+
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 = {
348
+ url: page.url(),
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
+ })));
361
+ } catch (e) {
362
+ result[key] = [];
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();
422
 
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
  }
 
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
  });