feat: ajout de la gestion des sources préférées pour les mangas, incluant la récupération et la configuration des sources via l'API, ainsi que l'intégration d'une modale pour l'interface utilisateur.
This commit is contained in:
parent
15d92d1aff
commit
75f8e1686c
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\ApiPlatform\Dto;
|
||||
|
||||
readonly class MangaPreferredSourcesDetail
|
||||
{
|
||||
public function __construct(
|
||||
public string $mangaId,
|
||||
public array $sources,
|
||||
public bool $hasPreferredSources
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||
use ApiPlatform\OpenApi\Model\Response;
|
||||
use App\Domain\Scraping\Infrastructure\ApiPlatform\Dto\MangaPreferredSourcesDetail;
|
||||
use App\Domain\Scraping\Infrastructure\ApiPlatform\State\Provider\GetMangaPreferredSourcesStateProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Scraping',
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/mangas/{id}/preferred-sources',
|
||||
provider: GetMangaPreferredSourcesStateProvider::class,
|
||||
output: MangaPreferredSourcesDetail::class,
|
||||
description: 'Récupérer les sources préférées d\'un manga ou toutes les sources si aucune préférence',
|
||||
openapi: new OpenApiOperation(
|
||||
summary: 'Récupérer les sources préférées d\'un manga',
|
||||
description: 'Retourne les sources préférées configurées pour un manga, ou toutes les sources disponibles si aucune préférence définie',
|
||||
responses: [
|
||||
'200' => new Response(
|
||||
description: 'Sources récupérées avec succès',
|
||||
content: new \ArrayObject([
|
||||
'application/json' => [
|
||||
'schema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'mangaId' => ['type' => 'string'],
|
||||
'hasPreferredSources' => ['type' => 'boolean'],
|
||||
'sources' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'baseUrl' => ['type' => 'string'],
|
||||
'description' => ['type' => 'string'],
|
||||
'isActive' => ['type' => 'boolean']
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'example' => [
|
||||
'mangaId' => '1',
|
||||
'hasPreferredSources' => true,
|
||||
'sources' => [
|
||||
[
|
||||
'id' => '1',
|
||||
'name' => 'MangaDex',
|
||||
'baseUrl' => 'https://mangadex.org',
|
||||
'description' => 'Source principale',
|
||||
'isActive' => true
|
||||
],
|
||||
[
|
||||
'id' => '2',
|
||||
'name' => 'MangaKakalot',
|
||||
'baseUrl' => 'https://mangakakalot.com',
|
||||
'description' => 'Source secondaire',
|
||||
'isActive' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)]
|
||||
class GetMangaPreferredSourcesResource
|
||||
{
|
||||
public function __construct(
|
||||
public string $id
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||
use ApiPlatform\OpenApi\Model\RequestBody;
|
||||
use App\Domain\Scraping\Infrastructure\ApiPlatform\State\Processor\SetMangaPreferredSourcesStateProcessor;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Scraping',
|
||||
operations: [
|
||||
new Post(
|
||||
uriTemplate: '/mangas/{id}/preferred-sources',
|
||||
processor: SetMangaPreferredSourcesStateProcessor::class,
|
||||
status: 200,
|
||||
description: 'Définir les sources préférées d\'un manga dans l\'ordre de priorité',
|
||||
openapi: new OpenApiOperation(
|
||||
summary: 'Configurer les sources préférées d\'un manga',
|
||||
description: 'Définit l\'ordre de priorité des sources de scraping pour un manga. Format attendu: {"sourceIds": ["source1", "source2"]}',
|
||||
requestBody: new RequestBody(
|
||||
content: new \ArrayObject([
|
||||
'application/json' => [
|
||||
'schema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'sourceIds' => [
|
||||
'type' => 'array',
|
||||
'items' => ['type' => 'string']
|
||||
]
|
||||
]
|
||||
],
|
||||
'example' => [
|
||||
'sourceIds' => ['1', '2']
|
||||
]
|
||||
]
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)]
|
||||
class SetMangaPreferredSourcesResource
|
||||
{
|
||||
public function __construct(
|
||||
#[Assert\NotNull]
|
||||
#[Assert\All([
|
||||
new Assert\Type('string')
|
||||
])]
|
||||
public array $sourceIds = []
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\ApiPlatform\State\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Domain\Scraping\Application\Command\SetMangaPreferredSources;
|
||||
use App\Domain\Scraping\Infrastructure\ApiPlatform\Resource\SetMangaPreferredSourcesResource;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
final class SetMangaPreferredSourcesStateProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MessageBusInterface $commandBus
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SetMangaPreferredSourcesResource $data
|
||||
*/
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
$mangaId = $uriVariables['id'] ?? null;
|
||||
|
||||
if (!$mangaId) {
|
||||
throw new \InvalidArgumentException('Manga ID is required');
|
||||
}
|
||||
|
||||
$this->commandBus->dispatch(
|
||||
new SetMangaPreferredSources(
|
||||
(string) $mangaId,
|
||||
$data->sourceIds
|
||||
)
|
||||
);
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\ApiPlatform\State\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Domain\Scraping\Application\Query\GetMangaPreferredSources;
|
||||
use App\Domain\Scraping\Application\QueryHandler\GetMangaPreferredSourcesHandler;
|
||||
use App\Domain\Scraping\Infrastructure\ApiPlatform\Dto\MangaPreferredSourcesDetail;
|
||||
|
||||
final class GetMangaPreferredSourcesStateProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GetMangaPreferredSourcesHandler $queryHandler
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): MangaPreferredSourcesDetail
|
||||
{
|
||||
$mangaId = $uriVariables['id'] ?? null;
|
||||
|
||||
if (!$mangaId) {
|
||||
throw new \InvalidArgumentException('Manga ID is required');
|
||||
}
|
||||
|
||||
$query = new GetMangaPreferredSources((string) $mangaId);
|
||||
$response = $this->queryHandler->handle($query);
|
||||
|
||||
// Convertir les objets Source en array pour l'API
|
||||
$sourcesData = array_map(function ($source) {
|
||||
return [
|
||||
'id' => $source->getId()->getValue(),
|
||||
'name' => $source->getName(),
|
||||
'baseUrl' => $source->getBaseUrl(),
|
||||
'description' => $source->getDescription(),
|
||||
'isActive' => $source->isActive()
|
||||
];
|
||||
}, $response->sources);
|
||||
|
||||
return new MangaPreferredSourcesDetail(
|
||||
$response->mangaId,
|
||||
$sourcesData,
|
||||
$response->hasPreferredSources
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Scraping\Infrastructure\CommandHandler;
|
||||
|
||||
use App\Domain\Scraping\Application\Command\SetMangaPreferredSources;
|
||||
use App\Domain\Scraping\Application\CommandHandler\SetMangaPreferredSourcesHandler;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
class SymfonySetMangaPreferredSourcesHandler
|
||||
{
|
||||
public function __construct(
|
||||
private SetMangaPreferredSourcesHandler $handler
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(SetMangaPreferredSources $command): void
|
||||
{
|
||||
$this->handler->handle($command);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Domain\Scraping\Infrastructure\Persistence;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Scraping\Domain\Model\Manga;
|
||||
use App\Entity\Manga as EntityManga;
|
||||
use App\Entity\ContentSource;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
||||
@@ -26,17 +27,49 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
||||
// Récupération des sources préférées
|
||||
$preferredSourceIds = [];
|
||||
foreach ($mangaEntity->getPreferredSources() as $source) {
|
||||
$preferredSourceIds[] = $source->getId();
|
||||
$preferredSourceIds[] = (string) $source->getId();
|
||||
}
|
||||
|
||||
return new Manga(
|
||||
$mangaEntity->getId(),
|
||||
(string) $mangaEntity->getId(),
|
||||
$mangaEntity->getTitle(),
|
||||
$mangaEntity->getSlug(),
|
||||
$mangaEntity->getDescription() ?? '',
|
||||
$mangaEntity->getAuthor() ?? '',
|
||||
$mangaEntity->getPublicationYear() ?? '',
|
||||
(string) ($mangaEntity->getPublicationYear() ?? ''),
|
||||
$preferredSourceIds,
|
||||
);
|
||||
}
|
||||
|
||||
public function updatePreferredSources(string $mangaId, array $sourceIds): void
|
||||
{
|
||||
/** @var EntityManga|null $mangaEntity */
|
||||
$mangaEntity = $this->entityManager->getRepository(EntityManga::class)->find($mangaId);
|
||||
|
||||
if (!$mangaEntity) {
|
||||
throw new \InvalidArgumentException("Manga not found with ID: {$mangaId}");
|
||||
}
|
||||
|
||||
// Si pas de sources, vider les sources préférées
|
||||
if (empty($sourceIds)) {
|
||||
$mangaEntity->setPreferredSources([]);
|
||||
$this->entityManager->flush();
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les sources existantes
|
||||
$sources = $this->entityManager->getRepository(ContentSource::class)->findBy(['id' => $sourceIds]);
|
||||
|
||||
// Maintenir l'ordre exact des sources comme dans l'ancien controller
|
||||
$orderedPreferredSources = array_map(
|
||||
fn ($id) => current(array_filter($sources, fn ($s) => $s->getId() == $id)),
|
||||
$sourceIds
|
||||
);
|
||||
|
||||
// Filtrer les sources nulles (au cas où certaines n'existeraient pas)
|
||||
$validSources = array_filter($orderedPreferredSources);
|
||||
|
||||
$mangaEntity->setPreferredSources($validSources);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,34 +17,16 @@ readonly class LegacySourceRepository implements SourceRepositoryInterface
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SourceNotFoundException
|
||||
*/
|
||||
public function getById(string $id): Source
|
||||
public function getById(string $id): ?Source
|
||||
{
|
||||
/** @var ContentSource|null $source */
|
||||
$source = $this->entityManager->getRepository(ContentSource::class)->find($id);
|
||||
|
||||
if (!$source) {
|
||||
throw new SourceNotFoundException("Source not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Source(
|
||||
id: new SourceId($source->getId()),
|
||||
name: $source->getCleanBaseUrl(),
|
||||
description: 'Legacy Source: ' . $source->getBaseUrl(),
|
||||
baseUrl: $source->getBaseUrl(),
|
||||
scrappingParameters: [
|
||||
'imageSelector' => $source->getImageSelector(),
|
||||
'nextPageSelector' => $source->getNextPageSelector(),
|
||||
'chapterUrlFormat' => $source->getChapterUrlFormat(),
|
||||
'scrapingType' => $source->getScrapingType(),
|
||||
'chapterSelector' => $source->getChapterSelector()
|
||||
],
|
||||
isActive: true,
|
||||
createdAt: new DateTimeImmutable(),
|
||||
updatedAt: new DateTimeImmutable()
|
||||
);
|
||||
return $this->convertEntityToModel($source);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,13 +45,70 @@ readonly class LegacySourceRepository implements SourceRepositoryInterface
|
||||
return $sources;
|
||||
}
|
||||
|
||||
public function validateSourcesExist(array $sourceIds): bool
|
||||
{
|
||||
if (empty($sourceIds)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compter le nombre de sources qui existent réellement
|
||||
$existingCount = $this->entityManager->getRepository(ContentSource::class)
|
||||
->createQueryBuilder('c')
|
||||
->select('COUNT(c.id)')
|
||||
->where('c.id IN (:ids)')
|
||||
->setParameter('ids', $sourceIds)
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
|
||||
// Vérifier que toutes les sources demandées existent
|
||||
return $existingCount === count($sourceIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getByIds(array $sourceIds): array
|
||||
{
|
||||
if (empty($sourceIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var ContentSource[] $sourceEntities */
|
||||
$sourceEntities = $this->entityManager->getRepository(ContentSource::class)->findBy(['id' => $sourceIds]);
|
||||
|
||||
// Maintenir l'ordre des IDs fournis
|
||||
$sourcesMap = [];
|
||||
foreach ($sourceEntities as $sourceEntity) {
|
||||
$sourcesMap[$sourceEntity->getId()] = $this->convertEntityToModel($sourceEntity);
|
||||
}
|
||||
|
||||
$orderedSources = [];
|
||||
foreach ($sourceIds as $sourceId) {
|
||||
if (isset($sourcesMap[$sourceId])) {
|
||||
$orderedSources[] = $sourcesMap[$sourceId];
|
||||
}
|
||||
}
|
||||
|
||||
return $orderedSources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getAllActive(): array
|
||||
{
|
||||
// Pour le moment, toutes les sources sont considérées comme actives
|
||||
// dans le système legacy
|
||||
return $this->getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une entité ContentSource en modèle Source
|
||||
*/
|
||||
private function convertEntityToModel(ContentSource $source): Source
|
||||
{
|
||||
return new Source(
|
||||
id: new SourceId($source->getId()),
|
||||
id: new SourceId((string) $source->getId()),
|
||||
name: $source->getCleanBaseUrl(),
|
||||
description: 'Legacy Source: ' . $source->getBaseUrl(),
|
||||
baseUrl: $source->getBaseUrl(),
|
||||
|
||||
Reference in New Issue
Block a user