- Endpoint GET /api/manga-discover via DiscoverMangaStateProvider + DiscoverMangaHandler
- Algorithme : top 5 manga de la collection → appel /manga/{id}/recommendation
par source → agrégation avec système de votes (multi-sources = plus pertinent)
- Filtrage : tags exclus (Oneshot, Doujinshi, Self-Published), contentRating,
et suppression des manga déjà en bibliothèque
- Page Vue DiscoverPage.vue : chargement auto au montage, bouton Actualiser,
modal détail, ajout à la bibliothèque
- Adapteurs InMemory de test mis à jour (discover + getMangaRecommendations)
208 lines
6.6 KiB
PHP
208 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace App\Domain\Manga\Infrastructure\Provider;
|
|
|
|
use App\Domain\Manga\Domain\Contract\Client\MangadexClientInterface;
|
|
use App\Domain\Manga\Domain\Contract\Provider\MangaProviderInterface;
|
|
use App\Domain\Manga\Domain\Model\Manga;
|
|
use App\Domain\Manga\Domain\Model\MangaCollection;
|
|
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
|
|
use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
|
|
use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
|
|
use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
|
|
use Ramsey\Uuid\Uuid;
|
|
use Symfony\Component\String\Slugger\SluggerInterface;
|
|
|
|
readonly class MangadexProvider implements MangaProviderInterface
|
|
{
|
|
public function __construct(
|
|
private MangadexClientInterface $client,
|
|
private SluggerInterface $slugger
|
|
) {
|
|
}
|
|
|
|
public function search(string $title): MangaCollection
|
|
{
|
|
$results = $this->client->searchManga($title);
|
|
|
|
if (empty($results['data'])) {
|
|
return new MangaCollection([]);
|
|
}
|
|
|
|
$mangas = $this->createMangasFromResults($results['data']);
|
|
$this->enrichWithRatings($mangas);
|
|
|
|
usort($mangas, fn ($a, $b) => ($b->getRating() ?? 0) <=> ($a->getRating() ?? 0));
|
|
|
|
return new MangaCollection($mangas);
|
|
}
|
|
|
|
/**
|
|
* @param array<mixed> $results
|
|
* @return Manga[]
|
|
*/
|
|
private function createMangasFromResults(array $results): array
|
|
{
|
|
$mangas = [];
|
|
foreach ($results as $result) {
|
|
$manga = $this->createMangaFromResult($result);
|
|
if ($manga !== null) {
|
|
$mangas[] = $manga;
|
|
}
|
|
}
|
|
|
|
return $mangas;
|
|
}
|
|
|
|
private function createMangaFromResult(array $result): ?Manga
|
|
{
|
|
try {
|
|
$attributes = $result['attributes'];
|
|
$title = $attributes['title']['en']
|
|
?? $attributes['title']['fr']
|
|
?? $attributes['title']['ja-ro']
|
|
?? $attributes['title']['ko-ro']
|
|
?? $attributes['title']['zh-ro']
|
|
?? (!empty($attributes['title']) ? reset($attributes['title']) : null);
|
|
|
|
if (!$title) {
|
|
return null;
|
|
}
|
|
|
|
$genres = array_map(
|
|
fn ($tag) => $tag['attributes']['name']['en'],
|
|
$attributes['tags']
|
|
);
|
|
|
|
$author = '';
|
|
$imageUrl = null;
|
|
foreach ($result['relationships'] as $relationship) {
|
|
if ($relationship['type'] === 'author') {
|
|
$author = $relationship['attributes']['name'];
|
|
}
|
|
if ($relationship['type'] === 'cover_art') {
|
|
$imageUrl = sprintf(
|
|
'https://uploads.mangadex.org/covers/%s/%s.512.jpg',
|
|
$result['id'],
|
|
$relationship['attributes']['fileName']
|
|
);
|
|
}
|
|
}
|
|
|
|
return new Manga(
|
|
id: new MangaId((string) Uuid::uuid4()),
|
|
title: new MangaTitle($title),
|
|
slug: new MangaSlug($this->slugger->slug($title)->lower()),
|
|
description: $attributes['description']['fr'] ?? $attributes['description']['en'] ?? '',
|
|
author: $author,
|
|
publicationYear: $attributes['year'] ?? 0,
|
|
genres: $genres,
|
|
status: $attributes['status'],
|
|
externalId: new ExternalId($result['id']),
|
|
imageUrl: $imageUrl,
|
|
rating: null,
|
|
imageUrls: null,
|
|
createdAt: new \DateTimeImmutable(),
|
|
);
|
|
} catch (\Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Manga[] $mangas
|
|
*/
|
|
private function enrichWithRatings(array $mangas): void
|
|
{
|
|
$externalIds = array_map(
|
|
fn (Manga $manga) => $manga->getExternalId()->getValue(),
|
|
$mangas
|
|
);
|
|
|
|
try {
|
|
$ratings = $this->client->getMangaRatings($externalIds);
|
|
} catch (\Exception $e) {
|
|
return;
|
|
}
|
|
|
|
if (isset($ratings['statistics'])) {
|
|
foreach ($mangas as $manga) {
|
|
$externalId = $manga->getExternalId()->getValue();
|
|
if (isset($ratings['statistics'][$externalId]['rating']['average'])) {
|
|
$manga->setRating($ratings['statistics'][$externalId]['rating']['average']);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function discover(array $sourceExternalIds): MangaCollection
|
|
{
|
|
if (empty($sourceExternalIds)) {
|
|
return new MangaCollection([]);
|
|
}
|
|
|
|
// Compter les votes : un manga recommandé par plusieurs sources est plus pertinent.
|
|
// On conserve aussi la position d'apparition pour départager les ex-aequo.
|
|
$votes = [];
|
|
$firstPosition = [];
|
|
$resultsById = [];
|
|
$position = 0;
|
|
|
|
foreach ($sourceExternalIds as $externalId) {
|
|
try {
|
|
$response = $this->client->getMangaRecommendations($externalId);
|
|
foreach ($response['data'] ?? [] as $result) {
|
|
$id = $result['id'];
|
|
$votes[$id] = ($votes[$id] ?? 0) + 1;
|
|
if (!isset($firstPosition[$id])) {
|
|
$firstPosition[$id] = $position++;
|
|
$resultsById[$id] = $result;
|
|
}
|
|
}
|
|
} catch (\Exception) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (empty($resultsById)) {
|
|
return new MangaCollection([]);
|
|
}
|
|
|
|
// Trier : votes décroissants (multi-sources = plus pertinent), puis position croissante (score API)
|
|
uksort($resultsById, function (string $a, string $b) use ($votes, $firstPosition): int {
|
|
$voteDiff = $votes[$b] - $votes[$a];
|
|
if ($voteDiff !== 0) {
|
|
return $voteDiff;
|
|
}
|
|
|
|
return $firstPosition[$a] <=> $firstPosition[$b];
|
|
});
|
|
|
|
$mangas = $this->createMangasFromResults(array_values($resultsById));
|
|
$this->enrichWithRatings($mangas);
|
|
|
|
return new MangaCollection($mangas);
|
|
}
|
|
|
|
public function findByExternalId(ExternalId $externalId): ?Manga
|
|
{
|
|
try {
|
|
$result = $this->client->getManga($externalId->getValue());
|
|
|
|
if (!isset($result['data'])) {
|
|
return null;
|
|
}
|
|
|
|
$manga = $this->createMangaFromResult($result['data']);
|
|
|
|
if ($manga) {
|
|
$this->enrichWithRatings([$manga]);
|
|
}
|
|
|
|
return $manga;
|
|
} catch (\Exception) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|