src/Service/BookCoverService.php line 11

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use Symfony\Contracts\HttpClient\HttpClientInterface;
  4. class BookCoverService
  5. {
  6. private ?HttpClientInterface $httpClient;
  7. public function __construct(HttpClientInterface $httpClient = null)
  8. {
  9. $this->httpClient = $httpClient;
  10. }
  11. /**
  12. * Nettoie l'ISBN en retirant les tirets et espaces
  13. */
  14. private function cleanIsbn(?string $isbn): ?string
  15. {
  16. if (empty($isbn)) {
  17. return null;
  18. }
  19. return preg_replace('/[^0-9X]/i', '', $isbn);
  20. }
  21. /**
  22. * Effectue une requête HTTP GET
  23. */
  24. private function httpGet(string $url): ?string
  25. {
  26. try {
  27. if ($this->httpClient === null) {
  28. $context = stream_context_create([
  29. 'http' => [
  30. 'timeout' => 10,
  31. 'follow_location' => true,
  32. ]
  33. ]);
  34. $response = @file_get_contents($url, false, $context);
  35. return $response !== false ? $response : null;
  36. } else {
  37. $response = $this->httpClient->request('GET', $url, [
  38. 'timeout' => 10,
  39. ]);
  40. return $response->getStatusCode() === 200 ? $response->getContent() : null;
  41. }
  42. } catch (\Exception $e) {
  43. return null;
  44. }
  45. }
  46. /**
  47. * Recherche une couverture via Google Books API
  48. */
  49. public function getCoverUrlFromGoogleBooks(?string $isbn): ?string
  50. {
  51. $isbn = $this->cleanIsbn($isbn);
  52. if (empty($isbn)) {
  53. return null;
  54. }
  55. $url = "https://www.googleapis.com/books/v1/volumes?q=isbn:{$isbn}";
  56. $response = $this->httpGet($url);
  57. if ($response) {
  58. $data = json_decode($response, true);
  59. if (isset($data['items'][0]['volumeInfo']['imageLinks']['thumbnail'])) {
  60. $imageUrl = $data['items'][0]['volumeInfo']['imageLinks']['thumbnail'];
  61. // Améliorer la qualité de l'image
  62. $imageUrl = str_replace('zoom=1', 'zoom=0', $imageUrl);
  63. $imageUrl = str_replace('&edge=curl', '', $imageUrl);
  64. $imageUrl = str_replace('http://', 'https://', $imageUrl);
  65. return $imageUrl;
  66. }
  67. }
  68. return null;
  69. }
  70. /**
  71. * Génère l'URL Open Library pour une couverture
  72. */
  73. public function getOpenLibraryCoverUrl(?string $isbn, string $size = 'L'): ?string
  74. {
  75. $isbn = $this->cleanIsbn($isbn);
  76. if (empty($isbn)) {
  77. return null;
  78. }
  79. return "https://covers.openlibrary.org/b/isbn/{$isbn}-{$size}.jpg";
  80. }
  81. /**
  82. * Vérifie si Open Library a une couverture pour cet ISBN
  83. */
  84. public function checkOpenLibraryCover(?string $isbn): bool
  85. {
  86. $isbn = $this->cleanIsbn($isbn);
  87. if (empty($isbn)) {
  88. return false;
  89. }
  90. $url = "https://openlibrary.org/api/books?bibkeys=ISBN:{$isbn}&format=json&jscmd=data";
  91. $response = $this->httpGet($url);
  92. if ($response) {
  93. $data = json_decode($response, true);
  94. if (!empty($data) && isset($data["ISBN:{$isbn}"])) {
  95. // Vérifier si une couverture existe
  96. return isset($data["ISBN:{$isbn}"]['cover']);
  97. }
  98. }
  99. return false;
  100. }
  101. /**
  102. * Recherche la meilleure image disponible en essayant plusieurs sources
  103. *
  104. * @param string|null $isbn ISBN du livre
  105. * @return array{url: string|null, source: string|null}
  106. */
  107. public function findBestCover(?string $isbn): array
  108. {
  109. $isbn = $this->cleanIsbn($isbn);
  110. if (empty($isbn)) {
  111. return ['url' => null, 'source' => null];
  112. }
  113. // 1. Essayer Google Books (meilleure qualité généralement)
  114. $url = $this->getCoverUrlFromGoogleBooks($isbn);
  115. if ($url) {
  116. return ['url' => $url, 'source' => 'Google Books'];
  117. }
  118. // 2. Essayer Open Library avec vérification
  119. if ($this->checkOpenLibraryCover($isbn)) {
  120. $url = $this->getOpenLibraryCoverUrl($isbn, 'L');
  121. if ($url && $this->isValidImage($url)) {
  122. return ['url' => $url, 'source' => 'Open Library'];
  123. }
  124. }
  125. // 3. Essayer Amazon page produit directe (meilleure qualité)
  126. $url = $this->scrapeAmazonProductPage($isbn);
  127. if ($url && $this->isValidImageUrl($url)) {
  128. return ['url' => $url, 'source' => 'Amazon (produit)'];
  129. }
  130. // 4. Essayer Amazon recherche
  131. $url = $this->scrapeAmazonFr($isbn);
  132. if ($url && $this->isValidImageUrl($url)) {
  133. return ['url' => $url, 'source' => 'Amazon (recherche)'];
  134. }
  135. // 5. Générer URL directe vers image Open Library (sans vérification)
  136. // Open Library redirige vers l'image si elle existe
  137. $url = $this->getOpenLibraryCoverUrl($isbn, 'L');
  138. if ($url) {
  139. return ['url' => $url, 'source' => 'Open Library (direct)'];
  140. }
  141. return ['url' => null, 'source' => null];
  142. }
  143. /**
  144. * Scrape Amazon.fr pour trouver l'image de couverture
  145. */
  146. public function scrapeAmazonFr(?string $isbn): ?string
  147. {
  148. $isbn = $this->cleanIsbn($isbn);
  149. if (empty($isbn)) {
  150. return null;
  151. }
  152. try {
  153. // Rechercher sur Amazon.fr
  154. $searchUrl = "https://www.amazon.fr/s?k={$isbn}";
  155. $context = stream_context_create([
  156. 'http' => [
  157. 'timeout' => 15,
  158. 'follow_location' => true,
  159. 'max_redirects' => 3,
  160. 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n" .
  161. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" .
  162. "Accept-Language: fr-FR,fr;q=0.9,en;q=0.8\r\n" .
  163. "Accept-Encoding: gzip, deflate\r\n" .
  164. "Connection: keep-alive\r\n" .
  165. "Upgrade-Insecure-Requests: 1\r\n"
  166. ]
  167. ]);
  168. $html = @file_get_contents($searchUrl, false, $context);
  169. if ($html) {
  170. // Patterns multiples pour capturer les images Amazon
  171. $patterns = [
  172. // Pattern 1: data-image-latency
  173. '/data-image-latency="s-product-image"[^>]*src="([^"]+)"/',
  174. // Pattern 2: class s-image
  175. '/<img[^>]+class="[^"]*s-image[^"]*"[^>]+src="([^"]+)"/',
  176. // Pattern 3: srcset avec la plus grande image
  177. '/srcset="([^"]+)"\s+class="[^"]*s-image/',
  178. // Pattern 4: data-old-hires
  179. '/data-old-hires="([^"]+)"/',
  180. // Pattern 5: image dans le JSON embarqué
  181. '/"hiRes":"([^"]+)"/',
  182. ];
  183. foreach ($patterns as $pattern) {
  184. if (preg_match($pattern, $html, $matches)) {
  185. $imageUrl = $matches[1];
  186. // Si c'est un srcset, prendre la première URL
  187. if (strpos($imageUrl, ',') !== false) {
  188. $parts = explode(',', $imageUrl);
  189. $imageUrl = trim(explode(' ', $parts[0])[0]);
  190. }
  191. // Nettoyer l'URL et améliorer la qualité
  192. $imageUrl = html_entity_decode($imageUrl);
  193. // Remplacer les dimensions pour avoir la meilleure qualité
  194. $imageUrl = preg_replace('/\._[A-Z]+[0-9]+_\./', '.', $imageUrl);
  195. $imageUrl = preg_replace('/\._[A-Z]{2}[0-9]+,?[0-9]*_\./', '.', $imageUrl);
  196. // Vérifier que c'est une vraie URL d'image
  197. if ($this->isValidImageUrl($imageUrl)) {
  198. return $imageUrl;
  199. }
  200. }
  201. }
  202. }
  203. } catch (\Exception $e) {
  204. // Silently fail
  205. }
  206. return null;
  207. }
  208. /**
  209. * Scrape la page produit Amazon.fr directement avec l'ISBN
  210. */
  211. public function scrapeAmazonProductPage(?string $isbn): ?string
  212. {
  213. $isbn = $this->cleanIsbn($isbn);
  214. if (empty($isbn)) {
  215. return null;
  216. }
  217. try {
  218. // Accéder directement à la page produit via ISBN
  219. $productUrl = "https://www.amazon.fr/dp/{$isbn}";
  220. $context = stream_context_create([
  221. 'http' => [
  222. 'timeout' => 15,
  223. 'follow_location' => true,
  224. 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n" .
  225. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" .
  226. "Accept-Language: fr-FR,fr;q=0.9,en;q=0.8\r\n"
  227. ]
  228. ]);
  229. $html = @file_get_contents($productUrl, false, $context);
  230. if ($html) {
  231. // Chercher l'image principale du produit
  232. if (preg_match('/"hiRes":"([^"]+)"/', $html, $matches)) {
  233. return str_replace('\\/', '/', $matches[1]);
  234. }
  235. if (preg_match('/"large":"([^"]+)"/', $html, $matches)) {
  236. return str_replace('\\/', '/', $matches[1]);
  237. }
  238. // Image dans le tag img principal
  239. if (preg_match('/<img[^>]+id="landingImage"[^>]+src="([^"]+)"/', $html, $matches)) {
  240. $imageUrl = $matches[1];
  241. $imageUrl = preg_replace('/\._[A-Z]+[0-9]+_\./', '.', $imageUrl);
  242. return $imageUrl;
  243. }
  244. }
  245. } catch (\Exception $e) {
  246. // Silently fail
  247. }
  248. return null;
  249. }
  250. /**
  251. * Vérifie si une URL pointe vers une vraie image (pas un placeholder)
  252. */
  253. private function isValidImage(string $url): bool
  254. {
  255. $content = $this->httpGet($url);
  256. if ($content === null) {
  257. return false;
  258. }
  259. // Vérifier la taille - une vraie couverture fait généralement plus de 5KB
  260. if (strlen($content) < 5000) {
  261. return false;
  262. }
  263. return true;
  264. }
  265. /**
  266. * Télécharge l'image et retourne son contenu binaire
  267. */
  268. public function downloadImage(string $url): ?string
  269. {
  270. $content = $this->httpGet($url);
  271. // Vérifier que c'est une vraie image (pas un placeholder)
  272. if ($content !== null && strlen($content) > 1000) {
  273. return $content;
  274. }
  275. return null;
  276. }
  277. /**
  278. * Scrape une page web et récupère toutes les images de produit
  279. *
  280. * @param string|null $pageUrl URL de la page à scraper
  281. * @return array Liste des URLs d'images trouvées
  282. */
  283. public function scrapeImagesFromUrl(?string $pageUrl): array
  284. {
  285. if (empty($pageUrl)) {
  286. return [];
  287. }
  288. $images = [];
  289. try {
  290. $context = stream_context_create([
  291. 'http' => [
  292. 'timeout' => 15,
  293. 'follow_location' => true,
  294. 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n" .
  295. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" .
  296. "Accept-Language: fr-FR,fr;q=0.9,en;q=0.8\r\n"
  297. ],
  298. 'ssl' => [
  299. 'verify_peer' => false,
  300. 'verify_peer_name' => false,
  301. ]
  302. ]);
  303. $html = @file_get_contents($pageUrl, false, $context);
  304. if (!$html) {
  305. return [];
  306. }
  307. // Détecter le type de site et adapter le scraping
  308. if (strpos($pageUrl, 'amazon') !== false) {
  309. $images = $this->extractAmazonImages($html);
  310. } elseif (strpos($pageUrl, 'fnac') !== false) {
  311. $images = $this->extractFnacImages($html);
  312. } elseif (strpos($pageUrl, 'babelio') !== false) {
  313. $images = $this->extractBabelioImages($html);
  314. } else {
  315. // Extraction générique
  316. $images = $this->extractGenericImages($html, $pageUrl);
  317. }
  318. } catch (\Exception $e) {
  319. // Silently fail
  320. }
  321. // Filtrer les doublons et les images trop petites
  322. return array_values(array_unique($images));
  323. }
  324. /**
  325. * Extrait les images depuis une page Amazon
  326. */
  327. private function extractAmazonImages(string $html): array
  328. {
  329. $images = [];
  330. // Images haute résolution dans le JSON
  331. if (preg_match_all('/"hiRes"\s*:\s*"([^"]+)"/', $html, $matches)) {
  332. foreach ($matches[1] as $url) {
  333. $url = str_replace('\\/', '/', $url);
  334. if ($this->isProductImage($url)) {
  335. $images[] = $url;
  336. }
  337. }
  338. }
  339. // Images "large" dans le JSON
  340. if (preg_match_all('/"large"\s*:\s*"([^"]+)"/', $html, $matches)) {
  341. foreach ($matches[1] as $url) {
  342. $url = str_replace('\\/', '/', $url);
  343. if ($this->isProductImage($url)) {
  344. $images[] = $url;
  345. }
  346. }
  347. }
  348. // Image principale (landingImage)
  349. if (preg_match('/<img[^>]+id="landingImage"[^>]+(?:src|data-old-hires)="([^"]+)"/', $html, $matches)) {
  350. $url = $matches[1];
  351. // Améliorer la qualité
  352. $url = preg_replace('/\._[A-Z]{2}[0-9]+_\./', '.', $url);
  353. if ($this->isProductImage($url)) {
  354. $images[] = $url;
  355. }
  356. }
  357. // Images dans data-a-dynamic-image
  358. if (preg_match('/data-a-dynamic-image="([^"]+)"/', $html, $matches)) {
  359. $json = html_entity_decode($matches[1]);
  360. if (preg_match_all('/(https:[^"]+\.jpg)/', $json, $urlMatches)) {
  361. foreach ($urlMatches[1] as $url) {
  362. $url = str_replace('\\/', '/', $url);
  363. if ($this->isProductImage($url)) {
  364. $images[] = $url;
  365. }
  366. }
  367. }
  368. }
  369. return $images;
  370. }
  371. /**
  372. * Extrait les images depuis une page Fnac
  373. */
  374. private function extractFnacImages(string $html): array
  375. {
  376. $images = [];
  377. // Images produit Fnac
  378. if (preg_match_all('/src="(https:\/\/static\.fnac-static\.com\/multimedia\/Images\/[^"]+)"/', $html, $matches)) {
  379. foreach ($matches[1] as $url) {
  380. if ($this->isProductImage($url)) {
  381. $images[] = $url;
  382. }
  383. }
  384. }
  385. // Data-src pour lazy loading
  386. if (preg_match_all('/data-src="(https:\/\/static\.fnac-static\.com\/multimedia\/Images\/[^"]+)"/', $html, $matches)) {
  387. foreach ($matches[1] as $url) {
  388. if ($this->isProductImage($url)) {
  389. $images[] = $url;
  390. }
  391. }
  392. }
  393. return $images;
  394. }
  395. /**
  396. * Extrait les images depuis une page Babelio
  397. */
  398. private function extractBabelioImages(string $html): array
  399. {
  400. $images = [];
  401. if (preg_match_all('/src="(https:\/\/[^"]*babelio[^"]*couverture[^"]*)"/', $html, $matches)) {
  402. foreach ($matches[1] as $url) {
  403. $images[] = $url;
  404. }
  405. }
  406. return $images;
  407. }
  408. /**
  409. * Extraction générique d'images depuis une page HTML
  410. */
  411. private function extractGenericImages(string $html, string $baseUrl): array
  412. {
  413. $images = [];
  414. // Extraire toutes les images
  415. if (preg_match_all('/<img[^>]+src="([^"]+)"[^>]*>/i', $html, $matches)) {
  416. foreach ($matches[1] as $url) {
  417. // Convertir les URLs relatives en absolues
  418. if (strpos($url, 'http') !== 0) {
  419. $parsedBase = parse_url($baseUrl);
  420. $url = $parsedBase['scheme'] . '://' . $parsedBase['host'] . '/' . ltrim($url, '/');
  421. }
  422. if ($this->isProductImage($url)) {
  423. $images[] = $url;
  424. }
  425. }
  426. }
  427. return $images;
  428. }
  429. /**
  430. * Vérifie si une URL ressemble à une image de produit (pas un logo, icône, etc.)
  431. */
  432. private function isProductImage(string $url): bool
  433. {
  434. // Exclure les petites images, icônes, logos
  435. $excludePatterns = [
  436. '/sprite/',
  437. '/icon/',
  438. '/logo/',
  439. '/button/',
  440. '/pixel/',
  441. '/transparent/',
  442. '/blank/',
  443. '/1x1/',
  444. '/_SS40_',
  445. '/_SS50_',
  446. '/_SX38_',
  447. '/_SY75_',
  448. '/nav-sprite/',
  449. '/loading/',
  450. ];
  451. foreach ($excludePatterns as $pattern) {
  452. if (stripos($url, $pattern) !== false) {
  453. return false;
  454. }
  455. }
  456. // Doit être une image
  457. if (!preg_match('/\.(jpg|jpeg|png|webp|gif)/i', $url)) {
  458. return false;
  459. }
  460. return true;
  461. }
  462. public function scrapeWithPuppeteerService(?string $isbn): ?string
  463. {
  464. $isbn = $this->cleanIsbn($isbn);
  465. if (empty($isbn)) {
  466. return null;
  467. }
  468. try {
  469. $apiKey = $_ENV['PUPPETEER_API_KEY'] ?? 'votre-cle-secrete';
  470. $serviceUrl = $_ENV['PUPPETEER_SERVICE_URL'] ?? 'http://votre-serveur.com:3000';
  471. $url = "{$serviceUrl}/scrape/amazon?isbn={$isbn}";
  472. $context = stream_context_create([
  473. 'http' => [
  474. 'timeout' => 60,
  475. 'header' => "X-API-Key: {$apiKey}\r\n"
  476. ]
  477. ]);
  478. $response = @file_get_contents($url, false, $context);
  479. if ($response) {
  480. $data = json_decode($response, true);
  481. if ($data['success'] && !empty($data['imageUrl'])) {
  482. return $data['imageUrl'];
  483. }
  484. }
  485. } catch (\Exception $e) {
  486. // Log error
  487. }
  488. return null;
  489. }
  490. /**
  491. * Scrape BDGuest pour trouver l'image de couverture
  492. */
  493. public function scrapeBDGuest(?string $isbn): ?string
  494. {
  495. $isbn = $this->cleanIsbn($isbn);
  496. if (empty($isbn)) {
  497. return null;
  498. }
  499. try {
  500. $searchUrl = "https://www.bdgest.com/search.php?RechSerie={$isbn}";
  501. $context = stream_context_create([
  502. 'http' => [
  503. 'timeout' => 15,
  504. 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\n"
  505. ]
  506. ]);
  507. $html = @file_get_contents($searchUrl, false, $context);
  508. if ($html) {
  509. // Pattern pour les images de couverture BDGuest
  510. if (preg_match('/<img[^>]+src="(https?:\/\/[^"]*bdgest[^"]*\/Couvertures[^"]+)"/', $html, $matches)) {
  511. return $matches[1];
  512. }
  513. // Alternative: chercher dans les vignettes
  514. if (preg_match('/<img[^>]+class="[^"]*couv[^"]*"[^>]+src="([^"]+)"/', $html, $matches)) {
  515. return $matches[1];
  516. }
  517. }
  518. } catch (\Exception $e) {
  519. // Silently fail
  520. }
  521. return null;
  522. }
  523. /**
  524. * Scrape Fnac pour trouver l'image de couverture
  525. */
  526. public function scrapeFnac(?string $isbn): ?string
  527. {
  528. $isbn = $this->cleanIsbn($isbn);
  529. if (empty($isbn)) {
  530. return null;
  531. }
  532. try {
  533. $searchUrl = "https://www.fnac.com/SearchResult/ResultList.aspx?Search={$isbn}";
  534. $context = stream_context_create([
  535. 'http' => [
  536. 'timeout' => 15,
  537. 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\n" .
  538. "Accept: text/html,application/xhtml+xml\r\n"
  539. ]
  540. ]);
  541. $html = @file_get_contents($searchUrl, false, $context);
  542. if ($html) {
  543. // Pattern pour les images Fnac
  544. if (preg_match('/<img[^>]+data-src="(https?:\/\/[^"]*static\.fnac-static\.com[^"]*\/multimedia[^"]+)"/', $html, $matches)) {
  545. return $matches[1];
  546. }
  547. // Alternative: src direct
  548. if (preg_match('/<img[^>]+src="(https?:\/\/[^"]*static\.fnac-static\.com[^"]*\/multimedia[^"]+)"/', $html, $matches)) {
  549. return $matches[1];
  550. }
  551. }
  552. } catch (\Exception $e) {
  553. // Silently fail
  554. }
  555. return null;
  556. }
  557. /**
  558. * Scrape Decitre pour trouver l'image de couverture
  559. */
  560. public function scrapeDecitre(?string $isbn): ?string
  561. {
  562. $isbn = $this->cleanIsbn($isbn);
  563. if (empty($isbn)) {
  564. return null;
  565. }
  566. try {
  567. $searchUrl = "https://www.decitre.fr/rechercher/result?q={$isbn}";
  568. $context = stream_context_create([
  569. 'http' => [
  570. 'timeout' => 15,
  571. 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\n"
  572. ]
  573. ]);
  574. $html = @file_get_contents($searchUrl, false, $context);
  575. if ($html) {
  576. // Pattern pour les images Decitre
  577. if (preg_match('/<img[^>]+data-src="(https?:\/\/[^"]*products\.decitre\.fr[^"]+)"/', $html, $matches)) {
  578. return $matches[1];
  579. }
  580. if (preg_match('/<img[^>]+src="(https?:\/\/[^"]*products\.decitre\.fr[^"]+)"/', $html, $matches)) {
  581. return $matches[1];
  582. }
  583. }
  584. } catch (\Exception $e) {
  585. // Silently fail
  586. }
  587. return null;
  588. }
  589. /**
  590. * Scrape ExcaliburComics pour trouver l'image de couverture
  591. */
  592. public function scrapeExcaliburComics(?string $isbn): ?string
  593. {
  594. $isbn = $this->cleanIsbn($isbn);
  595. if (empty($isbn)) {
  596. return null;
  597. }
  598. try {
  599. $searchUrl = "https://www.excaliburcomics.fr/search?q={$isbn}";
  600. $context = stream_context_create([
  601. 'http' => [
  602. 'timeout' => 15,
  603. 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\n"
  604. ]
  605. ]);
  606. $html = @file_get_contents($searchUrl, false, $context);
  607. if ($html) {
  608. // Pattern pour les images ExcaliburComics
  609. if (preg_match('/<img[^>]+class="[^"]*product[^"]*"[^>]+src="([^"]+)"/', $html, $matches)) {
  610. $imageUrl = $matches[1];
  611. if (!str_starts_with($imageUrl, 'http')) {
  612. $imageUrl = 'https://www.excaliburcomics.fr' . $imageUrl;
  613. }
  614. return $imageUrl;
  615. }
  616. }
  617. } catch (\Exception $e) {
  618. // Silently fail
  619. }
  620. return null;
  621. }
  622. /**
  623. * Recherche la meilleure image en essayant tous les sites disponibles via Puppeteer
  624. *
  625. * @param string|null $isbn
  626. * @return array Liste de toutes les images trouvées avec leur source
  627. */
  628. public function findAllCovers(?string $isbn): array
  629. {
  630. $isbn = $this->cleanIsbn($isbn);
  631. if (empty($isbn)) {
  632. return [];
  633. }
  634. // Utiliser le service Puppeteer pour tout scraper en une fois
  635. if (!($_ENV['PUPPETEER_SERVICE_URL'] ?? false)) {
  636. return [];
  637. }
  638. try {
  639. $apiKey = $_ENV['PUPPETEER_API_KEY'] ?? 'votre-cle-secrete';
  640. $serviceUrl = $_ENV['PUPPETEER_SERVICE_URL'] ?? 'http://localhost:3000';
  641. $url = "{$serviceUrl}/scrape/all?isbn={$isbn}";
  642. $context = stream_context_create([
  643. 'http' => [
  644. 'timeout' => 120, // 2 minutes pour scraper tous les sites
  645. 'header' => "X-API-Key: {$apiKey}\r\n"
  646. ]
  647. ]);
  648. $response = @file_get_contents($url, false, $context);
  649. if ($response) {
  650. $data = json_decode($response, true);
  651. if ($data['success'] && !empty($data['images'])) {
  652. return $data['images'];
  653. }
  654. }
  655. } catch (\Exception $e) {
  656. // Log error
  657. }
  658. return [];
  659. }
  660. }