From 377ea90bfe703edefadb88b03ac96650b8e2ba41 Mon Sep 17 00:00:00 2001 From: bivashy Date: Sat, 28 Feb 2026 00:42:48 +0500 Subject: [PATCH] Implement `/search` endpoint with basic info --- .../title/service/GreetingResource.java | 32 ---- .../service/mapper/KodikResponseMapper.java | 149 ++++++++++++++++++ .../service/model/SearchResponseDTO.java | 10 +- .../service/resource/SearchResource.java | 30 ++++ startup.sh | 3 + 5 files changed, 191 insertions(+), 33 deletions(-) delete mode 100644 src/main/java/com/backend/unifier/title/service/GreetingResource.java create mode 100644 src/main/java/com/backend/unifier/title/service/mapper/KodikResponseMapper.java create mode 100644 src/main/java/com/backend/unifier/title/service/resource/SearchResource.java create mode 100755 startup.sh diff --git a/src/main/java/com/backend/unifier/title/service/GreetingResource.java b/src/main/java/com/backend/unifier/title/service/GreetingResource.java deleted file mode 100644 index 1196aba..0000000 --- a/src/main/java/com/backend/unifier/title/service/GreetingResource.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.backend.unifier.title.service; - -import org.eclipse.microprofile.rest.client.inject.RestClient; -import org.jboss.logging.Logger; - -import com.backend.unifier.title.service.api.KodikSearchService; -import com.backend.unifier.title.service.api.ShikimoriSearchService; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; - -@Path("/hello") -public class GreetingResource { - Logger logger = Logger.getLogger(GreetingResource.class); - private final KodikSearchService kodikSearchService; - private final ShikimoriSearchService shikimoriSearchService; - - public GreetingResource(@RestClient KodikSearchService kodikSearchService, - @RestClient ShikimoriSearchService shikimoriSearchService) { - this.kodikSearchService = kodikSearchService; - this.shikimoriSearchService = shikimoriSearchService; - } - - @GET - @Produces(MediaType.TEXT_PLAIN) - public void hello() { - logger.info(kodikSearchService.search("Стальной алхимик")); - logger.info(shikimoriSearchService.findById("3701")); - } -} diff --git a/src/main/java/com/backend/unifier/title/service/mapper/KodikResponseMapper.java b/src/main/java/com/backend/unifier/title/service/mapper/KodikResponseMapper.java new file mode 100644 index 0000000..3a5b4ee --- /dev/null +++ b/src/main/java/com/backend/unifier/title/service/mapper/KodikResponseMapper.java @@ -0,0 +1,149 @@ +package com.backend.unifier.title.service.mapper; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.backend.metadata.kodik.service.api.model.KodikResponse; +import com.backend.metadata.kodik.service.api.model.MaterialData; +import com.backend.unifier.title.service.model.SearchResponseDTO; + +@Mapper +public interface KodikResponseMapper { + + default SearchResponseDTO toSearchResponseDTO(KodikResponse kodikResponse) { + if (kodikResponse == null || kodikResponse.results == null) { + return new SearchResponseDTO(List.of()); + } + + List entries = kodikResponse.results.stream() + .map(this::toSearchEntryDTO) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + return new SearchResponseDTO(entries); + } + + @Mapping(target = "posterURL", source = "materialData", qualifiedByName = "extractPosterUrl") + @Mapping(target = "title", source = "result", qualifiedByName = "extractTitle") + @Mapping(target = "episodeCount", source = "episodesCount") + @Mapping(target = "rating", source = "materialData", qualifiedByName = "extractRating") + @Mapping(target = "ratingSource", source = "materialData", qualifiedByName = "extractRatingSource") + @Mapping(target = "type", source = "type") + @Mapping(target = "studio", source = "materialData", qualifiedByName = "extractStudio") + @Mapping(target = "genres", source = "materialData", qualifiedByName = "extractGenres") + @Mapping(target = "durationMin", source = "materialData", qualifiedByName = "extractDurationMin") + SearchResponseDTO.SearchEntryDTO toSearchEntryDTO(KodikResponse.Result result); + + @Named("extractPosterUrl") + default String extractPosterUrl(MaterialData materialData) { + if (materialData == null) + return ""; + + if (materialData.animePosterUrl != null && !materialData.animePosterUrl.isEmpty()) { + return materialData.animePosterUrl; + } + return materialData.posterUrl != null ? materialData.posterUrl : ""; + } + + @Named("extractTitle") + default String extractTitle(KodikResponse.Result result) { + if (result == null) + return ""; + + MaterialData materialData = result.materialData; + if (materialData != null) { + if (materialData.animeTitle != null && !materialData.animeTitle.isEmpty()) { + return materialData.animeTitle; + } + if (materialData.title != null && !materialData.title.isEmpty()) { + return materialData.title; + } + if (materialData.titleEn != null && !materialData.titleEn.isEmpty()) { + return materialData.titleEn; + } + } + + if (result.title != null && !result.title.isEmpty()) { + return result.title; + } + + return result.titleOrig != null ? result.titleOrig : ""; + } + + @Named("extractRating") + default double extractRating(MaterialData materialData) { + if (materialData == null) + return 0.0; + + if (materialData.kinopoiskRating > 0) { + return materialData.kinopoiskRating; + } + if (materialData.imdbRating > 0) { + return materialData.imdbRating; + } + if (materialData.shikimoriRating > 0) { + return materialData.shikimoriRating; + } + + return 0.0; + } + + @Named("extractRatingSource") + default String extractRatingSource(MaterialData materialData) { + if (materialData == null) + return ""; + + if (materialData.kinopoiskRating > 0) { + return "Kinopoisk"; + } + if (materialData.imdbRating > 0) { + return "IMDb"; + } + if (materialData.shikimoriRating > 0) { + return "Shikimori"; + } + + return ""; + } + + @Named("extractStudio") + default String extractStudio(MaterialData materialData) { + if (materialData == null || materialData.animeStudios == null || materialData.animeStudios.isEmpty()) { + return ""; + } + + return String.join(", ", materialData.animeStudios); + } + + @Named("extractGenres") + default List extractGenres(MaterialData materialData) { + if (materialData == null) + return List.of(); + + if (materialData.animeGenres != null && !materialData.animeGenres.isEmpty()) { + return materialData.animeGenres; + } + if (materialData.genres != null && !materialData.genres.isEmpty()) { + return materialData.genres; + } + if (materialData.allGenres != null && !materialData.allGenres.isEmpty()) { + return materialData.allGenres; + } + + return List.of(); + } + + @Named("extractDurationMin") + default int extractDurationMin(MaterialData materialData) { + if (materialData == null || materialData.duration <= 0) { + return 0; + } + + return materialData.duration; + } +} diff --git a/src/main/java/com/backend/unifier/title/service/model/SearchResponseDTO.java b/src/main/java/com/backend/unifier/title/service/model/SearchResponseDTO.java index a56a0f6..23b6ead 100644 --- a/src/main/java/com/backend/unifier/title/service/model/SearchResponseDTO.java +++ b/src/main/java/com/backend/unifier/title/service/model/SearchResponseDTO.java @@ -3,7 +3,15 @@ package com.backend.unifier.title.service.model; import java.util.List; public record SearchResponseDTO(List result) { - public record SearchEntryDTO(String title, int episodeCount) { + public record SearchEntryDTO( + String posterURL, + String title, + int episodeCount, + double rating, String ratingSource, + String type, + String studio, + List genres, + int durationMin) { } } diff --git a/src/main/java/com/backend/unifier/title/service/resource/SearchResource.java b/src/main/java/com/backend/unifier/title/service/resource/SearchResource.java new file mode 100644 index 0000000..c2737e2 --- /dev/null +++ b/src/main/java/com/backend/unifier/title/service/resource/SearchResource.java @@ -0,0 +1,30 @@ +package com.backend.unifier.title.service.resource; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.mapstruct.factory.Mappers; + +import com.backend.unifier.title.service.api.KodikSearchService; +import com.backend.unifier.title.service.mapper.KodikResponseMapper; +import com.backend.unifier.title.service.model.SearchResponseDTO; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; + +@Path("/search") +public class SearchResource { + KodikResponseMapper responseMapper = Mappers.getMapper(KodikResponseMapper.class); + + private final KodikSearchService kodikSearchService; + + public SearchResource(@RestClient KodikSearchService kodikSearchService) { + this.kodikSearchService = kodikSearchService; + } + + @GET + public SearchResponseDTO search(@QueryParam("title") String title) { + var kodikResponse = kodikSearchService.search(title); + return responseMapper.toSearchResponseDTO(kodikResponse); + } + +} diff --git a/startup.sh b/startup.sh new file mode 100755 index 0000000..9b27996 --- /dev/null +++ b/startup.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./mvnw jar:jar install:install && ./mvnw clean package && docker compose -f src/main/docker-compose/jvm.yml up -d --build