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
239 lines
9.8 KiB
PHP
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()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|