baseUrl = config('gutendex.api_url', 'https://gutendex.com'); $this->timeout = config('gutendex.timeout', 30); $this->retryMaxAttempts = config('gutendex.retry.max_attempts', 3); $this->retryBackoff = config('gutendex.retry.backoff_multiplier', 2); $this->cacheEnabled = config('gutendex.cache.enabled', true); $this->cacheTtl = config('gutendex.cache.ttl', 86400); $this->rateLimit = config('gutendex.rate_limit_delay', 1000); } /** * Search for books with optional query and filters. * * @param string|null $query Search query * @param int $page Page number * @param array $filters Additional filters (language, topic, author, etc.) * @return array API response with results */ public function searchBooks(?string $query = null, int $page = 1, array $filters = []): array { $params = array_merge(['page' => $page], $filters); if ($query) { $params['search'] = $query; } $cacheKey = 'gutendex:search:' . md5(json_encode($params)); if ($this->cacheEnabled && Cache::has($cacheKey)) { Log::info('Gutendex: Cache hit for search', ['params' => $params]); return Cache::get($cacheKey); } $response = $this->makeRequest('GET', '/books', $params); if ($this->cacheEnabled && isset($response['results'])) { Cache::put($cacheKey, $response, $this->cacheTtl); } return $response; } /** * Get a specific book by its Gutenberg ID. * * @param int $gutenbergId The Gutenberg book ID * @return GutendexBook|null */ public function getBook(int $gutenbergId): ?GutendexBook { $cacheKey = 'gutendex:book:' . $gutenbergId; if ($this->cacheEnabled && Cache::has($cacheKey)) { Log::info('Gutendex: Cache hit for book', ['id' => $gutenbergId]); return Cache::get($cacheKey); } try { $response = $this->makeRequest('GET', "/books/{$gutenbergId}"); if (empty($response)) { return null; } $book = GutendexBook::fromApiResponse($response); if ($this->cacheEnabled) { Cache::put($cacheKey, $book, $this->cacheTtl); } return $book; } catch (\Exception $e) { Log::error('Gutendex: Failed to fetch book', [ 'id' => $gutenbergId, 'error' => $e->getMessage(), ]); return null; } } /** * Get books by language. * * @param string $language Language code (e.g., 'en', 'fr') * @param int $page Page number * @return array */ public function getBooksByLanguage(string $language = 'en', int $page = 1): array { return $this->searchBooks(null, $page, ['languages' => $language]); } /** * Get books filtered by copyright status. * * @param string $copyright 'true', 'false', or 'null' * @param int $page Page number * @return array */ public function getBooksByCopyright(string $copyright = 'false', int $page = 1): array { return $this->searchBooks(null, $page, ['copyright' => $copyright]); } /** * Fetch full text content from a given URL. * * @param string $textUrl URL to the full text * @return string|null The text content or null on failure */ public function fetchFullText(string $textUrl): ?string { try { Log::info('Gutendex: Fetching full text', ['url' => $textUrl]); $response = Http::timeout($this->timeout * 2) ->retry($this->retryMaxAttempts, $this->retryBackoff * 1000) ->get($textUrl); if ($response->successful()) { $text = $response->body(); Log::info('Gutendex: Successfully fetched full text', [ 'url' => $textUrl, 'size' => strlen($text), ]); return $text; } Log::warning('Gutendex: Failed to fetch full text', [ 'url' => $textUrl, 'status' => $response->status(), ]); return null; } catch (ConnectionException $e) { Log::error('Gutendex: Connection error fetching full text', [ 'url' => $textUrl, 'error' => $e->getMessage(), ]); return null; } catch (\Exception $e) { Log::error('Gutendex: Error fetching full text', [ 'url' => $textUrl, 'error' => $e->getMessage(), ]); return null; } } /** * Get books with pagination info. * * @param array $filters Filters to apply * @param int $page Page number * @return array Contains 'books' (GutendexBook[]), 'count', 'next', 'previous' */ public function getBooksWithPagination(array $filters = [], int $page = 1): array { $response = $this->searchBooks(null, $page, $filters); $books = array_map( fn($bookData) => GutendexBook::fromApiResponse($bookData), $response['results'] ?? [] ); return [ 'books' => $books, 'count' => $response['count'] ?? 0, 'next' => $response['next'] ?? null, 'previous' => $response['previous'] ?? null, ]; } /** * Check if the Gutendex API is accessible. * * @return bool */ public function checkConnection(): bool { try { $response = Http::timeout(10)->get($this->baseUrl . '/books?page=1'); return $response->successful(); } catch (\Exception $e) { Log::error('Gutendex: Connection check failed', ['error' => $e->getMessage()]); return false; } } /** * Make an HTTP request to the Gutendex API with retry logic. * * @param string $method HTTP method * @param string $endpoint API endpoint * @param array $params Query parameters * @return array Response data * @throws RequestException */ protected function makeRequest(string $method, string $endpoint, array $params = []): array { $url = $this->baseUrl . $endpoint; $startTime = microtime(true); try { $response = Http::timeout($this->timeout) ->retry( $this->retryMaxAttempts, function ($exception, $request) { // Exponential backoff return $this->retryBackoff * 1000; }, function ($exception, $request) { // Retry on connection errors and 429 rate limit if ($exception instanceof ConnectionException) { return true; } if (method_exists($exception, 'response')) { $response = $exception->response; return $response && $response->status() === 429; } return false; } ) ->acceptJson() ->$method($url, $params); $duration = microtime(true) - $startTime; if ($response->successful()) { Log::info('Gutendex: API request successful', [ 'method' => $method, 'endpoint' => $endpoint, 'duration' => round($duration, 2) . 's', ]); // Add rate limit delay if ($this->rateLimit > 0) { usleep($this->rateLimit * 1000); } return $response->json(); } Log::warning('Gutendex: API request failed', [ 'method' => $method, 'endpoint' => $endpoint, 'status' => $response->status(), 'duration' => round($duration, 2) . 's', ]); return []; } catch (ConnectionException $e) { Log::error('Gutendex: Connection error', [ 'method' => $method, 'endpoint' => $endpoint, 'error' => $e->getMessage(), ]); throw $e; } catch (RequestException $e) { $duration = microtime(true) - $startTime; Log::error('Gutendex: Request exception', [ 'method' => $method, 'endpoint' => $endpoint, 'error' => $e->getMessage(), 'duration' => round($duration, 2) . 's', ]); throw $e; } } /** * Clear all cached Gutendex data. * * @return void */ public function clearCache(): void { Cache::flush(); Log::info('Gutendex: Cache cleared'); } }