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
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user