Newer
Older
book-metadata-service / src / main / java / ru / mcs / metadatabook / client / LibGenClient.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.LibGenProperties;

import java.util.Iterator;
import java.util.Map;

/**
 * LibGen (libgen.li) json.php API — открытый, без ключей, но ЮРИДИЧЕСКИ СЕРАЯ ЗОНА
 * (теневая библиотека). Выключен по умолчанию (libgen.enabled=false), включается явно.
 *
 * GET https://libgen.li/json.php?object=e&addkeys=isbn&isbn={isbn}&fields=title,author,publisher,year,pages,cover_url
 *
 * ВАЖНО: ответ — JSON-ОБЪЕКТ (не массив!), ключи — edition_id, значения — данные издания.
 * Может быть замечено по факту с ISBN "Голодных игр": сервер матчит скорее по "work"
 * (произведению), чем строго по ISBN конкретного издания — поэтому на один ISBN может
 * прийти десяток разнородных записей (разные годы/издательства/языки одной книги).
 * Из-за этого доверие (confidence) к результату LibGen ниже, чем к остальным источникам.
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class LibGenClient {

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

    /** Возвращает "лучшую" запись издания по эвристике полноты данных, либо null. */
    public JsonNode findByIsbn(String isbn) {
        if (!properties.isEnabled()) {
            log.debug("LibGen disabled via libgen.enabled=false");
            return null;
        }

        String normalizedIsbn = isbn.replaceAll("[^0-9Xx]", "");
        String url = properties.getBaseUrl()
                + "?object=e&addkeys=isbn&isbn=" + normalizedIsbn
                + "&fields=title,author,publisher,year,pages,cover_url";

        try {
            String raw = restClient.get().uri(url).retrieve().body(String.class);
            JsonNode root = objectMapper.readTree(raw);

            if (!root.isObject() || root.isEmpty()) {
                log.debug("LibGen: ISBN {} not found", isbn);
                return null;
            }

            return pickBestMatch(root);
        } catch (Exception e) {
            log.error("LibGen request failed for ISBN {}", isbn, e);
            return null;
        }
    }

    /**
     * Эвристика выбора лучшей записи среди множества изданий/сканов одной книги:
     * приоритет записям с непустым автором, издательством и наличием файлов
     * (значит, это не просто пустая карточка метаданных, а реально существующий скан).
     */
    private JsonNode pickBestMatch(JsonNode root) {
        JsonNode best = null;
        int bestScore = -1;

        Iterator<Map.Entry<String, JsonNode>> fields = root.fields();
        while (fields.hasNext()) {
            JsonNode entry = fields.next().getValue();
            String title = entry.path("title").asText("");
            if (title.isBlank()) continue;

            int score = 0;
            if (!entry.path("author").asText("").isBlank()) score++;
            if (!entry.path("publisher").asText("").isBlank()) score++;
            if (entry.has("files") && entry.get("files").size() > 0) score++;

            if (score > bestScore) {
                bestScore = score;
                best = entry;
            }
        }
        return best;
    }
}