10 Commits

Author SHA1 Message Date
ext.jeremy.guillot@maxicoffee.domains
0482ec9f7f fix: listening on port 8081 + deployer 2025-02-02 18:05:08 +01:00
ext.jeremy.guillot@maxicoffee.domains
9318d0a9a0 fix: cbz path is now in public/cbz 2025-02-01 22:41:41 +01:00
ext.jeremy.guillot@maxicoffee.domains
447f1fbe84 fix: mercure credentials for prod 2025-02-01 19:08:50 +01:00
ext.jeremy.guillot@maxicoffee.domains
59cf4cd3c1 fix: mercureUrl for prod 2025-02-01 18:53:27 +01:00
ext.jeremy.guillot@maxicoffee.domains
d62907a38c fix: mercureUrl for prod 2025-02-01 18:40:59 +01:00
ext.jeremy.guillot@maxicoffee.domains
c6bd6ba549 fix: mercureUrl for prod 2025-02-01 18:16:46 +01:00
ThysTips
d4142012ec fix: npm deployment script
Signed-off-by: ThysTips <contact@antoinethys.com>
2025-02-01 17:02:55 +01:00
ThysTips
8811d3dd5e build: Add php deployer
Signed-off-by: ThysTips <contact@antoinethys.com>
2025-02-01 16:50:51 +01:00
ext.jeremy.guillot@maxicoffee.domains
2941bbecd1 Previous chapter fix
Chapter not found now redirect to chapters_show
2024-10-04 10:27:59 +02:00
ext.jeremy.guillot@maxicoffee.domains
5f15d14ae1 Convertion des images webp et png vers jpeg 2024-09-30 22:16:20 +02:00
19 changed files with 347 additions and 176 deletions

3
.env
View File

@@ -51,5 +51,4 @@ MERCURE_JWT_SECRET="Mangarr-JWT-Secret"
###< symfony/mercure-bundle ### ###< symfony/mercure-bundle ###
#Custom #Custom
MANGA_DATA_PATH=/mnt/c/Users/jerem/Mangas MANGA_DATA_PATH=/home/ext.jeremy.guillot@maxicoffee.domains/Mangarr
IMAGE_DATA_PATH=/mnt/c/Users/jerem/MangasImages

View File

@@ -30,8 +30,8 @@ export default class extends Controller {
} }
const mercureHubUrl = 'https://localhost/.well-known/mercure'; const mercureHubUrl = 'https://mangarr.test.nestor-server.fr/.well-known/mercure';
const eventSource = new EventSource(`${mercureHubUrl}?topic=activity`); const eventSource = new EventSource(`${mercureHubUrl}?topic=activity`, {withCredentials: true});
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);

View File

@@ -11,8 +11,8 @@ export default class extends Controller {
this.currentPage = 0; this.currentPage = 0;
this.totalPages = 0; this.totalPages = 0;
const mercureHubUrl = 'https://localhost/.well-known/mercure'; const mercureHubUrl = 'https://mangarr.test.nestor-server.fr/.well-known/mercure';
this.eventSource = new EventSource(`${mercureHubUrl}?topic=activity`); this.eventSource = new EventSource(`${mercureHubUrl}?topic=activity`, {withCredentials: true});
this.eventSource.onmessage = this.handleMessage.bind(this); this.eventSource.onmessage = this.handleMessage.bind(this);
} }

View File

@@ -9,8 +9,8 @@ export default class extends Controller {
// ... // ...
connect() { connect() {
const topic = this.data.get('topic'); const topic = this.data.get('topic');
const mercureHubUrl = 'https://localhost/.well-known/mercure'; const mercureHubUrl = 'https://mangarr.test.nestor-server.fr/.well-known/mercure';
const eventSource = new EventSource(`${mercureHubUrl}?topic=${topic}`); const eventSource = new EventSource(`${mercureHubUrl}?topic=${topic}`, {withCredentials: true});
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);

View File

@@ -25,7 +25,7 @@ services:
ports: ports:
# HTTP # HTTP
- target: 80 - target: 80
published: ${HTTP_PORT:-80} published: ${HTTP_PORT:-8081}
protocol: tcp protocol: tcp
# HTTPS # HTTPS
- target: 443 - target: 443

View File

@@ -9,6 +9,7 @@
"php": ">=8.3.1", "php": ">=8.3.1",
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-gd": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-zip": "*", "ext-zip": "*",
"api-platform/core": "^3.2", "api-platform/core": "^3.2",
@@ -104,6 +105,7 @@
}, },
"require-dev": { "require-dev": {
"dbrekelmans/bdi": "^1.3", "dbrekelmans/bdi": "^1.3",
"deployer/deployer": "^7.5",
"doctrine/doctrine-fixtures-bundle": "^3.5", "doctrine/doctrine-fixtures-bundle": "^3.5",
"friendsofphp/php-cs-fixer": "^3.48", "friendsofphp/php-cs-fixer": "^3.48",
"mtdowling/jmespath.php": "^2.7", "mtdowling/jmespath.php": "^2.7",

61
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "2533b3293b9694632fddb7de78675af4", "content-hash": "6258706876617c8b0c08f13c5a158fe7",
"packages": [ "packages": [
{ {
"name": "api-platform/core", "name": "api-platform/core",
@@ -9244,6 +9244,60 @@
}, },
"time": "2024-02-22T15:29:35+00:00" "time": "2024-02-22T15:29:35+00:00"
}, },
{
"name": "deployer/deployer",
"version": "v7.5.8",
"source": {
"type": "git",
"url": "https://github.com/deployphp/deployer.git",
"reference": "4900fe799ce5566d54a14103cdfd6e865b7c5d72"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/deployphp/deployer/zipball/4900fe799ce5566d54a14103cdfd6e865b7c5d72",
"reference": "4900fe799ce5566d54a14103cdfd6e865b7c5d72",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^8.0|^7.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.64",
"pestphp/pest": "^3.3",
"phpstan/phpstan": "^1.4",
"phpunit/php-code-coverage": "^11.0",
"phpunit/phpunit": "^11.4"
},
"bin": [
"bin/dep"
],
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Anton Medvedev",
"email": "anton@medv.io"
}
],
"description": "Deployment Tool",
"homepage": "https://deployer.org",
"support": {
"docs": "https://deployer.org/docs",
"issues": "https://github.com/deployphp/deployer/issues",
"source": "https://github.com/deployphp/deployer"
},
"funding": [
{
"url": "https://github.com/sponsors/antonmedv",
"type": "github"
}
],
"time": "2024-11-27T21:35:20+00:00"
},
{ {
"name": "doctrine/data-fixtures", "name": "doctrine/data-fixtures",
"version": "1.7.0", "version": "1.7.0",
@@ -11998,16 +12052,17 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": {},
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": ">=8.3.1", "php": ">=8.3.1",
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-gd": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-zip": "*" "ext-zip": "*"
}, },
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.6.0"
} }

39
deploy.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
namespace Deployer;
require 'recipe/symfony.php';
// require 'contrib/webpack_encore.php';
require 'contrib/npm.php';
// Config
set('nodejs_version', 'node_22.x');
set('keep_releases', '3');
set('repository', 'gitea@git.test.nestor-server.fr:Colgora/Mangarr.git');
set('webpack_encore/env', 'production');
set('composer_options', '--verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader');
set('shared_files', ['.env.local','var/log/prod.log']);
set('shared_dirs', ['config/secrets','public/cbz','public/tmp','public/images']);
// add('writable_dirs', []);
desc('Runs webpack encore build');
task('webpack_encore:build', function () {
run("cd {{release_path}} && npm run build");
});
desc('Run messenger consume');
task('messenger:consume', function () {
run("sudo supervisorctl restart messenger-consume:*");
});
host('mangarr.test.nestor-server.fr')
->set('remote_user', 'colgora')
->set('deploy_path', '/var/www/mangarr')
->set('branch', 'main');
// Hooks
after('deploy:vendors', 'npm:install');
after('npm:install', 'webpack_encore:build');
after('deploy:vendors', 'database:migrate');
after('deploy:symlink', 'messenger:consume');
after('deploy:failed', 'deploy:unlock');

View File

@@ -16,9 +16,9 @@
</properties> </properties>
<rule ref="rulesets/codesize.xml"/> <rule ref="rulesets/codesize.xml"/>
<rule ref="rulesets/cleancode.xml"/> <!-- <rule ref="rulesets/cleancode.xml"/>-->
<rule ref="rulesets/controversial.xml"/> <!-- <rule ref="rulesets/controversial.xml"/>-->
<rule ref="rulesets/design.xml"/> <!-- <rule ref="rulesets/design.xml"/>-->
<rule ref="rulesets/naming.xml"/> <rule ref="rulesets/naming.xml"/>
<rule ref="rulesets/unusedcode.xml"/> <rule ref="rulesets/unusedcode.xml"/>
</ruleset> </ruleset>

View File

@@ -203,6 +203,19 @@ class MangaController extends AbstractController
return new JsonResponse(['success' => 'CBZ file deleted.'], 200); 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')] #[Route('/hide_chapter/{id}', name: 'app_hide_chapter')]
public function hideChapter(Chapter $chapter): JsonResponse public function hideChapter(Chapter $chapter): JsonResponse
{ {
@@ -393,7 +406,7 @@ class MangaController extends AbstractController
'manga' => $manga, 'manga' => $manga,
'volume' => $volume, 'volume' => $volume,
'visible' => true 'visible' => true
]); ], ['number' => 'ASC']);
if (empty($volumeChapters)) { if (empty($volumeChapters)) {
$this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Aucun chapitre trouvé pour ce volume.']); $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Aucun chapitre trouvé pour ce volume.']);

View File

@@ -4,6 +4,7 @@ namespace App\Controller;
use App\Repository\MangaRepository; use App\Repository\MangaRepository;
use App\Service\CbzService; use App\Service\CbzService;
use App\Service\NotificationService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -12,8 +13,9 @@ use Symfony\Component\Routing\Attribute\Route;
class ReaderController extends AbstractController class ReaderController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly MangaRepository $mangaRepository, private readonly MangaRepository $mangaRepository,
private readonly CbzService $cbzService private readonly CbzService $cbzService,
private readonly NotificationService $notificationService,
) )
{ {
} }
@@ -32,7 +34,11 @@ class ReaderController extends AbstractController
} }
if (is_null($chapter->getCbzPath())) { if (is_null($chapter->getCbzPath())) {
throw $this->createNotFoundException("Le chapitre demandé n'a pas été scrapé."); $this->notificationService->sendUpdate([
'status' => 'error',
'message' => 'Le chapitre demandé n\'est pas encore disponible.',
]);
return $this->redirectToRoute('app_manga_show', ['mangaSlug' => $mangaSlug]);
} }
$totalPages = $this->cbzService->getPageCount($chapter->getCbzPath()); $totalPages = $this->cbzService->getPageCount($chapter->getCbzPath());
@@ -81,10 +87,9 @@ class ReaderController extends AbstractController
$chapters = array_values(array_map(fn($chapter) => [ $chapters = array_values(array_map(fn($chapter) => [
'number' => $chapter->getNumber(), 'number' => $chapter->getNumber(),
'title' => $chapter->getTitle() 'title' => $chapter->getTitle(),
], $chapters)); ], $chapters));
return $this->json($chapters); return $this->json($chapters);
} }
@@ -96,13 +101,17 @@ class ReaderController extends AbstractController
throw $this->createNotFoundException("Le manga demandé n'existe pas."); throw $this->createNotFoundException("Le manga demandé n'existe pas.");
} }
$previousChapter = $manga->getChapters() $chapters = $manga->getChapters()
->filter(fn($chapter) => $chapter->isVisible() && $chapter->getNumber() < $currentChapterNumber) ->filter(fn($chapter) => $chapter->isVisible() && $chapter->getNumber() < $currentChapterNumber)
->last(); ->toArray();
usort($chapters, fn($a, $b) => $b->getNumber() <=> $a->getNumber());
$previousChapter = reset($chapters) ?: null;
return $this->json($previousChapter ? [ return $this->json($previousChapter ? [
'number' => $previousChapter->getNumber(), 'number' => $previousChapter->getNumber(),
'title' => $previousChapter->getTitle() 'title' => $previousChapter->getTitle(),
] : null); ] : null);
} }
@@ -124,7 +133,7 @@ class ReaderController extends AbstractController
return $this->json($nextChapter ? [ return $this->json($nextChapter ? [
'number' => $nextChapter->getNumber(), 'number' => $nextChapter->getNumber(),
'title' => $nextChapter->getTitle() 'title' => $nextChapter->getTitle(),
] : null); ] : null);
} }
} }

View File

@@ -15,12 +15,11 @@ class FileSystemManager
private string $imageDirectory; private string $imageDirectory;
public function __construct( public function __construct(
private readonly string $projectDir, private readonly string $projectDir,
private readonly Filesystem $filesystem, private readonly Filesystem $filesystem,
private readonly SluggerInterface $slugger, private readonly SluggerInterface $slugger,
private readonly AppSettingsManager $appSettingsManager private readonly AppSettingsManager $appSettingsManager
) ) {
{
$this->loadSettings(); $this->loadSettings();
} }
@@ -43,17 +42,19 @@ class FileSystemManager
public function getImagePath(string $subDir = ''): string public function getImagePath(string $subDir = ''): string
{ {
if(!$this->filesystem->exists($this->projectDir. '/' . self::IMAGES_DIRECTORY . ($subDir ? "/$subDir" : ''))) { if (!$this->filesystem->exists($this->projectDir.'/'.self::IMAGES_DIRECTORY.($subDir ? "/$subDir" : ''))) {
$this->filesystem->mkdir($this->projectDir. '/' . self::IMAGES_DIRECTORY . ($subDir ? "/$subDir" : ''), 0755); $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 public function createMangaDirectory(string $mangaSlug, ?int $year): string
{ {
$year = $year ?? 'unknown'; $year = $year ?? 'unknown';
$directoryPath = $this->mangaDirectory . '/' . ucfirst($mangaSlug) . " ($year)"; $directoryPath = $this->projectDir.'/'.self::CBZ_DIRECTORY.'/'.ucfirst($mangaSlug)." ($year)";
$this->filesystem->mkdir($directoryPath, 0755); $this->filesystem->mkdir($directoryPath, 0755);
return $directoryPath; return $directoryPath;
} }
@@ -61,14 +62,16 @@ class FileSystemManager
{ {
$volumeDir = sprintf('%s/volume_%02d', $mangaDir, $volume); $volumeDir = sprintf('%s/volume_%02d', $mangaDir, $volume);
$this->filesystem->mkdir($volumeDir, 0755); $this->filesystem->mkdir($volumeDir, 0755);
return $volumeDir; return $volumeDir;
} }
public function moveUploadedFile(string $sourcePath, string $destinationDir, string $originalFilename): string public function moveUploadedFile(string $sourcePath, string $destinationDir, string $originalFilename): string
{ {
$newFilename = $this->generateUniqueFilename($originalFilename); $newFilename = $this->generateUniqueFilename($originalFilename);
$destinationPath = $destinationDir . '/' . $newFilename; $destinationPath = $destinationDir.'/'.$newFilename;
$this->filesystem->rename($sourcePath, $destinationPath, true); $this->filesystem->rename($sourcePath, $destinationPath, true);
return $destinationPath; return $destinationPath;
} }
@@ -98,18 +101,20 @@ class FileSystemManager
public function getUploadsDirectory(): string public function getUploadsDirectory(): string
{ {
return $this->projectDir . '/' . self::UPLOADS_DIRECTORY; return $this->projectDir.'/'.self::UPLOADS_DIRECTORY;
} }
private function generateUniqueFilename(string $originalFilename): string private function generateUniqueFilename(string $originalFilename): string
{ {
$safeFilename = $this->slugger->slug(pathinfo($originalFilename, PATHINFO_FILENAME)); $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 public function generateUniqueImageFilename(string $originalFilename): string
{ {
$safeFilename = $this->slugger->slug(pathinfo($originalFilename, PATHINFO_FILENAME)); $safeFilename = $this->slugger->slug(pathinfo($originalFilename, PATHINFO_FILENAME));
return $safeFilename . '-' . uniqid() . '.jpg';
return $safeFilename.'-'.uniqid().'.jpg';
} }
} }

View File

@@ -8,7 +8,6 @@ use App\Repository\ChapterRepository;
use App\Repository\ContentSourceRepository; use App\Repository\ContentSourceRepository;
use App\Service\NotificationService; use App\Service\NotificationService;
use App\Service\Scraper\MangaScraperService; use App\Service\Scraper\MangaScraperService;
use Exception;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Attribute\AsMessageHandler;
@@ -17,17 +16,15 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler;
readonly class DownloadChapterHandler readonly class DownloadChapterHandler
{ {
public function __construct( public function __construct(
private ChapterRepository $chapterRepository, private ChapterRepository $chapterRepository,
private MangaScraperService $mangaScraperService, private MangaScraperService $mangaScraperService,
private NotificationService $notificationService, private NotificationService $notificationService,
private ContentSourceRepository $contentSourceRepository private ContentSourceRepository $contentSourceRepository
) ) {
{
} }
/** /**
* @throws Exception * @throws \Exception
*/ */
public function __invoke(DownloadChapter $message): void public function __invoke(DownloadChapter $message): void
{ {
@@ -35,7 +32,7 @@ readonly class DownloadChapterHandler
if (!$chapter) { if (!$chapter) {
$this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapter not found.']); $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapter not found.']);
throw new BadRequestHttpException('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.']); $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapter already scraped.']);
throw new BadRequestHttpException('Chapter already downloaded'); throw new BadRequestHttpException('Chapter already downloaded');
} }
@@ -44,11 +41,17 @@ readonly class DownloadChapterHandler
$preferredSources = $manga->getPreferredSources()->toArray(); $preferredSources = $manga->getPreferredSources()->toArray();
$allSources = $this->contentSourceRepository->findAll(); $allSources = $this->contentSourceRepository->findAll();
$filteredSources = array_udiff($allSources, $preferredSources, function ($a, $b) { // $filteredSources = array_udiff($allSources, $preferredSources, function ($a, $b) {
return $a->getId() - $b->getId(); // 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[] = $sources[] =
(new ContentSource()) (new ContentSource())
@@ -57,19 +60,18 @@ readonly class DownloadChapterHandler
->setChapterUrlFormat('at-home/server/%s') ->setChapterUrlFormat('at-home/server/%s')
->setScrapingType('mangadex'); ->setScrapingType('mangadex');
// (new ContentSource())
// (new ContentSource()) // ->setBaseUrl('https://lelscans.net')
// ->setBaseUrl('https://lelscans.net') // ->setImageSelector('#image img')
// ->setImageSelector('#image img') // ->setChapterUrlFormat('https://lelscans.net/scan-%s/%s')
// ->setChapterUrlFormat('https://lelscans.net/scan-%s/%s') // ->setNextPageSelector('a[title="Suivant"]')
// ->setNextPageSelector('a[title="Suivant"]') // ->setScrapingType('html'),
// ->setScrapingType('html'), // (new ContentSource())
// (new ContentSource()) // ->setBaseUrl('https://darkscans.net/')
// ->setBaseUrl('https://darkscans.net/') // ->setImageSelector('.reading-content img')
// ->setImageSelector('.reading-content img') // ->setChapterUrlFormat('https://darkscans.net/mangas/%s/chapter-%s/')
// ->setChapterUrlFormat('https://darkscans.net/mangas/%s/chapter-%s/') // ->setNextPageSelector(null)
// ->setNextPageSelector(null) // ->setScrapingType('html')
// ->setScrapingType('html')
$scrapedSuccessfully = false; $scrapedSuccessfully = false;
@@ -78,10 +80,10 @@ readonly class DownloadChapterHandler
$this->mangaScraperService->scrapeChapter($chapter, $source); $this->mangaScraperService->scrapeChapter($chapter, $source);
$scrapedSuccessfully = true; $scrapedSuccessfully = true;
break; break;
} catch (Exception $e) { } catch (\Exception $e) {
$this->notificationService->sendUpdate([ $this->notificationService->sendUpdate([
'status' => 'warning', '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) { } catch (GuzzleException $e) {
@@ -91,9 +93,9 @@ readonly class DownloadChapterHandler
if (!$scrapedSuccessfully) { if (!$scrapedSuccessfully) {
$this->notificationService->sendUpdate([ $this->notificationService->sendUpdate([
'status' => 'error', '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.']); $this->notificationService->sendUpdate(['status' => 'success', 'message' => 'Chapter scraped successfully.']);

View File

@@ -3,38 +3,35 @@
namespace App\Service; namespace App\Service;
use App\Entity\Manga; use App\Entity\Manga;
use Exception;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\String\Slugger\SluggerInterface; use Symfony\Component\String\Slugger\SluggerInterface;
use ZipArchive;
class CbzService class CbzService
{ {
public function __construct(private SluggerInterface $slugger) public function __construct(private SluggerInterface $slugger)
{ {
} }
/** /**
* @throws Exception * @throws \Exception
*/ */
public function extractMetadata(string $filePath, string $originalFileName): array public function extractMetadata(string $filePath, string $originalFileName): array
{ {
$zip = new ZipArchive(); $zip = new \ZipArchive();
$fileInfo = $this->extractInfoFromFileName($originalFileName); $fileInfo = $this->extractInfoFromFileName($originalFileName);
$metadata['title'] = $fileInfo['title']; $metadata['title'] = $fileInfo['title'];
$metadata['volume'] = $fileInfo['volume'] !== null ? (int)$fileInfo['volume'] : null; $metadata['volume'] = null !== $fileInfo['volume'] ? (int) $fileInfo['volume'] : null;
$metadata['chapter'] = $fileInfo['chapter'] !== null ? (int)$fileInfo['chapter'] : null; $metadata['chapter'] = null !== $fileInfo['chapter'] ? (int) $fileInfo['chapter'] : null;
if (is_null($metadata['chapter'])) { if (is_null($metadata['chapter'])) {
try { try {
$zip->open($filePath); $zip->open($filePath);
$chapterNumbers = []; $chapterNumbers = [];
for ($i = 0; $i < $zip->numFiles; $i++) { for ($i = 0; $i < $zip->numFiles; ++$i) {
$stat = $zip->statIndex($i); $stat = $zip->statIndex($i);
$fileName = $stat['name']; $fileName = $stat['name'];
@@ -43,15 +40,15 @@ class CbzService
$chapterNumbers = array_unique($chapterNumbers); $chapterNumbers = array_unique($chapterNumbers);
if (count($chapterNumbers) === 1) { if (1 === count($chapterNumbers)) {
$metadata['chapter'] = array_values($chapterNumbers)[0] === '' ? null : (int)array_values($chapterNumbers)[0]; $metadata['chapter'] = '' === array_values($chapterNumbers)[0] ? null : (int) array_values($chapterNumbers)[0];
} elseif (count($chapterNumbers) > 1) { } elseif (count($chapterNumbers) > 1) {
$metadata['chapter'] = min($chapterNumbers); $metadata['chapter'] = min($chapterNumbers);
} }
$zip->close(); $zip->close();
} catch (Exception $e) { } catch (\Exception $e) {
throw new Exception("Impossible d'ouvrir le fichier CBZ. " . $e->getMessage()); 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 public function getPageContent(string $cbzPath, int $pageNumber): ?string
{ {
$zip = new ZipArchive(); $zip = new \ZipArchive();
if ($zip->open($cbzPath) === TRUE) { if (true === $zip->open($cbzPath)) {
$images = $this->getImageList($zip); $images = $this->getImageList($zip);
if (isset($images[$pageNumber - 1])) { if (isset($images[$pageNumber - 1])) {
$content = $zip->getFromName($images[$pageNumber - 1]); $content = $zip->getFromName($images[$pageNumber - 1]);
$zip->close(); $zip->close();
return $content; return $content;
} }
$zip->close(); $zip->close();
} }
return null; return null;
} }
public function getPageCount(string $cbzPath): int public function getPageCount(string $cbzPath): int
{ {
$zip = new ZipArchive(); $zip = new \ZipArchive();
if ($zip->open($cbzPath) === TRUE) { if (true === $zip->open($cbzPath)) {
$count = count($this->getImageList($zip)); $count = count($this->getImageList($zip));
$zip->close(); $zip->close();
return $count; return $count;
} }
return 0; return 0;
} }
@@ -91,9 +92,9 @@ class CbzService
$chapter = $this->extractChapter($fileName); $chapter = $this->extractChapter($fileName);
return [ return [
'title' => $title === '' ? null : $title, 'title' => '' === $title ? null : $title,
'volume' => $volume === '' ? null : $volume, 'volume' => '' === $volume ? null : $volume,
'chapter' => $chapter === '' ? null : $chapter, 'chapter' => '' === $chapter ? null : $chapter,
]; ];
} }
@@ -118,6 +119,7 @@ class CbzService
if (preg_match($volumePattern, $fileName, $matches)) { if (preg_match($volumePattern, $fileName, $matches)) {
return str_pad($matches['volume'], 2, '0', STR_PAD_LEFT); return str_pad($matches['volume'], 2, '0', STR_PAD_LEFT);
} }
return ''; return '';
} }
@@ -136,52 +138,54 @@ class CbzService
return ''; return '';
} }
private function getImageList(ZipArchive $zip): array private function getImageList(\ZipArchive $zip): array
{ {
$images = []; $images = [];
for ($i = 0; $i < $zip->numFiles; $i++) { for ($i = 0; $i < $zip->numFiles; ++$i) {
$filename = $zip->getNameIndex($i); $filename = $zip->getNameIndex($i);
if (preg_match('/\.(jpg|jpeg|png|gif)$/i', $filename)) { if (preg_match('/\.(jpg|jpeg|png|gif)$/i', $filename)) {
$images[] = $filename; $images[] = $filename;
} }
} }
sort($images); sort($images);
return $images; return $images;
} }
public function createVolumeArchive(array $chapters): string public function createVolumeArchive(array $chapters): string
{ {
$tempFile = tempnam(sys_get_temp_dir(), 'volume_cbz_'); $tempFile = tempnam(sys_get_temp_dir(), 'volume_cbz_');
$zip = new ZipArchive(); $zip = new \ZipArchive();
if ($zip->open($tempFile, ZipArchive::CREATE) !== TRUE) { if (true !== $zip->open($tempFile, \ZipArchive::CREATE)) {
throw new \RuntimeException("Impossible de créer le fichier ZIP temporaire."); throw new \RuntimeException('Impossible de créer le fichier ZIP temporaire.');
} }
foreach ($chapters as $chapter) { foreach ($chapters as $chapter) {
$chapterZip = new ZipArchive(); $chapterZip = new \ZipArchive();
if ($chapterZip->open($chapter->getCbzPath()) === TRUE) { if (true === $chapterZip->open($chapter->getCbzPath())) {
for ($i = 0; $i < $chapterZip->numFiles; $i++) { for ($i = 0; $i < $chapterZip->numFiles; ++$i) {
$filename = $chapterZip->getNameIndex($i); $filename = $chapterZip->getNameIndex($i);
$fileContent = $chapterZip->getFromIndex($i); $fileContent = $chapterZip->getFromIndex($i);
$zip->addFromString("Chapter " . $chapter->getNumber() . "/" . $filename, $fileContent); $zip->addFromString('Chapter '.$chapter->getNumber().'/'.$filename, $fileContent);
} }
$chapterZip->close(); $chapterZip->close();
} }
} }
$zip->close(); $zip->close();
return $tempFile; 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(); $sluggedTitle = $this->slugger->slug($manga->getTitle())->lower();
if ($volume !== null) { if (null !== $volume) {
return sprintf("%s_volume_%02d.cbz", $sluggedTitle, $volume); return sprintf('%s_volume_%02d.cbz', $sluggedTitle, $volume);
} elseif ($chapterNumber !== null) { } elseif (null !== $chapterNumber) {
return sprintf("%s_chapter_%s.cbz", $sluggedTitle, number_format($chapterNumber, 2)); return sprintf('%s_chapter_%s.cbz', $sluggedTitle, number_format($chapterNumber, 2));
} else { } 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, ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$fileName $fileName
); );
return $response; return $response;
} }
@@ -201,6 +206,7 @@ class CbzService
return false; return false;
} }
$firstCbzPath = $chapters[0]->getCbzPath(); $firstCbzPath = $chapters[0]->getCbzPath();
return array_reduce($chapters, function ($carry, $chapter) use ($firstCbzPath) { return array_reduce($chapters, function ($carry, $chapter) use ($firstCbzPath) {
return $carry && $chapter->getCbzPath() === $firstCbzPath; return $carry && $chapter->getCbzPath() === $firstCbzPath;
}, true); }, true);
@@ -209,7 +215,7 @@ class CbzService
public function doAllChaptersHaveCbz(array $chapters): bool public function doAllChaptersHaveCbz(array $chapters): bool
{ {
return array_reduce($chapters, function ($carry, $chapter) { return array_reduce($chapters, function ($carry, $chapter) {
return $carry && $chapter->getCbzPath() !== null; return $carry && null !== $chapter->getCbzPath();
}, true); }, true);
} }
} }

View File

@@ -18,19 +18,20 @@ readonly class MangadexProvider implements MetadataProviderInterface
public function search(?string $title): Collection public function search(?string $title): Collection
{ {
if ($title === null) { if (null === $title) {
return new ArrayCollection(); return new ArrayCollection();
} }
try { try {
$results = $this->client->get('/manga', [ $results = $this->client->get('/manga', [
'title' => $title, 'title' => $title,
'contentRating' => ['safe', 'suggestive'], 'contentRating' => ['safe', 'suggestive', 'erotica'],
'includes' => ['cover_art', 'author'], 'includes' => ['cover_art', 'author'],
'limit' => 25 'limit' => 50,
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->notificationService->sendUpdate('notification', ['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); $this->notificationService->sendUpdate('notification', ['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']);
return new ArrayCollection(); return new ArrayCollection();
} }
@@ -51,32 +52,34 @@ readonly class MangadexProvider implements MetadataProviderInterface
$mangas[count($mangas) - 1]->setGenres($tags); $mangas[count($mangas) - 1]->setGenres($tags);
foreach ($result['relationships'] as $relationship) { foreach ($result['relationships'] as $relationship) {
if ($relationship['type'] === 'author') { if ('author' === $relationship['type']) {
$mangas[count($mangas) - 1]->setAuthor($relationship['attributes']['name']); $mangas[count($mangas) - 1]->setAuthor($relationship['attributes']['name']);
} }
if ($relationship['type'] === 'cover_art') { if ('cover_art' === $relationship['type']) {
$mangas[count($mangas) - 1]->setImageUrl('https://mangadex.org/covers/' . $result['id'] . '/' . $relationship['attributes']['fileName']); $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', [ $ratings = $this->client->get('/statistics/manga', [
'manga' => $test 'manga' => $test,
]); ]);
foreach ($mangas as $manga) { foreach ($mangas as $manga) {
$manga->setRating($ratings['statistics'][$manga->getExternalId()]['rating']['average']); $manga->setRating($ratings['statistics'][$manga->getExternalId()]['rating']['average']);
} }
usort($mangas, fn ($a, $b) => $b->getRating() <=> $a->getRating());
return new ArrayCollection($mangas); return new ArrayCollection($mangas);
} }
public function getFeed(Manga $manga): array public function getFeed(Manga $manga): array
{ {
if ($manga->getExternalId() === null) { if (null === $manga->getExternalId()) {
return []; return [];
} }
@@ -90,7 +93,7 @@ readonly class MangadexProvider implements MetadataProviderInterface
} else { } else {
break; break;
} }
$page++; ++$page;
} while (count($chapters) < $results['total']); } while (count($chapters) < $results['total']);
return $this->getChaptersFromFeed($chapters, $manga); return $this->getChaptersFromFeed($chapters, $manga);
@@ -98,7 +101,7 @@ readonly class MangadexProvider implements MetadataProviderInterface
public function getLastFeed(Manga $manga, int $limit = 100): array public function getLastFeed(Manga $manga, int $limit = 100): array
{ {
if ($manga->getExternalId() === null) { if (null === $manga->getExternalId()) {
return []; return [];
} }
@@ -111,6 +114,7 @@ readonly class MangadexProvider implements MetadataProviderInterface
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching recent chapters from Mangadex.']); $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching recent chapters from Mangadex.']);
return []; return [];
} }
@@ -120,14 +124,15 @@ readonly class MangadexProvider implements MetadataProviderInterface
private function getFeedWithPagination(string $externalId, int $page, int $limit = 500, string $order = 'asc'): array private function getFeedWithPagination(string $externalId, int $page, int $limit = 500, string $order = 'asc'): array
{ {
try { try {
$response = $this->client->get('/manga/' . $externalId . '/feed', [ $response = $this->client->get('/manga/'.$externalId.'/feed', [
'limit' => $limit, 'limit' => $limit,
'translatedLanguage' => ['en', 'fr'], 'translatedLanguage' => ['en', 'fr'],
'order' => ['chapter' => $order], 'order' => ['chapter' => $order],
'offset' => $page * $limit 'offset' => $page * $limit,
]); ]);
} catch (\Exception $e) { } 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 []; return [];
} }
@@ -136,49 +141,44 @@ readonly class MangadexProvider implements MetadataProviderInterface
public function getMangaAggregate(Manga $manga): array public function getMangaAggregate(Manga $manga): array
{ {
if ($manga->getExternalId() === null) { if (null === $manga->getExternalId()) {
return []; return [];
} }
try { try {
$response = $this->client->get('/manga/' . $manga->getExternalId() . '/aggregate'); $response = $this->client->get('/manga/'.$manga->getExternalId().'/aggregate');
} catch (\Exception $e) { } 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 []; return [];
} }
$chapterEntities = []; $chapterEntities = [];
if ($response['result'] === 'ok') { if ('ok' === $response['result']) {
foreach ($response['volumes'] as $volume) { 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) { foreach ($volume['chapters'] as $chapter) {
$chapterEntity = new Chapter(); $chapterEntity = new Chapter();
$chapterEntity->setNumber((float)$chapter['chapter']) $chapterEntity->setNumber((float) $chapter['chapter'])
->setTitle('Chapter ' . $chapter['chapter']) ->setTitle('Chapter '.$chapter['chapter'])
->setVolume($volumeNumber) ->setVolume($volumeNumber)
->setExternalId(''); ->setExternalId('');
$chapterEntities[] = $chapterEntity; $chapterEntities[] = $chapterEntity;
// $manga->addChapter($chapterEntity); // $manga->addChapter($chapterEntity);
} }
} }
} }
return $chapterEntities; return $chapterEntities;
} }
/**
* @param mixed $chapters
* @param Manga $manga
* @param array $chapterEntities
* @return array
*/
public function getChaptersFromFeed(mixed $chapters, Manga $manga): array public function getChaptersFromFeed(mixed $chapters, Manga $manga): array
{ {
$chapterEntities = []; $chapterEntities = [];
$uniqueChapterNumbers = []; $uniqueChapterNumbers = [];
foreach ($chapters as $result) { 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 // Vérifiez si le chapitre existe déjà dans la base de données
$chapterExists = $manga->getChapters()->exists(function ($key, $existingChapter) use ($chapterNumber) { $chapterExists = $manga->getChapters()->exists(function ($key, $existingChapter) use ($chapterNumber) {
@@ -194,7 +194,7 @@ readonly class MangadexProvider implements MetadataProviderInterface
$chapter = new Chapter(); $chapter = new Chapter();
$chapter->setNumber($chapterNumber) $chapter->setNumber($chapterNumber)
->setTitle($result['attributes']['title']) ->setTitle($result['attributes']['title'])
->setVolume((int)$result['attributes']['volume'] ?? null) ->setVolume((int) $result['attributes']['volume'] ?? null)
->setExternalId($result['id']); ->setExternalId($result['id']);
$chapterEntities[] = $chapter; $chapterEntities[] = $chapter;
@@ -219,8 +219,9 @@ readonly class MangadexProvider implements MetadataProviderInterface
if (empty($allChapters)) { if (empty($allChapters)) {
$this->notificationService->sendUpdate([ $this->notificationService->sendUpdate([
'status' => 'error', 'status' => 'error',
'message' => 'No chapters found for this manga.' 'message' => 'No chapters found for this manga.',
]); ]);
return []; return [];
} }
@@ -247,6 +248,5 @@ readonly class MangadexProvider implements MetadataProviderInterface
{ {
$existingChapter->setVolume($newChapter->getVolume()); $existingChapter->setVolume($newChapter->getVolume());
$existingChapter->setExternalId($newChapter->getExternalId()); $existingChapter->setExternalId($newChapter->getExternalId());
} }
} }

View File

@@ -8,7 +8,6 @@ use App\Entity\Manga;
use App\Event\PageScrappingProgressEvent; use App\Event\PageScrappingProgressEvent;
use App\Manager\FileSystemManager; use App\Manager\FileSystemManager;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Exception;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
@@ -19,11 +18,10 @@ abstract class AbstractScraper implements ScraperInterface
protected Client $httpClient; protected Client $httpClient;
public function __construct( public function __construct(
protected FileSystemManager $fileSystemManager, protected FileSystemManager $fileSystemManager,
protected EventDispatcherInterface $eventDispatcher, protected EventDispatcherInterface $eventDispatcher,
protected EntityManagerInterface $entityManager protected EntityManagerInterface $entityManager
) ) {
{
$this->httpClient = new Client(); $this->httpClient = new Client();
} }
@@ -45,7 +43,8 @@ abstract class AbstractScraper implements ScraperInterface
{ {
try { try {
$response = $this->httpClient->head($url); $response = $this->httpClient->head($url);
return $response->getStatusCode() === 200;
return 200 === $response->getStatusCode();
} catch (RequestException $e) { } catch (RequestException $e) {
return false; return false;
} }
@@ -60,14 +59,15 @@ abstract class AbstractScraper implements ScraperInterface
$chapter->getVolume(), $chapter->getVolume(),
$chapter->getNumber() $chapter->getNumber()
); );
return $volumeDir . '/' . $fileName;
return $volumeDir.'/'.$fileName;
} }
protected function createCbzFile(array $pageData, string $cbzFilePath): void protected function createCbzFile(array $pageData, string $cbzFilePath): void
{ {
$zip = new \ZipArchive(); $zip = new \ZipArchive();
if ($zip->open($cbzFilePath, \ZipArchive::CREATE) === TRUE) { if (true === $zip->open($cbzFilePath, \ZipArchive::CREATE)) {
foreach ($pageData as $page) { foreach ($pageData as $page) {
$zip->addFile($page['local_image_url'], basename($page['local_image_url'])); $zip->addFile($page['local_image_url'], basename($page['local_image_url']));
} }
@@ -93,21 +93,67 @@ abstract class AbstractScraper implements ScraperInterface
/** /**
* @throws GuzzleException * @throws GuzzleException
* @throws Exception * @throws \Exception
*/ */
protected function downloadAndSaveImage(string $imageUrl, string $destinationPath): void protected function downloadAndSaveImage(string $imageUrl, string $destinationPath): string
{ {
try { try {
$response = $this->httpClient->get($imageUrl); $response = $this->httpClient->get($imageUrl);
$contentType = $response->getHeaderLine('Content-Type'); $contentType = $response->getHeaderLine('Content-Type');
if (str_starts_with($contentType, 'image/')) { if (!str_starts_with($contentType, 'image/')) {
file_put_contents($destinationPath, $response->getBody()->getContents()); throw new \Exception('Le contenu récupéré n\'est pas une image. Type de contenu : '.$contentType);
} else {
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';
}
} }

View File

@@ -4,17 +4,13 @@ namespace App\Service\Scraper;
use App\Entity\Chapter; use App\Entity\Chapter;
use App\Entity\ContentSource; use App\Entity\ContentSource;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\DomCrawler\Crawler;
class HtmlScraper extends AbstractScraper class HtmlScraper extends AbstractScraper
{ {
/** /**
* @throws Exception * @throws \Exception
* @throws GuzzleException * @throws GuzzleException
*/ */
public function scrapeChapter(Chapter $chapter, ContentSource $contentSource): array|bool public function scrapeChapter(Chapter $chapter, ContentSource $contentSource): array|bool
@@ -23,15 +19,15 @@ class HtmlScraper extends AbstractScraper
$chapterUrl = $this->getValidChapterUrl($contentSource, $manga, $chapter->getNumber()); $chapterUrl = $this->getValidChapterUrl($contentSource, $manga, $chapter->getNumber());
if (!$chapterUrl) { 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); mkdir($tempDir);
$pageData = []; $pageData = [];
if ($contentSource->getNextPageSelector() === null) { if (null === $contentSource->getNextPageSelector()) {
// Lecteur vertical // Lecteur vertical
$html = $this->fetchHtml($chapterUrl); $html = $this->fetchHtml($chapterUrl);
$pageData = $this->scrapeVerticalReader($html, $contentSource); $pageData = $this->scrapeVerticalReader($html, $contentSource);
@@ -43,13 +39,13 @@ class HtmlScraper extends AbstractScraper
// Télécharger et sauvegarder les images // Télécharger et sauvegarder les images
foreach ($pageData as $index => &$page) { foreach ($pageData as $index => &$page) {
$imageName = sprintf('%03d.%s', $index + 1, pathinfo(parse_url($page['image_url'], PHP_URL_PATH), PATHINFO_EXTENSION)); $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)); $this->dispatchProgressEvent($chapter, $index + 1, count($pageData));
$page['local_image_url'] = $imagePath; $page['local_image_url'] = $destinationPath;
} }
$cbzFilePath = $this->generateCbzPath($manga, $chapter); $cbzFilePath = $this->generateCbzPath($manga, $chapter);
@@ -59,26 +55,25 @@ class HtmlScraper extends AbstractScraper
$this->entityManager->persist($chapter); $this->entityManager->persist($chapter);
$this->entityManager->flush(); $this->entityManager->flush();
// Nettoyage du répertoire temporaire
$this->cleanupTempFiles($tempDir); $this->cleanupTempFiles($tempDir);
return $pageData; return $pageData;
} }
/** /**
* @throws Exception * @throws \Exception
*/ */
public function testScraping(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array public function testScraping(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array
{ {
$chapterUrl = $contentSource->getChapterUrl($mangaSlug, $chapterNumber); $chapterUrl = $contentSource->getChapterUrl($mangaSlug, $chapterNumber);
if (!$this->isChapterUrlValid($chapterUrl)) { 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); $html = $this->fetchHtml($chapterUrl);
if ($contentSource->getNextPageSelector() === null) { if (null === $contentSource->getNextPageSelector()) {
return $this->scrapeVerticalReader($html, $contentSource); return $this->scrapeVerticalReader($html, $contentSource);
} else { } else {
return $this->scrapeHorizontalReader($chapterUrl, $contentSource); return $this->scrapeHorizontalReader($chapterUrl, $contentSource);
@@ -87,7 +82,7 @@ class HtmlScraper extends AbstractScraper
public function supports(string $scrapingType): bool public function supports(string $scrapingType): bool
{ {
return $scrapingType === 'html'; return 'html' === $scrapingType;
} }
private function scrapeVerticalReader(string $html, ContentSource $contentSource): array 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 private function scrapeHorizontalReader(string $chapterUrl, ContentSource $contentSource): array
{ {
@@ -135,18 +130,18 @@ class HtmlScraper extends AbstractScraper
try { try {
$response = $this->httpClient->get($url, [ $response = $this->httpClient->get($url, [
'http_errors' => true, 'http_errors' => true,
'allow_redirects' => false 'allow_redirects' => false,
]); ]);
$statusCode = $response->getStatusCode(); $statusCode = $response->getStatusCode();
if ($statusCode >= 300 && $statusCode < 400 || $statusCode == 404) { if ($statusCode >= 300 && $statusCode < 400 || 404 == $statusCode) {
throw new Exception('Chapter Not Found at ' . $url); throw new \Exception('Chapter Not Found at '.$url);
} }
return (string)$response->getBody(); return (string) $response->getBody();
} catch (Exception $e) { } catch (\Exception $e) {
throw new Exception('Bad Request: ' . $e->getMessage()); throw new \Exception('Bad Request: '.$e->getMessage());
} }
} }
@@ -164,7 +159,7 @@ class HtmlScraper extends AbstractScraper
$urlComponents = parse_url($mangaSource->getBaseUrl()); $urlComponents = parse_url($mangaSource->getBaseUrl());
$scheme = $urlComponents['scheme']; $scheme = $urlComponents['scheme'];
$host = $urlComponents['host']; $host = $urlComponents['host'];
$imgUrl = $scheme . '://' . $host . '/' . ltrim($imgUrl, '/'); $imgUrl = $scheme.'://'.$host.'/'.ltrim($imgUrl, '/');
} }
return [ return [

View File

@@ -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)); $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)); $this->dispatchProgressEvent($chapter, $index + 1, count($pageData));
$page['local_image_url'] = $imagePath; $page['local_image_url'] = $destinationPath;
} }
$cbzFilePath = $this->generateCbzPath($manga, $chapter); $cbzFilePath = $this->generateCbzPath($manga, $chapter);

View File

@@ -20,6 +20,6 @@ class ScraperFactory
return $scraper; return $scraper;
} }
} }
throw new \InvalidArgumentException('Unsupported scraping type: ' . $contentSource->getScrapingType()); throw new \InvalidArgumentException('Unsupported scraping type: '.$contentSource->getScrapingType());
} }
} }