feat: ajout de la gestion des sources préférées pour les mangas, incluant la récupération et la configuration des sources via l'API, ainsi que l'intégration d'une modale pour l'interface utilisateur.
This commit is contained in:
parent
15d92d1aff
commit
75f8e1686c
@@ -141,4 +141,36 @@ export class ApiMangaRepository {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getPreferredSources(mangaId) {
|
||||
try {
|
||||
const response = await fetch(`/api/mangas/${mangaId}/preferred-sources`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch preferred sources');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setPreferredSources(mangaId, sourceIds) {
|
||||
try {
|
||||
const response = await fetch(`/api/mangas/${mangaId}/preferred-sources`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ sourceIds })
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to set preferred sources');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<TransitionRoot as="template" :show="isOpen">
|
||||
<Dialog as="div" class="relative z-50" @close="closeModal">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</TransitionChild>
|
||||
|
||||
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
||||
<div>
|
||||
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-blue-100">
|
||||
<Cog6ToothIcon class="h-6 w-6 text-blue-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<DialogTitle as="h3" class="text-base font-semibold leading-6 text-gray-900">
|
||||
Sources préférées
|
||||
</DialogTitle>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
Configurez l'ordre de priorité des sources pour ce manga. Les sources en haut de la liste seront privilégiées.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoading" class="mt-5 flex justify-center items-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div v-else-if="error" class="mt-5 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{{ error.message || 'Une erreur est survenue lors du chargement des sources.' }}
|
||||
</div>
|
||||
|
||||
<!-- Sources list -->
|
||||
<div v-else class="mt-5">
|
||||
<div v-if="localSources.length === 0" class="text-center py-8 text-gray-500">
|
||||
Aucune source disponible
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="(source, index) in localSources"
|
||||
:key="source.id"
|
||||
class="flex items-center p-3 bg-gray-50 rounded-lg border border-gray-200"
|
||||
>
|
||||
<div class="flex items-center space-x-2 mr-3">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50"
|
||||
:disabled="index === 0"
|
||||
@click="moveUp(index)"
|
||||
>
|
||||
<ChevronUpIcon class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50"
|
||||
:disabled="index === localSources.length - 1"
|
||||
@click="moveDown(index)"
|
||||
>
|
||||
<ChevronDownIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-gray-900">{{ source.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ source.description || source.baseUrl }}</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400">
|
||||
Priorité {{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 sm:col-start-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="isSaving || isLoading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
<div v-if="isSaving" class="flex items-center">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Sauvegarde...
|
||||
</div>
|
||||
<span v-else>Sauvegarder</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:col-start-1 sm:mt-0"
|
||||
@click="closeModal"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
|
||||
import { ChevronDownIcon, ChevronUpIcon, Cog6ToothIcon } from '@heroicons/vue/24/outline';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
sources: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'save']);
|
||||
|
||||
// Copie locale des sources pour le drag & drop
|
||||
const localSources = ref([]);
|
||||
|
||||
// Watcher pour mettre à jour la copie locale quand les props changent
|
||||
watch(
|
||||
() => props.sources,
|
||||
(newSources) => {
|
||||
localSources.value = [...newSources];
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const closeModal = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const moveUp = (index) => {
|
||||
if (index > 0) {
|
||||
const sources = [...localSources.value];
|
||||
[sources[index - 1], sources[index]] = [sources[index], sources[index - 1]];
|
||||
localSources.value = sources;
|
||||
}
|
||||
};
|
||||
|
||||
const moveDown = (index) => {
|
||||
if (index < localSources.value.length - 1) {
|
||||
const sources = [...localSources.value];
|
||||
[sources[index], sources[index + 1]] = [sources[index + 1], sources[index]];
|
||||
localSources.value = sources;
|
||||
}
|
||||
};
|
||||
|
||||
const saveChanges = () => {
|
||||
// Extraire seulement les IDs dans l'ordre actuel
|
||||
const sourceIds = localSources.value.map(source => source.id);
|
||||
emit('save', sourceIds);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||
import { computed } from 'vue';
|
||||
import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
|
||||
|
||||
export function useMangaPreferredSources(mangaId) {
|
||||
const mangaRepository = new ApiMangaRepository();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Query pour récupérer les sources préférées
|
||||
const preferredSourcesQuery = useQuery({
|
||||
queryKey: ['manga', mangaId, 'preferred-sources'],
|
||||
queryFn: () => {
|
||||
if (!mangaId.value) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return mangaRepository.getPreferredSources(mangaId.value);
|
||||
},
|
||||
enabled: computed(() => !!mangaId.value)
|
||||
});
|
||||
|
||||
// Mutation pour sauvegarder les sources préférées
|
||||
const setPreferredSourcesMutation = useMutation({
|
||||
mutationFn: ({ sourceIds }) => {
|
||||
return mangaRepository.setPreferredSources(mangaId.value, sourceIds);
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalider le cache pour refaire la requête de récupération
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['manga', mangaId.value, 'preferred-sources']
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const sources = computed(() => preferredSourcesQuery.data.value?.sources || []);
|
||||
const hasPreferredSources = computed(() => preferredSourcesQuery.data.value?.hasPreferredSources || false);
|
||||
const isLoading = computed(() => preferredSourcesQuery.isLoading.value);
|
||||
const error = computed(() => preferredSourcesQuery.error.value);
|
||||
const isSaving = computed(() => setPreferredSourcesMutation.isPending.value);
|
||||
|
||||
const savePreferredSources = (sourceIds) => {
|
||||
return setPreferredSourcesMutation.mutate({ sourceIds });
|
||||
};
|
||||
|
||||
return {
|
||||
sources,
|
||||
hasPreferredSources,
|
||||
isLoading,
|
||||
error,
|
||||
isSaving,
|
||||
savePreferredSources
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,17 @@
|
||||
</div>
|
||||
<MangaVolumeList v-else :volumes="volumes" :manga-slug="currentManga.slug" />
|
||||
</div>
|
||||
|
||||
<!-- Modale des sources préférées -->
|
||||
<MangaPreferredSourcesModal
|
||||
:is-open="isPreferredSourcesModalOpen"
|
||||
:sources="preferredSources"
|
||||
:is-loading="isLoadingSources"
|
||||
:error="sourcesError"
|
||||
:is-saving="isSavingSources"
|
||||
@close="closePreferredSourcesModal"
|
||||
@save="savePreferredSources"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isLoadingDetails" class="flex justify-center items-center h-64">
|
||||
@@ -44,13 +55,15 @@
|
||||
TrashIcon,
|
||||
WrenchIcon
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import { computed, onUnmounted, watch } from 'vue';
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { useMangaDetails } from '../composables/useMangaDetails';
|
||||
import { useMangaPreferredSources } from '../composables/useMangaPreferredSources';
|
||||
import { useMangaVolumes } from '../composables/useMangaVolumes';
|
||||
|
||||
import MangaHeader from '../components/MangaHeader.vue';
|
||||
import MangaPreferredSourcesModal from '../components/MangaPreferredSourcesModal.vue';
|
||||
import MangaVolumeList from '../components/MangaVolumeList.vue';
|
||||
import MercureListener from '../components/MercureListener.vue';
|
||||
|
||||
@@ -62,6 +75,9 @@
|
||||
|
||||
const mangaId = computed(() => route.params.id || null);
|
||||
|
||||
// État de la modale
|
||||
const isPreferredSourcesModalOpen = ref(false);
|
||||
|
||||
const {
|
||||
data: currentManga,
|
||||
isLoading: isLoadingDetails,
|
||||
@@ -76,6 +92,14 @@
|
||||
error: errorVolumes
|
||||
} = useMangaVolumes(mangaId);
|
||||
|
||||
const {
|
||||
sources: preferredSources,
|
||||
isLoading: isLoadingSources,
|
||||
error: sourcesError,
|
||||
isSaving: isSavingSources,
|
||||
savePreferredSources: saveSourcesOrder
|
||||
} = useMangaPreferredSources(mangaId);
|
||||
|
||||
// Charger les chapitres dans le store quand le manga est chargé
|
||||
watch(
|
||||
mangaId,
|
||||
@@ -87,6 +111,23 @@
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const openPreferredSourcesModal = () => {
|
||||
isPreferredSourcesModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closePreferredSourcesModal = () => {
|
||||
isPreferredSourcesModalOpen.value = false;
|
||||
};
|
||||
|
||||
const savePreferredSources = async (sourceIds) => {
|
||||
try {
|
||||
await saveSourcesOrder(sourceIds);
|
||||
closePreferredSourcesModal();
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde des sources préférées:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const toolbarConfig = computed(() => ({
|
||||
leftSection: [
|
||||
{
|
||||
@@ -111,7 +152,7 @@
|
||||
icon: Cog6ToothIcon,
|
||||
label: 'Preferred Sources',
|
||||
type: 'button',
|
||||
onClick: () => console.log('Preferred Sources')
|
||||
onClick: openPreferredSourcesModal
|
||||
}
|
||||
],
|
||||
rightSection: [
|
||||
|
||||
@@ -26,6 +26,7 @@ api_platform:
|
||||
mapping:
|
||||
paths:
|
||||
- '%kernel.project_dir%/src/Domain/Scraping/Infrastructure/ApiPlatform/Dto'
|
||||
- '%kernel.project_dir%/src/Domain/Scraping/Infrastructure/ApiPlatform/Resource'
|
||||
- '%kernel.project_dir%/src/Domain/Manga/Infrastructure/ApiPlatform/Resource'
|
||||
- '%kernel.project_dir%/src/Domain/Reader/Infrastructure/ApiPlatform/Resource'
|
||||
- '%kernel.project_dir%/src/Domain/Shared/Infrastructure/ApiPlatform/Resource'
|
||||
|
||||
@@ -1167,6 +1167,184 @@
|
||||
},
|
||||
"parameters": []
|
||||
},
|
||||
"/api/mangas/{id}/preferred-sources": {
|
||||
"get": {
|
||||
"operationId": "api_mangas_idpreferred-sources_get",
|
||||
"tags": [
|
||||
"Scraping"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Sources r\u00e9cup\u00e9r\u00e9es avec succ\u00e8s",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mangaId": {
|
||||
"type": "string"
|
||||
},
|
||||
"hasPreferredSources": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"mangaId": "1",
|
||||
"hasPreferredSources": true,
|
||||
"sources": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "MangaDex",
|
||||
"baseUrl": "https://mangadex.org",
|
||||
"description": "Source principale",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "MangaKakalot",
|
||||
"baseUrl": "https://mangakakalot.com",
|
||||
"description": "Source secondaire",
|
||||
"isActive": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found"
|
||||
}
|
||||
},
|
||||
"summary": "R\u00e9cup\u00e9rer les sources pr\u00e9f\u00e9r\u00e9es d'un manga",
|
||||
"description": "Retourne les sources pr\u00e9f\u00e9r\u00e9es configur\u00e9es pour un manga, ou toutes les sources disponibles si aucune pr\u00e9f\u00e9rence d\u00e9finie",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "GetMangaPreferredSourcesResource identifier",
|
||||
"required": true,
|
||||
"deprecated": false,
|
||||
"allowEmptyValue": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"style": "simple",
|
||||
"explode": false,
|
||||
"allowReserved": false
|
||||
}
|
||||
],
|
||||
"deprecated": false
|
||||
},
|
||||
"post": {
|
||||
"operationId": "api_mangas_idpreferred-sources_post",
|
||||
"tags": [
|
||||
"Scraping"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Scraping resource created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping"
|
||||
}
|
||||
},
|
||||
"application/ld+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping.jsonld"
|
||||
}
|
||||
},
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping"
|
||||
}
|
||||
},
|
||||
"application/hal+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Scraping.jsonhal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input"
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable entity"
|
||||
}
|
||||
},
|
||||
"summary": "Configurer les sources pr\u00e9f\u00e9r\u00e9es d'un manga",
|
||||
"description": "D\u00e9finit l'ordre de priorit\u00e9 des sources de scraping pour un manga. Format attendu: {\"sourceIds\": [\"source1\", \"source2\"]}",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "SetMangaPreferredSourcesResource identifier",
|
||||
"required": true,
|
||||
"deprecated": false,
|
||||
"allowEmptyValue": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"style": "simple",
|
||||
"explode": false,
|
||||
"allowReserved": false
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sourceIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"sourceIds": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
"deprecated": false
|
||||
},
|
||||
"parameters": []
|
||||
},
|
||||
"/api/reader/chapter/{chapterId}": {
|
||||
"get": {
|
||||
"operationId": "api_readerchapter_chapterId_get",
|
||||
@@ -3355,6 +3533,112 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scraping.MangaPreferredSourcesDetail": {
|
||||
"type": "object",
|
||||
"description": "R\u00e9cup\u00e9rer les sources pr\u00e9f\u00e9r\u00e9es d'un manga ou toutes les sources si aucune pr\u00e9f\u00e9rence",
|
||||
"deprecated": false,
|
||||
"properties": {
|
||||
"mangaId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hasPreferredSources": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scraping.MangaPreferredSourcesDetail.jsonhal": {
|
||||
"type": "object",
|
||||
"description": "R\u00e9cup\u00e9rer les sources pr\u00e9f\u00e9r\u00e9es d'un manga ou toutes les sources si aucune pr\u00e9f\u00e9rence",
|
||||
"deprecated": false,
|
||||
"properties": {
|
||||
"_links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"self": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"href": {
|
||||
"type": "string",
|
||||
"format": "iri-reference"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mangaId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hasPreferredSources": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scraping.MangaPreferredSourcesDetail.jsonld": {
|
||||
"type": "object",
|
||||
"description": "R\u00e9cup\u00e9rer les sources pr\u00e9f\u00e9r\u00e9es d'un manga ou toutes les sources si aucune pr\u00e9f\u00e9rence",
|
||||
"deprecated": false,
|
||||
"properties": {
|
||||
"@context": {
|
||||
"readOnly": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@vocab": {
|
||||
"type": "string"
|
||||
},
|
||||
"hydra": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"http://www.w3.org/ns/hydra/core#"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"@vocab",
|
||||
"hydra"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"@id": {
|
||||
"readOnly": true,
|
||||
"type": "string"
|
||||
},
|
||||
"@type": {
|
||||
"readOnly": true,
|
||||
"type": "string"
|
||||
},
|
||||
"mangaId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hasPreferredSources": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scraping.jsonhal": {
|
||||
"type": "object",
|
||||
"description": "",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Application\Command;
|
||||
|
||||
readonly class SetMangaPreferredSources
|
||||
{
|
||||
public function __construct(
|
||||
public string $mangaId,
|
||||
public array $sourceIds
|
||||
) {
|
||||
if (empty($mangaId)) {
|
||||
throw new \InvalidArgumentException('Manga ID cannot be empty');
|
||||
}
|
||||
|
||||
if (!is_array($sourceIds)) {
|
||||
throw new \InvalidArgumentException('Source IDs must be an array');
|
||||
}
|
||||
|
||||
// Valider que tous les éléments sont des strings
|
||||
foreach ($sourceIds as $sourceId) {
|
||||
if (!is_string($sourceId) && !is_int($sourceId)) {
|
||||
throw new \InvalidArgumentException('All source IDs must be strings or integers');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ readonly class ScrapeChapterHandler
|
||||
|
||||
// 3. Détermination des sources à utiliser
|
||||
$sources = $this->getSourcesToTry($manga);
|
||||
dd($sources);
|
||||
if (empty($sources)) {
|
||||
throw new \InvalidArgumentException("No sources available for scraping");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Application\CommandHandler;
|
||||
|
||||
use App\Domain\Scraping\Application\Command\SetMangaPreferredSources;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\SourceRepositoryInterface;
|
||||
|
||||
readonly class SetMangaPreferredSourcesHandler
|
||||
{
|
||||
public function __construct(
|
||||
private MangaRepositoryInterface $mangaRepository,
|
||||
private SourceRepositoryInterface $sourceRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(SetMangaPreferredSources $command): void
|
||||
{
|
||||
// 1. Vérifier que le manga existe
|
||||
$manga = $this->mangaRepository->getById($command->mangaId);
|
||||
if (!$manga) {
|
||||
throw new \InvalidArgumentException("Manga not found with ID: {$command->mangaId}");
|
||||
}
|
||||
|
||||
// 2. Si pas de sources spécifiées, supprimer les sources préférées
|
||||
if (empty($command->sourceIds)) {
|
||||
$this->mangaRepository->updatePreferredSources($command->mangaId, []);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Valider que toutes les sources existent et sont actives
|
||||
if (!$this->sourceRepository->validateSourcesExist($command->sourceIds)) {
|
||||
throw new \InvalidArgumentException('One or more sources do not exist or are not active');
|
||||
}
|
||||
|
||||
// 4. Sauvegarder les sources préférées dans l'ordre fourni
|
||||
$this->mangaRepository->updatePreferredSources($command->mangaId, $command->sourceIds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Application\Query;
|
||||
|
||||
readonly class GetMangaPreferredSources
|
||||
{
|
||||
public function __construct(
|
||||
public string $mangaId
|
||||
) {
|
||||
if (empty($mangaId)) {
|
||||
throw new \InvalidArgumentException('Manga ID cannot be empty');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Application\QueryHandler;
|
||||
|
||||
use App\Domain\Scraping\Application\Query\GetMangaPreferredSources;
|
||||
use App\Domain\Scraping\Application\Response\GetMangaPreferredSourcesResponse;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\SourceRepositoryInterface;
|
||||
|
||||
readonly class GetMangaPreferredSourcesHandler
|
||||
{
|
||||
public function __construct(
|
||||
private MangaRepositoryInterface $mangaRepository,
|
||||
private SourceRepositoryInterface $sourceRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(GetMangaPreferredSources $query): GetMangaPreferredSourcesResponse
|
||||
{
|
||||
// 1. Vérifier que le manga existe
|
||||
$manga = $this->mangaRepository->getById($query->mangaId);
|
||||
if (!$manga) {
|
||||
throw new \InvalidArgumentException("Manga not found with ID: {$query->mangaId}");
|
||||
}
|
||||
|
||||
// 2. Récupérer toutes les sources actives
|
||||
$allActiveSources = $this->sourceRepository->getAllActive();
|
||||
|
||||
// 3. Si le manga a des sources préférées, les organiser par ordre de préférence
|
||||
if ($manga->hasPreferredSources()) {
|
||||
$preferredSourceIds = $manga->getPreferredSources();
|
||||
|
||||
// Séparer les sources préférées et les autres
|
||||
$preferredSources = [];
|
||||
$otherSources = [];
|
||||
|
||||
// D'abord, ajouter les sources préférées dans l'ordre de préférence
|
||||
foreach ($preferredSourceIds as $preferredId) {
|
||||
foreach ($allActiveSources as $source) {
|
||||
if ($source->getId()->getValue() === $preferredId) {
|
||||
$preferredSources[] = $source;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensuite, ajouter les autres sources actives qui ne sont pas dans les préférées
|
||||
foreach ($allActiveSources as $source) {
|
||||
if (!in_array($source->getId()->getValue(), $preferredSourceIds, true)) {
|
||||
$otherSources[] = $source;
|
||||
}
|
||||
}
|
||||
|
||||
// Combiner les sources : préférées en premier, puis les autres
|
||||
$orderedSources = array_merge($preferredSources, $otherSources);
|
||||
|
||||
return new GetMangaPreferredSourcesResponse(
|
||||
$query->mangaId,
|
||||
$orderedSources,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Si pas de sources préférées, retourner toutes les sources actives
|
||||
return new GetMangaPreferredSourcesResponse(
|
||||
$query->mangaId,
|
||||
$allActiveSources,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Application\Response;
|
||||
|
||||
readonly class GetMangaPreferredSourcesResponse
|
||||
{
|
||||
/**
|
||||
* @param string $mangaId
|
||||
* @param array $sources Array of Source objects
|
||||
* @param bool $hasPreferredSources Whether sources are preferred or all available sources
|
||||
*/
|
||||
public function __construct(
|
||||
public string $mangaId,
|
||||
public array $sources,
|
||||
public bool $hasPreferredSources
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,13 @@ use App\Domain\Scraping\Domain\Model\Manga;
|
||||
interface MangaRepositoryInterface
|
||||
{
|
||||
public function getById(string $id): ?Manga;
|
||||
|
||||
/**
|
||||
* Update the preferred sources for a manga in the specified order
|
||||
*
|
||||
* @param string $mangaId
|
||||
* @param array $sourceIds Array of source IDs in preferred order
|
||||
* @return void
|
||||
*/
|
||||
public function updatePreferredSources(string $mangaId, array $sourceIds): void;
|
||||
}
|
||||
|
||||
@@ -12,4 +12,27 @@ interface SourceRepositoryInterface
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getAll(): array;
|
||||
|
||||
/**
|
||||
* Validate that all source IDs exist and are active
|
||||
*
|
||||
* @param array $sourceIds
|
||||
* @return bool
|
||||
*/
|
||||
public function validateSourcesExist(array $sourceIds): bool;
|
||||
|
||||
/**
|
||||
* Get sources by their IDs in the same order as provided
|
||||
*
|
||||
* @param array $sourceIds
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getByIds(array $sourceIds): array;
|
||||
|
||||
/**
|
||||
* Get all active sources
|
||||
*
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getAllActive(): array;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\ApiPlatform\Dto;
|
||||
|
||||
readonly class MangaPreferredSourcesDetail
|
||||
{
|
||||
public function __construct(
|
||||
public string $mangaId,
|
||||
public array $sources,
|
||||
public bool $hasPreferredSources
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||
use ApiPlatform\OpenApi\Model\Response;
|
||||
use App\Domain\Scraping\Infrastructure\ApiPlatform\Dto\MangaPreferredSourcesDetail;
|
||||
use App\Domain\Scraping\Infrastructure\ApiPlatform\State\Provider\GetMangaPreferredSourcesStateProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Scraping',
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/mangas/{id}/preferred-sources',
|
||||
provider: GetMangaPreferredSourcesStateProvider::class,
|
||||
output: MangaPreferredSourcesDetail::class,
|
||||
description: 'Récupérer les sources préférées d\'un manga ou toutes les sources si aucune préférence',
|
||||
openapi: new OpenApiOperation(
|
||||
summary: 'Récupérer les sources préférées d\'un manga',
|
||||
description: 'Retourne les sources préférées configurées pour un manga, ou toutes les sources disponibles si aucune préférence définie',
|
||||
responses: [
|
||||
'200' => new Response(
|
||||
description: 'Sources récupérées avec succès',
|
||||
content: new \ArrayObject([
|
||||
'application/json' => [
|
||||
'schema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'mangaId' => ['type' => 'string'],
|
||||
'hasPreferredSources' => ['type' => 'boolean'],
|
||||
'sources' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'baseUrl' => ['type' => 'string'],
|
||||
'description' => ['type' => 'string'],
|
||||
'isActive' => ['type' => 'boolean']
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'example' => [
|
||||
'mangaId' => '1',
|
||||
'hasPreferredSources' => true,
|
||||
'sources' => [
|
||||
[
|
||||
'id' => '1',
|
||||
'name' => 'MangaDex',
|
||||
'baseUrl' => 'https://mangadex.org',
|
||||
'description' => 'Source principale',
|
||||
'isActive' => true
|
||||
],
|
||||
[
|
||||
'id' => '2',
|
||||
'name' => 'MangaKakalot',
|
||||
'baseUrl' => 'https://mangakakalot.com',
|
||||
'description' => 'Source secondaire',
|
||||
'isActive' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)]
|
||||
class GetMangaPreferredSourcesResource
|
||||
{
|
||||
public function __construct(
|
||||
public string $id
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||
use ApiPlatform\OpenApi\Model\RequestBody;
|
||||
use App\Domain\Scraping\Infrastructure\ApiPlatform\State\Processor\SetMangaPreferredSourcesStateProcessor;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Scraping',
|
||||
operations: [
|
||||
new Post(
|
||||
uriTemplate: '/mangas/{id}/preferred-sources',
|
||||
processor: SetMangaPreferredSourcesStateProcessor::class,
|
||||
status: 200,
|
||||
description: 'Définir les sources préférées d\'un manga dans l\'ordre de priorité',
|
||||
openapi: new OpenApiOperation(
|
||||
summary: 'Configurer les sources préférées d\'un manga',
|
||||
description: 'Définit l\'ordre de priorité des sources de scraping pour un manga. Format attendu: {"sourceIds": ["source1", "source2"]}',
|
||||
requestBody: new RequestBody(
|
||||
content: new \ArrayObject([
|
||||
'application/json' => [
|
||||
'schema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'sourceIds' => [
|
||||
'type' => 'array',
|
||||
'items' => ['type' => 'string']
|
||||
]
|
||||
]
|
||||
],
|
||||
'example' => [
|
||||
'sourceIds' => ['1', '2']
|
||||
]
|
||||
]
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)]
|
||||
class SetMangaPreferredSourcesResource
|
||||
{
|
||||
public function __construct(
|
||||
#[Assert\NotNull]
|
||||
#[Assert\All([
|
||||
new Assert\Type('string')
|
||||
])]
|
||||
public array $sourceIds = []
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\ApiPlatform\State\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Domain\Scraping\Application\Command\SetMangaPreferredSources;
|
||||
use App\Domain\Scraping\Infrastructure\ApiPlatform\Resource\SetMangaPreferredSourcesResource;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
final class SetMangaPreferredSourcesStateProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MessageBusInterface $commandBus
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SetMangaPreferredSourcesResource $data
|
||||
*/
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
$mangaId = $uriVariables['id'] ?? null;
|
||||
|
||||
if (!$mangaId) {
|
||||
throw new \InvalidArgumentException('Manga ID is required');
|
||||
}
|
||||
|
||||
$this->commandBus->dispatch(
|
||||
new SetMangaPreferredSources(
|
||||
(string) $mangaId,
|
||||
$data->sourceIds
|
||||
)
|
||||
);
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\ApiPlatform\State\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Domain\Scraping\Application\Query\GetMangaPreferredSources;
|
||||
use App\Domain\Scraping\Application\QueryHandler\GetMangaPreferredSourcesHandler;
|
||||
use App\Domain\Scraping\Infrastructure\ApiPlatform\Dto\MangaPreferredSourcesDetail;
|
||||
|
||||
final class GetMangaPreferredSourcesStateProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GetMangaPreferredSourcesHandler $queryHandler
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): MangaPreferredSourcesDetail
|
||||
{
|
||||
$mangaId = $uriVariables['id'] ?? null;
|
||||
|
||||
if (!$mangaId) {
|
||||
throw new \InvalidArgumentException('Manga ID is required');
|
||||
}
|
||||
|
||||
$query = new GetMangaPreferredSources((string) $mangaId);
|
||||
$response = $this->queryHandler->handle($query);
|
||||
|
||||
// Convertir les objets Source en array pour l'API
|
||||
$sourcesData = array_map(function ($source) {
|
||||
return [
|
||||
'id' => $source->getId()->getValue(),
|
||||
'name' => $source->getName(),
|
||||
'baseUrl' => $source->getBaseUrl(),
|
||||
'description' => $source->getDescription(),
|
||||
'isActive' => $source->isActive()
|
||||
];
|
||||
}, $response->sources);
|
||||
|
||||
return new MangaPreferredSourcesDetail(
|
||||
$response->mangaId,
|
||||
$sourcesData,
|
||||
$response->hasPreferredSources
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\CommandHandler;
|
||||
|
||||
use App\Domain\Scraping\Application\Command\SetMangaPreferredSources;
|
||||
use App\Domain\Scraping\Application\CommandHandler\SetMangaPreferredSourcesHandler;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
class SymfonySetMangaPreferredSourcesHandler
|
||||
{
|
||||
public function __construct(
|
||||
private SetMangaPreferredSourcesHandler $handler
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(SetMangaPreferredSources $command): void
|
||||
{
|
||||
$this->handler->handle($command);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Domain\Scraping\Infrastructure\Persistence;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Scraping\Domain\Model\Manga;
|
||||
use App\Entity\Manga as EntityManga;
|
||||
use App\Entity\ContentSource;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
||||
@@ -26,17 +27,49 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
||||
// Récupération des sources préférées
|
||||
$preferredSourceIds = [];
|
||||
foreach ($mangaEntity->getPreferredSources() as $source) {
|
||||
$preferredSourceIds[] = $source->getId();
|
||||
$preferredSourceIds[] = (string) $source->getId();
|
||||
}
|
||||
|
||||
return new Manga(
|
||||
$mangaEntity->getId(),
|
||||
(string) $mangaEntity->getId(),
|
||||
$mangaEntity->getTitle(),
|
||||
$mangaEntity->getSlug(),
|
||||
$mangaEntity->getDescription() ?? '',
|
||||
$mangaEntity->getAuthor() ?? '',
|
||||
$mangaEntity->getPublicationYear() ?? '',
|
||||
(string) ($mangaEntity->getPublicationYear() ?? ''),
|
||||
$preferredSourceIds,
|
||||
);
|
||||
}
|
||||
|
||||
public function updatePreferredSources(string $mangaId, array $sourceIds): void
|
||||
{
|
||||
/** @var EntityManga|null $mangaEntity */
|
||||
$mangaEntity = $this->entityManager->getRepository(EntityManga::class)->find($mangaId);
|
||||
|
||||
if (!$mangaEntity) {
|
||||
throw new \InvalidArgumentException("Manga not found with ID: {$mangaId}");
|
||||
}
|
||||
|
||||
// Si pas de sources, vider les sources préférées
|
||||
if (empty($sourceIds)) {
|
||||
$mangaEntity->setPreferredSources([]);
|
||||
$this->entityManager->flush();
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les sources existantes
|
||||
$sources = $this->entityManager->getRepository(ContentSource::class)->findBy(['id' => $sourceIds]);
|
||||
|
||||
// Maintenir l'ordre exact des sources comme dans l'ancien controller
|
||||
$orderedPreferredSources = array_map(
|
||||
fn ($id) => current(array_filter($sources, fn ($s) => $s->getId() == $id)),
|
||||
$sourceIds
|
||||
);
|
||||
|
||||
// Filtrer les sources nulles (au cas où certaines n'existeraient pas)
|
||||
$validSources = array_filter($orderedPreferredSources);
|
||||
|
||||
$mangaEntity->setPreferredSources($validSources);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,34 +17,16 @@ readonly class LegacySourceRepository implements SourceRepositoryInterface
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SourceNotFoundException
|
||||
*/
|
||||
public function getById(string $id): Source
|
||||
public function getById(string $id): ?Source
|
||||
{
|
||||
/** @var ContentSource|null $source */
|
||||
$source = $this->entityManager->getRepository(ContentSource::class)->find($id);
|
||||
|
||||
if (!$source) {
|
||||
throw new SourceNotFoundException("Source not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Source(
|
||||
id: new SourceId($source->getId()),
|
||||
name: $source->getCleanBaseUrl(),
|
||||
description: 'Legacy Source: ' . $source->getBaseUrl(),
|
||||
baseUrl: $source->getBaseUrl(),
|
||||
scrappingParameters: [
|
||||
'imageSelector' => $source->getImageSelector(),
|
||||
'nextPageSelector' => $source->getNextPageSelector(),
|
||||
'chapterUrlFormat' => $source->getChapterUrlFormat(),
|
||||
'scrapingType' => $source->getScrapingType(),
|
||||
'chapterSelector' => $source->getChapterSelector()
|
||||
],
|
||||
isActive: true,
|
||||
createdAt: new DateTimeImmutable(),
|
||||
updatedAt: new DateTimeImmutable()
|
||||
);
|
||||
return $this->convertEntityToModel($source);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,13 +45,70 @@ readonly class LegacySourceRepository implements SourceRepositoryInterface
|
||||
return $sources;
|
||||
}
|
||||
|
||||
public function validateSourcesExist(array $sourceIds): bool
|
||||
{
|
||||
if (empty($sourceIds)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compter le nombre de sources qui existent réellement
|
||||
$existingCount = $this->entityManager->getRepository(ContentSource::class)
|
||||
->createQueryBuilder('c')
|
||||
->select('COUNT(c.id)')
|
||||
->where('c.id IN (:ids)')
|
||||
->setParameter('ids', $sourceIds)
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
|
||||
// Vérifier que toutes les sources demandées existent
|
||||
return $existingCount === count($sourceIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getByIds(array $sourceIds): array
|
||||
{
|
||||
if (empty($sourceIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var ContentSource[] $sourceEntities */
|
||||
$sourceEntities = $this->entityManager->getRepository(ContentSource::class)->findBy(['id' => $sourceIds]);
|
||||
|
||||
// Maintenir l'ordre des IDs fournis
|
||||
$sourcesMap = [];
|
||||
foreach ($sourceEntities as $sourceEntity) {
|
||||
$sourcesMap[$sourceEntity->getId()] = $this->convertEntityToModel($sourceEntity);
|
||||
}
|
||||
|
||||
$orderedSources = [];
|
||||
foreach ($sourceIds as $sourceId) {
|
||||
if (isset($sourcesMap[$sourceId])) {
|
||||
$orderedSources[] = $sourcesMap[$sourceId];
|
||||
}
|
||||
}
|
||||
|
||||
return $orderedSources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getAllActive(): array
|
||||
{
|
||||
// Pour le moment, toutes les sources sont considérées comme actives
|
||||
// dans le système legacy
|
||||
return $this->getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une entité ContentSource en modèle Source
|
||||
*/
|
||||
private function convertEntityToModel(ContentSource $source): Source
|
||||
{
|
||||
return new Source(
|
||||
id: new SourceId($source->getId()),
|
||||
id: new SourceId((string) $source->getId()),
|
||||
name: $source->getCleanBaseUrl(),
|
||||
description: 'Legacy Source: ' . $source->getBaseUrl(),
|
||||
baseUrl: $source->getBaseUrl(),
|
||||
|
||||
Reference in New Issue
Block a user