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:
ext.jeremy.guillot@maxicoffee.domains
2025-07-22 15:57:25 +02:00
parent d9e78b5229
commit 00d63dffeb
45 changed files with 2021 additions and 264 deletions

View File

@@ -0,0 +1,231 @@
<?php
namespace App\Domain\Manga\Infrastructure\Service;
use App\Domain\Manga\Domain\Contract\Client\MangadexClientInterface;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Contract\Service\ChapterSynchronizationServiceInterface;
use App\Domain\Manga\Domain\Model\Chapter;
use App\Domain\Manga\Domain\Model\Manga;
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
use Ramsey\Uuid\Uuid;
readonly class MangadxChapterSynchronizationService implements ChapterSynchronizationServiceInterface
{
public function __construct(
private MangadexClientInterface $mangadxClient,
private MangaRepositoryInterface $mangaRepository
) {}
public function synchronizeChapters(Manga $manga): array
{
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->mangadxClient->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,
null,
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
);
$newChapterIds = [];
// Sauvegarde uniquement les nouveaux chapitres et collecte leurs IDs
foreach ($chaptersByNumber as $chapterNumber => $chapter) {
if (!isset($existingChapters[(float) $chapterNumber])) {
$newChapterId = $this->mangaRepository->saveChapter($chapter);
$newChapterIds[] = $newChapterId->getValue(); // ✨ Collecte des IDs
}
}
return $newChapterIds;
}
/**
* 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];
$prevVolume = $prevChapter->getVolume();
$currentVolume = $currentChapter->getVolume();
$nextVolume = $nextChapter->getVolume();
// Règle 1: Si précédent et suivant sont null, alors actuel aussi
if ($prevVolume === null && $nextVolume === null && $currentVolume !== null) {
$chaptersByNumber[$currentChapterNum] = new Chapter(
new ChapterId($currentChapter->getId()),
$currentChapter->getMangaId(),
$currentChapter->getNumber(),
$currentChapter->getTitle(),
null, // volume = null
$currentChapter->isVisible(),
$currentChapter->getCbzPath(),
$currentChapter->getCreatedAt()
);
}
// Règle 2: Si précédent et suivant ont le même volume, alors actuel aussi
else if ($prevVolume !== null && $prevVolume === $nextVolume && $currentVolume !== $prevVolume) {
$chaptersByNumber[$currentChapterNum] = new Chapter(
new ChapterId($currentChapter->getId()),
$currentChapter->getMangaId(),
$currentChapter->getNumber(),
$currentChapter->getTitle(),
$prevVolume, // prend le volume des adjacents
$currentChapter->isVisible(),
$currentChapter->getCbzPath(),
$currentChapter->getCreatedAt()
);
}
}
// Deuxième passe : comblement des trous de volumes
$this->fillVolumeGaps($chaptersByNumber, $chapterNumbers);
}
/**
* Remplit les "trous" de volumes manquants dans une séquence
*/
private function fillVolumeGaps(array &$chaptersByNumber, array $chapterNumbers): void
{
$count = count($chapterNumbers);
for ($i = 0; $i < $count; $i++) {
$currentChapterNum = $chapterNumbers[$i];
$currentChapter = $chaptersByNumber[$currentChapterNum];
if ($currentChapter->getVolume() !== null) {
continue; // Ce chapitre a déjà un volume
}
// Cherche le volume précédent non-null
$prevVolume = null;
for ($j = $i - 1; $j >= 0; $j--) {
$prevChapter = $chaptersByNumber[$chapterNumbers[$j]];
if ($prevChapter->getVolume() !== null) {
$prevVolume = $prevChapter->getVolume();
break;
}
}
// Cherche le volume suivant non-null
$nextVolume = null;
for ($k = $i + 1; $k < $count; $k++) {
$nextChapter = $chaptersByNumber[$chapterNumbers[$k]];
if ($nextChapter->getVolume() !== null) {
$nextVolume = $nextChapter->getVolume();
break;
}
}
// Si on a trouvé un volume précédent et que le suivant est le même ou null, alors utilise le précédent
if ($prevVolume !== null && ($nextVolume === null || $nextVolume === $prevVolume)) {
$chaptersByNumber[$currentChapterNum] = new Chapter(
new ChapterId($currentChapter->getId()),
$currentChapter->getMangaId(),
$currentChapter->getNumber(),
$currentChapter->getTitle(),
$prevVolume,
$currentChapter->isVisible(),
$currentChapter->getCbzPath(),
$currentChapter->getCreatedAt()
);
}
// Si on a trouvé un volume suivant mais pas de précédent, utilise le suivant
else if ($nextVolume !== null && $prevVolume === null) {
$chaptersByNumber[$currentChapterNum] = new Chapter(
new ChapterId($currentChapter->getId()),
$currentChapter->getMangaId(),
$currentChapter->getNumber(),
$currentChapter->getTitle(),
$nextVolume,
$currentChapter->isVisible(),
$currentChapter->getCbzPath(),
$currentChapter->getCreatedAt()
);
}
}
}
}