feat: ajout de la gestion des chapitres dans le store Manga avec des actions pour charger et mettre à jour la disponibilité des chapitres, intégration d'un écouteur Mercure pour les mises à jour en temps réel, et amélioration des composants d'interface utilisateur pour gérer les états de chargement et d'erreur.

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-04-04 16:06:32 +02:00
parent e51712a800
commit 5928cfd5f0
11 changed files with 539 additions and 39 deletions

View File

@@ -28,6 +28,11 @@ export const useMangaStore = defineStore('manga', {
// mais les données détaillées ne sont plus stockées ici.
currentMangaId: null,
// --- Manga Chapters State ---
mangaChapters: {},
loadingChapters: false,
chaptersError: null,
// --- Search State ---
searchResults: [],
loadingSearch: false,
@@ -86,6 +91,60 @@ export const useMangaStore = defineStore('manga', {
this.currentMangaId = null;
},
// --- Chapters Actions ---
async loadChapters(mangaId) {
if (this.loadingChapters) return;
this.loadingChapters = true;
this.chaptersError = null;
try {
const chaptersData = await mangaRepository.getChapters(mangaId);
this.mangaChapters[mangaId] = chaptersData;
} catch (err) {
this.chaptersError = err.message;
} finally {
this.loadingChapters = false;
}
},
updateChapterAvailability(chapterId, isAvailable = true) {
console.log(`Mise à jour du chapitre ${chapterId}, disponible: ${isAvailable}`);
// Pour chaque manga dans notre store
Object.keys(this.mangaChapters).forEach(mangaId => {
const chaptersObj = this.mangaChapters[mangaId];
if (!chaptersObj || !chaptersObj.items) return;
const chapters = chaptersObj.items;
// Chercher le chapitre correspondant
const chapterIndex = chapters.findIndex(chapter => chapter.id === chapterId);
// Si on trouve le chapitre, mettre à jour son état
if (chapterIndex !== -1) {
console.log(`Chapitre trouvé dans le manga ${mangaId}, index: ${chapterIndex}`);
// Important: créer une nouvelle référence pour que Vue détecte le changement
const updatedChapter = {
...chapters[chapterIndex],
isAvailable: isAvailable
};
// Créer un nouveau tableau pour garantir la réactivité
const updatedChapters = [...chapters];
updatedChapters[chapterIndex] = updatedChapter;
// Mise à jour reactive du store
this.mangaChapters[mangaId] = {
...chaptersObj,
items: updatedChapters
};
console.log('Chapitre mis à jour avec succès');
}
});
},
// --- Search Actions ---
async searchMangaDex(query) {
if (this.loadingSearch) return;
@@ -130,16 +189,13 @@ export const useMangaStore = defineStore('manga', {
}
},
// --- Chapter Actions ---
// --- Scrape Chapter Action ---
async searchChapter(chapterId) {
try {
await mangaRepository.searchChapter(chapterId);
// Rafraîchir la collection après la recherche
await this.refreshCollectionInBackground();
return true;
} catch (error) {
console.error('Erreur lors de la recherche du chapitre:', error);
return false;
throw error;
}
}
}

View File

@@ -11,14 +11,13 @@
params: {
chapterId: chapter.id
}
}"
class="text-green-500">
}">
{{ chapter.title || 'Sans titre' }}
</router-link>
<span v-else>{{ chapter.title || 'Sans titre' }}</span>
</td>
<td class="px-4 py-2 flex justify-end gap-2">
<button v-if="!chapter.isAvailable" @click="handleSearch" class="text-gray-500 hover:text-green-500">
<button v-if="!chapter.isAvailable" @click="handleSearch" :class="buttonClass">
<MagnifyingGlassIcon class="h-5 w-5" />
</button>
<button v-else @click="handleDelete" class="text-gray-500 hover:text-green-500">
@@ -35,7 +34,8 @@
</template>
<script setup>
import { MagnifyingGlassIcon, ArrowDownTrayIcon, XMarkIcon, TrashIcon } from '@heroicons/vue/24/solid';
import { ArrowDownTrayIcon, MagnifyingGlassIcon, TrashIcon, XMarkIcon } from '@heroicons/vue/24/solid';
import { computed, ref, watch } from 'vue';
import { useMangaStore } from '../../application/store/mangaStore';
const props = defineProps({
@@ -50,11 +50,38 @@
});
const store = useMangaStore();
const isLoading = ref(false);
const buttonClass = computed(() => {
return isLoading.value ? 'text-yellow-500 cursor-wait' : 'text-gray-500 hover:text-green-500';
});
// Surveiller les changements d'état du chapitre
watch(
() => props.chapter.isAvailable,
(newValue, oldValue) => {
console.log(
`MangaChapter: État du chapitre ${props.chapter.number} (ID: ${props.chapter.id}) modifié - ${oldValue} => ${newValue}`
);
// Si le chapitre devient disponible, on arrête le chargement
if (newValue === true) {
isLoading.value = false;
}
}
);
const handleSearch = async () => {
try {
console.log(`MangaChapter: Recherche du chapitre ${props.chapter.number} (ID: ${props.chapter.id})`);
// Montrer l'indicateur de chargement
isLoading.value = true;
// Lancer la recherche du chapitre - L'UI sera mise à jour par l'événement Mercure
await store.searchChapter(props.chapter.id);
} catch (error) {
// En cas d'erreur, on arrête le chargement
isLoading.value = false;
console.error('Erreur lors de la recherche du chapitre:', error);
}
};

View File

@@ -32,8 +32,14 @@
<!-- Actions du volume -->
<div class="flex space-x-2 text-xl text-bold">
<button class="w-8 text-center" @click="handleSearch">
<MagnifyingGlassIcon class="h-6 w-6 text-gray-500 hover:text-green-500" />
<button
class="w-8 text-center"
@click="handleSearch"
:class="{
'text-yellow-500 cursor-wait': isSearching,
'text-gray-500 hover:text-green-500': !isSearching
}">
<MagnifyingGlassIcon class="h-6 w-6" />
</button>
<button class="w-8 text-center" @click="handleDownload">
<ArrowDownTrayIcon class="h-6 w-6 text-gray-500 hover:text-green-500" />
@@ -54,16 +60,16 @@
</template>
<script setup>
import { ref } from 'vue';
import {
ArrowDownTrayIcon,
BookmarkIcon,
ChevronUpIcon,
ChevronDownIcon,
MagnifyingGlassIcon,
ArrowDownTrayIcon
ChevronUpIcon,
MagnifyingGlassIcon
} from '@heroicons/vue/24/outline';
import MangaChapterList from './MangaChapterList.vue';
import { ref } from 'vue';
import { useMangaStore } from '../../application/store/mangaStore';
import MangaChapterList from './MangaChapterList.vue';
const props = defineProps({
volume: {
@@ -82,14 +88,48 @@
const store = useMangaStore();
const isOpen = ref(props.isOpen);
const isSearching = ref(false);
const toggleVolume = () => {
isOpen.value = !isOpen.value;
};
const handleSearch = async () => {
// TODO: Implémenter la recherche du volume
console.log('Recherche du volume:', props.volume.number);
if (isSearching.value) return; // Éviter les clicks multiples
try {
isSearching.value = true;
console.log(
`Recherche du volume ${props.volume.number} - Lancement du scraping de ${props.volume.chapters.length} chapitres`
);
// Récupérer tous les chapitres non disponibles du volume
const chaptersToSearch = props.volume.chapters
.filter(chapter => !chapter.isAvailable)
.map(chapter => chapter.id);
if (chaptersToSearch.length === 0) {
console.log('Tous les chapitres sont déjà disponibles !');
isSearching.value = false;
return;
}
console.log(`Chapitres à scraper: ${chaptersToSearch.length}`);
// Lancer le scraping de chaque chapitre non disponible en séquentiel
for (const chapterId of chaptersToSearch) {
console.log(`Scraping du chapitre ${chapterId}...`);
await store.searchChapter(chapterId);
// Petite pause entre chaque requête pour éviter de surcharger le serveur
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log(`Scraping des chapitres du volume ${props.volume.number} terminé`);
} catch (error) {
console.error(`Erreur lors du scraping du volume ${props.volume.number}:`, error);
} finally {
isSearching.value = false;
}
};
const handleDownload = async () => {

View File

@@ -0,0 +1,134 @@
<template>
<!-- Composant invisible qui écoute les événements Mercure -->
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch, inject } from 'vue';
import { useMangaStore } from '../../application/store/mangaStore';
import { useQueryClient } from '@tanstack/vue-query';
const props = defineProps({
mangaId: {
type: String,
required: true
}
});
const store = useMangaStore();
const eventSource = ref(null);
// On récupère le client de requête pour invalider le cache
const queryClient = useQueryClient();
const setupMercureEventSource = () => {
if (eventSource.value) {
eventSource.value.close();
}
// Créer les topics à écouter
const topics = [`manga/${props.mangaId}/chapters`, 'scraping/status'];
// Construire l'URL du hub Mercure avec les topics
const mercureHubUrl = new URL('/.well-known/mercure', window.location.origin);
topics.forEach(topic => {
mercureHubUrl.searchParams.append('topic', topic);
});
console.log(`MercureListener: Abonnement aux topics pour manga ${props.mangaId}`);
console.log(`MercureListener: URL Mercure - ${mercureHubUrl.toString()}`);
// Créer la source d'événements
eventSource.value = new EventSource(mercureHubUrl, { withCredentials: true });
// Définir les gestionnaires d'événements
eventSource.value.onmessage = event => {
try {
console.log('MercureListener: Événement reçu', event.data);
const data = JSON.parse(event.data);
handleMercureEvent(data);
} catch (error) {
console.error("Erreur lors du traitement de l'événement Mercure:", error);
}
};
eventSource.value.onerror = error => {
console.error('Erreur de connexion à Mercure:', error);
// Tenter de reconnecter après un délai
setTimeout(() => {
if (eventSource.value) {
setupMercureEventSource();
}
}, 5000);
};
};
const handleMercureEvent = data => {
if (!data || !data.type) {
console.warn('MercureListener: Événement sans type reçu', data);
return;
}
switch (data.type) {
case 'chapter.scraped':
console.log(`MercureListener: Chapitre ${data.chapterNumber} scrappé avec succès!`, data);
// Vérifier que l'ID du chapitre est présent et au bon format
if (!data.chapterId) {
console.error("MercureListener: ID du chapitre manquant dans l'événement", data);
return;
}
// Mettre à jour l'état du chapitre dans le store
try {
// Mettre à jour le store Pinia
store.updateChapterAvailability(data.chapterId, true);
console.log(
`MercureListener: Chapitre ${data.chapterNumber} (ID: ${data.chapterId}) marqué comme disponible`
);
// Invalider le cache des requêtes pour forcer un rechargement frais
console.log(`MercureListener: Invalidation du cache pour les chapitres du manga ${props.mangaId}`);
queryClient.invalidateQueries(['manga', ref(props.mangaId), 'chapters']);
// Force le rechargement des chapitres via le store
setTimeout(() => {
console.log('MercureListener: Rechargement forcé des chapitres via le store');
store.loadChapters(props.mangaId);
}, 100);
} catch (error) {
console.error('MercureListener: Erreur lors de la mise à jour du chapitre', error);
}
break;
case 'chapter.scraping.failed':
console.error(`MercureListener: Échec du scraping du chapitre ${data.chapterNumber}:`, data.reason);
break;
default:
console.log('MercureListener: Événement Mercure non géré:', data);
}
};
watch(
() => props.mangaId,
(newMangaId, oldMangaId) => {
console.log(`MercureListener: MangaId changé de ${oldMangaId} à ${newMangaId}`);
if (newMangaId) {
setupMercureEventSource();
} else if (eventSource.value) {
eventSource.value.close();
eventSource.value = null;
}
}
);
onMounted(() => {
setupMercureEventSource();
});
onBeforeUnmount(() => {
if (eventSource.value) {
eventSource.value.close();
eventSource.value = null;
}
});
</script>

View File

@@ -11,11 +11,25 @@ export function useMangaChapters(mangaId) {
if (!mangaId.value) {
return Promise.resolve([]); // Retourne un tableau vide si pas d'ID
}
console.log(`useMangaChapters: Chargement des chapitres pour le manga ${mangaId.value}`);
const response = await mangaRepository.getChapters(mangaId.value);
// Log pour déboguer
console.log(`useMangaChapters: ${response.items?.length || 0} chapitres chargés`);
// Assure de toujours retourner un tableau
return Array.isArray(response) ? response : response?.items ?? [];
},
enabled: computed(() => !!mangaId.value)
// Refresh toutes les 30 secondes en arrière-plan
refetchInterval: 30000,
// S'assurer que si le composant est visible à nouveau, on récupère les données fraîches
refetchOnWindowFocus: true,
// Query activée uniquement si mangaId est défini
enabled: computed(() => !!mangaId.value),
// Options pour conserver les données entre les requêtes
staleTime: 60000, // Considère les données comme "périmées" après 1 minute
cacheTime: 5 * 60 * 1000 // Garde les données en cache pendant 5 minutes
});
// Retourne le résultat de useQuery (contenant data, isLoading, etc.)

View File

@@ -1,18 +1,39 @@
import { computed } from 'vue';
import { computed, watch } from 'vue';
import { useMangaStore } from '../../application/store/mangaStore';
import { useMangaChapters } from './useMangaChapters'; // Importe le composable des chapitres
export function useMangaVolumes(mangaId) {
// Récupération du store pour avoir accès aux chapitres mis à jour en temps réel
const mangaStore = useMangaStore();
// Utilise le composable des chapitres pour récupérer les données brutes et les états
const { data: rawChaptersData, isLoading, isFetching, error, status } = useMangaChapters(mangaId);
const { data: rawChaptersData, isLoading, isFetching, error, status, refetch } = useMangaChapters(mangaId);
// Fonction pour forcer le rechargement des données
const refresh = () => {
console.log('useMangaVolumes: Rechargement forcé des chapitres');
refetch();
};
// Surveiller les changements dans le store pour les chapitres du manga actuel
watch(
() => mangaStore.mangaChapters[mangaId.value],
() => {
console.log('useMangaVolumes: Changement détecté dans les chapitres du store');
refresh();
},
{ deep: true }
);
// Calcule les volumes à partir des données des chapitres
const volumes = computed(() => {
console.log('useMangaVolumes: Recalcul des volumes');
const chaptersArray = rawChaptersData.value || []; // Utilise la data retournée par useMangaChapters
if (chaptersArray.length === 0) return [];
const volumeMap = new Map();
chaptersArray.forEach(chapter => {
const volumeNumber = chapter.volume || 'Unknown';
const volumeNumber = chapter.volume || '00';
if (!volumeMap.has(volumeNumber)) {
volumeMap.set(volumeNumber, {
number: volumeNumber,
@@ -33,8 +54,13 @@ export function useMangaVolumes(mangaId) {
});
return Array.from(volumeMap.values()).sort((a, b) => {
const numA = a.number === 'Unknown' ? -Infinity : Number(a.number);
const numB = b.number === 'Unknown' ? -Infinity : Number(b.number);
// Cas spécial pour le volume 00, qui doit apparaître en premier
if (a.number === '00') return -1;
if (b.number === '00') return 1;
// Pour tous les autres volumes, tri décroissant
const numA = Number(a.number);
const numB = Number(b.number);
return numB - numA;
});
});
@@ -45,7 +71,8 @@ export function useMangaVolumes(mangaId) {
isLoading, // L'état de chargement initial des chapitres
isFetching, // L'état de rafraîchissement des chapitres
error, // L'erreur potentielle lors du fetch des chapitres
status // L'état global ('pending', 'error', 'success')
status, // L'état global ('pending', 'error', 'success')
refresh
// On pourrait aussi retourner rawChaptersData si nécessaire ailleurs
};
}

View File

@@ -4,6 +4,9 @@
</div>
<div v-else-if="currentManga" class="relative">
<!-- Composant invisible qui écoute les mises à jour Mercure -->
<MercureListener :manga-id="mangaId" />
<Toolbar :config="toolbarConfig" />
<div v-if="isRefreshingDetails" class="absolute top-2 right-2 text-gray-500">
@@ -31,7 +34,7 @@
</template>
<script setup>
import { computed, onUnmounted, watch } from 'vue';
import { computed, onUnmounted, watch, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import {
ArrowPathIcon,
@@ -49,6 +52,7 @@
import MangaHeader from '../components/MangaHeader.vue';
import MangaVolumeList from '../components/MangaVolumeList.vue';
import MercureListener from '../components/MercureListener.vue';
import { useMangaStore } from '../../application/store/mangaStore';
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
@@ -72,6 +76,17 @@
error: errorVolumes
} = useMangaVolumes(mangaId);
// Charger les chapitres dans le store quand le manga est chargé
watch(
mangaId,
newId => {
if (newId) {
mangaStore.loadChapters(newId);
}
},
{ immediate: true }
);
const toolbarConfig = computed(() => ({
leftSection: [
{

View File

@@ -29,6 +29,7 @@ readonly class FetchMangaChaptersHandler
$limit = 500;
$hasMore = true;
$chaptersByNumber = [];
$chapterLanguages = []; // Pour stocker la langue de chaque chapitre
$chapterNumbers = [];
while ($hasMore) {
@@ -41,27 +42,40 @@ readonly class FetchMangaChaptersHandler
foreach ($feed['data'] as $chapterData) {
$chapterNumber = (float) $chapterData['attributes']['chapter'];
$language = $chapterData['attributes']['translatedLanguage'];
$title = $chapterData['attributes']['title'];
// On ne traite que les chapitres en français ou en anglais
// Pour les langues autres que français et anglais, on utilise un titre générique
if (!in_array($language, ['fr', 'en'])) {
continue;
$title = "Chapter {$chapterNumber}";
}
// Si le chapitre n'existe pas encore ou si c'est une version française
if (!isset($chaptersByNumber[$chapterNumber]) || $language === 'fr') {
$chapter = new Chapter(
// Définir les règles de priorité des langues (fr > en > autres)
$shouldReplaceChapter = false;
if (!isset($chaptersByNumber[$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[$chapterNumber] !== 'fr') {
// L'anglais est prioritaire sur les autres langues, sauf le français
$shouldReplaceChapter = true;
}
if ($shouldReplaceChapter) {
$chaptersByNumber[$chapterNumber] = new Chapter(
new ChapterId((string) Uuid::uuid4()),
$manga->getId()->getValue(),
$chapterNumber,
$chapterData['attributes']['title'],
$title,
isset($chapterData['attributes']['volume']) ? (int) $chapterData['attributes']['volume'] : null,
true,
false,
new \DateTimeImmutable()
);
$chaptersByNumber[$chapterNumber] = $chapter;
$chapterNumbers[] = $chapterNumber;
$chapterLanguages[$chapterNumber] = $language;
}
}
@@ -69,6 +83,9 @@ readonly class FetchMangaChaptersHandler
$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(),
@@ -82,4 +99,56 @@ readonly class FetchMangaChaptersHandler
}
}
}
/**
* 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
*/
private function harmonizeVolumes(array &$chaptersByNumber): void
{
// Trie les chapitres par numéro pour faciliter la recherche des adjacents
ksort($chaptersByNumber);
$chapterNumbers = array_keys($chaptersByNumber);
$count = count($chapterNumbers);
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) {
// On doit créer un nouveau chapitre car les objets sont immuables
$chaptersByNumber[$currentChapterNum] = new Chapter(
id: new ChapterId($currentChapter->getId()),
mangaId: $currentChapter->getMangaId(),
number: $currentChapter->getNumber(),
title: $currentChapter->getTitle(),
volume: $newVolume, // On met à jour le volume
isVisible: $currentChapter->isVisible(),
isAvailable: $currentChapter->isAvailable(),
createdAt: $currentChapter->getCreatedAt()
);
}
}
}
}

View File

@@ -107,7 +107,7 @@ class MangadexClient implements MangadexClientInterface
{
return $this->get('/manga/' . $mangaId . '/feed', [
'limit' => $limit,
'translatedLanguage' => ['en', 'fr'],
// 'translatedLanguage' => ['en'],
'order' => ['chapter' => $order],
'offset' => $offset,
]);

View File

@@ -69,6 +69,10 @@ readonly class ScrapeChapterHandler
$chapter->chapterNumber,
$source->getId()->getValue()
);
// Ajout de l'ID du chapitre dans le contexte du job
$job->context['chapterId'] = $command->chapterId;
$job->start();
$this->jobRepository->save($job);

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Domain\Scraping\Infrastructure\EventSubscriber;
use App\Domain\Scraping\Domain\Event\ChapterScraped;
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
class ScrapingEventSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly HubInterface $hub,
private readonly ChapterRepositoryInterface $chapterRepository,
private readonly JobRepositoryInterface $jobRepository,
private readonly LoggerInterface $logger
) {
}
public static function getSubscribedEvents(): array
{
return [
// Les événements sont capturés via le système de message handlers
];
}
#[AsMessageHandler]
public function onChapterScraped(ChapterScraped $event): void
{
$jobId = $event->getJobId();
$this->logger->info('ChapterScraped reçu pour le job: ' . $jobId);
$job = $this->jobRepository->get($jobId);
if (!$job) {
$this->logger->warning('Job non trouvé pour l\'ID: ' . $jobId);
return;
}
// Récupérer le chapitre associé au job
$chapterId = $job->context['chapterId'] ?? null;
$this->logger->info('ChapterId extrait du job: ' . $chapterId);
$chapter = $this->chapterRepository->getById($chapterId);
if (!$chapter) {
$this->logger->warning('Chapitre non trouvé pour l\'ID: ' . $chapterId);
return;
}
$this->logger->info('Chapitre trouvé - ID: ' . $chapter->id . ', MangaId: ' . $chapter->mangaId . ', Number: ' . $chapter->chapterNumber);
// Préparer les données à envoyer au front
$data = [
'type' => 'chapter.scraped',
'chapterId' => $chapter->id,
'mangaId' => $chapter->mangaId,
'chapterNumber' => $chapter->chapterNumber,
'isAvailable' => true,
'timestamp' => (new \DateTimeImmutable())->format('c')
];
$this->logger->info('Données préparées pour Mercure: ' . json_encode($data));
// Publier une mise à jour sur le hub Mercure
$topics = [
'manga/chapter/' . $chapter->id, // Topic spécifique au chapitre
'manga/' . $chapter->mangaId . '/chapters', // Topic pour tous les chapitres d'un manga
'scraping/status' // Topic général pour les événements de scraping
];
$this->logger->info('Topics Mercure: ' . implode(', ', $topics));
$update = new Update($topics, json_encode($data));
$this->hub->publish($update);
$this->logger->info('Mise à jour publiée sur Mercure');
}
#[AsMessageHandler]
public function onChapterScrapingFailed(ChapterScrapingFailed $event): void
{
$this->logger->info('ChapterScrapingFailed reçu pour mangaId: ' . $event->getMangaId() . ', chapter: ' . $event->getChapterNumber());
// Préparer les données à envoyer au front
$data = [
'type' => 'chapter.scraping.failed',
'mangaId' => $event->getMangaId(),
'chapterNumber' => $event->getChapterNumber(),
'reason' => $event->getReason(),
'timestamp' => (new \DateTimeImmutable())->format('c')
];
$this->logger->info('Données préparées pour Mercure: ' . json_encode($data));
// Publier une mise à jour sur le hub Mercure
$topics = [
'manga/' . $event->getMangaId() . '/chapters', // Topic pour tous les chapitres d'un manga
'scraping/status' // Topic général pour les événements de scraping
];
$this->logger->info('Topics Mercure: ' . implode(', ', $topics));
$update = new Update($topics, json_encode($data));
$this->hub->publish($update);
$this->logger->info('Mise à jour publiée sur Mercure');
}
}