Compare commits
1 Commits
feat/monit
...
b7f4ee9082
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7f4ee9082 |
@@ -36,6 +36,7 @@ framework:
|
|||||||
'App\Domain\Manga\Domain\Event\MangaCreated': events
|
'App\Domain\Manga\Domain\Event\MangaCreated': events
|
||||||
'App\Domain\Shared\Domain\Event\ChapterImported': events
|
'App\Domain\Shared\Domain\Event\ChapterImported': events
|
||||||
'App\Domain\Shared\Domain\Event\VolumeImported': events
|
'App\Domain\Shared\Domain\Event\VolumeImported': events
|
||||||
|
'App\Domain\Shared\Domain\Event\ChapterScraped': events
|
||||||
|
|
||||||
# Legacy messages (à garder si nécessaire)
|
# Legacy messages (à garder si nécessaire)
|
||||||
'App\Message\DownloadChapter': commands
|
'App\Message\DownloadChapter': commands
|
||||||
|
|||||||
@@ -126,7 +126,12 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: messenger.message_handler, bus: command.bus }
|
- { name: messenger.message_handler, bus: command.bus }
|
||||||
|
|
||||||
App\Domain\Scraping\Infrastructure\Service\CbzGenerator: ~
|
App\Domain\Scraping\Domain\Contract\Service\ImageStorageInterface:
|
||||||
|
alias: App\Domain\Scraping\Infrastructure\Service\LocalImageStorage
|
||||||
|
|
||||||
|
App\Domain\Scraping\Infrastructure\Service\LocalImageStorage:
|
||||||
|
arguments:
|
||||||
|
$storagePath: '%env(MANGA_DATA_PATH)%'
|
||||||
|
|
||||||
# Shared Manga Path/File Manager
|
# Shared Manga Path/File Manager
|
||||||
App\Domain\Shared\Domain\Contract\MangaPathManagerInterface:
|
App\Domain\Shared\Domain\Contract\MangaPathManagerInterface:
|
||||||
|
|||||||
@@ -12,10 +12,8 @@ services:
|
|||||||
class: 'App\Tests\Domain\Scraping\Adapter\InMemoryScrapingJobRepository'
|
class: 'App\Tests\Domain\Scraping\Adapter\InMemoryScrapingJobRepository'
|
||||||
public: true
|
public: true
|
||||||
|
|
||||||
App\Domain\Scraping\Domain\Contract\Service\CbzGeneratorInterface:
|
App\Domain\Scraping\Domain\Contract\Service\ImageStorageInterface:
|
||||||
class: 'App\Tests\Domain\Scraping\Adapter\InMemoryCbzGenerator'
|
class: 'App\Tests\Domain\Scraping\Adapter\InMemoryImageStorage'
|
||||||
arguments:
|
|
||||||
$projectDir: '%kernel.project_dir%'
|
|
||||||
public: true
|
public: true
|
||||||
|
|
||||||
App\Domain\Manga\Domain\Contract\Service\ImageProcessorInterface:
|
App\Domain\Manga\Domain\Contract\Service\ImageProcessorInterface:
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ return static function (Config $config): void {
|
|||||||
->that(new ResideInOneOfTheseNamespaces("App\Domain\\$domain\Application"))
|
->that(new ResideInOneOfTheseNamespaces("App\Domain\\$domain\Application"))
|
||||||
->should(new NotHaveDependencyOutsideNamespace(
|
->should(new NotHaveDependencyOutsideNamespace(
|
||||||
"App\Domain\\$domain",
|
"App\Domain\\$domain",
|
||||||
array_merge($standardExceptions, $externalDependencies, ['App\Domain\Shared\Domain\Contract'])
|
array_merge($standardExceptions, $externalDependencies, ['App\Domain\Shared\Domain\Contract', 'App\Domain\Shared\Domain\Event'])
|
||||||
))
|
))
|
||||||
->because("la couche Application de $domain ne peut dépendre que de son propre domaine, des contrats partagés et des dépendances autorisées");
|
->because("la couche Application de $domain ne peut dépendre que de son propre domaine, des contrats partagés et des dépendances autorisées");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Application\EventListener;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||||
|
use App\Domain\Shared\Domain\Event\ChapterScraped;
|
||||||
|
|
||||||
|
readonly class ChapterScrapedEventListener
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private MangaRepositoryInterface $mangaRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(ChapterScraped $event): void
|
||||||
|
{
|
||||||
|
$chapter = $this->mangaRepository->findChapterById($event->chapterId);
|
||||||
|
if (!$chapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$manga = $this->mangaRepository->findById($chapter->getMangaId()->getValue());
|
||||||
|
if (!$manga) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$manga->updateChapterPages($chapter, $event->pagesDirectory, $event->pageCount);
|
||||||
|
$this->mangaRepository->save($manga);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Infrastructure\MessageHandler;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Application\EventListener\ChapterScrapedEventListener;
|
||||||
|
use App\Domain\Shared\Domain\Event\ChapterScraped;
|
||||||
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||||
|
|
||||||
|
#[AsMessageHandler]
|
||||||
|
readonly class ChapterScrapedMessageHandler
|
||||||
|
{
|
||||||
|
public function __construct(private ChapterScrapedEventListener $listener)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(ChapterScraped $event): void
|
||||||
|
{
|
||||||
|
$this->listener->__invoke($event);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,10 @@ readonly class ChapterContext
|
|||||||
private ?int $volume,
|
private ?int $volume,
|
||||||
private int $totalPages,
|
private int $totalPages,
|
||||||
private bool $isVisible,
|
private bool $isVisible,
|
||||||
private \DateTimeImmutable $createdAt
|
private \DateTimeImmutable $createdAt,
|
||||||
) {}
|
private ?string $pagesDirectory = null,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ChapterId
|
public function getId(): ChapterId
|
||||||
{
|
{
|
||||||
@@ -57,6 +59,11 @@ readonly class ChapterContext
|
|||||||
return $this->cbzPath;
|
return $this->cbzPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPagesDirectory(): ?string
|
||||||
|
{
|
||||||
|
return $this->pagesDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
public function getVolume(): ?int
|
public function getVolume(): ?int
|
||||||
{
|
{
|
||||||
return $this->volume;
|
return $this->volume;
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private EntityManagerInterface $entityManager
|
private EntityManagerInterface $entityManager
|
||||||
) {}
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public function getPagesForChapter(ChapterId $chapterId, int $page = 1, int $itemsPerPage = 20): array
|
public function getPagesForChapter(ChapterId $chapterId, int $page = 1, int $itemsPerPage = 20): array
|
||||||
{
|
{
|
||||||
@@ -28,12 +29,187 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
|||||||
'id' => $chapterId->getValue()
|
'id' => $chapterId->getValue()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$cbzPath = $chapter->getCbzPath();
|
$pagesDirectory = $chapter->getPagesDirectory();
|
||||||
|
if ($pagesDirectory && is_dir($pagesDirectory)) {
|
||||||
|
return $this->getPagesFromDirectory($chapterId, $pagesDirectory, $page, $itemsPerPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cbzPath = $chapter->getCbzPath();
|
||||||
if (!$cbzPath) {
|
if (!$cbzPath) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this->getPagesFromCbz($chapterId, $cbzPath, $page, $itemsPerPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChapterContext(ChapterId $chapterId): ChapterContext
|
||||||
|
{
|
||||||
|
/** @var ChapterEntity $chapter */
|
||||||
|
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||||
|
'id' => $chapterId->getValue()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$chapter) {
|
||||||
|
throw ChapterNotFoundException::forChapter($chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ChapterContext(
|
||||||
|
id: $chapterId,
|
||||||
|
previousChapterId: $this->getPreviousChapterId($chapterId),
|
||||||
|
nextChapterId: $this->getNextChapterId($chapterId),
|
||||||
|
mangaTitle: $chapter->getManga()->getTitle(),
|
||||||
|
number: $chapter->getNumber(),
|
||||||
|
chapterTitle: $chapter->getTitle(),
|
||||||
|
cbzPath: $chapter->getCbzPath(),
|
||||||
|
volume: $chapter->getVolume(),
|
||||||
|
totalPages: 0,
|
||||||
|
isVisible: $chapter->isVisible(),
|
||||||
|
createdAt: new \DateTimeImmutable(),
|
||||||
|
pagesDirectory: $chapter->getPagesDirectory(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalPagesForChapter(ChapterId $chapterId): int
|
||||||
|
{
|
||||||
|
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||||
|
'id' => $chapterId->getValue()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$chapter) {
|
||||||
|
throw ChapterNotFoundException::forChapter($chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pagesDirectory = $chapter->getPagesDirectory();
|
||||||
|
if ($pagesDirectory && is_dir($pagesDirectory)) {
|
||||||
|
return count($this->getImageFiles($pagesDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
$cbzPath = $chapter->getCbzPath();
|
||||||
|
if (!$cbzPath) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
$zip->open($cbzPath);
|
||||||
|
$count = $zip->numFiles;
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPreviousChapterId(ChapterId $chapterId): ?ChapterId
|
||||||
|
{
|
||||||
|
$currentChapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||||
|
'id' => $chapterId->getValue()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$qb = $this->entityManager->createQueryBuilder();
|
||||||
|
$qb->select('c')
|
||||||
|
->from(ChapterEntity::class, 'c')
|
||||||
|
->where('c.manga = :manga')
|
||||||
|
->andWhere('c.number < :number')
|
||||||
|
->andWhere('c.visible = true')
|
||||||
|
->andWhere('c.pagesDirectory IS NOT NULL OR c.cbzPath IS NOT NULL')
|
||||||
|
->orderBy('c.number', 'DESC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->setParameters([
|
||||||
|
'manga' => $currentChapter->getManga(),
|
||||||
|
'number' => $currentChapter->getNumber()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$previousChapter = $qb->getQuery()->getOneOrNullResult();
|
||||||
|
|
||||||
|
return $previousChapter ? new ChapterId((string) $previousChapter->getId()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNextChapterId(ChapterId $chapterId): ?ChapterId
|
||||||
|
{
|
||||||
|
$currentChapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||||
|
'id' => $chapterId->getValue()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$qb = $this->entityManager->createQueryBuilder();
|
||||||
|
$qb->select('c')
|
||||||
|
->from(ChapterEntity::class, 'c')
|
||||||
|
->where('c.manga = :manga')
|
||||||
|
->andWhere('c.number > :number')
|
||||||
|
->andWhere('c.visible = true')
|
||||||
|
->andWhere('c.pagesDirectory IS NOT NULL OR c.cbzPath IS NOT NULL')
|
||||||
|
->orderBy('c.number', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->setParameters([
|
||||||
|
'manga' => $currentChapter->getManga(),
|
||||||
|
'number' => $currentChapter->getNumber()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$nextChapter = $qb->getQuery()->getOneOrNullResult();
|
||||||
|
|
||||||
|
return $nextChapter ? new ChapterId((string) $nextChapter->getId()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPageContent(ChapterId $chapterId, PageNumber $pageNumber): PageContent
|
||||||
|
{
|
||||||
|
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||||
|
'id' => $chapterId->getValue()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$chapter) {
|
||||||
|
throw ChapterNotFoundException::forChapter($chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pagesDirectory = $chapter->getPagesDirectory();
|
||||||
|
if ($pagesDirectory && is_dir($pagesDirectory)) {
|
||||||
|
return $this->getPageContentFromDirectory($chapterId, $pagesDirectory, $pageNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cbzPath = $chapter->getCbzPath();
|
||||||
|
if (!$cbzPath || !file_exists($cbzPath)) {
|
||||||
|
throw ChapterNotFoundException::forChapter($chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getPageContentFromCbz($chapterId, $cbzPath, $pageNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getImageFiles(string $pagesDirectory): array
|
||||||
|
{
|
||||||
|
$files = glob($pagesDirectory . '/*.{jpg,jpeg,png,webp,gif}', GLOB_BRACE) ?: [];
|
||||||
|
sort($files);
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPagesFromDirectory(ChapterId $chapterId, string $pagesDirectory, int $page, int $itemsPerPage): array
|
||||||
|
{
|
||||||
|
$files = $this->getImageFiles($pagesDirectory);
|
||||||
|
$start = ($page - 1) * $itemsPerPage;
|
||||||
|
$end = min($start + $itemsPerPage, count($files));
|
||||||
|
$pages = [];
|
||||||
|
|
||||||
|
for ($i = $start; $i < $end; $i++) {
|
||||||
|
$imageContent = file_get_contents($files[$i]);
|
||||||
|
if ($imageContent === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageSize = @getimagesizefromstring($imageContent);
|
||||||
|
if ($imageSize === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pages[] = new Page(
|
||||||
|
basename($files[$i]),
|
||||||
|
new PageNumber($i + 1),
|
||||||
|
sprintf('/api/chapters/%s/pages/%d', $chapterId->getValue(), $i + 1),
|
||||||
|
$imageSize[0],
|
||||||
|
$imageSize[1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPagesFromCbz(ChapterId $chapterId, string $cbzPath, int $page, int $itemsPerPage): array
|
||||||
|
{
|
||||||
$zip = new ZipArchive();
|
$zip = new ZipArchive();
|
||||||
$zip->open($cbzPath);
|
$zip->open($cbzPath);
|
||||||
|
|
||||||
@@ -67,121 +243,44 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$zip->close();
|
$zip->close();
|
||||||
|
|
||||||
return $pages;
|
return $pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getChapterContext(ChapterId $chapterId): ChapterContext
|
private function getPageContentFromDirectory(ChapterId $chapterId, string $pagesDirectory, PageNumber $pageNumber): PageContent
|
||||||
{
|
{
|
||||||
/** @var ChapterEntity $chapter */
|
$files = $this->getImageFiles($pagesDirectory);
|
||||||
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
|
||||||
'id' => $chapterId->getValue()
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$chapter) {
|
if (!$files || $pageNumber->getValue() > count($files)) {
|
||||||
throw ChapterNotFoundException::forChapter($chapterId);
|
throw PageNotFoundException::forPage($chapterId, $pageNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ChapterContext(
|
$filePath = $files[$pageNumber->getValue() - 1];
|
||||||
id: $chapterId,
|
$imageContent = file_get_contents($filePath);
|
||||||
previousChapterId: $this->getPreviousChapterId($chapterId),
|
|
||||||
nextChapterId: $this->getNextChapterId($chapterId),
|
if ($imageContent === false) {
|
||||||
mangaTitle: $chapter->getManga()->getTitle(),
|
throw PageNotFoundException::forPage($chapterId, $pageNumber);
|
||||||
number: $chapter->getNumber(),
|
}
|
||||||
chapterTitle: $chapter->getTitle(),
|
|
||||||
cbzPath: $chapter->getCbzPath(),
|
$imageSize = @getimagesizefromstring($imageContent);
|
||||||
volume: $chapter->getVolume(),
|
if ($imageSize === false) {
|
||||||
totalPages: 0,
|
throw PageNotFoundException::forPage($chapterId, $pageNumber);
|
||||||
isVisible: $chapter->isVisible(),
|
}
|
||||||
createdAt: new \DateTimeImmutable()
|
|
||||||
|
$mimeType = $imageSize['mime'] ?? 'image/jpeg';
|
||||||
|
|
||||||
|
return new PageContent(
|
||||||
|
basename($filePath),
|
||||||
|
$pageNumber,
|
||||||
|
base64_encode($imageContent),
|
||||||
|
$mimeType,
|
||||||
|
$imageSize[0],
|
||||||
|
$imageSize[1]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTotalPagesForChapter(ChapterId $chapterId): int
|
private function getPageContentFromCbz(ChapterId $chapterId, string $cbzPath, PageNumber $pageNumber): PageContent
|
||||||
{
|
{
|
||||||
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
|
||||||
'id' => $chapterId->getValue()
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$chapter) {
|
|
||||||
throw ChapterNotFoundException::forChapter($chapterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cbzPath = $chapter->getCbzPath();
|
|
||||||
|
|
||||||
if (!$cbzPath) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$zip = new ZipArchive();
|
|
||||||
$zip->open($cbzPath);
|
|
||||||
return $zip->numFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPreviousChapterId(ChapterId $chapterId): ?ChapterId
|
|
||||||
{
|
|
||||||
$currentChapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
|
||||||
'id' => $chapterId->getValue()
|
|
||||||
]);
|
|
||||||
|
|
||||||
$qb = $this->entityManager->createQueryBuilder();
|
|
||||||
$qb->select('c')
|
|
||||||
->from(ChapterEntity::class, 'c')
|
|
||||||
->where('c.manga = :manga')
|
|
||||||
->andWhere('c.number < :number')
|
|
||||||
->andWhere('c.visible = true')
|
|
||||||
->andWhere('c.cbzPath IS NOT NULL')
|
|
||||||
->orderBy('c.number', 'DESC')
|
|
||||||
->setMaxResults(1)
|
|
||||||
->setParameters([
|
|
||||||
'manga' => $currentChapter->getManga(),
|
|
||||||
'number' => $currentChapter->getNumber()
|
|
||||||
]);
|
|
||||||
|
|
||||||
$previousChapter = $qb->getQuery()->getOneOrNullResult();
|
|
||||||
|
|
||||||
return $previousChapter ? new ChapterId((string) $previousChapter->getId()) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getNextChapterId(ChapterId $chapterId): ?ChapterId
|
|
||||||
{
|
|
||||||
$currentChapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
|
||||||
'id' => $chapterId->getValue()
|
|
||||||
]);
|
|
||||||
|
|
||||||
$qb = $this->entityManager->createQueryBuilder();
|
|
||||||
$qb->select('c')
|
|
||||||
->from(ChapterEntity::class, 'c')
|
|
||||||
->where('c.manga = :manga')
|
|
||||||
->andWhere('c.number > :number')
|
|
||||||
->andWhere('c.visible = true')
|
|
||||||
->andWhere('c.cbzPath IS NOT NULL')
|
|
||||||
->orderBy('c.number', 'ASC')
|
|
||||||
->setMaxResults(1)
|
|
||||||
->setParameters([
|
|
||||||
'manga' => $currentChapter->getManga(),
|
|
||||||
'number' => $currentChapter->getNumber()
|
|
||||||
]);
|
|
||||||
|
|
||||||
$nextChapter = $qb->getQuery()->getOneOrNullResult();
|
|
||||||
|
|
||||||
return $nextChapter ? new ChapterId((string) $nextChapter->getId()) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPageContent(ChapterId $chapterId, PageNumber $pageNumber): PageContent
|
|
||||||
{
|
|
||||||
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
|
||||||
'id' => $chapterId->getValue()
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$chapter) {
|
|
||||||
throw ChapterNotFoundException::forChapter($chapterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cbzPath = $chapter->getCbzPath();
|
|
||||||
if (!$cbzPath || !file_exists($cbzPath)) {
|
|
||||||
throw ChapterNotFoundException::forChapter($chapterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$zip = new ZipArchive();
|
$zip = new ZipArchive();
|
||||||
$zip->open($cbzPath);
|
$zip->open($cbzPath);
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,14 @@ use App\Domain\Scraping\Application\Command\ScrapeChapter;
|
|||||||
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
|
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||||
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
|
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||||
use App\Domain\Scraping\Domain\Contract\Repository\SourceRepositoryInterface;
|
use App\Domain\Scraping\Domain\Contract\Repository\SourceRepositoryInterface;
|
||||||
use App\Domain\Scraping\Domain\Contract\Service\CbzGeneratorInterface;
|
use App\Domain\Scraping\Domain\Contract\Service\ImageStorageInterface;
|
||||||
use App\Domain\Scraping\Domain\Contract\Service\ImageDownloaderInterface;
|
use App\Domain\Scraping\Domain\Contract\Service\ImageDownloaderInterface;
|
||||||
use App\Domain\Scraping\Domain\Contract\Service\ScraperInterface;
|
|
||||||
use App\Domain\Scraping\Domain\Contract\Service\ScraperFactoryInterface;
|
use App\Domain\Scraping\Domain\Contract\Service\ScraperFactoryInterface;
|
||||||
use App\Domain\Scraping\Domain\Event\ChapterScraped;
|
use App\Domain\Shared\Domain\Event\ChapterScraped;
|
||||||
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
|
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
|
||||||
use App\Domain\Scraping\Domain\Model\Chapter;
|
use App\Domain\Scraping\Domain\Model\Chapter;
|
||||||
use App\Domain\Scraping\Domain\Model\ScrapingJob;
|
use App\Domain\Scraping\Domain\Model\ScrapingJob;
|
||||||
use App\Domain\Scraping\Domain\Model\Source;
|
use App\Domain\Scraping\Domain\Model\Source;
|
||||||
use App\Domain\Scraping\Domain\Model\ValueObject\CbzGenerationRequest;
|
|
||||||
use App\Domain\Scraping\Domain\Model\ValueObject\ScrapingRequest;
|
use App\Domain\Scraping\Domain\Model\ValueObject\ScrapingRequest;
|
||||||
use App\Domain\Scraping\Domain\Model\ValueObject\TempDirectory;
|
use App\Domain\Scraping\Domain\Model\ValueObject\TempDirectory;
|
||||||
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
||||||
@@ -28,7 +26,7 @@ readonly class ScrapeChapterHandler
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private ScraperFactoryInterface $scraperFactory,
|
private ScraperFactoryInterface $scraperFactory,
|
||||||
private ImageDownloaderInterface $imageDownloader,
|
private ImageDownloaderInterface $imageDownloader,
|
||||||
private CbzGeneratorInterface $cbzGenerator,
|
private ImageStorageInterface $imageStorage,
|
||||||
private JobRepositoryInterface $jobRepository,
|
private JobRepositoryInterface $jobRepository,
|
||||||
private ChapterRepositoryInterface $chapterRepository,
|
private ChapterRepositoryInterface $chapterRepository,
|
||||||
private MangaRepositoryInterface $mangaRepository,
|
private MangaRepositoryInterface $mangaRepository,
|
||||||
@@ -110,30 +108,19 @@ readonly class ScrapeChapterHandler
|
|||||||
$job->id
|
$job->id
|
||||||
);
|
);
|
||||||
|
|
||||||
// 7. Génération du CBZ
|
// 7. Stockage des images individuelles
|
||||||
$cbzRequest = new CbzGenerationRequest(
|
$localPaths = array_map(fn ($r) => $r->getLocalPath(), $downloadResults);
|
||||||
$manga->getTitle(),
|
$pagesDirectory = $this->imageStorage->storeChapterImages($command->chapterId, $localPaths);
|
||||||
$manga->getPublicationYear(),
|
$pageCount = count($downloadResults);
|
||||||
$chapter->volumeNumber,
|
|
||||||
$chapter->chapterNumber,
|
|
||||||
$tempDir,
|
|
||||||
array_map(fn($r) => $r->getLocalPath(), $downloadResults)
|
|
||||||
);
|
|
||||||
|
|
||||||
$cbzPath = $this->cbzGenerator->generate($cbzRequest);
|
|
||||||
|
|
||||||
// 8. Mise à jour et sauvegarde
|
|
||||||
$chapter->cbzPath = $cbzPath->getPath();
|
|
||||||
$this->chapterRepository->save($chapter);
|
|
||||||
|
|
||||||
$job->complete();
|
$job->complete();
|
||||||
$this->jobRepository->save($job);
|
$this->jobRepository->save($job);
|
||||||
|
|
||||||
$this->entityManager->commit();
|
$this->entityManager->commit();
|
||||||
|
|
||||||
$this->eventBus->dispatch(new ChapterScraped($job->id));
|
$this->eventBus->dispatch(new ChapterScraped($job->id, $command->chapterId, $pagesDirectory, $pageCount));
|
||||||
|
|
||||||
// 9. Nettoyage
|
// 8. Nettoyage
|
||||||
$tempDir->cleanup();
|
$tempDir->cleanup();
|
||||||
|
|
||||||
// Scraping réussi, pas besoin d'essayer d'autres slugs ni d'autres sources
|
// Scraping réussi, pas besoin d'essayer d'autres slugs ni d'autres sources
|
||||||
|
|||||||
@@ -11,5 +11,4 @@ interface ChapterRepositoryInterface
|
|||||||
* @throws ChapterNotFoundException
|
* @throws ChapterNotFoundException
|
||||||
*/
|
*/
|
||||||
public function getByMangaIdAndChapterNumber(string $mangaId, float $chapterNumber): Chapter;
|
public function getByMangaIdAndChapterNumber(string $mangaId, float $chapterNumber): Chapter;
|
||||||
public function save(Chapter $chapter): void;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Scraping\Domain\Contract\Service;
|
|
||||||
|
|
||||||
use App\Domain\Scraping\Domain\Model\ValueObject\CbzGenerationRequest;
|
|
||||||
use App\Domain\Scraping\Domain\Model\ValueObject\CbzPath;
|
|
||||||
|
|
||||||
interface CbzGeneratorInterface
|
|
||||||
{
|
|
||||||
public function generate(CbzGenerationRequest $request): CbzPath;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Scraping\Domain\Contract\Service;
|
||||||
|
|
||||||
|
interface ImageStorageInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Copies images to permanent storage. Returns the pagesDirectory path.
|
||||||
|
*
|
||||||
|
* @param string $chapterId The chapter UUID used as directory name
|
||||||
|
* @param string[] $localImagePaths Paths to the locally downloaded image files
|
||||||
|
*
|
||||||
|
* @return string Absolute path to the directory where images were stored
|
||||||
|
*/
|
||||||
|
public function storeChapterImages(string $chapterId, array $localImagePaths): string;
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Scraping\Domain\Event;
|
|
||||||
|
|
||||||
readonly class ChapterScraped
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private string $jobId
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getJobId(): string
|
|
||||||
{
|
|
||||||
return $this->jobId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Scraping\Domain\Exception;
|
|
||||||
|
|
||||||
class CbzGenerationException extends \RuntimeException
|
|
||||||
{
|
|
||||||
public static function unableToCreateDirectory(string $path): self
|
|
||||||
{
|
|
||||||
return new self(sprintf('Impossible de créer le répertoire : %s', $path));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function unableToCreateCbz(string $path): self
|
|
||||||
{
|
|
||||||
return new self(sprintf('Impossible de créer le fichier CBZ : %s', $path));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function unableToAddFileToArchive(string $filePath): self
|
|
||||||
{
|
|
||||||
return new self(sprintf('Impossible d\'ajouter le fichier à l\'archive : %s', $filePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,6 @@ class Chapter
|
|||||||
public readonly string $mangaId,
|
public readonly string $mangaId,
|
||||||
public readonly float $chapterNumber,
|
public readonly float $chapterNumber,
|
||||||
public readonly ?int $volumeNumber,
|
public readonly ?int $volumeNumber,
|
||||||
public ?string $cbzPath,
|
) {
|
||||||
) {}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Scraping\Domain\Model\ValueObject;
|
|
||||||
|
|
||||||
readonly class CbzGenerationRequest
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private string $mangaTitle,
|
|
||||||
private string $publicationYear,
|
|
||||||
private int $volumeNumber,
|
|
||||||
private float $chapterNumber,
|
|
||||||
private TempDirectory $sourceDirectory,
|
|
||||||
private array $files
|
|
||||||
) {
|
|
||||||
if (empty($mangaTitle)) {
|
|
||||||
throw new \InvalidArgumentException('Manga title cannot be empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($publicationYear)) {
|
|
||||||
throw new \InvalidArgumentException('Publication year cannot be empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($volumeNumber < 1) {
|
|
||||||
throw new \InvalidArgumentException('Volume number must be greater than 0');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($chapterNumber <= 0) {
|
|
||||||
throw new \InvalidArgumentException('Chapter number must be greater than 0');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($files)) {
|
|
||||||
throw new \InvalidArgumentException('Files array cannot be empty');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMangaTitle(): string
|
|
||||||
{
|
|
||||||
return $this->mangaTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPublicationYear(): string
|
|
||||||
{
|
|
||||||
return $this->publicationYear;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getVolumeNumber(): int
|
|
||||||
{
|
|
||||||
return $this->volumeNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getChapterNumber(): float
|
|
||||||
{
|
|
||||||
return $this->chapterNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSourceDirectory(): TempDirectory
|
|
||||||
{
|
|
||||||
return $this->sourceDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFiles(): array
|
|
||||||
{
|
|
||||||
return $this->files;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Scraping\Domain\Model\ValueObject;
|
|
||||||
|
|
||||||
readonly class CbzPath
|
|
||||||
{
|
|
||||||
public function __construct(private string $path)
|
|
||||||
{
|
|
||||||
if (empty($path)) {
|
|
||||||
throw new \InvalidArgumentException('Le chemin du fichier CBZ ne peut pas être vide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public function getPath(): string
|
|
||||||
{
|
|
||||||
return $this->path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domain\Scraping\Infrastructure\EventSubscriber;
|
namespace App\Domain\Scraping\Infrastructure\EventSubscriber;
|
||||||
|
|
||||||
use App\Domain\Scraping\Domain\Event\ChapterScraped;
|
use App\Domain\Shared\Domain\Event\ChapterScraped;
|
||||||
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
|
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
|
||||||
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
|
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||||
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
||||||
|
|||||||
@@ -7,15 +7,14 @@ use App\Domain\Scraping\Domain\Exception\ChapterNotFoundException;
|
|||||||
use App\Domain\Scraping\Domain\Model\Chapter;
|
use App\Domain\Scraping\Domain\Model\Chapter;
|
||||||
use App\Entity\Chapter as EntityChapter;
|
use App\Entity\Chapter as EntityChapter;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
) {}
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère un chapitre par son identifiant
|
|
||||||
*/
|
|
||||||
public function getById(string $id): ?Chapter
|
public function getById(string $id): ?Chapter
|
||||||
{
|
{
|
||||||
$chapterEntity = $this->entityManager->getRepository(EntityChapter::class)->find($id);
|
$chapterEntity = $this->entityManager->getRepository(EntityChapter::class)->find($id);
|
||||||
@@ -29,7 +28,6 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
|||||||
mangaId: $chapterEntity->getManga()->getId(),
|
mangaId: $chapterEntity->getManga()->getId(),
|
||||||
chapterNumber: $chapterEntity->getNumber(),
|
chapterNumber: $chapterEntity->getNumber(),
|
||||||
volumeNumber: $chapterEntity->getVolume(),
|
volumeNumber: $chapterEntity->getVolume(),
|
||||||
cbzPath: $chapterEntity->getCbzPath(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,26 +50,6 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
|||||||
mangaId: $entity->getManga()->getId(),
|
mangaId: $entity->getManga()->getId(),
|
||||||
chapterNumber: $entity->getNumber(),
|
chapterNumber: $entity->getNumber(),
|
||||||
volumeNumber: $entity->getVolume(),
|
volumeNumber: $entity->getVolume(),
|
||||||
cbzPath: $entity->getCbzPath(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws ChapterNotFoundException
|
|
||||||
*/
|
|
||||||
public function save(Chapter $chapter): void
|
|
||||||
{
|
|
||||||
$chapterEntity = $this->entityManager->getRepository(EntityChapter::class)->findOneBy([
|
|
||||||
'id' => $chapter->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$chapterEntity) {
|
|
||||||
throw new ChapterNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
$chapterEntity->setCbzPath($chapter->cbzPath);
|
|
||||||
|
|
||||||
$this->entityManager->persist($chapterEntity);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Scraping\Infrastructure\Service;
|
|
||||||
|
|
||||||
use App\Domain\Scraping\Domain\Contract\Service\CbzGeneratorInterface;
|
|
||||||
use App\Domain\Scraping\Domain\Model\ValueObject\CbzGenerationRequest;
|
|
||||||
use App\Domain\Scraping\Domain\Model\ValueObject\CbzPath;
|
|
||||||
use App\Domain\Shared\Domain\Contract\MangaPathManagerInterface;
|
|
||||||
|
|
||||||
readonly class CbzGenerator implements CbzGeneratorInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private MangaPathManagerInterface $mangaPathManager,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function generate(CbzGenerationRequest $request): CbzPath
|
|
||||||
{
|
|
||||||
$cbzPath = $this->mangaPathManager->buildChapterCbzPath(
|
|
||||||
$request->getMangaTitle(),
|
|
||||||
$request->getPublicationYear(),
|
|
||||||
$request->getVolumeNumber(),
|
|
||||||
$request->getChapterNumber(),
|
|
||||||
);
|
|
||||||
$this->mangaPathManager->createCbzArchive($request->getFiles(), $cbzPath);
|
|
||||||
return new CbzPath($cbzPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Scraping\Infrastructure\Service;
|
||||||
|
|
||||||
|
use App\Domain\Scraping\Domain\Contract\Service\ImageStorageInterface;
|
||||||
|
|
||||||
|
readonly class LocalImageStorage implements ImageStorageInterface
|
||||||
|
{
|
||||||
|
public function __construct(private string $storagePath)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeChapterImages(string $chapterId, array $localImagePaths): string
|
||||||
|
{
|
||||||
|
$targetDir = $this->storagePath . '/pages/' . $chapterId;
|
||||||
|
|
||||||
|
if (!is_dir($targetDir)) {
|
||||||
|
mkdir($targetDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
sort($localImagePaths);
|
||||||
|
|
||||||
|
foreach ($localImagePaths as $index => $localPath) {
|
||||||
|
$extension = pathinfo($localPath, PATHINFO_EXTENSION) ?: 'jpg';
|
||||||
|
$targetFile = sprintf('%s/%03d.%s', $targetDir, $index + 1, $extension);
|
||||||
|
copy($localPath, $targetFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $targetDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Domain/Shared/Domain/Event/ChapterScraped.php
Normal file
21
src/Domain/Shared/Domain/Event/ChapterScraped.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Domain\Event;
|
||||||
|
|
||||||
|
readonly class ChapterScraped
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $jobId,
|
||||||
|
public string $chapterId,
|
||||||
|
public string $pagesDirectory,
|
||||||
|
public int $pageCount,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJobId(): string
|
||||||
|
{
|
||||||
|
return $this->jobId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Domain\Manga\Application\EventListener;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Application\EventListener\ChapterScrapedEventListener;
|
||||||
|
use App\Domain\Manga\Domain\Model\Chapter;
|
||||||
|
use App\Domain\Manga\Domain\Model\Manga;
|
||||||
|
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
|
||||||
|
use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
|
||||||
|
use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
|
||||||
|
use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
|
||||||
|
use App\Domain\Shared\Domain\Event\ChapterScraped;
|
||||||
|
use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ChapterScrapedListenerTest extends TestCase
|
||||||
|
{
|
||||||
|
private InMemoryMangaRepository $mangaRepository;
|
||||||
|
private ChapterScrapedEventListener $listener;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->mangaRepository = new InMemoryMangaRepository();
|
||||||
|
$this->listener = new ChapterScrapedEventListener($this->mangaRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItUpdatesPagesDirectoryOnChapter(): void
|
||||||
|
{
|
||||||
|
$mangaId = 'manga-1';
|
||||||
|
$chapterId = '42';
|
||||||
|
|
||||||
|
$chapter = new Chapter(
|
||||||
|
id: new ChapterId($chapterId),
|
||||||
|
mangaId: new MangaId($mangaId),
|
||||||
|
number: 5.0,
|
||||||
|
title: 'Chapter 5',
|
||||||
|
volume: 1,
|
||||||
|
isVisible: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
$manga = new Manga(
|
||||||
|
new MangaId($mangaId),
|
||||||
|
new MangaTitle('Test Manga'),
|
||||||
|
new MangaSlug('test-manga'),
|
||||||
|
'Description',
|
||||||
|
'Author',
|
||||||
|
2020,
|
||||||
|
[],
|
||||||
|
'ongoing'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->mangaRepository->save($manga);
|
||||||
|
// Add chapter via separate path (bypass aggregate to seed state)
|
||||||
|
$manga->addChapter($chapter);
|
||||||
|
$this->mangaRepository->save($manga);
|
||||||
|
|
||||||
|
$event = new ChapterScraped(
|
||||||
|
jobId: 'job-abc',
|
||||||
|
chapterId: $chapterId,
|
||||||
|
pagesDirectory: '/data/pages/' . $chapterId,
|
||||||
|
pageCount: 25,
|
||||||
|
);
|
||||||
|
|
||||||
|
($this->listener)($event);
|
||||||
|
|
||||||
|
$updatedChapter = $this->mangaRepository->findChapterById($chapterId);
|
||||||
|
$this->assertNotNull($updatedChapter);
|
||||||
|
$this->assertEquals('/data/pages/' . $chapterId, $updatedChapter->getPagesDirectory());
|
||||||
|
$this->assertEquals(25, $updatedChapter->getPageCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItDoesNothingWhenChapterNotFound(): void
|
||||||
|
{
|
||||||
|
$event = new ChapterScraped(
|
||||||
|
jobId: 'job-xyz',
|
||||||
|
chapterId: 'non-existent-chapter',
|
||||||
|
pagesDirectory: '/data/pages/non-existent-chapter',
|
||||||
|
pageCount: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
($this->listener)($event);
|
||||||
|
|
||||||
|
$this->assertNull($this->mangaRepository->findChapterById('non-existent-chapter'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ class InMemoryChapterRepository implements ChapterRepositoryInterface
|
|||||||
$this->chapters[$chapter->id] = $chapter;
|
$this->chapters[$chapter->id] = $chapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function clear(): void
|
public function clear(): void
|
||||||
{
|
{
|
||||||
$this->chapters = [];
|
$this->chapters = [];
|
||||||
|
|||||||
19
tests/Domain/Scraping/Adapter/InMemoryImageStorage.php
Normal file
19
tests/Domain/Scraping/Adapter/InMemoryImageStorage.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Domain\Scraping\Adapter;
|
||||||
|
|
||||||
|
use App\Domain\Scraping\Domain\Contract\Service\ImageStorageInterface;
|
||||||
|
|
||||||
|
class InMemoryImageStorage implements ImageStorageInterface
|
||||||
|
{
|
||||||
|
/** @var array<string, string> chapterId => pagesDirectory */
|
||||||
|
public array $stored = [];
|
||||||
|
|
||||||
|
public function storeChapterImages(string $chapterId, array $localImagePaths): string
|
||||||
|
{
|
||||||
|
$dir = '/fake/pages/' . $chapterId;
|
||||||
|
$this->stored[$chapterId] = $dir;
|
||||||
|
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,28 +4,26 @@ namespace App\Tests\Domain\Scraping\Application\CommandHandler;
|
|||||||
|
|
||||||
use App\Domain\Scraping\Application\Command\ScrapeChapter;
|
use App\Domain\Scraping\Application\Command\ScrapeChapter;
|
||||||
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;
|
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;
|
||||||
use App\Domain\Scraping\Domain\Event\ChapterScraped;
|
|
||||||
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
|
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
|
||||||
use App\Domain\Scraping\Domain\Event\ChapterScrapingStarted;
|
|
||||||
use App\Domain\Scraping\Domain\Model\Chapter;
|
use App\Domain\Scraping\Domain\Model\Chapter;
|
||||||
use App\Domain\Shared\Domain\Model\JobStatus;
|
use App\Domain\Shared\Domain\Event\ChapterScraped;
|
||||||
use App\Tests\Domain\Scraping\Adapter\InMemoryChapterRepository;
|
use App\Tests\Domain\Scraping\Adapter\InMemoryChapterRepository;
|
||||||
use App\Tests\Domain\Scraping\Adapter\InMemoryCbzGenerator;
|
|
||||||
use App\Tests\Domain\Scraping\Adapter\InMemoryEventBus;
|
use App\Tests\Domain\Scraping\Adapter\InMemoryEventBus;
|
||||||
use App\Tests\Domain\Scraping\Adapter\InMemoryImageDownloader;
|
use App\Tests\Domain\Scraping\Adapter\InMemoryImageDownloader;
|
||||||
|
use App\Tests\Domain\Scraping\Adapter\InMemoryImageStorage;
|
||||||
use App\Tests\Domain\Scraping\Adapter\InMemoryMangaRepository;
|
use App\Tests\Domain\Scraping\Adapter\InMemoryMangaRepository;
|
||||||
use App\Tests\Domain\Scraping\Adapter\InMemoryScraperFactory;
|
use App\Tests\Domain\Scraping\Adapter\InMemoryScraperFactory;
|
||||||
use App\Tests\Domain\Scraping\Adapter\InMemorySourceRepository;
|
use App\Tests\Domain\Scraping\Adapter\InMemorySourceRepository;
|
||||||
use App\Tests\Domain\Shared\Adapter\InMemoryJobRepository;
|
use App\Tests\Domain\Shared\Adapter\InMemoryJobRepository;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
class ScrapeChapterHandlerTest extends TestCase
|
class ScrapeChapterHandlerTest extends TestCase
|
||||||
{
|
{
|
||||||
private InMemoryScraperFactory $scraperFactory;
|
private InMemoryScraperFactory $scraperFactory;
|
||||||
private InMemoryImageDownloader $imageDownloader;
|
private InMemoryImageDownloader $imageDownloader;
|
||||||
private InMemoryCbzGenerator $cbzGenerator;
|
private InMemoryImageStorage $imageStorage;
|
||||||
private InMemoryJobRepository $jobRepository;
|
private InMemoryJobRepository $jobRepository;
|
||||||
private InMemoryChapterRepository $chapterRepository;
|
private InMemoryChapterRepository $chapterRepository;
|
||||||
private InMemoryMangaRepository $mangaRepository;
|
private InMemoryMangaRepository $mangaRepository;
|
||||||
@@ -38,7 +36,7 @@ class ScrapeChapterHandlerTest extends TestCase
|
|||||||
{
|
{
|
||||||
$this->scraperFactory = new InMemoryScraperFactory();
|
$this->scraperFactory = new InMemoryScraperFactory();
|
||||||
$this->imageDownloader = new InMemoryImageDownloader();
|
$this->imageDownloader = new InMemoryImageDownloader();
|
||||||
$this->cbzGenerator = new InMemoryCbzGenerator('/test/project/dir');
|
$this->imageStorage = new InMemoryImageStorage();
|
||||||
$this->jobRepository = new InMemoryJobRepository();
|
$this->jobRepository = new InMemoryJobRepository();
|
||||||
$this->chapterRepository = new InMemoryChapterRepository();
|
$this->chapterRepository = new InMemoryChapterRepository();
|
||||||
$this->mangaRepository = new InMemoryMangaRepository();
|
$this->mangaRepository = new InMemoryMangaRepository();
|
||||||
@@ -55,13 +53,12 @@ class ScrapeChapterHandlerTest extends TestCase
|
|||||||
mangaId: 'test-manga',
|
mangaId: 'test-manga',
|
||||||
chapterNumber: 2,
|
chapterNumber: 2,
|
||||||
volumeNumber: 1,
|
volumeNumber: 1,
|
||||||
cbzPath: null,
|
|
||||||
));
|
));
|
||||||
|
|
||||||
$this->handler = new ScrapeChapterHandler(
|
$this->handler = new ScrapeChapterHandler(
|
||||||
$this->scraperFactory,
|
$this->scraperFactory,
|
||||||
$this->imageDownloader,
|
$this->imageDownloader,
|
||||||
$this->cbzGenerator,
|
$this->imageStorage,
|
||||||
$this->jobRepository,
|
$this->jobRepository,
|
||||||
$this->chapterRepository,
|
$this->chapterRepository,
|
||||||
$this->mangaRepository,
|
$this->mangaRepository,
|
||||||
@@ -87,9 +84,9 @@ class ScrapeChapterHandlerTest extends TestCase
|
|||||||
$this->assertCount(1, $dispatchedMessages);
|
$this->assertCount(1, $dispatchedMessages);
|
||||||
$this->assertInstanceOf(ChapterScraped::class, $dispatchedMessages[0]);
|
$this->assertInstanceOf(ChapterScraped::class, $dispatchedMessages[0]);
|
||||||
$this->assertEquals($job->id, $dispatchedMessages[0]->getJobId());
|
$this->assertEquals($job->id, $dispatchedMessages[0]->getJobId());
|
||||||
|
$this->assertEquals('1', $dispatchedMessages[0]->chapterId);
|
||||||
$chapter = $this->chapterRepository->getById('1');
|
$this->assertEquals('/fake/pages/1', $dispatchedMessages[0]->pagesDirectory);
|
||||||
$this->assertNotNull($chapter->cbzPath);
|
$this->assertNotNull($this->imageStorage->stored['1'] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user