imageManager = new ImageManager(new Driver()); } #[Route('/', name: 'app_manga')] 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; } }