diff --git a/build.gradle b/build.gradle index 8b9437b..677e8c6 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,8 @@ dependencies { implementation name: 'testlib-fat-0.2.1-all' + implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.12.0' + } tasks.named('test') { diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java b/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java index c67ac58..d7f145c 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java @@ -1,7 +1,6 @@ package ru.soune.nocopy.entity.tarif; import jakarta.persistence.*; -import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 1ad624b..385d64b 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -3,19 +3,23 @@ package ru.soune.nocopy.handler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import okhttp3.Response; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.ImageSearchRequest; +import ru.soune.nocopy.dto.file.YandexReverseSearchResponse; import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.service.file.CheckCounterService; +import ru.soune.nocopy.service.search.SearchApiToYandexResponseMapper; import ru.soune.nocopy.service.search.YandexSearchService; import ru.soune.nocopy.service.search.GoogleVisionSearchService; +import java.io.IOException; import java.util.Map; @Slf4j @@ -33,6 +37,8 @@ public class ImageFoundRequestHandler implements RequestHandler { private final FileEntityRepository fileEntityRepository; + private final SearchApiToYandexResponseMapper mapper = new SearchApiToYandexResponseMapper(); + @Override public BaseResponse handle(BaseRequest request) throws Exception { ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), @@ -47,14 +53,16 @@ public class ImageFoundRequestHandler implements RequestHandler { Map.of("fileId",fileId))); }); - YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileEntity); +// YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileEntity); //TODO uncommited when add billing // GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId); // checkCounterService.incrementCheckCount(fileEntity.getUserId()); + String yandexReverseSearchResponse = yandexSearchService.searchReverseByPublicUrl(fileEntity); + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - MessageCode.SUCCESS.getDescription(), response); + MessageCode.SUCCESS.getDescription(), mapper.mapToYandexResponse(yandexReverseSearchResponse)); } } diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchApiToYandexResponseMapper.java b/src/main/java/ru/soune/nocopy/service/search/SearchApiToYandexResponseMapper.java new file mode 100644 index 0000000..8bb8158 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/search/SearchApiToYandexResponseMapper.java @@ -0,0 +1,62 @@ +package ru.soune.nocopy.service.search; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import ru.soune.nocopy.dto.file.YandexSearchResponse; +import java.util.ArrayList; +import java.util.List; + +public class SearchApiToYandexResponseMapper { + private final ObjectMapper objectMapper = new ObjectMapper(); + + public YandexSearchResponse mapToYandexResponse(String searchApiJson) throws Exception { + JsonNode root = objectMapper.readTree(searchApiJson); + + YandexSearchResponse response = new YandexSearchResponse(); + List imageResults = new ArrayList<>(); + + JsonNode visualMatches = root.path("visual_matches"); + + if (visualMatches.isArray()) { + for (JsonNode match : visualMatches) { + YandexSearchResponse.ImageResult result = new YandexSearchResponse.ImageResult(); + + + JsonNode imageNode = match.path("image"); + if (!imageNode.isMissingNode()) { + result.setUrl(imageNode.path("link").asText()); + result.setWidth(imageNode.path("width").asInt()); + result.setHeight(imageNode.path("height").asInt()); + } + + result.setPageUrl(match.path("link").asText()); + + result.setPageTitle(match.path("title").asText()); + + String source = match.path("source").asText(); + result.setHost(extractHostFromSource(source)); + + if (result.getUrl() != null && !result.getUrl().isEmpty()) { + imageResults.add(result); + } + } + } + + response.setImages(imageResults); + return response; + } + + private String extractHostFromSource(String source) { + if (source == null || source.isEmpty()) { + return ""; + } + source = source.replaceFirst("^(https?://)?(www\\.)?", ""); + + int slashIndex = source.indexOf('/'); + if (slashIndex > 0) { + return source.substring(0, slashIndex); + } + + return source; + } +} diff --git a/src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java b/src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java index 76497b1..8004107 100644 --- a/src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.DeserializationFeature; import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import okhttp3.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.BaseResponse; @@ -13,7 +13,6 @@ import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.exception.NotValidFieldException; -import ru.soune.nocopy.repository.FileEntityRepository; import java.io.*; import java.net.HttpURLConnection; @@ -22,15 +21,28 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Base64; import java.util.Map; +import java.util.concurrent.TimeUnit; @Slf4j @Service -@RequiredArgsConstructor public class YandexSearchService { - private final FileEntityRepository fileEntityRepository; private final ObjectMapper objectMapper; + private final OkHttpClient httpClient; + + public YandexSearchService() { + this.httpClient = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(120, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .callTimeout(180, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build(); + + this.objectMapper = new ObjectMapper(); + } + @Value("${yandex.api-key}") private String apiKey; @@ -40,6 +52,12 @@ public class YandexSearchService { @Value("${yandex.search-url}") private String searchUrl; + @Value("${searchapi.api-key:}") + private String searchApiKey; + + @Value("${server.baseurl}") + private String appBaseUrl; + @PostConstruct public void init() { objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); @@ -68,6 +86,63 @@ public class YandexSearchService { return callYandexApi(fileBytes); } + + public String searchReverseByPublicUrl(FileEntity fileEntity) throws IOException { + if (!isImageFile(fileEntity)) { + log.error("File not image: {}", fileEntity.getMimeType()); + throw new NotValidFieldException("File not image", new BaseResponse(20007, + MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), + Map.of("file_type", fileEntity.getMimeType()))); + } + + String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); + + log.info("Searching reverse for image: {}", publicUrl); + + return callReverseImageApiByUrl(publicUrl); + } + + + private String callReverseImageApiByUrl(String imageUrl) throws IOException { + if (searchApiKey == null || searchApiKey.isEmpty()) { + throw new IllegalStateException("SearchAPI key not configured. Set searchapi.api-key property"); + } + + HttpUrl url = HttpUrl.parse("https://searchapi.io/api/v1/search") + .newBuilder() + .addQueryParameter("engine", "yandex_reverse_image") + .addQueryParameter("api_key", searchApiKey) + .addQueryParameter("url", imageUrl) + .addQueryParameter("wait", "true") + .addQueryParameter("timeout", "30000") + .build(); + + log.info("Calling Yandex Reverse Image API: {}", url); + + Request request = new Request.Builder() + .url(url) + .header("Accept", "application/json") + .header("User-Agent", "Mozilla/5.0") + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + log.info("Response code: {}", response.code()); + log.info("Response headers: {}", response.headers()); + + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : "No error body"; + log.error("API request failed: {} - {}", response.code(), errorBody); + throw new IOException("API request failed: " + response.code() + " - " + response.message()); + } + + String responseBody = response.body().string(); + log.info("Response body length: {} characters", responseBody.length()); + + return responseBody; + } + } + + private boolean isImageFile(FileEntity fileEntity) { String mimeType = fileEntity.getMimeType(); return mimeType != null && mimeType.startsWith("image"); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 536a718..fea82f1 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -88,6 +88,10 @@ yandex: folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd} search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} +searchapi: + api-key: ${SEARCHAPI_API_KEY:2xBChafVmojL7G1ZXzzUuA6o} + reverse-image-url: "https://searchapi.io/api/v1/search" + app: email: verification: