Newer
Older
book-metadata-service / src / main / java / ru / mcs / metadatabook / client / GoogleBooksClient.java
package ru.mcs.metadatabook.client;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import ru.mcs.metadatabook.config.GoogleBooksProperties;

/**
 * Google Books API. С 2024-2026 анонимная (без ключа) квота на общий пул
 * запросов со всего мира периодически обнуляется Google (quota_limit_value=0,
 * 429 RESOURCE_EXHAUSTED) — на практике API без ключа работать перестал.
 * Поэтому ключ теперь обязателен для стабильной работы, хотя формально бесплатен.
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class GoogleBooksClient {

    private static final String BASE_URL = "https://www.googleapis.com/books/v1/volumes";

    private final GoogleBooksProperties properties;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final RestClient restClient = RestClient.create();

    public JsonNode findByIsbn(String isbn) {
        String normalizedIsbn = isbn.replaceAll("[^0-9Xx]", "");
        String url = withKey(BASE_URL + "?q=isbn:" + normalizedIsbn);

        try {
            String raw = restClient.get().uri(url).retrieve().body(String.class);
            JsonNode root = objectMapper.readTree(raw);
            JsonNode items = root.get("items");
            if (items == null || !items.isArray() || items.isEmpty()) {
                log.debug("Google Books: ISBN {} not found", isbn);
                return null;
            }
            return items.get(0).get("volumeInfo");
        } catch (org.springframework.web.client.HttpClientErrorException.TooManyRequests e) {
            log.error("Google Books quota exceeded. Configure google.books.api-key " +
                    "(console.cloud.google.com -> Books API -> Credentials).");
            return null;
        } catch (Exception e) {
            log.error("Google Books request failed for ISBN {}", isbn, e);
            return null;
        }
    }

    public JsonNode searchByTitle(String title, int limit) {
        String url = withKey(BASE_URL + "?q=intitle:" + title.replace(" ", "+") + "&maxResults=" + limit);
        try {
            String raw = restClient.get().uri(url).retrieve().body(String.class);
            JsonNode root = objectMapper.readTree(raw);
            JsonNode items = root.get("items");
            if (items == null || !items.isArray() || items.isEmpty()) {
                return null;
            }
            return items.get(0).get("volumeInfo");
        } catch (Exception e) {
            log.error("Google Books title search failed for '{}'", title, e);
            return null;
        }
    }

    private String withKey(String url) {
        if (properties.getApiKey() != null && !properties.getApiKey().isBlank()) {
            return url + "&key=" + properties.getApiKey();
        }
        log.warn("google.books.api-key not configured — anonymous quota is likely exhausted (429)");
        return url;
    }
}