gutendex integration phase 2
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
class GutendexBook
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $title,
|
||||
public array $authors,
|
||||
public array $subjects,
|
||||
public array $bookshelves,
|
||||
public array $languages,
|
||||
public ?string $copyright,
|
||||
public string $mediaType,
|
||||
public array $formats,
|
||||
public int $downloadCount,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a GutendexBook DTO from the Gutendex API response.
|
||||
*
|
||||
* @param array $data API response data
|
||||
* @return self
|
||||
*/
|
||||
public static function fromApiResponse(array $data): self
|
||||
{
|
||||
return new self(
|
||||
id: $data['id'] ?? 0,
|
||||
title: $data['title'] ?? '',
|
||||
authors: self::parseAuthors($data['authors'] ?? []),
|
||||
subjects: $data['subjects'] ?? [],
|
||||
bookshelves: $data['bookshelves'] ?? [],
|
||||
languages: $data['languages'] ?? [],
|
||||
copyright: $data['copyright'] ?? null,
|
||||
mediaType: $data['media_type'] ?? 'Text',
|
||||
formats: $data['formats'] ?? [],
|
||||
downloadCount: $data['download_count'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse authors from API response into a simplified array.
|
||||
*
|
||||
* @param array $authors Raw authors data from API
|
||||
* @return array Formatted authors with name, birth_year, death_year
|
||||
*/
|
||||
protected static function parseAuthors(array $authors): array
|
||||
{
|
||||
return array_map(function ($author) {
|
||||
return [
|
||||
'name' => $author['name'] ?? 'Unknown',
|
||||
'birth_year' => $author['birth_year'] ?? null,
|
||||
'death_year' => $author['death_year'] ?? null,
|
||||
];
|
||||
}, $authors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary author name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrimaryAuthorName(): string
|
||||
{
|
||||
if (empty($this->authors)) {
|
||||
return 'Unknown Author';
|
||||
}
|
||||
|
||||
return $this->authors[0]['name'] ?? 'Unknown Author';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all author names as a string.
|
||||
*
|
||||
* @param string $separator Separator between author names
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorNamesString(string $separator = ', '): string
|
||||
{
|
||||
$names = array_map(fn($author) => $author['name'], $this->authors);
|
||||
return implode($separator, $names) ?: 'Unknown Author';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plain text URL if available.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPlainTextUrl(): ?string
|
||||
{
|
||||
// Priority order for text formats
|
||||
$preferredFormats = [
|
||||
'text/plain; charset=utf-8',
|
||||
'text/plain; charset=us-ascii',
|
||||
'text/plain',
|
||||
];
|
||||
|
||||
foreach ($preferredFormats as $format) {
|
||||
if (isset($this->formats[$format])) {
|
||||
return $this->formats[$format];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: look for any text/plain format
|
||||
foreach ($this->formats as $format => $url) {
|
||||
if (str_contains($format, 'text/plain')) {
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML URL if available.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getHtmlUrl(): ?string
|
||||
{
|
||||
return $this->formats['text/html'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cover image URL.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCoverImageUrl(): ?string
|
||||
{
|
||||
return $this->formats['image/jpeg'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary language.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrimaryLanguage(): string
|
||||
{
|
||||
return $this->languages[0] ?? 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the most appropriate category from subjects and bookshelves.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function suggestCategory(): ?string
|
||||
{
|
||||
$categoryMapping = config('gutendex.category_mapping', []);
|
||||
|
||||
// Check bookshelves first (usually more specific)
|
||||
foreach ($this->bookshelves as $bookshelf) {
|
||||
foreach ($categoryMapping as $key => $slug) {
|
||||
if (stripos($bookshelf, $key) !== false) {
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then check subjects
|
||||
foreach ($this->subjects as $subject) {
|
||||
foreach ($categoryMapping as $key => $slug) {
|
||||
if (stripos($subject, $key) !== false) {
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default category based on common patterns
|
||||
$allText = implode(' ', array_merge($this->subjects, $this->bookshelves));
|
||||
$allTextLower = strtolower($allText);
|
||||
|
||||
if (str_contains($allTextLower, 'fiction')) {
|
||||
return 'fiction';
|
||||
}
|
||||
|
||||
if (str_contains($allTextLower, 'history')) {
|
||||
return 'history';
|
||||
}
|
||||
|
||||
if (str_contains($allTextLower, 'science')) {
|
||||
return 'science';
|
||||
}
|
||||
|
||||
if (str_contains($allTextLower, 'philosophy')) {
|
||||
return 'philosophy';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a description from subjects and bookshelves.
|
||||
*
|
||||
* @param int $maxLength Maximum length of description
|
||||
* @return string
|
||||
*/
|
||||
public function generateDescription(int $maxLength = 200): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
if (!empty($this->subjects)) {
|
||||
$parts[] = 'Subjects: ' . implode(', ', array_slice($this->subjects, 0, 3));
|
||||
}
|
||||
|
||||
if (!empty($this->bookshelves)) {
|
||||
$parts[] = 'Categories: ' . implode(', ', array_slice($this->bookshelves, 0, 2));
|
||||
}
|
||||
|
||||
$description = implode('. ', $parts);
|
||||
|
||||
if (strlen($description) > $maxLength) {
|
||||
$description = substr($description, 0, $maxLength - 3) . '...';
|
||||
}
|
||||
|
||||
return $description ?: 'A book from Project Gutenberg.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this book is in the public domain.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPublicDomain(): bool
|
||||
{
|
||||
return $this->copyright === null || $this->copyright === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the DTO to an array suitable for database insertion.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toBookArray(): array
|
||||
{
|
||||
return [
|
||||
'gutenberg_id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'author' => $this->getPrimaryAuthorName(),
|
||||
'cover_image' => $this->getCoverImageUrl(),
|
||||
'description' => $this->generateDescription(),
|
||||
'language' => $this->getPrimaryLanguage(),
|
||||
'full_text_url' => $this->getPlainTextUrl(),
|
||||
'source' => 'gutendex',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all author names for the many-to-many relationship.
|
||||
*
|
||||
* @return array Array of author names
|
||||
*/
|
||||
public function getAuthorNames(): array
|
||||
{
|
||||
return array_map(fn($author) => $author['name'], $this->authors);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user