diff --git a/assets/controllers/toolbar_controller.js b/assets/controllers/toolbar_controller.js index a4afda0..229001c 100644 --- a/assets/controllers/toolbar_controller.js +++ b/assets/controllers/toolbar_controller.js @@ -9,8 +9,32 @@ export default class extends Controller { currentStatus: String } - refreshMetadata() { - console.log("Refreshing..."); + refreshMetadata(event) { + const mangaId = event.currentTarget.dataset.mangaid; + const url = `/refresh_metadata`; + + 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 => { + console.log('Metadata refreshed:', data); + // Traitez la réponse ici, par exemple en mettant à jour l'interface utilisateur + }) + .catch(error => { + console.error('Error:', error); + // Gérez l'erreur ici, par exemple en affichant un message à l'utilisateur + }); } searchLastChapter() { diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index ba1cd3d..8005ebd 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -15,6 +15,7 @@ framework: routing: # Route your messages to the transports 'App\Message\DownloadChapter': async + 'App\Message\RefreshMetadata': async # when@test: # framework: diff --git a/src/Controller/MangaController.php b/src/Controller/MangaController.php index 99ff61c..f737f76 100644 --- a/src/Controller/MangaController.php +++ b/src/Controller/MangaController.php @@ -6,6 +6,7 @@ use App\Entity\Chapter; use App\Entity\Manga; use App\Manager\Toolbar\Factory\ToolbarFactory; use App\Message\DownloadChapter; +use App\Message\RefreshMetadata; use App\Repository\ChapterRepository; use App\Repository\MangaRepository; use App\Service\CbzService; @@ -33,8 +34,7 @@ class MangaController extends AbstractController private readonly CbzService $cbzService, private readonly ToolbarFactory $toolbarFactory, private readonly MangadexProvider $mangadexProvider, - private readonly EntityManagerInterface $entityManager, - private readonly NotificationService $notificationService + private readonly EntityManagerInterface $entityManager ) { } @@ -92,11 +92,10 @@ class MangaController extends AbstractController } return $b <=> $a; }); - return $this->render('manga/show_chapters.html.twig', [ 'chapters_by_volume' => $chaptersByVolume, 'manga' => $manga, - 'toolbar' => $this->toolbarFactory->createToolbar('chapter_list')->getGroups(), + 'toolbar' => $this->toolbarFactory->createToolbar('chapter_list', ['mangaId' => $manga->getId()])->getGroups(), ]); } @@ -167,39 +166,12 @@ class MangaController extends AbstractController ->setRating($request->request->get('rating')) ->setExternalId($request->request->get('externalId')); - $mangaFeed = $this->mangadexProvider->getFeed($manga); - $mangaAggregate = $this->mangadexProvider->getMangaAggregate($manga); + $mergedChapters = $this->mangadexProvider->addAllChaptersToManga($manga); - $allChapters = array_merge($mangaFeed, $mangaAggregate); - - if (empty($allChapters)) { - $this->notificationService->sendUpdate([ - 'status' => 'error', - 'message' => 'No chapters found for this manga.' - ]); + if (empty($mergedChapters)) { return $this->redirectToRoute('app_manga_search', ['query' => $manga->getTitle()]); } - $mergedChapters = []; - foreach ($allChapters as $chapter) { - $number = $chapter->getNumber(); - - if (isset($mergedChapters[$number])) { - $existingChapter = $mergedChapters[$number]; - - if (!empty($chapter->getExternalId()) || - (empty($existingChapter->getExternalId()) && !strpos($chapter->getTitle(), 'Chapter ') == 0)) { - $mergedChapters[$number] = $chapter; - } - } else { - $mergedChapters[$number] = $chapter; - } - } - - foreach ($mergedChapters as $chapter) { - $manga->addChapter($chapter); - } - try { foreach ($manga->getChapters() as $chapter) { $this->entityManager->persist($chapter); @@ -264,6 +236,19 @@ class MangaController extends AbstractController 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); + } + private function isFullVolume(Chapter $chapter): bool { $volumeChapters = $this->chapterRepository->findBy([ diff --git a/src/Manager/Toolbar/Definition/ChapterListToolbar.php b/src/Manager/Toolbar/Definition/ChapterListToolbar.php index 8133e4e..de0ea52 100644 --- a/src/Manager/Toolbar/Definition/ChapterListToolbar.php +++ b/src/Manager/Toolbar/Definition/ChapterListToolbar.php @@ -7,19 +7,19 @@ use App\Manager\Toolbar\Element\ToolbarDivider; class ChapterListToolbar extends Toolbar { - public function __construct() + public function __construct(array $contextData = []) { $this - ->addToLeftGroup(new ToolbarButton('arrows-rotate', 'Refresh metadata', 'refreshMetadata')) + ->addToLeftGroup(new ToolbarButton('arrows-rotate', 'Refresh metadata', 'refreshMetadata', $contextData)) ->addToLeftGroup(new ToolbarDivider()) ->addToLeftGroup(new ToolbarButton('keyboard', 'Rename chapters', 'renameChapters')) - ->addToLeftGroup(new ToolbarButton('file-zipper', 'Manage cbz', 'manageCbz')) - ->addToLeftGroup(new ToolbarButton('history', 'History', 'history')) + ->addToLeftGroup(new ToolbarButton('file-zipper', 'Manage cbz', 'manageCbz', $contextData)) + ->addToLeftGroup(new ToolbarButton('history', 'History', 'history', $contextData)) - ->addToRightGroup(new ToolbarButton('bookmark', 'Monitoring', 'monitoring')) - ->addToRightGroup(new ToolbarButton('wrench', 'Edit', 'editManga')) - ->addToRightGroup(new ToolbarButton('trash-can', 'Delete', 'deleteManga')) + ->addToRightGroup(new ToolbarButton('bookmark', 'Monitoring', 'monitoring', $contextData)) + ->addToRightGroup(new ToolbarButton('wrench', 'Edit', 'editManga', $contextData)) + ->addToRightGroup(new ToolbarButton('trash-can', 'Delete', 'deleteManga', $contextData)) ->addToRightGroup(new ToolbarDivider()) ->addToRightGroup(new ToolbarButton('chevron-down', 'Expand all', 'expandAll')); } diff --git a/src/Manager/Toolbar/Definition/MangaListToolbar.php b/src/Manager/Toolbar/Definition/MangaListToolbar.php index d25fdcc..b827b6c 100644 --- a/src/Manager/Toolbar/Definition/MangaListToolbar.php +++ b/src/Manager/Toolbar/Definition/MangaListToolbar.php @@ -8,7 +8,7 @@ use App\Manager\Toolbar\Element\ToolbarDropdown; class MangaListToolbar extends Toolbar { - public function __construct() + public function __construct(array $contextData = []) { $this->addToLeftGroup(new ToolbarButton('arrows-rotate', 'Refresh', 'refreshMetadata')) ->addToLeftGroup(new ToolbarButton('search', 'Search', 'searchLastChapter')) diff --git a/src/Manager/Toolbar/Element/ToolbarButton.php b/src/Manager/Toolbar/Element/ToolbarButton.php index ac7c40b..924c420 100644 --- a/src/Manager/Toolbar/Element/ToolbarButton.php +++ b/src/Manager/Toolbar/Element/ToolbarButton.php @@ -2,13 +2,24 @@ namespace App\Manager\Toolbar\Element; -use App\Manager\Toolbar\Element\AbstractToolbarElement; - class ToolbarButton extends AbstractToolbarElement { + protected array $data; + + public function __construct(string $icon, string $label, string $action, array $data = []) + { + parent::__construct($icon, $label, $action); + $this->data = $data; + } + public function getType(): string { return 'button'; } + + public function getAdditionalProperties(): array + { + return ['data' => $this->data]; + } } diff --git a/src/Manager/Toolbar/Factory/ToolbarFactory.php b/src/Manager/Toolbar/Factory/ToolbarFactory.php index d4731c6..39fb6a5 100644 --- a/src/Manager/Toolbar/Factory/ToolbarFactory.php +++ b/src/Manager/Toolbar/Factory/ToolbarFactory.php @@ -8,11 +8,11 @@ use App\Manager\Toolbar\Definition\Toolbar; class ToolbarFactory { - public function createToolbar(string $type): Toolbar + public function createToolbar(string $type, array $context = []): Toolbar { return match ($type) { 'manga_list' => new MangaListToolbar(), - 'chapter_list' => new ChapterListToolbar(), + 'chapter_list' => new ChapterListToolbar($context), default => throw new \InvalidArgumentException("Unknown toolbar type: $type"), }; } diff --git a/src/Message/DownloadChapter.php b/src/Message/DownloadChapter.php index d0c0489..243b3bd 100644 --- a/src/Message/DownloadChapter.php +++ b/src/Message/DownloadChapter.php @@ -2,13 +2,10 @@ namespace App\Message; -class DownloadChapter +readonly class DownloadChapter { - private int $chapterId; - - public function __construct(int $chapterId) + public function __construct(private int $chapterId) { - $this->chapterId = $chapterId; } public function getChapterId(): int diff --git a/src/Message/RefreshMetadata.php b/src/Message/RefreshMetadata.php new file mode 100644 index 0000000..0d3dd29 --- /dev/null +++ b/src/Message/RefreshMetadata.php @@ -0,0 +1,15 @@ +mangaId; + } +} diff --git a/src/MessageHandler/RefreshMetadataHandler.php b/src/MessageHandler/RefreshMetadataHandler.php new file mode 100644 index 0000000..2f4db4c --- /dev/null +++ b/src/MessageHandler/RefreshMetadataHandler.php @@ -0,0 +1,50 @@ +mangaRepository->find($message->getMangaId()); + if (!$manga) { + return; + } + + $lastChapters = $this->mangadexProvider->addAllChaptersToManga($manga); + + try { + foreach ($manga->getChapters() 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/Service/MangadexProvider.php b/src/Service/MangadexProvider.php index 3e089ce..3d0f7f7 100644 --- a/src/Service/MangadexProvider.php +++ b/src/Service/MangadexProvider.php @@ -18,18 +18,18 @@ readonly class MangadexProvider implements MetadataProviderInterface public function search(?string $title): Collection { - if($title === null) { + if ($title === null) { return new ArrayCollection(); } - try{ + try { $results = $this->client->get('/manga', [ 'title' => $title, 'contentRating' => ['safe', 'suggestive'], 'includes' => ['cover_art', 'author'], 'limit' => 25 ]); - }catch(\Exception $e){ + } catch (\Exception $e) { $this->notificationService->sendUpdate('notification', ['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); return new ArrayCollection(); } @@ -42,22 +42,21 @@ readonly class MangadexProvider implements MetadataProviderInterface ->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']) - ; + ->setStatus($result['attributes']['status']); $tags = []; - foreach($result['attributes']['tags'] as $tag){ + foreach ($result['attributes']['tags'] as $tag) { $tags[] = $tag['attributes']['name']['en']; } $mangas[count($mangas) - 1]->setGenres($tags); - foreach($result['relationships'] as $relationship) { - if($relationship['type'] === 'author') { + foreach ($result['relationships'] as $relationship) { + if ($relationship['type'] === 'author') { $mangas[count($mangas) - 1]->setAuthor($relationship['attributes']['name']); } - if($relationship['type'] === 'cover_art') { - $mangas[count($mangas) - 1]->setImageUrl('https://mangadex.org/covers/' . $result['id'] . '/' .$relationship['attributes']['fileName']); + if ($relationship['type'] === 'cover_art') { + $mangas[count($mangas) - 1]->setImageUrl('https://mangadex.org/covers/' . $result['id'] . '/' . $relationship['attributes']['fileName']); } } } @@ -68,7 +67,7 @@ readonly class MangadexProvider implements MetadataProviderInterface 'manga' => $test ]); - foreach($mangas as $manga) { + foreach ($mangas as $manga) { $manga->setRating($ratings['statistics'][$manga->getExternalId()]['rating']['average']); } @@ -77,12 +76,11 @@ readonly class MangadexProvider implements MetadataProviderInterface public function getFeed(Manga $manga): array { - if($manga->getExternalId() === null) { + if ($manga->getExternalId() === null) { return []; } $chapters = []; - $chapterEntities = []; $page = 0; do { @@ -95,44 +93,40 @@ readonly class MangadexProvider implements MetadataProviderInterface $page++; } while (count($chapters) < $results['total']); - foreach($chapters as $result) { - $chapterNumber = (float)$result['attributes']['chapter']; - - // Utilisez la méthode exists de Doctrine pour vérifier si un chapitre avec le même numéro existe déjà - $chapterExists = $manga->getChapters()->exists(function($key, $existingChapter) use ($chapterNumber) { - return $existingChapter->getNumber() === $chapterNumber; - }); - - // Si le chapitre existe déjà, on skip - if ($chapterExists) { - 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; -// $manga->addChapter($chapter); - } - - return $chapterEntities; + return $this->getChaptersFromFeed($chapters, $manga); } - private function getFeedWithPagination(string $externalId, int $page): array + public function getLastFeed(Manga $manga, int $limit = 100): array + { + if ($manga->getExternalId() === null) { + 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' => 500, - 'translatedLanguage' =>['en', 'fr'], - 'order' => ['chapter' => 'asc'], - 'offset' => $page * 500 + 'limit' => $limit, + 'translatedLanguage' => ['en', 'fr'], + 'order' => ['chapter' => $order], + 'offset' => $page * $limit ]); - }catch(\Exception $e){ + } catch (\Exception $e) { $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); return []; } @@ -142,24 +136,24 @@ readonly class MangadexProvider implements MetadataProviderInterface public function getMangaAggregate(Manga $manga): array { - if($manga->getExternalId() === null) { + if ($manga->getExternalId() === null) { return []; } try { $response = $this->client->get('/manga/' . $manga->getExternalId() . '/aggregate'); - }catch(\Exception $e){ + } catch (\Exception $e) { // $this->notificationService->sendUpdate(['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']); return []; } $chapterEntities = []; - if($response['result'] === 'ok'){ - foreach($response['volumes'] as $volume){ - $volumeNumber = $volume['volume'] === 'none' ? 0 : (float) $volume['volume']; - foreach($volume['chapters'] as $chapter){ + if ($response['result'] === 'ok') { + foreach ($response['volumes'] as $volume) { + $volumeNumber = $volume['volume'] === 'none' ? 0 : (float)$volume['volume']; + foreach ($volume['chapters'] as $chapter) { $chapterEntity = new Chapter(); - $chapterEntity->setNumber((float) $chapter['chapter']) + $chapterEntity->setNumber((float)$chapter['chapter']) ->setTitle('Chapter ' . $chapter['chapter']) ->setVolume($volumeNumber) ->setExternalId(''); @@ -171,4 +165,89 @@ readonly class MangadexProvider implements MetadataProviderInterface } return $chapterEntities; } + + /** + * @param mixed $chapters + * @param Manga $manga + * @param array $chapterEntities + * @return array + */ + 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/Twig/Components/ToolBarButton.php b/src/Twig/Components/ToolBarButton.php index 45f4edf..45725ab 100644 --- a/src/Twig/Components/ToolBarButton.php +++ b/src/Twig/Components/ToolBarButton.php @@ -3,10 +3,13 @@ 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/templates/components/ToolBarButton.html.twig b/templates/components/ToolBarButton.html.twig index 541d770..3278d04 100644 --- a/templates/components/ToolBarButton.html.twig +++ b/templates/components/ToolBarButton.html.twig @@ -1,10 +1,19 @@ {# templates/components/ToolbarButton.html.twig #} +{% set buttonAttributes = {} %} + +{% if data is defined and data is not empty %} + {% for key, value in data %} + {% set dataAttribute = 'data-' ~ key|replace({'_': '-'})|lower ~ '=' ~ value %} + {% set buttonAttributes = buttonAttributes|merge({dataAttribute}) %} + {% endfor %} +{% endif %} +