From 5f15d14ae1f539b42a37b4b24c05e25c9db079c5 Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Mon, 30 Sep 2024 22:16:20 +0200 Subject: [PATCH] Convertion des images webp et png vers jpeg --- .env | 3 +- composer.json | 1 + composer.lock | 3 +- phpmd.xml | 6 +- src/Controller/MangaController.php | 15 +++- src/Manager/FileSystemManager.php | 29 ++++--- src/MessageHandler/DownloadChapterHandler.php | 62 ++++++++------- src/Service/CbzService.php | 76 +++++++++--------- src/Service/MangadexProvider.php | 62 +++++++-------- src/Service/Scraper/AbstractScraper.php | 78 +++++++++++++++---- src/Service/Scraper/HtmlScraper.php | 43 +++++----- src/Service/Scraper/JavascriptScraper.php | 4 +- src/Service/Scraper/ScraperFactory.php | 2 +- 13 files changed, 226 insertions(+), 158 deletions(-) diff --git a/.env b/.env index 4adb167..645af36 100644 --- a/.env +++ b/.env @@ -51,5 +51,4 @@ MERCURE_JWT_SECRET="Mangarr-JWT-Secret" ###< symfony/mercure-bundle ### #Custom -MANGA_DATA_PATH=/mnt/c/Users/jerem/Mangas -IMAGE_DATA_PATH=/mnt/c/Users/jerem/MangasImages +MANGA_DATA_PATH=/home/ext.jeremy.guillot@maxicoffee.domains/Mangarr diff --git a/composer.json b/composer.json index 1db3f69..8b12956 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "php": ">=8.3.1", "ext-ctype": "*", "ext-curl": "*", + "ext-gd": "*", "ext-iconv": "*", "ext-zip": "*", "api-platform/core": "^3.2", diff --git a/composer.lock b/composer.lock index 3d6d6dd..fe35cc4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2533b3293b9694632fddb7de78675af4", + "content-hash": "b4e296cfb0a526abcac95fe791a98dc7", "packages": [ { "name": "api-platform/core", @@ -12005,6 +12005,7 @@ "php": ">=8.3.1", "ext-ctype": "*", "ext-curl": "*", + "ext-gd": "*", "ext-iconv": "*", "ext-zip": "*" }, diff --git a/phpmd.xml b/phpmd.xml index 353539d..22855b2 100644 --- a/phpmd.xml +++ b/phpmd.xml @@ -16,9 +16,9 @@ - - - + + + diff --git a/src/Controller/MangaController.php b/src/Controller/MangaController.php index 7a7614a..f024c4d 100644 --- a/src/Controller/MangaController.php +++ b/src/Controller/MangaController.php @@ -203,6 +203,19 @@ class MangaController extends AbstractController return new JsonResponse(['success' => 'CBZ file deleted.'], 200); } + #[Route('/chapter/{id}/edit', name: 'app_chapter_edit', methods: ['POST'])] + public function editChapter(Request $request, Chapter $chapter): JsonResponse + { + $data = json_decode($request->getContent(), true); + + $chapter->setNumber($data['number']); + $chapter->setTitle($data['title']); + + $this->entityManager->flush(); + + return new JsonResponse(['success' => true, 'message' => 'Chapter updated successfully']); + } + #[Route('/hide_chapter/{id}', name: 'app_hide_chapter')] public function hideChapter(Chapter $chapter): JsonResponse { @@ -393,7 +406,7 @@ class MangaController extends AbstractController 'manga' => $manga, 'volume' => $volume, 'visible' => true - ]); + ], ['number' => 'ASC']); if (empty($volumeChapters)) { $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Aucun chapitre trouvé pour ce volume.']); diff --git a/src/Manager/FileSystemManager.php b/src/Manager/FileSystemManager.php index 39042b4..16de11d 100644 --- a/src/Manager/FileSystemManager.php +++ b/src/Manager/FileSystemManager.php @@ -15,12 +15,11 @@ class FileSystemManager private string $imageDirectory; public function __construct( - private readonly string $projectDir, - private readonly Filesystem $filesystem, + private readonly string $projectDir, + private readonly Filesystem $filesystem, private readonly SluggerInterface $slugger, private readonly AppSettingsManager $appSettingsManager - ) - { + ) { $this->loadSettings(); } @@ -43,17 +42,19 @@ class FileSystemManager public function getImagePath(string $subDir = ''): string { - if(!$this->filesystem->exists($this->projectDir. '/' . self::IMAGES_DIRECTORY . ($subDir ? "/$subDir" : ''))) { - $this->filesystem->mkdir($this->projectDir. '/' . self::IMAGES_DIRECTORY . ($subDir ? "/$subDir" : ''), 0755); + if (!$this->filesystem->exists($this->projectDir.'/'.self::IMAGES_DIRECTORY.($subDir ? "/$subDir" : ''))) { + $this->filesystem->mkdir($this->projectDir.'/'.self::IMAGES_DIRECTORY.($subDir ? "/$subDir" : ''), 0755); } - return $this->projectDir. '/' . self::IMAGES_DIRECTORY . ($subDir ? "/$subDir" : ''); + + return $this->projectDir.'/'.self::IMAGES_DIRECTORY.($subDir ? "/$subDir" : ''); } public function createMangaDirectory(string $mangaSlug, ?int $year): string { $year = $year ?? 'unknown'; - $directoryPath = $this->mangaDirectory . '/' . ucfirst($mangaSlug) . " ($year)"; + $directoryPath = $this->mangaDirectory.'/'.ucfirst($mangaSlug)." ($year)"; $this->filesystem->mkdir($directoryPath, 0755); + return $directoryPath; } @@ -61,14 +62,16 @@ class FileSystemManager { $volumeDir = sprintf('%s/volume_%02d', $mangaDir, $volume); $this->filesystem->mkdir($volumeDir, 0755); + return $volumeDir; } public function moveUploadedFile(string $sourcePath, string $destinationDir, string $originalFilename): string { $newFilename = $this->generateUniqueFilename($originalFilename); - $destinationPath = $destinationDir . '/' . $newFilename; + $destinationPath = $destinationDir.'/'.$newFilename; $this->filesystem->rename($sourcePath, $destinationPath, true); + return $destinationPath; } @@ -98,18 +101,20 @@ class FileSystemManager public function getUploadsDirectory(): string { - return $this->projectDir . '/' . self::UPLOADS_DIRECTORY; + return $this->projectDir.'/'.self::UPLOADS_DIRECTORY; } private function generateUniqueFilename(string $originalFilename): string { $safeFilename = $this->slugger->slug(pathinfo($originalFilename, PATHINFO_FILENAME)); - return $safeFilename . '-' . uniqid() . '.' . pathinfo($originalFilename, PATHINFO_EXTENSION); + + return $safeFilename.'-'.uniqid().'.'.pathinfo($originalFilename, PATHINFO_EXTENSION); } public function generateUniqueImageFilename(string $originalFilename): string { $safeFilename = $this->slugger->slug(pathinfo($originalFilename, PATHINFO_FILENAME)); - return $safeFilename . '-' . uniqid() . '.jpg'; + + return $safeFilename.'-'.uniqid().'.jpg'; } } diff --git a/src/MessageHandler/DownloadChapterHandler.php b/src/MessageHandler/DownloadChapterHandler.php index 460dcf4..1e4b774 100644 --- a/src/MessageHandler/DownloadChapterHandler.php +++ b/src/MessageHandler/DownloadChapterHandler.php @@ -8,7 +8,6 @@ use App\Repository\ChapterRepository; use App\Repository\ContentSourceRepository; use App\Service\NotificationService; use App\Service\Scraper\MangaScraperService; -use Exception; use GuzzleHttp\Exception\GuzzleException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -17,17 +16,15 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler; readonly class DownloadChapterHandler { public function __construct( - private ChapterRepository $chapterRepository, - private MangaScraperService $mangaScraperService, - private NotificationService $notificationService, + private ChapterRepository $chapterRepository, + private MangaScraperService $mangaScraperService, + private NotificationService $notificationService, private ContentSourceRepository $contentSourceRepository - ) - { - + ) { } /** - * @throws Exception + * @throws \Exception */ public function __invoke(DownloadChapter $message): void { @@ -35,7 +32,7 @@ readonly class DownloadChapterHandler if (!$chapter) { $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapter not found.']); throw new BadRequestHttpException('Chapter not found'); - } elseif ($chapter->getCbzPath() !== null) { + } elseif (null !== $chapter->getCbzPath()) { $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapter already scraped.']); throw new BadRequestHttpException('Chapter already downloaded'); } @@ -44,11 +41,17 @@ readonly class DownloadChapterHandler $preferredSources = $manga->getPreferredSources()->toArray(); $allSources = $this->contentSourceRepository->findAll(); - $filteredSources = array_udiff($allSources, $preferredSources, function ($a, $b) { - return $a->getId() - $b->getId(); - }); + // $filteredSources = array_udiff($allSources, $preferredSources, function ($a, $b) { + // return $a->getId() - $b->getId(); + // }); + // + // $sources = array_merge($preferredSources, $filteredSources); - $sources = array_merge($preferredSources, $filteredSources); + if (count($preferredSources) > 0) { + $sources = $preferredSources; + } else { + $sources = $allSources; + } $sources[] = (new ContentSource()) @@ -57,19 +60,18 @@ readonly class DownloadChapterHandler ->setChapterUrlFormat('at-home/server/%s') ->setScrapingType('mangadex'); - -// (new ContentSource()) -// ->setBaseUrl('https://lelscans.net') -// ->setImageSelector('#image img') -// ->setChapterUrlFormat('https://lelscans.net/scan-%s/%s') -// ->setNextPageSelector('a[title="Suivant"]') -// ->setScrapingType('html'), -// (new ContentSource()) -// ->setBaseUrl('https://darkscans.net/') -// ->setImageSelector('.reading-content img') -// ->setChapterUrlFormat('https://darkscans.net/mangas/%s/chapter-%s/') -// ->setNextPageSelector(null) -// ->setScrapingType('html') + // (new ContentSource()) + // ->setBaseUrl('https://lelscans.net') + // ->setImageSelector('#image img') + // ->setChapterUrlFormat('https://lelscans.net/scan-%s/%s') + // ->setNextPageSelector('a[title="Suivant"]') + // ->setScrapingType('html'), + // (new ContentSource()) + // ->setBaseUrl('https://darkscans.net/') + // ->setImageSelector('.reading-content img') + // ->setChapterUrlFormat('https://darkscans.net/mangas/%s/chapter-%s/') + // ->setNextPageSelector(null) + // ->setScrapingType('html') $scrapedSuccessfully = false; @@ -78,10 +80,10 @@ readonly class DownloadChapterHandler $this->mangaScraperService->scrapeChapter($chapter, $source); $scrapedSuccessfully = true; break; - } catch (Exception $e) { + } catch (\Exception $e) { $this->notificationService->sendUpdate([ 'status' => 'warning', - 'message' => 'An error occurred while scraping with source: ' . $source->getBaseUrl() . '. Trying next source...' + 'message' => 'An error occurred while scraping with source: '.$source->getBaseUrl().'. Trying next source...', ]); } catch (GuzzleException $e) { @@ -91,9 +93,9 @@ readonly class DownloadChapterHandler if (!$scrapedSuccessfully) { $this->notificationService->sendUpdate([ 'status' => 'error', - 'message' => 'All sources failed to scrape the chapter ' . $chapter->getManga()->getTitle() . ' ' . $chapter->getNumber() . '.' + 'message' => 'All sources failed to scrape the chapter '.$chapter->getManga()->getTitle().' '.$chapter->getNumber().'.', ]); - throw new Exception('All sources failed to scrape the chapter ' . $chapter->getManga()->getTitle() . ' ' . $chapter->getNumber() . '.'); + throw new \Exception('All sources failed to scrape the chapter '.$chapter->getManga()->getTitle().' '.$chapter->getNumber().'.'); } $this->notificationService->sendUpdate(['status' => 'success', 'message' => 'Chapter scraped successfully.']); diff --git a/src/Service/CbzService.php b/src/Service/CbzService.php index 1cd5f4c..569ccde 100644 --- a/src/Service/CbzService.php +++ b/src/Service/CbzService.php @@ -3,38 +3,35 @@ namespace App\Service; use App\Entity\Manga; -use Exception; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\String\Slugger\SluggerInterface; -use ZipArchive; class CbzService { - public function __construct(private SluggerInterface $slugger) { } /** - * @throws Exception + * @throws \Exception */ public function extractMetadata(string $filePath, string $originalFileName): array { - $zip = new ZipArchive(); + $zip = new \ZipArchive(); $fileInfo = $this->extractInfoFromFileName($originalFileName); $metadata['title'] = $fileInfo['title']; - $metadata['volume'] = $fileInfo['volume'] !== null ? (int)$fileInfo['volume'] : null; - $metadata['chapter'] = $fileInfo['chapter'] !== null ? (int)$fileInfo['chapter'] : null; + $metadata['volume'] = null !== $fileInfo['volume'] ? (int) $fileInfo['volume'] : null; + $metadata['chapter'] = null !== $fileInfo['chapter'] ? (int) $fileInfo['chapter'] : null; if (is_null($metadata['chapter'])) { try { $zip->open($filePath); $chapterNumbers = []; - for ($i = 0; $i < $zip->numFiles; $i++) { + for ($i = 0; $i < $zip->numFiles; ++$i) { $stat = $zip->statIndex($i); $fileName = $stat['name']; @@ -43,15 +40,15 @@ class CbzService $chapterNumbers = array_unique($chapterNumbers); - if (count($chapterNumbers) === 1) { - $metadata['chapter'] = array_values($chapterNumbers)[0] === '' ? null : (int)array_values($chapterNumbers)[0]; + if (1 === count($chapterNumbers)) { + $metadata['chapter'] = '' === array_values($chapterNumbers)[0] ? null : (int) array_values($chapterNumbers)[0]; } elseif (count($chapterNumbers) > 1) { $metadata['chapter'] = min($chapterNumbers); } $zip->close(); - } catch (Exception $e) { - throw new Exception("Impossible d'ouvrir le fichier CBZ. " . $e->getMessage()); + } catch (\Exception $e) { + throw new \Exception("Impossible d'ouvrir le fichier CBZ. ".$e->getMessage()); } } @@ -60,27 +57,31 @@ class CbzService public function getPageContent(string $cbzPath, int $pageNumber): ?string { - $zip = new ZipArchive(); - if ($zip->open($cbzPath) === TRUE) { + $zip = new \ZipArchive(); + if (true === $zip->open($cbzPath)) { $images = $this->getImageList($zip); if (isset($images[$pageNumber - 1])) { $content = $zip->getFromName($images[$pageNumber - 1]); $zip->close(); + return $content; } $zip->close(); } + return null; } public function getPageCount(string $cbzPath): int { - $zip = new ZipArchive(); - if ($zip->open($cbzPath) === TRUE) { + $zip = new \ZipArchive(); + if (true === $zip->open($cbzPath)) { $count = count($this->getImageList($zip)); $zip->close(); + return $count; } + return 0; } @@ -91,9 +92,9 @@ class CbzService $chapter = $this->extractChapter($fileName); return [ - 'title' => $title === '' ? null : $title, - 'volume' => $volume === '' ? null : $volume, - 'chapter' => $chapter === '' ? null : $chapter, + 'title' => '' === $title ? null : $title, + 'volume' => '' === $volume ? null : $volume, + 'chapter' => '' === $chapter ? null : $chapter, ]; } @@ -118,6 +119,7 @@ class CbzService if (preg_match($volumePattern, $fileName, $matches)) { return str_pad($matches['volume'], 2, '0', STR_PAD_LEFT); } + return ''; } @@ -136,52 +138,54 @@ class CbzService return ''; } - private function getImageList(ZipArchive $zip): array + private function getImageList(\ZipArchive $zip): array { $images = []; - for ($i = 0; $i < $zip->numFiles; $i++) { + for ($i = 0; $i < $zip->numFiles; ++$i) { $filename = $zip->getNameIndex($i); if (preg_match('/\.(jpg|jpeg|png|gif)$/i', $filename)) { $images[] = $filename; } } sort($images); + return $images; } public function createVolumeArchive(array $chapters): string { $tempFile = tempnam(sys_get_temp_dir(), 'volume_cbz_'); - $zip = new ZipArchive(); - if ($zip->open($tempFile, ZipArchive::CREATE) !== TRUE) { - throw new \RuntimeException("Impossible de créer le fichier ZIP temporaire."); + $zip = new \ZipArchive(); + if (true !== $zip->open($tempFile, \ZipArchive::CREATE)) { + throw new \RuntimeException('Impossible de créer le fichier ZIP temporaire.'); } foreach ($chapters as $chapter) { - $chapterZip = new ZipArchive(); - if ($chapterZip->open($chapter->getCbzPath()) === TRUE) { - for ($i = 0; $i < $chapterZip->numFiles; $i++) { + $chapterZip = new \ZipArchive(); + if (true === $chapterZip->open($chapter->getCbzPath())) { + for ($i = 0; $i < $chapterZip->numFiles; ++$i) { $filename = $chapterZip->getNameIndex($i); $fileContent = $chapterZip->getFromIndex($i); - $zip->addFromString("Chapter " . $chapter->getNumber() . "/" . $filename, $fileContent); + $zip->addFromString('Chapter '.$chapter->getNumber().'/'.$filename, $fileContent); } $chapterZip->close(); } } $zip->close(); + return $tempFile; } - public function generateFileName(Manga $manga, ?int $volume = null, ?float $chapterNumber = null): string + public function generateFileName(Manga $manga, int $volume = null, float $chapterNumber = null): string { $sluggedTitle = $this->slugger->slug($manga->getTitle())->lower(); - if ($volume !== null) { - return sprintf("%s_volume_%02d.cbz", $sluggedTitle, $volume); - } elseif ($chapterNumber !== null) { - return sprintf("%s_chapter_%s.cbz", $sluggedTitle, number_format($chapterNumber, 2)); + if (null !== $volume) { + return sprintf('%s_volume_%02d.cbz', $sluggedTitle, $volume); + } elseif (null !== $chapterNumber) { + return sprintf('%s_chapter_%s.cbz', $sluggedTitle, number_format($chapterNumber, 2)); } else { - throw new \InvalidArgumentException("Either volume or chapter number must be provided"); + throw new \InvalidArgumentException('Either volume or chapter number must be provided'); } } @@ -192,6 +196,7 @@ class CbzService ResponseHeaderBag::DISPOSITION_ATTACHMENT, $fileName ); + return $response; } @@ -201,6 +206,7 @@ class CbzService return false; } $firstCbzPath = $chapters[0]->getCbzPath(); + return array_reduce($chapters, function ($carry, $chapter) use ($firstCbzPath) { return $carry && $chapter->getCbzPath() === $firstCbzPath; }, true); @@ -209,7 +215,7 @@ class CbzService public function doAllChaptersHaveCbz(array $chapters): bool { return array_reduce($chapters, function ($carry, $chapter) { - return $carry && $chapter->getCbzPath() !== null; + return $carry && null !== $chapter->getCbzPath(); }, true); } } diff --git a/src/Service/MangadexProvider.php b/src/Service/MangadexProvider.php index 41f98c9..34d7af4 100644 --- a/src/Service/MangadexProvider.php +++ b/src/Service/MangadexProvider.php @@ -18,19 +18,20 @@ readonly class MangadexProvider implements MetadataProviderInterface public function search(?string $title): Collection { - if ($title === null) { + if (null === $title) { return new ArrayCollection(); } try { $results = $this->client->get('/manga', [ 'title' => $title, - 'contentRating' => ['safe', 'suggestive'], + 'contentRating' => ['safe', 'suggestive', 'erotica'], 'includes' => ['cover_art', 'author'], - 'limit' => 25 + 'limit' => 50, ]); } catch (\Exception $e) { $this->notificationService->sendUpdate('notification', ['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); + return new ArrayCollection(); } @@ -51,32 +52,34 @@ readonly class MangadexProvider implements MetadataProviderInterface $mangas[count($mangas) - 1]->setGenres($tags); foreach ($result['relationships'] as $relationship) { - if ($relationship['type'] === 'author') { + if ('author' === $relationship['type']) { $mangas[count($mangas) - 1]->setAuthor($relationship['attributes']['name']); } - if ($relationship['type'] === 'cover_art') { - $mangas[count($mangas) - 1]->setImageUrl('https://mangadex.org/covers/' . $result['id'] . '/' . $relationship['attributes']['fileName']); + if ('cover_art' === $relationship['type']) { + $mangas[count($mangas) - 1]->setImageUrl('https://mangadex.org/covers/'.$result['id'].'/'.$relationship['attributes']['fileName']); } } } - $test = array_map(fn($manga) => $manga->getExternalId(), $mangas); + $test = array_map(fn ($manga) => $manga->getExternalId(), $mangas); $ratings = $this->client->get('/statistics/manga', [ - 'manga' => $test + 'manga' => $test, ]); foreach ($mangas as $manga) { $manga->setRating($ratings['statistics'][$manga->getExternalId()]['rating']['average']); } + usort($mangas, fn ($a, $b) => $b->getRating() <=> $a->getRating()); + return new ArrayCollection($mangas); } public function getFeed(Manga $manga): array { - if ($manga->getExternalId() === null) { + if (null === $manga->getExternalId()) { return []; } @@ -90,7 +93,7 @@ readonly class MangadexProvider implements MetadataProviderInterface } else { break; } - $page++; + ++$page; } while (count($chapters) < $results['total']); return $this->getChaptersFromFeed($chapters, $manga); @@ -98,7 +101,7 @@ readonly class MangadexProvider implements MetadataProviderInterface public function getLastFeed(Manga $manga, int $limit = 100): array { - if ($manga->getExternalId() === null) { + if (null === $manga->getExternalId()) { return []; } @@ -111,6 +114,7 @@ readonly class MangadexProvider implements MetadataProviderInterface } } catch (\Exception $e) { $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching recent chapters from Mangadex.']); + return []; } @@ -120,14 +124,15 @@ readonly class MangadexProvider implements MetadataProviderInterface private function getFeedWithPagination(string $externalId, int $page, int $limit = 500, string $order = 'asc'): array { try { - $response = $this->client->get('/manga/' . $externalId . '/feed', [ + $response = $this->client->get('/manga/'.$externalId.'/feed', [ 'limit' => $limit, 'translatedLanguage' => ['en', 'fr'], 'order' => ['chapter' => $order], - 'offset' => $page * $limit + 'offset' => $page * $limit, ]); } catch (\Exception $e) { $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); + return []; } @@ -136,49 +141,44 @@ readonly class MangadexProvider implements MetadataProviderInterface public function getMangaAggregate(Manga $manga): array { - if ($manga->getExternalId() === null) { + if (null === $manga->getExternalId()) { return []; } try { - $response = $this->client->get('/manga/' . $manga->getExternalId() . '/aggregate'); + $response = $this->client->get('/manga/'.$manga->getExternalId().'/aggregate'); } catch (\Exception $e) { -// $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); + // $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); return []; } $chapterEntities = []; - if ($response['result'] === 'ok') { + if ('ok' === $response['result']) { foreach ($response['volumes'] as $volume) { - $volumeNumber = $volume['volume'] === 'none' ? 0 : (float)$volume['volume']; + $volumeNumber = 'none' === $volume['volume'] ? 0 : (float) $volume['volume']; foreach ($volume['chapters'] as $chapter) { $chapterEntity = new Chapter(); - $chapterEntity->setNumber((float)$chapter['chapter']) - ->setTitle('Chapter ' . $chapter['chapter']) + $chapterEntity->setNumber((float) $chapter['chapter']) + ->setTitle('Chapter '.$chapter['chapter']) ->setVolume($volumeNumber) ->setExternalId(''); $chapterEntities[] = $chapterEntity; -// $manga->addChapter($chapterEntity); + // $manga->addChapter($chapterEntity); } } } + return $chapterEntities; } - /** - * @param mixed $chapters - * @param Manga $manga - * @param array $chapterEntities - * @return array - */ public function getChaptersFromFeed(mixed $chapters, Manga $manga): array { $chapterEntities = []; $uniqueChapterNumbers = []; foreach ($chapters as $result) { - $chapterNumber = (float)$result['attributes']['chapter']; + $chapterNumber = (float) $result['attributes']['chapter']; // Vérifiez si le chapitre existe déjà dans la base de données $chapterExists = $manga->getChapters()->exists(function ($key, $existingChapter) use ($chapterNumber) { @@ -194,7 +194,7 @@ readonly class MangadexProvider implements MetadataProviderInterface $chapter = new Chapter(); $chapter->setNumber($chapterNumber) ->setTitle($result['attributes']['title']) - ->setVolume((int)$result['attributes']['volume'] ?? null) + ->setVolume((int) $result['attributes']['volume'] ?? null) ->setExternalId($result['id']); $chapterEntities[] = $chapter; @@ -219,8 +219,9 @@ readonly class MangadexProvider implements MetadataProviderInterface if (empty($allChapters)) { $this->notificationService->sendUpdate([ 'status' => 'error', - 'message' => 'No chapters found for this manga.' + 'message' => 'No chapters found for this manga.', ]); + return []; } @@ -247,6 +248,5 @@ readonly class MangadexProvider implements MetadataProviderInterface { $existingChapter->setVolume($newChapter->getVolume()); $existingChapter->setExternalId($newChapter->getExternalId()); - } } diff --git a/src/Service/Scraper/AbstractScraper.php b/src/Service/Scraper/AbstractScraper.php index 7327eca..dea8e16 100644 --- a/src/Service/Scraper/AbstractScraper.php +++ b/src/Service/Scraper/AbstractScraper.php @@ -8,7 +8,6 @@ use App\Entity\Manga; use App\Event\PageScrappingProgressEvent; use App\Manager\FileSystemManager; use Doctrine\ORM\EntityManagerInterface; -use Exception; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\RequestException; @@ -19,11 +18,10 @@ abstract class AbstractScraper implements ScraperInterface protected Client $httpClient; public function __construct( - protected FileSystemManager $fileSystemManager, + protected FileSystemManager $fileSystemManager, protected EventDispatcherInterface $eventDispatcher, - protected EntityManagerInterface $entityManager - ) - { + protected EntityManagerInterface $entityManager + ) { $this->httpClient = new Client(); } @@ -45,7 +43,8 @@ abstract class AbstractScraper implements ScraperInterface { try { $response = $this->httpClient->head($url); - return $response->getStatusCode() === 200; + + return 200 === $response->getStatusCode(); } catch (RequestException $e) { return false; } @@ -60,14 +59,15 @@ abstract class AbstractScraper implements ScraperInterface $chapter->getVolume(), $chapter->getNumber() ); - return $volumeDir . '/' . $fileName; + + return $volumeDir.'/'.$fileName; } protected function createCbzFile(array $pageData, string $cbzFilePath): void { $zip = new \ZipArchive(); - if ($zip->open($cbzFilePath, \ZipArchive::CREATE) === TRUE) { + if (true === $zip->open($cbzFilePath, \ZipArchive::CREATE)) { foreach ($pageData as $page) { $zip->addFile($page['local_image_url'], basename($page['local_image_url'])); } @@ -93,21 +93,67 @@ abstract class AbstractScraper implements ScraperInterface /** * @throws GuzzleException - * @throws Exception + * @throws \Exception */ - protected function downloadAndSaveImage(string $imageUrl, string $destinationPath): void + protected function downloadAndSaveImage(string $imageUrl, string $destinationPath): string { try { $response = $this->httpClient->get($imageUrl); $contentType = $response->getHeaderLine('Content-Type'); - if (str_starts_with($contentType, 'image/')) { - file_put_contents($destinationPath, $response->getBody()->getContents()); - } else { - throw new Exception('Le contenu récupéré n\'est pas une image. Type de contenu : ' . $contentType); + if (!str_starts_with($contentType, 'image/')) { + throw new \Exception('Le contenu récupéré n\'est pas une image. Type de contenu : '.$contentType); } - } catch (Exception $e) { - throw new Exception('Erreur lors de la récupération de l\'image : ' . $e->getMessage()); + + $imageData = $response->getBody()->getContents(); + $tempFilePath = $this->saveTempFile($imageData); + + $image = $this->createImageResource($tempFilePath, $contentType); + if (false === $image) { + throw new \Exception('Échec de la création de la ressource image.'); + } + + $destinationPath = $this->ensureJpgExtension($destinationPath); + if (!imagejpeg($image, $destinationPath)) { + imagedestroy($image); + unlink($tempFilePath); + throw new \Exception('Échec de la sauvegarde de l\'image en JPG.'); + } + + imagedestroy($image); + unlink($tempFilePath); + + return $destinationPath; + } catch (\Exception $e) { + throw new \Exception('Erreur lors de la récupération de l\'image : '.$e->getMessage()); } } + + private function saveTempFile(string $data): string + { + $tempFilePath = tempnam(sys_get_temp_dir(), 'manga_img_'); + file_put_contents($tempFilePath, $data); + + return $tempFilePath; + } + + /** + * @throws \Exception + */ + private function createImageResource(string $filePath, string $contentType) + { + return match ($contentType) { + 'image/webp' => imagecreatefromwebp($filePath), + 'image/png' => imagecreatefrompng($filePath), + 'image/jpeg', 'image/jpg' => imagecreatefromjpeg($filePath), + default => throw new \Exception('Format d\'image non pris en charge : '.$contentType), + }; + } + + private function ensureJpgExtension(string $path): string + { + $info = pathinfo($path); + + return $info['dirname'].'/'.$info['filename'].'.jpg'; + } } diff --git a/src/Service/Scraper/HtmlScraper.php b/src/Service/Scraper/HtmlScraper.php index ea383aa..419951f 100644 --- a/src/Service/Scraper/HtmlScraper.php +++ b/src/Service/Scraper/HtmlScraper.php @@ -4,17 +4,13 @@ namespace App\Service\Scraper; use App\Entity\Chapter; use App\Entity\ContentSource; -use Doctrine\ORM\EntityManagerInterface; -use Exception; -use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\DomCrawler\Crawler; class HtmlScraper extends AbstractScraper { /** - * @throws Exception + * @throws \Exception * @throws GuzzleException */ public function scrapeChapter(Chapter $chapter, ContentSource $contentSource): array|bool @@ -23,15 +19,15 @@ class HtmlScraper extends AbstractScraper $chapterUrl = $this->getValidChapterUrl($contentSource, $manga, $chapter->getNumber()); if (!$chapterUrl) { - throw new Exception("Aucune URL valide trouvée pour le chapitre {$chapter->getNumber()} du manga {$manga->getTitle()}"); + throw new \Exception("Aucune URL valide trouvée pour le chapitre {$chapter->getNumber()} du manga {$manga->getTitle()}"); } - $tempDir = sys_get_temp_dir() . '/' . uniqid('manga_scraper_'); + $tempDir = sys_get_temp_dir().'/'.uniqid('manga_scraper_'); mkdir($tempDir); $pageData = []; - if ($contentSource->getNextPageSelector() === null) { + if (null === $contentSource->getNextPageSelector()) { // Lecteur vertical $html = $this->fetchHtml($chapterUrl); $pageData = $this->scrapeVerticalReader($html, $contentSource); @@ -43,13 +39,13 @@ class HtmlScraper extends AbstractScraper // Télécharger et sauvegarder les images foreach ($pageData as $index => &$page) { $imageName = sprintf('%03d.%s', $index + 1, pathinfo(parse_url($page['image_url'], PHP_URL_PATH), PATHINFO_EXTENSION)); - $imagePath = $tempDir . '/' . $imageName; + $imagePath = $tempDir.'/'.$imageName; - $this->downloadAndSaveImage($page['image_url'], $imagePath); + $destinationPath = $this->downloadAndSaveImage($page['image_url'], $imagePath); $this->dispatchProgressEvent($chapter, $index + 1, count($pageData)); - $page['local_image_url'] = $imagePath; + $page['local_image_url'] = $destinationPath; } $cbzFilePath = $this->generateCbzPath($manga, $chapter); @@ -59,26 +55,25 @@ class HtmlScraper extends AbstractScraper $this->entityManager->persist($chapter); $this->entityManager->flush(); - // Nettoyage du répertoire temporaire $this->cleanupTempFiles($tempDir); return $pageData; } /** - * @throws Exception + * @throws \Exception */ public function testScraping(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array { $chapterUrl = $contentSource->getChapterUrl($mangaSlug, $chapterNumber); if (!$this->isChapterUrlValid($chapterUrl)) { - throw new \Exception("Invalid URL, check format and slug"); + throw new \Exception('Invalid URL, check format and slug'); } $html = $this->fetchHtml($chapterUrl); - if ($contentSource->getNextPageSelector() === null) { + if (null === $contentSource->getNextPageSelector()) { return $this->scrapeVerticalReader($html, $contentSource); } else { return $this->scrapeHorizontalReader($chapterUrl, $contentSource); @@ -87,7 +82,7 @@ class HtmlScraper extends AbstractScraper public function supports(string $scrapingType): bool { - return $scrapingType === 'html'; + return 'html' === $scrapingType; } private function scrapeVerticalReader(string $html, ContentSource $contentSource): array @@ -108,7 +103,7 @@ class HtmlScraper extends AbstractScraper } /** - * @throws Exception + * @throws \Exception */ private function scrapeHorizontalReader(string $chapterUrl, ContentSource $contentSource): array { @@ -135,18 +130,18 @@ class HtmlScraper extends AbstractScraper try { $response = $this->httpClient->get($url, [ 'http_errors' => true, - 'allow_redirects' => false + 'allow_redirects' => false, ]); $statusCode = $response->getStatusCode(); - if ($statusCode >= 300 && $statusCode < 400 || $statusCode == 404) { - throw new Exception('Chapter Not Found at ' . $url); + if ($statusCode >= 300 && $statusCode < 400 || 404 == $statusCode) { + throw new \Exception('Chapter Not Found at '.$url); } - return (string)$response->getBody(); - } catch (Exception $e) { - throw new Exception('Bad Request: ' . $e->getMessage()); + return (string) $response->getBody(); + } catch (\Exception $e) { + throw new \Exception('Bad Request: '.$e->getMessage()); } } @@ -164,7 +159,7 @@ class HtmlScraper extends AbstractScraper $urlComponents = parse_url($mangaSource->getBaseUrl()); $scheme = $urlComponents['scheme']; $host = $urlComponents['host']; - $imgUrl = $scheme . '://' . $host . '/' . ltrim($imgUrl, '/'); + $imgUrl = $scheme.'://'.$host.'/'.ltrim($imgUrl, '/'); } return [ diff --git a/src/Service/Scraper/JavascriptScraper.php b/src/Service/Scraper/JavascriptScraper.php index a9aea02..775fde5 100644 --- a/src/Service/Scraper/JavascriptScraper.php +++ b/src/Service/Scraper/JavascriptScraper.php @@ -40,10 +40,10 @@ class JavascriptScraper extends AbstractScraper $imageName = sprintf('%03d.%s', $index + 1, pathinfo(parse_url($page['image_url'], PHP_URL_PATH), PATHINFO_EXTENSION)); $imagePath = $tempDir . '/' . $imageName; - $this->downloadAndSaveImage($page['image_url'], $imagePath); + $destinationPath = $this->downloadAndSaveImage($page['image_url'], $imagePath); $this->dispatchProgressEvent($chapter, $index + 1, count($pageData)); - $page['local_image_url'] = $imagePath; + $page['local_image_url'] = $destinationPath; } $cbzFilePath = $this->generateCbzPath($manga, $chapter); diff --git a/src/Service/Scraper/ScraperFactory.php b/src/Service/Scraper/ScraperFactory.php index d7741c9..cd7ddaa 100644 --- a/src/Service/Scraper/ScraperFactory.php +++ b/src/Service/Scraper/ScraperFactory.php @@ -20,6 +20,6 @@ class ScraperFactory return $scraper; } } - throw new \InvalidArgumentException('Unsupported scraping type: ' . $contentSource->getScrapingType()); + throw new \InvalidArgumentException('Unsupported scraping type: '.$contentSource->getScrapingType()); } }