feat: ajout de la fonctionnalité de monitoring des mangas, incluant l'activation et la désactivation du suivi, la synchronisation des chapitres, et la mise à jour de l'API pour gérer ces nouvelles actions. Création de nouveaux composants Vue pour le rafraîchissement des chapitres et l'affichage des notifications. Intégration de tests unitaires pour valider le bon fonctionnement de ces fonctionnalités.
This commit is contained in:
parent
d9e78b5229
commit
00d63dffeb
@@ -3,249 +3,25 @@
|
||||
namespace App\Domain\Manga\Application\CommandHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Command\FetchMangaChapters;
|
||||
use App\Domain\Manga\Domain\Contract\Client\MangadexClientInterface;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Model\Chapter;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use App\Domain\Manga\Domain\Contract\Service\ChapterSynchronizationServiceInterface;
|
||||
|
||||
readonly class FetchMangaChaptersHandler
|
||||
{
|
||||
public function __construct(
|
||||
private MangadexClientInterface $mangadexClient,
|
||||
private MangaRepositoryInterface $mangaRepository
|
||||
private MangaRepositoryInterface $mangaRepository,
|
||||
private ChapterSynchronizationServiceInterface $chapterSynchronizationService
|
||||
) {}
|
||||
|
||||
public function handle(FetchMangaChapters $command): void
|
||||
{
|
||||
$manga = $this->mangaRepository->findById($command->mangaId);
|
||||
$manga = $this->mangaRepository->findById($command->mangaId->getValue());
|
||||
|
||||
if ($manga === null) {
|
||||
throw new \RuntimeException('Manga not found');
|
||||
}
|
||||
|
||||
if ($manga->getExternalId() === null) {
|
||||
throw new \RuntimeException('Manga has no external ID');
|
||||
}
|
||||
|
||||
$externalId = $manga->getExternalId()->getValue();
|
||||
|
||||
$offset = 0;
|
||||
$limit = 500;
|
||||
$hasMore = true;
|
||||
$chaptersByNumber = [];
|
||||
$chapterLanguages = []; // Pour stocker la langue de chaque chapitre
|
||||
$chapterNumbers = [];
|
||||
|
||||
while ($hasMore) {
|
||||
$feed = $this->mangadexClient->getMangaFeed(
|
||||
$externalId,
|
||||
$offset,
|
||||
$limit
|
||||
);
|
||||
|
||||
foreach ($feed['data'] as $chapterData) {
|
||||
$chapterNumber = (float) $chapterData['attributes']['chapter'];
|
||||
$language = $chapterData['attributes']['translatedLanguage'];
|
||||
$title = $chapterData['attributes']['title'];
|
||||
|
||||
// Pour les langues autres que français et anglais, on utilise un titre générique
|
||||
if (!in_array($language, ['fr', 'en'])) {
|
||||
$title = "Chapter {$chapterNumber}";
|
||||
}
|
||||
|
||||
// Définir les règles de priorité des langues (fr > en > autres)
|
||||
$shouldReplaceChapter = false;
|
||||
|
||||
if (!isset($chaptersByNumber[(string) $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[(string) $chapterNumber] !== 'fr') {
|
||||
// L'anglais est prioritaire sur les autres langues, sauf le français
|
||||
$shouldReplaceChapter = true;
|
||||
}
|
||||
|
||||
if ($shouldReplaceChapter) {
|
||||
$chaptersByNumber[(string) $chapterNumber] = new Chapter(
|
||||
new ChapterId((string) Uuid::uuid4()),
|
||||
$manga->getId()->getValue(),
|
||||
$chapterNumber,
|
||||
$title,
|
||||
isset($chapterData['attributes']['volume']) ? (int) $chapterData['attributes']['volume'] : null,
|
||||
true,
|
||||
false,
|
||||
new \DateTimeImmutable()
|
||||
);
|
||||
$chapterLanguages[(string) $chapterNumber] = $language;
|
||||
}
|
||||
}
|
||||
|
||||
$offset += $limit;
|
||||
$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(),
|
||||
$chapterNumbers
|
||||
);
|
||||
|
||||
// Sauvegarde uniquement les nouveaux chapitres
|
||||
foreach ($chaptersByNumber as $chapterNumber => $chapter) {
|
||||
if (!isset($existingChapters[(float) $chapterNumber])) {
|
||||
$this->mangaRepository->saveChapter($chapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* - Remplit les "trous" de volumes manquants dans une séquence
|
||||
*/
|
||||
private function harmonizeVolumes(array &$chaptersByNumber): void
|
||||
{
|
||||
// Trie les chapitres par numéro pour faciliter la recherche des adjacents
|
||||
uksort($chaptersByNumber, fn($a, $b) => (float)$a <=> (float)$b);
|
||||
|
||||
$chapterNumbers = array_keys($chaptersByNumber);
|
||||
$count = count($chapterNumbers);
|
||||
|
||||
// Première passe : harmonisation locale (chapitres adjacents)
|
||||
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) {
|
||||
$chaptersByNumber[$currentChapterNum] = $this->createChapterWithNewVolume($currentChapter, $newVolume);
|
||||
}
|
||||
}
|
||||
|
||||
// Deuxième passe : remplissage des trous de volumes
|
||||
$this->fillVolumeGaps($chaptersByNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remplit les "trous" de volumes dans une séquence de chapitres.
|
||||
* Par exemple, si on a : Ch.317(Vol.34), Ch.318(Vol.34), Ch.319(null), Ch.320(null), Ch.321(Vol.34)
|
||||
* Alors Ch.319 et Ch.320 seront assignés au Vol.34
|
||||
*/
|
||||
private function fillVolumeGaps(array &$chaptersByNumber): void
|
||||
{
|
||||
$chapterNumbers = array_keys($chaptersByNumber);
|
||||
$count = count($chapterNumbers);
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$currentChapterNum = $chapterNumbers[$i];
|
||||
$currentChapter = $chaptersByNumber[$currentChapterNum];
|
||||
|
||||
// Si le chapitre actuel n'a pas de volume, on cherche à le combler
|
||||
if ($currentChapter->getVolume() === null) {
|
||||
$volumeToAssign = $this->findVolumeForGap($chaptersByNumber, $chapterNumbers, $i);
|
||||
|
||||
if ($volumeToAssign !== null) {
|
||||
$chaptersByNumber[$currentChapterNum] = $this->createChapterWithNewVolume($currentChapter, $volumeToAssign);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve le volume à assigner pour un chapitre sans volume en analysant son contexte
|
||||
*/
|
||||
private function findVolumeForGap(array $chaptersByNumber, array $chapterNumbers, int $currentIndex): ?int
|
||||
{
|
||||
$count = count($chapterNumbers);
|
||||
|
||||
// Cherche le volume précédent non-null
|
||||
$prevVolume = null;
|
||||
for ($i = $currentIndex - 1; $i >= 0; $i--) {
|
||||
$prevChapter = $chaptersByNumber[$chapterNumbers[$i]];
|
||||
if ($prevChapter->getVolume() !== null) {
|
||||
$prevVolume = $prevChapter->getVolume();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cherche le volume suivant non-null
|
||||
$nextVolume = null;
|
||||
for ($i = $currentIndex + 1; $i < $count; $i++) {
|
||||
$nextChapter = $chaptersByNumber[$chapterNumbers[$i]];
|
||||
if ($nextChapter->getVolume() !== null) {
|
||||
$nextVolume = $nextChapter->getVolume();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Si les volumes précédent et suivant sont identiques et non-null, on utilise ce volume
|
||||
if ($prevVolume !== null && $prevVolume === $nextVolume) {
|
||||
return $prevVolume;
|
||||
}
|
||||
|
||||
// Si on a seulement un volume précédent, on vérifie s'il est raisonnable de l'utiliser
|
||||
// (pas plus de 10 chapitres d'écart pour éviter les erreurs)
|
||||
if ($prevVolume !== null && $nextVolume === null) {
|
||||
$currentChapterNumber = (float) $chapterNumbers[$currentIndex];
|
||||
|
||||
// Trouve le numéro du chapitre qui a ce volume précédent
|
||||
for ($i = $currentIndex - 1; $i >= 0; $i--) {
|
||||
$prevChapter = $chaptersByNumber[$chapterNumbers[$i]];
|
||||
if ($prevChapter->getVolume() === $prevVolume) {
|
||||
$prevChapterNumber = (float) $chapterNumbers[$i];
|
||||
// Si l'écart est raisonnable (moins de 10 chapitres), on assigne le volume
|
||||
if ($currentChapterNumber - $prevChapterNumber <= 10) {
|
||||
return $prevVolume;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un nouveau chapitre avec un volume différent (les chapitres sont immuables)
|
||||
*/
|
||||
private function createChapterWithNewVolume(Chapter $chapter, ?int $newVolume): Chapter
|
||||
{
|
||||
return new Chapter(
|
||||
id: new ChapterId($chapter->getId()),
|
||||
mangaId: $chapter->getMangaId(),
|
||||
number: $chapter->getNumber(),
|
||||
title: $chapter->getTitle(),
|
||||
volume: $newVolume,
|
||||
isVisible: $chapter->isVisible(),
|
||||
cbzPath: $chapter->getCbzPath(),
|
||||
createdAt: $chapter->getCreatedAt()
|
||||
);
|
||||
// Synchronisation initiale (pas d'événements)
|
||||
$this->chapterSynchronizationService->synchronizeChapters($manga);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user