feat: ajout de la gestion des chapitres dans le store Manga avec des actions pour charger et mettre à jour la disponibilité des chapitres, intégration d'un écouteur Mercure pour les mises à jour en temps réel, et amélioration des composants d'interface utilisateur pour gérer les états de chargement et d'erreur.
This commit is contained in:
parent
e51712a800
commit
5928cfd5f0
@@ -29,6 +29,7 @@ readonly class FetchMangaChaptersHandler
|
||||
$limit = 500;
|
||||
$hasMore = true;
|
||||
$chaptersByNumber = [];
|
||||
$chapterLanguages = []; // Pour stocker la langue de chaque chapitre
|
||||
$chapterNumbers = [];
|
||||
|
||||
while ($hasMore) {
|
||||
@@ -41,27 +42,40 @@ readonly class FetchMangaChaptersHandler
|
||||
foreach ($feed['data'] as $chapterData) {
|
||||
$chapterNumber = (float) $chapterData['attributes']['chapter'];
|
||||
$language = $chapterData['attributes']['translatedLanguage'];
|
||||
$title = $chapterData['attributes']['title'];
|
||||
|
||||
// On ne traite que les chapitres en français ou en anglais
|
||||
// Pour les langues autres que français et anglais, on utilise un titre générique
|
||||
if (!in_array($language, ['fr', 'en'])) {
|
||||
continue;
|
||||
$title = "Chapter {$chapterNumber}";
|
||||
}
|
||||
|
||||
// Si le chapitre n'existe pas encore ou si c'est une version française
|
||||
if (!isset($chaptersByNumber[$chapterNumber]) || $language === 'fr') {
|
||||
$chapter = new Chapter(
|
||||
// Définir les règles de priorité des langues (fr > en > autres)
|
||||
$shouldReplaceChapter = false;
|
||||
|
||||
if (!isset($chaptersByNumber[$chapterNumber])) {
|
||||
// Si c'est le premier chapitre avec ce numéro qu'on rencontre
|
||||
$shouldReplaceChapter = true;
|
||||
$chapterNumbers[] = $chapterNumber;
|
||||
} else if ($language === 'fr') {
|
||||
// Le français est toujours prioritaire
|
||||
$shouldReplaceChapter = true;
|
||||
} else if ($language === 'en' && $chapterLanguages[$chapterNumber] !== 'fr') {
|
||||
// L'anglais est prioritaire sur les autres langues, sauf le français
|
||||
$shouldReplaceChapter = true;
|
||||
}
|
||||
|
||||
if ($shouldReplaceChapter) {
|
||||
$chaptersByNumber[$chapterNumber] = new Chapter(
|
||||
new ChapterId((string) Uuid::uuid4()),
|
||||
$manga->getId()->getValue(),
|
||||
$chapterNumber,
|
||||
$chapterData['attributes']['title'],
|
||||
$title,
|
||||
isset($chapterData['attributes']['volume']) ? (int) $chapterData['attributes']['volume'] : null,
|
||||
true,
|
||||
false,
|
||||
new \DateTimeImmutable()
|
||||
);
|
||||
|
||||
$chaptersByNumber[$chapterNumber] = $chapter;
|
||||
$chapterNumbers[] = $chapterNumber;
|
||||
$chapterLanguages[$chapterNumber] = $language;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +83,9 @@ readonly class FetchMangaChaptersHandler
|
||||
$hasMore = count($feed['data']) === $limit;
|
||||
}
|
||||
|
||||
// Harmonisation des volumes: si le chapitre précédent et suivant ont un volume null, alors le chapitre actuel aussi
|
||||
$this->harmonizeVolumes($chaptersByNumber);
|
||||
|
||||
// Récupère les chapitres existants
|
||||
$existingChapters = $this->mangaRepository->findExistingChaptersByNumbers(
|
||||
$manga->getId()->getValue(),
|
||||
@@ -82,4 +99,56 @@ readonly class FetchMangaChaptersHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Harmonise les volumes des chapitres:
|
||||
* - Si le chapitre précédent et suivant ont un volume null, alors le chapitre actuel aussi
|
||||
* - Si le chapitre précédent et suivant ont le même volume, alors le chapitre actuel aura ce volume
|
||||
*/
|
||||
private function harmonizeVolumes(array &$chaptersByNumber): void
|
||||
{
|
||||
// Trie les chapitres par numéro pour faciliter la recherche des adjacents
|
||||
ksort($chaptersByNumber);
|
||||
|
||||
$chapterNumbers = array_keys($chaptersByNumber);
|
||||
$count = count($chapterNumbers);
|
||||
|
||||
for ($i = 1; $i < $count - 1; $i++) {
|
||||
$prevChapterNum = $chapterNumbers[$i - 1];
|
||||
$currentChapterNum = $chapterNumbers[$i];
|
||||
$nextChapterNum = $chapterNumbers[$i + 1];
|
||||
|
||||
$prevChapter = $chaptersByNumber[$prevChapterNum];
|
||||
$currentChapter = $chaptersByNumber[$currentChapterNum];
|
||||
$nextChapter = $chaptersByNumber[$nextChapterNum];
|
||||
|
||||
$shouldUpdateVolume = false;
|
||||
$newVolume = $currentChapter->getVolume();
|
||||
|
||||
// Si les chapitres adjacents ont un volume null, alors le chapitre actuel aussi
|
||||
if ($prevChapter->getVolume() === null && $nextChapter->getVolume() === null && $currentChapter->getVolume() !== null) {
|
||||
$shouldUpdateVolume = true;
|
||||
$newVolume = null;
|
||||
}
|
||||
// Si les chapitres adjacents ont le même volume non-null, alors le chapitre actuel aura ce volume
|
||||
else if ($prevChapter->getVolume() !== null && $prevChapter->getVolume() === $nextChapter->getVolume() && $currentChapter->getVolume() !== $prevChapter->getVolume()) {
|
||||
$shouldUpdateVolume = true;
|
||||
$newVolume = $prevChapter->getVolume();
|
||||
}
|
||||
|
||||
if ($shouldUpdateVolume) {
|
||||
// On doit créer un nouveau chapitre car les objets sont immuables
|
||||
$chaptersByNumber[$currentChapterNum] = new Chapter(
|
||||
id: new ChapterId($currentChapter->getId()),
|
||||
mangaId: $currentChapter->getMangaId(),
|
||||
number: $currentChapter->getNumber(),
|
||||
title: $currentChapter->getTitle(),
|
||||
volume: $newVolume, // On met à jour le volume
|
||||
isVisible: $currentChapter->isVisible(),
|
||||
isAvailable: $currentChapter->isAvailable(),
|
||||
createdAt: $currentChapter->getCreatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class MangadexClient implements MangadexClientInterface
|
||||
]);
|
||||
|
||||
$data = $response->toArray();
|
||||
|
||||
|
||||
if (!isset($data['access_token'], $data['refresh_token'])) {
|
||||
throw new MangadexAuthenticationException('Invalid authentication response from Mangadex');
|
||||
}
|
||||
@@ -70,7 +70,7 @@ class MangadexClient implements MangadexClientInterface
|
||||
]);
|
||||
|
||||
$data = $response->toArray();
|
||||
|
||||
|
||||
if (!isset($data['access_token'])) {
|
||||
throw new MangadexAuthenticationException('Invalid refresh token response from Mangadex');
|
||||
}
|
||||
@@ -107,7 +107,7 @@ class MangadexClient implements MangadexClientInterface
|
||||
{
|
||||
return $this->get('/manga/' . $mangaId . '/feed', [
|
||||
'limit' => $limit,
|
||||
'translatedLanguage' => ['en', 'fr'],
|
||||
// 'translatedLanguage' => ['en'],
|
||||
'order' => ['chapter' => $order],
|
||||
'offset' => $offset,
|
||||
]);
|
||||
@@ -160,4 +160,4 @@ class MangadexClient implements MangadexClientInterface
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,10 @@ readonly class ScrapeChapterHandler
|
||||
$chapter->chapterNumber,
|
||||
$source->getId()->getValue()
|
||||
);
|
||||
|
||||
// Ajout de l'ID du chapitre dans le contexte du job
|
||||
$job->context['chapterId'] = $command->chapterId;
|
||||
|
||||
$job->start();
|
||||
$this->jobRepository->save($job);
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\EventSubscriber;
|
||||
|
||||
use App\Domain\Scraping\Domain\Event\ChapterScraped;
|
||||
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
class ScrapingEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HubInterface $hub,
|
||||
private readonly ChapterRepositoryInterface $chapterRepository,
|
||||
private readonly JobRepositoryInterface $jobRepository,
|
||||
private readonly LoggerInterface $logger
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
// Les événements sont capturés via le système de message handlers
|
||||
];
|
||||
}
|
||||
|
||||
#[AsMessageHandler]
|
||||
public function onChapterScraped(ChapterScraped $event): void
|
||||
{
|
||||
$jobId = $event->getJobId();
|
||||
$this->logger->info('ChapterScraped reçu pour le job: ' . $jobId);
|
||||
|
||||
$job = $this->jobRepository->get($jobId);
|
||||
|
||||
if (!$job) {
|
||||
$this->logger->warning('Job non trouvé pour l\'ID: ' . $jobId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer le chapitre associé au job
|
||||
$chapterId = $job->context['chapterId'] ?? null;
|
||||
$this->logger->info('ChapterId extrait du job: ' . $chapterId);
|
||||
|
||||
$chapter = $this->chapterRepository->getById($chapterId);
|
||||
|
||||
if (!$chapter) {
|
||||
$this->logger->warning('Chapitre non trouvé pour l\'ID: ' . $chapterId);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->info('Chapitre trouvé - ID: ' . $chapter->id . ', MangaId: ' . $chapter->mangaId . ', Number: ' . $chapter->chapterNumber);
|
||||
|
||||
// Préparer les données à envoyer au front
|
||||
$data = [
|
||||
'type' => 'chapter.scraped',
|
||||
'chapterId' => $chapter->id,
|
||||
'mangaId' => $chapter->mangaId,
|
||||
'chapterNumber' => $chapter->chapterNumber,
|
||||
'isAvailable' => true,
|
||||
'timestamp' => (new \DateTimeImmutable())->format('c')
|
||||
];
|
||||
|
||||
$this->logger->info('Données préparées pour Mercure: ' . json_encode($data));
|
||||
|
||||
// Publier une mise à jour sur le hub Mercure
|
||||
$topics = [
|
||||
'manga/chapter/' . $chapter->id, // Topic spécifique au chapitre
|
||||
'manga/' . $chapter->mangaId . '/chapters', // Topic pour tous les chapitres d'un manga
|
||||
'scraping/status' // Topic général pour les événements de scraping
|
||||
];
|
||||
|
||||
$this->logger->info('Topics Mercure: ' . implode(', ', $topics));
|
||||
|
||||
$update = new Update($topics, json_encode($data));
|
||||
$this->hub->publish($update);
|
||||
|
||||
$this->logger->info('Mise à jour publiée sur Mercure');
|
||||
}
|
||||
|
||||
#[AsMessageHandler]
|
||||
public function onChapterScrapingFailed(ChapterScrapingFailed $event): void
|
||||
{
|
||||
$this->logger->info('ChapterScrapingFailed reçu pour mangaId: ' . $event->getMangaId() . ', chapter: ' . $event->getChapterNumber());
|
||||
|
||||
// Préparer les données à envoyer au front
|
||||
$data = [
|
||||
'type' => 'chapter.scraping.failed',
|
||||
'mangaId' => $event->getMangaId(),
|
||||
'chapterNumber' => $event->getChapterNumber(),
|
||||
'reason' => $event->getReason(),
|
||||
'timestamp' => (new \DateTimeImmutable())->format('c')
|
||||
];
|
||||
|
||||
$this->logger->info('Données préparées pour Mercure: ' . json_encode($data));
|
||||
|
||||
// Publier une mise à jour sur le hub Mercure
|
||||
$topics = [
|
||||
'manga/' . $event->getMangaId() . '/chapters', // Topic pour tous les chapitres d'un manga
|
||||
'scraping/status' // Topic général pour les événements de scraping
|
||||
];
|
||||
|
||||
$this->logger->info('Topics Mercure: ' . implode(', ', $topics));
|
||||
|
||||
$update = new Update($topics, json_encode($data));
|
||||
$this->hub->publish($update);
|
||||
|
||||
$this->logger->info('Mise à jour publiée sur Mercure');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user