Implement basic detail endpoint
This commit is contained in:
@@ -4,7 +4,6 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||
|
||||
import com.backend.metadata.kodik.api.model.KodikResponse;
|
||||
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
@@ -36,8 +35,4 @@ public interface KodikAPI {
|
||||
@GET
|
||||
@Path("/imdb/{id}")
|
||||
KodikResponse findByImdbId(@PathParam("id") String id);
|
||||
|
||||
@GET
|
||||
@Path("/search")
|
||||
Uni<KodikResponse> searchAsync(@QueryParam("title") String title);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.backend.unifier.title.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Context;
|
||||
import org.mapstruct.IterableMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.Named;
|
||||
import org.mapstruct.NullValuePropertyMappingStrategy;
|
||||
|
||||
import com.backend.metadata.kodik.api.model.KodikResponse;
|
||||
import com.backend.metadata.kodik.api.model.KodikResponse.Result;
|
||||
import com.backend.metadata.shikimori.api.model.Anime;
|
||||
import com.backend.metadata.shikimori.api.model.CharacterRole;
|
||||
import com.backend.metadata.shikimori.api.model.Genre;
|
||||
import com.backend.metadata.shikimori.api.model.Related;
|
||||
import com.backend.unifier.title.model.ContentDetailEntry;
|
||||
import com.backend.unifier.title.model.ContentDetailEntry.CharacterDto;
|
||||
import com.backend.unifier.title.model.ContentDetailEntry.RelatedDto;
|
||||
import com.backend.unifier.title.model.ContentDetailEntry.TranslationDto;
|
||||
|
||||
@Mapper(componentModel = "jakarta", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
public interface KodikAndShikimoriDetailMapper {
|
||||
@Mapping(target = "title", source = "result.materialData.titleEn")
|
||||
@Mapping(target = "kind", source = "result.materialData.animeKind")
|
||||
@Mapping(target = "status", source = "result.materialData.animeStatus")
|
||||
@Mapping(target = "origin", ignore = true)
|
||||
@Mapping(target = "rating", source = "result.materialData.shikimoriRating")
|
||||
@Mapping(target = "episodes", source = "result.materialData.episodesTotal")
|
||||
@Mapping(target = "duration", source = "result.materialData.duration")
|
||||
@Mapping(target = "releasedOn", source = "result.materialData.releasedAt")
|
||||
@Mapping(target = "description", source = "result.materialData.animeDescription")
|
||||
@Mapping(target = "posterUrl", source = "result.materialData.animePosterUrl")
|
||||
@Mapping(target = "genres", source = "result.materialData.animeGenres")
|
||||
@Mapping(target = "translations", ignore = true)
|
||||
@Mapping(target = "studios", source = "result.materialData.animeStudios")
|
||||
@Mapping(target = "producers", source = "result.materialData.producers")
|
||||
@Mapping(target = "directors", source = "result.materialData.directors")
|
||||
@Mapping(target = "related", ignore = true)
|
||||
@Mapping(target = "characters", ignore = true)
|
||||
ContentDetailEntry toDto(Result result, @Context KodikResponse allKodikResults, @Context Anime anime);
|
||||
|
||||
@AfterMapping
|
||||
default void mergeShikimoriAndKodik(@MappingTarget ContentDetailEntry dto,
|
||||
Result result,
|
||||
@Context KodikResponse allKodikResults,
|
||||
@Context Anime anime) {
|
||||
|
||||
if (allKodikResults != null && allKodikResults.results != null) {
|
||||
Map<Integer, TranslationDto> seen = allKodikResults.results.stream()
|
||||
.filter(r -> r.translation != null)
|
||||
.collect(Collectors.toMap(
|
||||
r -> r.translation.id,
|
||||
r -> new TranslationDto(r.translation.id, r.translation.title, r.translation.type),
|
||||
(a, b) -> a));
|
||||
dto.translations = List.copyOf(seen.values());
|
||||
}
|
||||
|
||||
if (anime == null)
|
||||
return;
|
||||
|
||||
dto.related = toRelatedDtos(anime.getRelated());
|
||||
dto.characters = toCharacterDtos(anime.getCharacterRoles());
|
||||
|
||||
if (dto.origin == null && anime.getOrigin() != null)
|
||||
dto.origin = anime.getOrigin().name();
|
||||
|
||||
if (dto.title == null)
|
||||
dto.title = anime.getEnglish();
|
||||
|
||||
if (dto.kind == null)
|
||||
dto.kind = anime.getKind();
|
||||
if (dto.rating == null && anime.getRating() != null)
|
||||
dto.rating = anime.getRating().name();
|
||||
if (dto.episodes == null)
|
||||
dto.episodes = anime.getEpisodes();
|
||||
if (dto.duration == null)
|
||||
dto.duration = anime.getDuration();
|
||||
if (dto.description == null)
|
||||
dto.description = anime.getDescription();
|
||||
|
||||
if (dto.posterUrl == null && anime.getPoster() != null)
|
||||
dto.posterUrl = anime.getPoster().mainUrl();
|
||||
|
||||
if (dto.releasedOn == null && anime.getReleasedOn() != null && anime.getReleasedOn().getDate() != null)
|
||||
dto.releasedOn = anime.getReleasedOn().getDate().toString();
|
||||
|
||||
if ((dto.genres == null || dto.genres.isEmpty()) && anime.getGenres() != null)
|
||||
dto.genres = anime.getGenres().stream().map(Genre::name).toList();
|
||||
}
|
||||
|
||||
@Named("toRelatedDto")
|
||||
default RelatedDto toRelatedDto(Related related) {
|
||||
if (related == null)
|
||||
return null;
|
||||
|
||||
String id;
|
||||
String name;
|
||||
String posterUrl;
|
||||
|
||||
if (related.anime() != null) {
|
||||
id = related.anime().id();
|
||||
name = related.anime().name();
|
||||
posterUrl = related.anime().poster().mainUrl();
|
||||
} else if (related.manga() != null) {
|
||||
id = related.manga().id();
|
||||
name = related.manga().name();
|
||||
posterUrl = related.manga().poster().mainUrl();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RelatedDto(
|
||||
id,
|
||||
related.relationKind(),
|
||||
name,
|
||||
posterUrl);
|
||||
}
|
||||
|
||||
@IterableMapping(qualifiedByName = "toRelatedDto")
|
||||
List<RelatedDto> toRelatedDtos(List<Related> related);
|
||||
|
||||
@Named("toCharacterDto")
|
||||
default CharacterDto toCharacterDto(CharacterRole role) {
|
||||
if (role == null || role.character() == null)
|
||||
return null;
|
||||
|
||||
com.backend.metadata.shikimori.api.model.Character c = role.character();
|
||||
String type = firstOf(role.rolesEn());
|
||||
|
||||
return new CharacterDto(
|
||||
c.id(),
|
||||
type,
|
||||
c.name(),
|
||||
c.poster() != null ? c.poster().mainUrl() : null);
|
||||
}
|
||||
|
||||
@IterableMapping(qualifiedByName = "toCharacterDto")
|
||||
List<CharacterDto> toCharacterDtos(List<CharacterRole> roles);
|
||||
|
||||
default String firstOf(List<String> list) {
|
||||
return (list != null && !list.isEmpty()) ? list.get(0) : null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,45 @@
|
||||
package com.backend.unifier.title.model;
|
||||
|
||||
public record ContentDetailEntry() {
|
||||
import java.util.List;
|
||||
|
||||
public class ContentDetailEntry {
|
||||
public String title;
|
||||
public String posterUrl;
|
||||
public String description;
|
||||
public String kind;
|
||||
|
||||
public Integer episodes;
|
||||
public Integer duration; // minutes per episode
|
||||
|
||||
public String status; // released, ongoing, anons
|
||||
public String releasedOn;
|
||||
public List<TranslationDto> translations;
|
||||
public List<String> genres;
|
||||
public String origin; // manga, novel, original …
|
||||
public String rating; // pg_13, r_plus, r …
|
||||
public List<String> producers;
|
||||
|
||||
public List<RelatedDto> related;
|
||||
public List<CharacterDto> characters;
|
||||
|
||||
public List<String> studios;
|
||||
public List<String> directors;
|
||||
|
||||
public record TranslationDto(int id, String title, String type) {
|
||||
}
|
||||
|
||||
public record RelatedDto(
|
||||
String id,
|
||||
String type,
|
||||
String name,
|
||||
String posterUrl) {
|
||||
}
|
||||
|
||||
public record CharacterDto(
|
||||
String id,
|
||||
String type,
|
||||
String name,
|
||||
String posterUrl) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
package com.backend.unifier.title.resource;
|
||||
|
||||
import com.backend.unifier.title.model.ContentDetailEntry;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.backend.unifier.title.model.ContentDetailEntry;
|
||||
import com.backend.unifier.title.service.GeneralDetailService;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
|
||||
@Path("/")
|
||||
public class DetailResource {
|
||||
private final GeneralDetailService detailService;
|
||||
|
||||
public DetailResource(GeneralDetailService detailService) {
|
||||
this.detailService = detailService;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/detail")
|
||||
public ContentDetailEntry detail(@QueryParam("id") String id) {
|
||||
return null;
|
||||
public ContentDetailEntry detail(@QueryParam("id") UUID id) {
|
||||
if (id == null)
|
||||
throw new BadRequestException("missing 'id' parameter");
|
||||
return detailService.details(id).orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.backend.unifier.title.service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.backend.unifier.title.model.ContentDetailEntry;
|
||||
|
||||
public interface DetailService extends ContentProvider {
|
||||
Optional<ContentDetailEntry> details(String id);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.backend.unifier.title.service;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.backend.unifier.title.model.ContentDetailEntry;
|
||||
import com.backend.unifier.title.model.ContentIdentifier;
|
||||
import com.backend.unifier.title.model.ContentProviderSource;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Instance;
|
||||
|
||||
@ApplicationScoped
|
||||
public class GeneralDetailService {
|
||||
private final IdentifierMaskService maskService;
|
||||
private final Instance<DetailService> detailServices;
|
||||
|
||||
public GeneralDetailService(IdentifierMaskService maskService, Instance<DetailService> detailServices) {
|
||||
this.maskService = maskService;
|
||||
this.detailServices = detailServices;
|
||||
}
|
||||
|
||||
public Optional<ContentDetailEntry> details(UUID id) {
|
||||
ContentIdentifier realIdentifier = maskService.realIdentifier(id);
|
||||
if (realIdentifier == null)
|
||||
throw new IllegalStateException("content identifier not found for id '" + id + "'");
|
||||
ContentProviderSource source = ContentProviderSource.valueOf(realIdentifier.source());
|
||||
for (DetailService detailService : detailServices) {
|
||||
if (detailService.source() == source)
|
||||
return detailService.details(realIdentifier.id());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.backend.unifier.title.service.kodik;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
|
||||
import com.backend.unifier.title.api.KodikAPI;
|
||||
import com.backend.unifier.title.api.ShikimoriAPI;
|
||||
import com.backend.unifier.title.mapper.KodikAndShikimoriDetailMapper;
|
||||
import com.backend.unifier.title.model.ContentDetailEntry;
|
||||
import com.backend.unifier.title.model.ContentProviderSource;
|
||||
import com.backend.unifier.title.service.DetailService;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@ApplicationScoped
|
||||
public class KodikDetailService implements DetailService {
|
||||
private final KodikAPI kodikAPI;
|
||||
private final ShikimoriAPI shikimoriAPI;
|
||||
private final KodikAndShikimoriDetailMapper detailMapper;
|
||||
|
||||
public KodikDetailService(@RestClient KodikAPI kodikAPI, @RestClient ShikimoriAPI shikimoriAPI,
|
||||
KodikAndShikimoriDetailMapper detailMapper) {
|
||||
this.kodikAPI = kodikAPI;
|
||||
this.shikimoriAPI = shikimoriAPI;
|
||||
this.detailMapper = detailMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentProviderSource source() {
|
||||
return ContentProviderSource.KODIK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ContentDetailEntry> details(String id) {
|
||||
var kodikResponse = kodikAPI.findByKodikId(id);
|
||||
int totalSize = kodikResponse.total;
|
||||
if (kodikResponse.results != null)
|
||||
totalSize = kodikResponse.results.size();
|
||||
if (totalSize <= 0)
|
||||
return Optional.empty();
|
||||
var kodikResult = kodikResponse.results.get(0);
|
||||
var shikimoriResponse = shikimoriAPI.findById(kodikResult.shikimoriId);
|
||||
return Optional.ofNullable(detailMapper.toDto(kodikResult, kodikResponse, shikimoriResponse));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user