diff --git a/Dockerfile b/Dockerfile index 4669d62..2dfc14b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,10 @@ FROM php:8.3-apache RUN a2enmod rewrite headers \ - && docker-php-ext-install opcache + && apt-get update \ + && apt-get install -y --no-install-recommends libcurl4-openssl-dev \ + && docker-php-ext-install opcache curl \ + && rm -rf /var/lib/apt/lists/* COPY docker/apache.conf /etc/apache2/sites-available/000-default.conf COPY docker/php.ini /usr/local/etc/php/conf.d/jeuxvideotech.ini @@ -11,7 +14,6 @@ WORKDIR /var/www/html COPY public /var/www/html/public COPY src /var/www/html/src COPY templates /var/www/html/templates -COPY data /var/www/html/data RUN mkdir -p /var/www/html/data/jaquettes /var/www/html/data/cache \ && chown -R www-data:www-data /var/www/html/data diff --git a/src/Controllers/JeuController.php b/src/Controllers/JeuController.php index da80610..d0e8fef 100644 --- a/src/Controllers/JeuController.php +++ b/src/Controllers/JeuController.php @@ -69,7 +69,9 @@ final class JeuController "titrePage" => $jeu["titre"], "utilisateur" => $utilisateur, "jeu" => $jeu, - "console" => $console + "console" => $console, + "jaquetteAbsente" => Helpers::jaquetteAbsente($jeu["jaquette"] ?? null), + "apiDisponible" => $this->enrichissementService->estDisponible() ]); } @@ -93,6 +95,7 @@ final class JeuController try { $donnees = $this->collecterDonneesFormulaire(); + $donnees = $this->completerJaquetteSiAbsente($donnees); $jeu = $this->jeuService->creer($utilisateur["id"], $donnees); Flash::definir("success", "Jeu ajouté à votre bibliothèque."); Helpers::rediriger("/jeux/" . $jeu["id"]); @@ -134,6 +137,7 @@ final class JeuController try { $donnees = $this->collecterDonneesFormulaire(); + $donnees = $this->completerJaquetteSiAbsente($donnees); $jeu = $this->jeuService->modifier($id, $utilisateur["id"], $donnees); Flash::definir("success", "Jeu mis à jour."); Helpers::rediriger("/jeux/" . $jeu["id"]); @@ -229,6 +233,76 @@ final class JeuController Helpers::rediriger($retour . $separateur . $query); } + public function recupererJaquette(string $id): void + { + $utilisateur = $this->auth->exigerConnexion(); + Csrf::exiger(); + + $jeu = $this->jeuService->trouverPourUtilisateur($id, $utilisateur["id"]); + + if ($jeu === null) + { + Flash::definir("danger", "Jeu introuvable."); + Helpers::rediriger("/jeux"); + } + + if (!Helpers::jaquetteAbsente($jeu["jaquette"] ?? null)) + { + Flash::definir("success", "Ce jeu a déjà une jaquette."); + Helpers::rediriger("/jeux/" . $id); + } + + if (!$this->enrichissementService->estDisponible()) + { + Flash::definir("warning", "Clé API RAWG absente : impossible de récupérer la jaquette."); + Helpers::rediriger("/jeux/" . $id); + } + + $fichier = $this->enrichissementService->recupererJaquetteParTitre($jeu["titre"] ?? ""); + + if ($fichier === null) + { + Flash::definir("warning", "Aucune jaquette trouvée pour ce titre."); + Helpers::rediriger("/jeux/" . $id); + } + + $this->jeuService->modifier($id, $utilisateur["id"], array_merge($jeu, [ + "jaquette" => $fichier, + "genres" => $jeu["genres"] ?? [], + "sourceEnrichissement" => ($jeu["sourceEnrichissement"] ?? "") === "manuel" ? "api" : ($jeu["sourceEnrichissement"] ?? "api") + ])); + + Flash::definir("success", "Jaquette récupérée depuis RAWG."); + Helpers::rediriger("/jeux/" . $id); + } + + private function completerJaquetteSiAbsente(array $donnees): array + { + if (!Helpers::jaquetteAbsente($donnees["jaquette"] ?? null)) + { + return $donnees; + } + + if (!$this->enrichissementService->estDisponible()) + { + return $donnees; + } + + $fichier = $this->enrichissementService->recupererJaquetteParTitre($donnees["titre"] ?? ""); + + if ($fichier !== null) + { + $donnees["jaquette"] = $fichier; + + if (($donnees["sourceEnrichissement"] ?? "manuel") === "manuel") + { + $donnees["sourceEnrichissement"] = "api"; + } + } + + return $donnees; + } + private function collecterDonneesFormulaire(): array { return [ diff --git a/src/Helpers.php b/src/Helpers.php index eae2c67..dec461f 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -54,17 +54,34 @@ final class Helpers public static function urlJaquette(?string $chemin): string { - if ($chemin === null || $chemin === "") + if (self::jaquetteAbsente($chemin)) { return "/assets/img/jaquette-manquante.svg"; } - if (str_starts_with($chemin, "http://") || str_starts_with($chemin, "https://")) + if (str_starts_with((string) $chemin, "http://") || str_starts_with((string) $chemin, "https://")) { - return $chemin; + return (string) $chemin; } - return "/jaquettes/" . ltrim($chemin, "/"); + return "/jaquettes/" . ltrim((string) $chemin, "/"); + } + + public static function jaquetteAbsente(?string $chemin): bool + { + if ($chemin === null || trim($chemin) === "") + { + return true; + } + + if (str_starts_with($chemin, "http://") || str_starts_with($chemin, "https://")) + { + return false; + } + + $fichier = Config::get("jaquettesPath") . "/" . basename($chemin); + + return !is_file($fichier); } public static function tronquer(string $texte, int $longueur = 140): string diff --git a/src/Services/EnrichissementService.php b/src/Services/EnrichissementService.php index acd1685..7fc22b7 100644 --- a/src/Services/EnrichissementService.php +++ b/src/Services/EnrichissementService.php @@ -82,6 +82,50 @@ final class EnrichissementService return $detail; } + /** + * Cherche une jaquette RAWG pour un titre et la télécharge localement. + * Ne fait rien (retourne null) si l'API est indisponible ou aucun résultat. + */ + public function recupererJaquetteParTitre(string $titre): ?string + { + $titre = trim($titre); + + if ($titre === "" || !$this->estDisponible()) + { + return null; + } + + $resultats = $this->rechercher($titre, 5); + + foreach ($resultats as $resultat) + { + $url = $resultat["jaquetteUrl"] ?? null; + + if ($url === null || $url === "") + { + continue; + } + + $fichier = $this->telechargerJaquette($url); + + if ($fichier !== null && !str_starts_with($fichier, "http")) + { + return $fichier; + } + + if ($fichier !== null) + { + return $fichier; + } + } + + return null; + } + + /** + * Télécharge une image distante dans data/jaquettes/. + * En cas d'échec réseau, conserve l'URL distante comme repli. + */ public function telechargerJaquette(?string $urlImage): ?string { if ($urlImage === null || $urlImage === "") @@ -89,8 +133,15 @@ final class EnrichissementService return null; } + $dossier = Config::get("jaquettesPath"); + + if (!is_dir($dossier)) + { + mkdir($dossier, 0775, true); + } + $extension = pathinfo(parse_url($urlImage, PHP_URL_PATH) ?? "", PATHINFO_EXTENSION); - $extension = $extension !== "" ? strtolower($extension) : "jpg"; + $extension = $extension !== "" ? strtolower(explode("?", $extension)[0]) : "jpg"; if (!in_array($extension, ["jpg", "jpeg", "png", "webp", "gif"], true)) { @@ -98,18 +149,10 @@ final class EnrichissementService } $nomFichier = Helpers::genererId() . "." . $extension; - $cheminLocal = Config::get("jaquettesPath") . "/" . $nomFichier; + $cheminLocal = $dossier . "/" . $nomFichier; + $contenu = $this->telechargerContenu($urlImage); - $contexte = stream_context_create([ - "http" => [ - "timeout" => 10, - "header" => "User-Agent: JeuxVideoTech/1.0\r\n" - ] - ]); - - $contenu = @file_get_contents($urlImage, false, $contexte); - - if ($contenu === false) + if ($contenu === null || $contenu === "") { return $urlImage; } @@ -122,6 +165,51 @@ final class EnrichissementService return $nomFichier; } + private function telechargerContenu(string $url): ?string + { + if (function_exists("curl_init")) + { + $curl = curl_init($url); + + if ($curl !== false) + { + curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_USERAGENT => "JeuxVideoTech/1.0", + CURLOPT_SSL_VERIFYPEER => true + ]); + + $contenu = curl_exec($curl); + $codeHttp = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + + if ($contenu !== false && $codeHttp >= 200 && $codeHttp < 300) + { + return $contenu; + } + } + } + + $contexte = stream_context_create([ + "http" => [ + "method" => "GET", + "timeout" => 15, + "follow_location" => 1, + "header" => "User-Agent: JeuxVideoTech/1.0\r\n" + ], + "ssl" => [ + "verify_peer" => true, + "verify_peer_name" => true + ] + ]); + + $contenu = @file_get_contents($url, false, $contexte); + + return $contenu === false ? null : $contenu; + } + private function mapperResultatListe(array $jeu): array { $genres = []; diff --git a/src/bootstrap.php b/src/bootstrap.php index 375a91b..717bb5b 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -73,6 +73,7 @@ $router->post("/jeux/api/appliquer", [$jeuController, "appliquerEnrichissement"] $router->get("/jeux/{id}", [$jeuController, "detail"]); $router->get("/jeux/{id}/modifier", [$jeuController, "modifier"]); $router->post("/jeux/{id}/modifier", [$jeuController, "modifier"]); +$router->post("/jeux/{id}/jaquette", [$jeuController, "recupererJaquette"]); $router->post("/jeux/{id}/supprimer", [$jeuController, "supprimer"]); $router->get("/admin/utilisateurs", [$adminController, "index"]); diff --git a/templates/jeux/detail.php b/templates/jeux/detail.php index d23ab3d..04b136f 100644 --- a/templates/jeux/detail.php +++ b/templates/jeux/detail.php @@ -23,6 +23,14 @@ /modifier" class="btn btn-neon"> Modifier + +
+