Compare commits

..

6 Commits

12 changed files with 174 additions and 86 deletions

View File

@@ -8,6 +8,8 @@ services:
env_file: ../../../.env
networks:
- anyame-consul
ports:
- 0:8080
deploy:
resources:
limits:

View File

@@ -1,4 +1,4 @@
package com.backend.metadata.kodik.service;
package com.backend.metadata.kodik;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

View File

@@ -1,4 +1,4 @@
package com.backend.metadata.kodik.service;
package com.backend.metadata.kodik;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

View File

@@ -1,4 +1,4 @@
package com.backend.metadata.kodik.service.api;
package com.backend.metadata.kodik.api;
import jakarta.enterprise.context.ApplicationScoped;

View File

@@ -1,6 +1,6 @@
package com.backend.metadata.kodik.service.api;
package com.backend.metadata.kodik.api;
import com.backend.metadata.kodik.service.api.model.KodikResponse;
import com.backend.metadata.kodik.api.model.KodikResponse;
import io.github.easyretrofit.core.annotation.RetrofitBuilder;
import retrofit2.Call;
@@ -10,6 +10,21 @@ import retrofit2.http.POST;
@RetrofitBuilder(baseUrl = "${kodik.api.url}", addConverterFactory = { JacksonConvertFactoryBuilder.class })
public interface KodikAPI {
@FormUrlEncoded
@POST("list")
Call<KodikResponse> list(
@Field("token") String token,
@Field("limit") int limit,
@Field("with_material_data") int withMaterialData);
@FormUrlEncoded
@POST("list")
Call<KodikResponse> list(
@Field("token") String token,
@Field("next") String next,
@Field("limit") int limit,
@Field("with_material_data") int withMaterialData);
@FormUrlEncoded
@POST("search")
Call<KodikResponse> search(

View File

@@ -1,7 +1,8 @@
package com.backend.metadata.kodik.service.api.model;
package com.backend.metadata.kodik.api.model;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -9,6 +10,10 @@ import com.fasterxml.jackson.annotation.JsonProperty;
public class KodikResponse {
public int total;
public List<Result> results;
@JsonProperty("prev_page")
public String previousPage;
@JsonProperty("next_page")
public String nextPage;
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Result {
@@ -21,6 +26,7 @@ public class KodikResponse {
@JsonProperty("other_title")
public String otherTitle;
public Translation translation;
public List<Translation> translations;
public int year;
@JsonProperty("last_season")
public int lastSeason;
@@ -67,6 +73,40 @@ public class KodikResponse {
public int id;
public String title;
public String type;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
result = prime * result + ((title == null) ? 0 : title.hashCode());
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Translation other = (Translation) obj;
if (id != other.id)
return false;
if (title == null) {
if (other.title != null)
return false;
} else if (!title.equals(other.title))
return false;
if (type == null) {
if (other.type != null)
return false;
} else if (!type.equals(other.type))
return false;
return true;
}
}
@Override

View File

@@ -1,4 +1,4 @@
package com.backend.metadata.kodik.service.api.model;
package com.backend.metadata.kodik.api.model;
import java.util.List;

View File

@@ -1,4 +1,4 @@
package com.backend.metadata.kodik.service.resource;
package com.backend.metadata.kodik.resource;
import java.io.IOException;
@@ -6,10 +6,10 @@ import javax.naming.ServiceUnavailableException;
import org.jboss.logging.Logger;
import com.backend.metadata.kodik.service.api.KodikAPI;
import com.backend.metadata.kodik.service.api.model.KodikResponse;
import com.backend.metadata.kodik.service.service.KodikAPITokenProvider;
import com.backend.metadata.kodik.service.service.KodikSearchFilterService;
import com.backend.metadata.kodik.api.KodikAPI;
import com.backend.metadata.kodik.api.model.KodikResponse;
import com.backend.metadata.kodik.service.KodikAPITokenProvider;
import com.backend.metadata.kodik.service.KodikSearchFilterService;
import io.quarkiverse.retrofit.runtime.EnableRetrofit;
import jakarta.ws.rs.BadRequestException;
@@ -19,7 +19,7 @@ import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
import retrofit2.Response;
@EnableRetrofit("com.backend.metadata.kodik.service.api")
@EnableRetrofit("com.backend.metadata.kodik.api")
@Path("/kodik")
public class SearchResource {
private static final Logger LOG = Logger.getLogger(SearchResource.class);
@@ -34,6 +34,24 @@ public class SearchResource {
this.searchFilterService = searchFilterService;
}
@GET
@Path("/list")
public KodikResponse list() throws ServiceUnavailableException {
try {
Response<KodikResponse> response = kodikAPI.list(tokenProvider.getKodikToken(), 100, 1).execute();
if (!response.isSuccessful()) {
LOG.errorv("failed list request with response code {0}, message {1}",
response.code(),
response.message());
throw new BadRequestException("bad response, code: " + response.code());
}
return filterResults(response.body());
} catch (IOException e) {
LOG.warn("i/o error", e);
throw new ServiceUnavailableException("i/o error");
}
}
@GET
@Path("/search")
public KodikResponse search(@QueryParam("title") String title) throws ServiceUnavailableException {
@@ -45,7 +63,7 @@ public class SearchResource {
response.message());
throw new BadRequestException("bad response, code: " + response.code());
}
return searchFilterService.filter(response.body());
return filterResults(response.body());
} catch (IOException e) {
LOG.warn("i/o error", e);
throw new ServiceUnavailableException("i/o error");
@@ -56,14 +74,14 @@ public class SearchResource {
@Path("/id/{id}")
public KodikResponse findByKodikId(@PathParam("id") String id) throws ServiceUnavailableException {
try {
Response<KodikResponse> response = kodikAPI.findByKodikID(tokenProvider.getKodikToken(), id, 1, 1)
Response<KodikResponse> response = kodikAPI.findByKodikID(tokenProvider.getKodikToken(), id, 100, 1)
.execute();
if (!response.isSuccessful()) {
LOG.errorv("failed find by kodik id {0}, response code {1}, message {2}", id,
response.code(), response.message());
throw new BadRequestException("bad response, code: " + response.code());
}
return searchFilterService.filter(response.body());
return filterResults(response.body());
} catch (IOException e) {
LOG.warn("i/o error", e);
throw new ServiceUnavailableException("i/o error");
@@ -74,14 +92,14 @@ public class SearchResource {
@Path("/shikimori/{id}")
public KodikResponse findByShikimoriId(@PathParam("id") String id) throws ServiceUnavailableException {
try {
Response<KodikResponse> response = kodikAPI.findByShikimoriID(tokenProvider.getKodikToken(), id, 1, 1)
Response<KodikResponse> response = kodikAPI.findByShikimoriID(tokenProvider.getKodikToken(), id, 100, 1)
.execute();
if (!response.isSuccessful()) {
LOG.errorv("failed find by shikimori id {0}, response code {1}, message {2}", id,
response.code(), response.message());
throw new BadRequestException("bad response, code: " + response.code());
}
return searchFilterService.filter(response.body());
return filterResults(response.body());
} catch (IOException e) {
LOG.warn("i/o error", e);
throw new ServiceUnavailableException("i/o error");
@@ -92,14 +110,14 @@ public class SearchResource {
@Path("/kinopoisk/{id}")
public KodikResponse findByKinopoiskId(@PathParam("id") String id) throws ServiceUnavailableException {
try {
Response<KodikResponse> response = kodikAPI.findByKinopoiskID(tokenProvider.getKodikToken(), id, 1, 1)
Response<KodikResponse> response = kodikAPI.findByKinopoiskID(tokenProvider.getKodikToken(), id, 100, 1)
.execute();
if (!response.isSuccessful()) {
LOG.errorv("failed find by kinopoisk id {0}, response code {1}, message {2}", id,
response.code(), response.message());
throw new BadRequestException("bad response, code: " + response.code());
}
return searchFilterService.filter(response.body());
return filterResults(response.body());
} catch (IOException e) {
LOG.warn("i/o error", e);
throw new ServiceUnavailableException("i/o error");
@@ -110,17 +128,23 @@ public class SearchResource {
@Path("/imdb/{id}")
public KodikResponse findByImdbId(@PathParam("id") String id) throws ServiceUnavailableException {
try {
Response<KodikResponse> response = kodikAPI.findByImdbID(tokenProvider.getKodikToken(), id, 1, 1).execute();
Response<KodikResponse> response = kodikAPI.findByImdbID(tokenProvider.getKodikToken(), id, 100, 1)
.execute();
if (!response.isSuccessful()) {
LOG.errorv("failed find by imdb id {0}, response code {1}, message {2}", id,
response.code(), response.message());
throw new BadRequestException("bad response, code: " + response.code());
}
return searchFilterService.filter(response.body());
return filterResults(response.body());
} catch (IOException e) {
LOG.warn("i/o error", e);
throw new ServiceUnavailableException("i/o error");
}
}
private KodikResponse filterResults(KodikResponse response) {
response.results = searchFilterService.filter(response.results);
return response;
}
}

View File

@@ -1,4 +1,4 @@
package com.backend.metadata.kodik.service.service;
package com.backend.metadata.kodik.service;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;

View File

@@ -0,0 +1,69 @@
package com.backend.metadata.kodik.service;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.backend.metadata.kodik.api.model.KodikResponse;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class KodikSearchFilterService {
private static final Set<String> ALLOWED_TYPES = Set.of("anime-serial", "anime");
public List<KodikResponse.Result> filter(List<KodikResponse.Result> target) {
return filter(target, Collections.emptyList());
}
public List<KodikResponse.Result> filter(List<KodikResponse.Result> target, List<KodikResponse.Result> fullSeen) {
Set<String> seenIds = fullSeen.stream()
.map(this::identifier)
.collect(Collectors.toSet());
Map<String, List<KodikResponse.Result>> grouped = Stream.concat(fullSeen.stream(), target.stream())
.filter(this::isAllowedType)
.collect(Collectors.groupingBy(this::identifier, LinkedHashMap::new, Collectors.toList()));
return grouped.entrySet().stream()
.filter(e -> !seenIds.contains(e.getKey()))
.map(entry -> {
List<KodikResponse.Result> group = entry.getValue();
KodikResponse.Result first = group.get(0);
first.translations = group.stream()
.map(r -> r.translation)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
return first;
})
.collect(Collectors.toList());
}
private boolean isAllowedType(KodikResponse.Result result) {
return result != null && ALLOWED_TYPES.contains(result.type);
}
public String identifier(KodikResponse.Result result) {
if (result == null)
return "null-0";
String primaryId = Arrays.asList(
result.shikimoriId,
result.kinopoiskId,
result.imdbId,
result.worldartLink).stream()
.filter(id -> id != null && !id.isBlank())
.findFirst()
.orElse(result.id);
return primaryId + "-" + Math.max(result.lastSeason, 1);
}
}

View File

@@ -1,62 +0,0 @@
package com.backend.metadata.kodik.service.service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.jboss.logging.Logger;
import com.backend.metadata.kodik.service.api.model.KodikResponse;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class KodikSearchFilterService {
private static final Logger LOG = Logger.getLogger(KodikSearchFilterService.class);
private static final Set<String> ALLOWED_TYPES = Set.of("anime-serial", "anime");
public KodikResponse filter(KodikResponse body) {
Objects.requireNonNull(body, "KodikResponse body cannot be null");
Objects.requireNonNull(body.results, "KodikResponse.results cannot be null");
Set<String> seenIds = new HashSet<>();
List<KodikResponse.Result> filteredResults = new ArrayList<>();
for (KodikResponse.Result result : body.results) {
if (isAllowedType(result) && isUnique(result, seenIds)) {
filteredResults.add(result);
}
}
body.results = filteredResults;
return body;
}
private boolean isAllowedType(KodikResponse.Result result) {
return result != null && ALLOWED_TYPES.contains(result.type);
}
private boolean isUnique(KodikResponse.Result result, Set<String> seenIds) {
return seenIds.add(identifier(result));
}
public String identifier(KodikResponse.Result result) {
if (result == null)
return "null-0";
String primaryId = Arrays.asList(
result.kinopoiskId,
result.imdbId,
result.shikimoriId,
result.worldartLink).stream()
.filter(id -> id != null && !id.isBlank())
.findFirst()
.orElse(result.id);
return primaryId + "-" + result.lastSeason;
}
}

View File

@@ -1,5 +1,5 @@
quarkus.application.name=kodik-metadata-service
kodik.api.url=https://kodikapi.com/
kodik.api.url=https://kodik-api.com/
quarkus.stork.kodik-metadata-service.service-registrar.type=consul