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());
}
}