refactor(scraping): DDD refactoring — stockage images individuelles
Le domaine Scraping ne génère plus d'archives CBZ ni ne modifie les
entités du domaine Manga directement. Il scrape, stocke les images
individuellement, et émet un événement partagé.
- Suppression : CbzGeneratorInterface, CbzGenerator, CbzGenerationRequest,
CbzPath, CbzGenerationException
- Suppression : save() de ChapterRepositoryInterface (Scraping)
- Suppression : cbzPath du modèle Chapter (Scraping)
- Ajout : ImageStorageInterface + LocalImageStorage
(stockage dans {MANGA_DATA_PATH}/pages/{chapterId}/)
- ScrapeChapterHandler utilise ImageStorage au lieu du générateur CBZ
- ChapterScraped déplacé dans Domain/Shared/Domain/Event/
avec jobId, chapterId, pagesDirectory, pageCount
- Routing Messenger ajouté
- Ajout : ChapterScrapedEventListener + ChapterScrapedMessageHandler
pour mettre à jour Chapter.pagesDirectory via le Repository Manga
- LegacyChapterRepository en dual-mode :
pagesDirectory en priorité, fallback cbzPath (backward compat)
- Requêtes prev/next : filtrent pagesDirectory IS NOT NULL OR cbzPath IS NOT NULL
- ChapterContext expose pagesDirectory
- phparkitect.php : App\Domain\Shared\Domain\Event autorisé dans
les couches Application (correction violations pré-existantes
ChapterImported/VolumeImported + nouvelle ChapterScraped)
- 218/218 tests passent (+3 nouveaux)
- InMemoryImageStorage créé pour les tests unitaires
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d444f86315
commit
c311cfe80c
@@ -29,12 +29,187 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
||||
'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) {
|
||||
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->open($cbzPath);
|
||||
|
||||
@@ -68,121 +243,44 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return $pages;
|
||||
}
|
||||
|
||||
public function getChapterContext(ChapterId $chapterId): ChapterContext
|
||||
private function getPageContentFromDirectory(ChapterId $chapterId, string $pagesDirectory, PageNumber $pageNumber): PageContent
|
||||
{
|
||||
/** @var ChapterEntity $chapter */
|
||||
$chapter = $this->entityManager->getRepository(ChapterEntity::class)->findOneBy([
|
||||
'id' => $chapterId->getValue()
|
||||
]);
|
||||
$files = $this->getImageFiles($pagesDirectory);
|
||||
|
||||
if (!$chapter) {
|
||||
throw ChapterNotFoundException::forChapter($chapterId);
|
||||
if (!$files || $pageNumber->getValue() > count($files)) {
|
||||
throw PageNotFoundException::forPage($chapterId, $pageNumber);
|
||||
}
|
||||
|
||||
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()
|
||||
$filePath = $files[$pageNumber->getValue() - 1];
|
||||
$imageContent = file_get_contents($filePath);
|
||||
|
||||
if ($imageContent === false) {
|
||||
throw PageNotFoundException::forPage($chapterId, $pageNumber);
|
||||
}
|
||||
|
||||
$imageSize = @getimagesizefromstring($imageContent);
|
||||
if ($imageSize === false) {
|
||||
throw PageNotFoundException::forPage($chapterId, $pageNumber);
|
||||
}
|
||||
|
||||
$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->open($cbzPath);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user