Integrate PosterUrlValidator, record-builder
This commit is contained in:
23
pom.xml
23
pom.xml
@@ -1,7 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.backend.unifier.title.service</groupId>
|
<groupId>com.backend.unifier.title.service</groupId>
|
||||||
<artifactId>anyame-unifier-title-backend</artifactId>
|
<artifactId>anyame-unifier-title-backend</artifactId>
|
||||||
@@ -20,6 +18,7 @@
|
|||||||
<shikimori-service.version>0.0.1-SNAPSHOT</shikimori-service.version>
|
<shikimori-service.version>0.0.1-SNAPSHOT</shikimori-service.version>
|
||||||
<quarkus-mapstruct.version>1.1.0</quarkus-mapstruct.version>
|
<quarkus-mapstruct.version>1.1.0</quarkus-mapstruct.version>
|
||||||
<mapstruct.version>1.6.3</mapstruct.version>
|
<mapstruct.version>1.6.3</mapstruct.version>
|
||||||
|
<record-builder.version>52</record-builder.version>
|
||||||
<skipITs>true</skipITs>
|
<skipITs>true</skipITs>
|
||||||
<surefire-plugin.version>3.5.4</surefire-plugin.version>
|
<surefire-plugin.version>3.5.4</surefire-plugin.version>
|
||||||
</properties>
|
</properties>
|
||||||
@@ -77,6 +76,16 @@
|
|||||||
<artifactId>mapstruct</artifactId>
|
<artifactId>mapstruct</artifactId>
|
||||||
<version>${mapstruct.version}</version>
|
<version>${mapstruct.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.soabase.record-builder</groupId>
|
||||||
|
<artifactId>record-builder-core</artifactId>
|
||||||
|
<version>${record-builder.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-junit</artifactId>
|
<artifactId>quarkus-junit</artifactId>
|
||||||
@@ -108,6 +117,11 @@
|
|||||||
<artifactId>mapstruct-processor</artifactId>
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
<version>${mapstruct.version}</version>
|
<version>${mapstruct.version}</version>
|
||||||
</path>
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>io.soabase.record-builder</groupId>
|
||||||
|
<artifactId>record-builder-processor</artifactId>
|
||||||
|
<version>${record-builder.version}</version>
|
||||||
|
</path>
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
@@ -136,8 +150,7 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<argLine>@{argLine}</argLine>
|
<argLine>@{argLine}</argLine>
|
||||||
<systemPropertyVariables>
|
<systemPropertyVariables>
|
||||||
<native.image.path>
|
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
|
||||||
${project.build.directory}/${project.build.finalName}-runner</native.image.path>
|
|
||||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||||
<maven.home>${maven.home}</maven.home>
|
<maven.home>${maven.home}</maven.home>
|
||||||
</systemPropertyVariables>
|
</systemPropertyVariables>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
|||||||
|
|
||||||
import com.backend.metadata.kodik.service.api.model.KodikResponse;
|
import com.backend.metadata.kodik.service.api.model.KodikResponse;
|
||||||
|
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.PathParam;
|
import jakarta.ws.rs.PathParam;
|
||||||
@@ -31,4 +32,8 @@ public interface KodikSearchService {
|
|||||||
@GET
|
@GET
|
||||||
@Path("/imdb/{id}")
|
@Path("/imdb/{id}")
|
||||||
KodikResponse findByImdbId(@PathParam("id") String id);
|
KodikResponse findByImdbId(@PathParam("id") String id);
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/search")
|
||||||
|
Uni<KodikResponse> searchAsync(@QueryParam("title") String title);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.backend.unifier.title.service.mapper;
|
package com.backend.unifier.title.service.mapper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -10,17 +11,17 @@ import org.mapstruct.Named;
|
|||||||
|
|
||||||
import com.backend.metadata.kodik.service.api.model.KodikResponse;
|
import com.backend.metadata.kodik.service.api.model.KodikResponse;
|
||||||
import com.backend.metadata.kodik.service.api.model.MaterialData;
|
import com.backend.metadata.kodik.service.api.model.MaterialData;
|
||||||
|
import com.backend.unifier.title.service.model.SearchEntryDTO;
|
||||||
import com.backend.unifier.title.service.model.SearchResponseDTO;
|
import com.backend.unifier.title.service.model.SearchResponseDTO;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface KodikResponseMapper {
|
public interface KodikResponseMapper {
|
||||||
|
|
||||||
default SearchResponseDTO toSearchResponseDTO(KodikResponse kodikResponse) {
|
default SearchResponseDTO toSearchResponseDTO(KodikResponse kodikResponse) {
|
||||||
if (kodikResponse == null || kodikResponse.results == null) {
|
if (kodikResponse == null || kodikResponse.results == null) {
|
||||||
return new SearchResponseDTO(List.of());
|
return new SearchResponseDTO(List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SearchResponseDTO.SearchEntryDTO> entries = kodikResponse.results.stream()
|
List<SearchEntryDTO> entries = kodikResponse.results.stream()
|
||||||
.map(this::toSearchEntryDTO)
|
.map(this::toSearchEntryDTO)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@@ -28,7 +29,8 @@ public interface KodikResponseMapper {
|
|||||||
return new SearchResponseDTO(entries);
|
return new SearchResponseDTO(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mapping(target = "posterURL", source = "materialData", qualifiedByName = "extractPosterUrl")
|
@Mapping(target = "id", source = "result", qualifiedByName = "extractID")
|
||||||
|
@Mapping(target = "posterURLs", source = "materialData", qualifiedByName = "extractPosterUrls")
|
||||||
@Mapping(target = "title", source = "result", qualifiedByName = "extractTitle")
|
@Mapping(target = "title", source = "result", qualifiedByName = "extractTitle")
|
||||||
@Mapping(target = "episodeCount", source = "episodesCount")
|
@Mapping(target = "episodeCount", source = "episodesCount")
|
||||||
@Mapping(target = "rating", source = "materialData", qualifiedByName = "extractRating")
|
@Mapping(target = "rating", source = "materialData", qualifiedByName = "extractRating")
|
||||||
@@ -37,17 +39,29 @@ public interface KodikResponseMapper {
|
|||||||
@Mapping(target = "studio", source = "materialData", qualifiedByName = "extractStudio")
|
@Mapping(target = "studio", source = "materialData", qualifiedByName = "extractStudio")
|
||||||
@Mapping(target = "genres", source = "materialData", qualifiedByName = "extractGenres")
|
@Mapping(target = "genres", source = "materialData", qualifiedByName = "extractGenres")
|
||||||
@Mapping(target = "durationMin", source = "materialData", qualifiedByName = "extractDurationMin")
|
@Mapping(target = "durationMin", source = "materialData", qualifiedByName = "extractDurationMin")
|
||||||
SearchResponseDTO.SearchEntryDTO toSearchEntryDTO(KodikResponse.Result result);
|
SearchEntryDTO toSearchEntryDTO(KodikResponse.Result result);
|
||||||
|
|
||||||
@Named("extractPosterUrl")
|
@Named("extractPosterUrls")
|
||||||
default String extractPosterUrl(MaterialData materialData) {
|
default List<String> extractPosterUrls(MaterialData materialData) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
if (materialData == null)
|
if (materialData == null)
|
||||||
return "";
|
return result;
|
||||||
|
|
||||||
if (materialData.animePosterUrl != null && !materialData.animePosterUrl.isEmpty()) {
|
if (materialData.animePosterUrl != null && !materialData.animePosterUrl.isEmpty()) {
|
||||||
return materialData.animePosterUrl;
|
result.add(materialData.animePosterUrl);
|
||||||
}
|
}
|
||||||
return materialData.posterUrl != null ? materialData.posterUrl : "";
|
|
||||||
|
if (materialData.posterUrl != null && !materialData.posterUrl.isEmpty()) {
|
||||||
|
result.add(materialData.posterUrl);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Named("extractID")
|
||||||
|
default String extractID(KodikResponse.Result result) {
|
||||||
|
if (result == null)
|
||||||
|
return "";
|
||||||
|
return result.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Named("extractTitle")
|
@Named("extractTitle")
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.backend.unifier.title.service.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||||
|
|
||||||
|
@RecordBuilder
|
||||||
|
public record SearchEntryDTO(
|
||||||
|
String id,
|
||||||
|
List<String> posterURLs,
|
||||||
|
String title,
|
||||||
|
int episodeCount,
|
||||||
|
double rating,
|
||||||
|
String ratingSource,
|
||||||
|
String type,
|
||||||
|
String studio,
|
||||||
|
List<String> genres,
|
||||||
|
int durationMin) implements SearchEntryDTOBuilder.With {
|
||||||
|
}
|
||||||
@@ -2,16 +2,9 @@ package com.backend.unifier.title.service.model;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record SearchResponseDTO(List<SearchEntryDTO> result) {
|
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||||
public record SearchEntryDTO(
|
|
||||||
String posterURL,
|
@RecordBuilder
|
||||||
String title,
|
public record SearchResponseDTO(List<SearchEntryDTO> result) implements SearchResponseDTOBuilder.With {
|
||||||
int episodeCount,
|
|
||||||
double rating, String ratingSource,
|
|
||||||
String type,
|
|
||||||
String studio,
|
|
||||||
List<String> genres,
|
|
||||||
int durationMin) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
package com.backend.unifier.title.service.resource;
|
package com.backend.unifier.title.service.resource;
|
||||||
|
|
||||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
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.api.KodikSearchService;
|
||||||
import com.backend.unifier.title.service.mapper.KodikResponseMapper;
|
|
||||||
import com.backend.unifier.title.service.model.SearchResponseDTO;
|
import com.backend.unifier.title.service.model.SearchResponseDTO;
|
||||||
|
import com.backend.unifier.title.service.service.KodikResponseConvertService;
|
||||||
|
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.QueryParam;
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
|
||||||
@Path("/search")
|
@Path("/search")
|
||||||
public class SearchResource {
|
public class SearchResource {
|
||||||
KodikResponseMapper responseMapper = Mappers.getMapper(KodikResponseMapper.class);
|
|
||||||
|
|
||||||
private final KodikSearchService kodikSearchService;
|
private final KodikSearchService kodikSearchService;
|
||||||
|
private final KodikResponseConvertService kodikConvertService;
|
||||||
|
|
||||||
public SearchResource(@RestClient KodikSearchService kodikSearchService) {
|
public SearchResource(@RestClient KodikSearchService kodikSearchService,
|
||||||
|
KodikResponseConvertService kodikConvertService) {
|
||||||
this.kodikSearchService = kodikSearchService;
|
this.kodikSearchService = kodikSearchService;
|
||||||
|
this.kodikConvertService = kodikConvertService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
public SearchResponseDTO search(@QueryParam("title") String title) {
|
public Uni<SearchResponseDTO> search(@QueryParam("title") String title) {
|
||||||
var kodikResponse = kodikSearchService.search(title);
|
return kodikSearchService.searchAsync(title)
|
||||||
return responseMapper.toSearchResponseDTO(kodikResponse);
|
.onItem().ifNotNull()
|
||||||
|
.transformToUni(response -> kodikConvertService.convertAsync(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.backend.unifier.title.service.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.mapstruct.factory.Mappers;
|
||||||
|
|
||||||
|
import com.backend.metadata.kodik.service.api.model.KodikResponse;
|
||||||
|
import com.backend.unifier.title.service.mapper.KodikResponseMapper;
|
||||||
|
import com.backend.unifier.title.service.model.SearchEntryDTO;
|
||||||
|
import com.backend.unifier.title.service.model.SearchResponseDTO;
|
||||||
|
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class KodikResponseConvertService {
|
||||||
|
|
||||||
|
private final KodikResponseMapper responseMapper = Mappers.getMapper(KodikResponseMapper.class);
|
||||||
|
private final PosterUrlValidator posterUrlValidator;
|
||||||
|
|
||||||
|
public KodikResponseConvertService(PosterUrlValidator posterUrlValidator) {
|
||||||
|
this.posterUrlValidator = posterUrlValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchResponseDTO convert(KodikResponse response) {
|
||||||
|
return responseMapper.toSearchResponseDTO(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Uni<SearchResponseDTO> convertAsync(KodikResponse response) {
|
||||||
|
if (response == null || response.results == null) {
|
||||||
|
return Uni.createFrom().item(new SearchResponseDTO(List.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Uni<SearchEntryDTO>> entries = responseMapper.toSearchResponseDTO(response).result()
|
||||||
|
.stream()
|
||||||
|
.map(this::resolveEntryPosters)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Uni.combine().all().unis(entries)
|
||||||
|
.with(list -> new SearchResponseDTO((List<SearchEntryDTO>) list));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uni<SearchEntryDTO> resolveEntryPosters(SearchEntryDTO entry) {
|
||||||
|
return posterUrlValidator.resolvePosters(entry.posterURLs())
|
||||||
|
.map(entry::withPosterURLs)
|
||||||
|
.onFailure().recoverWithItem(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.backend.unifier.title.service.service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import io.vertx.core.Vertx;
|
||||||
|
import io.vertx.ext.web.client.WebClient;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class PosterUrlValidator {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "poster.url.timeoutMs", defaultValue = "3000")
|
||||||
|
long timeoutMs;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Vertx vertx;
|
||||||
|
|
||||||
|
private WebClient webClient;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
void init() {
|
||||||
|
webClient = WebClient.create(vertx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uni<List<String>> resolvePosters(List<String> urls) {
|
||||||
|
if (urls == null || urls.isEmpty()) {
|
||||||
|
return Uni.createFrom().item(List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> normalized = urls.stream().map(this::normalizeDomain).toList();
|
||||||
|
return findFirstReachable(normalized, 0)
|
||||||
|
.map(firstIndex -> reorderToFront(normalized, firstIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uni<Integer> findFirstReachable(List<String> urls, int index) {
|
||||||
|
if (index >= urls.size()) {
|
||||||
|
return Uni.createFrom().item(-1);
|
||||||
|
}
|
||||||
|
return isReachable(urls.get(index))
|
||||||
|
.flatMap(ok -> ok ? Uni.createFrom().item(index) : findFirstReachable(urls, index + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> reorderToFront(List<String> urls, int firstIndex) {
|
||||||
|
if (firstIndex <= 0)
|
||||||
|
return urls;
|
||||||
|
List<String> reordered = new ArrayList<>(urls);
|
||||||
|
reordered.add(0, reordered.remove(firstIndex));
|
||||||
|
return reordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeDomain(String url) {
|
||||||
|
if (url == null)
|
||||||
|
return null;
|
||||||
|
return url.contains("shikimori") ? url.replaceFirst("https?://[^/]*shikimori[^/]*/", "https://shikimori.io/")
|
||||||
|
: url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uni<Boolean> isReachable(String url) {
|
||||||
|
if (url == null || url.isEmpty()) {
|
||||||
|
return Uni.createFrom().item(false);
|
||||||
|
}
|
||||||
|
return Uni.createFrom().completionStage(
|
||||||
|
webClient.headAbs(url).timeout(timeoutMs).send().toCompletionStage())
|
||||||
|
.map(response -> response.statusCode() >= 200 && response.statusCode() < 400)
|
||||||
|
.onFailure().recoverWithItem(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,5 +11,8 @@ quarkus.stork.shikimori-metadata-service.service-discovery.type=consul
|
|||||||
quarkus.stork.shikimori-metadata-service.service-discovery.consul-host=${consul.host}
|
quarkus.stork.shikimori-metadata-service.service-discovery.consul-host=${consul.host}
|
||||||
quarkus.stork.shikimori-metadata-service.service-discovery.consul-port=${consul.port}
|
quarkus.stork.shikimori-metadata-service.service-discovery.consul-port=${consul.port}
|
||||||
|
|
||||||
|
quarkus.http.cors.enabled=true
|
||||||
|
quarkus.http.cors.origins=http://localhost:3000
|
||||||
|
quarkus.http.cors.methods=GET,PUT,POST
|
||||||
|
quarkus.http.cors.access-control-max-age=24H
|
||||||
|
quarkus.http.cors.access-control-allow-credentials=true
|
||||||
|
|||||||
Reference in New Issue
Block a user