- Portage des fonctionnalités de la branche main

- Ajout de node et npm dans la Dockerfile

- Ajout des Factories et Fixtures

- Ajout de npm-install dans Make install
This commit is contained in:
Jérémy Guillot
2024-06-03 19:41:24 +02:00
parent 41a1a8c44c
commit 291e85338a
53 changed files with 11825 additions and 18 deletions

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Service;
use Symfony\Component\BrowserKit\HttpBrowser as Client;
use Symfony\Component\DomCrawler\Crawler;
class LelScansProviderService implements MangaProviderInterface
{
const PROVIDER_URL = 'https://lelscans.net/';
const MANGA_SLUG = '/{manga}/{chapter}/{page}';
private Client $client;
public function __construct()
{
$this->client = new Client();
}
public function getMangaList(): array
{
$crawler = $this->client->request('GET', self::PROVIDER_URL);
$mangaList = [];
$crawler->filter('select > option')->each(function (Crawler $node) use (&$mangaList) {
$mangaName = $node->text();
$mangaUrl = $node->attr('value');
if ($mangaName && $mangaUrl && !preg_match('/^\d+(\.\d+)?$/', $mangaName)) {
$mangaList[] = [
'name' => $mangaName,
'url' => $mangaUrl,
];
}
});
return $mangaList;
}
public function getChapterList($mangaSlug): array
{
$crawler = $this->client->request('GET', self::PROVIDER_URL . 'lecture-en-ligne-' . $mangaSlug . '.php');
$chapterList = [];
$crawler->filter('select > option')->each(function (Crawler $node) use (&$chapterList) {
$chapterName = $node->text();
$chapterUrl = $node->attr('value');
if ($chapterName && $chapterUrl && preg_match('/^\d+(\.\d+)?$/', $chapterName)) {
$chapterList[] = [
'number' => $chapterName,
];
}
});
return $chapterList;
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Service;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use ZipArchive;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
class MangaExportService
{
const IMG_BASE_DIR = '/public/manga-images';
const EXPORT_BASE_DIR = '/public/manga-export';
private string $projectDir;
public function __construct($projectDir)
{
$this->projectDir = $projectDir;
}
public function exportMangaChapter(string $mangaTitle, int $chapterNumber): bool|string
{
$chapterDir = $this->getMangaDir($mangaTitle, $chapterNumber);
$cbzFilePath = $this->getExportDir($mangaTitle, $chapterNumber);
if(!is_dir($chapterDir)){
return false;
}
$cbzDirectory = dirname($cbzFilePath);
if (!is_dir($cbzDirectory)) {
mkdir($cbzDirectory, 0755, true);
}
$fileSystem = new Filesystem();
if($fileSystem->exists($cbzFilePath)){
return 'already_exported';
}
return $this->createCbzFromDirectory($chapterDir, $cbzFilePath);
}
public function downloadCbz(string $mangaTitle, int $chapterNumber): BinaryFileResponse|bool
{
$filePathCbz = $this->getExportDir($mangaTitle, $chapterNumber);
$fileSystem = new Filesystem();
if($fileSystem->exists($filePathCbz)){
return new BinaryFileResponse($filePathCbz);
}
$chapterDir = $this->getMangaDir($mangaTitle, $chapterNumber);
if(is_dir($chapterDir)){
if($this->exportMangaChapter($mangaTitle, $chapterNumber)){
return new BinaryFileResponse($filePathCbz);
}
}
return false;
}
private function createCbzFromDirectory(string $sourceDirectory, string $cbzFilePath): bool
{
$zip = new ZipArchive();
// Ouvre le fichier .cbz en écriture
if ($zip->open($cbzFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
return false;
}
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourceDirectory),
RecursiveIteratorIterator::LEAVES_ONLY
);
// Ajoute les fichiers d'image au fichier .cbz
foreach ($files as $file) {
if (!$file->isDir()) {
$filePath = $file->getRealPath();
$relativePath = substr($filePath, strlen($sourceDirectory) + 1);
$zip->addFile($filePath, $relativePath);
}
}
$zip->close();
return true;
}
private function getMangaDir(string $mangaTitle, int $chapterNumber): string
{
return sprintf('%s/%s/%d', $this->projectDir . self::IMG_BASE_DIR, $mangaTitle, $chapterNumber);
}
private function getExportDir(string $mangaTitle, int $chapterNumber): string
{
return sprintf('%s/%s/%d', $this->projectDir . self::EXPORT_BASE_DIR, $mangaTitle, $chapterNumber) . '.cbz';
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Service;
class MangaProviderFactory
{
public static function create($providerName): MangaProviderInterface
{
return match ($providerName) {
'LelScans' => new LelScansProviderService(),
'AutreManga' => new AutreMangaProviderService(),
default => throw new \Exception("Provider {$providerName} non supporté."),
};
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Service;
interface MangaProviderInterface
{
public function getMangaList(): array;
public function getChapterList(string $mangaSlug): array;
}

View File

@@ -0,0 +1,145 @@
<?php
namespace App\Service;
use App\Event\MangaScrapedEvent;
use GuzzleHttp\Client;
use PHPUnit\Util\PHP\AbstractPhpProcess;
use Psr\Container\ContainerInterface;
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;
class MangaScraperService
{
const IMG_BASE_DIR = '/public/manga-images';
private string $projectDir;
private EventDispatcherInterface $eventDispatcher;
public function __construct($projectDir, EventDispatcherInterface $eventDispatcher)
{
$this->projectDir = $projectDir;
$this->eventDispatcher = $eventDispatcher;
}
public function extractMangaPageData(string $html): array
{
$baseUrl = 'https://lelscans.net';
$crawler = new Crawler($html);
$imgUrl = $crawler->filter('img')->attr('src');
$nextLink = $crawler->filter('a[title="Suivant"]');
if (!preg_match('/^https?:\/\//', $imgUrl)) {
$urlComponents = parse_url($baseUrl);
$scheme = $urlComponents['scheme'];
$host = $urlComponents['host'];
// Construit l'URL absolue de l'image
$imgUrl = $scheme . '://' . $host . '/' . ltrim($imgUrl, '/');
}
if($nextLink->count() > 0){
$nextUrl = $nextLink->attr('href');
}else{
$nextUrl = null;
}
return [
'image_url' => $imgUrl,
'next_page_url' => $nextUrl,
];
}
public function scrapeMangaChapter(string $chapterUrl, string $mangaTitle, float $chapterNumber): array|bool
{
if(!$this->isChapterAvailable($chapterUrl, $chapterNumber)){
return false;
}
$pageData = [];
$currentPageUrl = $chapterUrl;
$mangaDir = sprintf('%s/%s', $this->projectDir . self::IMG_BASE_DIR, $mangaTitle);
if (!is_dir($mangaDir)) {
mkdir($mangaDir, 0755, true);
}
// Créez le dossier du chapitre s'il n'existe pas
$chapterDir = sprintf('%s/%s', $mangaDir, $chapterNumber);
if (!is_dir($chapterDir)) {
mkdir($chapterDir, 0755, true);
}
do {
$html = $this->fetchHtml($currentPageUrl);
$page = $this->extractMangaPageData($html);
$pageData[] = $page;
$currentPageUrl = $page['next_page_url'];
// Construisez le nom de fichier de l'image
$imageName = sprintf('%03d.jpg', count($pageData));
// Construisez le chemin du fichier de l'image
$imagePath = sprintf('%s/%s', $chapterDir, $imageName);
// Téléchargez et enregistrez l'image
$this->downloadAndSaveImage($page['image_url'], $imagePath);
// Modifiez les données de la page pour inclure l'URL de l'image stockée localement
$pageData[count($pageData) - 1]['local_image_url'] = sprintf('/manga-images/%s/%s/%s', $mangaTitle, $chapterNumber, $imageName);
$pageData[count($pageData) - 1]['page_number'] = count($pageData);
} while ($currentPageUrl);
$event = new MangaScrapedEvent($mangaTitle, $chapterNumber, $pageData);
$this->eventDispatcher->dispatch($event, MangaScrapedEvent::NAME);
return $pageData;
}
private function fetchHtml(string $url): string
{
$client = new Client();
$response = $client->get($url);
return (string) $response->getBody();
}
private function downloadAndSaveImage(string $imageUrl, string $destinationPath): void
{
$client = new Client();
$response = $client->get($imageUrl);
file_put_contents($destinationPath, $response->getBody()->getContents());
}
private function isChapterAvailable(string $chapterUrl, float $chapterNumber): bool
{
$html = $this->fetchHtml($chapterUrl);
$crawler = new Crawler($html);
$nextLink = $crawler->filter('a[title="Suivant"]');
if($nextLink->count() === 0){
return false;
}else{
$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);
if((float) $parameters['chapter'] !== $chapterNumber){
return false;
}
return true;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Service;
use Goutte\Client;
class SushiScanProviderService implements MangaProviderInterface
{
const PROVIDER_URL = 'https://sushiscan.com/';
const MANGA_SLUG = '/{manga}/{chapter}/{page}';
private Client $client;
public function __construct()
{
$this->client = new Client();
}
/**
* @return array
*/
public function getMangaList(): array
{
// TODO: Implement getMangaList() method.
}
/**
* @param string $mangaSlug
* @return array
*/
public function getChapterList(string $mangaSlug): array
{
// TODO: Implement getChapterList() method.
}
}