feat: ajout de la gestion des chapitres de manga, incluant la récupération et la sauvegarde des chapitres en français et en anglais, ainsi que l'optimisation de la logique de sauvegarde pour éviter les doublons

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-04-01 16:01:55 +02:00
parent 34dfa57dc0
commit 0111f1b5f1
12 changed files with 254 additions and 78 deletions

View File

@@ -20,7 +20,7 @@ readonly class FetchMangaChaptersHandler
public function handle(FetchMangaChapters $command): void
{
$manga = $this->mangaRepository->findByExternalId(new ExternalId($command->externalId));
if ($manga === null) {
throw new \RuntimeException('Manga not found');
}
@@ -28,6 +28,8 @@ readonly class FetchMangaChaptersHandler
$offset = 0;
$limit = 500;
$hasMore = true;
$chaptersByNumber = [];
$chapterNumbers = [];
while ($hasMore) {
$feed = $this->mangadexClient->getMangaFeed(
@@ -37,22 +39,47 @@ readonly class FetchMangaChaptersHandler
);
foreach ($feed['data'] as $chapterData) {
$chapter = new Chapter(
new ChapterId((string) Uuid::uuid4()),
$manga->getId()->getValue(),
(float) $chapterData['attributes']['chapter'],
$chapterData['attributes']['title'],
isset($chapterData['attributes']['volume']) ? (int) $chapterData['attributes']['volume'] : null,
true,
false,
new \DateTimeImmutable()
);
$chapterNumber = (float) $chapterData['attributes']['chapter'];
$language = $chapterData['attributes']['translatedLanguage'];
$this->mangaRepository->saveChapter($chapter);
// On ne traite que les chapitres en français ou en anglais
if (!in_array($language, ['fr', 'en'])) {
continue;
}
// Si le chapitre n'existe pas encore ou si c'est une version française
if (!isset($chaptersByNumber[$chapterNumber]) || $language === 'fr') {
$chapter = new Chapter(
new ChapterId((string) Uuid::uuid4()),
$manga->getId()->getValue(),
$chapterNumber,
$chapterData['attributes']['title'],
isset($chapterData['attributes']['volume']) ? (int) $chapterData['attributes']['volume'] : null,
true,
false,
new \DateTimeImmutable()
);
$chaptersByNumber[$chapterNumber] = $chapter;
$chapterNumbers[] = $chapterNumber;
}
}
$offset += $limit;
$hasMore = count($feed['data']) === $limit;
}
// Récupère les chapitres existants
$existingChapters = $this->mangaRepository->findExistingChaptersByNumbers(
$manga->getId()->getValue(),
$chapterNumbers
);
// Sauvegarde uniquement les nouveaux chapitres
foreach ($chaptersByNumber as $chapterNumber => $chapter) {
if (!isset($existingChapters[$chapterNumber])) {
$this->mangaRepository->saveChapter($chapter);
}
}
}
}
}

View File

@@ -21,4 +21,5 @@ interface MangaRepositoryInterface
public function findBySlug(MangaSlug $slug): ?Manga;
public function search(string $query, int $page = 1, int $limit = 20): array;
public function countSearch(string $query): int;
}
public function findExistingChaptersByNumbers(string $mangaId, array $chapterNumbers): array;
}

View File

@@ -200,6 +200,26 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
->getSingleScalarResult();
}
public function findExistingChaptersByNumbers(string $mangaId, array $chapterNumbers): array
{
$queryBuilder = $this->entityManager->createQueryBuilder()
->select('c')
->from(EntityChapter::class, 'c')
->where('c.manga = :mangaId')
->andWhere('c.number IN (:chapterNumbers)')
->setParameter('mangaId', $mangaId)
->setParameter('chapterNumbers', $chapterNumbers);
$chapters = $queryBuilder->getQuery()->getResult();
$chaptersByNumber = [];
foreach ($chapters as $chapter) {
$chaptersByNumber[$chapter->getNumber()] = $this->toChapterDomain($chapter);
}
return $chaptersByNumber;
}
private function toDomain(EntityManga $entity): DomainManga
{
return new DomainManga(

View File

@@ -5,14 +5,12 @@ namespace App\Domain\Scraping\Application\CommandHandler;
use App\Domain\Scraping\Application\Command\ScrapeChapter;
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Scraping\Domain\Contract\Repository\ScrapingJobRepositoryInterface;
use App\Domain\Scraping\Domain\Contract\Repository\SourceRepositoryInterface;
use App\Domain\Scraping\Domain\Contract\Service\CbzGeneratorInterface;
use App\Domain\Scraping\Domain\Contract\Service\ImageDownloaderInterface;
use App\Domain\Scraping\Domain\Contract\Service\ScraperInterface;
use App\Domain\Scraping\Domain\Event\ChapterScraped;
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
use App\Domain\Scraping\Domain\Event\ChapterScrapingStarted;
use App\Domain\Scraping\Domain\Model\ScrapingJob;
use App\Domain\Scraping\Domain\Model\ValueObject\CbzGenerationRequest;
use App\Domain\Scraping\Domain\Model\ValueObject\ScrapingRequest;

View File

@@ -2,13 +2,15 @@
namespace App\Domain\Scraping\Domain\Event;
class ChapterScraped
readonly class ChapterScraped
{
public function __construct(
private string $jobId
) {
}
/**
* @param string $getId
*/
public function __construct(string $getId)
public function getJobId(): string
{
return $this->jobId;
}
}

View File

@@ -10,6 +10,7 @@ use App\Domain\Shared\Application\Query\ListJobsQuery;
use App\Domain\Shared\Application\QueryHandler\ListJobsQueryHandler;
use App\Domain\Shared\Domain\Model\JobStatus;
use App\Domain\Shared\Infrastructure\ApiPlatform\Resource\GetJobListResource;
use ApiPlatform\State\Pagination\ArrayPaginator;
readonly class GetJobListStateProvider implements ProviderInterface
{
@@ -17,7 +18,7 @@ readonly class GetJobListStateProvider implements ProviderInterface
private ListJobsQueryHandler $handler
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ArrayPaginator
{
$filters = $context['filters'] ?? [];
@@ -60,15 +61,13 @@ readonly class GetJobListStateProvider implements ProviderInterface
$response = $this->handler->handle($query);
return [
'items' => array_map(
return new ArrayPaginator(
array_map(
fn($job) => GetJobListResource::fromJob($job),
$response->items
),
'total' => $response->total,
'page' => $response->page,
'limit' => $response->limit,
'pages' => $response->pages
];
0,
$response->total
);
}
}

View File

@@ -20,12 +20,10 @@ readonly class DoctrineJobRepository implements JobRepositoryInterface
public function save(Job $job): void
{
dump('save', $job);
/** @var JobEntity|null $existingJobEntity */
$existingJobEntity = $this->entityManager->find(JobEntity::class, $job->id);
if ($existingJobEntity) {
dump('existingJobEntity', $existingJobEntity);
$existingJobEntity->setStatus($job->status->value);
$existingJobEntity->setStartedAt($job->startedAt);
$existingJobEntity->setCompletedAt($job->completedAt);
@@ -33,14 +31,11 @@ readonly class DoctrineJobRepository implements JobRepositoryInterface
$existingJobEntity->setAttempts($job->attempts);
$existingJobEntity->setContext($job->context);
$this->entityManager->persist($existingJobEntity);
dump('updated', $existingJobEntity);
} else {
$entity = $this->mapper->toEntity($job);
$this->entityManager->persist($entity);
dump('created', $entity);
}
$this->entityManager->flush();
dump('flushed');
}
public function get(string $id): Job