From 2cd42960c448cc93f10842d9681bc87b5d8b6258 Mon Sep 17 00:00:00 2001 From: Cedric Date: Wed, 1 Jul 2026 09:35:21 +0200 Subject: [PATCH] Actualiser api.php --- api.php | 203 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 157 insertions(+), 46 deletions(-) diff --git a/api.php b/api.php index 5739fd6..f156a7b 100644 --- a/api.php +++ b/api.php @@ -74,6 +74,39 @@ function httpGet($url, $timeout = 10, $headers = []) { return ($code === 200) ? $res : null; } +// Vérifie qu'une URL d'image répond bien en 200 (utilisé pour valider les URLs +// de couverture "devinées" à partir d'un ID, sans avoir à parser du HTML fragile) +function urlExists($url) { + if (empty($url)) return false; + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_NOBODY => true, // requête HEAD + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 8, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0', + ]); + curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + return $code === 200; +} + +// Retire les accents/diacritiques et normalise une chaîne pour construire +// une URL de type moviecovers.com (ex: "L'Étrange Noël" -> "L ETRANGE NOEL") +function removeAccentsForUrl($str) { + $str = str_replace(['œ', 'Œ'], ['oe', 'OE'], $str); + $str = str_replace(['æ', 'Æ'], ['ae', 'AE'], $str); + // Translittère les accents (fonctionne même si l'extension intl est absente) + $transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str); + if ($transliterated !== false && $transliterated !== '') { + $str = $transliterated; + } + return $str; +} + function cleanTitle($title) { $clean = preg_replace('/\s*[\[\(].*?[\]\)]\s*/', '', $title); $clean = preg_replace('/\s*-\s*(Édition|Edition|Collector|Simple|Spéciale|Digibook|Ultimate|Intégrale|Combo|SteelBook|Boîtier).*$/i', '', $clean); @@ -435,13 +468,23 @@ function fetchFromBlurayCom($ean) { return $empty; } - // Extraction des données - regex améliorées - // Titre - if (preg_match('/]*>([^<]+)<\/h1>/i', $movieHtml, $m)) { + // Extraction des données - regex robustes face à la refonte du site + // (le site a été redesigné : le

contient désormais un lien imbriqué, + // la div#movie_info / #movie_review_intro qui encadrait les infos a disparu, + // et le span#runtime a été remplacé par du texte brut dans une cellule de tableau) + + // Titre - le

peut désormais contenir un , donc on autorise les balises + // imbriquées avec (.*?) puis on nettoie avec strip_tags + if (preg_match('/]*>(.*?)<\/h1>/is', $movieHtml, $m)) { $rawTitle = trim(strip_tags($m[1])); $empty['title'] = trim(preg_replace('/\s*(Blu-ray|4K|3D|DVD|UHD).*$/i', '', $rawTitle)); } - + if (empty($empty['title']) && preg_match('/([^<]+)<\/title>/i', $movieHtml, $m)) { + // Filet de sécurité : la balise <title> de la page contient toujours le nom du film + $rawTitle = trim(html_entity_decode(strip_tags($m[1]))); + $empty['title'] = trim(preg_replace('/\s*(Blu-ray|4K|3D|DVD|UHD).*$/i', '', $rawTitle)); + } + // Année if (preg_match('/href="[^"]*year=(\d{4})[^"]*"[^>]*>(\d{4})<\/a>/i', $movieHtml, $m)) { $empty['year'] = $m[1]; @@ -452,13 +495,20 @@ function fetchFromBlurayCom($ean) { $empty['publisher'] = trim($m[1]); } - // Durée - if (preg_match('/<span[^>]*id="runtime"[^>]*>(\d+)\s*min<\/span>/i', $movieHtml, $m)) { + // Durée - le span#runtime n'existe plus ; on cherche "NNN min" à proximité + // du lien "année" déjà trouvé plus haut (ex: "... | 2012 | 123 min | Rated PG | ...") + if (preg_match('/movies\.php\?year=\d{4}[^>]*>\d{4}<\/a>.{0,80}?(\d{2,3})\s*min\b/is', $movieHtml, $m)) { + $empty['length'] = $m[1] . ' min'; + } elseif (preg_match('/\b(\d{2,3})\s*min\b/i', $movieHtml, $m)) { + // Filet de sécurité, moins précis $empty['length'] = $m[1] . ' min'; } - // Aspect ratio - if (preg_match('/Aspect[\s-]*ratio:\s*([\d\.]+:[\d\.]+)/i', $movieHtml, $m)) { + // Aspect ratio - "Original aspect ratio: X.XX:1" est plus fiable que le premier + // "Aspect ratio:" qui peut lister plusieurs valeurs (plans différents dans le film) + if (preg_match('/Original aspect ratio:\s*([\d\.]+:[\d\.]+)/i', $movieHtml, $m)) { + $empty['aspect_ratio'] = trim($m[1]); + } elseif (preg_match('/Aspect[\s-]*ratio:\s*([\d\.]+:[\d\.]+)/i', $movieHtml, $m)) { $empty['aspect_ratio'] = trim($m[1]); } @@ -480,41 +530,67 @@ function fetchFromBlurayCom($ean) { $empty['format'] = 'Blu-ray'; } - // Affiche HD - plusieurs patterns - if (preg_match('/src="(https:\/\/images\.static-bluray\.com\/movies\/covers\/\d+_front\.jpg[^"]*)"/i', $movieHtml, $m)) { + // Affiche HD - le site charge désormais les jaquettes en JS (lazy-load, + // le HTML statique ne contient qu'un visuel "Temporary cover art" en placeholder). + // On essaie d'abord les anciens motifs (au cas où ils réapparaissent sur certaines + // pages), puis on retombe sur l'URL CDN prévisible construite à partir de l'ID du + // film, en la validant par une requête HEAD avant de l'utiliser. + if (preg_match('/(?:src|data-src)="(https:\/\/images\d*\.static-bluray\.com\/movies\/covers\/\d+_(?:front|large)\.jpg[^"]*)"/i', $movieHtml, $m)) { $empty['poster'] = $m[1]; - } elseif (preg_match('/<img[^>]*class="coverfront"[^>]*src="([^"]+)"/i', $movieHtml, $m)) { - $posterUrl = $m[1]; - $posterUrl = preg_replace('/_large\.jpg/', '_front.jpg', $posterUrl); + } elseif (preg_match('/<img[^>]*class="[^"]*coverfront[^"]*"[^>]*(?:src|data-src)="([^"]+)"/i', $movieHtml, $m)) { + $posterUrl = preg_replace('/_large\.jpg/', '_front.jpg', $m[1]); $empty['poster'] = $posterUrl; - } - - // Synopsis, Réalisateur, Acteurs - if (preg_match('/<div[^>]*id="movie_info"[^>]*>(.*?)<div[^>]*id="movie_review_intro"/is', $movieHtml, $infoBlock)) { - $infoHtml = $infoBlock[1]; - - // Synopsis - if (preg_match('/<\/center><br>\s*(.*?)<br><br><br>Director:/is', $infoHtml, $m)) { - $synopsis = trim(strip_tags($m[1])); - $synopsis = preg_replace('/\s+/', ' ', $synopsis); - $empty['description'] = $synopsis; - } - - // Réalisateur - if (preg_match('/Director:\s*<a[^>]*>([^<]+)<\/a>/i', $infoHtml, $m)) { - $empty['director'] = trim($m[1]); - } - - // Acteurs - if (preg_match('/Starring:\s*(.*?)(?:<br>|<\/div>)/is', $infoHtml, $m)) { - $actorsHtml = $m[1]; - preg_match_all('/<a[^>]*>([^<]+)<\/a>/i', $actorsHtml, $actorMatches); - if (!empty($actorMatches[1])) { - $empty['actors'] = implode(', ', array_map('trim', array_slice($actorMatches[1], 0, 6))); + } else { + error_log("Blu-ray.com: ⚠️ Jaquette absente du HTML statique (lazy-load JS), tentative CDN prévisible pour ID $movieId"); + foreach (['_large.jpg', '_front.jpg'] as $suffix) { + $candidate = "https://images.static-bluray.com/movies/covers/{$movieId}{$suffix}"; + if (urlExists($candidate)) { + $empty['poster'] = $candidate; + break; } } + if (empty($empty['poster'])) { + error_log("Blu-ray.com: ❌ Aucune URL de jaquette CDN valide trouvée pour ID $movieId (fallback TMDB attendu)"); + } } - + + // Synopsis, Réalisateur, Acteurs + // La div#movie_info / #movie_review_intro qui encadrait ces infos a disparu de la + // refonte du site : "Director:" et "Starring:" apparaissent maintenant directement + // dans le flux HTML de la page, donc on cherche sur $movieHtml en entier plutôt + // que dans un sous-bloc délimité par ces anciens ID. + + // Réalisateur + if (preg_match('/Director:\s*<a[^>]*>([^<]+)<\/a>/i', $movieHtml, $m)) { + $empty['director'] = trim(html_entity_decode($m[1])); + } else { + error_log("Blu-ray.com: ⚠️ Réalisateur non trouvé (structure HTML probablement changée)"); + } + + // Acteurs + if (preg_match('/Starring:\s*(.*?)(?:<br\s*\/?>|<\/(?:p|div|td)>)/is', $movieHtml, $m)) { + preg_match_all('/<a[^>]*>([^<]+)<\/a>/i', $m[1], $actorMatches); + if (!empty($actorMatches[1])) { + $empty['actors'] = implode(', ', array_map(fn($a) => trim(html_entity_decode($a)), array_slice($actorMatches[1], 0, 6))); + } + } else { + error_log("Blu-ray.com: ⚠️ Acteurs non trouvés (structure HTML probablement changée)"); + } + + // Synopsis - on capture le dernier bloc de texte "libre" (hors balises) qui + // précède immédiatement "Director:", en tolérant quelques balises intermédiaires + if (preg_match('/>([^<]{60,900})<(?:[^>]*>){0,5}\s*Director:/is', $movieHtml, $m)) { + $synopsis = trim(html_entity_decode(strip_tags($m[1]))); + $synopsis = preg_replace('/\s+/', ' ', $synopsis); + $empty['description'] = $synopsis; + } else { + error_log("Blu-ray.com: ⚠️ Synopsis non trouvé (structure HTML probablement changée)"); + } + + if (empty($empty['title'])) { + error_log("Blu-ray.com: ⚠️ Titre non extrait malgré une page trouvée pour ID $movieId — vérifier la structure du <h1>/<title>"); + } + error_log("Blu-ray.com: ✅ Données récupérées pour EAN $ean → " . $empty['title']); return $empty; } @@ -528,24 +604,55 @@ function fetchFromMovieCovers($title, $year = '') { ]; if (empty($title)) return $empty; - - // Nettoyer le titre pour l'URL - $urlTitle = strtoupper(str_replace(' ', '+', $title)); + + // Nettoyer le titre pour l'URL : moviecovers.com attend un titre en MAJUSCULES + // SANS accents ni apostrophes/ponctuation (ex: "L'Étrange Noël" -> "L+ETRANGE+NOEL"). + // C'est la cause principale des échecs silencieux : un titre accentué produit une + // URL qui ne correspond à aucune fiche et retombe sur la page "non trouvé". + $normalized = removeAccentsForUrl($title); + $normalized = str_replace(['œ', 'Œ'], ['OE', 'OE'], $normalized); + $normalized = preg_replace('/[^A-Za-z0-9 ]+/', ' ', $normalized); // apostrophes, ':', '-', etc. -> espace + $normalized = preg_replace('/\s{2,}/', ' ', trim($normalized)); + $urlTitle = strtoupper(str_replace(' ', '+', $normalized)); + + if ($urlTitle === '') { + error_log("MovieCovers: ❌ Titre vide après normalisation pour '$title'"); + return $empty; + } + $url = "https://moviecovers.com/film/titre_{$urlTitle}.html"; - + error_log("MovieCovers: 🔍 Recherche '$title' → $url"); + $html = httpGet($url, 10); - if (!$html) return $empty; - + if (!$html) { + error_log("MovieCovers: ❌ Échec HTTP pour $url"); + return $empty; + } + + // Le site répond en 200 même quand le film n'existe pas ("Je n'ai pas trouvé de film") + if (stripos($html, "n'ai pas trouv") !== false) { + error_log("MovieCovers: ❌ Film non trouvé pour '$title' (URL: $url)"); + return $empty; + } + // Extraire le titre if (preg_match('/<TITLE>([^<]+)<\/TITLE>/i', $html, $m)) { $empty['title'] = trim($m[1]); + } elseif (preg_match('/<meta[^>]*property="og:title"[^>]*content="([^"]+)"/i', $html, $m)) { + $empty['title'] = trim(html_entity_decode($m[1])); } - // Extraire l'affiche + // Extraire l'affiche - plusieurs filets de sécurité, l'ordre des attributs + // (src/alt/title) dans le <img> n'étant pas garanti if (preg_match('/<img[^>]*src="(https:\/\/moviecovers\.com\/DATA\/thumbs\/[^"]+)"[^>]*title="Recto/i', $html, $m)) { $empty['poster'] = $m[1]; + } elseif (preg_match('/title="Recto[^"]*"[^>]*src="(https:\/\/moviecovers\.com\/DATA\/thumbs\/[^"]+)"/i', $html, $m)) { + // Même image mais avec title AVANT src dans la balise + $empty['poster'] = $m[1]; } elseif (preg_match('/<meta[^>]*property="og:image"[^>]*content="([^"]+)"/i', $html, $m)) { $empty['poster'] = $m[1]; + } elseif (preg_match('/<meta[^>]*(?:property|name)="twitter:image"[^>]*content="([^"]+)"/i', $html, $m)) { + $empty['poster'] = $m[1]; } // Extraire le réalisateur @@ -576,8 +683,12 @@ function fetchFromMovieCovers($title, $year = '') { // Format haute qualité de l'affiche if ($empty['poster']) { $empty['poster'] = str_replace('/thumbs/', '/films-l/', $empty['poster']); + } else { + error_log("MovieCovers: ⚠️ Jaquette non trouvée pour '$title' (URL: $url) — structure HTML probablement changée"); } - + + error_log("MovieCovers: " . (!empty($empty['title']) ? "✅ Données récupérées pour '$title' → " . $empty['title'] : "⚠️ Page trouvée mais titre non extrait pour '$title'")); + return $empty; }