Files
Mangarr/src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php
ext.jeremy.guillot@maxicoffee.domains 969f4569f5 fix(monitoring): corriger la résolution de l'ID chapitre après synchronisation MangaDex
synchronizeChapters() retournait des UUID temporaires générés en mémoire. Ces UUID
n'étant jamais persistés, le Scraping domain ne pouvait pas retrouver le chapitre
(SQLSTATE 22P02 : invalid input syntax for type integer).

- ChapterSynchronizationServiceInterface : retourne float[] (numéros) au lieu de string[] (UUID)
- MangadxChapterSynchronizationService : retourne getNumber() au lieu de getId()
- RefreshMangaChaptersHandler : après save(), retrouve chaque chapitre par manga+numéro
  via findChapterByMangaIdAndNumber() pour obtenir le vrai PK integer avant de dispatcher
  ChapterReadyForScraping
2026-03-27 15:03:05 +01:00

239 lines
9.8 KiB
PHP

<?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 (null === $manga->getExternalId()) {
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;
} elseif ('fr' === $language) {
// Le français est toujours prioritaire
$shouldReplaceChapter = true;
} elseif ('en' === $language && 'fr' !== $chapterLanguages[(string) $chapterNumber]) {
// 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(),
$chapterNumber,
$title,
isset($chapterData['attributes']['volume']) ? (int) $chapterData['attributes']['volume'] : null,
true,
null,
0,
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 numéros
foreach ($chaptersByNumber as $chapterNumber => $chapter) {
if (!isset($existingChapters[(float) $chapterNumber])) {
$manga->addChapter($chapter);
$newChapterIds[] = $chapter->getNumber();
}
}
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 (null === $prevVolume && null === $nextVolume && null !== $currentVolume) {
$chaptersByNumber[$currentChapterNum] = new Chapter(
new ChapterId($currentChapter->getId()),
$currentChapter->getMangaId(),
$currentChapter->getNumber(),
$currentChapter->getTitle(),
null, // volume = null
$currentChapter->isVisible(),
$currentChapter->getPagesDirectory(),
$currentChapter->getPageCount(),
$currentChapter->getCreatedAt()
);
}
// Règle 2: Si précédent et suivant ont le même volume, alors actuel aussi
elseif (null !== $prevVolume && $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->getPagesDirectory(),
$currentChapter->getPageCount(),
$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 (null !== $currentChapter->getVolume()) {
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 (null !== $prevChapter->getVolume()) {
$prevVolume = $prevChapter->getVolume();
break;
}
}
// Cherche le volume suivant non-null
$nextVolume = null;
for ($k = $i + 1; $k < $count; ++$k) {
$nextChapter = $chaptersByNumber[$chapterNumbers[$k]];
if (null !== $nextChapter->getVolume()) {
$nextVolume = $nextChapter->getVolume();
break;
}
}
// Priorité au volume précédent : le chapitre appartient à la fin du volume en cours
// Couvre les cas : milieu de volume (prev=next), transition entre deux volumes (prev≠next)
if (null !== $prevVolume) {
$chaptersByNumber[$currentChapterNum] = new Chapter(
new ChapterId($currentChapter->getId()),
$currentChapter->getMangaId(),
$currentChapter->getNumber(),
$currentChapter->getTitle(),
$prevVolume,
$currentChapter->isVisible(),
$currentChapter->getPagesDirectory(),
$currentChapter->getPageCount(),
$currentChapter->getCreatedAt()
);
}
// Sinon utilise le volume suivant (chapitres en début de série)
elseif (null !== $nextVolume) {
$chaptersByNumber[$currentChapterNum] = new Chapter(
new ChapterId($currentChapter->getId()),
$currentChapter->getMangaId(),
$currentChapter->getNumber(),
$currentChapter->getTitle(),
$nextVolume,
$currentChapter->isVisible(),
$currentChapter->getPagesDirectory(),
$currentChapter->getPageCount(),
$currentChapter->getCreatedAt()
);
}
}
}
}