diff --git a/assets/app.js b/assets/app.js deleted file mode 100644 index edc04bf..0000000 --- a/assets/app.js +++ /dev/null @@ -1,17 +0,0 @@ -import './bootstrap.js'; - -import '@fortawesome/fontawesome-free/js/all.js'; -/* - * Welcome to your app's main JavaScript file! - * - * We recommend including the built version of this JavaScript file - * (and its CSS file) in your base layout (base.html.twig). - */ - -// any CSS you import will output into a single css file (app.css in this case) -import './styles/app.scss'; - -// start the Stimulus application -import './bootstrap'; - -// La ligne registerReactControllerComponents a déjà été commentée diff --git a/assets/bootstrap.js b/assets/bootstrap.js deleted file mode 100644 index cfa3957..0000000 --- a/assets/bootstrap.js +++ /dev/null @@ -1,35 +0,0 @@ -import { startStimulusApp } from '@symfony/stimulus-bridge'; - -// Registers Stimulus controllers from controllers.json and in the controllers/ directory -export const app = startStimulusApp(require.context( - '@symfony/stimulus-bridge/lazy-controller-loader!./controllers', - true, - /\.[jt]sx?$/ -)); - -// register any custom, 3rd party controllers here -// app.register('some_controller_name', SomeImportedController); - -//DEBUG TURBO -// import * as Turbo from "@hotwired/turbo" -// -// Turbo.session.drive = false -// Turbo.start() -// -// // Écouteurs existants -// document.addEventListener("turbo:before-stream-render", (event) => { -// console.log("Before stream render", event.target); -// }); -// -// document.addEventListener("turbo:stream-render", (event) => { -// console.log("Stream rendered", event.target); -// }); -// -// // Nouvel écouteur pour les événements de création -// document.addEventListener("turbo:before-fetch-request", (event) => { -// console.log("Before fetch request", event.detail.fetchOptions); -// }); -// -// document.addEventListener("turbo:before-fetch-response", (event) => { -// console.log("Before fetch response", event.detail.fetchResponse); -// }); diff --git a/assets/controllers.json b/assets/controllers.json deleted file mode 100644 index 3dfe062..0000000 --- a/assets/controllers.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "controllers": { - "@symfony/ux-live-component": { - "live": { - "enabled": true, - "fetch": "eager", - "autoimport": { - "@symfony/ux-live-component/dist/live.min.css": true - } - } - }, - "@symfony/ux-react": { - "react": { - "enabled": true, - "fetch": "eager" - } - }, - "@symfony/ux-turbo": { - "turbo-core": { - "enabled": true, - "fetch": "eager" - }, - "mercure-turbo-stream": { - "enabled": true, - "fetch": "eager" - } - } - }, - "entrypoints": [] -} diff --git a/assets/controllers/activity_controller.js b/assets/controllers/activity_controller.js deleted file mode 100644 index 259a8f7..0000000 --- a/assets/controllers/activity_controller.js +++ /dev/null @@ -1,54 +0,0 @@ -import {Controller} from '@hotwired/stimulus'; - -/* -* The following line makes this controller "lazy": it won't be downloaded until needed -* See https://github.com/symfony/stimulus-bridge#lazy-controllers -*/ -/* stimulusFetch: 'lazy' */ -export default class extends Controller { - static targets = ['activity'] - - // ... - async connect() { - try { - const response = await fetch(`/activity/status`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest' - } - }); - - const data = await response.json(); - // Handle the response data as needed - this.activityTarget.innerHTML = data.length; - if (data.length > 0) { - this.activityTarget.classList.remove('hidden'); - } - } catch (error) { - console.error('Error:', error); - } - - - const mercureHubUrl = 'https://mangarr.test.nestor-server.fr/.well-known/mercure'; - const eventSource = new EventSource(`${mercureHubUrl}?topic=activity`, {withCredentials: true}); - - eventSource.onmessage = (event) => { - const data = JSON.parse(event.data); - if (data.processing !== undefined && data.pending !== undefined) { - let totalActivities = data.processing.length + data.pending.length; - this.activityTarget.innerHTML = totalActivities; - if (totalActivities > 0) { - this.activityTarget.classList.remove('hidden'); - }else if (totalActivities === 0) { - this.activityTarget.classList.add('hidden'); - } - } - }; - - eventSource - .onerror = (event) => { - console.error('EventSource failed:', event); - }; - } -} diff --git a/assets/controllers/addmanga_controller.js b/assets/controllers/addmanga_controller.js deleted file mode 100644 index b78e347..0000000 --- a/assets/controllers/addmanga_controller.js +++ /dev/null @@ -1,14 +0,0 @@ -// assets/controllers/addmanga_controller.js -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - static values = { - index: Number - } - - openModal(event) { - event.preventDefault() - const openEvent = new CustomEvent(`openAddMangaModal${this.indexValue}`) - document.dispatchEvent(openEvent) - } -} diff --git a/assets/controllers/alert_controller.js b/assets/controllers/alert_controller.js deleted file mode 100644 index ae5b8d9..0000000 --- a/assets/controllers/alert_controller.js +++ /dev/null @@ -1,60 +0,0 @@ -import {Controller} from '@hotwired/stimulus'; - -/* -* The following line makes this controller "lazy": it won't be downloaded until needed -* See https://github.com/symfony/stimulus-bridge#lazy-controllers -*/ -/* stimulusFetch: 'lazy' */ -export default class extends Controller { - static targets = ['alert', 'icon', 'message'] - - connect() { - window.addEventListener('alert:show', this.showAlert.bind(this)); - } - - // ... - showAlert(event) { - const detail = event.detail; - const message = detail.message; - const level = detail.level; - - let alertClass = ""; - let iconClass = ""; - switch (level) { - case 'success': - alertClass = "bg-green-500"; - iconClass = "fa-circle-check"; - break; - case 'warning': - alertClass = "bg-yellow-500"; - iconClass = "fa-circle-exclamation"; - break; - case 'error': - alertClass = "bg-red-500"; - iconClass = "fa-circle-xmark"; - break; - case 'info': - default: - alertClass = "bg-blue-500"; - iconClass = "fa-circle-info"; - break; - } - - this.messageTarget.innerHTML = message; - this.alertTarget.classList.add(alertClass); - this.iconTarget.classList.add(iconClass); - this.alertTarget.style.display = "block"; - - setTimeout(() => { - this.alertTarget.style.opacity = 0; - - setTimeout(() => { - this.alertTarget.style.display = 'none'; - this.alertTarget.classList.remove(alertClass); - this.alertTarget.style.opacity = 1; - this.iconTarget.classList.remove(iconClass); - this.messageTarget.innerHTML = message; - }, 1000); - }, 3000); - } -} diff --git a/assets/controllers/chapter_progress_controller.js b/assets/controllers/chapter_progress_controller.js deleted file mode 100644 index 76b9d7a..0000000 --- a/assets/controllers/chapter_progress_controller.js +++ /dev/null @@ -1,45 +0,0 @@ -import { Controller } from '@hotwired/stimulus'; - -/* stimulusFetch: 'lazy' */ -export default class extends Controller { - static targets = ['progressBar', 'progressText'] - static values = { - chapterId: Number - } - - connect() { - this.currentPage = 0; - this.totalPages = 0; - - const mercureHubUrl = 'https://mangarr.test.nestor-server.fr/.well-known/mercure'; - this.eventSource = new EventSource(`${mercureHubUrl}?topic=activity`, {withCredentials: true}); - - this.eventSource.onmessage = this.handleMessage.bind(this); - } - - disconnect() { - if (this.eventSource) { - this.eventSource.close(); - } - } - - handleMessage(event) { - const data = JSON.parse(event.data); - if (data.status === "scrapping.progress" && data.chapterId === this.chapterIdValue) { - this.handleProgressUpdate(data); - } - } - - handleProgressUpdate(data) { - this.currentPage = data.pageIndex; - this.totalPages = data.totalPages; - - this.updateProgressBar(); - } - - updateProgressBar() { - const progress = (this.currentPage / this.totalPages) * 100; - this.progressBarTarget.style.width = `${progress}%`; - this.progressTextTarget.textContent = `${this.currentPage} / ${this.totalPages}`; - } -} diff --git a/assets/controllers/collection_controller.js b/assets/controllers/collection_controller.js deleted file mode 100644 index ac5a6a1..0000000 --- a/assets/controllers/collection_controller.js +++ /dev/null @@ -1,26 +0,0 @@ -import {Controller} from '@hotwired/stimulus'; - -/* -* The following line makes this controller "lazy": it won't be downloaded until needed -* See https://github.com/symfony/stimulus-bridge#lazy-controllers -*/ -/* stimulusFetch: 'lazy' */ -export default class extends Controller { - static targets = ['container', 'template', 'item']; - - connect() { - this.index = this.itemTargets.length; - } - - add(event) { - event.preventDefault(); - const template = this.templateTarget.innerHTML.replace(/__name__/g, this.index); - this.containerTarget.insertAdjacentHTML('beforeend', template); - this.index++; - } - - remove(event) { - event.preventDefault(); - event.target.closest('.collection-item').remove(); - } -} diff --git a/assets/controllers/download_controller.js b/assets/controllers/download_controller.js deleted file mode 100644 index 685c4b9..0000000 --- a/assets/controllers/download_controller.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Controller } from '@hotwired/stimulus'; - -/* stimulusFetch: 'lazy' */ -export default class extends Controller { - static targets = ['icon'] - static values = { - url: String - } - - connect() { - this.defaultIconClass = this.iconTarget.classList.value; - } - - async download(event) { - event.preventDefault(); - - // Change the icon to a loader - this.iconTarget.classList.remove("fa-download", "fa-search"); - this.iconTarget.classList.add("fa-spinner", "fa-spin"); - - try { - const response = await fetch(this.urlValue, { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'X-Requested-With': 'XMLHttpRequest' - } - }); - - const contentType = response.headers.get("Content-Type"); - if (contentType && contentType.includes("application/json")) { - const data = await response.json(); - if (data.error) { - this.dispatchAlert(data.error, 'error'); - } else if (data.success) { - this.dispatchAlert(data.success, 'success'); - } - } else { - // C'est un fichier à télécharger - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - const contentDisposition = response.headers.get('Content-Disposition'); - const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; - const matches = filenameRegex.exec(contentDisposition); - let filename = 'download'; - if (matches != null && matches[1]) { - filename = matches[1].replace(/['"]/g, ''); - } - a.download = filename; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - } - } finally { - // Revert the icon back to the original one - this.iconTarget.classList.value = this.defaultIconClass; - } - } - - dispatchAlert(message, level) { - const event = new CustomEvent('alert:show', { - detail: { message: message, level: level } - }); - window.dispatchEvent(event); - } -} diff --git a/assets/controllers/dropdown_controller.js b/assets/controllers/dropdown_controller.js deleted file mode 100644 index 25989ea..0000000 --- a/assets/controllers/dropdown_controller.js +++ /dev/null @@ -1,45 +0,0 @@ -// assets/controllers/dropdown_controller.js -import {Controller} from "@hotwired/stimulus" -import {useClickOutside} from "stimulus-use" - -export default class extends Controller { - static targets = ["button", "menu"] - - connect() { - useClickOutside(this) - } - - toggle(event) { - this.menuTarget.classList.toggle('hidden') - if (!this.menuTarget.classList.contains('hidden')) { - this.positionMenu() - } - } - - clickOutside(event) { - this.menuTarget.classList.add('hidden') - } - - positionMenu() { - const buttonRect = this.buttonTarget.getBoundingClientRect() - const menuRect = this.menuTarget.getBoundingClientRect() - const spaceRight = window.innerWidth - buttonRect.right - const spaceBottom = window.innerHeight - buttonRect.bottom - - if (spaceRight < menuRect.width && buttonRect.left > menuRect.width) { - this.menuTarget.style.left = 'auto' - this.menuTarget.style.right = '0' - } else { - this.menuTarget.style.left = '0' - this.menuTarget.style.right = 'auto' - } - - if (spaceBottom < menuRect.height && buttonRect.top > menuRect.height) { - this.menuTarget.style.top = 'auto' - this.menuTarget.style.bottom = '100%' - } else { - this.menuTarget.style.top = '100%' - this.menuTarget.style.bottom = 'auto' - } - } -} diff --git a/assets/controllers/hello_controller.js b/assets/controllers/hello_controller.js deleted file mode 100644 index e847027..0000000 --- a/assets/controllers/hello_controller.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Controller } from '@hotwired/stimulus'; - -/* - * This is an example Stimulus controller! - * - * Any element with a data-controller="hello" attribute will cause - * this controller to be executed. The name "hello" comes from the filename: - * hello_controller.js -> "hello" - * - * Delete this file or adapt it for your use! - */ -export default class extends Controller { - connect() { - this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js'; - } -} diff --git a/assets/controllers/import_match_controller.js b/assets/controllers/import_match_controller.js deleted file mode 100644 index e53d91d..0000000 --- a/assets/controllers/import_match_controller.js +++ /dev/null @@ -1,51 +0,0 @@ -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - static targets = ["checkbox", "modal", "modalContent"] - - toggleAllCheckboxes(event) { - this.checkboxTargets.forEach(checkbox => { - checkbox.checked = event.target.checked; - }); - } - - updateMangaInfo(event) { - const select = event.target; - const selectedOption = select.options[select.selectedIndex]; - const mangaInfo = JSON.parse(selectedOption.dataset.mangaInfo); - } - - showDetails(event) { - const fileId = event.currentTarget.dataset.fileId; - const select = document.querySelector(`select[name="manga_slug[${fileId}]"]`); - const mangaInfo = JSON.parse(select.options[select.selectedIndex].dataset.mangaInfo); - - this.modalContentTarget.innerHTML = ` -

${mangaInfo.title}

-
-

Author: ${mangaInfo.author || 'N/A'}

-

Publication Year: ${mangaInfo.publicationYear || 'N/A'}

-

Genres: ${mangaInfo.genres ? mangaInfo.genres.join(', ') : 'N/A'}

-

Description: ${this.truncate(mangaInfo.description || 'N/A', 200)}

-
- `; - - this.modalTarget.classList.remove('hidden'); - } - - closeModal() { - this.modalTarget.classList.add('hidden'); - } - - confirmSelected(event) { - const selectedFiles = this.checkboxTargets.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value); - if (selectedFiles.length === 0) { - event.preventDefault(); - alert('Veuillez sélectionner au moins un fichier à importer.'); - } - } - - truncate(str, length) { - return str.length > length ? str.substring(0, length) + '...' : str; - } -} diff --git a/assets/controllers/loading_button_controller.js b/assets/controllers/loading_button_controller.js deleted file mode 100644 index a2a3255..0000000 --- a/assets/controllers/loading_button_controller.js +++ /dev/null @@ -1,21 +0,0 @@ -// assets/controllers/loading_button_controller.js -import {Controller} from "@hotwired/stimulus" - -export default class extends Controller { - static targets = ["text", "loader"]; - static values = {form: String}; - - startLoading(event) { - event.preventDefault(); - this.textTarget.classList.add("hidden"); - this.loaderTarget.classList.remove("hidden"); - this.element.disabled = true; - - if (this.hasFormValue) { - const form = document.getElementById(this.formValue); - if (form) { - form.submit(); - } - } - } -} diff --git a/assets/controllers/menu_controller.js b/assets/controllers/menu_controller.js deleted file mode 100644 index b0bd6d2..0000000 --- a/assets/controllers/menu_controller.js +++ /dev/null @@ -1,10 +0,0 @@ -// assets/controllers/menu_controller.js -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - static targets = ["sidebar"] - - toggleMenu() { - this.sidebarTarget.classList.toggle('-translate-x-full') - } -} diff --git a/assets/controllers/mercure_controller.js b/assets/controllers/mercure_controller.js deleted file mode 100644 index 27983fb..0000000 --- a/assets/controllers/mercure_controller.js +++ /dev/null @@ -1,33 +0,0 @@ -import {Controller} from '@hotwired/stimulus'; - -/* -* The following line makes this controller "lazy": it won't be downloaded until needed -* See https://github.com/symfony/stimulus-bridge#lazy-controllers -*/ -/* stimulusFetch: 'lazy' */ -export default class extends Controller { - // ... - connect() { - const topic = this.data.get('topic'); - const mercureHubUrl = 'https://mangarr.test.nestor-server.fr/.well-known/mercure'; - const eventSource = new EventSource(`${mercureHubUrl}?topic=${topic}`, {withCredentials: true}); - - eventSource.onmessage = (event) => { - const data = JSON.parse(event.data); - console.log('Received Mercure update:', data); - - this.dispatchAlert(data.message, data.status); - }; - - eventSource.onerror = (event) => { - console.error('EventSource failed:', event); - }; - } - - dispatchAlert(message, level) { - const event = new CustomEvent('alert:show', { - detail: { message: message, level: level } - }); - window.dispatchEvent(event); - } -} diff --git a/assets/controllers/modal_controller.js b/assets/controllers/modal_controller.js deleted file mode 100644 index c8c1dac..0000000 --- a/assets/controllers/modal_controller.js +++ /dev/null @@ -1,37 +0,0 @@ -// assets/controllers/modal_controller.js -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - static targets = ["modal"] - static values = { - openTrigger: String, - closeTrigger: String - } - - connect() { - if (this.hasOpenTriggerValue) { - document.addEventListener(this.openTriggerValue, this.open.bind(this)) - } - if (this.hasCloseTriggerValue) { - document.addEventListener(this.closeTriggerValue, this.close.bind(this)) - } - } - - disconnect() { - if (this.hasOpenTriggerValue) { - document.removeEventListener(this.openTriggerValue, this.open.bind(this)) - } - if (this.hasCloseTriggerValue) { - document.removeEventListener(this.closeTriggerValue, this.close.bind(this)) - } - } - - open() { - console.log("Opening modal...") - this.modalTarget.classList.remove('hidden') - } - - close() { - this.modalTarget.classList.add('hidden') - } -} diff --git a/assets/controllers/preferred_sources_controller.js b/assets/controllers/preferred_sources_controller.js deleted file mode 100644 index 3a4c6e1..0000000 --- a/assets/controllers/preferred_sources_controller.js +++ /dev/null @@ -1,101 +0,0 @@ -// assets/controllers/preferred-sources_controller.js - -import {Controller} from "@hotwired/stimulus" -import Sortable from 'sortablejs' - -export default class extends Controller { - static targets = ["preferredList", "availableList"] - static values = { - mangaId: Number, - preferredSources: Array, - allSources: Array - } - - connect() { - this.initSortable() - } - - initSortable() { - new Sortable(this.preferredListTarget, { - animation: 150, - ghostClass: 'bg-gray-300', - onEnd: this.handleDragEnd.bind(this) - }) - } - - handleDragEnd() { - this.updatePreferredSources() - } - - addSource(event) { - const sourceId = parseInt(event.currentTarget.dataset.sourceId) - if (!this.preferredSourcesValue.includes(sourceId)) { - this.preferredSourcesValue = [...this.preferredSourcesValue, sourceId] - this.updateLists() - this.save() - } - } - - removeSource(event) { - const sourceId = parseInt(event.currentTarget.dataset.sourceId) - this.preferredSourcesValue = this.preferredSourcesValue.filter(id => id !== sourceId) - this.updateLists() - this.save() - } - - updatePreferredSources() { - this.preferredSourcesValue = Array.from(this.preferredListTarget.children).map(li => parseInt(li.dataset.id)) - this.save() - } - - updateLists() { - this.preferredListTarget.innerHTML = this.preferredSourcesValue - .map(id => this.allSourcesValue.find(s => s.id === id)) - .map(source => this.sourceTemplate(source, true)) - .join('') - - this.availableListTarget.innerHTML = this.allSourcesValue - .filter(source => !this.preferredSourcesValue.includes(source.id)) - .map(source => this.sourceTemplate(source, false)) - .join('') - - this.initSortable() - } - - sourceTemplate(source, isPreferred) { - return ` -
  • - ${source.name} - -
  • - ` - } - - async save() { - try { - const response = await fetch(`/manga/${this.mangaIdValue}/preferred-sources`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest' - }, - body: JSON.stringify({ - preferredSources: this.preferredSourcesValue - }) - }) - - if (response.ok) { - console.log('Preferred sources saved successfully') - // Optionally show a success message - } else { - console.error('Error saving preferred sources') - // Optionally show an error message - } - } catch (error) { - console.error('Error:', error) - // Optionally show an error message - } - } -} diff --git a/assets/controllers/reader_controller.js b/assets/controllers/reader_controller.js deleted file mode 100644 index 771244c..0000000 --- a/assets/controllers/reader_controller.js +++ /dev/null @@ -1,127 +0,0 @@ -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - static targets = ['pageContainer', 'currentPage', 'chapterSelect', 'readingModeButton'] - static values = { - mangaSlug: String, - chapterNumber: Number, - totalPages: Number, - currentPage: { type: Number, default: 1 }, - readingMode: { type: String, default: 'horizontal' } - } - - connect() { - this.loadChapters(); - this.loadPages(); - } - - async loadChapters() { - try { - const response = await fetch(`/api/chapters/${this.mangaSlugValue}`); - const chapters = await response.json(); - - this.chapterSelectTarget.innerHTML = chapters.map(chapter => - `` - ).join(''); - } catch (error) { - console.error('Error loading chapters:', error); - } - } - - async loadPages() { - this.pageContainerTarget.innerHTML = ''; - if (this.readingModeValue === 'horizontal') { - await this.loadPage(this.currentPageValue); - } else { - for (let i = 1; i <= this.totalPagesValue; i++) { - await this.loadPage(i, true); - } - } - } - - async loadPage(pageNumber, isVertical = false) { - const response = await fetch(`/api/read/${this.mangaSlugValue}/${this.chapterNumberValue}/${pageNumber}`); - const pageContent = await response.text(); - - const img = document.createElement('img'); - img.src = `data:image/jpeg;base64,${pageContent}`; - img.alt = `Page ${pageNumber}`; - img.classList.add('shadow-lg', 'w-full', 'h-auto'); - - if (this.readingModeValue === 'horizontal') { - img.classList.add('cursor-pointer'); - img.dataset.action = 'click->reader#pageClick'; - this.pageContainerTarget.innerHTML = ''; - } - - if (isVertical) { - img.loading = 'lazy'; - img.classList.add('mb-4'); - } - - this.pageContainerTarget.appendChild(img); - - if (!isVertical) { - this.currentPageTarget.textContent = pageNumber; - this.currentPageValue = pageNumber; - } - } - - pageClick(event) { - if (this.readingModeValue === 'horizontal') { - const pageWidth = event.target.offsetWidth; - const clickX = event.offsetX; - - if (clickX < pageWidth / 2) { - this.previousPage(); - } else { - this.nextPage(); - } - } - } - - previousPage() { - if (this.currentPageValue > 1) { - this.loadPage(this.currentPageValue - 1); - } else { - this.previousChapter(); - } - } - - nextPage() { - if (this.currentPageValue < this.totalPagesValue) { - this.loadPage(this.currentPageValue + 1); - } else { - this.nextChapter(); - } - } - - async previousChapter() { - const response = await fetch(`/api/previous-chapter/${this.mangaSlugValue}/${this.chapterNumberValue}`); - const previousChapter = await response.json(); - if (previousChapter) { - window.location.href = `/read/${this.mangaSlugValue}/${previousChapter.number}`; - } - } - - async nextChapter() { - const response = await fetch(`/api/next-chapter/${this.mangaSlugValue}/${this.chapterNumberValue}`); - const nextChapter = await response.json(); - if (nextChapter) { - window.location.href = `/read/${this.mangaSlugValue}/${nextChapter.number}`; - } - } - - changeChapter(event) { - const selectedChapterNumber = event.target.value; - window.location.href = `/read/${this.mangaSlugValue}/${selectedChapterNumber}`; - } - - toggleReadingMode() { - this.readingModeValue = this.readingModeValue === 'horizontal' ? 'vertical' : 'horizontal'; - this.readingModeButtonTarget.textContent = this.readingModeValue === 'horizontal' ? 'Passer en mode vertical' : 'Passer en mode horizontal'; - this.loadPages(); - } -} diff --git a/assets/controllers/scrapper_configure_controller.js b/assets/controllers/scrapper_configure_controller.js deleted file mode 100644 index ad157d1..0000000 --- a/assets/controllers/scrapper_configure_controller.js +++ /dev/null @@ -1,76 +0,0 @@ -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - static targets = ['form', 'testForm', 'imageSelector', 'nextPageSelector', 'testResults', 'scrapingType'] - - connect() { - } - - async saveConfiguration(event) { - event.preventDefault(); - this.formTarget.submit(); - } - - async testConfiguration(event) { - event.preventDefault(); - const formData = new FormData(this.formTarget); - const testFormData = new FormData(this.testFormTarget); - - for (let [key, value] of formData.entries()) { - const cleanKey = key.replace(/^content_source\[(.+)]$/, '$1'); - testFormData.append(`content_source[${cleanKey}]`, value); - } - - try { - const response = await fetch(this.testFormTarget.action, { - method: 'POST', - body: testFormData - }); - - const result = await response.json(); - - if (result.success) { - this.displayTestResults(result.data); - } else { - this.displayError(result.message, result.errors); - } - } catch (error) { - console.log(error) - this.displayError('An error occurred while testing the configuration'); - } - } - - displayTestResults(data) { - let html = '

    Test Results

    '; - html += '
    '; - data.forEach(page => { - html += ` -
    - Page ${page.page_number} -

    Page ${page.page_number}

    -
    - `; - }); - html += '
    '; - this.testResultsTarget.innerHTML = html; - } - - displayError(message, errors = []) { - let errorHtml = ` - '; - this.testResultsTarget.innerHTML = errorHtml; - } -} diff --git a/assets/controllers/scrapper_import_controller.js b/assets/controllers/scrapper_import_controller.js deleted file mode 100644 index 98b5adc..0000000 --- a/assets/controllers/scrapper_import_controller.js +++ /dev/null @@ -1,81 +0,0 @@ -import { Controller } from '@hotwired/stimulus'; - -/* -* The following line makes this controller "lazy": it won't be downloaded until needed -* See https://github.com/symfony/stimulus-bridge#lazy-controllers -*/ -/* stimulusFetch: 'lazy' */ -export default class extends Controller { - // ... - static targets = ["textarea", "submitButton"] - - connect() { - document.addEventListener('openImportModal', this.prepareImportModal.bind(this)); - document.addEventListener('openExportModal', this.prepareExportModal.bind(this)); - } - - disconnect() { - document.removeEventListener('openImportModal', this.prepareImportModal.bind(this)); - document.removeEventListener('openExportModal', this.prepareExportModal.bind(this)); - } - - async prepareExportModal() { - try { - const response = await fetch('/settings/export_scrappers'); - const data = await response.json(); - this.textareaTarget.value = JSON.stringify(data, null, 2); - this.submitButtonTarget.textContent = 'Copy to Clipboard'; - this.submitButtonTarget.dataset.action = 'scrapper-import#copyToClipboard'; - this.openModal('Export Scrapper Configurations'); - } catch (error) { - console.error('Error:', error); - } - } - - prepareImportModal() { - this.textareaTarget.value = ''; - this.submitButtonTarget.textContent = 'Import'; - this.submitButtonTarget.dataset.action = 'scrapper-import#submitImport'; - this.openModal('Import Scrapper Configurations'); - } - - openModal(title) { - const event = new CustomEvent('openScrapperModal', { detail: { title: title } }); - document.dispatchEvent(event); - } - - async submitImport() { - const jsonData = this.textareaTarget.value; - - try { - const response = await fetch('/settings/import_scrappers', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: jsonData - }); - - const result = await response.json(); - - if (response.ok) { - console.log(result.message); - document.dispatchEvent(new CustomEvent('closeScrapperModal')); - window.location.reload(); - } else { - console.error(result.error); - } - } catch (error) { - console.error('Error:', error); - } - } - - copyToClipboard() { - navigator.clipboard.writeText(this.textareaTarget.value).then(() => { - console.log('Copied to clipboard'); - document.dispatchEvent(new CustomEvent('closeScrapperModal')); - }, (err) => { - console.error('Could not copy text: ', err); - }); - } -} diff --git a/assets/controllers/search_controller.js b/assets/controllers/search_controller.js deleted file mode 100644 index e29e332..0000000 --- a/assets/controllers/search_controller.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Controller } from '@hotwired/stimulus'; - -/* -* The following line makes this controller "lazy": it won't be downloaded until needed -* See https://github.com/symfony/stimulus-bridge#lazy-controllers -*/ -/* stimulusFetch: 'lazy' */ -export default class extends Controller { - static targets = ['input'] - - clearSearch() { - this.inputTarget.value = ''; - this.inputTarget.focus(); - } -} diff --git a/assets/controllers/table_controller.js b/assets/controllers/table_controller.js deleted file mode 100644 index 534bdd5..0000000 --- a/assets/controllers/table_controller.js +++ /dev/null @@ -1,35 +0,0 @@ -import {Controller} from '@hotwired/stimulus'; - -/* -* The following line makes this controller "lazy": it won't be downloaded until needed -* See https://github.com/symfony/stimulus-bridge#lazy-controllers -*/ -/* stimulusFetch: 'lazy' */ -export default class extends Controller { - static targets = ["body", "toggleIcon"] - static values = { open: Boolean } - - connect() { - if (!this.openValue) { - this.close() - } - } - - toggle() { - if (this.bodyTarget.style.display === "none") { - this.open() - } else { - this.close() - } - } - - open() { - this.bodyTarget.style.display = "block" - this.toggleIconTarget.classList.replace("fa-chevron-down", "fa-chevron-up") - } - - close() { - this.bodyTarget.style.display = "none" - this.toggleIconTarget.classList.replace("fa-chevron-up", "fa-chevron-down") - } -} diff --git a/assets/controllers/toolbar_controller.js b/assets/controllers/toolbar_controller.js deleted file mode 100644 index dc068e8..0000000 --- a/assets/controllers/toolbar_controller.js +++ /dev/null @@ -1,198 +0,0 @@ -// assets/controllers/toolbar_controller.js -import { Controller } from "@hotwired/stimulus" -import { visit } from "@hotwired/turbo" - -export default class extends Controller { - static targets = ["dropdown", "icon", "text"] - static values = { - currentSort: String, - currentOrder: String, - currentStatus: String, - mangaId: Number - } - - connect() { - window.addEventListener('alert:show', this.stopLoading.bind(this)); - } - - stopLoading(event) { - if(event.currentTarget.dataset !== undefined){ - this.iconTarget.classList.remove('fa-spin'); - } - } - - refreshMetadata(event) { - const mangaId = event.currentTarget.dataset.mangaid; - const url = `/refresh_metadata`; - - this.iconTarget.classList.add('fa-spin'); - - fetch(url, { - method: 'POST', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ mangaId: mangaId }) - }) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }); - } - - searchLastChapter() { - console.log("Searching last chapter..."); - } - - import() { - console.log("Importing..."); - } - - monitoring(event){ - const mangaId = event.currentTarget.dataset.mangaid; - const currentTarget = event.currentTarget; - - const url = `/toggle_monitored`; - - fetch(url, { - method: 'POST', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ mangaId: mangaId }) - }) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }).then(data => { - if(data.isMonitored === true){ - currentTarget.classList.remove('text-white'); - currentTarget.classList.add('text-green-500'); - this.textTarget.innerHTML = "Monitored"; - }else if(data.isMonitored === false){ - currentTarget.classList.remove('text-green-500'); - currentTarget.classList.add('text-white'); - this.textTarget.innerHTML = "Monitoring"; - } - // console.log(data.isMonitored); - }); - - } - - editMangas() { - console.log("Editing mangas..."); - } - - editManga() { - const event = new CustomEvent('openEditModal'); - document.dispatchEvent(event); - } - - editPreferredSources() { - const event = new CustomEvent('openPreferredSourcesModal'); - document.dispatchEvent(event); - } - - openImportModal() { - const importEvent = new CustomEvent('openImportModal'); - document.dispatchEvent(importEvent); - } - - openExportModal() { - const exportEvent = new CustomEvent('openExportModal'); - document.dispatchEvent(exportEvent); - } - - deleteMangas() { - console.log("Deleting mangas..."); - } - - deleteManga() { - const event = new CustomEvent('openDeleteModal'); - document.dispatchEvent(event); - } - - confirmDelete(event) { - event.preventDefault(); - const url = `/manga/delete/${this.mangaIdValue}`; - - fetch(url, { - method: 'DELETE', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/json', - } - }) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(data => { - if (data.success) { - visit('/', {}); - } else { - throw new Error(data.error); - } - }) - .catch(error => { - console.error('Error:', error); - // Show error message to user - }); - } - - showOptions() { - console.log("Showing options..."); - } - - expandAll() { - console.log("Expanding all..."); - } - - changeView(event) { - event.preventDefault(); - const viewOption = event.currentTarget.dataset.view; - - const url = new URL(window.location); - url.searchParams.set('view', viewOption); - - window.location = url.toString(); - } - - sort(event) { - event.preventDefault() - const sortOption = event.currentTarget.dataset.sort; - let order = 'asc'; - - if (sortOption === this.currentSortValue && this.currentOrderValue === 'asc') { - order = 'desc'; - } - - const url = new URL(window.location); - url.searchParams.set('sort', sortOption); - url.searchParams.set('order', order); - - window.location = url.toString(); - } - - filter(event) { - event.preventDefault(); - const filterOption = event.currentTarget.dataset.filter; - - const url = new URL(window.location); - url.searchParams.set('status', filterOption); - - // Réinitialiser la page à 1 si on utilise la pagination - // url.searchParams.set('page', '1'); - - window.location = url.toString(); - } - -} diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index 4e3572a..5329ad6 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -37,10 +37,6 @@ framework: 'App\Domain\Shared\Domain\Event\VolumeImported': events 'App\Domain\Shared\Domain\Event\ChapterScraped': events - # Legacy messages (à garder si nécessaire) - 'App\Message\DownloadChapter': commands - 'App\Message\RefreshMetadata': commands - 'App\Message\RefreshAndDownloadChapters': commands # when@test: # framework: diff --git a/config/routes.yaml b/config/routes.yaml index 365bdb7..c61f8dd 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -7,8 +7,3 @@ vue_app: requirements: req: "^(?!api/|legacy).*" -controllers: - resource: - path: ../src/Controller/ - namespace: App\Controller - type: attribute diff --git a/config/services.yaml b/config/services.yaml index 7713181..29f5dca 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -26,10 +26,6 @@ services: # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones - App\EventListener\ExceptionListener: - tags: - - { name: kernel.event_listener, event: kernel.exception, method: onKernelException } - GuzzleHttp\Client: class: GuzzleHttp\Client arguments: @@ -43,63 +39,11 @@ services: protocols: [ 'http', 'https' ] track_redirects: true - App\Service\MangaScraperService: - arguments: - $projectDir: '%kernel.project_dir%' - - App\Controller\TestController: - arguments: - $projectDir: '%kernel.project_dir%' - App\Domain\Conversion\Infrastructure\Service\ConversionService: arguments: $projectDir: '%kernel.project_dir%' - App\Service\CbrToCbzConverter: - arguments: - $projectDir: '%kernel.project_dir%' - - App\Manager\FileSystemManager: - arguments: - $projectDir: '%kernel.project_dir%' - - App\EventSubscriber\QueueStatusSubscriber: - tags: - - { name: kernel.event_subscriber } - - App\Client\MangadexClient: - arguments: - $httpClient: '@GuzzleHttp\Client' - $clientId: '%env(MANGADEX_CLIENT_ID)%' - $clientSecret: '%env(MANGADEX_CLIENT_SECRET)%' - $username: '%env(MANGADEX_USERNAME)%' - $password: '%env(MANGADEX_PASSWORD)%' - - App\Service\MangadexProvider: - arguments: - $client: '@App\Client\MangadexClient' - - # Scraper Service - App\Service\Scraper\HtmlScraper: - tags: [ 'app.scraper' ] - - App\Service\Scraper\JavascriptScraper: - tags: [ 'app.scraper' ] - - App\Service\Scraper\MangadexScraper: - tags: [ 'app.scraper' ] - - # Scraper Factory - App\Service\Scraper\ScraperFactory: - arguments: - $scrapers: !tagged_iterator app.scraper - - # Manga Scraper Service - App\Service\Scraper\MangaScraperService: - arguments: - $scraperFactory: '@App\Service\Scraper\ScraperFactory' - - # New Scrapers Factory for Domain Layer + # Scrapers Factory for Domain Layer App\Domain\Scraping\Infrastructure\Service\ScraperFactory: arguments: $projectDir: '%kernel.project_dir%' @@ -187,20 +131,6 @@ services: App\Domain\Scraping\Domain\Contract\Repository\ContentSourceHealthRepositoryInterface: alias: App\Domain\Setting\Infrastructure\Persistence\Repository\DoctrineContentSourceForHealthCheckRepository - # Import Domain Services - App\Domain\Import\Infrastructure\Service\FilenameAnalyzer: ~ - - App\Domain\Import\Domain\Service\FilenameAnalyzerInterface: - alias: App\Domain\Import\Infrastructure\Service\FilenameAnalyzer - - # Import Domain Query/Command Handlers - App\Domain\Import\Application\QueryHandler\AnalyzeFilenameQueryHandler: ~ - App\Domain\Import\Application\CommandHandler\ImportFileCommandHandler: ~ - - # Import Domain API Platform Services - App\Domain\Import\Infrastructure\ApiPlatform\State\Processor\AnalyzeFilenameStateProcessor: ~ - App\Domain\Import\Infrastructure\ApiPlatform\State\Processor\ImportFileStateProcessor: ~ - # System Domain App\Domain\System\Domain\Contract\Repository\SystemStatusRepositoryInterface: alias: App\Domain\System\Infrastructure\Persistence\Repository\DoctrineSystemStatusRepository diff --git a/src/ApiResource/.gitignore b/src/ApiResource/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Client/MangadexClient.php b/src/Client/MangadexClient.php deleted file mode 100644 index f830a7f..0000000 --- a/src/Client/MangadexClient.php +++ /dev/null @@ -1,86 +0,0 @@ -httpClient = $httpClient; - $this->clientId = $clientId; - $this->clientSecret = $clientSecret; - $this->username = $username; - $this->password = $password; - $this->authenticate(); - } - - public function authenticate(): void - { - $response = $this->httpClient->request('POST', self::AUTHENTICATION_URL, [ - 'form_params' => [ - 'grant_type' => 'password', - 'username' => $this->username, - 'password' => $this->password, - 'client_id' => $this->clientId, - 'client_secret' => $this->clientSecret, - ], - ]); - - $data = json_decode($response->getBody()->getContents(), true); - $this->accessToken = $data['access_token']; - $this->refreshToken = $data['refresh_token']; - } - - public function refresh(): void - { - $response = $this->httpClient->request('POST', self::AUTHENTICATION_URL, [ - 'form_params' => [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $this->refreshToken, - 'client_id' => $this->clientId, - 'client_secret' => $this->clientSecret, - ], - ]); - - $data = json_decode($response->getBody()->getContents(), true); - $this->accessToken = $data['access_token']; - } - - private function request(string $method, string $endpoint, array $options = []): array - { - $options['headers']['Authorization'] = 'Bearer ' . $this->accessToken; - - $response = $this->httpClient->request($method, self::API_URL . $endpoint, $options); - - if ($response->getStatusCode() === 429) { - $this->refresh(); - $options['headers']['Authorization'] = 'Bearer ' . $this->accessToken; - $response = $this->httpClient->request($method, self::API_URL . $endpoint, $options); - } - - return json_decode($response->getBody()->getContents(), true); - } - - public function get(string $endpoint, array $params = []): array - { - return $this->request('GET', $endpoint, ['query' => $params]); - } - - public function post(string $endpoint, array $data): array - { - return $this->request('POST', $endpoint, ['json' => $data]); - } -} diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php deleted file mode 100644 index 2cfd630..0000000 --- a/src/Controller/ActivityController.php +++ /dev/null @@ -1,120 +0,0 @@ -getQueueStatus(); - $decodedPending = $this->decodeMessages($queueStatus['pending']); - $decodedProcessing = $this->decodeMessages($queueStatus['processing']); - - $status = array_merge( - $this->buildStatusActivity($decodedPending), - $this->buildStatusActivity($decodedProcessing) - ); - - return $this->render('activity/index.html.twig', [ - 'controller_name' => 'ActivityController', - 'status' => $status, - 'toolbar' => $this->toolbarFactory->createToolbar('activity')->getGroups(), - ]); - } - - #[Route('/activity/status', name: 'app_activity_status', methods: ['GET'])] - public function getStatus(): JsonResponse - { - $queueStatus = $this->getQueueStatus(); - $decodedPending = $this->decodeMessages($queueStatus['pending']); - $decodedProcessing = $this->decodeMessages($queueStatus['processing']); - $status = array_merge( - $this->buildStatusActivity($decodedPending), - $this->buildStatusActivity($decodedProcessing) - ); - - return new JsonResponse($status); - } - - // TODO refactorer ce code avec celui du QueueStatusSubscriber - private function getQueueStatus(): array - { - // Requête pour récupérer les messages en attente - $sqlPending = 'SELECT * FROM messenger_messages WHERE queue_name = :queue AND available_at IS NULL'; - $pending = $this->connection->fetchAllAssociative($sqlPending, ['queue' => 'default']); - - // Requête pour récupérer les messages en cours de traitement - $sqlProcessing = 'SELECT * FROM messenger_messages WHERE queue_name = :queue AND available_at IS NOT NULL'; - $processing = $this->connection->fetchAllAssociative($sqlProcessing, ['queue' => 'default']); - - return [ - 'pending' => $pending, - 'processing' => $processing - ]; - } - - private function buildStatusActivity(array $activity): array - { - $status = []; - foreach ($activity as $envelope) { - $envelope = $envelope['body']; - if ($envelope instanceof Envelope) { - if (!$envelope->getMessage() instanceof DownloadChapter) { - continue; - } - - $chapter = $this->chapterRepository->find($envelope->getMessage()->getChapterId()); - $manga = $chapter->getManga(); - $status[] = [ - 'manga' => $manga->getTitle(), - 'volume' => $chapter->getVolume(), - 'chapter' => $chapter->getNumber(), - 'chapterId' => $chapter->getId(), - 'title' => $chapter->getTitle(), - ]; - } - } - - return $status; - } - - private function decodeMessages(array $messages): array - { - $decodedMessages = []; - - foreach ($messages as $message) { - $decodedMessages[] = [ - 'id' => $message['id'], - 'body' => $this->decodeMessageBody($message['body']), - 'headers' => json_decode($message['headers'], true), - ]; - } - - return $decodedMessages; - } - - private function decodeMessageBody(string $body) - { - return unserialize(stripcslashes($body)); - } -} diff --git a/src/Controller/CalendarController.php b/src/Controller/CalendarController.php deleted file mode 100644 index 2e2ca99..0000000 --- a/src/Controller/CalendarController.php +++ /dev/null @@ -1,18 +0,0 @@ -render('calendar/index.html.twig', [ - 'controller_name' => 'CalendarController', - ]); - } -} diff --git a/src/Controller/ConversionController.php b/src/Controller/ConversionController.php deleted file mode 100644 index bd04d72..0000000 --- a/src/Controller/ConversionController.php +++ /dev/null @@ -1,64 +0,0 @@ -isMethod('POST')) { - /** @var UploadedFile $file */ - $file = $request->files->get('file'); - - if ($file && $file->getClientOriginalExtension() === 'cbr') { - $originalFileName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); - $tempFilePath = $file->getPathname(); - - try { - $cbzPath = $this->cbrToCbzConverter->convert($tempFilePath); - - $response = new BinaryFileResponse($cbzPath); - $response->setContentDisposition( - ResponseHeaderBag::DISPOSITION_ATTACHMENT, - $originalFileName . '.cbz' - ); - $response->headers->set('Content-Type', 'application/x-cbz'); - $response->headers->set('Turbo-Visit-Control', 'reload'); - - $response->deleteFileAfterSend(true); - - return $response; - } catch (\Exception $e) { - $this->notificationService->sendUpdate([ - 'status' => 'error', - 'message' => 'Une erreur est survenue lors de la conversion : ' . $e->getMessage() - ]); - } - } else { - $this->notificationService->sendUpdate([ - 'status' => 'error', - 'message' => 'Veuillez sélectionner un fichier CBR valide.' - ]); - } - } - - return $this->render('conversion/index.html.twig'); - } -} diff --git a/src/Controller/ImportController.php b/src/Controller/ImportController.php deleted file mode 100644 index 6c0d1d9..0000000 --- a/src/Controller/ImportController.php +++ /dev/null @@ -1,220 +0,0 @@ -isMethod('POST')) { - $files = $request->files->get('files'); - if ($files) { - $importFiles = []; - foreach ($files as $file) { - if ($file && in_array($file->getClientOriginalExtension(), ['cbz', 'cbr'])) { - $originalFileName = $file->getClientOriginalName(); - - try { - $tmpPath = $this->fileSystemManager->moveUploadedFile( - $file->getPathname(), - $this->fileSystemManager->getUploadsDirectory(), - $file->getClientOriginalName() - ); - $importFiles[] = [ - 'id' => uniqid(), - 'path' => $tmpPath, - 'original_name' => $originalFileName, - ]; - } catch (FileException $e) { - $this->notificationService->sendUpdate([ - 'status' => 'error', - 'message' => 'Une erreur est survenue lors de l\'import du fichier ' . $originalFileName, - ]); - } - } else { - $this->notificationService->sendUpdate([ - 'status' => 'error', - 'message' => 'Le fichier ' . $file->getClientOriginalName() . ' doit être au format CBZ ou CBR.', - ]); - } - } - - if (!empty($importFiles)) { - $session->set('import_files', $importFiles); - return $this->redirectToRoute('import_match'); - } - } else { - $this->notificationService->sendUpdate([ - 'status' => 'error', - 'message' => 'Aucun fichier n\'a été sélectionné.', - ]); - } - } - - return $this->render('import/index.html.twig'); - } - - /** - * @throws Exception - */ - #[Route('/import/match', name: 'import_match')] - public function match(SessionInterface $session): Response - { - $files = $session->get('import_files', []); - if (empty($files)) { - return $this->redirectToRoute('app_manga_import'); - } - - $processedFiles = []; - foreach ($files as $fileId => $fileInfo) { - $filePath = $fileInfo['path']; - $originalFileName = $fileInfo['original_name']; - - $fileExtension = pathinfo($filePath, PATHINFO_EXTENSION); - if (strtolower($fileExtension) === 'cbr') { - $cbzPath = $this->cbrToCbzConverter->convert($filePath); - $filePath = $cbzPath; - $originalFileName = pathinfo($originalFileName, PATHINFO_FILENAME) . '.cbz'; - $files[$fileId]['path'] = $filePath; - $files[$fileId]['original_name'] = $originalFileName; - } - - $metadata = $this->cbzService->extractMetadata($filePath, $originalFileName); - $mangas = $this->mangaRepository->findBySlug($metadata['title']); - - $mangaOptions = []; - foreach ($mangas as $manga) { - $mangaOptions[] = [ - 'slug' => $manga->getSlug(), - 'title' => $manga->getTitle(), - 'author' => $manga->getAuthor(), - 'publicationYear' => $manga->getPublicationYear(), - 'genres' => $manga->getGenres(), - 'description' => $manga->getDescription() - ]; - } - - $processedFiles[] = [ - 'id' => $fileId, - 'originalFileName' => $originalFileName, - 'fileSize' => $this->formatBytes(filesize($filePath)), - 'metadata' => $metadata, - 'mangaOptions' => $mangaOptions - ]; - } - - $session->set('import_files', $files); - - return $this->render('import/match.html.twig', [ - 'files' => $processedFiles - ]); - } - - private function formatBytes($bytes, $precision = 2) - { - $units = ['B', 'KB', 'MB', 'GB', 'TB']; - $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count($units) - 1); - $bytes /= (1 << (10 * $pow)); - return round($bytes, $precision) . ' ' . $units[$pow]; - } - - #[Route('/import/confirm', name: 'import_confirm', methods: ['POST'])] - public function confirm(Request $request, SessionInterface $session): Response - { - $files = $session->get('import_files', []); - $selectedFiles = $request->request->all('selected'); - $mangaSlugs = $request->request->all('manga_slug'); - $volumes = $request->request->all('volume'); - $chapters = $request->request->all('chapter'); - - $importedFiles = []; - $errors = []; - - foreach ($selectedFiles as $fileId) { - if (!isset($files[$fileId])) { - continue; - } - - $file = $files[$fileId]; - $mangaSlug = $mangaSlugs[$fileId] ?? null; - $volume = $volumes[$fileId] ?? null; - $chapter = $chapters[$fileId] ?? null; - - try { - $manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]); - if (!$manga) { - throw new \Exception('Manga non trouvé.'); - } - - if (!is_null($chapter)) { - $chapter = $manga->getChapterByNumber($chapter); - if (!$chapter) { - throw new \Exception('Chapitre non trouvé.'); - } - } - - $importedFiles[] = $file['original_name']; - $this->mangaImportService->importFile($manga, $volume, $chapter, $file['path']); - } catch (\Exception $e) { - $errors[] = "Erreur lors de l'import de {$file['original_name']} : " . $e->getMessage(); - } - } - - // Nettoyer les fichiers temporaires non importés - foreach ($files as $file) { - $this->fileSystemManager->deleteFile($file['path']); - } - - // Nettoyer la session - $session->remove('import_files'); - - // Préparer le message de notification - if (!empty($importedFiles)) { - $successMessage = 'Fichiers importés avec succès : ' . implode(', ', $importedFiles); - $this->notificationService->sendUpdate([ - 'status' => 'success', - 'message' => $successMessage - ]); - } - - if (!empty($errors)) { - $errorMessage = implode("\n", $errors); - $this->notificationService->sendUpdate([ - 'status' => 'error', - 'message' => $errorMessage - ]); - } - - return $this->redirectToRoute('app_manga'); - } -} diff --git a/src/Controller/MangaController.php b/src/Controller/MangaController.php deleted file mode 100644 index 492845a..0000000 --- a/src/Controller/MangaController.php +++ /dev/null @@ -1,475 +0,0 @@ -imageManager = new ImageManager(new Driver()); - } - - #[Route('/legacy', name: 'app_legacy')] - public function index(Request $request): Response - { - $sort = $request->query->get('sort', 'title'); - $order = $request->query->get('order', 'asc'); - $status = $request->query->get('status', 'all'); - $view = $request->query->get('view', 'poster'); - - $mangas = $this->mangaRepository->findAllSortedAndFiltered($sort, $order, $status); - - return $this->render('manga/index.html.twig', [ - 'mangas' => $mangas, - 'toolbar' => $this->toolbarFactory->createToolbar('manga_list')->getGroups(), - 'currentStatus' => $status, - 'currentView' => $view, - ]); - } - - #[Route('/manga/chapters/{mangaSlug}', name: 'app_manga_show')] - public function showChapters(string $mangaSlug, Request $request): Response - { - // $manga = $this->mangaRepository->findOneWithChapterBy(['slug' => $mangaSlug]); - $manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]); - - if (!$manga) { - throw new NotFoundHttpException("Le manga demandé n'existe pas."); - } - - $form = $this->createForm(MangaEditType::class, $manga); - $contentSources = $this->contentSourceRepository->findAll(); - - return $this->render('manga/show_chapters.html.twig', [ - 'manga' => $manga, - 'toolbar' => $this->toolbarFactory->createToolbar('chapter_list', ['mangaId' => $manga->getId(), 'isMonitored' => (int)$manga->isMonitored()])->getGroups(), - 'form' => $form->createView(), - 'contentSources' => $contentSources, - ]); - } - - #[Route('/manga/delete/{id}', name: 'app_manga_delete', methods: ['DELETE'])] - public function deleteManga(Manga $manga): JsonResponse - { - try { - foreach ($manga->getChapters() as $chapter) { - file_exists($chapter->getCbzPath()) ?? unlink($chapter->getCbzPath()); - $this->entityManager->remove($chapter); - } - $this->entityManager->remove($manga); - $this->entityManager->flush(); - - return new JsonResponse(['success' => true]); - } catch (\Exception $e) { - return new JsonResponse(['success' => false, 'error' => 'Unable to delete manga.'], 500); - } - } - - #[Route('/manga/{id}/edit', name: 'app_manga_edit', methods: ['POST'])] - public function edit(Request $request, Manga $manga, EntityManagerInterface $entityManager): JsonResponse|Response - { - $form = $this->createForm(MangaEditType::class, $manga); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $entityManager->flush(); - - return $this->redirectToRoute('app_manga_show', ['mangaSlug' => $manga->getSlug()]); - } - - $errors = []; - foreach ($form->getErrors(true) as $error) { - $errors[] = $error->getMessage(); - } - - return new JsonResponse(['errors' => $errors], 400); - } - - #[Route('/manga/{id}/preferred-sources', name: 'manga_preferred_sources', methods: ['POST'])] - public function updatePreferredSources( - Request $request, - Manga $manga, - ContentSourceRepository $contentSourceRepository - ): JsonResponse { - $data = json_decode($request->getContent(), true); - $preferredSourceIds = $data['preferredSources'] ?? []; - - $preferredSources = $contentSourceRepository->findBy(['id' => $preferredSourceIds]); - - // This will maintain the order of the sources as they were sent in the request - $orderedPreferredSources = array_map( - fn ($id) => current(array_filter($preferredSources, fn ($s) => $s->getId() == $id)), - $preferredSourceIds - ); - - $manga->setPreferredSources(array_filter($orderedPreferredSources)); - $this->entityManager->flush(); - - return new JsonResponse(['success' => true]); - } - - public function _chaptersByManga(int $id): Response - { - $manga = $this->mangaRepository->find($id); - $chaptersByVolume = []; - foreach ($manga->getChapters() as $chapter) { - $volume = $chapter->getVolume() ?? 'Not Found'; - $chaptersByVolume[$volume][] = $chapter; - } - - foreach ($chaptersByVolume as $volume => &$chapters) { - usort($chapters, function ($a, $b) { - return $b->getNumber() <=> $a->getNumber(); - }); - } - unset($chapters); - - uksort($chaptersByVolume, function ($a, $b) { - if ($a == 0) { - return -1; - } - if ($b == 0) { - return 1; - } - return $b <=> $a; - }); - - return $this->render('manga/_chapter_list.html.twig', [ - 'manga' => $manga, - 'chapters_by_volume' => $chaptersByVolume - ]); - } - - #[Route('/delete_cbz/{id}', name: 'app_delete_cbz')] - public function deleteChapterCbz(Chapter $chapter): JsonResponse - { - $cbzPath = $chapter->getCbzPath(); - if (!$cbzPath) { - return new JsonResponse(['error' => 'No CBZ path for this chapter.'], 400); - } - - file_exists($cbzPath) ?? unlink($cbzPath); - - $chapter->setCbzPath(null); - $this->entityManager->persist($chapter); - $this->entityManager->flush(); - - return new JsonResponse(['success' => 'CBZ file deleted.'], 200); - } - - #[Route('/chapter/{id}/edit', name: 'app_chapter_edit', methods: ['POST'])] - public function editChapter(Request $request, Chapter $chapter): JsonResponse - { - $data = json_decode($request->getContent(), true); - - $chapter->setNumber($data['number']); - $chapter->setTitle($data['title']); - - $this->entityManager->flush(); - - return new JsonResponse(['success' => true, 'message' => 'Chapter updated successfully']); - } - - #[Route('/hide_chapter/{id}', name: 'app_hide_chapter')] - public function hideChapter(Chapter $chapter): JsonResponse - { - $chapter->setVisible(false); - $this->entityManager->persist($chapter); - $this->entityManager->flush(); - - return new JsonResponse(['success' => 'Chapter hidden.'], 200); - } - - #[Route('/manga/search/{query}', name: 'app_manga_search')] - public function search(string $query = ''): Response - { - return $this->render('manga/add_new.html.twig', [ - 'query' => $query, - ]); - } - - - /** - * @throws GuzzleException - */ - #[Route('/addManga', name: 'app_manga_add')] - public function addManga(Request $request): Response - { - $manga = $this->mangaRepository->findOneBy(['slug' => $request->request->get('slug')]); - if ($manga) { - return $this->redirectToRoute('app_manga_show', ['mangaSlug' => $manga->getSlug()]); - } - - $manga = new Manga(); - $manga->setTitle($request->request->get('title')) - ->setSlug($request->request->get('slug')) - ->setDescription($request->request->get('description')) - ->setStatus($request->request->get('status')) - ->setGenres(explode(',', $request->request->get('genres'))) - ->setAuthor($request->request->get('author')) - ->setPublicationYear($request->request->get('publicationYear')) - ->setRating($request->request->get('rating')) - ->setExternalId($request->request->get('externalId')) - ->setMonitored(false); - - // Traitement de l'image - $imageUrl = $request->request->get('imageUrl'); - try { - $imageUrls = $this->processAndSaveImage($imageUrl); - $manga->setImageUrl($imageUrls['full']); - $manga->setThumbnailUrl($imageUrls['thumbnail']); - } catch (\Exception|GuzzleException $e) { - throw $e; - } - - $mergedChapters = $this->mangadexProvider->addAllChaptersToManga($manga); - - if (empty($mergedChapters)) { - return $this->redirectToRoute('app_manga_search', ['query' => $manga->getTitle()]); - } - - try { - foreach ($manga->getChapters() as $chapter) { - $this->entityManager->persist($chapter); - } - - $this->entityManager->persist($manga); - $this->entityManager->flush(); - } catch (\Exception $e) { - if ($e instanceof UniqueConstraintViolationException) { - return $this->redirectToRoute('app_manga_show', ['mangaSlug' => $manga->getSlug()]); - } - throw $e; - } - - return $this->redirectToRoute('app_manga_show', ['mangaSlug' => $manga->getSlug()]); - } - - /** - * @throws GuzzleException - */ - private function processAndSaveImage(string $imageUrl): array - { - $client = new Client(); - $response = $client->get($imageUrl); - $tempImage = tmpfile(); - fwrite($tempImage, $response->getBody()->getContents()); - $tempImagePath = stream_get_meta_data($tempImage)['uri']; - - // Générer un nom de fichier unique - $originalFilename = pathinfo($imageUrl, PATHINFO_FILENAME); - $newFilename = $this->fileSystemManager->generateUniqueImageFilename($imageUrl); - - try { - // Créer et sauvegarder la miniature - $thumbnail = $this->imageManager->read($tempImagePath); - $thumbnail->cover(300, 440); - $thumbnail->save($this->fileSystemManager->getImagePath('thumbnails') . '/' . $newFilename, quality: 85); - - // Sauvegarder l'image en taille réelle - $fullImage = $this->imageManager->read($tempImagePath); - $fullImage->save($this->fileSystemManager->getImagePath('full') . '/' . $newFilename, quality: 90); - - // Fermer et supprimer le fichier temporaire - fclose($tempImage); - - return [ - 'full' => '/images/full/' . $newFilename, - 'thumbnail' => '/images/thumbnails/' . $newFilename - ]; - - } catch (FileException $e) { - // Fermer le fichier temporaire en cas d'erreur - fclose($tempImage); - throw $e; - } - } - - #[Route('/searchChapter/{id}', name: 'search_chapter')] - public function addChapterMessenger(int $id): JsonResponse - { - $chapter = $this->chapterRepository->find($id); - if (!$chapter) { - return new JsonResponse(['error' => 'Chapter Not Found.'], 400); - } elseif ($chapter->getCbzPath() !== null) { - return new JsonResponse(['error' => 'Chapter already scraped.'], 400); - } - - $this->bus->dispatch(new DownloadChapter($id)); - - return new JsonResponse(['success' => 'Scrapping started...'], 200); - } - - #[Route('/searchVolume/{mangaSlug}/{volume}', name: 'search_volume')] - public function searchVolume(string $mangaSlug, int $volume): JsonResponse - { - $manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]); - if (!$manga) { - return new JsonResponse(['error' => 'Manga Not Found.'], 400); - } - - $volumeChapters = $this->chapterRepository->findBy([ - 'manga' => $manga, - 'volume' => $volume, - 'visible' => true - ]); - - if (empty($volumeChapters)) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'No chapters found for this volume.']); - return new JsonResponse(['error' => 'No chapters found for this volume.'], 200); - } - - foreach ($volumeChapters as $chapter) { - if ($chapter->getCbzPath() === null) { - $this->bus->dispatch(new DownloadChapter($chapter->getId())); - } - } - - return new JsonResponse(['success' => 'Scrapping started...'], 200); - } - - #[Route('/download-cbz/{chapterId}', name: 'download_cbz')] - public function downloadChapter(int $chapterId): BinaryFileResponse|JsonResponse - { - $chapter = $this->chapterRepository->find($chapterId); - if (!$chapter) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapitre non trouvé.']); - return new JsonResponse(['error' => 'Chapitre non trouvé.'], 200); - } - - $cbzPath = $chapter->getCbzPath(); - if (!$cbzPath || !file_exists($cbzPath)) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Le fichier CBZ n\'existe pas.']); - return new JsonResponse(['error' => 'Le fichier CBZ n\'existe pas.'], 200); - } - - $isFullVolume = $this->isFullVolume($chapter); - $fileName = $isFullVolume - ? $this->cbzService->generateFileName($chapter->getManga(), $chapter->getVolume()) - : $this->cbzService->generateFileName($chapter->getManga(), null, $chapter->getNumber()); - - return $this->cbzService->createBinaryFileResponse($cbzPath, $fileName); - } - - #[Route('/download-volume/{mangaSlug}/{volume}', name: 'download_volume')] - public function downloadVolume(string $mangaSlug, int $volume): BinaryFileResponse|JsonResponse - { - $manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]); - - $volumeChapters = $this->chapterRepository->findBy([ - 'manga' => $manga, - 'volume' => $volume, - 'visible' => true - ], ['number' => 'ASC']); - - if (empty($volumeChapters)) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Aucun chapitre trouvé pour ce volume.']); - } - - if (!$this->cbzService->doAllChaptersHaveCbz($volumeChapters)) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Tous les chapitres du volume ne sont pas scrapés.']); - return new JsonResponse(['error' => 'Tous les chapitres du volume ne sont pas scrapés.'], 200); - } - - $fileName = $this->cbzService->generateFileName($manga, $volume); - - if ($this->cbzService->areAllChaptersCbzIdentical($volumeChapters)) { - return $this->cbzService->createBinaryFileResponse($volumeChapters[0]->getCbzPath(), $fileName); - } else { - $tempFile = $this->cbzService->createVolumeArchive($volumeChapters); - $response = $this->cbzService->createBinaryFileResponse($tempFile, $fileName); - $response->deleteFileAfterSend(true); - return $response; - } - } - - #[Route('/refresh_metadata', name: 'refresh_metadata')] - public function refreshMetadata(Request $request): JsonResponse - { - $mangaId = json_decode($request->getContent(), true)['mangaId']; - $manga = $this->mangaRepository->find($mangaId); - if (!$manga) { - return new JsonResponse(['error' => 'Manga Not Found.'], 400); - } - $this->bus->dispatch(new RefreshMetadata($mangaId)); - - return new JsonResponse(['success' => 'Metadata refresh started...'], 200); - } - - #[Route('/toggle_monitored', name: 'toggle_monitored')] - public function toogleMonitored(Request $request): JsonResponse - { - $id = json_decode($request->getContent(), true)['mangaId']; - $manga = $this->mangaRepository->find($id); - if (!$manga) { - return new JsonResponse(['error' => 'Manga Not Found.'], 400); - } - - $manga->setMonitored(!$manga->isMonitored()); - $this->entityManager->persist($manga); - $this->entityManager->flush(); - - return new JsonResponse(['success' => 'Monitored status updated.', 'isMonitored' => $manga->isMonitored()], 200); - } - - private function isFullVolume(Chapter $chapter): bool - { - $volumeChapters = $this->chapterRepository->findBy([ - 'manga' => $chapter->getManga(), - 'volume' => $chapter->getVolume() - ]); - - $firstChapterPath = $volumeChapters[0]->getCbzPath(); - foreach ($volumeChapters as $volumeChapter) { - if ($volumeChapter->getCbzPath() !== $firstChapterPath) { - return false; - } - } - - return true; - } -} diff --git a/src/Controller/ReaderController.php b/src/Controller/ReaderController.php deleted file mode 100644 index f7e4945..0000000 --- a/src/Controller/ReaderController.php +++ /dev/null @@ -1,138 +0,0 @@ -mangaRepository->findOneBy(['slug' => $mangaSlug]); - if (!$manga) { - throw $this->createNotFoundException("Le manga demandé n'existe pas."); - } - - $chapter = $manga->getChapterByNumber($chapterNumber); - if (!$chapter) { - throw $this->createNotFoundException("Le chapitre demandé n'existe pas."); - } - - if (is_null($chapter->getCbzPath())) { - $this->notificationService->sendUpdate([ - 'status' => 'error', - 'message' => 'Le chapitre demandé n\'est pas encore disponible.', - ]); - return $this->redirectToRoute('app_manga_show', ['mangaSlug' => $mangaSlug]); - } - - $totalPages = $this->cbzService->getPageCount($chapter->getCbzPath()); - - return $this->render('reader/index.html.twig', [ - 'manga' => $manga, - 'chapter' => $chapter, - 'totalPages' => $totalPages, - ]); - } - - #[Route('/api/read/{mangaSlug}/{chapterNumber}/{pageNumber}', name: 'app_reader_page')] - public function getPage(string $mangaSlug, float $chapterNumber, int $pageNumber): Response - { - $manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]); - if (!$manga) { - throw $this->createNotFoundException("Le manga demandé n'existe pas."); - } - - $chapter = $manga->getChapterByNumber($chapterNumber); - if (!$chapter) { - throw $this->createNotFoundException("Le chapitre demandé n'existe pas."); - } - - $pageContent = $this->cbzService->getPageContent($chapter->getCbzPath(), $pageNumber); - if (!$pageContent) { - throw $this->createNotFoundException("La page demandée n'existe pas."); - } - - return new Response(base64_encode($pageContent), 200, ['Content-Type' => 'text/plain']); - } - - #[Route('/api/chapters/{mangaSlug}', name: 'app_reader_chapters')] - public function getChapters(string $mangaSlug): JsonResponse - { - $manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]); - if (!$manga) { - throw $this->createNotFoundException("Le manga demandé n'existe pas."); - } - - $chapters = $manga->getChapters() - ->filter(fn ($chapter) => $chapter->isVisible() && !is_null($chapter->getCbzPath())) - ->toArray(); - - usort($chapters, fn ($a, $b) => $b->getNumber() <=> $a->getNumber()); - - $chapters = array_values(array_map(fn ($chapter) => [ - 'number' => $chapter->getNumber(), - 'title' => $chapter->getTitle(), - ], $chapters)); - - return $this->json($chapters); - } - - #[Route('/api/previous-chapter/{mangaSlug}/{currentChapterNumber}', name: 'app_reader_previous_chapter')] - public function getPreviousChapter(string $mangaSlug, float $currentChapterNumber): JsonResponse - { - $manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]); - if (!$manga) { - throw $this->createNotFoundException("Le manga demandé n'existe pas."); - } - - $chapters = $manga->getChapters() - ->filter(fn ($chapter) => $chapter->isVisible() && $chapter->getNumber() < $currentChapterNumber) - ->toArray(); - - usort($chapters, fn ($a, $b) => $b->getNumber() <=> $a->getNumber()); - - $previousChapter = reset($chapters) ?: null; - - return $this->json($previousChapter ? [ - 'number' => $previousChapter->getNumber(), - 'title' => $previousChapter->getTitle(), - ] : null); - } - - #[Route('/api/next-chapter/{mangaSlug}/{currentChapterNumber}', name: 'app_reader_next_chapter')] - public function getNextChapter(string $mangaSlug, float $currentChapterNumber): JsonResponse - { - $manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]); - if (!$manga) { - throw $this->createNotFoundException("Le manga demandé n'existe pas."); - } - - $nextChapter = $manga->getChapters() - ->filter(fn ($chapter) => $chapter->isVisible() && $chapter->getNumber() > $currentChapterNumber) - ->toArray(); - - usort($nextChapter, fn ($a, $b) => $a->getNumber() <=> $b->getNumber()); - - $nextChapter = reset($nextChapter) ?: null; - - return $this->json($nextChapter ? [ - 'number' => $nextChapter->getNumber(), - 'title' => $nextChapter->getTitle(), - ] : null); - } -} diff --git a/src/Controller/SettingsController.php b/src/Controller/SettingsController.php deleted file mode 100644 index 6b1f100..0000000 --- a/src/Controller/SettingsController.php +++ /dev/null @@ -1,203 +0,0 @@ -render('settings/index.html.twig', [ - 'controller_name' => 'SettingsController', - ]); - } - - #[Route('/settings/general', name: 'app_settings_general')] - public function general(): Response - { - return $this->render('settings/index.html.twig', [ - 'controller_name' => 'SettingsController', - ]); - } - - #[Route('/settings/folders', name: 'app_settings_folders')] - public function folders(Request $request, AppSettingsManager $settingsManager): Response - { - $currentSettings = $settingsManager->getSettings(); - - $form = $this->createForm(AppSettingsType::class, $currentSettings); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $newSettings = $form->getData(); - $settingsManager->updateSettings($newSettings); - - $this->notificationService->sendUpdate(['status' => 'success', 'message' => 'Settings updated successfully.']); - return $this->json(['success' => true]); - } - - return $this->render('settings/folders.html.twig', [ - 'form' => $form->createView(), - ]); - } - - #[Route('/settings/scrappers/list', name: 'app_settings_scrappers_list')] - public function list(ContentSourceRepository $repository, ToolbarFactory $toolbarFactory): Response - { - $contentSources = $repository->findAll(); - - return $this->render('settings/scrapper_list.html.twig', [ - 'contentSources' => $contentSources, - 'toolbar' => $toolbarFactory->createToolbar('scraper_list')->getGroups(), - ]); - } - - #[Route('/settings/scrappers/{id}', name: 'app_settings_scrappers', defaults: ['id' => null])] - public function scrappers(Request $request, ?ContentSource $contentSource): Response - { - $isNew = $contentSource === null; - $contentSource = $contentSource ?? new ContentSource(); - - $form = $this->createForm(ContentSourceType::class, $contentSource); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $this->entityManager->persist($contentSource); - $this->entityManager->flush(); - $this->notificationService->sendUpdate(['status' => 'success', 'message' => ($isNew ? 'New scrapper configuration saved' : 'Scrapper configuration updated') . ' successfully.']); - return $this->redirectToRoute('app_settings_scrappers_list'); - } - - return $this->render('settings/scrappers.html.twig', [ - 'form' => $form->createView(), - 'isNew' => $isNew, - ]); - } - - /** - * @throws GuzzleException - */ - #[Route('/settings/scrappers_test', name: 'app_settings_scrappers_test', methods: ['POST'])] - public function scrapperTest(Request $request): JsonResponse - { - $contentSource = new ContentSource(); - $form = $this->createForm(ContentSourceType::class, $contentSource); - $form->submit($request->request->all()['content_source']); - - if ($form->isValid()) { - $mangaSlug = $request->request->get('mangaSlug'); - $chapterNumber = $request->request->get('chapterNumber'); - - try { - $scrapedData = $this->mangaScraperService->testScraping($mangaSlug, $chapterNumber, $contentSource); - } catch (\Exception $e) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => $e->getMessage()]); - return new JsonResponse([ - 'success' => false, - 'message' => $e->getMessage(), - ]); - } - - return new JsonResponse([ - 'success' => true, - 'message' => 'Test successful', - 'data' => $scrapedData - ]); - } else { - return new JsonResponse([ - 'success' => false, - 'message' => 'Invalid form submission', - 'errors' => $this->getFormErrors($form) - ]); - } - } - - private function getFormErrors($form): array - { - $errors = []; - foreach ($form->getErrors(true) as $error) { - $errors[] = $error->getMessage(); - } - return $errors; - } - - #[Route('/settings/ui', name: 'app_settings_ui')] - public function ui(): Response - { - return $this->render('settings/index.html.twig', [ - 'controller_name' => 'SettingsController', - ]); - } - - #[Route('/settings/export_scrappers', name: 'app_settings_scrappers_export', methods: ['GET'])] - public function exportScrappers(): JsonResponse - { - $contentSources = $this->contentSourceRepository->findAll(); - $data = []; - - foreach ($contentSources as $source) { - $data[] = [ - 'baseUrl' => $source->getBaseUrl(), - 'imageSelector' => $source->getImageSelector(), - 'nextPageSelector' => $source->getNextPageSelector(), - 'chapterUrlFormat' => $source->getChapterUrlFormat(), - 'scrapingType' => $source->getScrapingType(), - 'chapterSelector' => $source->getChapterSelector(), //TODO à renommer en chapterListSelector - ]; - } - - return new JsonResponse($data); - } - - #[Route('/settings/import_scrappers', name: 'app_settings_scrappers_import', methods: ['POST'])] - public function importScrappers(Request $request): JsonResponse - { - $content = $request->getContent(); - $data = json_decode($content, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Invalid JSON data']); - return new JsonResponse(['error' => 'Invalid JSON data'], 400); - } - - foreach ($data as $sourceData) { - $contentSource = new ContentSource(); - $contentSource->setBaseUrl($sourceData['baseUrl']); - $contentSource->setImageSelector($sourceData['imageSelector']); - $contentSource->setNextPageSelector($sourceData['nextPageSelector']); - $contentSource->setChapterUrlFormat($sourceData['chapterUrlFormat']); - $contentSource->setScrapingType($sourceData['scrapingType']); - - $this->entityManager->persist($contentSource); - } - - $this->entityManager->flush(); - - return new JsonResponse(['message' => 'Content sources imported successfully']); - } -} diff --git a/src/Controller/SystemController.php b/src/Controller/SystemController.php deleted file mode 100644 index c2b4fa8..0000000 --- a/src/Controller/SystemController.php +++ /dev/null @@ -1,50 +0,0 @@ -render('system/index.html.twig', [ - 'controller_name' => 'SystemController', - ]); - } - - #[Route('/system/status', name: 'app_system_status')] - public function status(): Response - { - return $this->render('system/index.html.twig', [ - 'controller_name' => 'SettingsController', - ]); - } - - #[Route('/system/backup', name: 'app_system_backup')] - public function backup(): Response - { - return $this->render('system/index.html.twig', [ - 'controller_name' => 'SettingsController', - ]); - } - - #[Route('/system/logs', name: 'app_system_logs')] - public function logs(): Response - { - return $this->render('system/index.html.twig', [ - 'controller_name' => 'SettingsController', - ]); - } - - #[Route('/system/updates', name: 'app_system_updates')] - public function update(): Response - { - return $this->render('system/index.html.twig', [ - 'controller_name' => 'SettingsController', - ]); - } -} diff --git a/src/Controller/TestController.php b/src/Controller/TestController.php deleted file mode 100644 index af3fdef..0000000 --- a/src/Controller/TestController.php +++ /dev/null @@ -1,100 +0,0 @@ -imageManager = new ImageManager(new Driver()); - } - - #[Route('/test', name: 'test')] - public function test(): Response - { - $mangas = $this->mangaRepository->findAll(); - - $changed = 0; - foreach ($mangas as $manga) { - //si getImageUrl() retourne un lien sous la forme d'une URL (https ou http) - if ($manga->getImageUrl()) { - $imageUrls = $this->processAndSaveImage($manga->getImageUrl()); - $manga->setThumbnailUrl($imageUrls['thumbnail']); - $this->mangaRepository->save($manga, true); - $changed++; - } - } - - - return new JsonResponse(['changed' => $changed]); - } - - /** - * @throws GuzzleException - */ - private function processAndSaveImage(string $imageUrl): array - { - $image = file_get_contents($this->projectDir . '/public' .$imageUrl); - $tempImage = tmpfile(); - fwrite($tempImage, $image); - $tempImagePath = stream_get_meta_data($tempImage)['uri']; - - // Générer un nom de fichier unique - $originalFilename = pathinfo($imageUrl, PATHINFO_FILENAME); - $safeFilename = $this->slugger->slug($originalFilename); - $newFilename = $safeFilename . '-' . uniqid() . '.' . 'jpg'; - - try { - // Créer et sauvegarder la miniature - $thumbnail = $this->imageManager->read($tempImagePath); - $thumbnail->cover(300, 440); - $thumbnail->save($this->projectDir . '/public/images/thumbnails/' . $newFilename, quality: 85); - - // Sauvegarder l'image en taille réelle - // $fullImage = $this->imageManager->read($tempImagePath); - // $fullImage->save($this->projectDir . '/public/images/full/' . $newFilename, quality: 90); - - // Fermer et supprimer le fichier temporaire - fclose($tempImage); - - return [ - 'full' => '/images/full/' . $newFilename, - 'thumbnail' => '/images/thumbnails/' . $newFilename - ]; - - } catch (FileException $e) { - // Fermer le fichier temporaire en cas d'erreur - fclose($tempImage); - throw $e; - } - } -} diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php deleted file mode 100644 index 115ae6d..0000000 --- a/src/DataFixtures/AppFixtures.php +++ /dev/null @@ -1,45 +0,0 @@ - UserFactory::random() - ]; - }); - - $mangas = MangaFactory::createMany(25); - - foreach ($mangas as $manga) { - - for ($i = 1; $i <= 5; $i++) { - $manga->addChapter(ChapterFactory::createOne([ - 'manga' => $manga, - 'number' => $i - ])->object()); - } - - foreach ($manga->getChapters() as $chapter) { - for ($i = 1; $i <= 5; $i++) { - $chapter->addPagesLink(PageFactory::createOne([ - 'chapter' => $chapter, - 'number' => $i - ])->object()); - } - } - } - } -} diff --git a/src/Entity/Chapter.php b/src/Entity/Chapter.php index de4cfef..cfb1552 100644 --- a/src/Entity/Chapter.php +++ b/src/Entity/Chapter.php @@ -6,10 +6,8 @@ use App\Repository\ChapterRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use Symfony\UX\Turbo\Attribute\Broadcast; #[ORM\Entity(repositoryClass: ChapterRepository::class)] -#[Broadcast()] class Chapter { #[ORM\Id] diff --git a/src/Event/PageScrappingProgressEvent.php b/src/Event/PageScrappingProgressEvent.php deleted file mode 100644 index bd2e9b7..0000000 --- a/src/Event/PageScrappingProgressEvent.php +++ /dev/null @@ -1,36 +0,0 @@ -chapterId = $chapterId; - $this->pageIndex = $pageIndex; - $this->totalPages = $totalPages; - } - - public function getChapterId(): int - { - return $this->chapterId; - } - - public function getPageIndex(): int - { - return $this->pageIndex; - } - - public function getTotalPages(): int - { - return $this->totalPages; - } -} diff --git a/src/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php deleted file mode 100644 index 47cc9e2..0000000 --- a/src/EventListener/ExceptionListener.php +++ /dev/null @@ -1,49 +0,0 @@ -getThrowable(); - // - // $response = match(true) { - // $exception instanceof FilterValidationException, - // $exception instanceof BadRequestException => $this->createResponse($exception, Response::HTTP_BAD_REQUEST), - // $exception instanceof NotFoundHttpException, - // $exception instanceof ItemNotFoundException => $this->createResponse($exception, Response::HTTP_NOT_FOUND), - // $exception instanceof AccessDeniedHttpException => $this->createResponse($exception, Response::HTTP_FORBIDDEN), - // $exception instanceof ValidationException, - // $exception instanceof NotNormalizableValueException => $this->createResponse($exception, Response::HTTP_UNPROCESSABLE_ENTITY), - // default => null, - // }; - // - // if ($response) { - // $event->setResponse($response); - // }else{ - // $this->logger->error($exception->getMessage(), ['exception' => $exception]); - // } - } - - private function createResponse(\Throwable $exception, int $statusCode): Response - { - $this->logger->info($exception->getMessage(), ['exception' => $exception]); - return new Response(json_encode(['message' => $exception->getMessage()]), $statusCode, ['Content-Type' => 'application/json']); - } -} diff --git a/src/EventSubscriber/QueueStatusSubscriber.php b/src/EventSubscriber/QueueStatusSubscriber.php deleted file mode 100644 index a9e7264..0000000 --- a/src/EventSubscriber/QueueStatusSubscriber.php +++ /dev/null @@ -1,145 +0,0 @@ - 'onMessageReceived', - WorkerMessageHandledEvent::class => 'onMessageHandled', - WorkerMessageFailedEvent::class => 'onMessageFailed', - PageScrappingProgressEvent::NAME => 'onPageScrapingProgress', - ]; - } - - public function onMessageReceived(WorkerMessageReceivedEvent $event): void - { - $envelope = $event->getEnvelope(); - $message = $envelope->getMessage(); - - if ($message instanceof DownloadChapter) { - $this->activityService->sendUpdate($this->getActivity()); - } - } - - public function onMessageHandled(WorkerMessageHandledEvent $event): void - { - $envelope = $event->getEnvelope(); - $message = $envelope->getMessage(); - - if ($message instanceof DownloadChapter) { - $this->activityService->sendUpdate($this->getActivity()); - } - } - - public function onMessageFailed(WorkerMessageFailedEvent $event): void - { - $envelope = $event->getEnvelope(); - $message = $envelope->getMessage(); - - if ($message instanceof DownloadChapter) { - $this->activityService->sendUpdate($this->getActivity()); - } - } - - public function onPageScrapingProgress(PageScrappingProgressEvent $event): void - { - $data = [ - 'status' => 'scrapping.progress', - 'chapterId' => $event->getChapterId(), - 'pageIndex' => $event->getPageIndex(), - 'totalPages' => $event->getTotalPages(), - ]; - $this->activityService->sendUpdate($data); - } - - private function getActivity(): array - { - $queueStatus = $this->getQueueStatus(); - return [ - 'processing' => $this->buildStatusActivity($this->decodeMessages($queueStatus['processing'])), - 'pending' => $this->buildStatusActivity($this->decodeMessages($queueStatus['pending'])) - ]; - } - - //TODO refactorer ce code avec celui du ActivityController - private function buildStatusActivity(array $activity): array - { - $status = []; - foreach ($activity as $envelope) { - $envelope = $envelope['body']; - if ($envelope instanceof Envelope) { - if (!$envelope->getMessage() instanceof DownloadChapter) { - continue; - } - - $chapter = $this->chapterRepository->find($envelope->getMessage()->getChapterId()); - $manga = $chapter->getManga(); - $status[] = [ - 'manga' => $manga->getTitle(), - 'volume' => $chapter->getVolume(), - 'chapter' => $chapter->getNumber(), - 'title' => $chapter->getTitle(), - ]; - } - } - - return $status; - } - - private function getQueueStatus(): array - { - // Requête pour récupérer les messages en attente - $sqlPending = 'SELECT * FROM messenger_messages WHERE queue_name = :queue AND available_at IS NULL'; - $pending = $this->connection->fetchAllAssociative($sqlPending, ['queue' => 'default']); - - // Requête pour récupérer les messages en cours de traitement - $sqlProcessing = 'SELECT * FROM messenger_messages WHERE queue_name = :queue AND available_at IS NOT NULL'; - $processing = $this->connection->fetchAllAssociative($sqlProcessing, ['queue' => 'default']); - - return [ - 'pending' => $pending, - 'processing' => $processing - ]; - } - - private function decodeMessages(array $messages): array - { - $decodedMessages = []; - - foreach ($messages as $message) { - $decodedMessages[] = [ - 'id' => $message['id'], - 'body' => $this->decodeMessageBody($message['body']), - 'headers' => json_decode($message['headers'], true), - ]; - } - - return $decodedMessages; - } - - private function decodeMessageBody(string $body) - { - return unserialize(stripcslashes($body)); - } -} diff --git a/src/Form/AppSettingsType.php b/src/Form/AppSettingsType.php deleted file mode 100644 index d6e7371..0000000 --- a/src/Form/AppSettingsType.php +++ /dev/null @@ -1,30 +0,0 @@ -add('mangaDirectory', TextType::class, [ - 'label' => 'Manga Directory', - ]) - ->add('imageDirectory', TextType::class, [ - 'label' => 'Image Directory', - ]); - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'data_class' => AppSettings::class, - ]); - } -} diff --git a/src/Form/ContentSourceType.php b/src/Form/ContentSourceType.php deleted file mode 100644 index 9fa3b28..0000000 --- a/src/Form/ContentSourceType.php +++ /dev/null @@ -1,50 +0,0 @@ -add('baseUrl', UrlType::class, [ - 'label' => 'Base URL', - ]) - ->add('imageSelector', TextType::class, [ - 'label' => 'Image Selector', - ]) - ->add('chapterUrlFormat', TextType::class, [ - 'label' => 'Chapter URL Format ({slug}, {chapterNumber})', - ]) - ->add('nextPageSelector', TextType::class, [ - 'label' => 'Next Page Selector (let empty if vertical reader)', - 'required' => false, - ]) - ->add('ChapterSelector', TextType::class, [ - 'label' => 'Chapter Selector (required for Javascript scraping)', - 'required' => false, - ]) - ->add('scrapingType', ChoiceType::class, [ - 'label' => 'Scraping Type', - 'choices' => [ - 'HTML' => 'html', - 'JavaScript' => 'javascript' - ], - ]); - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'data_class' => ContentSource::class, - ]); - } -} diff --git a/src/Form/MangaEditType.php b/src/Form/MangaEditType.php deleted file mode 100644 index 5a48968..0000000 --- a/src/Form/MangaEditType.php +++ /dev/null @@ -1,95 +0,0 @@ -add('title', TextType::class, [ - 'label' => 'Titre', - 'attr' => ['class' => 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500'] - ]) - ->add('slug', TextType::class, [ - 'label' => 'Slug', - 'attr' => [ - 'readonly' => true, - 'class' => 'bg-gray-100 w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500' - ], - ]) - ->add('alternativeSlugs', CollectionType::class, [ - 'entry_type' => TextType::class, - 'allow_add' => true, - 'allow_delete' => true, - 'by_reference' => false, - 'label' => false, - 'prototype' => true, - 'entry_options' => ['attr' => ['class' => 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500'], 'label' => false], - 'required' => false, - ]) - ->add('publicationYear', NumberType::class, [ - 'label' => 'Année de publication', - 'attr' => ['class' => 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500'] - ]) - ->add('description', TextareaType::class, [ - 'label' => 'Description', - 'attr' => ['class' => 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500', 'rows' => 8] - ]) - ->add('genres', CollectionType::class, [ - 'entry_type' => TextType::class, - 'allow_add' => true, - 'allow_delete' => true, - 'by_reference' => false, - 'label' => 'Genres', - 'entry_options' => ['attr' => ['class' => 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500']], - 'required' => false, - ]) - ->add('rating', NumberType::class, [ - 'label' => 'Note', - 'attr' => ['class' => 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500'], - 'required' => false, - ]) - ->add('author', TextType::class, [ - 'label' => 'Auteur', - 'attr' => ['class' => 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500'], - 'required' => false, - ]) - ->add('status', TextType::class, [ - 'label' => 'Statut', - 'attr' => ['class' => 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500'], - 'required' => false, - ]) - ; - - $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { - $data = $event->getData(); - $manga = $event->getForm()->getData(); - - if ($manga && $manga->getSlug()) { - $data['slug'] = $manga->getSlug(); - } - - $event->setData($data); - }); - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'data_class' => Manga::class, - ]); - } -} diff --git a/src/Interface/ClientInterface.php b/src/Interface/ClientInterface.php deleted file mode 100644 index f38aa92..0000000 --- a/src/Interface/ClientInterface.php +++ /dev/null @@ -1,9 +0,0 @@ -entityManager->getRepository(AppSettings::class)->findOneBy([]); - if (!$settings) { - $settings = $this->createDefaultSettings(); - } - - return $settings; - } - - public function updateSettings(AppSettings $newSettings): void - { - $settings = $this->entityManager->getRepository(AppSettings::class)->findOneBy([]); - if (!$settings) { - $settings = new AppSettings(); - } - - $settings->setMangaDirectory($newSettings->getMangaDirectory()); - $settings->setImageDirectory($newSettings->getImageDirectory()); - - $this->entityManager->persist($settings); - $this->entityManager->flush(); - } - - private function createDefaultSettings(): AppSettings - { - $settings = new AppSettings(); - $settings->setMangaDirectory(self::DEFAULT_MANGA_DIRECTORY); - $settings->setImageDirectory(self::DEFAULT_IMAGE_DIRECTORY); - - $this->entityManager->persist($settings); - $this->entityManager->flush(); - - return $settings; - } -} diff --git a/src/Manager/FileSystemManager.php b/src/Manager/FileSystemManager.php deleted file mode 100644 index df11a6d..0000000 --- a/src/Manager/FileSystemManager.php +++ /dev/null @@ -1,120 +0,0 @@ -loadSettings(); - } - - private function loadSettings(): void - { - $settings = $this->appSettingsManager->getSettings(); - $this->mangaDirectory = $settings->getMangaDirectory(); - $this->imageDirectory = $settings->getImageDirectory(); - } - - public function getMangaDirectory(): string - { - return $this->mangaDirectory; - } - - public function getImageDirectory(): string - { - return $this->imageDirectory; - } - - public function getImagePath(string $subDir = ''): string - { - if (!$this->filesystem->exists($this->projectDir.'/'.self::IMAGES_DIRECTORY.($subDir ? "/$subDir" : ''))) { - $this->filesystem->mkdir($this->projectDir.'/'.self::IMAGES_DIRECTORY.($subDir ? "/$subDir" : ''), 0755); - } - - return $this->projectDir.'/'.self::IMAGES_DIRECTORY.($subDir ? "/$subDir" : ''); - } - - public function createMangaDirectory(string $mangaSlug, ?int $year): string - { - $year = $year ?? 'unknown'; - $directoryPath = $this->projectDir.'/'.self::CBZ_DIRECTORY.'/'.ucfirst($mangaSlug)." ($year)"; - $this->filesystem->mkdir($directoryPath, 0755); - - return $directoryPath; - } - - public function createVolumeDirectory(string $mangaDir, int $volume): string - { - $volumeDir = sprintf('%s/volume_%02d', $mangaDir, $volume); - $this->filesystem->mkdir($volumeDir, 0755); - - return $volumeDir; - } - - public function moveUploadedFile(string $sourcePath, string $destinationDir, string $originalFilename): string - { - $newFilename = $this->generateUniqueFilename($originalFilename); - $destinationPath = $destinationDir.'/'.$newFilename; - $this->filesystem->rename($sourcePath, $destinationPath, true); - - return $destinationPath; - } - - public function deleteFile(string $filePath): void - { - if ($this->filesystem->exists($filePath)) { - $this->filesystem->remove($filePath); - } - } - - public function deleteDirectory(string $directoryPath): void - { - if ($this->filesystem->exists($directoryPath)) { - $this->filesystem->remove($directoryPath); - } - } - - public function fileExists(string $filePath): bool - { - return $this->filesystem->exists($filePath); - } - - public function moveFile(string $sourcePath, string $destinationPath): void - { - $this->filesystem->rename($sourcePath, $destinationPath, true); - } - - public function getUploadsDirectory(): string - { - return $this->projectDir.'/'.self::UPLOADS_DIRECTORY; - } - - private function generateUniqueFilename(string $originalFilename): string - { - $safeFilename = $this->slugger->slug(pathinfo($originalFilename, PATHINFO_FILENAME)); - - return $safeFilename.'-'.uniqid().'.'.pathinfo($originalFilename, PATHINFO_EXTENSION); - } - - public function generateUniqueImageFilename(string $originalFilename): string - { - $safeFilename = $this->slugger->slug(pathinfo($originalFilename, PATHINFO_FILENAME)); - - return $safeFilename.'-'.uniqid().'.jpg'; - } -} diff --git a/src/Manager/Toolbar/Definition/ActivityToolbar.php b/src/Manager/Toolbar/Definition/ActivityToolbar.php deleted file mode 100644 index 3c6b666..0000000 --- a/src/Manager/Toolbar/Definition/ActivityToolbar.php +++ /dev/null @@ -1,20 +0,0 @@ -addToLeftGroup(new ToolbarButton('arrows-rotate', 'Refresh', 'toolbar#refreshActivity')) - ->addToLeftGroup(new ToolbarDivider()) - ->addToLeftGroup(new ToolbarButton('trash-can', 'Remove Selected', 'toolbar#removeActivity')) - - ->addToRightGroup(new ToolbarButton('th-large', 'Options', 'toolbar#optionActivity')) - ; - } -} diff --git a/src/Manager/Toolbar/Definition/ChapterListToolbar.php b/src/Manager/Toolbar/Definition/ChapterListToolbar.php deleted file mode 100644 index 77a586d..0000000 --- a/src/Manager/Toolbar/Definition/ChapterListToolbar.php +++ /dev/null @@ -1,29 +0,0 @@ -addToLeftGroup(new ToolbarButton('arrows-rotate', 'Refresh metadata', 'toolbar#refreshMetadata', $contextData)) - ->addToLeftGroup(new ToolbarDivider()) - ->addToLeftGroup(new ToolbarButton('keyboard', 'Rename chapters', 'toolbar#renameChapters')) - ->addToLeftGroup(new ToolbarButton('file-zipper', 'Manage cbz', 'toolbar#manageCbz', $contextData)) - ->addToLeftGroup(new ToolbarButton('gear', 'Preferred Sources', 'toolbar#editPreferredSources', $contextData)) - - - ->addToRightGroup(new ToolbarButton('bookmark', $monitoredTitle, 'toolbar#monitoring', array_merge($contextData, ['buttonClass' => $monitoredColor]))) - ->addToRightGroup(new ToolbarButton('wrench', 'Edit', 'toolbar#editManga', $contextData)) - ->addToRightGroup(new ToolbarButton('trash-can', 'Delete', 'toolbar#deleteManga', $contextData)) - ->addToRightGroup(new ToolbarDivider()) - ->addToRightGroup(new ToolbarButton('chevron-down', 'Expand all', 'toolbar#expandAll')); - } -} diff --git a/src/Manager/Toolbar/Definition/MangaListToolbar.php b/src/Manager/Toolbar/Definition/MangaListToolbar.php deleted file mode 100644 index 7f3c561..0000000 --- a/src/Manager/Toolbar/Definition/MangaListToolbar.php +++ /dev/null @@ -1,35 +0,0 @@ -addToLeftGroup(new ToolbarButton('arrows-rotate', 'Refresh', 'toolbar#refreshMetadata')) - ->addToLeftGroup(new ToolbarButton('search', 'Search', 'toolbar#searchLastChapter')) - - ->addToRightGroup(new ToolbarButton('th-large', 'Options', 'toolbar#options')) - ->addToRightGroup(new ToolbarDivider()) - ->addToRightGroup(new ToolbarDropdown('eye', 'View', 'changeView', [ - ['text' => 'Poster View', 'action' => 'changeView', 'data' => ['view' => 'poster']], - ['text' => 'Table View', 'action' => 'changeView', 'data' => ['view' => 'table']], - ['text' => 'Resume View', 'action' => 'changeView', 'data' => ['view' => 'resume']] - ])) - ->addToRightGroup(new ToolbarDropdown('sort', 'Sort', 'sort', [ - ['text' => 'Par titre', 'action' => 'sort', 'data' => ['sort' => 'title']], - ['text' => 'Par année de publication', 'action' => 'sort', 'data' => ['sort' => 'publicationYear']], - ['text' => 'Par date d\'ajout', 'action' => 'sort', 'data' => ['sort' => 'createdAt']] - ])) - ->addToRightGroup(new ToolbarDropdown('filter', 'Filter', 'filter', [ - ['text' => 'Tous les mangas', 'action' => 'filter', 'data' => ['filter' => 'all']], - ['text' => 'Mangas en cours', 'action' => 'filter', 'data' => ['filter' => 'ongoing']], - ['text' => 'Mangas terminés', 'action' => 'filter', 'data' => ['filter' => 'completed']] - ])) - ; - } -} diff --git a/src/Manager/Toolbar/Definition/ScraperListToolbar.php b/src/Manager/Toolbar/Definition/ScraperListToolbar.php deleted file mode 100644 index a27b2ce..0000000 --- a/src/Manager/Toolbar/Definition/ScraperListToolbar.php +++ /dev/null @@ -1,16 +0,0 @@ -addToRightGroup(new ToolbarButton('file-import', 'Import Json', 'toolbar#openImportModal')) - ->addToRightGroup(new ToolbarDivider()) - ->addToRightGroup(new ToolbarButton('file-export', 'Export Json', 'toolbar#openExportModal')); - } -} diff --git a/src/Manager/Toolbar/Definition/Toolbar.php b/src/Manager/Toolbar/Definition/Toolbar.php deleted file mode 100644 index 3c60582..0000000 --- a/src/Manager/Toolbar/Definition/Toolbar.php +++ /dev/null @@ -1,31 +0,0 @@ -leftGroup[] = $element; - return $this; - } - - public function addToRightGroup(ToolbarElement $element): self - { - $this->rightGroup[] = $element; - return $this; - } - - public function getGroups(): array - { - return [ - 'leftGroup' => $this->leftGroup, - 'rightGroup' => $this->rightGroup, - ]; - } -} diff --git a/src/Manager/Toolbar/Element/AbstractToolbarElement.php b/src/Manager/Toolbar/Element/AbstractToolbarElement.php deleted file mode 100644 index f47de3d..0000000 --- a/src/Manager/Toolbar/Element/AbstractToolbarElement.php +++ /dev/null @@ -1,37 +0,0 @@ -icon = $icon; - $this->text = $text; - $this->action = $action; - } - - public function getIcon(): string - { - return $this->icon; - } - - public function getText(): string|array - { - return $this->text; - } - - public function getAction(): string - { - return $this->action; - } - - public function getAdditionalProperties(): array - { - return []; - } -} diff --git a/src/Manager/Toolbar/Element/ToolbarButton.php b/src/Manager/Toolbar/Element/ToolbarButton.php deleted file mode 100644 index fceaae6..0000000 --- a/src/Manager/Toolbar/Element/ToolbarButton.php +++ /dev/null @@ -1,24 +0,0 @@ -data = $data; - } - - public function getType(): string - { - return 'button'; - } - - public function getAdditionalProperties(): array - { - return ['data' => $this->data]; - } -} diff --git a/src/Manager/Toolbar/Element/ToolbarDivider.php b/src/Manager/Toolbar/Element/ToolbarDivider.php deleted file mode 100644 index 5fbd3bb..0000000 --- a/src/Manager/Toolbar/Element/ToolbarDivider.php +++ /dev/null @@ -1,15 +0,0 @@ -items = $items; - } - - public function getType(): string - { - return 'dropdown'; - } - - public function getAdditionalProperties(): array - { - return ['items' => $this->items]; - } -} diff --git a/src/Manager/Toolbar/Element/ToolbarElement.php b/src/Manager/Toolbar/Element/ToolbarElement.php deleted file mode 100644 index db3bcd4..0000000 --- a/src/Manager/Toolbar/Element/ToolbarElement.php +++ /dev/null @@ -1,12 +0,0 @@ - new MangaListToolbar(), - 'chapter_list' => new ChapterListToolbar($context), - 'activity' => new ActivityToolbar($context), - 'scraper_list' => new ScraperListToolbar($context), - default => throw new \InvalidArgumentException("Unknown toolbar type: $type"), - }; - } -} diff --git a/src/Message/DownloadChapter.php b/src/Message/DownloadChapter.php deleted file mode 100644 index 243b3bd..0000000 --- a/src/Message/DownloadChapter.php +++ /dev/null @@ -1,15 +0,0 @@ -chapterId; - } -} diff --git a/src/Message/RefreshAndDownloadChapters.php b/src/Message/RefreshAndDownloadChapters.php deleted file mode 100644 index 2631a8e..0000000 --- a/src/Message/RefreshAndDownloadChapters.php +++ /dev/null @@ -1,23 +0,0 @@ -name = $name; - // } - - // public function getName(): string - // { - // return $this->name; - // } -} diff --git a/src/Message/RefreshMetadata.php b/src/Message/RefreshMetadata.php deleted file mode 100644 index 0d3dd29..0000000 --- a/src/Message/RefreshMetadata.php +++ /dev/null @@ -1,15 +0,0 @@ -mangaId; - } -} diff --git a/src/MessageHandler/DownloadChapterHandler.php b/src/MessageHandler/DownloadChapterHandler.php deleted file mode 100644 index ba91a75..0000000 --- a/src/MessageHandler/DownloadChapterHandler.php +++ /dev/null @@ -1,103 +0,0 @@ -chapterRepository->find($message->getChapterId()); - if (!$chapter) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapter not found.']); - throw new BadRequestHttpException('Chapter not found'); - } elseif (null !== $chapter->getCbzPath()) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapter already scraped.']); - throw new BadRequestHttpException('Chapter already downloaded'); - } - - $manga = $chapter->getManga(); - $preferredSources = $manga->getPreferredSources()->toArray(); - $allSources = $this->contentSourceRepository->findAll(); - - $filteredSources = array_udiff($allSources, $preferredSources, function ($a, $b) { - return $a->getId() - $b->getId(); - }); - - $sources = array_merge($preferredSources, $filteredSources); - - if (count($preferredSources) > 0) { - $sources = $preferredSources; - } else { - $sources = $allSources; - } - - // $sources[] = - // (new ContentSource()) - // ->setBaseUrl('https://api.mangadex.org/') - // ->setImageSelector('img') - // ->setChapterUrlFormat('at-home/server/%s') - // ->setScrapingType('mangadex'); - - // (new ContentSource()) - // ->setBaseUrl('https://lelscans.net') - // ->setImageSelector('#image img') - // ->setChapterUrlFormat('https://lelscans.net/scan-%s/%s') - // ->setNextPageSelector('a[title="Suivant"]') - // ->setScrapingType('html'), - // (new ContentSource()) - // ->setBaseUrl('https://darkscans.net/') - // ->setImageSelector('.reading-content img') - // ->setChapterUrlFormat('https://darkscans.net/mangas/%s/chapter-%s/') - // ->setNextPageSelector(null) - // ->setScrapingType('html') - - $scrapedSuccessfully = false; - - foreach ($sources as $source) { - try { - $this->mangaScraperService->scrapeChapter($chapter, $source); - $scrapedSuccessfully = true; - break; - } catch (\Exception $e) { - $this->notificationService->sendUpdate([ - 'status' => 'warning', - 'message' => 'An error occurred while scraping with source: '.$source->getBaseUrl().'. Trying next source...', - ]); - } catch (GuzzleException $e) { - - } - } - - if (!$scrapedSuccessfully) { - $this->notificationService->sendUpdate([ - 'status' => 'error', - 'message' => 'All sources failed to scrape the chapter '.$chapter->getManga()->getTitle().' '.$chapter->getNumber().'.', - ]); - throw new \Exception('All sources failed to scrape the chapter '.$chapter->getManga()->getTitle().' '.$chapter->getNumber().'.'); - } - - $this->notificationService->sendUpdate(['status' => 'success', 'message' => 'Chapter scraped successfully.']); - } -} diff --git a/src/MessageHandler/RefreshAndDownloadChaptersHandler.php b/src/MessageHandler/RefreshAndDownloadChaptersHandler.php deleted file mode 100644 index cb655f3..0000000 --- a/src/MessageHandler/RefreshAndDownloadChaptersHandler.php +++ /dev/null @@ -1,60 +0,0 @@ -mangaRepository->findBy(['monitored' => true]); - - foreach ($mangas as $manga) { - $chapters = $this->refreshMangas($manga); - - if (empty($chapters)) { - continue; - } - - /** @var Chapter $chapter */ - foreach ($chapters as $chapter) { - $this->bus->dispatch(new DownloadChapter($chapter->getId())); - } - } - - } - - private function refreshMangas(Manga $manga): array - { - $lastChapters = $this->mangadexProvider->addAllChaptersToManga($manga); - - foreach ($lastChapters as $chapter) { - $this->entityManager->persist($chapter); - } - - $this->entityManager->persist($manga); - $this->entityManager->flush(); - - return $lastChapters; - } -} diff --git a/src/MessageHandler/RefreshMetadataHandler.php b/src/MessageHandler/RefreshMetadataHandler.php deleted file mode 100644 index d63abd7..0000000 --- a/src/MessageHandler/RefreshMetadataHandler.php +++ /dev/null @@ -1,49 +0,0 @@ -mangaRepository->find($message->getMangaId()); - if (!$manga) { - return; - } - - $lastChapters = $this->mangadexProvider->addAllChaptersToManga($manga); - - try { - foreach ($lastChapters as $chapter) { - $this->entityManager->persist($chapter); - } - - $this->entityManager->persist($manga); - $this->entityManager->flush(); - } catch (\Exception $e) { - if ($e instanceof UniqueConstraintViolationException) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while refreshing ' . $manga->getTitle() . '.']); - return; - } - } - - $this->notificationService->sendUpdate(['status' => 'success', 'message' => $manga->getTitle() . ' refreshed, ' . count($lastChapters) . ' new chapters added.']); - } -} diff --git a/src/Scheduler/MainSchedule.php b/src/Scheduler/MainSchedule.php deleted file mode 100644 index 539f7e0..0000000 --- a/src/Scheduler/MainSchedule.php +++ /dev/null @@ -1,25 +0,0 @@ -add( - RecurringMessage::every('6 hours', new RefreshAndDownloadChapters()) - ) - ->stateful($this->cache); - } -} diff --git a/src/Service/ActivityService.php b/src/Service/ActivityService.php deleted file mode 100644 index e87b8cf..0000000 --- a/src/Service/ActivityService.php +++ /dev/null @@ -1,20 +0,0 @@ -hub->publish($update); - } -} diff --git a/src/Service/CbrToCbzConverter.php b/src/Service/CbrToCbzConverter.php deleted file mode 100644 index 7fe41cb..0000000 --- a/src/Service/CbrToCbzConverter.php +++ /dev/null @@ -1,67 +0,0 @@ -tempDir = $projectDir . '/public/tmp'; - $this->filesystem = new Filesystem(); - } - - public function convert(string $cbrPath): string - { - $tempDir = $this->tempDir . '/' . uniqid('cbr_conversion_'); - $this->filesystem->mkdir($tempDir); - - $extractDir = $tempDir . '/extract'; - $this->filesystem->mkdir($extractDir); - - $process = new Process(['unrar-free', 'x', $cbrPath, $extractDir]); - $process->run(); - - // Si unrar échoue, essayer avec 7z - if (!$process->isSuccessful()) { - $process = new Process(['7z', 'x', $cbrPath, "-o$extractDir"]); - $process->run(); - - if (!$process->isSuccessful()) { - throw new \RuntimeException("Extraction failed: " . $process->getErrorOutput()); - } - } - - // Créer le CBZ - $cbzFileName = pathinfo($cbrPath, PATHINFO_FILENAME) . '.cbz'; - $cbzPath = $this->tempDir . '/' . $cbzFileName; - $zip = new \ZipArchive(); - if ($zip->open($cbzPath, \ZipArchive::CREATE) !== true) { - throw new \RuntimeException("Cannot create ZIP file"); - } - - $files = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($extractDir), - \RecursiveIteratorIterator::LEAVES_ONLY - ); - - foreach ($files as $file) { - if (!$file->isDir()) { - $filePath = $file->getRealPath(); - $relativePath = substr($filePath, strlen($extractDir) + 1); - $zip->addFile($filePath, $relativePath); - } - } - - $zip->close(); - - $this->filesystem->remove($tempDir); - - return $cbzPath; - } -} diff --git a/src/Service/CbzService.php b/src/Service/CbzService.php deleted file mode 100644 index 569ccde..0000000 --- a/src/Service/CbzService.php +++ /dev/null @@ -1,221 +0,0 @@ -extractInfoFromFileName($originalFileName); - - $metadata['title'] = $fileInfo['title']; - $metadata['volume'] = null !== $fileInfo['volume'] ? (int) $fileInfo['volume'] : null; - $metadata['chapter'] = null !== $fileInfo['chapter'] ? (int) $fileInfo['chapter'] : null; - - if (is_null($metadata['chapter'])) { - try { - $zip->open($filePath); - $chapterNumbers = []; - - for ($i = 0; $i < $zip->numFiles; ++$i) { - $stat = $zip->statIndex($i); - $fileName = $stat['name']; - - $chapterNumbers[] = $this->extractChapter($fileName); - } - - $chapterNumbers = array_unique($chapterNumbers); - - if (1 === count($chapterNumbers)) { - $metadata['chapter'] = '' === array_values($chapterNumbers)[0] ? null : (int) array_values($chapterNumbers)[0]; - } elseif (count($chapterNumbers) > 1) { - $metadata['chapter'] = min($chapterNumbers); - } - - $zip->close(); - } catch (\Exception $e) { - throw new \Exception("Impossible d'ouvrir le fichier CBZ. ".$e->getMessage()); - } - } - - return $metadata; - } - - public function getPageContent(string $cbzPath, int $pageNumber): ?string - { - $zip = new \ZipArchive(); - if (true === $zip->open($cbzPath)) { - $images = $this->getImageList($zip); - if (isset($images[$pageNumber - 1])) { - $content = $zip->getFromName($images[$pageNumber - 1]); - $zip->close(); - - return $content; - } - $zip->close(); - } - - return null; - } - - public function getPageCount(string $cbzPath): int - { - $zip = new \ZipArchive(); - if (true === $zip->open($cbzPath)) { - $count = count($this->getImageList($zip)); - $zip->close(); - - return $count; - } - - return 0; - } - - private function extractInfoFromFileName(string $fileName): array - { - $title = $this->extractTitle($fileName); - $volume = $this->extractVolume($fileName); - $chapter = $this->extractChapter($fileName); - - return [ - 'title' => '' === $title ? null : $title, - 'volume' => '' === $volume ? null : $volume, - 'chapter' => '' === $chapter ? null : $chapter, - ]; - } - - private function extractTitle(string $fileName): string - { - $titlePattern = '/^(?P.+?)(?:\s*-\s*|\s+)?(?:(?:[Tt]ome|[Vv]ol\.?|[Tt]|[Cc]hap(?:itre|ter)?)\s*\d+)/'; - if (preg_match($titlePattern, $fileName, $matches)) { - return $this->slugger->slug(trim($matches['title']), '-')->lower()->toString(); - } - - $newFormatPattern = '/^(?P<title>.*?)_\d+/'; - if (preg_match($newFormatPattern, $fileName, $matches)) { - return $this->slugger->slug(trim($matches['title']), '-')->lower()->toString(); - } - - return $this->slugger->slug(pathinfo($fileName, PATHINFO_FILENAME), '-')->lower()->toString(); - } - - private function extractVolume(string $fileName): string - { - $volumePattern = '/(?:[Tt]ome|[Vv]ol\.?|[Tt])\s*(?P<volume>\d+)/'; - if (preg_match($volumePattern, $fileName, $matches)) { - return str_pad($matches['volume'], 2, '0', STR_PAD_LEFT); - } - - return ''; - } - - private function extractChapter(string $fileName): string - { - $chapterPattern = '/[Cc]hap(?:itre|ter)?\s*(?P<chapter>\d+)/'; - if (preg_match($chapterPattern, $fileName, $matches)) { - return $matches['chapter']; - } - - $newFormatPattern = '/_(?P<chapter>\d+)(?:\.\w+)?$/'; - if (preg_match($newFormatPattern, $fileName, $matches)) { - return $matches['chapter']; - } - - return ''; - } - - private function getImageList(\ZipArchive $zip): array - { - $images = []; - for ($i = 0; $i < $zip->numFiles; ++$i) { - $filename = $zip->getNameIndex($i); - if (preg_match('/\.(jpg|jpeg|png|gif)$/i', $filename)) { - $images[] = $filename; - } - } - sort($images); - - return $images; - } - - public function createVolumeArchive(array $chapters): string - { - $tempFile = tempnam(sys_get_temp_dir(), 'volume_cbz_'); - $zip = new \ZipArchive(); - if (true !== $zip->open($tempFile, \ZipArchive::CREATE)) { - throw new \RuntimeException('Impossible de créer le fichier ZIP temporaire.'); - } - - foreach ($chapters as $chapter) { - $chapterZip = new \ZipArchive(); - if (true === $chapterZip->open($chapter->getCbzPath())) { - for ($i = 0; $i < $chapterZip->numFiles; ++$i) { - $filename = $chapterZip->getNameIndex($i); - $fileContent = $chapterZip->getFromIndex($i); - $zip->addFromString('Chapter '.$chapter->getNumber().'/'.$filename, $fileContent); - } - $chapterZip->close(); - } - } - - $zip->close(); - - return $tempFile; - } - - public function generateFileName(Manga $manga, int $volume = null, float $chapterNumber = null): string - { - $sluggedTitle = $this->slugger->slug($manga->getTitle())->lower(); - if (null !== $volume) { - return sprintf('%s_volume_%02d.cbz', $sluggedTitle, $volume); - } elseif (null !== $chapterNumber) { - return sprintf('%s_chapter_%s.cbz', $sluggedTitle, number_format($chapterNumber, 2)); - } else { - throw new \InvalidArgumentException('Either volume or chapter number must be provided'); - } - } - - public function createBinaryFileResponse(string $filePath, string $fileName): BinaryFileResponse - { - $response = new BinaryFileResponse($filePath); - $response->setContentDisposition( - ResponseHeaderBag::DISPOSITION_ATTACHMENT, - $fileName - ); - - return $response; - } - - public function areAllChaptersCbzIdentical(array $chapters): bool - { - if (empty($chapters)) { - return false; - } - $firstCbzPath = $chapters[0]->getCbzPath(); - - return array_reduce($chapters, function ($carry, $chapter) use ($firstCbzPath) { - return $carry && $chapter->getCbzPath() === $firstCbzPath; - }, true); - } - - public function doAllChaptersHaveCbz(array $chapters): bool - { - return array_reduce($chapters, function ($carry, $chapter) { - return $carry && null !== $chapter->getCbzPath(); - }, true); - } -} diff --git a/src/Service/ChapterUrlGenerator.php b/src/Service/ChapterUrlGenerator.php deleted file mode 100644 index 5ec6829..0000000 --- a/src/Service/ChapterUrlGenerator.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -namespace App\Service; - -use Symfony\Component\HttpClient\Exception\InvalidArgumentException; - -class ChapterUrlGenerator -{ - private string $chapterUrlFormat; - - public function __construct(string $chapterUrlFormat) - { - $this->chapterUrlFormat = $chapterUrlFormat; - $this->validateUrlFormat($chapterUrlFormat); - } - - public function getChapterUrl(string $mangaTitle, float $chapterNumber): string - { - $placeholders = [ - '{chapterNumber}' => $chapterNumber, - '{slug}' => $mangaTitle, - ]; - - return str_replace(array_keys($placeholders), array_values($placeholders), $this->chapterUrlFormat); - } - - private function validateUrlFormat(string $format): void - { - if (!str_contains($format, '{slug}')) { - throw new InvalidArgumentException("The URL format must contain both {slug} and {chapterNumber} placeholders."); - } - } - -} diff --git a/src/Service/MangaImportService.php b/src/Service/MangaImportService.php deleted file mode 100644 index 25411a4..0000000 --- a/src/Service/MangaImportService.php +++ /dev/null @@ -1,103 +0,0 @@ -<?php - -namespace App\Service; - -use App\Entity\Chapter; -use App\Entity\Manga; -use App\Manager\FileSystemManager; -use App\Repository\ChapterRepository; -use Doctrine\ORM\EntityManagerInterface; -use Exception; -use Symfony\Component\String\Slugger\SluggerInterface; - -readonly class MangaImportService -{ - public function __construct( - private FileSystemManager $fileSystemManager, - private EntityManagerInterface $entityManager, - private ChapterRepository $chapterRepository, - private SluggerInterface $slugger - ) { - } - - /** - * @throws Exception - */ - public function importFile(Manga $manga, ?int $volume, ?Chapter $chapter, string $tempFilePath): void - { - if ($chapter !== null) { - $this->importChapter($manga, $chapter, $tempFilePath); - } elseif ($volume !== null) { - $this->importVolume($manga, $volume, $tempFilePath); - } else { - throw new \RuntimeException("Impossible de déterminer s'il s'agit d'un volume ou d'un chapitre."); - } - } - - /** - * @throws Exception - */ - private function importVolume(Manga $manga, int $volume, string $tempFilePath): void - { - $permanentFileName = $this->createPermanentFileName($manga, $volume); - $mangaDirectory = $this->fileSystemManager->createMangaDirectory($manga->getSlug(), $manga->getPublicationYear()); - $volumeDirectory = $this->fileSystemManager->createVolumeDirectory($mangaDirectory, $volume); - $permanentFilePath = $volumeDirectory . '/' . $permanentFileName; - - if ($this->fileSystemManager->fileExists($permanentFilePath)) { - throw new \RuntimeException("Un fichier pour ce volume existe déjà."); - } - - $this->fileSystemManager->moveFile($tempFilePath, $permanentFilePath); - - $this->updateVolumeChapters($manga, $volume, $permanentFilePath); - $this->entityManager->flush(); - } - - /** - * @throws Exception - */ - private function importChapter(Manga $manga, Chapter $chapter, string $tempFilePath): void - { - $volume = $chapter->getVolume(); - $permanentFileName = $this->createPermanentFileName($manga, $volume, $chapter->getNumber()); - $mangaDirectory = $this->fileSystemManager->createMangaDirectory($manga->getSlug(), $manga->getPublicationYear()); - $volumeDirectory = $this->fileSystemManager->createVolumeDirectory($mangaDirectory, $chapter->getVolume()); - $permanentFilePath = $volumeDirectory . '/' . $permanentFileName; - - if ($this->fileSystemManager->fileExists($permanentFilePath)) { - throw new \RuntimeException("Un fichier pour ce chapitre existe déjà."); - } - - $this->fileSystemManager->moveFile($tempFilePath, $permanentFilePath); - - $chapter->setCbzPath($permanentFilePath); - - $this->entityManager->flush(); - } - - private function createPermanentFileName(Manga $manga, int $volume, ?float $chapterNumber = null): string - { - $baseFileName = $this->slugger->slug($manga->getTitle()) . '_vol' . sprintf('%02d', $volume); - if ($chapterNumber !== null) { - $baseFileName .= '_ch' . $chapterNumber; - } - return $baseFileName . '.cbz'; - } - - private function updateVolumeChapters(Manga $manga, int $volume, string $cbzPath): void - { - $chapters = $this->chapterRepository->findBy([ - 'manga' => $manga, - 'volume' => $volume - ]); - - if (empty($chapters)) { - throw new \RuntimeException("Aucun chapitre trouvé pour le volume $volume en base de données."); - } - - foreach ($chapters as $chapter) { - $chapter->setCbzPath($cbzPath); - } - } -} diff --git a/src/Service/MangaScraperService.php b/src/Service/MangaScraperService.php deleted file mode 100644 index d68d3f7..0000000 --- a/src/Service/MangaScraperService.php +++ /dev/null @@ -1,625 +0,0 @@ -<?php - -namespace App\Service; - -use App\Entity\Chapter; -use App\Entity\Manga; -use App\Entity\ContentSource; -use App\Event\PageScrappingProgressEvent; -use App\Repository\ChapterRepository; -use App\Repository\MangaRepository; -use Doctrine\ORM\EntityManagerInterface; -use Exception; -use Facebook\WebDriver\Remote\RemoteWebElement; -use Facebook\WebDriver\WebDriverExpectedCondition; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Exception\RequestException; -use Symfony\Component\DomCrawler\Crawler; -use Symfony\Component\Routing\Matcher\UrlMatcher; -use Symfony\Component\Routing\RequestContext; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Panther\Client as PantherClient; - -class MangaScraperService -{ - public const string PUBLIC_CBZ = '/public/cbz'; - - public function __construct( - private readonly string $projectDir, - private readonly EventDispatcherInterface $eventDispatcher, - private readonly EntityManagerInterface $entityManager, - private readonly MangaRepository $mangaRepository, - ) { - } - - private function extractMangaPageData(string $html, ContentSource $mangaSource): array - { - $crawler = new Crawler($html); - $imgUrl = $crawler->filter($mangaSource->getImageSelector())->attr('src') - ?? $crawler->filter($mangaSource->getImageSelector())->attr('data-src'); - - // dd($imgUrl); - - // if (empty($imgUrl)) { - // throw new \Exception('No valid image found on the page.'); - // } - - $nextLink = $crawler->filter($mangaSource->getNextPageSelector()); - $nextUrl = $nextLink->count() > 0 ? $nextLink->attr('href') : null; - - // Convert relative URLs to absolute URLs - if (!preg_match('/^https?:\/\//', $imgUrl)) { - $urlComponents = parse_url($mangaSource->getBaseUrl()); - $scheme = $urlComponents['scheme']; - $host = $urlComponents['host']; - $imgUrl = $scheme . '://' . $host . '/' . ltrim($imgUrl, '/'); - } - - return [ - 'image_url' => $imgUrl, - 'next_page_url' => $nextUrl, - ]; - } - - /** - * @throws GuzzleException - */ - public function scrapeManga(Manga $manga, ContentSource $mangaSource): array - { - $allChaptersData = []; - - foreach ($manga->getChapters() as $chapter) { - $chapterData = $this->scrapeChapter($chapter, $mangaSource); - if ($chapterData !== false) { - $allChaptersData[$chapter->getNumber()] = $chapterData; - } - } - - return $allChaptersData; - } - - /** - * @throws GuzzleException - * @throws Exception - */ - public function scrapeChapter(Chapter $chapter, ContentSource $mangaSource): array|bool - { - return match ($mangaSource->getScrapingType()) { - 'html' => $this->scrapeChapterHtml($chapter->getManga(), $chapter, $mangaSource), - 'javascript' => $this->scrapeChapterJavaScript($chapter->getManga(), $chapter, $mangaSource), - 'mangadex' => $this->scrapeChapterMangadex($chapter, $mangaSource), - default => throw new Exception('Unsupported scraping type: ' . $mangaSource->getScrapingType()), - }; - } - - /** - * @throws GuzzleException - * @throws Exception - */ - private function scrapeChapterMangadex(Chapter $chapter, ContentSource $mangaSource): bool - { - $client = new Client(); - $chapterUrl = $mangaSource->getBaseUrl() . sprintf($mangaSource->getChapterUrlFormat(), $chapter->getExternalId()); - $manga = $chapter->getManga(); - $pageData = []; - - $response = $client->get($chapterUrl); - $results = json_decode($response->getBody()->getContents(), true); - - if ($results['result'] !== 'ok' || count($results['chapter']['dataSaver']) === 0) { - throw new Exception('Error while fetching chapter data from Mangadex ' . $manga->getTitle() . ' ' . $chapter->getNumber()); - } - - $tempDir = sys_get_temp_dir() . '/' . uniqid('manga_scraper_'); - mkdir($tempDir); - - foreach ($results['chapter']['dataSaver'] as $index => $page) { - $pageUrl = $results['baseUrl'] . '/data-saver/' . $results['chapter']['hash'] . '/' . $page; - $imagePath = $tempDir . '/' . sprintf('%03d.%s', $index + 1, pathinfo($page, PATHINFO_EXTENSION)); - - $this->downloadAndSaveImage($pageUrl, $imagePath); - - $event = new PageScrappingProgressEvent($chapter->getId(), $index + 1, count($results['chapter']['dataSaver'])); - $this->eventDispatcher->dispatch($event, PageScrappingProgressEvent::NAME); - - $pageData[] = [ - 'image_url' => $pageUrl, - 'local_image_url' => $imagePath, - 'page_number' => $index + 1, - ]; - } - - $cbzFilePath = $this->generateCbzPath($manga, $chapter); - $this->createCbzFile($tempDir, $pageData, $cbzFilePath); - - $chapter->setCbzPath($cbzFilePath); - $this->entityManager->persist($chapter); - $this->entityManager->flush(); - - // Nettoyage du répertoire temporaire - $this->cleanupTempFiles($tempDir); - - return true; - } - - private function scrapeChapterJavascript(Manga $manga, Chapter $chapter, ContentSource $mangaSource): array|bool - { - $pantherClient = PantherClient::createChromeClient(); - $chapterUrl = $mangaSource->getChapterUrl($manga->getSlug(), $chapter->getNumber()); - - $pantherClient->request('GET', $chapterUrl); - - // Sélection du chapitre dans le menu déroulant - try { - $crawler = $pantherClient->waitFor('body'); - $select = $crawler->filter('#selectChapitres'); - - if ($select->count() > 0) { - $chapterNumber = $chapter->getNumber(); - $options = $select->filter('option'); - $targetindex = null; - - /** @var RemoteWebElement $option */ - foreach ($options->getIterator() as $index => $option) { - $optionText = $option->getText(); - // Recherche plus flexible du numéro de chapitre - if (preg_match("/\b{$chapterNumber}\b/", $optionText)) { - $targetIndex = $index; - break; - } - } - - - if ($targetIndex !== null) { - $pantherClient->executeScript(" - var select = document.querySelector('#selectChapitres'); - select.selectedIndex = $targetIndex; - select.dispatchEvent(new Event('change')); - "); - - // Attendre que la page se mette à jour après la sélection - $pantherClient->wait(60000)->until( // 60 secondes de timeout - function ($driver) { - return $driver->executeScript(" - var scansPlacement = document.querySelector('#scansPlacement'); - if (!scansPlacement) return false; - - var lazyImages = scansPlacement.querySelectorAll('img.lazy'); - var loadingGif = scansPlacement.querySelector('img[src*=\"loading_scans.gif\"]'); - - // Vérifier que toutes les images lazy sont chargées et que le GIF de chargement n'est plus présent - var allImagesLoaded = Array.from(lazyImages).every(img => img.complete && img.naturalWidth > 0); - - return lazyImages.length > 0 && allImagesLoaded && !loadingGif; - "); - } - ); - } else { - throw new \Exception("Chapitre $chapterNumber non trouvé dans le menu déroulant"); - } - } - } catch (\Exception $e) { - // $this->logger->warning('Erreur lors de la sélection du chapitre : ' . $e->getMessage()); - $pantherClient->close(); - return false; - } - - $pageData = []; - - try { - if ($mangaSource->getNextPageSelector() === null) { - // Lecteur vertical - $pageData = $this->scrapeVerticalReaderJavascript($pantherClient, $mangaSource, $chapter); - } else { - // Lecteur horizontal - $pageData = $this->scrapeHorizontalReaderJavascript($pantherClient, $mangaSource, $chapter); - } - } catch (\Exception $e) { - throw $e; - // $this->logger->warning('Erreur lors du scraping du chapitre ' . $chapter->getNumber() . ' du manga ' . $manga->getTitle() . ': ' . $e->getMessage()); - } finally { - $pantherClient->close(); - } - - return $pageData; - } - - private function scrapeVerticalReaderJavascript(PantherClient $pantherClient, ContentSource $mangaSource, Chapter $chapter): array - { - $pageData = []; - $pageNumber = 1; - - $crawler = $pantherClient->waitFor($mangaSource->getImageSelector()); - $images = $crawler->filter($mangaSource->getImageSelector()); - - foreach ($images->getIterator() as $image) { - $imageUrl = $image->getAttribute('src') ?: $image->getAttribute('data-src'); - - $pageData[] = [ - 'image_url' => $this->cleanImageUrl($imageUrl), - 'page_number' => $pageNumber, - ]; - - $event = new PageScrappingProgressEvent($chapter->getId(), $pageNumber, $images->count()); - $this->eventDispatcher->dispatch($event, PageScrappingProgressEvent::NAME); - - $pageNumber++; - } - - return $pageData; - } - - private function scrapeHorizontalReaderJavascript(PantherClient $pantherClient, ContentSource $mangaSource, Chapter $chapter): array - { - $pageData = []; - $pageNumber = 1; - - while (true) { - try { - $crawler = $pantherClient->waitFor($mangaSource->getImageSelector()); - - $imageElement = $crawler->filter($mangaSource->getImageSelector())->first(); - if ($imageElement->count() === 0) { - break; // Fin du chapitre - } - - $imageUrl = $imageElement->attr('src') ?: $imageElement->attr('data-src'); - - $pageData[] = [ - 'image_url' => $this->cleanImageUrl($imageUrl), - 'page_number' => $pageNumber, - ]; - - $event = new PageScrappingProgressEvent($chapter->getId(), $pageNumber, 0); - $this->eventDispatcher->dispatch($event, PageScrappingProgressEvent::NAME); - - // Passer à la page suivante - $nextButton = $pantherCrawler->filter($mangaSource->getNextPageSelector()); - if ($nextButton->count() === 0) { - break; // Pas de bouton suivant, fin du chapitre - } - - $nextButton->click(); - - // Attendre que la page change - $pantherClient->waitFor($mangaSource->getImageSelector(), 10); - - // Mettre à jour le crawler avec le nouveau contenu de la page - $pantherCrawler = $pantherClient->refreshCrawler(); - - $pageNumber++; - } catch (\Exception $e) { - throw $e; - // $this->logger->warning('Erreur lors du scraping de la page ' . $pageNumber . ' du chapitre ' . $chapter->getNumber() . ': ' . $e->getMessage()); - break; - } - } - - return $pageData; - } - - private function fetchImagesUsingPuppeteer(string $url, string $imageSelector, string $nextButtonSelector): array - { - // Appeler le script Puppeteer avec les paramètres nécessaires - $output = []; - $command = sprintf('node puppeteer-script.js "%s" "%s" "%s" 2>&1', $url, $imageSelector, $nextButtonSelector); // Redirect stderr to stdout - // dump($command); - // exec($command, $output, $return_var); - - // dd($command, $output); - - // Convertir la sortie JSON en tableau PHP - return json_decode(implode("", $output), true); - } - - public function testScraping(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array - { - return match ($contentSource->getScrapingType()) { - 'html' => $this->testScrapingHtml($mangaSlug, $chapterNumber, $contentSource), - 'javascript' => $this->testScrapingJavascript($mangaSlug, $chapterNumber, $contentSource), - default => throw new Exception('Unsupported scraping type: ' . $contentSource->getScrapingType()), - }; - } - - /** - * @throws Exception - */ - public function testScrapingJavascript(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array - { - $manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]); - $chapter = $manga->getChapterByNumber($chapterNumber); - - return $this->scrapeChapterJavascript($manga, $chapter, $contentSource); - } - - /** - * @throws GuzzleException - */ - public function testScrapingHtml(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array - { - $chapterUrl = $contentSource->getChapterUrl($mangaSlug, $chapterNumber); - $html = $this->fetchHtml($chapterUrl); - - if ($contentSource->getNextPageSelector() === null) { - return $this->scrapeVerticalReader($html, $contentSource); - } else { - return $this->scrapeHorizontalReader($chapterUrl, $contentSource); - } - } - - /** - * @throws GuzzleException - */ - private function scrapeChapterHtml(Manga $manga, Chapter $chapter, ContentSource $mangaSource): array|bool - { - $chapterUrl = $mangaSource->getChapterUrl($manga->getSlug(), $chapter->getNumber()); - - $tempDir = sys_get_temp_dir() . '/' . uniqid('manga_scraper_'); - mkdir($tempDir); - - $pageData = []; - - if ($mangaSource->getNextPageSelector() === null) { - // Lecteur vertical - $html = $this->fetchHtml($chapterUrl); - $pageData = $this->scrapeVerticalReader($html, $mangaSource); - } else { - // Lecteur horizontal (paginé) - $pageData = $this->scrapeHorizontalReader($chapterUrl, $mangaSource); - } - - // Télécharger et sauvegarder les images - foreach ($pageData as $index => &$page) { - $imageName = sprintf('%03d.%s', $index + 1, pathinfo(parse_url($page['image_url'], PHP_URL_PATH), PATHINFO_EXTENSION)); - $imagePath = $tempDir . '/' . $imageName; - - $this->downloadAndSaveImage($page['image_url'], $imagePath); - - $event = new PageScrappingProgressEvent($chapter->getId(), $index + 1, count($pageData)); - $this->eventDispatcher->dispatch($event, PageScrappingProgressEvent::NAME); - - $page['local_image_url'] = $imagePath; - } - - $cbzFilePath = $this->generateCbzPath($manga, $chapter); - $this->createCbzFile($tempDir, $pageData, $cbzFilePath); - - $chapter->setCbzPath($cbzFilePath); - $this->entityManager->persist($chapter); - $this->entityManager->flush(); - - // Nettoyage du répertoire temporaire - $this->cleanupTempFiles($tempDir); - - return $pageData; - } - - private function scrapeVerticalReader(string $html, ContentSource $contentSource): array - { - $crawler = new Crawler($html); - $images = $crawler->filter($contentSource->getImageSelector()); - - $pageData = []; - foreach ($images as $index => $image) { - if ($image->getAttribute('src') === '') { - $imgUrl = $image->getAttribute('data-src'); - } else { - $imgUrl = $image->getAttribute('src'); - } - $pageData[] = [ - 'image_url' => $this->cleanImageUrl($imgUrl), - 'page_number' => $index + 1, - ]; - } - - return $pageData; - } - - /** - * @throws GuzzleException - */ - private function scrapeHorizontalReader(string $chapterUrl, ContentSource $contentSource): array - { - $pageData = []; - $currentPageUrl = $chapterUrl; - - do { - $html = $this->fetchHtml($currentPageUrl); - $page = $this->extractMangaPageData($html, $contentSource); - - $pageData[] = [ - 'image_url' => $this->cleanImageUrl($page['image_url']), - 'page_number' => count($pageData) + 1, - ]; - - $currentPageUrl = $page['next_page_url']; - } while ($currentPageUrl); - - return $pageData; - } - - /** - * Processes a single image - * @throws GuzzleException - */ - private function processImage(string $imgUrl, string $tempDir, array &$pageData, int $index, Chapter $chapter): void - { - $imgUrl = $this->cleanImageUrl($imgUrl); - $imageName = sprintf('%03d.%s', $index + 1, pathinfo(parse_url($imgUrl, PHP_URL_PATH), PATHINFO_EXTENSION)); - $imagePath = $tempDir . '/' . $imageName; - - $this->downloadAndSaveImage($imgUrl, $imagePath); - - // $event = new PageScrappingProgressEvent($chapter->getId(), $index + 1, 0); - // $this->eventDispatcher->dispatch($event, PageScrappingProgressEvent::NAME); - - $pageData[] = [ - 'image_url' => $imgUrl, - 'local_image_url' => $imagePath, - 'page_number' => $index + 1, - ]; - } - - private function cleanImageUrl(string $url): string - { - return preg_replace('/[\x00-\x1F\x7F]/', '', trim($url)); - } - - /** - * @throws GuzzleException - * @throws Exception - */ - private function fetchHtml(string $url): string - { - $client = new Client(); - - try { - $response = $client->get($url, [ - 'http_errors' => true, - 'allow_redirects' => false - ]); - - $statusCode = $response->getStatusCode(); - - if ($statusCode >= 300 && $statusCode < 400) { - throw new Exception('Chapter Not Found at ' . $url); - } elseif ($statusCode == 404) { - throw new Exception('Chapter Not Found at ' . $url); - } - - return (string)$response->getBody(); - } catch (Exception $e) { - throw new Exception('Bad Request: ' . $e->getMessage()); - } - } - - /** - * @throws GuzzleException - */ - private function downloadAndSaveImage(string $imageUrl, string $destinationPath): void - { - $client = new Client(); - $startTime = microtime(true); - - try { - $response = $client->get($imageUrl); - $endTime = microtime(true); - $contentType = $response->getHeaderLine('Content-Type'); - $xCacheHeader = $response->getHeaderLine('X-Cache'); - $isCached = str_starts_with($xCacheHeader, 'HIT'); - $contentLength = $response->getHeaderLine('Content-Length'); - - if (str_starts_with($contentType, 'image/')) { - file_put_contents($destinationPath, $response->getBody()->getContents()); - // if ($this->scrapingType === 'mangadex') { - // $this->sendReport($imageUrl, true, $isCached, (int)$contentLength, ($endTime - $startTime) * 1000); - // } - } else { - // if ($this->scrapingType === 'mangadex') { - // $this->sendReport($imageUrl, false, $isCached, (int)$contentLength, ($endTime - $startTime) * 1000); - // } - throw new \Exception('Le contenu récupéré n\'est pas une image. Type de contenu : ' . $contentType); - } - } catch (RequestException $e) { - throw new \Exception('Erreur lors de la récupération de l\'image : ' . $e->getMessage()); - } - } - - /** - * @throws GuzzleException - */ - private function isChapterAvailable(string $chapterUrl, float $chapterNumber, ContentSource $mangaSource): bool - { - $html = $this->fetchHtml($chapterUrl); - $crawler = new Crawler($html); - $nextLink = $crawler->filter($mangaSource->getNextPageSelector()); - - if ($nextLink->count() === 0) { - return false; - } - - $nextUrl = $nextLink->attr('href'); - $routeCollection = new RouteCollection(); - $routeCollection->add('manga_chapter', new Route('/scan-{manga}/{chapter}/{page}')); - $context = new RequestContext('/'); - $matcher = new UrlMatcher($routeCollection, $context); - $path = parse_url($nextUrl, PHP_URL_PATH); - $parameters = $matcher->match($path); - - return (float)$parameters['chapter'] === $chapterNumber; - } - - private function sendReport(string $imageUrl, bool $success, bool $cached, int $bytes, float $duration): void - { - $client = new Client(); - - try { - $client->post('https://api.mangadex.network/report', [ - 'headers' => [ - 'Content-Type' => 'application/json', - ], - 'json' => [ - 'url' => $imageUrl, - 'success' => $success, - 'cached' => $cached, - 'bytes' => $bytes, - 'duration' => $duration, - ], - ]); - } catch (RequestException $e) { - // Gérer les exceptions de requête pour le rapport - throw new \Exception('Erreur lors de l\'envoi du rapport : ' . $e->getMessage()); - } - } - - private function createCbzFile(string $tempDir, array $pageData, string $cbzFilePath): void - { - $zip = new \ZipArchive(); - - if ($zip->open($cbzFilePath, \ZipArchive::CREATE) === true) { - foreach ($pageData as $page) { - $zip->addFile($page['local_image_url'], basename($page['local_image_url'])); - } - $zip->close(); - } - } - - private function generateCbzPath(Manga $manga, Chapter $chapter): string - { - $volumeDir = $this->createDirectories($manga, $chapter->getVolume()); - $fileName = sprintf( - '%s_vol%d_ch%s.cbz', - $manga->getSlug(), - $chapter->getVolume(), - $chapter->getNumber() - ); - return $volumeDir . '/' . $fileName; - } - - private function createDirectories(Manga $manga, int $volume): string - { - $mangaYear = $manga->getPublicationYear() ?? 'unknown'; - $mangaDir = sprintf('%s/%s (%s)', $this->projectDir . self::PUBLIC_CBZ, ucfirst($manga->getSlug()), $mangaYear); - $volumeDir = sprintf('%s/volume_%d', $mangaDir, sprintf('%02d', $volume)); - - if (!is_dir($volumeDir)) { - mkdir($volumeDir, 0755, true); - } - - return $volumeDir; - } - - private function cleanupTempFiles(string $directory): void - { - $files = glob($directory . '/*'); - foreach ($files as $file) { - if (is_file($file)) { - unlink($file); - } - } - rmdir($directory); - } -} diff --git a/src/Service/MangaUpdatesMetadataProvider.php b/src/Service/MangaUpdatesMetadataProvider.php deleted file mode 100644 index 1cfaff8..0000000 --- a/src/Service/MangaUpdatesMetadataProvider.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php - -namespace App\Service; - -use App\Entity\Manga; -use App\Interface\MetadataProviderInterface; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; -use Exception; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; -use Symfony\Component\String\Slugger\SluggerInterface; - -class MangaUpdatesMetadataProvider implements MetadataProviderInterface -{ - private Client $client; - - public function __construct(private readonly SluggerInterface $slugger) - { - $this->client = new Client(); - } - - /** - * @throws Exception - */ - public function search(string $title): Collection - { - try { - $response = $this->client->request('PUT', 'https://api.mangaupdates.com/v1/account/login', [ - 'json' => [ - 'username' => 'Colgora', - 'password' => '7TK5jv33NDn*SLV', - ] - ]) - ->withHeader('Content-Type', 'application/json'); - - $jwt = json_decode($response->getBody()->getContents(), true)['context']['session_token']; - - $results = $this->client->request('POST', 'https://api.mangaupdates.com/v1/series/search', [ - 'json' => [ - 'search' => $title, - 'licensed' => 'yes', - 'type' => ['Manga'], - 'exclude_genre' => ['Doujinshi', 'Adult', 'Hentai', 'Ecchi', 'Yaoi', 'Yuri', 'Josei', 'Smut', 'Gender Bender'], - 'orderby' => 'score', - ] - ])->withHeader('Authorization', 'Bearer ' . $jwt) - ->withHeader('Content-Type', 'application/json') - ->getBody() - ->getContents(); - - $mangas = []; - foreach (json_decode($results, true)['results'] as $record) { - $record = $record['record']; - - $genres = []; - foreach ($record['genres'] as $genre) { - $genres[] = $genre['genre']; - } - - $mangas[] = (new Manga()) - ->setTitle($record['title']) - ->setSlug($this->slugger->slug($record['title'])->lower()) - ->setDescription($record['description']) - ->setImageUrl($record['image']['url']['original']) - ->setGenres($genres) - ->setPublicationYear((int)$record['year']) - ->setRating((float)$record['bayesian_rating']) - ; - } - - return new ArrayCollection($mangas); - } catch (GuzzleException $e) { - throw new Exception($e->getMessage()); - } - } -} diff --git a/src/Service/MangadexProvider.php b/src/Service/MangadexProvider.php deleted file mode 100644 index 34d7af4..0000000 --- a/src/Service/MangadexProvider.php +++ /dev/null @@ -1,252 +0,0 @@ -<?php - -namespace App\Service; - -use App\Entity\Chapter; -use App\Entity\Manga; -use App\Interface\ClientInterface; -use App\Interface\MetadataProviderInterface; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; -use Symfony\Component\String\Slugger\SluggerInterface; - -readonly class MangadexProvider implements MetadataProviderInterface -{ - public function __construct(private ClientInterface $client, private SluggerInterface $slugger, private NotificationService $notificationService) - { - } - - public function search(?string $title): Collection - { - if (null === $title) { - return new ArrayCollection(); - } - - try { - $results = $this->client->get('/manga', [ - 'title' => $title, - 'contentRating' => ['safe', 'suggestive', 'erotica'], - 'includes' => ['cover_art', 'author'], - 'limit' => 50, - ]); - } catch (\Exception $e) { - $this->notificationService->sendUpdate('notification', ['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); - - return new ArrayCollection(); - } - - $mangas = []; - foreach ($results['data'] as $result) { - $mangas[] = (new Manga()) - ->setExternalId($result['id']) - ->setTitle($result['attributes']['title']['en']) - ->setSlug($this->slugger->slug($result['attributes']['title']['en'])->lower()) - ->setDescription($result['attributes']['description']['fr'] ?? $result['attributes']['description']['en'] ?? '') - ->setPublicationYear($result['attributes']['year']) - ->setStatus($result['attributes']['status']); - $tags = []; - foreach ($result['attributes']['tags'] as $tag) { - $tags[] = $tag['attributes']['name']['en']; - } - - $mangas[count($mangas) - 1]->setGenres($tags); - - foreach ($result['relationships'] as $relationship) { - if ('author' === $relationship['type']) { - $mangas[count($mangas) - 1]->setAuthor($relationship['attributes']['name']); - } - - if ('cover_art' === $relationship['type']) { - $mangas[count($mangas) - 1]->setImageUrl('https://mangadex.org/covers/'.$result['id'].'/'.$relationship['attributes']['fileName']); - } - } - } - - $test = array_map(fn ($manga) => $manga->getExternalId(), $mangas); - - $ratings = $this->client->get('/statistics/manga', [ - 'manga' => $test, - ]); - - foreach ($mangas as $manga) { - $manga->setRating($ratings['statistics'][$manga->getExternalId()]['rating']['average']); - } - - usort($mangas, fn ($a, $b) => $b->getRating() <=> $a->getRating()); - - return new ArrayCollection($mangas); - } - - public function getFeed(Manga $manga): array - { - if (null === $manga->getExternalId()) { - return []; - } - - $chapters = []; - $page = 0; - - do { - $results = $this->getFeedWithPagination($manga->getExternalId(), $page); - if (isset($results['data'])) { - $chapters = array_merge($chapters, $results['data']); - } else { - break; - } - ++$page; - } while (count($chapters) < $results['total']); - - return $this->getChaptersFromFeed($chapters, $manga); - } - - public function getLastFeed(Manga $manga, int $limit = 100): array - { - if (null === $manga->getExternalId()) { - return []; - } - - $chapters = []; - - try { - $results = $this->getFeedWithPagination($manga->getExternalId(), 0, $limit, 'desc'); - if (isset($results['data'])) { - $chapters = $results['data']; - } - } catch (\Exception $e) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching recent chapters from Mangadex.']); - - return []; - } - - return $this->getChaptersFromFeed($chapters, $manga); - } - - private function getFeedWithPagination(string $externalId, int $page, int $limit = 500, string $order = 'asc'): array - { - try { - $response = $this->client->get('/manga/'.$externalId.'/feed', [ - 'limit' => $limit, - 'translatedLanguage' => ['en', 'fr'], - 'order' => ['chapter' => $order], - 'offset' => $page * $limit, - ]); - } catch (\Exception $e) { - $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); - - return []; - } - - return $response; - } - - public function getMangaAggregate(Manga $manga): array - { - if (null === $manga->getExternalId()) { - return []; - } - - try { - $response = $this->client->get('/manga/'.$manga->getExternalId().'/aggregate'); - } catch (\Exception $e) { - // $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); - return []; - } - - $chapterEntities = []; - if ('ok' === $response['result']) { - foreach ($response['volumes'] as $volume) { - $volumeNumber = 'none' === $volume['volume'] ? 0 : (float) $volume['volume']; - foreach ($volume['chapters'] as $chapter) { - $chapterEntity = new Chapter(); - $chapterEntity->setNumber((float) $chapter['chapter']) - ->setTitle('Chapter '.$chapter['chapter']) - ->setVolume($volumeNumber) - ->setExternalId(''); - - $chapterEntities[] = $chapterEntity; - // $manga->addChapter($chapterEntity); - } - } - } - - return $chapterEntities; - } - - public function getChaptersFromFeed(mixed $chapters, Manga $manga): array - { - $chapterEntities = []; - $uniqueChapterNumbers = []; - - foreach ($chapters as $result) { - $chapterNumber = (float) $result['attributes']['chapter']; - - // Vérifiez si le chapitre existe déjà dans la base de données - $chapterExists = $manga->getChapters()->exists(function ($key, $existingChapter) use ($chapterNumber) { - return $existingChapter->getNumber() === $chapterNumber; - }); - - // Si le chapitre existe déjà dans la base de données ou dans notre nouvelle liste, on skip - if ($chapterExists || in_array($chapterNumber, $uniqueChapterNumbers)) { - continue; - } - - // Créez et ajoutez le nouveau chapitre - $chapter = new Chapter(); - $chapter->setNumber($chapterNumber) - ->setTitle($result['attributes']['title']) - ->setVolume((int) $result['attributes']['volume'] ?? null) - ->setExternalId($result['id']); - - $chapterEntities[] = $chapter; - $uniqueChapterNumbers[] = $chapterNumber; - } - - // Trier les chapitres par numéro - usort($chapterEntities, function ($a, $b) { - return $a->getNumber() <=> $b->getNumber(); - }); - - return $chapterEntities; - } - - public function addAllChaptersToManga(Manga $manga): array - { - $mangaFeed = $this->getFeed($manga); - $mangaAggregate = $this->getMangaAggregate($manga); - - $allChapters = array_merge($mangaFeed, $mangaAggregate); - - if (empty($allChapters)) { - $this->notificationService->sendUpdate([ - 'status' => 'error', - 'message' => 'No chapters found for this manga.', - ]); - - return []; - } - - $mergedChapters = []; - foreach ($allChapters as $chapter) { - $number = $chapter->getNumber(); - $existingChapter = $manga->getChapterByNumber($number); - if ($existingChapter) { - if ($existingChapter->getExternalId() !== $chapter->getExternalId() && is_null($existingChapter->getExternalId())) { - $this->updateChapter($existingChapter, $chapter); - $mergedChapters[$number] = $existingChapter; - } - } else { - // Add new chapter - $manga->addChapter($chapter); - $mergedChapters[$number] = $chapter; - } - } - - return array_values($mergedChapters); - } - - private function updateChapter(Chapter $existingChapter, Chapter $newChapter): void - { - $existingChapter->setVolume($newChapter->getVolume()); - $existingChapter->setExternalId($newChapter->getExternalId()); - } -} diff --git a/src/Service/NotificationService.php b/src/Service/NotificationService.php deleted file mode 100644 index 9563163..0000000 --- a/src/Service/NotificationService.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php - -namespace App\Service; - -use Symfony\Component\Mercure\HubInterface; -use Symfony\Component\Mercure\Update; - -class NotificationService -{ - public function __construct(private HubInterface $hub) - { - - } - - public function sendUpdate(mixed $data): void - { - $update = new Update('notification', json_encode($data)); - $this->hub->publish($update); - } -} diff --git a/src/Service/Scraper/AbstractScraper.php b/src/Service/Scraper/AbstractScraper.php deleted file mode 100644 index f16febe..0000000 --- a/src/Service/Scraper/AbstractScraper.php +++ /dev/null @@ -1,160 +0,0 @@ -<?php - -namespace App\Service\Scraper; - -use App\Entity\Chapter; -use App\Entity\ContentSource; -use App\Entity\Manga; -use App\Event\PageScrappingProgressEvent; -use App\Manager\FileSystemManager; -use Doctrine\ORM\EntityManagerInterface; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Exception\RequestException; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; - -abstract class AbstractScraper implements ScraperInterface -{ - protected Client $httpClient; - - public function __construct( - protected FileSystemManager $fileSystemManager, - protected EventDispatcherInterface $eventDispatcher, - protected EntityManagerInterface $entityManager - ) { - $this->httpClient = new Client(); - } - - protected function getValidChapterUrl(ContentSource $contentSource, Manga $manga, float $chapterNumber): ?string - { - $slugs = array_merge([$manga->getSlug()], $manga->getAlternativeSlugs() ?? []); - - foreach ($slugs as $slug) { - $url = $contentSource->getChapterUrl($slug, $chapterNumber); - if ($this->isChapterUrlValid($url)) { - return $url; - } - } - - return null; - } - - protected function isChapterUrlValid(string $url): bool - { - try { - $response = $this->httpClient->head($url); - - return 200 === $response->getStatusCode(); - } catch (RequestException $e) { - return false; - } - } - - protected function generateCbzPath(Manga $manga, Chapter $chapter): string - { - $mangaDir = $this->fileSystemManager->createMangaDirectory($manga->getSlug(), $manga->getPublicationYear()); - $volumeDir = $this->fileSystemManager->createVolumeDirectory($mangaDir, $chapter->getVolume()); - $fileName = sprintf( - '%s_vol%d_ch%s.cbz', - $manga->getSlug(), - $chapter->getVolume(), - $chapter->getNumber() - ); - - return $volumeDir.'/'.$fileName; - } - - protected function createCbzFile(array $pageData, string $cbzFilePath): void - { - $zip = new \ZipArchive(); - - if (true === $zip->open($cbzFilePath, \ZipArchive::CREATE)) { - foreach ($pageData as $page) { - $zip->addFile($page['local_image_url'], basename($page['local_image_url'])); - } - $zip->close(); - } - } - - protected function cleanupTempFiles(string $directory): void - { - $this->fileSystemManager->deleteDirectory($directory); - } - - protected function cleanImageUrl(string $url): string - { - return preg_replace('/[\x00-\x1F\x7F]/', '', trim($url)); - } - - protected function dispatchProgressEvent(Chapter $chapter, int $currentPage, int $totalPages): void - { - $event = new PageScrappingProgressEvent($chapter->getId(), $currentPage, $totalPages); - $this->eventDispatcher->dispatch($event, PageScrappingProgressEvent::NAME); - } - - /** - * @throws GuzzleException - * @throws \Exception - */ - protected function downloadAndSaveImage(string $imageUrl, string $destinationPath): string - { - try { - $response = $this->httpClient->get($imageUrl); - $contentType = $response->getHeaderLine('Content-Type'); - - if (!str_starts_with($contentType, 'image/')) { - throw new \Exception('Le contenu récupéré n\'est pas une image. Type de contenu : '.$contentType); - } - - $imageData = $response->getBody()->getContents(); - $tempFilePath = $this->saveTempFile($imageData); - - $image = $this->createImageResource($tempFilePath, $contentType); - if (false === $image) { - throw new \Exception('Échec de la création de la ressource image.'); - } - - $destinationPath = $this->ensureJpgExtension($destinationPath); - if (!imagejpeg($image, $destinationPath)) { - imagedestroy($image); - unlink($tempFilePath); - throw new \Exception('Échec de la sauvegarde de l\'image en JPG.'); - } - - imagedestroy($image); - unlink($tempFilePath); - - return $destinationPath; - } catch (\Exception $e) { - throw new \Exception('Erreur lors de la récupération de l\'image : '.$e->getMessage()); - } - } - - private function saveTempFile(string $data): string - { - $tempFilePath = tempnam(sys_get_temp_dir(), 'manga_img_'); - file_put_contents($tempFilePath, $data); - - return $tempFilePath; - } - - /** - * @throws \Exception - */ - private function createImageResource(string $filePath, string $contentType) - { - return match ($contentType) { - 'image/webp' => imagecreatefromwebp($filePath), - 'image/png' => imagecreatefrompng($filePath), - 'image/jpeg', 'image/jpg' => imagecreatefromjpeg($filePath), - default => throw new \Exception('Format d\'image non pris en charge : '.$contentType), - }; - } - - private function ensureJpgExtension(string $path): string - { - $info = pathinfo($path); - - return $info['dirname'].'/'.$info['filename'].'.jpg'; - } -} diff --git a/src/Service/Scraper/HtmlScraper.php b/src/Service/Scraper/HtmlScraper.php deleted file mode 100644 index 419951f..0000000 --- a/src/Service/Scraper/HtmlScraper.php +++ /dev/null @@ -1,170 +0,0 @@ -<?php - -namespace App\Service\Scraper; - -use App\Entity\Chapter; -use App\Entity\ContentSource; -use GuzzleHttp\Exception\GuzzleException; -use Symfony\Component\DomCrawler\Crawler; - -class HtmlScraper extends AbstractScraper -{ - /** - * @throws \Exception - * @throws GuzzleException - */ - public function scrapeChapter(Chapter $chapter, ContentSource $contentSource): array|bool - { - $manga = $chapter->getManga(); - $chapterUrl = $this->getValidChapterUrl($contentSource, $manga, $chapter->getNumber()); - - if (!$chapterUrl) { - throw new \Exception("Aucune URL valide trouvée pour le chapitre {$chapter->getNumber()} du manga {$manga->getTitle()}"); - } - - $tempDir = sys_get_temp_dir().'/'.uniqid('manga_scraper_'); - mkdir($tempDir); - - $pageData = []; - - if (null === $contentSource->getNextPageSelector()) { - // Lecteur vertical - $html = $this->fetchHtml($chapterUrl); - $pageData = $this->scrapeVerticalReader($html, $contentSource); - } else { - // Lecteur horizontal (paginé) - $pageData = $this->scrapeHorizontalReader($chapterUrl, $contentSource); - } - - // Télécharger et sauvegarder les images - foreach ($pageData as $index => &$page) { - $imageName = sprintf('%03d.%s', $index + 1, pathinfo(parse_url($page['image_url'], PHP_URL_PATH), PATHINFO_EXTENSION)); - $imagePath = $tempDir.'/'.$imageName; - - $destinationPath = $this->downloadAndSaveImage($page['image_url'], $imagePath); - - $this->dispatchProgressEvent($chapter, $index + 1, count($pageData)); - - $page['local_image_url'] = $destinationPath; - } - - $cbzFilePath = $this->generateCbzPath($manga, $chapter); - $this->createCbzFile($pageData, $cbzFilePath); - - $chapter->setCbzPath($cbzFilePath); - $this->entityManager->persist($chapter); - $this->entityManager->flush(); - - $this->cleanupTempFiles($tempDir); - - return $pageData; - } - - /** - * @throws \Exception - */ - public function testScraping(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array - { - $chapterUrl = $contentSource->getChapterUrl($mangaSlug, $chapterNumber); - - if (!$this->isChapterUrlValid($chapterUrl)) { - throw new \Exception('Invalid URL, check format and slug'); - } - - $html = $this->fetchHtml($chapterUrl); - - if (null === $contentSource->getNextPageSelector()) { - return $this->scrapeVerticalReader($html, $contentSource); - } else { - return $this->scrapeHorizontalReader($chapterUrl, $contentSource); - } - } - - public function supports(string $scrapingType): bool - { - return 'html' === $scrapingType; - } - - private function scrapeVerticalReader(string $html, ContentSource $contentSource): array - { - $crawler = new Crawler($html); - $images = $crawler->filter($contentSource->getImageSelector()); - - $pageData = []; - foreach ($images as $index => $image) { - $imgUrl = $image->getAttribute('src') ?: $image->getAttribute('data-src'); - $pageData[] = [ - 'image_url' => $this->cleanImageUrl($imgUrl), - 'page_number' => $index + 1, - ]; - } - - return $pageData; - } - - /** - * @throws \Exception - */ - private function scrapeHorizontalReader(string $chapterUrl, ContentSource $contentSource): array - { - $pageData = []; - $currentPageUrl = $chapterUrl; - - do { - $html = $this->fetchHtml($currentPageUrl); - $page = $this->extractMangaPageData($html, $contentSource); - - $pageData[] = [ - 'image_url' => $this->cleanImageUrl($page['image_url']), - 'page_number' => count($pageData) + 1, - ]; - - $currentPageUrl = $page['next_page_url']; - } while ($currentPageUrl); - - return $pageData; - } - - private function fetchHtml(string $url): string - { - try { - $response = $this->httpClient->get($url, [ - 'http_errors' => true, - 'allow_redirects' => false, - ]); - - $statusCode = $response->getStatusCode(); - - if ($statusCode >= 300 && $statusCode < 400 || 404 == $statusCode) { - throw new \Exception('Chapter Not Found at '.$url); - } - - return (string) $response->getBody(); - } catch (\Exception $e) { - throw new \Exception('Bad Request: '.$e->getMessage()); - } - } - - private function extractMangaPageData(string $html, ContentSource $mangaSource): array - { - $crawler = new Crawler($html); - $imgUrl = $crawler->filter($mangaSource->getImageSelector())->attr('src') - ?? $crawler->filter($mangaSource->getImageSelector())->attr('data-src'); - - $nextLink = $crawler->filter($mangaSource->getNextPageSelector()); - $nextUrl = $nextLink->count() > 0 ? $nextLink->attr('href') : null; - - // Convert relative URLs to absolute URLs - if (!preg_match('/^https?:\/\//', $imgUrl)) { - $urlComponents = parse_url($mangaSource->getBaseUrl()); - $scheme = $urlComponents['scheme']; - $host = $urlComponents['host']; - $imgUrl = $scheme.'://'.$host.'/'.ltrim($imgUrl, '/'); - } - - return [ - 'image_url' => $imgUrl, - 'next_page_url' => $nextUrl, - ]; - } -} diff --git a/src/Service/Scraper/JavascriptScraper.php b/src/Service/Scraper/JavascriptScraper.php deleted file mode 100644 index 775fde5..0000000 --- a/src/Service/Scraper/JavascriptScraper.php +++ /dev/null @@ -1,190 +0,0 @@ -<?php - -namespace App\Service\Scraper; - -use App\Entity\Chapter; -use App\Entity\ContentSource; -use Exception; -use GuzzleHttp\Exception\GuzzleException; -use Symfony\Component\Panther\Client as PantherClient; - -class JavascriptScraper extends AbstractScraper -{ - /** - * @throws Exception - */ - public function scrapeChapter(Chapter $chapter, ContentSource $contentSource): array|bool - { - $manga = $chapter->getManga(); - $pantherClient = PantherClient::createChromeClient(); - $chapterUrl = $this->getValidChapterUrl($contentSource, $manga, $chapter->getNumber()); - - if (!$chapterUrl) { - throw new Exception("Aucune URL valide trouvée pour le chapitre {$chapter->getNumber()} du manga {$manga->getTitle()}"); - } - - $pantherClient->request('GET', $chapterUrl); - - try { - $this->selectChapter($pantherClient, $chapter, $contentSource); - - $pageData = $contentSource->getNextPageSelector() === null - ? $this->scrapeVerticalReaderJavascript($pantherClient, $contentSource, $chapter) - : $this->scrapeHorizontalReaderJavascript($pantherClient, $contentSource, $chapter); - - $tempDir = sys_get_temp_dir() . '/' . uniqid('manga_scraper_'); - mkdir($tempDir); - - // Télécharger et sauvegarder les images - foreach ($pageData as $index => &$page) { - $imageName = sprintf('%03d.%s', $index + 1, pathinfo(parse_url($page['image_url'], PHP_URL_PATH), PATHINFO_EXTENSION)); - $imagePath = $tempDir . '/' . $imageName; - - $destinationPath = $this->downloadAndSaveImage($page['image_url'], $imagePath); - $this->dispatchProgressEvent($chapter, $index + 1, count($pageData)); - - $page['local_image_url'] = $destinationPath; - } - - $cbzFilePath = $this->generateCbzPath($manga, $chapter); - $this->createCbzFile($pageData, $cbzFilePath); - - $chapter->setCbzPath($cbzFilePath); - $this->entityManager->persist($chapter); - $this->entityManager->flush(); - - $this->cleanupTempFiles($tempDir); - - return $pageData; - } finally { - $pantherClient->close(); - } - } - - public function testScraping(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array - { - $chapterUrl = $contentSource->getChapterUrl($mangaSlug, $chapterNumber); - - if (!$this->isChapterUrlValid($chapterUrl)) { - throw new \Exception("Invalid URL, check format and slug"); - } - - $pantherClient = PantherClient::createChromeClient(); - $pantherClient->request('GET', $chapterUrl); - - try { - $chapter = new Chapter(); - $chapter->setNumber((float)$chapterNumber); - - $this->selectChapter($pantherClient, $chapter, $contentSource); - - return $contentSource->getNextPageSelector() === null - ? $this->scrapeVerticalReaderJavascript($pantherClient, $contentSource, $chapter) - : $this->scrapeHorizontalReaderJavascript($pantherClient, $contentSource, $chapter); - } catch (Exception $e) { - throw $e; - } finally { - $pantherClient->close(); - } - } - - public function supports(string $scrapingType): bool - { - return $scrapingType === 'javascript'; - } - - private function selectChapter(PantherClient $pantherClient, Chapter $chapter, ContentSource $contentSource): void - { - $chapterSelector = $contentSource->getChapterSelector(); - if (!$chapterSelector) { - return; - } - - $crawler = $pantherClient->waitFor($chapterSelector); - $select = $crawler->filter($chapterSelector); - - if ($select->count() > 0) { - $chapterNumber = $chapter->getNumber(); - $options = $select->filter('option'); - $targetIndex = null; - - foreach ($options as $index => $option) { - if (preg_match("/\b{$chapterNumber}\b/", $option->getText())) { - $targetIndex = $index; - break; - } - } - - if ($targetIndex !== null) { - $pantherClient->executeScript(" - var select = document.querySelector('$chapterSelector'); - select.selectedIndex = $targetIndex; - select.dispatchEvent(new Event('change')); - "); - - $this->waitForImagesLoaded($pantherClient, $contentSource); - } else { - throw new Exception("Chapitre $chapterNumber non trouvé dans le menu déroulant"); - } - } - } - - private function waitForImagesLoaded(PantherClient $pantherClient, ContentSource $contentSource): void - { - $imageSelector = $contentSource->getImageSelector(); - $pantherClient->wait(30)->until( - function ($driver) use ($imageSelector) { - return $driver->executeScript(" - return new Promise((resolve) => { - let lastImageCount = 0; - let stableCount = 0; - const stableThreshold = 10; - - function checkImages() { - const images = document.querySelectorAll('$imageSelector'); - const loadedImages = Array.from(images).filter(img => img.complete && img.naturalWidth > 0); - - if (loadedImages.length === lastImageCount) { - stableCount++; - } else { - stableCount = 0; - lastImageCount = loadedImages.length; - } - - if (stableCount >= stableThreshold) { - resolve(true); - } else { - setTimeout(checkImages, 200); - } - } - - checkImages(); - }); - "); - } - ); - } - - private function scrapeVerticalReaderJavascript(PantherClient $pantherClient, ContentSource $contentSource, Chapter $chapter): array - { - $pageData = []; - $crawler = $pantherClient->waitFor($contentSource->getImageSelector()); - $images = $crawler->filter($contentSource->getImageSelector()); - - foreach ($images as $index => $image) { - $imageUrl = $image->getAttribute('src') ?: $image->getAttribute('data-src'); - $pageData[] = [ - 'image_url' => $this->cleanImageUrl($imageUrl), - 'page_number' => $index + 1, - ]; - } - - return $pageData; - } - - private function scrapeHorizontalReaderJavascript(PantherClient $pantherClient, ContentSource $contentSource, Chapter $chapter): array - { - $pageData = []; - return $pageData; - } -} diff --git a/src/Service/Scraper/MangaScraperService.php b/src/Service/Scraper/MangaScraperService.php deleted file mode 100644 index a99c465..0000000 --- a/src/Service/Scraper/MangaScraperService.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php - -namespace App\Service\Scraper; - -use App\Entity\Chapter; -use App\Entity\ContentSource; - -class MangaScraperService -{ - private ScraperFactory $scraperFactory; - - public function __construct(ScraperFactory $scraperFactory) - { - $this->scraperFactory = $scraperFactory; - } - - public function scrapeChapter(Chapter $chapter, ContentSource $contentSource): array|bool - { - $scraper = $this->scraperFactory->createScraper($contentSource); - return $scraper->scrapeChapter($chapter, $contentSource); - } - - public function testScraping(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array - { - $scraper = $this->scraperFactory->createScraper($contentSource); - return $scraper->testScraping($mangaSlug, $chapterNumber, $contentSource); - } -} diff --git a/src/Service/Scraper/MangadexScraper.php b/src/Service/Scraper/MangadexScraper.php deleted file mode 100644 index 3101390..0000000 --- a/src/Service/Scraper/MangadexScraper.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php - -namespace App\Service\Scraper; - -use App\Entity\Chapter; -use App\Entity\ContentSource; -use Doctrine\ORM\EntityManagerInterface; -use GuzzleHttp\Client; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; - -class MangadexScraper extends AbstractScraper -{ - public function scrapeChapter(Chapter $chapter, ContentSource $contentSource): array|bool - { - $chapterUrl = $contentSource->getBaseUrl() . sprintf($contentSource->getChapterUrlFormat(), $chapter->getExternalId()); - $manga = $chapter->getManga(); - $pageData = []; - - try { - $response = $this->httpClient->get($chapterUrl); - $results = json_decode($response->getBody()->getContents(), true); - - if ($results['result'] !== 'ok' || count($results['chapter']['dataSaver']) === 0) { - throw new \Exception('Error while fetching chapter data from Mangadex ' . $manga->getTitle() . ' ' . $chapter->getNumber()); - } - - $tempDir = sys_get_temp_dir() . '/' . uniqid('manga_scraper_'); - mkdir($tempDir); - - foreach ($results['chapter']['dataSaver'] as $index => $page) { - $pageUrl = $results['baseUrl'] . '/data-saver/' . $results['chapter']['hash'] . '/' . $page; - $imagePath = $tempDir . '/' . sprintf('%03d.%s', $index + 1, pathinfo($page, PATHINFO_EXTENSION)); - - $this->downloadAndSaveImage($pageUrl, $imagePath); - - $this->dispatchProgressEvent($chapter, $index + 1, count($results['chapter']['dataSaver'])); - - $pageData[] = [ - 'image_url' => $pageUrl, - 'local_image_url' => $imagePath, - 'page_number' => $index + 1, - ]; - } - - $cbzFilePath = $this->generateCbzPath($manga, $chapter); - $this->createCbzFile($pageData, $cbzFilePath); - - $chapter->setCbzPath($cbzFilePath); - $this->entityManager->persist($chapter); - $this->entityManager->flush(); - - $this->cleanupTempFiles($tempDir); - - return $pageData; - } catch (\Exception $e) { - // Log the error - return false; - } - } - - public function testScraping(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array - { - // For Mangadex, we need the chapter's external ID, which we don't have in this context. - // We could potentially fetch it first, but for simplicity, let's return an empty array. - return []; - } - - public function supports(string $scrapingType): bool - { - return $scrapingType === 'mangadex'; - } -} diff --git a/src/Service/Scraper/ScraperFactory.php b/src/Service/Scraper/ScraperFactory.php deleted file mode 100644 index cd7ddaa..0000000 --- a/src/Service/Scraper/ScraperFactory.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -namespace App\Service\Scraper; - -use App\Entity\ContentSource; - -class ScraperFactory -{ - private array $scrapers; - - public function __construct(iterable $scrapers) - { - $this->scrapers = iterator_to_array($scrapers); - } - - public function createScraper(ContentSource $contentSource): ScraperInterface - { - foreach ($this->scrapers as $scraper) { - if ($scraper->supports($contentSource->getScrapingType())) { - return $scraper; - } - } - throw new \InvalidArgumentException('Unsupported scraping type: '.$contentSource->getScrapingType()); - } -} diff --git a/src/Service/Scraper/ScraperInterface.php b/src/Service/Scraper/ScraperInterface.php deleted file mode 100644 index 3cf27ed..0000000 --- a/src/Service/Scraper/ScraperInterface.php +++ /dev/null @@ -1,13 +0,0 @@ -<?php - -namespace App\Service\Scraper; - -use App\Entity\Chapter; -use App\Entity\ContentSource; - -interface ScraperInterface -{ - public function scrapeChapter(Chapter $chapter, ContentSource $contentSource): array|bool; - public function testScraping(string $mangaSlug, string $chapterNumber, ContentSource $contentSource): array; - public function supports(string $scrapingType): bool; -} diff --git a/src/Twig/Components/AddMangaModalComponent.php b/src/Twig/Components/AddMangaModalComponent.php deleted file mode 100644 index b6633f2..0000000 --- a/src/Twig/Components/AddMangaModalComponent.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -namespace App\Twig\Components; - -use App\Entity\Manga; -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveProp; -use Symfony\UX\LiveComponent\DefaultActionTrait; - -#[AsLiveComponent] -class AddMangaModalComponent -{ - use DefaultActionTrait; - - #[LiveProp(writable: true)] - public ?Manga $manga; - - public function open(Manga $manga): void - { - $this->manga = $manga; - } - - public function close(): void - { - $this->manga = null; - } -} diff --git a/src/Twig/Components/BootstrapModal.php b/src/Twig/Components/BootstrapModal.php deleted file mode 100644 index 331c5da..0000000 --- a/src/Twig/Components/BootstrapModal.php +++ /dev/null @@ -1,11 +0,0 @@ -<?php - -namespace App\Twig\Components; - -use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; - -#[AsTwigComponent] -class BootstrapModal -{ - public ?string $id = null; -} diff --git a/src/Twig/Components/DownloadChapter.php b/src/Twig/Components/DownloadChapter.php deleted file mode 100644 index 43c4274..0000000 --- a/src/Twig/Components/DownloadChapter.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php - -namespace App\Twig\Components; - -use App\Repository\ChapterRepository; -use App\Repository\MangaRepository; -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\DefaultActionTrait; - -#[AsLiveComponent] -final class DownloadChapter -{ - use DefaultActionTrait; - - public ?string $mangaSlug = ''; - public float $chapter; - - public function __construct() - { - } - - public function downloadChapter(MangaRepository $mangaRepository, ChapterRepository $chapterRepository): int - { - // $mangaSlug = $this->mangaSlug; - // $chapter = $this->chapter; - // $manga = $mangaRepository->findOneBy(['slug' => $mangaSlug]); - // $chapter = $chapterRepository->findOneBy(['manga' => $manga, 'number' => $chapter]); - - - return 0; - - } -} diff --git a/src/Twig/Components/DropdownMenu.php b/src/Twig/Components/DropdownMenu.php deleted file mode 100644 index 8b5cba2..0000000 --- a/src/Twig/Components/DropdownMenu.php +++ /dev/null @@ -1,16 +0,0 @@ -<?php - -namespace App\Twig\Components; - -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveProp; -use Symfony\UX\LiveComponent\DefaultActionTrait; - -#[AsLiveComponent] -class DropdownMenu -{ - use DefaultActionTrait; - - #[LiveProp(writable: true)] - public ?array $items = null; -} diff --git a/src/Twig/Components/MangaSearch.php b/src/Twig/Components/MangaSearch.php deleted file mode 100644 index 6189456..0000000 --- a/src/Twig/Components/MangaSearch.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php - -namespace App\Twig\Components; - -use App\Service\MangadexProvider; -use Doctrine\Common\Collections\Collection; -use Exception; -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveProp; -use Symfony\UX\LiveComponent\DefaultActionTrait; - -#[AsLiveComponent] -class MangaSearch -{ - use DefaultActionTrait; - - #[LiveProp(writable: true)] - public ?string $query = null; - - public function __construct(private readonly MangadexProvider $mangadexProvider) - { - } - - /** - * @throws Exception - */ - public function getMangas(): Collection|null - { - if ($this->query === null || $this->query === '') { - return null; - } - - return $this->mangadexProvider->search($this->query); - } -} diff --git a/src/Twig/Components/Search.php b/src/Twig/Components/Search.php deleted file mode 100644 index 9ddfede..0000000 --- a/src/Twig/Components/Search.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php - -namespace App\Twig\Components; - -use App\Repository\MangaRepository; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveProp; -use Symfony\UX\LiveComponent\DefaultActionTrait; - -#[AsLiveComponent] -final class Search -{ - use DefaultActionTrait; - - #[LiveProp(writable: true)] - public ?string $query = null; - - public function __construct(private readonly MangaRepository $mangaRepository) - { - } - - public function getMangas(): array - { - return $this->query ? $this->mangaRepository->findByTitle($this->query) : []; - } -} diff --git a/src/Twig/Components/ToolBarButton.php b/src/Twig/Components/ToolBarButton.php deleted file mode 100644 index 23b5676..0000000 --- a/src/Twig/Components/ToolBarButton.php +++ /dev/null @@ -1,15 +0,0 @@ -<?php - -namespace App\Twig\Components; - -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveProp; -use Symfony\UX\LiveComponent\DefaultActionTrait; - -#[AsLiveComponent] -final class ToolBarButton -{ - use DefaultActionTrait; - #[LiveProp(writable: true)] - public ?array $data = null; -} diff --git a/src/Twig/Extension/AppExtension.php b/src/Twig/Extension/AppExtension.php deleted file mode 100644 index 8f373f7..0000000 --- a/src/Twig/Extension/AppExtension.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php - -namespace App\Twig\Extension; - -use Twig\Extension\AbstractExtension; -use Twig\TwigFunction; - -class AppExtension extends AbstractExtension -{ - public function getFunctions(): array - { - return [ - new TwigFunction('get_placeholder', [$this, 'getPlaceholder']), - ]; - } - - public function getPlaceholder(string $fieldName): string - { - return match ($fieldName) { - 'baseUrl' => 'https://example.com', - 'imageSelector' => '.manga-image img', - 'chapterUrlFormat' => 'https://example.com/manga/{slug}/chapter-{number}', - 'nextPageSelector' => '.next-page', - 'scrapingType' => 'Select scraping type', - default => '', - }; - } -} diff --git a/src/Twig/Extension/TruncateExtension.php b/src/Twig/Extension/TruncateExtension.php deleted file mode 100644 index 56786ff..0000000 --- a/src/Twig/Extension/TruncateExtension.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php - -namespace App\Twig\Extension; - -use Twig\Extension\AbstractExtension; -use Twig\TwigFilter; - -class TruncateExtension extends AbstractExtension -{ - public function getFilters(): array - { - return [ - new TwigFilter('truncate', [$this, 'truncate']), - ]; - } - - public function truncate(?string $value, int $limit): string - { - if ($value === null) { - return ''; - } - return strlen($value) > $limit ? substr($value, 0, $limit) . '...' : $value; - } -} diff --git a/src/Twig/Runtime/TruncateExtensionRuntime.php b/src/Twig/Runtime/TruncateExtensionRuntime.php deleted file mode 100644 index 20b23f0..0000000 --- a/src/Twig/Runtime/TruncateExtensionRuntime.php +++ /dev/null @@ -1,18 +0,0 @@ -<?php - -namespace App\Twig\Runtime; - -use Twig\Extension\RuntimeExtensionInterface; - -class TruncateExtensionRuntime implements RuntimeExtensionInterface -{ - public function __construct() - { - // Inject dependencies if needed - } - - public function doSomething($value) - { - // ... - } -} diff --git a/templates/activity/index.html.twig b/templates/activity/index.html.twig deleted file mode 100644 index 0b2b32e..0000000 --- a/templates/activity/index.html.twig +++ /dev/null @@ -1,67 +0,0 @@ -{% extends 'base.html.twig' %} -{% block toolbar %} - {% if toolbar is defined %} - <twig:Toolbar toolbar="{{ toolbar }}"/> - {% endif %} -{% endblock %} -{% block body %} - <div {{ turbo_stream_listen('App\\Entity\\Chapter') }}></div> - <div class="container mx-auto mt-8 p-2"> - <div class="bg-white overflow-hidden"> - <div class="overflow-x-auto"> - <table class="min-w-full bg-white"> - <thead> - <tr class="bg-gray-200 text-gray-800"> - <th class="w-1/12 py-3 px-4 text-left"> - <input type="checkbox" class="form-checkbox h-5 w-5 text-green-600"> - </th> - <th class="w-2/12 py-3 px-4 text-left">Manga</th> - <th class="w-1/12 py-3 px-4 text-left">Volume</th> - <th class="w-3/12 py-3 px-4 text-left">Chapitre</th> - <th class="w-3/12 py-3 px-4 text-left">Titre</th> - <th class="w-3/12 py-3 px-4 text-left">Progress</th> - <th class="w-2/12 py-3 px-4 text-left">Actions</th> - </tr> - </thead> - <tbody class="text-gray-700"> - {% for manga in status %} - <tr id="activity-{{ manga.chapterId }}" class="border-b border-gray-200 hover:bg-gray-50 transition duration-150 ease-in-out"> - <td class="py-4 px-4 text-center"> - <input type="checkbox" class="form-checkbox h-5 w-5 text-green-600"> - </td> - <td class="py-4 px-4 font-medium">{{ manga.manga }}</td> - <td class="py-4 px-4">{{ manga.volume }}</td> - <td class="py-4 px-4"> - {{ manga.chapter }} - - </td> - <td class="py-4 px-4">{{ manga.title }}</td> - <td class="py-4 px-4"> - <div class="mt-2" - data-controller="chapter-progress" - data-chapter-progress-chapter-id-value="{{ manga.chapterId }}"> - <div class="relative bg-gray-200 rounded-full h-6 overflow-hidden"> - <div data-chapter-progress-target="progressBar" class="absolute top-0 left-0 h-full bg-green-400 transition-all duration-300 ease-out" style="width: 0"></div> - <div class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-white" data-chapter-progress-target="progressText"> - 0 / 0 - </div> - </div> - </div> - </td> - <td class="py-4 px-4"> - <button class="text-red-500 hover:text-red-700 transition duration-150 ease-in-out"> - <i class="fas fa-trash-alt"></i> - </button> - </td> - </tr> - {% else %} - <tr> - <td colspan="6" class="py-4 px-4 text-center text-gray-500">Aucune activité en cours.</td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - </div> - </div> -{% endblock %} diff --git a/templates/base.html.twig b/templates/base.html.twig deleted file mode 100644 index 4f2e8ad..0000000 --- a/templates/base.html.twig +++ /dev/null @@ -1,86 +0,0 @@ -<!DOCTYPE html> -<html lang="" class="h-full"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> - <title>{% block title %}Mangarr{% endblock %} - - {% block stylesheets %} - {{ encore_entry_link_tags('app') }} - {% endblock %} - {% block javascripts %} - {{ encore_entry_script_tags('app') }} - {# {{ encore_entry_script_tags('turbo') }} #} - {% endblock %} - - - -
    - -
    - -
    - - -
    -
    - - {#
    #} - {# #} - {# #} - {# #} - {# #} - {# #} - {# #} - {# #} - {# #} - {# #} - {# #} - {#
    #} - Mangarr -
    -
    - -
    - -
    -
    - - -
    - - - - -
    - - -
    - {% block toolbar %} - {% endblock %} -
    - - -
    - {% block body %} - {% endblock %} -
    -
    -
    - - diff --git a/templates/broadcast/Chapter.stream.html.twig b/templates/broadcast/Chapter.stream.html.twig deleted file mode 100644 index 6c2d2e1..0000000 --- a/templates/broadcast/Chapter.stream.html.twig +++ /dev/null @@ -1,21 +0,0 @@ -{% block create %} - - - -{% endblock %} - -{% block update %} - - - - - - -{% endblock %} - -{% block remove %} -{% endblock %} diff --git a/templates/bundles/TwigBundle/Exception/error404.html.twig b/templates/bundles/TwigBundle/Exception/error404.html.twig deleted file mode 100644 index 45cf13f..0000000 --- a/templates/bundles/TwigBundle/Exception/error404.html.twig +++ /dev/null @@ -1,9 +0,0 @@ -{# templates/bundles/TwigBundle/Exception/error404.html.twig #} -{% extends 'base.html.twig' %} - -{% block title %}Page non trouvée{% endblock %} - -{% block body %} -

    Page non trouvée

    -

    La page que vous cherchez n'existe pas.

    -{% endblock %} diff --git a/templates/calendar/index.html.twig b/templates/calendar/index.html.twig deleted file mode 100644 index b5e2de2..0000000 --- a/templates/calendar/index.html.twig +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}Hello CalendarController!{% endblock %} - -{% block body %} - - -
    -

    Hello {{ controller_name }}! ✅

    - - This friendly message is coming from: - -
    -{% endblock %} diff --git a/templates/components/AddMangaModal.html.twig b/templates/components/AddMangaModal.html.twig deleted file mode 100644 index 370c77b..0000000 --- a/templates/components/AddMangaModal.html.twig +++ /dev/null @@ -1,12 +0,0 @@ -{# templates/components/manga_modal.html.twig #} -
    - -
    diff --git a/templates/components/Divider.html.twig b/templates/components/Divider.html.twig deleted file mode 100644 index 6ce2b59..0000000 --- a/templates/components/Divider.html.twig +++ /dev/null @@ -1,2 +0,0 @@ -{# templates/components/Divider.html.twig #} -
    diff --git a/templates/components/DownloadChapter.html.twig b/templates/components/DownloadChapter.html.twig deleted file mode 100644 index df74fce..0000000 --- a/templates/components/DownloadChapter.html.twig +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/templates/components/DropdownMenu.html.twig b/templates/components/DropdownMenu.html.twig deleted file mode 100644 index 723b2a2..0000000 --- a/templates/components/DropdownMenu.html.twig +++ /dev/null @@ -1,30 +0,0 @@ -{# templates/components/DropdownMenu.html.twig #} -
    -
    - - -
    -
    diff --git a/templates/components/LoadingButton.html.twig b/templates/components/LoadingButton.html.twig deleted file mode 100644 index cb97a60..0000000 --- a/templates/components/LoadingButton.html.twig +++ /dev/null @@ -1,17 +0,0 @@ -{# templates/components/LoadingButton.html.twig #} - diff --git a/templates/components/MangaSearch.html.twig b/templates/components/MangaSearch.html.twig deleted file mode 100644 index 281ef91..0000000 --- a/templates/components/MangaSearch.html.twig +++ /dev/null @@ -1,114 +0,0 @@ -{# templates/components/MangaSearch.html.twig #} - -
    -
    -
    -
    - - - - - -
    -
    -
    - - {% if this.mangas %} -
    - {% for manga in this.mangas %} -
    -
    - - {{ manga.title }} - -
    -
    -
    -
    - {{ manga.title }} - ({{ manga.publicationYear }}) -
    - - - -
    - {% for genre in manga.genres %} - - {{ genre }} - - {% endfor %} -
    -
    -

    {{ manga.description|truncate(250) }}

    -
    -
    - - - {{ manga.rating }} - -
    -
    -
    - - - - {% block content %} -
    - - - - - - - - - - -
    - {{ manga.title }} -
    -

    {{ manga.description|truncate(250) }}

    -

    Année de publication: {{ manga.publicationYear }}

    -

    Genres: {{ manga.genres|join(', ') }}

    -

    Note: {{ manga.rating }}

    -
    -
    -
    - {% endblock %} - {% block footer %} - - {{ component('LoadingButton', { - text: 'Add ' ~ manga.title, - type: 'submit', - form: 'manga-' ~ loop.index, - color: 'green' - }) }} - {% endblock %} -
    -
    - {% endfor %} -
    - {% endif %} -
    diff --git a/templates/components/Modal.html.twig b/templates/components/Modal.html.twig deleted file mode 100644 index e228f28..0000000 --- a/templates/components/Modal.html.twig +++ /dev/null @@ -1,33 +0,0 @@ -{# templates/components/Modal.html.twig #} -
    - -
    diff --git a/templates/components/NewMangaForm.html.twig b/templates/components/NewMangaForm.html.twig deleted file mode 100644 index b01c084..0000000 --- a/templates/components/NewMangaForm.html.twig +++ /dev/null @@ -1,40 +0,0 @@ -
    - {% component BootstrapModal with {id: 'mangaModal' ~ index ~ '-' ~ manga.slug } %} - {% block modal_header %} -
    - - -
    - {% endblock %} - {% block modal_body %} -
    -
    - {{ manga.title }} -
    -

    {{ manga.description|truncate(250) }}

    -

    Année de publication: {{ manga.publicationYear }}

    -

    Genres: {{ manga.genres|join(', ') }}

    -

    Note: {{ manga.rating }}

    -
    -
    -
    - {% endblock %} - - {% block modal_footer %} - - {% endblock %} - {% endcomponent %} -
    diff --git a/templates/components/Search.html.twig b/templates/components/Search.html.twig deleted file mode 100644 index 444928e..0000000 --- a/templates/components/Search.html.twig +++ /dev/null @@ -1,38 +0,0 @@ -{# templates/components/Search.html.twig #} - -
    - - -
    - - {% if query %} -
    - -
    - {% endif %} - diff --git a/templates/components/ToolBarButton.html.twig b/templates/components/ToolBarButton.html.twig deleted file mode 100644 index df2bb18..0000000 --- a/templates/components/ToolBarButton.html.twig +++ /dev/null @@ -1,25 +0,0 @@ -{# templates/components/ToolbarButton.html.twig #} -{% set buttonAttributes = {} %} -{% set buttonClass = 'text-white' %} -{% if data is defined and data is not empty %} - {% for key, value in data %} - {% set dataKey = 'data-' ~ key|replace({'_': '-'})|lower %} - {% set buttonAttributes = buttonAttributes|merge({ (dataKey): value }) %} - {% endfor %} - {% if data['buttonClass'] is defined %} - {% set buttonClass = data['buttonClass'] %} - {% endif %} -{% endif %} - -
    - -
    diff --git a/templates/components/Toolbar.html.twig b/templates/components/Toolbar.html.twig deleted file mode 100644 index 199f701..0000000 --- a/templates/components/Toolbar.html.twig +++ /dev/null @@ -1,52 +0,0 @@ -{# templates/components/Toolbar.html.twig #} -
    -
    -
    - {% for element in toolbar.leftGroup %} - {% if element.type == 'button' %} - {% set actionParts = element.action|split('#') %} - - {% elseif element.type == 'divider' %} - - {% elseif element.type == 'dropdown' %} - - {% endif %} - {% endfor %} -
    -
    - {% for element in toolbar.rightGroup %} - {% if element.type == 'button' %} - {% set actionParts = element.action|split('#') %} - - {% elseif element.type == 'divider' %} - - {% elseif element.type == 'dropdown' %} - - {% endif %} - {% endfor %} -
    -
    -
    - diff --git a/templates/conversion/index.html.twig b/templates/conversion/index.html.twig deleted file mode 100644 index f103f7e..0000000 --- a/templates/conversion/index.html.twig +++ /dev/null @@ -1,43 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block body %} -
    -
    -
    -

    - Convertir CBR en CBZ -

    -
    -
    -
    -
    - -
    -
    - -
    - -

    ou glisser-déposer

    -
    -

    - CBR jusqu'à 100MB -

    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -{% endblock %} diff --git a/templates/import/confirm.html.twig b/templates/import/confirm.html.twig deleted file mode 100644 index 9c494e4..0000000 --- a/templates/import/confirm.html.twig +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block body %} -
    -

    Confirmer l'Importation

    -

    Titre: {{ title }}

    -

    Volume: {{ volume }}

    -
    - -
    -
    -{% endblock %} - diff --git a/templates/import/index.html.twig b/templates/import/index.html.twig deleted file mode 100644 index b0cdb51..0000000 --- a/templates/import/index.html.twig +++ /dev/null @@ -1,43 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block body %} -
    -
    -
    -

    - Importer des Mangas -

    -
    -
    -
    -
    - -
    -
    - -
    - -

    ou glisser-déposer

    -
    -

    - CBZ ou CBR jusqu'à 100MB chacun -

    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -{% endblock %} diff --git a/templates/import/match.html.twig b/templates/import/match.html.twig deleted file mode 100644 index 28fa31c..0000000 --- a/templates/import/match.html.twig +++ /dev/null @@ -1,103 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block body %} -
    -
    -
    -
    - - - - - - - - - - - - {% for file in files %} - - - - - - - - {% endfor %} - -
    - - Original FileMangaContentActions
    - - -
    -
    {{ file.originalFileName }}
    -
    {{ file.fileSize }}
    -
    -
    - - - {% if file.metadata.chapter %} - Chapter {{ file.metadata.chapter }} - - {% else %} - Volume {{ file.metadata.volume }} - - {% endif %} - - -
    -
    -
    - -
    - -
    -
    - -
    -{% endblock %} diff --git a/templates/manga/_chapter_list.html.twig b/templates/manga/_chapter_list.html.twig deleted file mode 100644 index 57f8b7a..0000000 --- a/templates/manga/_chapter_list.html.twig +++ /dev/null @@ -1,111 +0,0 @@ - -
    - {% for volume, chapters in chapters_by_volume %} - {% set is_first = loop.first %} - {% set volume_cbz_path = chapters|first.cbzPath %} - {% set all_chapters_same_cbz = chapters|reduce((carry, chapter) => carry and chapter.cbzPath == volume_cbz_path, true) %} - {% set available_chapters = chapters|filter(chapter => chapter.cbzPath is not null) %} - {% set total_chapters = chapters|filter(chapter => chapter.visible)|length %} - -
    -
    -
    - -
    - -

    Volume {{ '%02d'|format(volume) }}

    -
    - - {{ available_chapters|length }} / {{ total_chapters }} - -
    -
    - -
    - -
    - - -
    - -
    - - - - - - - - - - {% if all_chapters_same_cbz and volume_cbz_path is not null %} - - - - - - {% else %} - {% for chapter in chapters %} - {% include 'manga/_chapter_row.html.twig' with {'chapter': chapter, 'manga': manga} %} - {% endfor %} - {% endif %} - -
    #TitleActions
    - - {{ '%02d'|format(volume) }} - - - - Volume {{ '%02d'|format(volume) }} - - - - - -
    -
    -
    - -
    -
    -
    -
    -
    - {% endfor %} -
    -
    diff --git a/templates/manga/_chapter_row.html.twig b/templates/manga/_chapter_row.html.twig deleted file mode 100644 index 1045d2d..0000000 --- a/templates/manga/_chapter_row.html.twig +++ /dev/null @@ -1,65 +0,0 @@ -{% if chapter.visible %} - - {% if chapter.cbzPath is not null %} - - - {{ chapter.number < 10 ? '0' ~ chapter.number : chapter.number }} - - - {% else %} - {{ chapter.number < 10 ? '0' ~ chapter.number : chapter.number }} - {% endif %} - - - {% if chapter.cbzPath is not null %} - - {{ chapter.title ?? 'No title' }} - - {% else %} - {{ chapter.title ?? 'No title' }} - {% endif %} - - - {% if chapter.cbzPath is null %} - - {% else %} - - {% endif %} - - - - - - -{% endif %} diff --git a/templates/manga/_list.html.twig b/templates/manga/_list.html.twig deleted file mode 100644 index 8aa43e4..0000000 --- a/templates/manga/_list.html.twig +++ /dev/null @@ -1,92 +0,0 @@ -{% block body %} - {% if currentView == 'poster' %} -
    - {% for manga in mangas %} -
    - - {{ manga.title }} - -
    -
    -

    {{ manga.title }}

    -

    {{ manga.publicationYear }}

    -
    -

    Added: {{ manga.createdAt|date('M d, Y') }}

    -
    -
    - {% else %} -

    Aucun manga trouvé.

    - {% endfor %} -
    - {% elseif currentView == 'resume' %} - - {% elseif currentView == 'table' %} -
    - - - - - {# #} - - - - - {% for manga in mangas %} - - - {# #} - - - {% else %} - - - - {% endfor %} - -
    Manga TitleVolumesChapters
    -
    - {{ manga.title }} -
    -
    #} - {# {{ manga.volumes|length }} #} - {# - {% set total_chapters = manga.chapters|length %} - {% set available_chapters = manga.chapters|filter(chapter => chapter.cbzPath is not null)|length %} -
    -
    -
    -
    - {{ available_chapters }} / {{ total_chapters }} -
    -
    Aucun manga trouvé.
    -
    - {% endif %} -{% endblock %} diff --git a/templates/manga/_manga_details.html.twig b/templates/manga/_manga_details.html.twig deleted file mode 100644 index 6a7370a..0000000 --- a/templates/manga/_manga_details.html.twig +++ /dev/null @@ -1,240 +0,0 @@ -{% block body %} -
    -
    -
    -
    -
    - -
    -
    - -

    {{ manga.title }}

    -
    -
    - {{ manga.publicationYear }} - Chapters: {{ manga.chapters.count }} -
    -
    - - /media/mangas/{{ manga.title }} ({{ manga.publicationYear }}) - {{ manga.status ?? 'Terminé' }} -
    -
    - {% set genre_count = 0 %} - {% for genre in manga.genres %} - {% if genre_count < 5 %} - {{ genre }} - {% set genre_count = genre_count + 1 %} - {% endif %} - {% endfor %} - {% if genre_count == 5 and manga.genres|length > 5 %} - ... - {% endif %} -
    -
    -
    - - {{ manga.rating|round(2) }} -
    -

    {{ manga.description|truncate(500) }}

    -
    -
    -
    -
    -
    -
    - - - - {# Modal d'édition #} - - {% block content %} - {{ form_start(form, {'action': path('app_manga_edit', {'id': manga.id}), 'attr': {'id': 'editForm', 'data-turbo-form': 'true'}}) }} - {% do form.alternativeSlugs.setRendered() %} - -
    - {{ form_row(form.title, {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}, 'row_attr': {'class': 'mt-1'}}) }} - {{ form_row(form.slug, { - 'label_attr': {'class': 'block text-sm font-medium text-gray-700'}, - 'row_attr': {'class': 'mt-1'}, - 'attr': { - 'class': 'w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md focus:outline-none text-gray-500', - 'readonly': true - } - }) }} - {{ form_row(form.publicationYear, {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}, 'row_attr': {'class': 'mt-1'}}) }} - {{ form_row(form.description, {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}, 'row_attr': {'class': 'mt-1'}}) }} - {{ form_row(form.author, {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}, 'row_attr': {'class': 'mt-1'}}) }} - {{ form_row(form.status, {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}, 'row_attr': {'class': 'mt-1'}}) }} - {{ form_row(form.rating, {'label_attr': {'class': 'block text-sm font-medium text-gray-700'}, 'row_attr': {'class': 'mt-1'}}) }} - -
    - -
    - {% for slug in form.alternativeSlugs %} -
    - {{ form_widget(slug, {'attr': {'class': 'bg-transparent border-none focus:outline-none focus:border-b focus:border-green-500 p-0 w-full'}}) }} - -
    - {% endfor %} -
    - - -
    - -
    - -
    - {% for genre in form.genres %} -
    - {{ form_widget(genre, {'attr': {'class': 'bg-transparent border-none focus:outline-none focus:border-b focus:border-green-500 p-0 w-full'}}) }} - -
    - {% endfor %} -
    - - -
    -
    - {{ form_end(form) }} - {% endblock %} - {% block footer %} - - - {% endblock %} -
    - - {# Modal de confirmation de suppression #} - - -

    - Are you sure you want to delete this manga? This action cannot be undone. -

    -
    - - - - -
    - - - {% block content %} -
    s.id)|json_encode, - allSources: contentSources|map(s => { - id: s.id, - name: s.cleanBaseUrl - })|json_encode - }) }}> -
    -

    Preferred Sources

    -
      - {% for source in manga.preferredSources %} -
    • - {{ source.cleanBaseUrl }} - -
    • - {% endfor %} -
    -
    -
    -

    Available Sources

    -
      - {% for source in contentSources %} - {% if source not in manga.preferredSources %} -
    • - {{ source.cleanBaseUrl }} - -
    • - {% endif %} - {% endfor %} -
    -
    -
    - {% endblock %} - - {% block footer %} - - {% endblock %} -
    -{% endblock %} - diff --git a/templates/manga/add_new.html.twig b/templates/manga/add_new.html.twig deleted file mode 100644 index c73c410..0000000 --- a/templates/manga/add_new.html.twig +++ /dev/null @@ -1,6 +0,0 @@ -{% extends 'base.html.twig' %} -{% block body %} -
    - {{ component('MangaSearch', {query: query}) }} -
    -{% endblock %} diff --git a/templates/manga/index.html.twig b/templates/manga/index.html.twig deleted file mode 100644 index fc05465..0000000 --- a/templates/manga/index.html.twig +++ /dev/null @@ -1,9 +0,0 @@ -{% extends 'base.html.twig' %} -{% block toolbar %} - {% if toolbar is defined %} - - {% endif %} -{% endblock %} -{% block body %} - {% include 'manga/_list.html.twig' %} -{% endblock %} diff --git a/templates/manga/manga_reader.html.twig b/templates/manga/manga_reader.html.twig deleted file mode 100644 index 90b17f4..0000000 --- a/templates/manga/manga_reader.html.twig +++ /dev/null @@ -1,61 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}{{ manga.title }} - Chapitre {{ chapter.number }}{% endblock %} - -{% block body %} -
    -

    {{ manga.title }} - Chapitre {{ chapter.number }}

    - - {% set isCbz = chapter.cbzPath is not null %} - {% set totalPages = isCbz ? totalPages : pages|length %} - {% set currentPageNumber = isCbz ? currentPage : currentPage.number %} - -
    - {% if currentPageNumber > 1 %} - « Précédent - {% endif %} - {% if currentPageNumber < totalPages %} - Suivant » - {% endif %} -
    - -
    - {% if isCbz %} - {% if currentPageNumber < totalPages %} - - Page {{ currentPageNumber }} - - {% else %} - Page {{ currentPageNumber }} - {% endif %} - {% else %} - {% if currentPageNumber < totalPages %} - - Page {{ currentPageNumber }} - - {% else %} - Page {{ currentPageNumber }} - {% endif %} - {% endif %} -
    - -
    - {% if currentPageNumber > 1 %} - « Précédent - {% endif %} - {% if currentPageNumber < totalPages %} - Suivant » - {% endif %} -
    - -
    - Page {{ currentPageNumber }} sur {{ totalPages }} -
    -
    -{% endblock %} diff --git a/templates/manga/show_chapters.html.twig b/templates/manga/show_chapters.html.twig deleted file mode 100644 index 2e23e2d..0000000 --- a/templates/manga/show_chapters.html.twig +++ /dev/null @@ -1,10 +0,0 @@ -{% extends 'base.html.twig' %} -{% block toolbar %} - {% if toolbar is defined %} - - {% endif %} -{% endblock %} -{% block body %} - {% include 'manga/_manga_details.html.twig' %} -{% endblock %} - diff --git a/templates/menu/menu.html.twig b/templates/menu/menu.html.twig deleted file mode 100644 index d7db033..0000000 --- a/templates/menu/menu.html.twig +++ /dev/null @@ -1,68 +0,0 @@ - diff --git a/templates/react/index.html.twig b/templates/react/index.html.twig deleted file mode 100644 index 74bedc4..0000000 --- a/templates/react/index.html.twig +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - React App - {{ encore_entry_link_tags('react-app') }} - - -
    - {{ encore_entry_script_tags('react-app') }} - - \ No newline at end of file diff --git a/templates/reader/index.html.twig b/templates/reader/index.html.twig deleted file mode 100644 index 45fb441..0000000 --- a/templates/reader/index.html.twig +++ /dev/null @@ -1,31 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}{{ manga.title }} - Chapitre {{ chapter.number }}{% endblock %} - -{% block body %} -
    -

    {{ manga.title }} - Chapitre {{ chapter.number }}

    - -
    - - - -
    - -
    - -
    - -
    - -
    - -
    - Page 1 sur {{ totalPages }} -
    -
    -{% endblock %} diff --git a/templates/settings/folders.html.twig b/templates/settings/folders.html.twig deleted file mode 100644 index 343fcc9..0000000 --- a/templates/settings/folders.html.twig +++ /dev/null @@ -1,62 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}Application Settings{% endblock %} - -{% block body %} -
    -
    -
    -

    - Application Settings -

    -
    -
    - {{ form_start(form, {'attr': {'class': 'space-y-6'}}) }} -
    - {{ form_label(form.mangaDirectory, 'Manga Directory', {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }} - {{ form_widget(form.mangaDirectory, {'attr': { - 'class': 'mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm', - 'placeholder': '/path/to/manga/directory' - }}) }} - {{ form_errors(form.mangaDirectory) }} -
    - -
    - {{ form_label(form.imageDirectory, 'Image Directory', {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }} - {{ form_widget(form.imageDirectory, {'attr': { - 'class': 'mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm', - 'placeholder': '/path/to/image/directory' - }}) }} - {{ form_errors(form.imageDirectory) }} -
    - -
    - -
    - {{ form_end(form) }} -
    -
    - -
    -
    -

    - Current Settings -

    -
    -
    -
    -
    -
    Manga Directory
    -
    {{ form.mangaDirectory.vars.value }}
    -
    -
    -
    Image Directory
    -
    {{ form.imageDirectory.vars.value }}
    -
    -
    -
    -
    -
    -{% endblock %} diff --git a/templates/settings/index.html.twig b/templates/settings/index.html.twig deleted file mode 100644 index a94ff44..0000000 --- a/templates/settings/index.html.twig +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}Hello SettingsController!{% endblock %} - -{% block body %} - - -
    -

    Hello {{ controller_name }}! ✅

    - - This friendly message is coming from: - -
    -{% endblock %} diff --git a/templates/settings/scrapper_list.html.twig b/templates/settings/scrapper_list.html.twig deleted file mode 100644 index b3b48ca..0000000 --- a/templates/settings/scrapper_list.html.twig +++ /dev/null @@ -1,81 +0,0 @@ -{% extends 'base.html.twig' %} -{% block toolbar %} - {% if toolbar is defined %} - - {% endif %} -{% endblock %} -{% block title %}Scrapper Configurations{% endblock %} - -{% block body %} -
    -

    Scrapper Configurations

    - -
    - {% for contentSource in contentSources %} -
    -
    -
    -
    - {{ contentSource.baseUrl|replace({'http://': '', 'https://': ''})|trim('/', 'right') }} -
    - - - -
    -
    -
    -
    - - {{ contentSource.scrapingType }} - - - {{ contentSource.nextPageSelector ? 'Horizontal' : 'Vertical' }} - -
    -
    - - Edit configuration - -
    - {% endfor %} - - -
    - -

    Add New Configuration

    -
    -
    -
    -
    - - - {% block content %} -
    -
    - -
    - - -
    - {% endblock %} - {% block footer %} - {% endblock %} -
    -{% endblock %} diff --git a/templates/settings/scrappers.html.twig b/templates/settings/scrappers.html.twig deleted file mode 100644 index 242259c..0000000 --- a/templates/settings/scrappers.html.twig +++ /dev/null @@ -1,66 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}{{ isNew ? 'Create' : 'Edit' }} Scrapper Configuration{% endblock %} - -{% block body %} -
    -
    -
    -

    - {{ isNew ? 'Create' : 'Edit' }} Scrapper Configuration -

    -
    -
    - {{ form_start(form, {'attr': {'class': 'space-y-6', 'data-scrapper-configure-target': 'form', 'data-action': 'submit->scrapper-configure#saveConfiguration'}}) }} - - {% for field in form.children %} -
    - {{ form_label(field, null, {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }} - {{ form_widget(field, {'attr': { - 'class': 'mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm', - 'placeholder': get_placeholder(field.vars.name) - }}) }} -
    - {% endfor %} - -
    - -
    - - {{ form_end(form) }} - -
    -

    - Test Configuration -

    -
    -
    - - -
    -
    - - -
    -
    - -
    -
    -
    - -
    -
    -
    -
    -{% endblock %} diff --git a/templates/system/index.html.twig b/templates/system/index.html.twig deleted file mode 100644 index da85fb6..0000000 --- a/templates/system/index.html.twig +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}Hello SystemController!{% endblock %} - -{% block body %} - - -
    -

    Hello {{ controller_name }}! ✅

    - - This friendly message is coming from: - -
    -{% endblock %} diff --git a/src/Factory/ApiTokenFactory.php b/tests/Factory/ApiTokenFactory.php similarity index 98% rename from src/Factory/ApiTokenFactory.php rename to tests/Factory/ApiTokenFactory.php index a4b274f..88b190e 100644 --- a/src/Factory/ApiTokenFactory.php +++ b/tests/Factory/ApiTokenFactory.php @@ -1,6 +1,6 @@ $title, - 'slug' => $this->slugger->slug($title)->lower(), + 'slug' => strtolower(str_replace(' ', '-', $title)), 'imageUrl' => self::faker()->optional()->imageUrl(), 'publicationYear' => self::faker()->optional()->year(), 'description' => self::faker()->optional()->text(), diff --git a/src/Factory/PageFactory.php b/tests/Factory/PageFactory.php similarity index 98% rename from src/Factory/PageFactory.php rename to tests/Factory/PageFactory.php index 4e56d05..1f57ef0 100644 --- a/src/Factory/PageFactory.php +++ b/tests/Factory/PageFactory.php @@ -1,6 +1,6 @@