From 00d63dffebfbe1e33cae4f7548801bd71418b26e Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Tue, 22 Jul 2025 15:57:25 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20ajout=20de=20la=20fonctionnalit=C3=A9?= =?UTF-8?q?=20de=20monitoring=20des=20mangas,=20incluant=20l'activation=20?= =?UTF-8?q?et=20la=20d=C3=A9sactivation=20du=20suivi,=20la=20synchronisati?= =?UTF-8?q?on=20des=20chapitres,=20et=20la=20mise=20=C3=A0=20jour=20de=20l?= =?UTF-8?q?'API=20pour=20g=C3=A9rer=20ces=20nouvelles=20actions.=20Cr?= =?UTF-8?q?=C3=A9ation=20de=20nouveaux=20composants=20Vue=20pour=20le=20ra?= =?UTF-8?q?fra=C3=AEchissement=20des=20chapitres=20et=20l'affichage=20des?= =?UTF-8?q?=20notifications.=20Int=C3=A9gration=20de=20tests=20unitaires?= =?UTF-8?q?=20pour=20valider=20le=20bon=20fonctionnement=20de=20ces=20fonc?= =?UTF-8?q?tionnalit=C3=A9s.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manga/application/store/mangaStore.js | 30 +- .../infrastructure/api/apiMangaRepository.js | 56 ++++ .../composables/useMangaMonitoring.js | 67 ++++ .../composables/useMangaRefresh.js | 48 +++ .../manga/presentation/pages/MangaDetails.vue | 67 +++- .../components/ui/NotificationToast.vue | 103 ++++++ .../shared/composables/useNotifications.js | 65 ++++ config/packages/messenger.yaml | 2 + migrations/Version20250716105928.php | 33 ++ public/api-docs.json | 295 +++++++++++++++++- .../Command/CheckMonitoredMangas.php | 12 + .../Command/FetchMangaChapters.php | 4 +- .../Command/RefreshMangaChapters.php | 12 + .../Command/ToggleMangaMonitoring.php | 13 + .../CheckMonitoredMangasHandler.php | 32 ++ .../FetchMangaChaptersHandler.php | 236 +------------- .../RefreshMangaChaptersHandler.php | 43 +++ .../ToggleMangaMonitoringHandler.php | 31 ++ .../Application/Query/MonitoringCriteria.php | 13 + .../QueryHandler/GetMangaByIdHandler.php | 3 +- .../Application/Response/MangaResponse.php | 3 +- .../Repository/MangaRepositoryInterface.php | 9 +- ...ChapterSynchronizationServiceInterface.php | 14 + .../Domain/Event/ChapterReadyForScraping.php | 12 + src/Domain/Manga/Domain/Model/Manga.php | 39 ++- .../Domain/Model/ValueObject/ChapterId.php | 2 +- .../Model/ValueObject/MonitoringStatus.php | 30 ++ .../ApiPlatform/Dto/MangaDetail.php | 5 +- .../Resource/FetchMangaChaptersResource.php | 6 +- .../Resource/RefreshMangaChaptersResource.php | 43 +++ .../Resource/ToggleMonitoringResource.php | 70 +++++ .../Processor/FetchMangaChaptersProcessor.php | 3 +- .../RefreshMangaChaptersProcessor.php | 38 +++ .../Processor/ToggleMonitoringProcessor.php | 48 +++ .../State/Provider/GetMangaStateProvider.php | 3 +- .../SymfonyRefreshMangaChaptersHandler.php | 20 ++ .../Persistence/LegacyMangaRepository.php | 42 ++- .../Scheduler/MonitoringSchedule.php | 29 ++ .../MangadxChapterSynchronizationService.php | 231 ++++++++++++++ .../EventListener/AutoScrapingListener.php | 21 ++ src/Entity/Manga.php | 15 + .../Manga/Adapter/InMemoryMangaRepository.php | 26 +- .../Command/ToggleMangaMonitoringTest.php | 46 +++ .../ToggleMangaMonitoringHandlerTest.php | 166 ++++++++++ tests/Feature/Manga/ToggleMonitoringTest.php | 199 ++++++++++++ 45 files changed, 2021 insertions(+), 264 deletions(-) create mode 100644 assets/vue/app/domain/manga/presentation/composables/useMangaMonitoring.js create mode 100644 assets/vue/app/domain/manga/presentation/composables/useMangaRefresh.js create mode 100644 assets/vue/app/shared/components/ui/NotificationToast.vue create mode 100644 assets/vue/app/shared/composables/useNotifications.js create mode 100644 migrations/Version20250716105928.php create mode 100644 src/Domain/Manga/Application/Command/CheckMonitoredMangas.php create mode 100644 src/Domain/Manga/Application/Command/RefreshMangaChapters.php create mode 100644 src/Domain/Manga/Application/Command/ToggleMangaMonitoring.php create mode 100644 src/Domain/Manga/Application/CommandHandler/CheckMonitoredMangasHandler.php create mode 100644 src/Domain/Manga/Application/CommandHandler/RefreshMangaChaptersHandler.php create mode 100644 src/Domain/Manga/Application/CommandHandler/ToggleMangaMonitoringHandler.php create mode 100644 src/Domain/Manga/Application/Query/MonitoringCriteria.php create mode 100644 src/Domain/Manga/Domain/Contract/Service/ChapterSynchronizationServiceInterface.php create mode 100644 src/Domain/Manga/Domain/Event/ChapterReadyForScraping.php create mode 100644 src/Domain/Manga/Domain/Model/ValueObject/MonitoringStatus.php create mode 100644 src/Domain/Manga/Infrastructure/ApiPlatform/Resource/RefreshMangaChaptersResource.php create mode 100644 src/Domain/Manga/Infrastructure/ApiPlatform/Resource/ToggleMonitoringResource.php create mode 100644 src/Domain/Manga/Infrastructure/ApiPlatform/State/Processor/RefreshMangaChaptersProcessor.php create mode 100644 src/Domain/Manga/Infrastructure/ApiPlatform/State/Processor/ToggleMonitoringProcessor.php create mode 100644 src/Domain/Manga/Infrastructure/CommandHandler/SymfonyRefreshMangaChaptersHandler.php create mode 100644 src/Domain/Manga/Infrastructure/Scheduler/MonitoringSchedule.php create mode 100644 src/Domain/Manga/Infrastructure/Service/MangadxChapterSynchronizationService.php create mode 100644 src/Domain/Scraping/Infrastructure/EventListener/AutoScrapingListener.php create mode 100644 tests/Domain/Manga/Application/Command/ToggleMangaMonitoringTest.php create mode 100644 tests/Domain/Manga/Application/CommandHandler/ToggleMangaMonitoringHandlerTest.php create mode 100644 tests/Feature/Manga/ToggleMonitoringTest.php diff --git a/assets/vue/app/domain/manga/application/store/mangaStore.js b/assets/vue/app/domain/manga/application/store/mangaStore.js index 2cb9c19..156c78a 100644 --- a/assets/vue/app/domain/manga/application/store/mangaStore.js +++ b/assets/vue/app/domain/manga/application/store/mangaStore.js @@ -195,11 +195,37 @@ export const useMangaStore = defineStore('manga', { this.chaptersError = null; try { + // Déclenche la récupération initiale des chapitres depuis la source externe await mangaRepository.fetchMangaChapters(mangaId); - this.mangaChapters[mangaId] = chaptersData; - console.log('Chapitres récupérés avec succès'); + console.log('Récupération initiale des chapitres déclenchée avec succès'); + + // Note: Les nouveaux chapitres seront disponibles après traitement asynchrone + // Le MercureListener se chargera de mettre à jour l'interface } catch (err) { this.chaptersError = err.message; + console.error('Erreur lors de la récupération des chapitres:', err); + throw err; + } finally { + this.loadingChapters = false; + } + }, + + async refreshMangaChapters(mangaId) { + if (this.loadingChapters) return; + this.loadingChapters = true; + this.chaptersError = null; + + try { + // Déclenche la synchronisation incrémentale avec scraping automatique + await mangaRepository.refreshMangaChapters(mangaId); + console.log('Synchronisation incrémentale déclenchée avec succès'); + + // Note: Les chapitres mis à jour seront disponibles après traitement asynchrone + // Le MercureListener se chargera de mettre à jour l'interface + } catch (err) { + this.chaptersError = err.message; + console.error('Erreur lors de la synchronisation des chapitres:', err); + throw err; } finally { this.loadingChapters = false; } diff --git a/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js b/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js index dfe22d1..4eac98c 100644 --- a/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js +++ b/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js @@ -142,6 +142,26 @@ export class ApiMangaRepository { } } + async refreshMangaChapters(mangaId) { + try { + const response = await fetch(`/api/manga/${mangaId}/chapters/refresh`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({}) + }); + if (!response.ok) { + throw new Error('Failed to refresh manga chapters'); + } + // L'endpoint retourne 202 (Accepted), pas de contenu JSON à parser + return true; + } catch (error) { + console.error('API Error:', error); + throw error; + } + } + async searchChapter(chapterId) { try { const response = await fetch('/api/scraping/chapters', { @@ -321,4 +341,40 @@ export class ApiMangaRepository { throw error; } } + + async toggleMonitoring(mangaId, enabled) { + try { + const response = await fetch(`/api/manga/${mangaId}/monitoring/toggle`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ enabled }) + }); + + if (!response.ok) { + // Tenter de récupérer le message d'erreur détaillé de l'API + let errorMessage = 'Failed to toggle monitoring'; + try { + const errorData = await response.json(); + if (errorData.detail) { + errorMessage = errorData.detail; + } else if (errorData.message) { + errorMessage = errorData.message; + } else if (errorData.violations && errorData.violations.length > 0) { + errorMessage = errorData.violations.map(v => v.message).join(', '); + } + } catch (parseError) { + console.warn('Could not parse error response:', parseError); + } + throw new Error(errorMessage); + } + + // L'endpoint retourne un statut 204 (No Content), donc pas de données à retourner + return true; + } catch (error) { + console.error('API Error:', error); + throw error; + } + } } diff --git a/assets/vue/app/domain/manga/presentation/composables/useMangaMonitoring.js b/assets/vue/app/domain/manga/presentation/composables/useMangaMonitoring.js new file mode 100644 index 0000000..2935e7d --- /dev/null +++ b/assets/vue/app/domain/manga/presentation/composables/useMangaMonitoring.js @@ -0,0 +1,67 @@ +import { ref } from 'vue'; +import { useNotifications } from '../../../../shared/composables/useNotifications'; +import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository'; + +const mangaRepository = new ApiMangaRepository(); + +export function useMangaMonitoring() { + const { showSuccess, showError } = useNotifications(); + + const isToggling = ref(false); + const toggleError = ref(null); + + const toggleMonitoring = async (mangaId, enabled) => { + if (isToggling.value || !mangaId) return; + + isToggling.value = true; + toggleError.value = null; + + try { + console.log(`${enabled ? 'Activation' : 'Désactivation'} du monitoring pour le manga ${mangaId}`); + + await mangaRepository.toggleMonitoring(mangaId, enabled); + + const message = enabled + ? 'Monitoring activé avec succès. Vous recevrez les nouveaux chapitres automatiquement.' + : 'Monitoring désactivé avec succès. Les nouveaux chapitres ne seront plus téléchargés automatiquement.'; + + showSuccess(message); + + console.log(`Monitoring ${enabled ? 'activé' : 'désactivé'} avec succès`); + return true; + } catch (error) { + console.error('Erreur lors du changement de monitoring:', error); + toggleError.value = error.message || 'Erreur lors du changement de monitoring'; + + const errorMessage = enabled + ? `Erreur lors de l'activation du monitoring: ${error.message || 'Une erreur inattendue est survenue'}` + : `Erreur lors de la désactivation du monitoring: ${error.message || 'Une erreur inattendue est survenue'}`; + + showError(errorMessage); + throw error; + } finally { + isToggling.value = false; + } + }; + + const enableMonitoring = async (mangaId) => { + return await toggleMonitoring(mangaId, true); + }; + + const disableMonitoring = async (mangaId) => { + return await toggleMonitoring(mangaId, false); + }; + + const clearError = () => { + toggleError.value = null; + }; + + return { + isToggling, + toggleError, + toggleMonitoring, + enableMonitoring, + disableMonitoring, + clearError + }; +} diff --git a/assets/vue/app/domain/manga/presentation/composables/useMangaRefresh.js b/assets/vue/app/domain/manga/presentation/composables/useMangaRefresh.js new file mode 100644 index 0000000..93944c5 --- /dev/null +++ b/assets/vue/app/domain/manga/presentation/composables/useMangaRefresh.js @@ -0,0 +1,48 @@ +import { ref } from 'vue'; +import { useNotifications } from '../../../../shared/composables/useNotifications'; +import { useMangaStore } from '../../application/store/mangaStore'; + +export function useMangaRefresh() { + const mangaStore = useMangaStore(); + const { showSuccess, showError } = useNotifications(); + + const isRefreshing = ref(false); + const refreshError = ref(null); + + const refreshMetadata = async (mangaId) => { + if (isRefreshing.value || !mangaId) return; + + isRefreshing.value = true; + refreshError.value = null; + + try { + console.log(`Début du refresh des métadonnées pour le manga ${mangaId}`); + + // Appel à l'endpoint de refresh des chapitres + await mangaStore.refreshMangaChapters(mangaId); + + showSuccess('Refresh des métadonnées lancé avec succès. Les nouveaux chapitres apparaîtront sous peu.'); + + console.log('Refresh des métadonnées déclenché avec succès'); + return true; + } catch (error) { + console.error('Erreur lors du refresh des métadonnées:', error); + refreshError.value = error.message || 'Erreur lors du refresh des métadonnées'; + showError(`Erreur lors du refresh: ${error.message || 'Une erreur inattendue est survenue'}`); + throw error; + } finally { + isRefreshing.value = false; + } + }; + + const clearError = () => { + refreshError.value = null; + }; + + return { + isRefreshing, + refreshError, + refreshMetadata, + clearError + }; +} diff --git a/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue b/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue index bc00818..c09c20a 100644 --- a/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue +++ b/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue @@ -1,5 +1,8 @@