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 $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; } } }