Added:
- AdditionnalData for buttons - refresh manga metadata and chapters
This commit is contained in:
@@ -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([
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
15
src/Message/RefreshMetadata.php
Normal file
15
src/Message/RefreshMetadata.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
readonly class RefreshMetadata
|
||||
{
|
||||
public function __construct(private int $mangaId)
|
||||
{
|
||||
}
|
||||
|
||||
public function getMangaId(): int
|
||||
{
|
||||
return $this->mangaId;
|
||||
}
|
||||
}
|
||||
50
src/MessageHandler/RefreshMetadataHandler.php
Normal file
50
src/MessageHandler/RefreshMetadataHandler.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Message\RefreshMetadata;
|
||||
use App\Repository\MangaRepository;
|
||||
use App\Service\MangadexProvider;
|
||||
use App\Service\NotificationService;
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
readonly class RefreshMetadataHandler
|
||||
{
|
||||
public function __construct(
|
||||
private MangaRepository $mangaRepository,
|
||||
private MangadexProvider $mangadexProvider,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private NotificationService $notificationService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(RefreshMetadata $message): void
|
||||
{
|
||||
$manga = $this->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.']);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user