package ru.soune.nocopy.service.search; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.DeserializationFeature; import jakarta.annotation.PostConstruct; 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; 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 java.io.*; import java.net.HttpURLConnection; import java.net.URL; 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 public class YandexSearchService { 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; @Value("${yandex.folder-id}") private String folderId; @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); objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); } public YandexSearchResponse searchByFileEntity(FileEntity fileEntity) throws IOException { byte[] fileBytes; 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()))); } try { fileBytes = readFileFromDisk(fileEntity); } catch (IOException e) { throw new NotValidFieldException("File not found or cannot read file", new BaseResponse(20007, MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), Map.of("fileId", fileEntity.getId(), "filePath", fileEntity.getFilePath()))); } 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"); // } // // if (imageUrl == null || imageUrl.trim().isEmpty()) { // throw new IllegalArgumentException("Image URL cannot be null or empty"); // } // // 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 for URL: {}", imageUrl); // log.debug("Full API URL: {}", url); // // Request request = new Request.Builder() // .url(url) // .header("Accept", "application/json") // .header("User-Agent", "Mozilla/5.0 (compatible; YourApp/1.0)") // .header("Accept-Encoding", "gzip") // .build(); // // try (Response response = httpClient.newCall(request).execute()) { // ResponseBody responseBody = response.body(); // // log.info("Response code: {}", response.code()); // log.debug("Response headers: {}", response.headers()); // // if (!response.isSuccessful()) { // String errorBody = responseBody != null ? responseBody.string() : "No error body"; // log.error("API request failed: {} - {}", response.code(), errorBody); // throw new IOException("API request failed: " + response.code() + " - " + response.message()); // } // // if (responseBody == null) { // log.error("Response body is null for successful request"); // throw new IOException("Response body is null"); // } // // String responseContent = responseBody.string(); // log.info("Response body length: {} characters", responseContent.length()); // // if (log.isDebugEnabled() && responseContent.length() > 0) { // log.debug("Response body (first 500 chars): {}", // responseContent.substring(0, Math.min(responseContent.length(), 500))); // } // // return responseContent; // } catch (IOException e) { // log.error("Failed to call Yandex Reverse Image API for URL: {}", imageUrl, e); // throw e; // } // } 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"); } private byte[] readFileFromDisk(FileEntity fileEntity) throws IOException { Path filePath = Path.of(fileEntity.getFilePath()); if (!Files.exists(filePath)) { throw new IOException("File not found: " + fileEntity.getFilePath()); } if (!Files.isReadable(filePath)) { throw new IOException("Cannot read file: " + fileEntity.getFilePath()); } return Files.readAllBytes(filePath); } private YandexSearchResponse callYandexApi(byte[] imageBytes) throws IOException { String imageBase64 = Base64.getEncoder().encodeToString(imageBytes); String jsonRequest = String.format("{\"folderId\":\"%s\",\"data\":\"%s\",\"page\":0,\"showSimilarImages\":true}", folderId, imageBase64); URL url = new URL(searchUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); try { connection.setRequestMethod("POST"); connection.setRequestProperty("Authorization", "Api-Key " + apiKey); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Accept", "application/json"); connection.setConnectTimeout(30000); connection.setReadTimeout(30000); connection.setDoOutput(true); try (OutputStream os = connection.getOutputStream()) { byte[] input = jsonRequest.getBytes("utf-8"); os.write(input, 0, input.length); os.flush(); } int responseCode = connection.getResponseCode(); String responseBody; if (responseCode == HttpURLConnection.HTTP_OK) { try (BufferedReader br = new BufferedReader( new InputStreamReader(connection.getInputStream(), "utf-8"))) { responseBody = readAll(br); } } else { try (BufferedReader br = new BufferedReader( new InputStreamReader(connection.getErrorStream(), "utf-8"))) { responseBody = readAll(br); } throw new IOException("Error Yandex API: " + responseCode + " - " + responseBody); } return parseJsonResponse(responseBody); } finally { connection.disconnect(); } } private String readAll(BufferedReader reader) throws IOException { StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } return sb.toString(); } private YandexSearchResponse parseJsonResponse(String json) { YandexSearchResponse response = null; try { response = objectMapper.readValue(json, YandexSearchResponse.class); } catch (JsonProcessingException e) { throw new RuntimeException(e); } return response; } }