Compare commits

..

No commits in common. "a183ac4426f662fe28b9a0cda4201a010e15354e" and "284f40c2791832922820c6768d2d0ab7c94d50df" have entirely different histories.

5 changed files with 84 additions and 219 deletions

View File

@ -28,10 +28,13 @@ public interface JikanClient {
includes = Exception.class) includes = Exception.class)
MangaResponse getMangaById(@PathVariable Long id); MangaResponse getMangaById(@PathVariable Long id);
record SearchResponse(List<MangaData> data) {} record SearchResponse(List<MangaData> data) {
public record MangaData(Long mal_id, String title, List<TitleData> titles) {
record MangaResponse(MangaData data) {} public record TitleData(String type, String title) {}
}
}
record MangaResponse(MangaData data) {
public record MangaData( public record MangaData(
Long mal_id, Long mal_id,
ImageData images, ImageData images,
@ -46,10 +49,6 @@ public interface JikanClient {
List<AuthorData> authors, List<AuthorData> authors,
List<GenreData> genres, List<GenreData> genres,
List<ExplicitGenreData> explicit_genres) { List<ExplicitGenreData> explicit_genres) {
public record GenreData(Long mal_id, String name) {}
public record ExplicitGenreData(Long mal_id, String name) {}
public record ImageData(ImageUrls jpg) { public record ImageData(ImageUrls jpg) {
public record ImageUrls(String large_image_url) {} public record ImageUrls(String large_image_url) {}
} }
@ -57,5 +56,10 @@ public interface JikanClient {
public record PublishData(OffsetDateTime from, OffsetDateTime to) {} public record PublishData(OffsetDateTime from, OffsetDateTime to) {}
public record AuthorData(Long mal_id, String name) {} public record AuthorData(Long mal_id, String name) {}
public record GenreData(Long mal_id, String name) {}
public record ExplicitGenreData(Long mal_id, String name) {}
}
} }
} }

View File

@ -1,77 +0,0 @@
package com.magamochi.catalog.controller;
import com.magamochi.catalog.model.dto.MangaProxyDataDTO;
import com.magamochi.catalog.service.AniListService;
import com.magamochi.catalog.service.MyAnimeListService;
import com.magamochi.common.model.dto.DefaultResponseDTO;
import io.swagger.v3.oas.annotations.Operation;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/catalog")
@RequiredArgsConstructor
public class ProxyController {
private final AniListService aniListService;
private final MyAnimeListService myAnimeListService;
@Operation(
summary = "Get manga data from AniList by ID",
description =
"Fetches manga information from AniList using the provided ID. This endpoint serves as a proxy to retrieve manga data without directly exposing the AniList API.",
tags = {"Catalog Proxy"},
operationId = "getAniListMangaDataById")
@GetMapping("/proxy/anilist/{id}")
public DefaultResponseDTO<MangaProxyDataDTO> getAniListMangaDataById(@PathVariable Long id) {
return DefaultResponseDTO.ok(MangaProxyDataDTO.from(aniListService.getMangaDataById(id), id));
}
@Operation(
summary = "Get manga data from AniList by title",
description =
"Fetches manga information from AniList using the provided title. This endpoint serves as a proxy to search for manga data without directly exposing the AniList API.",
tags = {"Catalog Proxy"},
operationId = "searchAniListMangaDataByTitle")
@GetMapping("/proxy/anilist/{title}/search")
public DefaultResponseDTO<List<MangaProxyDataDTO>> searchAniListMangaDataByTitle(
@PathVariable String title) {
var results = aniListService.getMangasByTitle(title);
return DefaultResponseDTO.ok(
results.entrySet().stream()
.map(
mangaDataEntry ->
MangaProxyDataDTO.from(mangaDataEntry.getValue(), mangaDataEntry.getKey()))
.toList());
}
@Operation(
summary = "Get manga data from MyAnimeList by ID",
description =
"Fetches manga information from MyAnimeList using the provided ID. This endpoint serves as a proxy to retrieve manga data without directly exposing the MyAnimeList API.",
tags = {"Catalog Proxy"},
operationId = "getMyAnimeListMangaDataById")
@GetMapping("/proxy/myanimelist/{id}")
public DefaultResponseDTO<MangaProxyDataDTO> getMyAnimeListMangaDataById(@PathVariable Long id) {
return DefaultResponseDTO.ok(
MangaProxyDataDTO.from(myAnimeListService.getMangaDataById(id), id));
}
@Operation(
summary = "Get manga data from MyAnimeList by title",
description =
"Fetches manga information from MyAnimeList using the provided title. This endpoint serves as a proxy to search for manga data without directly exposing the MyAnimeList API.",
tags = {"Catalog Proxy"},
operationId = "searchMyAnimeListMangaDataByTitle")
@GetMapping("/proxy/myanimelist/{title}/search")
public DefaultResponseDTO<List<MangaProxyDataDTO>> searchMyAnimeListMangaDataByTitle(
@PathVariable String title) {
var results = myAnimeListService.getMangasByTitle(title);
return DefaultResponseDTO.ok(
results.entrySet().stream()
.map(
mangaDataEntry ->
MangaProxyDataDTO.from(mangaDataEntry.getValue(), mangaDataEntry.getKey()))
.toList());
}
}

View File

@ -1,24 +0,0 @@
package com.magamochi.catalog.model.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.OffsetDateTime;
import java.util.List;
public record MangaProxyDataDTO(
String imageUrl,
@NotBlank String title,
@NotNull List<String> alternativeTitles,
@NotNull List<String> authors,
OffsetDateTime publishedAt,
@NotNull Long id) {
public static MangaProxyDataDTO from(MangaDataDTO data, Long id) {
return new MangaProxyDataDTO(
data.coverImageUrl(),
data.title(),
data.alternativeTitles(),
data.authors(),
data.publishedFrom(),
id);
}
}

View File

@ -52,63 +52,46 @@ public class AniListService {
return Map.of(); return Map.of();
} }
public Map<Long, MangaDataDTO> getMangasByTitle(String title) {
var request = getSearchGraphQLRequest(title);
aniListRateLimiter.acquire();
var response = aniListClient.searchManga(request);
return response.data().page().media().stream()
.map(this::parseMangaData)
.flatMap(mangaDataMap -> mangaDataMap.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
public MangaDataDTO getMangaDataById(Long aniListId) { public MangaDataDTO getMangaDataById(Long aniListId) {
var request = getGraphQLRequest(aniListId); var request = getGraphQLRequest(aniListId);
aniListRateLimiter.acquire(); aniListRateLimiter.acquire();
var manga = aniListClient.getManga(request).data().Media(); var media = aniListClient.getManga(request).data().Media();
return parseMangaData(manga).get(aniListId);
}
private Map<Long, MangaDataDTO> parseMangaData(AniListClient.Manga manga) {
var authors = var authors =
manga.staff().edges().stream() media.staff().edges().stream()
.filter(edge -> isAuthorRole(edge.role())) .filter(edge -> isAuthorRole(edge.role()))
.map(edge -> edge.node().name().full()) .map(edge -> edge.node().name().full())
.distinct() .distinct()
.toList(); .toList();
boolean hasRomajiTitle = nonNull(manga.title().romaji()); boolean hasRomajiTitle = nonNull(media.title().romaji());
return Map.of( return MangaDataDTO.builder()
manga.id(), .title(hasRomajiTitle ? media.title().romaji() : media.title().english())
MangaDataDTO.builder()
.title(hasRomajiTitle ? manga.title().romaji() : manga.title().english())
.score( .score(
nonNull(manga.averageScore()) nonNull(media.averageScore())
? DoubleUtil.round((double) manga.averageScore() / 10, 2) ? DoubleUtil.round((double) media.averageScore() / 10, 2)
: 0) : 0)
.synopsis(manga.description()) .synopsis(media.description())
.chapterCount(manga.chapters()) .chapterCount(media.chapters())
.publishedFrom(convertFuzzyDate(manga.startDate())) .publishedFrom(convertFuzzyDate(media.startDate()))
.publishedTo(convertFuzzyDate(manga.endDate())) .publishedTo(convertFuzzyDate(media.endDate()))
.authors(authors) .authors(authors)
.genres(manga.genres()) .genres(media.genres())
// TODO: improve this // TODO: improve this
.alternativeTitles( .alternativeTitles(
hasRomajiTitle hasRomajiTitle
? nonNull(manga.title().english()) ? nonNull(media.title().english())
? List.of(manga.title().english(), manga.title().nativeTitle()) ? List.of(media.title().english(), media.title().nativeTitle())
: List.of(manga.title().nativeTitle()) : List.of(media.title().nativeTitle())
: List.of(manga.title().nativeTitle())) : List.of(media.title().nativeTitle()))
.coverImageUrl( .coverImageUrl(
nonNull(manga.coverImage().extraLarge()) nonNull(media.coverImage().extraLarge())
? manga.coverImage().extraLarge() ? media.coverImage().extraLarge()
: manga.coverImage().large()) : media.coverImage().large())
.status(mapStatus(manga.status())) .status(mapStatus(media.status()))
.adult(nonNull(manga.isAdult()) ? manga.isAdult() : false) .adult(nonNull(media.isAdult()) ? media.isAdult() : false)
.build()); .build();
} }
private static AniListClient.@NonNull GraphQLRequest getGraphQLRequest(Long aniListId) { private static AniListClient.@NonNull GraphQLRequest getGraphQLRequest(Long aniListId) {
@ -161,18 +144,6 @@ public class AniListService {
native native
} }
status status
startDate { year month day }
coverImage { large, extraLarge }
staff {
edges {
role
node {
name {
full
}
}
}
}
} }
} }
} }

View File

@ -34,21 +34,11 @@ public class MyAnimeListService {
return results.stream() return results.stream()
.collect( .collect(
Collectors.toMap( Collectors.toMap(
JikanClient.MangaData::title, JikanClient.SearchResponse.MangaData::title,
JikanClient.MangaData::mal_id, JikanClient.SearchResponse.MangaData::mal_id,
(existing, second) -> existing)); (existing, second) -> existing));
} }
public Map<Long, MangaDataDTO> getMangasByTitle(String title) {
jikanRateLimiter.acquire();
var response = jikanClient.mangaSearch(title).data();
return response.stream()
.map(this::parseMangaData)
.flatMap(mangaDataMap -> mangaDataMap.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
public MangaDataDTO getMangaDataById(Long malId) { public MangaDataDTO getMangaDataById(Long malId) {
jikanRateLimiter.acquire(); jikanRateLimiter.acquire();
var response = jikanClient.getMangaById(malId); var response = jikanClient.getMangaById(malId);
@ -58,32 +48,33 @@ public class MyAnimeListService {
} }
var responseData = response.data(); var responseData = response.data();
return parseMangaData(responseData).get(malId);
}
private Map<Long, MangaDataDTO> parseMangaData(JikanClient.MangaData data) { var authors =
var authors = data.authors().stream().map(JikanClient.MangaData.AuthorData::name).toList(); responseData.authors().stream()
.map(JikanClient.MangaResponse.MangaData.AuthorData::name)
.toList();
var genres = data.genres().stream().map(JikanClient.MangaData.GenreData::name).toList(); var genres =
responseData.genres().stream()
.map(JikanClient.MangaResponse.MangaData.GenreData::name)
.toList();
var alternativeTitles = data.title_synonyms(); var alternativeTitles = responseData.title_synonyms();
return Map.of( return MangaDataDTO.builder()
data.mal_id(), .title(responseData.title())
MangaDataDTO.builder() .score(nonNull(responseData.score()) ? DoubleUtil.round(responseData.score(), 2) : 0)
.title(data.title()) .synopsis(responseData.synopsis())
.score(nonNull(data.score()) ? DoubleUtil.round(data.score(), 2) : 0) .chapterCount(responseData.chapters())
.synopsis(data.synopsis()) .publishedFrom(responseData.published().from())
.chapterCount(data.chapters()) .publishedTo(responseData.published().to())
.publishedFrom(data.published().from())
.publishedTo(data.published().to())
.authors(authors) .authors(authors)
.genres(genres) .genres(genres)
.alternativeTitles(alternativeTitles) .alternativeTitles(alternativeTitles)
.coverImageUrl(data.images().jpg().large_image_url()) .coverImageUrl(responseData.images().jpg().large_image_url())
.status(mapStatus(data.status())) .status(mapStatus(responseData.status()))
.adult(nonNull(data.explicit_genres()) && !data.explicit_genres().isEmpty()) .adult(nonNull(responseData.explicit_genres()) && !responseData.explicit_genres().isEmpty())
.build()); .build();
} }
private MangaStatus mapStatus(String malStatus) { private MangaStatus mapStatus(String malStatus) {