- turbo + code adaptation
- cover & thumbnails download
This commit is contained in:
Jérémy Guillot
2024-07-06 21:25:07 +02:00
parent 7dee9d71be
commit 54c581b229
35 changed files with 1126 additions and 573 deletions

View File

@@ -89,6 +89,7 @@ class ActivityController extends AbstractController
'manga' => $manga->getTitle(),
'volume' => $chapter->getVolume(),
'chapter' => $chapter->getNumber(),
'chapterId' => $chapter->getId(),
'title' => $chapter->getTitle(),
];
}

View File

@@ -15,31 +15,42 @@ use App\Service\NotificationService;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\ImageManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\String\Slugger\SluggerInterface;
class MangaController extends AbstractController
{
private ImageManager $imageManager;
public function __construct(
private readonly string $projectDir,
private readonly MangaRepository $mangaRepository,
private readonly ChapterRepository $chapterRepository,
private readonly MessageBusInterface $bus,
private readonly CbzService $cbzService,
private readonly ToolbarFactory $toolbarFactory,
private readonly MangadexProvider $mangadexProvider,
private readonly EntityManagerInterface $entityManager
private readonly EntityManagerInterface $entityManager,
private readonly NotificationService $notificationService,
private readonly SluggerInterface $slugger
)
{
$this->imageManager = new ImageManager(new Driver());
}
#[Route('/manga', name: 'app_manga')]
#[Route('/', name: 'app_manga')]
public function index(Request $request): Response
{
$sort = $request->query->get('sort', 'title');
@@ -57,11 +68,8 @@ class MangaController extends AbstractController
]);
}
/**
* @throws NonUniqueResultException
*/
#[Route('/manga/chapters/{mangaSlug}', name: 'app_manga_show')]
public function showChapters(string $mangaSlug): Response
public function showChapters(string $mangaSlug, Request $request): Response
{
// $manga = $this->mangaRepository->findOneWithChapterBy(['slug' => $mangaSlug]);
$manga = $this->mangaRepository->findOneBy(['slug' => $mangaSlug]);
@@ -70,6 +78,16 @@ class MangaController extends AbstractController
throw new NotFoundHttpException("Le manga demandé n'existe pas.");
}
return $this->render('manga/show_chapters.html.twig', [
'manga' => $manga,
'toolbar' => $this->toolbarFactory->createToolbar('chapter_list', ['mangaId' => $manga->getId()])->getGroups(),
]);
}
public function _chaptersByManga(int $id): Response
{
$manga = $this->mangaRepository->find($id);
$chaptersByVolume = [];
foreach ($manga->getChapters() as $chapter) {
$volume = $chapter->getVolume() ?? 'Not Found';
@@ -92,10 +110,10 @@ class MangaController extends AbstractController
}
return $b <=> $a;
});
return $this->render('manga/show_chapters.html.twig', [
'chapters_by_volume' => $chaptersByVolume,
return $this->render('manga/_chapter_list.html.twig', [
'manga' => $manga,
'toolbar' => $this->toolbarFactory->createToolbar('chapter_list', ['mangaId' => $manga->getId()])->getGroups(),
'chapters_by_volume' => $chaptersByVolume
]);
}
@@ -154,11 +172,15 @@ class MangaController extends AbstractController
#[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'))
->setImageUrl($request->request->get('imageUrl'))
->setStatus($request->request->get('status'))
->setGenres(explode(',', $request->request->get('genres')))
->setAuthor($request->request->get('author'))
@@ -166,6 +188,16 @@ class MangaController extends AbstractController
->setRating($request->request->get('rating'))
->setExternalId($request->request->get('externalId'));
// Traitement de l'image
$imageUrl = $request->request->get('imageUrl');
try {
$imageUrls = $this->processAndSaveImage($imageUrl);
$manga->setImageUrl($imageUrls['full']);
$manga->setThumbnailUrl($imageUrls['thumbnail']);
} catch (\Exception $e) {
// Gérer l'exception (par exemple, logger l'erreur)
}
$mergedChapters = $this->mangadexProvider->addAllChaptersToManga($manga);
if (empty($mergedChapters)) {
@@ -189,7 +221,48 @@ class MangaController extends AbstractController
return $this->redirectToRoute('app_manga_show', ['mangaSlug' => $manga->getSlug()]);
}
#[Route('/addChapter/{id}', name: 'add_chapter')]
/**
* @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);
$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;
}
}
#[Route('/searchChapter/{id}', name: 'search_chapter')]
public function addChapterMessenger(int $id): JsonResponse
{
$chapter = $this->chapterRepository->find($id);
@@ -204,36 +277,85 @@ class MangaController extends AbstractController
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
]);
if (empty($volumeChapters)) {
$this->notificationService->sendUpdate(['error' => '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
public function downloadChapter(int $chapterId): BinaryFileResponse|JsonResponse
{
$chapter = $this->chapterRepository->find($chapterId);
if (!$chapter) {
throw $this->createNotFoundException("Le chapitre demandé n'existe pas.");
$this->notificationService->sendUpdate(['error' => 'Chapitre non trouvé.']);
return new JsonResponse(['error' => 'Chapitre non trouvé.'], 200);
}
$cbzPath = $chapter->getCbzPath();
if (!$cbzPath || !file_exists($cbzPath)) {
throw $this->createNotFoundException("Le fichier CBZ n'existe pas.");
$this->notificationService->sendUpdate(['error' => 'Le fichier CBZ n\'existe pas.']);
return new JsonResponse(['error' => 'Le fichier CBZ n\'existe pas.'], 200);
}
$response = new BinaryFileResponse($cbzPath);
// Vérifier si c'est un volume complet ou un chapitre individuel
$isFullVolume = $this->isFullVolume($chapter);
$fileName = $isFullVolume
? $this->cbzService->generateFileName($chapter->getManga(), $chapter->getVolume())
: $this->cbzService->generateFileName($chapter->getManga(), null, $chapter->getNumber());
if ($isFullVolume) {
$fileName = sprintf("%s_volume_%02d.cbz", $chapter->getManga()->getSlug(), $chapter->getVolume());
} else {
$fileName = sprintf("%s_chapter_%s.cbz", $chapter->getManga()->getSlug(), number_format($chapter->getNumber(), 2));
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
]);
if (empty($volumeChapters)) {
$this->notificationService->sendUpdate(['error' => 'Aucun chapitre trouvé pour ce volume.']);
}
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$fileName
);
if (!$this->cbzService->doAllChaptersHaveCbz($volumeChapters)) {
$this->notificationService->sendUpdate(['error' => '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);
}
return $response;
$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')]

View File

@@ -8,83 +8,94 @@ use App\Entity\Manga;
use App\Message\DownloadChapter;
use App\Repository\ChapterRepository;
use App\Repository\MangaRepository;
use App\Service\ActivityService;
use App\Service\MangadexProvider;
use App\Service\MangaScraperService;
use App\Service\MangaUpdatesMetadataProvider;
use App\Service\SushiScanProviderService;
use Doctrine\DBAL\Connection;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\ImageManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\String\Slugger\SluggerInterface;
class TestController extends AbstractController
{
private ImageManager $imageManager;
public function __construct(
private MangadexProvider $mangadexProvider,
private MangaRepository $mangaRepository,
private MessageBusInterface $bus,
private Connection $connection,
private SerializerInterface $serializer,
private readonly ChapterRepository $chapterRepository
private string $projectDir,
private SluggerInterface $slugger,
private MangaRepository $mangaRepository
)
{
$this->imageManager = new ImageManager(new Driver());
}
#[Route('/test', name: 'test')]
public function test(): Response
{
$sqlPending = 'SELECT * FROM messenger_messages WHERE queue_name = :queue';
$pending = $this->connection->fetchAllAssociative($sqlPending, ['queue' => 'default']);
$mangas = $this->mangaRepository->findAll();
// // 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']);
// dd($pending);
$decoded = $this->decodeMessages($pending);
$status = [];
foreach($decoded as $message) {
$message = $message['body'];
if($message instanceof Envelope) {
$chapter = $this->chapterRepository->find($message->getMessage()->getChapterId());
$manga = $chapter->getManga();
$status[] = [
'manga' => $manga->getTitle(),
'volume' => $chapter->getVolume(),
'chapter' => $chapter->getNumber(),
'title' => $chapter->getTitle(),
];
$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++;
}
}
// $this->bus->dispatch(new DownloadChapter(1));
dd($status);
return new JsonResponse(['changed' => $changed]);
}
private function decodeMessages(array $messages): array
/**
* @throws GuzzleException
*/
private function processAndSaveImage(string $imageUrl): array
{
$decodedMessages = [];
$image = file_get_contents($this->projectDir . '/public' .$imageUrl);
$tempImage = tmpfile();
fwrite($tempImage, $image);
$tempImagePath = stream_get_meta_data($tempImage)['uri'];
foreach ($messages as $message) {
$decodedMessages[] = [
'id' => $message['id'],
'body' => $this->decodeMessageBody($message['body']),
'headers' => json_decode($message['headers'], true),
// 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;
}
return $decodedMessages;
}
private function decodeMessageBody(string $body)
{
return unserialize(stripcslashes($body));
}
}