mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
249 lines
7.8 KiB
PHP
249 lines
7.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Exceptions\BookProviderException;
|
|
use App\Support\BookIsbn;
|
|
use Carbon\Carbon;
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\Exception\GuzzleException;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use JsonException;
|
|
|
|
class GoogleBooksService
|
|
{
|
|
protected const API_URL = 'https://www.googleapis.com/books/v1/volumes';
|
|
|
|
protected Client $client;
|
|
|
|
protected ?string $apiKey;
|
|
|
|
protected int $maxResults = 5;
|
|
|
|
public function __construct(?Client $client = null, ?string $apiKey = null)
|
|
{
|
|
$this->apiKey = trim($apiKey ?? (string) config('nntmux_api.google_books_api_key', '')) ?: null;
|
|
|
|
$this->client = $client ?? new Client([
|
|
'timeout' => 15,
|
|
'connect_timeout' => 10,
|
|
'http_errors' => false,
|
|
'headers' => [
|
|
'Accept' => 'application/json',
|
|
'User-Agent' => 'NNTmux/1.0',
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function hasApiKey(): bool
|
|
{
|
|
return $this->apiKey !== null;
|
|
}
|
|
|
|
public function isConfigured(): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function findByIsbn(string $isbn): ?array
|
|
{
|
|
$books = $this->searchBooks('', null, null, $isbn);
|
|
|
|
return $books[0] ?? null;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function searchBook(string $query, ?string $title = null, ?string $author = null): ?array
|
|
{
|
|
$books = $this->searchBooks($query, $title, $author);
|
|
|
|
return $books[0] ?? null;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function searchBooks(string $query, ?string $title = null, ?string $author = null, ?string $isbn = null): array
|
|
{
|
|
$query = trim($query);
|
|
$title = $title !== null ? trim($title) : null;
|
|
$author = $author !== null ? trim($author) : null;
|
|
$isbn = BookIsbn::normalize($isbn);
|
|
|
|
$searchQuery = $this->buildSearchQuery($query, $title, $author, $isbn);
|
|
if ($searchQuery === '') {
|
|
return [];
|
|
}
|
|
|
|
$cacheKey = 'google_books_search_'.md5($searchQuery.'_'.$this->maxResults.'_'.$this->apiKey);
|
|
$cached = Cache::get($cacheKey);
|
|
if (is_array($cached)) {
|
|
return $cached;
|
|
}
|
|
|
|
$response = $this->request([
|
|
'q' => $searchQuery,
|
|
'maxResults' => $this->maxResults,
|
|
'printType' => 'books',
|
|
]);
|
|
if (! is_array($response['items'] ?? null)) {
|
|
return [];
|
|
}
|
|
|
|
$books = [];
|
|
foreach ($response['items'] as $item) {
|
|
if (is_array($item)) {
|
|
$books[] = $this->normalizeBookResult($item);
|
|
}
|
|
}
|
|
|
|
Cache::put($cacheKey, $books, now()->addHours(12));
|
|
|
|
return $books;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $query
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
protected function request(array $query): ?array
|
|
{
|
|
try {
|
|
if ($this->apiKey !== null) {
|
|
$query['key'] = $this->apiKey;
|
|
}
|
|
|
|
$response = $this->client->get(self::API_URL, [
|
|
'http_errors' => false,
|
|
'query' => $query,
|
|
]);
|
|
if ($response->getStatusCode() !== 200) {
|
|
throw new BookProviderException(
|
|
'google_books',
|
|
'Google Books API request failed.',
|
|
$response->getStatusCode(),
|
|
);
|
|
}
|
|
|
|
$decoded = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
|
|
if (! is_array($decoded)) {
|
|
throw new BookProviderException('google_books', 'Google Books returned an invalid response payload.', 200);
|
|
}
|
|
|
|
return $decoded;
|
|
} catch (BookProviderException $exception) {
|
|
throw $exception;
|
|
} catch (JsonException $exception) {
|
|
throw new BookProviderException(
|
|
'google_books',
|
|
'Google Books returned malformed JSON.',
|
|
200,
|
|
null,
|
|
$exception,
|
|
);
|
|
} catch (GuzzleException $e) {
|
|
throw new BookProviderException(
|
|
'google_books',
|
|
'Google Books API request error: '.$e->getMessage(),
|
|
null,
|
|
null,
|
|
$e,
|
|
);
|
|
}
|
|
}
|
|
|
|
private function buildSearchQuery(string $query, ?string $title, ?string $author, ?string $isbn): string
|
|
{
|
|
if ($isbn !== null && $isbn !== '') {
|
|
return 'isbn:'.$isbn;
|
|
}
|
|
|
|
$parts = [];
|
|
if ($title !== null && $title !== '') {
|
|
$parts[] = 'intitle:'.$title;
|
|
}
|
|
if ($author !== null && $author !== '') {
|
|
$parts[] = 'inauthor:'.$author;
|
|
}
|
|
|
|
if ($parts !== []) {
|
|
return implode(' ', $parts);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $item
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function normalizeBookResult(array $item): array
|
|
{
|
|
$volumeInfo = is_array($item['volumeInfo'] ?? null) ? $item['volumeInfo'] : [];
|
|
$industryIdentifiers = is_array($volumeInfo['industryIdentifiers'] ?? null) ? $volumeInfo['industryIdentifiers'] : [];
|
|
|
|
$isbn13 = '';
|
|
$isbn10 = '';
|
|
foreach ($industryIdentifiers as $identifier) {
|
|
if (! is_array($identifier)) {
|
|
continue;
|
|
}
|
|
$type = (string) ($identifier['type'] ?? '');
|
|
$value = BookIsbn::normalize((string) ($identifier['identifier'] ?? ''));
|
|
if ($type === 'ISBN_13' && $value !== null && strlen($value) === 13) {
|
|
$isbn13 = $value;
|
|
}
|
|
if ($type === 'ISBN_10' && $value !== null && strlen($value) === 10) {
|
|
$isbn10 = $value;
|
|
}
|
|
}
|
|
|
|
$identifier = $isbn13 !== '' ? $isbn13 : ($isbn10 !== '' ? $isbn10 : (string) ($item['id'] ?? md5(json_encode($item) ?: '')));
|
|
$authors = is_array($volumeInfo['authors'] ?? null)
|
|
? array_values(array_filter(array_map('strval', $volumeInfo['authors'])))
|
|
: [];
|
|
$categories = is_array($volumeInfo['categories'] ?? null)
|
|
? array_values(array_filter(array_map('strval', $volumeInfo['categories'])))
|
|
: [];
|
|
$imageLinks = is_array($volumeInfo['imageLinks'] ?? null) ? $volumeInfo['imageLinks'] : [];
|
|
$coverUrl = (string) ($imageLinks['thumbnail'] ?? $imageLinks['smallThumbnail'] ?? '');
|
|
|
|
return [
|
|
'title' => (string) ($volumeInfo['title'] ?? ''),
|
|
'author' => implode(', ', $authors),
|
|
'asin' => 'googlebooks:'.$identifier,
|
|
'isbn' => $isbn13 !== '' ? $isbn13 : null,
|
|
'ean' => $isbn10 !== '' ? $isbn10 : null,
|
|
'url' => (string) ($volumeInfo['infoLink'] ?? ''),
|
|
'salesrank' => '',
|
|
'publisher' => (string) ($volumeInfo['publisher'] ?? ''),
|
|
'publishdate' => $this->normalizePublishDate($volumeInfo['publishedDate'] ?? null),
|
|
'pages' => isset($volumeInfo['pageCount']) && is_numeric($volumeInfo['pageCount']) ? (string) ((int) $volumeInfo['pageCount']) : '',
|
|
'overview' => strip_tags((string) ($volumeInfo['description'] ?? '')),
|
|
'genre' => implode(', ', $categories),
|
|
'coverurl' => $coverUrl,
|
|
'cover' => $coverUrl !== '' ? 1 : 0,
|
|
];
|
|
}
|
|
|
|
private function normalizePublishDate(mixed $date): ?string
|
|
{
|
|
if (! is_string($date) || trim($date) === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return Carbon::parse($date)->format('Y-m-d');
|
|
} catch (\Exception) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|